ABC-XYZ анализ на Python. Управление ассортиментом и схемами поставок

ABC анализ, классифицируем ассортимент в разрезе ассортиментных групп

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

У нас была достаточно широкая ассортиментная матрица , сегментированная на три уровня подгрупп. Ассортиментные группы уникальны по своему наполнению , характеристикам товара и количественно отличающиеся друг от друга.

Первая версия была разработана в Экселе на базе OLAP — куба. Проанализировав ее, я обнаружил ряд неточностей и недостатков:

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

  • Вторая проблема заключалась в том, что из OLAP — куба в отчет попадали только те позиции, по которым в отчетный период были продажи. С одной стороны это правильно, ведь мы делаем классификацию на основе продаж, а те товары, которые не продаются не нужно классифицировать, их нужно выводить из ассортимента. Но не все просто, когда вы начинаете делать аналитику не на яблоках, грушах, бананах из примеров на полях интернета, а на реальной матрице размером более 9000 SKU, разносторонней по консистенции и наполнению, имеющей размерные линейки и модельные ряды, различные ценовые сегменты в рамках ассортиментной группы, брендовое позиционирование, различные категории товаров под каналы продаж. В этом случае категорийному менеджеру становится сложно ориентироваться в своем ассортименте, и информацию о позициях без движения хорошо бы иметь в едином отчете.

  • ABC классификация была реализована в рамках верхнего уровня ассортиментных групп. Но поскольку наполнение групп было неравнозначным, группы нижнего уровня могли содержать разное количество SKU, а также имели разную важность для общего ассортимента, было принято решение сделать классификацию по всем уровням товарной матрицы , и предоставить право принятия решения по итоговой классификации категорийному менеджеру, глубоко понимающему значимость и логику деления своих категорий.

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

    Учитывая все вышеперечисленные требования и доработки традиционного ABC анализа, реализация задачи в Excel становится достаточно сложной и требующей большого временного ресурса для формирования и обновления отчета.

    Поэтому я решил реализовать его на Python. Перейдем к коду и посмотрим основные принципы формирования такого отчета.

    Импортируем необходимые библиотеки:

import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

Первым делом напишем функции для классификации товарной матрицы.

Основная функция: здесь позиции, отсортированные по убыванию, входящие нарастающим итогом в 80% вычисляемой метрики принимают класс A, входящие от 80% до 95% — класс B, от 95% до 100% — класс С.
Данная функция объявляет только классы в зависимости от итогов вычисления, сами преобразования и вычисления мы делаем в коде ниже.

def abc(cum):
  if cum <= 0.8:
    return 'A'
  elif cum <= 0.95:
    return 'B'
  else:
    return 'C'

Функция классификации в общепринятых в компании классах.
M — позиция основного ассортимента
O — позиция дополнительного ассортимента
U — позиции под заказ
W — позиции на вывод из ассортимента

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

def category(arg):
    categories = {
        "AAA": 'M', "AAB": 'M', "ABA": 'M', "ABB": 'M',
        "ACA": 'M', "BAA": 'M', "BBA": 'M',
        "AAC": 'O', "ABC": 'O', "ACB": 'O', "ACC": 'O',
        "BAB": 'O', "BAC": 'O', "BBB": 'O', "BBC": 'O',
        "BCA": 'O', "CAA": 'O', "CAB": 'O', "CBA": 'O',
        "BCB": 'U', "BCC": 'U', "CAC": 'U',
        "CBB": 'U', "CBC": 'U'
    }
    return categories.get(arg, 'W')

В этой функции используется метод .get () словаря, который возвращает значение для указанного ключа, если он существует в словаре, и возвращает 'W' (вывод из ассортимента, значение по умолчанию), если такого ключа нет.

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

ID

Description

Attribute

Category

Sub Cat

Group

2022–12

cost

revenue

q-ty

11437

art 1

M

Cat 1

Sub Cat 2

Group 3

30 000

14 000

350

11438

art 2

O

Cat 2

Sub Cat 4

Group 12

24 600

10 800

560

Загружаем данные, проводим необходимые предобработки:

df = pd.read_excel('Input_file.xlsx')

# отбираем нужные столбцы для построения отчета
df_r = df[['ID', 'Attribute', 'Category', 'Sub_cat','Group']]

#Добавим столбцы с расчетами, по которым будем делать классификацию
#рассчитываем товарооборот за отчетный период, выделим сумируемые колонки в 
#отдельный блок, чтобы в будущем можно было их легко изменить 
turn_over_to_sum = [
    '2022_12 cost', '2022_12 revenue',
    '2023_01 cost', '2023_01 revenue',
    '2023_02 cost', '2023_02 revenue',
    '2023_03 cost', '2023_03 revenue',
    '2023_04 cost', '2023_04 revenue',
    '2023_05 cost', '2023_05 revenue'
]

df_r['turn_over'] = df.loc[:, turn_over_to_sum].sum(axis=1)


#рассчитываем валовую прибыль за отчетный период
revenue_to_sum = [
    '2022_12 revenue',
    '2023_01 revenue',
    '2023_02 revenue',
    '2023_03 revenue',
    '2023_04 revenue',
    '2023_05 revenue'
]

df_r['revenue'] = df.loc[:, revenue_to_sum].sum(axis=1)

# рассчитываем количество проданных позиций

qty_to_sum = [
    '2022_12 qty',
    '2023_01 qty',
    '2023_02 qty',
    '2023_03 qty',
    '2023_04 qty',
    '2023_05 qty'
]

df_r['total_qty'] = df.loc[:, qty_to_sum].sum(axis=1)

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

#Фильтруем датафрейм, оставляем только нужные категории
df_r_filtred = df_r[(df_r['Attribute'] == 'M') |\
                    (df_r['Attribute'] == 'O') |\
                    (df_r['Attribute'] == 'U')]

Производим расчеты долей в процентах по ассортиментным группам

# Рассчитываем доли в процентах по Category
df_r_filtred['%TObyCategory']=df_r_filtred['turn_over'] / df_r_filtred.groupby('Category')['turn_over'].transform('sum')
df_r_filtred['%RVbyCategory']=df_r_filtred['revenue'] / df_r_filtred.groupby('Category')['revenue'].transform('sum')
df_r_filtred['%QTYbyCategory']=df_r_filtred['total_qty'] / df_r_filtred.groupby('Category')['total_qty'].transform('sum')

# Рассчитываем доли в процентах по Sub_cat
df_r_filtred['%TObySub_cat']=df_r_filtred['turn_over'] / df_r_filtred.groupby('Sub_cat')['turn_over'].transform('sum')
df_r_filtred['%RVbySub_cat']=df_r_filtred['revenue'] / df_r_filtred.groupby('Sub_cat')['revenue'].transform('sum')
df_r_filtred['%QTYbySub_cat']=df_r_filtred['total_qty'] / df_r_filtred.groupby('Sub_cat')['total_qty'].transform('sum')

# Рассчитываем доли в процентах по Group
df_r_filtred['%TObyGroup']=df_r_filtred['turn_over'] / df_r_filtred.groupby('Group')['turn_over'].transform('sum')
df_r_filtred['%RVbyGroup']=df_r_filtred['revenue'] / df_r_filtred.groupby('Group')['revenue'].transform('sum')
df_r_filtred['%QTYbyGroup']=df_r_filtred['total_qty'] / df_r_filtred.groupby('Group')['total_qty'].transform('sum')

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

df_r_filtred['TObyCategory_cum'] = df_r_filtred.sort_values('%TObyCategory', ascending=False).groupby('Category')['%TObyCategory'].cumsum()
df_r_filtred['RVbyCategory_cum'] = df_r_filtred.sort_values('%RVbyCategory', ascending=False).groupby('Category')['%RVbyCategory'].cumsum()
df_r_filtred['QTYbyCategory_cum'] = df_r_filtred.sort_values('%QTYbyCategory', ascending=False).groupby('Category')['%QTYbyCategory'].cumsum()

df_r_filtred['TObySub_cat_cum'] = df_r_filtred.sort_values('%TObySub_cat', ascending=False).groupby('Sub_cat')['%TObySub_cat'].cumsum()
df_r_filtred['RVbySub_cat_cum'] = df_r_filtred.sort_values('%RVbySub_cat', ascending=False).groupby('Sub_cat')['%RVbySub_cat'].cumsum()
df_r_filtred['QTYbySub_cat_cum'] = df_r_filtred.sort_values('%QTYbySub_cat', ascending=False).groupby('Sub_cat')['%QTYbySub_cat'].cumsum()

df_r_filtred['TObyGroup_cum'] = df_r_filtred.sort_values('%TObyGroup', ascending=False).groupby('Group')['%TObyGroup'].cumsum()
df_r_filtred['RVbyGroup_cum'] = df_r_filtred.sort_values('%RVbyGroup', ascending=False).groupby('Group')['%RVbyGroup'].cumsum()
df_r_filtred['QTYbyGroup_cum'] = df_r_filtred.sort_values('%QTYbyGroup', ascending=False).groupby('Group')['%QTYbyGroup'].cumsum()

Используя метод .applay () классифицируем позиции по ассортиментным группам.

#классифицируем позиции по Category
df_r_filtred['Cat_TObyCategory'] = df_r_filtred['TObyCategory_cum'].apply(abc)
df_r_filtred['Cat_RVbyCategory'] = df_r_filtred['RVbyCategory_cum'].apply(abc)
df_r_filtred['Cat_QTYbyCategory'] = df_r_filtred['QTYbyCategory_cum'].apply(abc)

#классифицируем позиции по Sub_cat
df_r_filtred['Cat_TObySub_cat'] = df_r_filtred['TObySub_cat_cum'].apply(abc)
df_r_filtred['Cat_RVbySub_cat'] = df_r_filtred['RVbySub_cat_cum'].apply(abc)
df_r_filtred['Cat_QTYbySub_cat'] = df_r_filtred['QTYbySub_cat_cum'].apply(abc)

#классифицируем позиции по Group
df_r_filtred['Cat_TObyGroup'] = df_r_filtred['TObyGroup_cum'].apply(abc)
df_r_filtred['Cat_RVbyGroup'] = df_r_filtred['RVbyGroup_cum'].apply(abc)
df_r_filtred['Cat_QTYbyGroup'] = df_r_filtred['QTYbyGroup_cum'].apply(abc)

Визуализируем результаты в разрезе основной ассортиментной группы Category и сделаем первые выводы из полученного ABC — анализа.

#построим сводную таблицу сгруппированную по классам ABC в Category и агрегированную 
#по колличеству позиций, количеству проданных единиц и товарообороту
df_abc_byCategory = df_r_filtred.groupby('Cat_TObyCategory').agg(
    total_skus=('ID', 'nunique'),
    total_units=('total_qty', sum),
    total_turn_over=('turn_over', sum),
).reset_index()
# Посмотрим на показатели товарооборота в категориях
f, ax = plt.subplots(figsize=(15, 6))
ax = sns.barplot(x="Cat_TObyCategory",
                 y="total_turn_over",
                 data=df_abc_byTK,
                 palette="Blues_d")\
                .set_title("Turn Over by ABC class by TK",fontsize=15)

Визуальное подтверждение принципа Парето, 80% товарооборота дают позиции категории A

Визуальное подтверждение принципа Парето, 80% товарооборота дают позиции категории A

#Посмотрим теперь на количество позиций в категориях 
f, ax = plt.subplots(figsize=(15, 6))
ax = sns.barplot(x="Cat_TObyCategory",
                 y="total_skus",
                 data=df_abc_byTK,
                 palette="Blues_d")\
                .set_title("SKUs by ABC class by TK",fontsize=15)

Большая часть позиций (более 5000 SKU) находится в категории C

Большая часть позиций (более 5000 SKU) находится в категории C

Визуализировав только два графика уже видим проблему, в том что товарная матрица недостаточно эффективная, больше половины матрицы находится в категории C.
Нужно срочно делать финальные преобразования и отдавать отчет в работу управленцам ассортимента, разбираться и повышать эффективность ассортимента.

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

#объеденим полученные признаки в единый. Выполним это по срезам категорий
df_r_filtred['ABCbyCategory'] = df_r_filtred['Cat_TObyCategory'] + df_r_filtred['Cat_RVbyCategory'] + df_r_filtred['Cat_QTYbyCategory']
df_r_filtred['ABCbySub_cat'] = df_r_filtred['Cat_TObySub_cat'] + df_r_filtred['Cat_RVbySub_cat'] + df_r_filtred['Cat_QTYbySub_cat']
df_r_filtred['ABCbyGroup'] = df_r_filtred['Cat_TObyGroup'] + df_r_filtred['Cat_RVbyGroup'] + df_r_filtred['Cat_QTYbyGroup']

#присваиваем рекомендованный признак-категорию в принятой
#системе классификации внутри компании
df_r_filtred['CATbyCategory'] = df_r_filtred['ABCbyCategory'].apply(category)
df_r_filtred['CATbySub_cat'] = df_r_filtred['ABCbySub_cat'].apply(category)
df_r_filtred['CATbyGroup'] = df_r_filtred['ABCbyGroup'].apply(category)

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

df_r_filtred.loc[df_r_filtred['ID'] > 1705727, 'CATbyCategory'] = "NEW"
df_r_filtred.loc[df_r_filtred['ID'] > 1705727, 'CATbySub_cut'] = "NEW"
df_r_filtred.loc[df_r_filtred['ID'] > 1705727, 'CATbyGroup'] = "NEW"

То есть, из расчетов для классификации мы эти позиции не убираем, поскольку они также влияют на общую картину ассортимента, но не оцениваем, давая возможность достичь им своего полного потенциала.

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

ABC-XYZ анализ. Помогаем закупкам управлять запасами

Давайте прежде всего поймем разницу между этими двумя подходами анализа ассортимента.

ABC анализ дает нам рейтинг позиций, и поскольку данный подход построен на принципе Парето, позиции, приносящие 80% прибыли находятся в признаке A, и за ними ведется постоянный контроль наличия товаров на складе в достаточном количестве.

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

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

Соответственно соединение этих двух методов, позволяет нам создать классы, которые показывают не только вклад позиции в продажи, но и изменчивость спроса, предоставляя менеджеру по закупкам полное понимание ассортимента и методы управления закупками и запасами.

Давайте сразу расшифруем эти классы и составим матрицу рекомендаций по управлению запасами, которую далее будем переносить в код.

Понимание классов ABC‑XYZ анализа

AX

BX

CX

Высокий ранг

Устойчивый спрос

Легко прогнозируются

Легко управляются

Средний ранг

Устойчивый спрос

Легко прогнозируются

Легко управляются

Низкий ранг

Устойчивый спрос

Легко прогнозируются

Легко управляются

AY

BY

CY

Высокий ранг

Изменчивый спрос

Сложнее прогнозируются

Сложнее управляются

Средний ранг

Изменчивый спрос

Сложнее прогнозируются

Сложнее управляются

Низкий ранг

Изменчивый спрос

Сложнее прогнозируются

Сложнее управляются

AZ

BZ

CZ

Высокий ранг

Спонтанный спрос

Трудно прогнозируются

Трудно управляются

Средний ранг

Спонтанный спрос

Трудно прогнозируются

Трудно управляются

Низкий ранг

Спонтанный спрос

Трудно прогнозируются

Трудно управляются

На основании понимания классов построим матрицу рекомендаций управления складскими запасами.

AX

BX

CX

Автоматическое пополнение запасов

Поставки JIT, низкий страховой запас

Постоянный складской запас

Автоматическое пополнение запасов

Периодическая корректировка прогнозирования

Низкий страховой запас

Автоматическое пополнение запасов

Периодическая оценка эффективности

Низкий страховой запас

AY

BY

CY

Полуавтоматическое пополнение запасов

Низкий страховой запас

Полуавтоматическое пополнение запасов

Увеличиваемый сезонный страховой запас

Полуавтоматическое пополнение запасов

Высокий страховой запас

AZ

BZ

CZ

Закупки под заказ

Без страхового запаса

Без складских остатков

Закупки под заказ

Без страхового запаса

Удлиненный срок выполнения заказов

Без складских остатков

Автоматическое пополнение запасов

Высокий страховой запас

Периодическая оценка целесообразности поддержания позиций в ассортименте

Управление запасами ABC XYZ представляет собой один из многих методов, применяемых в операционном управлении для контроля запасов, наряду с такими подходами, как HML, VED, SDF, SOS, GOLF и FNS. Каждый из этих методов разработан для адресации определенных уникальных задач, связанных с разнообразными типами запасов, и может включать учет особенностей запасов, которые которые предназначены для использования в производственном процессе.

Перейдем от теории к практике и напишем код для ABC — XYZ анализа.
Начнем с функций классификации, понимания классов и менеджмента позиций по классам.

# функция классификации XYZ
def xyz_classify_product(cov):
    if cov <= 0.5:
        return 'X'
    elif cov > 0.5 and cov <= 1.0:
        return 'Y'
    else:
        return 'Z'

# функция андестендинг
def understending(arg):
    values = {
        "AX": 'Высокая ценность, устойчивый спрос, легко прогнозировать, просто управлять',
        "BX": 'Средняя ценность, устойчивый спрос, легко прогнозировать, просто управлять',
        "CX": 'Низкая ценность, устойчивый спрос, легко прогнозировать, просто управлять',
        "AY": 'Высокая ценность, переменный спрос, сложнее прогнозировать, сложнее управлять',
        "BY": 'Средняя ценность, переменный спрос, сложнее прогнозировать, сложнее управлять',
        "CY": 'Низкая ценность, переменный спрос, сложнее прогнозировать, сложнее управлять',
        "AZ": 'Высокая ценность, спонтанный спрос, трудно прогнозировать, трудно управлять',
        "BZ": 'Средняя ценность, спонтанный спрос, трудно прогнозировать, трудно управлять',
        "CZ": 'Низкая ценность, спонтанный спрос, трудно прогнозировать, трудно управлять',
        "Cnan": 'Нет продаж'
    }
    return values.get(arg)


# функция менджмент
def managment(arg):
    strategies = {
        "AX": 'Автоматическое пополнение, низкий буфер, управление JIT',
        "BX": 'Автоматическое пополнение, переодический подсчет, низкий буфер',
        "CX": 'Автоматическое пополнение, переодическая оценка, низкий буфер',
        "AY": 'Полуавтоматическое пополнение, низкий буфер',
        "BY": 'Полуавтоматическое пополнение, сезонный буфер скорректированный вручную',
        "CY": 'Полуавтоматическое пополнение, высокий буфер',
        "AZ": 'Поставка под заказ, нет буфера, нет запаса',
        "BZ": 'Поставка под заказ, нет буфера, указано время поставки, нет запаса',
        "CZ": 'Автоматическое пополнение, высокий буфер, переодическая оценка',
        "Cnan": 'Вывод из матрицы если не новинка'
    }
    return strategies.get(arg)

      

Возвращаемся к первоначальному отчету и отбираем из него необходимые столбцы для построения XYZ анализа.

#Отбираем столбцы по лоличеству продаж в течение года
df_12m_units = df[['ID','Attribute','2022_06 qty',\
                                    '2022_07 qty',\
                                    '2022_08 qty',\
                                    '2022_09 qty',\
                                    '2022_10 qty',\
                                    '2022_11 qty',\
                                    '2022_12 qty',\
                                    '2023_01 qty',\
                                    '2023_02 qty',\
                                    '2023_03 qty',\
                                    '2023_04 qty',\
                                    '2023_05 qty']]

#Переименуем столбцы для удобства работы
df_12m_units.columns = ['ID','Attribute','m1','m2','m3','m4','m5','m6','m7','m8','m9','m10','m11','m12']

#Отбираем нужные категории для анализа
df_12m_units = df_12m_units[(df_12m_units['Attribute'] == 'O') |\
                            (df_12m_units['Attribute'] == 'M') |\
                            (df_12m_units['Attribute'] == 'U')

Далее проводим необходимые расчеты метрик для дальнейшей классификации позиций

#стандартное отклонении спроса
df_12m_units['std_demand'] = df_12m_units[['m1','m2','m3','m4','m5','m6','m7',\
                                           'm8','m9','m10','m11','m12']].std(axis=1)

#общий годовой спрос по каждой позиции
df_12m_units = df_12m_units.assign(total_demand = df_12m_units['m1'] + df_12m_units['m2'] +\
                                                  df_12m_units['m3'] + df_12m_units['m4'] +\
                                                  df_12m_units['m5'] + df_12m_units['m6'] +\
                                                  df_12m_units['m7'] + df_12m_units['m8'] +\
                                                  df_12m_units['m9'] + df_12m_units['m10'] +\
                                                  df_12m_units['m11'] + df_12m_units['m12'])

#среднемесячный спрос
df_12m_units = df_12m_units.assign(avg_demand = df_12m_units['total_demand'] / 12 )

#коэффициент вариации српоса
df_12m_units['cov_demand'] = df_12m_units['std_demand'] / df_12m_units['avg_demand']

После того, как мы провели необходимые расчеты, классифицируем полученный результат, применяя функцию xyz_classify_product () с помощью метода .applay () и выводим количества элементов по классам

df_12m_units['xyz_class'] = df_12m_units['cov_demand'].apply(xyz_classify_product)
df_12m_units.xyz_class.value_counts()

Вывод:
Z    4095
Y    1892
X     725
Name: xyz_class, dtype: int64 

Проведем предобработку данных для визуализации результата XYZ анализа

#сгруппируем полученные данные и агрегируем необходимые метрики
df_12m_units.groupby('xyz_class').agg(
    total_skus=('ID', 'nunique'),
    total_demand=('total_demand', 'sum'),
    std_demand=('std_demand', 'mean'),
    avg_demand=('avg_demand', 'mean'),
    avg_cov_demand=('cov_demand', 'mean'),
)

#разворачиваем данные по классам и месяцам для построения графиков
df_monthly = df_12m_units.groupby('xyz_class').agg(
    m1=('m1', 'sum'),
    m2=('m2', 'sum'),
    m3=('m3', 'sum'),
    m4=('m4', 'sum'),
    m5=('m5', 'sum'),
    m6=('m6', 'sum'),
    m7=('m7', 'sum'),
    m8=('m8', 'sum'),
    m9=('m9', 'sum'),
    m10=('m10', 'sum'),
    m11=('m11', 'sum'),
    m12=('m12', 'sum'),
)
#формируем датафрейм по месяцам, классам и количеству шт
df_monthly_unstacked = df_monthly.unstack('xyz_class').to_frame()
df_monthly_unstacked = df_monthly_unstacked.reset_index().rename(columns={'level_0': 'month', 0: 'demand'})

Визуализируем полученные данные

f, ax = plt.subplots(figsize=(15, 6))
ax = sns.barplot(x="month",
                 y="demand",
                 hue="xyz_class",
                 data=df_monthly_unstacked,
                 palette="Blues_d")\
                .set_title("XYZ demand by month",fontsize=15)

a6bfa9be7aef77528e79cb078544299b.png

Теперь, когда мы выполнили на наших данных обе классификации, можно приступать к объединению отчетов и получить инструмент управления складскими запасами по классификации ABC-XYZ

#создаем копии датафреймов для формирования обобщенного отчета
df_xyz = df_12m_units.copy()
df_abc = df_r_filtred.copy()

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

#соединяем два датасета
df_abc_xyz = df_abc.merge(df_xyz, on='ID', how='left')

#создаем abc_xyz class
df_abc_xyz['abc_xyz_class'] = df_abc_xyz['Cat_TObyCategory'].astype(str) + df_abc_xyz['xyz_class'].astype(str)

#добавляем колонки объяснения классов и рекомендации по управлению запасами 
df_abc_xyz['understanding'] = df_abc_xyz['abc_xyz_class'].apply(understending)
df_abc_xyz['managment'] = df_abc_xyz['abc_xyz_class'].apply(managment)

Для понимания проделанной работы построим визуализацию классов в количественном значении

df_abc_xyz_summary = df_abc_xyz.groupby('abc_xyz_class').agg(
    total_skus=('Артикул', 'nunique'),
    total_demand=('total_demand', sum),
    avg_demand=('avg_demand', 'mean'),
    total_turn_over=('turn_over', sum),
).reset_index()

df_abc_xyz_summary.sort_values(by='total_turn_over', ascending=False)

f, ax = plt.subplots(figsize=(15, 6))
ax = sns.barplot(x="abc_xyz_class",
                 y="total_skus",
                 data=df_abc_xyz_summary,
                 palette="Blues_d")\
                .set_title("SKUs by ABC-XYZ class",fontsize=15)

Количество позиций по классам

Количество позиций по классам

Заключение

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

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

С уважением,
Data Scientist, Вячеслав Гусев

© Habrahabr.ru