Введение, или почему красивые отчеты нужно выкидывать только в красивую корзину

Задумывались ли вы, насколько обманчивой может быть простая бизнес-метрика, если смотреть на неё без контекста?

Представьте типичную ситуацию на совещании: руководство смотрит на цифру среднего чека, и эта цифра в 4 раза меньше планируемой. Первая реакция топ-менеджера вполне предсказуема: «Почему так мало? У нас автосервис премиум брендов! Давайте избавимся от всех этих мелких работ вроде сезонного шиномонтажа и хранения колес. Они нам только портят красивую статистику, перегружают мастеров и размывают показатели!»

Звучит логично? На первый взгляд — абсолютно. Но если залезть в реальные данные с головой, выясняется, что подобное «оптимизаторское» решение способно лишить компанию доброй трети валовой выручки.

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

Если вы не любитель кода на Python — смело пролистывайте серые блоки и спойлеры. Я постарался сделать так, чтобы графики и бизнес-выводы говорили сами за себя.

О чем мы поговорим в этой статье:

  • Как очистить 10-летний архив заказов из базы и не сойти с ума от «зависших» заказов и странных номенклатур.

  • Почему слепое верующее отношение к среднему чеку приводит к катастрофе в маркетинге (и при чем тут Симпсон).

  • Как правильно разделить клиентов на «своих» (купивших авто у нас) и «чужих», и почему их поведение кардинально отличается.

  • Главная интрига: почему автовладельцы, регулярного приезжающие на «дешевую» сезонную переобувку, за свой жизненный цикл (LTV) приносят компании в 3 раза больше денег, чем те, кто заезжает исключительно на редкий крупный ремонт.

Итак, погружаемся в детали!

Шаг 1. Готовим данные: отсекаем шум и смотрим на макропоказатели

Как водится в любых реальных проектах, выгрузка из ERP-системы редко приходит в идеальном «стерильном» виде. А уж тем более если это исторический датасет за 10 лет работы предприятия.

В сыром датасете содержится 8 основных характеристик: уникальный ID заказа, даты создания, взятия в работу, обновления и завершения, хешированный VIN-номер автомобиля, а также начисленная и фактически уплаченная сумма.

Первым делом импортируем необходимые библиотеки и наведем порядок. Так же в рамках этой публикации нам придется убрать реальные цифры и перевести все в проценты:

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from matplotlib.ticker import FuncFormatter

# Загружаем выгрузку заказ-нарядов
data = pd.read_csv('data/orders_2015_2024.csv', sep=';')

# Приводим колонки с датами к формату datetime
date_columns = ['date_created', 'date_started', 'date_updated', 'date_completed']
for col in date_columns:
    data[col] = pd.to_datetime(data[col], format='%d.%m.%Y %H:%M')

# АНОНИМИЗАЦИЯ: Смотрим на структуру базы в относительных величинах (%)
total_records = len(data)
canceled_orders = data['sum_paid'].isna().sum()

print(f"Общая структура сырой базы: 100.0%")
print(f"Доля незавершенных/неоплаченных заказов: {(canceled_orders / total_records) * 100:.1f}%")

# Отфильтровываем только завершенные и оплаченные заказы
completed_orders = data[
    (data['date_completed'].notna()) & 
    (data['sum_paid'].notna())
].copy()

print(f"Доля успешно обработанных заказов для анализа: {(len(completed_orders) / total_records) * 100:.1f}%")
Общая структура сырой базы: 100.0%
Доля незавершенных/неоплаченных заказов: 8.4%
Доля успешно обработанных заказов для анализа: 91.6%

Около 8% заказов в базе оказались незавершенными или неоплаченными (отказы клиентов, технические дубли, ошибки менеджеров при вводе заказов). В этот раз не будем разбираться, почему эти заказы зависли — отнесем это к «мелким недоразумениям», одной рукой прикроем глаза, а второй просто выкинем их из выборки и больше не будем о них вспоминать.

В итоге у нас остается чистый массив, составляющий более 91% от исходного датасета. Накопленная за десятилетие плотность данных (счет идет на сотни тысяч завершенных заказов) гарантирует, что наше исследование будет репрезентативным, а статистические выбросы не исказят финальные бизнес-выводы.

Динамика выручки: 10 лет на качелях

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

# Группируем данные по месяцам даты завершения заказа
monthly_revenue = (
    completed_orders.set_index("date_completed")
    .groupby(pd.Grouper(freq="ME"))["sum_paid"]
    .sum()
    .reset_index()
)
base_value = monthly_revenue["sum_paid"].iloc[0]
monthly_revenue["revenue_pct"] = (monthly_revenue["sum_paid"] / base_value) * 100

Посмотреть код для настройки и отрисовки графика выручки
# Пусть график выглядит красиво
sns.set_theme(style="white")  # Убираем стандартную серую сетку Seaborn
plt.rcParams["font.family"] = "sans-serif"  # Чистый шрифт без засечек

fig, ax = plt.subplots(figsize=(11, 5.5), dpi=150)

# Построение графика
sns.lineplot(
    data=monthly_revenue,
    x="date_completed",
    y="revenue_pct",
    color="#0F62FE",  # Современный технологичный синий (IBM Design)
    linewidth=2.5,
    marker="o",
    markersize=7,
    markerfacecolor="#0F62FE",
    markeredgecolor="white",
    markeredgewidth=1.5,
    ax=ax,
)

# Тонкая настройка сетки и границ (десеткация)
ax.grid(
    axis="y", linestyle="--", color="#E0E0E0", linewidth=0.7
)  # Только горизонтальные линии
sns.despine(left=True, bottom=False)  # Удаляем верхнюю, правую и левую границы

# 5. Оформление текста (Выравнивание по левому краю для заголовка)
ax.text(
    0.0,
    1.12,
    "Динамика ежемесячной выручки",
    fontsize=16,
    fontweight="bold",
    color="#161616",
    transform=ax.transAxes,
)
ax.text(
    0.0,
    1.06,
    "Относительные показатели продаж компании (Базовый месяц = 100%)",
    fontsize=11,
    color="#525252",
    transform=ax.transAxes,
)

# Убираем стандартные крупные подписи осей (все понятно из контекста)
ax.set_xlabel("", fontsize=11, color="#525252", labelpad=10)
ax.set_ylabel("", fontsize=11, color="#525252")

# Форматирование дат на оси X
ax.tick_params(axis="both", which="major", labelsize=10, colors="#525252")
fig.autofmt_xdate(rotation=0, ha="center")  # Красивое выравнивание дат без наклона

# Форматирование оси Y
def percent_format(x, pos):
    return f"{int(x)}%"


ax.yaxis.set_major_formatter(FuncFormatter(percent_format))

# Аккуратные подписи значений (выводим через одну, но оставляем последнюю)
step = 2  # Шаг отображения: 2 = через одну, 3 = через две и так далее
total_points = len(monthly_revenue)

for i, row in monthly_revenue.iterrows():
    # Условие: выводим точку, если она кратна шагу ИЛИ если это самый последний месяц
    if (i % step == 0) or (i == total_points - 1):

        # Небольшая проверка: если последняя точка встает слишком близко к предыдущей по шагу,
        # можно пропустить предпоследнюю подпись во избежание наложения
        if (i == total_points - 1) and (
            (i - 1) % step == 0
        ) and (total_points > 1):
            # (Опционально) этот блок автоматически предотвратит наложение
            # последней точки на предпоследнюю, если они идут подряд
            pass

        ax.text(
            row["date_completed"],
            row["revenue_pct"] + 4,  # Смещение вверх относительно линии
            f"{int(row['revenue_pct'])}%",
            ha="center",
            va="bottom",
            fontsize=9,
            fontweight="semibold",
            color="#393939",
        )
plt.tight_layout()
plt.show()
Динамика выручки по месяцам
Динамика выручки по месяцам

На графике отчетливо видны три ключевых тренда:

  1. Ярко выраженная сезонность: Начало каждого года (январь–февраль) традиционно стартует с просадки. Конец года — короткий всплеск в ноябре с последующим затишьем в праздники.

  2. Устойчивый рост до 2022 года: Предприятие планомерно наращивало обороты, пик выручки пришелся на 2021 — начало 2022 года.

  3. Кризис и восстановление: В 2022 году произошел прогнозируемый обвал (уход европейских брендов, проблемы с поставками оригинальных запчастей). Однако хорошая новость в том, что к 2024 году выручка начала уверенно восстанавливаться.


Загадка слишком низкого чека

А теперь давайте посчитаем стандартную метрику, на которую так любит ориентироваться топ-менеджмент — средний чек. Нам снова придется перевести абсолютные финансовые показатели в относительный формат: примем средний чек за первый год наблюдений за 100%, а для остальных лет посчитаем индекс роста.

# Считаем средний чек по годам
economics_metrics = (
    completed_orders.set_index("date_completed")
    .groupby(pd.Grouper(freq="YE"))
    .agg(
        total_revenue=("sum_paid", "sum"),
        average_check=("sum_paid", "mean"),
        total_orders=("order_id", "count"),
    )
    .reset_index()
)

economics_metrics["year"] = economics_metrics["date_completed"].dt.year

# ==========================================
# АНОНИМИЗАЦИЯ: Расчет индекса среднего чека
# ==========================================
base_check = economics_metrics["average_check"].iloc[0]  # Первый год = 100%

for idx, row in economics_metrics.iterrows():
    # Считаем процент относительно базового года
    check_index = (row["average_check"] / base_check) * 100
    print(
        f"Год {int(row['year'])}: Индекс среднего чека = {check_index:.1f}%"
    )
Год 2015: Индекс среднего чека = 100.0%
Год 2017: Индекс среднего чека = 110.1%
Год 2020: Индекс среднего чека = 120.0%
Год 2022: Индекс среднего чека = 124.1%
Год 2024: Индекс среднего чека = 138.2%

Стоп. Давайте задумаемся.

Мы анализируем крупного дилера, работающего в премиум-сегменте (где запчасти и нормо-часы за 10 лет подорожали в разы). Почему к 2024 году индекс среднего чека вырос всего до 138% от уровня 2015 года? С учетом накопившейся за декаду инфляции (которая превысила 80–90%) это означает, что в реальном выражении наш средний чек не просто не вырос — он ощутимо «просел»!

Неужели клиенты премиальных марок стали настолько экономными? Или мы ремонтируем машины «вполсилы»?

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


Шаг 2. Разбор номенклатуры: кто сбил средний чек премиум-сервиса?

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

# Загружаем детальную историю по работам и запчастям внутри заказов
orders_breakdowns = pd.read_csv('data/orders_breakdowns_historical.csv', sep=';')

# Объединяем с базой завершенных заказов
completed_orders_detailed = completed_orders.merge(orders_breakdowns, on='order_id')

# Считаем долю каждой услуги от общего объема всех выполненных строк
top_items_pct = (completed_orders_detailed['item_id'].value_counts(normalize=True).head(10) * 100)

print("Топ-10 самых частых услуг в сервисе за 10 лет (в % от общего объема работ):")
print(top_items_pct.round(2).astype(str) + '%')
Топ-10 самых частых услуг в сервисе за 10 лет (в % от общего объема работ):
40-0001-02    12.2%  # Сезонное хранение комплекта колес
00-8030-01    12.2%  # Подготовка автомобиля к работе
54-1011-01     5.5%  # Входящий тест электрооборудования
40-1450-99     2.8%  # Снятие и установка рабочих колес
40-2070-99     2.8%  # Балансировка колес
40-2440-99     2.7%  # Замена шин (демонтаж/монтаж)
42-0004-01     2.6%  # Проверка тормозной системы
40-2181-01     2.5%  # Балансировочные грузики
00-9015-00     1.8%  # Осмотр ходовой части
54-0650-01     1.6%  # Поддержание напряжения бортовой сети при диагностике

Вот она, разгадка!

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

В базе найдутся десятки тысяч чеков на мелкие фиксированные суммы (составляющие едва ли четверть от целевого среднего чека компании), где кроме кода 40-0001-02 (Сезонное хранение) больше ничего нет.

Что происходит со средним чеком?

Включается банальный математический эффект: гигантский объем регулярных чеков с минимальной стоимостью «тянет» арифметическое среднее вниз. Бизнес видит скромную итоговую цифру в отчете и думает, что зарабатывает мало на одном клиенте.

💡 Аналитическая пауза. Привет, Симпсон!

Эдвард Симпсоон (1922–2019)

В 1951 году, он математически описал феномен, который сегодня называют Парадоксом Симпсона (Simpson’s paradox). Это ситуация в анализе данных, когда в отдельных группах наблюдается одна четкая тенденция, но стоит нам объединить эти группы в общий массив, как тренд полностью исчезает или переворачивается на противоположный. В бизнесе этот парадокс — главный враг топ-менеджмента. Когда руководство смотрит на «средний чек» или «общую маржинальность» по компании, агрегированные метрики маскируют реальное положение дел. Чтобы не совершить фатальную ошибку, аналитик обязан делить данные на когорты и сегменты — только там скрываются истинные инсайты.

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

# Коды услуг, относящиеся к шиномонтажу и хранению
tire_services = ['40-0001-02', '40-2070-99', '40-2440-99', '40-1450-99', '40-2181-01']

# Находим заказ-наряды, в которых есть шиномонтажные работы
orders_with_tires = set(completed_orders_detailed[
    completed_orders_detailed['item_id'].isin(tire_services)
]['order_id'])

# АНОНИМИЗАЦИЯ: Фильтруем через квантиль, чтобы не зашивать в код абсолютную сумму в рублях
# Отсекаем транзакции ниже 25-го перцентиля, убирая мелкий экспресс-шум
price_threshold = completed_orders['sum_paid'].quantile(0.25)

# Разделяем заказы: исключаем шиномонтаж и ультра-мелкие чеки
complex_repairs = completed_orders[
    ~completed_orders['order_id'].isin(orders_with_tires) & 
    (completed_orders['sum_paid'] > price_threshold)
].copy()

# Считаем исходный и скорректированный средний чек по годам
yearly_raw = completed_orders.groupby(completed_orders['date_completed'].dt.year)['sum_paid'].mean()
yearly_adjusted = complex_repairs.groupby(complex_repairs['date_completed'].dt.year)['sum_paid'].mean()

# Считаем коэффициент превышения (разрыв между чистым ремонтом и валовым чеком)
adjusted_gap = yearly_adjusted / yearly_raw

print("Превышение скорректированного чека над базовым (коэффициент):")
for year in:
    print(f"Год {year}: Чистый ремонт выше базового в {adjusted_gap[year]:.2f} раз(а)")
Превышение скорректированного чека над базовым (коэффициент):
Год 2020: Чистый ремонт выше базового в 1.98 раз(а)
Год 2021: Чистый ремонт выше базового в 2.11 раз(а)
Год 2022: Чистый ремонт выше базового в 2.23 раз(а)
Год 2023: Чистый ремонт выше базового в 2.35 раз(а)
Год 2024: Чистый ремонт выше базового в 2.40 раз(а)

Картина мгновенно меняется!

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


Искушение классического маркетолога

Глядя на эти цифры, неопытный аналитик или горячий директор по маркетингу бежит к руководству с сильной (как ему кажется) идеей:

«Смотрите! Сезонное обслуживание забивает существенную часть мощности ремзоны, загружает подъемники и мастеров, а приносит копейки! Давайте закроем услугу хранения колес и шиномонтаж, а освободившиеся посты отдадим под сложный ремонт, где средний чек на порядок выше! Мы поднимем выручку в разы!»

Звучит отлично. Ведь ресурсы ремзоны ограничены, а забивать их дешёвыми работами — управленческий грех.

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

В следующем шаге мы проверим гипотезу: а кто эти клиенты? Приезжают ли они только на переобувку, или за сезонным шиномонтажом скрывается что-то гораздо более масштабное?

Шаг 3. «Свои» vs «Чужие» и парадокс шиномонтажа

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

  1. «Свои» — купили автомобиль в нашем дилерском центре.

  2. «Чужие» — купили авто в другом месте, но приехали к нам на обслуживание.

# Маркируем заказы
completed_orders_detailed["purchased_vehicle"] = (
    completed_orders_detailed["purchase_date"].notna()
)

# Считаем абсолютные значения
sales_service_sync = (
    completed_orders_detailed.groupby("purchased_vehicle")
    .agg(
        total_orders=("order_id", "nunique"),
        unique_customers=("vin_number", "nunique"),
    )
    .reset_index()
)

# Переводим в проценты от общего итога
total_orders_sum = sales_service_sync["total_orders"].sum()
total_customers_sum = sales_service_sync["unique_customers"].sum()

sales_service_sync["total_orders (%)"] = (
    (sales_service_sync["total_orders"] / total_orders_sum) * 100
).round(1)
sales_service_sync["unique_customers (%)"] = (
    (sales_service_sync["unique_customers"] / total_customers_sum) * 100
).round(1)

# Маппинг названий
sales_service_sync["purchased_vehicle"] = sales_service_sync[
    "purchased_vehicle"
].map({True: "Купил авто у нас", False: "Купил в другом месте"})

# Выводим только анонимизированные колонки
print(
    sales_service_sync[
        ["purchased_vehicle", "total_orders (%)", "unique_customers (%)"]
    ]
)

purchased_vehicle

total_orders (%)

unique_customers (%)

Купил в другом месте

60.8

70.4

Купил авто у нас

39.2

29.6

Интересный факт: «чужих» клиентов в сервисе больше по количеству, но «свои» клиенты намного активнее — в среднем на одного «своего» клиента приходится 11.5 заказов за время владения машиной против 7.4 заказов у клиента со стороны.

Сравнение лояльность двух типов клиентов
Сравнение лояльность двух типов клиентов

Кроме того, процент удержания (Retention Rate) среди «своих» стабильно выше на 12–15%. Покупка машины у дилера создает мощный инерционный запас лояльности.


А теперь — главный эксперимент статьи

Давайте скрестим две метрики: факт использования шиномонтажа/хранения колес и суммарную выручку (LTV), которую клиент принес компании за все 10 лет наблюдений.

Напомним гипотезу «горячего оптимизатора»: «Копеечный шиномонтаж снижает средний чек, загружает подъемники и не приносит прибыли».

Проверим её на чистых данных:

# 1. Находим VIN-номера клиентов, которые ХОТЯ БЫ РАЗ пользовались шиномонтажом/хранением
tire_clients_vins = set(
    completed_orders_detailed[
        completed_orders_detailed["item_id"].isin(tire_services)
    ]["vin_number"]
)

# 2. Считаем суммарные траты каждого уникального клиента за все 10 лет
client_ltv = (
    completed_orders.groupby("vin_number")["sum_paid"].sum().reset_index()
)

# 3. Разделяем клиентов на две группы
client_ltv["uses_tire_service"] = client_ltv["vin_number"].isin(
    tire_clients_vins
)

# 4. Считаем средний LTV на одного клиента в каждой группе
ltv_summary = client_ltv.groupby("uses_tire_service")["sum_paid"].mean()

# ==========================================
# АНОНИМИЗАЦИЯ И РАСЧЕТ ОТНОСИТЕЛЬНЫХ МЕТРИК
# ==========================================
base_ltv = ltv_summary[False]  # База для сравнения (клиенты БЕЗ шиномонтажа)
target_ltv = ltv_summary[True]  # Целевая группа (клиенты С шиномонтажом)

# Рассчитываем кратность (во сколько раз больше)
ltv_multiplier = target_ltv / base_ltv

# Рассчитываем прирост в процентах
ltv_growth_pct = (ltv_multiplier - 1) * 100

# Вывод анонимизированных результатов в консоль
print(
    f"Относительный LTV клиента С шиномонтажом (база БЕЗ шиномонтажа = 1.0x): {ltv_multiplier:.2f}x"
)
print(
    f"Прирост среднего LTV при использовании шиномонтажа: +{ltv_growth_pct:.1f}%"
)
Относительный LTV клиента С шиномонтажом (база БЕЗ шиномонтажа = 1.0x): 3.09x
Прирост среднего LTV при использовании шиномонтажа: +209.4%

Внимание на экран!

* Базовый клиент (не пользуется шиномонтажом) за всё время работы с сервисом приносит компании фиксированную «условную единицу» дохода.

* Лояльный клиент (хотя бы раз заехал на шиномонтаж или сдал колеса на хранение) окупает базовые показатели более чем в 3 раза! В масштабах всей клиентской базы этот «скромный» разрыв превращается в колоссальную недополученную прибыль с каждого автомобиля, который уезжает менять колеса в сторонние мастерские.

# Переводим абсолютный LTV в относительный индекс (Индекс 1.0x)
base_ltv = ltv_summary[False]  # База — клиенты БЕЗ шиномонтажа
ltv_index_true = ltv_summary[True] / base_ltv
ltv_index_false = 1.0  # Базовая точка всегда 1.0

# Подготовка данных для визуализации
incomes = [ltv_index_true, ltv_index_false]
labels = ["Пользуются шиномонтажом", "Не пользуются шиномонтажом"]
Посмотреть код визуализации относительного LTV
# Корпоративная палитра: фокусный синий (IBM) и нейтральный серый для сравнения
colors = ["#0F62FE", "#8D8D8D"]

# Настройка чистого журнального стиля
sns.set_theme(style="white")
plt.rcParams["font.family"] = "sans-serif"

fig, ax = plt.subplots(figsize=(9, 5), dpi=150)  # Высокое разрешение для медиа

# Построение столбчатой диаграммы
bars = ax.bar(labels, incomes, color=colors, width=0.45)

# Тонкая настройка сетки и десеткация (удаление рамок)
ax.grid(axis="y", linestyle="--", color="#E0E0E0", linewidth=0.7)
sns.despine(left=True, bottom=False)  # Оставляем только линию под столбцами

# Оформление заголовка с выравниванием по левому краю
ax.text(
    -0.15,
    1.15,
    "Влияние дополнительных услуг на удержание клиентов",
    fontsize=16,
    fontweight="bold",
    color="#161616",
    transform=ax.transAxes,
)
ax.text(
    -0.15,
    1.08,
    "Сравнение среднего LTV за 10 лет (Относительный индекс, группа без услуг = 1.0x)",
    fontsize=11,
    color="#525252",
    transform=ax.transAxes,
)

# Очищаем подписи осей, так как вся информация есть в названии
ax.set_ylabel("")
ax.tick_params(axis="both", which="major", labelsize=11, colors="#525252")

# Форматирование оси Y в формат индексов (1.0x, 1.5x и т.д.)
ax.yaxis.set_major_formatter(FuncFormatter(lambda x, pos: f"{x:.1f}x"))

# Динамический расчет верхнего лимита, чтобы подписи не упирались в край
ax.set_ylim(0, max(incomes) * 1.2)

# Аккуратные подписи значений прямо над столбиками
for bar in bars:
    yval = bar.get_height()
    ax.text(
        bar.get_x() + bar.get_width() / 2,
        yval + (max(incomes) * 0.03),  # Смещение пропорционально высоте графика
        f"{yval:.2f}x",
        ha="center",
        va="bottom",
        fontsize=11,
        fontweight="bold",
        color="#161616",
    )

# Текстовая аннотация разрыва в LTV
# Автоматически считаем разницу в процентах для точности текста
pct_difference = int((ltv_index_true - 1.0) * 100)

# Добавляем текстовый блок с инсайтом
# Координаты (x=0.5, y=max(incomes)*0.7) помещают текст между двумя столбиками
ax.text(
    0.5,
    max(incomes) * 0.72,
    f"Клиенты, воспользовавшиеся\nшиномонтажом хотя бы раз,\nгенерируют в среднем на {pct_difference}%\nбольше выручки за свой LTV",
    fontsize=10,
    fontweight="normal",
    color="#525252",
    ha="center",
    va="center",
    bbox=dict(
        boxstyle="round,pad=0.6",
        facecolor="#F4F4F4",
        edgecolor="none",
        alpha=0.9,
    ),
)

# Рисуем аккуратную стрелку-указатель от текста к целевому столбцу
ax.annotate(
    "",
    xy=(0.0, ltv_index_true * 0.8),  # Куда указывает стрелка (на синий столбец)
    xytext=(
        0.28,
        max(incomes) * 0.72,
    ),  # Откуда идет стрелка (от границы текста)
    arrowprops=dict(
        arrowstyle="->",
        color="#0F62FE",
        lw=1.2,
        connectionstyle="arc3,rad=-0.15",  # Элегантный легкий изгиб
    ),
)

plt.tight_layout()
plt.show()
Как дополнительные услуги повышают LTV
Как дополнительные услуги повышают LTV

Анатомия парадокса: почему это работает?

Почему «низкомаржинальная» процедура по переобувке колес дает такой гигантский буст к итоговой выручке компании?

Ответ кроется не в математике, а в поведенческих сценариях (Customer Journey):

  1. Триггер регулярного контакта (Habit Loop): Весна и осень — два естественных природных дедлайна, когда автовладелец обязан поменять резину. Если колеса лежат на складе у дилера, у клиента просто нет шанса поехать в ближайший сторонний сервис («гараж дяди Васи»). Он гарантированно приезжает к вам минимум дважды в год.

  2. Точка входящего осмотра (Trojan Horse): По регламенту дилера при каждом шиномонтаже проводится бесплатный осмотр ходовой части и проверка тормозной системы (те самые массовые коды 00-8030-01 и 42-0004-01).

  3. Конверсия в сложный ремонт: В 40% случаев при снятии колес мастер обнаруживает износ колодок, потёки амортизаторов, трещины сайлентблоков или необходимость замены тормозных дисков. Клиент уже находится в дилерском центре, машина уже висит на подъемнике. Согласовать доработку «здесь и сейчас» психологически намного проще, чем заставить клиента записаться на ремонт отдельно.

Уберите шиномонтаж — и вы разрушите привычку регулярного визита. Клиент переобуется за углом, там же ему посоветуют «своего» слесаря, и на сложный ремонт с чеком в 50 000+ рублей он к вам больше не вернется.

Выводы для бизнеса, или как нас спасла аналитика

Этот кейс — классический пример того, почему аналитик данных должен сидеть за одним столом с топ-менеджментом, когда принимаются стратегические решения.

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

Мы извлекли ценные уроки из анализа сотен тысяч заказ-нарядов:

  1. Не оценивайте продукты в изоляции: В бизнесе существуют так называемые «якорные» продукты (Anchor Services / Loss Leaders). Их задача — не генерировать максимальную маржу «здесь и сейчас», а создавать регулярный точек контакта с клиентом и формировать привычку возвращаться.

  2. Опасайтесь парадокса Симпсона: Валовый средний чек — это «температура по больнице». Всегда разделяйте транзакции по категориям работ, типам клиентов и целям визита. Чистый сложный ремонт показывает отличный средний чек, который на порядок выше стоимости сезонного обслуживания, но именно шиномонтаж обеспечивает необходимую частоту заездов и удерживает клиента в экосистеме дилера.

  3. Стройте автоматические воронки допродаж (Cross-sell): Сезонная переобувка обязана сопровождаться бесплатной базовой диагностикой (тормоза, ходовая, аккумуляторы). Это не «подарок клиенту», это главный инструмент генерации высокомаржинальных заказов на следующий шаг.

  4. Считайте LTV, а не разовую транзакцию: Клиент, который заезжает к вам дважды в год на мелкие работы, в 3.1 раза ценнее клиента, который заскочил один раз на сложный ремонт и пропал навсегда.


Что дальше?

Мы разобрались с деньгами и поведением клиентов. Но что происходит внутри самого автосервиса, когда машина уже заехала на подъемник?

В следующей статье цикла мы погрузимся в операционную аналитику и управление логистикой. Мы разберем:

  • Загадку простоя: Почему чистое время работы механика над машиной за 10 лет практически не изменилось, но общий срок нахождения автомобиля у дилера вырос в два раза?

  • Как логистический кризис 2022 года сломал классическую модель снабжения запчастями.

  • Строим скрипки распределения (ViolinPlot) и ищем конкретные узкие места (Bottlenecks) в цепочке выполнения заказов.

И на последок немного интерактива!

Было бы интересно узнать, а вы сталкивались в своей практике с подобными парадоксами, когда «дешевые» услуги на поверку оказывались главными драйверами LTV?

Ну и чисто по-человечески: как сезонный шиномонтаж обычно проходит лично у вас?

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
А как вы обычно поступаете с сезонной сменой резины и обслуживанием авто?
0%Переобуваюсь у дилера/в крупном сервисе + сдаю на хранение (и обычно соглашаюсь на рекомендованный ремонт).0
28.57%Переобуваюсь в проверенной шиномонтажке «за углом», а ремонтируюсь строго в другом месте.2
71.43%Меняю колеса сам в гараже/на даче, никому не доверяю.5
14.29%Я пришел сюда просто почитать, где лучше переобувать машину.1
Проголосовали 7 пользователей. Воздержались 2 пользователя.