Files
xiaohongshu-cover-ctr-analysis/render_content_statistics.py
2026-11-05 01:04:53 +08:00

138 lines
9.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import json
import math
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
ROOT = Path(__file__).parent
r = json.loads((ROOT / 'content_statistics_results.json').read_text())
data = json.loads((ROOT / 'analysis_data.json').read_text())
FONT = '/System/Library/Fonts/Supplemental/Arial Unicode.ttf'
im = Image.new('RGB', (1660, 1000), '#F8FAFC')
dr = ImageDraw.Draw(im)
def txt(x, y, value, size=24, color='#334155', anchor=None):
dr.text((x, y), value, font=ImageFont.truetype(FONT, size), fill=color, anchor=anchor)
txt(65, 38, '视频与图文的观看差异:统计检验与稳健性', 40, '#0F172A')
txt(65, 105, '24篇有效笔记,7篇视频、17篇图文;观察性比较,不代表形式的因果效应', 24, '#64748B')
txt(95, 185, '观看量分布', 29, '#0F172A')
left, right, top, bottom = 150, 690, 265, 755
def yview(v):
return bottom - math.log10(v) / 5.2 * (bottom - top)
for exponent in range(6):
yy = yview(10**exponent)
dr.line((left, yy, right, yy), fill='#E2E8F0', width=2)
txt(left - 15, yy, f'{10**exponent:,}', 21, '#64748B', 'rm')
dr.line((left, top, left, bottom, right, bottom), fill='#94A3B8', width=2)
for name, flag, x, color in [('图文', 0, 300, '#2563EB'), ('视频', 1, 570, '#D97706')]:
values = sorted([p for p in data if p['video'] == flag], key=lambda p: p['观看量'])
for i, p in enumerate(values):
xx = x + (((i * 7) % 11) - 5) * 7
yy = yview(p['观看量'])
dr.ellipse((xx - 7, yy - 7, xx + 7, yy + 7), fill=color, outline='white', width=2)
if p['source_row'] == 9:
txt(xx, yy - 18, '77,822', 21, '#92400E', 'mb')
median = r['genre']['观看量']['median_a' if flag else 'median_b']
yy = yview(median)
dr.line((x - 55, yy, x + 55, yy), fill=color, width=5)
txt(x, bottom + 23, f'{name} n={len(values)}', 24, '#334155', 'mt')
txt(x, bottom + 63, f'中位数 {median:,.0f}', 22, color, 'mt')
txt(95, 860, '双侧精确秩和检验;3项指标Holm校正', 22)
txt(95, 900, '观看量 p=0.00113;Cliff’s δ=0.866', 24, '#0F172A')
txt(820, 185, '控制发布月份后的观看量比', 29, '#0F172A')
txt(820, 230, '点为几何均值比,横线为HC3稳健95%置信区间', 21, '#64748B')
fleft, fright = 1035, 1570
def xratio(v):
return fleft + math.log2(v) / 8 * (fright - fleft)
for tick in [1, 4, 16, 64, 256]:
xx = xratio(tick)
dr.line((xx, 300, xx, 720), fill='#CBD5E1' if tick == 1 else '#E2E8F0', width=2)
txt(xx, 742, f'{tick}倍', 21, '#64748B', 'mt')
for i, (key, label) in enumerate([
('all_观看量_format_cohort', '全部24篇'),
('without_top_观看量_format_cohort', '去掉最高流量'),
('september_观看量_format_cohort', '仅9月18篇')
]):
yy = 355 + i * 145
term = r['regressions'][key]['terms']['video']
ratio = math.exp(term['coef'])
lo, hi = [math.exp(v) for v in term['hc3_ci95_t_approx']]
txt(820, yy - 14, label, 22)
dr.line((xratio(lo), yy, xratio(hi), yy), fill='#0F766E', width=4)
for end in [lo, hi]:
dr.line((xratio(end), yy - 8, xratio(end), yy + 8), fill='#0F766E', width=3)
xx = xratio(ratio)
dr.ellipse((xx - 8, yy - 8, xx + 8, yy + 8), fill='#0F766E')
txt(xx, yy + 23, f'{ratio:.1f} [{lo:.1f}, {hi:.1f}]', 22, '#0F766E', 'mt')
txt(820, 860, '题材与形式高度重叠,不能稳定分离各自作用。', 22)
txt(820, 900, '视频内仅1篇非展示内容,题材比较 p=0.286。', 22, '#0F172A')
txt(65, 962, '数据来源:笔记列表明细表.xlsx;观看量轴、倍数轴均为对数刻度。', 20, '#64748B')
im.save(ROOT / '内容统计检验.png')
lines = [
'# 内容与流量的统计分析', '',
'源文件:/Users/zzyang/Downloads/笔记列表明细表.xlsx,Sheet1。原始26篇,排除第3、4行曝光为0的最新笔记,分析24篇。', '',
'主要结果变量为观看量,曝光与封面点击率为辅助结果。所有结果均为单账号观察性、探索性分析。题材标签为根据标题进行的事后人工归类,未读取正文或封面。', '',
'## 方法', '',
'- 形式比较:双侧精确置换秩和检验,以秩和偏离零假设期望的绝对值为统计量,枚举346,104种分组;观看、曝光、点击率3项p值作Holm校正。检验反映分布/秩差异,并非在任意分布下只检验中位数。',
'- 效应量:中位数之比、Cliff’s delta、随机一篇视频观看高于随机一篇图文的样本配对比例。中位数比及delta的区间为组内重抽样50,000次的百分位Bootstrap区间。',
'- 题材总体比较:含并列秩修正的Kruskal–Wallis H统计量,200,000次标签置换估计p值,p=(极端次数+1)/(置换次数+1)。3项结果作Holm校正;观看量的10组两两比较另作Holm校正。',
'- 回归:ln(观看量)=截距+视频指示变量+10月发布指示变量。9月为参照。HC3异方差稳健标准误,使用残差自由度t分布近似计算p值及95%区间。系数指数化报告条件几何均值比。回归p值未额外作多重校正。',
'- 稳健性:去掉第9行最高流量笔记、仅分析9月18篇、在9月与10月内分别固定视频篇数作分层精确置换(61,200种分组)。',
'- 预测评估:以帽子矩阵计算留一交叉验证PRESS及log尺度R²,区别于样本内R²。',
'- 随机种子20261017。检验将笔记近似视为独立、可交换的观察;同账号连续发布可能不满足严格独立性。', '',
'## 图文和视频', '',
'|结果|视频中位数|图文中位数|U|原始p|Holm校正p|Cliff delta|',
'|---|---:|---:|---:|---:|---:|---:|'
]
for outcome in ['观看量', '曝光', '封面点击率']:
s = r['genre'][outcome]
a = f'{s["median_a"]:.1%}' if outcome == '封面点击率' else f'{s["median_a"]:,.0f}'
b = f'{s["median_b"]:.1%}' if outcome == '封面点击率' else f'{s["median_b"]:,.0f}'
lines.append(f'|{outcome}|{a}|{b}|{s["U"]:.1f}|{s["p_exact"]:.6f}|{s["p_holm_3"]:.6f}|{s["cliff_delta"]:.3f}|')
s = r['genre']['观看量']
lines += ['', f'观看量中位数比={s["median_ratio"]:.2f},95% Bootstrap区间={s["median_ratio_ci95_percentile"]}。Cliff delta区间={s["cliff_delta_ci95_percentile"]}。视频高于图文的样本配对比例={s["superiority_probability"]:.2%},该比例不是未来成功概率。', '',
'## 内容题材', '', '|题材|篇数|视频篇数|观看量中位数|曝光中位数|点击率中位数|', '|---|---:|---:|---:|---:|---:|']
for s in r['topic_summary']:
lines.append(f'|{s["group"]}|{s["n"]}|{s["video_n"]}|{s["median_views"]:,.0f}|{s["median_impressions"]:,.1f}|{s["median_ctr"]:.1%}|')
lines += ['', '|结果|H|置换p|Holm校正p|epsilon²|', '|---|---:|---:|---:|---:|']
for outcome in ['观看量', '曝光', '封面点击率']:
s = r['topic_omnibus'][outcome]
lines.append(f'|{outcome}|{s["H"]:.3f}|{s["p_permutation"]:.6f}|{s["p_holm_3"]:.6f}|{s["epsilon_squared"]:.3f}|')
lines += ['', 'epsilon²为秩统计量效应量,不能解读为解释了原始观看量的相同比例方差。', '',
'### 观看量的全部10组两两比较', '', '|A|B|原始p|Holm校正p|Cliff delta|去掉最高流量后校正p|', '|---|---|---:|---:|---:|---:|']
for s, t in zip(r['topic_pairwise_views'], r['topic_pairwise_views_without_top']):
lines.append(f'|{s["a"]}|{s["b"]}|{s["p_exact"]:.6f}|{s["p_holm_10"]:.6f}|{s["cliff_delta"]:.3f}|{t["p_holm_10"]:.6f}|')
lines += ['', '## 回归及稳健性', '', '|样本|结果|视频系数|几何均值比|HC3 95%区间|稳健p|样本内R²|留一R²(log尺度)|', '|---|---|---:|---:|---|---:|---:|---:|']
for key, m in r['regressions'].items():
s = m['terms']['video']
lo, hi = [math.exp(a) for a in s['hc3_ci95_t_approx']]
lines.append(f'|{key}|log结果|{s["coef"]:.4f}|{math.exp(s["coef"]):.2f}|[{lo:.2f}, {hi:.2f}]|{s["hc3_p_t_approx"]:.6f}|{m["r2"]:.3f}|{m["loo_r2_log_target"]:.3f}|')
lines += ['', '全样本观看量方程:ln(观看量)=4.2505+3.0640×视频−0.1126×10月。月份控制只粗略区分两个发布批次,不能替代统一发布后观察窗口。', '',
f'按月份分层的精确置换检验,观看量p={r["genre"]["views_month_stratified_permutation"]["p_exact"]:.6f}。', '',
'## 能下的结论与限制', '',
'1. 视频与图文的观看分布存在较大差异,去掉最高流量笔记及仅保留9月笔记后仍存在。CTR差异未检出,不能据此证明两种形式CTR等同。',
'2. 题材总体存在差异。展示类对模型动态类的校正p从0.039变为去掉最高流量后的0.071,具体题材结论比形式比较更脆弱。',
'3. 展示类6篇全部为视频,7篇视频中仅1篇非展示类。视频内的题材秩和检验p=0.286,只有1篇参照,统计功效不足。不能稳定分离题材和形式各自的作用。',
'4. 24篇不足以支持包含大量内容标签及交互项的可靠选题模型。观看量发布前信息模型的留一R²为0.230,预测精度有限,且留一法不能充分检验未来时间上的泛化。',
'5. 所有题材分组及后续敏感性分析为探索性分析,未覆盖所有可能尝试的分组方式。Holm校正仅对应明确列出的比较族,不会消除事后选标签带来的偏差。',
'6. 未知统一统计窗口、投流、账号粉丝基数、受众变化及平台分发状态。回归倍数为条件关联,不是把图文换成视频的因果增幅。', '',
'## 题材标签与源行号', '']
for summary in r['topic_summary']:
matching = [x for x in data if (x['source_row'] in {'技术作品/工具效果展示':[7,9,20,24,25,27], 'Vibe Coding指南':[11,15,16], '模型动态/观点':[5,6,8,17,18], '安全/隐私事件':[14,19,21], '其他':[10,12,13,22,23,26,28]}[summary['group']])]
lines.append(f'### {summary["group"]}')
lines += ['', *[f'- 第{x["source_row"]}行:{x["笔记标题"]}' for x in matching], '']
(ROOT / '内容统计分析报告.md').write_text('\n'.join(lines))