← Назад к вопросам
Как строить воронку конверсии и какие метрики на каждом этапе важны?
1.3 Junior🔥 251 комментариев
#Метрики и KPI
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Построение воронки конверсии: архитектура и метрики
Воронка конверсии — основной инструмент для понимания пути пользователя и выявления узких мест. Покажу как я её строю и анализирую.
1. Архитектура воронки
Классический путь e-commerce:
Пользователь посетил сайт
↓ (Landing Page View)
100% (100k)
↓
Посмотрел каталог товаров
↓ (Category View)
75% (75k) ← потеря 25%
↓
Добавил в корзину
↓ (Add to Cart)
30% (30k) ← потеря 45% (!)
↓
Начал оформлять заказ
↓ (Checkout Started)
25% (25k) ← потеря 5%
↓
Добавил способ оплаты
↓ (Payment Added)
20% (20k) ← потеря 5% (!
↓
Завершил покупку
↓ (Order Completed)
18% (18k) ← потеря 2% ← узкое место!
2. SQL для построения воронки
-- Метод 1: Event-based (рекомендуемый)
-- Считаю количество пользователей на каждом этапе
WITH funnel_events AS (
SELECT
user_id,
session_id,
event_type,
created_at,
-- Определяю последовательность
ROW_NUMBER() OVER (
PARTITION BY user_id, session_id
ORDER BY created_at
) as event_sequence,
-- Флаги для каждого события
CASE WHEN event_type = 'page_view' THEN 1 ELSE 0 END as saw_page,
CASE WHEN event_type = 'category_view' THEN 1 ELSE 0 END as viewed_category,
CASE WHEN event_type = 'add_to_cart' THEN 1 ELSE 0 END as added_to_cart,
CASE WHEN event_type = 'checkout_started' THEN 1 ELSE 0 END as started_checkout,
CASE WHEN event_type = 'payment_added' THEN 1 ELSE 0 END as added_payment,
CASE WHEN event_type = 'order_completed' THEN 1 ELSE 0 END as completed_order
FROM events
WHERE created_at >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)
),
-- Определяю этап каждого пользователя (максимальный достигнутый)
user_stages AS (
SELECT
user_id,
session_id,
CASE
WHEN MAX(completed_order) = 1 THEN 'Order Completed'
WHEN MAX(added_payment) = 1 THEN 'Payment Added'
WHEN MAX(started_checkout) = 1 THEN 'Checkout Started'
WHEN MAX(added_to_cart) = 1 THEN 'Cart'
WHEN MAX(viewed_category) = 1 THEN 'Category'
WHEN MAX(saw_page) = 1 THEN 'Landing Page'
ELSE 'Unknown'
END as final_stage,
MAX(completed_order) = 1 as converted
FROM funnel_events
GROUP BY user_id, session_id
)
-- Финальный отчёт
SELECT
final_stage,
COUNT(DISTINCT user_id) as unique_users,
COUNT(*) as total_sessions,
ROUND(100.0 * COUNT(*) / SUM(COUNT(*)) OVER (), 2) as percentage_of_total,
SUM(CASE WHEN converted THEN 1 ELSE 0 END) as conversions,
ROUND(100.0 * SUM(CASE WHEN converted THEN 1 ELSE 0 END) / COUNT(*), 2) as conversion_rate
FROM user_stages
GROUP BY final_stage
ORDER BY COUNT(*) DESC;
Результат:
final_stage | unique_users | percentage | conversions | conversion_rate
Landing Page | 100,000 | 100% | 18,000 | 18%
Category | 75,000 | 75% | 18,000 | 24%
Cart | 30,000 | 30% | 18,000 | 60%
Checkout Started | 25,000 | 25% | 18,000 | 72%
Payment Added | 20,000 | 20% | 18,000 | 90%
Order Completed | 18,000 | 18% | 18,000 | 100%
3. Python для анализа воронки
import pandas as pd
import numpy as np
from scipy.stats import chi2_contingency
# Загрузил данные из SQL
df = pd.read_csv('funnel_data.csv')
class FunnelAnalysis:
"""Анализ воронки конверсии"""
def __init__(self, df, stages):
self.df = df
self.stages = stages # ['Landing', 'Category', 'Cart', 'Checkout', 'Payment', 'Order']
self.stage_counts = {}
def calculate_funnel(self):
"""Считаю количество пользователей на каждом этапе"""
for stage in self.stages:
count = len(self.df[self.df['final_stage'] == stage])
self.stage_counts[stage] = count
return self.stage_counts
def calculate_dropoff(self):
"""На каком этапе теряю больше всего?"""
stages_list = list(self.stage_counts.values())
dropoffs = {}
for i in range(len(stages_list) - 1):
current_stage = self.stages[i]
next_stage = self.stages[i + 1]
dropout_rate = 1 - (stages_list[i + 1] / stages_list[i])
dropoffs[f"{current_stage} → {next_stage}"] = {
'dropout_rate': dropout_rate,
'lost_users': stages_list[i] - stages_list[i + 1]
}
# Отсортирую по количеству потерянных пользователей
return sorted(dropoffs.items(),
key=lambda x: x[1]['lost_users'],
reverse=True)
def compare_segments(self, segment_col):
"""Сравниваю воронку по сегментам (мобила vs десктоп)"""
segments = self.df[segment_col].unique()
for segment in segments:
segment_df = self.df[self.df[segment_col] == segment]
total = len(segment_df)
converted = len(segment_df[segment_df['converted'] == True])
conv_rate = converted / total if total > 0 else 0
print(f"{segment}: {conv_rate:.1%} ({converted}/{total})")
def identify_bottleneck(self):
"""Находит самое узкое место"""
dropoffs = self.calculate_dropoff()
if dropoffs:
bottleneck, stats = dropoffs[0]
print(f"УЗКОЕ МЕСТО: {bottleneck}")
print(f"Теряю: {stats['lost_users']:,} пользователей")
print(f"Dropout rate: {stats['dropout_rate']:.1%}")
return bottleneck
return None
# Использую
funnel = FunnelAnalysis(df, ['Landing', 'Category', 'Cart', 'Checkout', 'Payment', 'Order'])
funnel.calculate_funnel()
print("\nСамое узкое место:")
funnel.identify_bottleneck()
print("\nДропауты по этапам:")
for stage, data in funnel.calculate_dropoff():
print(f"{stage}: {data['dropout_rate']:.1%}")
print("\nСравнение по устройствам:")
funnel.compare_segments('device_type')
4. Критические метрики на каждом этапе
Уровень 1: Landing Page → Category
# Метрика: Bounce Rate (отскок)
bounce_users = df[df['final_stage'] == 'Landing Page']
bounce_rate = len(bounce_users) / len(df[df['final_stage'].notna()]) * 100
print(f"Bounce Rate: {bounce_rate:.1f}%")
# Действие: Если > 50%, проблема с:
# - Page speed (проверить lighthouse)
# - Relevance (проверить трафик source)
# - UX (A/B test landing page)
Уровень 2: Category → Cart
# Метрика: Browse Depth (сколько товаров посмотрели)
products_viewed = df.groupby('user_id')['product_view_count'].max()
print(f"Среднее товаров просмотрено: {products_viewed.mean():.1f}")
print(f"Медиана: {products_viewed.median():.1f}")
# Действие: Если среднее < 3, то
# - Плохая фильтрация категории
# - Товары не привлекательны (фото, описание)
# - Нужно A/B тест рекомендаций
Уровень 3: Cart → Checkout (ГЛАВНОЕ УЗКОЕ МЕСТО)
# Метрика 1: Add-to-Cart Rate
added_to_cart = len(df[df['added_to_cart'] == True])
added_to_cart_rate = added_to_cart / len(df) * 100
print(f"Add-to-Cart Rate: {added_to_cart_rate:.1f}%")
# Метрика 2: Cart Abandonment Rate
cart_abandonment = len(df[(df['added_to_cart'] == True) &
(df['started_checkout'] == False)]) / added_to_cart * 100
print(f"Cart Abandonment: {cart_abandonment:.1f}%")
# Если > 70%, главные виновники (по моему опыту):
# 1. Скрытые затраты (доставка, налоги показаны только в checkout)
# 2. Нет интеграции с popular payment методами
# 3. Обязательна регистрация (нужен guest checkout)
# 4. Mobile оптимизация плохая
# Решение: A/B test
# Контроль: текущий процесс
# Вариант A: Явно показать все затраты на странице товара
# Вариант B: Добавить "Guest Checkout"
from scipy.stats import chi2_contingency
control_data = np.array([
[1000, 700], # added to cart, completed checkout
])
treatment_data = np.array([
[1100, 880], # added to cart, completed checkout (вариант B)
])
total = np.vstack([control_data, treatment_data])
chi2, p_val, dof, expected = chi2_contingency(total)
if p_val < 0.05:
improvement = (880/1100 - 700/1000) / (700/1000)
print(f"ВАРИАНТ B ЛУЧШЕ НА {improvement:.1%}")
Уровень 4: Checkout → Payment
# Метрика: Form Abandonment (бросают в процессе заполнения)
started_checkout = len(df[df['started_checkout'] == True])
finished_checkout = len(df[df['payment_method'] != None])
form_abandonment = (started_checkout - finished_checkout) / started_checkout * 100
print(f"Form Abandonment: {form_abandonment:.1f}%")
# Обычно причины:
# - Много полей для заполнения
# - Валидация не понятна
# - Security concerns (не видят SSL badge)
# - Mobile: keyboard скрывает форму
Уровень 5: Payment → Order (последняя линия защиты)
# Метрика: Payment Success Rate
payments_attempted = len(df[df['payment_added'] == True])
payments_successful = len(df[df['order_completed'] == True])
payment_success_rate = payments_successful / payments_attempted * 100
print(f"Payment Success Rate: {payment_success_rate:.1f}%")
# Если < 90%, анализирую отказы по причинам:
error_reasons = df[df['payment_failed'] == True].groupby('error_code').size()
print("\nОтказы по причинам:")
print(error_reasons.sort_values(ascending=False))
# Top причины:
# 1. 3D Secure (банк требует подтверждение) — 30%
# 2. Expired card — 15%
# 3. Insufficient funds — 20%
# 4. Fraud detection — 10%
# 5. Gateway timeout — 5%
# Решение: Retry logic, улучшить UX для 3DS
5. Визуализация воронки
import matplotlib.pyplot as plt
import plotly.graph_objects as go
# Интерактивная воронка (Plotly)
stages = ['Landing Page', 'Category', 'Cart', 'Checkout', 'Payment', 'Order']
values = [100000, 75000, 30000, 25000, 20000, 18000]
colors = ['#1f77b4', '#1f77b4', '#ff7f0e', '#ff7f0e', '#d62728', '#2ca02c']
fig = go.Figure(go.Funnel(
y=stages,
x=values,
textposition='inside',
textinfo='value+percent previous',
marker=dict(color=colors, line=dict(width=2))
))
fig.update_layout(
title="Conversion Funnel (Last 30 Days)",
height=600
)
fig.show()
6. Когда мониторю воронку
Ежедневно:
- General conversion rate (тренд)
- Top dropout stage
- Revenue per session
Еженедельно:
- Segment comparison (мобила vs десктоп, новые vs постоянные)
- Device-specific issues
- Traffic source quality
Ежемесячно:
- YoY тренды
- A/B тест результаты
- Cohort analysis (когда начали пользователи → когда конвертировались)
7. Вывод: Действие план
Когда вижу drop в воронке:
-
Количественный анализ
- Сколько теряю пользователей?
- В какой сегмент наибольший drop?
- Статистически значим ли drop?
-
Качественный анализ
- Спрашиваю пользователей (опрос, интервью)
- Session recording (Hotjar, LogRocket)
- Проверяю логи ошибок
-
Гипотеза
- Формулирую одну причину (не несколько!)
- Пример: "Пользователи не видят кнопку 'Add to Cart' на мобиле"
-
Решение + A/B тест
- Не гадаю, тестирую
- Минимум статистической мощности: 1000 пользователей на вариант
- Продолжительность: пока не достигну 95% confidence
-
Мониторинг
- Не развёртываю побеждённый вариант и не забываю
- Следю за метриками неделю после релиза
- Проверяю не сломалось ли что-то другое