ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

负载分析与磁盘容量预测:Python时间序列建模与分级告警实践

负载分析与磁盘容量预测:Python时间序列建模与分级告警实践

简介:面向具备Python与数据挖掘基础的学习者,这份源码包围绕应用系统负载分析与磁盘容量预测展开,提供一套可运行的建模练习材料,适合课程实验、算法验证与容量规划场景的入门实践。压缩包共6个文件,约20KB,包含4个xls数据表、1个py脚本和1个txt说明文件:表格分别承载原始数据、预处理结果与预测输入,脚本用于串联分析流程,说明文件则提示运行所需的导入模块,便于快速理解数据挖掘从数据处理到模型应用的完整链路。已有563人学习下载,说明该主题在运维分析与预测建模方向具有一定参考需求。读者可据此复现负载特征分析、磁盘容量趋势预测等核心环节,并基于现有脚本调整算法参数、替换数据集,积累从数据准备到结果解读的实操经验,也可作为相关课程作业或小型项目的参考底稿。

1. 从一次凌晨磁盘写满说起:负载分析与容量预测先要立住什么

应用系统负载分析与磁盘容量预测这套 Python 源码,起点是一个很具体的麻烦:某开发者维护的业务系统在凌晨被写满的磁盘拖垮,事后翻监控才发现,磁盘使用率那条曲线三天前就已经是一根很稳的斜线,只是没人盯得住。这份源码要做的事就是把“看曲线”换成“算天数”——采集 CPU、内存、磁盘 IO、连接数等负载指标,同时对磁盘使用量做时间序列建模,直接输出“按当前增速还剩多少天写满”,并在越过阈值时分级告警。

它适合两类人。一类是要交毕设或课程设计、需要一个从采集到预测到页面全部跑得通的完整链路的同学;另一类是手里已经有一堆监控 CSV 和日志、想快速搭个容量看板、同时又要能讲清每一步为什么这么做的运维从业者。整套东西用 Python 写,依赖不重,核心逻辑不到千行,改起来不心疼。

2. 需求与技术选型:指标口径怎么定、算法为什么只留两个

2.1 把“我要做负载分析”拆成能验收的需求

接到这类题目,很多人的第一反应是打开编辑器先写采集脚本,写到一半才发现不知道自己最终要产出什么,于是越写越发散。我一般会先把需求列成一张表,每一条都必须对应一个能被检查的结果,做不到的条目当场砍掉。

编号需求描述可验收的结果
R1定时采集主机与应用负载指标每分钟一条记录,落库后可按时间范围查询
R2计算负载画像输出均值、P95、峰值、峰值时刻、变异系数
R3预测磁盘使用量未来走势给出未来 72 小时预测序列与拟合优度
R4计算剩余可用天数输出 RDA 数值,与人工估算偏差在 20% 以内
R5分级预警三级阈值,每级生成结构化告警记录
R6可视化与导出一个 Web 页面加一份 CSV 或 HTML 报表

这张表看着简单,价值在于它决定了后面每个目录存在的理由。R2 对应analysis/profile.py,R3、R4 对应analysis/forecast.py,R6 对应web/。如果某条需求被砍掉,对应模块也能整块删掉,不会留下半死不活的代码。

答辩现场被问得最多的其实是 R4:“你这个剩余天数是怎么算的,准不准?”所以这条需求在设计阶段就必须自带验证方法,不能只交一个数字上去。通常的做法是留出最后一段历史数据做回测,把预测值和真实值放在一起比,误差写进验收表——这一步后面单独讲。

2.2 技术选型:为什么不上完整监控栈

一提容量监控,直觉反应是 Prometheus 加 Grafana 再加一堆 exporter。这套组合当然成熟,但对这个项目场景未必划算。我在几个类似项目里做过对比,结论是:交付周期紧、又要讲清算法细节的场合,轻量方案更稳。

方案采集能力算法扩展性上手成本可解释性
Prometheus + Grafana + exporter强,开箱即用弱,只能靠 PromQL 做简单外推高,要多组件部署中,告警规则好讲,预测不好讲
ELK 加自写脚本中,偏日志中高低,链路太长
本方案:Python 脚本 + SQLite + Flask + ECharts中,够用且可插拔强,换模型只改一个文件低,单机可跑高,每一步都能落到代码行

关键差别在“可解释性”这一列。课程设计或毕设的时间窗口通常只有八到十二周,真正写代码的时间不到一半;答辩现场要回答“预测怎么来的”,一个几十行的最小二乘拟合比一套调参黑匣子好讲太多。而从从业者角度看,如果公司里已经有 Prometheus,这套代码更合适的定位是“容量预测插件”——采集层换成 Prometheus 适配器,只保留分析和预测两层,一样能用。所以架构上把采集做成抽象基类、可插拔适配器,是个看起来很小题大做、实际很省事的设计。

2.3 指标口径与采样粒度

口径不统一是这类项目里最隐蔽的问题,两种单位混着存,跑出来的结论能差出一个数量级。所以指标表要在写代码之前定死。

指标来源存储单位聚合方式主要用途
cpu_pctpsutil.cpu_percent百分比分钟级均值负载画像
mem_pctpsutil.virtual_memory百分比分钟级均值负载画像
used_bytes / total_bytespsutil.disk_usage字节小时级取最后值容量预测
write_bytespsutil.disk_io_counters字节(累计值)先差分再求和写入速率估计
load1 / load5 / load15os.getloadavg无量纲分钟级均值负载画像
conn_cnt应用侧导出或连接池统计个分钟级取最后值负载画像

三条规则要记牢:容量类指标一律以字节入库,展示时再换算成人话;百分比只用于展示和阈值判断,不参与趋势拟合;write_bytes这类累计计数器必须先做差分,直接拿原始值画图会得到一根单调上升的直线,看着像“磁盘要爆了”,其实是计数器在涨。

采样粒度分开处理。磁盘增长是慢变量,用小时级就够,7 天也就 168 个点,小样本模型扛得住;CPU 抖动必须分钟级才看得见,否则 P95 会被抹平。同一套库里存两种粒度,粗粒度的表负责预测,细粒度的表负责画像,互不干扰。

2.4 预测算法候选与选型依据

候选算法不必求多,能讲清楚比堆模型重要。

算法原理优点短板
线性最小二乘拟合整体斜率可解释、输出直接是“每天涨多少”对周期性和异常点敏感
移动平均窗口平滑简单、抗噪无法外推,出不了 RDA
Holt-Winters趋势项加季节项指数平滑参数少、能出未来曲线样本少于两个周期时不稳定
ARIMA差分自回归理论完备定阶麻烦,参数解释成本高
Prophet分解式建模自动处理节假日依赖重,答辩不好讲

这套源码默认跑两个模型:线性回归负责给出 RDA 这个“人话指标”,Holt-Winters 负责给出未来 72 小时的预测曲线。两个结果都落库,展示层把两条线画在一张图上。差异大的时候不只是换个颜色,而是触发一次人工复核提示——差异本身就是在告诉你数据里可能藏着季节性或者异常点,这比单模型闷头出一个数是更负责的做法。

3. 总体架构设计:四层数据流与三张表的职责边界

3.1 四层数据流:从采集到告警的完整链路

整体是四层结构,层与层之间用数据表解耦,不做跨层函数调用。

采集层负责“把数弄进来”,抽象基类BaseCollector只暴露一个sample()方法,本机采集、CSV 回放、时序数据库拉取各写一个子类。这一层只写不读,永远不关心分析层怎么用。

存储层是一个 SQLite 单文件,两张明细表加一张结果表。选它而不是时序数据库的理由很实际:单文件可随源码打包,换机器拷过去就能跑,答辩现场不用现场装服务。

分析层是核心,顺序是clean→profile→forecast→alert。它只读明细表、只写结果表,重跑分析不需要重新采集数据。这条约束价值很大,模型调参的时候可以反复跑几十遍而不用等采集。

展示层是 Flask 加 ECharts 的单页面,只读结果表,页面里不出现任何拟合代码。渲染和计算分开之后,前端改样式不会碰坏模型。

数据流方向是单向的:采集层 → 明细表 → 分析层 → 结果表 → 展示层。任何一层挂掉,其他层还能独立运行,分析结果可以离线生成再拷过去展示。

3.2 目录结构与模块职责划分

capacity-forecast/ ├── config/ │ └── setting.yaml # 阈值、采样间隔、模型参数集中管理 ├── collector/ │ ├── base.py # 采集器抽象基类,统一 sample() 接口 │ ├── psutil_collector.py # 本机指标采集 │ └── csv_collector.py # 历史 CSV 回放,方便离线复现 ├── analysis/ │ ├── clean.py # 重采样、插值、异常点标记 │ ├── profile.py # 负载画像与综合评分 │ ├── forecast.py # 线性回归 / Holt-Winters / RDA │ └── alert.py # 分级预警与告警落库 ├── web/ │ ├── app.py # Flask 路由 │ └── templates/index.html # ECharts 页面 ├── storage/ │ └── schema.sql # 建表语句 └── main.py # 调度入口:采集 / 分析 / 服务

职责边界上有两条硬规矩。第一,analysis/下任何文件都不允许import collector,需要数据就查表,这样模型可以拿历史数据反复验证,也能拿别人给的数据集跑。第二,web/下任何文件都不允许import analysis,页面只查结果表,避免一次刷新触发一次全量拟合。这两条规矩听着教条,但正是它们让这套代码从“一次性脚本”变成“能交付的东西”。

3.3 数据表设计:三张表撑起全部分析

-- 磁盘容量明细:容量预测的唯一事实来源 CREATE TABLE IF NOT EXISTS disk_metric ( id INTEGER PRIMARY KEY AUTOINCREMENT, ts TEXT NOT NULL, -- ISO8601 且统一 UTC,禁止本地时间混入 mount TEXT NOT NULL, -- 挂载点或逻辑卷标识 total_bytes INTEGER NOT NULL, used_bytes INTEGER NOT NULL, used_percent REAL NOT NULL, write_bytes INTEGER DEFAULT 0 -- 累计计数器,使用前必须差分 ); CREATE INDEX IF NOT EXISTS idx_disk_ts ON disk_metric(mount, ts); -- 负载指标明细:分钟级,服务于画像而不是预测 CREATE TABLE IF NOT EXISTS load_metric ( id INTEGER PRIMARY KEY AUTOINCREMENT, ts TEXT NOT NULL, host TEXT NOT NULL, cpu_pct REAL, mem_pct REAL, load1 REAL, conn_cnt INTEGER ); -- 预测结果:只存结论,重算时按 mount + model 先删后插 CREATE TABLE IF NOT EXISTS forecast_result ( id INTEGER PRIMARY KEY AUTOINCREMENT, created_at TEXT NOT NULL, mount TEXT NOT NULL, model TEXT NOT NULL, -- linear / hw horizon_ts TEXT NOT NULL, predict_bytes INTEGER NOT NULL, rda_days REAL, r2 REAL );

几个字段的选择值得说明。ts存 ISO8601 字符串而不是整数时间戳,因为 SQLite 没有原生日期类型,字符串可以直接比较和排序,pandas 读进来to_datetime一步到位,省掉一次换算。write_bytes保留原始累计值而不是直接存速率,是因为差分的窗口长度在分析阶段才确定,入库时就差分等于把选择权提前锁死了。forecast_result单独一张表,是为了让“重算预测”变成一次幂等操作——先按mount + model删旧记录再插新的,页面上永远不会出现两条互相矛盾的预测线。

3.4 配置文件:把阈值和模型参数从代码里拿出来

collector: type: psutil # psutil | csv | prometheus,切换只改这一行 interval_sec: 60 storage: db_path: data/metrics.db forecast: freq: 1h # 重采样频率 freq_per_day: 24 # 必须与 freq 匹配,1h 对应 24 horizon_hours: 72 seasonal_periods: 24 # 小时级数据的一天长度 min_points: 72 # 少于此点数不出预测,避免小样本过拟合 min_r2: 0.70 # 拟合优度低于此值,RDA 只提示不告警 alert: levels: - { name: notice, used_percent: 75 } - { name: warning, used_percent: 85 } - { name: critical, used_percent: 92 } rda_warning_days: 30 # 按当前斜率不足 30 天写满,直接进 warning

min_points和min_r2是两个防呆参数。前者挡住“只有两天数据就敢预测三个月”的情况,后者挡住“数据抖成一团还硬报一个 RDA”的情况。这两个值不需要多精确,但它们让系统在数据不足时明确说“算不了”,而不是给个看起来很确定的错误答案——这一点在答辩时反而是加分项。

4. 核心模块实现:从分钟级负载聚合到剩余可用天数计算

4.1 采集与清洗:把原始指标变成等间隔时间序列

采集部分只做一件事:按固定间隔把一个字典写进表里。

# collector/psutil_collector.py import time, sqlite3, psutil from datetime import datetime, timezone def sample_disk(path="/data"): """采集单个挂载点的容量指标,容量一律以字节返回""" usage = psutil.disk_usage(path) io = psutil.disk_io_counters() return { # 统一 UTC,避免跨时区或夏令时切换时序列错位 "ts": datetime.now(timezone.utc).isoformat(timespec="seconds"), "mount": path, "total_bytes": usage.total, "used_bytes": usage.used, "used_percent": round(usage.percent, 2), "write_bytes": io.write_bytes if io else 0, } def run(interval=60, db_path="data/metrics.db"): conn = sqlite3.connect(db_path) sql = ("INSERT INTO disk_metric(ts, mount, total_bytes, used_bytes, used_percent, write_bytes) " "VALUES(:ts, :mount, :total_bytes, :used_bytes, :used_percent, :write_bytes)") while True: conn.execute(sql, sample_disk()) conn.commit() time.sleep(interval) # 简单轮询即可,容量是慢变量,不需要高精度定时

interval默认 60 秒,对着容量曲线这个频率绰绰有余,一天也就 1440 行。mount字段留着是为了多挂载点扩展,一台机器上/data和/var/log的增长速度经常差十倍,混在一起算平均值没有意义。timezone.utc不是可选项,只要序列里混进两种时区,后面重采样就会出现“同一小时两个点”或者“某一小时凭空消失”,排查起来极其费劲。

原始数据进来之后,清洗层负责把它变成等间隔序列。

# analysis/clean.py import sqlite3, pandas as pd def load_series(db_path, mount, freq="1h"): """从明细表读出指定挂载点的使用量序列,重采样到固定频率""" df = pd.read_sql_query( "SELECT ts, used_bytes FROM disk_metric WHERE mount = ? ORDER BY ts", sqlite3.connect(db_path), params=(mount,)) idx = pd.to_datetime(df["ts"], utc=True) s = pd.Series(df["used_bytes"].values, index=idx).sort_index() # 同一周期内多条采样取最后一条,避免重复点把斜率拉歪 s = s.resample(freq).last() # 只对内部短缺口插值,连续缺 3 个周期以上视为采集中断,不做脑补 s = s.interpolate(limit=3, limit_area="inside") return s.dropna()

resample("1h").last()这一步决定了后面所有计算的基准。取last而不是mean,是因为磁盘使用量在一个小时内基本单调,均值会人为压低水位,让 RDA 偏乐观。limit=3是刻意保守的:采集脚本挂掉半天造成的大段空白,插值填出来的是一条假曲线,宁可让这段数据缺失,让下游的min_points判定拦住它。

4.2 负载分析:峰值、P95 与综合负载评分

负载画像的作用不是预测,是回答“这台机器现在到底累不累”。

# analysis/profile.py def load_profile(series): """输入分钟级指标序列,输出一组人看得懂的画像数字""" return { "mean": round(float(series.mean()), 2), "p95": round(float(series.quantile(0.95)), 2), # 比峰值更稳的高水位 "peak": round(float(series.max()), 2), "peak_ts": series.idxmax().isoformat(), "cv": round(float(series.std() / series.mean()), 3), # 变异系数,衡量抖动 } WEIGHTS = {"cpu": 0.35, "mem": 0.30, "disk_io": 0.20, "conn": 0.15} def composite_score(metrics): """metrics 为各指标归一化到 0~1 后的字典,输出加权综合负载分""" return round(sum(WEIGHTS[k] * min(v, 1.0) for k, v in metrics.items()), 4)

用 P95 而不是峰值做主指标是踩过坑之后的选择。峰值往往由一次备份、一次批量导入造成,用峰值判断水位会长期高估负载;P95 能反映“绝大多数时间有多忙”。cv变异系数是给运维看的补充信息:均值和 P95 都差不多的情况下,cv大的机器更容易出问题,因为它的负载在剧烈波动。

composite_score的权重写在模块常量里,是个简化处理。真实项目里这些权重应该来自业务侧访谈,比如一个 IO 密集型的应用,disk_io的权重会明显高于 0.20。归一化方式也需要注意:CPU 用百分比除以 100,连接数用当前值除以配置的最大连接数,磁盘 IO 用实测速率除以设备标称吞吐,三条线的量纲必须统一到 0~1,否则加权就是无意义的数字游戏。

4.3 磁盘容量预测:线性回归与 Holt-Winters 的双模型实现

# analysis/forecast.py import numpy as np from statsmodels.tsa.holtwinters import ExponentialSmoothing def fit_linear(series): """最小二乘拟合,斜率单位是「字节 / 采样周期」""" y = series.values.astype(float) x = np.arange(len(y)) slope, intercept = np.polyfit(x, y, 1) pred = slope * x + intercept ss_res = float(((y - pred) ** 2).sum()) ss_tot = float(((y - y.mean()) ** 2).sum()) r2 = 1 - ss_res / ss_tot if ss_tot else 0.0 return {"slope": float(slope), "intercept": float(intercept), "r2": round(r2, 4)} def fit_hw(series, period=24, horizon=72): """加性趋势 + 加性季节,适合以天为周期的写入规律""" model = ExponentialSmoothing( series, trend="add", seasonal="add", seasonal_periods=period, initialization_method="estimated") res = model.fit(optimized=True) return res.forecast(horizon) # 返回未来 horizon 个点的预测值

fit_linear返回的slope单位是“字节每采样周期”,不是“字节每天”,这个单位转换必须在调用方显式完成,写死在拟合函数里迟早出事。r2是判断这次拟合可不可信的第一道闸门,低于配置里的min_r2时,RDA 只作为提示展示,不触发告警。

fit_hw里seasonal_periods=24对应小时级数据的一天长度,如果重采样频率改成 30 分钟,这个值得同步改成 48,否则季节项会被错位解释成两天一个周期。这是我在一个类似项目里翻过的车,模型能跑通、图也画得出,就是预测线整体反相,样例会骗人,得靠肉眼比对历史曲线才能发现。

4.4 预警判定:剩余可用天数与三级阈值

def remaining_days(series, capacity_bytes, threshold=0.85, freq_per_day=24): """按线性斜率推算水位到达 threshold 所需天数""" fit = fit_linear(series) slope_per_day = fit["slope"] * freq_per_day # 周期斜率换算成天 current = float(series.iloc[-1]) target = capacity_bytes * threshold if slope_per_day <= 0: return {"rda_days": None, "slope_per_day": round(slope_per_day, 1), "note": "无增长趋势"} return { "rda_days": round((target - current) / slope_per_day, 1), "slope_per_day": round(slope_per_day, 1), "r2": fit["r2"], }

三个参数里最容易出错的是freq_per_day。它必须和load_series的重采样频率严格对应,1 小时对 24,30 分钟对 48,写错一倍,RDA 就差一倍——而且是那种看着特别合理的错误,数字平滑、曲线漂亮,不问一句“你重采样频率是多少”根本发现不了。threshold默认 0.85 是有意的:留给扩容和清理的操作窗口,等到 95% 才告警,处理时间往往不够。

等级触发条件建议动作
notice当前使用率 ≥ 75%记录,纳入周报观察
warning使用率 ≥ 85%,或 RDA ≤ 30 天通知负责人,排期清理或扩容
critical使用率 ≥ 92%,或 RDA ≤ 7 天立即处置,必要时限流写入

分级的意义在于把“要不要现在爬起来处理”这个判断交给规则,而不是交给人凌晨三点的判断力。阈值一定不要写死在代码里,setting.yaml里改一行就能调整,这在需求变更频繁的项目里能省下大量重新打包的时间。

5. 避坑与排查:五类最容易翻车的时间序列问题

5.1 周期性写入把线性拟合带偏

现象:预测显示三天后写满,实际过了一周还有 20% 余量;或者反过来,明明还能撑一个月,模型报七天告急。打开历史曲线一看,每天凌晨有一个陡坡,白天几乎平躺。

原因:业务日志、备份任务、批处理作业都是按天跑的,磁盘使用量不是一条直线加噪声,而是阶梯状加周期项。直接对全量数据做一次最小二乘,斜率会被那几个陡坡拉高,RDA 系统性偏小。

解决:拟合前先判断周期性。可以用按天分组的同比差分把周期项消掉,也可以干脆换成 Holt-Winters,让季节项去吸收那部分波动。我的习惯是两条腿都跑,两个 RDA 差得超过 30% 就说明数据有结构性问题,这时候人工看曲线比看模型输出更有用。拟合窗口也别贪长,通常取最近 14 天,太老的数据反映的是上一个版本的行为。

5.2 采样断点导致重采样后大面积空值

现象:load_series返回的序列长度只有预期的一半,画出来的曲线中间缺了一大段;或者更糟,插值把缺口填成了一条斜线,模型在假数据上训练。

原因:采集脚本被重启、机器重启、数据库被锁,都会造成时间戳不连续。重采样不会告诉你缺了多少,它只负责把有数据的地方对齐。

解决:清洗层要显式统计缺口比例,把“连续缺失超过 3 个周期”的区段整段丢弃,而不是插值。同时在结果表里记一个数据质量标记,展示层把这段区间画成灰色,让人一眼看出来“这里没数据”,而不是“这里磁盘用量很平稳”。这个改动成本很低,但在答辩现场解释“为什么模型在某段时间不输出结论”时特别有用。

5.3 清理日志和扩容造成的“回跳”

现象:历史曲线上能看到明显的断崖,某一天使用量突然掉了一半;模型在这种数据上拟合出来的斜率接近零,RDA 显示“无增长趋势”,但实际业务量在涨。

原因:运维做了一次日志清理或者扩容,人为改变了基线。这些点是真实发生的,不能当异常点随便删,但它们确实破坏了“单调增长”这个隐含假设。

解决:在采集层打标,把扩容、清理这类运维操作记录成事件表,拟合时按事件切段,只在最近一段连续的、未受干扰的区间上做预测。工程上更简单的做法是只取最近 N 天数据,同时用分位数回归代替均值回归,降低个别断崖对斜率的影响。这段逻辑最好写成可配置的,因为不同环境里清理频率差别很大。

5.4 单位混用:bytes、GB 与 GiB 的 7% 误差

现象:页面显示磁盘用了 800GB,系统里df -h显示 745G,两边对不上,汇报的时候被追问。

原因:psutil返回字节,除以 1000³ 得到 GB,除以 1024³ 得到 GiB,两者差 7.4%。混着用的时候,阈值判断会整体偏移,92% 的 critical 可能在真实 85% 的时候就触发了。

解决:存储层只存字节,所有换算集中在展示层一个格式化函数里,函数名叫to_gib就得真的除以 1024³。百分比字段单独存,不要每次从字节现算,避免不同模块用不同分母。这条看着像吹毛求疵,但只要项目里有两个人写代码,就一定会有人写成 1000。

5.5 时间戳与时区错位

现象:预测曲线的起点对不上最后一个实测点,或者页面上的“未来 72 小时”实际上从昨天开始。

原因:采集用本地时间,分析时pd.to_datetime按 UTC 解析,或者反过来。夏令时切换的那一天甚至会凭空多出或少掉一小时。

解决:全链路统一 UTC,从采集的第一个字段开始就带+00:00,只在最终展示时转成用户时区,而且转换只在一个函数里做。数据库里存字符串时也要校验格式,2024-03-01 08:00:00这种不带时区的值一律拒收——宁可让采集报错,也别让一条脏数据混进序列,这种错误事后极难定位。

6. 验收与答辩:用回测数字证明这套预测站得住

这套东西能不能交,不取决于页面好不好看,取决于你能不能回答“你凭什么信这个数”。我一般会在验收前做一次留出法回测:把最后 7 天的数据切出来当测试集,用前面的数据训练,预测这 7 天,然后比对预测值和真实值。

def backtest(series, capacity_bytes, holdout_days=7, freq_per_day=24): """留出最后 N 天做验证,返回预测误差指标""" k = holdout_days * freq_per_day train, test = series[:-k], series[-k:] pred = fit_hw(train, horizon=k) mape = float((abs(pred.values - test.values) / test.values).mean() * 100) rda_train = remaining_days(train, capacity_bytes, freq_per_day=freq_per_day) rda_real = remaining_days(series, capacity_bytes, freq_per_day=freq_per_day) return {"mape_pct": round(mape, 2), "rda_shift_days": round(abs(rda_train["rda_days"] - rda_real["rda_days"]), 1)}

验收表按这个思路列,每一项都要能现场取数:

验收项判定标准取数方式
采集完整性24 小时内缺失记录少于 5%按小时分组计数比对
拟合可信度主要挂载点 r2 ≥ 0.70查forecast_result
预测精度7 天回测 MAPE ≤ 5%跑一次backtest
RDA 稳定性相邻两次重算的 RDA 偏移 ≤ 2 天连续跑两遍对比
告警准确性人工构造超阈值数据能触发对应等级造数测试

答辩技巧上,准备三张图就够了:原始曲线加拟合线、未来 72 小时预测带区间、一张误差表。老师问“为什么不用 LSTM”,老老实实答“样本量只有几百个点,深度模型会过拟合,而且我没法解释每一层的含义”——这个回答比硬套一个模型可信得多。血泪经验是,别在答辩前一晚改模型参数,改完之后 RDA 全变了,图对不上,讲不清楚。

从那以后我每次做这类预测项目,都会在写第一行代码之前先把回测脚本写好,哪怕它只有二十行。没有回测的预测模型就是黑匣子,自己都不敢信,更别说让别人信。这套源码里我把backtest单独放在验收目录,就是为了强制自己每次调完参数都跑一遍。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表