2025, Dec 25 21:01

Агрегирование пронумерованных диапазонов в pandas: wide_to_long и построчные min/max

Как получить для каждого name минимальный start и максимальный end в pandas. Два подхода: wide_to_long + groupby и построчные min/max. Примеры кода и объяснения

Когда один и тот же логический признак разнесён по пронумерованным столбцам, групповые агрегирования становятся непростыми. Типичный случай — по два диапазона в строке, например start_1/end_1 и start_2/end_2. Цель — для каждого name вычислить общий минимальный start и максимальный end по всем этим столбцам.

Постановка задачи

Предположим, у вас есть таблица, где у каждого name несколько диапазонов в широком формате. Нужно получить глобальные start и end для каждого name:

start_1  end_1  start_2  end_2  name
 100      200    300      400    ABC
 100      200    300      400    ABC
 150      250    300      400    ABC
 300      200    300      900    DEF
 50       200    300      1000   DEF

Ожидаемый результат — по одной строке на name с минимальным start и максимальным end:

start  end  name
 100   400  ABC
 50    1000 DEF

Пример кода, воспроизводящий входные данные

Ниже фрагмент, который собирает ту же структуру, чтобы вы могли воспроизвести задачу целиком:

import pandas as pd

data_map = {
    'start_1': [100, 100, 150, 300, 50],
    'end_1':   [200, 200, 250, 200, 200],
    'start_2': [300, 300, 300, 300, 300],
    'end_2':   [400, 400, 400, 900, 1000],
    'name':    ['ABC', 'ABC', 'ABC', 'DEF', 'DEF']
}

frame = pd.DataFrame(data_map)

Что здесь происходит на самом деле

Данные представлены в широком виде: две пары столбцов описывают два диапазона в каждой строке. Чтобы корректно агрегировать по name, можно либо преобразовать таблицу в длинный формат, выровняв все start и end в одних и тех же столбцах, либо вычислить построчно минимум по всем столбцам start_* и максимум по всем столбцам end_*, а затем сгруппировать по name.

Решение 1: преобразовать с wide_to_long и затем выполнить groupby-agg

Перевод пронумерованных столбцов в длинный формат делает агрегацию простой. После преобразования сгруппируйте по name и возьмите min для start и max для end — получится нужный результат.

result_set = (
    pd.wide_to_long(
        frame.reset_index(),
        stubnames=['start', 'end'],
        i=['index', 'name'],
        j=' ',
        sep='_'
    )
    .groupby('name')
    .agg(start=('start', 'min'), end=('end', 'max'))
    .reset_index()
)

print(result_set)

Вывод:

  name  start   end
0  ABC    100   400
1  DEF     50  1000

Почему это работает

После преобразования каждая исходная строка даёт две записи: одну для набора «_1» и одну для «_2». В итоге получаем аккуратную таблицу start и end, выровненную по name:

print(
    pd.wide_to_long(
        frame.reset_index(),
        stubnames=['start', 'end'],
        i=['index', 'name'],
        j=' ',
        sep='_'
    )
)
              start   end
index name               
0     ABC  1    100   200
           2    300   400
1     ABC  1    100   200
           2    300   400
2     ABC  1    150   250
           2    300   400
3     DEF  1    300   200
           2    300   900
4     DEF  1     50   200
           2    300  1000

Решение 2: посчитать построчные min/max и затем агрегировать

Другой подход — добавить в каждую строку два вспомогательных столбца: минимальный среди всех start_* и максимальный среди всех end_*. После этого сгруппируйте по name, взяв min для start и max для end — получите тот же результат.

final_view = (
    frame.assign(
        start=frame.filter(like='start').min(axis=1),
        end=frame.filter(like='end').max(axis=1)
    )
    .groupby('name')
    .agg(start=('start', 'min'), end=('end', 'max'))
    .reset_index()
)

print(final_view)

Результат совпадает с целью:

  name  start   end
0  ABC    100   400
1  DEF     50  1000

Чтобы увидеть построчные вспомогательные значения до группировки:

print(
    frame.assign(
        start=frame.filter(like='start').min(axis=1),
        end=frame.filter(like='end').max(axis=1)
    )
)
   start_1  end_1  start_2  end_2 name  start   end
0      100    200      300    400  ABC    100   400
1      100    200      300    400  ABC    100   400
2      150    250      300    400  ABC    150   400
3      300    200      300    900  DEF    300   900
4       50    200      300   1000  DEF     50  1000

Зачем это знать

Реальные датасеты часто приходят в широком формате с пронумерованными полями. Быстро нормализуя такие структуры, вы получаете возможность просто применять groupby-операции. В pandas выбор между преобразованием в длинный вид и вычислением построчной статистики зависит от того, нужно ли вам сохранять/переиспользовать длинную форму для дальнейшего анализа. Оба шаблона лаконичны и хорошо масштабируются на похожие случаи с несколькими суффиксами.

Вывод

Если нужно получить по группам минимумы и максимумы по пронумерованным столбцам start/end, преобразование через wide_to_long с последующей агрегацией groupby даёт чистый и наглядный конвейер. Когда важен лишь итоговый результат по группам, а имена столбцов следуют единой конвенции, вычисление построчных min и max с помощью filter(like=...) и последующая агрегация столь же эффективны. Выберите путь, соответствующий вашему рабочему процессу, и держите суффиксы столбцов единообразными — так преобразования будут предсказуемыми.