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

До/После — трекер эффекта обновления контента

Автоматическая система доказательства ROI: измеряем влияние обновлённого контента на продажи в 5 контрольных точках и превращаем цифры в аргумент для удержания клиента.

Проблема

Самая болезненная точка контент-агентства — невозможность доказать результат. Клиент платит 80 000 рублей за фотосъёмку и через месяц спрашивает: «А оно вообще окупилось?» Без системного трекинга ответить нечего.

Последствия отсутствия измерений:

  • Клиент не видит ценности — «заплатил за фото, а продажи вроде те же». Даже если продажи выросли, без цифр клиент не связывает рост с обновлением контента
  • Отток 60% — клиенты не продлевают сотрудничество, потому что не могут обосновать расходы перед собственным руководством или партнёрами
  • Возражение на старте — новые клиенты спрашивают «а у вас есть кейсы с цифрами?», и студии нечего показать, кроме портфолио
  • Нет базы для upsell — без данных невозможно предложить обновление ещё 5 карточек и обосновать бюджет
  • Конкуренты дешевле — если нет доказательства ROI, клиент выбирает по цене, а не по результату
Почему это проблема номер один для Fotofactor

Fotofactor делает качественный контент. Но «качественный» — субъективная оценка. Клиенту нужна объективная: «Ваши продажи выросли на 67%, позиция поднялась с #45 на #22, ROI = 340%». Именно эта цифра превращает разовую съёмку в долгосрочное сотрудничество.

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

Ключевой эндпоинт MPStats

Основной источник данных — ежедневная статистика продаж по артикулу:

GET /wb/get/item/{sku}/sales

Возвращает массив ежедневных данных: продажи, выручка, остатки, цена. Дополнительно используем:

ЭндпоинтДанныеЗачем
GET /wb/get/item/{sku}/salesПродажи, выручка, остатки по днямБазовые метрики до/после обновления
GET /wb/get/item/{sku}/by_categoryПозиция в категории по днямОтслеживание роста позиции
GET /wb/get/item/{sku}/by_keywordsПозиции по ключевым словамSEO-эффект обновления контента
POST /wb/get/categoryСредние показатели категорииСравнение роста с рынком (клиент vs тренд)

Протокол 5 контрольных точек

Каждое обновление контента отслеживается в 5 временных точках — от baseline до долгосрочного эффекта.

gantt
title Before/After Timeline -- 5 точек замера
dateFormat YYYY-MM-DD
axisFormat %d.%m

section Baseline
T-30d Baseline (30 дней до) :done, baseline, 2026-02-01, 30d

section Update
Обновление контента :crit, update, 2026-03-03, 1d

section Tracking
T+7d Первый эффект :milestone, t1, 2026-03-10, 0d
T+14d Стабилизация :milestone, t2, 2026-03-17, 0d
T+30d Месячный результат :milestone, t3, 2026-04-02, 0d
T+90d Долгосрочный эффект :milestone, t4, 2026-06-01, 0d
Точка замераПериодЧто измеряемОжидаемый паттерн
T-30dЗа 30 дней до обновленияБазовые метрики (benchmark)Начальные значения — точка отсчёта
T+7dЧерез 7 дней послеПервый эффект — индексация, первые реакцииВозможен кратковременный спад (переиндексация WB)
T+14dЧерез 14 днейСтабилизация — алгоритм WB адаптировалсяНачало устойчивого роста позиции
T+30dЧерез 30 днейМесячный результат — устоявшиеся метрикиОсновной рост продаж и позиции
T+90dЧерез 90 днейДолгосрочный эффект — устойчивость результатаСтабильный уровень или продолжение роста
Зачем 5 точек, а не 2?

Две точки (до и после) не дают полной картины. На T+7d позиция может упасть — WB переиндексирует карточку, и клиент в панике звонит: «Стало хуже!» Если заранее предупредить про 5-точечный протокол, клиент спокойно ждёт T+30d. А если замерить только T+30d — можно пропустить быстрый рост на T+14d, который покажет, что контент работает даже лучше, чем ожидалось.

Детальное описание каждой точки замера и статистической значимости результатов: Before/After трекер.

Анализ

Алгоритм сбора и расчёта метрик

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

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


@dataclass
class MeasurementPoint:
"""Снапшот метрик товара в контрольной точке."""
label: str # t_baseline, t_7d, t_14d, t_30d, t_90d
period_start: str # YYYY-MM-DD
period_end: str # YYYY-MM-DD
avg_daily_sales: float # Средние продажи в день
total_revenue: float # Суммарная выручка за период
avg_position: float | None # Средняя позиция в категории
avg_price: float # Средняя цена


def fetch_sales_data(sku: int, d1: str, d2: str) -> list[dict]:
"""Загружает ежедневные продажи по SKU за период."""
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 fetch_category_position(sku: int, d1: str, d2: str) -> list[dict]:
"""Загружает позицию в категории по дням."""
resp = httpx.get(
f"{BASE_URL}/wb/get/item/{sku}/by_category",
headers=HEADERS,
params={"d1": d1, "d2": d2},
timeout=30,
)
resp.raise_for_status()
return resp.json()


def measure_point(
sku: int, label: str, period_start: str, period_end: str
) -> MeasurementPoint:
"""
Замеряет метрики в одной контрольной точке.
Собирает данные о продажах и позиции за указанный период.
"""
# Продажи
sales_data = fetch_sales_data(sku, period_start, period_end)
days = len(sales_data) or 1

total_sales = sum(day.get("sales", 0) for day in sales_data)
total_revenue = sum(day.get("revenue", 0) for day in sales_data)
avg_price = (
sum(day.get("price", 0) for day in sales_data) / days
if sales_data else 0
)

# Позиция в категории
position_data = fetch_category_position(sku, period_start, period_end)
positions = [
day.get("position")
for day in position_data
if day.get("position") is not None
]
avg_position = sum(positions) / len(positions) if positions else None

return MeasurementPoint(
label=label,
period_start=period_start,
period_end=period_end,
avg_daily_sales=round(total_sales / days, 1),
total_revenue=round(total_revenue, 0),
avg_position=round(avg_position, 0) if avg_position else None,
avg_price=round(avg_price, 0),
)


def run_before_after(sku: int, content_update_date: str) -> list[MeasurementPoint]:
"""
Запускает 5-точечный протокол замеров.

Args:
sku: Артикул товара на Wildberries
content_update_date: Дата обновления контента (YYYY-MM-DD)

Returns:
Список из 5 MeasurementPoint
"""
update = datetime.strptime(content_update_date, "%Y-%m-%d")

# Определяем периоды для каждой точки
points_config = [
("T-30d (baseline)", update - timedelta(days=30), update - timedelta(days=1)),
("T+7d", update + timedelta(days=1), update + timedelta(days=7)),
("T+14d", update + timedelta(days=1), update + timedelta(days=14)),
("T+30d", update + timedelta(days=1), update + timedelta(days=30)),
("T+90d", update + timedelta(days=1), update + timedelta(days=90)),
]

results = []
for label, start, end in points_config:
point = measure_point(
sku, label,
start.strftime("%Y-%m-%d"),
end.strftime("%Y-%m-%d"),
)
results.append(point)

return results


def calculate_deltas(points: list[MeasurementPoint]) -> list[dict]:
"""
Рассчитывает delta (%) для каждой точки относительно baseline.
"""
baseline = points[0]
deltas = []

for point in points:
delta_sales = (
(point.avg_daily_sales - baseline.avg_daily_sales)
/ baseline.avg_daily_sales * 100
if baseline.avg_daily_sales > 0 else 0
)
delta_revenue = (
(point.total_revenue - baseline.total_revenue)
/ baseline.total_revenue * 100
if baseline.total_revenue > 0 else 0
)
delta_position = None
if point.avg_position and baseline.avg_position:
# Позиция: снижение = улучшение (с #45 на #22 = рост)
delta_position = (
(baseline.avg_position - point.avg_position)
/ baseline.avg_position * 100
)

deltas.append({
"label": point.label,
"avg_daily_sales": point.avg_daily_sales,
"delta_sales_pct": round(delta_sales, 1),
"total_revenue": point.total_revenue,
"delta_revenue_pct": round(delta_revenue, 1),
"avg_position": point.avg_position,
"delta_position_pct": round(delta_position, 1) if delta_position else None,
})

return deltas


def calculate_roi(
delta_revenue_30d: float,
baseline_revenue_30d: float,
fotofactor_cost: float,
) -> float:
"""
ROI = (дополнительная выручка - стоимость контента) / стоимость контента * 100%

delta_revenue_30d: выручка за 30 дней ПОСЛЕ обновления
baseline_revenue_30d: выручка за 30 дней ДО обновления
fotofactor_cost: стоимость услуг Fotofactor
"""
extra_revenue = delta_revenue_30d - baseline_revenue_30d
roi = (extra_revenue - fotofactor_cost) / fotofactor_cost * 100
return round(roi, 1)
Формула ROI контента
ROI = (Дополнительная выручка − Стоимость контента) / Стоимость контента × 100%

Где:

  • Дополнительная выручка = Выручка за период ПОСЛЕ обновления минус Выручка за аналогичный период ДО обновления
  • Стоимость контента = Полная стоимость услуг Fotofactor (съёмка + обработка + загрузка)

Пример: Выручка ДО = 1 350 000 руб/мес, выручка ПОСЛЕ = 2 250 000 руб/мес, стоимость съёмки = 80 000 руб.

ROI = (2 250 000 − 1 350 000 − 80 000) / 80 000 × 100% = 1025%

Каждый вложенный рубль принёс 10.25 руб дополнительной выручки.

Пример отчёта: 5 точек замера

Реальный сценарий — женская кожаная сумка, SKU 12345678, стоимость съёмки 80 000 руб:

МетрикаДо (T-30d)T+7dT+14dT+30dT+90d
Продажи/день15 шт18 шт (+20%)22 шт (+47%)25 шт (+67%)28 шт (+87%)
Выручка/день45 000 руб54 000 руб (+20%)66 000 руб (+47%)75 000 руб (+67%)84 000 руб (+87%)
Позиция в категории#45#38 (+16%)#28 (+38%)#22 (+51%)#18 (+60%)
ContentScore42/10078/10078/10080/10080/100
ROI контента------+125%+340%

Типичная кривая роста после обновления контента

graph LR
A["T-30d<br/>Baseline<br/>15 продаж/день<br/>Позиция #45"] --> B["T+7d<br/>Первый эффект<br/>18 продаж/день (+20%)<br/>Позиция #38"]
B --> C["T+14d<br/>Стабилизация<br/>22 продажи/день (+47%)<br/>Позиция #28"]
C --> D["T+30d<br/>Основной рост<br/>25 продаж/день (+67%)<br/>ROI +125%"]
D --> E["T+90d<br/>Долгосрочный<br/>28 продаж/день (+87%)<br/>ROI +340%"]

style A fill:#ff6b6b,color:#fff
style B fill:#ffd93d,color:#333
style C fill:#ffd93d,color:#333
style D fill:#6bcb77,color:#fff
style E fill:#4d96ff,color:#fff

Сравнение с рынком — изоляция эффекта контента

Ключевой вопрос скептичного клиента: «А может, вся категория выросла, и вы тут ни при чём?» Для этого сравниваем рост клиента с ростом категории:

def compare_with_market(
client_deltas: list[dict],
category_path: str,
d1: str,
d2: str,
) -> dict:
"""
Сравнивает рост клиента со средним ростом категории.
Если клиент вырос на 67%, а категория на 10% -- чистый эффект контента = 57%.
"""
# Средние продажи категории за тот же период
resp = httpx.post(
f"{BASE_URL}/wb/get/category",
headers=HEADERS,
json={
"path": category_path,
"d1": d1,
"d2": d2,
"startRow": 0,
"endRow": 100,
},
timeout=30,
)
resp.raise_for_status()
category_data = resp.json().get("data", [])

avg_category_growth = sum(
item.get("sales_growth_percent", 0) for item in category_data
) / len(category_data) if category_data else 0

# Чистый эффект = рост клиента - рост рынка
client_growth = client_deltas[-1]["delta_sales_pct"] # T+90d
net_effect = client_growth - avg_category_growth

return {
"client_growth_pct": client_growth,
"market_growth_pct": round(avg_category_growth, 1),
"net_content_effect_pct": round(net_effect, 1),
"attribution": "content" if net_effect > 10 else "mixed",
}

Пример сравнения:

ПоказательКлиентКатегория в среднемЧистый эффект контента
Рост продаж за 90 дней+87%+12%+75%
Рост позиции+60%+5%+55%
Вывод----Контент = основной драйвер

Действие Fotofactor

Автоматическая рассылка ROI-отчётов

Система автоматически отправляет клиенту красивый отчёт в каждой контрольной точке:

graph TD
A["Обновление контента<br/>фиксируем SKU + дату"] --> B["Cron-задача<br/>проверяет наступление<br/>контрольных точек"]
B --> C{"Наступила<br/>контрольная<br/>точка?"}
C -->|"Нет"| B
C -->|"Да"| D["Сбор метрик<br/>MPStats API"]
D --> E["Расчёт delta<br/>+ ROI + сравнение<br/>с рынком"]
E --> F{"Метрики<br/>упали?"}
F -->|"Нет"| G["Позитивный отчёт<br/>рост, ROI, графики"]
F -->|"Да"| H["Алерт менеджеру<br/>+ диагностика причин"]
G --> I["Отправка клиенту<br/>email / Telegram"]
H --> J["Проактивная реакция<br/>разбор + план действий"]

Структура отчёта для клиента

Каждый отчёт содержит:

  1. Заголовок: «Отчёт по эффекту обновления контента — T+30d»
  2. Ключевые цифры: продажи, выручка, позиция — до и после (delta %)
  3. График роста: визуальная кривая по 5 точкам
  4. Сравнение с рынком: «Категория выросла на 12%, вы — на 67%. Чистый эффект контента: +55%»
  5. ROI: «Ваш ROI за 30 дней: +125%. Каждый вложенный рубль принёс 2.25 руб»
  6. Рекомендация: upsell или подтверждение стратегии

Цитата из T+90d отчёта:

Ваш ROI за 90 дней: 340%. Каждый вложенный рубль в обновление контента принёс 3.4 рубля дополнительной выручки. Категория за тот же период выросла на 12% — ваш рост в 7 раз выше рынка.

Система алертов при падении метрик

Если после обновления контента метрики падают — это сигнал для проактивной реакции, а не повод ждать:

СитуацияПорог срабатыванияДействие Fotofactor
Продажи упали на T+7dDelta < -10%Нормально: предупредить клиента о переиндексации
Продажи упали на T+14dDelta < -5%Проверить: изменения цены, акции конкурентов, сезон
Продажи упали на T+30dDelta < 0%Критично: разбор причин, корректировка контента
Позиция ухудшилась на T+30dDelta position < 0%Анализ: SEO, отзывы, ценовой фактор

Upsell на основе данных

Данные из ROI-отчёта — идеальный триггер для допродаж:

Результат по SKU 12345678 за 90 дней:
Продажи: +87%
ROI: +340%

→ Предложение клиенту:
"У вас ещё 12 карточек с ContentScore ниже 50.
Обновление 5 карточек по аналогичному сценарию
может дать дополнительно 400 000 руб/мес выручки.
Стоимость пакета: 320 000 руб. Ожидаемый ROI: 275%+"

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

Доказанная ценность контента

  • Чёткий ответ на вопрос «окупилось ли?» — в цифрах, а не словах
  • 5-точечный протокол снимает тревожность: клиент знает, когда ждать результат
  • Сравнение с рынком доказывает, что рост — не случайность, а результат работы Fotofactor

Влияние на удержание клиентов

МетрикаБез ROI-отчётовС ROI-отчётами
Продление сотрудничества (renewal)~40%80%+
Средний LTV клиента120 000 руб350 000 руб
Upsell (допродажи)10% клиентов45% клиентов
Рекомендации (сарафанное радио)РедкоКаждый 3-й клиент
Возражение «дорого»ЧастоеРедкое (ROI доказан)

Стоимость для Fotofactor

КомпонентСтоимостьКомментарий
MPStats API-запросы~5 запросов на точку × 5 точек = 25 запросовУкладывается в подписку
АвтоматизацияОднократная разработка cron-задачиДалее работает автоматически
Время менеджера10 мин на проверку отчётаТолько при алертах — ручное вмешательство
Итого на клиента~0 руб маржинальных затратВключено в стоимость услуги

Unit-экономика ROI-трекера

Вход:  1 SKU + дата обновления контента

Автоматика: 5 замеров × 25 API-запросов = 125 запросов за 90 дней

Выход: 5 отчётов клиенту (T+7d, T+14d, T+30d, T+90d + итоговый)

Эффект: Retention 40% → 80% = +100% LTV
+ Upsell 45% клиентов
+ Кейсы для новых продаж

ROI инструмента: ∞ (затрат ~0, эффект — удержание всей клиентской базы)
Почему это инструмент удержания #1

ROI-трекер не стоит Fotofactor почти ничего (API-запросы включены в подписку MPStats, отчёты генерируются автоматически). При этом он удваивает retention — а удержание существующего клиента в 5-7 раз дешевле привлечения нового. Это самая рентабельная инвестиция в клиентский сервис.

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

Минимальный рабочий пайплайн: от замера baseline до генерации отчёта с ROI.

import httpx
import json
from datetime import datetime, timedelta

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

# --- Конфигурация ---
CLIENT_SKU = 12345678
CONTENT_UPDATE_DATE = "2026-03-03"
FOTOFACTOR_COST = 80_000 # Стоимость съёмки, руб


def fetch_sales(sku: int, d1: str, d2: str) -> list[dict]:
"""Загружает ежедневные продажи."""
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 fetch_position(sku: int, d1: str, d2: str) -> list[dict]:
"""Загружает позицию в категории."""
resp = httpx.get(
f"{BASE_URL}/wb/get/item/{sku}/by_category",
headers=HEADERS,
params={"d1": d1, "d2": d2},
timeout=30,
)
resp.raise_for_status()
return resp.json()


def measure(sku: int, label: str, d1: str, d2: str) -> dict:
"""Собирает метрики за период."""
sales_data = fetch_sales(sku, d1, d2)
pos_data = fetch_position(sku, d1, d2)

days = len(sales_data) or 1
total_sales = sum(d.get("sales", 0) for d in sales_data)
total_revenue = sum(d.get("revenue", 0) for d in sales_data)

positions = [d.get("position") for d in pos_data if d.get("position")]
avg_pos = round(sum(positions) / len(positions)) if positions else None

return {
"label": label,
"period": f"{d1} -- {d2}",
"avg_daily_sales": round(total_sales / days, 1),
"total_revenue": round(total_revenue),
"avg_position": avg_pos,
}


def run_protocol(sku: int, update_date: str) -> list[dict]:
"""Запускает 5-точечный протокол."""
ud = datetime.strptime(update_date, "%Y-%m-%d")

configs = [
("T-30d (baseline)", ud - timedelta(days=30), ud - timedelta(days=1)),
("T+7d", ud + timedelta(days=1), ud + timedelta(days=7)),
("T+14d", ud + timedelta(days=1), ud + timedelta(days=14)),
("T+30d", ud + timedelta(days=1), ud + timedelta(days=30)),
("T+90d", ud + timedelta(days=1), ud + timedelta(days=90)),
]

return [
measure(sku, label, s.strftime("%Y-%m-%d"), e.strftime("%Y-%m-%d"))
for label, s, e in configs
]


def generate_report(points: list[dict], cost: float) -> str:
"""Генерирует текстовый отчёт с delta и ROI."""
baseline = points[0]
lines = [
f"# ROI-ОТЧЁТ: До/После обновления контента",
f"# SKU: {CLIENT_SKU}",
f"# Дата обновления: {CONTENT_UPDATE_DATE}",
f"# Стоимость контента: {cost:,.0f} руб",
f"# Дата отчёта: {datetime.now().strftime('%Y-%m-%d')}",
"",
f"{'Точка':<20} {'Продажи/день':<15} {'Delta':<10} "
f"{'Выручка':<15} {'Позиция':<10}",
"-" * 70,
]

for point in points:
delta_sales = (
(point["avg_daily_sales"] - baseline["avg_daily_sales"])
/ baseline["avg_daily_sales"] * 100
if baseline["avg_daily_sales"] > 0 else 0
)

delta_str = f"+{delta_sales:.0f}%" if delta_sales > 0 else f"{delta_sales:.0f}%"
if point["label"] == baseline["label"]:
delta_str = "baseline"

pos_str = f"#{point['avg_position']}" if point["avg_position"] else "n/a"

lines.append(
f"{point['label']:<20} "
f"{point['avg_daily_sales']:<15} "
f"{delta_str:<10} "
f"{point['total_revenue']:>12,.0f} руб "
f"{pos_str:<10}"
)

# ROI для T+30d и T+90d
if len(points) >= 4:
t30 = points[3]
extra_30 = t30["total_revenue"] - baseline["total_revenue"]
roi_30 = (extra_30 - cost) / cost * 100 if cost > 0 else 0
lines.append(f"\nROI за 30 дней: {roi_30:+.0f}%")

if len(points) >= 5:
t90 = points[4]
extra_90 = t90["total_revenue"] - baseline["total_revenue"]
roi_90 = (extra_90 - cost) / cost * 100 if cost > 0 else 0
lines.append(f"ROI за 90 дней: {roi_90:+.0f}%")
roi_rub = extra_90 / cost if cost > 0 else 0
lines.append(f"Каждый вложенный рубль принёс: {roi_rub:.1f} руб")

return "\n".join(lines)


# --- Основной пайплайн ---
if __name__ == "__main__":
print(f"Запуск 5-точечного протокола для SKU {CLIENT_SKU}...")
print(f"Дата обновления контента: {CONTENT_UPDATE_DATE}")
print(f"Стоимость контента: {FOTOFACTOR_COST:,} руб\n")

# 1. Сбор данных по 5 точкам
points = run_protocol(CLIENT_SKU, CONTENT_UPDATE_DATE)

# 2. Генерация отчёта
report = generate_report(points, FOTOFACTOR_COST)
print(report)

# 3. Сохранение
output = {
"sku": CLIENT_SKU,
"content_update_date": CONTENT_UPDATE_DATE,
"fotofactor_cost": FOTOFACTOR_COST,
"measurement_points": points,
"report": report,
"generated_at": datetime.now().isoformat(),
}

filename = f"roi_report_{CLIENT_SKU}_{datetime.now():%Y%m%d}.json"
with open(filename, "w", encoding="utf-8") as f:
json.dump(output, f, ensure_ascii=False, indent=2)
print(f"\nСохранено: {filename}")

Что дальше