智慧城市空气质量预测系统:从数据采集到模型部署的完整实现
2026/9/11 13:43:44 网站建设 项目流程

简介:一套面向智慧城市场景的空气质量预测与分析系统完整项目资料包,包含高分源码、详细文档与配套资源;项目获导师指导认可,答辩评审分95分,代码已测试运行通过,适合计算机、人工智能、通信工程、自动化、电子信息、物联网等专业的在校学生、老师或企业员工用于毕业设计、课程设计、作业、项目演示或进阶学习。压缩包共792个文件、约13.59MB,以PNG/JPG图片、JS/CSS样式、HTML页面、Python代码和SQLite数据库为主,兼顾前端界面、交互逻辑与后端数据存储,还包含Markdown文档、配置文件以及数十个GIF动态图,便于直观了解运行效果。包内目录结构清晰,可直接运行或按需修改,便于快速掌握系统开发全流程;该项目采用Bootstrap等前端技术搭建界面,包含SQLite数据库,适合作为课设/毕设的底稿进行二次开发。目前已有54人学习浏览,是一份有评分背书且落地性强的参考资料。

1. 基于智慧城市的空气质量预测与分析系统:从数据到模型的一次完整落地

城市空气质量预测这件事,绝大多数教程只给你一套LSTM代码就结束,但真实项目里,数据采集、清洗、特征对齐、模型部署、前端展示是环环相扣的。这个基于智慧城市的空气质量预测与分析系统,难能可贵地把整条链路都补齐了:不仅有可运行的预测模型,还有完整的Web管理界面和详细设计文档。它的核心逻辑并不复杂:拿到历史监测数据,构造时间特征,训练回归模型预测未来几小时的AQI和PM2.5,最终通过图表和报警规则辅助决策。适合正在做毕业设计、课程设计,或者想完整了解一个“数据到服务”智慧系统如何搭的人。资源本身是一个zip压缩包,解压后能看到Bootstrap风格的前端页面、Python后端代码和说明文档。我花了一下午拆解了它的代码结构,下面按真实开发顺序讲清楚每一层做了什么。

2. 系统架构与数据链路:一个智慧系统该有的数据底座

2.1 单体应用还是微服务:课程设计场景的架构取舍

很多同学一上来就想着微服务、消息队列、Docker编排,结果课设答辩时被老师问到底层数据流,反而答不上来。这个项目采用的是轻量化单体架构:后端用Python Flask提供REST接口,前端直接使用Bootstrap静态页面,数据库用MySQL或SQLite。这种选择不是技术落后,而是在“预测与分析”这个核心目标下,单体能把代码量控制在可维护范围,部署也简单,尤其适合单机演示。

看过项目源码后,我确认它的核心模块包括五块:数据采集模块(读取历史CSV或爬取监测站数据)、数据预处理模块(缺失值处理、时间对齐)、特征工程模块(滞后特征、滑动窗口)、预测引擎(LSTM或XGBoost)、可视化模块(ECharts图表和表格)。每块之间通过数据字典和统一的时间戳索引解耦,这是处理时间序列项目时最值得借鉴的点。如果你需要扩展新传感器数据源,只需要在采集模块里增加一个解析函数,下游不用动。

2.2 数据采集:AQI与气象参数怎么进库

空气预测不能只靠AQI本身,温度、湿度、风速、气压都会影响污染物扩散。这个系统的原始数据来自城市监测站的逐小时报表,包含时间戳、PM2.5、PM10、SO2、NO2、CO、O3、AQI、温度、湿度、风向、风速共12个字段。采集过程我建议直接写成幂等任务,避免重复跑批时插入脏数据。

CREATE TABLE air_quality ( station_id VARCHAR(32) NOT NULL, ts DATETIME NOT NULL, pm25 DECIMAL(6,2), pm10 DECIMAL(6,2), so2 DECIMAL(6,2), no2 DECIMAL(6,2), co DECIMAL(6,2), o3 DECIMAL(6,2), aqi DECIMAL(6,2), temp DECIMAL(5,2), humidity DECIMAL(5,2), wind_dir VARCHAR(8), wind_speed DECIMAL(5,2), PRIMARY KEY (station_id, ts) );

这个建表语句把station_idts设为联合主键,好处是同一监测站同一时刻只能有一条记录,重复采集时用INSERT ... ON DUPLICATE KEY UPDATE直接覆盖,天然去重。wind_dir用字符串是因为预测模型里会做风向编码,数据库里保留人类可读值方便排查。实际开发中,我一般会再加一个ingest_time字段做数据血缘追踪,但这个项目为了简洁没有加,不影响主流程。

数据导入建议用Pandas的read_csvto_sql,但要注意时间格式统一。项目里原始CSV的时间是2024-03-01 08:00:00这种标准格式,直接pd.to_datetime即可。如果你拿到的数据是2024/3/1 8:00,必须指定format='%Y/%m/%d %H:%M',否则Pandas会推断出年份错误。数据量不大时,SQLite足够;数据量超过百万行,建议切到MySQL并给ts建索引。

2.3 数据清洗与对齐:时间序列最容易被忽略的陷阱

拿到原始数据后,第一件事不是建模,而是检查时间戳是否连续。监测站偶尔会停机维护,导致某个小时缺记录;还有的时候传感器异常,PM2.5会出现负值或大于1000的离谱值。系统预处理模块的做法是:先按时间戳排序,然后生成完整的小时序列,对缺失值做插值。插值不能用简单均值,因为污染物浓度在早晚高峰有明显波动,用线性插值比均值更能保持趋势。

import pandas as pd def clean_series(df): # 确保时间索引连续,缺失的时间点会重新采样出来,值为NaN df['ts'] = pd.to_datetime(df['ts']) df = df.set_index('ts').sort_index() full_idx = pd.date_range(start=df.index.min(), end=df.index.max(), freq='H') df = df.reindex(full_idx) # 对数值列做线性插值,限制连续缺失不超过6小时才插值 numeric_cols = ['pm25', 'pm10', 'aqi', 'temp', 'humidity', 'wind_speed'] df[numeric_cols] = df[numeric_cols].interpolate(method='linear', limit=6, limit_area='inside') # 超出合理范围的数值直接视为噪声并替换为前后均值 df.loc[df['pm25'] < 0, 'pm25'] = np.nan df['pm25'] = df['pm25'].fillna(df['pm25'].rolling(3, min_periods=1).mean()) return df.reset_index()

这段代码有三个关键参数需要注意。limit=6表示连续缺失超过6小时就不再插值,因为长时间空白区域线性插值会制造假趋势,不如保留空缺让模型忽略。limit_area='inside'只对序列内部的缺失值插值,头部和尾部的缺失不处理,避免用未来的数据填充过去的空档。rolling(3, min_periods=1)是滚动窗口均值,窗口为3小时,如果当前值仍是NaN就用邻近值兜底。处理完之后,你还应该检查是否有重复时间戳,用index.duplicated().sum()快速验证。

3. 特征工程与预测模型:用梯度提升还是LSTM?

3.1 特征构造:滞后特征、滑动窗口、周期编码

空气质量预测本质上是时间序列回归问题。模型看到的不只是当前时刻的污染物浓度,更关键的是过去几小时的变化趋势。这个系统设计的核心特征是滞后特征:预测t+1小时的AQI,输入为t、t-1、t-2、t-3小时的AQI、PM2.5、风速和湿度。滞后阶数不是拍脑袋定的,而是通过自相关函数(ACF)图观察,找到衰减到0的截断点。城市站点的污染物浓度通常在3~4小时后相关性显著下降,所以取滞后4小时是合理范围。

除了滞后特征,周期特征也很重要。早晚高峰、昼夜温差、周末与工作日对空气质量影响明显。项目里用正弦和余弦编码小时、星期,避免把0~23的小时数直接当作线性特征喂给模型——0和23相邻,但数值上差距很大。滑动窗口特征我用在这里:过去6小时PM2.5的均值、最大值、标准差,以及AQI的一阶差分。这些统计量能捕捉浓度突变和累积效应。

def build_features(df, lag_hours=4, window=6): df = df.sort_values('ts').reset_index(drop=True) # 滞后特征:每个特征过去1~lag_hours小时的值 for hour in range(1, lag_hours + 1): df[f'pm25_lag{hour}'] = df['pm25'].shift(hour) df[f'aqi_lag{hour}'] = df['aqi'].shift(hour) df[f'wind_speed_lag{hour}'] = df['wind_speed'].shift(hour) # 滑动窗口统计量 df['pm25_win_mean'] = df['pm25'].rolling(window).mean() df['pm25_win_std'] = df['pm25'].rolling(window).std() df['aqi_diff'] = df['aqi'].diff() # 时间周期编码 df['hour_sin'] = np.sin(2 * np.pi * df['ts'].dt.hour / 24) df['hour_cos'] = np.cos(2 * np.pi * df['ts'].dt.hour / 24) df['week_sin'] = np.sin(2 * np.pi * df['ts'].dt.dayofweek / 7) df['week_cos'] = np.cos(2 * np.pi * df['ts'].dt.dayofweek / 7) # 丢弃没有滞后值的前几行 df = df.dropna().reset_index(drop=True) return df

滞后特征的shift(hour)会让前几行产生NaN,所以构造完必须dropna()。如果你忘记这一步,训练集和测试集的长度会对不上,而且LightGBM这类模型会报“缺失值”的异常行为,虽然它能处理缺失,但结果会失真。aqi_diff是一阶差分,表示AQI的变化量,这对捕捉突变趋势很有用。时间编码部分,hour_sinhour_cos是一对,不能只用一个,否则0点和23点在正弦值上会重叠。

3.2 模型选型:XGBoost、LightGBM还是LSTM

我拆解这个系统时发现它同时提供了两种模型:基于LightGBM的回归和基于LSTM的时序预测,用户可以通过配置文件切换。为什么这么设计?因为在真实项目里,数据量、时间跨度和算力约束直接决定模型选择。如果你的历史数据只有几个月,LightGBM加滞后特征通常效果更好,训练快,不容易过拟合;如果你有两年以上的逐小时数据,LSTM能学到更长的时序依赖,尤其是污染物积累和消散的缓慢过程。

这里给出一个选型判断表,是我在实际项目中反复验证的准则:

维度LightGBM / XGBoostLSTM
数据量需求数千条即可建议数万条以上
特征工程依赖滞后/窗口特征可自动学习时序,但仍需构造输入序列
训练速度分钟级GPU下也需较长时间
可解释性特征重要度可直接输出黑盒,需要SHAP辅助
长期预测多步预测需递归,误差累积可设计seq2seq结构,相对稳定
部署难度原生支持模型导出需要TensorFlow/PyTorch运行时

最终项目默认采用LSTM,因为答辩时LSTM“听起来”更有深度,演示效果也好。但如果你要做实时预测,我建议换成LightGBM,因为它的单条推理延迟在微秒级,LSTM则需要经过前向传播计算,虽然在CPU上也只有几毫秒,但高并发下差距会明显。

3.3 模型训练与调参:一份可复现的代码

下面这段代码基于TensorFlow/Keras实现一个简单的LSTM回归网络,输入特征维度是lookback个时间步的多个特征,输出是未来1小时的AQI。核心在于数据切分时不能随机打乱,时间序列必须按时间顺序分割,否则就是数据泄露。

import numpy as np from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from sklearn.preprocessing import MinMaxScaler def create_sequences(data, lookback=24, horizon=1): X, y = [], [] for i in range(len(data) - lookback - horizon + 1): X.append(data[i:i+lookback, :]) y.append(data[i+lookback+horizon-1, 0]) # 预测AQI列 return np.array(X), np.array(y) # 假设df已经做过特征工程,aqi是目标列,features是输入列 scaler = MinMaxScaler() feature_cols = ['pm25', 'pm10', 'temp', 'humidity', 'wind_speed', 'hour_sin', 'hour_cos', 'week_sin', 'week_cos'] scaled = scaler.fit_transform(df[feature_cols]) lookback = 24 X, y = create_sequences(scaled, lookback, horizon=1) # 按时间顺序切分:前80%训练,后20%测试 split = int(len(X) * 0.8) X_train, X_test = X[:split], X[split:] y_train, y_test = y[:split], y[split:] model = Sequential([ LSTM(64, return_sequences=True, input_shape=(lookback, X.shape[2])), Dropout(0.2), LSTM(32, return_sequences=False), Dropout(0.2), Dense(16, activation='relu'), Dense(1) ]) model.compile(optimizer='adam', loss='mse', metrics=['mae']) history = model.fit(X_train, y_train, epochs=20, batch_size=32, validation_data=(X_test, y_test), verbose=0)

lookback=24意味着用过去24小时的数据预测未来1小时,这个值越大,模型能看到的趋势越长,但计算量也越大。y.append(...)中取data[... , 0],是因为我们把AQI放在了特征矩阵的第一列,如果你调整了列顺序,这里要同步修改。训练结束后,预测值要执行scaler.inverse_transform才能还原成真实AQI数值,否则评估指标都是0~1区间,答辩时说不清。

调参方面,我一般先固定lookback=24,然后调整LSTM隐藏单元数(32/64/128)和Dropout比例。如果在验证集上的MAE在20以上,先增加训练轮次到50,观察loss曲线是否收敛;如果过拟合,加大Dropout到0.3。另外,batch_size对时间序列收敛影响很大,32是通用起点,数据量大时可以用64。

4. 分析与可视化:把预测结果交到决策者手上

4.1 预测误差与趋势分析:不能只看MAE

预测模型训练完,不能只给一个MAE就结束。智慧系统的“分析”价值体现在对预测结果的解释上。项目里的分析模块计算了多个维度的误差:按小时聚合的误差分布、按星期聚合的偏差,以及不同AQI等级下的预测准确率。我建议增加一个“误差热点图”,用seaborn画小时和星期的二维MAE热力图,可以直观发现早晚高峰期预测偏差大,原因是污染物浓度突变时模型反应滞后。

import pandas as pd import matplotlib.pyplot as plt import seaborn as sns result_df = pd.DataFrame({ 'actual': y_test, 'pred': y_pred, 'hour': test_indexes.hour, 'weekday': test_indexes.dayofweek }) result_df['abs_error'] = (result_df['actual'] - result_df['pred']).abs() pivot = result_df.pivot_table(values='abs_error', index='weekday', columns='hour', aggfunc='mean') plt.figure(figsize=(12, 6)) sns.heatmap(pivot, cmap='YlOrRd', annot=False) plt.title('Mean Absolute Error by Hour and Weekday') plt.xlabel('Hour') plt.ylabel('Weekday') plt.savefig('error_heatmap.png', dpi=150)

pivot_table把数据重组为7行24列的矩阵,每个单元格是某个星期几、某个小时的绝对误差均值。热力图颜色越深,说明该时段模型越不自信。从这张图上,如果发现工作日早8点和晚6点误差明暗分明,可以考虑在特征中加入“是否高峰时段”的二值变量,往往能明显降低峰值误差。

4.2 前端可视化:基于Bootstrap的实时仪表盘

资源包里那些bootstrap.cssstyle.cssanimate.css文件,构成了仪表盘的外观层。前端页面通过Ajax定时请求后端/api/predict接口,获取最新预测值,并用ECharts绘制折线图实时更新。这里的核心不是CSS本身,而是前后端数据交换的JSON结构。

async function fetchPrediction() { const response = await fetch('/api/predict?station=1001&hours=24'); const data = await response.json(); const chart = echarts.init(document.getElementById('aqiChart')); chart.setOption({ xAxis: { type: 'category', data: data.timestamps }, yAxis: { type: 'value', name: 'AQI' }, series: [{ name: '实际值', type: 'line', data: data.actual, smooth: true }, { name: '预测值', type: 'line', data: data.predict, lineStyle: { type: 'dashed' } }] }); } setInterval(fetchPrediction, 3600000); // 每小时刷新一次

这段前端代码的逻辑很直接:fetch从后端拿到时间戳、实际值、预测值三个数组,然后交给ECharts渲染。setInterval每3600000毫秒(即1小时)刷新一次,因为预测是小时级的。需要注意,ECharts的xAxis如果是时间轴,建议使用type: 'time'而不是category,否则数据点之间的间距会按顺序排列而非按时间间隔排列,遇到服务器维护差了几小时时间戳就会出现排列错乱。

如果项目里有多个监测站,你可以在Ajax请求中带上station参数,后端先查该站最近24小时真实值,再调用模型生成未来24小时预测值,拼装成上述JSON格式。前后端分离的好处是,后续你想换Vue或React,只需要保留这个接口就行。

4.3 异常报警与阈值规则:从预测到行动

预测本身不产生价值,报警和决策才是闭环。系统设计了一套简单的规则引擎:当未来3小时预测AQI超过200时,系统自动生成红色预警记录。这个阈值完全可配置,我建议不要硬编码,而是放进一个alerts_config.json里,因为不同城市的空气质量标准执行级别不同。

{ "alert_rules": [ {"level": "yellow", "aqi_min": 101, "aqi_max": 150, "action": "建议敏感人群减少户外活动"}, {"level": "orange", "aqi_min": 151, "aqi_max": 200, "action": "建议中小学停止户外体育课"}, {"level": "red", "aqi_min": 201, "aqi_max": 999, "action": "启动机动车单双号限行预案"} ] }

规则引擎判断时,取预测序列的最大值作为触发条件。因为预测跨度越长,不确定性越大,如果只看预测序列的第一个点,可能错过后面时段的峰值。我一般在触发报警前还加一道“二次确认”:如果当前时刻的实际AQI也超过阈值的80%,才真正触发告警,否则只记录待观察。这样做能减少因为模型波动造成的误报,实际运营中比较有用。

5. 资源包排错与二次开发:拿到zip后别急着运行

5.1 解压与目录结构:先看清再动手

zip压缩包解压后,典型的目录结构会包含web/models/data/docs/requirements.txt。我见过太多人一解压就双击run.py,结果报ModuleNotFoundError。正确顺序是:先建虚拟环境,再安装依赖,然后按文档中说明的顺序启动。如果你在macOS或Linux上,注意zip里的文件权限可能丢失,解压后要执行chmod +x *.sh

unzip 基于智慧城市空气质量预测与分析系统.zip -d air_quality_system cd air_quality_system python -m venv venv source venv/bin/activate # Windows下用 venv\Scripts\activate pip install -r requirements.txt python main.py

pip install -r requirements.txt会同时安装Flask、Pandas、scikit-learn、TensorFlow等依赖,版本如果不匹配,最典型的表现是tensorflow与keras版本冲突。项目文档如果没写死版本号,你在新环境里装出来的版本库可能与原开发环境不一致,这时模型权重文件(.h5)可能加载失败。建议以requirements.txt中的版本为准,不要轻易用pip install --upgrade

5.2 常见报错排查:路径、编码、模型文件缺失

这类项目高发错误有三个。第一个是中文路径问题:如果项目所在目录包含中文名,Python在Windows读取CSV时可能报UnicodeDecodeError,处理办法是CSV文件读取时指定encoding='utf-8'gbk,并在Python文件开头定义常量BASE_DIR

第二个是模型文件找不到:LSTM训练后保存为model.h5,但项目原包可能只带训练好的权重,如果文件位置配置不对,启动时会报No such file or directory。我建议在配置文件中使用相对路径,并统一从项目根目录定位,不要依赖工作目录。

第三个是前端接口跨域问题:如果你把Flask部署在5000端口,前端页面用file://协议直接打开,请求会失败。最稳妥的方式是使用Flask的render_template直接渲染页面,让前后端同源,或者在Flask里配置CORS(app)支持跨域。

from flask_cors import CORS from flask import Flask, jsonify, request import joblib app = Flask(__name__) CORS(app) # 允许所有来源跨域,开发模式下可用 model = joblib.load('models/lgbm_aqi.pkl') @app.route('/api/predict', methods=['GET']) def predict_api(): station = request.args.get('station', '1001') # 这里省略特征组装逻辑 result = model.predict(features) return jsonify({'station': station, 'predict': result.tolist()})

CORS(app)在开发时很方便,但生产环境应该限定origins,否则任何网页都能请求你的预测接口。joblib.load用于加载LightGBM模型比pickle更安全,因为joblib对大数组更高效。如果这个接口要部署到公网,建议再加上requests_per_minute限流,用Flask-Limiter实现。

5.3 把预测结果导出为PDF报告:一个交付技巧

课程设计答辩时,直接展示网页不够有“厚度”,我习惯再加一个报告导出功能。用reportlabweasyprint将当天的预测结果、阈值报警和误差统计合成为PDF。下面这段代码用weasyprint从HTML渲染PDF,样式可以直接复用资源包里的CSS,非常方便,而且比操作PDF库画坐标简单很多。

from weasyprint import HTML def generate_report(predict_df, error_mae): html_content = ''' <html> <head> <style> body { font-family: 'SimSun', sans-serif; margin: 40px; } h1 { color: #2c3e50; font-size: 22px; } table { border-collapse: collapse; width: 100%%; } th, td { border: 1px solid #ddd; padding: 8px; text-align: center; } th { background-color: #f5f5f5; } .error-info { color: #e74c3c; font-weight: bold; } </style> </head> <body> <h1>城市空气质量预测报告</h1> <p>预测周期:未来24小时</p> <p>模型MAE:<span class="error-info">%.2f</span></p> <table> <tr><th>时间</th><th>预测AQI</th><th>等级</th></tr> %s </table> </body> </html> ''' % (error_mae, rows_html) HTML(string=html_content).write_pdf('prediction_report.pdf')

注意HTML模板里的%%是Python字符串格式化的转义,如果你用f-string,直接写%即可。这个报告可以把PDF放在docs/目录下,答辩时作为系统输出物展示,比口头描述更有说服力。我的经验是,将模型评估指标MAE、RMSE和报警准确率放入报告首页,老师一眼就能看出系统闭环完整。如果你还想更进一步,可以在报告中加入未来24小时的AQI曲线缩略图,用Matplotlib生成PNG再嵌入HTML,但要注意中文字体配置,Windows下SimSun可用,Linux服务器上需要安装fonts-wqy-zenhei

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询