Как удалить Excel навсегда: делегируем юнит-экономику на Wildberries нейронке

0a35565eafa9c58e443b1a5ab85f14e1.png

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

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

Юнит-экономика — это многокомпонентная структура затрат, включающая комиссионные сборы маркетплейса, логистические издержки, расходы на рекламные кампании (таргетированная реклама, SEO, продвижение внутри WB), а также скрытые издержки (возвраты). 

Каждую метрику отражают другие десятки метрик — анализ становится многомерным. Представьте себе многомерную фигуру — вот и мы не можем. 

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

А Байес как раз и поможет в прогнозировании со своими вероятностями. Почитать о нем на Хабре можно — тут. 

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

В боте можно посчитать свою свою маржу и доходы конкурентов.

2a3e3bf8421e772ca333ff538db522be.png

Юнит-экономика на Wildberries и Байес: функционал бота?

440693caeca7794232dadb511705f755.png

Первой целью, естественно, остается автоматизация расчета ключевых метрик. Расчет таких показателей, как CAC (стоимость привлечения клиента), LTV (пожизненная ценность клиента), ROI (возврат на инвестиции), и маржинальность — это база любой стратегии ресселера. 

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

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

Третья цель — это предоставление аналитических рекомендаций. Здесь мы подходим к самой ироничной части: большинство ресселеров ожидают от таких ботов почти магических решений. Например, рекомендаций уровня «снизьте цену на 3 рубля, и продажи возрастут на 15%». 

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

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

Байес работает из принципа априорных (предварительных распределений) к апостериорным (после работы нейронки). BNN — это про вероятностные распределения, исходящие из голой, предоставленной вами статистики. 

Классическая нейронная сеть считает свои веса фиксированными значениями. В контексте BNN это выглядит так же неуместно, как доверие рекомендациям Wildberries, предполагающим «актуальность» товара. 

В BNN веса представляются не как фиксированные значения, а как вероятностные распределения. 

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

В коде это реализуется следующим образом:

import tensorflow as tf
import tensorflow_probability as tfp

# Определяем слои с вариационным выводом
def bayesian_dense(units, activation):
    return tfp.layers.DenseVariational(
        units,
        make_posterior_fn=tfp.layers.default_mean_field_normal_fn(),
        make_prior_fn=tfp.layers.default_mean_field_normal_fn(),
        activation=activation
    )

# Определяем модель
def create_bnn(input_dim):
    inputs = tf.keras.Input(shape=(input_dim,))
    x = bayesian_dense(64, activation="relu")(inputs)
    x = bayesian_dense(32, activation="relu")(x)
    outputs = bayesian_dense(1, activation="sigmoid")(x)  # Например, прогноз возвратов
    return tf.keras.Model(inputs, outputs)

model = create_bnn(input_dim=10)

Здесь мы используем слои DenseVariational из TF Probability, которые автоматически применяют вариационный вывод для аппроксимации распределений весов. 

Обучение с учетом штрафов за высокую энтропию предсказаний

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

Пример настройки функции потерь:

def custom_loss(y_true, y_pred, kl_divergence, weight=1e-3):
    # Базовая ошибка
    mse = tf.keras.losses.mean_squared_error(y_true, y_pred)
    # Штраф за энтропию через KL-дивергенцию
    kl_penalty = weight * kl_divergence
    return mse + kl_penalty

Теперь интегрируем это в процесс обучения:

# Компиляция модели с учётом KL-дивергенции
model.compile(
    optimizer=tf.keras.optimizers.Adam(),
    loss=lambda y_true, y_pred: custom_loss(y_true, y_pred, model.losses[0])
)

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

Применение BNN на практике

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

Пример предсказания:

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()

# Описываем входные данные через Pydantic
class MetricsRequest(BaseModel):
    costs: float
    revenue: float
    returns: float

# Простенький эндпоинт для расчетов
@app.post("/calculate_metrics/")
async def calculate_metrics(request: MetricsRequest):
    try:
        cac = request.costs / max(request.revenue, 1)  # Предотвращаем деление на ноль
        roi = (request.revenue - request.costs) / max(request.costs, 1)
        ltv = request.revenue * 1.2  # Простая модель расчета LTV для демо
        
        return {
            "CAC": round(cac, 2),
            "ROI": round(roi, 2),
            "LTV": round(ltv, 2)
        }
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Ошибка при вычислении: {str(e)}")

Модель позволяет говорить с ресселерами на языке вероятностей: например, «с вероятностью 80% возвраты не превысят 15%». 

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

Учет априорных предположений: формализация априорных распределений

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

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

Примеры априорных распределений

Рассмотрим типовые параметры:

  1. Ценовая эластичность. Обычно можно предположить, что она ограничена, скажем, в диапазоне от -3 (высокая чувствительность) до 0 (идеальный товар). Априорное распределение можно задать, например, как нормальное с отрицательным средним.

  2. Конверсия. Это параметр от 0 до 1, отражающий вероятность покупки. Для него наиболее естественным выбором станет бета-распределение.

  3. Вероятность возврата. Для оценки возвратов можно использовать распределение, чувствительное к редким, но возможным сценариям возвратов массовых партий (например, из-за смены алгоритмов в выдаче маркетплейса).

Реализация априорных предположений в коде

Для реализации априорных распределений используем возможности TensorFlow Probability (TFP):

import tensorflow_probability as tfp

# Ценовая эластичность: нормальное распределение с отрицательным средним
price_elasticity_prior = tfp.distributions.Normal(loc=-1.5, scale=0.5)

# Конверсия: бета-распределение
conversion_prior = tfp.distributions.Beta(concentration1=2.0, concentration0=5.0)

# Вероятность возвратов: усечённое нормальное распределение
returns_prior = tfp.distributions.TruncatedNormal(loc=0.1, scale=0.05, low=0.0, high=1.0)

# Генерируем случайные значения
price_elasticity_samples = price_elasticity_prior.sample(10).numpy()
conversion_samples = conversion_prior.sample(10).numpy()
returns_samples = returns_prior.sample(10).numpy()

print("Примеры ценовой эластичности:", price_elasticity_samples)
print("Примеры конверсии:", conversion_samples)
print("Примеры возвратов:", returns_samples)

Включение априорных распределений в модель

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

def make_prior_fn():
    return tfp.distributions.Normal(loc=0.1, scale=0.05)  # Априорное распределение для возвратов

def make_posterior_fn():
    return tfp.layers.default_mean_field_normal_fn()  # Используем стандартный вариационный вывод

# Создаем Байесовский слой с априорным распределением
bayesian_layer = tfp.layers.DenseVariational(
    units=1,
    make_prior_fn=make_prior_fn,
    make_posterior_fn=make_posterior_fn,
    activation="sigmoid"
)

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

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

Интеграция в обучение модели

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

# Компиляция модели с учётом априорных предположений
model.compile(
    optimizer=tf.keras.optimizers.Adam(),
    loss=lambda y_true, y_pred: custom_loss(y_true, y_pred, model.losses[0])
)

# Обучение
history = model.fit(X_train, y_train, epochs=10, batch_size=32)

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

Но стоит помнить: априоры — не догма, а скорее напоминание о том, как маркетплейсы (и их клиенты) обычно ведут себя, пока не возникнет непредвиденных ситуаций. О том, почему они возникают, читайте другую нашу статью про заработок на резкий комиссионных от Wildberries. 

От теории к практике: собираем нашу нейросеть на статистике Байеса

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

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

Пример запроса к API Wildberries:

import requests

def fetch_wb_data(api_key, date_from, date_to):
    """
    Загружает данные о продажах с Wildberries API.
    """
    url = "https://suppliers-api.wildberries.ru/api/v1/sales"
    headers = {"Authorization": api_key}
    params = {
        "dateFrom": date_from,
        "dateTo": date_to,
        "limit": 1000
    }
    
    response = requests.get(url, headers=headers, params=params)
    if response.status_code == 200:
        return response.json()
    else:
        raise Exception(f"Ошибка API: {response.status_code} - {response.text}")

# Пример вызова
api_key = "ваш_ключ_доступа"
sales_data = fetch_wb_data(api_key, "2024-01-01", "2024-01-31")

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

Устранение выбросов

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

import pandas as pd
import numpy as np

def remove_outliers(dataframe, column):
    """
    Удаляет выбросы из указанной колонки, используя IQR.
    """
    q1 = dataframe[column].quantile(0.25)
    q3 = dataframe[column].quantile(0.75)
    iqr = q3 - q1
    lower_bound = q1 - 1.5 * iqr
    upper_bound = q3 + 1.5 * iqr
    
    filtered_df = dataframe[(dataframe[column] >= lower_bound) & (dataframe[column] <= upper_bound)]
    return filtered_df

# Пример обработки
sales_df = pd.DataFrame(sales_data)  # Преобразуем JSON в DataFrame
filtered_sales = remove_outliers(sales_df, "price")

Нормализация данных

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

from sklearn.preprocessing import MinMaxScaler

def normalize_columns(dataframe, columns):
    """
    Нормализует указанные колонки DataFrame в диапазон [0, 1].
    """
    scaler = MinMaxScaler()
    dataframe[columns] = scaler.fit_transform(dataframe[columns])
    return dataframe

# Пример нормализации
columns_to_normalize = ["price", "return_rate", "logistics_cost"]
normalized_sales = normalize_columns(filtered_sales, columns_to_normalize)

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

def aggregate_time_series(dataframe, date_column, metric_column, freq="D"):
    """
    Агрегирует данные по времени с заданной частотой.
    """
    dataframe[date_column] = pd.to_datetime(dataframe[date_column])
    aggregated = dataframe.groupby(pd.Grouper(key=date_column, freq=freq))[metric_column].sum().reset_index()
    return aggregated

# Пример агрегации
aggregated_sales = aggregate_time_series(normalized_sales, "sale_date", "price")

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

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

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

Переход от детерминированных весов к апостериорным распределениям позволяет модели обучаться с учетом неопределенности в данных. Заюзаем библиотеку Pyro.

import pyro
import pyro.distributions as dist
import torch
import torch.nn as nn

# Байесовский регрессионный блок
class BayesianRegression(nn.Module):
    def __init__(self, input_dim):
        super().__init__()
        self.weight_mean = nn.Parameter(torch.zeros(input_dim))
        self.weight_scale = nn.Parameter(torch.ones(input_dim))
        self.bias_mean = nn.Parameter(torch.zeros(1))
        self.bias_scale = nn.Parameter(torch.ones(1))
    
    def forward(self, x):
        # Аппроксимация апостериорных распределений весов
        weight = pyro.sample("weight", dist.Normal(self.weight_mean, self.weight_scale))
        bias = pyro.sample("bias", dist.Normal(self.bias_mean, self.bias_scale))
        return torch.matmul(x, weight) + bias

Здесь weight_mean и bias_mean представляют собой априорные предположения о параметрах модели, тогда как weight_scale и bias_scale определяют степень их неопределенности. Подход с распределениями вместо фиксированных параметров позволяет модели быть гибкой и адаптироваться к новым данным. 

Каждый вызов метода forward соответствует вероятностному выводу с учетом этих распределений.

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

Для упрощения реализации используется стандартная архитектура с несколькими линейными слоями и функциями активации ReLU.

class DeepNeuralNetwork(nn.Module):
    def __init__(self, input_dim, hidden_dim):
        super().__init__()
        self.layer1 = nn.Linear(input_dim, hidden_dim)
        self.layer2 = nn.Linear(hidden_dim, hidden_dim // 2)
        self.output = nn.Linear(hidden_dim // 2, 1)
        self.relu = nn.ReLU()
    
    def forward(self, x):
        x = self.relu(self.layer1(x))
        x = self.relu(self.layer2(x))
        return self.output(x)

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

class HybridModel(nn.Module):
    def __init__(self, input_dim, hidden_dim):
        super().__init__()
        self.bayesian_block = BayesianRegression(input_dim)
  self.deep_block = DeepNeuralNetwork(input_dim, hidden_dim)
    def forward(self, x):
        # Байесовская регрессия для вероятностного вывода параметров
        bayesian_output = self.bayesian_block(x)
        # Глубокая нейронная сеть для обработки и генерации окончательного прогноза
        final_output = self.deep_block(bayesian_output)
        return final_output

Аритектура работает следующим образом: входные данные поступают в Байесовский блок, который формирует апостериорные распределения параметров на основе входных переменных. 

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

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

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

def generate_recommendations(predictions, thresholds):
    recommendations = []
    for metric, value in predictions.items():
        if value < thresholds[metric]:
            recommendations.append(f"Увеличьте маркетинговый бюджет для {metric}, текущий показатель: {value:.2f}")
        else:
            recommendations.append(f"{metric} находится в допустимых пределах: {value:.2f}")
    return recommendations

Здесь predictions — это словарь с ключевыми метриками (например, CAC, LTV), а thresholds задает пороговые значения для каждой из них. Такой механизм позволяет интегрировать результаты модели в бот для генерации полезных рекомендаций.

Теперь перейдем к построению самого API, которое станет основой для взаимодействия с нашими моделями и данными.

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()

# Описываем входные данные через Pydantic
class MetricsRequest(BaseModel):
    costs: float
    revenue: float
    returns: float

# Простенький эндпоинт для расчетов
@app.post("/calculate_metrics/")
async def calculate_metrics(request: MetricsRequest):
    try:
        cac = request.costs / max(request.revenue, 1)  # Предотвращаем деление на ноль
        roi = (request.revenue - request.costs) / max(request.costs, 1)
        ltv = request.revenue * 1.2  # Простая модель расчета LTV для демо
        
        return {
            "CAC": round(cac, 2),
            "ROI": round(roi, 2),
            "LTV": round(ltv, 2)
        }
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Ошибка при вычислении: {str(e)}")

Что здесь происходит?  

Мы создали простой эндпоинт /calculate_metrics/, который принимает входные данные в формате JSON, рассчитывает базовые метрики (CAC, ROI, LTV) и возвращает их в виде JSON-объекта. Pydantic здесь играет роль строгого надзирателя, не позволяя вашему API упасть от неожиданных данных вроде строки «пять» вместо числа.

Теперь о взаимодействии с пользователем. Telegram-боты — это, пожалуй, самый быстрый способ получить обратную связь от пользователей. Библиотека python-telegram-bot позволяет минимизировать боль интеграции, а асинхронный подход FastAPI обеспечивает легкость масштабирования. Вот пример создания Telegram-бота, который отправляет метрики через наш API:

from telegram import Update, Bot
from telegram.ext import Updater, CommandHandler, CallbackContext
import requests

TOKEN = "ваш_токен_бота"
API_URL = "http://127.0.0.1:8000/calculate_metrics/"

# Команда для расчета метрик
def calculate(update: Update, context: CallbackContext):
    chat_id = update.effective_chat.id
    bot: Bot = context.bot

    # Допустим, пользователь передает затраты, доход и возвраты через пробел
    try:
        costs, revenue, returns = map(float, context.args)
        data = {"costs": costs, "revenue": revenue, "returns": returns}
        response = requests.post(API_URL, json=data)
        
        if response.status_code == 200:
            metrics = response.json()
            message = (f"Рассчитанные метрики:\n"
                       f"✔️ CAC: {metrics['CAC']}\n"
                       f"✔️ ROI: {metrics['ROI']}\n"
                       f"✔️ LTV: {metrics['LTV']}")
        else:
            message = f"Ошибка: {response.json().get('detail', 'Неизвестная ошибка')}"
    except Exception as e:
        message = f"Некорректные данные или ошибка: {str(e)}"

    bot.send_message(chat_id=chat_id, text=message)

# Настройка бота
def main():
    updater = Updater(TOKEN)
    dp = updater.dispatcher

    dp.add_handler(CommandHandler("calculate", calculate))
    updater.start_polling()
    updater.idle()

if __name__ == "__main__":
    main()

Этот бот реагирует на команду /calculate, получая данные прямо из сообщения пользователя. 

Вы вводите что-то вроде /calculate 5000 20000 300, а он отправляет запрос в наш FastAPI-сервер и возвращает рассчитанные метрики. 

Таким образом, архитектура состоит из двух идеально взаимодействующих компонентов: API, который выполняет все вычисления, и Telegram-бота, обеспечивающего удобный интерфейс. 

Конечно, все это можно обернуть в Docker, запустить на Kubernetes и добавить ещё пару слоев сложностей, чтобы в будущем никто, включая вас, не смог разобраться, как всё это работает.

В конечном счете, необязательно мучиться именно так. Для тех, кто торгует нескольким товарами — достаточного простого бота для анализа конкурентов или своей юнит-экономики. 

© Habrahabr.ru