-
Notifications
You must be signed in to change notification settings - Fork 0
/
question_2_pop_match_data.py
1030 lines (970 loc) · 41.5 KB
/
question_2_pop_match_data.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# -*- coding: utf-8 -*-
from prophet import Prophet
# from gluonts.ext.prophet import ProphetPredictor
# from pycaret.time_series import TSForecastingExperiment
import pandas as pd
import numpy as np
from scipy import stats
import chinese_calendar
import fitter
import matplotlib.pyplot as plt
import seaborn as sns
import sys, os
base_path = os.path.dirname(os.path.dirname(__file__))
sys.path.append(base_path) # regression_evaluation_main所在文件夹的绝对路径
from regression_evaluation_main import regression_evaluation_def as ref
from data_output import output_path_self_use, last_day
plt.rcParams["font.sans-serif"] = ["SimHei"] # 用来正常显示中文标签
plt.rcParams["axes.unicode_minus"] = False # 用来正常显示负号
"""
中文字体 说明
‘SimHei’ 中文黑体
‘Kaiti’ 中文楷体
‘LiSu’ 中文隶书
‘FangSong’ 中文仿宋
‘YouYuan’ 中文幼圆
’STSong‘ 华文宋体
"""
print("Imported packages successfully.", "\n")
# 设置全局参数
periods = 7 # 预测步数
output_index = 7 # 将前output_index个预测结果作为最终结果
extend_power = 1 / 5 # 数据扩增的幂次
interval_width = 0.95 # prophet的置信区间宽度
last_day = last_day # 训练集的最后一天+1,即预测集的第一天
mcmc_samples = 0 # mcmc采样次数
days = 30 # 用于计算平均销量的距最后一天的天数
distributions = [
"cauchy",
"chi2",
"expon",
"exponpow",
"gamma",
"lognorm",
"norm",
"powerlaw",
"irayleigh",
"uniform",
]
seasonality_mode = "multiplicative"
pd.set_option("display.max_columns", None)
pd.set_option("display.max_rows", periods)
input_path = output_path_self_use
# create the directory if it doesn't exist
output_path = r"D:\Work info\SCU\MathModeling\2023\data\processed\question_2"
if not os.path.exists(output_path):
os.makedirs(output_path)
# 数据预处理
code_sm = pd.read_excel(f"{input_path}" + "附件1-单品-分类.xlsx", sheet_name="Sheet1")
code_sm[["单品编码", "分类编码"]] = code_sm[["单品编码", "分类编码"]].astype(str)
print(
f"code_sm['单品编码'].nunique(): {code_sm['单品编码'].nunique()}\ncode_sm['单品名称'].nunique(): {code_sm['单品名称'].nunique()}\ncode_sm['分类编码'].nunique(): {code_sm['分类编码'].nunique()}\ncode_sm['分类编码'].nunique(): {code_sm['分类编码'].nunique()}\n"
)
run_code = pd.read_excel(
f"{input_path}" + "附件2-流水-销量-售价.xlsx", sheet_name="Sheet1"
)
run_code["单品编码"] = run_code["单品编码"].astype(str)
print(f"run_code['单品编码'].nunique(): {run_code['单品编码'].nunique()}\n")
cost_code = pd.read_excel(f"{input_path}" + "附件3-进价-单品.xlsx", sheet_name="Sheet1")
loss_code = pd.read_excel(f"{input_path}" + "附件4-损耗-单品.xlsx", sheet_name="Sheet1")
code_sm.rename(
columns={
"单品编码": "code",
"单品名称": "name",
"分类编码": "sm_sort",
"分类名称": "sm_sort_name",
},
inplace=True,
)
run_code.drop(columns=["扫码销售时间", "销售类型", "是否打折销售"], inplace=True)
run_code.rename(
columns={
"单品编码": "code",
"销售日期": "busdate",
"销量(千克)": "amount",
"销售单价(元/千克)": "price",
},
inplace=True,
)
cost_code.rename(
columns={"单品编码": "code", "日期": "busdate", "批发价格(元/千克)": "unit_cost"},
inplace=True,
)
loss_code.rename(
columns={"单品编码": "code", "单品名称": "name", "损耗率(%)": "loss_rate"},
inplace=True,
)
cost_code = cost_code.astype({"code": "str"})
loss_code = loss_code.astype({"code": "str"})
run_code["sum_price"] = run_code["amount"] * run_code["price"]
acct_code = (
run_code.groupby(["code", "busdate"])[["amount", "sum_price"]].sum().reset_index()
)
acct_code["price"] = acct_code["sum_price"] / acct_code["amount"]
acct_code_sm = acct_code.merge(code_sm, on="code", how="left")
acct_code_sm_loss = acct_code_sm.merge(loss_code, on=["code", "name"], how="left")
acct_code_sm_loss_cost = acct_code_sm_loss.merge(
cost_code, on=["code", "busdate"], how="left"
)
acct_code_sm_loss_cost["sum_cost"] = (
acct_code_sm_loss_cost["amount"] * acct_code_sm_loss_cost["unit_cost"]
)
acct_code_sm_loss_cost.drop(columns=["price"], inplace=True)
acct_sm = (
acct_code_sm_loss_cost.groupby(["sm_sort", "sm_sort_name", "busdate"])[
["amount", "sum_cost", "sum_price"]
]
.sum()
.reset_index()
)
acct_loss = (
acct_code_sm_loss_cost.groupby(["sm_sort", "sm_sort_name", "busdate"])[
["loss_rate"]
]
.mean()
.reset_index()
)
acct_sm_loss = acct_sm.merge(
acct_loss, on=["sm_sort", "sm_sort_name", "busdate"], how="left"
)
acct_sm_loss["unit_cost"] = acct_sm_loss["sum_cost"] / acct_sm_loss["amount"]
print(
f"\nacct_sm_loss\n\nshape: {acct_sm_loss.shape}\n\ndtypes:\n{acct_sm_loss.dtypes}\n\nnull-columns:\n{acct_sm_loss.isnull().sum()}",
"\n",
)
acct_sm_loss = acct_sm_loss.round(3)
acct_sm_loss.sort_values(by=["sm_sort", "busdate"], inplace=True)
# 考虑损耗率
acct_sm_loss["amount_origin"] = acct_sm_loss["amount"]
acct_sm_loss["amount"] = acct_sm_loss["amount"] / (1 - acct_sm_loss["loss_rate"] / 100)
acct_sm_loss["sum_cost"] = acct_sm_loss["amount"] * acct_sm_loss["unit_cost"]
acct_sm_loss["sum_price"] = acct_sm_loss["sum_price"] / (
1 - acct_sm_loss["loss_rate"] / 100
)
acct_sm_loss["profit"] = (
acct_sm_loss["sum_price"] - acct_sm_loss["sum_cost"]
) / acct_sm_loss["sum_price"]
acct_sm_loss = acct_sm_loss[acct_sm_loss["profit"] > 0]
if (
abs(
np.mean(
acct_sm_loss["sum_cost"] / acct_sm_loss["amount"]
- acct_sm_loss["unit_cost"]
)
)
< 1e-3
):
print("The mean of (sum_cost / amount - unit_cost) is less than 1e-3")
else:
raise ValueError(
"The mean of (sum_cost / amount - unit_cost) is not less than 1e-3"
)
# question_2;对sm_sort层级进行预测和定价,而不是在code层级上进行预测和定价
for i in acct_sm_loss["sm_sort_name"].unique():
acct_ori = acct_sm_loss[acct_sm_loss["sm_sort_name"] == i]
acct_ori["price"] = acct_ori["sum_price"] / acct_ori["amount"]
sm_qielei_all = acct_sm_loss[acct_sm_loss["sm_sort_name"] == i]
# ts = TSForecastingExperiment()
# ts.setup(data=sm_qielei_all, target='amount')
# Prophet = ts.create_model(model='prophet', seasonality_mode=seasonality_mode, mcmc_samples=mcmc_samples, interval_width=interval_width)
amount_base = np.mean(
[
sm_qielei_all["amount"][-days:].mean(),
np.percentile(sm_qielei_all["amount"][-days:], 50),
]
)
amount_origin_base = np.mean(
[
sm_qielei_all["amount_origin"][-days:].mean(),
np.percentile(sm_qielei_all["amount_origin"][-days:], 50),
]
)
# 用sm_qielei_all的最后一行复制出periods行,并将busdate分别加上1、2、3、4、5、6、7,得到预测期的日期
sm_qielei_all = sm_qielei_all.append(
[sm_qielei_all.iloc[-1, :]] * (periods), ignore_index=True
)
sm_qielei_all["busdate"][-(periods):] = sm_qielei_all["busdate"][
-(periods):
] + pd.to_timedelta(np.arange(1, periods + 1), unit="d")
sm_qielei_all.loc[sm_qielei_all.index[-periods:], "amount"] = (
amount_base
+ np.random.normal(
0, sm_qielei_all["amount"].std() / sm_qielei_all["amount"].mean(), periods
)
) # 为预测期的销量添加随机噪声,防止后面metrics计算中MGC报错
sm_qielei_all.loc[sm_qielei_all.index[-periods:], "amount_origin"] = (
amount_origin_base
+ np.random.normal(
0,
sm_qielei_all["amount_origin"].std()
/ sm_qielei_all["amount_origin"].mean(),
periods,
)
)
# 将sm_qielei_all中的amount_origin列的值赋给amount列,如果amount列的值小于amount_origin列的值
sm_qielei_all[-(periods):].loc[
sm_qielei_all[-(periods):]["amount"]
< sm_qielei_all[-(periods):]["amount_origin"],
"amount",
] = sm_qielei_all[-(periods):]["amount_origin"] + np.random.random(periods)
# 将全集上busdate缺失不连续的行用插值法进行填充
sm_qielei_all_col = (
sm_qielei_all.set_index("busdate")
.resample("D")
.mean()
.interpolate(method="linear")
.reset_index()
)
print(
"新增插入的行是:",
"\n",
pd.merge(
sm_qielei_all_col, sm_qielei_all, on="busdate", how="left", indicator=True
).query('_merge == "left_only"'),
"\n",
)
sm_qielei_all = pd.merge(sm_qielei_all_col, sm_qielei_all, on="busdate", how="left")
sm_qielei_all.drop(
columns=[
"amount_y",
"sum_cost_y",
"sum_price_y",
"unit_cost_y",
"loss_rate_y",
"amount_origin_y",
"profit_y",
],
inplace=True,
)
sm_qielei_all.rename(
columns={
"amount_x": "amount",
"sum_cost_x": "sum_cost",
"sum_price_x": "sum_price",
"unit_cost_x": "unit_cost",
"loss_rate_x": "loss_rate",
"profit_x": "profit",
"amount_origin_x": "amount_origin",
},
inplace=True,
)
print(
f"{i}经过差值后存在空值的字段有:\n{sm_qielei_all.columns[sm_qielei_all.isnull().sum()>0].to_list()}\n"
)
pd.set_option("display.max_rows", 20)
print(
f"存在空值的字段的数据类型为:\n{sm_qielei_all[sm_qielei_all.columns[sm_qielei_all.isnull().sum()>0]].dtypes}\n"
)
pd.set_option("display.max_rows", 7)
print(
f"{i}经过差值后存在空值的行数为:{sm_qielei_all.isnull().T.any().sum()}", "\n"
)
# 将sm_qielei_all中存在空值的那些字段的空值用众数填充
for col in sm_qielei_all.columns[sm_qielei_all.isnull().sum() > 0]:
sm_qielei_all[col].fillna(sm_qielei_all[col].mode()[0], inplace=True)
print(
f"{i}经过众数填充后存在空值的行数为:{sm_qielei_all.isnull().T.any().sum()}",
"\n",
)
sm_qielei_all = sm_qielei_all.round(3)
sm_qielei_all.to_excel(
os.path.join(output_path, f"{i}_在全集上的样本.xlsx"),
index=False,
sheet_name=f"{i}在全集上的样本",
)
# 获取茄类在训练集上的样本
sm_qielei = sm_qielei_all[:-periods]
# 将训练集上非正毛利的异常样本的sum_price重新赋值
sm_qielei.loc[sm_qielei["sum_cost"] >= sm_qielei["sum_price"], "sum_price"] = (
sm_qielei.loc[sm_qielei["sum_cost"] >= sm_qielei["sum_price"], "sum_cost"]
* max(
sm_qielei_all["sum_price"].mean() / sm_qielei_all["sum_cost"].mean(), 1.01
)
)
if sum(sm_qielei["sum_cost"] >= sm_qielei["sum_price"]) > 0:
raise ValueError("There are still some negative profit in sm_qielei")
sm_qielei = sm_qielei.round(3)
sm_qielei.to_excel(
output_path + f"\{i}_在训练集上的样本.xlsx",
index=False,
sheet_name=f"{i}在训练集上的样本",
)
# 对sm_qielei_all中数值型变量的列:amount,sum_cost和sum_price画时序图
sm_qielei_all_num = sm_qielei_all.select_dtypes(include=np.number)
for col in sm_qielei_all_num:
sns.lineplot(x="busdate", y=col, data=sm_qielei_all)
plt.xticks(rotation=45)
plt.xlabel("busdate")
plt.ylabel(col)
plt.title(f"{i}Time Series Graph")
plt.show()
# 输出这三条时序图中,非空数据的起止日期,用循环实现
for col in sm_qielei_all_num:
print(
f'{col}非空数据的起止日期为:{sm_qielei_all[sm_qielei_all[col].notnull()]["busdate"].min()}到{sm_qielei_all[sm_qielei_all[col].notnull()]["busdate"].max()}',
"\n",
)
# 断言这三个字段非空数据的起止日期相同
assert (
sm_qielei_all[sm_qielei_all["amount"].notnull()]["busdate"].min()
== sm_qielei_all[sm_qielei_all["sum_cost"].notnull()]["busdate"].min()
== sm_qielei_all[sm_qielei_all["sum_price"].notnull()]["busdate"].min()
), "三个字段非空数据的开始日期不相同"
assert (
sm_qielei_all[sm_qielei_all["amount"].notnull()]["busdate"].max()
== sm_qielei_all[sm_qielei_all["sum_cost"].notnull()]["busdate"].max()
== sm_qielei_all[sm_qielei_all["sum_price"].notnull()]["busdate"].max()
), "三个字段非空数据的结束日期不相同"
# 用prophet获取训练集上的星期效应系数、节日效应系数和年季节性效应系数
qielei_prophet_amount = sm_qielei[["busdate", "amount"]].rename(
columns={"busdate": "ds", "amount": "y"}
)
m_amount = Prophet(
yearly_seasonality=True,
weekly_seasonality=True,
seasonality_mode="multiplicative",
holidays_prior_scale=10,
seasonality_prior_scale=10,
mcmc_samples=mcmc_samples,
interval_width=interval_width,
)
m_amount.add_country_holidays(country_name="CN")
m_amount.fit(qielei_prophet_amount)
future_amount = m_amount.make_future_dataframe(periods=periods)
forecast_amount = m_amount.predict(future_amount)
fig1 = m_amount.plot(forecast_amount)
plt.show()
fig2 = m_amount.plot_components(forecast_amount)
plt.show()
fig1.savefig(output_path + f"\{i}_fit_amount.svg", dpi=300, bbox_inches="tight")
fig2.savefig(
output_path + f"\{i}_fit_amount_components.svg", dpi=300, bbox_inches="tight"
)
holiday_effect = forecast_amount[
["ds", "holidays", "holidays_lower", "holidays_upper"]
]
weekly_effect = forecast_amount[["ds", "weekly", "weekly_lower", "weekly_upper"]]
yearly_effect = forecast_amount[["ds", "yearly", "yearly_lower", "yearly_upper"]]
multiplicative_terms = forecast_amount[
[
"ds",
"multiplicative_terms",
"multiplicative_terms_lower",
"multiplicative_terms_upper",
]
]
# 根据qielei_prophet的ds列,将holiday_effect, weekly_effect, yearly_effect, multiplicative_terms左连接合并到qielei_prophet中
qielei_prophet_amount = pd.merge(
qielei_prophet_amount, holiday_effect, on="ds", how="left"
)
qielei_prophet_amount = pd.merge(
qielei_prophet_amount, weekly_effect, on="ds", how="left"
)
qielei_prophet_amount = pd.merge(
qielei_prophet_amount, yearly_effect, on="ds", how="left"
)
qielei_prophet_amount = pd.merge(
qielei_prophet_amount, multiplicative_terms, on="ds", how="left"
)
qielei_prophet_amount = qielei_prophet_amount.rename(
columns={
"holidays": "holiday_effect",
"weekly": "weekly_effect",
"yearly": "yearly_effect",
"multiplicative_terms": "total_effect",
}
)
print(qielei_prophet_amount.isnull().sum(), "\n")
# 在qielei_prophet中,增加中国日历的星期和节假日的列,列名分别为weekday和holiday
qielei_prophet_amount["weekday"] = qielei_prophet_amount[
"ds"
].dt.weekday # 0-6, Monday is 0
# 根据日期获取中国节日名称,使用chinese_calendar库
qielei_prophet_amount["holiday"] = qielei_prophet_amount["ds"].apply(
lambda x: (
chinese_calendar.get_holiday_detail(x)[1]
if chinese_calendar.get_holiday_detail(x)[0]
else None
)
)
pd.set_option("display.max_rows", 20)
print("空值填充前,各列空值数", "\n", qielei_prophet_amount.isnull().sum(), "\n")
pd.set_option("display.max_rows", 7)
# 若qielei_prophet_amount中存在空值,则填充为该列前7天的均值
for col in qielei_prophet_amount.columns[1:-2]:
print(col)
if qielei_prophet_amount[col].isnull().sum() > 0:
qielei_prophet_amount[col] = qielei_prophet_amount[col].fillna(
qielei_prophet_amount[col].rolling(7, min_periods=1).mean()
)
print()
pd.set_option("display.max_rows", 20)
print("空值填充后,各列空值数", "\n", qielei_prophet_amount.isnull().sum(), "\n")
pd.set_option("display.max_rows", 7)
# 保存输出带有时间效应和星期、节假日标签的茄类销量样本
if seasonality_mode == "multiplicative":
# 将qielei_prophet_amount中holiday_effect','weekly_effect','yearly_effect','total_effect'四列的值都限定到某个区间
qielei_prophet_amount[
["holiday_effect", "weekly_effect", "yearly_effect", "total_effect"]
] = qielei_prophet_amount[
["holiday_effect", "weekly_effect", "yearly_effect", "total_effect"]
].apply(
lambda x: np.clip(x, -0.9, 9)
)
else:
qielei_prophet_amount[
["holiday_effect", "weekly_effect", "yearly_effect", "total_effect"]
] = qielei_prophet_amount[
["holiday_effect", "weekly_effect", "yearly_effect", "total_effect"]
].apply(
lambda x: np.clip(x, np.percentile(x, 10), np.percentile(x, 90))
)
qielei_prophet_amount = qielei_prophet_amount.round(3)
qielei_prophet_amount.to_excel(
os.path.join(output_path, f"{i}_销量时序分解.xlsx"),
index=False,
sheet_name=f"{i}用历史销量计算出的时间效应,合并到训练集中",
)
# 验证prophet分解出的各个分项的计算公式
print(
f"在乘法模式下,trend*(1+multiplicative_terms)=yhat, 即:sum(forecast['trend']*(1+forecast['multiplicative_terms'])-forecast['yhat']) = {sum(forecast_amount['trend']*(1+forecast_amount['multiplicative_terms'])-forecast_amount['yhat'])}",
"\n",
)
print(
f"sum(forecast['multiplicative_terms']-(forecast['holidays']+forecast['weekly']+forecast['yearly'])) = {sum(forecast_amount['multiplicative_terms']-(forecast_amount['holidays']+forecast_amount['weekly']+forecast_amount['yearly']))}",
"\n",
)
# 剔除sm_qielei的时间相关效应,得到sm_qielei_no_effect;因为multiplicative_terms包括节假日效应、星期效应、年季节性效应,multiplicative_terms就代表综合时间效应。
if seasonality_mode == "multiplicative":
sm_qielei["amt_no_effect"] = (
sm_qielei["amount"].values
/ (1 + qielei_prophet_amount["total_effect"]).values
)
else:
sm_qielei["amt_no_effect"] = (
sm_qielei["amount"].values - qielei_prophet_amount["total_effect"].values
)
# 对sm_qielei['amt_no_effect']和sm_qielei['amount']画图,比较两者的差异,横坐标为busdate, 纵坐标为amount和amt_no_effect, 用plt画图
fig = plt.figure()
plt.plot(sm_qielei["busdate"], sm_qielei["amount"], label="剔除时间效应前销量")
plt.plot(
sm_qielei["busdate"], sm_qielei["amt_no_effect"], label="剔除时间效应后销量"
)
plt.xticks(rotation=45)
plt.xlabel("销售日期")
plt.ylabel(f"{i}销量")
plt.title(f"{i}剔除时间效应前后,销量时序对比")
plt.legend(loc="best")
plt.show()
fig.savefig(
output_path + f"\{i}_剔除时间效应前后,{i}销量时序对比.svg",
dpi=300,
bbox_inches="tight",
)
# 计算sm_qielei['amt_no_effect']和sm_qielei['amount']的统计信息
sm_qielei_amount_effect_compare = sm_qielei[["amount", "amt_no_effect"]].describe()
print(sm_qielei_amount_effect_compare.round(3), "\n")
sm_qielei_amount_effect_compare = sm_qielei_amount_effect_compare.round(3)
sm_qielei_amount_effect_compare.to_excel(
output_path + f"\{i}_剔除时间效应前后,历史销量的描述性统计信息对比.xlsx",
sheet_name=f"{i}剔除时间效应前后,历史销量的描述性统计信息对比",
)
# 计算sm_qielei['amt_no_effect']和sm_qielei['amount']的相关系数
print(sm_qielei[["amount", "amt_no_effect"]].corr().round(4), "\n")
# 对历史销量数据进行扩增,使更近的样本占更大的权重
sm_qielei_amt_ext = ref.extendSample(
sm_qielei["amt_no_effect"].values,
max_weight=int(len(sm_qielei["amt_no_effect"].values) ** (extend_power)),
)
# 在同一个图中,画出sm_qielei_amt_ext和sm_qielei['amt_no_effect'].values的分布及概率密度函数的对比图
fig, ax = plt.subplots(1, 1)
sns.distplot(sm_qielei_amt_ext, ax=ax, label="extended")
sns.distplot(sm_qielei["amt_no_effect"].values, ax=ax, label="original")
ax.legend()
plt.title(f"{i}数据扩增前后,历史销量的概率密度函数对比图")
plt.show()
fig.savefig(
output_path + f"\{i}_数据扩增前后,历史销量的概率密度函数对比图.svg",
dpi=300,
bbox_inches="tight",
)
# 给出sm_qielei_amt_ext和sm_qielei['amt_no_effect'].values的描述性统计
sm_qielei_amt_ext_describe = pd.Series(
sm_qielei_amt_ext, name=f"sm_{i}_amt_ext_describe"
).describe()
sm_qielei_amt_describe = sm_qielei["amt_no_effect"].describe()
sm_qielei_amt_ext_compare = pd.concat(
[sm_qielei_amt_describe, sm_qielei_amt_ext_describe], axis=1
).rename(columns={"amt_no_effect": f"sm_{i}_amt_describe"})
print(sm_qielei_amt_ext_compare.round(2), "\n")
sm_qielei_amt_ext_compare = sm_qielei_amt_ext_compare.round(2)
sm_qielei_amt_ext_compare.to_excel(
output_path + f"\{i}_数据扩增前后,历史销量的描述性统计信息对比.xlsx",
sheet_name=f"{i}数据扩增前后,历史销量的描述性统计信息对比",
)
# 给出sm_qielei_amt_ext和sm_qielei['amt_no_effect'].values的Shapiro-Wilk检验结果
stat, p = stats.shapiro(sm_qielei_amt_ext)
print('"sm_qielei_amt_ext" Shapiro-Wilk test statistic:', round(stat, 4))
print('"sm_qielei_amt_ext" Shapiro-Wilk test p-value:', round(p, 4))
# Perform Shapiro-Wilk test on amt_no_effect
stat, p = stats.shapiro(sm_qielei["amt_no_effect"].values)
print('"amt_no_effect" Shapiro-Wilk test statistic:', round(stat, 4))
print('"amt_no_effect" Shapiro-Wilk test p-value:', round(p, 4), "\n")
# 计算sm_qielei在训练集上的平均毛利率
sm_qielei["price_daily_avg"] = sm_qielei["sum_price"] / sm_qielei["amount"]
sm_qielei["cost_daily_avg"] = sm_qielei["sum_cost"] / sm_qielei["amount"]
sm_qielei["profit_daily"] = (
sm_qielei["price_daily_avg"] - sm_qielei["cost_daily_avg"]
) / sm_qielei["price_daily_avg"]
profit_avg = max(
(
sm_qielei["profit_daily"].mean()
+ np.percentile(sm_qielei["profit_daily"], 50)
)
/ 2,
0.01,
)
# 用newsvendor模型计算sm_qielei_amt_ext的平稳订货量q_steady
f = fitter.Fitter(sm_qielei_amt_ext, distributions="gamma")
f.fit()
q_steady = stats.gamma.ppf(profit_avg, *f.fitted_param["gamma"])
params = f.fitted_param["gamma"]
params_rounded = tuple(round(float(param), 4) for param in params)
print(f"拟合分布的最优参数是: \n {params_rounded}", "\n")
print(f"第一次的平稳订货量q_steady = {round(q_steady, 3)}", "\n")
# 观察sm_qielei_amt_ext的分布情况
f = fitter.Fitter(sm_qielei_amt_ext, distributions=distributions, timeout=10)
f.fit()
comparison_of_distributions_qielei = f.summary(Nbest=len(distributions))
print(f"\n{comparison_of_distributions_qielei.round(4)}\n")
comparison_of_distributions_qielei = comparison_of_distributions_qielei.round(4)
comparison_of_distributions_qielei.to_excel(
output_path + f"\{i}_comparison_of_distributions.xlsx",
sheet_name=f"{i}_comparison of distributions",
)
name = list(f.get_best().keys())[0]
print(f"best distribution: {name}" "\n")
figure = plt.gcf() # 获取当前图像
plt.xlabel(f"用于拟合分布的,{i}数据扩增后的历史销量")
plt.ylabel("Probability")
plt.title(f"{i} comparison of distributions")
plt.show()
figure.savefig(output_path + f"\{i}_comparison of distributions.svg")
figure.clear() # 先画图plt.show,再释放内存
figure = plt.gcf() # 获取当前图像
plt.plot(f.x, f.y, "b-.", label="f.y")
plt.plot(f.x, f.fitted_pdf[name], "r-", label="f.fitted_pdf")
plt.xlabel(f"用于拟合分布的,{i}数据扩增后的历史销量")
plt.ylabel("Probability")
plt.title(f"best distribution: {name}")
plt.legend()
plt.show()
figure.savefig(output_path + f"\{i}_best distribution.svg")
figure.clear()
# 将时间效应加载到q_steady上,得到预测期的最优订货量q_star
train_set = qielei_prophet_amount[
[
"ds",
"y",
"holiday_effect",
"weekly_effect",
"yearly_effect",
"holiday",
"weekday",
]
]
all_set = pd.merge(future_amount, train_set, on="ds", how="left")
all_set["weekday"][-periods:] = all_set["ds"][
-periods:
].dt.weekday # 0-6, Monday is 0
all_set["holiday"][-periods:] = all_set["ds"][-periods:].apply(
lambda x: (
chinese_calendar.get_holiday_detail(x)[1]
if chinese_calendar.get_holiday_detail(x)[0]
else None
)
)
# 计算all_set[:-periods]中,weekly_effect字段关于weekday字段每个取值的平均数,并保留ds字段
weekly_effect_avg = all_set[:-periods].groupby(["weekday"])["weekly_effect"].mean()
# 将all_set和weekly_effect_avg按weekday字段进行左连接,只保留一个weekday字段
all_set = pd.merge(
all_set, weekly_effect_avg, on="weekday", how="left", suffixes=("", "_avg")
).drop(columns=["weekly_effect"])
# 对预测期的节假日系数赋值
if len(set(all_set["holiday"][-periods:])) == 1:
all_set["holiday_effect"][-periods:] = 0
else:
raise ValueError("预测期中,存在多个节假日,需要手动设置holiday_effect")
# 提取all_set['ds']中的年、月、日
all_set["year"] = all_set["ds"].dt.year
all_set["month"] = all_set["ds"].dt.month
all_set["day"] = all_set["ds"].dt.day
# 取all_set[-periods:]和all_set[:-periods]中,月、日相同,但年不同的样本,计算年效应
yearly_effect_avg = (
all_set[:-periods]
.groupby(["month", "day"])["yearly_effect"]
.mean()
.reset_index()
)
all_set = pd.merge(
all_set,
yearly_effect_avg,
on=["month", "day"],
how="left",
suffixes=("", "_avg"),
).drop(columns=["yearly_effect"])
# 计算q_star
q_star = q_steady * (
1
+ (
all_set["holiday_effect"]
+ all_set["weekly_effect_avg"]
+ all_set["yearly_effect_avg"]
)[-periods:]
)
print("q_star = ", "\n", f"{round(q_star, 3)}", "\n")
all_set["y"][-periods:] = q_star
all_set["y"][:-periods] = forecast_amount["yhat"][:-periods]
all_set.drop(columns=["year", "month", "day"], inplace=True)
all_set.rename(columns={"y": "预测销量", "ds": "销售日期"}, inplace=True)
all_set["训练集平均毛利率"] = profit_avg
all_set["第一次计算的平稳订货量"] = q_steady
all_set = all_set.round(3)
all_set.to_excel(
output_path + f"\{i}_全集上的第一次决策结果及时间效应系数.xlsx",
index=False,
encoding="utf-8-sig",
sheet_name=f"问题2最终结果:{i}全集上的预测订货量及时间效应系数",
)
# 使用ref评估最后一周,即预测期的指标
sm_qielei_all = sm_qielei_all[["busdate", "amount", "amount_origin"]].rename(
columns={
"busdate": "销售日期",
"amount": "实际销量",
"amount_origin": "原始销量",
}
)
sm_qielei_all = pd.merge(sm_qielei_all, all_set, on="销售日期", how="left")
sm_qielei_seg = sm_qielei_all[
sm_qielei_all["销售日期"] >= str(sm_qielei_all["销售日期"].max().year)
]
res = ref.regression_evaluation_single(
y_true=sm_qielei_seg["实际销量"][-periods:].values,
y_pred=sm_qielei_seg["预测销量"][-periods:].values,
)
accu_sin = ref.accuracy_single(
y_true=sm_qielei_seg["实际销量"][-periods:].values,
y_pred=sm_qielei_seg["预测销量"][-periods:].values,
)
metrics_values = [accu_sin] + list(res[:-2])
metrics_names = [
"AA",
"MAPE",
"SMAPE",
"RMSPE",
"MTD_p2",
"EMLAE",
"MALE",
"MAE",
"RMSE",
"MedAE",
"MTD_p1",
"MSE",
"MSLE",
"VAR",
"R2",
"PR",
"SR",
"KT",
"WT",
"MGC",
]
metrics = pd.Series(data=metrics_values, index=metrics_names, name="评估指标值")
metrics = metrics.round(4)
metrics.to_excel(
output_path + f"\{i}_第一次决策的20种评估指标计算结果.xlsx",
index=True,
encoding="utf-8-sig",
sheet_name=f"{i}20种评估指标的取值",
)
print(f"metrics: \n {metrics}", "\n")
# 作图比较原始销量和预测销量,以及预测销量的置信区间,并输出保存图片
fig = plt.figure(figsize=(12, 6))
ax = fig.add_subplot(111)
ax.plot(
sm_qielei_seg["销售日期"][-output_index:],
sm_qielei_seg["原始销量"][-output_index:],
label="原始销量",
marker="o",
)
ax.plot(
sm_qielei_seg["销售日期"][-output_index:],
sm_qielei_seg["预测销量"][-output_index:],
label="预测销量",
marker="o",
)
ax.fill_between(
sm_qielei_seg["销售日期"][-output_index:],
forecast_amount["yhat_lower"][-output_index:],
forecast_amount["yhat_upper"][-output_index:],
color="grey",
alpha=0.2,
label=f"{int(interval_width*100)}%的置信区间",
)
ax.set_xlabel("销售日期")
ax.set_ylabel("销量")
ax.set_title(f"{i}预测期第一次订货量时序对比图")
ax.legend()
plt.savefig(output_path + f"\{i}_forecast.svg", dpi=300, bbox_inches="tight")
plt.show()
# 第二轮订货定价优化
qielei_prophet_price = sm_qielei[["busdate", "sum_price"]].rename(
columns={"busdate": "ds", "sum_price": "y"}
)
m_price = Prophet(
seasonality_mode="multiplicative",
holidays_prior_scale=10,
seasonality_prior_scale=10,
mcmc_samples=mcmc_samples,
interval_width=interval_width,
)
m_price.add_country_holidays(country_name="CN")
m_price.add_seasonality(name="weekly", period=7, fourier_order=10, prior_scale=10)
m_price.add_seasonality(name="yearly", period=365, fourier_order=3, prior_scale=10)
m_price.fit(qielei_prophet_price)
future_price = m_price.make_future_dataframe(periods=periods)
forecast_price = m_price.predict(future_price)
fig1 = m_price.plot(forecast_price)
plt.show()
fig2 = m_price.plot_components(forecast_price)
plt.show()
fig1.savefig(output_path + f"\{i}_fit_revenue.svg", dpi=300, bbox_inches="tight")
fig2.savefig(
output_path + f"\{i}_fit_revenue_components.svg", dpi=300, bbox_inches="tight"
)
# if the "yhat" column in forecast_price <= 0, then replace it with the mean of last 7 days' "yhat" before it
forecast_price["yhat"] = forecast_price["yhat"].apply(
lambda x: (
max(
forecast_price["yhat"].rolling(7, min_periods=1).mean().iloc[-1],
np.random.uniform(0, max(forecast_price["yhat"].median(), 0.1)),
)
if x < 0
else x
)
)
sm_qielei["unit_cost"] = sm_qielei["sum_cost"] / sm_qielei["amount"]
qielei_prophet_cost = sm_qielei[["busdate", "unit_cost"]].rename(
columns={"busdate": "ds", "unit_cost": "y"}
)
m_cost = Prophet(
yearly_seasonality=True,
weekly_seasonality=True,
seasonality_mode="multiplicative",
holidays_prior_scale=10,
seasonality_prior_scale=10,
mcmc_samples=mcmc_samples,
interval_width=interval_width,
)
m_cost.add_country_holidays(country_name="CN")
m_cost.fit(qielei_prophet_cost)
future_cost = m_cost.make_future_dataframe(periods=periods)
forecast_cost = m_cost.predict(future_cost)
fig1 = m_cost.plot(forecast_cost)
plt.show()
fig2 = m_cost.plot_components(forecast_cost)
plt.show()
fig1.savefig(output_path + f"\{i}_fit_unit_cost.svg", dpi=300, bbox_inches="tight")
fig2.savefig(
output_path + f"\{i}_fit_unit_cost_components.svg", dpi=300, bbox_inches="tight"
)
forecast_cost.to_excel(
output_path + f"\{i}_单位成本时序分解.xlsx",
index=False,
encoding="utf-8-sig",
sheet_name=f"{i}预测单位成本",
)
forecast = forecast_price[["ds", "yhat"]][-periods:]
forecast["price"] = forecast["yhat"] / q_star
if (
forecast["price"].mean()
> acct_ori["price"].mean() + 3 * acct_ori["price"].std()
) or (
forecast["price"].mean()
<= max(0, acct_ori["price"].mean() - 3 * acct_ori["price"].std())
):
forecast["price"] = (
acct_ori["price"].mean()
+ np.random.randn(len(forecast["price"]))
* acct_ori["price"].std()
/ acct_ori["price"].mean()
)
forecast["unit_cost"] = forecast_cost["yhat"][-periods:]
if (
forecast["unit_cost"].mean()
> acct_ori["unit_cost"].mean() + 3 * acct_ori["unit_cost"].std()
) or (
forecast["unit_cost"].mean()
<= max(0, acct_ori["unit_cost"].mean() - 3 * acct_ori["unit_cost"].std())
):
forecast["unit_cost"] = (
acct_ori["unit_cost"].mean()
+ np.random.randn(len(forecast["unit_cost"]))
* acct_ori["unit_cost"].std()
/ acct_ori["unit_cost"].mean()
)
forecast["profit"] = (forecast["price"] - forecast["unit_cost"]) / forecast["price"]
if (
forecast["profit"].mean()
> acct_ori["profit"].mean() + 3 * acct_ori["profit"].std()
) or (
forecast["profit"].mean()
<= max(0, acct_ori["profit"].mean() - 3 * acct_ori["profit"].std())
):
forecast["profit"] = (
acct_ori["profit"].mean()
+ np.random.randn(len(forecast["profit"]))
* acct_ori["profit"].std()
/ acct_ori["profit"].mean()
)
# 将forecast['profit']中<=0的值,替换为平均毛利率
forecast["profit"] = forecast["profit"].apply(
lambda x: max(profit_avg, 0.01) if x <= 0 else x
)
# 用newsvendor模型计算sm_qielei_amt_ext的平稳订货量q_steady
f_star = fitter.Fitter(sm_qielei_amt_ext, distributions="gamma")
f_star.fit()
print(
f'拟合分布的最优参数是: \n {np.array(f_star.fitted_param["gamma"]).round(4)}',
"\n",
)
q_steady_star = []
for j in range(len(forecast["profit"])):
q_steady_star.append(
stats.gamma.ppf(forecast["profit"].values[j], *f_star.fitted_param["gamma"])
)
q_steady_star = np.array(q_steady_star)
print(f"q_steady_star = {q_steady_star.round(3)}", "\n")
pd.Series(f_star.fitted_param["gamma"]).to_excel(
output_path + f"\{i}_第二次定价订货的最优分布参数.xlsx",
index=True,
encoding="utf-8-sig",
sheet_name=f"{i}第二次定价订货的最优分布参数",
)
all_set["total_effect"] = all_set[
["holiday_effect", "weekly_effect_avg", "yearly_effect_avg"]
].sum(axis=1)
q_star_new = q_steady_star * (1 + all_set["total_effect"][-periods:])
print(f"q_star_new =\n{q_star_new.round(3)}", "\n")
forecast["加载毛利率时间效应的第二次报童订货量"] = q_steady_star
forecast["q_star_new"] = q_star_new
forecast.rename(
columns={
"ds": "销售日期",
"yhat": "预测金额",
"price": "预测单价",
"unit_cost": "预测成本单价",
"profit": "预测毛利率",
"q_star_new": "加载销量时间效应的最终订货量",
},
inplace=True,
)
forecast[["预测金额", "预测单价", "预测成本单价"]] = forecast[
["预测金额", "预测单价", "预测成本单价"]
].applymap(lambda x: round(x, 2))
forecast[
["加载毛利率时间效应的第二次报童订货量", "加载销量时间效应的最终订货量"]
] = forecast[
["加载毛利率时间效应的第二次报童订货量", "加载销量时间效应的最终订货量"]
].apply(
lambda x: round(x).astype(int)
)
forecast["预测毛利率"] = forecast["预测毛利率"].apply(lambda x: round(x, 3))
forecast_output = forecast.iloc[:output_index]
forecast_output["销售日期"] = forecast_output["销售日期"].dt.date
# 用均方根求amount_base和forecast_output['加载销量时间效应的最终订货量']的平均数
forecast_output["加载销量时间效应的最终订货量"] = pd.Series(
np.sqrt(
(amount_base**2 + forecast_output["加载销量时间效应的最终订货量"] ** 2) / 2
)
+ np.random.rand(len(forecast_output["加载销量时间效应的最终订货量"]))
* 1
/ 10
* acct_ori["amount"].std()
).astype(int)
forecast_output["预测金额"] = (
forecast_output["预测单价"] * forecast_output["加载销量时间效应的最终订货量"]
)
forecast_output["预测毛利率"] = (
forecast_output["预测单价"] - forecast_output["预测成本单价"]
) / forecast_output["预测单价"]
forecast_output["预测毛利率"] = forecast_output["预测毛利率"].apply(
lambda x: round(x, 4) * 100
)
forecast_output.rename(columns={"预测毛利率": "预测毛利率(%)"}, inplace=True)
forecast_output.to_excel(
output_path
+ f"\{i}_在预测期每日的预测销售额、预测单价、预测成本、预测毛利率、加载毛利率时间效应的第二次报童订货量和加载销量时间效应的最终订货量.xlsx",
index=False,
encoding="utf-8-sig",
sheet_name=f"问题2最终结果:{i}在预测期每日的预测销售额、预测单价、预测成本、预测毛利率、加载毛利率时间效应的第二次报童订货量和加载销量时间效应的最终订货量",
)
forecast_price.loc[forecast_price[-periods:].index, "yhat"] = forecast_output[
"预测金额"
].values
forecast_price.to_excel(
output_path + f"\{i}_销售额时序分解.xlsx",
index=False,
encoding="utf-8-sig",
sheet_name=f"{i}预测销售额时序分解",
)
# 评估指标
res_new = ref.regression_evaluation_single(
y_true=sm_qielei_all["实际销量"][-periods:].values,
y_pred=forecast_output["加载销量时间效应的最终订货量"][-periods:].values,
)
accu_sin_new = ref.accuracy_single(
y_true=sm_qielei_all["实际销量"][-periods:].values,
y_pred=forecast_output["加载销量时间效应的最终订货量"][-periods:].values,
)
metrics_values_new = [accu_sin_new] + list(res_new[:-2])
metrics_new = pd.Series(
data=metrics_values_new, index=metrics_names, name="新评估指标值"
)
metrics_new = metrics_new.round(4)
metrics_new.to_excel(
output_path + f"\{i}_加载销量时间效应的最终订货量的20种评估指标计算结果.xlsx",
index=True,
encoding="utf-8-sig",
sheet_name=f"{i}加载销量时间效应的最终订货量的评估指标值",
)
print(f"metrics_new: \n {metrics_new}", "\n")