|
|
@@ -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、进入分发率")
|