简介:2022年五一数学建模联赛C题(火灾报警系统问题)成品论文,面向数学建模竞赛参赛者、相关课程学习者及需要综合评估模型案例的研究者。资源围绕火灾探测器选型评估、火灾报警真实性研判与消防大队综合管理水平分析展开,基于附件数据综合运用层次分析法、熵权法,构建了熵权Topsis模型、灰色关联度模型和模糊综合评价模型,可得到探测器可靠率与故障率排名、真实火灾概率及改进建议,兼具方法讲解与实际算例。压缩包为1个PDF文件,约532KB,包含完整参赛论文正文与附录中的代码和数据,方便对照复现关键建模流程。目前已有2739人学习下载,适合需要快速掌握多准则决策、综合评价建模思路,或用于课程设计、作业参考的读者。
1. 2022年五一赛C题火灾报警系统问题-01:这道赛题背后到底要你建什么模型
火灾报警系统的核心矛盾从来不是"传感器不够灵敏",而是误报与漏报之间的那个平衡点。2022年五一赛C题"火灾报警系统问题-01"把这个问题抽象成了一道典型的数理建模题:给你若干传感器的时间序列数据,包括温度、烟雾浓度、一氧化碳浓度等,要求你判断每个监测时刻系统应当处于正常、预警还是报警状态,并给出置信度。说白了,这就是一个时序数据的多分类判定问题,但它和普通的分类题不一样——误报代价和漏报代价完全不对等,且数据本身带着传感器噪声和环境干扰。这道题适合三类读者:数学建模参赛者想拿奖需要一套完整的建模链路;物联网消防方向开发者想借鉴判定规则;以及任何需要从多传感时序数据中做状态判定的工程师。这篇文章就把这个题目从数据预处理到判定模型再到输出格式,完整拆开讲清楚。
2. 拆解问题-01的数据结构:传感器时间序列的预处理与特征工程
2.1 数据文件里到底有什么:多传感器时序数据的组织方式与对齐问题
这道题给出的数据通常是CSV或Excel格式,每个文件对应一个监测点位或一个传感器,记录字段一般包括时间戳、温度、烟雾浓度、一氧化碳浓度。有些赛题还会给湿度或气压数据。拿到数据的第一件事不是画图,而是搞清楚时间戳的采样频率是否一致。常见做法是每秒一条或每五秒一条,但不同传感器文件之间的起止时间可能差几秒甚至几十秒,这会给后续的"同一时刻多维度特征向量"构造带来麻烦。
我在处理这类数据时会先把所有传感器文件加载进来,统一以主传感器(通常是温度传感器)的时间戳为基准做对齐。对齐方式用插值而不是直接截断——如果温度传感器每秒一条,而烟雾传感器每两秒一条,直接把烟雾数据截断到温度时间轴上会造成信息损失,线性插值虽然简单但在这种采样密度下已经足够。
import pandas as pd import numpy as np # 假设temp.csv和smoke.csv分别是温度和烟雾传感器数据 temp_df = pd.read_csv('temp.csv', parse_dates=['timestamp']) smoke_df = pd.read_csv('smoke.csv', parse_dates=['timestamp']) # 以温度传感器时间轴为基准 base_time = temp_df['timestamp'] # 对烟雾浓度做线性插值,对齐到温度时间轴 smoke_aligned = np.interp( base_time.astype('int64'), smoke_df['timestamp'].astype('int64'), smoke_df['smoke_value'] ) temp_df['smoke_aligned'] = smoke_aligned这段代码的核心逻辑是用np.interp把烟雾浓度映射到温度传感器的时间戳上,astype('int64')是把时间戳转换成纳秒整数,这样才能做数值插值。注意线性插值在这里够用的前提是采样间隔比较均匀且缺失率低,如果数据有大量连续缺失,后续要单独做填补策略。
对齐之后还有一个动作容易被忽略——检查对齐后的数据是否有超出传感器量程的异常值。比如温度传感器量程是-20到150摄氏度,如果出现300度的读数,那几乎可以肯定是传感器故障而不是真实火灾。这类异常值如果不剔除,会在后面的趋势分析中把斜率直接拉爆。
2.2 噪声平滑与突变保留:滑动窗口滤波的窗口大小到底怎么定
传感器数据不可避免有噪声,但火灾信号本身是一个突变信号。这里有个本质矛盾:滤波窗口太大,会把真实的快速升温磨平,导致漏报;窗口太小,噪声还在,会导致误判。我调试下来比较稳的参数是温度用5到7秒的滑动窗口,烟雾浓度用3到5秒的窗口。为什么烟雾窗口要更短?因为烟雾传感器响应快但噪声也大,保持短窗口可以更好保留上升沿特征。
def smooth_with_window(series, window, min_periods=1): return series.rolling(window=window, min_periods=min_periods).mean() # 温度做7秒滑动平均 temp_df['temp_smooth'] = smooth_with_window(temp_df['temperature'], 7) # 烟雾浓度做5秒滑动平均 temp_df['smoke_smooth'] = smooth_with_window(temp_df['smoke_aligned'], 5) # 保存突变信息:原始值减去平滑值,代表高频分量 temp_df['temp_highfreq'] = temp_df['temperature'] - temp_df['temp_smooth']这段代码里min_periods=1很重要,它决定了数据开头窗口不足时不会变成NaN。我在初版代码里没设这个参数,结果前7条数据全部变成缺失值,后面做趋势分析时丢了一个完整的时间段。高频分量temp_highfreq可以用来做突变检测,如果某个时刻的高频分量绝对值超过历史均值的3倍标准差,大概率是传感器瞬时干扰或初期火苗。
滤波之后需要做一次特征工程,这是很多参赛队伍偷懒的地方。直接拿原始温度、烟雾浓度当特征去做判断,效果远不如构造"一阶差分"和"短窗斜率"来得稳。一阶差分代表变化量,短窗斜率(比如用最近10秒的数据做一次线性回归取斜率)代表变化速率。火灾初期的典型特征不是某个绝对值超标,而是温度上升速率突然从每秒0.01度跳到每秒0.5度以上,这个速率变化比绝对值更敏感。
3. 核心判定模型怎么选:从固定阈值到滑动窗口趋势判据
3.1 固定阈值判断为什么在真实环境里撑不住
很多队伍拿到数据后第一反应是设阈值:温度超过60度就报警,烟雾浓度超过某个值就报警。这种做法的数学模型非常简单,但在赛题里往往拿不到高分,原因在于真实环境的温度基线会漂移。一个没有发生火灾的监测点,夏季午后温度稳定在35度,傍晚降到28度,如果你把温度阈值设在45度,白天不误报但到了中午有太阳直射传感器的位置可能短暂超过45度,就误报了。更麻烦的是,如果阈值调高到60度,真正发生阴燃火灾的初期温度可能只有40到50度,这时候就漏报了。
固定阈值的失败本质上是"用一个静态值去判断一个动态过程"的不匹配。这是题目里埋的第一个大坑。正确的思路是构造相对判据——不是看温度绝对值有多高,而是看在单位时间内温度变化有多快,以及这个变化是否持续而非瞬时抖动。
这里我想说一个实际项目中的体会:消防系统报警规则的工程实现里,现在主流做法是"自适应阈值"或"相对变化率触发"。相对变化率的意思就是看当前时刻相对过去几分钟基线的偏离程度。但赛题给的数据没有提供长时间无火灾的基线,所以我们需要从数据本身的早期时间窗口来估计一个动态基线。
# 用前N个时间点的均值作为滚动基线 N = 30 # 30秒基线窗口 temp_df['baseline'] = temp_df['temp_smooth'].rolling(window=N).mean() temp_df['temp_deviation'] = temp_df['temp_smooth'] - temp_df['baseline'] # 判定条件1:温度偏离基线超过8度 cond_temp_dev = temp_df['temp_deviation'] > 8.0 # 判定条件2:最近10秒温度线性拟合的斜率超过每秒0.3度 from scipy import stats def rolling_slope(series, window=10): slopes = [] for i in range(len(series)): if i < window - 1: slopes.append(0.0) else: x = np.arange(window) y = series.iloc[i-window+1:i+1].values slope, _ = np.polyfit(x, y, 1) slopes.append(slope) return np.array(slopes) temp_df['temp_slope'] = rolling_slope(temp_df['temp_smooth'], 10) cond_slope = temp_df['temp_slope'] > 0.3这段代码里的滚动基线是"过去30秒均值",这样白天和夜晚的基线会自然跟随环境温度变化,解决了静态阈值的问题。temp_deviation表示当前温度比近期平均水平高多少,这个值比绝对温度更能反映异常。rolling_slope函数用np.polyfit做一阶线性拟合取斜率,窗口10秒。这里的两个条件:温度偏离8度+斜率每秒0.3度,是需要根据赛题数据做敏感性分析的初始参数。
参数设定的逻辑是这样的:如果数据噪声比较大,阈值就要放宽;如果数据比较干净,阈值可以收紧。我在实际调试中一般先画温度的分布直方图和差分序列的分布直方图,取差分序列95分位数作为斜率阈值的起点,而不是拍脑袋定0.3。这是一个可复现的调参方法。
3.2 多传感器联合判据:温度为主、烟雾与CO为辅的加权投票机制
温度变化率是火灾判定的核心特征,但不是唯一特征。燃烧产生的烟雾颗粒和CO浓度的上升往往比温度更早,尤其阴燃阶段温度变化不明显,但烟雾浓度已经持续升高。所以问题-01的建模不应该只靠一个传感器,而要把多个传感器组合成联合判据。
比较常用的方案是加权投票或逻辑回归。先分别对三个传感器单独做出"是否异常"的判断,然后加权汇总。权重怎么定?我建议用层次分析法或简单的熵权法。但对竞赛题来说,更直观的做法是查消防领域的经验值:温度权重最大约0.5,烟雾浓度约0.3,CO浓度约0.2。这个权重分配不是随便拍的——温度是火灾最直接的物理量,但烟雾和CO能在更早期给出信号。
# 烟雾浓度相对基线模型 N_smoke = 20 # 烟雾响应快,用20秒基线 temp_df['smoke_baseline'] = temp_df['smoke_smooth'].rolling(window=N_smoke).mean() temp_df['smoke_dev'] = temp_df['smoke_smooth'] - temp_df['smoke_baseline'] # CO浓度相对基线 temp_df['co_baseline'] = temp_df['co_smooth'].rolling(window=30).mean() temp_df['co_dev'] = temp_df['co_smooth'] - temp_df['co_baseline'] # 单传感器异常信号 temp_abnormal = (temp_df['temp_deviation'] > 8.0) & (temp_df['temp_slope'] > 0.3) smoke_abnormal = temp_df['smoke_dev'] > temp_df['smoke_dev'].std() * 3 co_abnormal = temp_df['co_dev'] > temp_df['co_dev'].std() * 3 # 加权综合得分 score = ( 0.5 * temp_abnormal.astype(float) + 0.3 * smoke_abnormal.astype(float) + 0.2 * co_abnormal.astype(float) ) # 分数超过0.6判定为报警状态 temp_df['alarm_trigger'] = score >= 0.6这个加权方案的好处是代码透明、参数解释性强,赛题评阅时很容易讲清楚每个权重的来源。score >= 0.6意味着必须至少有温度+烟雾同时触发或者三个条件里任意两个加权后达标,单个烟雾异常不会直接报警,这就避免了做饭油烟或水蒸气导致的误报。
不过加权投票的局限在于"单传感器判断的阈值"仍然带有主观性。比如temp_deviation > 8.0这个8度是怎么来的?如果换了一个数据集可能8度就不合适。更严谨的做法是用训练数据里的火灾标签反向标定最优阈值,但赛题问题-01一般没有给标签,所以这种无监督的阈值设定只能靠统计分布来定。我的习惯做法是把每个传感器的偏差序列算出来,取98分位数作为初始阈值,然后人工检验几个明显异常的时间段来微调。
3.3 状态转移模型:正常-预警-报警三态的进入与退出机制
有了连续的综合得分还不够,报警不应该只靠一瞬间的阈值判断,而要引入状态转移机制。这个思路参考了工业过程控制里的状态机设计:系统当前处于正常态、预警态还是报警态,由历史状态和当前信号共同决定。比如一个瞬时的高频噪声可能让综合得分短暂超过0.6,但下一秒就落回去,如果每次都触发报警,值班人员会疯掉。
状态转移的核心是"滞后"与"确认"。从正常态进入预警态需要得分超过预警阈值T1,从预警态进入报警态需要得分超过报警阈值T2且连续维持M秒。反过来,从报警态回落需要得分低于T2并持续N秒。这个滞后机制能有效过滤脉冲噪声。
state = 0 # 0=正常,1=预警,2=报警 state_seq = [] hold_count = 0 for score_val in score.values: if state == 0: if score_val >= 0.7: state = 1 hold_count = 1 else: hold_count = 0 elif state == 1: if score_val >= 0.7: hold_count += 1 if hold_count >= 5: # 持续5秒确认进入报警态 state = 2 elif score_val < 0.4: state = 0 hold_count = 0 else: # state == 2 if score_val < 0.4: hold_count += 1 if hold_count >= 10: # 持续10秒低于回落阈值才退出报警 state = 0 else: hold_count = 0 state_seq.append(state) temp_df['state'] = state_seq这段状态机代码里的关键参数有三个:预警阈值0.7、报警确认时间5秒、报警回落时间10秒。预警阈值设0.7意味着综合得分至少要触发两个传感器信号;5秒的保持时间能过滤掉1到2秒的短脉冲噪声;10秒回落时间则避免报警状态来回抖动的"振荡输出"。真实火灾检测系统里也遵循类似的机制,只是时间尺度可能更长。
4. 从模型到赛题交付:结果表格、可视化与显式输出
4.1 输出表结构设计:时刻、状态、置信度与触发依据
竞赛题目的评分不仅看模型是否有效,还要看输出结果是否规范。问题-01通常会要求把判断结果写成表格提交,每个时间点对应一个状态标签。很多队伍在这个环节丢分不是因为模型不对,而是输出格式混乱——有的只给状态不给置信度,有的把状态定义了中文字符串让评委没法直接读取比较。
我一般会生成一张标准的输出表,字段固定为:时间戳、温度、烟雾浓度、CO浓度、综合得分、状态标签、是否触发报警。状态标签用0、1、2分别代表正常、预警、报警,这样方便评委批量比较。置信度用一个0到1之间的概率值来表示当前判定有多大的可信度,计算方式是把综合得分做一个Logistic变换,让分数映射到概率区间。
# 综合得分转置信度:Logistic映射 confidence = 1 / (1 + np.exp(-10 * (score - 0.5))) temp_df['confidence'] = confidence # 状态标签重命名便于提交 state_map = {0: '正常', 1: '预警', 2: '报警'} temp_df['state_label'] = temp_df['state'].map(state_map) # 输出列选择 output_cols = ['timestamp', 'temperature', 'smoke_aligned', 'co_aligned', 'score', 'state', 'state_label', 'confidence'] output_df = temp_df[output_cols] output_df.to_csv('fire_alarm_result.csv', index=False, encoding='utf-8-sig')confidence公式里的10和0.5是缩放参数——10决定曲线陡峭程度,0.5是得分中位点。当score是0.7时置信度约为0.88,score是0.4时置信度约为0.27,这样置信度和状态有直观的对应关系。utf-8-sig编码是为了让Excel打开中文不乱码,这在最终提交时很实用。
4.2 可视化自查:三张图发现模型边界问题
不要一上来就做高深的图表,先用三张基础图自查。第一张是温度、烟雾、CO三个原始序列的叠加时间序列图,标注报警时间段的背景色,用于观察报警时段的传感器读数是否真的异常。第二张是综合得分随时间变化的曲线,和状态转移的阶梯线叠加在一起,这一步能直接看出状态切换是否干净。第三张是置信度直方图,如果大量样本的置信度集中在0.4到0.6之间,说明特征没有把正常和异常拉开,需要回到特征工程调参。
import matplotlib.pyplot as plt fig, ax = plt.subplots(3, 1, figsize=(12, 10), sharex=True) # 第一张:原始序列叠加 ax[0].plot(temp_df['timestamp'], temp_df['temperature'], label='温度', linewidth=0.8) ax[0].plot(temp_df['timestamp'], temp_df['smoke_aligned'], label='烟雾', linewidth=0.8) ax[0].plot(temp_df['timestamp'], temp_df['co_aligned'], label='CO', linewidth=0.8) ax[0].legend() # 第二张:状态曲线 ax[1].plot(temp_df['timestamp'], temp_df['state'], drawstyle='steps-post', label='状态') ax[1].set_yticks([0, 1, 2]) ax[1].legend() # 第三张:置信度直方图 ax[2].hist(temp_df['confidence'], bins=40, edgecolor='white') plt.tight_layout() plt.savefig('model_check.png', dpi=150)可视化不是为了放进论文当配图,而是模型验证的必需步骤。尤其要关注报警时间段的温度序列——如果报警时段温度曲线没有明显的上升沿,说明你的触发条件有逻辑漏洞。反过来,如果数据中有一段温度快速上升但模型没有报警,说明斜率阈值设得偏高或状态机的确认时间偏长,此时应该优先调这两个参数。
5. 火灾报警建模避坑指南:这五个坑让参赛队伍直接丢分
5.1 第一坑:不同传感器的时间戳没有对齐就直接拼特征
很多队伍拿到数据后直接把三个传感器的原始表按行合并,完全没考虑时间戳可能差几秒。现象是特征拼接后同一行里温度和烟雾浓度根本不在同一时刻,导致模型判断失误。原因在于对这个赛题的底层数据格式缺乏检查,默认所有表的时间轴完全一致。解决方法是先读取每个文件的时间戳范围做对比,如果有偏移就用np.interp或pandas.reindex做插值对齐,且一定要在论文里写出对齐前后的时间差统计。
5.2 第二坑:滑动平均窗口设置过大,把真实的升温信号磨平
有队伍为了彻底去噪,把温度平滑窗口设到30秒甚至60秒。现象是模型漏报率极高,明明数据里有一段非常明显的温度快速上升,但平滑后上升速率被摊薄到每秒0.05度以下,低于报警阈值。原因是滤波把高频信号和噪声一并滤除了。解决方法是分别做频谱分析——先画出温度的功率谱密度,看噪声的能量集中分布在哪个频段,一般火灾信号的能量集中在0.1到1Hz的频段,把滤波截止频率设置在噪声频段和信号频段之间。
5.3 第三坑:只做瞬时阈值判断,不做连续确认,导致报警状态反复横跳
有的模型设计的判定条件是"只要综合得分超过0.6,这一时刻就标为报警",没有引入状态保持机制。现象是报警状态在每两三秒就切换一次,输出表格里出现大面积的0-2-0-2-0跳变。原因是把连续时间序列当成了独立的样本逐个分类,忽略了时间维度上的连续性。解决方法是加状态机或至少加一个"确认计数"的逻辑——报警状态必须连续保持5个时间步以上才真正输出报警,这个逻辑在代码里就几行,但对结果的平滑性改善非常明显。
5.4 第四坑:置信度没有归一化,直接拿综合得分当作概率输出
有的队伍在结果表里直接把加权得分作为"置信度"字段提交,但得分范围是0到1,概率也应该在0到1之间,听起来好像没问题。实际上一旦权重体系调整过,比如把温度权重改成0.6后,综合得分整体抬升,最高能到0.9,但此时置信度的分布完全偏移,评委看到的置信度普遍偏高,缺少区分度。解决方法是把综合得分做一次统计意义上的归一化,用整个数据集得分的均值和标准差做Z-score变换后再映射到0到1区间,或者用Logistic映射函数压缩中间值。
5.5 第五坑:可视化只贴图不做解释,评委看不出模型边界
有队伍的论文里放了十几张图,但每张图下面只有一行小字"某时刻状态为报警",完全没有分析为什么这个时刻报警、用的是哪个判定条件。现象是评委看不懂模型的决策逻辑,没法判断模型的合理性。原因是把可视化当成了展示工具而不是验证工具。解决方法是每张关键图配三个信息:这个图回答什么问题、横纵轴物理量是什么、图中哪个区域对应哪次状态转移以及转移原因。宁可少放两张图,也要把每张图讲透。
6. 进阶思路:用模糊综合评价替代硬阈值,把报警判定从"非0即1"变成"置信度连续可调"
前面讲的模型本质上还是基于精确阈值的逻辑判断,状态转移虽然有滞后但仍然是硬边界。如果想在问题-01里体现出建模水平的差异化,可以考虑引入模糊综合评价:把温度偏差、温度斜率、烟雾偏离度、CO偏离度四个特征看成四个评价指标,分别定义"正常"和"异常"的隶属度函数,再用合成算子计算综合隶属度。
以温度斜率为例,不是简单判断"斜率是否大于0.3",而是定义两个隶属度函数:当斜率小于0.1时"异常隶属度"为0,当斜率在0.1到0.3之间时线性上升,超过0.3后"异常隶属度"为1。这样的好处是斜率在0.25和0.28之间的差异也能被捕获,而不是一刀切。模糊综合评价的核心就是把离散的判定边界变成连续的渐变区间,输出的状态置信度更平滑。
def fuzzy_membership(x, low, high): if x <= low: return 0.0 elif x >= high: return 1.0 else: return (x - low) / (high - low) # 四个特征的异常隶属度 μ_temp_dev = fuzzy_membership(temp_df['temp_deviation'], 4.0, 12.0) μ_temp_slope = fuzzy_membership(temp_df['temp_slope'], 0.1, 0.5) μ_smoke_dev = fuzzy_membership(temp_df['smoke_dev'], 1.5*temp_df['smoke_dev'].std(), 4.0*temp_df['smoke_dev'].std()) μ_co_dev = fuzzy_membership(temp_df['co_dev'], 1.5*temp_df['co_dev'].std(), 3.5*temp_df['co_dev'].std()) # 加权合成(与前面权重保持一致) μ_total = ( 0.4 * μ_temp_dev + 0.3 * μ_temp_slope + 0.2 * μ_smoke_dev + 0.1 * μ_co_dev ) # 最终状态输出:μ_total超过0.65进入报警 temp_df['fuzzy_state'] = np.where(μ_total >= 0.65, 2, np.where(μ_total >= 0.35, 1, 0))这个进阶模型的价值在于:每个特征的隶属度函数里的low和high参数是显式可调的,论文里可以写明"当温度偏差超过4度时开始怀疑异常,超过12度时高度确信异常",这种经验的量化写出来比一个硬阈值看起来严谨得多。同时模糊综合评价的输出不需要额外的状态机,因为隶属度本身已经是连续值,直接做阈值映射就得到三态。
回到问题-01本身的定位,这道题的本质是在有限传感器数据和没有明确标签的情况下,构造一个"可解释、可调参、抗噪"的火灾状态判定模型。我比较推荐的基础方案就是第2到第4章讲的时间对齐、滑动窗口特征、加权综合得分加状态机。如果时间和精力允许,再往模糊综合评价这个方向靠一靠,哪怕不做全,只在温度斜率这一个特征上引入隶属度函数,也能让模型的连续性和抗噪性提升一个档次。
最后说一个我在调试这类模型时的个人习惯:不要一开始就在完整数据集上跑,而是先切出前后各5分钟的两段数据做单元测试,一段是明显的正常时段,一段是包含报警事件的时段,模型能在这两段上行为正确,再放到全量数据上跑。这个习惯帮我少走了很多弯路,也希望帮到你。
本文还有配套的精品资源,点击获取