Изменение формы и объединение таблиц — Python: Pandas
В работе с данными приходится собирать их из разных источников и объединять в единую структуру. В библиотеке Pandas реализован функционал для таких операций над табличными данными в виде методов:
На данном уроке рассмотрим практические случаи, в которых они применяются.
Метод 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 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
Важно отметить, что join() не является методом Pandas, а применяется к датафрейму. Также важно, к какому датафрейму при объединении он применяется. Если поменять местами датафреймы в примере выше, то результат будет отличаться:
df_join_to_last = df_clicks_two_last.join( df_clicks_two_first ) print(df_join_to_last) # => SHOP3 SHOP4 SHOP1 SHOP2 # day # 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 NaN NaN # 7 481.0 409.0 NaN NaN
В примерах выше в результирующем датафрейме присутствуют только индексы датафрейма, к которому применялся данный метод. Такой способ объединения называется left join и применяется по умолчанию. Метод join() поддерживает различные сценарии объединения и включает такие случаи:
- inner join — объединение по пересечению индексов
- right 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 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 # # right join: # SHOP1 SHOP2 SHOP3 SHOP4 # day # 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 # # inner join: # SHOP1 SHOP2 SHOP3 SHOP4 # day # 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 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
Метод 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
Выводы
На данном уроке мы познакомились с различными методами Pandas для объединения табличных данных. Рассмотренные методы применяются по мере усложнения производимой операции:
- concat() — данные объединяются по строкам или по столбцам с сохранением всех значений
- join() — объединяюся датафреймы по индексам
Открыть доступ
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно
- 130 курсов, 2000+ часов теории
- 1000 практических заданий в браузере
- 360 000 студентов
Всё о методе shift() в pandas
Предположим, вы столкнулись с ситуацией, когда вам нужно сдвинуть все строки в датафрейме или вы хотите вычислить разницу в последовательных рядах. Метод Pandas shift() вам в помощь.
Затронем пару теоретических и пару практических моментов:
Сдвиг значений с помощью periods
Метод shift() сдвигает индексы в датафрейме на нужное количество значений. Простейший вызов этой функции должен содержать аргумент periods (по умолчанию periods=1 ), который представляет собой количество шагов сдвига для нужной оси. По умолчанию сдвигаются значения по вертикали вдоль оси 0 . Отсутствующие значения, появившихся в результате сдвига, будут автоматически заполнены NaN .
Давайте посмотрим, как это работает, на примере. Создадим датафрейм:
df = pd.DataFrame(< "A": [1, 2, 3, 4, 5], "B": [10, 20, 30, 40, 50] >)
Сдвинем индекс на 1 строку по вертикали:
df.shift(periods=1) # Так тоже можно: df.shift(1)
Для замены NaN можно использовать аргумент fill_value и проставить вместо пропусков 0 :
df.shift(periods=1, fill_value=0)
Кроме того, в periods можно передавать отрицательные числа, чтобы сдвигать значения в противоположном направлении.
Чтобы сдвинуть значения по горизонтали, можно задать axis=1 :
df.shift(periods=1, axis=1)
Сдвиг данных временного ряда с помощью freq
У метода shift() также есть аргумент freq . он позволяет выполнять сдвиг на основе некоей заданной частоты, что полезно при работе с данными временных рядов.
Чтобы использовать аргумент freq , необходимо убедиться, что индексами в вашем датафрейме являются типы date или datetime , иначе возникнет ошибка NotImplementedError .
Давайте посмотрим, как это работает, на примере:
df = pd.DataFrame(< "A": [1, 2, 3, 4, 5], "B": [10, 20, 30, 40, 50] >, index=pd.date_range("2020-01-01", freq="D", periods=5) )
Сдвинем индекс на 10 дней.
df.shift(freq='10D') # эквивиалент: df.shift(periods=10, freq="D")
При выполнении той же операции с помощью только одного аргумента periods вы получите следующий результат. Конечно, это не то, чего мы хотим.
Вычисление разницы в последовательных рядах
Предположим, вам нужно использовать значение предыдущего ряда для расчёта чего-либо. Тут тоже пригодится метод shift() :
df = pd.DataFrame(< "date": pd.date_range("2020-01-01", freq="D", periods=5), "sales": [22, 30, 32, 25, 42] >)
Рассчитаем изменение продаж в последовательных рядах.
df["diff"] = df["sales"] - df.shift(1)["sales"]
Короче, эта функция крайне полезна при работе с временными рядами. Давайте рассмотрим еще один практический пример.
Вычисление разницы для временного ряда
Теперь предположим, что вас попросили рассчитать изменение продаж за 7 дней следующим образом:
value_1 = Day_8 - Day_1 value_2 = Day_9 - Day_2 value_3 = Day_10 - Day_3 . value_n = Day_N - Day_N-7
Использование метода shift() с аргументом freq — идеальный способ решения этой задачи. Давайте воспользуемся методом read_csv() (пример данных здесь ) с аргументами parse_dates и index_col для формирования датафрейма.
df = pd.read_csv( "https://raw.githubusercontent.com/obulygin/content/main/pandas_shift/time_series.csv", parse_dates=["date"], index_col=["date"], )
Обратите внимание, что запись для «2020-01-08» отсутствует. Выполнив df.shift(freq=’7D’) , вы должны получить результат с записью для «2020-01-08» .
Затем мы можем рассчитать изменение продаж за 7 дней:
the_7_days_diff = df["sales"] - df.shift(freq="7D")["sales"]
- в результате есть запись для «2020-01-08»;
- значение «2020-01-08» — NaN , потому что изначально в данных не было такого значения;
- значения для даты с «2020-01-01» по «2020-01-07» — NaN , потому что их нет и в df.shift(freq=’7D’) ;
- последние 6 записей — NaN , потому что в датафрейме не было этих значений.
Заключение
Метод shift() может быть очень полезен, когда нужно сдвинуть все строки в датафрейме или требуется использовать предыдущую строку для вычислений. А ещё он применяется при работе с данными временных рядов.
Рекомендуем также ознакомиться с документацией метода.
pandas.DataFrame.pct_change¶
Percentage change between the current and a prior element.
Computes the percentage change from the immediately previous row by default. This is useful in comparing the percentage of change in a time series of elements.
periods : int, default 1
Periods to shift for forming percent change.
fill_method : str, default ‘pad’
How to handle NAs before computing percent changes.
limit : int, default None
The number of consecutive NAs to fill before stopping.
freq : DateOffset, timedelta, or offset alias string, optional
Increment to use from time series API (e.g. ‘M’ or BDay()).
**kwargs
Additional keyword arguments are passed into DataFrame.shift or Series.shift .
chg : Series or DataFrame
The same type as the calling object.
Series.diff Compute the difference of two elements in a Series. DataFrame.diff Compute the difference of two elements in a DataFrame. Series.shift Shift the index by some number of periods. DataFrame.shift Shift the index by some number of periods.
Series
>>> s = pd.Series([90, 91, 85]) >>> s 0 90 1 91 2 85 dtype: int64
>>> s.pct_change() 0 NaN 1 0.011111 2 -0.065934 dtype: float64
>>> s.pct_change(periods=2) 0 NaN 1 NaN 2 -0.055556 dtype: float64
See the percentage change in a Series where filling NAs with last valid observation forward to next valid.
>>> s = pd.Series([90, 91, None, 85]) >>> s 0 90.0 1 91.0 2 NaN 3 85.0 dtype: float64
>>> s.pct_change(fill_method='ffill') 0 NaN 1 0.011111 2 0.000000 3 -0.065934 dtype: float64
DataFrame
Percentage change in French franc, Deutsche Mark, and Italian lira from 1980-01-01 to 1980-03-01.
>>> df = pd.DataFrame( . 'FR': [4.0405, 4.0963, 4.3149], . 'GR': [1.7246, 1.7482, 1.8519], . 'IT': [804.74, 810.01, 860.13]>, . index=['1980-01-01', '1980-02-01', '1980-03-01']) >>> df FR GR IT 1980-01-01 4.0405 1.7246 804.74 1980-02-01 4.0963 1.7482 810.01 1980-03-01 4.3149 1.8519 860.13
>>> df.pct_change() FR GR IT 1980-01-01 NaN NaN NaN 1980-02-01 0.013810 0.013684 0.006549 1980-03-01 0.053365 0.059318 0.061876
Percentage of change in GOOG and APPL stock volume. Shows computing the percentage change between columns.
>>> df = pd.DataFrame( . '2016': [1769950, 30586265], . '2015': [1500923, 40912316], . '2014': [1371819, 41403351]>, . index=['GOOG', 'APPL']) >>> df 2016 2015 2014 GOOG 1769950 1500923 1371819 APPL 30586265 40912316 41403351
>>> df.pct_change(axis='columns') 2016 2015 2014 GOOG NaN -0.151997 -0.086016 APPL NaN 0.337604 0.012002
pandas.DataFrame.pct_change#
Fractional change between the current and a prior element.
Computes the fractional change from the immediately previous row by default. This is useful in comparing the fraction of change in a time series of elements.
Despite the name of this method, it calculates fractional change (also known as per unit change or relative change) and not percentage change. If you need the percentage change, multiply these values by 100.
Parameters : periods int, default 1
Periods to shift for forming percent change.
How to handle NAs before computing percent changes.
Deprecated since version 2.1: All options of fill_method are deprecated except fill_method=None .
limit int, default None
The number of consecutive NAs to fill before stopping.
Deprecated since version 2.1.
freq DateOffset, timedelta, or str, optional
Increment to use from time series API (e.g. ‘M’ or BDay()).
**kwargs
Additional keyword arguments are passed into DataFrame.shift or Series.shift .
Returns : Series or DataFrame
The same type as the calling object.
Compute the difference of two elements in a Series.
Compute the difference of two elements in a DataFrame.
Shift the index by some number of periods.
Shift the index by some number of periods.
Series
>>> s = pd.Series([90, 91, 85]) >>> s 0 90 1 91 2 85 dtype: int64
>>> s.pct_change() 0 NaN 1 0.011111 2 -0.065934 dtype: float64
>>> s.pct_change(periods=2) 0 NaN 1 NaN 2 -0.055556 dtype: float64
See the percentage change in a Series where filling NAs with last valid observation forward to next valid.
>>> s = pd.Series([90, 91, None, 85]) >>> s 0 90.0 1 91.0 2 NaN 3 85.0 dtype: float64
>>> s.ffill().pct_change() 0 NaN 1 0.011111 2 0.000000 3 -0.065934 dtype: float64
DataFrame
Percentage change in French franc, Deutsche Mark, and Italian lira from 1980-01-01 to 1980-03-01.
>>> df = pd.DataFrame( . 'FR': [4.0405, 4.0963, 4.3149], . 'GR': [1.7246, 1.7482, 1.8519], . 'IT': [804.74, 810.01, 860.13]>, . index=['1980-01-01', '1980-02-01', '1980-03-01']) >>> df FR GR IT 1980-01-01 4.0405 1.7246 804.74 1980-02-01 4.0963 1.7482 810.01 1980-03-01 4.3149 1.8519 860.13
>>> df.pct_change() FR GR IT 1980-01-01 NaN NaN NaN 1980-02-01 0.013810 0.013684 0.006549 1980-03-01 0.053365 0.059318 0.061876
Percentage of change in GOOG and APPL stock volume. Shows computing the percentage change between columns.
>>> df = pd.DataFrame( . '2016': [1769950, 30586265], . '2015': [1500923, 40912316], . '2014': [1371819, 41403351]>, . index=['GOOG', 'APPL']) >>> df 2016 2015 2014 GOOG 1769950 1500923 1371819 APPL 30586265 40912316 41403351
>>> df.pct_change(axis='columns', periods=-1) 2016 2015 2014 GOOG 0.179241 0.094112 NaN APPL -0.252395 -0.011860 NaN