这套标题看了就很有共鸣居民用电数据这种东西说难不难说简单也真不简单。很多做电力运维、物业节能、社区碳排管理的朋友手上攒了一堆电表数据却只能用Excel拉个透视表勉强算算总量想看看分时段负荷、抓一抓异常用电、预测一下明天峰值就非常吃力。Python配Flask做一套小型数据分析系统恰好能把数据清洗、指标计算、图表展示、网页交互整个串起来是我这几年做过同类项目里性价比最高的组合。这套系统说直白点就是把“居民用电数据”变成“能看、能查、能判断”的可视化分析平台。家庭用户、台区总表、抄表记录都能接入日用电趋势、峰谷结构、异常用电、月度账单对比都能出再套一个Flask的Web壳局域网里一部署物业经理和运维人员用浏览器就能看不需要装任何客户端。这篇文章我会从整体设计讲到具体代码再讲到部署阶段的坑适合有Python基础但没完整做过项目的朋友参考也适合正在做电力数据分析课设或小工具开发的人直接抄作业。1. 整体设计与技术选型思路1.1 为什么是Flask而不是Django或FastAPI我最早做这类系统时其实先在Django和Flask之间犹豫过一阵。Django确实“全家桶”自带Admin后台、ORM、迁移工具对大型CMS或者权限复杂的系统是省力的但麻烦也在这——框架替你决定了很多事你想把页面做得轻巧一点、接口返回得简单一点反而要绕不少路。FastAPI性能确实好异步支持一流但它在模板渲染和表单处理上不够顺手社区里关于数据分析可视化的现成案例也比较少。Flask在这中间是平衡得最好的一个。它足够轻核心就是一个路由分发加Jinja2模板引擎想怎么组织代码都可以周边生态又很成熟SQLAlchemy做ORM、Pandas做数据处理、ECharts做图表全是无缝衔接。对于居民用电分析这种“页面数量不多但计算逻辑复杂”的项目Flask的灵活度刚好够用而且部署时单个Gunicorn进程就能顶住几百个并发请求在小区物业这种量级下绰绰有余。选择Flask还有一个很实际的原因资料多、坑少。只要你Google“Flask 数据分析项目”能找到大量现成的目录结构、上传Excel的处理代码、图表渲染方案。项目工期紧张的时候能快速参考社区的成熟做法比什么都重要。1.2 系统功能模块划分这套系统的核心不是页面多炫而是分析逻辑能落得下去。我一般把整个系统拆成四个模块数据接入模块支持上传CSV/Excel抄表记录也可以直连MySQL读取历史数据库负责把原始数据统一成DataFrame格式。数据预处理模块处理缺失值、异常值、重复记录格式化时间戳按台区或户号分组生成时序完整的数据集。分析计算模块计算日用电量、月用电量、负荷峰值、峰谷比、环比增长率跑异常检测和简单的负荷预测。展示交互模块Flask路由负责提供页面和JSON接口前端用ECharts渲染趋势图、饼图、热力图表格用Bootstrap渲染。这四个模块的边界清楚之后开发时才可以并行推进。我通常先做数据接入和预处理因为这两块不通过Web也可以单独跑脚本验证等数据质量稳了再写分析函数最后套页面和接口调试效率会高很多。1.3 数据存储SQLite还是MySQL前期只有几栋楼、几百户的数据用SQLite完全够。SQLite是文件型数据库不需要单独启服务Pandas直接读、Flask直接连部署时拷贝一个文件就能迁移对中小型项目非常友好。等数据量到了几千万行、需要并发写入时再考虑换MySQL也不迟SQLAlchemy配置改一下就行分析代码基本不用动。我有个习惯数据仓库表设计遵循宽表原则能把多表关联的维度冗余进去就不用频繁JOIN。居民用电分析里最常用的一张宽表长这样字段名类型说明idINTEGER主键house_idINTEGER户号meter_idVARCHAR电表编号record_timeDATETIME抄表时间powerFLOAT当前电量累计示数power_usageFLOAT本次用电量度数districtVARCHAR所属台区/小区house_typeVARCHAR户型/用户类型有这张表日用电趋势、台区汇总、类型对比都能一条SQL或一个groupby搞定。再额外建一张异常记录表用来存检测结果一张预测结果表存次日负荷预测值分析模块和展示模块就各自解耦了。2. 数据获取与预处理2.1 用电数据从哪里来真实项目里用电数据源五花八门。最常见的是三种场景第一种从集抄系统导出CSV每天一个文件列名可能还不一样今天导出的是“示数”明天导出的是“表码”。这种数据最脏我一般先用一个配置映射表管理不同文件名对应的列名规则处理时动态匹配。第二种由硬件采集终端直接上报到数据库。终端按15分钟或1小时上报一次累计示数这种数据质量最好只需要做重复值去重和缺失补点。第三种手工抄表录入多用于老旧小区。这种频率低可能一个月一条。数据分析时能支撑的维度就少主要做月度趋势和年度对比。对于上传文件我通常会提供一个统一的导入接口代码逻辑就是读取Excel/CSV把列名标准化再做基础校验。import pandas as pd def load_electric_data(file_path): if file_path.endswith(.csv): df pd.read_csv(file_path, encodingutf-8-sig) else: df pd.read_excel(file_path) df.columns [c.strip().lower().replace( , _) for c in df.columns] df[record_time] pd.to_datetime(df[record_time], errorscoerce) df.sort_values([house_id, record_time], inplaceTrue) return df这里有个小细节CSV文件用utf-8-sig编码读取而不是utf-8。因为用Excel另存的CSV很可能是带BOM的直接按utf-8读会把第一列名解析出个隐藏字符\ufeff后面过滤列名时怎么都对不上。2.2 数据清洗的标准操作数据清洗是整个系统里最花时间的部分经常是技术不复杂但脏数据种类多。居民用电数据常见的问题有七类时间戳格式不统一2024/05/01、2024-05-01 08:00:00、20240501混在一个文件里。重复记录同一户同一时间有两行数据。累计示数回退换表或者抄错后一条比前一条小。瞬时值异常比如单户一小时用电几百千瓦时明显不合理。空值部分时间点没有采集到数据。户号格式不一致有的带前缀字母有的是纯数字还有的带横杠。分组类字段缺失台区、户型没填。针对这些我的标准是把缺失值、阈值、重复值各写一个函数全部串在pipeline里。比如处理示数回退可以用差分逻辑识别def fix_reverse_readings(df): df df.sort_values([house_id, record_time]).copy() df[diff] df.groupby(house_id)[power].diff() # 回退超过阈值视为换表或异常用前向值填补并标记 df[reverse_flag] df[diff] 0 df[power] df.groupby(house_id)[power].ffill() return df回退数据不能直接删因为换表场景下前后读数本来就是断开的直接把回退行删掉会导致后续累计电量计算为负值。我的做法是把回退后的读数改为前一条的延续值同时保留一个“疑似回退”标志列分析时可以过滤掉。2.3 特征工程虽小但决定分析深度的上限很多教程不讲特征工程觉得Python里Pandas一算就行。但居民用电数据有个特殊性——时间属性太重要了。同样是周一工作日和节假日的负荷曲线完全不同同样是夏天台风天和高温天空调用电差异巨大。我做特征工程时会额外生成几个时间派生字段小时、星期几、是否工作日、是否节假日、节假日前后一天、所处月份、季节。这些特征一是方便后续可视化分组二是做负荷预测时它们就是核心的输入变量。def build_time_features(df): t df[record_time] df[hour] t.dt.hour df[weekday] t.dt.weekday df[is_weekend] df[weekday].isin([5, 6]).astype(int) df[month] t.dt.month df[season] df[month].map({12: 4, 1: 4, 2: 4, 3: 1, 4: 1, 5: 1, 6: 2, 7: 2, 8: 2, 9: 3, 10: 3, 11: 3}) return df还有一个容易忽略的点就是滞后特征。预测今天20点的用电量昨天20点的用电量往往是最强的参考指标之一。我在预测模块里会生成lag_1、lag_7前七天同一时刻、rolling_mean_6h。不过滞后特征要小心泄露训练时必须保证时序切分不交叉不然验证指标虚高上线后直接翻车。3. 核心分析功能实现3.1 用电趋势分析从整体到个体趋势分析是做给管理者和运营看的核心诉求是“这个月整体用电水平怎么样、和去年同期比是升是降、哪几天异常突出”。代码层面就两个动作按时段聚合再算环比同比。def daily_trend(df, date_rangeNone): df df[df[record_time].between(*date_range)] if date_range else df daily df.set_index(record_time).resample(D)[power_usage].sum().reset_index() daily[monthly_avg] daily[power_usage].rolling(30, min_periods1).mean() daily[year_ago] daily[power_usage].shift(365) return daily这里rolling(30, min_periods1)是滚动30天均值目的是把节假日、单日天气波动造成的毛刺平滑掉暴露出真正的趋势。很多刚入行的人直接把每日原始曲线画出来结果折线全是锯齿看不出任何规律加了滚动均值一下就清楚了。展示时我习惯用双Y轴图左边是当日用电量右边是同比变化率。柱状图表示实际值折线图表示趋势均值。这样可以同时回答“用了多少”和“涨了多少”两个问题。3.2 峰谷平结构与分时分析居民峰谷分析直接跟电费相关也是供电公司、售电公司最关心的分析维度。分时电价一般划分为峰、平、谷三段不同地区的时段划分略有差异我通常把时段规则做成配置项放在系统设置里而不是写死在代码中。def mark_tariff_period(df, peak_hours, valley_hours): def period_of(h): if h in peak_hours: return peak if h in valley_hours: return valley return flat df[tariff_period] df[hour].map(period_of) return df分析输出上我会同时给两个维度一是“户均峰谷电占比”即分时段的用电量占总用电量的比例二是“峰谷差系数”公式是(峰段用电量 - 谷段用电量) / 总用电量。峰谷差系数越大说明用户用电习惯越不均衡对电网调峰压力越大也意味着如果推动用户错峰用电潜力越明显。这张表是可视化页面的核心用饼图展示各时段占比用堆叠柱状图展示每天分时段的用电量演变。3.3 异常用电检测不是所有波动都叫异常异常检测在居民用电场景里最实用的还是无监督方法。因为没有那么多“打标签”的真实异常数据监督学习不太可行。我常用的有梯度方法有三种而且都经过真实数据验证过。第一种阈值法。单户单小时用电量超过全小区均值3倍标准差记为疑似异常。适用于快速排查误报率偏高。第二种差分法。单户用电量环比突然增长超过100%记为一类事件骤降且接近0记为二类事件。前者可能是漏电、私接负载或电表故障后者可能是长时间外出或采集故障。第三种季节性分解。用statsmodels的STL把日用电序列分解成趋势项、季节项、残差项残差超过设定阈值才标记为异常。这个方法最准但计算量大适合对重点用户做深度分析。from statsmodels.tsa.seasonal import STL def detect_anomaly_stl(series, threshold1.5): stl STL(series, seasonal7).fit() resid stl.resid mad (resid - resid.mean()).abs().median() upper resid.mean() threshold * 1.4826 * mad lower resid.mean() - threshold * 1.4826 * mad return (resid upper) | (resid lower)这里用MAD绝对中位差而不是标准差是因为残差项往往有离群值直接用标准差会被个别极端值拉大反而检测不出异常了。1.4826这个系数是把MAD折算到标准差的常数。3.4 一个能用的负荷预测不需要多高深的模型很多人一听到负荷预测就想到LSTM或Transformer其实对于居民小区级数据传统机器学习方法在稳定性和可解释性上反而更好。我生产环境里用得最多的是LightGBM加时间特征输入就是日期时间特征加滞后特征输出是未来一小时或一天的用电量。为什么不用深度学习因为居民用电数据样本量经常只有几万到几十万条特征维度又低深度学习在这个体量上不仅没有优势训练和调参还会浪费大量时间。import lightgbm as lgb def train_forecast_model(df): features [hour, weekday, is_weekend, month, season, lag_1, lag_7, rolling_mean_6h] train_df df[df[record_time] cutoff].copy() valid_df df[df[record_time] cutoff].copy() model lgb.LGBMRegressor(n_estimators500, learning_rate0.05, num_leaves31, verbose-1) model.fit(train_df[features], train_df[power_usage]) return model预测结果的评估不能只看R2还要看误差百分比。电力负荷的早晚高峰绝对值大低谷绝对值小MAPE平均绝对百分比误差在低谷时段会被小分母放大。所以我通常同时报告MAPE和RMSE并额外观察一天内几个特定时段的误差分布。如果夜间MAPE很高但RMSE不高说明整体电量误差可控不算模型失败。4. Flask Web应用开发4.1 项目目录结构与路由设计一个清晰的目录结构是Flask项目能长期维护的根基。我见过太多把全部代码塞进一个app.py的项目最后改一行路由都要拉着整个文件滚动半天。推荐按模块拆分electric_analysis/ ├── app.py # 应用入口 ├── config.py # 配置参数 ├── models.py # SQLAlchemy模型 ├── utils/ │ ├── data_loader.py # 数据读取模块 │ ├── data_cleaner.py # 数据清洗模块 │ ├── analysis.py # 分析计算模块 │ └── forecast.py # 预测模块 ├── routes/ │ ├── main.py # 页面路由 │ └── api.py # JSON接口路由 ├── templates/ # Jinja2模板 │ ├── index.html │ ├── trend.html │ ├── abnormal.html │ └── predict.html ├── static/ │ ├── css/ │ ├── js/ │ └── echarts.min.js └── data/ └── electricity.db路由设计上页面类路由返回HTML模板数据类路由返回JSON。不要混在一起比如/api/daily_trend就只返回JSON不渲染页面这样前端可以随时切换渲染方式同时也方便其他系统通过接口对接数据。4.2 可视化方案ECharts是首选但不是唯一做数据展示时我的选择顺序是ECharts Chart.js 服务端matplotlib生成图片。ECharts的图表类型最全热力图、桑基图、关系图都有现成组件交互效果好缩放、提示框、数据刷选都是内置的。Chart.js轻量但复杂图表支持不够。matplotlib生成静态图片的方式基本只用在导出PDF报告的场景不推荐作为页面交互的主体因为用户想要悬停看具体数值时图片做不到。在Flask里用ECharts我一般不会用网上那种往模板里硬塞大段JavaScript的方式而是前端页面初始化时从后端拉取JSON数据再setOptionfetch(/api/daily_trend) .then(response response.json()) .then(data { var chart echarts.init(document.getElementById(trendChart)); chart.setOption({ tooltip: { trigger: axis }, legend: { data: [当日用电量, 30天均值] }, xAxis: { type: category, data: data.dates }, yAxis: { type: value }, series: [ { name: 当日用电量, type: bar, data: data.values, itemStyle: { color: #5470c6 } }, { name: 30天均值, type: line, data: data.avg_values, smooth: true } ] }); });后端对应的API只需要返回结构化数据from flask import jsonify app.route(/api/daily_trend) def api_daily_trend(): result daily_trend(df) return jsonify({ dates: result[record_time].dt.strftime(%Y-%m-%d).tolist(), values: result[power_usage].round(2).tolist(), avg_values: result[monthly_avg].round(2).tolist() })4.3 前后端交互表单筛选与数据联动做Web应用光有图表不够得让用户能自己筛选。小区管理人员常想看“某栋楼某个月”的用电情况这时候就需要筛选控件。我用GET参数方式传筛选条件URL范式为/trend?house_id1001start2024-01-01end2024-06-30。这种做法的好处是页面刷新、收藏、复制链接都能保留筛选状态比POST参数更适合报表场景。app.route(/trend) def trend_page(): house_id request.args.get(house_id, typeint) start request.args.get(start, typestr) end request.args.get(end, typestr) df_filtered filter_by_condition(df, house_id, start, end) return render_template(trend.html, datadf_filtered.to_dict(orientrecords))前端在筛选条件变化时重新构造URL并跳转而不是手动发Ajax。代码更简单而且浏览器的前进后退按钮也能正常工作。4.4 表格展示与导出分析系统最终要让用户能“拿走”数据。我会在页面底部给三个按钮导出当前筛选数据为Excel、导出分析报告为PDF用于月度例会汇报、复制图表配置项。Excel导出用pandas.to_excel配合BytesIO直接在响应里输出文件流PDF则用reportlab简单生成排版不必太复杂关键是能把保留两位小数的数据表放进去。from flask import send_file import io app.route(/export?typetrend) def export_trend(): result daily_trend(df_filtered) bio io.BytesIO() with pd.ExcelWriter(bio, engineopenpyxl) as writer: result.to_excel(writer, sheet_name日用电趋势, indexFalse) bio.seek(0) return send_file(bio, as_attachmentTrue, download_nametrend.xlsx)导出文件有几个坑一是ExcelWriter记得要bio.seek(0)不然下载下来的文件是空的二是文件名里的中文要做URL编码处理否则部分浏览器下载时文件名会乱码三是一份导出数据不要超过十万行否则Excel打开很卡考虑按月份分批导出。5. 部署与常见问题排查5.1 从开发模式到生产环境的三个改动本地调试用Flask自带的开发服务器非常方便改动代码自动重载跑起来就能看到报错。但到了生产环境有件事必须改不要用app.run()来跑服务。开发服务器是单进程的处理请求是串行的一个查询跑慢了后面所有用户都得排队。我的标准做法是用Gunicorn起多Workergunicorn -w 4 -b 0.0.0.0:8000 wsgi:app-w 4表示4个Worker进程通常按服务器CPU核心数加一配置。如果你的服务器是2核2G的小机器2个Worker就够了开太多反而内存容易被打满。wsgi:app里的wsgi指的是新建一个wsgi.py文件内容是from app import app if __name__ __main__: app.run()另外两个生产环境改动分别是关调试模式、设置密钥。app.config[DEBUG] False和app.config[SECRET_KEY] 你的随机字符串前者防止调试器暴露源码路径后者是Session签名需要的。5.2 前端图表常见问题速查表数据分析系统开发里前端的问题有时候比后端还多。我整理了实际项目中遇到频率最高的几个问题遇到直接对表排查就行。现象原因解决办法图表显示空白ECharts容器初始化时高度为0给容器div设置明确高度不要用百分比页面出现400错误日期参数格式不合法使用typestr接收后手动pd.to_datetime请求接口超时分析函数在大数据量下计算过慢加缓存装饰器或限制查询时间范围导出Excel为空文件忘记bio.seek(0)写入后把指针移回文件头页面样式错乱只有HTML没样式静态文件路径配置错误检查Flaskstatic_folder路径上传CSV后中文列名乱码编码用了utf-8而非utf-8-sig读取时指定encodingutf-8-sig5.3 数据量大时的性能优化经验我一开始开发这套系统时直接拿全量数据做聚合页面加载时间一度到了十秒以上而且每次刷新都要重新算一遍。后来我用三个方法解决第一给查询频率高的接口加缓存。对于日用电趋势这种实时性要求不高的接口用functools.lru_cache或者在不介意重启就丢缓存的环境用flask-caching设置过期时间10分钟就够。第二把数据预热到内存。系统启动时把“总表汇总后的日用电量”这种高频分析结果先算好放内存后续接口直接查内存数据不再重新读数据库。只有在用户显式筛选特定户号时才实时计算因为单户数据量小算起来很快。第三写SQL时把聚合下推到数据库。拿MySQL数据库源来说不要在Python里拉几百万行再groupby而是用SQL的GROUP BY date(record_time)在原库里直接聚合只把汇总结果拉回来网络IO差十几倍以上。6. 项目总结与经验分享我在这套系统上反复迭代了几次最有感触的一点是分析系统真正的价值不在代码写得多漂亮而在能不能稳定地把用户需要的指标算出来、把问题及时发现出来。数据清洗和数据质量校验的时间占比远超写页面和处理可视化这部分如果偷懒后面全是坑。实现的过程中还有一个小经验想分享给你们。做居民用电数据分析尽量留一个“对比视角”的按钮。比如页面上默认展示的是日电量曲线如果再加一个“同期对比”开关让用户能看到去年同期的一组曲线他们对季节性和节假日效应的理解会瞬间上了一个台阶。这个功能实现成本不高就是把时间筛选范围同时作用于两年的数据但使用频率极高是领导们最喜欢看的一种图。还有一个谈及最多也是我最后想说的不要试图在技术上追求大而全。一个小而精的Flask应用把趋势、峰谷、异常、预测四件事做到位配上清晰可导出的页面能解决的实际问题远超预期。先跑通一遍最小闭环再根据真实用户反馈增加功能这才是接地气的开发节奏。
