1. 赛题拆解与建模思路1.1 拿到题目先看什么先说结论这道题表面在问“电池能撑多久”实际在问“电池的衰减曲线到底长什么样”。第一问的落点是单块电池的容量退化模型但它不像中学物理那样给你一个公式往里代数据而是要你从测量数据里把规律“挖”出来。所以开局第一件事不是急着写代码而是把题目给的数据格式、变量含义、时间尺度全部列清楚。我翻了一下题目原文关键词集中在“连续时间建模”“智能手机电池”“充电周期与容量保持率”。结合参赛经验这种题型的潜台词是用尽可能少的假设建立一个能预测未来容量的数学结构并且要在论文里把“为什么选这个结构”讲明白。评审最反感的写法是“因为AIC最小所以选了它”这不算理由至少不是充分的理由。1.2 完整解题思路从数据清洗到模型落地整个第一问可以拆成四层第一层数据清洗与单位统一。题目给的原始记录往往包含充电起始时间、结束时间、充电前后电量百分比、循环编号。你需要先把“循环”定义清楚——是按充电次数算还是按完整充放电周期算这道题用的是连续时间观测所以每一行数据对应一个时间点而不是每一次循环对应一行。这个差别直接决定后续特征怎么构造。第二层特征构造。连续时间建模的核心特征是“相对时间差”和“累积循环数”。你不能直接用绝对时间戳做输入因为手机电池的衰减跟日历时间、充放电次数同时相关。我当时的做法是构造三个特征距首次使用的小时数、累积等效循环数、最近30天内的平均放电深度。第三个特征属于自己加的因为题目没有直接给但放电深度对容量衰减的影响在前人文献里已经很明确。第三层模型选择。第一问不需要上深度学习一个带随机效应的非线性混合模型就够用了。具体形式可以用指数衰减加线性漂移C(t) C0 * exp(-a * N(t)) - b * t ε其中C(t)是t时刻的可用容量N(t)是等效循环数a是循环老化系数b是日历老化系数ε是测量噪声。这种形式的优势在于参数都有物理意义画出来的拟合曲线也好看更重要的是——你能在论文里解释每一个参数的取值范围为什么合理。第四层预测与验证。拿前80%的时间数据做训练后20%做验证。注意这里不能随机切分必须按时间顺序切否则泄漏问题会让你的误差小得假。1.3 为什么选混合效应模型而不是纯机器学习我见过不少队伍一上来就XGBoost、LSTM最后结果分很低。原因不是模型不够强而是忽略了这道题的核心诉求解释性。MCM评审看重的从来不只是预测精度而是你是否理解数据生成机制。混合效应模型的优势在于它可以捕捉“同一型号电池之间的个体差异”——有些电池天生衰减快有些慢这种差异用固定效应表达不了必须引入随机截距和随机斜率。实际计算时我推荐用Python的statsmodels或者R的nlme包。如果你对贝叶斯熟悉用PyMC做后验采样会更出彩但耗时也高。对于三天的比赛我建议先用极大似然估计快速出一个结果如果时间富余再补贝叶斯。2. 第一问核心代码实现2.1 代码结构总览# 核心库 import numpy as np import pandas as pd from scipy.optimize import curve_fit from scipy.stats import norm import statsmodels.api as sm import statsmodels.formula.api as smf import matplotlib.pyplot as plt我只用了这几个库没有引入任何花哨的依赖。比赛环境里装包是个玄学问题越少依赖越稳。下面的代码是基于常见模拟数据格式写的如果你的数据字段不同改列名即可逻辑不用动。2.2 数据预处理把“离散循环”转为“连续状态”df pd.read_csv(battery_data.csv, parse_dates[timestamp]) # 按设备分组计算相对时间小时 df[device_id] df[device_id].astype(str) df df.sort_values([device_id, timestamp]).reset_index(dropTrue) # 为每台设备计算首次记录时间 first_time df.groupby(device_id)[timestamp].transform(min) df[hours_since_start] (df[timestamp] - first_time).dt.total_seconds() / 3600.0 # 计算等效循环数从记录中的累计能量吞吐量推算 # 假设一次完整循环 从0%充到100%再放空 df[charge_fraction] df[charged_percent] / 100.0 df[cumulative_charge] df.groupby(device_id)[charge_fraction].cumsum() df[equivalent_cycles] df[cumulative_charge].round(0)这里有个细节为什么用累积充电百分比的累加而不是直接数充电次数因为连续时间观测里用户可能充到80%就拔掉下一次又从40%开始充。这些“部分循环”对电池老化的影响不能忽略等效循环数的本质是把部分循环折算成完整循环这是电池研究里常用的安时积分法思想的简化版。2.3 建立衰减模型双机制叠加def battery_decay(t, C0, alpha, beta): 容量衰减模型 C(t) C0 - alpha * t - beta * sqrt(t) C0: 初始容量mAh alpha: 日历老化线性系数mAh/hour beta: 循环老化系数mAh / sqrt(hour) return C0 - alpha * t - beta * np.sqrt(t)我解释一下为什么用sqrt(t)而不是t。很多电池文献里容量衰减与循环数的平方根近似成正比这是因为活性锂损失受扩散过程控制。直接用线性项后期残差会呈现明显的弯曲用平方根项拟合曲线在长周期阶段更贴近真实趋势。当然你也可以在模型里同时放sqrt项和线性项但参数辨识度会变差容易过拟合。2.4 分组拟合与参数提取results [] for device, group in df.groupby(device_id): t group[hours_since_start].values y group[measured_capacity].values # 初始猜测C0取第一天的平均容量 p0 [np.mean(group.loc[group[hours_since_start] 24, measured_capacity]), 0.001, 0.1] try: popt, pcov curve_fit(battery_decay, t, y, p0p0, bounds([0, 0, 0], [np.inf, np.inf, np.inf]), maxfev10000) se np.sqrt(np.diag(pcov)) # 标准误 results.append({ device_id: device, C0: popt[0], alpha: popt[1], beta: popt[2], C0_se: se[0], alpha_se: se[1], beta_se: se[2], rmse: np.sqrt(np.mean((battery_decay(t, *popt) - y) ** 2)) }) except RuntimeError: print(fDevice {device} 拟合失败跳过)这里最关键的坑是初始值。如果你把p0全设成0curve_fit很容易陷入局部最优甚至直接报错。我的建议是先画一张原始数据散点图肉眼判断初始容量和衰减速率再填初始值。这个“先画图再拟合”的习惯能帮你省下大量排查时间。2.5 绘制拟合曲线fig, ax plt.subplots(1, 2, figsize(12, 4)) for device in list(results[device_id])[:6]: group df[df[device_id] device] t group[hours_since_start].values y group[measured_capacity].values ax[0].scatter(t, y, s8, alpha0.5, labeldevice) t_smooth np.linspace(0, t.max(), 200) ax[0].plot(t_smooth, battery_decay(t_smooth, results.loc[results[device_id]device, C0].values[0], results.loc[results[device_id]device, alpha].values[0], results.loc[results[device_id]device, beta].values[0]), linewidth2) ax[0].set_xlabel(时间 (小时)) ax[0].set_ylabel(容量 (mAh)) ax[0].set_title(容量衰减拟合曲线) ax[0].legend(fontsize6) # 残差图 residuals [] for device in results[device_id]: group df[df[device_id] device] pred battery_decay(group[hours_since_start].values, results.loc[results[device_id]device, C0].values[0], results.loc[results[device_id]device, alpha].values[0], results.loc[results[device_id]device, beta].values[0]) residuals.extend((group[measured_capacity].values - pred).tolist()) ax[1].hist(residuals, bins30, edgecolorwhite) ax[1].axvline(0, colorblack, linestyle--, linewidth0.8) ax[1].set_xlabel(残差 (mAh)) ax[1].set_title(残差分布直方图) plt.tight_layout() plt.savefig(fit_result.png, dpi200) plt.show()残差图一定要画。如果残差呈现钟形且均值在0附近说明模型假设基本成立如果残差随着时间变大而呈现喇叭状说明方差稳定性的假设被破坏了你需要考虑加权拟合或者对数变换。3. 参数解释与模型可解释性分析3.1 C0、alpha、beta分别代表什么C0不是用来凑数的常数项它的物理意义是电池出厂时的实际可用容量。你可能会发现C0比标称容量略低这太正常了因为电池在出厂后、用户拿到手之前已经经历了一段日历老化。alpha的单位是mAh/小时它反映的是“时间本身对电池的伤害”。即使你把手机放在抽屉里半年不用容量也会下降这就是日历老化。而beta对应循环老化它跟充电次数强相关。两种老化机制在论文里要有明确的区分因为后面的题目很可能要求你分别控制变量做优化。3.2 如何向评审展示模型的合理性我在论文里加了三个验证第一残差的正态性检验。用Shapiro-Wilk检验p值大于0.05说明残差基本正态模型没有被系统性误差主导。第二交叉验证的MAE和RMSE。我把后20%时间作为验证集算出MAE在30mAh以内换算成相对误差大约3%。这个数字不算惊艳但作为物理模型的精度已经够用了。第三参数稳定性。随机抽取50%的数据重拟合看alpha和beta的波动范围。如果波动很小说明参数辨识度高模型没有过度参数化。4. 常见问题与排查技巧实录4.1 曲线拟合报错“Optimal parameters not found”这个错误90%是初值给得离谱。我见过有人把C0设成1000实际数据是4500那curve_fit在参数空间里挖不到极小值点自然就报错。解决手段是先画数据概览图从图上读取最大最小值、衰减趋势再设置初始值。另一个手段是缩小bounds范围把alpha限制在[0,0.01]之间beta限制在[0,1]之间帮助优化器收敛。4.2 数据时间戳不连续出现跨天缺失手机日志不是每时每刻都在记录充电状态经常出现凌晨3点到早上7点没有记录。如果直接把缺失时间段删掉那么“hours_since_start”这列会跳变模型会误以为这段时间经历了一个巨大的时间差。正确做法是既然我们建模的是连续时间过程那么缺失期间的老化仍然在发生不能删。数据清洗时应保留时间索引让缺失值以NaN占位拟合时自动忽略NaN即可但计算相对时间时要用完整时间差。4.3 同一台设备出现“容量回升”现象锂电池容量测量值本来就有波动温度高的时候内阻低、测出来容量偏高温度低的时候偏低。所以你在散点图上会看到局部波动甚至偶尔一次性测量值比前一次高很多。这时候不要惊慌更不要为了追求光滑而把数据点硬删掉。保留这些点模型拟合时它们只是作为噪声出现对总体趋势影响不大。4.4 如何防止参数过拟合一个办法是正则化。如果你用的是sklearn或者curve_fit可以给目标函数加上惩罚项。但更直接的思路是减少模型复杂度。你想题目只给了几百个数据点你却用了一个六次多项式拟合肯定完美推到未来一秒就崩。所以尽量选择参数少、物理意义清晰的函数形式。C0-alpha-beta三参数模型在第一问里已经足够。5. 论文写作要点5.1 图表格式图片要放在该被引用到的段落附近不要堆在附录里。图题要具体比如“图1某设备容量随时间变化及模型拟合曲线”比“图1拟合结果”好十倍。坐标轴必须标单位和刻度刻度数量适中别用密密麻麻的0到5000全打上标签会被评审认为不好好排版。5.2 为什么你的结果要给出置信区间第二问很可能让你预测某个时间点后的容量如果只给一个点估计评审会觉得你不够专业。正确的表达是预测值 ± 1.96 * 标准误。这段置信区间包含了参数估计的不确定性和未来观测噪声的累积论文里写清楚计算方式就够了不用跑去搞贝叶斯预测区间。5.3 一页纸说清思路的方法MCM的论文页数受限第一问不要写太多模型推导过程。建议用一张图展示原始数据旁边配三行字说明模型假设然后直接上公式和拟合结果。写一大段文字描述“电池如何老化”对拿分没有帮助评审要的是可复现的分析。6. 个人经验总结连续时间建模这个题目在今年MCM里属于中等偏上难度因为它不像传统优化题那样有标准答案也不像机器学习题那样可以无脑堆特征。它的难点在于:你要在物理直觉和数学表达之间找平衡。我的经验是第一天不要急着跑模型先用半天时间把所有散点图画一遍观察不同设备之间的相似性和差异性然后才动手写代码。很多队伍死在第一天就拟合出一个很差的模型后面花了两天时间调参,却发现基础逻辑已经错了。我在实际操作中还有一个小技巧:把每台设备的C0设为该设备前48小时的容量均值alpha的初值设为0.005beta的初值设为0.1。这三个初值在绝大多数数据上都稳定收敛如果你遇到不收敛的情况优先检查数据清理而不是调整模型。有时候一条错误的记录比如容量直接跳成负数会毁掉整个拟合过程先排查干净再怀疑模型。最后想提醒的是第一问代码不要写得过于工程化MCM评审不会运行你的代码但会在论文里看你贴出的关键代码段。贴出核心拟合函数和残差检验部分即可。代码要有注释变量名要见名知义别用a、b、c这种无意义的名字这会让评审觉得你不专业。把代码当作论文的一部分来展示它也是你的表达方式。
阅读完成 · 觉得有帮助?