Алла Тамбовцева, НИУ ВШЭ
pandas
¶Представим, что у нас есть две таблицы, два датафрейма pandas
, и мы хотим объединить их по строкам. Например, у нас есть данные, выкачанные с сайта за прошлую неделю, и данные, выкачанные с него же за текущую неделю. Столбцы у нас в обоих датафреймах одинаковые, нужно просто добавить новые наблюдения в датафрейм с данными за прошлую неделю.
import pandas as pd
df1 = pd.DataFrame([[1, 2, 3], [4, 7, 8], [0, 8, 9]],
columns=["A", "B", "C"])
df2 = pd.DataFrame([[11, 12, 13], [10, 18, 19]],
columns=["A", "B", "C"])
df1
A | B | C | |
---|---|---|---|
0 | 1 | 2 | 3 |
1 | 4 | 7 | 8 |
2 | 0 | 8 | 9 |
df2
A | B | C | |
---|---|---|---|
0 | 11 | 12 | 13 |
1 | 10 | 18 | 19 |
full = pd.concat([df1, df2]) # список датафреймов
full
A | B | C | |
---|---|---|---|
0 | 1 | 2 | 3 |
1 | 4 | 7 | 8 |
2 | 0 | 8 | 9 |
0 | 11 | 12 | 13 |
1 | 10 | 18 | 19 |
По умолчанию функция concat()
«склеивает» датафреймы по строкам, если бы мы захотели объединить их по столбцам, то понадобилось бы указать ось, добавив аргумент axis
. Для примера добавим к датафрейму full
еще два столбца со значениями из df3
.
df3 = pd.DataFrame([[22, 33], [44, 45], [46, 10],
[11, 14], [18, 21]],
columns=['D', 'E'])
df3
D | E | |
---|---|---|
0 | 22 | 33 |
1 | 44 | 45 |
2 | 46 | 10 |
3 | 11 | 14 |
4 | 18 | 21 |
full2 = pd.concat([full, df3], axis=1)
--------------------------------------------------------------------------- InvalidIndexError Traceback (most recent call last) <ipython-input-8-59afddb23f0b> in <module>() ----> 1 full2 = pd.concat([full, df3], axis=1) /anaconda3/lib/python3.6/site-packages/pandas/core/reshape/concat.py in concat(objs, axis, join, join_axes, ignore_index, keys, levels, names, verify_integrity, sort, copy) 223 keys=keys, levels=levels, names=names, 224 verify_integrity=verify_integrity, --> 225 copy=copy, sort=sort) 226 return op.get_result() 227 /anaconda3/lib/python3.6/site-packages/pandas/core/reshape/concat.py in __init__(self, objs, axis, join, join_axes, keys, levels, names, ignore_index, verify_integrity, copy, sort) 376 self.copy = copy 377 --> 378 self.new_axes = self._get_new_axes() 379 380 def get_result(self): /anaconda3/lib/python3.6/site-packages/pandas/core/reshape/concat.py in _get_new_axes(self) 443 if i == self.axis: 444 continue --> 445 new_axes[i] = self._get_comb_axis(i) 446 else: 447 if len(self.join_axes) != ndim - 1: /anaconda3/lib/python3.6/site-packages/pandas/core/reshape/concat.py in _get_comb_axis(self, i) 464 return _get_objs_combined_axis(self.objs, axis=data_axis, 465 intersect=self.intersect, --> 466 sort=self.sort) 467 except IndexError: 468 types = [type(x).__name__ for x in self.objs] /anaconda3/lib/python3.6/site-packages/pandas/core/indexes/api.py in _get_objs_combined_axis(objs, intersect, axis, sort) 52 if hasattr(obj, '_get_axis')] 53 if obs_idxes: ---> 54 return _get_combined_index(obs_idxes, intersect=intersect, sort=sort) 55 56 /anaconda3/lib/python3.6/site-packages/pandas/core/indexes/api.py in _get_combined_index(indexes, intersect, sort) 67 index = index.intersection(other) 68 else: ---> 69 index = _union_indexes(indexes, sort=sort) 70 index = _ensure_index(index) 71 /anaconda3/lib/python3.6/site-packages/pandas/core/indexes/api.py in _union_indexes(indexes, sort) 105 else: 106 for other in indexes[1:]: --> 107 result = result.union(other) 108 return result 109 elif kind == 'array': /anaconda3/lib/python3.6/site-packages/pandas/core/indexes/base.py in union(self, other) 2766 result.extend([x for x in rvals if x not in value_set]) 2767 else: -> 2768 indexer = self.get_indexer(other) 2769 indexer, = (indexer == -1).nonzero() 2770 /anaconda3/lib/python3.6/site-packages/pandas/core/indexes/base.py in get_indexer(self, target, method, limit, tolerance) 3227 3228 if not self.is_unique: -> 3229 raise InvalidIndexError('Reindexing only valid with uniquely' 3230 ' valued Index objects') 3231 InvalidIndexError: Reindexing only valid with uniquely valued Index objects
Что-то пошло не так! На самом деле, проблема возникла ещё раньше: в full
встречаются одинаковые номера строк, чего в датафреймах быть не должно (номер строки – её уникальный идентификатор). По-хорошему это нужно было поправить ещё при объединении двух датафреймов в один:
# ignore_index - новые номера строк автоматом
full = pd.concat([df1, df2], ignore_index=True)
full # теперь все как надо, с 0 до 4
A | B | C | |
---|---|---|---|
0 | 1 | 2 | 3 |
1 | 4 | 7 | 8 |
2 | 0 | 8 | 9 |
3 | 11 | 12 | 13 |
4 | 10 | 18 | 19 |
И дальше всё «склеивается» тоже без ошибок:
full2 = pd.concat([full, df3], axis=1)
full2
A | B | C | D | E | |
---|---|---|---|---|---|
0 | 1 | 2 | 3 | 22 | 33 |
1 | 4 | 7 | 8 | 44 | 45 |
2 | 0 | 8 | 9 | 46 | 10 |
3 | 11 | 12 | 13 | 11 | 14 |
4 | 10 | 18 | 19 | 18 | 21 |
Однако часто мы сталкиваемся с более сложной задачей: объединить таблицы по определенному столбцу. Например, у нас есть таблица с разными показателями по регионам и таблица с рейтингами этих регионов. В обеих таблицах названия регионов указаны одинаково, но их порядок отличается. Воспользоваться обычным concat()
по столбцам мы не сможем (технически всё сработает, но мы тогда перемешаем строки для разных регионов). Поэтому нам понадобится функция merge()
. Для примера создадим два датафрейма с показателями по регионам:
reg1 = pd.DataFrame([['Архангельская область', 56.0, 45.0, 67.0],
['город Москва', 89.0, 32.5, 78.5],
['Красноярский край', 88.0, 54.5, 34.5]],
columns=['region', 'index1', 'index2', 'index3'])
reg2 = pd.DataFrame([['Красноярский край', 5, 8],
['Архангельская область', 7, 1],
['город Москва', 2, 7]],
columns=['region', 'rating1', 'rating2'])
reg1
region | index1 | index2 | index3 | |
---|---|---|---|---|
0 | Архангельская область | 56.0 | 45.0 | 67.0 |
1 | город Москва | 89.0 | 32.5 | 78.5 |
2 | Красноярский край | 88.0 | 54.5 | 34.5 |
reg2
region | rating1 | rating2 | |
---|---|---|---|
0 | Красноярский край | 5 | 8 |
1 | Архангельская область | 7 | 1 |
2 | город Москва | 2 | 7 |
Теперь попробуем объединить их по столбцу region
:
regs = pd.merge(reg1, reg2, on='region') # on - столбец-идентификатор
regs
region | index1 | index2 | index3 | rating1 | rating2 | |
---|---|---|---|---|---|---|
0 | Архангельская область | 56.0 | 45.0 | 67.0 | 7 | 1 |
1 | город Москва | 89.0 | 32.5 | 78.5 | 2 | 7 |
2 | Красноярский край | 88.0 | 54.5 | 34.5 | 5 | 8 |
Всё получилось! У каждого региона указан его рейтинг, а не какой-то другой, разный порядок строк в датафреймах не помешал. Очевидно, что merge()
будет работать корректно только в том случае, когда соответствия однозначны, то есть когда в столбце, по которому происходит объединение (назовём его столбец-идентификатор), нет повторяющихся значений. Здесь может возникнуть вопрос: а что будет, если какого-то значения в столбце-идентификаторе в одном из датафреймов не хватает? Например, во одном датафрейме три региона, а в другом – только два (по третьему информации нет, и строки с таким регионом тоже нет). Посмотрим. Для примера скопируем reg2
и удалим из него Красноярский край.
reg3 = reg2.copy()
reg3 = reg3[reg3.region != 'Красноярский край']
reg3
region | rating1 | rating2 | |
---|---|---|---|
1 | Архангельская область | 7 | 1 |
2 | город Москва | 2 | 7 |
Пытаемся объединить:
pd.merge(reg1, reg3, on='region')
region | index1 | index2 | index3 | rating1 | rating2 | |
---|---|---|---|---|---|---|
0 | Архангельская область | 56.0 | 45.0 | 67.0 | 7 | 1 |
1 | город Москва | 89.0 | 32.5 | 78.5 | 2 | 7 |
Информация по Красноярскому краю потерялась вообще, значения index1-index3, которые для этого региона были посчитаны, исчезли. Как это предотвратить? Объяснить Python, что нас интересует «расширенное» объединение, то есть склеивание датафреймов по всем ключам в датафрейме reg1
и датафрейме reg3
:
pd.merge(reg1, reg3, on='region', how='outer') # outer
region | index1 | index2 | index3 | rating1 | rating2 | |
---|---|---|---|---|---|---|
0 | Архангельская область | 56.0 | 45.0 | 67.0 | 7.0 | 1.0 |
1 | город Москва | 89.0 | 32.5 | 78.5 | 2.0 | 7.0 |
2 | Красноярский край | 88.0 | 54.5 | 34.5 | NaN | NaN |
Теперь данные по Красноярскому краю не потеряны, просто на месте отсутствующих рейтингов стоят пропущенные значения NaN
. Что такое этот outer
? В теории, посвященной базам данных, существует два основных способа объединения: inner
(внутренний) и outer
(внешний). Метод inner
означает склеивание баз данных по значениям, которые находятся в пересечении ключей этих баз, то есть по тем значениям столбца-идентификатора, которые являются общими для двух баз. Вспомним теорию множеств и применим её к нашим датафреймам выше.
Множество регионов в reg1
: $A = \{\text{Архангельская область, город Москва, Красноярский край}\}$
Множество регионов в reg3
: $B = \{\text{Архангельская область, город Москва}\}$
Пересечение множеств: $A \cap B = \{\text{Архангельская область, город Москва}\}$
Соответственно, те строки, которые относятся к регионам вне перечения множеств, отбрасываются. Метод inner
используется в merge()
по умолчанию, поэтому в примере выше мы сначала потеряли строку с Красноярским краем.
Метод outer
означает склеивание баз данных по значениям, которые находятся в объединении ключей этих баз, то есть по тем значениям, которые есть хотя бы в одном столбце-идентификаторе. Посмотрим на объединение множеств регионов в reg1
и reg3
:
$A \cup B = \{\text{Архангельская область, город Москва, Красноярский край}\}$.
Теперь ни один регион не будет потерян! И в случае, если какие-то ячейки в строке, соответствующей определенному региону, пустуют в одном из датафреймов, они просто будут заполнены пропущенными значениями (NaN
), что мы и видели.
Методы inner
и outer
– далеко не единственные способы объединения датафреймов. При необходимости можно оставлять в таблице только те регионы, которые есть в первой базе (метод left
) или во второй базе (метод right
).
В этом ноутбуке мы рассмотрели базовые случаи объединения датафреймов pandas
. Конечно, это лишь малая часть возможностей этой библиотеки, но с остальными тонкостями объединения таблиц (случаи с одинаково названными столбцами, случаи частичного совпадения ключей и прочее), читателям предлагается познакомиться самостоятельно.