
1. 为什么拿河南天气当练手项目选题思路与整体架构天气数据分析在数据分析领域算是入门友好、进阶够用的典型题目。数据量大但不至于撑爆本地环境字段丰富但结构不算复杂而且可视化维度多——温度、降水、风力、空气质量随便拆一个出来都能出图。河南这个地域选择也有讲究南北跨纬度较大西部有伏牛山、东部是黄淮海平原气候差异比很多人想象中大得多。拿一个省份做分析既能保证样本量又能挖出地域内部的差异这在项目汇报时是很讨巧的亮点。我做这个项目时选的技术栈是 Python pandas requests pyecharts Flask。没有上重型的Hadoop或Spark原因很直接单机处理天气数据完全够用没必要为了显得高级引入分布式框架。数据量在几万条级别时pandas的组内聚合和透视表操作都是毫秒级响应真正耗时间的反而是数据采集和前端渲染。这套组合下来的好处是环境搭建快、调试简单、源码交付后别人也能轻松跑起来。项目的整体链路可以拆成四个环节数据采集从公开天气接口拉取河南各地市的逐日天气数据包括最高温、最低温、天气现象、风力风向、AQI空气质量指数等字段。数据清洗与入库对缺失值、异常值、重复记录做处理统一时间格式入SQLite库或直接落CSV文件。分析与指标计算用pandas做按城市、按月、按季节的聚合统计计算极端天气频次、气温变化趋势、空气质量的季节性规律等。可视化大屏展示用pyecharts生成图表嵌入Flask搭建的Web服务中最终拼成一个可交互的河南天气可视化大屏。整个项目跑通之后交付物包含源码、配套文档、调试记录和可直接演示的可视化大屏。下面我把每个环节的实际操作细节、踩过的坑、调试思路都展开讲清楚内容偏落地导向照着做能复现出一个完整版本。2. 天气数据从哪里来采集方案对比与落地实现2.1 数据源选型API接口优先于网页爬虫做天气数据分析第一步就是解决数据源问题。市面上的方案大概有三种直接爬气象网站、调免费天气API、找现成的历史数据集。我最早想的是写爬虫抓页面数据毕竟这类教程网上很多。但实际操作下来发现页面结构改版频繁反爬机制也越来越严格验证码、请求频率限制、动态渲染这些都能把进度拖垮。更麻烦的是抓下来的数据要做大量HTML解析字段提取时一不留神就出错。爬虫技术本身值得学但如果你核心目标是做数据分析与可视化没必要在数据采集环节耗太多精力。最终我选用的是免费天气API方案。注册后拿到API Key通过HTTP请求按城市拉取数据返回的是结构化JSON直接就能转DataFrame。稳定性比爬网页高得多而且不涉及复杂的解析逻辑。这里有个操作细节值得提一下调用天气API时请求频率通常有限制每分钟几次到几十次不等。河南有17个地级市含济源示范区每个城市拉一年的逐日数据也就是365条记录。如果把请求间隔设为1秒不到20秒就能拉完。千万别写个循环不加sleep地猛请求否则很容易触发限流或封Key。以下是核心采集代码的简化版本import requests import pandas as pd import time def fetch_weather(city_code, api_key, date_range): all_data [] for date in date_range: url https://api.xxx.com/weather/daily params { key: api_key, city: city_code, date: date } resp requests.get(url, paramsparams) if resp.status_code 200: data resp.json() all_data.append({ date: date, city: city_code, temp_max: data[result][temp_max], temp_min: data[result][temp_min], weather: data[result][weather], wind_dir: data[result][wind_dir], wind_level: data[result][wind_level], aqi: data[result][aqi] }) time.sleep(1) return pd.DataFrame(all_data)注意代码里的city_code要用API平台提供的行政区划编码比如郑州是410100洛阳是410300别用市区拼音缩写否则容易查不到数据或返回错误城市。这个细节我一开始就搞错了用zhengzhou传参结果接口返回了空数据后来核对接口文档才发现要求的是标准行政区划代码。2.2 字段说明与存储结构设计天气API返回的字段比想象中丰富但真正用得上的核心字段就那么几个。我最终整理出来的表结构如下字段名类型含义清洗要点datestr/date日期统一为YYYY-MM-DD格式citystr城市名称统一为中文全称temp_maxfloat当日最高气温排除50℃的异常值temp_minfloat当日最低气温排除-30℃的异常值weatherstr天气现象如晴、多云、小雨等字面统一wind_dirstr风向东北风、西南风等wind_levelstr风力等级3-4级、5-6级等切出数值aqiint空气质量指数缺失值用中位数填充存储这块我选的是SQLite理由很实际单文件、零配置、pandas直接通过read_sql_table或read_csv都能读写。如果你的数据量更大也可以换MySQL或PostgreSQL代码改动也就一行连接串的事。以下建表语句供参考CREATE TABLE weather_daily ( id INTEGER PRIMARY KEY AUTOINCREMENT, date TEXT NOT NULL, city TEXT NOT NULL, temp_max REAL, temp_min REAL, weather TEXT, wind_dir TEXT, wind_level TEXT, aqi INTEGER );建表时可以顺手加一个唯一索引把date和city组合设为唯一键这样重复拉数据时不会产生脏记录CREATE UNIQUE INDEX idx_date_city ON weather_daily(date, city);有了这个索引后续做增量更新时还能用INSERT OR REPLACE直接覆盖写入即方便又防重。2.3 定时更新机制让大屏数据活起来可视化大屏如果只是静态展示历史数据演示效果会打折扣。真实的业务场景里大屏应该每天自动更新。我的做法是写一个定时任务脚本每天固定时间抓取前一天的数据补进SQLite。Windows环境直接用系统的任务计划程序就能实现Linux服务器用crontab更简单添加一行配置0 6 * * * cd /opt/weather_project python3 update_data.py更新脚本里要注意的一个关键点查询前一天不能用date.today()直接去减而要使用系统时区统一的日期计算否则跨日执行时容易重复或漏采。更稳妥的方式是判断数据表里已有数据的最大日期从最大日期继续往后拉形成断点续采的效果。def get_last_date_from_db(): df pd.read_sql_query(SELECT MAX(date) as max_date FROM weather_daily, conn) return df[max_date][0]这个逻辑对长期运行的采集任务非常重要。我的项目连续跑了一个多月没有出现过数据重复或漏档的情况靠的就是这个从最大日期继续的增量策略。3. 数据清洗与分析pandas操作的实战细节3.1 数据质量问题的真实场景缺失、异常、重复很多人拿到数据就直接groupby然后画图做过真实项目的人都知道这个流程太理想化了。天气数据虽然规范程度高但实际采集后仍然会遇到几类问题。第一类是缺失值。API偶尔会因为网络抖动或上游服务异常返回部分空字段尤其是AQI这个字段经常某几天查不到。缺失的处理策略要根据字段重要性区分温度字段对分析影响大缺失时可以用前后两天的均值做插值AQI缺失比例如果低于5%用中位数填充即可如果某个字段缺失超过30%干脆放弃该字段的分析别硬填。第二类是异常值。我清洗时就发现过一条记录7月份的最高温报了54摄氏度明显是异常数据。处理思路很简单温度值的合理区间可以根据历史极值设定如果超过阈值就标记为异常并剔除。具体实现可以用pandas的between方法做范围过滤。df df[df[temp_max].between(-10, 45)] df df[df[temp_min].between(-30, 35)]第三类是重复记录。虽然建了唯一索引但采集脚本如果跑了两遍且没有走replace逻辑还是可能出现重复。检查重复最直接的方法是duplicated_rows df[df.duplicated(subset[date, city], keepFalse)]这一步要放在前面做因为重复记录会影响后面所有聚合统计的结果。我习惯在数据入库前先做一次全量查重在入库后每周再做一次定期校验。3.2 风力和风向的处理技巧天气数据里最容易被忽略但最值得加工的是风力字段。API返回的wind_level通常是3-4级5-6级这样的区间字符串直接分析没法用。我做了一个映射函数把区间转成数值区间的中位数比如3-4级取3.55-6级取5.5。这样既保留了风力的相对大小又能参与数值计算。风向字段比较特殊属于环形数据360度和0度本质上是同一个方向。如果后续想做风向的统计可视化最科学的做法是先按照气象学的16方位分类北、东北偏北、东北……再统计频次。对于普通分析直接处理字符串频次就够了不必过度设计。我实际处理时还发现一个规律河南的夏季主导风向多为偏南风冬季多为偏北风。这个现象在后续做可视化时可以通过风力风向玫瑰图直观展示是汇报时很出彩的一张图。3.3 核心分析指标的计算逻辑经过清洗之后数据就可以进入正式分析阶段了。我在这个项目里最终敲定了五类核心指标每类指标都对应大屏上的一个或一组图表月均气温变化趋势按月份分组计算月均最高温、月均最低温反映全年冷暖变化曲线。这是所有天气分析项目的标配图表。各城市年均气温对比计算每座城市全年平均气温横向比较河南不同区域的冷热差异。这能支撑河南不同城市气候差异这个关键结论。降水/天气现象分布统计统计各类天气现象晴天、多云、阴、小雨、中雨等的出现天数占比用饼图或横向柱状图展示。空气质量月度变化按月统计AQI均值观察冬春季节和夏秋季节的污染差异。极端天气频次统计最高温超过35度高温日、最低温低于0度低温日的天数按城市汇总。这些指标的计算用pandas都能在十几行代码内完成核心逻辑是groupby加agg。举个例子计算各城市月均最高温result ( df.groupby([df[city], df[date].str[:7]])[temp_max] .mean() .reset_index() ) result.columns [city, month, avg_temp_max]这类代码没有复杂的算法但在写的时候要特别注意groupby之后是否需要reset_index。如果不reset结果是一个MultiIndex的Series后续转DataFrame或者merge时容易踩坑。我在调试时就在这里卡过一次报错信息指向不明最后用type()排查才发现是索引结构的问题。4. 可视化大屏搭建从pyecharts到前端布局4.1 大屏布局思路六张图怎么排才能讲好故事可视化大屏的核心不是炫技而是把数据结论按照阅读逻辑组织起来。我参考了网上很多后台管理大屏的布局风格最终定下来的整体框架是上下分层中间突出。顶部是标题栏显示河南天气数据分析平台下面放当前数据更新的截止日期和数据总量。中间区域最显眼的位置放地图——用河南地图做底图各城市用热力点或柱状标记展示年均气温或AQI均值。地图两侧分列月均气温折线图和天气现象分布饼图。底部一行排开三个图表各城市极端天气频次横向柱状图、空气质量月度变化折线图、风力风向统计玫瑰图。这样布局的好处是逻辑清晰地图先让人看到空间维度的全貌折线图和饼图承接时间维度和状态分布底部三个图补充细节指标。观看者可以按照从上到下、从整体到局部的节奏来理解数据。技术实现上我没有手写复杂的JavaScript而是用pyecharts生成图表后通过Flask把JSON配置和div容器暴露给前端页面。这样做的好处是后端写Python逻辑前端只做简单嵌入一个人能hold住全栈开发。4.2 Flask服务与大屏页面集成Flask在项目中承担的角色是静态文件服务器和JSON数据接口。我建了以下路由from flask import Flask, render_template, jsonify app Flask(__name__) app.route(/) def dashboard(): return render_template(dashboard.html) app.route(/api/weather/summary) def api_summary(): # 从SQLite读取聚合结果转JSON返回 return jsonify(summary_data) app.route(/api/weather/charts) def api_charts(): # 返回图表配置和渲染所需的JSON return jsonify(chart_data) if __name__ __main__: app.run(host0.0.0.0, port5000, debugTrue)大屏页面本身用的是HTML ECharts。ECharts是百度开源的可视化库图表类型丰富交互体验好而且配置方式直观。虽然我用了pyecharts生成部分图表但有的图表直接在前端用ECharts配置反而更灵活。pyecharts的本质是把Python数据转成ECharts配置所以两种方式之间可以随时切换。考虑到实时刷新我在前端用了setInterval定时器每10分钟请求一次/api/weather/charts获取最新数据一旦返回结果变化就调用setOption刷新图表。这个机制在演示时效果特别好数据更新后大屏会自动变化不需要手动刷新浏览器。4.3 图表选型与配色细节图表选型一定要匹配数据特性。我有几个亲测有效的选型经验时间序列数据用折线图。月均气温用平滑曲线比柱状图更自然地体现变化趋势。频次分布用柱状图。各城市的极端天气频次柱状图能直观排序对比。占比数据用环形饼图。天气现象分布用环形图比普通饼图美观占屏幕面积也更小。AQI带有序性用渐变色的柱状图或面积图。青绿色到橙红色的渐变本身就暗示空气质量从好到差。风向数据用玫瑰图。ECharts的polar坐标系支持玫瑰图展示一个图就能同时表达方向与频次两个维度。配色方案我采用的是深色系大屏风格。深蓝色背景#0a1428、白色文字、浅青色和橙黄色为主图表色。深色底色的好处是视觉聚焦效果好对比度高而且投影演示时不刺眼。曾经试过浅色背景的大屏方案会议室灯光一打就什么都看不清后来再也不用了。大屏里的数字有单位的一定要带单位。温度要写℃AQI不用加单位但要在图表标题里注明空气质量指数AQI。很多初学者的大屏做得信息密度很高但缺单位、缺标题、缺图例导致别人看不懂每个图表在表达什么。细节这块宁可多写几个字也不要让观看者猜。5. 调试与部署从报错到能演示的完整过程5.1 一次典型的调试过程复盘图表数据渲染不出来我把调试过程中印象最深的一个问题完整复盘一下。现象是大屏页面打开后其他图表都正常唯独各城市极端天气频次的横向柱状图始终空白控制台报错显示ECharts的data为空。一开始直觉认为是SQLite里没有查到数据但用SQL单独执行后是有结果的。后来打印接口返回的JSON发现极端天气频次为0的那些城市pandas聚合后不会生成数值0的行而是直接没有这个城市的记录。因为聚合逻辑是先筛选高温日再按城市计数所以某个城市全年没有超过35度的日子时groupby之后就不会出现这个城市。ECharts的柱状图数据传过去缺少某一类数据时那个城市就没有对应的柱子但也不至于整个图表空白。真正的问题出在x轴和y轴数据的对齐上我传给xAxis的城市列表是所有城市的全量列表而series里的数据只有有记录的城市长度不匹配ECharts的坐标系对不上图表自然就崩了。解决办法是在后端做一次补零操作。把所有城市的列表作为一个参照表用pandas的merge或reindex把缺失城市补上0值all_cities [郑州, 洛阳, 开封, 安阳, 新乡, ...] data data.set_index(city).reindex(all_cities, fill_value0).reset_index()补零之后x轴和series的数据长度就一致了图表立刻正常渲染。这个问题排查了一个多小时教训很深刻做图表接口时后端聚合结果的稀疏性一定要考虑到宁可做一次补零也不要让前端自己去对齐数据。5.2 pyecharts与Flask集成时的版本兼容问题pyecharts从v1版本开始API做了大幅调整。如果你网上找的老教程用的是0.5.x版本很多写法都不适用于新版本。我项目里用的是pyecharts 1.3.1与Flask集成的正确姿势是调用.render_embed()方法把图表生成的HTML片段直接嵌入到模板中。from pyecharts.charts import Line from pyecharts import options as opts def generate_temp_line_chart(): line ( Line() .add_xaxis(months) .add_yaxis(平均最高温, high_temps, is_smoothTrue) .add_yaxis(平均最低温, low_temps, is_smoothTrue) .set_global_opts(title_optsopts.TitleOpts(title月均气温变化)) ) return line.render_embed()在模板中这样调用div idtemp-line-chart{{ temp_line_chart|safe }}/div注意|safe过滤器是必须的否则Flask会把HTML转义图表直接显示成一堆标签源码。这个细节也是初学者极易踩的坑我第一次集成时就因为忘了加|safe页面显示的是整段ECharts配置代码看起来完全无从下手。5.3 部署到Linux服务器Nginx反向代理与Gunicorn本地调试通过不代表部署没问题。我把项目放到一台Ubuntu服务器上时调试模式下的Flask开发服务器不能直接用于生产环境性能和并发能力都不够。推荐使用Gunicorn作为WSGI服务器Nginx作为反向代理。Gunicorn启动命令很简单gunicorn -w 4 -b 127.0.0.1:5000 app:app-w参数是worker进程数一般设置为CPU核心数加1不必贪多。-b是指定监听本地的5000端口再用Nginx把80端口转发过去。Nginx的关键配置片段server { listen 80; server_name your_domain_or_ip; location / { proxy_pass http://127.0.0.1:5000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }部署完别忘了关闭Flask的debug模式并把SQLite数据库文件路径改成绝对路径。我在这件事上栽过跟头用相对路径启动Gunicorn后工作目录不同导致找不到数据库文件大屏数据全部为空。改成绝对路径并确保运行用户有读写权限后问题才解决。5.4 调试效率提升日志与断言最后分享一个能大幅提升调试效率的小技巧在你的数据采集、聚合、接口三个环节各加一行日志打印。日志不追求花哨但一定要包含关键信息比如采集到多少条记录、清洗后还剩多少条、接口返回了几张表的数据。数据管道哪一步断了看一眼日志就能定位。logger.info(f采集完成{len(raw_df)} 条) logger.info(f清洗完成{len(clean_df)} 条剔除 {len(raw_df) - len(clean_df)} 条异常记录)除此之外对核心函数加断言是防止静默出错的好习惯。比如清洗函数返回后断言date字段的格式统一、temp_max都大于temp_min等。断言失败说明逻辑有问题而不是等到大屏上出现诡异数据才反向排查。我在实际项目里就通过一条断言发现过数据类型错误某一次API返回的温度值是字符串33.5而pandas判定列类型为object后续做算数运算时全部报错或计算错误。加断言后一眼就看到了问题后来在字段解析时就统一做float强转再也没发生过类似情况。做数据类项目的调试思路核心就是分段验证、尽早暴露。不要等所有代码都写完了再统调而是每完成一个环节就手动验证输出结果。采集完打印前几行看字段格式清洗完用describe看描述性统计聚合完抽查几个数值跟SQL查询结果对比。养成这个习惯后排查问题的时间至少能砍掉一半。这个项目从立项到跑通大屏我前后大约用了一周时间其中一半时间花在调试和数据质量排查上。如果只做静态分析不加定时更新也不加大屏两三天就能完成。但正是加了这些额外的东西整个项目的完整度和可演示性才上了一个台阶。代码和文档目前都整理在一个仓库里分好了目录注释写了关键逻辑跑之前只需要在配置里替换自己的API Key即可。