2025, Oct 23 07:16
Разметка TrendUp и TrendDown в pandas по порогу длины серии
Как пометить последовательные рост и падение в pandas DataFrame: векторная разметка TrendUp/TrendDown по порогу длины серии с groupby.transform, без циклов
Помечать в pandas DataFrame отрезки последовательного роста и падения кажется простым, пока не потребуется аккуратная, векторизованная логика, которая масштабируется. Задача: присваивать диапазонам метки TrendUp или TrendDown, как только серия роста или снижения достигает порога (пять и более), начиная с точки, где серия стартовала; если условие не выполняется — подставлять NoTrend. Ниже — компактный способ сделать это без ручной итерации по строкам.
Исходные данные и базовая подготовка
Мы начинаем с одного числового столбца и выводим два вспомогательных столбца, которые считают подряд идущие повышения и понижения. Это задает основу для вычисления итоговой метки Trend.
import pandas as pd
import numpy as np
payload = {'col1': [234,321,284,286,287,300,301,303,305,299,288,300,299,287,286,280,279,270,269,301]}
table = pd.DataFrame(data=payload)
ups = []
downs = []
arr = np.array(table['col1'])
for j in range(len(table)):
    ups.append(0)
    downs.append(0)
    if arr[j] > arr[j-1]:
        ups[j] = ups[j-1] + 1
    else:
        ups[j] = 0
    if arr[j] < arr[j-1]:
        downs[j] = downs[j-1] + 1
    else:
        downs[j] = 0
table['cnsIncr'] = ups
table['cnsDecr'] = downs
В чем суть задачи на самом деле
Цель не столько посчитать длины серий, сколько отметить весь промежуток вплоть до и включая первую точку, где длина серии превышает порог. Иными словами, как только серия повышений достигает пяти и более, весь блок от сброса (где счетчик был 0) до этой точки должен получить метку TrendUp. То же самое для понижений и TrendDown. Если ни одна из сторон нигде не достигает порога, везде остается NoTrend.
Ключевое наблюдение: эти счетчики серий образуют блоки, разделенные сбросами в ноль. Внутри каждого блока последнее значение показывает, дошел ли он до порога. Если да — одинаковую метку присваиваем всему блоку.
Решение с помощью groupby.transform
Имея это в виду, реализация становится прямой: выявить блоки между нулями, вычислить для каждого блока последнюю длину серии, сравнить с порогом и «раздать» решение всем строкам блока. Для шага разметки явные циклы Python не нужны.
MIN_RUN = 5
# идентификаторы групп: каждый отрезок начинается там, где счетчик равен 0
grp_up = table['cnsIncr'].eq(0).cumsum()
grp_dn = table['cnsDecr'].eq(0).cumsum()
# достигает ли последнее значение в блоке порога?
mask_up = table['cnsIncr'].groupby(grp_up).transform('last').ge(MIN_RUN)
mask_dn = table['cnsDecr'].groupby(grp_dn).transform('last').ge(MIN_RUN)
# финальная разметка: сначала Up, затем Down, иначе NoTrend
table['Trend'] = np.select([mask_up, mask_dn], ['TrendUp', 'TrendDown'], 'NoTrend')
Это соответствует заданным правилам: TrendUp — когда внутри блока cnsIncr имеет значение не меньше 5; TrendDown — при том же условии для cnsDecr; иначе — NoTrend.
Почему это работает
Каждый счетчик (cnsIncr и cnsDecr) монотонно растет внутри серии и сбрасывается в ноль в начале следующей. Вызов eq(0).cumsum() превращает эти сбросы в устойчивые идентификаторы групп. Внутри группы transform('last') дает конечную длину серии для блока. Сравнение с порогом говорит, нужно ли пометить весь блок, а np.select транслирует это решение построчно за один проход.
Почему это стоит знать
Этот прием хорошо масштабируется и сохраняет декларативность логики. Он обходится без построчных циклов при разметке, опирается на устойчивые примитивы pandas — groupby и transform — и отражает то, как можно структурировать многие задачи по временным рядам и сегментации событий. Тот же подход — выделить блоки, посчитать сводный показатель по блоку, разнести его обратно — широко применим для классификации по длинам серий, пометки полос и порогового определения режимов.
Выводы
Когда нужно отмечать диапазоны на основе последовательного поведения, определяйте блоки по точкам сброса, вычисляйте решение на уровне блока один раз и распространяйте его на все строки блока. В этом примере так получается лаконичный столбец Trend со значениями TrendUp, TrendDown или NoTrend в векторизованном и читаемом виде, строго по исходным правилам: больше либо равно порогу и начиная со сброса, с которого серия пошла.
Статья основана на вопросе на StackOverflow от Giampaolo Levorato и ответе от mozway.