«Рост к прошлой неделе», «скользящее среднее за 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?