#!/usr/bin/env python3 """图表 v4: L1只看L1课程, L2只看L2课程""" import json, os from datetime import date, timedelta import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt import matplotlib.dates as mdates import matplotlib.font_manager as fm import numpy as np fm.fontManager.addfont('/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc') plt.rcParams['font.family'] = fm.FontProperties(fname='/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc').get_name() plt.rcParams['axes.unicode_minus'] = False with open('/root/.openclaw/workspace/output/course_data_v4.json') as f: data = json.load(f) results = data['results'] out = '/root/.openclaw/workspace/output' configs = { 'L1': {'prefix': 'L1', 'color': '#4A90D9', 'light': '#A8CFF1', 'label': 'L1'}, 'L2': {'prefix': 'L2', 'color': '#E85D47', 'light': '#F4A9A0', 'label': 'L2'}, } for key, cfg in configs.items(): pfx = cfg['prefix']; color = cfg['color']; light = cfg['light']; label = cfg['label'] first = next(i for i, r in enumerate(results) if r[f'{pfx}_paid'] > 0) data_sub = results[first:] dates = [date.fromisoformat(r['ws']) for r in data_sub] xs = [d + timedelta(days=3) for d in dates] paid = [r[f'{pfx}_paid'] for r in data_sub] cons_users = [r[f'{pfx}_cons_users'] for r in data_sub] no_cons = [r[f'{pfx}_no_cons'] for r in data_sub] avg_all = [r[f'{pfx}_avg_all'] for r in data_sub] avg_cons = [r[f'{pfx}_avg_cons'] for r in data_sub] # 图1: 堆叠柱状 fig, ax = plt.subplots(figsize=(18, 8)) x_idx = np.arange(len(xs)) ax.bar(x_idx, cons_users, 0.65, color=light, label='有课消用户', zorder=3) ax.bar(x_idx, no_cons, 0.65, bottom=cons_users, color='#D0D0D0', label='无课消用户', zorder=3) step = max(1, len(data_sub)//10) for i in range(0, len(data_sub), step): ax.annotate(str(paid[i]), (i, paid[i]), textcoords='offset points', xytext=(0, 5), fontsize=7.5, ha='center', color='#333333', fontweight='bold') ax.set_xticks(x_idx[::step]) ax.set_xticklabels([dates[i].strftime('%m/%d') for i in range(0, len(data_sub), step)], fontsize=8.5, rotation=45) ax.set_ylabel('用户数', fontsize=13) ax.set_title(f'{label}付费用户周课消分布(只看{label}课程,剔除U0)', fontsize=16, fontweight='bold') ax.legend(fontsize=12, loc='upper left') ax.grid(axis='y', alpha=0.3, zorder=0) ax.set_xlim(-0.5, len(x_idx)-0.5) no_rate = no_cons[-1]/paid[-1]*100 if paid[-1] else 0 ax.text(0.97, 0.95, f'付费{paid[-1]}人 | 无课消率{no_rate:.0f}%', transform=ax.transAxes, fontsize=11, ha='right', va='top', color='#666666', fontstyle='italic') plt.tight_layout() plt.savefig(f'{out}/{pfx}_users_stack_v4.png', dpi=150, bbox_inches='tight', facecolor='white') plt.close() print(f' ✅ {pfx}_users_stack_v4.png') # 图2: 折线 fig, ax = plt.subplots(figsize=(18, 8)) ax.plot(xs, avg_all, 'o-', color='#999999', linewidth=2.2, markersize=5, label='人均课消(全部付费用户)', markerfacecolor='white') ax.plot(xs, avg_cons, 's-', color=color, linewidth=2.8, markersize=5, label='人均课消(有课消用户)', markerfacecolor='white') ax.fill_between(xs, avg_all, avg_cons, alpha=0.08, color=color) for i in range(0, len(data_sub), max(1, len(data_sub)//8)): ax.annotate(f'{avg_all[i]:.1f}', (xs[i], avg_all[i]), textcoords='offset points', xytext=(0,-15), fontsize=7.5, color='#999999', ha='center') ax.annotate(f'{avg_cons[i]:.1f}', (xs[i], avg_cons[i]), textcoords='offset points', xytext=(0,7), fontsize=7.5, color=color, ha='center', fontweight='bold') ax.xaxis.set_major_formatter(mdates.DateFormatter('%m/%d')) ax.xaxis.set_major_locator(mdates.MonthLocator()) plt.setp(ax.xaxis.get_majorticklabels(), rotation=45, fontsize=9) ax.set_ylabel('课消数(节/周)', fontsize=13) ax.set_title(f'{label}付费用户周人均课消趋势(只看{label}课程,剔除U0)', fontsize=16, fontweight='bold') ax.legend(fontsize=12, loc='upper left') ax.grid(True, alpha=0.3) ax.set_xlim(date(2025,8,30), date(2026,5,12)) plt.tight_layout() plt.savefig(f'{out}/{pfx}_avg_trend_v4.png', dpi=150, bbox_inches='tight', facecolor='white') plt.close() print(f' ✅ {pfx}_avg_trend_v4.png') print('\n✅ 4张v4图表已生成')