Манипулирование данными в Pandas

Очень часто данные имеют форму, абсолютно непригодную для того или инного алгоритма машинного или глубокого обучения. Парадокс заключается в том, что на преобразование данных может уходить гораздо больше времени, чем на их анализ. Поэтому имеет смысл разобраться с тем, как этими данными эффективно манипулировать. Как правило все начинается с комбинирования и слияния наборов данных, а затем производится их очистка и преобразование.

Данные можно комбинировать разными способами:

  • pd.merge() - в этом случае данные комбинируются по одному или нескольким ключам в стиле реляционных баз данных;
  • pd.concat() - конкатенация или, проще говоря, "склеивание" наборов вдоль одной из осей;
  • .combine_first() - метод экземпляра, который позволяет "сращивать" перекрывающиеся наборы данных, что позволяет заполнить отсутствующие или не актуальные (устаревшие) элементы в одном наборе элементами из другого набора.

merge - слияние датафреймов в стиле баз данных

Слияние датафреймов как в реляционных базах данных заключается в том что нужно соединить строки таблиц по одному или нескольким ключам. Давайте начнем с простого примера:

df1 = pd.DataFrame({'key': list('AABBCC'),
                   'data_1': range(6)})
df1
key data_1
0 A 0
1 A 1
2 B 2
3 B 3
4 C 4
5 C 5
df2 = pd.DataFrame({'key': list('AABBDD'),
                    'data_2': range(6)})
df2
key data_2
0 A 0
1 A 1
2 B 2
3 B 3
4 D 4
5 D 5

Обратите внимание на то, что в этих датафреймах есть столбцы с одинаковым названием 'key', а множества элементов данных столбцов имеет пересечение, т.е. у них есть общие элементы 'A' и 'B' которые мы можем воспринимать как ключевые. В самом простом случае, можно взять и соединить ("смержить") строки датафреймов с одинаковыми ключевыми элементами:

pd.merge(df1, df2)
key data_1 data_2
0 A 0 0
1 A 0 1
2 A 1 0
3 A 1 1
4 B 2 2
5 B 2 3
6 B 3 2
7 B 3 3

А вот теперь обратите внимание на то, что у нас в этом примере получилось целых 8 строк. Такой тип слияния называется "многие-ко-многим", т.е. к каждой строке с ключом 'A' в df1 присоединяются все строки с тем же ключом из df2. Давайте приведем еще один пример, который мог бы это проиллюстрировать:

df1 = pd.DataFrame({'key': list('AAABBB'),
                   'data_1': range(6)})
df1
key data_1
0 A 0
1 A 1
2 A 2
3 B 3
4 B 4
5 B 5
df2 = pd.DataFrame({'key': list('AABB'),
                    'data_2': range(10, 41, 10)})
df2
key data_2
0 A 10
1 A 20
2 B 30
3 B 40
pd.merge(df1, df2)
key data_1 data_2
0 A 0 10
1 A 0 20
2 A 1 10
3 A 1 20
4 A 2 10
5 A 2 20
6 B 3 30
7 B 3 40
8 B 4 30
9 B 4 40
10 B 5 30
11 B 5 40

По умолчанию, функция merge() выполняет, так называемое, "внутреннее" ('inner') слияние, т.е. в результирующий датафрейм попадают строки с одинаковыми ключами. В то же время, слияние может быть "левым" ('left'), "правым" ('right') и "внешним" ('outer'). Давайте взглянем как это работает, для чего снова создадим два датафрейма:

df1 = pd.DataFrame({'key': list('AABBCC'),
                   'data_1': range(6)})
df1
key data_1
0 A 0
1 A 1
2 B 2
3 B 3
4 C 4
5 C 5
df2 = pd.DataFrame({'key': list('ABD'),
                    'data_2': range(10, 31, 10)})
df2
key data_2
0 A 10
1 B 20
2 D 30

А теперь с помощью параметра how укажем, что нужно выполнить "левое" слияние:

pd.merge(df1, df2, how='left')
key data_1 data_2
0 A 0 10.0
1 A 1 10.0
2 B 2 20.0
3 B 3 20.0
4 C 4 NaN
5 C 5 NaN

Чтож, вроде бы ничего особенного не произошло, просто помимо строк с одинаковыми ключами в результирующий датафрейм попали строки с ключами из "левого" датафрейма (в функции pd.merge(df1, df2, how='left') он указан слева). Догадайтесь, что произойдет если мы выполним правое слияние... верно, в нем окажется результат внутреннего слияния и строка с ключом 'D' из "правого" датафрейма:

pd.merge(df1, df2, how='right')
key data_1 data_2
0 A 0.0 10
1 A 1.0 10
2 B 2.0 20
3 B 3.0 20
4 D NaN 30

В случае "внешнего" соединения происходит объединение ключей, т.е. то же самое что получилось бы при одновременном выполнении "левого" и "правого" соединения:

pd.merge(df1, df2, how='outer')
key data_1 data_2
0 A 0.0 10.0
1 A 1.0 10.0
2 B 2.0 20.0
3 B 3.0 20.0
4 C 4.0 NaN
5 C 5.0 NaN
6 D NaN 30.0

Надеюсь, что вы обратили внимание на то что у нас сменился тип данных в некоторых столбцах? Это произошло из-за необходимости включения значения NaN которое является вещественным, т.е. принадлежит к типу float.

Чуть ранее мы упоминали, что бывает слияние "многие-ко-многим", т.е. когда к каждой строке в одном датафрейме присоединяется каждая строка с тем же самым ключом из другого датафрейма:

df1 = pd.DataFrame({'key': list('AABB'),
                   'data_1': range(4)})
df1
key data_1
0 A 0
1 A 1
2 B 2
3 B 3
df2 = pd.DataFrame({'key': list('AABB'),
                    'data_2': range(10, 41, 10)})
df2
key data_2
0 A 10
1 A 20
2 B 30
3 B 40
pd.merge(df1, df2)
key data_1 data_2
0 A 0 10
1 A 0 20
2 A 1 10
3 A 1 20
4 B 2 30
5 B 2 40
6 B 3 30
7 B 3 40

Как же выглядит слияние "многие-к-одному" спросите вы. Точно так же, просто в одном из датафреймов будет всего по одной строке с уникальным ключом:

df1 = pd.DataFrame({'key': list('AB'),
                   'data_1': range(2)})
df1
key data_1
0 A 0
1 B 1
df2 = pd.DataFrame({'key': list('AABB'),
                    'data_2': range(10, 41, 10)})
df2
key data_2
0 A 10
1 A 20
2 B 30
3 B 40
pd.merge(df1, df2)
key data_1 data_2
0 A 0 10
1 A 0 20
2 B 1 30
3 B 1 40

Как видите к одной строке с некоторым ключом, присоединяются все строки с тем же ключом.

Кстати, а как Pandas вообще смог понять что слияние приведенных выше датафреймов нужно выполнять именно по столбцам с именем 'key'? Все просто, если явно не казано по каким именно столбцам нужно выполнить слияние, то оно будет выполнено по столбцам с одинаковым именем. Но лучше всегда явно указывать по столбцы с ключами с помощью параметра on:

pd.merge(df1, df2, on='key')
key data_1 data_2
0 A 0 10
1 A 0 20
2 B 1 30
3 B 1 40

Если имена столбцов с ключами в датафреймах отличаются, то можно указать их имена отдельно для "левого" и "правого" датафрейма:

df1 = pd.DataFrame({'key_1': list('AB'),
                   'data_1': range(2)})
df1
key_1 data_1
0 A 0
1 B 1
df2 = pd.DataFrame({'key_2': list('AABB'),
                    'data_2': range(10, 41, 10)})
df2
key_2 data_2
0 A 10
1 A 20
2 B 30
3 B 40
pd.merge(df1, df2, left_on='key_1', right_on='key_2')
key_1 data_1 key_2 data_2
0 A 0 A 10
1 A 0 A 20
2 B 1 B 30
3 B 1 B 40

Слияние по нескольким ключам

Все предыдущие примеры касались только тех ситуаций когда каждый датафрейм обладал только одним множеством (столбцом) ключей. Но как вы думаете, можно ли выполнить слияние данных по нескольким ключам? Да! Но оно не кажется таким же очевидным как слияние по одному ключу. Давайте снова начнем с простых примеров. Пусть у нас есть следующие датафреймы:

df1 = pd.DataFrame({'key_1': list('AAB'),
                    'key_2': list('XYX'),
                    'data_1': range(3)})
df1
key_1 key_2 data_1
0 A X 0
1 A Y 1
2 B X 2
df2 = pd.DataFrame({'key_1': list('AABB'),
                    'key_2': list('XXXY'),
                    'data_2': range(10, 41, 10)})
df2
key_1 key_2 data_2
0 A X 10
1 A X 20
2 B X 30
3 B Y 40

Заметьте, что в результирующем датафрейме соединились только те строки, у которых происходит одновременное совпадение сразу двух ключей:

pd.merge(df1, df2, on=['key_1', 'key_2'])
key_1 key_2 data_1 data_2
0 A X 0 10
1 A X 0 20
2 B X 2 30

Если вам тяжело представить, как происходит такое слияние, то взгляните на следующий пример в котором ключи из двух столбцов в каждом датафрейме объединены в одну строку:

df1 = pd.DataFrame({'key_1': ['AX','AY','BX'],
                    'data_1': range(3)})
df1
key_1 data_1
0 AX 0
1 AY 1
2 BX 2
df2 = pd.DataFrame({'key_1': ['AX', 'AX', 'BX', 'BY'],
                    'data_2': range(10, 41, 10)})
df2
key_1 data_2
0 AX 10
1 AX 20
2 BX 30
3 BY 40
pd.merge(df1, df2)
key_1 data_1 data_2
0 AX 0 10
1 AX 0 20
2 BX 2 30

Такое представление, позволяет легче оценить множество строк, которые окажутся в результирующем датафрейме.

Бывает так, что датафреймы могут иметь абсолютно одинаковые имена столбцов, в результате чего может появиться путаница. Что бы избежать неоднозначности merge() добавляет в конец имени каждого столбца датафрейма суффиксы '_x' и '_y':

df1 = pd.DataFrame({'key': ['A','A','B'],
                    'data_1': range(3),
                    'data_2': list('xyz')})
df1
key data_1 data_2
0 A 0 x
1 A 1 y
2 B 2 z
df2 = pd.DataFrame({'key': ['B','A','B'],
                    'data_1': range(10, 31, 10),
                    'data_2': list('ijk')})
df2
key data_1 data_2
0 B 10 i
1 A 20 j
2 B 30 k
pd.merge(df1, df2, on='key')
key data_1_x data_2_x data_1_y data_2_y
0 A 0 x 20 j
1 A 1 y 20 j
2 B 2 z 10 i
3 B 2 z 30 k

С помощью параметра suffixes данные суффиксы можно определить самостоятельно:

pd.merge(df1, df2, on='key', suffixes=('_df1', '_df2'))
key data_1_df1 data_2_df1 data_1_df2 data_2_df2
0 A 0 x 20 j
1 A 1 y 20 j
2 B 2 z 10 i
3 B 2 z 30 k

Слияние по индексу и мультииндексу

Случается так, что ключ или ключи находятся в индексе или мультииндексе соответственно. Давайте пока отложим мультииндекс на десерт и сначала разберемся с ключами в индексе. Рассмотрим следующие датафреймы:

df1 = pd.DataFrame({'key': ['A','A','B'],
                    'data_1': range(3)})
df1
key data_1
0 A 0
1 A 1
2 B 2
df2 = pd.DataFrame({'data_1': range(10, 31, 10)},
                    index=['B','A','B'])
df2
data_1
B 10
A 20
B 30

Что бы выполнить слияние по индексу нужно установить параметр left_index или параметр right_index в значение True:

pd.merge(df1, df2, left_on='key', right_index=True)
key data_1_x data_1_y
0 A 0 20
1 A 1 20
2 B 2 10
2 B 2 30

В данном примере с помощью параметра left_on мы указали что в левом датафрейме ключи находятся в столбце 'key', а с помощью параметра right_index дали понять, что ключи в правом датафрейме находятся в индексе. Обратите внимание на то, каким стал индекс строк результирующего датафрейма - в нем появились повторяющиеся значения, при этом, его значения могут оказаться неупорядоченными. Если такое поведение нежелательно, то придется выполнить переиндексацию.

Теперь можем перейти к ситуации с иерархической индексацией строк. Здесь все немного интереснее. Пусть у нас имеются два следующих датафрейма:

df1 = pd.DataFrame({'key_1': list('AAABB'),
                    'key_2': list('XYZYZ'),
                    'data_1': range(10, 51, 10)})
df1
key_1 key_2 data_1
0 A X 10
1 A Y 20
2 A Z 30
3 B Y 40
4 B Z 50
df2 = pd.DataFrame(data=np.arange(14).reshape(7, 2),
                   index=[list('BBBAAAA'), list('YXXXYZZ')],
                   columns=['data_1', 'data_2'])
df2
data_1 data_2
B Y 0 1
X 2 3
X 4 5
A X 6 7
Y 8 9
Z 10 11
Z 12 13

Что бы выполнить слияние по нескольким ключам в df1 и ключам в мультииндексе df2 можно выполнить следующую команду:

pd.merge(df1, df2, left_on=['key_1', 'key_2'], right_index=True)
key_1 key_2 data_1_x data_1_y data_2
0 A X 10 6 7
1 A Y 20 8 9
2 A Z 30 10 11
2 A Z 30 12 13
3 B Y 40 0 1

На что следует обратить внимание? Во первых на последовательность имен столбцов в параметре left_on - она должна совпадать с иерархией индексов "правого" датафрейма, т.е. если бы мы вместо left_on=['key_1', 'key_2'] мы указали left_on=['key_2', 'key_1'], то в результате бы получили пустой датафрейм так как не произошло бы ни одного совпадения. Так же обратите внимание на добавленные суффиксы '_x' и '_y' добавленные к столбцам с одинаковыми именами.

Если в обоих датафреймах ключи находятся в индексе, то в этом случае объединение так же легко выполнимо:

df1 = pd.DataFrame(data=np.arange(5, 100, 10).reshape(5, 2),
                   index=[list('BBBAA'), list('ZXYYX')],
                   columns=['data_1', 'data_2'])
df1
data_1 data_2
B Z 5 15
X 25 35
Y 45 55
A Y 65 75
X 85 95
pd.merge(df1, df2, left_index=True, right_index=True, how='outer')
data_1_x data_2_x data_1_y data_2_y
A X 85.0 95.0 6.0 7.0
Y 65.0 75.0 8.0 9.0
Z NaN NaN 10.0 11.0
Z NaN NaN 12.0 13.0
B X 25.0 35.0 2.0 3.0
X 25.0 35.0 4.0 5.0
Y 45.0 55.0 0.0 1.0
Z 5.0 15.0 NaN NaN

В этом примере мы выполнили внешнее слияние двух датафреймов с мультииндексом.

В случае датафреймов с обычным индексом все выполняется аналогично и так же просто. Но иногда, если ключи находятся в индексах датафреймов, и у них нет одинаковых имен столбцов, то можно воспользоваться методом .join:

df1 = pd.DataFrame(data = np.arange(8).reshape(4, 2),
                   index=list('ABAC'),
                   columns=['C1', 'C2'])
df1
C1 C2
A 0 1
B 2 3
A 4 5
C 6 7
df2 = pd.DataFrame(data = np.arange(5, 60, 10).reshape(3, 2),
                   index=list('AAB'),
                   columns=['val_1', 'val_2'])
df2
val_1 val_2
A 5 15
A 25 35
B 45 55

По умолчанию, метод .join() выполняет левое внешнее присваивание:

df1.join(df2)
C1 C2 val_1 val_2
A 0 1 5.0 15.0
A 0 1 25.0 35.0
A 4 5 5.0 15.0
A 4 5 25.0 35.0
B 2 3 45.0 55.0
C 6 7 NaN NaN

Однако, благодаря своим параметрам, метод .join() может оказаться весьма удобен.