Python数据分析实战:用Pandas+Matplotlib做一套完整的用户行为漏斗分析

0 阅读7分钟

前言

数据分析师最常被问到的一个问题:用户从注册到付款,中间流失了多少?在哪一步流失最多?

这就是漏斗分析。它不复杂,但如果代码写得一团糟,每次都要重新捋逻辑,就很低效。

这篇文章,我用一个真实的电商场景,带你从原始数据到完整漏斗分析报告,走一遍完整流程。用到的库只有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 | 数据分析 + 职场真相 + 投资洞察