Перейти к основному содержимому

Карта упущенной прибыли -- квартальный стратегический отчёт

Как собрать все источники упущенной прибыли в одну карту, рассчитать суммарные потери клиента за квартал и сформировать приоритизированный план действий на 90 дней. Это кульминация всех 20 кейсов -- мастер-отчёт, который превращает Fotofactor из «фотографов для маркетплейсов» в стратегического партнёра по управлению выручкой.

Проблема

Селлер видит свою выручку. Он знает, сколько заработал. Но он не видит, сколько потерял. Упущенная прибыль -- невидимый враг:

  • Стоки: товар закончился на складе в пик спроса -- клиент даже не узнал, сколько заказов пропустил
  • Контент: карточки с плохими фото конвертируют на 30-50% хуже, чем могли бы -- но селлер не видит «потенциальную конверсию»
  • SEO: отсутствие 20 ключевых слов в заголовке = 15 000 потерянных показов в месяц -- но нет метрики «упущенные показы»
  • Цена: товар в неправильном ценовом сегменте теряет 20-40% потенциального трафика -- но селлер видит только текущие продажи
  • Сезонность: пропущенный пик = 3-6 месяцев ожидания следующего -- но без данных это не очевидно
  • Конкуренты: пока селлер не обновлял контент, 5 конкурентов обновились и забрали его долю -- но он думает, что «рынок упал»

Каждый из этих источников потерь -- тема отдельного кейса (1-19). Но суммарные потери никто никогда не считает. А когда считаешь -- цифры шокируют: типичный селлер с выручкой 5 000 000 руб/квартал теряет ещё 2 000 000-4 000 000 руб, которые мог бы зарабатывать.

Методология расчёта упущенной прибыли

Мы разделяем упущенную прибыль на 5 категорий, каждая из которых рассчитывается независимо:

  1. Контентные потери -- разрыв между текущей конверсией карточки и бенчмарком категории (кейсы 6, 8, 15)
  2. SEO-потери -- пропущенные ключевые слова x потенциальный трафик x конверсия (кейсы 7, 12)
  3. Ценовые потери -- товар в неоптимальном ценовом сегменте (кейс 5)
  4. Сезонные потери -- пропущенные сезонные пики (кейс 14)
  5. Конкурентные потери -- доля рынка, потерянная из-за обновления контента конкурентами (кейсы 13, 19)

Важно: категории могут пересекаться (плохой контент усиливает SEO-потери). Мы не складываем пересечения, а берём максимальный из перекрывающихся эффектов для честного расчёта.

Пайплайн данных

Архитектура мастер-анализа

Этот кейс -- агрегатор, который использует данные из всех предыдущих 19 кейсов. Каждый эндпоинт MPStats подаёт данные в одну или несколько категорий потерь:

flowchart TD
A["POST /wb/get/category\nрынок, бенчмарки, lost_profit"] --> AGG["АГРЕГАТОР\nКарта упущенной прибыли"]
B["GET /category/trends\nсезонность, динамика"] --> AGG
C["GET /item/{sku}/sales\nтренды продаж клиента"] --> AGG
D["GET /item/{sku}/by_keywords\nSEO-покрытие, пробелы"] --> AGG
E["GET /item/{sku}/by_category\nпозиции в категории"] --> AGG
AGG --> R1["Контентные\nпотери"]
AGG --> R2["SEO-\nпотери"]
AGG --> R3["Ценовые\nпотери"]
AGG --> R4["Сезонные\nпотери"]
AGG --> R5["Конкурентные\nпотери"]
R1 --> MAP["КАРТА\nУПУЩЕННОЙ ПРИБЫЛИ\n= сумма 5 категорий"]
R2 --> MAP
R3 --> MAP
R4 --> MAP
R5 --> MAP
MAP --> PLAN["ПЛАН ДЕЙСТВИЙ\n90 дней\nс ROI по каждому пункту"]

Источники данных

ЭндпоинтМетодКлючевые поляЧто показывает
POST /wb/get/categoryPOSTrevenue, sales, lost_profit, picscount, hasvideo, category_positionРыночный контекст, контентные бенчмарки, прямая метрика lost_profit
GET /wb/get/category/trendsGETПомесячные значения продаж категорииСезонные пики, тренды роста/падения
GET /wb/get/item/{sku}/salesGETsales, revenue, price по днямДинамика продаж клиента — падение, стагнация, рост
GET /wb/get/item/{sku}/by_keywordsGETkeyword, position, frequencySEO-покрытие: какие запросы работают, какие пропущены
GET /wb/get/item/{sku}/by_categoryGETposition, categoryПозиционирование в категории относительно конкурентов

Пайплайн шаг за шагом

  1. Сбор портфеля -- загрузить все SKU клиента через POST /wb/get/category (кейс 16)
  2. Контент-аудит -- рассчитать content gap для каждого SKU vs бенчмарк категории (кейсы 6, 15)
  3. SEO-аудит -- загрузить ключевые слова ТОП-SKU, найти пропущенные (кейс 7)
  4. Ценовой анализ -- определить оптимальный ценовой сегмент для каждого SKU (кейс 5)
  5. Сезонный анализ -- наложить тренды категории на план обновлений (кейс 14)
  6. Конкурентный анализ -- сравнить контент клиента с ТОП-10 конкурентов (кейсы 13, 19)
  7. Агрегация -- рассчитать суммарные потери по 5 категориям
  8. Формирование плана -- приоритизировать действия по ROI, назначить дедлайны

Анализ

Шаг 1. Сбор данных по всем категориям потерь

import httpx
from dataclasses import dataclass, field
from datetime import datetime, timedelta

MPSTATS_TOKEN = "YOUR_TOKEN"
BASE_URL = "https://mpstats.io/api"
HEADERS = {
"X-Mpstats-TOKEN": MPSTATS_TOKEN,
"Content-Type": "application/json",
}


@dataclass
class LostProfitCategory:
"""Одна категория упущенной прибыли."""
name: str
amount_quarterly: float # руб/квартал
pct_of_revenue: float # % от текущей выручки
priority: str # high / medium / low
source_cases: list[int] = field(default_factory=list)
actions: list[str] = field(default_factory=list)
estimated_recovery: float = 0.0 # сколько можно вернуть
investment_needed: float = 0.0 # сколько стоит исправить


@dataclass
class LostProfitMap:
"""Полная карта упущенной прибыли клиента."""
seller_name: str
quarter: str
current_revenue: float
categories: list[LostProfitCategory] = field(default_factory=list)

@property
def total_lost(self) -> float:
return sum(c.amount_quarterly for c in self.categories)

@property
def total_pct(self) -> float:
if self.current_revenue == 0:
return 0
return self.total_lost / self.current_revenue * 100

@property
def total_recoverable(self) -> float:
return sum(c.estimated_recovery for c in self.categories)

@property
def total_investment(self) -> float:
return sum(c.investment_needed for c in self.categories)

@property
def roi_multiplier(self) -> float:
if self.total_investment == 0:
return 0
return self.total_recoverable / self.total_investment

Шаг 2. Расчёт контентных потерь

Контентные потери -- самая крупная категория для контент-агентства. Рассчитываются как разрыв между текущей конверсией карточки и бенчмарком ТОП-10 в категории:

def calculate_content_losses(
client_skus: list[dict],
category_benchmarks: dict,
) -> LostProfitCategory:
"""
Контентные потери = сумма по каждому SKU:
(benchmark_conversion - sku_conversion) * sku_traffic * avg_order_value

Бенчмарк берётся из ТОП-10 категории (медиана конверсии).
"""
total_loss = 0.0

for sku in client_skus:
revenue = sku.get("revenue", 0)
sales = sku.get("sales", 0)
pics = sku.get("picscount", 0)
has_video = sku.get("hasvideo", False)
lost_profit_mpstats = sku.get("lost_profit", 0)

bench = category_benchmarks.get(sku.get("category", ""), {})
bench_pics = bench.get("median_pics", 10)
bench_video_pct = bench.get("video_pct", 0.6)

# Контент-скор: 0..1 (1 = на уровне бенчмарка)
pic_score = min(pics / bench_pics, 1.0) if bench_pics > 0 else 1.0
video_score = 1.0 if has_video else (1.0 - bench_video_pct * 0.3)
content_score = pic_score * 0.6 + video_score * 0.4

# Потенциальный прирост при доведении контента до бенчмарка
potential_uplift = (1.0 - content_score) * 0.5 # 50% gap = max uplift
sku_loss = revenue * potential_uplift * 3 # за квартал

# Если MPStats даёт lost_profit -- используем его как верхнюю границу
if lost_profit_mpstats > 0:
sku_loss = min(sku_loss, lost_profit_mpstats * 3)

total_loss += sku_loss

return LostProfitCategory(
name="Контентные потери",
amount_quarterly=round(total_loss),
pct_of_revenue=0, # заполним позже
priority="high" if total_loss > 500_000 else "medium",
source_cases=[6, 8, 15],
actions=[
"Пересъёмка ТОП-10 карточек по content gap",
"Добавление видео на карточки без видео",
"Обновление инфографики под текущие тренды",
],
estimated_recovery=round(total_loss * 0.45),
investment_needed=round(total_loss * 0.08),
)

Шаг 3. Расчёт SEO-потерь

def calculate_seo_losses(
client_skus: list[dict],
keyword_data: dict[int, list[dict]],
avg_conversion: float = 0.03,
avg_order_value: float = 2500,
) -> LostProfitCategory:
"""
SEO-потери = сумма пропущенных ключевых слов:
sum(keyword_frequency * (1 - position_factor) * conversion * AOV)

Пропущенное слово = слово из ТОП-50 категории,
по которому SKU клиента НЕ находится или стоит ниже 50-й позиции.
"""
total_loss = 0.0

for sku_data in client_skus:
sku_id = sku_data.get("id", 0)
keywords = keyword_data.get(sku_id, [])

# Ключи, по которым SKU вне ТОП-50
covered_kw = {
kw["keyword"]
for kw in keywords
if kw.get("position", 999) <= 50
}

# Потенциальные ключи из бенчмарка категории
bench_keywords = keyword_data.get("benchmark", [])
missed = [
kw for kw in bench_keywords
if kw["keyword"] not in covered_kw
and kw.get("frequency", 0) > 100
]

for kw in missed:
freq = kw.get("frequency", 0)
# Оценка кликов: ~3% CTR для позиции 10-20
estimated_clicks = freq * 0.03
estimated_revenue = estimated_clicks * avg_conversion * avg_order_value
total_loss += estimated_revenue * 3 # за квартал

return LostProfitCategory(
name="SEO-потери",
amount_quarterly=round(total_loss),
pct_of_revenue=0,
priority="high" if total_loss > 300_000 else "medium",
source_cases=[7, 12],
actions=[
"SEO-оптимизация заголовков ТОП-20 карточек",
"Обогащение описаний пропущенными ключевыми словами",
"A/B тест заголовков с высокочастотными запросами",
],
estimated_recovery=round(total_loss * 0.35),
investment_needed=round(total_loss * 0.05),
)

Шаг 4. Расчёт ценовых, сезонных и конкурентных потерь

def calculate_pricing_losses(
client_skus: list[dict],
price_segment_data: dict,
) -> LostProfitCategory:
"""
Ценовые потери = SKU в неоптимальном ценовом сегменте.
Если SKU стоит в сегменте с низкой конверсией,
а переход в соседний сегмент даёт +20-40% трафика.
"""
total_loss = 0.0

for sku in client_skus:
price = sku.get("price", 0)
revenue = sku.get("revenue", 0)
category = sku.get("category", "")

segments = price_segment_data.get(category, [])
current_segment = None
optimal_segment = None

for seg in segments:
if seg["min"] <= price <= seg["max"]:
current_segment = seg
if seg.get("is_optimal"):
optimal_segment = seg

if current_segment and optimal_segment:
if current_segment != optimal_segment:
traffic_gap = (
optimal_segment.get("avg_traffic", 0)
- current_segment.get("avg_traffic", 0)
)
if traffic_gap > 0 and current_segment.get("avg_traffic", 1) > 0:
uplift_pct = traffic_gap / current_segment["avg_traffic"]
total_loss += revenue * min(uplift_pct, 0.4) * 3

return LostProfitCategory(
name="Ценовые потери",
amount_quarterly=round(total_loss),
pct_of_revenue=0,
priority="medium",
source_cases=[5],
actions=[
"Пересмотр ценового позиционирования по сегментам",
"Тестирование цены в оптимальном сегменте",
],
estimated_recovery=round(total_loss * 0.30),
investment_needed=round(total_loss * 0.02),
)


def calculate_seasonal_losses(
client_skus: list[dict],
category_trends: dict[str, list[float]],
) -> LostProfitCategory:
"""
Сезонные потери = пропущенные пики спроса.
Если клиент не обновлял контент перед сезонным пиком,
он получает меньше трафика в самый горячий период.
"""
total_loss = 0.0

for sku in client_skus:
category = sku.get("category", "")
revenue = sku.get("revenue", 0)
trends = category_trends.get(category, [])

if len(trends) < 12:
continue

# Определяем сезонный пик (максимум за год)
recent_12 = trends[-12:]
avg_month = sum(recent_12) / 12
peak_month_val = max(recent_12)

if avg_month == 0:
continue

# Если пик > 1.5x от среднего -- значительная сезонность
peak_ratio = peak_month_val / avg_month
if peak_ratio > 1.5:
# Потенциальные потери = (пик - среднее) * доля упущенного
seasonal_uplift = (peak_ratio - 1.0) * 0.3 # 30% от пика недополучено
total_loss += revenue * seasonal_uplift

return LostProfitCategory(
name="Сезонные потери",
amount_quarterly=round(total_loss),
pct_of_revenue=0,
priority="medium",
source_cases=[14],
actions=[
"Составить сезонный контент-календарь на год",
"Подготовить сезонные креативы за 4-6 недель до пика",
"Запланировать пересъёмку перед ключевым сезоном",
],
estimated_recovery=round(total_loss * 0.50),
investment_needed=round(total_loss * 0.10),
)


def calculate_competitive_losses(
client_skus: list[dict],
competitor_updates: list[dict],
) -> LostProfitCategory:
"""
Конкурентные потери = доля рынка, потерянная из-за
обновления контента конкурентами за последний квартал.
"""
total_loss = 0.0

for sku in client_skus:
revenue = sku.get("revenue", 0)
category = sku.get("category", "")

# Сколько конкурентов обновило контент за квартал
updates_in_category = [
u for u in competitor_updates
if u.get("category") == category
]

if not updates_in_category:
continue

# Каждый обновившийся конкурент забирает ~2-5% доли
competitors_updated = len(updates_in_category)
market_share_lost = min(competitors_updated * 0.03, 0.25)
total_loss += revenue * market_share_lost * 3

return LostProfitCategory(
name="Конкурентные потери",
amount_quarterly=round(total_loss),
pct_of_revenue=0,
priority="high" if total_loss > 300_000 else "medium",
source_cases=[13, 19],
actions=[
"Мониторинг изменений у ТОП-5 конкурентов",
"Reverse-engineering успешных обновлений",
"Контратака: обновить контент лучше, чем конкурент",
],
estimated_recovery=round(total_loss * 0.40),
investment_needed=round(total_loss * 0.12),
)

Шаг 5. Агрегация -- сборка карты упущенной прибыли

def build_lost_profit_map(
seller_name: str,
quarter: str,
current_revenue: float,
client_skus: list[dict],
category_benchmarks: dict,
keyword_data: dict,
price_segment_data: dict,
category_trends: dict,
competitor_updates: list[dict],
) -> LostProfitMap:
"""
Мастер-функция: собирает все 5 категорий потерь
в единую карту упущенной прибыли.
"""
# Рассчитываем каждую категорию
content = calculate_content_losses(client_skus, category_benchmarks)
seo = calculate_seo_losses(client_skus, keyword_data)
pricing = calculate_pricing_losses(client_skus, price_segment_data)
seasonal = calculate_seasonal_losses(client_skus, category_trends)
competitive = calculate_competitive_losses(client_skus, competitor_updates)

categories = [content, seo, pricing, seasonal, competitive]

# Заполняем % от выручки
for cat in categories:
if current_revenue > 0:
cat.pct_of_revenue = round(
cat.amount_quarterly / current_revenue * 100, 1
)

# Сортируем по сумме потерь (от большего к меньшему)
categories.sort(key=lambda c: c.amount_quarterly, reverse=True)

return LostProfitMap(
seller_name=seller_name,
quarter=quarter,
current_revenue=current_revenue,
categories=categories,
)

Пример квартального отчёта

Результат анализа реального клиента -- селлер женской одежды, выручка 5 000 000 руб/квартал, 47 SKU:

Категория потерьСумма/квартал% от выручкиМожно вернутьИнвестицияROIПриоритет
Контентные1 200 000 руб24%540 000 руб96 000 руб5.6x🔴
SEO800 000 руб16%280 000 руб40 000 руб7.0x🔴
Конкурентные600 000 руб12%240 000 руб72 000 руб3.3x🔴
Ценовые450 000 руб9%135 000 руб9 000 руб15.0x:yellow_circle:
Сезонные350 000 руб7%175 000 руб35 000 руб5.0x:yellow_circle:
ИТОГО3 400 000 руб68%1 370 000 руб252 000 руб5.4x

Что это значит: клиент зарабатывает 5 млн руб/квартал, но теряет ещё 3.4 млн руб. Инвестировав 252 000 руб в контент и оптимизацию, он может вернуть 1 370 000 руб -- ROI 5.4x.

Действие Fotofactor

Формат квартальной стратегической сессии

Продолжительность: 2-3 часа (онлайн или очно)

Участники: собственник / директор по e-commerce + менеджер Fotofactor + аналитик

Структура сессии:

  1. Обзор квартала (30 мин)

    • Динамика выручки и продаж vs предыдущий квартал
    • Позиции в категории: вверх или вниз?
    • Что изменилось на рынке (новые конкуренты, тренды)
  2. Карта упущенной прибыли (45 мин)

    • Презентация 5 категорий потерь с визуализацией
    • Детальный разбор ТОП-5 SKU с наибольшими потерями
    • Сравнение с бенчмарками категории и конкурентами
  3. 90-дневный план действий (45 мин)

    • Приоритизированный список действий по ROI
    • Дедлайны и ответственные
    • Бюджет по каждому пункту
    • Ожидаемый результат с метриками
  4. Ревью прошлого квартала (30 мин, для повторных клиентов)

    • Что было сделано vs что было запланировано
    • Измеренный эффект каждого действия (кейс 11 -- До/После)
    • Корректировка стратегии

Deliverable: PDF-отчёт 40-50 страниц с визуализациями, таблицами и планом действий.

90-дневный план действий

На основе карты упущенной прибыли формируется конкретный план с дедлайнами:

@dataclass
class ActionItem:
"""Один пункт 90-дневного плана."""
priority: int # 1 = самый приоритетный
action: str
deadline_days: int # дней от начала квартала
investment: float # руб
expected_return: float # руб/квартал
roi: float
related_skus: list[int] = field(default_factory=list)
source_case: int = 0


def generate_90_day_plan(
lost_profit_map: LostProfitMap,
) -> list[ActionItem]:
"""
Генерирует приоритизированный план действий на 90 дней.
Сортировка: сначала quick wins (высокий ROI, низкая инвестиция),
потом стратегические проекты.
"""
plan = []
priority = 1

for category in lost_profit_map.categories:
for action_text in category.actions:
# Распределяем инвестиции и возврат равномерно по действиям
n_actions = len(category.actions)
action_invest = category.investment_needed / max(n_actions, 1)
action_return = category.estimated_recovery / max(n_actions, 1)

plan.append(ActionItem(
priority=priority,
action=action_text,
deadline_days=priority * 10, # каждые 10 дней
investment=round(action_invest),
expected_return=round(action_return),
roi=round(action_return / action_invest, 1) if action_invest > 0 else 0,
source_case=category.source_cases[0] if category.source_cases else 0,
))
priority += 1

# Пересортируем по ROI (quick wins сначала)
plan.sort(key=lambda a: a.roi, reverse=True)

# Переназначаем приоритеты
for i, item in enumerate(plan):
item.priority = i + 1
item.deadline_days = min((i + 1) * 7, 84) # в пределах 12 недель

return plan

Пример плана для нашего клиента:

ПриоритетДействиеДедлайнИнвестицияОжидаемый возвратROIКейс
1Пересмотр ценового позиционированияНеделя 19 000 руб135 000 руб15.0x5
2SEO-оптимизация заголовков ТОП-20Неделя 213 000 руб93 000 руб7.2x7
3A/B тест заголовков с ВЧ-запросамиНеделя 313 000 руб93 000 руб7.2x7
4Обогащение описаний ключевыми словамиНеделя 414 000 руб94 000 руб6.7x12
5Пересъёмка ТОП-10 карточекНеделя 532 000 руб180 000 руб5.6x6
6Добавление видео на карточки без видеоНеделя 632 000 руб180 000 руб5.6x15
7Сезонные креативы за 4-6 недель до пикаНеделя 712 000 руб58 000 руб4.8x14
8Сезонный контент-календарь на годНеделя 812 000 руб59 000 руб4.9x14
9Обновление инфографики под трендыНеделя 932 000 руб180 000 руб5.6x8
10Reverse-engineering обновлений конкурентовНеделя 1024 000 руб80 000 руб3.3x19
11Контратака: обновить контент лучшеНеделя 1124 000 руб80 000 руб3.3x13
12Мониторинг изменений ТОП-5 конкурентовНеделя 1224 000 руб80 000 руб3.3x19
ИТОГО12 недель252 000 руб1 370 000 руб5.4x

Бюджетное обоснование

Ключевой слайд для клиента -- одна цифра, которая продаёт всё:

«Вы теряете 3 400 000 руб в квартал. Инвестировав 252 000 руб в контент и оптимизацию, вы вернёте 1 370 000 руб. Это ROI 5.4x -- каждый вложенный рубль приносит 5 рублей 40 копеек.»

Ревью предыдущего квартала

Для повторных клиентов (а это цель -- сделать квартальный отчёт подпиской) показываем, что было сделано и какой эффект дало:

def generate_quarter_review(
planned_actions: list[ActionItem],
actual_results: list[dict],
) -> dict:
"""
Сравнивает план прошлого квартала с фактическими результатами.
Показывает: что сделали, что не сделали, какой эффект получили.
"""
completed = [a for a in actual_results if a.get("status") == "done"]
skipped = [a for a in actual_results if a.get("status") == "skipped"]

total_invested = sum(a.get("actual_cost", 0) for a in completed)
total_return = sum(a.get("measured_return", 0) for a in completed)

return {
"planned_actions": len(planned_actions),
"completed": len(completed),
"skipped": len(skipped),
"completion_rate": f"{len(completed) / max(len(planned_actions), 1) * 100:.0f}%",
"total_invested": total_invested,
"total_return_measured": total_return,
"actual_roi": round(total_return / total_invested, 1) if total_invested > 0 else 0,
"biggest_win": max(completed, key=lambda a: a.get("measured_return", 0), default=None),
"missed_opportunity": sum(
a.get("expected_return", 0) for a in skipped
),
}

Как все 20 кейсов соединяются в жизненном цикле клиента

Карта упущенной прибыли -- это не изолированный отчёт. Это точка сборки всех 20 кейсов в единую систему управления контентом клиента:

flowchart TD
subgraph ACQUIRE["ПРИВЛЕЧЕНИЕ (кейсы 1-5)"]
K1["1. Рентген категории"]
K2["2. Убитые карточки"]
K3["3. Охотник за новичками"]
K4["4. Ниша-скаут"]
K5["5. Ценовое окно"]
end

subgraph OPTIMIZE["ОПТИМИЗАЦИЯ (кейсы 6-10)"]
K6["6. Фото-формула ТОПа"]
K7["7. Ключ к поиску"]
K8["8. Голос покупателя"]
K9["9. Региональный прицел"]
K10["10. Размерная матрица"]
end

subgraph PROVE["ДОКАЗАТЕЛЬСТВО ROI (кейсы 11-13)"]
K11["11. До/После"]
K12["12. Позиционный радар"]
K13["13. Конкурентный рентген"]
end

subgraph RETAIN["УДЕРЖАНИЕ (кейсы 14-19)"]
K14["14. Сезонный календарь"]
K15["15. Контент-усталость"]
K16["16. Мультикарточный портфель"]
K17["17. Промо-готовность"]
K18["18. Похожие товары"]
K19["19. История конкурента"]
end

subgraph STRATEGIC["СТРАТЕГИЧЕСКИЙ ПАРТНЁР"]
K20["20. КАРТА\nУПУЩЕННОЙ ПРИБЫЛИ\n(квартальный отчёт)"]
end

ACQUIRE -->|"Клиент пришёл"| OPTIMIZE
OPTIMIZE -->|"Контент обновлён"| PROVE
PROVE -->|"ROI доказан"| RETAIN
RETAIN -->|"Данные за квартал"| K20
K20 -->|"Новый цикл"| OPTIMIZE

K1 --> K20
K5 --> K20
K6 --> K20
K7 --> K20
K13 --> K20
K14 --> K20
K15 --> K20
K16 --> K20
K19 --> K20

Цикл: Квартальный отчёт генерирует задания для кейсов 6-10 (оптимизация), которые затем измеряются через кейсы 11-13 (доказательство ROI), результаты кейсов 14-19 (удержание) подпитывают следующий квартальный отчёт. Замкнутый круг, в котором клиент никогда не уходит.

Результат для клиента

Полная видимость упущенных возможностей

Что клиент видит без отчётаЧто клиент видит с отчётом
«Выручка 5 млн, вроде нормально»«Выручка 5 млн, но теряю ещё 3.4 млн»
«Надо бы обновить карточки... когда-нибудь»«ТОП-3 карточки теряют 800К/квартал -- обновить сейчас»
«Фотограф стоит 80К, дорого»«Вложить 252К → вернуть 1 370К, ROI 5.4x»
«Конкуренты что-то делают»«5 конкурентов обновили контент, забрали 600К моей выручки»
«В следующем квартале посмотрим»«Через 6 недель сезонный пик -- готовим контент сейчас»

Экономика для Fotofactor

УслугаСтоимостьПериодичностьLTV/год
Квартальный стратегический отчёт50 000 - 80 000 рубРаз в квартал200 000 - 320 000 руб
Реализация плана (контент-проекты)200 000 - 500 000 рубКаждый квартал800 000 - 2 000 000 руб
Ежемесячный мониторинг (абонемент)20 000 - 30 000 рубЕжемесячно240 000 - 360 000 руб
ИТОГО на клиента1 240 000 - 2 680 000 руб/год

Позиционирование: от фотографа к стратегическому партнёру

Квартальный отчёт -- это вершина иерархии услуг Fotofactor:

УровеньУслугаЧекОтношения с клиентом
1Разовая съёмка30 000 - 80 000 рубПодрядчик
2Пакетное обновление SKU80 000 - 200 000 рубИсполнитель
3Ежемесячный мониторинг + контент100 000 - 150 000 руб/месКонтент-партнёр
4Квартальный стратегический отчёт + реализация250 000 - 580 000 руб/квСтратегический партнёр

На уровне 4 клиент не обсуждает стоимость фотосессии -- он обсуждает возврат инвестиций и стратегию роста. Это принципиально другой уровень взаимоотношений и принципиально другие деньги.

Годовой контракт

Идеальная модель -- годовой контракт с 4 квартальными сессиями:

Годовой контракт «Стратегический партнёр»
├── Q1: Стратегическая сессия + карта потерь + план → 500 000 руб
├── Q2: Ревью Q1 + новая карта + план → 500 000 руб
├── Q3: Ревью Q2 + новая карта + план → 500 000 руб
├── Q4: Ревью Q3 + годовой итог + стратегия на год → 600 000 руб
├── Ежемесячный мониторинг x12 → 300 000 руб
└── ИТОГО: 2 400 000 руб/год (200 000 руб/мес)

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

Для Fotofactor: предсказуемая выручка, глубокая экспертиза в нише клиента, максимальный LTV.

Аргумент, который закрывает сделку

«За прошлый квартал вы потеряли 3.4 миллиона рублей упущенной выручки. Из них 1.4 миллиона можно вернуть, инвестировав 252 тысячи в контент и оптимизацию. Вот план действий по неделям, с ROI по каждому пункту. Каждый день промедления стоит 15 тысяч рублей. Когда начинаем?»

Полный скрипт

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

import httpx
import json
from datetime import datetime, timedelta
from dataclasses import dataclass, field, asdict
from urllib.parse import quote

TOKEN = "YOUR_MPSTATS_TOKEN"
BASE_URL = "https://mpstats.io/api"
HEADERS = {
"X-Mpstats-TOKEN": TOKEN,
"Content-Type": "application/json",
}

# --- Конфигурация клиента ---
SELLER_NAME = "FashionStyle (женская одежда)"
CATEGORY_PATH = "Одежда/Женская одежда/Платья"
QUARTER = "Q1 2026"


def fetch_category_products(category_path: str, supplier_id: str = "") -> list[dict]:
"""Загрузить все товары категории (с фильтром по продавцу)."""
d2 = datetime.now().strftime("%Y-%m-%d")
d1 = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d")

body = {
"startRow": 0,
"endRow": 5000,
"filterModel": {},
"sortModel": [{"colId": "revenue", "sort": "desc"}],
}

if supplier_id:
body["filterModel"]["supplier_id"] = {
"filterType": "text",
"type": "equals",
"filter": supplier_id,
}

resp = httpx.post(
f"{BASE_URL}/wb/get/category",
headers=HEADERS,
params={"path": quote(category_path), "d1": d1, "d2": d2},
json=body,
timeout=60,
)
resp.raise_for_status()
data = resp.json()
return data.get("data", [])


def fetch_category_trends(category_path: str) -> list[float]:
"""Загрузить тренды категории (72 значения, помесячно)."""
resp = httpx.get(
f"{BASE_URL}/wb/get/category/trends",
headers=HEADERS,
params={"path": quote(category_path)},
timeout=30,
)
resp.raise_for_status()
return resp.json()


def fetch_sku_keywords(sku: int) -> list[dict]:
"""Загрузить ключевые слова для SKU."""
resp = httpx.get(
f"{BASE_URL}/wb/get/item/{sku}/by_keywords",
headers=HEADERS,
timeout=30,
)
return resp.json() if resp.status_code == 200 else []


def fetch_sku_sales(sku: int, days: int = 90) -> list[dict]:
"""Загрузить продажи SKU за N дней."""
d1 = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
d2 = datetime.now().strftime("%Y-%m-%d")

resp = httpx.get(
f"{BASE_URL}/wb/get/item/{sku}/sales",
headers=HEADERS,
params={"d1": d1, "d2": d2},
timeout=30,
)
resp.raise_for_status()
return resp.json()


def calculate_content_gap(sku: dict, benchmarks: dict) -> float:
"""Рассчитать контентный разрыв: 0 = идеально, 1 = полное отсутствие."""
pics = sku.get("picscount", 0)
has_video = sku.get("hasvideo", False)
bench_pics = benchmarks.get("median_pics", 10)
bench_video_pct = benchmarks.get("video_pct", 0.6)

pic_gap = max(1.0 - pics / bench_pics, 0) if bench_pics > 0 else 0
video_gap = bench_video_pct * 0.3 if not has_video else 0

return pic_gap * 0.6 + video_gap * 0.4


def build_lost_profit_summary(
client_products: list[dict],
all_category_products: list[dict],
category_trends: list[float],
keyword_data: dict[int, list[dict]],
) -> dict:
"""
Полный расчёт карты упущенной прибыли.
Возвращает словарь с 5 категориями потерь и планом действий.
"""
# Бенчмарки категории (медианы ТОП-50)
top_50 = sorted(all_category_products, key=lambda x: x.get("revenue", 0), reverse=True)[:50]
benchmarks = {
"median_pics": sorted([p.get("picscount", 0) for p in top_50])[25] if top_50 else 10,
"video_pct": sum(1 for p in top_50 if p.get("hasvideo")) / max(len(top_50), 1),
"median_revenue": sorted([p.get("revenue", 0) for p in top_50])[25] if top_50 else 0,
}

current_revenue = sum(p.get("revenue", 0) for p in client_products) * 3

# 1. Контентные потери
content_loss = 0
for p in client_products:
gap = calculate_content_gap(p, benchmarks)
lost = p.get("lost_profit", 0)
content_loss += max(
p.get("revenue", 0) * gap * 0.5 * 3,
lost * 3 * 0.3,
)

# 2. SEO-потери
seo_loss = 0
for p in client_products:
sku_id = p.get("id", 0)
keywords = keyword_data.get(sku_id, [])
covered = len([kw for kw in keywords if kw.get("position", 999) <= 50])
potential = len(keyword_data.get("benchmark", []))
if potential > covered:
missed_traffic = (potential - covered) * 50 * 0.03 * 2500
seo_loss += missed_traffic * 3

# 3. Ценовые потери (упрощённо)
pricing_loss = 0
for p in client_products:
lost = p.get("lost_profit", 0)
pricing_loss += lost * 0.2 * 3

# 4. Сезонные потери
seasonal_loss = 0
if len(category_trends) >= 12:
recent = category_trends[-12:]
avg_m = sum(recent) / 12
peak = max(recent)
if avg_m > 0 and peak / avg_m > 1.5:
seasonal_loss = current_revenue * (peak / avg_m - 1) * 0.1

# 5. Конкурентные потери (оценка по обновлениям в ТОП-50)
competitive_loss = current_revenue * 0.12

categories = [
{
"name": "Контентные потери",
"amount": round(content_loss),
"pct": round(content_loss / max(current_revenue, 1) * 100, 1),
"priority": "high",
"recoverable_pct": 45,
},
{
"name": "SEO-потери",
"amount": round(seo_loss),
"pct": round(seo_loss / max(current_revenue, 1) * 100, 1),
"priority": "high",
"recoverable_pct": 35,
},
{
"name": "Конкурентные потери",
"amount": round(competitive_loss),
"pct": round(competitive_loss / max(current_revenue, 1) * 100, 1),
"priority": "high",
"recoverable_pct": 40,
},
{
"name": "Ценовые потери",
"amount": round(pricing_loss),
"pct": round(pricing_loss / max(current_revenue, 1) * 100, 1),
"priority": "medium",
"recoverable_pct": 30,
},
{
"name": "Сезонные потери",
"amount": round(seasonal_loss),
"pct": round(seasonal_loss / max(current_revenue, 1) * 100, 1),
"priority": "medium",
"recoverable_pct": 50,
},
]

total_lost = sum(c["amount"] for c in categories)
total_recoverable = sum(
c["amount"] * c["recoverable_pct"] / 100
for c in categories
)
total_investment = round(total_recoverable * 0.18)

return {
"seller": SELLER_NAME,
"quarter": QUARTER,
"current_revenue": current_revenue,
"total_lost_profit": total_lost,
"total_lost_pct": round(total_lost / max(current_revenue, 1) * 100, 1),
"total_recoverable": round(total_recoverable),
"total_investment": total_investment,
"roi": round(total_recoverable / max(total_investment, 1), 1),
"categories": categories,
}


# --- Основной пайплайн ---
if __name__ == "__main__":
print(f"Карта упущенной прибыли: {SELLER_NAME}")
print(f"Квартал: {QUARTER}")
print("=" * 60)

# 1. Загружаем все товары категории
all_products = fetch_category_products(CATEGORY_PATH)
print(f"Товаров в категории: {len(all_products)}")

# 2. Фильтруем клиентские SKU (пример: по supplier_id)
client_products = all_products[:47] # заменить на реальный фильтр
print(f"SKU клиента: {len(client_products)}")

# 3. Тренды категории
trends = fetch_category_trends(CATEGORY_PATH)
print(f"Точек тренда: {len(trends)}")

# 4. Ключевые слова ТОП-10 SKU
keyword_data = {}
top_skus = sorted(client_products, key=lambda x: x.get("revenue", 0), reverse=True)[:10]
for p in top_skus:
sku_id = p.get("id", 0)
keyword_data[sku_id] = fetch_sku_keywords(sku_id)
print(f" SKU {sku_id}: {len(keyword_data[sku_id])} keywords")

# 5. Генерация карты
result = build_lost_profit_summary(
client_products=client_products,
all_category_products=all_products,
category_trends=trends,
keyword_data=keyword_data,
)

# 6. Вывод
print("\n" + "=" * 60)
print(f"КАРТА УПУЩЕННОЙ ПРИБЫЛИ: {result['seller']}")
print(f"Текущая выручка: {result['current_revenue']:,.0f} руб/квартал")
print(f"Упущенная прибыль: {result['total_lost_profit']:,.0f} руб ({result['total_lost_pct']}%)")
print(f"Можно вернуть: {result['total_recoverable']:,.0f} руб")
print(f"Инвестиция: {result['total_investment']:,.0f} руб")
print(f"ROI: {result['roi']}x")
print()

for cat in result["categories"]:
priority_icon = "!!!" if cat["priority"] == "high" else "!"
print(f" {priority_icon} {cat['name']}: {cat['amount']:,.0f} руб ({cat['pct']}%)")

# 7. Сохранение
filename = f"lost_profit_map_{datetime.now():%Y%m%d}.json"
with open(filename, "w", encoding="utf-8") as f:
json.dump(result, f, ensure_ascii=False, indent=2)
print(f"\nОтчёт сохранён: {filename}")

Что дальше

Карта упущенной прибыли -- это финальный кейс, который замыкает цикл. Но это не конец, а начало нового цикла: