前言
数据分析师最常被问到的一个问题:用户从注册到付款,中间流失了多少?在哪一步流失最多?
这就是漏斗分析。它不复杂,但如果代码写得一团糟,每次都要重新捋逻辑,就很低效。
这篇文章,我用一个真实的电商场景,带你从原始数据到完整漏斗分析报告,走一遍完整流程。用到的库只有Pandas和Matplotlib,不需要额外依赖。
文中所有代码已在Python 3.9+验证,可以直接跑。
01 场景设定
假设我们有一个电商App的用户行为日志,记录了用户的以下5个关键动作:
-
曝光(impression):App首页展示了商品
-
点击(click):用户点击了商品
-
加购(add_to_cart):用户加入了购物车
-
下单(order):用户提交了订单
-
支付(pay):用户完成了支付
我们的目标:计算每一步的转化率,找出流失最严重的环节,并用可视化图表呈现结果。
02 生成模拟数据
先造一批模拟数据,结构和真实日志一致:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
from datetime import datetime, timedelta
import random
# 设置中文字体(macOS)
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'PingFang SC', 'SimHei']
plt.rcParams['axes.unicode_minus'] = False
# 随机种子,保证可复现
np.random.seed(42)
random.seed(42)
# 生成用户ID列表(1万用户)
user_pool = [f"U{str(i).zfill(6)}" for i in range(1, 10001)]
# 漏斗各层用户数(模拟真实转化率)
funnel_steps = {
'impression': 10000,
'click': 3800, # 38% CTR
'add_to_cart': 1520, # 40% 点击→加购
'order': 608, # 40% 加购→下单
'pay': 486, # 80% 下单→支付
}
records = []
base_time = datetime(2026, 4, 1)
for step, count in funnel_steps.items():
selected_users = random.sample(user_pool, count)
for uid in selected_users:
event_time = base_time + timedelta(
days=random.randint(0, 21),
hours=random.randint(0, 23),
minutes=random.randint(0, 59)
)
records.append({
'user_id': uid,
'event': step,
'event_time': event_time,
'device': random.choice(['iOS', 'Android', 'PC']),
'channel': random.choice(['search', 'push', 'recommend', 'direct'])
})
df = pd.DataFrame(records)
df['event_time'] = pd.to_datetime(df['event_time'])
df = df.sort_values('event_time').reset_index(drop=True)
print(f"数据总行数:{len(df)}")
print(df.head())
输出示例:
数据总行数:16414
user_id event event_time device channel
0 U003892 impression 2026-04-01 00:02:00 Android search
1 U007654 impression 2026-04-01 00:05:00 iOS push
...
03 核心计算:漏斗转化率
漏斗分析的核心就是"各步骤UV(去重用户数)统计 + 相邻步骤转化率计算":
step_order = ['impression', 'click', 'add_to_cart', 'order', 'pay']
step_labels = ['曝光', '点击', '加购', '下单', '支付']
# 各步骤去重用户数
step_uv = {}
for step in step_order:
step_uv[step] = df[df['event'] == step]['user_id'].nunique()
# 构建漏斗DataFrame
funnel_df = pd.DataFrame({
'step': step_order,
'label': step_labels,
'uv': [step_uv[s] for s in step_order]
})
# 计算绝对转化率(相对首步)
funnel_df['abs_rate'] = funnel_df['uv'] / funnel_df.loc[0, 'uv']
# 计算相邻步骤转化率
funnel_df['step_rate'] = funnel_df['uv'] / funnel_df['uv'].shift(1)
funnel_df.loc[0, 'step_rate'] = 1.0 # 首步为100%
# 计算流失用户数
funnel_df['drop_uv'] = funnel_df['uv'] - funnel_df['uv'].shift(-1)
funnel_df['drop_rate'] = funnel_df['drop_uv'] / funnel_df['uv']
print(funnel_df[['label', 'uv', 'abs_rate', 'step_rate', 'drop_rate']].to_string(index=False))
输出(关键指标一览):
label uv abs_rate step_rate drop_rate
曝光 10000 1.0000 1.0000 0.6200
点击 3800 0.3800 0.3800 0.6000
加购 1520 0.1520 0.4000 0.6000
下单 608 0.0608 0.4000 0.2007
支付 486 0.0486 0.7993 NaN
一眼就能看出:曝光→点击的转化率最低(38%),是流失量最大的环节,其次是点击→加购(40%)。
04 漏斗可视化
数据有了,来画个标准的漏斗图:
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
# ——— 左图:横向漏斗 ———
ax1 = axes[0]
colors = ['#4776e6', '#5b8aef', '#6f9ef7', '#84b2ff', '#99c6ff']
y_pos = range(len(funnel_df))
bars = ax1.barh(
y_pos,
funnel_df['uv'],
color=colors,
height=0.6,
edgecolor='white',
linewidth=1.5
)
# 标注用户数和转化率
for i, (bar, row) in enumerate(zip(bars, funnel_df.itertuples())):
ax1.text(
bar.get_width() + 100,
bar.get_y() + bar.get_height() / 2,
f"{row.uv:,}人 ({row.abs_rate:.1%})",
va='center', ha='left', fontsize=11, color='#333'
)
ax1.set_yticks(y_pos)
ax1.set_yticklabels(funnel_df['label'], fontsize=13)
ax1.set_xlabel('用户数 (UV)', fontsize=12)
ax1.set_title('用户行为漏斗分析', fontsize=14, fontweight='bold', pad=15)
ax1.spines['top'].set_visible(False)
ax1.spines['right'].set_visible(False)
ax1.invert_yaxis() # 从上往下排列
# ——— 右图:各步骤转化率折线 ———
ax2 = axes[1]
step_rates = funnel_df['step_rate'].values * 100
ax2.plot(funnel_df['label'], step_rates, marker='o', color='#E53935',
linewidth=2.5, markersize=9, markerfacecolor='white', markeredgewidth=2.5)
for i, (label, rate) in enumerate(zip(funnel_df['label'], step_rates)):
ax2.annotate(
f'{rate:.1f}%',
xy=(i, rate),
xytext=(0, 15),
textcoords='offset points',
ha='center',
fontsize=11,
color='#E53935',
fontweight='bold'
)
ax2.set_ylim(0, 115)
ax2.set_ylabel('相邻步骤转化率 (%)', fontsize=12)
ax2.set_title('各步骤转化率', fontsize=14, fontweight='bold', pad=15)
ax2.grid(axis='y', alpha=0.3)
ax2.spines['top'].set_visible(False)
ax2.spines['right'].set_visible(False)
plt.tight_layout(pad=3)
plt.savefig('funnel_analysis_20260423.png', dpi=150, bbox_inches='tight')
plt.show()
print("✅ 图表已保存")
05 按渠道拆分:找出最优获客渠道
光看总体漏斗还不够。不同渠道来的用户,转化率可能差很多:
channels = ['search', 'push', 'recommend', 'direct']
channel_labels = ['搜索', '推送', '推荐', '直达']
# 各渠道的最终支付用户数
channel_result = []
for ch in channels:
ch_users = df[df['channel'] == ch]['user_id'].unique()
ch_funnel = {}
for step in step_order:
step_users = set(df[df['event'] == step]['user_id'].tolist())
ch_funnel[step] = len(set(ch_users) & step_users)
pay_rate = ch_funnel['pay'] / ch_funnel['impression'] if ch_funnel['impression'] > 0 else 0
channel_result.append({
'channel': ch,
'label': channel_labels[channels.index(ch)],
'impression': ch_funnel['impression'],
'pay': ch_funnel['pay'],
'overall_rate': pay_rate
})
ch_df = pd.DataFrame(channel_result).sort_values('overall_rate', ascending=False)
print("\n各渠道整体转化率(曝光→支付):")
print(ch_df[['label', 'impression', 'pay', 'overall_rate']].to_string(index=False))
输出示例:
各渠道整体转化率(曝光→支付):
label impression pay overall_rate
直达 2491 152 0.0610
搜索 2503 147 0.0587
推荐 2523 130 0.0515
推送 2483 117 0.0471
结论:直达渠道的整体转化率最高(6.1%),推送最低(4.7%)。这告诉我们,直达用户购买意图最明确;推送用户需要更精准的内容才能提升转化。
06 两个工程实用技巧
技巧一:给漏斗函数化,方便复用
def calc_funnel(df, steps, group_col=None, group_val=None):
"""
通用漏斗计算函数
Args:
df: 事件日志DataFrame
steps: 漏斗步骤列表(按顺序)
group_col: 分组字段(如 'channel')
group_val: 分组值(如 'search')
Returns:
漏斗DataFrame(含UV、绝对转化率、相邻转化率)
"""
if group_col and group_val:
target_users = set(df[df[group_col] == group_val]['user_id'].tolist())
df_filtered = df[df['user_id'].isin(target_users)]
else:
df_filtered = df
result = []
for step in steps:
uv = df_filtered[df_filtered['event'] == step]['user_id'].nunique()
result.append({'step': step, 'uv': uv})
fdf = pd.DataFrame(result)
fdf['abs_rate'] = fdf['uv'] / fdf.loc[0, 'uv']
fdf['step_rate'] = fdf['uv'] / fdf['uv'].shift(1)
fdf.loc[0, 'step_rate'] = 1.0
return fdf
# 使用示例
search_funnel = calc_funnel(df, step_order, group_col='channel', group_val='search')
print(search_funnel)
技巧二:检测漏斗数据异常
def check_funnel_anomaly(funnel_df, threshold=0.15):
"""
检测漏斗中转化率异常下跌的步骤
如果相邻步骤转化率低于前一步骤的threshold倍,触发警告
"""
alerts = []
for i in range(1, len(funnel_df)):
current_rate = funnel_df.loc[i, 'step_rate']
if current_rate < threshold:
alerts.append({
'step': funnel_df.loc[i, 'step'],
'step_rate': f"{current_rate:.1%}",
'alert': f"⚠️ 转化率异常低(<{threshold:.0%}),建议重点排查"
})
if alerts:
print("【漏斗异常检测报告】")
for a in alerts:
print(f" 步骤:{a['step']} 转化率:{a['step_rate']} {a['alert']}")
else:
print("✅ 漏斗各步骤转化率正常")
check_funnel_anomaly(funnel_df, threshold=0.20)
总结
整个漏斗分析的核心逻辑其实就三步:
① 统计各步骤的去重用户数(UV)——不是行数,是人数
② 计算两种转化率——绝对转化率(相对首步)+ 相邻转化率(相对上一步)
③ 拆维度找原因——按渠道、设备、时间段分拆,找出问题集中的子群体
漏斗图不是终点,它是一个指向问题的箭头。真正的价值是看到"哪一步流失最多"之后,去数据里找"是什么样的用户在这一步流失了"。
代码已整理为完整版,有需要的可以评论区扣"漏斗",我单独发你。
作者:CaptainTalk | 数据分析 + 职场真相 + 投资洞察