#!/usr/bin/env python3 """危大方案看板数据工作簿 v5 — 双数据源 + 纯Excel公式(认定vsOA用COUNTIFS直写)""" import sys import pandas as pd from openpyxl import Workbook from openpyxl.styles import Font, PatternFill, Alignment, Border, Side from openpyxl.utils import get_column_letter from datetime import datetime if len(sys.argv) > 1: REPORT_DATE = sys.argv[1] else: REPORT_DATE = datetime.now().strftime('%Y-%m-%d') BASE = f"/mnt/y/Openclaw_Hub/03.资源/实施项目 wiki/dashboard/data/{REPORT_DATE}" CERT_DIR = "/mnt/y/Openclaw_Hub/03.资源/实施项目 wiki/dashboard/data/认定数据/2026" OUT = f"{BASE}/cleaned/危大方案看板数据工作簿.xlsx" if len(sys.argv) > 2: OUT = sys.argv[2] # override output path (for when WPS locks the file) # ════ 数据源 ════ df = pd.read_csv(f"{BASE}/cleaned/methods_cleaned.csv") SRC_COLS = list(df.columns[:24]) valid_all = df[df['是否有效登记'] == True].copy() valid_2026 = valid_all[valid_all['开工年份'] >= 2026].copy() m = valid_2026 tot = len(m); gen = (m['是否超一定规模']!='是').sum(); sup = tot-gen completed = m['是否完成审批'].sum(); unfinished = tot-completed projects = m['项目名称'].nunique(); countries = m['所属国别'].value_counts().to_dict() warn_df = m[m['预警信号']!='none'].sort_values('分部分项工程计划开工日期') warn_total = len(warn_df); orange = (warn_df['预警信号']=='orange').sum(); yellow = warn_total-orange cert_raw = pd.read_csv(f"{CERT_DIR}/certified_schemes_detail.csv") cert_raw['计划开工日期_p'] = pd.to_datetime(cert_raw['计划开工日期'].astype(str).str.replace('.','-'), errors='coerce') cert_valid = cert_raw[cert_raw['计划开工日期_p'].dt.year >= 2026].copy() cert_tot = len(cert_valid) cert_sup = (cert_valid['是否超一定规模']=='是').sum(); cert_gen = cert_tot-cert_sup tech_raw = pd.read_csv(f"{CERT_DIR}/certified_tech_schemes_detail.csv") tech_raw['计划开工日期_p'] = pd.to_datetime(tech_raw['计划开工日期'].astype(str).str.replace('.','-'), errors='coerce') tech_valid = tech_raw[tech_raw['计划开工日期_p'].dt.year >= 2026].copy() tech_tot = len(tech_valid) print(f"OA: {tot}(一般{gen}/超规{sup}) 完成{completed} | 认定: 表1{cert_tot}+表2{tech_tot}={cert_tot+tech_tot}") # ════ 样式 ════ HDR_F=Font(name='微软雅黑',bold=True,size=10,color='FFFFFF'); HDR_BG=PatternFill('solid',fgColor='1A3A5C') TITLE_F=Font(name='微软雅黑',bold=True,size=14,color='1A3A5C'); DATA_F=Font(name='微软雅黑',size=10) BOLD_F=Font(name='微软雅黑',bold=True,size=10); GRAY_F=Font(name='微软雅黑',size=9,color='8899AA') GREEN_F=Font(name='微软雅黑',bold=True,size=10,color='2E7D32'); RED_F=Font(name='微软雅黑',bold=True,size=10,color='D94E34') BLUE_F=Font(name='微软雅黑',bold=True,size=10,color='1A3A5C'); ORANGE_F=Font(name='微软雅黑',bold=True,size=10,color='E65100') FORMULA_F=Font(name='Consolas',size=10,color='1A3A5C') WARN_BG=PatternFill('solid',fgColor='FFF3E0'); INFO_BG=PatternFill('solid',fgColor='F0F4FA') BORDER=Border(left=Side('thin','DBE2EA'),right=Side('thin','DBE2EA'),top=Side('thin','DBE2EA'),bottom=Side('thin','DBE2EA')) CENTER=Alignment(horizontal='center',vertical='center',wrap_text=True) GOLD_BD=Border(bottom=Side(style='medium',color='C8962E')) REF="'有效≥2026'"; CREF="'认定数据'" def hdr_row(ws,r,cols): for i,h in enumerate(cols): c=ws.cell(r,i+1,h); c.font=HDR_F; c.fill=HDR_BG; c.border=BORDER; c.alignment=CENTER def write_data_sheet(ws,df_out,title,cols): ncol=len(cols) ws.merge_cells(start_row=1,start_column=1,end_row=1,end_column=ncol) ws.cell(1,1,title).font=TITLE_F; ws.cell(1,1).border=GOLD_BD hdr_row(ws,3,cols) for ri,(_,row) in enumerate(df_out.iterrows()): rr=ri+4 for ci,col in enumerate(cols): v=row.get(col,'') if pd.isna(v): v='' elif isinstance(v,(pd.Timestamp,)): v=str(v)[:10] elif isinstance(v,(float,)) and v==int(v): v=int(v) ws.cell(rr,ci+1,v).font=DATA_F; ws.cell(rr,ci+1,v).border=BORDER ws.auto_filter.ref=f'A3:{get_column_letter(ncol)}{len(df_out)+3}' ws.freeze_panes='A4' for i,col in enumerate(cols): ws.column_dimensions[get_column_letter(i+1)].width=max(10,min(40,len(str(col))*2.2)) def write_formula_sheet(ws,title,subtitle,formulas,col_widths): ws.merge_cells(start_row=1,start_column=1,end_row=1,end_column=4) ws.cell(1,1,title).font=TITLE_F; ws.cell(1,1).border=GOLD_BD if subtitle: ws.merge_cells(start_row=2,start_column=1,end_row=2,end_column=4) ws.cell(2,1,subtitle).font=Font(name='微软雅黑',size=9,color='8899AA') for col_letter,row_num,formula,label in formulas: cell=ws.cell(row_num,ord(col_letter)-64,formula); cell.font=FORMULA_F; cell.border=BORDER if label: ws.cell(row_num,ord(col_letter)-63,label).font=GRAY_F for col_letter,w in col_widths: ws.column_dimensions[col_letter].width=w ws.merge_cells(start_row=12,start_column=1,end_row=12,end_column=4) ws.cell(12,1,'💡 源数据更新后公式自动刷新(WPS新版/Excel 365)').font=GRAY_F; ws.cell(12,1).fill=INFO_BG wb = Workbook() # ═══ S1-S3 数据源 ═══ s1=wb.active; s1.title='清洗后数据' write_data_sheet(s1,valid_all.reset_index(drop=True), f'OA登记·清洗后数据(有效登记·全部年份·{len(valid_all)}行)', SRC_COLS+['方案状态_clean','是否完成审批','是否有效登记','开工年份','开工月份','预警信号']) s2=wb.create_sheet('有效≥2026') write_data_sheet(s2,valid_2026.reset_index(drop=True), f'OA登记·有效≥2026年开工({len(valid_2026)}行)', SRC_COLS+['方案状态_clean','是否完成审批','是否有效登记','开工年份','开工月份','预警信号']) s3=wb.create_sheet('认定数据') CERT_COLS=['所属区域','所属国别','项目名称','方案名称','编制单位','工程类别','分部工程类别','是否超一定规模','计划开工日期'] write_data_sheet(s3,cert_valid.reset_index(drop=True), f'2026年度公司认定危大方案明细(中港科技便〔2026〕6号·{cert_tot}项)',CERT_COLS) # ═══ S4: 公式-认定vsOA ═══ # 项目名从认定数据 C4:C200 提取,省去 UNIQUE(WPS兼容性更好) # 用 COUNTIFS 统计 超规("是") 和 一般("否") s4=wb.create_sheet('公式-认定vsOA') COMP_HDR=['项目名称','认定超规','认定一般','OA超规','OA一般','差额超规','差额一般'] ncol_c=len(COMP_HDR) s4.merge_cells(start_row=1,start_column=1,end_row=1,end_column=ncol_c) s4.cell(1,1,f'认定 vs OA登记 项目级对比({REPORT_DATE}·COUNTIFS公式)').font=TITLE_F; s4.cell(1,1).border=GOLD_BD s4.merge_cells(start_row=2,start_column=1,end_row=2,end_column=ncol_c) s4.cell(2,1,'A列=项目 | B=认定超规 | C=认定一般 | D=OA超规 | E=OA一般 | F=差额超规 | G=差额一般').font=GRAY_F hdr_row(s4,3,COMP_HDR) # 用 Python 预填项目名(因为 UNIQUE 在部分WPS版本不支持) # 然后 COUNTIFS 引用项目名列 proj_list = sorted(cert_valid['项目名称'].unique()) for ri, proj_name in enumerate(proj_list): r = ri + 4 s4.cell(r, 1, proj_name).font=DATA_F; s4.cell(r, 1).border=BORDER # 公式行:B-H 全部用 =COUNTIFS / =差额(预填20行) for r in range(4, 24): ar = f'$A{r}' sr = str(r) # B: 认定超规 s4.cell(r, 2, f'=COUNTIFS(' + "'认定数据'!$C$4:$C$200," + f'{ar},' + "'认定数据'!$H$4:$H$200," + '"是")').font=FORMULA_F; s4.cell(r, 2).border=BORDER # C: 认定一般 s4.cell(r, 3, f'=COUNTIFS(' + "'认定数据'!$C$4:$C$200," + f'{ar},' + "'认定数据'!$H$4:$H$200," + '"否")').font=FORMULA_F; s4.cell(r, 3).border=BORDER # D: OA超规 s4.cell(r, 4, f'=COUNTIFS(' + "'有效≥2026'!$D$4:$D$200," + f'{ar},' + "'有效≥2026'!$K$4:$K$200," + '"是")').font=FORMULA_F; s4.cell(r, 4).border=BORDER # E: OA一般 s4.cell(r, 5, f'=COUNTIFS(' + "'有效≥2026'!$D$4:$D$200," + f'{ar},' + "'有效≥2026'!$K$4:$K$200," + '"否")').font=FORMULA_F; s4.cell(r, 5).border=BORDER # F: 差额超规 = D - B s4.cell(r, 6, f'=D{sr}-B{sr}').font=FORMULA_F; s4.cell(r, 6).border=BORDER # G: 差额一般 = E - C s4.cell(r, 7, f'=E{sr}-C{sr}').font=FORMULA_F; s4.cell(r, 7).border=BORDER s4.auto_filter.ref='A3:G23' for w,col in zip([40,12,12,12,12,12,12],'ABCDEFG'): s4.column_dimensions[col].width=w # ═══ S5: 认定技术方案 ═══ s5=wb.create_sheet('认定技术方案') TECH_COLS=['所属国别','项目名称','方案名称','编制单位','工程类别','方案等级','工程特点/说明','计划开工日期'] write_data_sheet(s5,tech_valid.reset_index(drop=True), f'2026年度公司认定技术方案明细(ⅠⅡⅢ类·{tech_tot}项)',TECH_COLS) # ═══ S6-S10b: 公式(GROUPBY用于分组聚合 + COUNTIF用于多指标混合) ═══ # ── S6: 公式-年度认定 ── s6=wb.create_sheet('公式-年度认定') s6.merge_cells('A1:C1'); s6.cell(1,1,'OA年度认定(≥2026开工·GROUPBY公式)').font=TITLE_F; s6.cell(1,1).border=GOLD_BD s6.merge_cells('A2:C2'); s6.cell(2,1,'=GROUPBY(有效≥2026!K3:K200,有效≥2026!A3:A200,COUNTA,3,0)').font=GRAY_F s6.cell(4,1,f'=GROUPBY({REF}!K3:K200,{REF}!A3:A200,COUNTA,3,0)').font=FORMULA_F; s6.cell(4,1).border=BORDER for w,c in zip([18,12],'AB'): s6.column_dimensions[c].width=w # 提示行 s6.merge_cells('A10:C10'); s6.cell(10,1,'💡 若WPS显示@前缀,选中单元格→删除@即可正常溢出').font=GRAY_F; s6.cell(10,1).fill=INFO_BG # ── S7: 公式-国别分布 ── s7=wb.create_sheet('公式-国别分布') s7.merge_cells('A1:C1'); s7.cell(1,1,'OA国别分布(自动排序·GROUPBY公式)').font=TITLE_F; s7.cell(1,1).border=GOLD_BD s7.merge_cells('A2:C2'); s7.cell(2,1,'=GROUPBY(有效≥2026!C3:C200,有效≥2026!A3:A200,COUNTA,3,0,-2)').font=GRAY_F s7.cell(4,1,f'=GROUPBY({REF}!C3:C200,{REF}!A3:A200,COUNTA,3,0,-2)').font=FORMULA_F; s7.cell(4,1).border=BORDER for w,c in zip([30,12],'AB'): s7.column_dimensions[c].width=w s7.merge_cells('A10:C10'); s7.cell(10,1,'💡 若WPS显示@前缀,选中单元格→删除@即可正常溢出').font=GRAY_F; s7.cell(10,1).fill=INFO_BG # ── S8: 公式-审批进度(多指标混合·COUNTIF) ── s8=wb.create_sheet('公式-审批进度') s8.merge_cells('A1:C1'); s8.cell(1,1,'OA审批进度 & 预警(COUNTIF公式)').font=TITLE_F; s8.cell(1,1).border=GOLD_BD hdr_row(s8,3,['指标','数值','备注']) rows8 = [ ('方案总数',f'=COUNTA({REF}!A4:A200)','≥2026年开工'), ('已完成审批',f'=COUNTIF({REF}!Z4:Z200,TRUE)','含"已完成"状态'), ('未完成审批',f'=COUNTIF({REF}!Z4:Z200,FALSE)','审批中+未审批'), ('🟠 橙色预警',f'=COUNTIF({REF}!AD4:AD200,"orange")','距开工≤30天'), ('🟡 黄色预警',f'=COUNTIF({REF}!AD4:AD200,"yellow")','距开工≤45天'), ('预警合计','=B7+B8','橙色+黄色'), ] for ri,(lab,fm,note) in enumerate(rows8): r=ri+4 s8.cell(r,1,lab).font=DATA_F; s8.cell(r,1).border=BORDER s8.cell(r,2,fm).font=FORMULA_F; s8.cell(r,2).border=BORDER s8.cell(r,3,note).font=GRAY_F; s8.cell(r,3).border=BORDER for w,c in zip([18,10,35],'ABC'): s8.column_dimensions[c].width=w # ── S9: 公式-预警明细 ── FILTER 动态筛选 s9=wb.create_sheet('公式-预警明细') s9.merge_cells('A1:C1'); s9.cell(1,1,'OA预警明细(FILTER动态筛选)').font=TITLE_F; s9.cell(1,1).border=GOLD_BD s9.merge_cells('A2:C2'); s9.cell(2,1,'=FILTER(有效≥2026!A3:AD200,有效≥2026!AD3:AD200<>"none","无预警")').font=GRAY_F s9.cell(4,1,f'=FILTER({REF}!A3:AD200,{REF}!AD3:AD200<>"none","🎉 无预警项")').font=FORMULA_F; s9.cell(4,1).border=BORDER for w,c in zip([22],'A'): s9.column_dimensions[c].width=w s9.merge_cells('A10:C10'); s9.cell(10,1,'💡 若WPS显示@前缀,选中单元格→删除@即可正常溢出').font=GRAY_F; s9.cell(10,1).fill=INFO_BG # ── S10: 公式-认定分类 ── s10=wb.create_sheet('公式-认定分类') s10.merge_cells('A1:C1'); s10.cell(1,1,'认定危大方案分类(GROUPBY公式)').font=TITLE_F; s10.cell(1,1).border=GOLD_BD s10.merge_cells('A2:C2'); s10.cell(2,1,'=GROUPBY(认定数据!H3:H200,认定数据!D3:D200,COUNTA,3,0)').font=GRAY_F s10.cell(4,1,f'=GROUPBY({CREF}!H3:H200,{CREF}!D3:D200,COUNTA,3,0)').font=FORMULA_F; s10.cell(4,1).border=BORDER for w,c in zip([20,12],'AB'): s10.column_dimensions[c].width=w s10.merge_cells('A10:C10'); s10.cell(10,1,'💡 若WPS显示@前缀,选中单元格→删除@即可正常溢出').font=GRAY_F; s10.cell(10,1).fill=INFO_BG # ── S10b: 公式-技术方案分类 ── s10b=wb.create_sheet('公式-技术方案分类') TREF="'认定技术方案'" s10b.merge_cells('A1:C1'); s10b.cell(1,1,'认定技术方案等级分布(GROUPBY公式)').font=TITLE_F; s10b.cell(1,1).border=GOLD_BD s10b.merge_cells('A2:C2'); s10b.cell(2,1,'=GROUPBY(认定技术方案!F3:F200,认定技术方案!C3:C200,COUNTA,3,0,-2)').font=GRAY_F s10b.cell(4,1,f'=GROUPBY({TREF}!F3:F200,{TREF}!C3:C200,COUNTA,3,0,-2)').font=FORMULA_F; s10b.cell(4,1).border=BORDER for w,c in zip([12,12],'AB'): s10b.column_dimensions[c].width=w s10b.merge_cells('A10:C10'); s10b.cell(10,1,'💡 若WPS显示@前缀,选中单元格→删除@即可正常溢出').font=GRAY_F; s10b.cell(10,1).fill=INFO_BG # ═══ 静态汇总 ═══ s11=wb.create_sheet('年度认定汇总') s11.merge_cells('A1:E1'); s11.cell(1,1,f'OA年度认定(≥2026·静态)').font=TITLE_F; s11.cell(1,1).border=GOLD_BD hdr_row(s11,3,['分类','方案数','项目数','占比','备注']) for r,(lab,val,proj,pct,note) in enumerate([ ('一般类',gen,valid_2026[valid_2026['是否超一定规模']!='是']['项目名称'].nunique(),f'{gen/tot*100:.0f}%','非超一定规模'), ('超规类',sup,valid_2026[valid_2026['是否超一定规模']=='是']['项目名称'].nunique(),f'{sup/tot*100:.0f}%','超一定规模'), ('合计',tot,projects,'100%',f'涵盖{len(countries)}国')]): fmts=[BOLD_F if '合计' in lab else DATA_F]*5 for c,(v,f) in enumerate(zip([lab,val,proj,pct,note],fmts)): cell=s11.cell(r+4,c+1,v); cell.font=f; cell.border=BORDER; cell.alignment=CENTER for w,col in zip([12,10,10,10,20],'ABCDE'): s11.column_dimensions[col].width=w s12=wb.create_sheet('国别×分类') s12.merge_cells('A1:D1'); s12.cell(1,1,'OA国别×分类·静态').font=TITLE_F; s12.cell(1,1).border=GOLD_BD ct=m.groupby(['所属国别','是否超一定规模']).size().unstack(fill_value=0) ct.columns=['一般类' if c=='否' else '超规类' for c in ct.columns]; ct['合计']=ct.sum(1); ct.loc['合计']=ct.sum() hdr_row(s12,3,['国别']+list(ct.columns)) for r,(idx,row) in enumerate(ct.iterrows()): for c,v in enumerate([idx]+[int(x) for x in row]): cell=s12.cell(r+4,c+1,v); cell.font=BOLD_F if '合计' in str(idx) else DATA_F; cell.border=BORDER; cell.alignment=CENTER s12.column_dimensions['A'].width=25 s13=wb.create_sheet('审批进度') s13.merge_cells('A1:D1'); s13.cell(1,1,f'OA审批进度·静态({REPORT_DATE})').font=TITLE_F; s13.cell(1,1).border=GOLD_BD hdr_row(s13,3,['指标','数值','占比','备注']) for r,(lab,val,pct,note,fmts) in enumerate([ ('方案总数',tot,'100%','≥2026年开工',[BLUE_F]*3+[GRAY_F]), ('已完成审批',int(completed),f'{completed/tot*100:.0f}%','含"已完成"',[GREEN_F]*3+[GRAY_F]), ('未完成审批',int(unfinished),f'{unfinished/tot*100:.0f}%','审批中+未审批',[DATA_F]*3+[GRAY_F]), ('🟠 橙色预警',int(orange),f'{orange/tot*100:.0f}%','≤30天',[ORANGE_F]*3+[GRAY_F]), ('🟡 黄色预警',int(yellow),f'{yellow/tot*100:.0f}%','≤45天',[Font(name='微软雅黑',bold=True,size=10,color='F9A825')]*3+[GRAY_F]), ('预警合计',int(warn_total),f'{warn_total/tot*100:.0f}%',f'🟠{orange}+🟡{yellow}',[RED_F]*3+[GRAY_F])]): for c,(v,f) in enumerate(zip([lab,val,pct,note],fmts)): cell=s13.cell(r+4,c+1,v); cell.font=f; cell.border=BORDER; cell.alignment=CENTER for w,col in zip([18,10,10,35],'ABCD'): s13.column_dimensions[col].width=w s14=wb.create_sheet('预警明细') s14.merge_cells('A1:H1'); s14.cell(1,1,f'OA预警明细·静态(共{warn_total}项)').font=TITLE_F; s14.cell(1,1).border=GOLD_BD hdr_row(s14,3,['信号','距开工','项目名称','方案名称','方案状态','计划开工','超规/一般','国别']) today=pd.Timestamp(REPORT_DATE) for r,(_,row) in enumerate(warn_df.iterrows()): days=(pd.to_datetime(row['分部分项工程计划开工日期'])-today).days vals=[{'orange':'🟠','yellow':'🟡','red':'🔴'}.get(row['预警信号'],''),f'{int(days)}天', row['项目名称'],row['方案名称'], row['方案状态_clean'] if pd.notna(row.get('方案状态_clean')) else row['方案状态'], str(row['分部分项工程计划开工日期'])[:10], '超规类' if row['是否超一定规模']=='是' else '一般类',row['所属国别']] for c,v in enumerate(vals): cell=s14.cell(r+4,c+1,v); cell.font=RED_F if c==1 and days<=3 else DATA_F; cell.border=BORDER; cell.fill=WARN_BG s14.auto_filter.ref=f'A3:H{warn_total+3}' for w,col in zip([6,8,40,35,18,12,10,20],'ABCDEFGH'): s14.column_dimensions[col].width=w wb.save(OUT) print(f"\n✅ {OUT}") print(f" S1-S3 数据源: OA{len(valid_all)}/{len(valid_2026)} + 认定{cert_tot}+技术{tech_tot}") print(f" S4 公式-认定vsOA: COUNTIFS项目级对比") print(f" S5 认定技术方案(表2): {tech_tot}行") print(f" S6-S10b 公式: GROUPBY+COUNTIF+FILTER(@问题有提示行)") print(f" S11-S14 静态汇总: 交叉验证")