Урок по треку Python для анализа данных 7 мин

Окна и сдвиги в pandas

shift, diff, pct_change, rolling, expanding, cumsum, rank и окна внутри групп.

«Рост к прошлой неделе», «скользящее среднее за 7 дней», «накопительная выручка», «дней с прошлого заказа»: это оконные вычисления. В SQL их делают через OVER, в pandas через shift, rolling, expanding и cumsum.

shift, diff, pct_change

import pandas as pd

s = pd.Series([100, 120, 90, 135])
s.shift(1)        # NaN, 100, 120, 90   значение предыдущей строки
s.diff()          # NaN, 20, -30, 45    s - s.shift(1)
s.pct_change()    # NaN, 0.2, -0.25, 0.5

Первая строка получает NaN: предыдущей нет. shift(-1) смотрит вперёд, на следующую строку.

Важно: сдвиг идёт по позиции строк, а не по датам. Если в данных пропущен день, shift(1) возьмёт позапрошлый день и назовёт его вчерашним. Для временных рядов либо сначала достройте пропущенные даты (asfreq('D'), reindex), либо сдвигайте по времени: s.shift(freq='D') у Series с DatetimeIndex.

rolling: скользящее окно

s.rolling(3).mean()                 # среднее по 3 последним строкам, первые 2 NaN
s.rolling(3, min_periods=1).mean()  # считать и по неполному окну
  • Окно по умолчанию заканчивается на текущей строке и включает её.
  • min_periods задаёт, сколько непустых значений нужно для результата. По умолчанию равно размеру окна.
  • center=True центрирует окно (полезно для сглаживания графика, но в прогнозах это подглядывание в будущее).

У ряда с DatetimeIndex окно можно задать временем: s.rolling('7D').sum(). Это 7 календарных дней, а не 7 строк: пропуски дат обработаются честно.

expanding и cumsum

expanding() растущее окно от начала до текущей строки: s.expanding().max() даёт максимум на текущий момент. Для суммы, произведения, максимума и минимума есть быстрые cumsum, cumprod, cummax, cummin.

s.cumsum()  # 100, 220, 310, 445

Окна внутри групп

Самое частое на собесе: то же самое, но отдельно для каждого пользователя.

df = df.sort_values(['user', 'ts'])
df['prev_ts'] = df.groupby('user')['ts'].shift(1)
df['gap'] = df['ts'] - df['prev_ts']
df['order_n'] = df.groupby('user').cumcount() + 1
df['running'] = df.groupby('user')['amount'].cumsum()

groupby(...).shift не пересекает границы групп: у первой строки каждого пользователя будет NaN, а не значение предыдущего пользователя. Если сделать просто df['ts'].shift(1), первый заказ одного клиента получит дату последнего заказа другого.

Скользящее окно в группе:

df.groupby('user')['amount'].rolling(3).mean()

Результат получит MultiIndex (user, исходный индекс). Чтобы присвоить в колонку, уберите первый уровень: .reset_index(level=0, drop=True). Или используйте transform(lambda x: x.rolling(3).mean()).

rank

s = pd.Series([300, 200, 200, 100])
s.rank(ascending=False, method='min')    # 1, 2, 2, 4  как RANK в SQL
s.rank(ascending=False, method='dense')  # 1, 2, 2, 3  как DENSE_RANK
s.rank(ascending=False, method='first')  # 1, 2, 3, 4  как ROW_NUMBER

По умолчанию method='average': у равных средний ранг (2.5 и 2.5).

Частые ошибки

  • Не отсортировали по времени перед shift или cumsum.
  • shift без groupby: данные одного пользователя протекают в другого.
  • Пропущенные даты и rolling(7) по строкам вместо rolling('7D').
  • center=True или shift(-1) в признаках модели: утечка будущего.
  • Ждут, что rank по умолчанию работает как ROW_NUMBER.

Что спросят на собесе

  • Как посчитать изменение к предыдущему дню и рост в процентах?
  • Как посчитать скользящее среднее за 7 дней, если в данных есть пропуски дат?
  • Как найти время между соседними заказами каждого клиента?
  • Чем rolling отличается от expanding?
  • Как получить аналоги ROW_NUMBER, RANK, DENSE_RANK в pandas?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.