Прежде чем приступать к работе с данными, их нужно подготовить и собрать в виде структуры, так чтобы они поддавались обработке с помощью инструментов из библиотеки pandas. Дальше перечислены некоторые из этапов подготовки.
- Загрузка
- Сборка
- Объединение (merge)
- Конкатенация (concatenating)
- Комбинирование (combining)
- Изменение
- Удаление
Прошлый материал был посвящен загрузке. На этом этапе происходит конвертация из разных форматов в одну структуру данных, такую как Dataframe
. Но даже после этого требуются дополнительные этапы подготовки. Поэтому дальше речь пойдет о том, как выполнять операции получения данных в объединенной структуре данных.
Данные из объектов pandas можно собрать несколькими путями:
- Объединение — функция
pandas.merge()
соединяет строки вDataframe
на основе одного или нескольких ключей. Этот вариант будет знаком всем, кто работал с языком SQL, поскольку там есть аналогичная операция. - Конкатенация — функция
pandas.concat()
конкатенирует объекты по оси. - Комбинирование — функция
pandas.DataFrame.combine_first()
является методом, который позволяет соединять пересекающиеся данные для заполнения недостающих значений в структуре, используя данные другой структуры.
Более того, частью подготовки является поворот — процесс обмена строк и колонок.
Соединение
Операция соединения (merge
), которая соответствует JOIN
из SQL, состоит из объединения данных за счет соединения строк на основе одного или нескольких ключей.
На самом деле, любой, кто работал с реляционными базами данных, обычно использует запрос JOIN
из SQL для получения данных из разных таблиц с помощью ссылочных значений (ключей) внутри них. На основе этих ключей можно получать новые данные в табличной форме как результат объединения других таблиц. Эта операция в библиотеке pandas называется соединением (merging), а merge()
— функция для ее выполнения.
Сначала нужно импортировать библиотеку pandas и определить два объекта Dataframe
, которые будут примерами в этом разделе.
>>> import numpy as np
>>> import pandas as pd
>>> frame1 = pd.DataFrame({'id':['ball','pencil','pen','mug','ashtray'],
... 'price': [12.33,11.44,33.21,13.23,33.62]})
>>> frame1
| | id | price |
|---|---------|-------|
| 0 | ball | 12.33 |
| 1 | pencil | 11.44 |
| 2 | pen | 33.21 |
| 3 | mug | 13.23 |
| 4 | ashtray | 33.62 |
>>> frame2 = pd.DataFrame({'id':['pencil','pencil','ball','pen'],
... 'color': ['white','red','red','black']})
>>> frame2
| | color | id |
|---|-------|--------|
| 0 | white | pencil |
| 1 | red | pencil |
| 2 | red | ball |
| 3 | black | pen |
Выполним соединение, применив функцию merge()
к двум объектам.
>>> pd.merge(frame1,frame2)
| | id | price | color |
|---|--------|-------|-------|
| 0 | ball | 12.33 | red |
| 1 | pencil | 11.44 | white |
| 2 | pencil | 11.44 | red |
| 3 | pen | 33.21 | black |
Получившийся объект Dataframe
состоит из всех строк с общим ID
. В дополнение к общей колонке добавлены и те, что присутствуют только в первом и втором объектах.
В этом случае функция merge()
была использована без явного определения колонок. Но чаще всего необходимо указывать, на основе какой колонки выполнять соединение.
Для этого нужно добавить свойство с названием колонки, которое будет ключом соединения.
>>> frame1 = pd.DataFrame({'id':['ball','pencil','pen','mug','ashtray'],
... 'color': ['white','red','red','black','green'],
... 'brand': ['OMG','ABC','ABC','POD','POD']})
>>> frame1
| | brand | color | id |
|---|-------|-------|---------|
| 0 | OMG | white | ball |
| 1 | ABC | red | pencil |
| 2 | ABC | red | pen |
| 3 | POD | black | mug |
| 4 | PPOD | green | ashtray |
>>> frame2 = pd.DataFrame({'id':['pencil','pencil','ball','pen'],
... 'brand': ['OMG','POD','ABC','POD']})
>>> frame2
| | brand | id |
|---|-------|--------|
| 0 | OMG | pencil |
| 1 | POD | pencil |
| 2 | ABC | ball |
| 3 | POD | pen |
В этом случае два объекта Dataframe
имеют колонки с одинаковыми названиями. Поэтому при запуске merge
результата не будет.
>>> pd.merge(frame1,frame2)
Empty DataFrame
Columns: [brand, color, id]
Index: []
Необходимо явно задавать условия соединения, которым pandas будет следовать, определяя название ключа в параметре on
.
>>> pd.merge(frame1,frame2,on='id')
| | brand_x | color | id | brand_y |
|---|---------|-------|--------|---------|
| 0 | OMG | white | ball | ABC |
| 1 | ABC | red | pencil | OMG |
| 2 | ABC | red | pencil | POD |
| 3 | ABC | red | pen | POD |
>>> pd.merge(frame1,frame2,on='brand')
| | brand | color | id_x | id_y |
|---|-------|-------|--------|--------|
| 0 | OMG | white | ball | pencil |
| 1 | ABC | red | pencil | ball |
| 2 | ABC | red | pen | ball |
| 3 | POD | black | mug | pencil |
| 4 | POD | black | mug | pen |
Как и ожидалось, результаты отличаются в зависимости от условий соединения.
Но часто появляется другая проблема, когда есть два Dataframe
без колонок с одинаковыми названиями. Для исправления ситуации нужно использовать left_on
и right_on
, которые определяют ключевые колонки для первого и второго объектов Dataframe
. Дальше следует пример.
>>> frame2.columns = ['brand','sid']
>>> frame2
| | brand | sid |
|---|-------|--------|
| 0 | OMG | pencil |
| 1 | POD | pencil |
| 2 | ABC | ball |
| 3 | POD | pen |
>>> pd.merge(frame1, frame2, left_on='id', right_on='sid')
| | brand_x | color | id | brand_y | sid |
|---|---------|-------|--------|---------|--------|
| 0 | OMG | white | ball | ABC | ball |
| 1 | ABC | red | pencil | OMG | pencil |
| 2 | ABC | red | pencil | POD | pencil |
| 3 | ABC | red | pen | POD | pen |
По умолчанию функция merge()
выполняет inner join
(внутреннее соединение). Ключ в финальном объекте — результат пересечения.
Другие возможные варианты: left join
, right join
и outer join
(левое, правое и внешнее соединение). Внешнее выполняет объединение всех ключей, комбинируя эффекты правого и левого соединений. Для выбора типа нужно использовать параметр how
.
>>> frame2.columns = ['brand','id']
>>> pd.merge(frame1,frame2,on='id')
| | brand_x | color | id | brand_y |
|---|---------|-------|--------|---------|
| 0 | OMG | white | ball | ABC |
| 1 | ABC | red | pencil | OMG |
| 2 | ABC | red | pencil | POD |
| 3 | ABC | red | pen | POD |
>>> pd.merge(frame1,frame2,on='id',how='outer')
| | brand_x | color | id | brand_y |
|---|---------|-------|---------|---------|
| 0 | OMG | white | ball | ABC |
| 1 | ABC | red | pencil | OMG |
| 2 | ABC | red | pencil | POD |
| 3 | ABC | red | pen | POD |
| 4 | POD | black | mug | NaN |
| 5 | PPOD | green | ashtray | NaN |
>>> pd.merge(frame1,frame2,on='id',how='left')
| | brand_x | color | id | brand_y |
|---|---------|-------|---------|---------|
| 0 | OMG | white | ball | ABC |
| 1 | ABC | red | pencil | OMG |
| 2 | ABC | red | pencil | POD |
| 3 | ABC | red | pen | POD |
| 4 | POD | black | mug | NaN |
| 5 | PPOD | green | ashtray | NaN |
>>> pd.merge(frame1,frame2,on='id',how='right')
| | brand_x | color | id | brand_y |
|---|---------|-------|--------|---------|
| 0 | OMG | white | ball | ABC |
| 1 | ABC | red | pencil | OMG |
| 2 | ABC | red | pencil | POD |
| 3 | ABC | red | pen | POD |
>>> pd.merge(frame1,frame2,on=['id','brand'],how='outer')
| | brand | color | id |
|---|-------|-------|---------|
| 0 | OMG | white | ball |
| 1 | ABC | red | pencil |
| 2 | ABC | red | pen |
| 3 | POD | black | mug |
| 4 | PPOD | green | ashtray |
| 5 | OMG | NaN | pencil |
| 6 | POD | NaN | pencil |
| 7 | ABC | NaN | ball |
| 8 | POD | NaN | pen |
Для соединения нескольких ключей, нужно просто в параметр on добавить список.
Соединение по индексу
В некоторых случаях вместо использования колонок объекта Dataframe
в качестве ключей для этих целей можно задействовать индексы. Затем для выбора конкретных индексов нужно задать значения True
для left_join
или right_join
. Они могут быть использованы и вместе.
>>> pd.merge(frame1,frame2,right_index=True, left_index=True)
| | brand_x | color | id_x | brand_y | id_y |
|---|---------|-------|--------|---------|--------|
| 0 | OMG | white | ball | OMG | pencil |
| 1 | ABC | red | pencil | POD | pencil |
| 2 | ABC | red | pen | ABC | ball |
| 3 | POD | black | mug | POD | pen |
Но у объектов Dataframe
есть и функция join()
, которая оказывается особенно полезной, когда необходимо выполнить соединение по индексам. Она же может быть использована для объединения множества объектов с одинаковыми индексами, но без совпадающих колонок.
При запуске такого кода
>>> frame1.join(frame2)
Будет ошибка, потому что некоторые колонки в объекте frame1
называются так же, как и во frame2
. Нужно переименовать их во втором объекте перед использованием join()
.
>>> frame2.columns = ['brand2','id2']
>>> frame1.join(frame2)
В этом примере соединение было выполнено на основе значений индексов, а не колонок. Также индекс 4 представлен только в объекте frame1
, но соответствующие значения колонок во frame2
равняются NaN
.
Конкатенация
Еще один тип объединения данных — конкатенация. NumPy предоставляет функцию concatenate()
для ее выполнения.
>>> array1 = np.arange(9).reshape((3,3))
>>> array1
array([[0, 1, 2],
[3, 4, 5],
[6, 7, 8]])
>>> array2 = np.arange(9).reshape((3,3))+6
>>> array2
array([[6, 7, 8],
[9, 10, 11],
[12, 13, 14]])
>>> np.concatenate([array1,array2],axis=1)
array([[0, 1, 2, 6, 7, 8],
[3, 4, 5, 9, 10, 11],
[6, 7, 8, 12, 13, 14]])
>>> np.concatenate([array1,array2],axis=0)
array([[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[6, 7, 8],
[9, 10, 11],
[12, 13, 14]])
С библиотекой pandas и ее структурами данных, такими как Series
и Dataframe
, именованные оси позволяют и дальше обобщать конкатенацию массивов. Для этого в pandas есть функция concat()
.
>>> ser1 = pd.Series(np.random.rand(4), index=[1,2,3,4])
>>> ser1
1 0.636584
2 0.345030
3 0.157537
4 0.070351
dtype: float64
>>> ser2 = pd.Series(np.random.rand(4), index=[5,6,7,8])
>>> ser2
5 0.411319
6 0.359946
7 0.987651
8 0.329173
dtype: float64
>>> pd.concat([ser1,ser2])
1 0.636584
2 0.345030
3 0.157537
4 0.070351
5 0.411319
6 0.359946
7 0.987651
8 0.329173
dtype: float64
По умолчанию функция concat()
работает на axis=0
и возвращает объект Series
. Если задать 1
значением axis
, то результатом будет объект Dataframe
.
>>> pd.concat([ser1,ser2],axis=1)
| | 0 | 1 |
|---|----------|----------|
| 1 | 0.953608 | NaN |
| 2 | 0.929539 | NaN |
| 3 | 0.036994 | NaN |
| 4 | 0.010650 | NaN |
| 5 | NaN | 0.200771 |
| 6 | NaN | 0.709060 |
| 7 | NaN | 0.813766 |
| 8 | NaN | 0.218998 |
Проблема с этой операцией в том, что конкатенированные части не определяются в итоговом объекте. Например, нужно создать иерархический индекс на оси конкатенации. Для этого требуется использовать параметр keys
.
>>> pd.concat([ser1,ser2], keys=[1,2])
1 1 0.953608
2 0.929539
3 0.036994
4 0.010650
2 5 0.200771
6 0.709060
7 0.813766
8 0.218998
dtype: float64
В случае объединения двух Series
по axis=1
ключи становятся заголовками колонок объекта Dataframe
.
>>> pd.concat([ser1,ser2], axis=1, keys=[1,2])
| | 1 | 2 |
|---|----------|----------|
| 1 | 0.953608 | NaN |
| 2 | 0.929539 | NaN |
| 3 | 0.036994 | NaN |
| 4 | 0.010650 | NaN |
| 5 | NaN | 0.200771 |
| 6 | NaN | 0.709060 |
| 7 | NaN | 0.813766 |
| 8 | NaN | 0.218998 |
Пока что в примерах конкатенация применялась только к объектам Series
, но та же логика работает и с Dataframe
.
>>> frame1 = pd.DataFrame(np.random.rand(9).reshape(3,3),
... index=[1,2,3], columns=['A','B','C'])
>>> frame2 = pd.DataFrame(np.random.rand(9).reshape(3,3),
... index=[4,5,6], columns=['A','B','C'])
>>> pd.concat([frame1, frame2])
| | A | B | C |
|---|----------|----------|----------|
| 1 | 0.231057 | 0.024329 | 0.843888 |
| 2 | 0.727480 | 0.296619 | 0.367309 |
| 3 | 0.282516 | 0.524227 | 0.462000 |
| 4 | 0.078044 | 0.751505 | 0.832853 |
| 5 | 0.843225 | 0.945914 | 0.141331 |
| 6 | 0.189217 | 0.799631 | 0.308749 |
>>> pd.concat([frame1, frame2], axis=1)
| | A | B | C | A | B | C |
|---|----------|----------|----------|----------|----------|----------|
| 1 | 0.231057 | 0.024329 | 0.843888 | NaN | NaN | NaN |
| 2 | 0.727480 | 0.296619 | 0.367309 | NaN | NaN | NaN |
| 3 | 0.282516 | 0.524227 | 0.462000 | NaN | NaN | NaN |
| 4 | NaN | NaN | NaN | 0.078044 | 0.751505 | 0.832853 |
| 5 | NaN | NaN | NaN | 0.843225 | 0.945914 | 0.141331 |
| 6 | NaN | NaN | NaN | 0.189217 | 0.799631 | 0.308749 |
Комбинирование
Есть еще одна ситуация, при которой объединение не работает за счет соединения или конкатенации. Например, если есть два набора данных с полностью или частично пересекающимися индексами.
Одна из функций для Series
называется combine_first()
. Она выполняет объединение с выравниваем данных.
>>> ser1 = pd.Series(np.random.rand(5),index=[1,2,3,4,5])
>>> ser1
1 0.075815
2 0.332282
3 0.884463
4 0.518336
5 0.089025
dtype: float64
>>> ser2 = pd.Series(np.random.rand(4),index=[2,4,5,6])
>>> ser2
2 0.315847
4 0.275937
5 0.352538
6 0.865549
dtype: float64
>>> ser1.combine_first(ser2)
1 0.075815
2 0.332282
3 0.884463
4 0.518336
5 0.089025
6 0.865549
dtype: float64
>>> ser2.combine_first(ser1)
1 0.075815
2 0.315847
3 0.884463
4 0.275937
5 0.352538
6 0.865549
dtype: float64
Если же требуется частичное пересечение, необходимо обозначить конкретные части Series
.
>>> ser1[:3].combine_first(ser2[:3])
1 0.075815
2 0.332282
3 0.884463
4 0.275937
5 0.352538
dtype: float64
Pivoting — сводные таблицы
В дополнение к сборке данных для унификации собранных из разных источников значений часто применяется операция поворота. На самом деле, выравнивание данных по строке и колонке не всегда подходит под конкретную ситуацию. Иногда требуется перестроить данные по значениям колонок в строках или наоборот.
Поворот с иерархическим индексированием
Вы уже знаете, что Dataframe
поддерживает иерархическое индексирование. Эта особенность может быть использована для перестраивания данных в объекте Dataframe
. В контексте поворота есть две базовые операции:
- Укладка (stacking) — поворачивает структуру данных, превращая колонки в строки
- Обратный процесс укладки (unstacking) — конвертирует строки в колонки
>>> frame1 = pd.DataFrame(np.arange(9).reshape(3,3),
... index=['white','black','red'],
... columns=['ball','pen','pencil'])
>>> frame1
| | ball | pen | pencil |
|-------|------|-----|--------|
| white | 0 | 1 | 2 |
| black | 3 | 4 | 5 |
| red | 6 | 7 | 8 |
С помощью функции stack()
в Dataframe
можно развернуть данные и превратить колонки в строки, получив Series
:
>>> ser5 = frame1.stack()
white ball 0
pen 1
pencil 2
black ball 3
pen 4
pencil 5
red ball 6
pen 7
pencil 8
dtype: int32
Из объекта Series
с иерархическим индексировании можно выполнить пересборку в развернутую таблицу с помощью unstack()
.
>>> ser5.unstack()
| | ball | pen | pencil |
|-------|------|-----|--------|
| white | 0 | 1 | 2 |
| black | 3 | 4 | 5 |
| red | 6 | 7 | 8 |
Обратный процесс можно выполнить и на другом уровне, определив количество уровней или название в качестве аргумента функции.
>>> ser5.unstack(0)
| | white | black | red |
|--------|-------|-------|-----|
| ball | 0 | 3 | 6 |
| pen | 1 | 4 | 7 |
| pencil | 2 | 5 | 8 |
Поворот из «длинного» в «широкий» формат
Наиболее распространенный способ хранения наборов данных — точная регистрация данных, которые будут заполнять строки текстового файла: CSV или таблицы в базе данных. Это происходит особенно в таких случаях: чтение выполняется с помощью инструментов; результаты вычислений итерируются; или данные вводятся вручную. Похожий пример таких файлов — логи, заполняемые строка за строкой с помощью постоянно поступающих данных.
Необычная особенность этого типа набора данных — элементы разных колонок, часто повторяющиеся в последующих строках. Они всегда в табличной форме, поэтому их можно воспринимать как формат длинных или упакованных.
Чтобы лучше разобраться с этой концепцией, рассмотрим следующий Dataframe
.
>>> longframe = pd.DataFrame({'color':['white','white','white',
... 'red','red','red',
... 'black','black','black'],
... 'item':['ball','pen','mug',
... 'ball','pen','mug',
... 'ball','pen','mug'],
... 'value': np.random.rand(9)})
>>> longframe
| | color | item | value |
|---|-------|------|----------|
| 0 | white | ball | 0.896313 |
| 1 | white | pen | 0.344864 |
| 2 | white | mug | 0.101891 |
| 3 | red | ball | 0.697267 |
| 4 | red | pen | 0.852835 |
| 5 | red | mug | 0.145385 |
| 6 | black | ball | 0.738799 |
| 7 | black | pen | 0.783870 |
| 8 | black | mug | 0.017153 |
Этот режим записи данных имеет свои недостатки. Первый — повторение некоторых полей. Если воспринимать колонки в качестве ключей, то данные в этом формате будет сложно читать, а особенно — понимать отношения между значениями ключей и остальными колонками.
Но существует замена для длинного формата — широкий способ организации данных в таблице. В таком режиме данные легче читать, а также налаживать связь между таблицами. Плюс, они занимают меньше места. Это более эффективный способ хранения данных, пусть и менее практичный, особенно на этапе заполнения объекта данными.
В качестве критерия нужно выбрать колонку или несколько из них как основной ключ. Значения в них должны быть уникальными.
pandas предоставляет функцию, которая позволяет выполнить трансформацию Dataframe
из длинного типа в широкий. Она называется pivot()
, а в качестве аргументов принимает одну или несколько колонок, которые будут выполнять роль ключа.
Еще с прошлого примера вы создавали Dataframe
в широком формате. Колонка color
выступала основным ключом, item
– вторым, а их значения формировали новые колонки в объекте.
>>> wideframe = longframe.pivot('color','item')
>>> wideframe
| | value | | |
|-------|----------|----------|----------|
| item | ball | mug | pen |
| color | | | |
| black | 0.738799 | 0.017153 | 0.783870 |
| red | 0.697267 | 0.145385 | 0.852835 |
| white | 0.896313 | 0.101891 | 0.344864 |
В таком формате Dataframe
более компактен, а данные в нем — куда более читаемые.
Удаление
Последний этап подготовки данных — удаление колонок и строк. Определим такой объект в качестве примера.
>>> frame1 = pd.DataFrame(np.arange(9).reshape(3,3),
... index=['white','black','red'],
... columns=['ball','pen','pencil'])
>>> frame1
| | ball | pen | pencil |
|-------|------|-----|--------|
| white | 0 | 1 | 2 |
| black | 3 | 4 | 5 |
| red | 6 | 7 | 8 |
Для удаления колонки используйте команду del
к Dataframe
с определенным названием колонки.
>>> del frame1['ball']
>>> frame1
| | pen | pencil |
|-------|-----|--------|
| white | 1 | 2 |
| black | 4 | 5 |
| red | 7 | 8 |
Для удаления нежеланной строки используйте функцию drop()
с меткой соответствующего индекса в аргументе.
>>> frame1.drop('white')
| | pen | pencil |
|-------|-----|--------|
| black | 4 | 5 |
| red | 7 | 8 |