Основы программирования в Python

Алла Тамбовцева, НИУ ВШЭ

Объединение датафреймов pandas

Представим, что у нас есть две таблицы, два датафрейма pandas, и мы хотим объединить их по строкам. Например, у нас есть данные, выкачанные с сайта за прошлую неделю, и данные, выкачанные с него же за текущую неделю. Столбцы у нас в обоих датафреймах одинаковые, нужно просто добавить новые наблюдения в датафрейм с данными за прошлую неделю.

In [1]:
import pandas as pd
In [2]:
df1 = pd.DataFrame([[1, 2, 3], [4, 7, 8], [0, 8, 9]], 
                   columns=["A", "B", "C"])
In [3]:
df2 = pd.DataFrame([[11, 12, 13], [10, 18, 19]], 
                   columns=["A", "B", "C"])
In [4]:
df1
Out[4]:
A B C
0 1 2 3
1 4 7 8
2 0 8 9
In [5]:
df2
Out[5]:
A B C
0 11 12 13
1 10 18 19
In [6]:
full = pd.concat([df1, df2]) # список датафреймов
full 
Out[6]:
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.

In [7]:
df3 = pd.DataFrame([[22, 33], [44, 45], [46, 10], 
                    [11, 14], [18, 21]], 
                  columns=['D', 'E'])
df3
Out[7]:
D E
0 22 33
1 44 45
2 46 10
3 11 14
4 18 21
In [8]:
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 встречаются одинаковые номера строк, чего в датафреймах быть не должно (номер строки – её уникальный идентификатор). По-хорошему это нужно было поправить ещё при объединении двух датафреймов в один:

In [9]:
# ignore_index - новые номера строк автоматом
full = pd.concat([df1, df2], ignore_index=True)  
full  # теперь все как надо, с 0 до 4
Out[9]:
A B C
0 1 2 3
1 4 7 8
2 0 8 9
3 11 12 13
4 10 18 19

И дальше всё «склеивается» тоже без ошибок:

In [10]:
full2 = pd.concat([full, df3], axis=1)
full2
Out[10]:
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(). Для примера создадим два датафрейма с показателями по регионам:

In [11]:
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'])
In [12]:
reg1
Out[12]:
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
In [13]:
reg2
Out[13]:
region rating1 rating2
0 Красноярский край 5 8
1 Архангельская область 7 1
2 город Москва 2 7

Теперь попробуем объединить их по столбцу region:

In [14]:
regs = pd.merge(reg1, reg2, on='region')  # on - столбец-идентификатор
regs
Out[14]:
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 и удалим из него Красноярский край.

In [15]:
reg3 = reg2.copy()
reg3 = reg3[reg3.region != 'Красноярский край']
reg3
Out[15]:
region rating1 rating2
1 Архангельская область 7 1
2 город Москва 2 7

Пытаемся объединить:

In [16]:
pd.merge(reg1, reg3, on='region')
Out[16]:
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:

In [17]:
pd.merge(reg1, reg3, on='region', how='outer')  # outer
Out[17]:
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. Конечно, это лишь малая часть возможностей этой библиотеки, но с остальными тонкостями объединения таблиц (случаи с одинаково названными столбцами, случаи частичного совпадения ключей и прочее), читателям предлагается познакомиться самостоятельно.

  • Документация по merge, join, concatenate: ссылка
  • Наглядный merge от Kaggle: ссылка