279 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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年度公司认定危大方案明细中港科技便20266号·{cert_tot}项)',CERT_COLS)
# ═══ S4: 公式-认定vsOA ═══
# 项目名从认定数据 C4:C200 提取,省去 UNIQUEWPS兼容性更好
# 用 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 静态汇总: 交叉验证")