日式极简服饰复购率分析程序,对比简约无Logo服饰与印花潮款长期留存数据。
构建一个基于生存分析与用户分群的复购率评估系统。
这个程序的核心逻辑是:利用Kaplan-Meier 生存分析来对比两种截然不同的时尚产品哲学——"极简无 Logo"与"印花潮款"——在用户生命周期中的留存表现。
实际应用场景描述
在《时尚产业与品牌创新》课程中,我们探讨了两种极端的品牌策略:
* 策略 A:日式极简(无 Logo)
* 哲学:侘寂(Wabi-sabi)、留白、去品牌化。
* 代表:Muji、Uniqlo U、The Row。
* 痛点:没有显眼的 Logo,用户为什么要"回来买"?是靠"衣橱基础件替换"驱动,还是靠"美学信仰"驱动?
* 策略 B:印花潮款(Graphic/Pattern)
* 哲学:视觉冲击、社交货币、话题驱动。
* 代表:Palace、Ggalleria、国潮印花。
* 痛点:社交媒体的"快时尚"逻辑。用户是不是"用完即弃"?复购是不是完全依赖"下一波新图案"?
品牌面临的灵魂拷问:
"我该坚持做高毛利的印花爆款(赌下一波趋势),还是转型做低毛利但高复购的基础极简款(赌用户终身价值 LTV)?"
引入痛点
1. 数据维度单一:大多数品牌只看"整体复购率"(Overall Retention Rate),这不严谨。极简款可能第 1 个月卖得差,但第 12 个月用户还在买(高 LTV);潮款可能首发爆了,但 3 个月后没人看了(低 LTV)。
2. 幸存者偏差:只看"买了两次以上的人"的复购率,忽略了"买一次就消失的人",导致高估了产品吸引力。
3. 缺乏对比框架:没有把"时间"作为一个核心变量纳入分析。你需要知道的是用户流失的速度,而不是一个静态的百分比。
核心逻辑讲解
我们将使用 Kaplan-Meier 生存分析(通常用于医疗临床试验,看病人存活率,这里我们看"用户存活率")。
1. 定义"死亡":在时尚语境下,"死亡" = 用户最后一次购买后,超过了"平均复购周期"仍未再次购买。
2. 定义"生存":用户虽然在观察期内没有再次购买,但还在"复购窗口期"内(比如平均 60 天买一次,30 天时他还没买,但他还没"死")。
3. 对比曲线:
* 极简组曲线:可能下降缓慢(用户买基础款是习惯,流失慢)。
* 潮款组曲线:可能前期骤降(用户追新不追旧,一旦热度过气,留存崩盘)。
4. 统计检验(Log-Rank Test):用一个 P 值来科学回答:"这两类产品的复购差异,是偶然的,还是本质上的?"
代码模块化
1.
"data_generator.py" (数据构建模块)
import numpy as np
import pandas as pd
class UserBehaviorGenerator:
"""
模拟用户购买行为数据生成器
基于两种产品哲学的差异构建合成数据集
"""
def __init__(self, seed=42):
np.random.seed(seed)
def generate_cohort(self, n_users=5000, product_type='minimalist'):
"""
生成用户队列数据
:param n_users: 用户数量
:param product_type: 'minimalist'(极简) or 'graphic'(潮款)
:return: DataFrame
"""
users = []
for uid in range(n_users):
if product_type == 'minimalist':
# --- 极简无 Logo 特征 ---
# 1. 购买频率较高 (复购周期短)
# 2. 客单价中等偏上 (基础款溢价)
# 3. 生命周期长 (不追潮流,看中质感)
purchase_cycle = max(7, int(np.random.normal(45, 15))) # 平均45天买一次
ltv_months = max(6, int(np.random.normal(18, 6))) # 平均活18个月
avg_spend = np.random.normal(350, 80) # 客单价 350 左右
churn_rate = 0.03 # 月流失率低
else:
# --- 印花潮款特征 ---
# 1. 购买频率前期高,但衰减极快
# 2. 客单价低 (冲动消费)
# 3. 生命周期短 (下一波热度来了就跑了)
purchase_cycle = max(5, int(np.random.normal(25, 10))) # 首发冲动强
ltv_months = max(2, int(np.random.exponential(6))) # 指数衰减,平均寿命短
avg_spend = np.random.normal(180, 50) # 客单价低
churn_rate = 0.15 # 月流失率高
# 生成购买记录
current_month = 0
total_spent = 0
purchase_count = 0
is_churned = False
while current_month < ltv_months:
# 模拟流失
if np.random.random() < churn_rate and purchase_count > 0:
is_churned = True
break
# 模拟购买
current_month += purchase_cycle
if current_month >= ltv_months:
break
# 花费波动
actual_spend = max(50, avg_spend * np.random.normal(1.0, 0.2))
total_spent += actual_spend
purchase_count += 1
users.append({
'user_id': f"{product_type[:3]}_{uid}",
'product_type': product_type,
'purchase_count': purchase_count,
'total_spent': round(total_spent, 2),
'ltv_months': ltv_months,
'is_churned': is_churned,
'avg_spend_per_purchase': round(total_spent / max(1, purchase_count), 2)
})
return pd.DataFrame(users)
2.
"retention_analyzer.py" (核心分析引擎)
import numpy as np
import pandas as pd
from scipy import stats
class RetentionAnalyzer:
"""
复购率与留存分析引擎
实现 Kaplan-Meier 生存分析与统计检验
"""
def __init__(self, df_minimalist, df_graphic):
self.df_m = df_minimalist
self.df_g = df_graphic
self.combined = pd.concat([df_minimalist, df_graphic], ignore_index=True)
def kaplan_meier_estimate(self, group_label='minimalist', max_periods=24):
"""
计算 Kaplan-Meier 生存曲线数据
返回每个时间点的"存活概率"
"""
if group_label == 'minimalist':
df = self.df_m
else:
df = self.df_g
# 按生命周期(月份)统计
# 在每个月份 t,计算"活过 t 个月"的概率
survival_data = []
for t in range(1, max_periods + 1):
# 在 t 时刻"仍存活"的用户数
# 逻辑:用户的 ltv_months >= t 意味着他们在 t 月时还没流失
at_risk = len(df[df['ltv_months'] >= t])
# 在 t 时刻"死亡"的用户数
# 逻辑:刚好在 t 月流失(简化处理:ltv 刚好等于 t)
events = len(df[df['ltv_months'] == t])
# 删失数据(还在观察期内,但未发生事件)
censored = len(df[(df['ltv_months'] > t) & (df['is_churned'] == False)])
survival_data.append({
'period': t,
'at_risk': at_risk,
'events': events,
'censored': censored,
'survival_prob': None # 待计算
})
# 计算生存概率
s_t = 1.0
for i, row in enumerate(survival_data):
if row['at_risk'] > 0:
s_t *= (1 - row['events'] / row['at_risk'])
row['survival_prob'] = round(s_t, 4)
return pd.DataFrame(survival_data)
def compare_retention(self, periods=12):
"""
对比两种产品在前 N 个月的留存率
输出详细的对比表格
"""
# 极简组
m_retention = []
for p in range(1, periods + 1):
alive = len(self.df_m[self.df_m['ltv_months'] >= p])
total = len(self.df_m)
m_retention.append(round(alive / total * 100, 2))
# 潮款组
g_retention = []
for p in range(1, periods + 1):
alive = len(self.df_g[self.df_g['ltv_months'] >= p])
total = len(self.df_g)
g_retention.append(round(alive / total * 100, 2))
comparison = pd.DataFrame({
'月份': range(1, periods + 1),
'极简无Logo留存率(%)': m_retention,
'印花潮款留存率(%)': g_retention,
})
comparison['差值(百分点)'] = (comparison['极简无Logo留存率(%)'] -
comparison['印花潮款留存率(%)']).round(2)
return comparison
def log_rank_test(self):
"""
Log-Rank 检验
检验两组生存曲线的差异是否具有统计显著性
H0: 两组生存曲线无差异
"""
# 简化版的 Log-Rank 检验
# 合并两组数据的时间线
all_times = []
for _, row in self.df_m.iterrows():
all_times.append((row['ltv_months'], 1, 'minimalist'))
for _, row in self.df_g.iterrows():
all_times.append((row['ltv_months'], 1, 'graphic'))
df_all = pd.DataFrame(all_times, columns=['time', 'event', 'group'])
df_all = df_all.sort_values('time').reset_index(drop=True)
# 计算期望事件数 (简化计算)
m_total = len(self.df_m)
g_total = len(self.df_g)
n_total = m_total + g_total
# 在每个时间点,计算观察值 vs 期望值
# 这里用简化的 Mantel-Haenszel 统计量
chi_square = 0
details = []
for t in sorted(df_all['time'].unique()):
at_risk_m = len(df_all[(df_all['time'] >= t) & (df_all['group'] == 'minimalist')])
at_risk_g = len(df_all[(df_all['time'] >= t) & (df_all['group'] == 'graphic')])
events_m = len(df_all[(df_all['time'] == t) & (df_all['group'] == 'minimalist') & (df_all['event'] == 1)])
events_g = len(df_all[(df_all['time'] == t) & (df_all['group'] == 'graphic') & (df_all['event'] == 1)])
total_at_risk = at_risk_m + at_risk_g
total_events = events_m + events_g
if total_at_risk > 0 and total_events > 0:
expected_m = total_events * (at_risk_m / total_at_risk)
expected_g = total_events * (at_risk_g / total_at_risk)
if expected_m > 0:
chi_square += ((events_m - expected_m) ** 2) / expected_m
if expected_g > 0:
chi_square += ((events_g - expected_g) ** 2) / expected_g
details.append({
'time': t,
'at_risk_m': at_risk_m,
'at_risk_g': at_risk_g,
'events_m': events_m,
'events_g': events_g,
'expected_m': round(expected_m, 1),
'expected_g': round(expected_g, 1),
})
# 自由度 = 1 的卡方分布
p_value = 1 - stats.chi2.cdf(chi_square, df=1)
return {
'chi_square': round(chi_square, 4),
'p_value': round(p_value, 6),
'is_significant': p_value < 0.05,
'details': pd.DataFrame(details)
}
def ltv_analysis(self):
"""计算并对比两组的用户终身价值(LTV)"""
m_avg_ltv = self.df_m['total_spent'].mean()
g_avg_ltv = self.df_g['total_spent'].mean()
m_avg_months = self.df_m['ltv_months'].mean()
g_avg_months = self.df_g['ltv_months'].mean()
m_avg_spend = self.df_m['avg_spend_per_purchase'].mean()
g_avg_spend = self.df_g['avg_spend_per_purchase'].mean()
m_avg_purchases = self.df_m['purchase_count'].mean()
g_avg_purchases = self.df_g['purchase_count'].mean()
return {
'minimalist': {
'avg_ltv': round(m_avg_ltv, 2),
'avg_lifetime_months': round(m_avg_months, 1),
'avg_spend_per_purchase': round(m_avg_spend, 2),
'avg_purchase_count': round(m_avg_purchases, 1),
},
'graphic': {
'avg_ltv': round(g_avg_ltv, 2),
'avg_lifetime_months': round(g_avg_months, 1),
'avg_spend_per_purchase': round(g_avg_spend, 2),
'avg_purchase_count': round(g_avg_purchases, 1),
}
}
3.
"visualizer.py" (可视化仪表盘)
import matplotlib.pyplot as plt
import numpy as np
class RetentionDashboard:
"""复购率分析可视化仪表盘"""
COLORS = {
'minimalist': '#2C3E50', # 极简黑灰
'graphic': '#E74C3C', # 潮款红
}
@classmethod
def plot_survival_curves(cls, analyzer, filename='survival_curves.png'):
"""绘制 Kaplan-Meier 生存曲线对比"""
fig, ax = plt.subplots(figsize=(12, 7))
# 极简组
km_m = analyzer.kaplan_meier_estimate('minimalist')
ax.plot(km_m['period'], km_m['survival_prob'] * 100,
color=cls.COLORS['minimalist'], linewidth=2.5, label='日式极简(无 Logo)',
marker='o', markersize=4)
# 潮款组
km_g = analyzer.kaplan_meier_estimate('graphic')
ax.plot(km_g['period'], km_g['survival_prob'] * 100,
color=cls.COLORS['graphic'], linewidth=2.5, label='印花潮款',
marker='s', markersize=4)
# 标注关键时间点
ax.axhline(y=50, color='gray', linestyle=':', alpha=0.5, label='50% 生存线')
# 找到中位数生存时间
m_median = km_m[km_m['survival_prob'] <= 0.5].iloc[0]['period'] if len(km_m[km_m['survival_prob'] <= 0.5]) > 0 else 24
g_median = km_g[km_g['survival_prob'] <= 0.5].iloc[0]['period'] if len(km_g[km_g['survival_prob'] <= 0.5]) > 0 else 24
ax.annotate(f'极简中位数: {m_median}月', xy=(m_median, 50),
xytext=(m_median + 1, 60), fontsize=9, color=cls.COLORS['minimalist'],
fontweight='bold', arrowprops=dict(arrowstyle='->'))
ax.annotate(f'潮款中位数: {g_median}月', xy=(g_median, 50),
xytext=(g_median - 3, 40), fontsize=9, color=cls.COLORS['graphic'],
fontweight='bold', arrowprops=dict(arrowstyle='->'))
ax.set_xlabel('时间(月)', fontsize=12)
ax.set_ylabel('用户存活率(%)', fontsize=12)
ax.set_title('用户生命周期对比:日式极简 vs 印花潮款', fontsize=14, fontweight='bold')
ax.legend(fontsize=10, loc='upper right')
ax.grid(True, alpha=0.3)
ax.set_xlim(0, 24)
ax.set_ylim(0, 105)
plt.tight_layout()
plt.savefig(filename, dpi=120)
plt.show()
print(f"[图表] 生存曲线已保存: {filename}")
@classmethod
def plot_retention_comparison(cls, comparison_df, filename='retention_comparison.png'):
"""绘制留存率对比柱状图"""
fig, ax = plt.subplots(figsize=(12, 6))
x = comparison_df['月份']
width = 0.35
ax.bar(x - width/2, comparison_df['极简无Logo留存率(%)'], width,
label='日式极简(无 Logo)', color=cls.COLORS['minimalist'], alpha=0.8)
ax.bar(x + width/2, comparison_df['印花潮款留存率(%)'], width,
label='印花潮款', color=cls.COLORS['graphic'], alpha=0.8)
ax.set_xlabel('时间(月)', fontsize=12)
ax.set_ylabel('留存率(%)', fontsize=12)
ax.set_title('月度留存率对比', fontsize=14, fontweight='bold')
ax.legend(fontsize=10)
ax.grid(axis='y', alpha=0.3)
ax.set_xlim(0.5, 13)
plt.tight_layout()
plt.savefig(filename, dpi=120)
plt.show()
print(f"[图表] 留存对比已保存: {filename}")
@classmethod
def plot_ltv_comparison(cls, ltv_data, filename='ltv_comparison.png'):
"""绘制 LTV 对比雷达图"""
fig, ax = plt.subplots(figsize=(8, 8), subplot_kw=dict(polar=True))
categories = ['平均 LTV', '生命周期(月)', '单次消费(元)', '购买频次']
N = len(categories)
angles = [n / float(N) * 2 * np.pi for n in range(N)]
angles += angles[:1]
# 极简组
m_vals = [
ltv_data['minimalist']['avg_ltv'] / 1000, # 归一化
ltv_data['minimalist']['avg_lifetime_months'] / 24,
ltv_data['minimalist']['avg_spend_per_purchase'] / 500,
ltv_data['minimalist']['avg_purchase_count'] / 10
]
m_vals += m_vals[:1]
ax.plot(angles, m_vals, 'o-', linewidth=2, color=cls.COLORS['minimalist'],
label='日式极简(无 Logo)')
ax.fill(angles, m_vals, alpha=0.15, color=cls.COLORS['minimalist'])
# 潮款组
g_vals = [
ltv_data['graphic']['avg_ltv'] / 1000,
ltv_data['graphic']['avg_lifetime_months'] / 24,
ltv_data['graphic']['avg_spend_per_purchase'] / 500,
ltv_data['graphic']['avg_purchase_count'] / 10
]
g_vals += g_vals[:1]
ax.plot(angles, g_vals, 's-', linewidth=2, color=cls.COLORS['graphic'],
label='印花潮款')
ax.fill(angles, g_vals, alpha=0.15, color=cls.COLORS['graphic'])
ax.set_xticks(angles[:-1])
ax.set_xticklabels(categories, fontsize=10)
ax.set_title('用户价值画像对比', fontsize=14, fontweight='bold', pad=20)
ax.legend(fontsize=9, loc='upper right', bbox_to_anchor=(1.25, 1.1))
plt.tight_layout()
plt.savefig(filename, dpi=120)
plt.show()
print(f"[图表] LTV 对比已保存: {filename}")
4.
"main.py" (主程序入口)
from data_generator import UserBehaviorGenerator
from retention_analyzer import RetentionAnalyzer
from visualizer import RetentionDashboard
def run_analysis():
"""端到端复购率分析流程"""
print("=" * 70)
print(" 日式极简 vs 印花潮款:复购率与用户留存对比分析")
print("=" * 70)
# Step 1: 生成模拟用户数据
print("\n[Step 1/5] 生成用户行为数据...")
generator = UserBehaviorGenerator(seed=42)
df_minimalist = generator.generate_cohort(n_users=5000, product_type='minimalist')
df_graphic = generator.generate_cohort(n_users=5000, product_type='graphic')
print(f" 极简组用户数: {len(df_minimalist):,}")
print(f" 潮款组用户数: {len(df_graphic):,}")
# Step 2: 初始化分析器
print("\n[Step 2/5] 初始化留存分析引擎...")
analyzer = RetentionAnalyzer(df_minimalist, df_graphic)
# Step 3: 留存率对比
print("\n[Step 3/5] 计算月度留存率对比...")
comparison = analyzer.compare_retention(periods=12)
print("\n ── 月度留存率对比 ───────────────────────")
print(f" {'月份':>4} {'极简无Logo':>10} {'印花潮款':>10} {'差值':>8}")
print(f" {'-'*44}")
for _, row in comparison.iterrows():
print(f" {row['月份']:>4.0f} {row['极简无Logo留存率(%)']:>9.1f}% "
f"{row['印花潮款留存率(%)']:>9.1f}% {row['差值(百分点)']:>+7.1f}")
# Step 4: LTV 分析
print("\n[Step 4/5] 计算用户终身价值(LTV)...")
ltv_data = analyzer.ltv_analysis()
print("\n ── LTV 对比 ──────────────────────────────")
print(f" {'指标':<20} {'极简无Logo':>12} {'印花潮款':>12}")
print(f" {'-'*48}")
m = ltv_data['minimalist']
g = ltv_data['graphic']
print(f" {'平均 LTV(元)':<20} {m['avg_ltv']:>12,.0f} {g['avg_ltv']:>12,.0f}")
print(f" {'生命周期(月)':<20} {m['avg_lifetime_months']:>12.1f} {g['avg_lifetime_months']:>12.1f}")
print(f" {'单次消费(元)':<20} {m['avg_spend_per_purchase']:>12,.0f} {g['avg_spend_per_purchase']:>12,.0f}")
print(f" {'购买频次':<20} {m['avg_purchase_count']:>12.1f} {g['avg_purchase_count']:>12.1f}")
# Step 5: 统计检验
print("\n[Step 5/5] 执行 Log-Rank 统计检验...")
test_result = analyzer.log_rank_test()
print(f"\n ── 统计检验结果 ─────────────────────────")
print(f" Chi-Square 值: {test_result['chi_square']:.4f}")
print(f" P 值: {test_result['p_value']:.6f}")
if test_result['is_significant']:
print(f" 结论: ✅ 两组差异具有统计显著性(P < 0.05)")
print(f" → 极简与潮款的留存差异不是偶然,是产品哲学的本质不同")
else:
print(f" 结论: ⚠️ 两组差异不显著(P >= 0.05)")
print(f" → 需要更多数据或样本量来验证差异")
# Step 6: 可视化
print("\n [可视化] 正在渲染图表...")
dashboard = RetentionDashboard()
dashboard.plot_survival_curves(analyzer)
dashboard.plot_retention_comparison(comparison)
dashboard.plot_ltv_comparison(ltv_data)
print("\n" + "=" * 70)
print(" 分析完成!所有图表已保存。")
print("=" * 70)
if __name__ == "__main__":
run_analysis()
README.md
# Japanese Minimalist vs Graphic Trend — 复购率对比分析器
## 📖 项目简介
基于 Kaplan-Meier 生存分析的复购率评估系统。
对比"日式极简(无 Logo)"与"印花潮款"的长期用户留存差异。
## ⚙️ 运行环境
- Python 3.8+
- 依赖库: `numpy`, `pandas`, `matplotlib`, `scipy`
## 🚀 快速开始
1. 安装依赖: `pip install numpy pandas matplotlib scipy`
2. 运行主程序: `python main.py`
## 📊 输出说明
程序将输出:
1. **终端报告**: 月度留存率对比表、LTV 分析、Log-Rank 检验结果。
2. **survival_curves.png**: Kaplan-Meier 生存曲线对比。
3. **retention_comparison.png**: 月度留存率柱状图。
4. **ltv_comparison.png**: 用户价值画像雷达图。
## 🛠️ 参数调整
在 `main.py` 的 `run_analysis()` 中修改:
- `n_users`: 模拟用户数(默认 5000/组)
- `seed`: 随机种子(保证可复现性)
在 `data_generator.py` 中调整用户行为参数:
- `purchase_cycle`: 平均购买周期(天)
- `ltv_months`: 用户生命周期(月)
- `avg_spend`: 平均客单价
- `churn_rate`: 月流失率
## 📁 目录结构
retention_analyzer/
├── data_generator.py # 用户行为数据生成器
├── retention_analyzer.py # 核心分析引擎
├── visualizer.py # 可视化仪表盘
├── main.py # 主程序入口
└── README.md
核心知识点卡片
┌──────────────────────────────────────────────────────┐
│ 知识点卡片:时尚产业用户留存与 LTV 分析 │
├──────────────────────────────────────────────────────┤
│ │
│ 1. Kaplan-Meier 生存分析 │
│ ────────────────────────────────────────────────── │
│ • 起源:医学临床试验(病人存活率分析) │
│ • 转译:用户的"存活"= 持续复购 │
│ • 核心公式:S(t) = Π(1 - d_i/n_i) │
│ • 优势:能处理"删失数据"(用户还在观察期内) │
│ │
│ 2. 复购率 vs 留存率 │
│ ────────────────────────────────────────────────── │
│ • 复购率:买过 2 次以上 / 总用户数(静态) │
│ • 留存率:经过 t 时间后仍活跃的用户比例(动态) │
│ • 关键洞察:极简款的 12 月留存 > 潮款 3 月留存 │
│ │
│ 3. Log-Rank 检验 │
│ ────────────────────────────────────────────────── │
│ • 零假设 H0:两组生存曲线完全相同 │
│ • 备择假设 H1:两组生存曲线有差异 │
│ • P < 0.05 → 差异显著,不是偶然 │
│ │
│
利用AI解决实际问题,如果你觉得这个工具好用,欢迎关注长安牧笛!