Условие
Дан лог logins(user_id, date) — даты, когда юзер был в приложении. Найти для каждого пользователя самый длинный подряд идущий стрик дней и текущий стрик (от последнего дня лога).
Решение
Идея
Если за один день стрика выполняется правило «дата − предыдущая дата = 1 день», то такие подряд идущие даты — это «остров». Классический трюк: для каждой даты считаем date - row_number = const внутри острова. Группируем по этой константе.
Реализация
import pandas as pd
logins = logins.drop_duplicates().sort_values(['user_id', 'date']).copy()
logins['date'] = pd.to_datetime(logins['date'])
# Номер строки в порядке дней внутри юзера
logins['rn'] = logins.groupby('user_id').cumcount()
# Группирующая «дата − rn»
logins['grp'] = logins['date'] - pd.to_timedelta(logins['rn'], unit='D')
# Длина каждой полосы
streaks = (
logins.groupby(['user_id', 'grp'])
.agg(start=('date', 'min'), end=('date', 'max'), length=('date', 'size'))
.reset_index()
)
# Самый длинный стрик
longest = streaks.sort_values(['user_id', 'length'], ascending=[True, False]) \
.drop_duplicates('user_id', keep='first')
# Текущий стрик: стрик, в который входит последняя дата у юзера
last_date = logins.groupby('user_id')['date'].max().rename('last_date')
streaks = streaks.merge(last_date, on='user_id')
current = streaks[(streaks['start'] <= streaks['last_date']) &
(streaks['end'] >= streaks['last_date'])]Альтернатива через diff
logins['new_island'] = logins.groupby('user_id')['date'].diff().dt.days.ne(1).astype(int)
logins['island_id'] = logins.groupby('user_id')['new_island'].cumsum()Подводные камни
- Без
drop_duplicatesстрик удваивается, если один день записан дважды. - Пропуски выходных могут означать «неактивные дни» — стрик ломается. Если выходные считать активными, заранее заполнить даты.
cumcountначинается с 0; смещение типаdate - rn*daysкорректно только при чистом порядке.- Текущий стрик — это стрик, заканчивающийся на
today(), а не на «последнем дне в логе». Уточняйте у заказчика. - На очень длинных историях gaps-islands отрабатывает за O(n) — это лучше, чем оконные функции.
Эталонный ответ
grp = date - cumcount() — острова получаются группами по grp. Длина = size, текущий = тот, что содержит сегодняшнюю дату.