项目背景

上一篇文章我们分析了电商订单数据,得到了"是什么"的答案。但订单只是用户行为的终点。要真正理解用户,我们需要追踪和分析用户的完整行为路径:他们从哪来,在站内做了什么,为什么最终选择购买或离开。

本实战项目使用用户事件日志数据,涵盖页面浏览、点击、加购、支付等行为。我们将围绕以下几个核心问题展开分析:

  1. 用户的留存情况如何?不同渠道获取的用户留存有无差异?
  2. 从访看到购买的转化漏斗中,哪个环节流失最严重?
  3. 用户的日活跃度、使用深度如何?
  4. 如何用 AARRR 框架系统评估用户生命周期?
  5. 不同用户群体的行为模式有何差异?
  6. 用户的终身价值该如何估算?

数据加载与预处理

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from datetime import datetime, timedelta
import warnings
warnings.filterwarnings('ignore')

plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

# 事件日志数据
events = pd.read_csv('user_events.csv')
print(f'事件总数: {len(events):,}')
print(f'唯一用户数: {events["user_id"].nunique():,}')
events.head()

事件日志数据通常包含以下字段:

字段 含义 示例
event_id 事件编号 唯一标识
user_id 用户编号 匿名字段
event_type 事件类型 page_view, click, add_to_cart, purchase
page_url 页面地址 /products/123
timestamp 事件时间 2025-01-15 14:30:00
session_id 会话编号 区分不同访问会话
channel 来源渠道 organic, paid, referral, social
device 设备类型 mobile, desktop, tablet
# 时间字段处理
events['timestamp'] = pd.to_datetime(events['timestamp'])
events['date'] = events['timestamp'].dt.date
events['hour'] = events['timestamp'].dt.hour
events['weekday'] = events['timestamp'].dt.dayofweek

# 确保事件类型一致
print('事件类型分布:')
print(events['event_type'].value_counts())

同期群分析(Cohort Analysis)

同期群分析是用户分析最有力的工具之一。它追踪同一时间段内注册的用户群体,观察他们在后续时间窗口内的行为变化。

计算留存同期群

# 找到每个用户的首次活跃日期
first_active = events.groupby('user_id')['date'].min().reset_index()
first_active.columns = ['user_id', 'first_active_date']

# 合并到事件数据
events_with_cohort = events.merge(first_active, on='user_id')

# 计算每个事件距离首次活跃的天数
events_with_cohort['days_since_first'] = (
    pd.to_datetime(events_with_cohort['date']) - 
    pd.to_datetime(events_with_cohort['first_active_date'])
).dt.days

# 定义同期群月份
events_with_cohort['cohort_month'] = pd.to_datetime(
    events_with_cohort['first_active_date']
).dt.to_period('M')

# 定义期间月份(每个事件发生的月份)
events_with_cohort['period_month'] = pd.to_datetime(
    events_with_cohort['date']
).dt.to_period('M')

# 计算月间隔
events_with_cohort['month_offset'] = (
    events_with_cohort['period_month'] - events_with_cohort['cohort_month']
).apply(lambda x: x.n)

构建留存矩阵

# 每个同期群在每个月的活跃用户数
cohort_data = events_with_cohort.groupby(
    ['cohort_month', 'month_offset']
)['user_id'].nunique().reset_index()

# 每个同期群的初始用户数(month_offset=0)
cohort_sizes = cohort_data[cohort_data['month_offset'] == 0][
    ['cohort_month', 'user_id']
].rename(columns={'user_id': 'cohort_size'})

# 合并并计算留存率
cohort_data = cohort_data.merge(cohort_sizes, on='cohort_month')
cohort_data['retention_rate'] = cohort_data['user_id'] / cohort_data['cohort_size']

# 透视成矩阵表格
cohort_pivot = cohort_data.pivot_table(
    index='cohort_month',
    columns='month_offset',
    values='retention_rate',
    aggfunc='mean'
)

# 可视化留存矩阵
fig, ax = plt.subplots(figsize=(14, 8))

sns.heatmap(cohort_pivot, annot=True, fmt='.0%', cmap='YlOrRd',
            vmin=0, vmax=0.5, ax=ax, cbar_kws={'label': '留存率'})
ax.set_title('用户月留存同期群矩阵', fontsize=14)
ax.set_ylabel('注册月份(同期群)')
ax.set_xlabel('注册后第 N 个月')
plt.show()

解读留存矩阵

留存矩阵的每一行代表一个同期群,每一列代表该群在后续月份的留存率。观察要点:

  • 对角线方向:如果从左下到右上的颜色基本一致,说明不同同期群的留存模式相似
  • 第一列(第1个月):通常下降最明显,称为"新用户激活期"
  • 尾部稳定值:留存率最终趋于稳定的水平,这个值衡量产品的长期黏性

按渠道的留存对比

# 每个用户的来源渠道(取首次事件的渠道)
user_first_event = events.sort_values('timestamp').groupby('user_id').first().reset_index()
user_channel = user_first_event[['user_id', 'channel']]

events_with_channel = events_with_cohort.merge(user_channel, on='user_id', suffixes=('', '_first'))

# 按渠道计算留存
def cohort_retention_by_channel(df, channel_name):
    channel_users = df[df['channel_first'] == channel_name]
    cohort = channel_users.groupby(['cohort_month', 'month_offset'])['user_id'].nunique().reset_index()
    sizes = cohort[cohort['month_offset'] == 0][['cohort_month', 'user_id']].rename(
        columns={'user_id': 'size'})
    cohort = cohort.merge(sizes, on='cohort_month')
    cohort['retention'] = cohort['user_id'] / cohort['size']
    return cohort

channels = events_with_channel['channel_first'].unique()
fig, ax = plt.subplots(figsize=(12, 6))

for ch in channels:
    ch_data = cohort_retention_by_channel(events_with_channel, ch)
    avg = ch_data.groupby('month_offset')['retention'].mean()
    ax.plot(avg.index, avg.values, marker='o', label=ch)

ax.set_title('不同渠道用户留存对比')
ax.set_xlabel('注册后月数')
ax.set_ylabel('留存率')
ax.legend()
ax.grid(True, alpha=0.3)
plt.show()

漏斗分析

漏斗分析帮助我们找到转化链条上的薄弱环节。

# 定义漏斗步骤和顺序
funnel_steps = ['page_view', 'product_detail', 'add_to_cart', 'initiate_checkout', 'purchase']

# 每个用户在每个步骤的转化情况
funnel_data = []

for step in funnel_steps:
    step_users = events[events['event_type'] == step]['user_id'].nunique()
    funnel_data.append({'step': step, 'users': step_users})

funnel_df = pd.DataFrame(funnel_data)

# 计算各步骤转化率
funnel_df['overall_conversion'] = funnel_df['users'] / funnel_df['users'].max()
funnel_df['step_conversion'] = funnel_df['users'] / funnel_df['users'].shift(1)
funnel_df['step_conversion'] = funnel_df['step_conversion'].fillna(1)

# 步骤名称映射
step_labels = {
    'page_view': '访问页面',
    'product_detail': '查看商品详情',
    'add_to_cart': '加入购物车',
    'initiate_checkout': '开始结算',
    'purchase': '完成购买'
}
funnel_df['step_label'] = funnel_df['step'].map(step_labels)

print(funnel_df[['step_label', 'users', 'overall_conversion', 'step_conversion']])

# 可视化漏斗
fig, ax = plt.subplots(figsize=(10, 6))

# 漏斗图的锥形
max_width = 0.8
bar_colors = ['#2E86AB', '#A23B72', '#F18F01', '#55A868', '#C44E52']

for i, (_, row) in enumerate(funnel_df.iterrows()):
    width = row['overall_conversion'] * max_width
    left = (1 - width) / 2
    
    ax.barh(i, width, left=left, height=0.6, color=bar_colors[i], 
            edgecolor='white', linewidth=1.5)
    
    label = f"{row['step_label']}\n{row['users']:,} ({row['overall_conversion']:.1%})"
    ax.text(0.5, i, label, ha='center', va='center', fontsize=11, color='white', fontweight='bold')

ax.set_yticks([])
ax.set_title('用户转化漏斗', fontsize=14)
ax.set_xlim(0, 1)
plt.tight_layout()
plt.show()

流失点分析

从每一步到下一步的转化率揭示了流失最严重的环节。最常见的流失点包括:

  • 商品详情到加购:可能是价格问题、竞争对比、或产品信息不足
  • 加购到结算:可能是有无货提示、运费门槛、或犹豫决策
  • 结算到支付:可能是支付流程复杂、支付方式不足、或技术故障
# 按渠道拆解漏斗
for channel in events['channel'].unique():
    channel_users = events[events['channel'] == channel]
    print(f'\n=== {channel} 渠道漏斗 ===')
    for i, step in enumerate(funnel_steps):
        if i == 0:
            users = channel_users[channel_users['event_type'] == step]['user_id'].nunique()
            overall = 1.0
        else:
            step_users = channel_users[channel_users['event_type'] == step]['user_id'].nunique()
            prev_users = channel_users[channel_users['event_type'] == funnel_steps[i-1]]['user_id'].nunique()
            step_conv = step_users / prev_users if prev_users > 0 else 0
            overall = step_users / funnel_df.iloc[0]['users']
            print(f'  {step_labels[step]}: {step_users} 人 | 步骤转化率: {step_conv:.1%} | 总体转化率: {overall:.1%}')

用户活跃度与参与度

DAU/MAU 分析

DAU/MAU 比率是衡量用户参与度的黄金指标,通常称为"用户黏性"。比值越高,用户越频繁使用产品。

# 计算 DAU
dau = events.groupby('date')['user_id'].nunique().reset_index()
dau.columns = ['date', 'dau']

# 计算 MAU(滚动30天)
events['month'] = pd.to_datetime(events['date']).dt.to_period('M')
mau = events.groupby('month')['user_id'].nunique().reset_index()
mau.columns = ['month', 'mau']

# 计算周均 DAU
dau['weekday'] = pd.to_datetime(dau['date']).dt.dayofweek
weekly_dau = dau.groupby(pd.to_datetime(dau['date']).dt.isocalendar().week)['dau'].mean()

fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# DAU 趋势
axes[0, 0].plot(pd.to_datetime(dau['date']), dau['dau'], color='#2E86AB', linewidth=1)
axes[0, 0].set_title('每日活跃用户 (DAU)')
axes[0, 0].set_ylabel('DAU')

# 星期几的 DAU
dow_avg = dau.groupby('weekday')['dau'].mean()
dow_labels = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
axes[0, 1].bar(dow_labels, dow_avg, color='#55A868')
axes[0, 1].set_title('各星期平均 DAU')

# MAU 趋势
axes[1, 0].bar(mau['month'].astype(str), mau['mau'], color='#A23B72')
axes[1, 0].set_title('每月活跃用户 (MAU)')
axes[1, 0].tick_params(axis='x', rotation=45)

# DAU/MAU 比率
events['year_month'] = events['date'].apply(lambda x: str(x)[:7])
monthly_dau = dau.groupby(pd.to_datetime(dau['date']).dt.to_period('M'))['dau'].mean()
dau_mau = pd.DataFrame({
    'dau': monthly_dau,
    'mau': mau.set_index('month')['mau']
}).dropna()
dau_mau['stickiness'] = dau_mau['dau'] / dau_mau['mau']

axes[1, 1].plot(dau_mau.index.astype(str), dau_mau['stickiness'], 
                marker='o', color='#F18F01')
axes[1, 1].axhline(y=0.2, color='gray', linestyle='--', alpha=0.5, label='优秀线 (20%)')
axes[1, 1].set_title('DAU/MAU 黏性指标')
axes[1, 1].tick_params(axis='x', rotation=45)
axes[1, 1].set_ylabel('DAU/MAU')
axes[1, 1].legend()

plt.tight_layout()
plt.show()

print(f'平均 DAU/MAU: {dau_mau["stickiness"].mean():.2%}')

会话深度分析

# 每个会话的页面浏览数和时长
session_stats = events.groupby('session_id').agg({
    'event_id': 'count',
    'timestamp': lambda x: (x.max() - x.min()).total_seconds() if len(x) > 1 else 0
}).rename(columns={
    'event_id': 'pages_per_session',
    'timestamp': 'session_duration'
})

print('会话深度统计:')
print(session_stats.describe())

# 页面浏览量分布
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

session_stats['pages_per_session'].plot(kind='hist', bins=30, ax=axes[0],
                                         color='#2E86AB', edgecolor='white')
axes[0].set_title('每次会话页面浏览量分布')
axes[0].set_xlabel('页面数')

session_stats['session_duration'].plot(kind='hist', bins=30, ax=axes[1],
                                        color='#A23B72', edgecolor='white')
axes[1].set_title('会话时长分布(秒)')
axes[1].set_xlabel('时长(秒)')

plt.tight_layout()
plt.show()

AARRR 框架分析

AARRR(海盗指标)是用户分析的经典框架,覆盖用户完整生命周期。

# 设定分析时间窗口
analysis_start = events['date'].min()
analysis_end = events['date'].max()
print(f'分析周期: {analysis_start}{analysis_end}')

# Acquisition(获取)
acquisition = events.groupby('channel')['user_id'].nunique().sort_values(ascending=False)

# Activation(激活)- 定义为完成关键动作(如浏览3个以上页面)
activation = session_stats[session_stats['pages_per_session'] >= 3]
activated_users = events[events['session_id'].isin(activation.index)]['user_id'].nunique()
total_users = events['user_id'].nunique()
activation_rate = activated_users / total_users

# Retention(留存)- 第7天留存
retention_d7 = events_with_cohort[
    (events_with_cohort['days_since_first'] == 7)
]['user_id'].nunique()
retention_d7_rate = retention_d7 / first_active['user_id'].nunique()

# Revenue(收入)
revenue_data = events[events['event_type'] == 'purchase']
total_revenue = revenue_data['amount'].sum() if 'amount' in revenue_data.columns else 0
arpu = total_revenue / total_users

# Referral(推荐)- 有分享行为的用户比例
referral_users = events[events['event_type'] == 'share']['user_id'].nunique()
referral_rate = referral_users / total_users

print('\n===== AARRR 海盗指标 =====')
print(f'获取 (Acquisition): 共 {total_users:,} 用户')
print(f'各渠道:')
for ch, cnt in acquisition.items():
    print(f'  {ch}: {cnt:,} ({cnt/total_users:.1%})')
print(f'\n激活 (Activation): {activation_rate:.1%}')
print(f'留存 (Retention) 第7天: {retention_d7_rate:.1%}')
print(f'收入 (Revenue): ARPU={arpu:.2f}')
print(f'推荐 (Referral): {referral_rate:.1%}')

# AARRR 雷达图
categories = ['获取', '激活', '留存', '收入', '推荐']
values = [1.0, activation_rate, retention_d7_rate, 
          min(arpu / 100, 1), referral_rate]

fig, ax = plt.subplots(figsize=(8, 8), subplot_kw=dict(polar=True))
angles = [n / float(len(categories)) * 2 * np.pi for n in range(len(categories))]
angles += angles[:1]
values += values[:1]

ax.plot(angles, values, 'o-', linewidth=2, color='#2E86AB')
ax.fill(angles, values, alpha=0.25, color='#2E86AB')
ax.set_xticks(angles[:-1])
ax.set_xticklabels(categories)
ax.set_title('AARRR 海盗指标概览', fontsize=14)
plt.show()

用户行为分群

使用聚类方法对用户的行为模式进行分群。

from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans

# 构建用户行为特征
user_features = events.groupby('user_id').agg({
    'event_type': 'count',  # 总事件数
    'session_id': 'nunique',  # 总会话数
    'date': lambda x: (x.max() - x.min()).days,  # 活跃天数跨度
}).rename(columns={
    'event_type': 'total_events',
    'session_id': 'total_sessions',
    'date': 'active_span'
})

# 添加购买行为
purchase_users = events[events['event_type'] == 'purchase'].groupby('user_id').agg(
    purchase_count=('event_id', 'count')
)
user_features = user_features.merge(purchase_users, how='left', on='user_id')
user_features['purchase_count'] = user_features['purchase_count'].fillna(0)

# 添加每小时事件数(活跃时段偏好)
hourly = events.groupby(['user_id', 'hour']).size().unstack(fill_value=0)
for h in range(24):
    col = f'active_hour_{h}'
    user_features[col] = hourly.get(h, pd.Series(0, index=user_features.index))

# 标准化
features_scaled = StandardScaler().fit_transform(user_features)

# 聚类
kmeans = KMeans(n_clusters=4, random_state=42, n_init=10)
user_features['cluster'] = kmeans.fit_predict(features_scaled)

# 分析各群体特征
cluster_profile = user_features.groupby('cluster').agg({
    'total_events': 'mean',
    'total_sessions': 'mean',
    'active_span': 'mean',
    'purchase_count': 'mean'
}).round(2)

print('用户行为分群特征:')
print(cluster_profile)

# 各分群大小
cluster_sizes = user_features['cluster'].value_counts().sort_index()
labels_map = {0: '轻度浏览型', 1: '活跃购买型', 2: '潜在观望型', 3: '重度忠诚型'}
for i, size in cluster_sizes.items():
    print(f'{labels_map[i]}: {size} 人 ({size/len(user_features):.1%})')

用户终身价值(LTV)估算

LTV 用于预测一个用户在其整个生命周期内能为业务贡献多少价值。

# 简单 LTV 估算方法
# LTV = 平均订单金额 × 购买频率 × 预期生命周期

# 计算每个用户的购买数据
user_ltv = events[events['event_type'] == 'purchase'].groupby('user_id').agg({
    'amount': ['sum', 'mean', 'count']
})
user_ltv.columns = ['total_revenue', 'avg_order_value', 'order_count']

# 计算用户的活跃天数
user_days = events.groupby('user_id')['date'].nunique()
user_ltv['active_days'] = user_days

# 历史 LTV
user_ltv['historical_ltv'] = user_ltv['total_revenue']

# 简单预测 LTV(假设未来半年保持当前消费速率)
avg_monthly_spend = user_ltv['total_revenue'] / user_ltv['active_days'] * 30
user_ltv['predicted_ltv_6m'] = user_ltv['total_revenue'] + avg_monthly_spend * 6

print('LTV 统计:')
print(user_ltv[['historical_ltv', 'predicted_ltv_6m']].describe())

# LTV 分布
fig, ax = plt.subplots(figsize=(10, 5))
user_ltv['historical_ltv'].clip(upper=user_ltv['historical_ltv'].quantile(0.95)).hist(
    bins=40, ax=ax, color='#C44E52', edgecolor='white')
ax.set_title('用户终身价值分布(截尾 95%)')
ax.set_xlabel('LTV')
ax.set_ylabel('用户数')
plt.show()

LTV 分层运营策略

LTV 分位 特征 运营策略
高(前20%) 高频购买,高客单价 VIP 服务,个推优先,推荐同品类高客单价商品
中(20%-60%) 偶尔购买,中等客单价 提高复购率,交叉销售相关品类
低(后40%) 极少购买或仅浏览 激活策略,首单优惠,个性化推荐引流

关键发现总结

完成以上分析后,我们可以给出以下总结性的洞察:

1. 留存洞察

  • 首月留存是产品健康度的核心指标
  • 自然搜索渠道的用户留存通常高于付费渠道
  • 留存曲线在 3-6 个月趋于稳定,这个稳定值代表产品核心价值

2. 漏斗优化机会

  • 最大的转化损耗通常发生在"商品详情到加购"环节,说明产品信息和定价是关键
  • 移动端的结算转化率可能低于桌面端,需优化移动支付体验
  • 不同渠道的漏斗形态差异显著,需要针对性优化

3. 用户分层建议

  • 重度忠诚用户(10-20%)贡献了 50-80% 的收入,值得最高级别的运营投入
  • 潜在观望用户需要通过个性化推荐和限时优惠来激活
  • 轻度浏览用户适合自动化培育流程,逐步建立信任

4. 数据驱动决策

  • 建立用户行为指标体系,实时监控 DAU/MAU、核心转化率等指标
  • 每周生成 AARRR 看板,跟踪各环节变化趋势
  • 用 A/B 测试验证运营策略的有效性

小结

用户行为分析比单纯的订单分析更深入。它让我们看到用户从进入产品到离开的完整旅程,找到产品体验的痛点和增长机会。

本篇文章涉及的核心技能:

  • 同期群分析(Cohort Analysis)是理解用户留存的必备工具
  • 漏斗分析帮助定位转化瓶颈
  • DAU/MAU 等指标量化用户参与度
  • AARRR 框架系统化用户生命周期管理
  • 行为分群和 LTV 估算指导精细化运营

在下一篇文章中,我们将切换视角,学习如何构建自动化的数据管道,让分析工作更加高效和可持续。