Python windrose:风玫瑰图绘制与风向风速统计的完整实践指南
发布时间:2026/9/28 8:38:25来源:尧图网络
上个月接到一个环评配套项目的风况分析甲方要求很明确交付物里得有风玫瑰图还得有一张按16方位统计的风频、平均风速、最大风速明细表。这类任务我做过很多次但每次换数据源都会在细节上重新折腾一遍风玫瑰图的统计口径和表格数据对不上是常事。后来我固定用Python的windrose库来处理画图是一个脚本统计回填也是同一套计算逻辑图和表永远出自同一次运算交付时才不会自己打自己脸。这篇就把我现在的完整流程拆开讲包括数据清洗、扇区统计、绘图参数和导出表格每个环节以及几个版本迭代过程中踩过的坑。如果你也是气象、环境、新能源这类经常跟风数据打交道的人这套流程可以直接拿去改少走不少弯路。1. 为什么是windrose三种画法对比之后的取舍1.1 先说说另外两条路有多痛最早我做风玫瑰图是在Excel里硬画的。思路是先把风向转成角度再按16个方位统计样本数和频率最后插入一个雷达图把数据映射成玫瑰花瓣形状。单做一张图问题不大但项目里往往要按月份、按季节、按站点各出一张每张都要手动改数据区域、改图表类型一个不留神就串了行。更麻烦的是报告里的表和图必须口径一致Excel里透视表算一遍、画图又用另一套公式两边稍有出入光核对就要花掉半天。也试过专门的气象绘图软件比如WRPLOT View这类工具。功能确实全能出很标准的极坐标图还内置了EPA的统计口径。但它的数据导入格式要求严格日平均风速、逐时观测数据的字段命名都要重新整理出图之后的数字要导出到表格还得另外设置统计项批量化处理多个站点时特别不顺手。对“图要进报告、表要进附件”这种需求来说显得头重脚轻。1.2 windrose的定位和安装windrose本质是Matplotlib的一个极坐标系扩展不是一个独立软件。它的核心类叫WindRoseAxes底层还是大家熟悉的ax.bar、ax.set_legend这套Matplotlib接口所以只要是稍微碰过Python画图的人上手成本很低。它最大的价值是把“风向分扇区、按风速分级统计频率”这件事封装成了bar()函数里的一个normed参数几行代码就能画出一张标准的风玫瑰图。安装没什么特别直接用pip就行pip install windrose它会自动把matplotlib、numpy、pandas这些依赖拉齐。需要提醒的是windrose这个名字在Python生态里有一套同名实现早期有些基于旧版matplotlib的代码会报module matplotlib has no attribute rcParams这类错误遇到就升级一下matplotlib版本别在旧环境里死磕。1.3 什么情况下我不建议用windrose如果你只需要画一张单次观测的示意图总共几十条记录Excel雷达图确实更快没必要为了一个图搭一套Python环境。另外如果项目不允许安装第三方库那也只能用公司已有的气象软件。但只要数据量过千、需要分时段批量出图、或者表格里要带出统计明细我用windrose的概率几乎是100%。因为它把清洗、统计、绘图、导出完全打通在一个脚本里换数据源只要改文件路径重跑一遍什么都有了。2. 数据清洗风向风速进图之前先把三类脏数据处理掉2.1 风向字段的三种“面孔”要统一成角度风玫瑰图的横纵坐标本质是极坐标里的角度和长度而风向数据在原始表里常有三种写法用16方位字母表示比如N、NNE、SE用0到360的数字表示还有直接用中文描述的比如“东北偏北”。这三种写法混在一个项目里并不罕见我甚至见过同一份Excel里中英文方位混着来的情况。统一成角度是第一步。16方位每个方位占22.5度方位名称对应的是该扇区的中心角度正北是0度顺时针依次增加。我在项目里固定用下面这个字典做转换DIR_16 { N: 0, NNE: 22.5, NE: 45, ENE: 67.5, E: 90, ESE: 112.5, SE: 135, SSE: 157.5, S: 180, SSW: 202.5, SW: 225, WSW: 247.5, W: 270, WNW: 292.5, NW: 315, NNW: 337.5 }如果是32方位甚至36方位的数据就需要按“每个方位间隔360/n度、角度取区间中心值”的规则生成字典本质上是一样的。读数据的时候直接把字符串列map一下遇到无法识别的值先置为NaN别硬转。2.2 静风和缺测样本要单独标记不能一删了之静风在气象上指平均风速低于起报阈值的现象落到观测数据里通常表现为风速为0或低于0.5m/s风向列则是空白、C或者一个无效值。画图时这类样本不能直接参与方向统计因为风速为0的点在极坐标下没有明确方向硬塞进去会让扇区统计变得没有意义图上也容易在圆心处堆出一个难看的原点。我的处理原则是绘图统计时剔除静风但单独计算静风频率并在最终表格里单列一行“静风风速0.5m/s”。这样图上的频率代表“有效风向样本内各风向占比”表格里的静风频率则按全部观测样本计算两套口径都在页面上写清楚后面评审才不会被追问“为什么百分比加起来不是100%”。缺测也类似。风向有值、风速缺测或者反过来这种样本既不能参加绘图也不能留着当有效数。但剔除之前要记个数我习惯在脚本里预留一个变量保存剔除量交付数据说明时能直接引用。2.3 风速单位也要先统一我遇到过监测站输出的是km/h或者某些国外数据源给的是mph而风玫瑰图的色带和统计表通常用m/s。换算关系很简单1m/s等于3.6km/h约等于2.237mph。建议在读取数据后立刻统一换算不要等到算平均风速时再临时处理否则分月统计时很容易用错系数。完成清洗后我至少会保留四列时间戳、风向角度、风速(m/s)、原始风向标记。原始标记留着是为了出图前随机抽几条肉眼核对转换是否准确。3. 扇区划分和统计口径画图前不定清楚图和表必然打架3.1 扇区数量不是随便填的windrose绘图时有个nsector参数默认16。16扇区是环评和气象报告里最常见的口径每个扇区22.5度兼顾了分辨率和可读性。如果报告篇幅有限用8扇区每个45度也行看图更直观。做污染物输送通道分析时我偶尔会用到36扇区每个10度但那种图信息密度很高适合研究用不太适合放进常规交付报告。扇区确定后每个扇区的角度区间是确定的。比如16扇区第一个扇区以0度为中心区间是[-11.25, 11.25)度第二个是[11.25, 33.75)度依此类推。写统计代码时要注意环形边界风向359度应该分到0度那个扇区而不是被整除操作抛到最后一个扇区外面。最稳的做法是用取模运算import numpy as np import pandas as pd def assign_sector(degrees, n_sectors16): sector_width 360 / n_sectors sector ((degrees sector_width / 2) // sector_width).astype(int) % n_sectors return sector小于0的度数在进函数前要先取模转成0到360之间的值否则结果会偏一个扇区。3.2 频率计算的分母决定表格最终长什么样频率的分母有两种选择一是所有有效风向样本数二是包含静风在内的全部观测样本数。windrose库在normedTrue时用的是前者也就是有效风向样本的百分比。如果你在表格里直接用全部观测数当分母两个数字对不上就会被评审专家盯上。我的表格设计是主体部分用有效风向样本口径和图上完全一致表尾加两行“样本信息”一行写静风频率一行写缺测数据量。这样既回应了“图为什么没有静风”的疑问又不破坏图表的统计口径。“平均风速”和“最大风速”也有讲究。平均风速就是落在这个扇区内的所有样本风速的算术平均不需要加权最大风速建议同时记录出现时间报告里被追问“这个极值发生在哪次过程”时能直接回答。3.3 一个可复制的统计函数把上面的讨论落成代码我项目里固定用下面这个函数def calc_stats(df, dir_coldir_angle, spd_colwind_speed, n_sectors16): sector_width 360 / n_sectors df df.copy() df[sector] assign_sector(df[dir_col], n_sectors) stats df.groupby(sector).agg( sample_n(spd_col, count), avg_speed(spd_col, mean), max_speed(spd_col, max) ) stats[freq] stats[sample_n] / stats[sample_n].sum() * 100 stats[center_angle] (stats.index * sector_width) % 360 return stats这个函数只接受清洗干净的数据静风被剔除风向角度和风速单位都已统一。统计完成后还能顺便算一下总样本量sample_n.sum()绘图时的图注里要用。4. 让出图能直接放进报告windrose参数调整心得4.1 核心绘图流程我习惯用projectionwindrose创建一个子图然后调用ax.bar。下面这段代码是我的模板import matplotlib.pyplot as plt from windrose import WindroseAxes fig plt.figure(figsize(8, 8)) ax fig.add_subplot(111, projectionwindrose) ax.bar( directiondf[dir_angle], vardf[wind_speed], normedTrue, nsector16, opening0.8, edgecolorwhite, linewidth0.5, bins[0.5, 2, 4, 6, 8, 10, 20], cmapplt.get_cmap(jet) ) ax.set_legend(titleWind Speed (m/s)) fig.savefig(windrose.png, dpi300, bbox_inchestight)这里几个参数值得说清楚。normedTrue表示按百分比归一化y轴显示的是频率而非样本数这也是报告里最常见的形式。opening控制每个扇区内相邻柱条之间的空隙值越大条越窄默认0.8比较合适。bins是风速分级区间我用的这组是环境评估项目里比较常规的分档你可以按项目要求改成[1,2,3,4,5,6]之类但一定要和统计表里的分级保持一致这是图和表不打架的前提。4.2 图例和中文标注的几个细节windrose库默认图例显示的是风速区间如果你的中文环境没配置好图例里会出现方块乱码。我不太建议在这种扇形图上折腾中文字体更省事的做法是图面保持英文图注里写中文。比如北极方向加一个N标注图例写Wind Speed (m/s)标题用中文单独放在外部。如果一定要在图上显示中文需要先设置中文字体plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False第二行必加否则负号在部分字体下会显示成方块。set_legend默认会把这类符号带进图例中文环境下显示不友好我一般手动改一下标签文本或者干脆用ax.legend接管图例样式。实测下来set_legend(title...)在多数版本里够用遇到特殊需求再翻源码调整。4.3 输出图片的清晰度报告用的图至少300dpibbox_inchestight必须带上否则扇区外侧的图例和“N”指向箭头容易被裁掉。保存成PNG能满足大多数场景如果要出版级印刷建议同时导出一份SVG或PDF矢量图WindRoseAxes在矢量格式下文字细节保留得很完整后期编辑也方便。5. 把数据拽回表格图和表共用同一套统计逻辑5.1 为什么不建议直接从图对象里“拽”数据有人可能觉得“我图都画出来了数据肯定已经算好了直接从图对象里取不就行了”windrose库内部确实保存了统计结果存在ax._info里但这个结构是绘图用的中间态主要记录每个扇区内各个风速等级的频率直方图并不直接对应报告里要的“总风频、平均风速、最大风速”汇总表。而且_info是私有属性库版本一升级就可能变名或变更结构靠它取数属于自找麻烦。稳妥的做法恰恰相反先自己算好统计结果再让绘图和导出共用同一份DataFrame。图只是在展示已经算好的数字表同样从这份数字生成两边天然一致。就算哪天图例和表格里的数字被怀疑往上游查一步就能定位不用在图对象和透视表之间来回对账。5.2 统计结果转表格的标准结构我把第3节里calc_stats的结果整理成一个新的DataFrame增加两个字段扇区角度范围说明以及最大风速出现的时间。导出之前再按频率从高到低排个序用途是报告附件里的“主导风向”快速查看。stats calc_stats(df_clean, n_sectors16) stats_out stats.reset_index() stats_out[angle_range] stats_out[sector].apply( lambda s: f{int((s*22.5-11.25)%360)}~{int(((s1)*22.5-11.25)%360)} ) stats_out stats_out.sort_values(freq, ascendingFalse)这个表可以直接写进CSV或者Excel。写CSV时有个坑必须注意用encodingutf-8-sig这样Excel直接双击打开不会出现中文乱码。普通utf-8在Windows环境的Excel里经常乱utf-8-sig会带一个BOM头实测最稳。5.3 导出Excel的排版与多Sheet设计交付用的表格我一般分三个Sheet第一个放总表第二个放分月或分季度统计第三个放数据说明。用pandas的ExcelWriter配合xlsxwriter引擎还能顺手设置列宽、表头格式和冻结首行with pd.ExcelWriter(wind_stats.xlsx, enginexlsxwriter) as writer: stats_out.to_excel(writer, sheet_name风向统计总表, indexFalse) monthly_stats.to_excel(writer, sheet_name分月统计, indexFalse) notes.to_excel(writer, sheet_name数据说明, indexFalse) worksheet writer.sheets[风向统计总表] worksheet.set_column(A:F, 18)分月统计的分组逻辑很简单用pd.Grouper(freqME)按自然月聚合再对每个月调用同一个calc_stats函数循环一遍就行。这样报告撰写方引用八月、九月不同章节的风向描述时可以直接从对应Sheet里取数不用再找人二次加工。6. 复盘真实项目里翻过车的三个细节6.1 风向“来向”和数学角度的混淆有一次出图玫瑰图整体形状和当地气象站发布的参考图完全镜像东西南北全反了。排查方式很简单随机抽了十几条原始记录人工对照风向字母与数据文件里的角度值发现这份数据文件的角度坐标是“数学角度”即从正东起算、逆时针递增而气象风向是“风的来向”以正北为0度、顺时针递增。修正方法也很直接把数学角度转换过来dir_angle (90 - raw_angle) % 360这行代码在项目里救了我好几次。现在凡是收到角度型风向数据我都会先做一次抽样人工核对再决定要不要转换绝不盲信数据源的表头说明。6.2 风速分级区间的边界重复另一次导出的表格里各风速级别的频率加起来超过了100%。原因不复杂我在用pd.cut对风速分级时用了默认的右闭区间导致2m/s同时落进了“0.5~2”和“2~4”两个级别。图上的色带也连带出现重复计数整个图和表一起错。修正方式是在pd.cut里显式指定rightFalse让区间变成左闭右开。这类问题单看数据不容易发现但一旦算总和误差就现形了。现在我在统计函数里固定加上rightFalse并把分级的边界写进数据说明文档避免后手的人改错。6.3 静风频率的分母口径被评审质疑有一版交付表格里16个扇区的频率合计是97.6%剩下的2.4%被剔除的静风样本占掉了。评审专家的直接问题是“你们这个表是不是数据缺测了为什么加不满100%”其实统计口径本身没错但表里没有说明就会引发误读。从那以后凡是带静风的频率统计我都在表尾固定加一行“静风风速低于0.5m/s2.4%样本数xxx”同时图注里写明“Nxxxx不含静风”。这个习惯帮我挡掉过大量评审追问。数据说明Sheet里也会写清楚图中频率分母为有效风向样本静风样本单独列出。最后分享一个小习惯图表交付前我会额外跑一道自动核对把图上每个扇区的可见柱高和统计表里的频率做一次相关性检查确认排序一致、整体趋势吻合才发出去。这个步骤在数据量小的时候显得多余但在多站点、多月份批量出图时会救大命。另一个固定动作是在每张图的右下角标注样本量N导出表里也带上同一行说明。评审会上被问到“你这份统计基于多少数据”直接指着图回答问题就行不用现场翻原始数据表体感会专业很多。
网站建设高端定制企业官网