#!/usr/bin/env python3 """Excel v4: L1只看L1课程, L2只看L2课程""" import json, openpyxl from datetime import date from openpyxl.styles import Font, Alignment, PatternFill, Border, Side from openpyxl.chart import LineChart, BarChart, Reference from openpyxl.utils import get_column_letter with open('/root/.openclaw/workspace/output/course_data_v4.json') as f: raw = json.load(f) results = raw['results'] for r in results: r['ws'] = date.fromisoformat(r['ws']) r['we'] = date.fromisoformat(r['we']) wb = openpyxl.Workbook() wb.remove(wb.active) hfont = Font(name='微软雅黑', bold=True, size=9, color='FFFFFF') hfill = PatternFill(start_color='002F5496', end_color='002F5496', fill_type='solid') dfont = Font(name='微软雅黑', size=9) tfont = Font(name='微软雅黑', bold=True, size=14, color='002F5496') sfont = Font(name='微软雅黑', bold=True, size=11, color='002F5496') bd = Border(left=Side(style='thin'), right=Side(style='thin'), top=Side(style='thin'), bottom=Side(style='thin')) ctr = Alignment(horizontal='center', vertical='center') def ac(ws, r, c, v, font=dfont, fill=None, align=ctr): cl = ws.cell(row=r, column=c, value=v) cl.font, cl.border, cl.alignment = font, bd, align if fill: cl.fill = fill def ah(ws, r, c, v): cl = ws.cell(row=r, column=c, value=v) cl.font, cl.fill, cl.border, cl.alignment = hfont, hfill, bd, ctr # Sheet 1 ws1 = wb.create_sheet("概览") ws1.merge_cells('A1:H1') ac(ws1,1,1,"付费用户课消分析 v4(只看对应级别课程,剔除U0)",font=tfont,align=Alignment(horizontal='left')) notes = [ "口径:L1付费群 = 买过L1商品的付费用户, 只看L1课程课消 | L2付费群 = 买过L2商品的付费用户, 只看L2课程课消", "L1+L2用户:在L1视角只统计L1课程课消, L2视角只统计L2课程课消", "课消:用户首次完成某一课时(剔除U0序章)", "付费用户:status=1 + 未删除 + 有未退款订单", ] for i,n in enumerate(notes): ws1.merge_cells(f'A{3+i}:H{3+i}') ac(ws1,3+i,1,n,font=Font(name='微软雅黑',size=9,color='666666'),align=Alignment(horizontal='left')) row=9 ws1.merge_cells(f'A{row}:H{row}') ac(ws1,row,1,"汇总(截至最后一周)",font=sfont,align=Alignment(horizontal='left')) row+=1 for j,h in enumerate(['分类','付费用户','有课消','无课消','无课消率','人均课消','有消人均'],1): ah(ws1,row,j,h) row+=1 last=results[-1] skus = [ ('L1付费群(只看L1课程)', last['L1_paid'],last['L1_cons_users'],last['L1_no_cons'],last['L1_avg_all'],last['L1_avg_cons'], '00A8CFF1'), ('L2付费群(只看L2课程)', last['L2_paid'],last['L2_cons_users'],last['L2_no_cons'],last['L2_avg_all'],last['L2_avg_cons'], '00F4A9A0'), ('合计(去重)', last['total_paid'],last['total_cons_users'],last['total_no_cons'],last['total_avg_all'],last['total_avg_cons'], '00C8E6C9'), ] for name,p,cu,nc,aa,ac_,clr in skus: no_rate=f"{nc/p*100:.0f}%" if p else "0%" fl=PatternFill(start_color=clr,end_color=clr,fill_type='solid') for j,v in enumerate([name,p,cu,nc,no_rate,aa,ac_],1): ac(ws1,row,j,v,font=Font(name='微软雅黑',bold=(j==1),size=10),fill=fl) row+=1 # Sheet 2 ws2=wb.create_sheet("每周明细") headers=['周','周一起','周日'] for pfx in ['合计','L1付费群','L2付费群']: for m in ['付费','有消','无消','课消','人均','有消人均']: headers.append(f'{pfx}{m}') for j,h in enumerate(headers,1): ah(ws2,1,j,h) for ri,r in enumerate(results): rw=ri+2 ac(ws2,rw,1,r['ws'].strftime('%m/%d')) ac(ws2,rw,2,r['ws'].strftime('%Y-%m-%d')) ac(ws2,rw,3,r['we'].strftime('%Y-%m-%d')) col=4 for prefix in ['total','L1','L2']: for k in ['paid','cons_users','no_cons','cons','avg_all','avg_cons']: ac(ws2,rw,col,r[f'{prefix}_{k}']) col+=1 for ci in range(1,len(headers)+1): ws2.column_dimensions[get_column_letter(ci)].width=11 if ci<=3 else 10 ws2.freeze_panes='D2' # Sheet 3+4: charts for lvl, pf, clr in [('L1','L1','4A90D9'),('L2','L2','E85D47')]: ws=wb.create_sheet(f"{pf}图表") lh=['周','付费用户','有课消用户','无课消用户','课消总数','人均课消','有消人均'] first=next(i for i,r in enumerate(results) if r[f'{pf}_paid']>0) ld=results[first:] for j,h in enumerate(lh,1): ah(ws,1,j,h) for ri,r in enumerate(ld): rw=ri+2 ac(ws,rw,1,r['ws'].strftime('%m/%d')) for j,k in enumerate([f'{pf}_paid',f'{pf}_cons_users',f'{pf}_no_cons',f'{pf}_cons',f'{pf}_avg_all',f'{pf}_avg_cons'],2): ac(ws,rw,j,r[k]) n=len(ld) cr=Reference(ws,min_col=1,min_row=2,max_row=n+1) ch1=BarChart(); ch1.type="col"; ch1.grouping="stacked" ch1.title=f"{pf}付费用户周课消分布(只看{pf}课程)"; ch1.style=10; ch1.width=24; ch1.height=13 ch1.add_data(Reference(ws,min_col=3,min_row=1,max_row=n+1),titles_from_data=True) ch1.add_data(Reference(ws,min_col=4,min_row=1,max_row=n+1),titles_from_data=True) ch1.set_categories(cr) ch1.series[0].graphicalProperties.solidFill='A8CFF1' if pf=='L1' else 'F4A9A0' ch1.series[1].graphicalProperties.solidFill='D9D9D9' ch1.y_axis.title='用户数'; ch1.legend.position='b' ws.add_chart(ch1,"A9") ch2=LineChart(); ch2.title=f"{pf}付费用户周人均课消趋势(只看{pf}课程)"; ch2.style=10; ch2.width=24; ch2.height=13 ch2.add_data(Reference(ws,min_col=6,min_row=1,max_row=n+1),titles_from_data=True) ch2.add_data(Reference(ws,min_col=7,min_row=1,max_row=n+1),titles_from_data=True) ch2.set_categories(cr) ch2.series[0].graphicalProperties.line.solidFill='999999'; ch2.series[0].graphicalProperties.line.width=20000 ch2.series[1].graphicalProperties.line.solidFill=clr; ch2.series[1].graphicalProperties.line.width=28000 ch2.y_axis.scaling.min=0; ch2.y_axis.title='课消数(节/周)'; ch2.legend.position='b' ws.add_chart(ch2,"A27") for ci in range(1,8): ws.column_dimensions[get_column_letter(ci)].width=12 path='/root/.openclaw/workspace/output/course_consumption_by_level_v4.xlsx' wb.save(path) print(f'✅ {path}')