基于用户行为数据的留存动因分析与预警策略研究
1.1 项目背景
在电商和订阅式商业模式中,用户留存率是衡量产品健康度和商业可持续性的核心指标。获取新用户的成本远高于维护老用户,因此,通过数据分析定位流失原因、预测流失风险并制定留存策略,是数据分析师的核心工作之一。
本项目旨在通过对用户行为、交易记录和画像数据的分析,回答以下业务问题:
哪些用户特征最能预示流失风险?
会员制度、客服体验、参与度等因素如何影响留存?
如何将数据洞察转化为可落地的运营策略?
1.2 数据来源
| 项目 | 说明 |
| 数据集名称 | E-commerce Customer Churn Dataset |
| 来源平台 | Kaggle |
| 数据性质 | 完全合成数据,但变量关系模拟真实电商模式 |
| 原始文件 | ecommerce_customer_features.csv(特征表)、ecommerce_customer_targets.csv(标签表) |
| 数据规模 | 6,000条用户记录,15个特征字段,1个目标变量 |
| 数据特点 | 包含缺失值(本版本无)、无重复值、业务逻辑清晰 |
合成数据的优势:逻辑清晰、结论可验证,适合作为分析项目练习;真实业务中需额外关注数据质量和脏数据处理。
第二部分:分析工具与流程
2.1 工具链
| 阶段 | 工具 | 主要任务 |
| 数据清洗与合并 | Python (Pandas, NumPy) | 读取、合并、清洗、类型转换、异常处理 |
| 探索性分析 | Python (Matplotlib, Seaborn) | 特征分布、流失对比、相关性分析 |
| 数据查询与分组分析 | SQL (DBeaver) | Cohort分析、分组对比、特征聚合 |
| 可视化与仪表盘 | Tableau | 交互式看板、趋势图 |
2.2 分析流程
原始数据 → 数据清洗 → 探索性分析(EDA) → SQL取数与分组分析 → 可视化看板 → 业务结论与建议
第三阶段:数据清洗与预处理(Python)
3.1 原始数据情况
两个文件通过
Customer_ID关联特征表:15列(用户行为 + 画像)
标签表:1列(churned:Yes/No)
3.2 清洗步骤
| 步骤 | 操作 | 代码/方法 |
| 1 | 读取两个CSV文件 | pd.read_csv() |
| 2 | 合并特征表与标签表 | pd.merge() |
| 3 | 检查缺失值 | df.isnull().sum() → 本版本无缺失 |
| 4 | 异常值处理 | 截尾1%-99%分位数 |
| 5 | 重复值处理 | drop_duplicates() → 无重复 |
| 6 | 转换目标变量 | churned: Yes→1, No→0 |
| 7 | 导出清洗后数据 | cleaned_ecommerce.csv |
3.3 清洗代码
import pandas as pd import numpy as np import os # ============================================ # 读取数据 # ============================================ features_path = r'D:\Users\qimiao\Desktop\数据分析\archive1\ecommerce_customer_features.csv' targets_path = r'D:\Users\qimiao\Desktop\数据分析\archive1\ecommerce_customer_targets.csv' features = pd.read_csv(features_path) targets = pd.read_csv(targets_path) print("=== 数据读取成功 ===") print(f"特征表: {features.shape}") print(f"标签表: {targets.shape}") print(f"\n特征表列名:\n{features.columns.tolist()}") print(f"\n标签表前5行:\n{targets.head()}") # ============================================ # 合并两个文件 # ============================================ df = features.merge(targets, on='Customer_ID', how='inner') print(f"\n=== 合并完成 ===") print(f"合并后形状: {df.shape}") # ============================================ # 检查缺失值 # ============================================ print("\n=== 缺失值检查 ===") missing = df.isnull().sum() missing_pct = missing / len(df) * 100 missing_df = pd.DataFrame({'缺失数量': missing, '缺失比例(%)': missing_pct}) print(missing_df[missing_df['缺失数量'] > 0]) # ============================================ # 处理缺失值 # ============================================ numeric_cols = df.select_dtypes(include=[np.number]).columns for col in numeric_cols: if df[col].isnull().sum() > 0: df[col].fillna(df[col].median(), inplace=True) print(f"已填充 {col} 的缺失值") categorical_cols = df.select_dtypes(include=['object']).columns for col in categorical_cols: if col != 'Customer_ID' and df[col].isnull().sum() > 0: mode_val = df[col].mode()[0] if len(df[col].mode()) > 0 else 'Unknown' df[col].fillna(mode_val, inplace=True) print(f"已填充 {col} 的缺失值") # ============================================ # 检查异常值 # ============================================ print("\n=== 异常值检查 ===") for col in numeric_cols: min_val = df[col].min() max_val = df[col].max() if col == 'discount_usage_rate' and max_val > 1: print(f"{col}: 最大值 {max_val} > 1,存在异常") if col == 'account_age_months' and max_val > 120: print(f"{col}: 最大值 {max_val} > 120") # 截尾处理(可选) for col in numeric_cols: q99 = df[col].quantile(0.99) q01 = df[col].quantile(0.01) df[col] = df[col].clip(lower=q01, upper=q99) print("已对数值列进行1%-99%截尾处理") # ============================================ # 转换目标变量 # ============================================ df['churned'] = df['churned'].map({'No': 0, 'Yes': 1}) print(f"\n=== 目标变量分布 ===") print(df['churned'].value_counts()) print(f"流失率: {df['churned'].mean():.2%}") # ============================================ # 删除重复值 # ============================================ initial_rows = len(df) df = df.drop_duplicates(subset=['Customer_ID']) print(f"\n删除重复行: {initial_rows - len(df)} 行") # ============================================ # 保存清洗后的数据(保存到你指定的桌面文件夹) # ============================================ output_path = r'D:\Users\qimiao\Desktop\数据分析\cleaned_ecommerce.csv' df.to_csv(output_path, index=False) print(f"\n清洗完成!已保存为: {output_path}") print(f"最终数据形状: {df.shape}") # ============================================ # 快速验证 # ============================================ print("\n=== 快速验证 ===") print(f"1. 剩余缺失值数量: {df.isnull().sum().sum()}") print(f"2. churned 唯一值: {df['churned'].unique()}") print(f"3. 数据前3行:\n{df.head(3)}")import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns # 设置中文显示(避免图表乱码) plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'Arial Unicode MS'] plt.rcParams['axes.unicode_minus'] = False # ============================================ # 读取清洗后的数据 # ============================================ df = pd.read_csv(r'D:\Users\qimiao\Desktop\数据分析\cleaned_ecommerce.csv') print("=== 数据加载成功 ===") print(f"数据形状: {df.shape}") print(f"列名: {df.columns.tolist()}\n") # ============================================ # 1. 目标变量分布(流失 vs 未流失) # ============================================ print("=== 1. 目标变量分布 ===") churn_counts = df['churned'].value_counts() churn_pct = df['churned'].value_counts(normalize=True) print(f"未流失(0): {churn_counts[0]} 人, 占比 {churn_pct[0]:.2%}") print(f"流失(1): {churn_counts[1]} 人, 占比 {churn_pct[1]:.2%}") # 可视化 fig, axes = plt.subplots(1, 2, figsize=(10, 4)) df['churned'].value_counts().plot(kind='bar', ax=axes[0], color=['green', 'red']) axes[0].set_title('流失用户分布') axes[0].set_xticklabels(['未流失', '流失'], rotation=0) axes[0].set_ylabel('用户数') df['churned'].value_counts(normalize=True).plot(kind='pie', ax=axes[1], autopct='%1.1f%%', colors=['green', 'red']) axes[1].set_title('流失用户占比') plt.tight_layout() plt.savefig(r'D:\Users\qimiao\Desktop\数据分析\1_churn_distribution.png', dpi=150) plt.show() # ============================================ # 2. 数值特征统计摘要 # ============================================ print("\n=== 2. 数值特征统计摘要 ===") numeric_cols = df.select_dtypes(include=[np.number]).columns numeric_cols = [col for col in numeric_cols if col != 'churned'] print(df[numeric_cols].describe()) # ============================================ # 3. 流失用户 vs 未流失用户 特征对比 # ============================================ print("\n=== 3. 流失 vs 未流失 特征均值对比 ===") compare_df = df.groupby('churned')[numeric_cols].mean().T compare_df.columns = ['未流失', '流失'] compare_df['差异'] = compare_df['流失'] - compare_df['未流失'] compare_df['差异百分比'] = (compare_df['差异'] / compare_df['未流失']) * 100 print(compare_df.sort_values('差异', ascending=False)) # 可视化:Top 5 差异最大的特征 fig, ax = plt.subplots(figsize=(12, 6)) top_features = compare_df['差异'].abs().sort_values(ascending=False).head(8).index diff_plot = compare_df.loc[top_features, ['未流失', '流失']] diff_plot.plot(kind='bar', ax=ax, color=['green', 'red']) ax.set_title('流失 vs 未流失 用户特征对比(差异最大的8个特征)') ax.set_ylabel('均值') ax.set_xlabel('特征') ax.legend(['未流失', '流失']) ax.tick_params(axis='x', rotation=45) plt.tight_layout() plt.savefig(r'D:\Users\qimiao\Desktop\数据分析\2_feature_comparison.png', dpi=150) plt.show() # ============================================ # 4. 相关性矩阵分析 # ============================================ print("\n=== 4. 特征与流失的相关性 ===") correlations = df[numeric_cols + ['churned']].corr()['churned'].sort_values(ascending=False) print(correlations) # 可视化热力图 plt.figure(figsize=(14, 10)) corr_matrix = df[numeric_cols + ['churned']].corr() mask = np.triu(np.ones_like(corr_matrix, dtype=bool)) sns.heatmap(corr_matrix, mask=mask, annot=True, fmt='.2f', cmap='RdBu_r', center=0, square=True, linewidths=0.5) plt.title('特征相关性热力图', fontsize=14) plt.tight_layout() plt.savefig(r'D:\Users\qimiao\Desktop\数据分析\3_correlation_heatmap.png', dpi=150) plt.show() # ============================================ # 5. 关键特征的分组分析 # ============================================ print("\n=== 5. 关键特征分析 ===") # 5.1 账号年龄 vs 流失 print("\n5.1 不同账号年龄段的流失率:") df['age_group'] = pd.cut(df['account_age_months'], bins=[0, 3, 6, 12, 24, 120], labels=['0-3月', '4-6月', '7-12月', '13-24月', '24月以上']) age_churn = df.groupby('age_group')['churned'].agg(['count', 'mean']) age_churn.columns = ['用户数', '流失率'] print(age_churn) # 5.2 会员 vs 非会员 print("\n5.2 会员与非会员的流失率:") loyalty_churn = df.groupby('loyalty_member')['churned'].agg(['count', 'mean']) loyalty_churn.columns = ['用户数', '流失率'] print(loyalty_churn) # 5.3 客服工单数 vs 流失 print("\n5.3 不同客服工单数的流失率:") ticket_churn = df.groupby('customer_support_tickets')['churned'].agg(['count', 'mean']) ticket_churn.columns = ['用户数', '流失率'] print(ticket_churn) # 可视化:年龄分组流失率 fig, axes = plt.subplots(1, 3, figsize=(15, 4)) # 年龄分组 age_churn['流失率'].plot(kind='bar', ax=axes[0], color='coral') axes[0].set_title('不同账号年龄段的流失率') axes[0].set_xlabel('账号年龄') axes[0].set_ylabel('流失率') axes[0].tick_params(axis='x', rotation=45) # 会员 vs 非会员 loyalty_churn['流失率'].plot(kind='bar', ax=axes[1], color=['green', 'red']) axes[1].set_title('会员 vs 非会员 流失率') axes[1].set_xlabel('是否会员') axes[1].set_ylabel('流失率') axes[1].set_xticklabels(['会员(Yes)', '非会员(No)'], rotation=0) # 客服工单数 ticket_churn['流失率'].plot(kind='bar', ax=axes[2], color='steelblue') axes[2].set_title('不同客服工单数的流失率') axes[2].set_xlabel('客服工单数') axes[2].set_ylabel('流失率') plt.tight_layout() plt.savefig(r'D:\Users\qimiao\Desktop\数据分析\4_key_features_analysis.png', dpi=150) plt.show() # ============================================ # 6. 流失用户的画像总结 # ============================================ print("\n=== 6. 流失用户画像总结 ===") churned_users = df[df['churned'] == 1] retained_users = df[df['churned'] == 0] print("流失用户的典型特征(与未流失用户对比):") for col in numeric_cols: churned_mean = churned_users[col].mean() retained_mean = retained_users[col].mean() diff_pct = (churned_mean - retained_mean) / retained_mean * 100 direction = "↑ 更高" if diff_pct > 0 else "↓ 更低" if abs(diff_pct) > 5: # 只显示差异超过5%的特征 print(f" {col}: {direction} ({diff_pct:+.1f}%)") # ============================================ # 保存分析结果 # ============================================ # 将对比结果保存为CSV compare_df.to_csv(r'D:\Users\qimiao\Desktop\数据分析\feature_comparison.csv') print("\n分析完成!图表和结果已保存到桌面数据分析文件夹")3.4 清洗后数据概览
| 指标 | 数值 |
| 总样本数 | 6,000 |
| 特征数量 | 14 |
| 流失用户(churned=1) | 929 |
| 未流失用户(churned=0) | 5,071 |
| 整体流失率 | 15.48% |
关键字段示例:'Customer_ID', 'account_age_months', 'avg_order_value', 'total_orders', 'days_since_last_purchase', 'discount_usage_rate', 'return_rate', 'customer_support_tickets', 'loyalty_member', 'browsing_frequency_per_week', 'cart_abandonment_rate', 'product_review_score_avg', 'engagement_score', 'satisfaction_score', 'price_sensitivity_index', 'churned'
清洗结论:数据质量良好,无缺失值、无重复行,可直接用于分析。
第四阶段:探索性数据分析(EDA,Python)
4.1 流失用户对比分析
通过对比流失组(churned=1)和留存组(churned=0)的特征均值,识别关键差异:
| 特征 | 流失组均值 | 留存组均值 | 差异 | 方向 |
| days_since_last_purchase | 79.5天 | 20.1天 | 2.96 | ↑ 显著更高 |
| engagement_score | 2.36 | 5.34 | -56% | ↓ 显著更低 |
| customer_support_tickets | 1.11 | 0.81 | 0.37 | ↑ 更高 |
| total_orders | 7.07 | 8.73 | -19% | ↓ 更低 |
| satisfaction_score | 7.7 | 8.15 | -5.50% | ↓ 略低 |
4.2 相关性分析
| 特征 | 与流失的相关性 | 说明 |
| days_since_last_purchase | 0.77 | 最强正相关 |
| engagement_score | -0.73 | 最强负相关 |
| customer_support_tickets | 0.11 | 弱正相关 |
| satisfaction_score | -0.13 | 弱负相关 |
EDA结论:参与度评分和距上次购买天数是预测流失的最强信号。
第五阶段:SQL分组分析与Cohort分析(DBeaver)
5.1 分析目标
在 DBeaver 中对清洗后的数据执行分组聚合,回答四个核心业务问题。
5.2 核心SQL查询与结果
查询1:整体流失率
SELECT COUNT(*) AS total_users, SUM(churned) AS churned_users, ROUND(CAST(SUM(churned) AS FLOAT) / COUNT(*) * 100, 2) AS churn_rate_pct FROM cleaned_ecommerce;| total_users | churned_users | churn_rate_pct |
| 6,000 | 929 | 15.48% |
查询2:会员 vs 非会员流失率
SELECT loyalty_member, COUNT(*) AS user_count, ROUND(AVG(churned) * 100, 2) AS churn_rate_pct FROM cleaned_ecommerce GROUP BY loyalty_member;| loyalty_member | user_count | churn_rate_pct |
| No | 4,915 | 16.91% |
| Yes | 1,085 | 9.03% |
会员流失率比非会员低约 7.9个百分点。
查询3:客服工单数 vs 流失率
SELECT customer_support_tickets, COUNT(*) AS user_count, ROUND(AVG(churned) * 100, 2) AS churn_rate_pct FROM cleaned_ecommerce GROUP BY customer_support_tickets ORDER BY customer_support_tickets;| tickets | user_count | churn_rate_pct |
| 0 | 2,660 | 11.99% |
| 1 | 2,076 | 16.52% |
| 2 | 850 | 18.12% |
| 3 | 308 | 24.68% |
| 4+ | 106 | 34.91% |
工单≥3次的用户,流失率是平均水平的 2倍以上。
查询4:账号年龄分组流失率(Cohort)
SELECT CASE WHEN account_age_months <= 3 THEN '0-3月' WHEN account_age_months <= 6 THEN '4-6月' WHEN account_age_months <= 12 THEN '7-12月' WHEN account_age_months <= 24 THEN '13-24月' ELSE '24月以上' END AS age_group, COUNT(*) AS user_count, ROUND(AVG(churned) * 100, 2) AS churn_rate_pct FROM cleaned_ecommerce GROUP BY age_group;| age_group | user_count | churn_rate_pct |
| 0-3月 | 303 | 15.18% |
| 4-6月 | 295 | 17.63% |
| 7-12月 | 567 | 17.81% |
| 13-24月 | 1,204 | 15.20% |
| 24月以上 | 3,631 | 15.06% |
流失最高峰出现在注册后4-12个月,而非新手期。
查询5:参与度评分 vs 流失率(最重要发现)
SELECT CASE WHEN engagement_score <= 2 THEN '低(0-2)' WHEN engagement_score <= 4 THEN '中低(2-4)' WHEN engagement_score <= 6 THEN '中(4-6)' WHEN engagement_score <= 8 THEN '中高(6-8)' ELSE '高(8-10)' END AS engagement_level, COUNT(*) AS user_count, ROUND(AVG(churned) * 100, 2) AS churn_rate_pct FROM cleaned_ecommerce GROUP BY engagement_level;| 参与度等级 | 用户数 | 流失率 |
| 低(0-2) | 365 | 99.73% |
| 中低(2-4) | 1,058 | 49.62% |
| 中(4-6) | 3,212 | 1.25% |
| 中高(6-8) | 1,365 | 0.00% |
参与度评分≤2的用户几乎必流失,≥6的用户几乎不流失。 这是整个项目中最具预测力的留存指标。
第六阶段:Tableau可视化看板
6.1 看板组成
| 工作表名称 | 图表类型 | 核心信息 |
| 参与度评分 vs 留存率 | 散点图/折线图 | 评分<4流失率飙升,≥6留存稳定 |
| 客服工单数 vs 留存率 | 柱状图/折线图 | 工单≥3,留存率下降15%+ |
| 会员 vs 非会员留存率 | 并排柱状图 | 会员留存率91% vs 非会员83% |
| 账号年龄分组留存率 | 柱状图 | 4-12个月是流失高峰期 |
6.2 看板截图
订阅用户留存与流失分析 | Tableau Public
项目延伸:A/B 测试方案
——验证“低参与度用户激活策略”对留存的影响
一、测试背景(基于项目核心发现)
本报告已发现:
参与度评分 < 4 的用户,留存率不足 50%;评分 < 2 的用户几乎必流失。
这说明:
低参与度用户是流失的高危人群
对这些用户进行早期干预,是提升留存的潜在杠杆
为此,设计如下 A/B 测试。
二、实验假设
原假设 H₀:对低参与度用户进行干预,不会显著提升留存率
备择假设 H₁:干预能显著提升 14 日留存率
三、实验对象与分组
| 项目 | 说明 |
|---|---|
| 实验对象 | 新注册用户,且前 7 天参与度评分 ≤ 4 |
| 分组方式 | 随机均匀分流(50% / 50%) |
| 对照组 | 不施加任何额外干预(常规流程) |
| 实验组 | 施加「激活干预策略」 |
| 实验周期 | 4 周(积累足够样本) |
| 预估样本量 | 每组 ≥ 500 人 |
四、实验组干预策略(可执行方案)
针对实验组用户在以下三个时间点,触发自动化干预:
| 时间点 | 动作 |
|---|---|
| 注册后第 3 天 | 个性化推送(如“发现你的兴趣好物”) |
| 注册后第 7 天 | 小额无门槛优惠券 + 浏览任务引导 |
| 注册后第 14 天 | 专属活动提醒 + 会员权益预告 |
所有动作均可通过现有运营系统自动化完成。
五、核心指标
| 指标类型 | 指标名称 | 计算方式 |
|---|---|---|
| 核心指标 | 14 日留存率 | 注册后第 14 天仍有活跃行为的用户占比 |
| 辅助指标 | 参与度评分变化 | 实验前后评分提升幅度 |
| 护栏指标 | 客单价 | 确保干预不损害消费能力 |
| 反向指标 | 优惠券滥用率 | 避免“薅羊毛”行为 |
六、实验结果判断标准
使用Z 检验 / 卡方检验,显著性水平 α = 0.05:
| 结果 | 结论 | 后续动作 |
|---|---|---|
| 实验组 14 日留存率显著更高(p < 0.05) | 策略有效 | 全量上线 |
| 无显著差异 | 策略无效 | 下线或重新设计干预方式 |
| 留存提升但客单价下降 | 存在副作用 | 优化优惠券门槛或任务设计 |
第七阶段:核心业务结论
基于以上分析,得出 4条核心结论:
结论1:参与度是留存的“前置预警指标”
参与度评分<4的用户,留存率低于50%;评分<2的用户,留存率接近0%。 建议:设定参与度<3.5为自动激活阈值(Push/优惠券/任务引导)。
结论2:会员身份对留存有独立正向影响
会员留存率91% vs 非会员83%,且在同龄段中差异依然显著。 建议:新用户注册流程中强化会员权益展示,对高价值非会员定向推送会员试用。
结论3:客服工单是“流失放大器”
工单≥3次的用户,留存率下降15–23个百分点。 建议:对高频投诉用户建立主动回访机制,产品侧减少用户求助场景。
结论4:流失最高峰在注册后4–12个月
该阶段留存率最低(82.2%),并非“新手期”。 建议:在第3/6/9个月设计系统化的留存干预动作。
第八部分:可落地业务建议汇总
| 优先级 | 建议 | 预期效果 | 责任方 |
| P0 | 参与度<3.5用户自动激活流程 | 挽回50%+中低参与用户 | 运营+产品 |
| P0 | 工单≥3次用户主动回访 | 降低流失率10–15% | 客服+运营 |
| P1 | 新注册用户会员引导强化 | 长期降低流失2–3% | 产品+运营 |
| P1 | 3/6/9个月留存干预活动 | 平滑4-12月流失高峰 | 运营+市场 |
第九部分:局限性与改进方向
9.1 当前局限
数据为合成数据,缺乏真实业务的“脏乱”特征
缺少时序数据,无法分析行为变化趋势
9.2 改进方向
对接真实业务数据,增加数据清洗复杂度
构造时序特征(如参与度下降速度、购买间隔变化趋势)
