Месяцы в названиях столбцов: собираем длинную таблицу

Продажи пришли в колонках sales_2026_09 и sales_2026_10. Для графика и добавления нового месяца нужен один столбец периода. Ручное выражение для каждого заголовка быстро превращает расчёт в набор исключений.

Изменить единицу строки

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

melt переносит выбранные заголовки в отдельное поле. Выбирайте колонки явно: служебное поле с похожим префиксом не должно стать периодом. Для регулярной выгрузки проверьте формат имён и выводите незнакомые колонки как изменение схемы.

Превратить составное имя в дату

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

Проверим число строк, уникальность нового ключа и сумму. Эти контроли защищают разные свойства: одинаковая сумма не исключает перестановки значений между товарами.

import re
import pandas as pd
wide = pd.DataFrame({"sku": ["A", "B"], "sales_2026_09": [10, 20],
                     "sales_2026_10": [15, 0]})
assert wide["sku"].is_unique
cols = [c for c in wide if re.fullmatch(r"sales_\d{4}_\d{2}", c)]
long = wide.melt(id_vars="sku", value_vars=cols, var_name="source", value_name="sales")
parts = long["source"].str.extract(r"sales_(\d{4})_(\d{2})")
long["month"] = pd.to_datetime(parts[0] + "-" + parts[1] + "-01", format="%Y-%m-%d")
long = long[["sku", "month", "sales"]].sort_values(["sku", "month"])
print(long.to_string(index=False))
assert len(long) == 4 and not long.duplicated(["sku", "month"]).any()
assert long["sales"].sum() == wide[cols].to_numpy().sum() == 45
assert long.query("sku == 'B'").iloc[-1]["sales"] == 0

Получились четыре наблюдения. Нулевые октябрьские продажи B сохранились как значение. Новый месячный столбец будет обработан тем же правилом, если его имя соответствует договору.

Сохранить смысл пустых ячеек

melt переносит пропуски вместе с числами. Не удаляйте их автоматически: пустота может означать неполную поставку данных, ноль — подтверждённое отсутствие продаж. Если источник гарантирует равенство пустоты нулю, примените это правило явно и сохраните его в описании выгрузки.

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

На маленьком входе проверьте обратимость: каждая исходная ячейка должна находиться по товару и месяцу. Оставьте адресные проверки для нескольких sku. Это ловит ошибки привязки, которые не замечает общая сумма.

Проверьте диапазон месяцев. Имя sales_2096_10 тоже успешно станет датой, хотя поставка относится к 2026 году. Синтаксически правильное имя не доказывает правильность периода. После нормализации таблица готова к присоединению календаря, сравнению периодов и расчёту сводок без отдельного кода для каждого заголовка.

Закрепить на практике

В бесплатном уроке широкого и длинного формата вы выполните задачи с melt и pivot_table. После них разберите составные заголовки из примера и проверьте ключ полученной строки. Откройте бесплатный урок и задачи. Полная последовательность тем — в бесплатном курсе.

Продолжить на практике

  • NumPy и pandas: практический тренажёр ↗

    Бесплатный тренажёр NumPy и pandas для тех, кто знает основы Python. Научитесь очищать таблицы, объединять данные и собирать отчёты: 31 урок и 124 задачи с кодом и автоматической проверкой.