|
|
@@ -34,67 +34,130 @@ print(f"分析文件: {latest_file.name}")
|
|
|
print(f"时间范围: {df['dt'].min()} ~ {df['dt'].max()}")
|
|
|
|
|
|
# ============================================================
|
|
|
-# 数据准备
|
|
|
+# 日期列表
|
|
|
# ============================================================
|
|
|
|
|
|
-# 渠道整体
|
|
|
-channel_stats = df.groupby('channel').agg({
|
|
|
- '点击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 = 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)
|
|
|
-
|
|
|
-# 渠道×品类
|
|
|
+all_dates = sorted([str(d) for d in df['dt'].unique()])
|
|
|
+date_options = ['all'] + all_dates # 'all' 表示汇总
|
|
|
+latest_date = all_dates[-1] if all_dates else 'all'
|
|
|
+
|
|
|
+print(f"日期数: {len(all_dates)}")
|
|
|
+
|
|
|
+# ============================================================
|
|
|
+# 数据准备函数
|
|
|
+# ============================================================
|
|
|
+
|
|
|
+def calc_channel_stats(data):
|
|
|
+ """计算渠道整体数据"""
|
|
|
+ stats = data.groupby('channel').agg({'点击uv': 'sum', '裂变uv': 'sum'}).reset_index()
|
|
|
+ for ch in stats['channel']:
|
|
|
+ ch_df = data[data['channel'] == ch]
|
|
|
+ uv = ch_df['点击uv'].sum()
|
|
|
+ if uv > 0:
|
|
|
+ stats.loc[stats['channel'] == ch, '进入分发率'] = (ch_df['进入分发率'] * ch_df['点击uv']).sum() / uv
|
|
|
+ stats.loc[stats['channel'] == ch, '整体裂变率'] = (ch_df['整体裂变率'] * ch_df['点击uv']).sum() / uv
|
|
|
+ stats.loc[stats['channel'] == ch, '头部裂变率'] = (ch_df['头部裂变率'] * ch_df['点击uv']).sum() / uv
|
|
|
+ stats.loc[stats['channel'] == ch, '推荐裂变率'] = (ch_df['推荐裂变率'] * ch_df['点击uv']).sum() / uv
|
|
|
+ return stats.sort_values('点击uv', ascending=False)
|
|
|
+
|
|
|
+def calc_category_stats(data):
|
|
|
+ """计算一级品类整体数据"""
|
|
|
+ stats = data.groupby('merge一级品类').agg({'点击uv': 'sum', '裂变uv': 'sum'}).reset_index()
|
|
|
+ for cat in stats['merge一级品类']:
|
|
|
+ cat_df = data[data['merge一级品类'] == cat]
|
|
|
+ uv = cat_df['点击uv'].sum()
|
|
|
+ if uv > 0:
|
|
|
+ stats.loc[stats['merge一级品类'] == cat, '进入分发率'] = (cat_df['进入分发率'] * cat_df['点击uv']).sum() / uv
|
|
|
+ stats.loc[stats['merge一级品类'] == cat, '整体裂变率'] = (cat_df['整体裂变率'] * cat_df['点击uv']).sum() / uv
|
|
|
+ stats.loc[stats['merge一级品类'] == cat, '头部裂变率'] = (cat_df['头部裂变率'] * cat_df['点击uv']).sum() / uv
|
|
|
+ stats.loc[stats['merge一级品类'] == cat, '推荐裂变率'] = (cat_df['推荐裂变率'] * cat_df['点击uv']).sum() / uv
|
|
|
+ return stats.sort_values('点击uv', ascending=False)
|
|
|
+
|
|
|
+def calc_cat2_stats(data):
|
|
|
+ """计算二级品类整体数据"""
|
|
|
+ stats = data.groupby(['merge一级品类', 'merge二级品类']).agg({'点击uv': 'sum', '裂变uv': 'sum'}).reset_index()
|
|
|
+ stats['cat2_label'] = stats.apply(
|
|
|
+ lambda r: f"{str(r['merge一级品类'])}/{str(r['merge二级品类'])}"
|
|
|
+ if pd.notna(r['merge二级品类']) else str(r['merge一级品类']), axis=1
|
|
|
+ )
|
|
|
+ for idx in stats.index:
|
|
|
+ cat1, cat2 = stats.loc[idx, 'merge一级品类'], stats.loc[idx, 'merge二级品类']
|
|
|
+ mask = (data['merge一级品类'] == cat1) & (data['merge二级品类'] == cat2) if pd.notna(cat2) else (data['merge一级品类'] == cat1) & (data['merge二级品类'].isna())
|
|
|
+ cat_df = data[mask]
|
|
|
+ uv = cat_df['点击uv'].sum()
|
|
|
+ if uv > 0:
|
|
|
+ stats.loc[idx, '进入分发率'] = (cat_df['进入分发率'] * cat_df['点击uv']).sum() / uv
|
|
|
+ stats.loc[idx, '整体裂变率'] = (cat_df['整体裂变率'] * cat_df['点击uv']).sum() / uv
|
|
|
+ stats.loc[idx, '头部裂变率'] = (cat_df['头部裂变率'] * cat_df['点击uv']).sum() / uv
|
|
|
+ stats.loc[idx, '推荐裂变率'] = (cat_df['推荐裂变率'] * cat_df['点击uv']).sum() / uv
|
|
|
+ return stats.sort_values('点击uv', ascending=False)
|
|
|
+
|
|
|
+# ============================================================
|
|
|
+# 为每个日期计算数据
|
|
|
+# ============================================================
|
|
|
+
|
|
|
+daily_data = {}
|
|
|
+for dt in date_options:
|
|
|
+ data = df if dt == 'all' else df[df['dt'].astype(str) == dt]
|
|
|
+ daily_data[dt] = {
|
|
|
+ 'channel_stats': calc_channel_stats(data),
|
|
|
+ 'category_stats': calc_category_stats(data),
|
|
|
+ 'cat2_stats': calc_cat2_stats(data),
|
|
|
+ }
|
|
|
+
|
|
|
+# 使用汇总数据作为默认
|
|
|
+channel_stats = daily_data['all']['channel_stats']
|
|
|
+category_stats = daily_data['all']['category_stats']
|
|
|
+cat2_overall = daily_data['all']['cat2_stats']
|
|
|
+
|
|
|
+# 矩阵数据计算函数
|
|
|
+def calc_channel_category(data):
|
|
|
+ """计算渠道×一级品类矩阵"""
|
|
|
+ cc = data.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()
|
|
|
+ return {
|
|
|
+ 'ror': cc.pivot(index='merge一级品类', columns='channel', values='整体裂变率'),
|
|
|
+ 'ror_orig': cc.pivot(index='merge一级品类', columns='channel', values='头部裂变率'),
|
|
|
+ 'ror_rec': cc.pivot(index='merge一级品类', columns='channel', values='推荐裂变率'),
|
|
|
+ 'uv': cc.pivot(index='merge一级品类', columns='channel', values='点击uv').fillna(0),
|
|
|
+ 'recommend': cc.pivot(index='merge一级品类', columns='channel', values='进入分发率'),
|
|
|
+ }
|
|
|
+
|
|
|
+def calc_channel_cat2(data):
|
|
|
+ """计算渠道×二级品类矩阵"""
|
|
|
+ cc2 = data.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
|
|
|
+ }), include_groups=False
|
|
|
+ ).reset_index()
|
|
|
+ cc2['cat2_label'] = cc2.apply(
|
|
|
+ lambda r: f"{str(r['merge一级品类'])}/{str(r['merge二级品类'])}"
|
|
|
+ if pd.notna(r['merge二级品类']) else str(r['merge一级品类']), axis=1
|
|
|
+ )
|
|
|
+ return {
|
|
|
+ 'ror': cc2.pivot_table(index='cat2_label', columns='channel', values='整体裂变率'),
|
|
|
+ 'ror_orig': cc2.pivot_table(index='cat2_label', columns='channel', values='头部裂变率'),
|
|
|
+ 'ror_rec': cc2.pivot_table(index='cat2_label', columns='channel', values='推荐裂变率'),
|
|
|
+ 'uv': cc2.pivot_table(index='cat2_label', columns='channel', values='点击uv', fill_value=0),
|
|
|
+ }
|
|
|
+
|
|
|
+# 为每个日期计算矩阵数据
|
|
|
+for dt in date_options:
|
|
|
+ data = df if dt == 'all' else df[df['dt'].astype(str) == dt]
|
|
|
+ daily_data[dt]['matrix'] = calc_channel_category(data)
|
|
|
+ daily_data[dt]['matrix2'] = calc_channel_cat2(data)
|
|
|
+
|
|
|
+# 使用汇总数据
|
|
|
channel_category = df.groupby(['channel', 'merge一级品类']).apply(
|
|
|
lambda x: pd.Series({
|
|
|
'点击uv': x['点击uv'].sum(),
|
|
|
@@ -109,267 +172,151 @@ pivot_ror_orig = channel_category.pivot(index='merge一级品类', columns='chan
|
|
|
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
|
|
|
- }), include_groups=False
|
|
|
-).reset_index()
|
|
|
-
|
|
|
# ============================================================
|
|
|
# 统一渐变颜色函数
|
|
|
# ============================================================
|
|
|
|
|
|
def get_gradient_color(val, max_val, min_val=0):
|
|
|
- """
|
|
|
- 统一的渐变颜色:白色(min) -> 绿色(max)
|
|
|
- 所有矩阵使用相同的渐变逻辑,统一黑色字体
|
|
|
- """
|
|
|
+ """统一的渐变颜色:白色(min) -> 绿色(max)"""
|
|
|
if val is None or pd.isna(val) or val <= min_val:
|
|
|
return "background: #f8f9fa"
|
|
|
ratio = min((val - min_val) / (max_val - min_val), 1.0)
|
|
|
- # 白色 (255,255,255) -> 绿色 (40,167,69)
|
|
|
r = int(255 - ratio * 215)
|
|
|
g = int(255 - ratio * 88)
|
|
|
b = int(255 - ratio * 186)
|
|
|
return f"background: rgb({r},{g},{b})"
|
|
|
|
|
|
# ============================================================
|
|
|
-# 准备图表数据
|
|
|
+# 表格HTML生成函数
|
|
|
# ============================================================
|
|
|
|
|
|
-# 主要渠道(UV > 10000)
|
|
|
-main_channels = channel_stats[channel_stats['点击uv'] > 10000]['channel'].tolist()
|
|
|
-valid_categories = pivot_uv[pivot_uv.sum(axis=1) >= 1000].index.tolist()
|
|
|
-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():
|
|
|
- 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='{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}</th>" for c in heatmap_cols]) + "</tr>"
|
|
|
-
|
|
|
-# 2.1 整体裂变率
|
|
|
-ror_rows = []
|
|
|
-for cat in valid_categories:
|
|
|
- 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)
|
|
|
- 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}</th>" for c in heatmap_cols]) + "</tr>"
|
|
|
-uv_rows = []
|
|
|
-for cat in valid_categories:
|
|
|
- cells = [f"<td>{str(cat)}</td>"]
|
|
|
- for ch in heatmap_cols:
|
|
|
- if ch in pivot_uv.columns:
|
|
|
- val = pivot_uv.loc[cat, ch]
|
|
|
- if val > 0:
|
|
|
- style = get_gradient_color(val, 100000) # UV max=10万
|
|
|
- cells.append(f'<td style="{style}">{int(val):,}</td>')
|
|
|
+def gen_channel_rows(stats):
|
|
|
+ """生成渠道表格行"""
|
|
|
+ rows = []
|
|
|
+ max_uv = stats['点击uv'].max() if len(stats) > 0 else 1
|
|
|
+ for _, row in stats.iterrows():
|
|
|
+ uv_style = get_gradient_color(row['点击uv'], max_uv)
|
|
|
+ rec_style = get_gradient_color(row.get('进入分发率', 0), 1.0, 0.6)
|
|
|
+ ror_style = get_gradient_color(row.get('整体裂变率', 0), 0.8)
|
|
|
+ orig_style = get_gradient_color(row.get('头部裂变率', 0), 0.3)
|
|
|
+ rec_ror_style = get_gradient_color(row.get('推荐裂变率', 0), 0.5)
|
|
|
+ rows.append(
|
|
|
+ f"<tr><td>{row['channel']}</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>"
|
|
|
+ )
|
|
|
+ return "".join(rows)
|
|
|
+
|
|
|
+def gen_category_rows(stats):
|
|
|
+ """生成品类表格行"""
|
|
|
+ rows = []
|
|
|
+ max_uv = stats['点击uv'].max() if len(stats) > 0 else 1
|
|
|
+ for _, row in stats.iterrows():
|
|
|
+ cat_name = str(row['merge一级品类']) if pd.notna(row['merge一级品类']) else '-'
|
|
|
+ uv_style = get_gradient_color(row['点击uv'], max_uv)
|
|
|
+ rec_style = get_gradient_color(row.get('进入分发率', 0), 1.0, 0.6)
|
|
|
+ ror_style = get_gradient_color(row.get('整体裂变率', 0), 0.8)
|
|
|
+ orig_style = get_gradient_color(row.get('头部裂变率', 0), 0.3)
|
|
|
+ rec_ror_style = get_gradient_color(row.get('推荐裂变率', 0), 0.5)
|
|
|
+ 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.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>"
|
|
|
+ )
|
|
|
+ return "".join(rows)
|
|
|
+
|
|
|
+def gen_cat2_rows(stats):
|
|
|
+ """生成二级品类表格行"""
|
|
|
+ rows = []
|
|
|
+ max_uv = stats['点击uv'].max() if len(stats) > 0 else 1
|
|
|
+ for _, row in stats.iterrows():
|
|
|
+ uv_style = get_gradient_color(row['点击uv'], max_uv)
|
|
|
+ rec_style = get_gradient_color(row.get('进入分发率', 0), 1.0, 0.6)
|
|
|
+ ror_style = get_gradient_color(row.get('整体裂变率', 0), 0.8)
|
|
|
+ orig_style = get_gradient_color(row.get('头部裂变率', 0), 0.3)
|
|
|
+ rec_ror_style = get_gradient_color(row.get('推荐裂变率', 0), 0.5)
|
|
|
+ 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>"
|
|
|
+ )
|
|
|
+ return "".join(rows)
|
|
|
+
|
|
|
+def gen_matrix_rows(pivot_df, valid_rows, cols, max_val, min_val=0, is_int=False):
|
|
|
+ """生成矩阵热力图行"""
|
|
|
+ rows = []
|
|
|
+ for cat in valid_rows:
|
|
|
+ if cat not in pivot_df.index:
|
|
|
+ continue
|
|
|
+ cells = [f"<td>{str(cat)}</td>"]
|
|
|
+ for ch in cols:
|
|
|
+ if ch in pivot_df.columns and pd.notna(pivot_df.loc[cat, ch]):
|
|
|
+ val = pivot_df.loc[cat, ch]
|
|
|
+ style = get_gradient_color(val, max_val, min_val)
|
|
|
+ if is_int:
|
|
|
+ cells.append(f'<td style="{style}">{int(val):,}</td>' if val > 0 else '<td>-</td>')
|
|
|
+ else:
|
|
|
+ cells.append(f'<td style="{style}">{val:.4f}</td>')
|
|
|
else:
|
|
|
cells.append("<td>-</td>")
|
|
|
- else:
|
|
|
- cells.append("<td>-</td>")
|
|
|
- uv_rows.append("<tr>" + "".join(cells) + "</tr>")
|
|
|
-
|
|
|
-# 4. 进入分发率热力图(使用统一渐变)
|
|
|
-pivot_recommend = df.groupby(['merge一级品类', 'channel']).apply(
|
|
|
- lambda x: (x['进入分发率'] * x['点击uv']).sum() / x['点击uv'].sum() if x['点击uv'].sum() > 0 else 0,
|
|
|
- include_groups=False
|
|
|
-).unstack()
|
|
|
-
|
|
|
-recommend_rows = []
|
|
|
-for cat in valid_categories:
|
|
|
- if cat not in pivot_recommend.index:
|
|
|
- continue
|
|
|
- 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]
|
|
|
- style = get_gradient_color(val, 1.0, 0.6) # 推荐率 0.6~1.0
|
|
|
- cells.append(f'<td style="{style}">{val:.4f}</td>')
|
|
|
- else:
|
|
|
- cells.append("<td>-</td>")
|
|
|
- recommend_rows.append("<tr>" + "".join(cells) + "</tr>")
|
|
|
-
|
|
|
-# 5. 二级品类热力图
|
|
|
-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
|
|
|
- }), include_groups=False
|
|
|
-).reset_index()
|
|
|
+ rows.append("<tr>" + "".join(cells) + "</tr>")
|
|
|
+ return "".join(rows)
|
|
|
|
|
|
-# 创建二级品类标签
|
|
|
-channel_cat2['cat2_label'] = channel_cat2.apply(
|
|
|
- lambda r: f"{str(r['merge一级品类'])}/{str(r['merge二级品类'])}"
|
|
|
- if pd.notna(r['merge二级品类']) else str(r['merge一级品类']), axis=1
|
|
|
-)
|
|
|
+# ============================================================
|
|
|
+# 为每个日期生成表格HTML
|
|
|
+# ============================================================
|
|
|
|
|
|
-# 二级品类汇总
|
|
|
-cat2_stats = channel_cat2.groupby('cat2_label').agg({'点击uv': 'sum'}).reset_index()
|
|
|
-cat2_stats = cat2_stats[cat2_stats['点击uv'] >= 1000].sort_values('点击uv', ascending=False)
|
|
|
-valid_cat2_labels = cat2_stats.head(20)['cat2_label'].tolist()
|
|
|
+# 先计算汇总数据确定有效品类和渠道
|
|
|
+all_channel_stats = daily_data['all']['channel_stats']
|
|
|
+main_channels = all_channel_stats[all_channel_stats['点击uv'] > 10000]['channel'].tolist()
|
|
|
+all_matrix = daily_data['all']['matrix']
|
|
|
+valid_categories = all_matrix['uv'][all_matrix['uv'].sum(axis=1) >= 1000].index.tolist()
|
|
|
+heatmap_cols = [c for c in main_channels if c in all_matrix['ror'].columns]
|
|
|
+
|
|
|
+# 二级品类有效列表
|
|
|
+all_matrix2 = daily_data['all']['matrix2']
|
|
|
+cat2_uv_sum = all_matrix2['uv'].sum(axis=1)
|
|
|
+valid_cat2_labels = cat2_uv_sum[cat2_uv_sum >= 1000].sort_values(ascending=False).head(20).index.tolist()
|
|
|
+
|
|
|
+daily_html = {}
|
|
|
+for dt in date_options:
|
|
|
+ data = daily_data[dt]
|
|
|
+ matrix = data['matrix']
|
|
|
+ matrix2 = data['matrix2']
|
|
|
+
|
|
|
+ daily_html[dt] = {
|
|
|
+ 'channel_rows': gen_channel_rows(data['channel_stats']),
|
|
|
+ 'category_rows': gen_category_rows(data['category_stats']),
|
|
|
+ 'cat2_rows': gen_cat2_rows(data['cat2_stats']),
|
|
|
+ # 渠道×一级品类矩阵
|
|
|
+ 'matrix_ror': gen_matrix_rows(matrix['ror'], valid_categories, heatmap_cols, 0.8),
|
|
|
+ 'matrix_ror_orig': gen_matrix_rows(matrix['ror_orig'], valid_categories, heatmap_cols, 0.3),
|
|
|
+ 'matrix_ror_rec': gen_matrix_rows(matrix['ror_rec'], valid_categories, heatmap_cols, 0.5),
|
|
|
+ 'matrix_uv': gen_matrix_rows(matrix['uv'], valid_categories, heatmap_cols, 100000, is_int=True),
|
|
|
+ 'matrix_recommend': gen_matrix_rows(matrix['recommend'], valid_categories, heatmap_cols, 1.0, 0.6),
|
|
|
+ # 渠道×二级品类矩阵
|
|
|
+ 'matrix2_ror': gen_matrix_rows(matrix2['ror'], valid_cat2_labels, heatmap_cols, 0.8),
|
|
|
+ 'matrix2_ror_orig': gen_matrix_rows(matrix2['ror_orig'], valid_cat2_labels, heatmap_cols, 0.3),
|
|
|
+ 'matrix2_ror_rec': gen_matrix_rows(matrix2['ror_rec'], valid_cat2_labels, heatmap_cols, 0.5),
|
|
|
+ 'matrix2_uv': gen_matrix_rows(matrix2['uv'], valid_cat2_labels, heatmap_cols, 100000, is_int=True),
|
|
|
+ }
|
|
|
|
|
|
-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)
|
|
|
+# ============================================================
|
|
|
+# 生成表头
|
|
|
+# ============================================================
|
|
|
|
|
|
-# 二级品类回流率热力图(使用统一渐变)
|
|
|
+ror_header = "<tr><th>品类</th>" + "".join([f"<th>{c}</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}</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)
|
|
|
- cells.append(f'<td style="{style}">{val:.4f}</td>')
|
|
|
- else:
|
|
|
- cells.append("<td>-</td>")
|
|
|
- cat2_ror_rows.append("<tr>" + "".join(cells) + "</tr>")
|
|
|
-
|
|
|
-# 头部裂变率
|
|
|
-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}</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)
|
|
|
- cells.append(f'<td style="{style}">{int(val):,}</td>')
|
|
|
- else:
|
|
|
- cells.append("<td>-</td>")
|
|
|
- else:
|
|
|
- cells.append("<td>-</td>")
|
|
|
- cat2_uv_rows.append("<tr>" + "".join(cells) + "</tr>")
|
|
|
-
|
|
|
# ============================================================
|
|
|
# 生成 HTML
|
|
|
# ============================================================
|
|
|
@@ -380,6 +327,14 @@ channel_count = df['channel'].nunique()
|
|
|
category_count = df['merge一级品类'].nunique()
|
|
|
date_range = f"{df['dt'].min()} ~ {df['dt'].max()}"
|
|
|
|
|
|
+# 日期选项HTML
|
|
|
+date_options_html = "".join([
|
|
|
+ f'<option value="{dt}" {"selected" if dt == latest_date else ""}>'
|
|
|
+ f'{"汇总" if dt == "all" else dt}</option>'
|
|
|
+ for dt in date_options
|
|
|
+])
|
|
|
+dates_json = json.dumps(date_options)
|
|
|
+
|
|
|
html_content = f"""<!DOCTYPE html>
|
|
|
<html>
|
|
|
<head>
|
|
|
@@ -429,7 +384,37 @@ html_content = f"""<!DOCTYPE html>
|
|
|
.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>
|
|
|
+ .date-switcher {{ display: inline-flex; align-items: center; gap: 5px; margin-left: 15px; }}
|
|
|
+ .date-switcher select {{ padding: 4px 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 13px; }}
|
|
|
+ .date-switcher button {{ padding: 4px 10px; border: 1px solid #ccc; border-radius: 4px;
|
|
|
+ background: white; cursor: pointer; font-size: 13px; }}
|
|
|
+ .date-switcher button:hover:not(:disabled) {{ background: #e9ecef; }}
|
|
|
+ .date-switcher button:disabled {{ opacity: 0.5; cursor: not-allowed; }}
|
|
|
+ .date-switcher .play-btn {{ background: #28a745; color: white; border-color: #28a745; }}
|
|
|
+ .date-switcher .play-btn:hover {{ background: #218838; }}
|
|
|
+ .date-switcher .play-btn.playing {{ background: #dc3545; border-color: #dc3545; }}
|
|
|
+ .global-date {{ background: #e3f2fd; padding: 10px 15px; border-radius: 8px; margin: 15px 0;
|
|
|
+ display: flex; align-items: center; }}
|
|
|
+ .global-date label {{ font-weight: 600; margin-right: 10px; }}
|
|
|
+ .table-date {{ float: right; }}
|
|
|
+ .date-content {{ display: none; opacity: 0; }}
|
|
|
+ .date-content.active {{ display: block; opacity: 1; }}
|
|
|
+ .date-content table tr {{ transition: transform 0.5s ease, opacity 0.3s ease; }}
|
|
|
+ .date-content.animating table tr {{ transform: translateY(0); }}
|
|
|
+ @keyframes slideUpColor {{
|
|
|
+ 0% {{ transform: translateY(20px); opacity: 0; color: #28a745; }}
|
|
|
+ 40% {{ transform: translateY(0); opacity: 1; color: #28a745; }}
|
|
|
+ 100% {{ transform: translateY(0); opacity: 1; color: inherit; }}
|
|
|
+ }}
|
|
|
+ @keyframes slideDownColor {{
|
|
|
+ 0% {{ transform: translateY(-20px); opacity: 0; color: #dc3545; }}
|
|
|
+ 40% {{ transform: translateY(0); opacity: 1; color: #dc3545; }}
|
|
|
+ 100% {{ transform: translateY(0); opacity: 1; color: inherit; }}
|
|
|
+ }}
|
|
|
+ .row-up {{ animation: slideUpColor 1s ease forwards; }}
|
|
|
+ .row-down {{ animation: slideDownColor 1s ease forwards; }}
|
|
|
+ .row-same {{ animation: none; }}
|
|
|
+ </style>
|
|
|
</head>
|
|
|
<body>
|
|
|
<nav class="sidebar">
|
|
|
@@ -476,118 +461,194 @@ html_content = f"""<!DOCTYPE html>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
+ <div class="global-date">
|
|
|
+ <label>全局日期切换:</label>
|
|
|
+ <div class="date-switcher" id="global-switcher">
|
|
|
+ <button class="prev-btn" title="前一天">◀</button>
|
|
|
+ <select class="date-select">{date_options_html}</select>
|
|
|
+ <button class="next-btn" title="后一天">▶</button>
|
|
|
+ <button class="play-btn" title="播放">▶ 播放</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
<h2 id="sec1">一、整体表现</h2>
|
|
|
|
|
|
<h3 id="sec1-1">1.1 渠道整体</h3>
|
|
|
- <div class="chart-container">
|
|
|
- <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 class="chart-container" data-section="channel">
|
|
|
+ <div class="legend">
|
|
|
+ 颜色越深=数值越高 | 点击表头排序
|
|
|
+ <div class="date-switcher table-date" data-section="channel">
|
|
|
+ <button class="prev-btn">◀</button>
|
|
|
+ <select class="date-select">{date_options_html}</select>
|
|
|
+ <button class="next-btn">▶</button>
|
|
|
+ <button class="play-btn">▶</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ {"".join([f'''<div class="date-content{'active' if dt == latest_date else ''}" data-date="{dt}">
|
|
|
+ <table class="sortable"><tr><th>渠道</th><th>点击UV</th><th>进入分发率</th><th>整体裂变率</th><th>头部裂变率</th><th>推荐裂变率</th></tr>
|
|
|
+ {daily_html[dt]['channel_rows']}</table></div>''' for dt in date_options])}
|
|
|
</div>
|
|
|
|
|
|
<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 class="chart-container" data-section="category">
|
|
|
+ <div class="legend">
|
|
|
+ 颜色越深=数值越高 | 点击表头排序
|
|
|
+ <div class="date-switcher table-date" data-section="category">
|
|
|
+ <button class="prev-btn">◀</button>
|
|
|
+ <select class="date-select">{date_options_html}</select>
|
|
|
+ <button class="next-btn">▶</button>
|
|
|
+ <button class="play-btn">▶</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ {"".join([f'''<div class="date-content{' active' if dt == latest_date else ''}" data-date="{dt}">
|
|
|
+ <table class="sortable"><tr><th>一级品类</th><th>点击UV</th><th>进入分发率</th><th>整体裂变率</th><th>头部裂变率</th><th>推荐裂变率</th></tr>
|
|
|
+ {daily_html[dt]['category_rows']}</table></div>''' for dt in date_options])}
|
|
|
</div>
|
|
|
|
|
|
<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 class="chart-container" data-section="cat2">
|
|
|
+ <div class="legend">
|
|
|
+ 颜色越深=数值越高 | 点击表头排序
|
|
|
+ <div class="date-switcher table-date" data-section="cat2">
|
|
|
+ <button class="prev-btn">◀</button>
|
|
|
+ <select class="date-select">{date_options_html}</select>
|
|
|
+ <button class="next-btn">▶</button>
|
|
|
+ <button class="play-btn">▶</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ {"".join([f'''<div class="date-content{' active' if dt == latest_date else ''}" data-date="{dt}">
|
|
|
+ <table class="sortable"><tr><th>二级品类</th><th>点击UV</th><th>进入分发率</th><th>整体裂变率</th><th>头部裂变率</th><th>推荐裂变率</th></tr>
|
|
|
+ {daily_html[dt]['cat2_rows']}</table></div>''' for dt in date_options])}
|
|
|
</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 | 点击表头/行名排序 <button class="reset-btn" id="reset-3">重置</button></div>
|
|
|
- <table class="sortable">
|
|
|
- {ror_header}
|
|
|
- {"".join(ror_rows)}
|
|
|
- </table>
|
|
|
+ <div class="chart-container heatmap matrix-section" data-section="m1_ror">
|
|
|
+ <div class="legend">max=0.80 | 点击表头/行名排序
|
|
|
+ <div class="date-switcher table-date" data-section="m1_ror">
|
|
|
+ <button class="prev-btn">◀</button>
|
|
|
+ <select class="date-select">{date_options_html}</select>
|
|
|
+ <button class="next-btn">▶</button>
|
|
|
+ <button class="play-btn">▶</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ {"".join([f'''<div class="date-content{' active' if dt == latest_date else ''}" data-date="{dt}">
|
|
|
+ <table class="sortable">{ror_header}{daily_html[dt]['matrix_ror']}</table></div>''' for dt in date_options])}
|
|
|
</div>
|
|
|
|
|
|
<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 class="chart-container heatmap matrix-section" data-section="m1_orig">
|
|
|
+ <div class="legend">max=0.30 | 点击表头/行名排序
|
|
|
+ <div class="date-switcher table-date" data-section="m1_orig">
|
|
|
+ <button class="prev-btn">◀</button>
|
|
|
+ <select class="date-select">{date_options_html}</select>
|
|
|
+ <button class="next-btn">▶</button>
|
|
|
+ <button class="play-btn">▶</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ {"".join([f'''<div class="date-content{' active' if dt == latest_date else ''}" data-date="{dt}">
|
|
|
+ <table class="sortable">{ror_header}{daily_html[dt]['matrix_ror_orig']}</table></div>''' for dt in date_options])}
|
|
|
</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 class="chart-container heatmap matrix-section" data-section="m1_rec">
|
|
|
+ <div class="legend">max=0.50 | 点击表头/行名排序
|
|
|
+ <div class="date-switcher table-date" data-section="m1_rec">
|
|
|
+ <button class="prev-btn">◀</button>
|
|
|
+ <select class="date-select">{date_options_html}</select>
|
|
|
+ <button class="next-btn">▶</button>
|
|
|
+ <button class="play-btn">▶</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ {"".join([f'''<div class="date-content{' active' if dt == latest_date else ''}" data-date="{dt}">
|
|
|
+ <table class="sortable">{ror_header}{daily_html[dt]['matrix_ror_rec']}</table></div>''' for dt in date_options])}
|
|
|
</div>
|
|
|
|
|
|
<h3 id="sec2-4">2.4 点击UV</h3>
|
|
|
- <div class="chart-container heatmap matrix-section">
|
|
|
- <div class="legend">max=10万 | 点击表头/行名排序 <button class="reset-btn" id="reset-6">重置</button></div>
|
|
|
- <table class="sortable">
|
|
|
- {uv_header}
|
|
|
- {"".join(uv_rows)}
|
|
|
- </table>
|
|
|
+ <div class="chart-container heatmap matrix-section" data-section="m1_uv">
|
|
|
+ <div class="legend">max=10万 | 点击表头/行名排序
|
|
|
+ <div class="date-switcher table-date" data-section="m1_uv">
|
|
|
+ <button class="prev-btn">◀</button>
|
|
|
+ <select class="date-select">{date_options_html}</select>
|
|
|
+ <button class="next-btn">▶</button>
|
|
|
+ <button class="play-btn">▶</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ {"".join([f'''<div class="date-content{' active' if dt == latest_date else ''}" data-date="{dt}">
|
|
|
+ <table class="sortable">{ror_header}{daily_html[dt]['matrix_uv']}</table></div>''' for dt in date_options])}
|
|
|
</div>
|
|
|
|
|
|
<h3 id="sec2-5">2.5 进入分发率</h3>
|
|
|
- <div class="chart-container heatmap matrix-section">
|
|
|
- <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 class="chart-container heatmap matrix-section" data-section="m1_recommend">
|
|
|
+ <div class="legend">min=0.60, max=1.00 | 点击表头/行名排序
|
|
|
+ <div class="date-switcher table-date" data-section="m1_recommend">
|
|
|
+ <button class="prev-btn">◀</button>
|
|
|
+ <select class="date-select">{date_options_html}</select>
|
|
|
+ <button class="next-btn">▶</button>
|
|
|
+ <button class="play-btn">▶</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ {"".join([f'''<div class="date-content{' active' if dt == latest_date else ''}" data-date="{dt}">
|
|
|
+ <table class="sortable">{ror_header}{daily_html[dt]['matrix_recommend']}</table></div>''' for dt in date_options])}
|
|
|
</div>
|
|
|
|
|
|
<h2 id="sec3">三、渠道×二级品类</h2>
|
|
|
|
|
|
<h3 id="sec3-1">3.1 整体裂变率</h3>
|
|
|
- <div class="chart-container heatmap matrix-section">
|
|
|
- <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 class="chart-container heatmap matrix-section" data-section="m2_ror">
|
|
|
+ <div class="legend">max=0.80 | 点击表头/行名排序
|
|
|
+ <div class="date-switcher table-date" data-section="m2_ror">
|
|
|
+ <button class="prev-btn">◀</button>
|
|
|
+ <select class="date-select">{date_options_html}</select>
|
|
|
+ <button class="next-btn">▶</button>
|
|
|
+ <button class="play-btn">▶</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ {"".join([f'''<div class="date-content{' active' if dt == latest_date else ''}" data-date="{dt}">
|
|
|
+ <table class="sortable">{cat2_ror_header}{daily_html[dt]['matrix2_ror']}</table></div>''' for dt in date_options])}
|
|
|
</div>
|
|
|
|
|
|
<h3 id="sec3-2">3.2 头部裂变率</h3>
|
|
|
- <div class="chart-container heatmap matrix-section">
|
|
|
- <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 class="chart-container heatmap matrix-section" data-section="m2_orig">
|
|
|
+ <div class="legend">max=0.30 | 点击表头/行名排序
|
|
|
+ <div class="date-switcher table-date" data-section="m2_orig">
|
|
|
+ <button class="prev-btn">◀</button>
|
|
|
+ <select class="date-select">{date_options_html}</select>
|
|
|
+ <button class="next-btn">▶</button>
|
|
|
+ <button class="play-btn">▶</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ {"".join([f'''<div class="date-content{' active' if dt == latest_date else ''}" data-date="{dt}">
|
|
|
+ <table class="sortable">{cat2_ror_header}{daily_html[dt]['matrix2_ror_orig']}</table></div>''' for dt in date_options])}
|
|
|
</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 class="chart-container heatmap matrix-section" data-section="m2_rec">
|
|
|
+ <div class="legend">max=0.50 | 点击表头/行名排序
|
|
|
+ <div class="date-switcher table-date" data-section="m2_rec">
|
|
|
+ <button class="prev-btn">◀</button>
|
|
|
+ <select class="date-select">{date_options_html}</select>
|
|
|
+ <button class="next-btn">▶</button>
|
|
|
+ <button class="play-btn">▶</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ {"".join([f'''<div class="date-content{' active' if dt == latest_date else ''}" data-date="{dt}">
|
|
|
+ <table class="sortable">{cat2_ror_header}{daily_html[dt]['matrix2_ror_rec']}</table></div>''' for dt in date_options])}
|
|
|
</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 class="chart-container heatmap matrix-section" data-section="m2_uv">
|
|
|
+ <div class="legend">max=10万 | 点击表头/行名排序
|
|
|
+ <div class="date-switcher table-date" data-section="m2_uv">
|
|
|
+ <button class="prev-btn">◀</button>
|
|
|
+ <select class="date-select">{date_options_html}</select>
|
|
|
+ <button class="next-btn">▶</button>
|
|
|
+ <button class="play-btn">▶</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ {"".join([f'''<div class="date-content{' active' if dt == latest_date else ''}" data-date="{dt}">
|
|
|
+ <table class="sortable">{cat2_ror_header}{daily_html[dt]['matrix2_uv']}</table></div>''' for dt in date_options])}
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
@@ -684,6 +745,156 @@ html_content = f"""<!DOCTYPE html>
|
|
|
}});
|
|
|
}}
|
|
|
}});
|
|
|
+
|
|
|
+ // ============================================================
|
|
|
+ // 日期切换功能
|
|
|
+ // ============================================================
|
|
|
+ const dates = {dates_json};
|
|
|
+ let playIntervals = {{}};
|
|
|
+
|
|
|
+ // 存储每个section当前的行顺序
|
|
|
+ const sectionRowOrder = {{}};
|
|
|
+
|
|
|
+ function getRowKeys(container) {{
|
|
|
+ const active = container.querySelector('.date-content.active');
|
|
|
+ if (!active) return [];
|
|
|
+ return Array.from(active.querySelectorAll('table tr')).slice(1).map(tr => tr.cells[0]?.textContent || '');
|
|
|
+ }}
|
|
|
+
|
|
|
+ function switchDate(section, date) {{
|
|
|
+ const container = document.querySelector(`.chart-container[data-section="${{section}}"]`);
|
|
|
+ if (!container) return;
|
|
|
+
|
|
|
+ // 记录切换前的行顺序
|
|
|
+ const oldOrder = getRowKeys(container);
|
|
|
+ const oldOrderMap = {{}};
|
|
|
+ oldOrder.forEach((key, idx) => oldOrderMap[key] = idx);
|
|
|
+
|
|
|
+ // 切换内容显示
|
|
|
+ container.querySelectorAll('.date-content').forEach(el => {{
|
|
|
+ el.classList.toggle('active', el.dataset.date === String(date));
|
|
|
+ }});
|
|
|
+
|
|
|
+ // 获取新的行顺序并应用动画
|
|
|
+ const newContent = container.querySelector('.date-content.active');
|
|
|
+ if (newContent && oldOrder.length > 0) {{
|
|
|
+ const rows = Array.from(newContent.querySelectorAll('table tr')).slice(1);
|
|
|
+ rows.forEach((tr, newIdx) => {{
|
|
|
+ const key = tr.cells[0]?.textContent || '';
|
|
|
+ const oldIdx = oldOrderMap[key];
|
|
|
+ tr.classList.remove('row-up', 'row-down', 'row-same');
|
|
|
+ if (oldIdx === undefined) {{
|
|
|
+ tr.classList.add('row-up'); // 新出现的行
|
|
|
+ }} else if (newIdx < oldIdx) {{
|
|
|
+ tr.classList.add('row-up'); // 排名上升
|
|
|
+ }} else if (newIdx > oldIdx) {{
|
|
|
+ tr.classList.add('row-down'); // 排名下降
|
|
|
+ }} else {{
|
|
|
+ tr.classList.add('row-same');
|
|
|
+ }}
|
|
|
+ }});
|
|
|
+ }}
|
|
|
+
|
|
|
+ // 更新下拉框
|
|
|
+ const switcher = document.querySelector(`.date-switcher[data-section="${{section}}"]`);
|
|
|
+ if (switcher) {{
|
|
|
+ switcher.querySelector('.date-select').value = date;
|
|
|
+ updateNavButtons(switcher);
|
|
|
+ }}
|
|
|
+ }}
|
|
|
+
|
|
|
+ function updateNavButtons(switcher) {{
|
|
|
+ const select = switcher.querySelector('.date-select');
|
|
|
+ const idx = dates.indexOf(select.value);
|
|
|
+ switcher.querySelector('.prev-btn').disabled = (idx <= 0);
|
|
|
+ switcher.querySelector('.next-btn').disabled = (idx >= dates.length - 1);
|
|
|
+ }}
|
|
|
+
|
|
|
+ function switchAll(date) {{
|
|
|
+ // 所有可切换的 section
|
|
|
+ const allSections = [
|
|
|
+ 'channel', 'category', 'cat2',
|
|
|
+ 'm1_ror', 'm1_orig', 'm1_rec', 'm1_uv', 'm1_recommend',
|
|
|
+ 'm2_ror', 'm2_orig', 'm2_rec', 'm2_uv'
|
|
|
+ ];
|
|
|
+ allSections.forEach(section => switchDate(section, date));
|
|
|
+ document.querySelector('#global-switcher .date-select').value = date;
|
|
|
+ updateNavButtons(document.querySelector('#global-switcher'));
|
|
|
+ }}
|
|
|
+
|
|
|
+ function playSection(switcher, section) {{
|
|
|
+ const playBtn = switcher.querySelector('.play-btn');
|
|
|
+ const isGlobal = switcher.id === 'global-switcher';
|
|
|
+ const key = isGlobal ? 'global' : section;
|
|
|
+
|
|
|
+ if (playIntervals[key]) {{
|
|
|
+ clearInterval(playIntervals[key]);
|
|
|
+ playIntervals[key] = null;
|
|
|
+ playBtn.classList.remove('playing');
|
|
|
+ playBtn.textContent = isGlobal ? '▶ 播放' : '▶';
|
|
|
+ return;
|
|
|
+ }}
|
|
|
+
|
|
|
+ playBtn.classList.add('playing');
|
|
|
+ playBtn.textContent = isGlobal ? '⏸ 暂停' : '⏸';
|
|
|
+
|
|
|
+ let idx = 0;
|
|
|
+ const play = () => {{
|
|
|
+ if (idx >= dates.length) {{
|
|
|
+ clearInterval(playIntervals[key]);
|
|
|
+ playIntervals[key] = null;
|
|
|
+ playBtn.classList.remove('playing');
|
|
|
+ playBtn.textContent = isGlobal ? '▶ 播放' : '▶';
|
|
|
+ return;
|
|
|
+ }}
|
|
|
+ if (isGlobal) {{
|
|
|
+ switchAll(dates[idx]);
|
|
|
+ }} else {{
|
|
|
+ switchDate(section, dates[idx]);
|
|
|
+ }}
|
|
|
+ idx++;
|
|
|
+ }};
|
|
|
+ play();
|
|
|
+ playIntervals[key] = setInterval(play, 1500);
|
|
|
+ }}
|
|
|
+
|
|
|
+ // 绑定全局切换器
|
|
|
+ const globalSwitcher = document.querySelector('#global-switcher');
|
|
|
+ globalSwitcher.querySelector('.date-select').addEventListener('change', e => switchAll(e.target.value));
|
|
|
+ globalSwitcher.querySelector('.prev-btn').addEventListener('click', () => {{
|
|
|
+ const select = globalSwitcher.querySelector('.date-select');
|
|
|
+ const idx = dates.indexOf(select.value);
|
|
|
+ if (idx > 0) switchAll(dates[idx - 1]);
|
|
|
+ }});
|
|
|
+ globalSwitcher.querySelector('.next-btn').addEventListener('click', () => {{
|
|
|
+ const select = globalSwitcher.querySelector('.date-select');
|
|
|
+ const idx = dates.indexOf(select.value);
|
|
|
+ if (idx < dates.length - 1) switchAll(dates[idx + 1]);
|
|
|
+ }});
|
|
|
+ globalSwitcher.querySelector('.play-btn').addEventListener('click', () => playSection(globalSwitcher, null));
|
|
|
+ updateNavButtons(globalSwitcher);
|
|
|
+
|
|
|
+ // 绑定各表格切换器
|
|
|
+ document.querySelectorAll('.table-date').forEach(switcher => {{
|
|
|
+ const section = switcher.dataset.section;
|
|
|
+ switcher.querySelector('.date-select').addEventListener('change', e => switchDate(section, e.target.value));
|
|
|
+ switcher.querySelector('.prev-btn').addEventListener('click', () => {{
|
|
|
+ const select = switcher.querySelector('.date-select');
|
|
|
+ const idx = dates.indexOf(select.value);
|
|
|
+ if (idx > 0) {{
|
|
|
+ switchDate(section, dates[idx - 1]);
|
|
|
+ }}
|
|
|
+ }});
|
|
|
+ switcher.querySelector('.next-btn').addEventListener('click', () => {{
|
|
|
+ const select = switcher.querySelector('.date-select');
|
|
|
+ const idx = dates.indexOf(select.value);
|
|
|
+ if (idx < dates.length - 1) {{
|
|
|
+ switchDate(section, dates[idx + 1]);
|
|
|
+ }}
|
|
|
+ }});
|
|
|
+ switcher.querySelector('.play-btn').addEventListener('click', () => playSection(switcher, section));
|
|
|
+ updateNavButtons(switcher);
|
|
|
+ }});
|
|
|
</script>
|
|
|
</body>
|
|
|
</html>
|