|
@@ -215,27 +215,60 @@ def calc_channel_categories(channel, date=None):
|
|
|
).reset_index()
|
|
).reset_index()
|
|
|
return cats.sort_values('点击uv', ascending=False)
|
|
return cats.sort_values('点击uv', ascending=False)
|
|
|
|
|
|
|
|
-# 计算单渠道的品类×时间矩阵
|
|
|
|
|
-def calc_channel_category_time(channel):
|
|
|
|
|
|
|
+# 计算单渠道的品类×时间矩阵(支持一级/二级品类)
|
|
|
|
|
+def calc_channel_category_time(channel, level='cat1'):
|
|
|
"""计算单渠道下各品类每天的数据"""
|
|
"""计算单渠道下各品类每天的数据"""
|
|
|
ch_df = df[df['channel'] == channel]
|
|
ch_df = df[df['channel'] == channel]
|
|
|
- ct = ch_df.groupby(['merge一级品类', 'dt']).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()
|
|
|
|
|
- # 转为矩阵格式:{品类: {日期: 值}}
|
|
|
|
|
- uv_pivot = ct.pivot(index='merge一级品类', columns='dt', values='点击uv').fillna(0)
|
|
|
|
|
- ror_pivot = ct.pivot(index='merge一级品类', columns='dt', values='整体裂变率')
|
|
|
|
|
|
|
+
|
|
|
|
|
+ if level == 'cat1':
|
|
|
|
|
+ ct = ch_df.groupby(['merge一级品类', 'dt']).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()
|
|
|
|
|
+ cat_col = 'merge一级品类'
|
|
|
|
|
+ else:
|
|
|
|
|
+ ct = ch_df.groupby(['merge一级品类', 'merge二级品类', 'dt']).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()
|
|
|
|
|
+ ct['cat2_label'] = ct.apply(
|
|
|
|
|
+ lambda r: f"{str(r['merge一级品类'])}/{str(r['merge二级品类'])}"
|
|
|
|
|
+ if pd.notna(r['merge二级品类']) else str(r['merge一级品类']), axis=1
|
|
|
|
|
+ )
|
|
|
|
|
+ cat_col = 'cat2_label'
|
|
|
|
|
+
|
|
|
|
|
+ # 转为矩阵格式
|
|
|
|
|
+ uv_pivot = ct.pivot(index=cat_col, columns='dt', values='点击uv').fillna(0)
|
|
|
|
|
+ rec_pivot = ct.pivot(index=cat_col, columns='dt', values='进入分发率')
|
|
|
|
|
+ ror_pivot = ct.pivot(index=cat_col, columns='dt', values='整体裂变率')
|
|
|
|
|
+ orig_pivot = ct.pivot(index=cat_col, columns='dt', values='头部裂变率')
|
|
|
|
|
+ rec_ror_pivot = ct.pivot(index=cat_col, columns='dt', values='推荐裂变率')
|
|
|
|
|
+
|
|
|
# 按总UV排序
|
|
# 按总UV排序
|
|
|
all_cats = uv_pivot.sum(axis=1).sort_values(ascending=False).index.tolist()
|
|
all_cats = uv_pivot.sum(axis=1).sort_values(ascending=False).index.tolist()
|
|
|
dates = sorted([int(d) for d in uv_pivot.columns])
|
|
dates = sorted([int(d) for d in uv_pivot.columns])
|
|
|
|
|
+
|
|
|
|
|
+ def to_dict(pivot, is_int=False):
|
|
|
|
|
+ return {str(cat): {int(d): int(pivot.loc[cat, d]) if is_int else float(pivot.loc[cat, d]) if d in pivot.columns and pd.notna(pivot.loc[cat, d]) else 0 for d in dates} for cat in all_cats}
|
|
|
|
|
+
|
|
|
return {
|
|
return {
|
|
|
'categories': all_cats,
|
|
'categories': all_cats,
|
|
|
'dates': dates,
|
|
'dates': dates,
|
|
|
- 'uv': {str(cat): {int(d): int(uv_pivot.loc[cat, d]) if d in uv_pivot.columns else 0 for d in dates} for cat in all_cats},
|
|
|
|
|
- 'ror': {str(cat): {int(d): float(ror_pivot.loc[cat, d]) if d in ror_pivot.columns and pd.notna(ror_pivot.loc[cat, d]) else 0 for d in dates} for cat in all_cats},
|
|
|
|
|
|
|
+ 'uv': to_dict(uv_pivot, is_int=True),
|
|
|
|
|
+ 'rec': to_dict(rec_pivot),
|
|
|
|
|
+ 'ror': to_dict(ror_pivot),
|
|
|
|
|
+ 'orig': to_dict(orig_pivot),
|
|
|
|
|
+ 'rec_ror': to_dict(rec_ror_pivot),
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
# 计算整体平均数据(用于对比)
|
|
# 计算整体平均数据(用于对比)
|
|
@@ -252,7 +285,8 @@ channel_analysis_data = {}
|
|
|
for ch in main_channel_list:
|
|
for ch in main_channel_list:
|
|
|
trend = calc_channel_daily_trend(ch)
|
|
trend = calc_channel_daily_trend(ch)
|
|
|
cats = calc_channel_categories(ch)
|
|
cats = calc_channel_categories(ch)
|
|
|
- cat_time = calc_channel_category_time(ch)
|
|
|
|
|
|
|
+ cat_time_cat1 = calc_channel_category_time(ch, 'cat1')
|
|
|
|
|
+ cat_time_cat2 = calc_channel_category_time(ch, 'cat2')
|
|
|
ch_stats = channel_stats[channel_stats['channel'] == ch].iloc[0]
|
|
ch_stats = channel_stats[channel_stats['channel'] == ch].iloc[0]
|
|
|
# 计算每日品类数据
|
|
# 计算每日品类数据
|
|
|
daily_cats = {'all': cats.to_dict('records')}
|
|
daily_cats = {'all': cats.to_dict('records')}
|
|
@@ -263,7 +297,7 @@ for ch in main_channel_list:
|
|
|
'trend': trend.to_dict('records'),
|
|
'trend': trend.to_dict('records'),
|
|
|
'categories': cats.to_dict('records'),
|
|
'categories': cats.to_dict('records'),
|
|
|
'daily_categories': daily_cats,
|
|
'daily_categories': daily_cats,
|
|
|
- 'category_time': cat_time,
|
|
|
|
|
|
|
+ 'category_time': {'cat1': cat_time_cat1, 'cat2': cat_time_cat2},
|
|
|
'summary': {
|
|
'summary': {
|
|
|
'点击uv': int(ch_stats['点击uv']),
|
|
'点击uv': int(ch_stats['点击uv']),
|
|
|
'进入分发率': float(ch_stats['进入分发率']),
|
|
'进入分发率': float(ch_stats['进入分发率']),
|
|
@@ -890,10 +924,16 @@ html_content = f"""<!DOCTYPE html>
|
|
|
<span class="channel-selector-inline">
|
|
<span class="channel-selector-inline">
|
|
|
渠道:<select class="channel-select-sync" onchange="syncChannelSelect(this.value)">{channel_options}</select>
|
|
渠道:<select class="channel-select-sync" onchange="syncChannelSelect(this.value)">{channel_options}</select>
|
|
|
</span>
|
|
</span>
|
|
|
- 各品类时间趋势
|
|
|
|
|
- <select id="cat-time-metric" onchange="updateCategoryTimeMatrix()" style="margin-left: 10px; padding: 4px;">
|
|
|
|
|
- <option value="ror">整体裂变率</option>
|
|
|
|
|
|
|
+ 品类:<select id="cat-time-level" onchange="updateCategoryTimeMatrix()" style="padding: 4px;">
|
|
|
|
|
+ <option value="cat1">一级品类</option>
|
|
|
|
|
+ <option value="cat2">二级品类</option>
|
|
|
|
|
+ </select>
|
|
|
|
|
+ 指标:<select id="cat-time-metric" onchange="updateCategoryTimeMatrix()" style="padding: 4px;">
|
|
|
<option value="uv">点击UV</option>
|
|
<option value="uv">点击UV</option>
|
|
|
|
|
+ <option value="rec">进入分发率</option>
|
|
|
|
|
+ <option value="ror">整体裂变率</option>
|
|
|
|
|
+ <option value="orig">头部裂变率</option>
|
|
|
|
|
+ <option value="rec_ror">推荐裂变率</option>
|
|
|
</select>
|
|
</select>
|
|
|
<button class="reset-btn" onclick="updateCategoryTimeMatrix()">重置</button>
|
|
<button class="reset-btn" onclick="updateCategoryTimeMatrix()">重置</button>
|
|
|
</div>
|
|
</div>
|
|
@@ -1605,20 +1645,24 @@ html_content = f"""<!DOCTYPE html>
|
|
|
const channel = document.getElementById('channel-select').value;
|
|
const channel = document.getElementById('channel-select').value;
|
|
|
if (!channelData[channel] || !channelData[channel].category_time) return;
|
|
if (!channelData[channel] || !channelData[channel].category_time) return;
|
|
|
|
|
|
|
|
- const catTime = channelData[channel].category_time;
|
|
|
|
|
|
|
+ const level = document.getElementById('cat-time-level').value;
|
|
|
const metric = document.getElementById('cat-time-metric').value;
|
|
const metric = document.getElementById('cat-time-metric').value;
|
|
|
|
|
+ const catTime = channelData[channel].category_time[level];
|
|
|
|
|
+ if (!catTime) return;
|
|
|
|
|
+
|
|
|
const data = catTime[metric];
|
|
const data = catTime[metric];
|
|
|
const categories = catTime.categories;
|
|
const categories = catTime.categories;
|
|
|
const dates = catTime.dates;
|
|
const dates = catTime.dates;
|
|
|
|
|
|
|
|
// 生成表头(可点击排序)
|
|
// 生成表头(可点击排序)
|
|
|
const dateLabels = dates.map(d => String(d).slice(-4));
|
|
const dateLabels = dates.map(d => String(d).slice(-4));
|
|
|
|
|
+ const levelLabel = level === 'cat1' ? '一级品类' : '二级品类';
|
|
|
document.getElementById('category-time-header').innerHTML = `
|
|
document.getElementById('category-time-header').innerHTML = `
|
|
|
- <tr><th style="cursor:pointer" onclick="sortCatTimeTable(0)">品类</th>${{dateLabels.map((d, i) =>
|
|
|
|
|
|
|
+ <tr><th style="cursor:pointer" onclick="sortCatTimeTable(0)">${{levelLabel}}</th>${{dateLabels.map((d, i) =>
|
|
|
`<th style="cursor:pointer" onclick="sortCatTimeTable(${{i+1}})">${{d}}</th>`).join('')}}</tr>
|
|
`<th style="cursor:pointer" onclick="sortCatTimeTable(${{i+1}})">${{d}}</th>`).join('')}}</tr>
|
|
|
`;
|
|
`;
|
|
|
|
|
|
|
|
- // 计算最大最小值用于渐变
|
|
|
|
|
|
|
+ // 根据指标设置渐变范围
|
|
|
let maxVal = 0, minVal = 0;
|
|
let maxVal = 0, minVal = 0;
|
|
|
categories.forEach(cat => {{
|
|
categories.forEach(cat => {{
|
|
|
dates.forEach(dt => {{
|
|
dates.forEach(dt => {{
|
|
@@ -1626,8 +1670,10 @@ html_content = f"""<!DOCTYPE html>
|
|
|
if (val > maxVal) maxVal = val;
|
|
if (val > maxVal) maxVal = val;
|
|
|
}});
|
|
}});
|
|
|
}});
|
|
}});
|
|
|
- if (metric === 'ror') maxVal = Math.max(maxVal, 0.5);
|
|
|
|
|
- if (metric === 'uv') maxVal = Math.max(maxVal, 10000);
|
|
|
|
|
|
|
+ // 设置最小阈值
|
|
|
|
|
+ const thresholds = {{ uv: 10000, rec: 1.0, ror: 0.5, orig: 0.2, rec_ror: 0.3 }};
|
|
|
|
|
+ if (metric === 'rec') minVal = 0.6;
|
|
|
|
|
+ maxVal = Math.max(maxVal, thresholds[metric] || 0.5);
|
|
|
|
|
|
|
|
// 生成数据行(行头可点击排序)
|
|
// 生成数据行(行头可点击排序)
|
|
|
document.getElementById('category-time-body').innerHTML = categories.map((cat, rowIdx) => {{
|
|
document.getElementById('category-time-body').innerHTML = categories.map((cat, rowIdx) => {{
|