Зарегистрируйтесь, чтобы продолжить обучение

Изменение формы и объединение таблиц Python: Pandas

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

  • concat()
  • join()
  • merge()

На данном уроке рассмотрим практические случаи, в которых они применяются.

Метод concat()

За основу возьмем данные о кликах на сайтах 4 магазинов за 28 дней.

df_clicks = pd.read_csv('./data/Cite_clicks.csv', index_col=0)
print(df_clicks.head())
     SHOP1  SHOP2  SHOP3  SHOP4
day
1    319.0 -265.0  319.0  328.0
2    292.0  274.0  292.0  301.0
3    283.0  301.0  274.0  283.0
4    328.0  364.0  328.0    NaN
5    391.0  355.0  373.0  337.0

Разобьем исходные данные на два датафрейма за первую и вторую недели месяца. Это операция делается с использованием срезов.

df_clicks_first_week = df_clicks[:7]
df_clicks_second_week = df_clicks[7:14]
print(df_clicks_first_week)
print(df_clicks_second_week)
     SHOP1  SHOP2  SHOP3  SHOP4
day
1    319.0 -265.0  319.0  328.0
2    292.0  274.0  292.0  301.0
3    283.0  301.0  274.0  283.0
4    328.0  364.0  328.0    NaN
5    391.0  355.0  373.0  337.0
6    445.0  418.0  409.0  445.0
7    481.0  400.0  481.0  409.0
     SHOP1  SHOP2  SHOP3  SHOP4
day
8      NaN  267.0  333.0  344.0
9    300.0  278.0  300.0  311.0
10   289.0  311.0 -278.0  289.0
11   344.0  388.0  344.0  333.0
12   421.0  377.0  399.0  355.0
13   487.0  454.0 -443.0  487.0
14   531.0  432.0  531.0  443.0

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

df_weeks_concat = pd.concat([
    df_clicks_first_week,
    df_clicks_second_week
])
print(df_weeks_concat)
SHOP1  SHOP2  SHOP3  SHOP4
day
1    319.0 -265.0  319.0  328.0
2    292.0  274.0  292.0  301.0
3    283.0  301.0  274.0  283.0
4    328.0  364.0  328.0    NaN
5    391.0  355.0  373.0  337.0
6    445.0  418.0  409.0  445.0
7    481.0  400.0  481.0  409.0
8      NaN  267.0  333.0  344.0
9    300.0  278.0  300.0  311.0
10   289.0  311.0 -278.0  289.0
11   344.0  388.0  344.0  333.0
12   421.0  377.0  399.0  355.0
13   487.0  454.0 -443.0  487.0
14   531.0  432.0  531.0  443.0

Рассмотрим ситуацию, в которой данные разбиты по парам магазинов. Возможно, это данные из разных городов, и лежат они в разных таблицах баз данных:

df_clicks_two_first = df_clicks[['SHOP1', 'SHOP2']]
df_clicks_two_last = df_clicks[['SHOP3', 'SHOP4']]
print(df_clicks_two_first.head())
print(df_clicks_two_last.head())
SHOP1  SHOP2
day
1    319.0 -265.0
2    292.0  274.0
3    283.0  301.0
4    328.0  364.0
5    391.0  355.0
     SHOP3  SHOP4
day
1    319.0  328.0
2    292.0  301.0
3    274.0  283.0
4    328.0    NaN
5    373.0  337.0

Чтобы собрать их в единое целое, применяется метод concat(), но указывается направление объединения с помощью параметра axis:

  • 0 - объединение происходит по строкам
  • 1 - по столбцам
df_shop_concat = pd.concat([
    df_clicks_two_first,
    df_clicks_two_last
], axis=1)

print(df_shop_concat.head())
# =>   SHOP1  SHOP2  SHOP3  SHOP4
# day
# 1    319.0 -265.0  319.0  328.0
# 2    292.0  274.0  292.0  301.0
# 3    283.0  301.0  274.0  283.0
# 4    328.0  364.0  328.0    NaN
# 5    391.0  355.0  373.0  337.0

Стоит быть внимательными с направлением объединения. Если его не указать в данном примере, то результат будет не тот, который ожидается:

print(pd.concat([
    df_clicks_two_first,
    df_clicks_two_last
]))
# =>   SHOP1  SHOP2  SHOP3  SHOP4
# day
# 1    319.0 -265.0    NaN    NaN
# 2    292.0  274.0    NaN    NaN
# 3    283.0  301.0    NaN    NaN
# 4    328.0  364.0    NaN    NaN
# 5    391.0  355.0    NaN    NaN

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

df_clicks_two_first = df_clicks[['SHOP1', 'SHOP2']][:5]
df_clicks_two_last = df_clicks[['SHOP3', 'SHOP4']][2:7]
print(df_clicks_two_first)
print(df_clicks_two_last)
# =>   SHOP1  SHOP2
# day
# 1    319.0 -265.0
# 2    292.0  274.0
# 3    283.0  301.0
# 4    328.0  364.0
# 5    391.0  355.0
#
#      SHOP3  SHOP4
# day
# 3    274.0  283.0
# 4    328.0    NaN
# 5    373.0  337.0
# 6    409.0  445.0
# 7    481.0  409.0

Здесь дни месяца не совпадают, но пересекаются. Для объединения также используется метод concat(), который объединит по соответствующим индексам и оставит пропуски в тех днях, для которых значений нет.

df_concat = pd.concat([
    df_clicks_two_first,
    df_clicks_two_last
],axis=1)
print(df_concat)
# =>   SHOP1  SHOP2  SHOP3  SHOP4
# day
# 1    319.0 -265.0    NaN    NaN
# 2    292.0  274.0    NaN    NaN
# 3    283.0  301.0  274.0  283.0
# 4    328.0  364.0  328.0    NaN
# 5    391.0  355.0  373.0  337.0
# 6      NaN    NaN  409.0  445.0
# 7      NaN    NaN  481.0  409.0

Метод join()

Метод concat() позволяет производить операции конкатенации по направлениям. Однако при работе с данными часто требуется более сложное объединение по индексам. Одним из методов для таких операций является метод join():

df_join_to_first = df_clicks_two_first.join(
    df_clicks_two_last
)
print(df_join_to_first)
     SHOP1  SHOP2  SHOP3  SHOP4
day
1    319.0 -265.0  319.0  328.0
2    292.0  274.0  292.0  301.0
3    283.0  301.0  274.0  283.0
4    328.0  364.0  328.0    NaN
5    391.0  355.0  373.0  337.0
6    445.0  418.0  409.0  445.0
7    481.0  400.0  481.0  409.0
8      NaN  267.0  333.0  344.0
9    300.0  278.0  300.0  311.0
10   289.0  311.0 -278.0  289.0
...

Важно отметить, что join() — это метод объекта DataFrame, который позволяет объединять два датафрейма по индексам. При этом результат зависит от того, к какому датафрейму применяется метод. Если поменять местами датафреймы в примере выше, результат будет отличаться:

df_join_to_last = df_clicks_two_last.join(
    df_clicks_two_first
)
print(df_join_to_last)
     SHOP3  SHOP4  SHOP1  SHOP2
day
1    319.0  328.0  319.0 -265.0
2    292.0  301.0  292.0  274.0
3    274.0  283.0  283.0  301.0
4    328.0    NaN  328.0  364.0
5    373.0  337.0  391.0  355.0
6    409.0  445.0  445.0  418.0
7    481.0  409.0  481.0  400.0
8    333.0  344.0    NaN  267.0
9    300.0  311.0  300.0  278.0
10  -278.0  289.0  289.0  311.0
...

В примерах выше результирующий датафрейм содержит все индексы того датафрейма, к которому применялся метод. Такой способ объединения соответствует left join и применяется по умолчанию. Метод join() поддерживает различные сценарии объединения, включая:

  • inner join — объединение по пересечению индексов
  • left join — включает все индексы левого датафрейма
  • right join — включает все индексы правого датафрейма
  • outer join — объединение по всем индексам обоих датафреймов

Для их использования задаётся параметр how:

print('left join:')
print(df_clicks_two_first.join(
    df_clicks_two_last,
    how='left'
))

print('right join:')
print(df_clicks_two_first.join(
    df_clicks_two_last,
    how='right'
))

print('inner join:')
print(df_clicks_two_first.join(
    df_clicks_two_last,
    how='inner'
))

print('outer join:')
print(df_clicks_two_first.join(
    df_clicks_two_last,
    how='outer'
))

left join:

     SHOP1  SHOP2  SHOP3  SHOP4
day
1    319.0 -265.0  319.0  328.0
2    292.0  274.0  292.0  301.0
3    283.0  301.0  274.0  283.0
4    328.0  364.0  328.0    NaN
5    391.0  355.0  373.0  337.0

right join:

     SHOP1  SHOP2  SHOP3  SHOP4
day
1    319.0 -265.0  319.0  328.0
2    292.0  274.0  292.0  301.0
3    283.0  301.0  274.0  283.0
4    328.0  364.0  328.0    NaN
5    391.0  355.0  373.0  337.0
...

inner join:

     SHOP1  SHOP2  SHOP3  SHOP4
day
1    319.0 -265.0  319.0  328.0
2    292.0  274.0  292.0  301.0
3    283.0  301.0  274.0  283.0
4    328.0  364.0  328.0    NaN
5    391.0  355.0  373.0  337.0

outer join:

     SHOP1  SHOP2  SHOP3  SHOP4
day
1    319.0 -265.0  319.0  328.0
2    292.0  274.0  292.0  301.0
3    283.0  301.0  274.0  283.0
4    328.0  364.0  328.0    NaN
5    391.0  355.0  373.0  337.0
...
Тип join Что включается Пример поведения Порядок колонок
left Все индексы левого датафрейма; значения из правого там, где есть совпадение df1.join(df2, how='left') — берём все строки df1, добавляем данные из df2, если индекс совпадает, иначе NaN Колонки левого df слева, колонки правого df справа
right Все индексы правого датафрейма; значения из левого там, где есть совпадение df1.join(df2, how='right') — берём все строки df2, добавляем данные из df1, если индекс совпадает, иначе NaN Колонки правого df слева, колонки левого df справа
inner Только пересечение индексов df1 и df2 df1.join(df2, how='inner') — берём только индексы, которые есть в обоих df Колонки объединяются: сначала df1, потом df2
outer Все индексы из обоих датафреймов df1.join(df2, how='outer') — объединяет все индексы, там, где нет совпадений, ставит NaN Колонки объединяются: сначала df1, потом df2

Метод join() работает по индексам по умолчанию, но можно указывать конкретные колонки через параметр on. Если нужно объединять по колонкам с разными названиями, лучше использовать merge().

Метод merge()

Объединение данных можно производить не только по индексам, но и по столбцам значений двух датафреймов. Для этого не достаточно функционала метода join(), который может производить объединения по индексам датафреймов. Для таких случаев в Pandas используется метод merge().

Рассмотрим датасет с кликами, в котором дни месяца указаны в столбце day, а не в индексе строк:

df_clicks = df_clicks.reset_index()
print(df_clicks.head())
# => day  SHOP1  SHOP2  SHOP3  SHOP4
# 0    1  319.0 -265.0  319.0  328.0
# 1    2  292.0  274.0  292.0  301.0
# 2    3  283.0  301.0  274.0  283.0
# 3    4  328.0  364.0  328.0    NaN
# 4    5  391.0  355.0  373.0  337.0

Будем решать задачу по объединению двух датасетов, содержащих пятидневные срезы по парам магазинов:

df_clicks_two_first = df_clicks[['day', 'SHOP1', 'SHOP2']][:5]
df_clicks_two_last = df_clicks[['day', 'SHOP3', 'SHOP4']][2:7]
print(df_clicks_two_first)
print(df_clicks_two_last)
# => day  SHOP1  SHOP2
# 0    1  319.0 -265.0
# 1    2  292.0  274.0
# 2    3  283.0  301.0
# 3    4  328.0  364.0
# 4    5  391.0  355.0
#
#    day  SHOP3  SHOP4
# 2    3  274.0  283.0
# 3    4  328.0    NaN
# 4    5  373.0  337.0
# 5    6  409.0  445.0
# 6    7  481.0  409.0

Для их объединения необходимо указать сперва левый, а затем правый датафреймы. Также нужно определить по каким столбцам в каждом из датафреймов будет происходить объединение.

df_merged = pd.merge(
    df_clicks_two_first, df_clicks_two_last,
    left_on='day',
    right_on='day'
)
print(df_merged)
# => day  SHOP1  SHOP2  SHOP3  SHOP4
# 0    3  283.0  301.0  274.0  283.0
# 1    4  328.0  364.0  328.0    NaN
# 2    5  391.0  355.0  373.0  337.0

Также как и в методе join() в методе merge() поддерживаются различные сценарии объединения данных:

print('inner merge:')
print(pd.merge(
    df_clicks_two_first, df_clicks_two_last,
    left_on='day',
    right_on='day',
    how='inner'
))

print('left merge:')
print(pd.merge(
    df_clicks_two_first, df_clicks_two_last,
    left_on='day',
    right_on='day',
    how='left'
))

print('right merge:')
print(pd.merge(
    df_clicks_two_first, df_clicks_two_last,
    left_on='day',
    right_on='day',
    how='right'
))

print('outer merge:')
print(pd.merge(
    df_clicks_two_first, df_clicks_two_last,
    left_on='day',
    right_on='day',
    how='outer'
))

inner merge:

   day  SHOP1  SHOP2  SHOP3  SHOP4
0    3  283.0  301.0  274.0  283.0
1    4  328.0  364.0  328.0    NaN
2    5  391.0  355.0  373.0  337.0

left merge:

   day  SHOP1  SHOP2  SHOP3  SHOP4
0    1  319.0 -265.0    NaN    NaN
1    2  292.0  274.0    NaN    NaN
2    3  283.0  301.0  274.0  283.0
3    4  328.0  364.0  328.0    NaN
4    5  391.0  355.0  373.0  337.0

right merge:

   day  SHOP1  SHOP2  SHOP3  SHOP4
0    3  283.0  301.0  274.0  283.0
1    4  328.0  364.0  328.0    NaN
2    5  391.0  355.0  373.0  337.0
3    6    NaN    NaN  409.0  445.0
4    7    NaN    NaN  481.0  409.0

outer merge:

   day  SHOP1  SHOP2  SHOP3  SHOP4
0    1  319.0 -265.0    NaN    NaN
1    2  292.0  274.0    NaN    NaN
2    3  283.0  301.0  274.0  283.0
3    4  328.0  364.0  328.0    NaN
4    5  391.0  355.0  373.0  337.0
5    6    NaN    NaN  409.0  445.0
6    7    NaN    NaN  481.0  409.0
Тип merge Что включается Пример использования Особенности
inner Только строки, где значения ключей совпадают в обоих датафреймах pd.merge(df1, df2, on='key', how='inner') Результат содержит только общие ключи; аналог внутреннего join в SQL
left Все строки левого датафрейма; правый добавляется там, где ключи совпадают pd.merge(df1, df2, on='key', how='left') Если совпадения нет, в колонках правого датафрейма появятся NaN
right Все строки правого датафрейма; левый добавляется там, где ключи совпадают pd.merge(df1, df2, on='key', how='right') Если совпадения нет, в колонках левого датафрейма появятся NaN
outer Все строки обоих датафреймов pd.merge(df1, df2, on='key', how='outer') Там, где нет совпадения, ставятся NaN; аналог полного внешнего объединения
on / left_on / right_on Определяет колонки для объединения pd.merge(df1, df2, left_on='A', right_on='B') Используется, когда имена колонок для объединения различаются
suffixes Добавляет суффиксы к повторяющимся колонкам pd.merge(df1, df2, on='key', suffixes=('_left','_right')) Позволяет различать колонки с одинаковыми именами

Метод merge() чаще используется для объединения по колонкам, а join() — по индексам. Можно объединять по нескольким колонкам одновременно, передав список: on=['key1','key2']. Метод merge() полностью аналогичен SQL JOIN по типу объединения (inner, left, right, outer).

Выводы

На данном уроке мы познакомились с различными методами Pandas для объединения табличных данных. Рассмотренные методы применяются по мере усложнения производимой операции:

  • concat() - данные объединяются по строкам или по столбцам с сохранением всех значений
  • join() - объединяюся датафреймы по индексам
  • merge() - объединяются датафреймы по наборам столбцов

Как мы увидели, методы join() и merge() поддерживают при этом различные сценарии объединений. Используя набор данных методов, аналитик может собирать довольно сложные таблицы по набору фрагментов данных.


Самостоятельная работа

Склонируйте репозиторий и используйте базовый набор data/Cite_clicks.csv. По нему отработайте техники concat(), join() и merge().

Подготовка

  1. Считайте таблицу кликов (index_col=0) и выведите первые 10 строк.
  2. Разбейте датафрейм на две недельные части (df[:7], df[7:14]) и на пары столбцов ([['SHOP1','SHOP2']], [['SHOP3','SHOP4']]).

concat()

  1. Склейте недельные фрагменты по строкам (pd.concat([...])) и убедитесь, что порядок сохранился.
  2. Склейте пары столбцов по столбцам (axis=1) и сравните с вариантом без axis.
  3. Повторите конкатенацию, если пары магазинов охватывают разные дни (например, первые пять дней vs дни 3–7) — зафиксируйте появление NaN.

join()

  1. Выполните df_first.join(df_last) и df_last.join(df_first), чтобы увидеть зависимость от «левого» датафрейма.
  2. Попробуйте how='left' | 'right' | 'inner' | 'outer' и сравните индексы результата.

merge()

  1. Сбросьте индекс (reset_index()), чтобы получить столбец day.
  2. Сформируйте срезы по парам магазинов (например, [['day','SHOP1','SHOP2']] и [['day','SHOP3','SHOP4']]) с перекрывающимися днями.
  3. Объедините их через pd.merge(..., left_on='day', right_on='day'), затем повторите для how='inner' | 'left' | 'right' | 'outer'.

Контрольный список

  • Все примеры выполняются без ошибок, понятно, почему появляются NaN.
  • Для каждого метода (concat, join, merge) записаны выводы, в каких случаях он удобнее.
  • Понимаете разницу между объединением по индексам и по значениям столбцов.

Дополнительные материалы

  1. Метод join()
  2. Метод merge()
  3. Функция pandas.concat()

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff