Оборачиваемость запасов в pandas: считаем дни хранения по ежедневным остаткам

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

Приводим запас и продажи к одной оценке

В нашем отчёте DIO — это средний запас в закупочной стоимости, делённый на себестоимость продаж за период и умноженный на число календарных дней. Результат выражен в днях. Он описывает, на сколько дней продаж выбранного периода хватает среднего запаса при наблюдавшемся темпе расхода.

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

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

Как подготовить ежедневные снимки

В примере четыре дня и два товара. Для каждой пары sku–day должна быть одна строка; число уникальных дней должно совпадать с длиной периода. В рабочем расчёте сравните фактические даты с полным календарём, потому что одинаковое количество строк ещё не исключает пропущенный день и лишнюю дату за пределами периода.

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

Товар с запасом и нулевой себестоимостью продаж не получает DIO=0. Его срок оборота на данном периоде не определён, и именно такие позиции часто требуют внимания закупщика. В коде заменяем нулевой знаменатель пропуском перед делением. Исходную нулевую себестоимость сохраняем рядом, чтобы отсутствие показателя было объяснимым.

import pandas as pd
stock = pd.DataFrame({"sku":["A"]*4+["B"]*4,"day":[1,2,3,4]*2,"stock_cost":[1000,1000,600,200,800,800,800,800]})
assert not stock.duplicated(["sku","day"]).any()
assert stock.groupby("sku").day.nunique().eq(4).all()
assert stock.stock_cost.ge(0).all()
avg = stock.groupby("sku").stock_cost.mean().rename("average_stock")
cogs = pd.Series({"A":1400,"B":0},name="cogs")
r = avg.to_frame().join(cogs)
r["DIO"] = r.average_stock.div(r.cogs.where(r.cogs.gt(0)))*4
print(r.to_string())
assert r.loc["A","average_stock"] == 700
assert r.loc["A","DIO"] == 2
assert pd.isna(r.loc["B","DIO"])

Средний запас A равен 700, себестоимость четырёхдневных продаж — 1400, DIO — два дня. Конечный запас 200 дал бы существенно меньший срок, хотя товар занимал склад весь период. У B средний запас 800 и нет продаж: показатель пустой, позиция попадает в очередь проверки залежалого ассортимента.

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

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

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

Как разобрать товар без продаж

Для B прежде всего проверьте, действительно ли остаток доступен покупателям. Он может находиться в браке, резерве или на складе, который не обслуживает магазин. Затем посмотрите карточку товара и дату последней продажи. Эти проверки разделяют отсутствие спроса, техническую недоступность и ошибку данных.

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

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

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

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

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

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