2026, Jan 04 06:02

Как преобразовать диагональные когорты в удобную таблицу pandas

Пошагово превращаем диагональную раскладку когорт в удобный wide-формат с иерархическими столбцами в pandas: считаем стартовый возраст, делаем melt и pivot.

Когда метрики по когортам размещены по диагонали в нескольких столбцах с годами, любые последующие вычисления становятся неудобными. Приходится гоняться за смещениями между столбцами, чтобы выровнять когорты, возраста и годы. Гораздо проще преобразовать такую диагональную раскладку в вертикальную таблицу, а затем развернуть её в широкий формат с иерархическими заголовками, отражающими Когорту и Возраст на момент начала. В таком виде извлечь целый логический столбец — это одна операция, а не набор вычислений со смещениями.

Постановка задачи: с диагональной раскладкой неудобно работать

Исходные данные хранят значения в столбцах «Year 1…Year N», а диагональная структура показывает, как возраст человека растёт по годам. Проще говоря, 18-летний в Year 1 становится 19-летним в Year 2, 20-летним в Year 3 и так далее. При нескольких когортах диагональный узор повторяется, и быстро становится утомительно индексировать такие данные через смещения.

import pandas as pd

seed_data = {
    'Cohort': [1]*4 + [2]*4,
    'age': [18, 19, 20, 21]*2,
    'Year 1': [1, 2, 3, 4, 1.5, 2.5, 3.5, 4.5],
    'year 2': [None, 1, 2, 3, None, 1.5, 2.5, 3.5],
    'year 3': [None, None, 1, 2, None, None, 1.5, 2.5],
    'year 4': [None, None, None, 1, None, None, None, 1.5]
}

tbl = pd.DataFrame(seed_data)
print(tbl)

Такая заготовка показательная: несколько когорт, возраст с момента входа и дальше, а значения разложены по диагонали. Работать с этой структурой напрямую неудобно, когда нужны аккуратные столбцы вроде «Когорта», «Возраст на момент начала» и «Год».

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

Ключ в том, чтобы явно вычислить возраст на момент начала из текущего возраста строки и индекса года. Если Year — это n-й период, то возраст на момент начала равен age − (n − 1). Как только это значение получено, можно выполнить сводное преобразование к таблице с понятным многоуровневым заголовком столбцов: сначала по Когорте, затем по Возрасту на старте. Годы становятся индексом строк, а значения попадают в свои корректные пересечения. Любые ячейки, для которых нет соответствующего диагонального значения, остаются пустыми.

Решение: преобразовать, вычислить, pivot

Следующий фрагмент кода превращает диагональную метрику в широкую таблицу с иерархическими заголовками. В уровне столбцов сохраняются Когорта и Возраст при начале, а «Year 1…Year N» выводятся на индекс. Пропуски отображаются пустыми строками, а «почти целые» значения печатаются без дробной части.

import pandas as pd
import numpy as np

src = {
    'Cohort': [1]*4 + [2]*4,
    'age': [18, 19, 20, 21]*2,
    'Year 1': [1, 2, 3, 4, 1.5, 2.5, 3.5, 4.5],
    'year 2': [None, 1, 2, 3, None, 1.5, 2.5, 3.5],
    'year 3': [None, None, 1, 2, None, None, 1.5, 2.5],
    'year 4': [None, None, None, 1, None, None, None, 1.5]
}

raw_df = pd.DataFrame(src)

unpvt = raw_df.melt(
    id_vars=['Cohort', 'age'],
    var_name='Period',
    value_name='metric'
)

unpvt['period_n'] = unpvt['Period'].str.extract(r'(\d+)').astype(int)

unpvt['start_age'] = unpvt['age'] - (unpvt['period_n'] - 1)

unpvt = unpvt.dropna(subset=['metric'])

wide_tbl = unpvt.pivot(
    index='period_n',
    columns=['Cohort', 'start_age'],
    values='metric'
)

wide_tbl = wide_tbl.sort_index(axis=1, level=[0, 1])
wide_tbl.index = [f'Year {i}' for i in wide_tbl.index]

def fmt_cell(val):
    if pd.isna(val):
        return ''
    if float(val).is_integer():
        return str(int(val))
    return str(val)

pretty_wide = wide_tbl.apply(lambda s: s.map(fmt_cell))

print(pretty_wide)

Если вы обрабатываете большие датафреймы, векторизованный вариант избегает поклеточных вызовов Python для форматирования и использует операции NumPy для представления чисел и пустых значений. Такой подход предпочтительнее для больших входных данных и не идеален для очень маленьких.

import pandas as pd
import numpy as np

src2 = {
    'Cohort': [1]*4 + [2]*4,
    'age': [18, 19, 20, 21]*2,
    'Year 1': [1, 2, 3, 4, 1.5, 2.5, 3.5, 4.5],
    'year 2': [None, 1, 2, 3, None, 1.5, 2.5, 3.5],
    'year 3': [None, None, 1, 2, None, None, 1.5, 2.5],
    'year 4': [None, None, None, 1, None, None, None, 1.5]
}

raw2 = pd.DataFrame(src2)

flat = pd.melt(
    raw2,
    id_vars=['Cohort', 'age'],
    var_name='Period',
    value_name='metric'
)

flat['period_n'] = flat['Period'].str.extract(r'(\d+)').astype(np.uint8)

flat['start_age'] = flat['age'] - (flat['period_n'] - 1)

flat.dropna(subset=['metric'], inplace=True)

wide2 = flat.pivot_table(
    index='period_n',
    columns=['Cohort', 'start_age'],
    values='metric',
    aggfunc='first'
)

wide2.sort_index(axis=1, level=[0, 1], inplace=True)
wide2.index = [f'Year {i}' for i in wide2.index]

arr = wide2.to_numpy()
ints_mask = np.isclose(arr % 1, 0, equal_nan=False)
rendered = np.where(np.isnan(arr), '', np.where(ints_mask, arr.astype(int).astype(str), arr.astype(str)))

pretty2 = pd.DataFrame(rendered, index=wide2.index, columns=wide2.columns)
print(pretty2)

Как читать результат

В преобразованном выводе столбцы образуют многоуровневый индекс: сначала Когорта, затем Возраст на момент начала. Каждая строка — это Year k, а ячейка показывает метрику для этой когорты и начального возраста в соответствующем году. Если под «Возраст на момент начала = 18» в Year 2 есть значение, это отражает записи, где текущий возраст равен 19 в Year 2, но рассчитанный начальный возраст — 18. Преобразование размещает значения строго по этой логике.

Почему это преобразование важно

Когда таблица организована по Когорте и Возрасту при начале, больше не нужны смещения, чтобы найти правильные значения. Можно выбирать смежные столбцы для среза «когорта–возраст», агрегировать или объединять с внешними источниками без диагональной арифметики. Такой макет также предотвращает случайные несоответствия при изменении числа когорт или лет со временем.

Выводы

Сохраняйте диагональный источник, но явно вычисляйте «возраст на момент начала» из номера года. Преобразуйте в long-формат (melt), получите индекс года, удалите null-значения и выполните pivot к многоуровневым столбцам. Отсортируйте столбцы для стабильного порядка и, если важна подача, отображайте целые числа без дробной части и показывайте пустые строки для пропусков. Для больших таблиц полагайтесь на векторизованный путь, чтобы сделать время выполнения предсказуемым. С такой структурой когортная аналитика становится проще, надёжнее и лучше масштабируется.