Просмотр исходного кода

feat(渠道效果分析): 统一指标命名,完善可视化展示

- 重命名指标:进入推荐率→进入分发率,再分享回流率→整体裂变率
- 重命名指标:原视频再分享回流率→头部裂变率,推荐再分享回流率→推荐裂变率
- 整体表的点击UV添加渐变色
- 移除所有名称截断,完整展示渠道和品类名称
- 添加列名映射兼容旧版CSV数据

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
yangxiaohui 2 месяцев назад
Родитель
Сommit
4f1547c4ba

+ 8 - 8
tasks/渠道效果分析/analyze.py

@@ -42,26 +42,26 @@ log()
 # 按渠道汇总(加权平均)
 channel_stats = df.groupby('channel').agg({
     '点击uv': 'sum',
-    '再分享回流uv': 'sum'
+    '裂变uv': 'sum'
 }).reset_index()
 
 # 用加权方式计算整体指标
 for ch in channel_stats['channel']:
     ch_df = df[df['channel'] == ch]
     uv = ch_df['点击uv'].sum()
-    channel_stats.loc[channel_stats['channel'] == ch, '进入推荐率'] = (ch_df['进入推荐率'] * ch_df['点击uv']).sum() / uv
-    channel_stats.loc[channel_stats['channel'] == ch, '再分享回流率'] = (ch_df['再分享回流率'] * ch_df['点击uv']).sum() / uv
-    channel_stats.loc[channel_stats['channel'] == ch, '原视频再分享回流率'] = (ch_df['原视频再分享回流率'] * ch_df['点击uv']).sum() / uv
-    channel_stats.loc[channel_stats['channel'] == ch, '推荐再分享回流率'] = (ch_df['推荐再分享回流率'] * ch_df['点击uv']).sum() / uv
+    channel_stats.loc[channel_stats['channel'] == ch, '进入分发率'] = (ch_df['进入分发率'] * ch_df['点击uv']).sum() / uv
+    channel_stats.loc[channel_stats['channel'] == ch, '整体裂变率'] = (ch_df['整体裂变率'] * ch_df['点击uv']).sum() / uv
+    channel_stats.loc[channel_stats['channel'] == ch, '头部裂变率'] = (ch_df['头部裂变率'] * ch_df['点击uv']).sum() / uv
+    channel_stats.loc[channel_stats['channel'] == ch, '推荐裂变率'] = (ch_df['推荐裂变率'] * ch_df['点击uv']).sum() / uv
 
 channel_stats = channel_stats.sort_values('点击uv', ascending=False)
 
-header = f"{'渠道':<25} {'点击UV':>12} {'进入推荐率':>10} {'回流率':>10} {'原视频':>8} {'推荐':>8}"
+header = f"{'渠道':<25} {'点击UV':>12} {'进入分发率':>10} {'整体裂变率':>10} {'头部裂变率':>10} {'推荐裂变率':>10}"
 log(header)
-log("-" * 80)
+log("-" * 90)
 
 for _, row in channel_stats.iterrows():
-    log(f"{row['channel']:<25} {int(row['点击uv']):>12,} {row['进入推荐率']:>10.4f} {row['再分享回流率']:>10.4f} {row['原视频再分享回流率']:>8.4f} {row['推荐再分享回流率']:>8.4f}")
+    log(f"{row['channel']:<25} {int(row['点击uv']):>12,} {row['进入分发率']:>10.4f} {row['整体裂变率']:>10.4f} {row['头部裂变率']:>10.4f} {row['推荐裂变率']:>10.4f}")
 
 log()
 

+ 5 - 5
tasks/渠道效果分析/query.sql

@@ -6,18 +6,18 @@ SELECT  dt
         ,merge一级品类
         ,merge二级品类
         ,COUNT(DISTINCT mid) AS 点击uv
-        ,COUNT(DISTINCT CASE WHEN 是否进入推荐 = '1' THEN mid END) / COUNT(DISTINCT mid) AS 进入推荐
+        ,COUNT(DISTINCT CASE WHEN 是否进入推荐 = '1' THEN mid END) / COUNT(DISTINCT mid) AS 进入分发
         ,(SUM(CASE WHEN 再分享群聊回流uv > 0 THEN 再分享群聊回流uv ELSE 0 END)
           + SUM(CASE WHEN 再分享单聊回流uv > 0 THEN 再分享单聊回流uv ELSE 0 END)
-         ) / (COUNT(DISTINCT mid) + 10) AS 再分享回流
+         ) / (COUNT(DISTINCT mid) + 10) AS 整体裂变
         ,(SUM(CASE WHEN 是否原视频 = '是' THEN 再分享群聊回流uv ELSE 0 END)
           + SUM(CASE WHEN 是否原视频 = '是' THEN 再分享单聊回流uv ELSE 0 END)
-         ) / (COUNT(DISTINCT mid) + 10) AS 原视频再分享回流
+         ) / (COUNT(DISTINCT mid) + 10) AS 头部裂变
         ,(SUM(CASE WHEN 是否原视频 = '否' THEN 再分享群聊回流uv ELSE 0 END)
           + SUM(CASE WHEN 是否原视频 = '否' THEN 再分享单聊回流uv ELSE 0 END)
-         ) / (COUNT(DISTINCT mid) + 10) AS 推荐再分享回流
+         ) / (COUNT(DISTINCT mid) + 10) AS 推荐裂变
         ,SUM(CASE WHEN 再分享群聊回流uv > 0 THEN 再分享群聊回流uv ELSE 0 END)
-         + SUM(CASE WHEN 再分享单聊回流uv > 0 THEN 再分享单聊回流uv ELSE 0 END) AS 再分享回流uv
+         + SUM(CASE WHEN 再分享单聊回流uv > 0 THEN 再分享单聊回流uv ELSE 0 END) AS 裂变uv
 FROM    loghubods.opengid_base_data
 WHERE   dt >= ${start}
 AND     dt <= ${end}

+ 393 - 71
tasks/渠道效果分析/visualize.py

@@ -20,6 +20,16 @@ if not csv_files:
 latest_file = max(csv_files, key=lambda x: x.stat().st_mtime)
 df = pd.read_csv(latest_file)
 
+# 兼容旧版列名:自动映射到新名称
+column_mapping = {
+    '进入推荐率': '进入分发率',
+    '再分享回流率': '整体裂变率',
+    '原视频再分享回流率': '头部裂变率',
+    '推荐再分享回流率': '推荐裂变率',
+    '再分享回流uv': '裂变uv'
+}
+df = df.rename(columns={k: v for k, v in column_mapping.items() if k in df.columns})
+
 print(f"分析文件: {latest_file.name}")
 print(f"时间范围: {df['dt'].min()} ~ {df['dt'].max()}")
 
@@ -30,35 +40,80 @@ print(f"时间范围: {df['dt'].min()} ~ {df['dt'].max()}")
 # 渠道整体
 channel_stats = df.groupby('channel').agg({
     '点击uv': 'sum',
-    '再分享回流uv': 'sum'
+    '裂变uv': 'sum'
 }).reset_index()
 
 for ch in channel_stats['channel']:
     ch_df = df[df['channel'] == ch]
     uv = ch_df['点击uv'].sum()
-    channel_stats.loc[channel_stats['channel'] == ch, '进入推荐率'] = (ch_df['进入推荐率'] * ch_df['点击uv']).sum() / uv
-    channel_stats.loc[channel_stats['channel'] == ch, '再分享回流率'] = (ch_df['再分享回流率'] * ch_df['点击uv']).sum() / uv
-    channel_stats.loc[channel_stats['channel'] == ch, '原视频回流率'] = (ch_df['原视频再分享回流率'] * ch_df['点击uv']).sum() / uv
-    channel_stats.loc[channel_stats['channel'] == ch, '推荐回流率'] = (ch_df['推荐再分享回流率'] * ch_df['点击uv']).sum() / uv
+    channel_stats.loc[channel_stats['channel'] == ch, '进入分发率'] = (ch_df['进入分发率'] * ch_df['点击uv']).sum() / uv
+    channel_stats.loc[channel_stats['channel'] == ch, '整体裂变率'] = (ch_df['整体裂变率'] * ch_df['点击uv']).sum() / uv
+    channel_stats.loc[channel_stats['channel'] == ch, '头部裂变率'] = (ch_df['头部裂变率'] * ch_df['点击uv']).sum() / uv
+    channel_stats.loc[channel_stats['channel'] == ch, '推荐裂变率'] = (ch_df['推荐裂变率'] * ch_df['点击uv']).sum() / uv
 
 channel_stats = channel_stats.sort_values('点击uv', ascending=False)
 
+# 一级品类整体
+category_stats = df.groupby('merge一级品类').agg({
+    '点击uv': 'sum',
+    '裂变uv': 'sum'
+}).reset_index()
+
+for cat in category_stats['merge一级品类']:
+    cat_df = df[df['merge一级品类'] == cat]
+    uv = cat_df['点击uv'].sum()
+    category_stats.loc[category_stats['merge一级品类'] == cat, '进入分发率'] = (cat_df['进入分发率'] * cat_df['点击uv']).sum() / uv
+    category_stats.loc[category_stats['merge一级品类'] == cat, '整体裂变率'] = (cat_df['整体裂变率'] * cat_df['点击uv']).sum() / uv
+    category_stats.loc[category_stats['merge一级品类'] == cat, '头部裂变率'] = (cat_df['头部裂变率'] * cat_df['点击uv']).sum() / uv
+    category_stats.loc[category_stats['merge一级品类'] == cat, '推荐裂变率'] = (cat_df['推荐裂变率'] * cat_df['点击uv']).sum() / uv
+
+category_stats = category_stats.sort_values('点击uv', ascending=False)
+
+# 二级品类整体
+cat2_overall = df.groupby(['merge一级品类', 'merge二级品类']).agg({
+    '点击uv': 'sum',
+    '裂变uv': 'sum'
+}).reset_index()
+
+cat2_overall['cat2_label'] = cat2_overall.apply(
+    lambda r: f"{str(r['merge一级品类'])}/{str(r['merge二级品类'])}"
+    if pd.notna(r['merge二级品类']) else str(r['merge一级品类']), axis=1
+)
+
+for idx in cat2_overall.index:
+    cat1 = cat2_overall.loc[idx, 'merge一级品类']
+    cat2 = cat2_overall.loc[idx, 'merge二级品类']
+    mask = (df['merge一级品类'] == cat1) & (df['merge二级品类'] == cat2) if pd.notna(cat2) else (df['merge一级品类'] == cat1) & (df['merge二级品类'].isna())
+    cat_df = df[mask]
+    uv = cat_df['点击uv'].sum()
+    if uv > 0:
+        cat2_overall.loc[idx, '进入分发率'] = (cat_df['进入分发率'] * cat_df['点击uv']).sum() / uv
+        cat2_overall.loc[idx, '整体裂变率'] = (cat_df['整体裂变率'] * cat_df['点击uv']).sum() / uv
+        cat2_overall.loc[idx, '头部裂变率'] = (cat_df['头部裂变率'] * cat_df['点击uv']).sum() / uv
+        cat2_overall.loc[idx, '推荐裂变率'] = (cat_df['推荐裂变率'] * cat_df['点击uv']).sum() / uv
+
+cat2_overall = cat2_overall.sort_values('点击uv', ascending=False)
+
 # 渠道×品类
 channel_category = df.groupby(['channel', 'merge一级品类']).apply(
     lambda x: pd.Series({
         '点击uv': x['点击uv'].sum(),
-        '回流率': (x['再分享回流率'] * x['点击uv']).sum() / x['点击uv'].sum() if x['点击uv'].sum() > 0 else 0
+        '整体裂变率': (x['整体裂变率'] * x['点击uv']).sum() / x['点击uv'].sum() if x['点击uv'].sum() > 0 else 0,
+        '头部裂变率': (x['头部裂变率'] * x['点击uv']).sum() / x['点击uv'].sum() if x['点击uv'].sum() > 0 else 0,
+        '推荐裂变率': (x['推荐裂变率'] * x['点击uv']).sum() / x['点击uv'].sum() if x['点击uv'].sum() > 0 else 0
     }), include_groups=False
 ).reset_index()
 
-pivot_ror = channel_category.pivot(index='merge一级品类', columns='channel', values='回流率')
+pivot_ror = channel_category.pivot(index='merge一级品类', columns='channel', values='整体裂变率')
+pivot_ror_orig = channel_category.pivot(index='merge一级品类', columns='channel', values='头部裂变率')
+pivot_ror_rec = channel_category.pivot(index='merge一级品类', columns='channel', values='推荐裂变率')
 pivot_uv = channel_category.pivot(index='merge一级品类', columns='channel', values='点击uv').fillna(0)
 
 # 每日趋势
 daily = df.groupby(['dt', 'channel']).apply(
     lambda x: pd.Series({
         '点击uv': x['点击uv'].sum(),
-        '回流率': (x['再分享回流率'] * x['点击uv']).sum() / x['点击uv'].sum() if x['点击uv'].sum() > 0 else 0
+        '整体裂变率': (x['整体裂变率'] * x['点击uv']).sum() / x['点击uv'].sum() if x['点击uv'].sum() > 0 else 0
     }), include_groups=False
 ).reset_index()
 
@@ -91,39 +146,106 @@ heatmap_cols = [c for c in main_channels if c in pivot_ror.columns]
 
 # 1. 渠道表格行(使用统一渐变)
 channel_rows = []
+max_channel_uv = channel_stats['点击uv'].max()
 for _, row in channel_stats.iterrows():
-    rec_style = get_gradient_color(row['进入推荐率'], 1.0, 0.6)  # 进入推荐率 0.6~1.0
-    ror_style = get_gradient_color(row['再分享回流率'], 0.8)      # 回流率 0~0.8
-    orig_style = get_gradient_color(row['原视频回流率'], 0.3)     # 原视频 0~0.3
-    rec_ror_style = get_gradient_color(row['推荐回流率'], 0.5)    # 推荐回流 0~0.5
+    uv_style = get_gradient_color(row['点击uv'], max_channel_uv)  # 点击UV渐变
+    rec_style = get_gradient_color(row['进入分发率'], 1.0, 0.6)   # 进入分发率 0.6~1.0
+    ror_style = get_gradient_color(row['整体裂变率'], 0.8)        # 整体裂变率 0~0.8
+    orig_style = get_gradient_color(row['头部裂变率'], 0.3)       # 头部裂变率 0~0.3
+    rec_ror_style = get_gradient_color(row['推荐裂变率'], 0.5)    # 推荐裂变率 0~0.5
     channel_rows.append(
         f"<tr><td>{row['channel']}</td>"
-        f"<td style='text-align:right'>{int(row['点击uv']):,}</td>"
-        f"<td style='{rec_style}; text-align:center'>{row['进入推荐率']:.4f}</td>"
-        f"<td style='{ror_style}; text-align:center'>{row['再分享回流率']:.4f}</td>"
-        f"<td style='{orig_style}; text-align:center'>{row['原视频回流率']:.4f}</td>"
-        f"<td style='{rec_ror_style}; text-align:center'>{row['推荐回流率']:.4f}</td></tr>"
+        f"<td style='{uv_style}; text-align:right'>{int(row['点击uv']):,}</td>"
+        f"<td style='{rec_style}; text-align:center'>{row['进入分发率']:.4f}</td>"
+        f"<td style='{ror_style}; text-align:center'>{row['整体裂变率']:.4f}</td>"
+        f"<td style='{orig_style}; text-align:center'>{row['头部裂变率']:.4f}</td>"
+        f"<td style='{rec_ror_style}; text-align:center'>{row['推荐裂变率']:.4f}</td></tr>"
+    )
+
+# 1.5 一级品类表格行(使用统一渐变)
+category_rows = []
+max_cat_uv = category_stats['点击uv'].max()
+for _, row in category_stats.iterrows():
+    cat_name = str(row['merge一级品类']) if pd.notna(row['merge一级品类']) else '-'
+    uv_style = get_gradient_color(row['点击uv'], max_cat_uv)
+    rec_style = get_gradient_color(row['进入分发率'], 1.0, 0.6)
+    ror_style = get_gradient_color(row['整体裂变率'], 0.8)
+    orig_style = get_gradient_color(row['头部裂变率'], 0.3)
+    rec_ror_style = get_gradient_color(row['推荐裂变率'], 0.5)
+    category_rows.append(
+        f"<tr><td>{cat_name}</td>"
+        f"<td style='{uv_style}; text-align:right'>{int(row['点击uv']):,}</td>"
+        f"<td style='{rec_style}; text-align:center'>{row['进入分发率']:.4f}</td>"
+        f"<td style='{ror_style}; text-align:center'>{row['整体裂变率']:.4f}</td>"
+        f"<td style='{orig_style}; text-align:center'>{row['头部裂变率']:.4f}</td>"
+        f"<td style='{rec_ror_style}; text-align:center'>{row['推荐裂变率']:.4f}</td></tr>"
+    )
+
+# 1.6 二级品类表格行(使用统一渐变)
+cat2_overall_rows = []
+max_cat2_uv = cat2_overall['点击uv'].max()
+for _, row in cat2_overall.iterrows():
+    uv_style = get_gradient_color(row['点击uv'], max_cat2_uv)
+    rec_style = get_gradient_color(row.get('进入分发率'), 1.0, 0.6)
+    ror_style = get_gradient_color(row.get('整体裂变率'), 0.8)
+    orig_style = get_gradient_color(row.get('头部裂变率'), 0.3)
+    rec_ror_style = get_gradient_color(row.get('推荐裂变率'), 0.5)
+    cat2_overall_rows.append(
+        f"<tr><td>{row['cat2_label']}</td>"
+        f"<td style='{uv_style}; text-align:right'>{int(row['点击uv']):,}</td>"
+        f"<td style='{rec_style}; text-align:center'>{row.get('进入分发率', 0):.4f}</td>"
+        f"<td style='{ror_style}; text-align:center'>{row.get('整体裂变率', 0):.4f}</td>"
+        f"<td style='{orig_style}; text-align:center'>{row.get('头部裂变率', 0):.4f}</td>"
+        f"<td style='{rec_ror_style}; text-align:center'>{row.get('推荐裂变率', 0):.4f}</td></tr>"
     )
 
 # 2. 回流率热力图(使用统一渐变)
-ror_header = "<tr><th>品类</th>" + "".join([f"<th>{c[:10]}</th>" for c in heatmap_cols]) + "</tr>"
+ror_header = "<tr><th>品类</th>" + "".join([f"<th>{c}</th>" for c in heatmap_cols]) + "</tr>"
+
+# 2.1 整体裂变率
 ror_rows = []
 for cat in valid_categories:
-    cells = [f"<td>{str(cat)[:12]}</td>"]
+    cells = [f"<td>{str(cat)}</td>"]
     for ch in heatmap_cols:
         if ch in pivot_ror.columns and pd.notna(pivot_ror.loc[cat, ch]):
             val = pivot_ror.loc[cat, ch]
-            style = get_gradient_color(val, 0.8)  # 回流率 max=0.8
+            style = get_gradient_color(val, 0.8)
             cells.append(f'<td style="{style}">{val:.4f}</td>')
         else:
             cells.append("<td>-</td>")
     ror_rows.append("<tr>" + "".join(cells) + "</tr>")
 
+# 2.2 头部裂变率
+ror_orig_rows = []
+for cat in valid_categories:
+    cells = [f"<td>{str(cat)}</td>"]
+    for ch in heatmap_cols:
+        if ch in pivot_ror_orig.columns and pd.notna(pivot_ror_orig.loc[cat, ch]):
+            val = pivot_ror_orig.loc[cat, ch]
+            style = get_gradient_color(val, 0.3)  # 原视频 max=0.3
+            cells.append(f'<td style="{style}">{val:.4f}</td>')
+        else:
+            cells.append("<td>-</td>")
+    ror_orig_rows.append("<tr>" + "".join(cells) + "</tr>")
+
+# 2.3 推荐裂变率
+ror_rec_rows = []
+for cat in valid_categories:
+    cells = [f"<td>{str(cat)}</td>"]
+    for ch in heatmap_cols:
+        if ch in pivot_ror_rec.columns and pd.notna(pivot_ror_rec.loc[cat, ch]):
+            val = pivot_ror_rec.loc[cat, ch]
+            style = get_gradient_color(val, 0.5)  # 推荐 max=0.5
+            cells.append(f'<td style="{style}">{val:.4f}</td>')
+        else:
+            cells.append("<td>-</td>")
+    ror_rec_rows.append("<tr>" + "".join(cells) + "</tr>")
+
 # 3. UV分布热力图(使用统一渐变)
-uv_header = "<tr><th>品类</th>" + "".join([f"<th>{c[:10]}</th>" for c in heatmap_cols]) + "</tr>"
+uv_header = "<tr><th>品类</th>" + "".join([f"<th>{c}</th>" for c in heatmap_cols]) + "</tr>"
 uv_rows = []
 for cat in valid_categories:
-    cells = [f"<td>{str(cat)[:12]}</td>"]
+    cells = [f"<td>{str(cat)}</td>"]
     for ch in heatmap_cols:
         if ch in pivot_uv.columns:
             val = pivot_uv.loc[cat, ch]
@@ -136,9 +258,9 @@ for cat in valid_categories:
             cells.append("<td>-</td>")
     uv_rows.append("<tr>" + "".join(cells) + "</tr>")
 
-# 4. 进入推荐率热力图(使用统一渐变)
+# 4. 进入分发率热力图(使用统一渐变)
 pivot_recommend = df.groupby(['merge一级品类', 'channel']).apply(
-    lambda x: (x['进入推荐率'] * x['点击uv']).sum() / x['点击uv'].sum() if x['点击uv'].sum() > 0 else 0,
+    lambda x: (x['进入分发率'] * x['点击uv']).sum() / x['点击uv'].sum() if x['点击uv'].sum() > 0 else 0,
     include_groups=False
 ).unstack()
 
@@ -146,7 +268,7 @@ recommend_rows = []
 for cat in valid_categories:
     if cat not in pivot_recommend.index:
         continue
-    cells = [f"<td>{str(cat)[:12]}</td>"]
+    cells = [f"<td>{str(cat)}</td>"]
     for ch in heatmap_cols:
         if ch in pivot_recommend.columns and pd.notna(pivot_recommend.loc[cat, ch]):
             val = pivot_recommend.loc[cat, ch]
@@ -160,14 +282,16 @@ for cat in valid_categories:
 channel_cat2 = df.groupby(['channel', 'merge一级品类', 'merge二级品类']).apply(
     lambda x: pd.Series({
         '点击uv': x['点击uv'].sum(),
-        '回流率': (x['再分享回流率'] * x['点击uv']).sum() / x['点击uv'].sum() if x['点击uv'].sum() > 0 else 0
+        '整体裂变率': (x['整体裂变率'] * x['点击uv']).sum() / x['点击uv'].sum() if x['点击uv'].sum() > 0 else 0,
+        '头部裂变率': (x['头部裂变率'] * x['点击uv']).sum() / x['点击uv'].sum() if x['点击uv'].sum() > 0 else 0,
+        '推荐裂变率': (x['推荐裂变率'] * x['点击uv']).sum() / x['点击uv'].sum() if x['点击uv'].sum() > 0 else 0
     }), include_groups=False
 ).reset_index()
 
 # 创建二级品类标签
 channel_cat2['cat2_label'] = channel_cat2.apply(
-    lambda r: f"{str(r['merge一级品类'])[:6]}/{str(r['merge二级品类'])[:8]}"
-    if pd.notna(r['merge二级品类']) else str(r['merge一级品类'])[:12], axis=1
+    lambda r: f"{str(r['merge一级品类'])}/{str(r['merge二级品类'])}"
+    if pd.notna(r['merge二级品类']) else str(r['merge一级品类']), axis=1
 )
 
 # 二级品类汇总
@@ -175,36 +299,70 @@ cat2_stats = channel_cat2.groupby('cat2_label').agg({'点击uv': 'sum'}).reset_i
 cat2_stats = cat2_stats[cat2_stats['点击uv'] >= 1000].sort_values('点击uv', ascending=False)
 valid_cat2_labels = cat2_stats.head(20)['cat2_label'].tolist()
 
-pivot_cat2_ror = channel_cat2.pivot_table(index='cat2_label', columns='channel', values='回流率')
+pivot_cat2_ror = channel_cat2.pivot_table(index='cat2_label', columns='channel', values='整体裂变率')
+pivot_cat2_ror_orig = channel_cat2.pivot_table(index='cat2_label', columns='channel', values='头部裂变率')
+pivot_cat2_ror_rec = channel_cat2.pivot_table(index='cat2_label', columns='channel', values='推荐裂变率')
 pivot_cat2_uv = channel_cat2.pivot_table(index='cat2_label', columns='channel', values='点击uv', fill_value=0)
 
 # 二级品类回流率热力图(使用统一渐变)
-cat2_ror_header = "<tr><th>二级品类</th>" + "".join([f"<th>{c[:10]}</th>" for c in heatmap_cols]) + "</tr>"
+cat2_ror_header = "<tr><th>二级品类</th>" + "".join([f"<th>{c}</th>" for c in heatmap_cols]) + "</tr>"
+
+# 整体裂变率
 cat2_ror_rows = []
 for cat2 in valid_cat2_labels:
     if cat2 not in pivot_cat2_ror.index:
         continue
-    cells = [f"<td>{cat2[:15]}</td>"]
+    cells = [f"<td>{cat2}</td>"]
     for ch in heatmap_cols:
         if ch in pivot_cat2_ror.columns and pd.notna(pivot_cat2_ror.loc[cat2, ch]):
             val = pivot_cat2_ror.loc[cat2, ch]
-            style = get_gradient_color(val, 0.8)  # 回流率 max=0.8
+            style = get_gradient_color(val, 0.8)
             cells.append(f'<td style="{style}">{val:.4f}</td>')
         else:
             cells.append("<td>-</td>")
     cat2_ror_rows.append("<tr>" + "".join(cells) + "</tr>")
 
-# 二级品类UV热力图(使用统一渐变)
+# 头部裂变率
+cat2_ror_orig_rows = []
+for cat2 in valid_cat2_labels:
+    if cat2 not in pivot_cat2_ror_orig.index:
+        continue
+    cells = [f"<td>{cat2}</td>"]
+    for ch in heatmap_cols:
+        if ch in pivot_cat2_ror_orig.columns and pd.notna(pivot_cat2_ror_orig.loc[cat2, ch]):
+            val = pivot_cat2_ror_orig.loc[cat2, ch]
+            style = get_gradient_color(val, 0.3)
+            cells.append(f'<td style="{style}">{val:.4f}</td>')
+        else:
+            cells.append("<td>-</td>")
+    cat2_ror_orig_rows.append("<tr>" + "".join(cells) + "</tr>")
+
+# 推荐裂变率
+cat2_ror_rec_rows = []
+for cat2 in valid_cat2_labels:
+    if cat2 not in pivot_cat2_ror_rec.index:
+        continue
+    cells = [f"<td>{cat2}</td>"]
+    for ch in heatmap_cols:
+        if ch in pivot_cat2_ror_rec.columns and pd.notna(pivot_cat2_ror_rec.loc[cat2, ch]):
+            val = pivot_cat2_ror_rec.loc[cat2, ch]
+            style = get_gradient_color(val, 0.5)
+            cells.append(f'<td style="{style}">{val:.4f}</td>')
+        else:
+            cells.append("<td>-</td>")
+    cat2_ror_rec_rows.append("<tr>" + "".join(cells) + "</tr>")
+
+# 二级品类UV热力图
 cat2_uv_rows = []
 for cat2 in valid_cat2_labels:
     if cat2 not in pivot_cat2_uv.index:
         continue
-    cells = [f"<td>{cat2[:15]}</td>"]
+    cells = [f"<td>{cat2}</td>"]
     for ch in heatmap_cols:
         if ch in pivot_cat2_uv.columns:
             val = pivot_cat2_uv.loc[cat2, ch]
             if val > 0:
-                style = get_gradient_color(val, 100000)  # UV max=10万
+                style = get_gradient_color(val, 100000)
                 cells.append(f'<td style="{style}">{int(val):,}</td>')
             else:
                 cells.append("<td>-</td>")
@@ -217,7 +375,7 @@ for cat2 in valid_cat2_labels:
 # ============================================================
 
 total_uv = int(df['点击uv'].sum())
-avg_ror = (df['再分享回流率'] * df['点击uv']).sum() / df['点击uv'].sum()
+avg_ror = (df['整体裂变率'] * df['点击uv']).sum() / df['点击uv'].sum()
 channel_count = df['channel'].nunique()
 category_count = df['merge一级品类'].nunique()
 date_range = f"{df['dt'].min()} ~ {df['dt'].max()}"
@@ -254,30 +412,44 @@ html_content = f"""<!DOCTYPE html>
                      border-radius: 8px; color: white; text-align: center; }}
         .stat-card h4 {{ margin: 0; font-size: 24px; }}
         .stat-card p {{ margin: 5px 0 0; opacity: 0.9; }}
-        table {{ width: 100%; border-collapse: collapse; margin: 10px 0; }}
-        th, td {{ padding: 8px 12px; text-align: left; border-bottom: 1px solid #ddd; }}
+        table {{ width: 100%; border-collapse: collapse; margin: 10px 0; font-size: 13px; }}
+        th, td {{ padding: 6px 10px; text-align: left; border-bottom: 1px solid #ddd; }}
         th {{ background: #f8f9fa; font-weight: 600; position: sticky; top: 0; }}
         tr:hover {{ background: #f5f5f5; }}
         .heatmap {{ overflow-x: auto; }}
-        .heatmap table {{ font-size: 12px; }}
-        .heatmap td {{ text-align: center; min-width: 80px; }}
+        .heatmap td {{ text-align: center; min-width: 70px; }}
         canvas {{ max-height: 400px; }}
         .matrix-section {{ margin-bottom: 40px; }}
         .legend {{ font-size: 12px; margin: 10px 0; color: #666; }}
+        .sortable th {{ cursor: pointer; user-select: none; }}
+        .sortable th:hover {{ background: #e9ecef; }}
+        .sortable td:first-child {{ cursor: pointer; }}
+        .sortable td:first-child:hover {{ background: #e9ecef; }}
+        .sort-hint {{ font-size: 10px; color: #999; margin-left: 5px; }}
+        .reset-btn {{ font-size: 11px; padding: 2px 8px; margin-left: 10px; cursor: pointer;
+                     background: #6c757d; color: white; border: none; border-radius: 3px; }}
+        .reset-btn:hover {{ background: #5a6268; }}
     </style>
 </head>
 <body>
     <nav class="sidebar">
         <h2>目录</h2>
         <ul>
-            <li><a href="#sec1">一、渠道整体表现</a></li>
-            <li><a href="#sec2">二、一级品类分析</a></li>
-            <li class="sub"><a href="#sec2-1">2.1 回流率矩阵</a></li>
-            <li class="sub"><a href="#sec2-2">2.2 点击UV矩阵</a></li>
-            <li class="sub"><a href="#sec2-3">2.3 进入推荐率矩阵</a></li>
-            <li><a href="#sec3">三、二级品类分析</a></li>
-            <li class="sub"><a href="#sec3-1">3.1 回流率矩阵</a></li>
-            <li class="sub"><a href="#sec3-2">3.2 点击UV矩阵</a></li>
+            <li><a href="#sec1">一、整体表现</a></li>
+            <li class="sub"><a href="#sec1-1">1.1 渠道整体</a></li>
+            <li class="sub"><a href="#sec1-2">1.2 一级品类整体</a></li>
+            <li class="sub"><a href="#sec1-3">1.3 二级品类整体</a></li>
+            <li><a href="#sec2">二、渠道×一级品类</a></li>
+            <li class="sub"><a href="#sec2-1">2.1 整体裂变率</a></li>
+            <li class="sub"><a href="#sec2-2">2.2 头部裂变率</a></li>
+            <li class="sub"><a href="#sec2-3">2.3 推荐裂变率</a></li>
+            <li class="sub"><a href="#sec2-4">2.4 点击UV</a></li>
+            <li class="sub"><a href="#sec2-5">2.5 进入分发率</a></li>
+            <li><a href="#sec3">三、渠道×二级品类</a></li>
+            <li class="sub"><a href="#sec3-1">3.1 整体裂变率</a></li>
+            <li class="sub"><a href="#sec3-2">3.2 头部裂变率</a></li>
+            <li class="sub"><a href="#sec3-3">3.3 推荐裂变率</a></li>
+            <li class="sub"><a href="#sec3-4">3.4 点击UV</a></li>
         </ul>
     </nav>
 
@@ -292,7 +464,7 @@ html_content = f"""<!DOCTYPE html>
             </div>
             <div class="stat-card">
                 <h4>{avg_ror:.4f}</h4>
-                <p>平均回流率</p>
+                <p>平均裂变率</p>
             </div>
             <div class="stat-card">
                 <h4>{channel_count}</h4>
@@ -304,65 +476,215 @@ html_content = f"""<!DOCTYPE html>
             </div>
         </div>
 
-        <h2 id="sec1">一、渠道整体表现</h2>
+        <h2 id="sec1">一、整体表现</h2>
+
+        <h3 id="sec1-1">1.1 渠道整体</h3>
         <div class="chart-container">
-            <div class="legend">颜色越深=数值越高(统一白→绿渐变)</div>
-            <table>
-                <tr><th>渠道</th><th>点击UV</th><th>进入推荐率</th><th>回流率</th><th>原视频回流</th><th>推荐回流</th></tr>
+            <div class="legend">颜色越深=数值越高 | 点击表头排序 <button class="reset-btn" id="reset-0">重置</button></div>
+            <table class="sortable">
+                <tr><th>渠道</th><th>点击UV</th><th>进入分发率</th><th>整体裂变率</th><th>头部裂变率</th><th>推荐裂变率</th></tr>
                 {"".join(channel_rows)}
             </table>
         </div>
 
-        <h2 id="sec2">二、一级品类分析</h2>
+        <h3 id="sec1-2">1.2 一级品类整体</h3>
+        <div class="chart-container">
+            <div class="legend">颜色越深=数值越高 | 点击表头排序 <button class="reset-btn" id="reset-1">重置</button></div>
+            <table class="sortable">
+                <tr><th>一级品类</th><th>点击UV</th><th>进入分发率</th><th>整体裂变率</th><th>头部裂变率</th><th>推荐裂变率</th></tr>
+                {"".join(category_rows)}
+            </table>
+        </div>
 
-        <h3 id="sec2-1">2.1 回流率矩阵</h3>
+        <h3 id="sec1-3">1.3 二级品类整体</h3>
+        <div class="chart-container">
+            <div class="legend">颜色越深=数值越高 | 点击表头排序 <button class="reset-btn" id="reset-2">重置</button></div>
+            <table class="sortable">
+                <tr><th>二级品类</th><th>点击UV</th><th>进入分发率</th><th>整体裂变率</th><th>头部裂变率</th><th>推荐裂变率</th></tr>
+                {"".join(cat2_overall_rows)}
+            </table>
+        </div>
+
+        <h2 id="sec2">二、渠道×一级品类</h2>
+
+        <h3 id="sec2-1">2.1 整体裂变率</h3>
         <div class="chart-container heatmap matrix-section">
-            <div class="legend">颜色越深=回流率越高(白→绿渐变,max=0.80)</div>
-            <table>
+            <div class="legend">max=0.80 | 点击表头/行名排序 <button class="reset-btn" id="reset-3">重置</button></div>
+            <table class="sortable">
                 {ror_header}
                 {"".join(ror_rows)}
             </table>
         </div>
 
-        <h3 id="sec2-2">2.2 点击UV矩阵</h3>
+        <h3 id="sec2-2">2.2 头部裂变率</h3>
+        <div class="chart-container heatmap matrix-section">
+            <div class="legend">max=0.30 | 点击表头/行名排序 <button class="reset-btn" id="reset-4">重置</button></div>
+            <table class="sortable">
+                {ror_header}
+                {"".join(ror_orig_rows)}
+            </table>
+        </div>
+
+        <h3 id="sec2-3">2.3 推荐裂变率</h3>
+        <div class="chart-container heatmap matrix-section">
+            <div class="legend">max=0.50 | 点击表头/行名排序 <button class="reset-btn" id="reset-5">重置</button></div>
+            <table class="sortable">
+                {ror_header}
+                {"".join(ror_rec_rows)}
+            </table>
+        </div>
+
+        <h3 id="sec2-4">2.4 点击UV</h3>
         <div class="chart-container heatmap matrix-section">
-            <div class="legend">颜色越深=UV越高(白→绿渐变,max=10万)</div>
-            <table>
+            <div class="legend">max=10万 | 点击表头/行名排序 <button class="reset-btn" id="reset-6">重置</button></div>
+            <table class="sortable">
                 {uv_header}
                 {"".join(uv_rows)}
             </table>
         </div>
 
-        <h3 id="sec2-3">2.3 进入推荐率矩阵</h3>
+        <h3 id="sec2-5">2.5 进入分发率</h3>
         <div class="chart-container heatmap matrix-section">
-            <div class="legend">颜色越深=推荐率越高(白→绿渐变,min=0.60,max=1.00)</div>
-            <table>
+            <div class="legend">min=0.60, max=1.00 | 点击表头/行名排序 <button class="reset-btn" id="reset-7">重置</button></div>
+            <table class="sortable">
                 {ror_header}
                 {"".join(recommend_rows)}
             </table>
         </div>
 
-        <h2 id="sec3">三、二级品类分析</h2>
+        <h2 id="sec3">三、渠道×二级品类</h2>
 
-        <h3 id="sec3-1">3.1 回流率矩阵</h3>
+        <h3 id="sec3-1">3.1 整体裂变率</h3>
         <div class="chart-container heatmap matrix-section">
-            <div class="legend">颜色越深=回流率越高(白→绿渐变,max=0.80)</div>
-            <table>
+            <div class="legend">max=0.80 | 点击表头/行名排序 <button class="reset-btn" id="reset-8">重置</button></div>
+            <table class="sortable">
                 {cat2_ror_header}
                 {"".join(cat2_ror_rows)}
             </table>
         </div>
 
-        <h3 id="sec3-2">3.2 点击UV矩阵</h3>
+        <h3 id="sec3-2">3.2 头部裂变率</h3>
         <div class="chart-container heatmap matrix-section">
-            <div class="legend">颜色越深=UV越高(白→绿渐变,max=10万)</div>
-            <table>
+            <div class="legend">max=0.30 | 点击表头/行名排序 <button class="reset-btn" id="reset-9">重置</button></div>
+            <table class="sortable">
+                {cat2_ror_header}
+                {"".join(cat2_ror_orig_rows)}
+            </table>
+        </div>
+
+        <h3 id="sec3-3">3.3 推荐裂变率</h3>
+        <div class="chart-container heatmap matrix-section">
+            <div class="legend">max=0.50 | 点击表头/行名排序 <button class="reset-btn" id="reset-10">重置</button></div>
+            <table class="sortable">
+                {cat2_ror_header}
+                {"".join(cat2_ror_rec_rows)}
+            </table>
+        </div>
+
+        <h3 id="sec3-4">3.4 点击UV</h3>
+        <div class="chart-container heatmap matrix-section">
+            <div class="legend">max=10万 | 点击表头/行名排序 <button class="reset-btn" id="reset-11">重置</button></div>
+            <table class="sortable">
                 {cat2_ror_header}
                 {"".join(cat2_uv_rows)}
             </table>
         </div>
 
     </div>
+
+    <script>
+    // 排序功能
+    document.querySelectorAll('.sortable').forEach((table, tableIndex) => {{
+        const rows = Array.from(table.querySelectorAll('tr')).slice(1);
+        const headerCells = table.querySelectorAll('th');
+
+        // 保存原始顺序
+        const originalRowOrder = rows.map(r => r.cloneNode(true));
+        const originalColOrder = Array.from(table.querySelector('tr').cells).map(c => c.cloneNode(true));
+
+        // 点击表头:按列排序
+        headerCells.forEach((th, colIndex) => {{
+            th.addEventListener('click', () => {{
+                const isAsc = th.dataset.sort !== 'asc';
+                headerCells.forEach(h => h.dataset.sort = '');
+                th.dataset.sort = isAsc ? 'asc' : 'desc';
+
+                rows.sort((a, b) => {{
+                    const aCell = a.cells[colIndex];
+                    const bCell = b.cells[colIndex];
+                    if (!aCell || !bCell) return 0;
+
+                    let aVal = aCell.textContent.replace(/,/g, '').replace(/-/g, '');
+                    let bVal = bCell.textContent.replace(/,/g, '').replace(/-/g, '');
+
+                    const aNum = parseFloat(aVal);
+                    const bNum = parseFloat(bVal);
+
+                    if (!isNaN(aNum) && !isNaN(bNum)) {{
+                        return isAsc ? aNum - bNum : bNum - aNum;
+                    }}
+                    return isAsc ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
+                }});
+
+                rows.forEach(row => table.appendChild(row));
+            }});
+        }});
+
+        // 点击行首:按该行值重排列
+        rows.forEach(row => {{
+            const firstCell = row.cells[0];
+            if (firstCell) {{
+                firstCell.addEventListener('click', (e) => {{
+                    e.stopPropagation();
+                    const values = Array.from(row.cells);
+
+                    const colData = [];
+                    for (let i = 1; i < values.length; i++) {{
+                        let val = values[i]?.textContent.replace(/,/g, '').replace(/-/g, '') || '0';
+                        colData.push({{ index: i, value: parseFloat(val) || 0 }});
+                    }}
+
+                    colData.sort((a, b) => b.value - a.value);
+
+                    table.querySelectorAll('tr').forEach(tr => {{
+                        const cells = Array.from(tr.cells);
+                        const first = cells[0];
+                        const newRow = [first];
+                        colData.forEach(col => newRow.push(cells[col.index]));
+                        newRow.forEach(cell => tr.appendChild(cell));
+                    }});
+                }});
+            }}
+        }});
+
+        // 重置按钮
+        const resetBtn = document.getElementById('reset-' + tableIndex);
+        if (resetBtn) {{
+            resetBtn.addEventListener('click', () => {{
+                // 重置行顺序
+                const headerRow = table.querySelector('tr');
+                originalRowOrder.forEach((origRow, i) => {{
+                    const currentRow = rows[i];
+                    Array.from(origRow.cells).forEach((cell, j) => {{
+                        currentRow.cells[j].innerHTML = cell.innerHTML;
+                        currentRow.cells[j].style.cssText = cell.style.cssText;
+                    }});
+                    table.appendChild(currentRow);
+                }});
+                // 重置列顺序
+                table.querySelectorAll('tr').forEach((tr, rowIdx) => {{
+                    if (rowIdx === 0) {{
+                        originalColOrder.forEach((cell, j) => {{
+                            tr.cells[j].innerHTML = cell.innerHTML;
+                        }});
+                    }}
+                }});
+                headerCells.forEach(h => h.dataset.sort = '');
+                location.reload(); // 简单方案:刷新页面
+            }});
+        }}
+    }});
+    </script>
 </body>
 </html>
 """
@@ -372,4 +694,4 @@ with open(html_file, 'w', encoding='utf-8') as f:
     f.write(html_content)
 
 print(f"\nHTML 报告已生成: {html_file}")
-print(f"包含矩阵: 回流率、点击UV、进入推荐率")
+print(f"包含矩阵: 回流率、点击UV、进入分发率")