Валовая прибыль в pandas: как собрать отчёт по товарам и не потерять себестоимость

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

Сначала договоримся, какую прибыль считаем

В примере выручка — сумма после скидок, себестоимость — закупочная стоимость одной проданной единицы. Доставка, эквайринг и зарплаты сюда не входят. Полученный показатель позволяет сравнивать товарную маржу; для операционной прибыли понадобятся дополнительные расходы. Суммы храним в целых рублях, потому что в учебных данных нет копеек. В рабочей выгрузке удобнее использовать целые копейки.

Одна строка продаж соответствует товару в заказе. В справочнике одна строка соответствует артикулу. Такой договор позволяет требовать many_to_one при соединении. Если справочник случайно содержит две цены на один артикул, pandas остановит расчёт вместо удвоения выручки.

Неизвестная себестоимость должна остаться видимой

Для отсутствующей закупочной цены оставим NaN. Заполнение нулём означало бы, что товар достался бесплатно, и завысило бы прибыль. Для группы, у которой нет ни одной известной стоимости, sum(min_count=1) сохранит пропуск. Отдельно посчитаем выручку строк без себестоимости: она показывает размер ещё не разобранной части отчёта.

import pandas as pd
sales = pd.DataFrame({"sku":["A","A","B","C"], "qty":[2,1,3,1], "revenue":[500,250,900,200]})
costs = pd.DataFrame({"sku":["A","B"], "unit_cost":[150,200]})
d = sales.merge(costs, on="sku", how="left", validate="many_to_one")
d["cost"] = d.qty * d.unit_cost
d["gross_profit"] = d.revenue - d.cost
d["uncovered_revenue"] = d.revenue.where(d.unit_cost.isna(), 0)
report = d.groupby("sku")[["revenue","cost","gross_profit","uncovered_revenue"]].sum(min_count=1)
print(report.to_string())
assert report.loc["A","gross_profit"] == 300
assert report.loc["B","gross_profit"] == 300
assert pd.isna(report.loc["C","gross_profit"])
assert report.uncovered_revenue.sum() == 200

У товара A выручка 750 и себестоимость 450: прибыль равна 300. У B прибыль тоже 300, хотя выручка выше. Для C прибыль пока неизвестна, а неподтверждённая выручка составляет 200. Поэтому итог «заработали 600» следует читать как прибыль по обеспеченной себестоимостью части продаж.

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

Как превратить прибыль в решение о товаре

Равная прибыль A и B ещё не означает одинаковую эффективность. На A приходится 300 прибыли при 750 выручки, то есть 40%; на B — 300 при 900, примерно 33,3%. Абсолютная прибыль отвечает, какой вклад товар внёс в результат. Доля прибыли отвечает, сколько остаётся с рубля продаж. Для решения об ассортименте нужны оба показателя: высокая доля на небольшом объёме может давать меньше денег, чем низкая доля на массовом товаре.

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

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

Отчёт можно принимать только после сверки выручки. Сумма revenue после соединения должна равняться сумме входных продаж; число строк при many_to_one тоже сохраняется. В нашем примере проверка мощности соединения уже заложена в merge. Дальше стоит сверить итоги с системой заказов за тот же период и с теми же статусами. Заказ, который создан в октябре, а оплачен в ноябре, не должен переходить между версиями отчёта из-за случайной смены поля даты.

Практическое действие по результату состоит из двух очередей. Сначала восстановить себестоимость C, чтобы покрытие стало полным. Затем разобрать товары с низкой или отрицательной маржой: размер скидки, закупочную цену, долю возвратов. Это позволяет обсуждать конкретное изменение цены или условий закупки. Удалять товар из ассортимента только по проценту маржи рано: он может приводить покупателей к другим позициям корзины.

Разложение роста выручки поможет отделить изменение объёма бизнеса от изменения прибыльности: Выручка выросла на 2%: как разделить вклад заказов и среднего чека.

Сколько отчёта уже можно использовать

В нашем примере обеспечены себестоимостью 1650 из 1850 выручки, примерно 89,2%. Эти числа полезно вывести в шапке отчёта. Пользователь сразу видит, какая часть продаж ещё не вошла в подтверждённую прибыль, и не принимает промежуточный итог за весь магазин. Для сравнения двух месяцев покрытие нужно проверить в обоих: улучшение справочника может само по себе изменить наблюдаемую прибыль.

Следующая рабочая задача

В «Практической аналитике данных» вы разберёте продажи интернет-магазина «Маяк», проверите исходные таблицы, выберете метрики и подготовите аналитическую записку. Можно идти по маршруту pandas или SQL; результаты затем превращаются в графики и дашборд DataLens. Откройте программу курса «Практическая аналитика данных».

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

  • Практическая аналитика данных ↗

    Превратите знания SQL или Python в аналитическую работу: проверяйте данные, считайте бизнес-метрики, стройте дашборды и объясняйте выводы на трёх прикладных кейсах.