|
|
|
|
|
<template>
|
|
|
|
|
|
<div class="channel app-container">
|
|
|
|
|
|
<div class="handren">
|
|
|
|
|
|
<div class="left">
|
|
|
|
|
|
<el-button style="width: 13rem">季节指数预测法</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="right">
|
|
|
|
|
|
<el-button @click="dialogVisible = true" type="primary" id="brtn">任务背景与目标</el-button>
|
|
|
|
|
|
<popModel :showModel="dialogVisible" title="任务背景:" @closePop="dialogVisible = false">
|
|
|
|
|
|
<template v-slot:content>
|
|
|
|
|
|
<p style="font-size: 1rem">
|
|
|
|
|
|
电商产品采购需求预测的主要目的是确定未来一段时间内不同商品的需求量,以便企业能够合理安排采购计划,避免库存积压或缺货现象的发生,从而提高资金利用效率和客户满意度。
|
|
|
|
|
|
<br />
|
|
|
|
|
|
<br />
|
|
|
|
|
|
|
|
|
|
|
|
预测方法
|
|
|
|
|
|
<br />
|
|
|
|
|
|
|
|
|
|
|
|
定量预测法:
|
|
|
|
|
|
<br />
|
|
|
|
|
|
|
|
|
|
|
|
趋势型预测:利用线性回归法等数学模型,结合产品的生命周期管控,预测未来需求量的变化趋势。这种方法适用于需求呈现稳定增长或下降趋势的商品。
|
|
|
|
|
|
<br />
|
|
|
|
|
|
|
|
|
|
|
|
季节型预测:针对季节性商品,根据历史销售数据,结合季节性因子进行预测。这种方法需要考虑季节性波动对需求量的影响。
|
|
|
|
|
|
<br />
|
|
|
|
|
|
|
|
|
|
|
|
平滑型预测:对于需求相对平稳的商品,可以采用简单平均法或移动平均法等方法进行预测。
|
|
|
|
|
|
<br />
|
|
|
|
|
|
|
|
|
|
|
|
定性预测法:
|
|
|
|
|
|
<br />
|
|
|
|
|
|
|
|
|
|
|
|
主观判断法:依靠经验人士的意见、知识和直觉进行预测。这种方法适用于新品或历史数据相关性低的商品。
|
|
|
|
|
|
<br />
|
|
|
|
|
|
|
|
|
|
|
|
德尔菲法:通过多次征询专家意见,逐步收敛预测结果,提高预测的准确性。这种方法适用于需要综合考虑多种因素的复杂预测场景。
|
|
|
|
|
|
<br />
|
|
|
|
|
|
|
|
|
|
|
|
机器学习预测法:
|
|
|
|
|
|
<br />
|
|
|
|
|
|
|
|
|
|
|
|
利用机器学习算法(如时间序列模型、树模型、深度学习模型等)对历史销售数据进行分析,挖掘数据中的规律和模式,从而预测未来需求量。这种方法适用于数据量较大、需求变化复杂的场景。
|
|
|
|
|
|
<br />
|
|
|
|
|
|
<br />
|
|
|
|
|
|
|
|
|
|
|
|
任务目标:
|
|
|
|
|
|
<br />
|
|
|
|
|
|
<br />
|
|
|
|
|
|
|
|
|
|
|
|
使用季节指数预测法,根据某电商平台过去两年的月度采购数据,预测未来一年的采购量。
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</popModel>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p style="font-size: 16px; font-weight: normal">季节指数预测法,亦称“温斯特法”,是根据时间序列中的数据资料所呈现的季节变动规律性,对预测目标未来状况作出预测的方法。它首先建立预测模型来求得历史上各期的趋势值,然后用实际值除以趋势值来求出季节指数。若是有两个以上的波动周期,还需要求出相同时期的季节指数平均值,再用它去修正预测值。</p>
|
|
|
|
|
|
<div class="caseBacktitle">
|
|
|
|
|
|
<img src="@/assets/images/步骤.png" alt="" />
|
|
|
|
|
|
<span>实验实训</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p>假设某电商平台店铺主营夏季产品,如泳装、泳帽、泳镜、游泳圈等产品,这类产品具有明显的季节波动性,公司需要预测未来12个月的采购需求,请使用季节指数预测法完成任务。</p>
|
|
|
|
|
|
<p>
|
|
|
|
|
|
应用步骤
|
|
|
|
|
|
<br />
|
|
|
|
|
|
数据收集:收集历史采购数据,确保数据覆盖多个完整的季节周期。
|
|
|
|
|
|
<br />
|
|
|
|
|
|
数据预处理:清理数据,处理缺失值和异常值。
|
|
|
|
|
|
<br />
|
|
|
|
|
|
计算移动平均值:计算每个时间点的移动平均值,以平滑数据。
|
|
|
|
|
|
<br />
|
|
|
|
|
|
计算季节指数:通过将实际值除以移动平均值,计算每个月份的季节指数。
|
|
|
|
|
|
<br />
|
|
|
|
|
|
预测未来需求:利用季节指数和趋势数据预测未来的采购需求。
|
|
|
|
|
|
<br />
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div class="online">
|
|
|
|
|
|
<span>1.数据准备</span>
|
|
|
|
|
|
<el-button type="primary" class="ml50" @click="downloadData">数据下载</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p>公司2022到2024年三年期间月度采购量如下所示:</p>
|
|
|
|
|
|
<el-table
|
|
|
|
|
|
:data="advertisementData"
|
|
|
|
|
|
border
|
|
|
|
|
|
style="width: 90%"
|
|
|
|
|
|
:header-cell-style="{
|
|
|
|
|
|
color: '#fff',
|
|
|
|
|
|
fontSize: '14px',
|
|
|
|
|
|
backgroundColor: '#6A7DE9!important',
|
|
|
|
|
|
}"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-table-column prop="year" label="年份" align="center"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="jan" label="1月" align="center"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="feb" label="2月" align="center"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="mar" label="3月" align="center"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="apr" label="4月" align="center"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="may" label="5月" align="center"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="jun" label="6月" align="center"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="jul" label="7月" align="center"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="aug" label="8月" align="center"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="sep" label="9月" align="center"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="oct" label="10月" align="center"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="nov" label="11月" align="center"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="dec" label="12月" align="center"></el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
<p>
|
|
|
|
|
|
数据特征说明:
|
|
|
|
|
|
<br />
|
|
|
|
|
|
季节性规律
|
|
|
|
|
|
<br />
|
|
|
|
|
|
旺季(6-8月):夏季需求高峰,采购量逐月攀升,7月达年度峰值(如2023年7月920件);
|
|
|
|
|
|
<br />
|
|
|
|
|
|
淡季(12-2月):冬季需求低迷,采购量显著低于其他月份(如2024年1月140件)。
|
|
|
|
|
|
<br />
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p>
|
|
|
|
|
|
年度增长趋势
|
|
|
|
|
|
<br />
|
|
|
|
|
|
每年采购量整体增长约 5%-10%,符合行业稳定扩张特征(如2023年6月较2022年增长8.3%);
|
|
|
|
|
|
<br />
|
|
|
|
|
|
冬季淡季增速较低(如2023年12月较2022年增长5.6%),夏季旺季增速较高(如2024年7月较2023年增长3.3%)。
|
|
|
|
|
|
<br />
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p>
|
|
|
|
|
|
过渡月份波动
|
|
|
|
|
|
<br />
|
|
|
|
|
|
春季(3-5月)和秋季(9-11月)为过渡期,采购量随气温变化逐步上升或下降(如4月采购量介于300-400件);
|
|
|
|
|
|
<br />
|
|
|
|
|
|
9月后需求快速回落,反映季节性商品换季特征(如2022年9月采购量环比下降33%)
|
|
|
|
|
|
<br />
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p>月度数据我们采用12期移动平均,计算简单移动平均值数据,计算结果取整:</p>
|
|
|
|
|
|
<el-table
|
|
|
|
|
|
:data="tableData"
|
|
|
|
|
|
border
|
|
|
|
|
|
style="width: 50%"
|
|
|
|
|
|
height="600"
|
|
|
|
|
|
:header-cell-style="{
|
|
|
|
|
|
color: '#fff',
|
|
|
|
|
|
fontSize: '14px',
|
|
|
|
|
|
backgroundColor: '#6A7DE9!important',
|
|
|
|
|
|
}"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-table-column prop="month" label="月份" align="center"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="actual" label="实际采购量" align="center"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="ma12" label="MA(12)12期移动平均值" align="center">
|
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
|
<el-input v-model="scope.row.ma12" placeholder=""></el-input>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
<p>计算季节指数,先用上述各月份实际值除以移动平均值,计算季节性不规则成分,然后按月份分组求均值,得出初版季节性指数,最后再用各月份初版季节性指数除以初版季节性指数的均值,得出标准化季节性指数,请在下表填写标准化季节性指数(结果保留3位小数):</p>
|
|
|
|
|
|
<el-table
|
|
|
|
|
|
:data="tableData1"
|
|
|
|
|
|
border
|
|
|
|
|
|
style="width: 50%"
|
|
|
|
|
|
:header-cell-style="{
|
|
|
|
|
|
color: '#fff',
|
|
|
|
|
|
fontSize: '14px',
|
|
|
|
|
|
backgroundColor: '#6A7DE9!important',
|
|
|
|
|
|
}"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-table-column prop="month" label="月份" align="center"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="index" label="标准化季节性指数" align="center">
|
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
|
<el-input v-model="scope.row.index" placeholder=""></el-input>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
<p>未来需求预测,使用线性回归模型预测2025年采购量:</p>
|
|
|
|
|
|
<el-table
|
|
|
|
|
|
:data="forecastData"
|
|
|
|
|
|
border
|
|
|
|
|
|
style="width: 50%"
|
|
|
|
|
|
:header-cell-style="{
|
|
|
|
|
|
color: '#fff',
|
|
|
|
|
|
fontSize: '14px',
|
|
|
|
|
|
backgroundColor: '#6A7DE9!important',
|
|
|
|
|
|
}"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-table-column prop="month" label="月份" align="center"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="forecast" label="预测采购量" align="center">
|
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
|
<el-input v-model="scope.row.forecast" placeholder=""></el-input>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
<p>
|
|
|
|
|
|
<el-button @click="drawForecastChart" type="primary">点击进行采购量预测可视化</el-button>
|
|
|
|
|
|
<el-button @click="runResultShow = true" type="primary">查看代码</el-button>
|
|
|
|
|
|
<el-button @click="go">跳转到Python在线编程环境</el-button>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div id="forecastChart" ref="forecastChart" style="width: 80%; height: 400px"></div>
|
|
|
|
|
|
<div class="taskfooter">
|
|
|
|
|
|
<el-button @click="submitTask">提交任务</el-button>
|
|
|
|
|
|
<el-button @click="resetTask">重新实训</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<pop-model :showModel="runResultShow" title="设置代码" @closePop="closeCode">
|
|
|
|
|
|
<template v-slot:content>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<codemirror :code="code"></codemirror>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</pop-model>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import * as analysisAPI from "@/api/market-demand-mining.js";
|
|
|
|
|
|
import * as echarts from "echarts";
|
|
|
|
|
|
import useUserStore from "@/store/modules/user";
|
|
|
|
|
|
const userStore = useUserStore();
|
|
|
|
|
|
const dialogVisible = ref(false);
|
|
|
|
|
|
import codemirror from "@/components/codemirror/index.vue";
|
|
|
|
|
|
import popModel from "@/views/components/popModal.vue";
|
|
|
|
|
|
const { proxy } = getCurrentInstance();
|
|
|
|
|
|
const runResultShow = ref(false);
|
|
|
|
|
|
const code = ` X
|
|
|
|
|
|
import pandas as pd
|
|
|
|
|
|
import numpy as np
|
|
|
|
|
|
import matplotlib.pyplot as plt
|
|
|
|
|
|
import matplotlib as mpl
|
|
|
|
|
|
from sklearn.linear_model import LinearRegression
|
|
|
|
|
|
mpl.rcParams['font.sans-serif'] = ['SimHei']#SimHeri是中文字体名称
|
|
|
|
|
|
mpl.rcParams['axes.unicode_minus'] = False
|
|
|
|
|
|
# ====================== 1. 数据准备 ======================
|
|
|
|
|
|
data = {
|
|
|
|
|
|
"Year": [2022]*12 + [2023]*12 + [2024]*12,
|
|
|
|
|
|
"Month": list(range(1,13)) * 3,
|
|
|
|
|
|
"Purchase": [
|
|
|
|
|
|
120, 150, 280, 350, 480, 720, 850, 800, 600, 400, 250, 180, # 2022
|
|
|
|
|
|
130, 160, 300, 380, 520, 780, 920, 870, 640, 430, 270, 190, # 2023
|
|
|
|
|
|
140, 170, 320, 400, 550, 830, 950, 900, 680, 460, 290, 200 # 2024
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
df = pd.DataFrame(data)
|
|
|
|
|
|
df["Date"] = pd.to_datetime(df["Year"].astype(str) + "-" + df["Month"].astype(str))
|
|
|
|
|
|
df = df.set_index("Date").sort_index()
|
|
|
|
|
|
# ====================== 2. 计算移动平均值(取整) ======================
|
|
|
|
|
|
# 计算移动平均(计算时保留小数,输出时取整)
|
|
|
|
|
|
df["MA12"] = df["Purchase"].rolling(window=12, center=True).mean()
|
|
|
|
|
|
# ====================== 3. 输出移动平均值(整数形式) ======================
|
|
|
|
|
|
print("历史数据与移动平均值(取整):")
|
|
|
|
|
|
valid_ma = df.dropna(subset=["MA12"]).copy()
|
|
|
|
|
|
# 对移动平均值四舍五入取整
|
|
|
|
|
|
valid_ma["MA12"] = valid_ma["MA12"].round().astype(int) # 关键修改点
|
|
|
|
|
|
print(valid_ma[["Purchase", "MA12"]])
|
|
|
|
|
|
# ====================== 4. 季节指数计算 ======================
|
|
|
|
|
|
# 计算时仍使用原始移动平均值保证精度
|
|
|
|
|
|
df["Seasonal_Irregular"] = df["Purchase"] / df["MA12"]
|
|
|
|
|
|
seasonal_index = df.groupby("Month")["Seasonal_Irregular"].mean()
|
|
|
|
|
|
seasonal_index = (seasonal_index / seasonal_index.mean()).round(3)
|
|
|
|
|
|
# ====================== 5. 趋势预测 ======================
|
|
|
|
|
|
trend_data = df.dropna(subset=["MA12"]).copy()
|
|
|
|
|
|
trend_data["Time"] = np.arange(len(trend_data))
|
|
|
|
|
|
model = LinearRegression()
|
|
|
|
|
|
model.fit(trend_data[["Time"]], trend_data["MA12"])
|
|
|
|
|
|
future_time = np.arange(len(trend_data), len(trend_data)+12).reshape(-1, 1)
|
|
|
|
|
|
future_trend = model.predict(future_time)
|
|
|
|
|
|
# ====================== 6. 预测采购量(取整数) ======================
|
|
|
|
|
|
future_dates = pd.date_range(start="2025-01-01", periods=12, freq="MS")
|
|
|
|
|
|
future_df = pd.DataFrame({"Date": future_dates})
|
|
|
|
|
|
future_df["Month"] = future_df["Date"].dt.month
|
|
|
|
|
|
future_df["Trend"] = future_trend.round(1) # 趋势值保留1位小数用于后续计算
|
|
|
|
|
|
future_df["Season_Index"] = future_df["Month"].map(seasonal_index)
|
|
|
|
|
|
future_df["Forecast"] = (future_df["Trend"] * future_df["Season_Index"]).round().astype(int)
|
|
|
|
|
|
# ====================== 7. 可视化与输出 ======================
|
|
|
|
|
|
plt.figure(figsize=(14, 7))
|
|
|
|
|
|
ax = plt.gca()
|
|
|
|
|
|
# 绘制历史数据与趋势线
|
|
|
|
|
|
plt.plot(df.index, df["Purchase"], label="历史采购量", marker="o", linestyle="-", markersize=6)
|
|
|
|
|
|
plt.plot(trend_data.index, trend_data["MA12"], label="移动平均趋势", linestyle="--", color="darkgreen")
|
|
|
|
|
|
# 绘制预测数据点并添加数值标签
|
|
|
|
|
|
forecast_line = plt.plot(future_df["Date"], future_df["Forecast"],
|
|
|
|
|
|
label="2025预测值", color="red", marker="o", linestyle="-")
|
|
|
|
|
|
# 在每个预测点上添加数值标签
|
|
|
|
|
|
for date, value in zip(future_df["Date"], future_df["Forecast"]):
|
|
|
|
|
|
ax.text(date, value + 20, f'{value}', # 数值显示在点上方20单位
|
|
|
|
|
|
ha='center', va='bottom', fontsize=9, color='red',
|
|
|
|
|
|
bbox=dict(facecolor='white', edgecolor='none', alpha=0.8)) # 白色背景框
|
|
|
|
|
|
# 坐标轴与图例设置
|
|
|
|
|
|
ax.xaxis.set_major_locator(mdates.MonthLocator(interval=3)) # 每3个月显示一个刻度
|
|
|
|
|
|
ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m'))
|
|
|
|
|
|
plt.xticks(rotation=45)
|
|
|
|
|
|
plt.title("采购量预测可视化", fontsize=14)
|
|
|
|
|
|
plt.xlabel("日期", fontsize=12)
|
|
|
|
|
|
plt.ylabel("采购量(件)", fontsize=12)
|
|
|
|
|
|
plt.grid(True, linestyle='--', alpha=0.5)
|
|
|
|
|
|
plt.legend(loc='upper left')
|
|
|
|
|
|
plt.tight_layout()
|
|
|
|
|
|
plt.show()
|
|
|
|
|
|
print("\n季节指数(保留3位小数):")
|
|
|
|
|
|
print(seasonal_index)
|
|
|
|
|
|
print("\n2025年预测采购量(整数):")
|
|
|
|
|
|
print(future_df[["Date", "Forecast"]].set_index("Date"))`;
|
|
|
|
|
|
const advertisementData = [
|
|
|
|
|
|
{
|
|
|
|
|
|
year: 2022,
|
|
|
|
|
|
jan: 120,
|
|
|
|
|
|
feb: 150,
|
|
|
|
|
|
mar: 280,
|
|
|
|
|
|
apr: 350,
|
|
|
|
|
|
may: 480,
|
|
|
|
|
|
jun: 720,
|
|
|
|
|
|
jul: 850,
|
|
|
|
|
|
aug: 800,
|
|
|
|
|
|
sep: 600,
|
|
|
|
|
|
oct: 400,
|
|
|
|
|
|
nov: 250,
|
|
|
|
|
|
dec: 180,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
year: 2023,
|
|
|
|
|
|
jan: 130,
|
|
|
|
|
|
feb: 160,
|
|
|
|
|
|
mar: 300,
|
|
|
|
|
|
apr: 380,
|
|
|
|
|
|
may: 520,
|
|
|
|
|
|
jun: 780,
|
|
|
|
|
|
jul: 920,
|
|
|
|
|
|
aug: 870,
|
|
|
|
|
|
sep: 640,
|
|
|
|
|
|
oct: 430,
|
|
|
|
|
|
nov: 270,
|
|
|
|
|
|
dec: 190,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
year: 2024,
|
|
|
|
|
|
jan: 140,
|
|
|
|
|
|
feb: 170,
|
|
|
|
|
|
mar: 320,
|
|
|
|
|
|
apr: 400,
|
|
|
|
|
|
may: 550,
|
|
|
|
|
|
jun: 830,
|
|
|
|
|
|
jul: 950,
|
|
|
|
|
|
aug: 900,
|
|
|
|
|
|
sep: 680,
|
|
|
|
|
|
oct: 460,
|
|
|
|
|
|
nov: 290,
|
|
|
|
|
|
dec: 200,
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
const tableDatas = ref([
|
|
|
|
|
|
{ month: "2022年1月", actual: 120 },
|
|
|
|
|
|
{ month: "2022年2月", actual: 150 },
|
|
|
|
|
|
{ month: "2022年3月", actual: 280 },
|
|
|
|
|
|
{ month: "2022年4月", actual: 350 },
|
|
|
|
|
|
{ month: "2022年5月", actual: 480 },
|
|
|
|
|
|
{ month: "2022年6月", actual: 720 },
|
|
|
|
|
|
{ month: "2022年7月", actual: 850 },
|
|
|
|
|
|
{ month: "2022年8月", actual: 800 },
|
|
|
|
|
|
{ month: "2022年9月", actual: 600 },
|
|
|
|
|
|
{ month: "2022年10月", actual: 400 },
|
|
|
|
|
|
{ month: "2022年11月", actual: 250 },
|
|
|
|
|
|
{ month: "2022年12月", actual: 180 },
|
|
|
|
|
|
{ month: "2023年1月", actual: 130 },
|
|
|
|
|
|
{ month: "2023年2月", actual: 160 },
|
|
|
|
|
|
{ month: "2023年3月", actual: 300 },
|
|
|
|
|
|
{ month: "2023年4月", actual: 380 },
|
|
|
|
|
|
{ month: "2023年5月", actual: 520 },
|
|
|
|
|
|
{ month: "2023年6月", actual: 780 },
|
|
|
|
|
|
{ month: "2023年7月", actual: 920 },
|
|
|
|
|
|
{ month: "2023年8月", actual: 870 },
|
|
|
|
|
|
{ month: "2023年9月", actual: 640 },
|
|
|
|
|
|
{ month: "2023年10月", actual: 430 },
|
|
|
|
|
|
{ month: "2023年11月", actual: 270 },
|
|
|
|
|
|
{ month: "2023年12月", actual: 190 },
|
|
|
|
|
|
{ month: "2024年1月", actual: 140 },
|
|
|
|
|
|
{ month: "2024年2月", actual: 170 },
|
|
|
|
|
|
{ month: "2024年3月", actual: 320 },
|
|
|
|
|
|
{ month: "2024年4月", actual: 400 },
|
|
|
|
|
|
{ month: "2024年5月", actual: 550 },
|
|
|
|
|
|
{ month: "2024年6月", actual: 830 },
|
|
|
|
|
|
{ month: "2024年7月", actual: 950 },
|
|
|
|
|
|
{ month: "2024年8月", actual: 900 },
|
|
|
|
|
|
{ month: "2024年9月", actual: 680 },
|
|
|
|
|
|
{ month: "2024年10月", actual: 460 },
|
|
|
|
|
|
{ month: "2024年11月", actual: 290 },
|
|
|
|
|
|
{ month: "2024年12月", actual: 200 },
|
|
|
|
|
|
]);
|
|
|
|
|
|
const tableData = ref([
|
|
|
|
|
|
{ month: "2022年7月", actual: 850, ma12: null },
|
|
|
|
|
|
{ month: "2022年8月", actual: 800, ma12: null },
|
|
|
|
|
|
{ month: "2022年9月", actual: 600, ma12: null },
|
|
|
|
|
|
{ month: "2022年10月", actual: 400, ma12: null },
|
|
|
|
|
|
{ month: "2022年11月", actual: 250, ma12: null },
|
|
|
|
|
|
{ month: "2022年12月", actual: 180, ma12: null },
|
|
|
|
|
|
{ month: "2023年1月", actual: 130, ma12: null },
|
|
|
|
|
|
{ month: "2023年2月", actual: 160, ma12: null },
|
|
|
|
|
|
{ month: "2023年3月", actual: 300, ma12: null },
|
|
|
|
|
|
{ month: "2023年4月", actual: 380, ma12: null },
|
|
|
|
|
|
{ month: "2023年5月", actual: 520, ma12: null },
|
|
|
|
|
|
{ month: "2023年6月", actual: 780, ma12: null },
|
|
|
|
|
|
{ month: "2023年7月", actual: 920, ma12: null },
|
|
|
|
|
|
{ month: "2023年8月", actual: 870, ma12: null },
|
|
|
|
|
|
{ month: "2023年9月", actual: 640, ma12: null },
|
|
|
|
|
|
{ month: "2023年10月", actual: 430, ma12: null },
|
|
|
|
|
|
{ month: "2023年11月", actual: 270, ma12: null },
|
|
|
|
|
|
{ month: "2023年12月", actual: 190, ma12: null },
|
|
|
|
|
|
{ month: "2024年1月", actual: 140, ma12: null },
|
|
|
|
|
|
{ month: "2024年2月", actual: 170, ma12: null },
|
|
|
|
|
|
{ month: "2024年3月", actual: 320, ma12: null },
|
|
|
|
|
|
{ month: "2024年4月", actual: 400, ma12: null },
|
|
|
|
|
|
{ month: "2024年5月", actual: 550, ma12: null },
|
|
|
|
|
|
{ month: "2024年6月", actual: 830, ma12: null },
|
|
|
|
|
|
{ month: "2024年7月", actual: 950, ma12: null },
|
|
|
|
|
|
]);
|
|
|
|
|
|
const tableData1 = ref([
|
|
|
|
|
|
{ month: "1月", index: null },
|
|
|
|
|
|
{ month: "2月", index: null },
|
|
|
|
|
|
{ month: "3月", index: null },
|
|
|
|
|
|
{ month: "4月", index: null },
|
|
|
|
|
|
{ month: "5月", index: null },
|
|
|
|
|
|
{ month: "6月", index: null },
|
|
|
|
|
|
{ month: "7月", index: null },
|
|
|
|
|
|
{ month: "8月", index: null },
|
|
|
|
|
|
{ month: "9月", index: null },
|
|
|
|
|
|
{ month: "10月", index: null },
|
|
|
|
|
|
{ month: "11月", index: null },
|
|
|
|
|
|
{ month: "12月", index: null },
|
|
|
|
|
|
]);
|
|
|
|
|
|
const forecastData = ref([
|
|
|
|
|
|
{ month: "2025年01月", forecast: "" },
|
|
|
|
|
|
{ month: "2025年02月", forecast: "" },
|
|
|
|
|
|
{ month: "2025年03月", forecast: "" },
|
|
|
|
|
|
{ month: "2025年04月", forecast: "" },
|
|
|
|
|
|
{ month: "2025年05月", forecast: "" },
|
|
|
|
|
|
{ month: "2025年06月", forecast: "" },
|
|
|
|
|
|
{ month: "2025年07月", forecast: "" },
|
|
|
|
|
|
{ month: "2025年08月", forecast: "" },
|
|
|
|
|
|
{ month: "2025年09月", forecast: "" },
|
|
|
|
|
|
{ month: "2025年10月", forecast: "" },
|
|
|
|
|
|
{ month: "2025年11月", forecast: "" },
|
|
|
|
|
|
{ month: "2025年12月", forecast: "" },
|
|
|
|
|
|
]);
|
|
|
|
|
|
// 生成完整的月份列表(包括 2024年空隙)
|
|
|
|
|
|
const getFullMonths = () => {
|
|
|
|
|
|
const months = [];
|
|
|
|
|
|
const startDate = new Date("2022-01-01");
|
|
|
|
|
|
const endDate = new Date("2025-12-01");
|
|
|
|
|
|
while (startDate <= endDate) {
|
|
|
|
|
|
months.push(`${startDate.getFullYear()}年${(startDate.getMonth() + 1).toString().padStart(2, "0")}月`);
|
|
|
|
|
|
startDate.setMonth(startDate.getMonth() + 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
return months;
|
|
|
|
|
|
};
|
|
|
|
|
|
/// 合并数据并生成时间轴
|
|
|
|
|
|
const chartData = computed(() => {
|
|
|
|
|
|
const fullMonths = getFullMonths();
|
|
|
|
|
|
const actualData = tableDatas.value.map((item) => item.actual);
|
|
|
|
|
|
const forecastDataArray = Array(fullMonths.length).fill(null);
|
|
|
|
|
|
forecastData.value.forEach((item, index) => {
|
|
|
|
|
|
const idx = fullMonths.indexOf(item.month);
|
|
|
|
|
|
if (idx !== -1) forecastDataArray[idx] = item.forecast;
|
|
|
|
|
|
});
|
|
|
|
|
|
const ma12Data = tableData.value.map((item) => item.ma12);
|
|
|
|
|
|
// ma12Data数组头部添加
|
|
|
|
|
|
ma12Data.unshift(null, null, null, null, null, null);
|
|
|
|
|
|
return {
|
|
|
|
|
|
actual: actualData,
|
|
|
|
|
|
forecast: forecastDataArray,
|
|
|
|
|
|
ma12: ma12Data,
|
|
|
|
|
|
months: fullMonths,
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
const drawForecastChart = () => {
|
|
|
|
|
|
let myChart = echarts.init(proxy.$refs.forecastChart);
|
|
|
|
|
|
const option = {
|
|
|
|
|
|
title: {
|
|
|
|
|
|
text: "采购量预测可视化",
|
|
|
|
|
|
left: "center",
|
|
|
|
|
|
textStyle: { fontSize: 16 },
|
|
|
|
|
|
},
|
|
|
|
|
|
tooltip: {
|
|
|
|
|
|
trigger: "axis",
|
|
|
|
|
|
formatter: function (params) {
|
|
|
|
|
|
let result = `${params[0].axisValue}<br/>`;
|
|
|
|
|
|
params.forEach((param) => {
|
|
|
|
|
|
result += `${param.marker} ${param.seriesName}: ${param.value || "N/A"}${param.seriesName === "指数" ? "" : ""}<br/>`;
|
|
|
|
|
|
});
|
|
|
|
|
|
return result;
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
legend: {
|
|
|
|
|
|
data: ["历史采购量", "移动平均趋势", "2025预测值"],
|
|
|
|
|
|
top: "8%",
|
|
|
|
|
|
// 下移一点点
|
|
|
|
|
|
},
|
|
|
|
|
|
grid: {
|
|
|
|
|
|
left: "3%",
|
|
|
|
|
|
right: "4%",
|
|
|
|
|
|
bottom: "1%",
|
|
|
|
|
|
containLabel: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
xAxis: {
|
|
|
|
|
|
type: "category",
|
|
|
|
|
|
data: chartData.value.months,
|
|
|
|
|
|
axisLabel: { interval: 0, rotate: 45 },
|
|
|
|
|
|
},
|
|
|
|
|
|
yAxis: [
|
|
|
|
|
|
{
|
|
|
|
|
|
type: "value",
|
|
|
|
|
|
name: "采购量(件)",
|
|
|
|
|
|
axisLabel: { formatter: "{value}" },
|
|
|
|
|
|
nameGap: 40,
|
|
|
|
|
|
min: 0,
|
|
|
|
|
|
nameLocation: "middle",
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
series: [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "历史采购量",
|
|
|
|
|
|
type: "line",
|
|
|
|
|
|
yAxisIndex: 0,
|
|
|
|
|
|
data: chartData.value.actual,
|
|
|
|
|
|
itemStyle: { color: "#5470c6" },
|
|
|
|
|
|
lineStyle: { width: 2 },
|
|
|
|
|
|
connectNulls: false, // 不连接空值
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "移动平均趋势",
|
|
|
|
|
|
type: "line",
|
|
|
|
|
|
yAxisIndex: 0,
|
|
|
|
|
|
data: chartData.value.ma12,
|
|
|
|
|
|
itemStyle: { color: "#91cc75" },
|
|
|
|
|
|
lineStyle: { type: "dashed", width: 2 },
|
|
|
|
|
|
connectNulls: false, // 不连接空值
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "2025预测值",
|
|
|
|
|
|
type: "line",
|
|
|
|
|
|
yAxisIndex: 0,
|
|
|
|
|
|
data: chartData.value.forecast,
|
|
|
|
|
|
itemStyle: { color: "#ee6666" },
|
|
|
|
|
|
lineStyle: { width: 2 },
|
|
|
|
|
|
connectNulls: false, // 不连接空值
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
};
|
|
|
|
|
|
myChart.setOption(option);
|
|
|
|
|
|
// 响应式调整
|
|
|
|
|
|
window.addEventListener("resize", () => myChart.resize());
|
|
|
|
|
|
};
|
|
|
|
|
|
// 下载数据
|
|
|
|
|
|
const downloadData = () => {
|
|
|
|
|
|
window.open(`${import.meta.env.VITE_APP_BASE_API}api/cgxqyc/exportCgxqycData`);
|
|
|
|
|
|
};
|
|
|
|
|
|
// 提交
|
|
|
|
|
|
const submitTask = () => {
|
|
|
|
|
|
// 获取monthlyData月度数据
|
|
|
|
|
|
const monthlyData = tableData.value.map((item) => item.ma12);
|
|
|
|
|
|
// 获取forecastData预测数据
|
|
|
|
|
|
const seasonalIndex = tableData1.value.map((item) => item.index);
|
|
|
|
|
|
// 调用接口
|
|
|
|
|
|
const demandForecast = forecastData.value.map((item) => item.forecast);
|
|
|
|
|
|
// 判断都填写完毕了没
|
|
|
|
|
|
if (monthlyData.includes(null) || seasonalIndex.includes(null) || demandForecast.includes(null)) {
|
|
|
|
|
|
proxy.$modal.msgWarning("请填写完整的数据");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
analysisAPI
|
|
|
|
|
|
.procurementDemandForecastSubmit({
|
|
|
|
|
|
monthlyData: JSON.stringify(monthlyData),
|
|
|
|
|
|
seasonalIndex: JSON.stringify(seasonalIndex),
|
|
|
|
|
|
demandForecast: JSON.stringify(demandForecast),
|
|
|
|
|
|
userId: userStore.userInfo.userId,
|
|
|
|
|
|
})
|
|
|
|
|
|
.then((res) => {
|
|
|
|
|
|
proxy.$modal.msgSuccess("提交成功");
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
// 回显
|
|
|
|
|
|
const echoData = () => {
|
|
|
|
|
|
analysisAPI.procurementDemandForecastEcho({ userId: userStore.userInfo.userId }).then((res) => {
|
|
|
|
|
|
if (!res.data) return;
|
|
|
|
|
|
const { monthlyData, seasonalIndex, demandForecast } = res.data;
|
|
|
|
|
|
JSON.parse(monthlyData).forEach((item, index) => {
|
|
|
|
|
|
tableData.value[index].ma12 = item;
|
|
|
|
|
|
forecastData.value[index].forecast = JSON.parse(demandForecast)[index];
|
|
|
|
|
|
});
|
|
|
|
|
|
JSON.parse(seasonalIndex).forEach((item, index) => {
|
|
|
|
|
|
tableData1.value[index].index = item;
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
// 重新实训
|
|
|
|
|
|
const resetTask = () => {
|
|
|
|
|
|
analysisAPI.procurementDemandForecastReTrain({ userId: userStore.userInfo.userId }).then((res) => {
|
|
|
|
|
|
// 清空对应数据
|
|
|
|
|
|
forecastData.value.forEach((item) => {
|
|
|
|
|
|
item.forecast = null;
|
|
|
|
|
|
});
|
|
|
|
|
|
tableData.value.forEach((item) => {
|
|
|
|
|
|
item.ma12 = null;
|
|
|
|
|
|
});
|
|
|
|
|
|
tableData1.value.forEach((item) => {
|
|
|
|
|
|
item.index = null;
|
|
|
|
|
|
});
|
|
|
|
|
|
proxy.$modal.msgSuccess("重新实训成功!");
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
const go = () => {
|
|
|
|
|
|
window.open("https://jrdsj.sztzjy.com:8000/user/zy84cf311/lab");
|
|
|
|
|
|
};
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
echoData();
|
|
|
|
|
|
});
|
|
|
|
|
|
</script>
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
|
.channel {
|
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|