Сравнение прайс-листов поставщиков в pandas: цена зависит от объёма заказа

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

Договоримся, как устроены ступени

В примере цена действует на все единицы заказа после достижения порога min_qty. Если заказали 120 штук, применяется цена ступени от 100 ко всем 120. Это отличается от прогрессивного тарифа, где первые 99 единиц стоят по одной цене, а следующие — по другой. Правило нужно взять из коммерческого предложения, прежде чем писать формулу.

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

Полная стоимость важнее цены строки

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

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

Исполняемый пример

Все цены и доставка заданы в копейках, поэтому итог — целое число. Отдельная таблица логистики содержит уникального поставщика; проверка many_to_one защищает от случайного размножения предложений при соединении. Результат сохраняет выбранный порог и цену: закупщик сможет объяснить, откуда взялась сумма.

import pandas as pd
price = pd.DataFrame([
    ('A',1,10000), ('A',100,9000),
    ('B',1,9800), ('B',100,9500)
],columns=['supplier','min_qty','unit_price'])
shipping = pd.DataFrame([('A',50000),('B',0)],columns=['supplier','delivery'])
orders = pd.DataFrame([(1,80),(2,120)],columns=['scenario','qty'])
assert not price.duplicated(['supplier','min_qty']).any()
assert (price.unit_price > 0).all()
candidates = orders.merge(price,how='cross')
candidates = candidates[candidates.min_qty <= candidates.qty]
chosen = (candidates.sort_values('min_qty')
          .drop_duplicates(['scenario','supplier'],keep='last'))
chosen = chosen.merge(shipping,on='supplier',validate='many_to_one')
assert chosen.delivery.notna().all()
assert len(chosen) == len(orders)*price.supplier.nunique()
chosen['total'] = chosen.qty*chosen.unit_price+chosen.delivery
chosen = chosen.sort_values(['scenario','total','supplier'])
print(chosen[['scenario','qty','supplier','min_qty','total']].to_string(index=False))
winners = chosen.drop_duplicates('scenario')
assert winners.supplier.tolist() == ['B','A']
assert winners.total.tolist() == [784000,1130000]

Почему победитель поменялся

При 80 единицах A обойдётся в 8500 рублей с доставкой, B — в 7840. При 120 единицах у A сработает ступень от 100: итог 11300 рублей против 11400 у B. Поэтому фраза «поставщик B дешевле» верна только вместе с объёмом, датой и условиями заказа. Отчёт без этих полей быстро превращается в неверную рекомендацию.

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

Какие ограничения добавить в рабочий подбор

Проверьте минимальный заказ и кратность упаковки до сравнения. Если нужно 81 штука, а поставщик отгружает коробками по 20, оплачиваемый объём будет 100. Остаток после закупки влияет на решение: экономия за счёт скидки может сопровождаться лишним запасом. Сравнивайте одинаково определённую потребность и явно показывайте купленное количество.

Если в прайсе нет базовой ступени, некоторые поставщики не смогут обслужить малый объём. В примере это остановит проверка полноты. В рабочем отчёте разумнее сохранить такое предложение со статусом «нет допустимого тарифа» и объяснить причину, чем выдавать пустую сумму за нулевую стоимость. Так закупщик увидит все рассмотренные варианты.

Как передать результат закупщику

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

Проверка на пороге скидки

Перед загрузкой нового прайса рассчитайте стоимость непосредственно до порога и на самом пороге. При тарифе на все единицы заказ из 100 штук иногда оказывается дешевле заказа из 99. Это допустимое следствие условий, но оно должно быть видно закупщику. Иначе программа предложит докупить лишнюю единицу, а пользователь примет неожиданную сумму за ошибку.

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

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

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

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

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

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