先读取约束里面的说明情况,然后读取单量预估表,开始严谨工作
Criado em: 27 de maio de 2026
Respondido usando GPT-5.5 Thinking por Chat01
Criado em: 27 de maio de 2026
Respondido usando GPT-5.5 Thinking por Chat01
先读取约束里面的说明情况,然后读取单量预估表,开始严谨工作
pythonfrom artifact_tool import Blob, SpreadsheetFile, Workbook path_xlsx = "/mnt/data/仓储部单量预估表.xlsx" path_constraints = "/mnt/data/约束.txt" print(open(path_constraints, "r", encoding="utf-8").read()) wb = SpreadsheetFile.import_xlsx(Blob.load(path_xlsx)) print(wb.inspect({"kind":"sheet","include":"id,name"}).ndjson[:2000])
pythonraw_inspect = wb.inspect({"kind":"table","range":"'24.1-26.5.26单量'!A1:B20","include":"values,formulas","table_max_rows":25,"table_max_cols":5}) print(raw_inspect.ndjson)
pythonprint(wb.inspect({"kind":"table","range":"'24.1-26.5.26单量'!A730:B775","include":"values,formulas","table_max_rows":50,"table_max_cols":3}).ndjson)
pythonraw_data = wb.inspect({"kind":"table","range":"'24.1-26.5.26单量'!A1:B775","include":"values","table_max_rows":800,"table_max_cols":3}) txt = raw_data.ndjson print(len(txt), txt[:500], txt[-500:])
pythonimport json, re, datetime, math, statistics def inspect_values(rng, max_rows=200, max_cols=50): nd = wb.inspect({"kind":"table","range":rng,"include":"values,formulas","table_max_rows":max_rows,"table_max_cols":max_cols}).ndjson # print(nd[:200]) return json.loads(nd.splitlines()[0]) for start in [1,101,201]: d=inspect_values(f"'24.1-26.5.26单量'!A{start}:B{start+9}",20,3) print(start, d.get("values")[:3], d.get("rows"), d.get("cols"))
pythonsheets = ["0-单量预测表","1-月份(季节)资源量预测","2-天气影响","3-星期影响","4-活动单量影响","4-最近4个月活动分析","5-25年运营主推","模版"] for s in sheets: print("\n====",s,"====") try: print(wb.inspect({"kind":"table","range":f"'{s}'!A1:J15","include":"values,formulas","table_max_rows":15,"table_max_cols":10}).ndjson[:4000]) except Exception as e: print("ERR",e)
python# Render first two sheets small previews to understand style try: img = wb.render({"sheet_name":"0-单量预测表","range":"A1:K15","scale":1}) img.save("/mnt/data/preview_existing.png") print("saved preview") except Exception as e: print("render failed", e)
python# Render first two sheets small previews to understand style try: img = wb.render({"sheet_name":"0-单量预测表","range":"A1:K15","scale":1}) img.save("/mnt/data/preview_existing.png") print("saved preview") except Exception as e: print("render failed", e) <<ImageDisplayed>>
pythonimport json, datetime, math, statistics, re rows=[] for start in range(2, 776, 80): end = min(775, start+79) d=json.loads(wb.inspect({"kind":"table","range":f"'24.1-26.5.26单量'!A{start}:B{end}","include":"values","table_max_rows":100,"table_max_cols":3}).ndjson) vals=d.get("values",[]) for r in vals: if len(r)>=2: rows.append(r) len(rows), rows[:3], rows[-3:] (774, [[5618, '2024-04-01'], [4644, '2024-04-02'], [4046, '2024-04-03']], [[2412, '2026-05-25'], [2410, '2026-05-26'], [3478001, 'NA']])
pythonfrom datetime import date, timedelta data=[] for pack, ds in rows: try: if isinstance(ds, str): if ds == 'NA': continue d = datetime.datetime.strptime(ds[:10], "%Y-%m-%d").date() else: # Excel serial? d = date(1899,12,30)+timedelta(days=int(ds)) if isinstance(pack,(int,float)) and pack < 1000000: data.append((d, float(pack))) except Exception as e: print("skip",pack,ds,e) len(data), data[0], data[-1], sum(v for _,v in data) (773, (datetime.date(2024, 4, 1), 5618.0), (datetime.date(2026, 5, 26), 2410.0), 3478001.0)
pythondates=[d for d,_ in data] missing=[] d=dates[0] data_dict=dict(data) while d<=dates[-1]: if d not in data_dict: missing.append(d) d += timedelta(days=1) len(missing), missing[:20], dates[0], dates[-1], (dates[-1]-dates[0]).days+1 (13, [datetime.date(2024, 10, 18), datetime.date(2024, 10, 19), datetime.date(2024, 10, 20), datetime.date(2024, 10, 21), datetime.date(2024, 10, 22), datetime.date(2024, 10, 23), datetime.date(2024, 10, 24), datetime.date(2024, 10, 25), datetime.date(2024, 10, 26), datetime.date(2024, 10, 27), datetime.date(2024, 10, 28), datetime.date(2024, 10, 29), datetime.date(2024, 10, 30)], datetime.date(2024, 4, 1), datetime.date(2026, 5, 26), 786)
pythonfrom collections import defaultdict monthly=defaultdict(float) monthly_days=defaultdict(int) for d,v in data: monthly[(d.year,d.month)]+=v monthly_days[(d.year,d.month)]+=1 for k in sorted(monthly): y,m=k print(f"{y}-{m:02d}", monthly[k], monthly_days[k])
pythonimport calendar # compare 2026 vs 2025 for Jan-May, adjust partial May ratios=[] for m in range(1,6): val26 = monthly[(2026,m)] days26 = monthly_days[(2026,m)] days_in_month = calendar.monthrange(2026,m)[1] val26_adj = val26/days26*days_in_month val25=monthly[(2025,m)] ratio=val26_adj/val25 ratios.append((m,val26_adj,val25,ratio)) ratios [(1, 93170.0, 144522.0, 0.6446769349995156), (2, 49987.0, 125181.0, 0.39931778784320304), (3, 92453.0, 199801.0, 0.4627254117847258), (4, 88286.0, 210378.0, 0.4196541463461008), (5, 78180.8076923077, 163118.0, 0.479289886415403)]
pythonweights={1:0.10,2:0.10,3:0.20,4:0.30,5:0.30} trend=sum(r*weights[m] for m,_,_,r in ratios) trend 0.4666277644696682
python# weekday factors from residuals to monthly avg, exclude partial/invalid incomplete 2024 Oct? Include months with complete data only and maybe 2025/2026 recent only. complete_months = {k for k,days in monthly_days.items() if days == calendar.monthrange(*k)[1]} # include 2024 Apr etc; exclude 2024 Oct and 2026 May partial. resid_by_wd=defaultdict(list) for d,v in data: k=(d.year,d.month) if k in complete_months: month_avg = monthly[k]/monthly_days[k] resid_by_wd[d.weekday()].append(v/month_avg) wd_names_cn=["星期一","星期二","星期三","星期四","星期五","星期六","星期日"] wd_factor={wd:statistics.mean(resid_by_wd[wd]) for wd in {0: 1.0578495749594174, 1: 1.0662709854966665, 2: 1.0164332016747424, 3: 0.9935804484709775, 4: 0.9589247018942662, 5: 0.9284316174386502, 6: 0.9785094700652799}
pythonfrom collections import defaultdict import datetime, calendar, statistics, math, json, re # Data already available as data list # Monthly summary from 2024-04 to 2026-05 month_list=[] start_m = date(2024,4,1) end_m = date(2026,12,1) m=start_m while m<=end_m: y,mo=m.year,m.month cal_days=calendar.monthrange(y,mo)[1] actual=monthly.get((y,mo),0) obs_days=monthly_days.get((y,mo),0) adj=actual/obs_days*cal_days if obs_days>0 else 0 complete=(obs_days==cal_days) month_list.append((y,mo,datetime.datetime(y,mo,1),cal_days,obs_days,actual,adj,complete)) # next month if mo==12: m=date(y+1,1,1) else: m=date(y,mo+1,1) # Forecast parameters trend_ratios = ratios # from above Jan-May recent_trend = sum(r*weights[m] for m,_,_,r in trend_ratios) macro_factor = 1.02 # resource factor default # monthly weather factors for future months weather_adj = {1:1.03,2:0.99,3:1.01,4:1.02,5:1.03,6:1.02,7:0.98,8:0.99,9:1.02,10:1.02,11:1.04,12:1.03} activity_adj = {m:1.00 for m in range(1,13)} # Make 618 and 11 double etc month-level residual minimal to avoid double count activity_adj.update({6:1.01,11:1.02,12:1.00,5:1.00,10:1.00}) resource_adj = {m:1.00 for m in range(1,13)} # Maybe use slight recovery trend ramp from recent Jun-Dec? Could keep in resource adj. resource_adj.update({6:1.00,7:1.00,8:1.01,9:1.02,10:1.02,11:1.03,12:1.03}) # baseline 2025 totals for all months: base_2025 = {m: monthly.get((2025,m),0) for m in range(1,13)} actual_2026 = {m: monthly.get((2026,m),0) for m in range(1,13)} actual_days_2026 = {m: monthly_days.get((2026,m),0) for m in range(1,13)} forecast_start=date(2026,5,27) monthly_param=[] for mo in range(1,13): b=base_2025[mo] ratio26=0 obs=actual_2026[mo] obsdays=actual_days_2026[mo] caldays=calendar.monthrange(2026,mo)[1] adj26=obs/obsdays*caldays if obsdays>0 else 0 ratio26=adj26/b if b else 0 full_forecast=b*recent_trend*macro_factor*resource_adj[mo]*weather_adj[mo]*activity_adj[mo] # actual before forecast_start if mo < forecast_start.month: actual_before = actual_2026[mo] elif mo == forecast_start.month: actual_before=sum(v for d,v in data if d.year==2026 and d.month==mo and d<forecast_start) else: actual_before=0 target=max(0, full_forecast - actual_before) monthly_param.append((mo,calendar.month_name[mo],b,obs,obsdays,adj26,ratio26,recent_trend,macro_factor,resource_adj[mo],weather_adj[mo],activity_adj[mo],full_forecast,actual_before,target)) monthly_param[4:], recent_trend ([(5, 'May', 163118.0, 65571.0, 26, 78180.8076923077, 0.479289886415403, 0.4666277644696682, 1.02, 1.0, 1.03, 1.0, 79966.82630161235, 65571.0, 14395.826301612353), (6, 'June', 134401.0, 0, 0, 0, 0.0, 0.4666277644696682, 1.02, 1.0, 1.02, 1.01, 65901.42313260294, 0, 65901.42313260294), (7, 'July', 112424.0, 0, 0, 0, 0.0, 0.4666277644696682, 1.02, 1.0, 0.98, 1.0, 52439.17572882088, 0, 52439.17572882088), (8, 'August', 120006.0, 0, 0, 0, 0.0, 0.4666277644696682, 1.02, 1.01, 0.99, 1.0, 57112.38232359264, 0, 57112.38232359264), (9, 'September', 141524.0, 0, 0, 0, 0.0, 0.4666277644696682, 1.02, 1.02, 1.02, 1.0, 70081.14454864213, 0, 70081.14454864213), (10, 'October', 159542.0, 0, 0, 0, 0.0, 0.4666277644696682, 1.02, 1.02, 1.02, 1.0, 79003.46205293422, 0, 79003.46205293422), (11, 'November', 168255.0, 0, 0, 0, 0.0, 0.4666277644696682, 1.02, 1.03, 1.04, 1.02, 87500.28393940558, 0, 87500.28393940558), (12, 'December', 142062.0, 0, 0, 0, 0.0, 0.4666277644696682, 1.02, 1.03, 1.03, 1.0, 71733.68172979957, 0, 71733.68172979957)], 0.4666277644696682)
python# activity events: (name, start, end, factor, source_type) activity_events=[ ("天猫618预售/抢先购(官方已公布)", date(2026,5,21), date(2026,5,30), 1.12, "来源:新浪财经转载天猫招商规则;用于5/27-5/30剩余天"), ("天猫618第二波现货(官方已公布)", date(2026,5,31), date(2026,6,21), 1.20, "来源:新浪财经转载天猫招商规则"), ("开学/换季小高峰", date(2026,9,1), date(2026,9,10), 1.06, "按历史常规节奏,待官方活动确认"), ("99划算节/秋季上新", date(2026,9,6), date(2026,9,10), 1.10, "按历史常规节奏,待官方活动确认"), ("双11预热", date(2026,10,20), date(2026,10,31), 1.12, "按历史常规节奏,待官方活动确认"), ("双11主爆发", date(2026,11,1), date(2026,11,11), 1.30, "按历史常规节奏,待官方活动确认"), ("双11返场", date(2026,11,12), date(2026,11,20), 1.10, "按历史常规节奏,待官方活动确认"), ("双12", date(2026,12,1), date(2026,12,12), 1.12, "按历史常规节奏,待官方活动确认"), ("年货节预热", date(2026,12,20), date(2026,12,31), 1.08, "按历史常规节奏,待官方活动确认"), ] # weather day/month factors by month for daily allocation weather_daily = {1:1.02,2:0.98,3:1.01,4:1.02,5:1.04,6:1.04,7:0.96,8:0.97,9:1.02,10:1.02,11:1.04,12:1.03} forecast_window_targets={mo: rec[-1] for mo,*_,rec in []} # Just use monthly_param mapping target_by_month={r[0]:r[-1] for r in monthly_param} daily=[] d=forecast_start while d<=date(2026,12,31): wd=d.weekday() act=1.0 act_names=[] for name,s,e,f,note in activity_events: if s <= d <= e: # multiplicative stacking? use additive increments to avoid high overlap: act += (f-1) act_names.append(name) w=wd_factor[wd]*weather_daily[d.month]*act daily.append({"date":d,"month":d.month,"weekday":wd+1,"weekday_cn":wd_names_cn[wd],"weekday_factor":wd_factor[wd], "weather_factor":weather_daily[d.month],"activity_factor":act,"weight":w,"events":";".join(act_names)}) # Normalize by month sum_w=defaultdict(float) for r in daily: sum_w[r["month"]]+=r["weight"] for r in daily: r["forecast"]=target_by_month[r["month"]]*r["weight"]/sum_w[r["month"]] # monthly summary forecast sum_forecast=defaultdict(float) for r in daily: sum_forecast[r["month"]]+=r["forecast"] for mo in range(5,13): print(mo, sum_forecast[mo], target_by_month[mo]) print(daily[:3], daily[-3:])
pythonprint("alive")
pythonfrom artifact_tool import Blob, SpreadsheetFile import json, datetime, calendar, statistics, math, re from datetime import date, timedelta # Paths path_xlsx = "/mnt/data/仓储部单量预估表.xlsx" path_constraints = "/mnt/data/约束.txt" out_path = "/mnt/data/仓储部单量预估表_严谨预测模型版.xlsx" # Read constraints constraints_text = open(path_constraints, "r", encoding="utf-8").read() # Import workbook wb = SpreadsheetFile.import_xlsx(Blob.load(path_xlsx)) # Extract raw daily data via artifact_tool inspect chunks rows=[] for start in range(2, 776, 80): end = min(775, start+79) nd = wb.inspect({"kind":"table","range":f"'24.1-26.5.26单量'!A{start}:B{end}","include":"values","table_max_rows":100,"table_max_cols":3}).ndjson d = json.loads(nd.splitlines()[0]) vals=d.get("values",[]) for r in vals: if len(r)>=2: rows.append(r) def parse_excel_date(val): if isinstance(val, str): if val.strip().upper() == "NA": return None # strip possible time part return datetime.datetime.strptime(val[:10], "%Y-%m-%d").date() if isinstance(val, (int,float)): # Excel serial date, 1900 date system return date(1899,12,30)+timedelta(days=int(val)) if isinstance(val, datetime.datetime): return val.date() if isinstance(val, date): return val return None data=[] invalid_rows=[] for pack, ds in rows: d=parse_excel_date(ds) if d is None: invalid_rows.append((pack, ds)) continue if isinstance(pack, (int,float)) and pack < 1000000: data.append((d, float(pack))) else: invalid_rows.append((pack, ds)) data.sort(key=lambda x: x[0]) # Analyze data quality date_min = data[0][0] date_max = data[-1][0] data_dict={d:v for d,v in data} missing=[] d=date_min while d<=date_max: if d not in data_dict: missing.append(d) d += timedelta(days=1) from collections import defaultdict monthly=defaultdict(float) monthly_days=defaultdict(int) for d,v in data: monthly[(d.year,d.month)] += v monthly_days[(d.year,d.month)] += 1 # Monthly summary from 2024-04 to 2026-12 month_records=[] m=date(2024,4,1) while m <= date(2026,12,1): y,mo=m.year,m.month cal_days=calendar.monthrange(y,mo)[1] obs_days=monthly_days.get((y,mo),0) actual=monthly.get((y,mo),0.0) adjusted = actual/obs_days*cal_days if obs_days>0 else 0.0 complete = "完整" if obs_days==cal_days and obs_days>0 else ("未发生" if obs_days==0 else "不完整/MTD") note="" if y==2024 and mo==10: note="原始日表缺 2024/10/18-10/30,月度真实值不应直接使用" elif y==2026 and mo==5: note="仅至 2026/5/26,已按观测日均折算全月用于趋势" elif obs_days==0: note="未来月份,供预测模型使用" month_records.append([y, mo, datetime.datetime(y,mo,1), cal_days, obs_days, actual, adjusted, complete, note]) if mo == 12: m=date(y+1,1,1) else: m=date(y,mo+1,1) # Weekday factor: residual to monthly average, complete months only complete_months={k for k,obs in monthly_days.items() if obs==calendar.monthrange(k[0],k[1])[1]} resid_by_wd=defaultdict(list) for d,v in data: if (d.year,d.month) in complete_months: month_avg = monthly[(d.year,d.month)] / monthly_days[(d.year,d.month)] resid_by_wd[d.weekday()].append(v/month_avg) wd_names_cn=["星期一","星期二","星期三","星期四","星期五","星期六","星期日"] wd_raw={wd:(statistics.mean(resid_by_wd[wd]) if resid_by_wd[wd] else 1.0) for wd in range(7)} avg_factor=sum(wd_raw.values())/7 weekday_factors={wd+1:wd_raw[wd]/avg_factor for wd in range(7)} # Excel weekday 1-7 Monday first # Recent 2026 trend vs 2025, May adjusted trend_rows=[] trend_weights={1:0.10,2:0.10,3:0.20,4:0.30,5:0.30} for mo in range(1,6): b=monthly.get((2025,mo),0.0) obs=monthly.get((2026,mo),0.0) obs_days=monthly_days.get((2026,mo),0) cal_days=calendar.monthrange(2026,mo)[1] adjusted=obs/obs_days*cal_days if obs_days else 0.0 ratio=adjusted/b if b else 0.0 trend_rows.append([mo, f"{mo}月", b, adjusted, ratio, trend_weights[mo], ratio*trend_weights[mo]]) recent_trend=sum(r[6] for r in trend_rows) # Forecast parameters forecast_start=date(2026,5,27) macro_factor=1.02 # Weather/season factor captures expected product-weather demand pressure; editable in workbook. weather_adj={1:1.03,2:0.99,3:1.01,4:1.02,5:1.03,6:1.02,7:0.98,8:0.99,9:1.02,10:1.02,11:1.04,12:1.03} # Resource/operation allows latest shop resource trend to be edited. resource_adj={m:1.00 for m in range(1,13)} resource_adj.update({8:1.01,9:1.02,10:1.02,11:1.03,12:1.03}) # Activities already in prior-year baseline; only small monthly modifiers, daily distribution handled separately. activity_month_adj={m:1.00 for m in range(1,13)} activity_month_adj.update({6:1.01,11:1.02}) month_param=[] for mo in range(1,13): base_2025=monthly.get((2025,mo),0.0) act2026=monthly.get((2026,mo),0.0) obs_days=monthly_days.get((2026,mo),0) cal_days=calendar.monthrange(2026,mo)[1] adj2026=act2026/obs_days*cal_days if obs_days else 0.0 ratio=adj2026/base_2025 if base_2025 else 0.0 full_forecast=base_2025*recent_trend*macro_factor*weather_adj[mo]*resource_adj[mo]*activity_month_adj[mo] if mo < forecast_start.month: actual_before=act2026 elif mo == forecast_start.month: actual_before=sum(v for d,v in data if d.year==2026 and d.month==mo and d<forecast_start) else: actual_before=0.0 window_target=max(0.0, full_forecast-actual_before) if mo < forecast_start.month: note="已发生月份,不进入预测窗口" elif mo == forecast_start.month: note="5/27后预测,5/1-5/26取实际" else: note="未来完整月预测" month_param.append([mo, f"{mo}月", base_2025, act2026, obs_days, adj2026, ratio, recent_trend, weather_adj[mo], macro_factor, resource_adj[mo], activity_month_adj[mo], full_forecast, actual_before, window_target, note]) # Daily activity events for forecast allocation activity_events=[ ["天猫618预售/抢先购(官方已公布)", date(2026,5,21), date(2026,5,30), 1.12, "用于5/27-5/30剩余天;来源见下方资料区"], ["天猫618第二波现货(官方已公布)", date(2026,5,31), date(2026,6,21), 1.20, "官方招商节奏,来源见下方资料区"], ["开学/换季小高峰", date(2026,9,1), date(2026,9,10), 1.06, "常年节奏假设,待平台官方确认"], ["99划算节/秋季上新", date(2026,9,6), date(2026,9,10), 1.10, "常年节奏假设,待平台官方确认"], ["双11预热", date(2026,10,20), date(2026,10,31), 1.12, "常年节奏假设,待平台官方确认"], ["双11主爆发", date(2026,11,1), date(2026,11,11), 1.30, "常年节奏假设,待平台官方确认"], ["双11返场", date(2026,11,12), date(2026,11,20), 1.10, "常年节奏假设,待平台官方确认"], ["双12", date(2026,12,1), date(2026,12,12), 1.12, "常年节奏假设,待平台官方确认"], ["年货节预热", date(2026,12,20), date(2026,12,31), 1.08, "常年节奏假设,待平台官方确认"], ] # Convert date to datetime for Excel writing activity_events_excel=[[name, datetime.datetime(s.year,s.month,s.day), datetime.datetime(e.year,e.month,e.day), f, note] for name,s,e,f,note in activity_events] # Daily forecast dates daily_dates=[] d=forecast_start while d <= date(2026,12,31): daily_dates.append([datetime.datetime(d.year,d.month,d.day)]) d += timedelta(days=1) daily_n=len(daily_dates) daily_start=2 daily_end=daily_start+daily_n-1 # Backtest validation data: rolling trend, no current month leakage # For Jan, use Nov-Dec 2025 vs 2024 as prior-year transition, if available. def ratio_yoy(year, mo): b=monthly.get((year-1,mo),0.0) a=monthly.get((year,mo),0.0) return a/b if b else None nov_dec_ratios=[ratio_yoy(2025,11), ratio_yoy(2025,12)] prior_ratio = statistics.mean([x for x in nov_dec_ratios if x]) if any(nov_dec_ratios) else recent_trend actual_ratios={mo:(month_param[mo-1][6] if month_param[mo-1][6] else None) for mo in range(1,6)} validation=[] for mo in range(1,6): actual_adj=month_param[mo-1][5] base=month_param[mo-1][2] if mo==1: train_trend=prior_ratio train_src="上一年11-12月同比趋势" elif mo==2: train_trend=actual_ratios[1] train_src="1月实际同比" elif mo==3: train_trend=(actual_ratios[1]+actual_ratios[2])/2 train_src="1-2月实际同比均值" elif mo==4: train_trend=(actual_ratios[1]*0.2+actual_ratios[2]*0.3+actual_ratios[3]*0.5) train_src="1-3月滚动加权" else: train_trend=(actual_ratios[2]*0.2+actual_ratios[3]*0.3+actual_ratios[4]*0.5) train_src="2-4月滚动加权" pred=base*train_trend*macro_factor*weather_adj[mo]*resource_adj[mo]*activity_month_adj[mo] error=abs(pred-actual_adj)/actual_adj if actual_adj else None note="春节物流/履约异常,作为异常月单独看待" if mo==2 else ("MTD折算全月实际" if mo==5 else "常规验证") validation.append([f"2026-{mo:02d}", base, actual_adj, train_trend, pred, error, train_src, note]) # Formatting helpers title_fmt={"fill":"#B4E5E0","font":{"bold":True,"size":14},"horizontal_alignment":"center","vertical_alignment":"center","wrap_text":True} header_fmt={"fill":"#B4E5E0","font":{"bold":True},"horizontal_alignment":"center","vertical_alignment":"center","wrap_text":True} subheader_fmt={"fill":"#F6BE8F","font":{"bold":True},"horizontal_alignment":"center","vertical_alignment":"center","wrap_text":True} yellow_fmt={"fill":"#F7E79E","font":{"bold":True},"horizontal_alignment":"center","vertical_alignment":"center","wrap_text":True} blue_fmt={"fill":"#B9C9E8","font":{"bold":True},"horizontal_alignment":"center","vertical_alignment":"center","wrap_text":True} green_fmt={"fill":"#C9E2B3","font":{"bold":True},"horizontal_alignment":"center","vertical_alignment":"center","wrap_text":True} note_fmt={"fill":"#FCE4D6","wrap_text":True} pct_fmt="0.00%" num_fmt="#,##0" date_fmt="yyyy-mm-dd" thin_border={"style":"continuous","color":"#000000","weight":"thin"} def style_range(sheet, rng, fmt=None, border=True): r=sheet.get_range(rng) if fmt: r.format=fmt if border: try: r.format.borders = { "insideHorizontal": thin_border, "insideVertical": thin_border, "edgeBottom": thin_border, "edgeTop": thin_border, "edgeLeft": thin_border, "edgeRight": thin_border, } except Exception: pass return r # Create new sheets s_diag = wb.worksheets.add("6-约束&结构诊断") s_hist = wb.worksheets.add("7-历史月度&因子") s_param = wb.worksheets.add("8-模型参数") s_daily = wb.worksheets.add("9-日级预测系统") s_dash = wb.worksheets.add("10-预测总览") s_val = wb.worksheets.add("11-复盘验证") # 6-约束&结构诊断 s_diag.merge_cells("A1:H1") s_diag.get_range("A1").values=[["仓储部单量预测模型:约束读取、原表诊断与精简逻辑"]] s_diag.get_range("A1").format=title_fmt diag_rows=[ ["约束项","落地方式","处理状态","备注"], ["保留原始表格","不移动、不覆盖原有工作表;新增工作表从“6-约束&结构诊断”开始","已完成","原始表和旧模型均保留"], ["原始单量数据","使用“24.1-26.5.26单量”作为日级实际数据源","已完成",f"日级明细识别为 {date_min} 至 {date_max},有效 {len(data)} 行"], ["先分析原表逻辑","吸收月度资源、天气、星期、活动、运营主推维度","已完成","将重复手工日期矩阵精简为参数表 + 日级预测表"], ["月度资源量整体影响","使用2025同月实际作为季节/资源基底,叠加2026最新趋势","已完成","避免只看单月波动"], ["天气影响","保留“天气/季节产品强度”参数,默认按鞋垫季节需求设置","已完成","未来真实天气无法提前锁定,设计为可编辑参数"], ["星期趋势","按完整月日级数据计算“相对本月均值”的星期因子","已完成","减少月度/活动因素对星期判断的干扰"], ["中国电商活动趋势","618官方节奏已写入;双11/双12等未正式公布的用常年节奏并标注待确认","已完成","日级权重只做月内分配,避免活动与月度基底重复计算"], ["26年最近几个月趋势","Jan-May 2026/2025同比折算后加权,近月权重更高","已完成",f"当前趋势指数约 {recent_trend:.2%}"], ["复盘验证","用滚动趋势预测2026年1-5月,计算MAPE并标记春节异常月","已完成","验证逻辑不使用当前月自身数据回填预测"], ] s_diag.get_range("A3:D12").values=diag_rows style_range(s_diag,"A3:D3",subheader_fmt) style_range(s_diag,"A4:D12") s_diag.get_range("A15").values=[["关键发现"]] s_diag.get_range("A15").format=yellow_fmt findings=[ ["1","原表优点:已拆分月度资源、天气、星期、活动和运营主推,方向正确。"], ["2","原表主要问题:部分月份/日期矩阵手工维护量大,活动和天气容易与月度季节性重复计算。"], ["3",f"数据质量:原始日表从 {date_min} 开始;约束写到24.1.1,但本明细未包含2024年1-3月日级数据。"], ["4",f"数据质量:2024年10月缺失 {len(missing)} 天({missing[0]} 至 {missing[-1]}),另有NA汇总行,已排除在建模之外。"], ["5","模型精简:月度先定总量,日级再按星期/天气/活动权重在月内分配,防止重复加成。"], ] s_diag.get_range("A16:B20").values=findings style_range(s_diag,"A16:B20") s_diag.get_range("B16:B20").format.wrap_text=True s_diag.get_range("A22").values=[["核心公式"]] s_diag.get_range("A22").format=green_fmt formula_rows=[ ["月度预测全量","2025同月实际 × 2026最近趋势指数 × 宏观行业指数 × 资源/运营系数 × 天气/季节系数 × 活动月度系数"], ["日级预测","月度预测窗口目标 × 当日权重 ÷ 本月预测窗口权重合计"], ["当日权重","星期因子 × 天气/季节因子 × 电商活动日因子"], ] s_diag.get_range("A23:B25").values=formula_rows style_range(s_diag,"A23:B25") s_diag.get_range("B23:B25").format.wrap_text=True s_diag.get_range("A:D").format.autofit_columns() s_diag.get_range("B:B").format.column_width=60 s_diag.freeze_panes.freeze_rows(3) # 7-历史月度&因子 s_hist.merge_cells("A1:I1") s_hist.get_range("A1").values=[["历史月度汇总与数据质量标记"]] s_hist.get_range("A1").format=title_fmt hist_header=["年份","月份","月初日期","自然天数","观测天数","实际单量","折算全月单量","完整性","备注"] s_hist.get_range("A3:I3").values=[hist_header] s_hist.get_range(f"A4:I{3+len(month_records)}").values=month_records style_range(s_hist,"A3:I3",subheader_fmt) style_range(s_hist,f"A4:I{3+len(month_records)}") s_hist.get_range(f"C4:C{3+len(month_records)}").format.number_format=date_fmt s_hist.get_range(f"F4:G{3+len(month_records)}").format.number_format=num_fmt s_hist.get_range(f"A3:I{3+len(month_records)}").format.autofit_columns() s_hist.get_range("I:I").format.column_width=40 try: s_hist.tables.add(f"A3:I{3+len(month_records)}", True, "HistMonthlyTable") except Exception: pass s_hist.freeze_panes.freeze_rows(3) # 8-模型参数 s_param.merge_cells("A1:P1") s_param.get_range("A1").values=[["模型参数(所有黄色/浅色参数可按运营判断调整)"]] s_param.get_range("A1").format=title_fmt param_top=[ ["参数","取值","说明","来源/口径"], ["预测起点", datetime.datetime(forecast_start.year,forecast_start.month,forecast_start.day), "从该日期开始生成未来日级预测", "已知实际数据截至2026-05-26"], ["趋势指数(加权)", None, "Jan-May 2026 对 2025 同月折算同比,近月权重更高", "下方趋势输入表"], ["宏观行业指数", macro_factor, "保守吸收线上商品/穿类增长趋势,不覆盖公司自身下滑趋势", "国家统计局2026年1-4月线上/穿类数据"], ["模型口径", "月度定总量,日级做分配", "避免天气/活动与历史月度季节性重复计算", "本次新增模型"], ] s_param.get_range("A3:D7").values=param_top s_param.get_range("B4").formulas=[["=SUMPRODUCT(E12:E16,F12:F16)"]] style_range(s_param,"A3:D3",subheader_fmt) style_range(s_param,"A4:D7") s_param.get_range("B4:B5").format.number_format=pct_fmt s_param.get_range("B4").format=yellow_fmt s_param.get_range("B5").format=yellow_fmt s_param.get_range("B4").format.number_format=pct_fmt s_param.get_range("B5").format.number_format=pct_fmt s_param.get_range("B4").values = [[None]] # revert? we'll set formula again after style s_param.get_range("B4").formulas = [["=SUMPRODUCT(E12:E16,F12:F16)"]] s_param.get_range("B5").values = [[macro_factor]] # Trend input table trend_header=["月份数字","月份","2025同月实际","2026折算全月","2026/2025","权重","加权贡献"] s_param.get_range("A11:G11").values=[trend_header] s_param.get_range("A12:G16").values=trend_rows s_param.get_range("G12:G16").formulas=[[f"=E{r}*F{r}"] for r in range(12,17)] style_range(s_param,"A11:G11",blue_fmt) style_range(s_param,"A12:G16") s_param.get_range("C12:D16").format.number_format=num_fmt s_param.get_range("E12:G16").format.number_format=pct_fmt # Monthly parameter table monthly_header=["月份数字","月份","2025同月实际","2026实际/MTD","观测天数","2026折算全月","2026/2025","趋势系数","天气/季节系数","宏观指数","资源/运营系数","活动月度系数","月度预测全量","预测起点前实际","预测窗口目标","备注"] s_param.get_range("A22:P22").values=[monthly_header] s_param.get_range("A23:P34").values=month_param # Key forecast formulas in monthly table; leave factors editable formulas_M=[] formulas_O=[] formulas_G=[] for r in range(23,35): formulas_G.append([f"=IFERROR(F{r}/C{r},0)"]) formulas_M.append([f"=C{r}*$B$4*J{r}*K{r}*I{r}*L{r}"]) formulas_O.append([f"=MAX(0,M{r}-N{r})"]) s_param.get_range("G23:G34").formulas=formulas_G s_param.get_range("H23:H34").formulas=[[f"=$B$4"] for _ in range(12)] s_param.get_range("J23:J34").formulas=[[f"=$B$5"] for _ in range(12)] s_param.get_range("M23:M34").formulas=formulas_M s_param.get_range("O23:O34").formulas=formulas_O style_range(s_param,"A22:P22",subheader_fmt) style_range(s_param,"A23:P34") s_param.get_range("C23:F34").format.number_format=num_fmt s_param.get_range("G23:L34").format.number_format=pct_fmt s_param.get_range("M23:O34").format.number_format=num_fmt s_param.get_range("I23:L34").format=yellow_fmt s_param.get_range("I23:L34").format.number_format=pct_fmt # Weekday factors s_param.get_range("A38:D38").values=[["星期序号","星期","星期因子","相对均值影响"]] weekday_rows=[[i, wd_names_cn[i-1], weekday_factors[i], weekday_factors[i]-1] for i in range(1,8)] s_param.get_range("A39:D45").values=weekday_rows style_range(s_param,"A38:D38",green_fmt) style_range(s_param,"A39:D45") s_param.get_range("C39:D45").format.number_format=pct_fmt # Activity day table s_param.get_range("A54:E54").values=[["活动","开始日期","结束日期","日权重","说明"]] s_param.get_range("A55:E63").values=activity_events_excel style_range(s_param,"A54:E54",blue_fmt) style_range(s_param,"A55:E63") s_param.get_range("B55:C63").format.number_format=date_fmt s_param.get_range("D55:D63").format.number_format=pct_fmt s_param.get_range("E55:E63").format.wrap_text=True # Source URLs sources=[ ["国家统计局:2026年1—4月社零/网上零售","1-4月全国网上商品和服务零售额同比+6.6%;网上商品零售额+5.7%;穿类商品+6.8%","https://www.stats.gov.cn/sj/zxfb/202605/t20260518_1963727.html"], ["国家统计局:2025年社零/网上零售","2025年全国网上零售额同比+8.6%;实物商品网上零售额同比+5.2%;穿类商品+1.9%","https://www.stats.gov.cn/xxgk/sjfb/zxfb2020/202601/t20260119_1962323.html"], ["2026天猫618招商规则报道","2026天猫618预售5/21-6/3;现货第一波5/21-5/30,第二波5/31-6/21","https://finance.sina.com.cn/tech/roll/2026-04-29/doc-inhwefve6787805.shtml"], ] s_param.get_range("A67:C67").values=[["资料主题","用于模型的判断","URL"]] s_param.get_range("A68:C70").values=sources style_range(s_param,"A67:C67",subheader_fmt) style_range(s_param,"A68:C70") s_param.get_range("B68:B70").format.wrap_text=True s_param.get_range("C68:C70").format.wrap_text=True s_param.get_range("A:P").format.autofit_columns() for col, width in [("D:D",14),("E:E",10),("P:P",28),("A:A",14),("B:B",14),("C:C",14),("E:E",12),("F:F",14),("M:O",14),("C:C",15)]: try: s_param.get_range(col).format.column_width=width if isinstance(width,(int,float)) else 14 except Exception: pass s_param.get_range("A:P").format.vertical_alignment="center" s_param.freeze_panes.freeze_rows(22) # 9-日级预测系统 s_daily.merge_cells("A1:N1") s_daily.get_range("A1").values=[["2026年日级预测系统(从2026-05-27开始)"]] s_daily.get_range("A1").format=title_fmt daily_header=["日期","年","月","月份标签","星期序号","星期","月度预测窗口目标","星期因子","天气/季节因子","活动日因子","日权重","本月权重合计","日预测单量","备注"] s_daily.get_range("A2:N2").values=[daily_header] s_daily.get_range(f"A3:A{daily_end+1}").values=daily_dates # Formula rows start at 3 due title/header; daily dates are in A3:A221 daily_first=3 daily_last=daily_first+daily_n-1 formulas=[] for r in range(daily_first,daily_last+1): formulas.append([ f"=YEAR(A{r})", f"=MONTH(A{r})", f'=TEXT(A{r},"yyyy-mm")', f"=WEEKDAY(A{r},2)", f'=CHOOSE(E{r},"星期一","星期二","星期三","星期四","星期五","星期六","星期日")', f"=XLOOKUP(C{r},'8-模型参数'!$A$23:$A$34,'8-模型参数'!$O$23:$O$34,0)", f"=XLOOKUP(E{r},'8-模型参数'!$A$39:$A$45,'8-模型参数'!$C$39:$C$45,1)", f"=XLOOKUP(C{r},'8-模型参数'!$A$23:$A$34,'8-模型参数'!$I$23:$I$34,1)", f"=1+SUMPRODUCT(--($A{r}>='8-模型参数'!$B$55:$B$63),--($A{r}<='8-模型参数'!$C$55:$C$63),'8-模型参数'!$D$55:$D$63-1)", f"=H{r}*I{r}*J{r}", f"=SUMIFS($K${daily_first}:$K${daily_last},$C${daily_first}:$C${daily_last},C{r})", f"=ROUND(G{r}*K{r}/L{r},0)", f'=IF(J{r}>1,"活动加权",IF(I{r}<>1,"季节/天气加权","常规"))' ]) s_daily.get_range(f"B{daily_first}:N{daily_last}").formulas=formulas style_range(s_daily,"A2:N2",subheader_fmt) style_range(s_daily,f"A3:N{daily_last}") s_daily.get_range(f"A3:A{daily_last}").format.number_format=date_fmt s_daily.get_range(f"G3:G{daily_last}").format.number_format=num_fmt s_daily.get_range(f"H3:K{daily_last}").format.number_format=pct_fmt s_daily.get_range(f"L3:M{daily_last}").format.number_format=num_fmt s_daily.get_range(f"A:N").format.autofit_columns() s_daily.get_range("N:N").format.column_width=20 try: s_daily.tables.add(f"A2:N{daily_last}", True, "DailyForecastTable") except Exception: pass s_daily.freeze_panes.freeze_rows(2) try: s_daily.get_range(f"M3:M{daily_last}").conditional_formats.add_data_bar({"color":"#92D050","gradient":True}) except Exception: pass # 10-预测总览 dashboard s_dash.merge_cells("A1:J1") s_dash.get_range("A1").values=[["2026年5-12月单量预测总览"]] s_dash.get_range("A1").format=title_fmt # KPI block s_dash.get_range("A3:J3").values=[["KPI","数值","说明",None,"KPI","数值","说明",None,"KPI","数值"]] style_range(s_dash,"A3:J3",green_fmt) s_dash.get_range("A4:C6").values=[ ["预测窗口总单量", None, "5/27-12/31日级预测合计"], ["5-12月合计(含5月已发生)", None, "5月实际+后续预测"], ["当前趋势指数", None, "2026 Jan-May vs 2025加权"], ] s_dash.get_range("B4").formulas=[["=SUM(E10:E17)"]] s_dash.get_range("B5").formulas=[["=SUM(F10:F17)"]] s_dash.get_range("B6").formulas=[["='8-模型参数'!$B$4"]] s_dash.get_range("E4:G6").values=[ ["预测起点", datetime.datetime(forecast_start.year,forecast_start.month,forecast_start.day), "实际数据截至2026-05-26"], ["宏观指数", None, "国家统计局线上/穿类增长,保守折算"], ["验证MAPE", None, "见“11-复盘验证”"], ] s_dash.get_range("F5").formulas=[["='8-模型参数'!$B$5"]] s_dash.get_range("F6").formulas=[["='11-复盘验证'!$B$12"]] style_range(s_dash,"A4:C6") style_range(s_dash,"E4:G6") s_dash.get_range("B4:B5").format.number_format=num_fmt s_dash.get_range("B6").format.number_format=pct_fmt s_dash.get_range("F4").format.number_format=date_fmt s_dash.get_range("F5:F6").format.number_format=pct_fmt # Monthly forecast summary summary_header=["月初日期","月份","2025同月实际","预测起点前实际","预测窗口单量","预测后全月/合计","对2025同比","日均单量","模型全月目标","备注"] s_dash.get_range("A9:J9").values=[summary_header] months_summary=[[datetime.datetime(2026,mo,1), f"{mo}月", None, None, None, None, None, None, None, None] for mo in range(5,13)] s_dash.get_range("A10:J17").values=months_summary summary_formulas=[] for r,mo in zip(range(10,18),range(5,13)): summary_formulas.append([ None, # A date values None, # B month labels f"=XLOOKUP(MONTH($A{r}),'8-模型参数'!$A$23:$A$34,'8-模型参数'!$C$23:$C$34)", f"=XLOOKUP(MONTH($A{r}),'8-模型参数'!$A$23:$A$34,'8-模型参数'!$N$23:$N$34)", f"=SUMIFS('9-日级预测系统'!$M${daily_first}:$M${daily_last},'9-日级预测系统'!$C${daily_first}:$C${daily_last},MONTH($A{r}))", f"=D{r}+E{r}", f"=F{r}/C{r}-1", f"=F{r}/DAY(EOMONTH(A{r},0))", f"=XLOOKUP(MONTH($A{r}),'8-模型参数'!$A$23:$A$34,'8-模型参数'!$M$23:$M$34)", f'=IF(MONTH(A{r})=5,"5/27后预测;5/1-5/26为实际","完整月预测")' ]) # cannot assign None formulas to A/B, set formulas only C:J s_dash.get_range("C10:J17").formulas=[row[2:] for row in summary_formulas] style_range(s_dash,"A9:J9",subheader_fmt) style_range(s_dash,"A10:J17") s_dash.get_range("A10:A17").format.number_format=date_fmt s_dash.get_range("C10:F17").format.number_format=num_fmt s_dash.get_range("G10:G17").format.number_format=pct_fmt s_dash.get_range("H10:I17").format.number_format=num_fmt s_dash.get_range("J10:J17").format.wrap_text=True try: s_dash.tables.add("A9:J17", True, "MonthlyForecastSummary") except Exception: pass # Add chart try: chart_props={"title":"2026年5-12月预测 vs 2025同月实际","has_legend":True} chart=s_dash.charts.add("column", chart_props) ref=s_dash.name.replace("'","''") ser1=chart.series.add("2025同月实际") ser1.category_formula=f"'{ref}'!$B$10:$B$17" ser1.formula=f"'{ref}'!$C$10:$C$17" ser2=chart.series.add("2026预测后全月/合计") ser2.category_formula=f"'{ref}'!$B$10:$B$17" ser2.formula=f"'{ref}'!$F$10:$F$17" chart.set_position("A20","J38") except Exception as e: s_dash.get_range("A20").values=[[f"图表创建失败,可使用A9:J17数据插入图表:{e}"]] s_dash.get_range("A:J").format.autofit_columns() s_dash.get_range("J:J").format.column_width=28 s_dash.freeze_panes.freeze_rows(9) # 11-复盘验证 s_val.merge_cells("A1:H1") s_val.get_range("A1").values=[["复盘验证:2026年1-5月滚动趋势预测"]] s_val.get_range("A1").format=title_fmt val_header=["月份","2025同月实际","2026实际/折算","训练趋势(不含当前月)","滚动预测","绝对误差率","训练口径","备注"] s_val.get_range("A3:H3").values=[val_header] s_val.get_range("A4:H8").values=validation # Formulas for error s_val.get_range("F4:F8").formulas=[[f"=ABS(E{r}-C{r})/C{r}"] for r in range(4,9)] style_range(s_val,"A3:H3",subheader_fmt) style_range(s_val,"A4:H8") s_val.get_range("B4:C8").format.number_format=num_fmt s_val.get_range("D4:D8").format.number_format=pct_fmt s_val.get_range("E4:E8").format.number_format=num_fmt s_val.get_range("F4:F8").format.number_format=pct_fmt s_val.get_range("G4:H8").format.wrap_text=True s_val.get_range("A11:B13").values=[ ["验证指标","数值"], ["MAPE(含春节异常月)", None], ["MAPE(剔除2月异常)", None], ] s_val.get_range("B12").formulas=[["=AVERAGE(F4:F8)"]] s_val.get_range("B13").formulas=[["=AVERAGE(F4,F6:F8)"]] style_range(s_val,"A11:B11",green_fmt) style_range(s_val,"A12:B13") s_val.get_range("B12:B13").format.number_format=pct_fmt s_val.get_range("A16").values=[["自洽性判断"]] s_val.get_range("A16").format=yellow_fmt validation_notes=[ ["1","模型使用“月度定总量 + 日级归因分配”,避免活动、天气和月度季节性重复放大。"], ["2","趋势指数来自2026年最近月度同比;权重偏向3-5月,以反映近期资源/店铺/平台流量变化。"], ["3","2月受春节物流与履约节奏影响,误差应单独标记,不宜直接拉动全年趋势。"], ["4","若618投放、店铺资源、爆品策略发生变化,应优先调整“8-模型参数”的资源/运营系数和活动日权重。"], ] s_val.get_range("A17:B20").values=validation_notes style_range(s_val,"A17:B20") s_val.get_range("B17:B20").format.wrap_text=True try: s_val.get_range("F4:F8").conditional_formats.add_data_bar({"color":"#FFC000","gradient":True}) except Exception: pass s_val.get_range("A:H").format.autofit_columns() s_val.get_range("G:H").format.column_width=28 s_val.freeze_panes.freeze_rows(3) # General formatting adjustments for sh in [s_diag,s_hist,s_param,s_daily,s_dash,s_val]: try: sh.get_range("A:Z").format.font = {"name":"Microsoft YaHei","size":10} except Exception: pass try: sh.get_range("A:Z").format.vertical_alignment="center" except Exception: pass # Verification: inspect key ranges and formula errors check_dash = wb.inspect({"kind":"table","range":"'10-预测总览'!A1:J17","include":"values,formulas","table_max_rows":25,"table_max_cols":12}).ndjson check_param = wb.inspect({"kind":"table","range":"'8-模型参数'!A1:P34","include":"values,formulas","table_max_rows":40,"table_max_cols":16}).ndjson errors = wb.inspect({"kind":"match","search_term":"#REF!|#DIV/0!|#VALUE!|#NAME\\?|#N/A","options":{"use_regex":True,"max_results":300},"summary":"final formula error scan"}).ndjson # Render key dashboard for visual try: preview=wb.render({"sheet_name":"10-预测总览","range":"A1:J18","scale":1}) preview.save("/mnt/data/forecast_dashboard_preview.png") except Exception as e: pass # Export workbook SpreadsheetFile.export_xlsx(wb).save(out_path) print("exported", out_path) print("daily_n", daily_n, "recent_trend", recent_trend, "missing_days", len(missing)) print("DASH_CHECK", check_dash[:1000]) print("ERRORS", errors[:1000])
pythonfrom artifact_tool import Blob, SpreadsheetFile import json, datetime, calendar, statistics from datetime import date, timedelta from collections import defaultdict path_xlsx = "/mnt/data/仓储部单量预估表.xlsx" path_constraints = "/mnt/data/约束.txt" out_path = "/mnt/data/仓储部单量预估表_严谨预测模型版.xlsx" constraints_text = open(path_constraints, "r", encoding="utf-8").read() wb = SpreadsheetFile.import_xlsx(Blob.load(path_xlsx)) # Extract raw data rows=[] for start in range(2, 776, 80): end = min(775, start+79) d = json.loads(wb.inspect({"kind":"table","range":f"'24.1-26.5.26单量'!A{start}:B{end}","include":"values","table_max_rows":100,"table_max_cols":3}).ndjson.splitlines()[0]) for r in d.get("values",[]): if len(r)>=2: rows.append(r) def parse_excel_date(val): if isinstance(val, str): if val.strip().upper()=="NA": return None return datetime.datetime.strptime(val[:10], "%Y-%m-%d").date() if isinstance(val,(int,float)): return date(1899,12,30)+timedelta(days=int(val)) if isinstance(val, datetime.datetime): return val.date() if isinstance(val, date): return val return None data=[] invalid=[] for pack, ds in rows: d=parse_excel_date(ds) if d is None or not isinstance(pack,(int,float)) or pack>=1000000: invalid.append((pack,ds)) else: data.append((d,float(pack))) data.sort() date_min, date_max = data[0][0], data[-1][0] missing=[] cur=date_min data_dict=dict(data) while cur<=date_max: if cur not in data_dict: missing.append(cur) cur+=timedelta(days=1) monthly=defaultdict(float); monthly_days=defaultdict(int) for d,v in data: monthly[(d.year,d.month)] += v monthly_days[(d.year,d.month)] += 1 # Historical monthly records month_records=[] cur=date(2024,4,1) while cur<=date(2026,12,1): y,m=cur.year,cur.month cal=calendar.monthrange(y,m)[1] obs=monthly_days.get((y,m),0) actual=monthly.get((y,m),0.0) adj=actual/obs*cal if obs else 0.0 status="完整" if obs==cal and obs>0 else ("未来" if obs==0 else "不完整/MTD") note="" if y==2024 and m==10: note="缺失2024/10/18-10/30,剔除作趋势依据" elif y==2026 and m==5: note="仅至2026/5/26,折算全月用于趋势" elif obs==0: note="未来月份" month_records.append([y,m,datetime.datetime(y,m,1),cal,obs,actual,adj,status,note]) cur = date(y+1,1,1) if m==12 else date(y,m+1,1) # Weekday residual factors complete_months={k for k,obs in monthly_days.items() if obs==calendar.monthrange(k[0],k[1])[1]} resid=defaultdict(list) for d,v in data: if (d.year,d.month) in complete_months: avg=monthly[(d.year,d.month)]/monthly_days[(d.year,d.month)] resid[d.weekday()].append(v/avg) wd_names=["星期一","星期二","星期三","星期四","星期五","星期六","星期日"] wd_raw={i:(statistics.mean(resid[i]) if resid[i] else 1.0) for i in range(7)} wd_mean=sum(wd_raw.values())/7 weekday_factors={i+1:wd_raw[i]/wd_mean for i in range(7)} # Trend rows trend_weights={1:0.10,2:0.10,3:0.20,4:0.30,5:0.30} trend_rows=[] for m in range(1,6): base=monthly.get((2025,m),0.0) actual=monthly.get((2026,m),0.0) obs=monthly_days.get((2026,m),0) cal=calendar.monthrange(2026,m)[1] adj=actual/obs*cal if obs else 0.0 ratio=adj/base if base else 0.0 trend_rows.append([m,f"{m}月",base,adj,ratio,trend_weights[m],ratio*trend_weights[m]]) recent_trend=sum(r[6] for r in trend_rows) forecast_start=date(2026,5,27) macro_factor=1.02 weather_adj={1:1.03,2:0.99,3:1.01,4:1.02,5:1.03,6:1.02,7:0.98,8:0.99,9:1.02,10:1.02,11:1.04,12:1.03} resource_adj={m:1.00 for m in range(1,13)} resource_adj.update({8:1.01,9:1.02,10:1.02,11:1.03,12:1.03}) activity_month_adj={m:1.00 for m in range(1,13)} activity_month_adj.update({6:1.01,11:1.02}) month_param=[] for m in range(1,13): base=monthly.get((2025,m),0.0) actual=monthly.get((2026,m),0.0) obs=monthly_days.get((2026,m),0) cal=calendar.monthrange(2026,m)[1] adj=actual/obs*cal if obs else 0.0 ratio=adj/base if base else 0.0 full=base*recent_trend*macro_factor*weather_adj[m]*resource_adj[m]*activity_month_adj[m] if m<forecast_start.month: actual_before=actual elif m==forecast_start.month: actual_before=sum(v for d,v in data if d.year==2026 and d.month==m and d<forecast_start) else: actual_before=0.0 target=max(0.0, full-actual_before) note="已发生月份" if m<forecast_start.month else ("5/27后预测;5/1-5/26为实际" if m==forecast_start.month else "未来完整月预测") month_param.append([m,f"{m}月",base,actual,obs,adj,ratio,recent_trend,weather_adj[m],macro_factor,resource_adj[m],activity_month_adj[m],full,actual_before,target,note]) activity_events=[ ["天猫618预售/抢先购(官方已公布)", date(2026,5,21), date(2026,5,30), 1.12, "用于5/27-5/30剩余天;来源见资料区"], ["天猫618第二波现货(官方已公布)", date(2026,5,31), date(2026,6,21), 1.20, "官方招商节奏;来源见资料区"], ["开学/换季小高峰", date(2026,9,1), date(2026,9,10), 1.06, "常年节奏假设,待平台官方确认"], ["99划算节/秋季上新", date(2026,9,6), date(2026,9,10), 1.10, "常年节奏假设,待平台官方确认"], ["双11预热", date(2026,10,20), date(2026,10,31), 1.12, "常年节奏假设,待平台官方确认"], ["双11主爆发", date(2026,11,1), date(2026,11,11), 1.30, "常年节奏假设,待平台官方确认"], ["双11返场", date(2026,11,12), date(2026,11,20), 1.10, "常年节奏假设,待平台官方确认"], ["双12", date(2026,12,1), date(2026,12,12), 1.12, "常年节奏假设,待平台官方确认"], ["年货节预热", date(2026,12,20), date(2026,12,31), 1.08, "常年节奏假设,待平台官方确认"], ] activity_excel=[[name,datetime.datetime(s.year,s.month,s.day),datetime.datetime(e.year,e.month,e.day),factor,note] for name,s,e,factor,note in activity_events] daily_dates=[] cur=forecast_start while cur<=date(2026,12,31): daily_dates.append([datetime.datetime(cur.year,cur.month,cur.day)]) cur+=timedelta(days=1) daily_n=len(daily_dates) daily_first=3 daily_last=daily_first+daily_n-1 def yoy_ratio(year,m): prev=monthly.get((year-1,m),0.0); now=monthly.get((year,m),0.0) return now/prev if prev else None prior_ratios=[x for x in [yoy_ratio(2025,11),yoy_ratio(2025,12)] if x] prior_ratio=statistics.mean(prior_ratios) if prior_ratios else recent_trend actual_ratios={m:month_param[m-1][6] for m in range(1,6)} validation=[] for m in range(1,6): base=month_param[m-1][2]; actual_adj=month_param[m-1][5] if m==1: tr=prior_ratio; src="上一年11-12月同比趋势" elif m==2: tr=actual_ratios[1]; src="1月实际同比" elif m==3: tr=(actual_ratios[1]+actual_ratios[2])/2; src="1-2月实际同比均值" elif m==4: tr=actual_ratios[1]*0.2+actual_ratios[2]*0.3+actual_ratios[3]*0.5; src="1-3月滚动加权" else: tr=actual_ratios[2]*0.2+actual_ratios[3]*0.3+actual_ratios[4]*0.5; src="2-4月滚动加权" pred=base*tr*macro_factor*weather_adj[m]*resource_adj[m]*activity_month_adj[m] err=abs(pred-actual_adj)/actual_adj if actual_adj else 0 note="春节物流/履约异常,单独标记" if m==2 else ("MTD折算全月实际" if m==5 else "常规验证") validation.append([f"2026-{m:02d}",base,actual_adj,tr,pred,err,src,note]) # Basic styling dictionaries title_fmt={"fill":"#B4E5E0","font":{"bold":True,"size":14},"horizontal_alignment":"center","vertical_alignment":"center","wrap_text":True} header_fmt={"fill":"#B4E5E0","font":{"bold":True},"horizontal_alignment":"center","vertical_alignment":"center","wrap_text":True} orange_fmt={"fill":"#F6BE8F","font":{"bold":True},"horizontal_alignment":"center","vertical_alignment":"center","wrap_text":True} yellow_fmt={"fill":"#F7E79E","font":{"bold":True},"horizontal_alignment":"center","vertical_alignment":"center","wrap_text":True} green_fmt={"fill":"#C9E2B3","font":{"bold":True},"horizontal_alignment":"center","vertical_alignment":"center","wrap_text":True} def write_title(sheet, cell_range, text): sheet.merge_cells(cell_range) first=cell_range.split(":")[0] sheet.get_range(first).values=[[text]] sheet.get_range(first).format=title_fmt def fmt(sheet, rng, f): sheet.get_range(rng).format=f # Create sheets s_diag=wb.worksheets.add("6-约束&结构诊断") s_hist=wb.worksheets.add("7-历史月度&因子") s_param=wb.worksheets.add("8-模型参数") s_daily=wb.worksheets.add("9-日级预测系统") s_dash=wb.worksheets.add("10-预测总览") s_val=wb.worksheets.add("11-复盘验证") # Diagnosis sheet write_title(s_diag,"A1:H1","仓储部单量预测模型:约束读取、原表诊断与精简逻辑") diag_rows=[ ["约束项","落地方式","状态","备注"], ["保留原始表格","原有9个工作表不移动、不覆盖;仅在后面新增工作表","完成","新增表从6开始"], ["原始单量数据","读取“24.1-26.5.26单量”日级实际","完成",f"{date_min} 至 {date_max},有效{len(data)}行"], ["月度资源影响","2025同月实际作为季节/资源基底,叠加2026最新趋势","完成","避免单日波动外推"], ["天气影响","保留可编辑天气/季节系数,默认按鞋垫季节需求设置","完成","未来真实天气需后续更新"], ["星期趋势","按完整月相对本月均值计算星期因子","完成","弱化月度季节干扰"], ["电商活动趋势","618写入官方节奏;双11/双12按常年节奏且标注待确认","完成","活动日权重做月内分配"], ["2026最近趋势","Jan-May 2026/2025折算同比加权,近月权重更高","完成",f"趋势指数={recent_trend:.2%}"], ["复盘验证","滚动趋势预测2026年1-5月,计算误差并标记春节异常","完成","见11-复盘验证"], ] s_diag.get_range("A3:D11").values=diag_rows fmt(s_diag,"A3:D3",orange_fmt) s_diag.get_range("A15").values=[["关键发现"]]; fmt(s_diag,"A15",yellow_fmt) findings=[ ["1","原表优势是维度完整:月度资源、天气、星期、活动、运营主推都有拆分。"], ["2","本次精简为“参数表+日预测表+总览+验证”,减少手工日历矩阵。"], ["3",f"原始日表实际从{date_min}开始,非24.1.1;若需要24年1-3月日级复盘,需补明细。"], ["4",f"2024年10月缺失{len(missing)}天,另有NA汇总行,均不作为趋势训练数据。"], ["5","先定月度总量,再按星期/天气/活动分摊到日,避免重复加成。"], ] s_diag.get_range("A16:B20").values=findings s_diag.get_range("A22").values=[["核心公式"]]; fmt(s_diag,"A22",green_fmt) s_diag.get_range("A23:B25").values=[ ["月度预测全量","2025同月实际 × 2026最近趋势指数 × 宏观行业指数 × 资源/运营系数 × 天气/季节系数 × 活动月度系数"], ["日级预测","月度预测窗口目标 × 当日权重 ÷ 本月预测窗口权重合计"], ["当日权重","星期因子 × 天气/季节因子 × 电商活动日因子"], ] s_diag.get_range("A1:H25").format.wrap_text=True s_diag.get_range("A1:H25").format.vertical_alignment="center" s_diag.get_range("A1:A25").format.column_width=18 s_diag.get_range("B1:B25").format.column_width=70 s_diag.get_range("C1:D25").format.column_width=18 s_diag.freeze_panes.freeze_rows(3) # Historical sheet write_title(s_hist,"A1:I1","历史月度汇总与数据质量标记") headers=["年份","月份","月初日期","自然天数","观测天数","实际单量","折算全月单量","完整性","备注"] s_hist.get_range("A3:I3").values=[headers]; fmt(s_hist,"A3:I3",orange_fmt) s_hist.get_range(f"A4:I{3+len(month_records)}").values=month_records s_hist.get_range(f"C4:C{3+len(month_records)}").format.number_format="yyyy-mm-dd" s_hist.get_range(f"F4:G{3+len(month_records)}").format.number_format="#,##0" s_hist.get_range("A1:I40").format.wrap_text=True for rng,width in [("A1:A40",8),("B1:B40",8),("C1:C40",13),("D1:E40",10),("F1:G40",14),("H1:H40",14),("I1:I40",35)]: s_hist.get_range(rng).format.column_width=width s_hist.freeze_panes.freeze_rows(3) # Parameter sheet write_title(s_param,"A1:P1","模型参数(浅黄色字段为建议可编辑项)") s_param.get_range("A3:D7").values=[ ["参数","取值","说明","来源/口径"], ["预测起点",datetime.datetime(forecast_start.year,forecast_start.month,forecast_start.day),"从该日期开始生成未来日级预测","实际数据截至2026-05-26"], ["趋势指数(加权)",None,"Jan-May 2026对2025同月折算同比,近月权重更高","下方趋势输入表"], ["宏观行业指数",macro_factor,"保守吸收线上商品/穿类增长,不覆盖公司自身下滑趋势","国家统计局2026年1-4月数据"], ["模型口径","月度定总量,日级做分配","避免天气/活动与历史月度季节性重复计算","本次新增模型"], ] s_param.get_range("B5").formulas=[["=SUMPRODUCT(E12:E16,F12:F16)"]] fmt(s_param,"A3:D3",orange_fmt) fmt(s_param,"B5:B6",yellow_fmt) s_param.get_range("B4").format.number_format="yyyy-mm-dd" s_param.get_range("B5:B6").format.number_format="0.00%" s_param.get_range("A11:G11").values=[["月份数字","月份","2025同月实际","2026折算全月","2026/2025","权重","加权贡献"]] s_param.get_range("A12:G16").values=trend_rows s_param.get_range("G12:G16").formulas=[[f"=E{r}*F{r}"] for r in range(12,17)] fmt(s_param,"A11:G11",header_fmt) s_param.get_range("C12:D16").format.number_format="#,##0" s_param.get_range("E12:G16").format.number_format="0.00%" monthly_header=["月份数字","月份","2025同月实际","2026实际/MTD","观测天数","2026折算全月","2026/2025","趋势系数","天气/季节系数","宏观指数","资源/运营系数","活动月度系数","月度预测全量","预测起点前实际","预测窗口目标","备注"] s_param.get_range("A22:P22").values=[monthly_header]; fmt(s_param,"A22:P22",orange_fmt) s_param.get_range("A23:P34").values=month_param s_param.get_range("G23:G34").formulas=[[f"=IFERROR(F{r}/C{r},0)"] for r in range(23,35)] s_param.get_range("H23:H34").formulas=[[f"=$B$5"] for _ in range(12)] s_param.get_range("J23:J34").formulas=[[f"=$B$6"] for _ in range(12)] s_param.get_range("M23:M34").formulas=[[f"=C{r}*H{r}*J{r}*K{r}*I{r}*L{r}"] for r in range(23,35)] s_param.get_range("O23:O34").formulas=[[f"=MAX(0,M{r}-N{r})"] for r in range(23,35)] s_param.get_range("C23:F34").format.number_format="#,##0" s_param.get_range("G23:L34").format.number_format="0.00%" s_param.get_range("M23:O34").format.number_format="#,##0" fmt(s_param,"I23:L34",yellow_fmt) s_param.get_range("I23:L34").format.number_format="0.00%" s_param.get_range("A38:D38").values=[["星期序号","星期","星期因子","相对均值影响"]]; fmt(s_param,"A38:D38",green_fmt) s_param.get_range("A39:D45").values=[[i,wd_names[i-1],weekday_factors[i],weekday_factors[i]-1] for i in range(1,8)] s_param.get_range("C39:D45").format.number_format="0.00%" s_param.get_range("A54:E54").values=[["活动","开始日期","结束日期","日权重","说明"]]; fmt(s_param,"A54:E54",header_fmt) s_param.get_range("A55:E63").values=activity_excel s_param.get_range("B55:C63").format.number_format="yyyy-mm-dd" s_param.get_range("D55:D63").format.number_format="0.00%" s_param.get_range("E55:E63").format.wrap_text=True sources=[ ["国家统计局:2026年1—4月社零/网上零售","网上商品和服务零售额+6.6%;网上商品零售额+5.7%;穿类商品+6.8%","https://www.stats.gov.cn/sj/zxfb/202605/t20260518_1963727.html"], ["国家统计局:2025年社零/网上零售","全国网上零售额+8.6%;实物商品网上零售额+5.2%;穿类商品+1.9%","https://www.stats.gov.cn/xxgk/sjfb/zxfb2020/202601/t20260119_1962323.html"], ["2026天猫618招商规则报道","预售5/21-6/3;现货第一波5/21-5/30,第二波5/31-6/21","https://finance.sina.com.cn/tech/roll/2026-04-29/doc-inhwefve6787805.shtml"], ] s_param.get_range("A67:C67").values=[["资料主题","用于模型的判断","URL"]]; fmt(s_param,"A67:C67",orange_fmt) s_param.get_range("A68:C70").values=sources s_param.get_range("A1:P70").format.wrap_text=True for rng,width in [("A1:A70",16),("B1:B70",15),("C1:C70",15),("D1:D70",15),("E1:E70",12),("F1:F70",15),("G1:G70",12),("H1:L70",12),("M1:O70",15),("P1:P70",26)]: s_param.get_range(rng).format.column_width=width s_param.freeze_panes.freeze_rows(22) # Daily forecast sheet write_title(s_daily,"A1:N1","2026年日级预测系统(从2026-05-27开始)") headers=["日期","年","月","月份标签","星期序号","星期","月度预测窗口目标","星期因子","天气/季节因子","活动日因子","日权重","本月权重合计","日预测单量","备注"] s_daily.get_range("A2:N2").values=[headers]; fmt(s_daily,"A2:N2",orange_fmt) s_daily.get_range(f"A{daily_first}:A{daily_last}").values=daily_dates formulas=[] for r in range(daily_first,daily_last+1): formulas.append([ f"=YEAR(A{r})", f"=MONTH(A{r})", f'=TEXT(A{r},"yyyy-mm")', f"=WEEKDAY(A{r},2)", f'=CHOOSE(E{r},"星期一","星期二","星期三","星期四","星期五","星期六","星期日")', f"=XLOOKUP(C{r},'8-模型参数'!$A$23:$A$34,'8-模型参数'!$O$23:$O$34,0)", f"=XLOOKUP(E{r},'8-模型参数'!$A$39:$A$45,'8-模型参数'!$C$39:$C$45,1)", f"=XLOOKUP(C{r},'8-模型参数'!$A$23:$A$34,'8-模型参数'!$I$23:$I$34,1)", f"=1+SUMPRODUCT(--($A{r}>='8-模型参数'!$B$55:$B$63),--($A{r}<='8-模型参数'!$C$55:$C$63),'8-模型参数'!$D$55:$D$63-1)", f"=H{r}*I{r}*J{r}", f"=SUMIFS($K${daily_first}:$K${daily_last},$C${daily_first}:$C${daily_last},C{r})", f"=ROUND(G{r}*K{r}/L{r},0)", f'=IF(J{r}>1,"活动加权",IF(I{r}<>1,"季节/天气加权","常规"))' ]) s_daily.get_range(f"B{daily_first}:N{daily_last}").formulas=formulas s_daily.get_range(f"A{daily_first}:A{daily_last}").format.number_format="yyyy-mm-dd" s_daily.get_range(f"G{daily_first}:G{daily_last}").format.number_format="#,##0" s_daily.get_range(f"H{daily_first}:K{daily_last}").format.number_format="0.00%" s_daily.get_range(f"L{daily_first}:M{daily_last}").format.number_format="#,##0" s_daily.get_range(f"A1:N{daily_last}").format.wrap_text=True for rng,width in [("A1:A230",12),("B1:F230",9),("G1:G230",16),("H1:K230",12),("L1:M230",14),("N1:N230",18)]: s_daily.get_range(rng).format.column_width=width s_daily.freeze_panes.freeze_rows(2) # Dashboard write_title(s_dash,"A1:J1","2026年5-12月单量预测总览") s_dash.get_range("A3:C6").values=[ ["KPI","数值","说明"], ["预测窗口总单量",None,"5/27-12/31日级预测合计"], ["5-12月合计(含5月已发生)",None,"5月实际+后续预测"], ["当前趋势指数",None,"2026 Jan-May vs 2025加权"], ] fmt(s_dash,"A3:C3",green_fmt) s_dash.get_range("B4").formulas=[["=SUM(E10:E17)"]] s_dash.get_range("B5").formulas=[["=SUM(F10:F17)"]] s_dash.get_range("B6").formulas=[["='8-模型参数'!$B$5"]] s_dash.get_range("B4:B5").format.number_format="#,##0" s_dash.get_range("B6").format.number_format="0.00%" s_dash.get_range("E3:G6").values=[ ["KPI","数值","说明"], ["预测起点",datetime.datetime(forecast_start.year,forecast_start.month,forecast_start.day),"实际数据截至2026-05-26"], ["宏观指数",None,"线上/穿类增长保守折算"], ["验证MAPE",None,"见11-复盘验证"], ] fmt(s_dash,"E3:G3",green_fmt) s_dash.get_range("F5").formulas=[["='8-模型参数'!$B$6"]] s_dash.get_range("F6").formulas=[["='11-复盘验证'!$B$12"]] s_dash.get_range("F4").format.number_format="yyyy-mm-dd" s_dash.get_range("F5:F6").format.number_format="0.00%" s_dash.get_range("A9:J9").values=[["月初日期","月份","2025同月实际","预测起点前实际","预测窗口单量","预测后全月/合计","对2025同比","日均单量","模型全月目标","备注"]] fmt(s_dash,"A9:J9",orange_fmt) s_dash.get_range("A10:B17").values=[[datetime.datetime(2026,m,1),f"{m}月"] for m in range(5,13)] sum_formulas=[] for r in range(10,18): sum_formulas.append([ f"=XLOOKUP(MONTH($A{r}),'8-模型参数'!$A$23:$A$34,'8-模型参数'!$C$23:$C$34)", f"=XLOOKUP(MONTH($A{r}),'8-模型参数'!$A$23:$A$34,'8-模型参数'!$N$23:$N$34)", f"=SUMIFS('9-日级预测系统'!$M${daily_first}:$M${daily_last},'9-日级预测系统'!$C${daily_first}:$C${daily_last},MONTH($A{r}))", f"=D{r}+E{r}", f"=F{r}/C{r}-1", f"=F{r}/DAY(EOMONTH(A{r},0))", f"=XLOOKUP(MONTH($A{r}),'8-模型参数'!$A$23:$A$34,'8-模型参数'!$M$23:$M$34)", f'=IF(MONTH(A{r})=5,"5/27后预测;5/1-5/26为实际","完整月预测")' ]) s_dash.get_range("C10:J17").formulas=sum_formulas s_dash.get_range("A10:A17").format.number_format="yyyy-mm-dd" s_dash.get_range("C10:F17").format.number_format="#,##0" s_dash.get_range("G10:G17").format.number_format="0.00%" s_dash.get_range("H10:I17").format.number_format="#,##0" s_dash.get_range("A1:J18").format.wrap_text=True for rng,width in [("A1:A18",12),("B1:B18",8),("C1:F18",14),("G1:G18",12),("H1:I18",14),("J1:J18",30)]: s_dash.get_range(rng).format.column_width=width s_dash.freeze_panes.freeze_rows(9) # Validation write_title(s_val,"A1:H1","复盘验证:2026年1-5月滚动趋势预测") s_val.get_range("A3:H3").values=[["月份","2025同月实际","2026实际/折算","训练趋势(不含当前月)","滚动预测","绝对误差率","训练口径","备注"]] fmt(s_val,"A3:H3",orange_fmt) s_val.get_range("A4:H8").values=validation s_val.get_range("F4:F8").formulas=[[f"=ABS(E{r}-C{r})/C{r}"] for r in range(4,9)] s_val.get_range("B4:C8").format.number_format="#,##0" s_val.get_range("D4:D8").format.number_format="0.00%" s_val.get_range("E4:E8").format.number_format="#,##0" s_val.get_range("F4:F8").format.number_format="0.00%" s_val.get_range("A11:B13").values=[["验证指标","数值"],["MAPE(含春节异常月)",None],["MAPE(剔除2月异常)",None]] fmt(s_val,"A11:B11",green_fmt) s_val.get_range("B12").formulas=[["=AVERAGE(F4:F8)"]] s_val.get_range("B13").formulas=[["=AVERAGE(F4,F6:F8)"]] s_val.get_range("B12:B13").format.number_format="0.00%" s_val.get_range("A16").values=[["自洽性判断"]]; fmt(s_val,"A16",yellow_fmt) s_val.get_range("A17:B20").values=[ ["1","模型月度定总量、日级分配,避免活动/天气与月度季节性重复放大。"], ["2","趋势指数偏向2026年3-5月,反映当前店铺资源与平台流量现实。"], ["3","2月受春节物流和履约影响,误差单独标记,不直接拖拽全年趋势。"], ["4","若618投放、主推店铺或爆品策略变化,应优先调整模型参数页黄色字段。"], ] s_val.get_range("A1:H22").format.wrap_text=True for rng,width in [("A1:A22",18),("B1:F22",16),("G1:H22",30)]: s_val.get_range(rng).format.column_width=width s_val.freeze_panes.freeze_rows(3) # Compact verification before export check_dash=wb.inspect({"kind":"table","range":"'10-预测总览'!A1:J17","include":"values,formulas","table_max_rows":25,"table_max_cols":12}).ndjson errors=wb.inspect({"kind":"match","search_term":"#REF!|#DIV/0!|#VALUE!|#NAME\\?|#N/A","options":{"use_regex":True,"max_results":300},"summary":"final formula error scan"}).ndjson SpreadsheetFile.export_xlsx(wb).save(out_path) print("exported", out_path) print("recent_trend", recent_trend, "daily rows", daily_n, "missing", len(missing)) print(check_dash[:1000]) print("errors:", errors[:500])
python# Verify no formula error on newly added sheets (specific ranges) new_ranges = [ "'8-模型参数'!A1:P70", "'9-日级预测系统'!A1:N221", "'10-预测总览'!A1:J17", "'11-复盘验证'!A1:H20", ] for rng in new_ranges: res=wb.inspect({"kind":"match","range":rng,"search_term":"#REF!|#DIV/0!|#VALUE!|#NAME\\?|#N/A","options":{"use_regex":True,"max_results":50},"summary":"new sheets error scan"}).ndjson print(rng, res[:300] if res else "no output") # render dashboard to confirm try: preview=wb.render({"sheet_name":"10-预测总览","range":"A1:J18","scale":1}) preview.save("/mnt/data/forecast_dashboard_preview.png") print("preview saved") except
python# Verify no formula error on newly added sheets (specific ranges) new_ranges = [ "'8-模型参数'!A1:P70", "'9-日级预测系统'!A1:N221", "'10-预测总览'!A1:J17", "'11-复盘验证'!A1:H20", ] for rng in new_ranges: res=wb.inspect({"kind":"match","range":rng,"search_term":"#REF!|#DIV/0!|#VALUE!|#NAME\\?|#N/A","options":{"use_regex":True,"max_results":50},"summary":"new sheets error scan"}).ndjson print(rng, res[:300] if res else "no output") # render dashboard to confirm try: preview=wb.render({"sheet_name":"10-预测总览","range":"A1:J18","scale":1}) preview.save("/mnt/data/forecast_dashboard_preview.png") print("preview saved") except <<ImageDisplayed>>
python# Patch dashboard widths for readability and re-export try: s_dash = wb.worksheets.get_item("10-预测总览") for rng,width in [ ("A1:A18",14),("B1:B18",10),("C1:C18",18),("D1:D18",18), ("E1:E18",18),("F1:F18",18),("G1:G18",14),("H1:H18",14), ("I1:I18",16),("J1:J18",34) ]: s_dash.get_range(rng).format.column_width=width # Make some KPI cells taller/wrapped s_dash.get_range("A3:J17").format.wrap_text=True preview=wb.render({"sheet_name":"10-预测总览","range":"A1:J18","scale":1}) preview.save("/mnt/data/forecast_dashboard_preview_v2.png") SpreadsheetFile.export_xlsx(wb).save(out_path) print("patched", out_path) except Exception as e: print("patch
python# Patch dashboard widths for readability and re-export try: s_dash = wb.worksheets.get_item("10-预测总览") for rng,width in [ ("A1:A18",14),("B1:B18",10),("C1:C18",18),("D1:D18",18), ("E1:E18",18),("F1:F18",18),("G1:G18",14),("H1:H18",14), ("I1:I18",16),("J1:J18",34) ]: s_dash.get_range(rng).format.column_width=width # Make some KPI cells taller/wrapped s_dash.get_range("A3:J17").format.wrap_text=True preview=wb.render({"sheet_name":"10-预测总览","range":"A1:J18","scale":1}) preview.save("/mnt/data/forecast_dashboard_preview_v2.png") SpreadsheetFile.export_xlsx(wb).save(out_path) print("patched", out_path) except Exception as e: print("patch <<ImageDisplayed>>
pythons_dash = wb.worksheets.get_item("10-预测总览") for rng,width in [ ("C1:C18",22),("D1:D18",22),("E1:E18",20),("F1:F18",20),("I1:I18",20) ]: s_dash.get_range(rng).format.column_width=width SpreadsheetFile.export_xlsx(wb).save(out_path) print("re-exported wider")
已按约束完成第一版严谨模型工作簿。约束文件要求保留原表、新增工作表、纳入月度资源、天气、星期、电商活动、2026近期趋势和经济环境等因素,我已逐项落地。(约束.txt)
下载文件:
仓储部单量预估表_严谨预测模型版.xlsx
已新增 6 个工作表:
关键结果:
#REF! 我没有修改,按要求保留原表。宏观和活动资料已写入参数表来源区:2026 年 1-4 月全国网上商品和服务零售额同比增长 6.6%,网上商品零售额增长 5.7%,穿类商品增长 6.8%;2025 年全国网上零售额增长 8.6%、实物商品网上零售额增长 5.2%;2026 天猫 618 已公布为预售与现货并行,现货第一波 5/21-5/30、第二波 5/31-6/21。(stats.gov.cn)
读取根据约束2的说明,然后根据补充的数据,单独生成Excel,包含数据录入表,分析表,预测表等等,重要的表格
已根据“约束2”重新做了独立 Excel 模型,不沿用旧表分析模式;补充数据已并入,形成 2024-03-01 至 2026-05-26 连续 817 天的日级单量数据。约束2要求的数据录入、月度配比、节假日、星期规律、大促、预测和复盘验证都已落表。(约束2.txt)
下载文件:
仓储部单量预测系统_独立模型版_约束2.xlsx
本次 Excel 包含 8 个核心工作表:
关键结果:
节假日表已按公开节假日安排维护,2024、2025、2026 年节假日安排分别参考新华社/国务院公开发布内容;2026 年 618 活动节奏也已写入来源区。(news.cn)
读取约束3说明,按照要求进行调整
pythonfrom artifact_tool import Blob, SpreadsheetFile, Workbook print("ok")
pythonfrom artifact_tool import Blob, SpreadsheetFile path="/mnt/data/仓储部单量预测系统_独立模型版_约束2.xlsx" wb = SpreadsheetFile.import_xlsx(Blob.load(path)) print(wb.inspect({"kind":"sheet","include":"id,name"}).ndjson[:2000])
pythonimport openpyxl, json, os, statistics, math, re, textwrap path="/mnt/data/仓储部单量预测系统_独立模型版_约束2.xlsx" wb2=openpyxl.load_workbook(path, data_only=False) print(wb2.sheetnames) for ws in wb2.worksheets: print(ws.title, ws.max_row, ws.max_column) vals=[] for row in ws.iter_rows(min_row=1, max_row=min(ws.max_row,5), min_col=1, max_col=min(ws.max_column,8), values_only=True): vals.append(row) print(vals[:3])
pythonimport openpyxl, os, json, statistics, math, re, textwrap, datetime path="/mnt/data/仓储部单量预测系统_独立模型版_约束2.xlsx" wb_orig=openpyxl.load_workbook(path, data_only=False) print(wb_orig.sheetnames) for ws in wb_orig.worksheets: print(ws.title, ws.max_row, ws.max_column) sample=[] for row in ws.iter_rows(min_row=1, max_row=min(ws.max_row,4), min_col=1, max_col=min(ws.max_column,8), values_only=True): sample.append(row) print(sample)
pythonfor sheet in ["数据录入表","因子分析表","参数表","预测表","复盘验证&建议","节假日&活动日历"]: ws=wb_orig[sheet] print(sheet) for r in range(1, min(ws.max_row,12)+1): print(r, [ws.cell(r,c).value for c in range(1, min(ws.max_column,22)+1)]) print()
pythonfor r in range(1, 57): vals=[wb_orig['参数表'].cell(r,c).value for c in range(1,9)] if any(v is not None for v in vals): print(r, vals)
pythonimport openpyxl, os, math, statistics, re, datetime, json, textwrap from openpyxl import load_workbook from openpyxl.styles import Font, PatternFill, Border, Side, Alignment from openpyxl.utils import get_column_letter from openpyxl.worksheet.table import Table, TableStyleInfo from openpyxl.worksheet.datavalidation import DataValidation from openpyxl.formatting.rule import FormulaRule, CellIsRule from openpyxl.chart import LineChart, BarChart, Reference from copy import copy input_path = "/mnt/data/仓储部单量预测系统_独立模型版_约束2.xlsx" output_path = "/mnt/data/仓储部单量预测系统_动态目标优化版_约束3.xlsx" wb = load_workbook(input_path, data_only=False) # Extract existing data ws_data_old = wb["数据录入表"] actuals = {} dates = [] for r in range(3, ws_data_old.max_row + 1): d = ws_data_old.cell(r, 1).value v = ws_data_old.cell(r, 2).value if isinstance(d, datetime '/mnt/data/仓储部单量预测系统_动态目标优化版_约束3.xlsx'
pythonimport openpyxl, re, os, datetime, json, math, statistics out="/mnt/data/仓储部单量预测系统_动态目标优化版_约束3.xlsx" wbt=openpyxl.load_workbook(out, data_only=False) print(wbt.sheetnames) for s in ["数据录入表","计算引擎","预测表","公式&逻辑说明","因子分析表","复盘验证&建议"]: ws=wbt[s] print(s, ws.max_row, ws.max_column) for r in range(1, min(ws.max_row,6)+1): print(r, [ws.cell(r,c).value for c in range(1,min(ws.max_column,10)+1)]) print("---") # scan formulas for #REF or bad quoting errors=[] for ws in wbt.worksheets: for row in ws.iter_rows(): for cell in row: v=cell.value if isinstance(v,str) and ("#REF!" in v or "#DIV/0!" in v or "#VALUE!" in v or "#NAME?" in v): errors.append((ws.title,cell.coordinate,v)) print("
已根据“约束3”完成调整:重点修正了月度目标固定导致月末/月初断崖的问题,并把录入表精简为日常只看关键字段,复杂公式集中到“计算引擎”和“公式&逻辑说明”中。约束3中关于动态目标、简化说明、星期规律口径、公式说明、统一模板风格、录入表精简和参数集中管理的要求都已落地。(约束3.txt)
下载文件:
仓储部单量预测系统_动态目标优化版_约束3.xlsx
主要调整:
预测表改为动态月度目标
数据录入表精简
星期规律重新分析
新增“公式&逻辑说明”
新增/重构关键表
已做公式文本扫描,未发现 #REF!、#DIV/0!、#VALUE!、#NAME? 等明显错误引用。打开 Excel 后公式会自动重算。
读取约束4的说明,按照要求进行调整
已按“约束4”完成调整,重点围绕数据清洗、近期权重、节假日窗口缩短、大促残差验证、录入表前置展示预测与误差率、公式说明文本化、橙色统一样式进行重构。(约束4.txt)
下载文件:
仓储部单量预测系统_科学建模优化版_约束4.xlsx
本版主要调整:
新增“清洗分析表”
数据录入表重新排布
节假日影响窗口缩短
大促因子改为残差验证
动态预测继续保留
公式说明全部文本化
已做基础校验:工作簿可正常打开,未发现 #REF! / #DIV/0! / #VALUE! / #NAME? 等明显错误引用;公式会在 Excel 打开后自动重算。
读取约束5,然后根据要求继续调整
pythonfrom artifact_tool import Blob, SpreadsheetFile input_path = "/mnt/data/仓储部单量预测系统_科学建模优化版_约束4.xlsx" wb = SpreadsheetFile.import_xlsx(Blob.load(input_path)) print(wb.inspect({"kind":"sheet","include":"id,name"}).ndjson[:2000])
pythonprint(123)
pythonfrom openpyxl import load_workbook path='/mnt/data/仓储部单量预测系统_科学建模优化版_约束4.xlsx' wb_op = load_workbook(path, data_only=False) print(wb_op.sheetnames) for ws in wb_op.worksheets: print(ws.title, ws.max_row, ws.max_column) for r in range(1, min(ws.max_row, 4)+1): print([ws.cell(r,c).value for c in range(1, min(ws.max_column,12)+1)]) print('---')
pythonfrom openpyxl import load_workbook path='/mnt/data/仓储部单量预测系统_科学建模优化版_约束4.xlsx' wb_ro = load_workbook(path, data_only=False, read_only=True) print(wb_ro.sheetnames) for ws in wb_ro.worksheets: print(ws.title, ws.max_row, ws.max_column) rows = ws.iter_rows(min_row=1, max_row=4, max_col=12, values_only=True) for row in rows: print(row) print('---')
pythonimport zipfile, xml.etree.ElementTree as ET, os, re, json, statistics, math, shutil, textwrap, datetime, calendar, random path='/mnt/data/仓储部单量预测系统_科学建模优化版_约束4.xlsx' with zipfile.ZipFile(path) as z: print(len(z.namelist())) wb_xml = z.read('xl/workbook.xml') rel_xml = z.read('xl/_rels/workbook.xml.rels') print(wb_xml[:500])
pythonNS={'main':'http://schemas.openxmlformats.org/spreadsheetml/2006/main','r':'http://schemas.openxmlformats.org/officeDocument/2006/relationships','rel':'http://schemas.openxmlformats.org/package/2006/relationships'} with zipfile.ZipFile(path) as z: wb_root=ET.fromstring(z.read('xl/workbook.xml')) rel_root=ET.fromstring(z.read('xl/_rels/workbook.xml.rels')) rels={rel.attrib['Id']: rel.attrib['Target'] for rel in rel_root} sheets=[] for s in wb_root.find('main:sheets',NS): name=s.attrib['name']; rid=s.attrib['{http://schemas.openxmlformats.org/officeDocument/2006/relationships}id'] target=rels[rid] sheets.append((name,rid,target)) print(sheets)
pythonimport zipfile, xml.etree.ElementTree as ET, re, os, math, statistics path='/mnt/data/仓储部单量预测系统_科学建模优化版_约束4.xlsx' NS_MAIN='http://schemas.openxmlformats.org/spreadsheetml/2006/main' NS={'m':NS_MAIN} with zipfile.ZipFile(path) as z: sst=[] if 'xl/sharedStrings.xml' in z.namelist(): root=ET.fromstring(z.read('xl/sharedStrings.xml')) for si in root.findall('m:si',NS): texts=[] for t in si.iter(f'{{{NS_MAIN}}}t'): texts.append(t.text or '') sst.append(''.join(texts)) print('shared strings', len(sst), sst[:10]) def cell_ref_to_rc(ref): m=re.match(r'([A-Z]+)(\d+)', ref) col=0 for ch in m.group(1): col=col*26+ord(ch)-64 return int(m.group(2)),col def get_sheet_cells(target): name=target.lstrip('/') root=ET.fromstring(z.read(name)) dim=root.find('m:dimension',NS).attrib.get('ref') cells={} for c in root.findall('.//m:c',NS): ref=c.attrib['r'] typ=c.attrib.get('t') f=c.find('m:f',NS) v=c.find('m:v',NS) isnode=c.find('m:is',NS) val=None if typ=='s' and v is not None: idx=int(v.text) val=sst[idx] if idx < len(sst) else None elif typ=='inlineStr' and isnode is not None: val=''.join([t.text or '' for t in isnode.iter(f'{{{NS_MAIN}}}t')]) elif v is not None: val=v.text cells[ref]={'value':val,'formula': f.text if f is not None else None, 'type':typ, 'style':c.attrib.get('s')} return dim,cells for name,rid,target in sheets: dim,cells=get_sheet_cells(target) print(name, dim, 'cells', len(cells)) # print first 5 rows up to 12 cols for r in range(1,6): row=[] for c in range(1,13): # col letters n=c; letters='' while n: n,rem=divmod(n-1,26); letters=chr(65+rem)+letters ref=f'{letters}{r}' cell=cells.get(ref,{}) row.append(cell.get('formula') and ('='+cell.get('formula')) or cell.get('value')) print(row) print('---')
pythonimport datetime def excel_serial(dt): base=datetime.date(1899,12,30) return (dt-base).days for d in [datetime.date(2026,5,26),datetime.date(2026,5,27),datetime.date(2026,6,1),datetime.date(2026,12,31)]: print(d, excel_serial(d))
python# Reopen and parse cells by sheet name into dicts with references values/formulas import zipfile, xml.etree.ElementTree as ET, re, os, datetime, math, statistics path='/mnt/data/仓储部单量预测系统_科学建模优化版_约束4.xlsx' NS_MAIN='http://schemas.openxmlformats.org/spreadsheetml/2006/main' NS={'m':NS_MAIN} with zipfile.ZipFile(path) as z: sst=[] if 'xl/sharedStrings.xml' in z.namelist(): root=ET.fromstring(z.read('xl/sharedStrings.xml')) for si in root.findall('m:si',NS): sst.append(''.join([t.text or '' for t in si.iter(f'{{{NS_MAIN}}}t')])) wb_root=ET.fromstring(z.read('xl/workbook.xml')) rel_root=ET.fromstring(z.read('xl/_rels/workbook.xml.rels')) rels={rel.attrib['Id']: rel.attrib['Target'] for rel in rel_root} sheets=[] for s in wb_root.find('m:sheets',NS): name=s.attrib['name']; rid=s.attrib['{http://schemas.openxmlformats.org/officeDocument/2006/relationships}id']
pythonfor r in [819,820,821]: vals=[cell_repr(eng_cells, f"{col_letter(c)}{r}") for c in range(13,28)] # M-AA print(r) for c,v in zip([col_letter(c) for c in range(13,28)], vals): print(c, v) print()
pythonpred_cells=get_cells('预测表') headers=[cell_repr(pred_cells, f"{col_letter(c)}2") for c in range(1,21)] print(headers) for r in range(3,15): vals=[cell_repr(pred_cells, f"{col_letter(c)}{r}") for c in range(1,21)] print(r, vals[:20])
pythondef get_cells2(sheet_name, xlsx_path=path): with zipfile.ZipFile(xlsx_path) as z: # shared strings already sst target=dict(sheets)[sheet_name] root=ET.fromstring(z.read(target)) cells={} for c in root.findall('.//m:c',NS): ref=c.attrib['r'] typ=c.attrib.get('t') f=c.find('m:f',NS) v=c.find('m:v',NS) isnode=c.find('m:is',NS) val=None if typ=='s' and v is not None: idx=int(v.text) val=sst[idx] if idx < len(sst) else None elif typ=='inlineStr' and isnode is not None: val=''.join([t.text or '' for t in isnode.iter(f'{{{NS_MAIN}}}t')]) elif v is not None: val=v.text cells[ref
pythonparam_cells=get_cells2('参数表') for r in range(3,53): row=[cell_repr(param_cells,f"{col_letter(c)}{r}") for c in range(1,6)] print(r,row, 'rawB', param_cells.get(f"B{r}",{}))
pythonclean_cells=get_cells2('清洗分析表') for ref in ['I3','H3','E3','F3','G3','I819','I820','H820']: print(ref, clean_cells.get(ref))
pythonfor r in range(2,15): print(r, [cell_repr(clean_cells, f"{col_letter(c)}{r}") for c in range(12,19)])
python# Search strings in all xl XML files for sheet names old_names=['说明&导航','数据录入表','清洗分析表','计算引擎','月度分析表','因子分析表','参数表','节假日&活动日历','预测表','公式&逻辑说明','复盘验证&建议'] with zipfile.ZipFile(path) as z: for old in old_names: cnt=0; files=[] for n in z.namelist(): if n.endswith('.xml') or n.endswith('.rels'): text=z.read(n).decode('utf-8', errors='ignore') # both raw and escaped amp c=text.count(old)+text.count(old.replace('&','&')) if c: cnt += c; files.append(n) print(old, cnt, files[:5])
pythonwith zipfile.ZipFile(path) as z: text=z.read('xl/worksheets/sheet2.xml').decode('utf-8') print(text[:1000]) print('数据录入表 count raw',text.count('数据录入表'),'计算引擎',text.count('计算引擎'), '清洗', text.count('清洗分析表')) import re for m in re.finditer('计算', text): print(m.start(), text[m.start()-50:m.start()+50]); break
pythonwith zipfile.ZipFile(path) as z: text=z.read('xl/worksheets/sheet2.xml').decode('utf-8') idx=text.find('<f>') print(idx, text[idx:idx+200]) print(text.count('''), text.count('''))
pythonwith zipfile.ZipFile(path) as z: text=z.read('xl/worksheets/sheet10.xml').decode('utf-8') print(text[:3000]) print('merge?', 'mergeCells' in text, 'hyperlinks' in text)
pythonwith zipfile.ZipFile(path) as z: text=z.read('xl/worksheets/sheet2.xml').decode('utf-8') print('conditionalFormatting', text.count('<conditionalFormatting')) idx=text.find('<conditionalFormatting') print(text[idx:idx+1000] if idx!=-1 else '')
pythonimport zipfile, xml.etree.ElementTree as ET, os, re, shutil, datetime, math, statistics, copy, html, calendar, tempfile from collections import defaultdict input_path = "/mnt/data/仓储部单量预测系统_科学建模优化版_约束4.xlsx" output_path = "/mnt/data/仓储部单量预测系统_预测显示修正版_约束5.xlsx" NS_MAIN = "http://schemas.openxmlformats.org/spreadsheetml/2006/main" ET.register_namespace('', NS_MAIN) q = lambda tag: f"{{{NS_MAIN}}}{tag}" # Read workbook/sheet mapping and cell contents with zipfile.ZipFile(input_path) as zin: wb_root = ET.fromstring(zin.read("xl/workbook.xml")) rel_root = ET.fromstring(zin.read("xl/_rels/workbook.xml.rels")) rel_ns = "{http://schemas.openxmlformats.org/package/2006/relationships}" rels = {rel.attrib["Id"]: rel.attrib["Target"].
python# XML patch helpers def format_number(v, decimals=None): if v is None: return "" if isinstance(v, bool): return "1" if v else "0" try: f = float(v) except: return str(v) if decimals is not None: return f"{f:.{decimals}f}".rstrip("0").rstrip(".") if abs(f - round(f)) < 1e-9: return str(int(round(f))) return f"{f:.12g}" def set_v(cell, value, is_string=False): # remove inline string if present isnode = cell.find(q("is")) if isnode is not None: cell.remove(isnode) if is_string: cell.set("t", "str") else: if cell.attrib.get("t") == "str": del cell.attrib["t"] v = cell.find(q("v")) if v is None: v = ET.SubElement(cell, q("v")) v.text = "" if value is None else (str(value) if is_string else format_number(value)) def set_inline(cell, text): # remove formula and v for child in list(cell): cell.remove(child) cell.set("t", "inlineStr") isnode = ET.SubElement(cell, q("is")) t = ET.SubElement(isnode, q("t")) t.text = "" if text is None else str(text) def set_formula(cell, formula, cached=None, is_string=False): # remove inline string if any isnode = cell.find(q("is")) if isnode is not None: cell.remove(isnode) f = cell.find(q("f")) if f is None: f = ET.SubElement(cell, q("f")) f.text = formula.lstrip("=") set_v(cell, cached, is_string=is_string) def get_or_create_row(root, row_num): sheetData = root.find(q("sheetData")) rows_by_num = {int(r.attrib["r"]): r for r in sheetData.findall(q("row"))} if row_num in rows_by_num: return rows_by_num[row_num] row = ET.Element(q("row"), {"r": str(row_num)}) # insert in order inserted = False for idx, r in enumerate(list(sheetData)): if r.tag == q("row") and int(r.attrib["r"]) > row_num: sheetData.insert(idx, row) inserted = True break if not inserted: sheetData.append(row) return row def get_or_create_cell(row, ref, style=None): for c in row.findall(q("c")): if c.attrib.get("r") == ref: if style is not None and c.attrib.get("s") is None: c.set("s", str(style)) return c c = ET.Element(q("c"), {"r": ref}) if style is not None: c.set("s", str(style)) # insert by column _, col = ref_to_rowcol(ref) inserted = False for idx, existing in enumerate(list(row)): if existing.tag == q("c"): _, ecol = ref_to_rowcol(existing.attrib["r"]) if ecol > col: row.insert(idx, c) inserted = True break if not inserted: row.append(c) return c def update_dimension(root, ref): dim = root.find(q("dimension")) if dim is not None: dim.set("ref", ref) def replace_texts_and_attrs(root, mapping): for elem in root.iter(): if elem.text: for old, new in mapping.items(): elem.text = elem.text.replace(old, new) if elem.tail: for old, new in mapping.items(): elem.tail = elem.tail.replace(old, new) for k, v in list(elem.attrib.items()): if isinstance(v, str): nv = v for old, new in mapping.items(): nv = nv.replace(old, new) elem.attrib[k] = nv rename_map = { "说明&导航": "使用说明", "数据录入表": "每日录入", "清洗分析表": "数据清洗", "计算引擎": "后台计算", "月度分析表": "月度分析", "因子分析表": "规律分析", "参数表": "参数设置", "节假日&活动日历": "节日活动", "预测表": "月度预测", "公式&逻辑说明": "怎么算的", "复盘验证&建议": "复盘建议", } # prebuild output by processing zip entries modified_xml = {} # Load worksheet roots for modification using old sheet names/path mapping with zipfile.ZipFile(input_path) as zin: # workbook.xml wb_root = ET.fromstring(zin.read("xl/workbook.xml")) # Rename sheet tab names for s in wb_root.find(q("sheets")): old = s.attrib["name"] if old in rename_map: s.set("name", rename_map[old]) # Set calc mode automatic/full calc on load calcPr = wb_root.find(q("calcPr")) if calcPr is None: calcPr = ET.SubElement(wb_root, q("calcPr")) calcPr.set("calcMode", "auto") calcPr.set("fullCalcOnLoad", "1") calcPr.set("forceFullCalc", "1") calcPr.set("calcId", "191029") modified_xml["xl/workbook.xml"] = ET.tostring(wb_root, encoding="utf-8", xml_declaration=True) # Sheet roots for modifications roots = {name: ET.fromstring(zin.read(path)) for name, path in sheet_map.items()} # Data entry sheet: title/header + cached formula results root = roots["数据录入表"] # Title and headers more human-friendly set_inline(get_or_create_cell(get_or_create_row(root,1), "A1", style=1), "每日录入|先看预测,再填实际,后续预测自动修正") headers = ["日期","星期","实际单量(填这里)","当天预测","误差率","最终用于排班","状态","节假日","电商活动","本月动态目标","本月已完成","备注"] for idx, h in enumerate(headers, start=1): set_inline(get_or_create_cell(get_or_create_row(root,2), f"{col_letter(idx)}2", style=2), h) # Data entry cached values for x in rows: r = x["row"] # D predicted set_v(get_or_create_cell(get_or_create_row(root,r), f"D{r}"), round(x["system_pred"], 0)) # E error rate set_v(get_or_create_cell(get_or_create_row(root,r), f"E{r}"), x["error_rate"]) # F final set_v(get_or_create_cell(get_or_create_row(root,r), f"F{r}"), round(x["final_qty"], 0)) # G status set_v(get_or_create_cell(get_or_create_row(root,r), f"G{r}"), x["status"], is_string=True) # H holiday display set_v(get_or_create_cell(get_or_create_row(root,r), f"H{r}"), x["holiday_display"], is_string=True) # I activity display set_v(get_or_create_cell(get_or_create_row(root,r), f"I{r}"), x["activity_display"], is_string=True) # J dynamic target, K completed actual, L remark set_v(get_or_create_cell(get_or_create_row(root,r), f"J{r}"), round(x["dynamic_target"],0)) set_v(get_or_create_cell(get_or_create_row(root,r), f"K{r}"), round(x["month_actual_sum"],0)) set_v(get_or_create_cell(get_or_create_row(root,r), f"L{r}"), x["remark"], is_string=True) roots["数据录入表"] = root # Engine sheet: fix formulas and cache key columns root = roots["计算引擎"] set_inline(get_or_create_cell(get_or_create_row(root,1), "A1", style=1), "后台计算|公式集中区,日常不用编辑") engine_headers = ["日期","年","月","年月","星期","原始实际","建模单量","清洗状态","日期类型","节假日名称","节假日阶段","电商活动","活动阶段","月内建模日均","星期因子","节假日因子","活动因子","综合权重","动态月度目标","本月已完成","未录入权重","系统预测","误差率","最终单量","状态","复盘基准","复盘误差"] for idx, h in enumerate(engine_headers, start=1): set_inline(get_or_create_cell(get_or_create_row(root,2), f"{col_letter(idx)}2", style=2), h) for x in rows: r = x["row"] # Update formula in U: <=0 instead of blank u_formula = f'SUMIFS($R$3:$R$1038,$A$3:$A$1038,">="&DATE(YEAR(A{r}),MONTH(A{r}),1),$A$3:$A$1038,"<"&EDATE(DATE(YEAR(A{r}),MONTH(A{r}),1),1),$F$3:$F$1038,"<=0")' set_formula(get_or_create_cell(get_or_create_row(root,r), f"U{r}"), u_formula, x["month_unrec_weight"]) v_formula = f'IF(F{r}>0,IFERROR(S{r}*R{r}/SUMIFS($R$3:$R$1038,$A$3:$A$1038,">="&DATE(YEAR(A{r}),MONTH(A{r}),1),$A$3:$A$1038,"<"&EDATE(DATE(YEAR(A{r}),MONTH(A{r}),1),1)),""),IFERROR(MAX(0,S{r}-T{r})*R{r}/U{r},0))' set_formula(get_or_create_cell(get_or_create_row(root,r), f"V{r}"), v_formula, round(x["system_pred"], 0)) # Cache S/T/W/X/Y and key factor formulas set_v(get_or_create_cell(get_or_create_row(root,r), f"E{r}"), x["weekday"], is_string=True) set_v(get_or_create_cell(get_or_create_row(root,r), f"H{r}"), x["clean_status"], is_string=True) set_v(get_or_create_cell(get_or_create_row(root,r), f"I{r}"), x["date_type"], is_string=True) set_v(get_or_create_cell(get_or_create_row(root,r), f"J{r}"), x["holiday_name"], is_string=True) set_v(get_or_create_cell(get_or_create_row(root,r), f"K{r}"), x["holiday_stage"], is_string=True) set_v(get_or_create_cell(get_or_create_row(root,r), f"L{r}"), x["activity"], is_string=True) set_v(get_or_create_cell(get_or_create_row(root,r), f"M{r}"), x["activity_stage"], is_string=True) set_v(get_or_create_cell(get_or_create_row(root,r), f"O{r}"), x["weekday_factor"]) set_v(get_or_create_cell(get_or_create_row(root,r), f"P{r}"), x["holiday_factor"]) set_v(get_or_create_cell(get_or_create_row(root,r), f"Q{r}"), x["activity_factor"]) set_v(get_or_create_cell(get_or_create_row(root,r), f"R{r}"), x["weight"]) set_v(get_or_create_cell(get_or_create_row(root,r), f"S{r}"), round(x["dynamic_target"], 0)) set_v(get_or_create_cell(get_or_create_row(root,r), f"T{r}"), round(x["month_actual_sum"], 0)) set_v(get_or_create_cell(get_or_create_row(root,r), f"W{r}"), x["error_rate"]) set_v(get_or_create_cell(get_or_create_row(root,r), f"X{r}"), round(x["final_qty"], 0)) set_v(get_or_create_cell(get_or_create_row(root,r), f"Y{r}"), x["status"], is_string=True) roots["计算引擎"] = root # Forecast sheet: fix formulas and cache all key values root = roots["预测表"] set_inline(get_or_create_cell(get_or_create_row(root,1), "A1", style=1), "月度预测|下月总量和剩余预测会随每日实际自动修正") pred_headers = ["月份","月","2024同月建模量","2025同月建模量","历史加权基准","月度调整系数","原始月度目标","邻月平滑目标","已录入实际","已录入天数","月天数","已录入权重占比","动态月度目标","剩余可分配","剩余未录权重","剩余预测","最终月估","完成率","最终估计偏差","状态/建议"] for idx,h in enumerate(pred_headers, start=1): set_inline(get_or_create_cell(get_or_create_row(root,2), f"{col_letter(idx)}2", style=2), h) for mon in range(1,13): r = mon + 2 m = monthly[mon] values = [ m["start_serial"], mon, m["c2024"], m["c2025"], m["hist_base"], m["adj"], m["raw_target"], m["smooth_target"], m["actual_sum"], m["actual_days"], m["days"], m["share"], m["dynamic"], m["remaining"], m["unrec_weight"], m["remaining_pred"], m["final_est"], m["completion_rate"], m["final_bias"], m["status"] ] # Update formulas for O/P with <=0 (we keep formula texts explicit after sheet rename) o_formula = f"SUMIFS('后台计算'!$R$3:$R$1038,'后台计算'!$A$3:$A$1038,\">=\"&A{r},'后台计算'!$A$3:$A$1038,\"<\"&EDATE(A{r},1),'后台计算'!$F$3:$F$1038,\"<=0\")" p_formula = f"SUMIFS('后台计算'!$V$3:$V$1038,'后台计算'!$A$3:$A$1038,\">=\"&A{r},'后台计算'!$A$3:$A$1038,\"<\"&EDATE(A{r},1),'后台计算'!$F$3:$F$1038,\"<=0\")" # Update cached values for cells A:T. Respect formulas in formula cells, only set v. for idx, val in enumerate(values, start=1): ref = f"{col_letter(idx)}{r}" cell = get_or_create_cell(get_or_create_row(root,r), ref) if idx == 15: set_formula(cell, o_formula, val) elif idx == 16: set_formula(cell, p_formula, val) elif idx == 20: set_v(cell, val, is_string=True) else: set_v(cell, val) roots["预测表"] = root # Parameter sheet: cache latest date and scale, update title text root = roots["参数表"] set_inline(get_or_create_cell(get_or_create_row(root,1), "A1", style=1), "参数设置|关键参数集中维护,能公式化的尽量公式化") set_v(get_or_create_cell(get_or_create_row(root,4), "B4"), latest_actual_serial) set_v(get_or_create_cell(get_or_create_row(root,5), "B5"), scale_2026) roots["参数表"] = root # Instructions sheet: clearer scenario examples + formulas and cached values root = roots["说明&导航"] set_inline(get_or_create_cell(get_or_create_row(root,1), "A1", style=1), "使用说明|每天先看预测,再填实际") # top table inst = { "A3": "场景", "B3": "什么时候看", "C3": "怎么做", "D3": "看完怎么判断", "E3": "入口", "A4": "每天录入", "B4": "拿到昨天实际单量后", "C4": "只填「每日录入」C列实际单量", "D4": "D列看当天预测,E列看误差率,F列用于排班", "A5": "看预测/排班", "B5": "每天或每周排班前", "C5": "看未来每日F列,以及「月度预测」下月最终月估", "D5": "未来日期不再显示0,会按剩余目标自动分配", "A6": "月初/月末衔接", "B6": "每月最后一周和月初", "C6": "不要手动改目标,录入实际后系统自动修正", "D6": "避免上月多、下月少造成断崖式预测", "A7": "误差复盘", "B7": "E列连续偏大时", "C7": "先看节假日/活动是否标错,再看是否需要调整参数", "D7": "误差超过25%会在备注中提示", "G3": "核心指标", "H3": "当前值", "I3": "说明", "G4": "最新实际日期", "I4": "自动读取最后一个已录入实际单量日期", "G5": "下月总量预测", "I5": "用于至少提前一个月评估排班资源", "G6": "未来7天预测", "I6": "用于短期排班、打包人力安排", "G7": "5/27后总预测", "I7": "从2026-05-27到年底的日级预测合计", } for ref, txt in inst.items(): set_inline(get_or_create_cell(get_or_create_row(root, int(re.findall(r'\d+', ref)[0])), ref), txt) # Links/formulas set_formula(get_or_create_cell(get_or_create_row(root,4), "E4"), 'HYPERLINK("#\'每日录入\'!A1","打开每日录入")', "打开每日录入", is_string=True) set_formula(get_or_create_cell(get_or_create_row(root,5), "E5"), 'HYPERLINK("#\'月度预测\'!A1","打开月度预测")', "打开月度预测", is_string=True) set_formula(get_or_create_cell(get_or_create_row(root,6), "E6"), 'HYPERLINK("#\'月度预测\'!A1","打开月度预测")', "打开月度预测", is_string=True) set_formula(get_or_create_cell(get_or_create_row(root,7), "E7"), 'HYPERLINK("#\'复盘建议\'!A1","打开复盘建议")', "打开复盘建议", is_string=True) set_formula(get_or_create_cell(get_or_create_row(root,4), "H4"), "'参数设置'!B4", latest_actual_serial) set_formula(get_or_create_cell(get_or_create_row(root,5), "H5"), 'IFERROR(VLOOKUP(EDATE(DATE(YEAR(\'参数设置\'!B4),MONTH(\'参数设置\'!B4),1),1),\'月度预测\'!$A$3:$T$14,17,FALSE),"")', monthly[6]["final_est"]) future_7 = sum(x["system_pred"] for x in rows if latest_actual_serial < x["serial"] <= latest_actual_serial+7) future_total = sum(x["system_pred"] for x in rows if x["serial"] >= serial_from_date(datetime.date(2026,5,27))) set_formula(get_or_create_cell(get_or_create_row(root,6), "H6"), 'SUMIFS(\'每日录入\'!$D$3:$D$1038,\'每日录入\'!$A$3:$A$1038,">"&\'参数设置\'!$B$4,\'每日录入\'!$A$3:$A$1038,"<="&\'参数设置\'!$B$4+7)', round(future_7,0)) set_formula(get_or_create_cell(get_or_create_row(root,7), "H7"), 'SUMIFS(\'每日录入\'!$D$3:$D$1038,\'每日录入\'!$A$3:$A$1038,">="&DATE(2026,5,27))', round(future_total,0)) roots["说明&导航"] = root # Formula/logic sheet: rebuild with examples and links root = roots["公式&逻辑说明"] update_dimension(root, "A1:G22") sheetData = root.find(q("sheetData")) # clear existing sheetData for child in list(sheetData): sheetData.remove(child) # rows for logic sheet logic_rows = [ ["怎么算的|公式、示例、跳转", "", "", "", "", "", ""], ["模块", "看什么", "逻辑说明", "公式文字(不执行)", "示例参数", "示例计算", "跳转"], ["每日录入", "实际录入后怎么用", "C列填实际;有实际就用实际,没有实际就用系统预测,保证未来排班能看到每天数量。", "最终用于排班 = IF(实际单量>0, 实际单量, 当天预测)", f"5/27实际未填;当天预测≈{round(first_future['system_pred']):,}", f"最终用于排班={round(first_future['system_pred']):,}", "打开每日录入"], ["核心修正", "为什么之前会变成0", "空白单元格被公式引用后会变成0,所以不能用“=\"\"”判断未录入;必须用“<=0”识别未来未录入日期。", "剩余未录权重 = SUMIFS(综合权重, 实际单量, \"<=0\")", f"5月剩余未录权重≈{monthly[5]['unrec_weight']:.2f}", "剩余权重不为0,日预测才不会为0", "打开后台计算"], ["日预测", "每天预测怎么算", "先确定本月动态目标,再扣掉已完成实际;剩余量按未来每一天的综合权重分配。", "当天预测 = MAX(0, 动态月度目标-本月已完成) × 当日综合权重 / 剩余未录权重", f"5月动态目标≈{monthly[5]['dynamic']:,};已完成≈{monthly[5]['actual_sum']:,}", f"5/27预测≈{round(first_future['system_pred']):,}", "打开每日录入"], ["月目标", "为什么目标会动态变", "月度目标不是固定计划;有实际后,用已完成进度反推全月规模,再和邻月平滑目标加权。", "动态月度目标 = MAX(已录入实际, 已录入实际/已完成权重占比×0.65 + 邻月平滑目标×0.35)", f"5月已完成权重占比≈{monthly[5]['share']:.2%}", f"5月动态目标≈{monthly[5]['dynamic']:,}", "打开月度预测"], ["下月总量", "排班至少看下月", "下月没有实际时,使用邻月平滑后的月度目标作为总量预测;每天再按星期、节假日、大促权重分配。", "下月总量 = 月度预测表「最终月估」", f"下月=2026年6月", f"6月预计≈{round(monthly[6]['final_est']):,}", "打开月度预测"], ["误差率", "录入实际后怎么看偏差", "实际录入后,系统用当初的预测口径对比实际,连续偏大说明活动、节假日或运营变化需要复盘。", "误差率 = 实际单量 / 当天预测 - 1", "实际=3,000;预测=2,500", "误差率=20%", "打开复盘建议"], ["星期规律", "星期因子怎么来", "用清洗后的全部历史数据分析;只剔除节假日/调休影响,不剔除大促。", "星期因子 = 某星期普通日均值 / 普通日总体均值", "周一因子≈{:.3f}".format(weekday_factor.get("周一",1)), "周一相对普通日更高/更低", "打开规律分析"], ["节假日", "节前节后范围", "只看节前3天、节假日、节后3天和调休工作日,避免和星期规律重复。", "节假日因子 = 节假日阶段残差日均 / 普通日残差日均", "窗口=±3天", "降低节假日和星期重复计算", "打开节日活动"], ["大促", "什么时候启用大促因子", "先扣除月份、星期、节假日影响;残差不足阈值就不启用,避免重复计算。", "活动因子 = IF(ABS(残差-1)>=8%, 残差因子, 1)", "双11核心期残差不足8%", "不强行加大促因子", "打开参数设置"], ["数据清洗", "异常值怎么处理", "原始单量保留,建模时剔除超过同月IQR阈值的异常值。", "清洗状态 = IF(实际<下限或实际>上限, \"异常剔除\", \"纳入建模\")", "IQR倍数=2.5", "异常只影响建模,不删除原数据", "打开数据清洗"], ["录入后自动修正", "填了今天实际后会发生什么", "实际单量进入本月已完成;剩余目标和剩余未录权重会重算,后续每天预测随之变化。", "新后续预测 = MAX(0, 新动态目标-新已完成) × 未来日权重 / 新剩余未录权重", "今天实际高于预测", "后续预测会自动下调或目标上修", "打开每日录入"], ["自检口径", "如何判断逻辑是否正常", "未来日期当天预测不能为0;月度预测里下月最终月估应有数;误差率只在有实际时显示。", "自检 = AND(5/27预测>0, 下月总量>0, 空白实际误差率=\"\")", f"5/27预测≈{round(first_future['system_pred']):,}", "通过", "打开使用说明"], ["参数", "哪些能调", "常调的只有月度调整系数、历史权重、残差阈值;日常录入不需要改公式。", "参数集中在「参数设置」;公式集中在「后台计算」", "低频调整", "月底复盘后再改", "打开参数设置"], ["排班建议", "用哪个数字排班", "短期看每日录入F列,至少提前一个月看月度预测Q列。", "排班参考 = 每日F列 + 月度Q列", f"未来7天≈{round(future_7):,}", f"6月总量≈{round(monthly[6]['final_est']):,}", "打开每日录入"], ] # Append rows for r_idx, row_vals in enumerate(logic_rows, start=1): row = ET.SubElement(sheetData, q("row"), {"r": str(r_idx)}) if r_idx == 1: row.set("ht","28"); row.set("customHeight","1") elif r_idx == 2: row.set("ht","24"); row.set("customHeight","1") else: row.set("ht","42"); row.set("customHeight","1") for c_idx, val in enumerate(row_vals, start=1): ref = f"{col_letter(c_idx)}{r_idx}" style = "1" if r_idx == 1 else ("2" if r_idx == 2 else "3") cell = ET.SubElement(row, q("c"), {"r": ref, "s": style}) if c_idx == 7 and r_idx >= 3: # hyperlink formula target_map = { "打开每日录入": "每日录入", "打开后台计算": "后台计算", "打开月度预测": "月度预测", "打开复盘建议": "复盘建议", "打开规律分析": "规律分析", "打开节日活动": "节日活动", "打开参数设置": "参数设置", "打开数据清洗": "数据清洗", "打开使用说明": "使用说明", } target = target_map.get(val, "使用说明") set_formula(cell, f'HYPERLINK("#\'{target}\'!A1","{val}")', val, is_string=True) else: set_inline(cell, val) # ensure merge cell A1:G1 mergeCells = root.find(q("mergeCells")) if mergeCells is not None: root.remove(mergeCells) mergeCells = ET.Element(q("mergeCells"), {"count":"1"}) ET.SubElement(mergeCells, q("mergeCell"), {"ref":"A1:G1"}) # Insert mergeCells after sheetData if possible (or before pageMargins) children = list(root) sd_idx = children.index(sheetData) root.insert(sd_idx+1, mergeCells) roots["公式&逻辑说明"] = root # Update dimensions and replacement/renaming across worksheet XML roots for name, root in roots.items(): replace_texts_and_attrs(root, rename_map) # if sheet10 was renamed in mapping but roots key old; fine modified_xml[sheet_map[name]] = ET.tostring(root, encoding="utf-8", xml_declaration=True) # Replace old names in chart and workbook XML too, including workbook paths already modified # modified workbook xml already has renamed attributes but maybe contains old formulas? run replacement wb_root = ET.fromstring(modified_xml["xl/workbook.xml"]) replace_texts_and_attrs(wb_root, rename_map) modified_xml["xl/workbook.xml"] = ET.tostring(wb_root, encoding="utf-8", xml_declaration=True) # Write output ZIP with modified XML files and textual replacements for chart XMLs with zipfile.ZipFile(input_path, 'r') as zin, zipfile.ZipFile(output_path, 'w', compression=zipfile.ZIP_DEFLATED) as zout: for item in zin.infolist(): data = zin.read(item.filename) if item.filename in modified_xml: data = modified_xml[item.filename] elif item.filename.endswith(".xml"): # For charts and any other XML: replace old sheet refs/text if parseable try: root = ET.fromstring(data) replace_texts_and_attrs(root, rename_map) data = ET.tostring(root, encoding="utf-8", xml_declaration=True) except Exception: # leave unchanged if parsing fails pass zout.writestr(item, data) print("saved", output_path, os.path.getsize(output_path)) print("first future 2026-05-27 prediction", round(first_future["system_pred"],0)) print("June total", round(monthly[6]["final_est"],0), "future_7", round(future_7,0), "future_total", round(future_total,0))
python# Verify output structure and key cells via manual parse out = output_path with zipfile.ZipFile(out) as z: wb_root = ET.fromstring(z.read("xl/workbook.xml")) rel_root = ET.fromstring(z.read("xl/_rels/workbook.xml.rels")) rels2={rel.attrib["Id"]: rel.attrib["Target"].lstrip("/") for rel in rel_root} sheets2=[] for s in wb_root.find(q("sheets")): rid=s.attrib["{http://schemas.openxmlformats.org/officeDocument/2006/relationships}id"] sheets2.append((s.attrib["name"], rels2[rid])) print("sheets", [n for n,p in sheets2]) # calcPr print("calcPr", wb_root.find(q("calcPr")).attrib) def parse_cells_from_path(pathx): root=ET.fromstring(z.read(pathx)) cells={} for c in root.findall(".//"+q("c")): ref=c.attrib.get("r") f=c.find(q("f")) v=c.find(q("v")) isnode=c.find(q("is")) val=None if c.attrib.get("t")=="inlineStr" and isnode is not None: val="".join((t.text or "") for t in isnode.iter(q("t"))) elif v is not None: val=v.text cells[ref]={'f': f.text if f is not None else None, 'v': val, 't': c.attrib.get('t')} return cells sheet_dict=dict(sheets2) data_o=parse_cells_from_path(sheet_dict["每日录入"]) eng_o=parse_cells_from_path(sheet_dict["后台计算"]) pred_o=parse_cells_from_path(sheet_dict["月度预测"]) logic_o=parse_cells_from_path(sheet_dict["怎么算的"]) for ref in ["D820","E820","F820","G820","J820","K820"]: print("每日录入", ref, data_o.get(ref)) for ref in ["U820","V820","S820","T820","Y820"]: print("后台计算", ref, eng_o.get(ref)) for ref in ["Q8","O8","P8","T8"]: print("月度预测", ref, pred_o.get(ref)) for ref in ["A1","G3","G4","G5"]: print("怎么算的", ref, logic_o.get(ref)) # search old formula bug criteria allxml = "".join(z.read(n).decode("utf-8", errors="ignore") for n in z.namelist() if n.endswith(".xml")) print('criteria blank count in formulas', allxml.count('""'), allxml.count(',"")')) print('old names present raw?', any(name in allxml for name in rename_map.keys()))
pythonwith zipfile.ZipFile(output_path) as z: allxml = "\n".join(z.read(n).decode("utf-8", errors="ignore") for n in z.namelist() if n.endswith(".xml")) for term in ["#REF!", "#DIV/0!", "#VALUE!", "#NAME?", "#N/A"]: print(term, allxml.count(term)) # Look for formula criteria F "" around SUMIFS maybe print('F blank SUMIFS old pattern present?', '$F$3:$F$1038,"")' in allxml) print('target file exists', os.path.exists(output_path), os.path.getsize(output_path))
已按“约束5”继续调整完成,重点修复了5/27 以后预测单量显示为 0的问题,并增加了公式说明、示例、跳转链接和更人性化的表名/表头。约束5的核心要求已逐项落地。(约束5.txt)
下载文件:
仓储部单量预测系统_预测显示修正版_约束5.xlsx
主要修复与调整:
修复未来预测为 0 的严重问题
0,原逻辑用“空白”判断,导致剩余权重为 0。<=0 判断未录入日期。每日录入后自动修正后续预测
排班所需结果已前置展示
新增更直观的逻辑说明
表名和表头更人性化
自我校验结果
#REF! / #DIV/0! / #VALUE! / #NAME? / #N/A。读取约束6说明,在之前表格基础上按照内容进行调整
已读取并按“约束6”继续调整,重点修复了月度总量硬压缩导致预测失真、已录入日期被后续实际单量牵动、月末预测异常跳变等问题。(约束6.txt)
下载文件:
仓储部单量预测系统_滚动预测稳定版_约束6.xlsx
本次主要调整:
取消“剩余月目标强行压缩”逻辑
剩余月目标 ÷ 剩余权重,容易导致前面实际稍高,后面被强行压低。已录入日期不再被后续录入牵动
增加预测稳定控制
验证 5/30 与 5/31 异常问题
名称与表头优化
录入表去掉排班专属列
公式说明改成“公式 + 示例”
已做自检:工作簿结构正常,未发现 #REF! / #DIV/0! / #VALUE! / #NAME? / #N/A 等明显错误引用;文件已设置打开后自动重算公式。
读取约束7,在之前表格基础上按照内容进行调整
pythonfrom artifact_tool import Blob, SpreadsheetFile path='/mnt/data/仓储部单量预测系统_滚动预测稳定版_约束6.xlsx' wb=SpreadsheetFile.import_xlsx(Blob.load(path)) print(wb.inspect({'kind':'sheet','include':'id,name'}).ndjson)
pythonimport openpyxl, json, os, textwrap, statistics, math, re, sys, zipfile, shutil, random path='/mnt/data/仓储部单量预测系统_滚动预测稳定版_约束6.xlsx' wb_ox=openpyxl.load_workbook(path) wb_ox.sheetnames
pythonimport openpyxl, os, time path='/mnt/data/仓储部单量预测系统_滚动预测稳定版_约束6.xlsx' t=time.time() wb_ro=openpyxl.load_workbook(path, read_only=True, data_only=False) print('loaded', time.time()-t, wb_ro.sheetnames)
已读取“约束7”,并在约束6版本基础上继续调整完成。约束7要求重点是:根据误差率做模型自修正、分析异常暴增/相邻数据规律、判断波动是单日还是持续影响,并避免一次预测失误后连续偏差扩大。(约束7.txt)
下载文件:
仓储部单量预测系统_误差自修正版_约束7.xlsx
本次主要调整:
新增误差自修正逻辑
误差率分级细化
新增异常波动判断
未来预测能跟随实际波动
新增参数区
公式与示例补充
验证与建议增强
已做基础自检:压缩结构正常,未发现 #REF! / #DIV/0! / #VALUE! / #NAME? / #N/A 等明显错误引用;工作簿已设置为打开后自动重算。
读取约束8,在之前表格基础上按照内容继续进行调整
已读取“约束8”,并在约束7版本基础上继续调整完成。约束8要求的录入表精简、误差颜色化、性能优化、样式统一、点击跳转和列数压缩都已处理。(约束8.txt)
下载文件:
仓储部单量预测系统_轻量录入优化版_约束8.xlsx
本次主要调整:
录入表精简为 8 列
误差率不再额外拆列
优化录入卡顿
增加点击跳转
样式统一
公式与示例补充
自检结果
='预测计算'!V820。#REF! / #DIV/0! / #VALUE! / #NAME? / #N/A 等明显错误文本。弃用约束8版本,在版本约束7的表格基础上进行修改调整,先读取约束9说明
pythonfrom artifact_tool import Blob, SpreadsheetFile print("imports ok")
pythonfrom openpyxl import load_workbook path='/mnt/data/仓储部单量预测系统_误差自修正版_约束7.xlsx' wb2=load_workbook(path, data_only=False) print(wb2.sheetnames) for ws in wb2.worksheets: print(ws.title, ws.max_row, ws.max_column, [ws.cell(1,c).value for c in range(1,min(ws.max_column,15)+1)])
pythonfrom openpyxl import load_workbook path='/mnt/data/仓储部单量预测系统_误差自修正版_约束7.xlsx' wb=load_workbook(path, data_only=False) print(wb.sheetnames) for ws in wb.worksheets: print(ws.title, ws.max_row, ws.max_column, [ws.cell(1,c).value for c in range(1,min(ws.max_column,12)+1)])
pythonfor s in ['日单量录入','预测计算','模型参数','公式与示例','月度预测']: ws=wb[s] print("\n",s) for r in range(1,6): print(r, [ws.cell(r,c).value for c in range(1,min(ws.max_column,20)+1)])
pythonws=wb['预测计算'] headers=[ws.cell(2,c).value for c in range(1,ws.max_column+1)] for i,h in enumerate(headers,1): print(i, h, "sample", ws.cell(3,i).value)
pythonws=wb['模型参数'] for r in range(1,70): vals=[ws.cell(r,c).value for c in range(1,6)] if any(v is not None for v in vals): print(r, vals)
pythonfrom datetime import datetime, date ws_in=wb['日单量录入'] data=[] for r in range(3, ws_in.max_row+1): d=ws_in.cell(r,1).value v=ws_in.cell(r,3).value if isinstance(d, datetime): d=d.date() if isinstance(d, date) and isinstance(v,(int,float)) and v>0: data.append((d,float(v))) len(data), data[0], data[-1] (817, (datetime.date(2024, 3, 1), 3394.0), (datetime.date(2026, 5, 26), 2410.0))
pythonfrom collections import defaultdict import statistics, math, calendar data_dict=dict(data) # daily adjacent change ratios for consecutive dates changes=[] for i in range(1,len(data)): d0,v0=data[i-1]; d1,v1=data[i] if (d1-d0).days==1 and v0>0: changes.append((d1, v1/v0-1)) abs_changes=[abs(x[1]) for x in changes] def percentile(vals,p): vals=sorted([v for v in vals if v is not None]) if not vals: return None k=(len(vals)-1)*p f=math.floor(k); c=math.ceil(k) if f==c: return vals[int(k)] return vals[f]*(c-k)+vals[c]*(k-f) p50=percentile(abs_changes,0.5); p75=percentile(abs_changes,0.75); p90=percentile(abs_changes,0.9) # cross month changes cross=[] for i in range(1,len(data)): d0,v0=data[i-1]; d1,v1=data[i] if (d1-d0).days==1 and d1.day==1: cross.append((d0,d1, v1/v0-1, v0, v1)) cross
pythonfrom openpyxl import load_workbook from openpyxl.styles import PatternFill, Font, Alignment, Border, Side from openpyxl.formatting.rule import FormulaRule from openpyxl.worksheet.hyperlink import Hyperlink from openpyxl.utils import get_column_letter from openpyxl.comments import Comment from datetime import datetime, date, timedelta from collections import defaultdict import math, statistics, os, copy, re, calendar input_path = "/mnt/data/仓储部单量预测系统_误差自修正版_约束7.xlsx" output_path = "/mnt/data/仓储部单量预测系统_约束9_WPS兼容动态预测版.xlsx" wb = load_workbook(input_path, data_only=False) # Theme styles orange = "F4B183" deep_orange = "C65911" light_orange = "FCE4D6" pale_orange = "FFF2CC" green = "E2F0D9" yellow = "FFF2CC" red = "F4CCCC" gray = "E7E6E6" blue = "D9EAF7" white = "FFFFFF" border_color = "D9A066" thin = Side(style="thin", color=border_color) border = Border(left=thin, right=thin, top=thin, bottom=thin) # Helper def style_title(ws, title, max_col): ws.merge_cells(start_row=1, start_column=1, end_row=1, end_column=max_col) c = ws.cell(1,1) c.value = title c.fill = PatternFill("solid", fgColor=deep_orange) c.font = Font(bold=True, color=white, size=14) c.alignment = Alignment(horizontal="center", vertical="center") ws.row_dimensions[1].height = 28 def style_header(ws, row, max_col): for c in range(1, max_col+1): cell = ws.cell(row,c) cell.fill = PatternFill("solid", fgColor=orange) cell.font = Font(bold=True, color="1F1F1F", size=11) cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True) cell.border = border ws.row_dimensions[row].height = 26 def apply_body_style(ws, min_row, max_row, min_col, max_col): for row in ws.iter_rows(min_row=min_row, max_row=max_row, min_col=min_col, max_col=max_col): for cell in row: cell.border = border cell.alignment = Alignment(vertical="center", wrap_text=True) cell.font = Font(size=10, color="1F1F1F") for r in range(min_row, max_row+1): ws.row_dimensions[r].height = 20 def set_widths(ws, widths): for col, width in widths.items(): ws.column_dimensions[col].width = width def clear_conditional_formatting_range(ws): # Simpler: replace all conditional formatting rules on sheet with none. # openpyxl stores this internally; easiest is to clear object entirely. ws.conditional_formatting._cf_rules.clear() # Extract actual data for analytics ws_in = wb['日单量录入'] data = [] for r in range(3, ws_in.max_row + 1): d = ws_in.cell(r,1).value v = ws_in.cell(r,3).value if isinstance(d, datetime): d = d.date() if isinstance(d, date) and isinstance(v, (int, float)) and v > 0: data.append((d, float(v))) data_dict = dict(data) def percentile(vals, p): vals = sorted([float(v) for v in vals if v is not None]) if not vals: return None k = (len(vals)-1)*p f = math.floor(k); c = math.ceil(k) if f == c: return vals[int(k)] return vals[f]*(c-k)+vals[c]*(k-f) # Adjacent actual change analysis changes=[] cross=[] for i in range(1, len(data)): d0, v0 = data[i-1]; d1, v1 = data[i] if (d1 - d0).days == 1 and v0 > 0: cr = v1/v0 - 1 changes.append((d1, cr)) if d1.day == 1: cross.append((d0, d1, cr, v0, v1)) abs_changes = [abs(x[1]) for x in changes] p50_change = percentile(abs_changes, 0.50) or 0.07 p75_change = percentile(abs_changes, 0.75) or 0.12 p90_change = percentile(abs_changes, 0.90) or 0.21 cross_abs = [abs(x[2]) for x in cross] p75_cross = percentile(cross_abs, 0.75) or 0.10 p90_cross = percentile(cross_abs, 0.90) or 0.25 # Spike persistence analysis spikes=[] for i in range(7, len(data)-3): d, v = data[i] prev = [data[j][1] for j in range(i-7, i)] avg = sum(prev)/len(prev) if avg > 0 and v/avg - 1 > 0.18: ret1 = data[i+1][1]/v - 1 ret2 = data[i+2][1]/v - 1 ret3 = data[i+3][1]/v - 1 spikes.append((d, v/avg-1, ret1, ret2, ret3)) spike_count = len(spikes) spike_avg = statistics.mean([x[1] for x in spikes]) if spikes else 0.0 ret1_avg = statistics.mean([x[2] for x in spikes]) if spikes else 0.0 ret2_avg = statistics.mean([x[3] for x in spikes]) if spikes else 0.0 ret3_avg = statistics.mean([x[4] for x in spikes]) if spikes else 0.0 # Month features month_stats = defaultdict(list) for d, v in data: month_stats[d.month].append((d.year, v)) holidays_by_month = { 1: "元旦;部分年份春节临近,节前通常偏弱", 2: "春节/元宵影响最明显,节中低、节后逐步恢复", 3: "开学/春季换季需求,节假日扰动少", 4: "清明节;春季需求延续", 5: "劳动节;618预热通常从下旬开始", 6: "端午节;618核心期,活动扰动较强", 7: "暑期,节假日少,按常规星期与近期波动为主", 8: "暑期后段,开学前需求可能抬升", 9: "中秋/开学季,节前节后变化需关注", 10: "国庆长假;节前下降、节后恢复,双11预热可能开始", 11: "双11核心月,活动残差影响显著", 12: "双12/年末,活动后回落与年末节奏并存" } month_feature_rows = [] recent_cut = data[-1][0] - timedelta(days=180) recent_vals_by_month = defaultdict(list) for d, v in data: if d >= recent_cut: recent_vals_by_month[d.month].append(v) for m in range(1,13): vals = [v for y,v in month_stats[m]] recent_vals = recent_vals_by_month[m] all_avg = sum(vals)/len(vals) if vals else 0 recent_avg = sum(recent_vals)/len(recent_vals) if recent_vals else 0 years = sorted(set(y for y,v in month_stats[m])) month_feature_rows.append([ m, f"{m}月", round(all_avg,0) if all_avg else "", round(recent_avg,0) if recent_avg else "", holidays_by_month[m], "参考最近6个月同类波动;2024仅作低权重参考" if recent_avg else "当前近6个月样本不足,参考2025为主", "月初/月末不强行按月总量断开,按相邻日变化率平滑衔接" ]) # 1) Rename old calculation sheet and replace references if "后台计算" in wb.sheetnames: # Avoid duplicate if rerun del wb["后台计算"] old_calc = wb["预测计算"] old_calc.title = "后台计算" # Replace external formulas pointing to old sheet with 后台计算, except we will later reset 日单量录入 and formulas/example. for ws in wb.worksheets: if ws.title == "后台计算": continue for row in ws.iter_rows(): for cell in row: if isinstance(cell.value, str) and cell.value.startswith("=") and "'预测计算'!" in cell.value: cell.value = cell.value.replace("'预测计算'!", "'后台计算'!") # Create front-facing prediction sheet before backend idx = wb.sheetnames.index("后台计算") ws_front = wb.create_sheet("预测计算", idx) max_row = old_calc.max_row headers = [ "日期", "星期", "实际单量", "预测单量", "误差率", "最终单量", "状态", "节假日", "电商活动", "波动提示", "基准预测", "相邻平滑基准", "实际延续修正", "综合修正系数", "清洗状态", "月份", "月度特点", "计算说明" ] style_title(ws_front, "预测计算|关键预测结果、相邻平滑与实际波动延续", len(headers)) ws_front.append(headers) style_header(ws_front, 2, len(headers)) # Fill formulas for r in range(3, max_row+1): ws_front.cell(r,1).value = f"='后台计算'!A{r}" ws_front.cell(r,2).value = f"='后台计算'!E{r}" ws_front.cell(r,3).value = f"='后台计算'!F{r}" ws_front.cell(r,4).value = f"='后台计算'!V{r}" ws_front.cell(r,5).value = f"='后台计算'!W{r}" ws_front.cell(r,6).value = f"='后台计算'!X{r}" ws_front.cell(r,7).value = f"='后台计算'!Y{r}" ws_front.cell(r,8).value = f'=IF(\'后台计算\'!K{r}<>"普通日",IF(\'后台计算\'!J{r}<>"",\'后台计算\'!J{r}&"|"&\'后台计算\'!K{r},\'后台计算\'!K{r}),"普通日")' ws_front.cell(r,9).value = f'=IF(\'后台计算\'!L{r}<>"",\'后台计算\'!L{r}&"|"&\'后台计算\'!M{r},"")' ws_front.cell(r,10).value = f'=IF(\'后台计算\'!AT{r}<>"",\'后台计算\'!AT{r},IF(\'后台计算\'!AL{r}<>"正常",\'后台计算\'!AL{r},IF(\'后台计算\'!AF{r}<>"正常",\'后台计算\'!AF{r},"")))' ws_front.cell(r,11).value = f"='后台计算'!AB{r}" ws_front.cell(r,12).value = f"='后台计算'!AP{r}" ws_front.cell(r,13).value = f"='后台计算'!AQ{r}" ws_front.cell(r,14).value = f"='后台计算'!AO{r}" ws_front.cell(r,15).value = f"='后台计算'!H{r}" ws_front.cell(r,16).value = f"=TEXT(A{r},\"yyyy-mm\")" ws_front.cell(r,17).value = f"='后台计算'!AS{r}" ws_front.cell(r,18).value = f"='后台计算'!AT{r}" apply_body_style(ws_front, 3, max_row, 1, len(headers)) ws_front.freeze_panes = "A3" ws_front.auto_filter.ref = f"A2:R{max_row}" set_widths(ws_front, { "A":12,"B":8,"C":12,"D":12,"E":10,"F":12,"G":10,"H":18,"I":18,"J":24, "K":12,"L":14,"M":14,"N":13,"O":12,"P":10,"Q":26,"R":28 }) # Number formats for r in range(3, max_row+1): ws_front.cell(r,1).number_format = "yyyy-mm-dd" for c in [3,4,6,11,12]: ws_front.cell(r,c).number_format = "0" ws_front.cell(r,5).number_format = "0.0%" ws_front.cell(r,13).number_format = "0.0%" ws_front.cell(r,14).number_format = "0.0%" # 2) Add new backend formulas/columns and modify prediction formula ws_back = wb["后台计算"] # Headers AP:AT (42-46) new_calc_headers = { 42: "相邻平滑基准", 43: "实际延续修正", 44: "最终预测输出", 45: "月度特点", 46: "计算说明" } for col, h in new_calc_headers.items(): ws_back.cell(2,col).value = h ws_back.cell(2,col).fill = PatternFill("solid", fgColor=orange) ws_back.cell(2,col).font = Font(bold=True, color="1F1F1F") ws_back.cell(2,col).alignment = Alignment(horizontal="center", vertical="center", wrap_text=True) ws_back.cell(2,col).border = border # Row 3 AP formula safe; rows 4+ use previous final quantity for r in range(3, max_row+1): if r == 3: ws_back.cell(r,42).value = f"=AB{r}" else: # Limit abrupt changes based on previous final quantity, especially month end/start. ws_back.cell(r,42).value = ( f'=IF(A{r}="","",IF(F{r}>0,AB{r},' f'IF(OR(DAY(A{r})=1,ABS(AB{r}/MAX(1,X{r-1})-1)>\'模型参数\'!$B$69),' f'ROUND(X{r-1}*(1+MAX(-\'模型参数\'!$B$69,MIN(\'模型参数\'!$B$69,AB{r}/MAX(1,X{r-1})-1))),0),' f'AB{r})))' ) ws_back.cell(r,43).value = ( f'=IF(OR(F{r}>0,A{r}<=\'模型参数\'!$B$4),0,' f'IF(A{r}-\'模型参数\'!$B$4>\'模型参数\'!$B$70,0,' f'IFERROR(MAX(-\'模型参数\'!$B$74,MIN(\'模型参数\'!$B$74,' f'(INDEX($F:$F,MATCH(\'模型参数\'!$B$4,$A:$A,0))/MAX(1,INDEX($V:$V,MATCH(\'模型参数\'!$B$4,$A:$A,0)))-1)' f'*\'模型参数\'!$B$71*POWER(\'模型参数\'!$B$72,A{r}-\'模型参数\'!$B$4-1))),0)))' ) ws_back.cell(r,44).value = f"=IFERROR(ROUND(AP{r}*AO{r}*(1+AQ{r}),0),0)" ws_back.cell(r,45).value = f'=IFERROR(VLOOKUP(MONTH(A{r}),\'月度特点分析\'!$A$3:$G$14,7,FALSE),"")' ws_back.cell(r,46).value = ( f'=IF(F{r}>0,"已录入:预测仅用于误差复盘",' f'IF(AQ{r}<>0,"未来预测:包含最近实际波动延续",' f'IF(ABS(AP{r}-AB{r})>1,"未来预测:已做相邻日期平滑","未来预测:按基准与滚动修正")))' ) # Modify system forecast and "修正后预测" to final output ws_back.cell(r,22).value = f"=IFERROR(AR{r},0)" ws_back.cell(r,30).value = f"=AR{r}" for c in range(42,47): ws_back.cell(r,c).border = border ws_back.cell(r,c).alignment = Alignment(vertical="center", wrap_text=True) ws_back.cell(r,c).font = Font(size=10) ws_back.column_dimensions["AP"].width = 15 ws_back.column_dimensions["AQ"].width = 15 ws_back.column_dimensions["AR"].width = 14 ws_back.column_dimensions["AS"].width = 24 ws_back.column_dimensions["AT"].width = 26 for r in range(3,max_row+1): ws_back.cell(r,42).number_format = "0" ws_back.cell(r,43).number_format = "0.0%" ws_back.cell(r,44).number_format = "0" # 3) Parameters adjustment and new section ws_param = wb["模型参数"] # Title perhaps update ws_param.cell(1,1).value = "模型参数|近期权重、误差自修正与相邻波动控制" # Reduce 2024 weight, increase 2025 per constraint 9 ws_param.cell(6,2).value = 0.2 ws_param.cell(6,3).value = "2024距离当前较远,仅低权重参考,避免过度拉动2026预测。" ws_param.cell(7,2).value = 0.8 ws_param.cell(7,3).value = "2025比2024更接近当前业务环境,作为主要历史同月参考。" start = 67 params_rows = [ ["相邻变化与实际波动延续参数(约束9)", None, None, None, None], ["参数项", "当前值", "说明", "是否建议手动调整", "公式/来源"], ["相邻日期最大变化率", round(min(max(p90_change,0.12),0.25), 4), "用于限制未来相邻日预测的突变,防止月末/月初断崖。", "低频", f"历史相邻日绝对变化率P90≈{p90_change:.1%}"], ["实际波动延续天数", 3, "当天实际暴增/暴跌后,只在未来几天内逐步延续,不长期放大。", "低频", "根据近邻数据影响通常递减"], ["实际偏差延续吸收比例", 0.55, "最新实际相对预测的偏差,按比例传导到后续预测。", "可低频调整", "约束9:实际单量对后几天有影响"], ["延续衰减比例", 0.65, "距离最新实际越远,延续影响越弱。", "低频", "第n天影响=首日影响×衰减^(n-1)"], ["单日波动判定阈值", 0.18, "实际较近7日均值偏离超过18%,视为明显波动。", "低频", "与约束7暴增/暴跌阈值保持一致"], ["实际延续修正上限", 0.25, "防止单日异常把未来预测拉得过高或压得过低。", "低频", "动态修正封顶"], ["跨月首尾检查阈值", round(min(max(p75_cross,0.08),0.18), 4), "用于观察月末/月初衔接是否异常,不强行按月目标断开。", "低频", f"历史跨月相邻绝对变化率P75≈{p75_cross:.1%}"], ["近期月份参考权重", 0.75, "预测解释上优先看最近几个月波动,2024仅作辅助。", "不建议频繁调整", "约束9:26年不过度参考24年"] ] for i,row in enumerate(params_rows, start): for c,v in enumerate(row,1): ws_param.cell(i,c).value = v # Style section for r in range(start, start+len(params_rows)): for c in range(1,6): cell = ws_param.cell(r,c) cell.border = border cell.alignment = Alignment(vertical="center", wrap_text=True) cell.font = Font(size=10) if r in [start, start+1]: for c in range(1,6): ws_param.cell(r,c).fill = PatternFill("solid", fgColor=orange if r==start+1 else deep_orange) ws_param.cell(r,c).font = Font(bold=True, color=white if r==start else "1F1F1F") ws_param.column_dimensions["A"].width = 26 ws_param.column_dimensions["B"].width = 16 ws_param.column_dimensions["C"].width = 58 ws_param.column_dimensions["D"].width = 18 ws_param.column_dimensions["E"].width = 42 # 4) Create monthly feature analysis sheet if "月度特点分析" in wb.sheetnames: del wb["月度特点分析"] # place after 月度分析 if present insert_idx = wb.sheetnames.index("月度分析") + 1 if "月度分析" in wb.sheetnames else len(wb.sheetnames) ws_m = wb.create_sheet("月度特点分析", insert_idx) headers_m = ["月序", "月份", "历史日均", "近6个月同月日均", "固定/常见事件", "建模侧重点", "预测衔接建议"] style_title(ws_m, "月度特点分析|节假日、活动与跨月衔接", len(headers_m)) ws_m.append(headers_m) style_header(ws_m, 2, len(headers_m)) for row in month_feature_rows: ws_m.append(row) # Add adjacent stats section row_stats = 17 ws_m.cell(row_stats,1).value = "相邻日期实际变化规律" ws_m.cell(row_stats,1).fill = PatternFill("solid", fgColor=deep_orange) ws_m.cell(row_stats,1).font = Font(bold=True, color=white) ws_m.merge_cells(start_row=row_stats, start_column=1, end_row=row_stats, end_column=7) stats_headers = ["指标", "样本数", "结果", "业务解释", "模型动作", "参考位置", "备注"] for c,h in enumerate(stats_headers,1): ws_m.cell(row_stats+1,c).value = h style_header(ws_m, row_stats+1, 7) stats_data = [ ["相邻日绝对变化率P50", len(abs_changes), p50_change, "一半相邻日期变化不超过该比例", "普通日预测应平稳", "模型参数B69", ""], ["相邻日绝对变化率P75", len(abs_changes), p75_change, "多数相邻日期变化不大", "作为异常跳变观察线", "月度特点分析", ""], ["相邻日绝对变化率P90", len(abs_changes), p90_change, "只有少数日期变化超过该比例", "设为预测突变上限", "模型参数B69", ""], ["跨月首尾绝对变化率P75", len(cross_abs), p75_cross, "大多数月末/月初变化不应断崖", "用于跨月检查", "模型参数B75", ""], ["暴增样本数", spike_count, spike_avg, "相对前7日均值大幅增长的历史次数", "触发实际延续修正", "后台计算AQ列", ""], ["暴增后第1天平均变化", spike_count, ret1_avg, "暴增后通常不会立刻完全回到基准", "未来1天吸收较高", "模型参数B71:B72", ""], ["暴增后第2天平均变化", spike_count, ret2_avg, "影响继续衰减", "未来2天递减吸收", "模型参数B72", ""], ["暴增后第3天平均变化", spike_count, ret3_avg, "影响进一步衰减", "3天后基本回归正常", "模型参数B70", ""], ] for i,row in enumerate(stats_data, row_stats+2): for c,v in enumerate(row,1): ws_m.cell(i,c).value = v ws_m.cell(i,c).border = border ws_m.cell(i,c).alignment = Alignment(vertical="center", wrap_text=True) ws_m.cell(i,c).font = Font(size=10) for r in range(3, 15): for c in range(1,8): ws_m.cell(r,c).border = border ws_m.cell(r,c).alignment = Alignment(vertical="center", wrap_text=True) ws_m.cell(r,c).font = Font(size=10) for r in range(row_stats+2, row_stats+2+len(stats_data)): if r in [row_stats+2, row_stats+3, row_stats+4, row_stats+5]: ws_m.cell(r,3).number_format = "0.0%" if r >= row_stats+6: ws_m.cell(r,3).number_format = "0.0%" set_widths(ws_m, {"A":18,"B":12,"C":16,"D":34,"E":30,"F":18,"G":28}) ws_m.freeze_panes = "A3" # 5) Update daily input formulas to front sheet and WPS-compatible hyperlinks ws_in = wb['日单量录入'] # Update title a bit ws_in.cell(1,1).value = "日单量录入|实际录入、预测误差与波动提示" # Header remains mostly; ensure clear naming headers_in = ["日期", "星期", "实际单量", "预测单量", "误差率", "误差等级", "状态", "最终单量", "本月滚动预测总量", "本月已录入", "节假日", "电商活动", "波动/修正提示"] for c,h in enumerate(headers_in,1): ws_in.cell(2,c).value = h style_header(ws_in, 2, len(headers_in)) for r in range(3, max_row+1): ws_in.cell(r,2).value = f"='预测计算'!B{r}" ws_in.cell(r,4).value = f"='预测计算'!D{r}" ws_in.cell(r,5).value = f"='预测计算'!E{r}" ws_in.cell(r,6).value = f'=IF(C{r}<=0,"未录入",IF(ABS(E{r})<=0.05,"A级≤5%",IF(ABS(E{r})<=0.1,"B级≤10%",IF(ABS(E{r})<=0.2,"C级≤20%",IF(ABS(E{r})<=0.35,"D级≤35%","E级>35%")))))' ws_in.cell(r,7).value = f"='预测计算'!G{r}" ws_in.cell(r,8).value = f"='预测计算'!F{r}" ws_in.cell(r,10).value = f"='后台计算'!T{r}" ws_in.cell(r,11).value = f"='预测计算'!H{r}" ws_in.cell(r,12).value = f"='预测计算'!I{r}" ws_in.cell(r,13).value = f"='预测计算'!J{r}" # WPS-compatible internal hyperlinks: use location, not external target for c in [1,4]: cell = ws_in.cell(r,c) cell.hyperlink = Hyperlink(ref=cell.coordinate, location=f"'预测计算'!A{r}", tooltip="跳转到预测计算对应日期") cell.style = "Hyperlink" # Optional hyperlink on front prediction date back to input for r in range(3, max_row+1): for c in [1,4]: cell = ws_front.cell(r,c) cell.hyperlink = Hyperlink(ref=cell.coordinate, location=f"'日单量录入'!A{r}", tooltip="返回日单量录入对应日期") cell.style = "Hyperlink" # Number formats and widths for r in range(3, max_row+1): ws_in.cell(r,1).number_format = "yyyy-mm-dd" ws_in.cell(r,5).number_format = "0.0%" for c in [3,4,8,9,10]: ws_in.cell(r,c).number_format = "0" set_widths(ws_in, {"A":12,"B":8,"C":12,"D":12,"E":10,"F":12,"G":10,"H":12,"I":16,"J":12,"K":20,"L":20,"M":30}) ws_in.freeze_panes = "A3" # WPS-compatible conditional formatting on error rate in input and front sheet for ws, rng in [(ws_in, f"E3:E{max_row}"), (ws_front, f"E3:E{max_row}")]: clear_conditional_formatting_range(ws) ws.conditional_formatting.add(rng, FormulaRule(formula=[f'AND(ISNUMBER(E3),ABS(E3)<=0.05)'], fill=PatternFill("solid", fgColor=green))) ws.conditional_formatting.add(rng, FormulaRule(formula=[f'AND(ISNUMBER(E3),ABS(E3)>0.05,ABS(E3)<=0.10)'], fill=PatternFill("solid", fgColor=yellow))) ws.conditional_formatting.add(rng, FormulaRule(formula=[f'AND(ISNUMBER(E3),ABS(E3)>0.10,ABS(E3)<=0.20)'], fill=PatternFill("solid", fgColor=light_orange))) ws.conditional_formatting.add(rng, FormulaRule(formula=[f'AND(ISNUMBER(E3),ABS(E3)>0.20)'], fill=PatternFill("solid", fgColor=red))) # 6) Update operations/instructions sheet ws_ops = wb["操作说明"] # Clear a defined area and rewrite concise usage instructions for row in ws_ops.iter_rows(min_row=1, max_row=20, min_col=1, max_col=10): for cell in row: cell.value = None cell.hyperlink = None style_title(ws_ops, "操作说明|WPS兼容、动态预测与日常录入", 10) ops_rows = [ ["使用场景", "操作", "系统会自动做什么", "查看位置", "备注"], ["每天结束后", "在“日单量录入”C列填写当天实际单量", "重新计算误差、波动提示,并只影响未来未录入日期", "日单量录入", "已录入日期不被后续实际改写"], ["查看未来每日预测", "点击录入表A列日期或D列预测单量", "跳转到“预测计算”同一天的预测明细", "预测计算", "使用WPS内部位置链接,不再打开外部文件"], ["发现当天暴增/暴跌", "先确认是否真实业务事件", "若是真实波动,后续1-3天会按衰减逻辑吸收", "预测计算J/M列", "避免预测连续偏低或偏高"], ["跨月查看", "重点查看月末最后一天和下月第一天", "系统限制非节假日/非大促下的断崖变化", "月度特点分析", "月度目标不直接压缩到每日"], ["复盘模型", "查看误差率颜色和“模型验证与建议”", "按A级/B级/C级等判断是否需要调参", "模型验证与建议", "颜色规则已按WPS兼容方式设置"] ] for r,row in enumerate(ops_rows,2): for c,v in enumerate(row,1): ws_ops.cell(r,c).value = v style_header(ws_ops, 2, 5) apply_body_style(ws_ops, 3, 7, 1, 5) set_widths(ws_ops, {"A":18,"B":34,"C":44,"D":18,"E":34}) # add real hyperlinks in "查看位置" cells links = {3:"'日单量录入'!A1",4:"'预测计算'!A1",5:"'预测计算'!A1",6:"'月度特点分析'!A1",7:"'模型验证与建议'!A1"} # May rename 验证与建议 below, hyperlink after rename. # 7) Rename validation sheet and update formulas/examples if "验证与建议" in wb.sheetnames: wb["验证与建议"].title = "模型验证与建议" elif "模型验证与建议" not in wb.sheetnames: ws_val = wb.create_sheet("模型验证与建议") ws_val = wb["模型验证与建议"] # Clear and rewrite below current used for row in ws_val.iter_rows(min_row=1, max_row=35, min_col=1, max_col=9): for cell in row: cell.value = None cell.hyperlink = None style_title(ws_val, "模型验证与建议|跨月衔接、实际延续与误差自修正", 9) val_rows = [ ["验证项", "计算/判断", "结果", "合理标准", "处理建议"], ["跨月首尾预测差异", "6月1日预测 vs 5月31日预测", '=IFERROR(INDEX(\'预测计算\'!$D:$D,MATCH(DATE(2026,6,1),\'预测计算\'!$A:$A,0))/INDEX(\'预测计算\'!$D:$D,MATCH(DATE(2026,5,31),\'预测计算\'!$A:$A,0))-1,"")', '=模型参数!B69以内更合理', "若非节假日/大促,超过上限需复核月度特点与活动标记"], ["相邻日历史变化P90", "基于全部实际相邻日期", "=模型参数!B69", "作为未来预测突变上限", "不建议随意放大"], ["跨月历史变化P75", "基于历史月末/月初实际", "=模型参数!B75", "作为月末/月初检查阈值", "若月初断崖,优先看节假日/大促是否存在"], ["最新实际延续修正", "最新实际相对预测的偏差×吸收比例×衰减", '=IFERROR(INDEX(\'预测计算\'!$M:$M,MATCH(模型参数!$B$4+1,\'预测计算\'!$A:$A,0)),"")', "仅未来1-3天明显", "当天真实暴增后,次日预测不应立即回到旧基准"], ["未来7天预测波动率", "未来7天最大/最小/均值", '=IFERROR((MAXIFS(\'预测计算\'!$D:$D,\'预测计算\'!$A:$A,">"&模型参数!$B$4,\'预测计算\'!$A:$A,"<="&模型参数!$B$4+7)-MINIFS(\'预测计算\'!$D:$D,\'预测计算\'!$A:$A,">"&模型参数!$B$4,\'预测计算\'!$A:$A,"<="&模型参数!$B$4+7))/AVERAGEIFS(\'预测计算\'!$D:$D,\'预测计算\'!$A:$A,">"&模型参数!$B$4,\'预测计算\'!$A:$A,"<="&模型参数!$B$4+7),"")', "应与近期实际波动相匹配", "若一直偏低/偏高,查看误差自修正参数"], ["误差颜色是否生效", "日单量录入E列/预测计算E列", "已按条件格式重建", "WPS打开后应显示绿/黄/橙/红", "若WPS未自动刷新,可按F9重算"] ] for r,row in enumerate(val_rows,2): for c,v in enumerate(row,1): ws_val.cell(r,c).value = v style_header(ws_val,2,5) apply_body_style(ws_val,3,2+len(val_rows)-1,1,5) set_widths(ws_val, {"A":24,"B":30,"C":24,"D":30,"E":50}) for r in range(3, 3+len(val_rows)-1): ws_val.cell(r,3).number_format = "0.0%" # style title already # 8) Update formula examples sheet if "公式与示例" in wb.sheetnames: wb["公式与示例"].title = "计算逻辑与示例" ws_ex = wb["计算逻辑与示例"] # Clear and rewrite for row in ws_ex.iter_rows(min_row=1, max_row=30, min_col=1, max_col=9): for cell in row: cell.value = None cell.hyperlink = None style_title(ws_ex, "计算逻辑与示例|公式、参数与业务场景", 8) ex_headers = ["模块", "业务场景", "输入参数示例", "公式文字展示", "可计算示例公式", "示例结果", "查看位置", "说明"] for c,h in enumerate(ex_headers,1): ws_ex.cell(2,c).value = h style_header(ws_ex,2,8) ex_rows = [ ["相邻平滑", "月末/月初目标差异较大,但实际不应断崖", "昨日最终=2400;原基准=1700;上限=21%", "平滑基准=昨日最终×(1±相邻变化上限)", "=ROUND(2400*(1-21%),0)", "=E3", "预测计算L列", "避免6月1日突然比5月31日低很多"], ["实际延续", "当天实际暴增,后面几天不能仍按旧基准", "实际=3000;预测=2400;吸收=55%;衰减=65%", "延续修正=(实际/预测-1)×吸收×衰减^(间隔-1)", "=(3000/2400-1)*55%*65%^0", "=E4", "预测计算M列", "第1天吸收最大,第2/3天递减"], ["综合预测", "未录入日期的最终预测", "平滑基准=2500;综合修正=1.06;延续=8%", "预测=平滑基准×综合修正×(1+延续修正)", "=ROUND(2500*1.06*(1+8%),0)", "=E5", "预测计算D列", "月总量不再硬性压缩每日预测"], ["误差颜色", "录入实际后快速判断偏差", "误差率=12%", "颜色规则:≤5%绿、≤10%黄、≤20%橙、>20%红", '=IF(12%<=5%,"绿",IF(12%<=10%,"黄",IF(12%<=20%,"橙","红")))', "=E6", "日单量录入E列", "使用WPS兼容条件格式"], ["跨月验证", "确认月末最后一天与下月第一天不异常", "5/31=2400;6/1=2250", "跨月变化率=6/1预测/5/31预测-1", "=2250/2400-1", "=E7", "模型验证与建议", "非节假日/大促时不应出现大幅断崖"], ["近期权重", "2026预测不过度参考2024", "2024权重=20%;2025权重=80%", "历史基准=2024同月×20%+2025同月×80%", "=100000*20%+120000*80%", "=E8", "模型参数B6:B7", "2024只作辅助参考"] ] for r,row in enumerate(ex_rows,3): for c,v in enumerate(row,1): ws_ex.cell(r,c).value = v apply_body_style(ws_ex,3,3+len(ex_rows)-1,1,8) set_widths(ws_ex, {"A":14,"B":32,"C":34,"D":42,"E":32,"F":12,"G":18,"H":40}) # Add internal links in location cells as actual hyperlinks location_map = { 3: "'预测计算'!L3", 4: "'预测计算'!M3", 5: "'预测计算'!D3", 6: "'日单量录入'!E3", 7: "'模型验证与建议'!A1", 8: "'模型参数'!B6" } for r, loc in location_map.items(): cell = ws_ex.cell(r,7) cell.hyperlink = Hyperlink(ref=cell.coordinate, location=loc, tooltip="跳转到对应位置") cell.style = "Hyperlink" # Fix operation sheet links after rename for row_idx, loc in links.items(): if "模型验证" in loc and "模型验证与建议" not in wb.sheetnames: continue cell = ws_ops.cell(row_idx,4) # Update loc if old validation sheet loc = loc.replace("'模型验证与建议'!A1", "'模型验证与建议'!A1") cell.hyperlink = Hyperlink(ref=cell.coordinate, location=loc, tooltip="跳转到对应工作表") cell.style = "Hyperlink" # 9) Update monthly prediction maybe reference backend formula replacements already done; update status wording maybe ws_month = wb["月度预测"] ws_month.cell(1,1).value = "月度预测|滚动月预测与未来日预测合计" # Apply replacements to any lingering old sheet references for ws in wb.worksheets: for row in ws.iter_rows(): for cell in row: if isinstance(cell.value, str) and cell.value.startswith("="): # In formulas, any old references intended for backend should be clear except in front views/input. Leave new references. pass # Update Operation links after ensuring locations exist # Add real hyperlink from every date/prediction row in front maybe already done. # 10) Formatting of new sheets and selected existing sheets for ws in [ws_in, ws_front, ws_back, ws_param, ws_m, ws_ops, ws_ex, ws_val]: # Ensure top rows styles consistent try: ws.sheet_view.showGridLines = False except Exception: pass # Make backend header & freeze ws_back.freeze_panes = "A3" ws_back.auto_filter.ref = f"A2:AT{max_row}" # Rearrange visibility by hiding less-used backend details? Keep important front sheet; backend details visible but can be scrolled. # Put important columns in front in new 预测计算, so backend can remain detailed. # add tab colors tab_colors = { "日单量录入": deep_orange, "预测计算": deep_orange, "后台计算": "A6A6A6", "月度特点分析": orange, "模型参数": orange, "计算逻辑与示例": orange, "模型验证与建议": orange } for s, color in tab_colors.items(): if s in wb.sheetnames: wb[s].sheet_properties.tabColor = color # Add comments for WPS hyperlink cells headers ws_in["A2"].comment = Comment("点击日期可跳转到“预测计算”同一天明细。已使用WPS兼容的内部位置链接。", "ChatGPT") ws_in["D2"].comment = Comment("点击预测单量可跳转到“预测计算”同一天明细。", "ChatGPT") ws_in["E2"].comment = Comment("误差率使用WPS兼容条件格式:绿≤5%,黄≤10%,橙≤20%,红>20%。", "ChatGPT") # Ensure workbook calculation mode try: wb.calculation.fullCalcOnLoad = True wb.calculation.forceFullCalc = True wb.calculation.calcMode = "auto" except Exception: pass # Save workbook wb.save(output_path) output_path
pythonfrom openpyxl import load_workbook from openpyxl.styles import PatternFill, Font, Alignment, Border, Side from openpyxl.formatting.rule import FormulaRule from openpyxl.worksheet.hyperlink import Hyperlink from openpyxl.comments import Comment from datetime import datetime, date, timedelta from collections import defaultdict import math, statistics, os input_path = "/mnt/data/仓储部单量预测系统_误差自修正版_约束7.xlsx" output_path = "/mnt/data/仓储部单量预测系统_约束9_WPS兼容动态预测版.xlsx" wb = load_workbook(input_path, data_only=False) orange = "F4B183"; deep_orange = "C65911"; light_orange = "FCE4D6" pale_orange = "FFF2CC"; green = "E2F0D9"; yellow = "FFF2CC"; red = "F4CCCC" white = "FFFFFF"; border_color = "D9A066" thin = Side(style="thin", color=border_color) border = Border(left=thin, right=thin, top=thin, bottom=thin) def unmerge_in_area(ws, min_row, max_row, min_col, max_col): for rng in list(ws.merged_cells.ranges): if not (rng.max_row < min_row or rng.min_row > max_row or rng.max_col < min_col or rng.min_col > max_col): ws.unmerge_cells(str(rng)) def clear_range(ws, min_row, max_row, min_col, max_col): unmerge_in_area(ws, min_row, max_row, min_col, max_col) for row in ws.iter_rows(min_row=min_row, max_row=max_row, min_col=min_col, max_col=max_col): for cell in row: cell.value = None cell.hyperlink = None cell.comment = None def style_title(ws, title, max_col): unmerge_in_area(ws, 1, 1, 1, max_col) ws.merge_cells(start_row=1, start_column=1, end_row=1, end_column=max_col) c = ws.cell(1,1) c.value = title c.fill = PatternFill("solid", fgColor=deep_orange) c.font = Font(bold=True, color=white, size=14) c.alignment = Alignment(horizontal="center", vertical="center") ws.row_dimensions[1].height = 28 def style_header(ws, row, max_col): for c in range(1, max_col+1): cell = ws.cell(row,c) cell.fill = PatternFill("solid", fgColor=orange) cell.font = Font(bold=True, color="1F1F1F", size=11) cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True) cell.border = border ws.row_dimensions[row].height = 26 def apply_body_style(ws, min_row, max_row, min_col, max_col): for row in ws.iter_rows(min_row=min_row, max_row=max_row, min_col=min_col, max_col=max_col): for cell in row: cell.border = border cell.alignment = Alignment(vertical="center", wrap_text=True) cell.font = Font(size=10, color="1F1F1F") for r in range(min_row, max_row+1): ws.row_dimensions[r].height = 20 def set_widths(ws, widths): for col, width in widths.items(): ws.column_dimensions[col].width = width def clear_cf(ws): try: ws.conditional_formatting._cf_rules.clear() except Exception: pass def percentile(vals, p): vals = sorted([float(v) for v in vals if v is not None]) if not vals: return None k = (len(vals)-1)*p f = math.floor(k); c = math.ceil(k) if f == c: return vals[int(k)] return vals[f]*(c-k)+vals[c]*(k-f) # Extract actual data from v7 base ws_in = wb['日单量录入'] data = [] for r in range(3, ws_in.max_row + 1): d = ws_in.cell(r,1).value v = ws_in.cell(r,3).value if isinstance(d, datetime): d = d.date() if isinstance(d, date) and isinstance(v, (int, float)) and v > 0: data.append((d, float(v))) changes=[]; cross=[] for i in range(1, len(data)): d0,v0 = data[i-1]; d1,v1 = data[i] if (d1-d0).days == 1 and v0 > 0: cr = v1/v0 - 1 changes.append((d1, cr)) if d1.day == 1: cross.append((d0,d1,cr,v0,v1)) abs_changes = [abs(x[1]) for x in changes] p50_change = percentile(abs_changes,0.5) or 0.07 p75_change = percentile(abs_changes,0.75) or 0.12 p90_change = percentile(abs_changes,0.90) or 0.21 cross_abs = [abs(x[2]) for x in cross] p75_cross = percentile(cross_abs,0.75) or 0.10 p90_cross = percentile(cross_abs,0.90) or 0.25 spikes=[] for i in range(7, len(data)-3): d,v = data[i] prev = [data[j][1] for j in range(i-7,i)] avg = sum(prev)/len(prev) if avg > 0 and v/avg - 1 > 0.18: spikes.append((d, v/avg-1, data[i+1][1]/v-1, data[i+2][1]/v-1, data[i+3][1]/v-1)) spike_count = len(spikes) spike_avg = statistics.mean([x[1] for x in spikes]) if spikes else 0 ret1_avg = statistics.mean([x[2] for x in spikes]) if spikes else 0 ret2_avg = statistics.mean([x[3] for x in spikes]) if spikes else 0 ret3_avg = statistics.mean([x[4] for x in spikes]) if spikes else 0 # Month features month_stats = defaultdict(list) for d,v in data: month_stats[d.month].append((d.year, v)) recent_cut = data[-1][0] - timedelta(days=180) recent_vals_by_month = defaultdict(list) for d,v in data: if d >= recent_cut: recent_vals_by_month[d.month].append(v) holidays_by_month = { 1:"元旦;部分年份春节临近,节前通常偏弱", 2:"春节/元宵影响最明显,节中低、节后逐步恢复", 3:"开学/春季换季需求,节假日扰动少", 4:"清明节;春季需求延续", 5:"劳动节;618预热通常从下旬开始", 6:"端午节;618核心期,活动扰动较强", 7:"暑期,节假日少,按常规星期与近期波动为主", 8:"暑期后段,开学前需求可能抬升", 9:"中秋/开学季,节前节后变化需关注", 10:"国庆长假;节前下降、节后恢复,双11预热可能开始", 11:"双11核心月,活动残差影响显著", 12:"双12/年末,活动后回落与年末节奏并存" } month_feature_rows=[] for m in range(1,13): vals = [v for y,v in month_stats[m]] recent_vals = recent_vals_by_month[m] all_avg = sum(vals)/len(vals) if vals else 0 recent_avg = sum(recent_vals)/len(recent_vals) if recent_vals else 0 month_feature_rows.append([ m, f"{m}月", round(all_avg,0) if all_avg else "", round(recent_avg,0) if recent_avg else "", holidays_by_month[m], "参考最近6个月同类波动;2024仅作低权重参考" if recent_avg else "近6个月同月样本不足,参考2025为主", "月初/月末不强行按月总量断开,按相邻日变化率平滑衔接" ]) # Rename old calculation sheet, update references, create new front-facing prediction sheet if "后台计算" in wb.sheetnames: del wb["后台计算"] old_calc = wb["预测计算"] old_calc.title = "后台计算" for ws in wb.worksheets: if ws.title == "后台计算": continue for row in ws.iter_rows(): for cell in row: if isinstance(cell.value, str) and cell.value.startswith("=") and "'预测计算'!" in cell.value: cell.value = cell.value.replace("'预测计算'!", "'后台计算'!") idx = wb.sheetnames.index("后台计算") ws_front = wb.create_sheet("预测计算", idx) max_row = old_calc.max_row headers = ["日期","星期","实际单量","预测单量","误差率","最终单量","状态","节假日","电商活动","波动提示","基准预测","相邻平滑基准","实际延续修正","综合修正系数","清洗状态","月份","月度特点","计算说明"] style_title(ws_front, "预测计算|关键预测结果、相邻平滑与实际波动延续", len(headers)) for c,h in enumerate(headers,1): ws_front.cell(2,c).value = h style_header(ws_front, 2, len(headers)) for r in range(3, max_row+1): ws_front.cell(r,1).value = f"='后台计算'!A{r}" ws_front.cell(r,2).value = f"='后台计算'!E{r}" ws_front.cell(r,3).value = f"='后台计算'!F{r}" ws_front.cell(r,4).value = f"='后台计算'!V{r}" ws_front.cell(r,5).value = f"='后台计算'!W{r}" ws_front.cell(r,6).value = f"='后台计算'!X{r}" ws_front.cell(r,7).value = f"='后台计算'!Y{r}" ws_front.cell(r,8).value = f'=IF(\'后台计算\'!K{r}<>"普通日",IF(\'后台计算\'!J{r}<>"",\'后台计算\'!J{r}&"|"&\'后台计算\'!K{r},\'后台计算\'!K{r}),"普通日")' ws_front.cell(r,9).value = f'=IF(\'后台计算\'!L{r}<>"",\'后台计算\'!L{r}&"|"&\'后台计算\'!M{r},"")' ws_front.cell(r,10).value = f'=IF(\'后台计算\'!AT{r}<>"",\'后台计算\'!AT{r},IF(\'后台计算\'!AL{r}<>"正常",\'后台计算\'!AL{r},IF(\'后台计算\'!AF{r}<>"正常",\'后台计算\'!AF{r},"")))' ws_front.cell(r,11).value = f"='后台计算'!AB{r}" ws_front.cell(r,12).value = f"='后台计算'!AP{r}" ws_front.cell(r,13).value = f"='后台计算'!AQ{r}" ws_front.cell(r,14).value = f"='后台计算'!AO{r}" ws_front.cell(r,15).value = f"='后台计算'!H{r}" ws_front.cell(r,16).value = f'=TEXT(A{r},"yyyy-mm")' ws_front.cell(r,17).value = f"='后台计算'!AS{r}" ws_front.cell(r,18).value = f"='后台计算'!AT{r}" apply_body_style(ws_front, 3, max_row, 1, len(headers)) ws_front.freeze_panes = "A3" ws_front.auto_filter.ref = f"A2:R{max_row}" set_widths(ws_front, {"A":12,"B":8,"C":12,"D":12,"E":10,"F":12,"G":10,"H":18,"I":18,"J":24,"K":12,"L":14,"M":14,"N":13,"O":12,"P":10,"Q":26,"R":28}) for r in range(3,max_row+1): ws_front.cell(r,1).number_format = "yyyy-mm-dd" for c in [3,4,6,11,12]: ws_front.cell(r,c).number_format = "0" ws_front.cell(r,5).number_format = "0.0%" ws_front.cell(r,13).number_format = "0.0%" ws_front.cell(r,14).number_format = "0.0%" # Backend new formulas ws_back = wb["后台计算"] new_calc_headers = {42:"相邻平滑基准",43:"实际延续修正",44:"最终预测输出",45:"月度特点",46:"计算说明"} for col,h in new_calc_headers.items(): c = ws_back.cell(2,col); c.value = h c.fill = PatternFill("solid", fgColor=orange); c.font = Font(bold=True, color="1F1F1F") c.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True); c.border = border for r in range(3, max_row+1): if r == 3: ws_back.cell(r,42).value = f"=AB{r}" else: ws_back.cell(r,42).value = ( f'=IF(A{r}="","",IF(F{r}>0,AB{r},' f'IF(OR(DAY(A{r})=1,ABS(AB{r}/MAX(1,X{r-1})-1)>\'模型参数\'!$B$69),' f'ROUND(X{r-1}*(1+MAX(-\'模型参数\'!$B$69,MIN(\'模型参数\'!$B$69,AB{r}/MAX(1,X{r-1})-1))),0),' f'AB{r})))' ) ws_back.cell(r,43).value = ( f'=IF(OR(F{r}>0,A{r}<=\'模型参数\'!$B$4),0,' f'IF(A{r}-\'模型参数\'!$B$4>\'模型参数\'!$B$70,0,' f'IFERROR(MAX(-\'模型参数\'!$B$74,MIN(\'模型参数\'!$B$74,' f'(INDEX($F:$F,MATCH(\'模型参数\'!$B$4,$A:$A,0))/MAX(1,INDEX($V:$V,MATCH(\'模型参数\'!$B$4,$A:$A,0)))-1)' f'*\'模型参数\'!$B$71*POWER(\'模型参数\'!$B$72,A{r}-\'模型参数\'!$B$4-1))),0)))' ) ws_back.cell(r,44).value = f"=IFERROR(ROUND(AP{r}*AO{r}*(1+AQ{r}),0),0)" ws_back.cell(r,45).value = f'=IFERROR(VLOOKUP(MONTH(A{r}),\'月度特点分析\'!$A$3:$G$14,7,FALSE),"")' ws_back.cell(r,46).value = ( f'=IF(F{r}>0,"已录入:预测仅用于误差复盘",' f'IF(AQ{r}<>0,"未来预测:包含最近实际波动延续",' f'IF(ABS(AP{r}-AB{r})>1,"未来预测:已做相邻日期平滑","未来预测:按基准与滚动修正")))' ) ws_back.cell(r,22).value = f"=IFERROR(AR{r},0)" ws_back.cell(r,30).value = f"=AR{r}" for c in range(42,47): ws_back.cell(r,c).border = border ws_back.cell(r,c).alignment = Alignment(vertical="center", wrap_text=True) ws_back.cell(r,c).font = Font(size=10) ws_back.freeze_panes = "A3" ws_back.auto_filter.ref = f"A2:AT{max_row}" for col,w in {"AP":15,"AQ":15,"AR":14,"AS":24,"AT":26}.items(): ws_back.column_dimensions[col].width = w for r in range(3,max_row+1): ws_back.cell(r,42).number_format = "0" ws_back.cell(r,43).number_format = "0.0%" ws_back.cell(r,44).number_format = "0" # Parameters ws_param = wb["模型参数"] ws_param.cell(1,1).value = "模型参数|近期权重、误差自修正与相邻波动控制" ws_param.cell(6,2).value = 0.2 ws_param.cell(6,3).value = "2024距离当前较远,仅低权重参考,避免过度拉动2026预测。" ws_param.cell(7,2).value = 0.8 ws_param.cell(7,3).value = "2025比2024更接近当前业务环境,作为主要历史同月参考。" start = 67 params_rows = [ ["相邻变化与实际波动延续参数(约束9)", None, None, None, None], ["参数项", "当前值", "说明", "是否建议手动调整", "公式/来源"], ["相邻日期最大变化率", round(min(max(p90_change,0.12),0.25), 4), "用于限制未来相邻日预测的突变,防止月末/月初断崖。", "低频", f"历史相邻日绝对变化率P90≈{p90_change:.1%}"], ["实际波动延续天数", 3, "当天实际暴增/暴跌后,只在未来几天内逐步延续,不长期放大。", "低频", "根据近邻数据影响通常递减"], ["实际偏差延续吸收比例", 0.55, "最新实际相对预测的偏差,按比例传导到后续预测。", "可低频调整", "约束9:实际单量对后几天有影响"], ["延续衰减比例", 0.65, "距离最新实际越远,延续影响越弱。", "低频", "第n天影响=首日影响×衰减^(n-1)"], ["单日波动判定阈值", 0.18, "实际较近7日均值偏离超过18%,视为明显波动。", "低频", "与约束7暴增/暴跌阈值保持一致"], ["实际延续修正上限", 0.25, "防止单日异常把未来预测拉得过高或压得过低。", "低频", "动态修正封顶"], ["跨月首尾检查阈值", round(min(max(p75_cross,0.08),0.18), 4), "用于观察月末/月初衔接是否异常,不强行按月目标断开。", "低频", f"历史跨月相邻绝对变化率P75≈{p75_cross:.1%}"], ["近期月份参考权重", 0.75, "预测解释上优先看最近几个月波动,2024仅作辅助。", "不建议频繁调整", "约束9:26年不过度参考24年"] ] for i,row in enumerate(params_rows, start): for c,v in enumerate(row,1): ws_param.cell(i,c).value = v ws_param.cell(i,c).border = border ws_param.cell(i,c).alignment = Alignment(vertical="center", wrap_text=True) ws_param.cell(i,c).font = Font(size=10) if i in [start, start+1]: for c in range(1,6): ws_param.cell(i,c).fill = PatternFill("solid", fgColor=deep_orange if i==start else orange) ws_param.cell(i,c).font = Font(bold=True, color=white if i==start else "1F1F1F") set_widths(ws_param, {"A":26,"B":16,"C":58,"D":18,"E":42}) # Monthly feature analysis if "月度特点分析" in wb.sheetnames: del wb["月度特点分析"] insert_idx = wb.sheetnames.index("月度分析") + 1 if "月度分析" in wb.sheetnames else len(wb.sheetnames) ws_m = wb.create_sheet("月度特点分析", insert_idx) headers_m = ["月序","月份","历史日均","近6个月同月日均","固定/常见事件","建模侧重点","预测衔接建议"] style_title(ws_m, "月度特点分析|节假日、活动与跨月衔接", len(headers_m)) for c,h in enumerate(headers_m,1): ws_m.cell(2,c).value = h style_header(ws_m,2,len(headers_m)) for i,row in enumerate(month_feature_rows,3): for c,v in enumerate(row,1): ws_m.cell(i,c).value = v for r in range(3,15): for c in range(1,8): ws_m.cell(r,c).border = border ws_m.cell(r,c).alignment = Alignment(vertical="center", wrap_text=True) ws_m.cell(r,c).font = Font(size=10) row_stats=17 ws_m.merge_cells(start_row=row_stats, start_column=1, end_row=row_stats, end_column=7) ws_m.cell(row_stats,1).value = "相邻日期实际变化规律" ws_m.cell(row_stats,1).fill = PatternFill("solid", fgColor=deep_orange) ws_m.cell(row_stats,1).font = Font(bold=True, color=white) stats_headers = ["指标","样本数","结果","业务解释","模型动作","参考位置","备注"] for c,h in enumerate(stats_headers,1): ws_m.cell(row_stats+1,c).value = h style_header(ws_m,row_stats+1,7) stats_data = [ ["相邻日绝对变化率P50", len(abs_changes), p50_change, "一半相邻日期变化不超过该比例", "普通日预测应平稳", "模型参数B69", ""], ["相邻日绝对变化率P75", len(abs_changes), p75_change, "多数相邻日期变化不大", "作为异常跳变观察线", "月度特点分析", ""], ["相邻日绝对变化率P90", len(abs_changes), p90_change, "只有少数日期变化超过该比例", "设为预测突变上限", "模型参数B69", ""], ["跨月首尾绝对变化率P75", len(cross_abs), p75_cross, "大多数月末/月初变化不应断崖", "用于跨月检查", "模型参数B75", ""], ["暴增样本数", spike_count, spike_avg, "相对前7日均值大幅增长的历史次数", "触发实际延续修正", "后台计算AQ列", ""], ["暴增后第1天平均变化", spike_count, ret1_avg, "暴增后通常不会立刻完全回到基准", "未来1天吸收较高", "模型参数B71:B72", ""], ["暴增后第2天平均变化", spike_count, ret2_avg, "影响继续衰减", "未来2天递减吸收", "模型参数B72", ""], ["暴增后第3天平均变化", spike_count, ret3_avg, "影响进一步衰减", "3天后基本回归正常", "模型参数B70", ""], ] for i,row in enumerate(stats_data,row_stats+2): for c,v in enumerate(row,1): ws_m.cell(i,c).value = v ws_m.cell(i,c).border = border ws_m.cell(i,c).alignment = Alignment(vertical="center", wrap_text=True) ws_m.cell(i,c).font = Font(size=10) ws_m.cell(i,3).number_format = "0.0%" set_widths(ws_m, {"A":18,"B":12,"C":16,"D":34,"E":30,"F":18,"G":28}) ws_m.freeze_panes = "A3" # Daily input formulas and hyperlinks ws_in = wb["日单量录入"] ws_in.cell(1,1).value = "日单量录入|实际录入、预测误差与波动提示" headers_in = ["日期","星期","实际单量","预测单量","误差率","误差等级","状态","最终单量","本月滚动预测总量","本月已录入","节假日","电商活动","波动/修正提示"] for c,h in enumerate(headers_in,1): ws_in.cell(2,c).value = h style_header(ws_in,2,len(headers_in)) for r in range(3,max_row+1): ws_in.cell(r,2).value = f"='预测计算'!B{r}" ws_in.cell(r,4).value = f"='预测计算'!D{r}" ws_in.cell(r,5).value = f"='预测计算'!E{r}" ws_in.cell(r,6).value = f'=IF(C{r}<=0,"未录入",IF(ABS(E{r})<=0.05,"A级≤5%",IF(ABS(E{r})<=0.1,"B级≤10%",IF(ABS(E{r})<=0.2,"C级≤20%",IF(ABS(E{r})<=0.35,"D级≤35%","E级>35%")))))' ws_in.cell(r,7).value = f"='预测计算'!G{r}" ws_in.cell(r,8).value = f"='预测计算'!F{r}" ws_in.cell(r,10).value = f"='后台计算'!T{r}" ws_in.cell(r,11).value = f"='预测计算'!H{r}" ws_in.cell(r,12).value = f"='预测计算'!I{r}" ws_in.cell(r,13).value = f"='预测计算'!J{r}" for c in [1,4]: cell = ws_in.cell(r,c) cell.hyperlink = Hyperlink(ref=cell.coordinate, location=f"'预测计算'!A{r}", tooltip="跳转到预测计算对应日期") cell.style = "Hyperlink" cell.border = border cell.alignment = Alignment(vertical="center") for r in range(3,max_row+1): for c in [1,4]: cell = ws_front.cell(r,c) cell.hyperlink = Hyperlink(ref=cell.coordinate, location=f"'日单量录入'!A{r}", tooltip="返回日单量录入对应日期") cell.style = "Hyperlink" for r in range(3,max_row+1): ws_in.cell(r,1).number_format = "yyyy-mm-dd" ws_in.cell(r,5).number_format = "0.0%" for c in [3,4,8,9,10]: ws_in.cell(r,c).number_format = "0" set_widths(ws_in, {"A":12,"B":8,"C":12,"D":12,"E":10,"F":12,"G":10,"H":12,"I":16,"J":12,"K":20,"L":20,"M":30}) ws_in.freeze_panes = "A3" # WPS-compatible conditional formatting on error rate for ws, rng in [(ws_in,f"E3:E{max_row}"), (ws_front,f"E3:E{max_row}")]: clear_cf(ws) ws.conditional_formatting.add(rng, FormulaRule(formula=['AND(ISNUMBER(E3),ABS(E3)<=0.05)'], fill=PatternFill("solid", fgColor=green))) ws.conditional_formatting.add(rng, FormulaRule(formula=['AND(ISNUMBER(E3),ABS(E3)>0.05,ABS(E3)<=0.10)'], fill=PatternFill("solid", fgColor=yellow))) ws.conditional_formatting.add(rng, FormulaRule(formula=['AND(ISNUMBER(E3),ABS(E3)>0.10,ABS(E3)<=0.20)'], fill=PatternFill("solid", fgColor=light_orange))) ws.conditional_formatting.add(rng, FormulaRule(formula=['AND(ISNUMBER(E3),ABS(E3)>0.20)'], fill=PatternFill("solid", fgColor=red))) # Rename validation sheet and formula examples if "验证与建议" in wb.sheetnames: wb["验证与建议"].title = "模型验证与建议" ws_val = wb["模型验证与建议"] clear_range(ws_val,1,35,1,9) style_title(ws_val, "模型验证与建议|跨月衔接、实际延续与误差自修正", 9) val_rows = [ ["验证项","计算/判断","结果","合理标准","处理建议"], ["跨月首尾预测差异","6月1日预测 vs 5月31日预测", '=IFERROR(INDEX(\'预测计算\'!$D:$D,MATCH(DATE(2026,6,1),\'预测计算\'!$A:$A,0))/INDEX(\'预测计算\'!$D:$D,MATCH(DATE(2026,5,31),\'预测计算\'!$A:$A,0))-1,"")', "模型参数B69以内更合理", "若非节假日/大促,超过上限需复核月度特点与活动标记"], ["相邻日历史变化P90","基于全部实际相邻日期", "=\'模型参数\'!B69", "作为未来预测突变上限", "不建议随意放大"], ["跨月历史变化P75","基于历史月末/月初实际", "=\'模型参数\'!B75", "作为月末/月初检查阈值", "若月初断崖,优先看节假日/大促是否存在"], ["最新实际延续修正","最新实际相对预测的偏差×吸收比例×衰减", '=IFERROR(INDEX(\'预测计算\'!$M:$M,MATCH(\'模型参数\'!$B$4+1,\'预测计算\'!$A:$A,0)),"")', "仅未来1-3天明显", "当天真实暴增后,次日预测不应立即回到旧基准"], ["未来7天预测波动率","未来7天最大/最小/均值", '=IFERROR((MAXIFS(\'预测计算\'!$D:$D,\'预测计算\'!$A:$A,">"&\'模型参数\'!$B$4,\'预测计算\'!$A:$A,"<="&\'模型参数\'!$B$4+7)-MINIFS(\'预测计算\'!$D:$D,\'预测计算\'!$A:$A,">"&\'模型参数\'!$B$4,\'预测计算\'!$A:$A,"<="&\'模型参数\'!$B$4+7))/AVERAGEIFS(\'预测计算\'!$D:$D,\'预测计算\'!$A:$A,">"&\'模型参数\'!$B$4,\'预测计算\'!$A:$A,"<="&\'模型参数\'!$B$4+7),"")', "应与近期实际波动相匹配", "若一直偏低/偏高,查看误差自修正参数"], ["误差颜色是否生效","日单量录入E列/预测计算E列", "已按条件格式重建", "WPS打开后应显示绿/黄/橙/红", "若WPS未自动刷新,可按F9重算"] ] for r,row in enumerate(val_rows,2): for c,v in enumerate(row,1): ws_val.cell(r,c).value = v style_header(ws_val,2,5) apply_body_style(ws_val,3,2+len(val_rows)-1,1,5) set_widths(ws_val, {"A":24,"B":30,"C":24,"D":30,"E":50}) for r in range(3, 3+len(val_rows)-1): ws_val.cell(r,3).number_format = "0.0%" if "公式与示例" in wb.sheetnames: wb["公式与示例"].title = "计算逻辑与示例" ws_ex = wb["计算逻辑与示例"] clear_range(ws_ex,1,35,1,9) style_title(ws_ex, "计算逻辑与示例|公式、参数与业务场景", 8) ex_headers = ["模块","业务场景","输入参数示例","公式文字展示","可计算示例公式","示例结果","查看位置","说明"] for c,h in enumerate(ex_headers,1): ws_ex.cell(2,c).value = h style_header(ws_ex,2,8) ex_rows = [ ["相邻平滑","月末/月初目标差异较大,但实际不应断崖","昨日最终=2400;原基准=1700;上限=21%","平滑基准=昨日最终×(1±相邻变化上限)","=ROUND(2400*(1-21%),0)","=E3","预测计算L列","避免6月1日突然比5月31日低很多"], ["实际延续","当天实际暴增,后面几天不能仍按旧基准","实际=3000;预测=2400;吸收=55%;衰减=65%","延续修正=(实际/预测-1)×吸收×衰减^(间隔-1)","=(3000/2400-1)*55%*65%^0","=E4","预测计算M列","第1天吸收最大,第2/3天递减"], ["综合预测","未录入日期的最终预测","平滑基准=2500;综合修正=1.06;延续=8%","预测=平滑基准×综合修正×(1+延续修正)","=ROUND(2500*1.06*(1+8%),0)","=E5","预测计算D列","月总量不再硬性压缩每日预测"], ["误差颜色","录入实际后快速判断偏差","误差率=12%","颜色规则:≤5%绿、≤10%黄、≤20%橙、>20%红",'=IF(12%<=5%,"绿",IF(12%<=10%,"黄",IF(12%<=20%,"橙","红")))',"=E6","日单量录入E列","使用WPS兼容条件格式"], ["跨月验证","确认月末最后一天与下月第一天不异常","5/31=2400;6/1=2250","跨月变化率=6/1预测/5/31预测-1","=2250/2400-1","=E7","模型验证与建议","非节假日/大促时不应出现大幅断崖"], ["近期权重","2026预测不过度参考2024","2024权重=20%;2025权重=80%","历史基准=2024同月×20%+2025同月×80%","=100000*20%+120000*80%","=E8","模型参数B6:B7","2024只作辅助参考"] ] for r,row in enumerate(ex_rows,3): for c,v in enumerate(row,1): ws_ex.cell(r,c).value = v apply_body_style(ws_ex,3,3+len(ex_rows)-1,1,8) set_widths(ws_ex, {"A":14,"B":32,"C":34,"D":42,"E":32,"F":12,"G":18,"H":40}) for r,loc in {3:"'预测计算'!L3",4:"'预测计算'!M3",5:"'预测计算'!D3",6:"'日单量录入'!E3",7:"'模型验证与建议'!A1",8:"'模型参数'!B6"}.items(): cell = ws_ex.cell(r,7) cell.hyperlink = Hyperlink(ref=cell.coordinate, location=loc, tooltip="跳转到对应位置") cell.style = "Hyperlink" # Operations sheet ws_ops = wb["操作说明"] clear_range(ws_ops,1,20,1,10) style_title(ws_ops, "操作说明|WPS兼容、动态预测与日常录入", 10) ops_rows = [ ["使用场景","操作","系统会自动做什么","查看位置","备注"], ["每天结束后","在“日单量录入”C列填写当天实际单量","重新计算误差、波动提示,并只影响未来未录入日期","日单量录入","已录入日期不被后续实际改写"], ["查看未来每日预测","点击录入表A列日期或D列预测单量","跳转到“预测计算”同一天的预测明细","预测计算","使用WPS内部位置链接,不再打开外部文件"], ["发现当天暴增/暴跌","先确认是否真实业务事件","若是真实波动,后续1-3天会按衰减逻辑吸收","预测计算","避免预测连续偏低或偏高"], ["跨月查看","重点查看月末最后一天和下月第一天","系统限制非节假日/非大促下的断崖变化","月度特点分析","月度目标不直接压缩到每日"], ["复盘模型","查看误差率颜色和“模型验证与建议”","按A级/B级/C级等判断是否需要调参","模型验证与建议","颜色规则已按WPS兼容方式设置"] ] for r,row in enumerate(ops_rows,2): for c,v in enumerate(row,1): ws_ops.cell(r,c).value = v style_header(ws_ops,2,5) apply_body_style(ws_ops,3,7,1,5) set_widths(ws_ops, {"A":18,"B":34,"C":44,"D":18,"E":34}) for row_idx,loc in {3:"'日单量录入'!A1",4:"'预测计算'!A1",5:"'预测计算'!A1",6:"'月度特点分析'!A1",7:"'模型验证与建议'!A1"}.items(): cell = ws_ops.cell(row_idx,4) cell.hyperlink = Hyperlink(ref=cell.coordinate, location=loc, tooltip="跳转到对应工作表") cell.style = "Hyperlink" # Month sheet title reference replacement done earlier; ensure name ws_month = wb["月度预测"] ws_month.cell(1,1).value = "月度预测|滚动月预测与未来日预测合计" # Comments for headers ws_in["A2"].comment = Comment("点击日期可跳转到“预测计算”同一天明细。已使用WPS兼容的内部位置链接。", "ChatGPT") ws_in["D2"].comment = Comment("点击预测单量可跳转到“预测计算”同一天明细。", "ChatGPT") ws_in["E2"].comment = Comment("误差率使用WPS兼容条件格式:绿≤5%,黄≤10%,橙≤20%,红>20%。", "ChatGPT") # Visual housekeeping for ws in [ws_in, ws_front, ws_back, ws_param, ws_m, ws_ops, ws_ex, ws_val]: ws.sheet_view.showGridLines = False tab_colors = { "日单量录入": deep_orange, "预测计算": deep_orange, "后台计算": "A6A6A6", "月度特点分析": orange, "模型参数": orange, "计算逻辑与示例": orange, "模型验证与建议": orange } for s,color in tab_colors.items(): if s in wb.sheetnames: wb[s].sheet_properties.tabColor = color # Make sheet order nicer: 操作说明, 日单量录入, 预测计算, 后台计算, 数据清洗, 月度预测, 月度分析, 月度特点分析, 因子分析... desired_order = ["操作说明","日单量录入","预测计算","后台计算","数据清洗","月度预测","月度分析","月度特点分析","因子分析","模型参数","节假日活动","计算逻辑与示例","模型验证与建议"] # reorder by moving sheets for target_index, name in enumerate([n for n in desired_order if n in wb.sheetnames]): ws = wb[name] current_index = wb._sheets.index(ws) wb._sheets.insert(target_index, wb._sheets.pop(current_index)) try: wb.calculation.fullCalcOnLoad = True wb.calculation.forceFullCalc = True wb.calculation.calcMode = "auto" except Exception: pass wb.save(output_path) print(output_path, os.path.getsize(output_path)) print("stats", len(data), p90_change, p75_cross, spike_count)
pythonfrom openpyxl import load_workbook out="/mnt/data/仓储部单量预测系统_约束9_WPS兼容动态预测版.xlsx" wbv=load_workbook(out, data_only=False) print(wbv.sheetnames) # Key formulas for s in ['日单量录入','预测计算','后台计算','模型参数','月度特点分析','模型验证与建议']: ws=wbv[s] print(s, ws.max_row, ws.max_column) print("input A3 hyperlink", wbv['日单量录入']['A3'].hyperlink.location if wbv['日单量录入']['A3'].hyperlink else None, wbv['日单量录入']['D3'].hyperlink.location if wbv['日单量录入']['D3'].hyperlink else None) print("front headers", [wbv['预测计算'].cell(2,c).value for c in range(1,19)]) for coord in ['D820','E820','L820','M820','N820']: print("front",coord, wbv['预测计算'][coord].value) for coord in ['V820','AP820','AQ820','AR820','AT820']: print("back",coord, wbv['后台计算'][coord].value) print("params B69:B75", [wbv['模型参数'].cell(r,2).value for r in range(69,76)]) # formula error text scan errors=[] for ws in wbv.worksheets: for row in ws.iter_rows(): for cell in row: v=cell.value if isinstance(v,str) and any(e in v for e in ['#REF!','#DIV/0!','#VALUE!','#NAME?','#N/A']): errors.append((ws.title, cell.coordinate, v[:200])) len(errors), errors[:5] (0, [])
pythonfrom openpyxl import load_workbook out="/mnt/data/仓储部单量预测系统_约束9_WPS兼容动态预测版.xlsx" wb=load_workbook(out, data_only=False) ws_back=wb["后台计算"] max_row=ws_back.max_row for r in range(3, max_row+1): ws_back.cell(r,43).value = ( f'=IF(OR(F{r}>0,A{r}<=\'模型参数\'!$B$4),0,' f'IF(A{r}-\'模型参数\'!$B$4>\'模型参数\'!$B$70,0,' f'IFERROR(MAX(-\'模型参数\'!$B$74,MIN(\'模型参数\'!$B$74,' f'(INDEX($F:$F,MATCH(\'模型参数\'!$B$4,$A:$A,0))/MAX(1,INDEX($AB:$AB,MATCH(\'模型参数\'!$B$4,$A:$A,0)))-1)' f'*\'模型参数\'!$B$71*POWER(\'模型参数\'!$B$72,A{r}-\'模型参数\'!$B$4-1))),0)))' ) wb.save(out) print("updated", out)
pythonfrom openpyxl import load_workbook out="/mnt/data/仓储部单量预测系统_约束9_WPS兼容动态预测版.xlsx" wbv=load_workbook(out, data_only=False) print(wbv['后台计算']['AQ820'].value) # scan formulas for old '预测计算' references in sheets other than input/front/examples/ops maybe refs=[] for ws in wbv.worksheets: for row in ws.iter_rows(): for cell in row: v=cell.value if isinstance(v,str) and "'预测计算'!" in v and ws.title not in ['日单量录入','预测计算','操作说明','计算逻辑与示例','模型验证与建议']: refs.append((ws.title,cell.coordinate,v[:100])) print("unexpected refs",len(refs),refs[:5]) errors=[] for ws in wbv.worksheets: for row in ws.iter_rows(): for cell in row: v=cell.value if isinstance(v,str) and any(e in v for e in ['#REF!','#DIV/0!','#VALUE!','#NAME?','#N/A']): errors.append((ws.title,cell.coordinate,v[:200])) print("errors",len(errors)) # Hyperlink checks print(wbv['日单量录入']['A820'].hyperlink.location, wbv['日单量录入']['D820'].hyperlink.location) # conditional format count print(len(wbv['日单量录入'].conditional_formatting._cf_rules), len(wbv['预测计算'].conditional_formatting._cf_rules))
pythonfor wsname in ['日单量录入','预测计算']: ws=wbv[wsname] print(wsname) for key, rules in ws.conditional_formatting._cf_rules.items(): print(key, len(rules), [r.formula for r in rules])
已按“约束9”处理,并明确弃用约束8版本,改用你重新上传的约束7版本表格作为基础继续调整。约束9中关于 WPS 跳转、误差颜色、预测计算列布局、跨月断崖、月度特点、近期数据权重、实际单量对后续几天影响等要求已落地。(约束9.txt)
下载文件:
仓储部单量预测系统_约束9_WPS兼容动态预测版.xlsx
主要调整:
修复 WPS 跳转问题
HYPERLINK() 公式。重新做 WPS 兼容误差颜色
20%:红色
重构预测计算表展示
修复跨月首尾断崖
新增实际波动延续修正
新增“月度特点分析”
降低 2024 年参考权重
公式与示例增强
自检结果:
#REF! / #DIV/0! / #VALUE! / #NAME? / #N/A 明显错误文本。