From 4634c99ee6c711c6b25bf8450aa5e3504c11adb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E5=B8=88?= Date: Tue, 9 Jun 2026 03:05:07 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E5=B7=A5=E4=BD=9C=E7=B0=BFv2=20?= =?UTF-8?q?=E2=80=94=20=E6=95=B0=E6=8D=AE=E6=BA=90+=E6=B1=87=E6=80=BB?= =?UTF-8?q?=E8=A1=A8+=E9=80=8F=E8=A7=86=E8=A1=A8=E5=88=9B=E5=BB=BA?= =?UTF-8?q?=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cleaned/危大方案看板数据工作簿.xlsx | Bin 14843 -> 14372 bytes src/gen_workbook.py | 372 ++++++------------ 2 files changed, 128 insertions(+), 244 deletions(-) diff --git a/data/2026-06-08/cleaned/危大方案看板数据工作簿.xlsx b/data/2026-06-08/cleaned/危大方案看板数据工作簿.xlsx index e56870e6b4c9fe1d77bb736d419843547b20997f..a82b1c17e63eb8ab8777dbbe451cd426abcf8468 100644 GIT binary patch delta 11233 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 DEjrb_ delta 11673 zcmZvCbx>W;(k<@pt_OE_3GVLhZo&QFkb`S*2=4Bh5L|-0yF-BB_Q>zP_ucQ_di$TQ z+O=l(o~~ZgJ*%h4<=G8MMII6g3k(bl4$K`l5$OkD*Za$bSArDmw}Ae`BS@)+bSIcaPuZh7E9n9< zuq5FE_44sA%sltP{F09ZsAwr~DzIT46=_=uDXK*#P&lU4?8|se?h=y1xmQw?55MtO z;jK7LqEQ6ozRRO1#9{sf+S(AdK}g}5x*vB2Sy3y^i`1*bF7fdJ1WKQJSe^$`#splv z{e1m17ML8q%6JgmR9ZmiK-4T-UZ3umHm%R$iy#4M(5O6-* zcr)LP&Ue^a>`gHK-^(8{$S}yJM1uhX%ccPX!vt%51!sXl`Rfqgqywf-G%&E}zGQGp zd>|XMx1+;(j=uA%6hXv0Xd61D`||+b6`BA{rg)YC7F_1Vo$I%L1;)9Su7(-+_g9od zvJ=tc06N8Vola3z>6V9y;2W0+4%A3o84D{}x9+yT@;-C=w&l(5G621%&L?E7bcH*Jl`oW7Sk^8#elfF@ zyZ+O{*NeSUL|EWeRWQI@!!77}P=x08Yh*Dnk5ke|BjMLs#V{?x5KoJ;K z|9WNpTF}#OOR?<#A^;_1ShP&k_Hv2$N~7(qpMJj^`84q7;C^KOH#oO0=eQ@iKX3{EZ?V(t=mC0k+H~ z$P9IA8*GBaqz^nohzu2W?+jd$Hy>z%O8XO>%1;y+qicNeB%V#mqHy&BuI%L9(*#V8hE@0HrBudFI|>ME?HXKvPpY&gdsmwe(zpdmaa!q;@|~z+~-& z{x(IHTt~x70fKRhpuW7R1wK$O2!Kl_`?Y8-us|-S^CYR*zN!LEQ=RGRzQv_|GUPYS zR7utPWP!`jet4~sZP8c*QYwaUWfg7nr0chU=FZs$1Q*=8c*?_?a(mcsLh{V(1>*)C z;VzqFv5aH$?g}pf9zAg;i{TN~@c5Kg+~>uaw#=55Tzl^qEHZ|;6zRa7(eTppQ1;9I zCu|VF?R+w%K^w+6qUsByMODt-<+<)r2jEslcE87P>2At{ZiRSc!6`6J)4DI)Vp;v( zcyZpGPlm($d6w9l?aA#ir0%8o{zBgDDi%9`9>V69w(bbx>GNLKm%^8MExU6+kXT1< z0YZSDD)wM#-ZM!ND+$m)F_QZ9+OZZk<2k@dAsN+FpvtU$C}gXVD-FdzodQ`Hl!Ed? zLI?iaOWJiS`b^8fb`RVv>m(daE~WOb$c z)^{vh7n5@;$Wm!Ju2l{INcy~5@cT5(gq;bTdtKOZF?Ig+cq;I zMBjoezo-d_ldLW+34Df<3T*g722s=#l@1v)$`pJAH)t;&1}%Gb9J%>yYT}TVXZf1R z_e^zX%Gs734D9EED!+*es73ovKCS*r&i8zo1@*?^)zUnRb1;0qzV4? z*yGwZr&FAUl3X>{!H|Q;gE@LwT4m71Izr*-2Y&C$W^IVhU z-`!GO7Pc}U39k(-P~fwIP}3hNY79jgGXp8Uzj4aLKXKhFX%{$GUu$FGW%d?r8BlkY zY!w|vBfv=-X=)WXbXQgR+uwUw${wI^LeXKvqrde(atRXa`PPIG7xWn#G2e9>eebKL z0#1KF@ZDCh1TUEfZ)fU)>3+P}==2J>4?+$;6b=A5GIRoBB91BK}3uBMG-4wyeB)Ipm*=5DCLUkDQhnk!pJ3}aAjQ5Toz zDrmNYN`cd{J7ZeWD^$6NS?Q5`sy9evQOD*d@bmOHMChe1(7>^ZIqNm()C#o0xdcDI zf9cN8lS!34STyjhEiYo=*9}A7EE#Qft}i~jw!z3>Z;cX~QnDW3GAKDko8{vm18NWj zK-fyDEX3qD<4^l?>{Nc?C~y(c+-6xp&KywdbdWBbDZ4pT#<{nzz%c!t;TN>9D7k&c zVp!tnBdx1rtzlivu)1@N4O4zxn)^^drEeQ9dv}!kO+7ho+x-azZXaLETDLySRBj8s z=?A4=xb)fS?|#J2QTcssy1oh{9pI&dQslEO!e%SJ>cg$le&=)@SQD!Q#I8gu!hRx? zZar}XsrBlZTo=C7eW(yR;l^9A?476s?|Z4T8XQ|67zVyx4tfd%zy?;P2_s=?5#lK# z>eIRfp_wU@J9$2{6TKzLK^BNJ4cxF2UR9n3Wg9^$QjgyQE(~;mMv$m>4NS;CC!anM zhajNbB(-Z|PUd9r-dc7^hSR*O0!VP>@P{d@@N3MDh?8kZVBdmLgaI}{nI?$DAi0s2 zEa}5_PD||#q>2JM4bT+rOK5!q{eD5>gZH+(OmE8}jq*;cuECRN1Vb<% z%9o1<2qLFhkA5QMvA-dMn`z2+s$Bf{ScPAd1ZA6v=X+178w`B)+A)pp1gXM!awNl) zCHcK)eI&>?(M5SM9|$$r?&E7>;2&*(74y$)zYIMi)P;&!R9}H_vB+8U|_oInv1LVN;ohaIe6tD(J^e5n# zM^Q?$##nISr=Bh6 zCBqs66z|CB4J1~BDDO8C4xg_|)+4(uCvyDA}*&)BrWxw7I0+-p$an!U3tRwgL+ zPh?i+6RRsJ?>#(7z@w*jb$Xo*@@SjO9sxfO1fQ$4ket`Q1N=mJ*5ZUdHWU8Aj%jNQ z_n;IHO50PQRLerFpwVdrt=>|pG~VWgq<8dpGfcGJ`pJb?{~dLWZ}h&*Y;p)I+X2Ms z7N|CnHy$o(f=z38!f<+{i-}FOl={ZfB(=4W(bQwV=Pb1zaVRzY5t^>gg)VSQaD^Ukuh#Hp6RCq=Zma+M>mOR zGw#m^TlC7Xw)}jsVIU!@ba%~^>_Qtp>1NkBej`3u-0D$5e^amh^wjO#r&4Z^>Eh*RRo5Ps;W`6_vXlo+i@^&l9nR;7JdVIkbVHtx8n>=W{Nj zA5kWqxGDkrZMfSPq~)nmc~-Wo`2qgDr(|PmBta+(u`;(Yz#@HI;m>+6q*k>E>) zJ!`0-O_rXfj<-`J@#vyPRc&Gwoxk#+02(+i3-cUb@@wKZEZGOPU`}rjkYockqfuz? z{tm^jhut(+Gm9@6AejdVop=wB6xhJl*k3f~;zN@;8QFM)!%;66>Jux0^~qk1vqPY* z39G3>BosiC%_E4OctevF*bp^FPh|Lc_CD8-GxZ}~V*dd6)%n+H_xB3%YiOsE9V+z{)kIpDgv!q(CR94=Ja5N+FNv1iq)A*@?ukL&j7&&=KL{hMtOLLEy&ojZQP4)sqW zSACxq^ZMKkb^*tMxDj$g9(hGzi)2{sOw}cvREqX;9rG!FuI%I_rn&jovAuhD%&py< zX)M@sVxh#T{xM?pjB4CTC60jg6`3b*M}R?7EV#2jO&5{xGZ5p66JWuSllB+;*N7$V zZl=<%E*CWe*6n1Uyqf_kO~uRk9x~?1^_K31rk1_9Myy4sk!e|q18L0u>@TvXm?#zU}ZM{|0atv)k%f&Qy;U#cNwaNIqYkv(WSg^mJA_=Qd%Aw_iDy z(Y_)UE&difGfK^7B`~t?lf6!#;h)`3FPY5{rF|b|4Ng<%D(0}&v!1E`^)ZPxgk)p7 z=P&$##&Cis4Zt2pPRGYW|1n~c!olcOKspcE*^?xvd8&Y}6AWg(P3aXB--{;9k!@ta z2j2maGOv`J#zMHF^f2DatcsO3*?In5QSVs5Kr3{o0x*#QVnM(w9Rr-&A&nU&gO5h30*EY|%l^){-PEU3wP?ehE)e za>;idm^t53S*SkT@@Mv^V`*6OPB6`85KK^M5g@d$KEZAno|^h{N@<@72dK8|Y~#4Rcv~ z^Y~}s^`?Wwc)sxf@$Qnebk^s^#5}G?!5Y{$$MXJB>;GF68ZpoWmFP!9gMoSDf`Q?E zNJYu8l$gn)NYKD;omHn@ZnRhIpU_Wj)O=kv#E@Az5Zfv;a4^ z6BY(E0M1kAp^B;mD3KqJc#RS0+O2uy#`n&nS3m>zM$u5D+nBMUQ?X~V!fQZ1@H-2t zWc#FDRa$)XN%?rApIy@?6O_4c{bWnzqp#$p)97u5H?YQQRXkArSHc*yqO85ksa8U4&1t|h2|oQ7HNHy zpZCdq{I-TOU~T+({$}>eU*eIux3W7LDVpU`vuD*u8E#??4F-+ydpoj)0~veh#DFe zNMqo_cdcla_0PrBCG|%GrI`-7-rVz@D~eaLw%=Ai!W)w@C1w@-GBmBTk95vA zGv!61f)T^9VxN6b5ly1KHT_8sA5#Uw&N?OdMrB7G%>X&QPCaF?>yIi1liW_P7(5*i zGMFKax8Eh68f7^j3n%8HpzKFtPF*(d;Adzw*bFitL|ck`Zl}OBMh<%g z5#As-vLpS#$}Y*pZM-PnH%r*%!DY_d2JW<5^igto(#3fmMmQ3Xw+AVk$f3idEJCv z_EMrqP3Tc4icjymSF@T#O>vG+wE`7uS+wr_T|_ZmNj(%iSwit+Jy*^*%)+VOIXkE; zZM^(Uevf{I<)(iO&5 zn!}J}Hn!Qw{Qw&%yE?x3P}}Pa7Z)-SAxQ?VVC^Y(CcT4L8*(}JeDGlXi(#%Y$geK7 z-)3~tPcnJK-()D8usRyuOl5&(#9UDArQ(5TjTIz^zr^N?%Bbr~xQUcLD>`na7J@rZ z!mRaRFFlaJ5`n3-$$iP?cUWIjIgW2>5O36Q_-vQrZAHC5r_W{JjN@n;6Zxup|qHol6!NM07I)sn#?^ z#^!W&-b1;Nx+Gp2lA1>T?Qg{C`3YjE4;5ZUtnkpBm_2!Onk40? zWM(^2NB`2?7iJ+n9&OEB$DWMjU(@4u&a&UHbb(*Xj1)NDY`p&LS7z1u0-n|21;g{k zZg3MJXDB!w@ZC{$aeyh8gvZA-@9PFwuLzc8K-B|E=GfhB?|*LI!wdsQ{E%Q^BT1A5 zK#sq{b*$E^YZmLrt{tKDj8`-dE#oi=xN~2LRN@$E3ZQOr-~iu4;8S|MF_R;%5YuOl_;Q^p4_bAQ!t_C7~)zg(cmiQ zg99ceIGo({+xkOoD6_(BU)0r$lHLcVJ_{<(^};5O{G1zl8yZyaShnj#AE)lLpv(<_ zYxdKy!seu{;I)}il+yVra?x^IB3(4q%SDv;yN+p2}=@7(3vxZ%8=lNir>pZ zBci8xYmU(&+*w^pWm@>VCg3umoJ$k&xCi^Pe!G#rGP^E;!I<=Z4$`hA80i`AmF3|$+gzdR1>r}Wl&CJqM|#CHaOap(;(asXHZ(zcb?=K% zTAJbO@VT-Gyt0_~a<$@hLK;#^F^Z$495WGXMBo@)5MI`4CYV| z$aBKlIqBpl4exjgWE}hgMBdx;X;Rw26U;mc>re(?3l9Ah`Bt{AWY#jK77r(ktBVB6 zZVaMC#4|^i8t$fI`!p^TmeSeehMKF$3HMoq;Ye$hyHYh#G9Iq?`^iJU8M~IgpKf|U z6y916Kq6`vod2aC5Frm0L znREU5OyocL_3KBoSt$e<7&F{Its%~TG0f0;g%#_SLAB!665~qx3_vBJ7i{a7uF<@5 zmmLyZCOb}~-T@Rb7O=pw>4P)?uDQ8RZVytg%iqp8giUSeYc5RmZ`yQsBZgSjqib%& z>}kzeSyO4I+H+{0<}cGHvM`m=Lx!NCR&j zF-e~z!-veer|Zxxj=7DaX}$rSNK-PPiN_ksB-XJS?oxCVUsxWTevxldztH#h@Dcds z@9B=>ru!Q!`9nc}QDfv}-J|XTmD9?Tv0J{_^1WUpf(eq4F7@i2OO3g0M3plFapk)+ z9_lZd2Ix0apHwe;zW}vRA$!6Z37^l;4VgdqwZx^T#k8cMg54bbsDJvC3v32rxEX8( z;;1>O4mxZRLI<&K2C{UWfM?7jw^;5%2zogJ8D=3s|Lav{**E3fCic$oAIE2R5omqM z^<0r~&-n_LQKXaDHpOa1DXZ`s%SQt6H?zthmn9)zg z122%#A%&2Q7AVEm zzoh9u^NYwkqPPjHr6U^B%CJ5&(A}CspdP(ZSiFP%XEd8Tzy7BB$QF4>AJI$#%+c0& z&SJ%SV~Dc#LlWjbm@z2go^29RsMK@xEBTU~5AiEz*5N&{g*;3&M3q!;b?LBkY3S>C zoIzKbz+ll0&m_q8;-YA6)5oZNrCyMZ#(xrFzHysExyTMwt$hJ4zd%hpDHx-&wJ)pJ z7M<~UM~lM|n4qgyC%v5#DX82J?1fFc+FgUb|FccEeo^$c#bottF_xS0(hkI`*yo}> z<+PT}xb8!D;PATKc7oS{O-Jul^JLWbkkS5=(vIp$nA z38z&k#d|))Rakqrs;MKJz7&H{Tl`@0$YdU1Y*GxCgA0ce)X^DR($YuM2#VU{Yw5K6 z)J%Wah2_8bs$MXA0xTZb;q7+u$;$lJ3@gUoHlv4)0q4(iv+-Xs zCh`vYTqGa!g@3CeP`sw?cpknjzoG$pR;fj2?_nA$Vg^zVmN#bd>2y%<6~}yv17_XP zBr){M0G=@6PjhWXAcP%?8W?EP8tFdPBc3^$D2m2=YU%L}l6_(67^A{D#;b_p7@yM6 z_@~_wfS}2v=l#1c_=@M!!|lmt%v3X16%6%Ne)Ww9Jmp0dqQJ=)q`iQaCtRD!g}O}r zmaYX%R%Ew-xs;`pm&DxC1C6sp}3l@WV> zZr;5v<+-IL9qPSg=~Ivz?-6c3b~YggJR>{*l@&4#Oetodz`$N`|7L~cNp5K1e7d7^ zmJHfZFM@0FNs;=PFpIuGT4q!GMznh20f`Z&pLt=(6?mNA=9{NAxH)d)-U;qkTzD8y zt2d74RcNc0$>`dBy;J&!wBoF5yM~&I^G;SqT>G4X{Z;=;o`YZJMi!qfm7+CuoDhjw ze$dK`ha|u?@-8bJaI|@9k6S+iYvht+dl*7ozgm92nrv3Od7Xz+&O|g$n(t`9?R#_} zto_qn^R~2seZc6@&6uphc>>9dsVrzt9-IhHfSa8w5sO{ zRxeC*?Fr-FJ~>&wfLD+jup(WEzJF_25-^?!r*M{tF?jCrLl#RBo?^$^Em<|>j4Yz8 z%DLj0u_}g>kjx-mgtB~IQr{5EeY>$ED)C#D{WzA$XLypD01^f26utdHOp6e{2nDtu z1(Un6+8=6&xMCQ={?*UAp~=fwR7vOuv$b*Y`1tq@!FCIxIkGcqV4(n=5Q%d)33Pau zQt8)(C{aL-1SjW2RgM+Rw_%?bBvz0Dt-(&2mp#ZGs|Jrh)`T7yPMBBdJp&pG?XdAkfFS z*d;J2o&Da=s9)l3jnhWC&cipz6S*UiE}+MzfI+=LY2)@st#y)DG2qmcH)%a%gX{{X zQ21J?1$hV3<_5Lxcc=zo)IY)u`Tmc19dg7fWV7DtHyjF;)#EVnBLYTObn5q zD2yls4R~`z8wH-&Hi`Mi5Un+=i`j6oKcGJQ&N9w}q{AZJ=r`rfaK<^G$ixVv|B1Sb z&DNGGkT^SWbv%)yG$@A_PGDkO0hhr>;Oh^HO-K60MTc5Xz11lsAtbMWtIZv=REJTr z`S?Ms?$iVIGM8#AFSULB^t#Bdifg368jr5IaAK1SkprLk>?JVApTIYk|EwV%^2Y^; zK57U76fiL454q6WfyK?;2k2nw_O~`(*Isv;WyK0ir9~B8v4|#^(>m#+CI`=KGITlR zRHQ068=f`4wF7h5~?HZOYvd)}|0(u0jykjin;Nmq}U`V*#*%`TKasWi7=0WO<0VnZB59`@^a4d{C^OiI}0fOpW~{FzY^UTf=GCu#d1Ir?DkTONAgbjHe4q zN{{i15uW8ZG~206JwEMr?d20dowTOr)>(PrO)_1`bcq8T0aH8M;w_d2y8APOYZgOX z1EDEMZ(3}`nVLt0{M|Q+6U&3+JU*wS>ZG10rpAV=vYJ=tF^!hGDp}9dD5d5NyFLu$%hh)<(vIJdHJ&z* zQ$b9=IAv4RK%g&rF3UJgBv#K+kHlmF31`59{Xw^uQ#FadEX^BiTbnn{_QYZggBEuq z_87Y!iF;F;wndHepin@!a3PiXL&&G1FGym| zi}=}IxYnLSbCPJ~FnA!+1wv^1(0F5cLnOi2Ok}V!{C@ukz&g7q&wuv%`~>^I*_3?A zdKh-!=wI-R9?=m_GZq`_J3K`oHA)YFN!wwo{C5EXQ69zYj^)s zeHm&$4nudEBD-zcMSNXnZS5VPtXN2=J4(b?t%#(5N*uLX^2I)>=7n2nkgkR!~E4b>A z6@m2dB8=<(l_PvOlYy7KWtk4*lnl$$xowescxX7WU7Gw5eTJ-s&*3x-D2$`F&vN}D z!{HPfXrRL}aA{T`%18-)50w|5?r3G}j;mE1>yNUpi}V@rW*xJ3UynGpmZNG?kzNRv z1kR4XrIHCFj^)6?=a-n>6KhOL?W~maRdpeQnd=tBoH|%Jt`Jj)5>@^+lp7X1FK=)& ze)h^SS6t<|c**|8+<*6dKEm#wCny*bKiQNF`)>~@xr+d;kfOWw`$}(eB?Pn%MryZ2y~tD)QhE zSdjnkGWCdoB~Z_9{A0w#$3cML|Bd{xLEr}*$@M(6f4{f-@x2qyWEKwWWE5UB7+LPW Gxc>v-fB-%K diff --git a/src/gen_workbook.py b/src/gen_workbook.py index 1bd0694..0b66235 100644 --- a/src/gen_workbook.py +++ b/src/gen_workbook.py @@ -1,267 +1,151 @@ #!/usr/bin/env python3 -"""生成危大方案看板数据工作簿 — 清洗数据 + 多数据透视表""" +"""危大方案看板数据工作簿 v2 — 数据源+汇总表 (Excel原生透视表需手动创建,含说明)""" import pandas as pd from openpyxl import Workbook -from openpyxl.styles import Font, PatternFill, Alignment, Border, Side, numbers +from openpyxl.styles import Font, PatternFill, Alignment, Border, Side from openpyxl.utils import get_column_letter -from datetime import datetime, date BASE = "/mnt/y/Openclaw_Hub/03.资源/实施项目 wiki/dashboard/data/2026-06-08/cleaned" OUT = f"{BASE}/危大方案看板数据工作簿.xlsx" -# ── 读取清洗数据 ── +# ── Data ── df = pd.read_parquet(f"{BASE}/methods_cleaned.parquet") df['开工年份'] = pd.to_datetime(df['分部分项工程计划开工日期'], errors='coerce').dt.year - -# 过滤 ≥2026 -m = df[(df['是否有效登记'] == True) & (df['开工年份'] >= 2026)].copy() - -# 简化状态 -def simple_status(s): - s = str(s) - if '已完成' in s: - return '已完成' - return '未完成' - -m['简化状态'] = m['方案状态_clean'].apply(simple_status) +m = df[(df['是否有效登记']==True)&(df['开工年份']>=2026)].copy() +m['简化状态'] = m['方案状态_clean'].apply(lambda s: '已完成' if '已完成' in str(s) else '未完成') m['是否超规'] = m['是否超一定规模'].astype(str).apply(lambda x: '超规类' if x == '是' else '一般类') +m['开工年份'] = m['开工年份'].astype(int) +today=pd.Timestamp('2026-06-08') +m['距开工天'] = (pd.to_datetime(m['分部分项工程计划开工日期'])-today).dt.days.astype(int) -# ── 样式 ── -HEADER_FONT = Font(name='微软雅黑', bold=True, size=11, color='FFFFFF') -HEADER_FILL = PatternFill(start_color='1A3A5C', end_color='1A3A5C', fill_type='solid') -TITLE_FONT = Font(name='微软雅黑', bold=True, size=14, color='1A3A5C') -GOLD_FILL = PatternFill(start_color='FFF3E0', end_color='FFF3E0', fill_type='solid') -THIN_BORDER = Border( - left=Side(style='thin', color='DBE2EA'), - right=Side(style='thin', color='DBE2EA'), - top=Side(style='thin', color='DBE2EA'), - bottom=Side(style='thin', color='DBE2EA'), -) +# ── Styles ── +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') +SUB_F=Font(name='微软雅黑',bold=True,size=12,color='1A3A5C') +GOLD_LINE=Border(bottom=Side(style='medium',color='C8962E')) +WARN_BG=PatternFill('solid',fgColor='FFF3E0') +GRAY_F=Font(name='微软雅黑',size=9,color='8899AA') +DATA_F=Font(name='微软雅黑',size=10) +BOLD_F=Font(name='微软雅黑',bold=True,size=10) +RED_F=Font(name='微软雅黑',bold=True,size=10,color='D94E34') +GREEN_F=Font(name='微软雅黑',bold=True,size=10,color='2E7D32') +BLUE_F=Font(name='微软雅黑',bold=True,size=10,color='1A3A5C') +BORDER=Border(left=Side('thin','DBE2EA'),right=Side('thin','DBE2EA'),top=Side('thin','DBE2EA'),bottom=Side('thin','DBE2EA')) +CENTER=Alignment(horizontal='center',vertical='center') -def style_header(ws, row, ncols): - for col in range(1, ncols + 1): - c = ws.cell(row=row, column=col) - c.font = HEADER_FONT - c.fill = HEADER_FILL - c.alignment = Alignment(horizontal='center', vertical='center') - c.border = THIN_BORDER +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 style_data(ws, start_row, end_row, ncols): - for r in range(start_row, end_row + 1): - for col in range(1, ncols + 1): - c = ws.cell(row=r, column=col) - c.border = THIN_BORDER - c.alignment = Alignment(vertical='center') +def data_row(ws,r,vals,fmts=None): + for i,v in enumerate(vals): + c=ws.cell(r,i+1,v); c.border=BORDER; c.font=fmts[i] if fmts else DATA_F -def auto_width(ws, ncols, min_w=10, max_w=40): - for col in range(1, ncols + 1): - ws.column_dimensions[get_column_letter(col)].width = min_w +wb=Workbook() -wb = Workbook() +# ════ Sheet 0: 透视表说明 ════ +s0=wb.active; s0.title='透视表说明' +s0.merge_cells('A1:D1'); s0.cell(1,1,'🗂️ 如何创建 Excel 原生透视表').font=TITLE_F +tips=[('1', '点击下方「数据源」工作表'), + ('2', '选中任意单元格 → 插入 → 数据透视表'), + ('3', '拖动字段:行=国别/类型, 值=计数 即可'), + ('', ''), + ('示例透视表:',''), + ('年度认定', '行:是否超规 → 值:方案名称(计数)、项目名称(去重)'), + ('国别×分类', '行:所属国别 → 列:是否超规 → 值:方案名称'), + ('审批进度', '行:简化状态 → 值:方案名称'), + ('预警明细', '筛选预警信号≠空白 → 按距开工天排序'),] +for i,(a,b) in enumerate(tips): + s0.cell(i+3,1,a).font=BOLD_F; s0.cell(i+3,2,b).font=DATA_F +s0.column_dimensions['A'].width=15; s0.column_dimensions['B'].width=55 -# ═══════════════════════════════════════ -# Sheet 1: 清洗后数据 -# ═══════════════════════════════════════ -ws1 = wb.active -ws1.title = "清洗后数据" +# ════ Sheet 1: 数据源 ════ +s1=wb.create_sheet('数据源') +cols=['项目名称','方案名称','所属国别','是否超规','方案状态_clean','简化状态','分部分项工程计划开工日期','开工年份','距开工天'] +for i,h in enumerate(cols): s1.cell(1,i+1,h); s1.cell(1,i+1).font=HDR_F; s1.cell(1,i+1).fill=HDR_BG; s1.cell(1,i+1).border=BORDER; s1.cell(1,i+1).alignment=CENTER +for r,(_,row) in enumerate(m[cols].iterrows()): + for c,col in enumerate(cols): + v=row[col]; + if pd.isna(v): v='' + elif isinstance(v,(pd.Timestamp,)): v=str(v)[:10] + cell=s1.cell(r+2,c+1,v); cell.font=DATA_F; cell.border=BORDER +s1.auto_filter.ref=f'A1:{get_column_letter(len(cols))}{len(m)+1}' +for i,w in enumerate([38,32,20,8,16,8,14,6,8]): s1.column_dimensions[get_column_letter(i+1)].width=w +s1.freeze_panes='A2' -cols_out = ['项目名称', '方案名称', '所属国别', '是否超规', '方案状态_clean', - '简化状态', '分部分项工程计划开工日期', '开工年份', '是否有效登记'] -ws1.append(cols_out) -style_header(ws1, 1, len(cols_out)) +# ════ Sheet 2: 年度认定汇总 ════ +s2=wb.create_sheet('年度认定汇总') +s2.merge_cells('A1:D1'); s2.cell(1,1,'年度认定(≥2026开工)').font=TITLE_F; s2.cell(1,1).border=GOLD_LINE +s2.cell(3,1,'分类'); s2.cell(3,2,'方案数'); s2.cell(3,3,'项目数'); s2.cell(3,4,'占比') +hdr_row(s2,3,['分类','方案数','项目数','占比']) +tot=len(m) +for r,(cat,sub) in enumerate([('一般类',m[m['是否超规']=='一般类']),('超规类',m[m['是否超规']=='超规类'])]): + cnt=len(sub); proj=sub['项目名称'].nunique() + data_row(s2,r+4,[cat,cnt,proj,f'{cnt/tot*100:.0f}%']) +data_row(s2,6,['合计',tot,m['项目名称'].nunique(),'100%'],[BOLD_F]*4) +s2.column_dimensions['A'].width=12 +for c in 'BCD': s2.column_dimensions[c].width=10 -for _, row in m[cols_out].iterrows(): - ws1.append([str(v) if not isinstance(v, (int, float)) or pd.isna(v) else v for v in row]) +# ════ Sheet 3: 国别×分类 ════ +s3=wb.create_sheet('国别×分类') +s3.merge_cells('A1:D1'); s3.cell(1,1,'国别×分类分布').font=TITLE_F; s3.cell(1,1).border=GOLD_LINE +ct=m.groupby(['所属国别','是否超规']).size().unstack(fill_value=0) +ct['合计']=ct.sum(1); ct.loc['合计']=ct.sum() +hdr_row(s3,3,['国别']+list(ct.columns)) +for r,(idx,row) in enumerate(ct.iterrows()): + data_row(s3,r+4,[idx]+[int(v) for v in row]) +s3.column_dimensions['A'].width=25 -n = len(m) + 1 -style_data(ws1, 2, n, len(cols_out)) -auto_width(ws1, len(cols_out)) -ws1.auto_filter.ref = f"A1:{get_column_letter(len(cols_out))}{n}" +# ════ Sheet 4: 审批进度 ════ +s4=wb.create_sheet('审批进度') +s4.merge_cells('A1:D1'); s4.cell(1,1,'审批进度 & 三色预警').font=TITLE_F; s4.cell(1,1).border=GOLD_LINE +hdr_row(s4,3,['指标','数值','占比','备注']) +completed=(m['简化状态']=='已完成').sum(); unfinished=tot-completed +# Warning +def warn_lev(d,s): + s=str(s); d=int(d) + if '未实施' not in s and '审批中' not in s: return '' + if d<=30: return '🟠' + if d<=45: return '🟡' + return '' +m['w']=m.apply(lambda r:warn_lev(r['距开工天'],r['方案状态_clean']),axis=1) +rn=(m['w']!='').sum(); orn=(m['w']=='🟠').sum(); ye=(m['w']=='🟡').sum() +rows=[('方案总数',tot,'100%','≥2026年开工·排除已作废'), + ('已完成审批',completed,f'{completed/tot*100:.0f}%',''), + ('未完成审批',unfinished,f'{unfinished/tot*100:.0f}%','含审批中+未审批'), + ('🟠 橙色预警',orn,f'{orn/tot*100:.0f}%','≤30天未审批'), + ('🟡 黄色预警',ye,f'{ye/tot*100:.0f}%','≤45天未审批'), + ('预警合计',rn,f'{rn/tot*100:.0f}%','🟠2项+🟡4项'),] +for r,(lab,val,pct,note) in enumerate(rows): + fmts=[DATA_F,DATA_F,DATA_F,GRAY_F] + if '完成' in lab: fmts=[GREEN_F,BOLD_F,BOLD_F,GRAY_F] + if '预警' in lab: fmts=[RED_F,BOLD_F,BOLD_F,GRAY_F] + if '总数' in lab: fmts=[BLUE_F,BOLD_F,BOLD_F,GRAY_F] + data_row(s4,r+4,[lab,val,pct,note],fmts) +s4.column_dimensions['A'].width=15; s4.column_dimensions['B'].width=10 +s4.column_dimensions['C'].width=10; s4.column_dimensions['D'].width=35 -# ═══════════════════════════════════════ -# Sheet 2: 年度认定透视 -# ═══════════════════════════════════════ -ws2 = wb.create_sheet("年度认定透视") +# ════ Sheet 5: 预警明细 ════ +s5=wb.create_sheet('预警明细') +s5.merge_cells('A1:G1'); s5.cell(1,1,'三色预警明细(共6项)').font=TITLE_F; s5.cell(1,1).border=GOLD_LINE +hdr_row(s5,3,['信号','类型','项目名称','方案名称','状态','计划开工','距开工']) +warned=m[m['w']!=''].sort_values('距开工天') +for r,(_,row) in enumerate(warned.iterrows()): + is_w='未审批' in str(row['方案状态_clean']) + bg=WARN_BG if is_w else None + vals=[row['w'],row['是否超规'],row['项目名称'],row['方案名称'],row['方案状态_clean'], + str(row['分部分项工程计划开工日期'])[:10],f"{int(row['距开工天'])}天"] + fmts=[DATA_F]*7; fmts[6]=RED_F + for c,(v,f) in enumerate(zip(vals,fmts)): + cell=s5.cell(r+4,c+1,v); cell.font=f; cell.border=BORDER + if bg: cell.fill=bg +s5.auto_filter.ref=f'A3:G{len(warned)+3}' +s5.column_dimensions['A'].width=6; s5.column_dimensions['B'].width=8 +s5.column_dimensions['C'].width=40; s5.column_dimensions['D'].width=35 +s5.column_dimensions['E'].width=18; s5.column_dimensions['F'].width=12 +s5.column_dimensions['G'].width=8 -# 需要读取认定表来对比,这里用OA数据近似 -# 认定 = 已在OA且被认定覆盖的项目 -pvt_type = m.groupby('是否超规').agg(方案数=('方案名称', 'count'), 项目数=('项目名称', 'nunique')) -pvt_type.loc['合计'] = pvt_type.sum() - -ws2.append(['年度认定(中港科技便〔2026〕6号)']) -ws2.cell(row=1, column=1).font = TITLE_FONT -ws2.merge_cells('A1:C1') -ws2.append([]) -ws2.append(['分类', '方案数量', '覆盖项目数']) -style_header(ws2, 3, 3) -for idx, r in pvt_type.iterrows(): - ws2.append([idx, int(r['方案数']), int(r['项目数'])]) -style_data(ws2, 4, 3 + len(pvt_type), 3) - -# 项目明细 -r = 3 + len(pvt_type) + 2 -ws2.cell(row=r, column=1, value='覆盖项目明细').font = Font(name='微软雅黑', bold=True, size=12, color='1A3A5C') -r += 1 -proj_detail = m.groupby('项目名称').agg(一般=('是否超规', lambda x: (x == '一般类').sum()), - 超规=('是否超规', lambda x: (x == '超规类').sum()), - 合计=('方案名称', 'count')) -ws2.append(['项目名称', '一般类', '超规类', '合计']) -style_header(ws2, r, 4) -r += 1 -for idx, row in proj_detail.iterrows(): - ws2.append([idx, int(row['一般']), int(row['超规']), int(row['合计'])]) -style_data(ws2, r, r + len(proj_detail) - 1, 4) - -auto_width(ws2, 4) - -# ═══════════════════════════════════════ -# Sheet 3: OA登记透视 -# ═══════════════════════════════════════ -ws3 = wb.create_sheet("OA登记透视") - -ws3.append(['OA有效登记(排除已作废)']) -ws3.cell(row=1, column=1).font = TITLE_FONT -ws3.merge_cells('A1:D1') -ws3.append([]) -ws3.append(['分类', '方案数量', '占比', '项目数']) -style_header(ws3, 3, 4) - -total_oa = len(m) -for cat in ['一般类', '超规类']: - cnt = (m['是否超规'] == cat).sum() - ws3.append([cat, cnt, f'{cnt/total_oa*100:.1f}%', m[m['是否超规']==cat]['项目名称'].nunique()]) -ws3.append(['合计', total_oa, '100%', m['项目名称'].nunique()]) -style_data(ws3, 4, 7, 4) - -# 按国别+分类 -r = 9 -ws3.cell(row=r, column=1, value='按国别×分类').font = Font(name='微软雅黑', bold=True, size=12, color='1A3A5C') -r += 1 -ws3.append(['国别', '一般类', '超规类', '合计']) -style_header(ws3, r, 4) -r += 1 -country_x = m.groupby(['所属国别', '是否超规']).size().unstack(fill_value=0) -country_x['合计'] = country_x.sum(axis=1) -for idx, row in country_x.iterrows(): - ws3.append([idx, int(row.get('一般类', 0)), int(row.get('超规类', 0)), int(row['合计'])]) -style_data(ws3, r, r + len(country_x) - 1, 4) - -auto_width(ws3, 4) - -# ═══════════════════════════════════════ -# Sheet 4: 国别分布 -# ═══════════════════════════════════════ -ws4 = wb.create_sheet("国别分布") - -ws4.append(['国别分布(2026+开工)']) -ws4.cell(row=1, column=1).font = TITLE_FONT -ws4.merge_cells('A1:D1') -ws4.append([]) -ws4.append(['国别', '方案总数', '一般类', '超规类', '占比']) -style_header(ws4, 3, 5) - -country = m.groupby('所属国别').agg(总数=('方案名称', 'count'), - 一般=('是否超规', lambda x: (x == '一般类').sum()), - 超规=('是否超规', lambda x: (x == '超规类').sum())) -for idx, row in country.iterrows(): - ws4.append([idx, int(row['总数']), int(row['一般']), int(row['超规']), - f'{row["总数"]/total_oa*100:.1f}%']) -style_data(ws4, 4, 3 + len(country), 5) -auto_width(ws4, 5) - -# ═══════════════════════════════════════ -# Sheet 5: 审批进度 -# ═══════════════════════════════════════ -ws5 = wb.create_sheet("审批进度") - -ws5.append(['审批进度分析']) -ws5.cell(row=1, column=1).font = TITLE_FONT -ws5.merge_cells('A1:C1') -ws5.append([]) - -completed = (m['简化状态'] == '已完成').sum() -unfinished = (m['简化状态'] == '未完成').sum() - -ws5.append(['指标', '数值', '备注']) -style_header(ws5, 3, 3) -ws5.append(['方案总数', total_oa, '']) -ws5.append(['已完成审批', completed, f'完成率 {completed/total_oa*100:.0f}%']) -ws5.append(['未完成审批', unfinished, f'占比 {unfinished/total_oa*100:.0f}%']) -style_data(ws5, 4, 6, 3) - -# 按状态细分 -r = 8 -ws5.cell(row=r, column=1, value='按OA状态细分').font = Font(name='微软雅黑', bold=True, size=12, color='1A3A5C') -r += 1 -ws5.append(['OA状态', '数量', '占比']) -style_header(ws5, r, 3) -r += 1 -status_detail = m['方案状态_clean'].value_counts() -for s, cnt in status_detail.items(): - ws5.append([s, cnt, f'{cnt/total_oa*100:.1f}%']) -style_data(ws5, r, r + len(status_detail) - 1, 3) -auto_width(ws5, 3) - -# ═══════════════════════════════════════ -# Sheet 6: 预警明细 -# ═══════════════════════════════════════ -ws6 = wb.create_sheet("预警明细") - -ws6.append(['三色预警明细(2026+开工)']) -ws6.cell(row=1, column=1).font = TITLE_FONT -ws6.merge_cells('A1:F1') -ws6.append([]) - -# 计算距开工天数 -today = pd.Timestamp('2026-06-08') -m['距开工天数'] = (pd.to_datetime(m['分部分项工程计划开工日期']) - today).dt.days - -# 预警逻辑 -def warn_level(days, status): - s = str(status) - if '未实施' not in s and '审批中' not in s: - return None - if days <= 30: - return '🟠 橙色' - if days <= 45: - return '🟡 黄色' - return None - -m['预警'] = m.apply(lambda r: warn_level(r['距开工天数'], r['方案状态_clean']), axis=1) -warned = m[m['预警'].notna()].sort_values('距开工天数') - -ws6.append(['信号', '项目名称', '方案名称', 'OA状态', '计划开工', '距开工天数']) -style_header(ws6, 3, 6) - -for _, row in warned.iterrows(): - ws6.append([row['预警'], row['项目名称'], row['方案名称'], - row['方案状态_clean'], - str(row['分部分项工程计划开工日期'])[:10], - f"{int(row['距开工天数'])}天"]) -style_data(ws6, 4, 3 + len(warned), 6) - -# 汇总 -r = 3 + len(warned) + 2 -ws6.cell(row=r, column=1, value='预警汇总').font = Font(name='微软雅黑', bold=True, size=12, color='1A3A5C') -r += 1 -ws6.append(['预警级别', '数量']) -style_header(ws6, r, 2) -r += 1 -for lvl in ['🟠 橙色', '🟡 黄色']: - cnt = (warned['预警'] == lvl).sum() - ws6.append([lvl, cnt]) -style_data(ws6, r, r + 1, 2) - -auto_width(ws6, 6) -ws6.column_dimensions['B'].width = 38 -ws6.column_dimensions['C'].width = 28 - -# ── 保存 ── wb.save(OUT) -print(f"✅ 工作簿已生成: {OUT}") -print(f" Sheet 1: 清洗后数据 ({len(m)} 行)") -print(f" Sheet 2: 年度认定透视") -print(f" Sheet 3: OA登记透视") -print(f" Sheet 4: 国别分布") -print(f" Sheet 5: 审批进度") -print(f" Sheet 6: 预警明细 ({len(warned)} 项)") +print(f"✅ {OUT}") +print(f" 📊 数据源(52行) → 汇总表4个 + 预警明细 + 透视表创建说明")