Детектор убитых карточек
Автоматизированный пайплайн для поиска SKU с высокой выручкой и плохим контентом. Каждая такая карточка -- потенциальный клиент Fotofactor с бюджетом и очевидной болью.
Проблема
В каждой категории на WB и Ozon есть товары, которые генерируют миллионы рублей выручки при ужасном контенте: мало фотографий, нет видео, низки й рейтинг, отсутствие инфографики. Такие продавцы -- идеальные клиенты для Fotofactor:
- У них есть деньги -- товар уже продаётся
- У них есть боль -- контент явно не работает на полную мощность
- У них есть мотивация -- lost_profit показывает, сколько они теряют
- Они готовы платить -- средний чек 80 000--150 000 рублей
Проблема в том, что найти таких продавцов вручную невозможно. В одной категории -- тысячи SKU. Нужен автоматический детектор.
Пайплайн данных
graph LR
A["MPStats API<br/>POST /wb/get/category"] --> B["Топ-500 товаров<br/>sort: revenue desc"]
B --> C{"Фильт р:<br/>revenue > 500K AND<br/>(picscount < 5 OR<br/>hasvideo == 0 OR<br/>lost_profit > 100K)"}
C -->|"Прошёл"| D["ContentProblemScore<br/>0-100 баллов"]
C -->|"Не прошёл"| E["Пропуск"]
D --> F["Топ-20 по скору"]
F --> G["Персонализированный<br/>cold-outreach"]
Шаг 1: Загрузка данных из MPStats
Запрос топ-500 товаров категории, отсортированных по выручке:
import httpx
from urllib.parse import quote
TOKEN = "YOUR_MPSTATS_TOKEN"
BASE_URL = "https://mpstats.io/api"
headers = {
"X-Mpstats-TOKEN": TOKEN,
"Content-Type": "application/json",
}
resp = httpx.post(
f"{BASE_URL}/wb/get/category",
headers=headers,
json={
"path": quote("Женская одежда", safe=""),
"d1": "2026-01-20",
"d2": "2026-02-19",
"startRow": 0,
"endRow": 500,
"sortModel": [{"colId": "revenue", "sort": "desc"}],
},
)
result = resp.json()
products = result.get("data", [])
print(f"Загружено {len(products)} товаров из {result.get('total', '?')}")
Шаг 2: Фильтрация «убитых карточек»
Карточка считается «убитой», если выполняется хотя бы одно из условий при выручке > 500 000 рублей за период:
| Критерий | Пороговое значение | Почему это проблема |
|---|---|---|
| Мало фото | picscount < 5 | ТОП-10 категории используют 10+ фото |
| Нет видео | hasvideo == 0 | Видео повышает конверсию на 20-40% |
| Высокая упущенная выручка | lost_profit > 100 000 ₽ | Прямые потери из-за out-of-stock и плохого контента |
| Мало отзывов при высоких продажах | comments < 50 AND sales > 500 | Низкое вовлечение покупателей |
Шаг 3: Ключевые поля API
Все необходимые данные приходят из одного запроса POST /wb/get/category:
| Поле | Тип | Смысл | Использование в скоринге |
|---|---|---|---|
picscount | int | Количество фото (бенчмарк ТОПа: 10+) | Чем меньше -- тем выше problem score |
hasvideo | 0/1 | Наличие видео | 0 = +20 к problem score |
lost_profit | float | Упущенная выручка (out-of-stock + плохой контент) | Чем больше -- тем выше opportunity |
revenue | float | Текущая выручка за период | Вес клиента (платёжеспособность) |
sales | int | Количество продаж | Подтверждение спроса |
comments | int | Количество отзывов | Индикатор вовлечённости |
rating | float | Рейтинг товара | Общий показатель качества |
final_price | float | Текущая цена | Контекст для outreach |
Анализ: ContentProblemScore
Каждая «убитая карточка» получает скор от 0 до 100. Чем выше -- тем больше потенциал для Fotofactor:
def content_problem_score(product: dict) -> int:
"""
Рассчитывает score проблем с контентом.
0 = всё хорошо, 100 = максимально убитая карточка.
"""
score = 0
# === Фото (0-30 баллов) ===
# Меньше фото = выше problem score
picscount = product.get("picscount", 0)
if picscount < 3:
score += 30 # Критически мало фото
elif picscount < 5:
score += 20 # Ниже минимума для конверсии
elif picscount < 8:
score += 10 # Ниже бенчмарка ТОПа (10+)
# === Видео (0-20 баллов) ===
# Нет видео = упущенная конверсия
if not product.get("hasvideo"):
score += 20
# === Упущенная выручка (0-25 баллов) ===
# Больше lost_profit = больше opportunity для нас
lost_profit = product.get("lost_profit", 0)
if lost_profit > 500_000:
score += 25 # Теряет полмиллиона+ -- огромная мотивация
elif lost_profit > 100_000:
score += 15 # Существенные потери
# === Вес клиента по выручке (0-25 баллов) ===
# Больше выручка = клиент может платить больше
revenue = product.get("revenue", 0)
if revenue > 5_000_000:
score += 25 # Крупный продавец (5M+)
elif revenue > 1_000_000:
score += 15 # Средний продавец (1-5M)
return score # 0-100
def find_dead_cards(products: list, min_revenue: float = 500_000) -> list:
"""
Находит убитые карточки и сортирует по problem score.
"""
dead_cards = []
for product in products:
revenue = product.get("revenue", 0)
if revenue < min_revenue:
continue
# Проверяем критерии «убитости»
picscount = product.get("picscount", 0)
hasvideo = product.get("hasvideo", 0)
lost_profit = product.get("lost_profit", 0)
is_dead = (
picscount < 5
or hasvideo == 0
or lost_profit > 100_000
)
if is_dead:
score = content_problem_score(product)
dead_cards.append({
"sku": product.get("id"),
"name": product.get("name", "")[:80],
"revenue": revenue,
"picscount": picscount,
"hasvideo": hasvideo,
"lost_profit": lost_profit,
"rating": product.get("rating", 0),
"comments": product.get("comments", 0),
"final_price": product.get("final_price", 0),
"problem_score": score,
})
# Сортируем: самые «убитые» с максимальной выручкой -- наверху
dead_cards.sort(key=lambda x: x["problem_score"], reverse=True)
return dead_cards[:20] # Топ-20 для outreach
Интерпретация скора
| Problem Score | Уровень | Описание | Приоритет outreach |
|---|---|---|---|
| 80-100 | Критический | Мало фото, нет видео, огромная выручка, большие потери | Звонить немедленно |
| 60-79 | Высокий | Несколько проблем с контентом, хорошая выручка | Outreach в течение дня |
| 40-59 | Средний | Одна явная проблема, средняя выручка | Outreach в течение недели |
| 20-39 | Низкий | Минорные проблемы или небольшая выручка | Добавить в nurture-цепочку |
Действие Fotofactor: персонализированный outreach
Каждое сообщение строится на конкретных данных о карточке продавца. Не «у нас классные услуги», а «вот сколько денег вы теряете».
Добрый день! Заметили ваш товар
{name}на Wildberries.При выручке
{revenue}рублей/мес у карточки всего{picscount}фото. Для сравнения: ТОП-3 в вашей категории используют 12+ фото + видеообзор.По данным аналитики, ваша упущенная выручка составляет
{lost_profit}рублей -- это то, что вы могли бы зарабатывать дополнительно при качественном визуальном контенте.Мы делаем продающий фото- и видеоконтент для маркетплейсов. Подготовили мини-аудит вашей карточки -- могу отправить?
Добрый день! Ваш товар
{name}продаётся на{revenue}рублей/мес -- отличный результат!Обратили внимание, что у карточки нет видео. По нашей статистике, добавление видеообзора увеличивает конверсию карточки на 20-40%.
При вашем текущем объёме продаж это может дать дополнительные
{revenue * 0.25}рублей/мес выручки.Мы снимаем видеообзоры для WB за 3-5 дней. Интересно обсудить?
Добрый день! Провели анализ карточки
{name}(артикул{sku}).Текущая ситуация:
- Выручка:
{revenue}рублей/мес- Фото:
{picscount}(бенчмарк категории: 10+)- Видео:
{'есть' if hasvideo else 'нет'}(бенчмарк: обязательно)- Упущенная выручка:
{lost_profit}рублейПо нашей оценке, обновление контента может вернуть
{lost_profit}рублей/мес упущенной выручки. Подготовили детальный аудит -- отправить?
Генерация outreach-сообщений
def generate_outreach(dead_card: dict) -> str:
"""Генерирует персонализированное outreach-сообщение."""
score = dead_card["problem_score"]
picscount = dead_card["picscount"]
hasvideo = dead_card["hasvideo"]
if score >= 70:
# Комплексная проблема -- полный аудит
template = "complex"
elif picscount < 5:
template = "few_photos"
elif not hasvideo:
template = "no_video"
else:
template = "lost_profit"
# В реальном пайплайне -- подставляем данные в шаблон
return f"[{template}] SKU {dead_card['sku']}: " \
f"score={score}, revenue={dead_card['revenue']:,.0f}₽, " \
f"photos={picscount}, video={'yes' if hasvideo else 'no'}, " \
f"lost_profit={dead_card['lost_profit']:,.0f}₽"
# Пример использования полного пайплайна
dead_cards = find_dead_cards(products, min_revenue=500_000)
print(f"\nНайдено {len(dead_cards)} убитых карточек:\n")
for i, card in enumerate(dead_cards, 1):
msg = generate_outreach(card)
print(f" {i}. {msg}")