80 lines
3.6 KiB
Python
80 lines
3.6 KiB
Python
import json
|
||
import math
|
||
from pathlib import Path
|
||
|
||
from PIL import Image, ImageDraw, ImageFont
|
||
|
||
ROOT = Path(__file__).parent
|
||
data = json.loads((ROOT / 'analysis_data.json').read_text())
|
||
results = json.loads((ROOT / 'regression_results.json').read_text())
|
||
models = results['models']
|
||
image = Image.new('RGB', (1600, 1010), '#F8FAFC')
|
||
draw = ImageDraw.Draw(image)
|
||
FONT = '/System/Library/Fonts/Supplemental/Arial Unicode.ttf'
|
||
|
||
|
||
def text(x, y, s, size=25, color='#334155', anchor=None):
|
||
draw.text((x, y), s, font=ImageFont.truetype(FONT, size), fill=color, anchor=anchor)
|
||
|
||
|
||
text(65, 40, '点击率与曝光、观看的回归关系', 42, '#0F172A')
|
||
text(65, 107, '24篇有效笔记;数量轴取对数,每个点代表一篇笔记', 25, '#64748B')
|
||
colors = {'图文': '#2563EB', '视频': '#D97706'}
|
||
for i, (genre, color) in enumerate(colors.items()):
|
||
xx = 1080 + i * 200
|
||
draw.ellipse((xx, 110, xx + 17, 127), fill=color)
|
||
text(xx + 30, 101, genre, 25)
|
||
|
||
for panel, (outcome, label, y_min, y_max) in enumerate([
|
||
('曝光', 'impressions', 1, 5.25), ('观看量', 'views', 0, 5.25)
|
||
]):
|
||
left, right = 135 + panel * 780, 735 + panel * 780
|
||
top, bottom = 245, 725
|
||
|
||
def xy(ctr, log_quantity):
|
||
return (left + ctr / 21 * (right - left),
|
||
bottom - (log_quantity - y_min) / (y_max - y_min) * (bottom - top))
|
||
|
||
text(left - 50, 182, f'{outcome}与点击率', 30, '#0F172A')
|
||
for power in range(int(y_min), 6):
|
||
if power > y_max:
|
||
continue
|
||
yy = xy(0, power)[1]
|
||
draw.line((left, yy, right, yy), fill='#E2E8F0', width=2)
|
||
text(left - 15, yy, f'{10**power:,}', 21, '#64748B', anchor='rm')
|
||
for tick in [0, 5, 10, 15, 20]:
|
||
xx = xy(tick, y_min)[0]
|
||
draw.line((xx, bottom, xx, bottom + 7), fill='#94A3B8', width=2)
|
||
text(xx, bottom + 15, f'{tick}%', 22, '#64748B', anchor='mt')
|
||
draw.line((left, top, left, bottom, right, bottom), fill='#94A3B8', width=2)
|
||
text((left + right) / 2, bottom + 62, '封面点击率', 24, anchor='mt')
|
||
|
||
for subset, color in [('all', '#0F172A'), ('without_top', '#94A3B8')]:
|
||
m = models[f'{subset}_{label}_ln']
|
||
b0, b1 = m['terms']['intercept']['coef'], m['terms']['ctr_pp']['coef']
|
||
points = [xy(c / 10, (b0 + b1 * c / 10) / math.log(10)) for c in range(20, 195)]
|
||
for j in range(len(points) - 1):
|
||
if subset == 'all' or (j // 7) % 2 == 0:
|
||
draw.line((*points[j], *points[j + 1]), fill=color, width=4)
|
||
|
||
for row in data:
|
||
xx, yy = xy(row['ctr_pp'], math.log10(row[outcome]))
|
||
draw.ellipse((xx - 7, yy - 7, xx + 7, yy + 7), fill=colors[row['体裁']], outline='white', width=2)
|
||
if row['source_row'] == results['top_source_row']:
|
||
draw.ellipse((xx - 12, yy - 12, xx + 12, yy + 12), outline='#D97706', width=2)
|
||
text(xx - 15, yy - 22, '最高流量笔记', 20, '#92400E', anchor='rb')
|
||
|
||
model = models[f'all_{label}_ln']
|
||
beta = model['terms']['ctr_pp']['coef']
|
||
p = model['terms']['ctr_pp']['hc3_p_t_approx']
|
||
text(left - 50, 840, f'ln({outcome}) = {model["terms"]["intercept"]["coef"]:.3f} + {beta:.3f} × 点击率(%)', 23)
|
||
text(left - 50, 883, f'R² = {model["r2"]:.3f} HC3稳健 p = {p:.3f}', 23)
|
||
|
||
draw.line((70, 962, 120, 962), fill='#0F172A', width=4)
|
||
text(134, 947, '全部24篇拟合', 22)
|
||
for x in range(410, 470, 18):
|
||
draw.line((x, 962, x + 10, 962), fill='#94A3B8', width=4)
|
||
text(490, 947, '去掉最高流量笔记后拟合(其余23篇)', 22)
|
||
text(1535, 947, '数据来源:用户提供的笔记明细表', 20, '#64748B', anchor='ra')
|
||
image.save(ROOT / '点击率回归分析.png')
|