Подготовка данных в pandas / pd 10

Прежде чем приступать к работе с данными, их нужно подготовить и собрать в виде структуры, так чтобы они поддавались обработке с помощью инструментов из библиотеки 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      |

Появились вопросы? Задайте на Яндекс Кью

У блога есть сообщество на Кью, подписывайтесь >> Python Q << и задавайте вопросы. Спрашивайте по контенту, про python и программирование в целом.

Обучение с трудоустройством

Профессия Python-разработчик / Skillbox

Профессия Python-разработчик / Skillbox

6 500 3 900 ₽/мес.
Факультет Python-разработки / GeekBrains

Факультет Python-разработки / GeekBrains

4 990 ₽/мес.
Профессия Fullstack-разработчик / Skillfactory

Профессия Fullstack-разработчик / SkillFactory

12 500 6 250 ₽/мес.
Профессия Data Scientist / Skillbox

Профессия Data Scientist / Skillbox

8 167 4 900 ₽/мес.

Вам помогла эта статья? Поделитесь в соцсетях или блоге. Репосты помогают сайту развиться.

Александр
Я создал этот блог в 2018 году, чтобы распространять полезные учебные материалы, документации и уроки на русском. На сайте опубликовано множество статей по основам python и библиотекам, уроков для начинающих и примеров написания программ.