2025, Sep 29 21:18
Как правильно использовать min_count при ресемплинге в pandas с agg по столбцам
Почему min_count пропадает при agg со словарём в pandas и как это исправить: ресемплинг временных рядов, kwargs по столбцам, приёмы с concat и срезами.
Ресемплинг временных рядов в pandas обычно кажется простым, пока не возникает необходимость управлять агрегацией по отдельным столбцам. Типичный сценарий: при ресемплинге нужен sum, но если в окне нет строк, вы хотите получить None/NaN, а не 0. С одной агрегирующей функцией это делается без усилий; подводный камень появляется, как только вы переходите на словарь агрегатов.
Постановка задачи
Рассмотрим минимальный пример с единственным индексом по времени и одним столбцом. Запрос ресемплинга с sum и min_count=1 даёт ожидаемое None для пустых окон:
import pandas as pd
data_tbl = pd.DataFrame(index=[pd.to_datetime('2020-01-01')],
                        columns=['metric'])
ok_out = data_tbl.resample('5min').agg('sum', min_count=1)
Результат:
           metric
2020-01-01    None
Но при переходе к словарю агрегатов по столбцам семантика min_count сразу теряется, и возвращается 0:
bad_out = data_tbl.resample('5min').agg({'metric': 'sum'}, min_count=1)
Результат:
           metric
2020-01-01       0
Что на самом деле происходит и почему
Передача именованных аргументов вроде min_count вместе со строковым агрегатором работает, потому что они проксируются в соответствующую реализацию Resampler.sum. Как только вы передаёте в agg словарь, pandas больше не прокидывает эти kwargs для каждого ключа. Иными словами, нельзя одновременно использовать отображение-словарь и передавать отдельные kwargs в базовые функции агрегации так, как вы могли бы ожидать. Такое поведение сейчас не поддерживается; есть/была схожая проблема, отмеченная для agg.
Рабочие приёмы
Когда при ресемплинге нужно управлять kwargs, есть несколько приёмов, которые надёжно сохраняют поведение, не полагаясь на неподдерживаемую передачу kwargs.
Если для нескольких столбцов требуется одна и та же агрегация, сделайте срез столбцов перед вызовом agg. Так min_count=1 будет работать как задумано:
data_tbl = pd.DataFrame(index=[pd.to_datetime('2020-01-01')],
                        columns=['metric', 'metric2', 'metric3'])
same_res = data_tbl.resample('5min')[['metric', 'metric2']].agg('sum', min_count=1)
Результат:
           metric metric2
2020-01-01   None    None
Если агрегации по столбцам различаются, собирайте результат через concat. Создайте один resampler, примените поколоночные агрегации с их kwargs и объедините по столбцам:
agg_plan = {'metric': 'sum', 'metric2': 'min'}
rs_obj = data_tbl.resample('5min')
mixed_res = pd.concat({col: rs_obj[col].agg([fn], min_count=1)
                       for col, fn in agg_plan.items()}, axis=1)
Результат:
           metric metric2
              sum     min
2020-01-01   None     NaN
Если и функции, и их kwargs различаются по столбцам, храните отображение kwargs для каждого столбца и применяйте его при concat:
agg_plan = {'metric': 'sum', 'metric2': 'min'}
kw_per_col = {'metric2': {'min_count': 1}}
rs_obj = data_tbl.resample('5min')
varkw_res = pd.concat({col: rs_obj[col].agg([fn], **kw_per_col.get(col, {}))
                       for col, fn in agg_plan.items()}, axis=1)
Результат:
           metric metric2
              sum     min
2020-01-01      0     NaN
В этом последнем примере только metric2 получил min_count=1, поэтому metric вернулся к поведению sum по умолчанию, которое выдаёт 0 для пустого интервала.
Почему это важно
Разница между нулями и None — не косметическая мелочь. В конвейерах обработки временных рядов последующая логика — отношения, скользящая статистика, флаги аномалий — ведёт себя по-разному в зависимости от того, трактуется ли пустое окно как отсутствие данных или как настоящий ноль. Предсказуемая семантика агрегации помогает избежать тихого дрейфа метрик.
Есть и аспект производительности. Использование словаря агрегатов уже вносит накладные расходы по сравнению с одиночным вызовом агрегатора. На показательном вводе с 10K строк и 3 столбцами и при заранее созданном resampler, r.agg('sum') измерялось около 402 μs ± 40.6 μs, словарная форма r.agg({'value': 'sum', 'value2': 'sum', 'value3': 'sum'}) — около 1.46 ms ± 112 μs, а подход с concat — примерно 2.15 ms ± 60.4 μs. Это даёт представление о относительной стоимости, когда нужен контроль по столбцам.
Выводы
Если для нескольких столбцов вам нужна одна и та же агрегация и одинаковые kwargs, сгруппируйте их в одном вызове agg на срезе — так код остаётся компактным и без лишних конкатенаций. Когда агрегации или kwargs различаются по столбцам, собирайте результат по столбцам и объединяйте. Ожидайте потери производительности по сравнению с одним агрегатором: подход со словарём уже дороже, а шаблон с concat добавляет ещё немного сверху. Если производительность критична, подготовьте воспроизводимый фрагмент своей задачи и замерьте каждую опцию в своей среде.
Пока проброс kwargs для dict-based agg не поддерживается, эти приёмы дают предсказуемый, явный контроль над поведением resample без ущерба для корректности.
Статья основана на вопросе на StackOverflow от KamiKimi 3 и ответе mozway.