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

585

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

Выполним соединение, применив функцию 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

Что вернет следующий код?
Что выведет этот код?
Какой будет результат выполнения этого кода?
Что выведет этот код?
Какая из следующих функций проверяет, что все символы строки в верхнем регистре?
Александр
Я создал этот блог в 2018 году, чтобы распространять полезные учебные материалы, документации и уроки на русском. На сайте опубликовано множество статей по основам python и библиотекам, уроков для начинающих и примеров написания программ. Пишу на популярные темы: веб-разработка, работа с базами данных, data sciense и другие...