130 lines
6.0 KiB
Python
130 lines
6.0 KiB
Python
#!/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}')
|