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.