some refactoring

This commit is contained in:
dan
2025-12-14 17:07:57 +03:00
parent 935639c3d6
commit cfee72470c
28 changed files with 7 additions and 1755 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,55 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "3d3a9c98",
"metadata": {},
"source": [
"# EDA «Коммуникации в Городе»\n",
"\n",
"## Кратко о данных\n",
"- 118189 строк, 8339 клиентов, 35 исходных столбцов + инженерные (totals, CTR/CR, флаги).\n",
"- Диапазон дат: 20250109 — 20251104 (284 дня).\n",
"- Категории сервисов: ent, super, transport, shopping, hotel, avia; активные и пассивные показы/клики, заказы по категориям.\n",
"- Дубликаты по ключу (id, business_dt): нет.\n",
"\n",
"## Качество данных\n",
"- Пропуски: несущественные (NaN почти нет), отрицательных значений не обнаружено.\n",
"- Возраст: 1580 лет, p1/p99 = 22/68, мусора (<14 или >100) нет.\n",
"- Гендер: 68.5% M, 31.5% F. Платформа после нормализации: ~52.5% iOS, ~46.7% Android, 1.1% iPadOS.\n",
"- Признаки «заспамленности» и агрегаты на клиента добавлены: imp/click/order totals, CTR/CR, contact_days, avg_impressions_per_contact_day, order_categories_count.\n",
"\n",
"## Каналы и эффективность (агрегировано по всем строкам)\n",
"- Active impressions ≈ 219.5k, passive impressions ≈ 473.1k.\n",
"- Active clicks ≈ 147.3k (CTR_active ≈ 0.67), passive clicks ≈ 18.1k (CTR_passive ≈ 0.038).\n",
"- Заказы всего: 12439; CR click→order ≈ 7.5%, CR imp→order ≈ 1.8%.\n",
"- Дневных точек: 284; daily агрегаты подготовлены (CTR/CR, day_of_week).\n",
"\n",
"## Демография и устройство vs эффективность (по клиентским агрегатам)\n",
"- Таблицы по полу/возрастным группам/платформам готовы в `04_clients_segmentation.ipynb` (средние impressions/clicks/orders и CTR/CR).\n",
"- Гипотезы: в 05-м ноутбуке добавлены примеры MannWhitney по CTR active vs passive и по полу; можно расширять на платформы и возраст.\n",
"\n",
"## Лаги и сезонность\n",
"- Дневные ряды и метрики CTR/CR по времени и по дням недели — см. `03_time_and_lags.ipynb`.\n",
"- Лаги: реализованы кросс-корреляции orders vs impressions/clicks (hotel, avia) для lag 07; по клиентам — first_imp/click/order и распределения дней до заказа.\n",
"\n",
"## Сегменты и «усталость»\n",
"- Сегменты каналов: only_active / only_passive / both + метрики; бины по числу категорий заказов.\n",
"- «Заспамленность»: bin по avg_impressions_per_contact_day с CTR/CR; stacked доли категорий заказов по возрасту.\n",
"\n",
"## Модели как часть EDA\n",
"- На клиентском уровне собран датасет для задачи `has_any_order`; pipeline с OHE + StandardScaler + LogisticRegression и RandomForest (ROC-AUC и важности).\n",
"- Выводы по коэффициентам/важности доступны в `05_exploratory_models.ipynb`.\n",
"\n",
"## Что делать дальше\n",
"- Прогнать все ноутбуки end-to-end (данные готовы, зависимости в `.venv`): `jupyter lab` или `jupyter nbconvert --execute`.\n",
"- Уточнить нормализацию категорий (при необходимости) и при желании сохранить `dataset/ds_clean.parquet` (флаг в `01_load_and_clean.ipynb`).\n",
"- Добавить/актуализировать бизнес-гипотезы (категории, платформы, возраст) и зафиксировать p-value в таблице гипотез.\n",
"- При необходимости усилить визуализацию: календарные heatmap по CTR, ECDF лагов по каждой категории, PDP для топ-фичей модели.\n"
]
}
],
"metadata": {},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@@ -1,41 +0,0 @@
# EDA «Коммуникации в Городе»
## Кратко о данных
- 118189 строк, 8339 клиентов, 35 исходных столбцов + инженерные (totals, CTR/CR, флаги).
- Диапазон дат: 20250109 — 20251104 (284 дня).
- Категории сервисов: ent, super, transport, shopping, hotel, avia; активные и пассивные показы/клики, заказы по категориям.
- Дубликаты по ключу (id, business_dt): нет.
## Качество данных
- Пропуски: несущественные (NaN почти нет), отрицательных значений не обнаружено.
- Возраст: 1580 лет, p1/p99 = 22/68, мусора (<14 или >100) нет.
- Гендер: 68.5% M, 31.5% F. Платформа после нормализации: ~52.5% iOS, ~46.7% Android, 1.1% iPadOS.
- Признаки «заспамленности» и агрегаты на клиента добавлены: imp/click/order totals, CTR/CR, contact_days, avg_impressions_per_contact_day, order_categories_count.
## Каналы и эффективность (агрегировано по всем строкам)
- Active impressions ≈ 219.5k, passive impressions ≈ 473.1k.
- Active clicks ≈ 147.3k (CTR_active ≈ 0.67), passive clicks ≈ 18.1k (CTR_passive ≈ 0.038).
- Заказы всего: 12439; CR click→order ≈ 7.5%, CR imp→order ≈ 1.8%.
- Дневных точек: 284; daily агрегаты подготовлены (CTR/CR, day_of_week).
## Демография и устройство vs эффективность (по клиентским агрегатам)
- Таблицы по полу/возрастным группам/платформам готовы в `04_clients_segmentation.ipynb` (средние impressions/clicks/orders и CTR/CR).
- Гипотезы: в 05-м ноутбуке добавлены примеры MannWhitney по CTR active vs passive и по полу; можно расширять на платформы и возраст.
## Лаги и сезонность
- Дневные ряды и метрики CTR/CR по времени и по дням недели — см. `03_time_and_lags.ipynb`.
- Лаги: реализованы кросс-корреляции orders vs impressions/clicks (hotel, avia) для lag 07; по клиентам — first_imp/click/order и распределения дней до заказа.
## Сегменты и «усталость»
- Сегменты каналов: only_active / only_passive / both + метрики; бины по числу категорий заказов.
- «Заспамленность»: bin по avg_impressions_per_contact_day с CTR/CR; stacked доли категорий заказов по возрасту.
## Модели как часть EDA
- На клиентском уровне собран датасет для задачи `has_any_order`; pipeline с OHE + StandardScaler + LogisticRegression и RandomForest (ROC-AUC и важности).
- Выводы по коэффициентам/важности доступны в `05_exploratory_models.ipynb`.
## Что делать дальше
- Прогнать все ноутбуки end-to-end (данные готовы, зависимости в `.venv`): `jupyter lab` или `jupyter nbconvert --execute`.
- Уточнить нормализацию категорий (при необходимости) и при желании сохранить `dataset/ds_clean.parquet` (флаг в `01_load_and_clean.ipynb`).
- Добавить/актуализировать бизнес-гипотезы (категории, платформы, возраст) и зафиксировать p-value в таблице гипотез.
- При необходимости усилить визуализацию: календарные heatmap по CTR, ECDF лагов по каждой категории, PDP для топ-фичей модели.

View File

@@ -1,154 +0,0 @@
from __future__ import annotations
from pathlib import Path
from typing import Dict, Iterable, List
import numpy as np
import pandas as pd
# Paths and column groups
DATA_PATH = Path("dataset/ds.csv")
CATEGORIES: List[str] = ["ent", "super", "transport", "shopping", "hotel", "avia"]
ACTIVE_IMP_COLS = [f"active_imp_{c}" for c in CATEGORIES]
PASSIVE_IMP_COLS = [f"passive_imp_{c}" for c in CATEGORIES]
ACTIVE_CLICK_COLS = [f"active_click_{c}" for c in CATEGORIES]
PASSIVE_CLICK_COLS = [f"passive_click_{c}" for c in CATEGORIES]
ORDER_COLS = [f"orders_amt_{c}" for c in CATEGORIES]
NUMERIC_COLS = (
ACTIVE_IMP_COLS
+ PASSIVE_IMP_COLS
+ ACTIVE_CLICK_COLS
+ PASSIVE_CLICK_COLS
+ ORDER_COLS
+ ["age"]
)
CAT_COLS = ["gender_cd", "device_platform_cd"]
def safe_divide(numerator: pd.Series | float, denominator: pd.Series | float) -> pd.Series:
"""Divide with protection against zero (works for Series and scalars)."""
if isinstance(denominator, pd.Series):
denom = denominator.replace(0, np.nan)
else:
denom = np.nan if float(denominator) == 0 else denominator
return numerator / denom
def normalize_gender(series: pd.Series) -> pd.Series:
cleaned = series.fillna("UNKNOWN").astype(str).str.strip().str.upper()
mapping = {"M": "M", "MALE": "M", "F": "F", "FEMALE": "F"}
return cleaned.map(mapping).fillna("UNKNOWN")
def normalize_device(series: pd.Series) -> pd.Series:
cleaned = series.fillna("unknown").astype(str).str.strip()
lowered = cleaned.str.lower().str.replace(" ", "").str.replace("_", "")
mapping = {"android": "Android", "ios": "iOS", "ipados": "iPadOS", "ipad": "iPadOS"}
mapped = lowered.map(mapping)
fallback = cleaned.str.title()
return mapped.fillna(fallback)
def add_age_group(df: pd.DataFrame) -> pd.DataFrame:
bins = [0, 25, 35, 45, 55, np.inf]
labels = ["<25", "25-34", "35-44", "45-54", "55+"]
df["age_group"] = pd.cut(df["age"], bins=bins, labels=labels, right=False)
return df
def add_totals(df: pd.DataFrame) -> pd.DataFrame:
df["active_imp_total"] = df[ACTIVE_IMP_COLS].sum(axis=1)
df["passive_imp_total"] = df[PASSIVE_IMP_COLS].sum(axis=1)
df["active_click_total"] = df[ACTIVE_CLICK_COLS].sum(axis=1)
df["passive_click_total"] = df[PASSIVE_CLICK_COLS].sum(axis=1)
df["orders_amt_total"] = df[ORDER_COLS].sum(axis=1)
df["click_total"] = df["active_click_total"] + df["passive_click_total"]
df["imp_total"] = df["active_imp_total"] + df["passive_imp_total"]
df["active_ctr"] = safe_divide(df["active_click_total"], df["active_imp_total"])
df["passive_ctr"] = safe_divide(df["passive_click_total"], df["passive_imp_total"])
df["ctr_all"] = safe_divide(df["click_total"], df["imp_total"])
df["cr_click2order"] = safe_divide(df["orders_amt_total"], df["click_total"])
df["cr_imp2order"] = safe_divide(df["orders_amt_total"], df["imp_total"])
return df
def add_flags(df: pd.DataFrame) -> pd.DataFrame:
df["has_active_comm"] = (df[ACTIVE_IMP_COLS + ACTIVE_CLICK_COLS].sum(axis=1) > 0).astype(int)
df["has_passive_comm"] = (df[PASSIVE_IMP_COLS + PASSIVE_CLICK_COLS].sum(axis=1) > 0).astype(int)
df["has_any_order"] = (df[ORDER_COLS].sum(axis=1) > 0).astype(int)
df["order_categories_count"] = (df[ORDER_COLS] > 0).sum(axis=1)
return df
def load_data(path: Path | str = DATA_PATH) -> pd.DataFrame:
df = pd.read_csv(path)
df["business_dt"] = pd.to_datetime(df["business_dt"])
df["gender_cd"] = normalize_gender(df["gender_cd"])
df["device_platform_cd"] = normalize_device(df["device_platform_cd"])
df = add_age_group(df)
df = add_totals(df)
df = add_flags(df)
return df
def describe_zero_share(df: pd.DataFrame, cols: Iterable[str]) -> pd.DataFrame:
stats = []
for col in cols:
series = df[col]
stats.append(
{
"col": col,
"count": series.count(),
"mean": series.mean(),
"median": series.median(),
"std": series.std(),
"min": series.min(),
"q25": series.quantile(0.25),
"q75": series.quantile(0.75),
"max": series.max(),
"share_zero": (series == 0).mean(),
"p95": series.quantile(0.95),
"p99": series.quantile(0.99),
}
)
return pd.DataFrame(stats)
def build_daily(df: pd.DataFrame) -> pd.DataFrame:
agg_cols = ACTIVE_IMP_COLS + PASSIVE_IMP_COLS + ACTIVE_CLICK_COLS + PASSIVE_CLICK_COLS + ORDER_COLS
daily = df.groupby("business_dt")[agg_cols].sum().reset_index()
daily = add_totals(daily)
daily["day_of_week"] = daily["business_dt"].dt.day_name()
return daily
def build_client(df: pd.DataFrame) -> pd.DataFrame:
agg_spec: Dict[str, str] = {col: "sum" for col in ACTIVE_IMP_COLS + PASSIVE_IMP_COLS + ACTIVE_CLICK_COLS + PASSIVE_CLICK_COLS + ORDER_COLS}
meta_spec: Dict[str, str | callable] = {
"age": "median",
"gender_cd": lambda s: s.mode().iat[0] if not s.mode().empty else "UNKNOWN",
"age_group": lambda s: s.mode().iat[0] if not s.mode().empty else np.nan,
"device_platform_cd": lambda s: s.mode().iat[0] if not s.mode().empty else "Other",
}
agg_spec.update(meta_spec)
client = df.groupby("id").agg(agg_spec).reset_index()
contact_days = df.groupby("id")["business_dt"].nunique().rename("contact_days")
imp_day = df.copy()
imp_day["imp_day_total"] = imp_day[ACTIVE_IMP_COLS + PASSIVE_IMP_COLS].sum(axis=1)
max_imp_day = imp_day.groupby("id")["imp_day_total"].max().rename("max_impressions_per_day")
client = add_totals(client)
client = add_flags(client)
client = client.merge(contact_days, on="id", how="left")
client = client.merge(max_imp_day, on="id", how="left")
client = add_contact_density(client)
return client
def add_contact_density(df: pd.DataFrame) -> pd.DataFrame:
# contact_days must already be present
if "contact_days" in df.columns:
df["avg_impressions_per_contact_day"] = safe_divide(df["imp_total"], df["contact_days"])
return df
return df

View File

@@ -1,368 +0,0 @@
# План полноценного преданализа датасета «Коммуникации в Городе»
Основано на описании датасета: есть ежедневные коммуникации с клиентами в экосистеме «Город Т-Банка», активные/пассивные каналы, показы/клики и заказы по категориям (ent, super, transport, shopping, hotel, avia), а также демография и устройство.
Обозначения:
- `*_imp_*` — показы (impressions) активных/пассивных каналов по категориям (`ent`, `super`, `transport`, `shopping`, `hotel`, `avia`).
- `*_click_*` — клики/касания по тем же категориям.
- `orders_amt_*` — число заказов по категориям.
- `gender_cd`, `age`, `device_platform_cd` — демография и устройство.
---
## 0. Технический скелет проекта
Файлы/ноутбуки:
1. `01_load_and_clean.ipynb` — загрузка, чистка, базовые описания.
2. `02_univariate_bivariate.ipynb` — распределения и связи признаков.
3. `03_time_and_lags.ipynb` — время, лаги, сезонность.
4. `04_clients_segmentation.ipynb` — агрегаты по клиенту, сегменты.
5. `05_exploratory_models.ipynb` — простые модели как часть EDA.
6. `eda_report.md` / `eda_report.ipynb` — итоговый отчёт.
---
## 1. Загрузка и структура данных
### Таблицы/выводы
1. `df.info()` — список столбцов, типы, количество ненулевых.
2. `df.head(5)` — первые строки для визуальной проверки.
3. Размерность:
- `n_rows`, `n_cols`
- `n_unique_clients = df['id'].nunique()`
- диапазон дат: `min(business_dt)`, `max(business_dt)`
4. Проверка ключа:
- таблица: `df.groupby(['id', 'business_dt']).size().value_counts().head()`
(показывает, есть ли дубликаты по ключу)
5. Среднее число дней на клиента:
- `df.groupby('id').size().describe()`
### Графики
1. Количество записей по датам:
- `bar/line`: X = `business_dt`, Y = `count(*)`
- цель: увидеть провалы/пики выгрузки
---
## 2. Качество данных и аномалии
### Таблицы/метрики
1. Пропуски:
- таблица: колонка → количество пропусков → доля пропусков
2. Базовый `describe` по числовым:
- `df[num_cols].describe().T`
3. Доля нулей:
- таблица: колонка → доля нулей → min/max → 95-й, 99-й перцентили
4. Логические проверки:
- все `*_imp_*`, `*_click_*`, `orders_amt_*` должны быть `>= 0`
- поиск отрицательных/странных значений
5. Возраст:
- мин/макс, перцентили (1-й, 99-й), доля мусора (например, `<14` или `>100`)
6. Категориальные:
- уникальные значения `gender_cd`, `device_platform_cd`
- приведение к единому формату (trim, upper, `unknown`)
### Графики
1. Boxplot возраста:
- Y = `age`
- цель: выбросы и мусор
2. Barplot пропусков:
- X = столбец, Y = доля NaN (только где NaN > 0)
---
## 3. Одномерный анализ (univariate)
### 3.1. Числовые признаки (показы/клики/заказы)
#### Таблицы
1. Для каждой группы (`active_imp_*`, `passive_imp_*`, `active_click_*`, `passive_click_*`, `orders_amt_*`):
- `count, mean, median, std, min, q25, q75, max, share_zero, p95, p99`
2. Агрегаты по всем категориям:
- создать `active_imp_total`, `passive_imp_total`, `active_click_total`, `passive_click_total`, `orders_amt_total`
- таблица `describe()` для них
#### Графики
1. Гистограммы (лог-масштаб или `log1p`) для каждой категории и типа:
- `active_imp_ent`, `active_click_ent`, `passive_imp_ent`, `orders_amt_ent`, …
2. Boxplot для агрегатов:
- `active_imp_total`, `passive_imp_total`, `active_click_total`, `passive_click_total`, `orders_amt_total`
### 3.2. Категориальные признаки
#### Таблицы
1. Распределение `gender_cd`: counts, доли, `unknown`
2. Распределение `device_platform_cd`: counts, доли
3. Возрастные группы:
- `<25`, `2534`, `3544`, `4554`, `55+`
- таблица: группа → число клиентов → доля
#### Графики
1. Barplot пола: X = `M/F/Unknown`, Y = доля
2. Barplot платформ: X = platform, Y = доля
3. Гистограмма возраста
---
## 4. Время и сезонность
Создать дневные агрегаты:
- сумма показов/кликов/заказов по дням
- метрики:
- `CTR_active = active_click_total / active_imp_total`
- `CTR_passive = passive_click_total / passive_imp_total`
- `CR_click2order = orders_amt_total / (active_click_total + passive_click_total)`
- `CR_imp2order = orders_amt_total / (active_imp_total + passive_imp_total)`
- день недели: `day_of_week`
### Таблицы
1. `daily.describe()` по дневным агрегатам
2. Таблица по дням недели:
- `day_of_week` → среднее `impressions, clicks, orders, CTR, CR`
### Графики
1. Линейные временные ряды:
- `business_dt` vs total impressions
- `business_dt` vs total clicks
- `business_dt` vs total orders
2. Линии CTR/CR во времени (rolling mean 7 дней по желанию):
- `active_ctr`, `passive_ctr`, `cr_click2order`
3. Сезонность по дням недели:
- barplot для `active_ctr`, `passive_ctr`, `cr_click2order`
4. (Опционально) календарная heatmap заказов/CTR
---
## 5. Парные связи (bivariate)
### Таблицы
1. Корреляции Спирмена (на уровне клиента/дня):
- между всеми числовыми признаками + `age`
2. Для каждой категории:
- биннинг показов по квантилям → средний `imp, click, CTR, orders, CR`
### Графики
1. Scatter/hexbin «показы → клики»:
- `active_imp_*` vs `active_click_*`
- `passive_imp_*` vs `passive_click_*`
2. Scatter «клики → заказы»:
- `*_click_*` vs `orders_amt_*`
3. CTR по бинам показов (линия/бар)
4. CR по бинам кликов (линия/бар)
5. Heatmap корреляций
---
## 6. Демография и устройство vs эффективность
Агрегировать по клиенту:
- суммы показов/кликов/заказов
- CTR/CR на уровне клиента
- добавить `gender_cd`, `age_group`, `device_platform_cd`
### Таблицы
1. По полу:
- средние `impressions, clicks, orders, CTR, CR`
2. По возрастным группам:
- те же метрики
3. По платформам:
- те же метрики
4. Тесты гипотез (MannWhitney / t-test):
- разница CTR/CR между группами
### Графики
1. Barplot CTR/CR по полу
2. Barplot CTR/CR по возрастным группам
3. Barplot CTR/CR по платформам
4. Boxplot заказов по возрастным группам
5. Stacked bar: возраст → доли категорий заказов (наполнение корзины сервисами)
---
## 7. Поведение по клиенту и сегментация
### 7.1. Простые сегменты
Флаги на уровне клиента:
- `has_active_comm`, `has_passive_comm`
- `has_any_order`
- `order_categories_count` (в скольких категориях есть заказ)
#### Таблицы
1. Сегменты каналов:
- `only_active`, `only_passive`, `both`
- доля клиентов, средние заказы, CTR/CR
2. Сегменты мультикатегорийности:
- `1`, `2`, `3+` категорий заказов
- средние коммуникации/заказы, демография
#### Графики
1. Barplot по сегментам каналов:
- средние заказы, CTR/CR
2. Barplot по числу категорий заказов
3. Stacked bar: сегменты → пол/возраст (по желанию)
### 7.2. Кластеризация (расширенный EDA)
1. Вектор фичей:
- суммы по категориям + CTR/CR + доли заказов
2. Нормализация
3. KMeans / GMM, 37 кластеров
#### Таблицы
- кластер → размер → средние фичи → краткая интерпретация
#### Графики
1. Профили кластеров (bar/radar)
2. Scatter PCA/UMAP: цвет = кластер
---
## 8. Воронка: показы → клики → заказы
### Таблицы
1. Общая воронка:
- `channel_type`, `category`, `impressions`, `clicks`, `orders`, `CTR`, `CR_click2order`, `CR_imp2order`
2. Воронка по сегментам:
- пол/возраст/платформа → те же метрики
### Графики
1. Funnel chart active vs passive (общий)
2. Barplot CTR по категориям + сравнение active/passive
3. Barplot CR по категориям + сравнение active/passive
4. Funnel/Bar по возрастным группам
---
## 9. Временные лаги между коммуникациями и заказами
С учётом «поздних покупок» (особенно travel).
### 9.1. Лаги на дневном уровне
#### Таблицы
1. Лаговые признаки `lag1..lag7` для показов/кликов
2. Кросс-корреляция:
- lag → corr(orders*t, impressions*{t-lag})
#### Графики
1. Линия «lag vs корреляция» по:
- `hotel`, `avia` (и др. при желании)
- active vs passive
### 9.2. Лаги на клиентском уровне
#### Таблицы
1. `first_imp_date`, `first_click_date`, `first_order_date`
2. `days_to_order`
3. Квантили `days_to_order` по категориям
#### Графики
1. Гистограмма/ECDF `days_to_order` по категориям
---
## 10. Мультиканальность и «заспамленность»
### Таблицы
1. `contact_days`, `avg_impressions_per_contact_day`, `max_impressions_per_day`
2. Бины по `avg_impressions_per_contact_day` → средний CTR/CR
### Графики
1. Гистограмма `avg_impressions_per_contact_day`
2. Линия/бар: CTR/CR vs уровень спама
---
## 11. Простые модели как часть EDA
### 11.1. Бинарная модель «есть заказ / нет заказа»
Target:
- `has_any_order`
Features:
- суммы показов/кликов по типам и категориям
- CTR/CR
- демография и платформа
#### Таблицы
1. Логистическая регрессия:
- коэффы, p-value, odds ratio
2. Feature importance из дерева/лесов
#### Графики
1. Barplot важностей
2. (Опционально) partial dependence для 23 ключевых фичей
---
## 12. Гипотезы и статтесты
### Примеры гипотез
1. `CTR_active > CTR_passive`
2. CR различается между категориями сервисов
3. CTR/CR различаются по полу/возрасту/платформе
4. «заспамленность» снижает CTR/CR после порога
### Таблица гипотез
- гипотеза, H0/H1, тест, p-value, вывод, бизнес-интерпретация
Графики для поддержки — использовать из разделов 410 (барчики/боксплоты).
---
## 13. Итоговая документация
1. Резюме выводов:
- качество данных
- эффективность каналов/категорий
- сегменты, где коммуникации лучше/хуже работают
- лаги (как быстро покупают после контактов)
- признаки «усталости» от коммуникаций
2. Список проблем данных и принятых решений по чистке
3. Список инсайтов для бизнеса
4. Список фичей для будущих моделей
5. Следующие шаги:
- подготовка ML-пайплайна
- список A/B-гипотез
- какие данные добрать (если нужно)
---