840个气象站点日照时数转日总太阳辐射的Python脚本

发布时间:2026/10/23 23:15:06
840个气象站点日照时数转日总太阳辐射的Python脚本

简介:一份面向科研人员与数据分析爱好者的Python代码包,可将全国840个气象站点的日照时数(SSD)转换为日总太阳辐射(SR),适用于气候变化研究、太阳能资源评估与气候模型构建等场景。资源包为rar压缩包,共3个文件,包含1个Python脚本和2个CSV数据文件(站点输入数据与辐射输出结果),整体大小275KB,轻量易用。脚本覆盖数据读取、预处理、基于林格曼方程或经验公式的转换计算、结果保存及可视化等完整流程,并包含错误处理与优化。已有6343人学习下载,适合需要批量处理气象站点辐射数据的科研人员,也适合想学习pandas数据处理与自动化脚本的Python爱好者。通过这套代码,读者可直接套用模板完成日照时数到日总太阳辐射的批量转换,也可借此理解大规模气象数据的清洗、计算与输出思路。

1. 840 个气象站点日照时数转日总太阳辐射:一份可以直接运行的 Python 脚本

某天我拿到一张全国 840 个气象站点的日照时数表,字段只有站点编号、经纬度、日期和日照小时数,任务是把它们转成日总太阳辐射。当时的直觉是“找个公式套进去不就行了”,真正动手才发现坑比想象中多:纬度要转弧度、日序不能按 365 天拍脑袋、天文辐射公式里那串三角函数写错一个符号,输出就全是 NaN。这篇文章就把这套 Python 脚本的完整实现拆开讲清楚——算法怎么选、代码怎么写、840 个站点批量跑起来怎么不出错、拿到结果怎么验证。适合正在做太阳能资源评估、农业气象或水文模型的开发者,这也是一个可以直接拿去跑、按自己数据改字段名就能用的脚本。

2. 算法与数据结构:从日照时数到天文辐射再到日总辐射的换算链路

2.1 840 个站点数据集的真实形态

先明确我们处理的是什么。全国 840 个气象站点的日照时数数据,常见的落盘格式是长表 CSV,一行代表一个站点某一天的观测记录,典型字段是:station_id、longitude、latitude、date、sunshine_hours。有些源数据会用宽表格式——每个站点一列、日期在行上,这种先melt成长表再处理。

sunshine_hours的物理含义要特别确认:它是一天中太阳直接辐照度超过 120 W/m² 的累计时长,单位通常是小时。有些数据集会把分钟数当小时数存,如果不做单位校验,后面算出来的日总辐射会整体偏大,这类问题很难从单一数值上发现,必须做整站总量复核。

提示:拿到数据第一步永远是df.describe()加df.isna().sum(),先了解每一列的缺失情况和量纲,再谈计算。

2.2 日照时数转日总辐射的四步换算

整个转换并不复杂,核心是经验公式 Angstrom-Prescott:日总太阳辐射 Rs 与太阳天文辐射 Ra 成线性关系,斜率由日照百分率决定。

Rs = (as + bs * n / N) * Ra

其中n是实际日照时数,N是当天最大可能日照时数,as、bs是经验系数。这公式看起来简单,麻烦的是N和Ra都要从天文参数推导:

  • 由日序求太阳赤纬 delta,进而求日落时角 ws;
  • 由 ws 算出最大可能日照时数 N;
  • 由日地距离修正因子和 ws 算出天文辐射 Ra;
  • 最后套 Angstrom-Prescott 公式得到日总辐射。

这四步对应的物理量必须一一算对。很多教程只给最终公式,把太阳赤纬、时角这些中间量直接跳过,导致新手把纬度、角度混着用,算出的 Ra 数值离谱。下面逐个参数拆开讲。

2.3 关键天文参数与 Angstrom-Prescott 系数的经验取值

日序(doy)直接用date.dt.dayofyear获取,注意闰年。太阳赤纬用 Cooper 近似公式,角度制下是23.45 * sin(2π * (284 + doy) / 365),这里极易踩坑的是 sin 内部的表达式用角度单位还是弧度单位。日地距离修正因子 dr 用1 + 0.033 * cos(2π * doy / 365)。日落时角 ws 是arccos(-tan(phi) * tan(delta)),其中 phi 必须转成弧度。

天文辐射 Ra 的单位是 MJ/m²/day,这是后续所有计算的基准单位。太阳常数 Gsc 取值 0.0820 MJ/m²/min,公式为24 * 60 / π * Gsc * dr * (ws * sin(phi) * sin(delta) + cos(phi) * cos(delta) * sin(ws))。最大可能日照时数 N 用24 / π * ws换算成小时。

as 和 bs 的经验值:水文模拟里常用 as=0.25、bs=0.50,两者之和接近 0.75,代表晴空条件下大气透射率上限;部分区域会用 as=0.18、bs=0.55,适用于高原清洁大气。脚本里把 as、bs 作为命令行参数暴露,换区域不必改代码。

参数含义典型值 / 单位
Gsc太阳常数0.0820 MJ/m²/min
dr日地距离修正因子0.97~1.03
delta太阳赤纬-23.45°~+23.45°
ws日落时角0~π
N最大可能日照时数0~24 h
asAngstrom 截距0.25
bsAngstrom 斜率0.50

3. 核心代码拆解:天文辐射 Ra 与日照百分率批量计算的实现

3.1 读取数据与字段归一化

不管源数据列名是什么,第一步统一映射。我用一个RENAME_MAP字典把不同命名风格的字段归一到固定名称,这样后续计算函数不用关心原始数据长什么样。

import numpy as np import pandas as pd RENAME_MAP = { "station": "station_id", "site_id": "station_id", "lat": "latitude", "lon": "longitude", "经度": "longitude", "纬度": "latitude", "日照时数": "sunshine_hours", } def load_data(path): df = pd.read_csv(path, encoding="utf-8-sig") df.columns = [RENAME_MAP.get(col, col) for col in df.columns] required = ["station_id", "longitude", "latitude", "date", "sunshine_hours"] missing = [col for col in required if col not in df.columns] if missing: raise ValueError(f"缺乏必需字段: {missing}") df["date"] = pd.to_datetime(df["date"]) df["sunshine_hours"] = pd.to_numeric(df["sunshine_hours"], errors="coerce") return df

逻辑说明:RENAME_MAP负责把中英文混用的表头统一成内部字段名,pd.to_datetime把日期字符串解析成 datetime 类型,解析失败会自动变 NaT,后面再统一处理。pd.to_numeric加errors="coerce"很关键——不这么写,一个脏字符就能让整列变成 object 类型,后续运算直接报错。参数说明:path是 CSV 路径;返回的 DataFrame 保证含五个必需字段。

3.2 一次性算出日序、赤纬、日落时角

接下来用 numpy 向量化计算天文参数。840 个站点 × 365 天,数据量在 30 万行级别,用 for 循环逐行算在 Python 里是灾难,numpy 列运算才是正解。

def calc_solar_params(df): doy = df["date"].dt.dayofyear.to_numpy() lat_rad = np.radians(df["latitude"].to_numpy()) delta = np.radians( 23.45 * np.sin(np.radians(360 * (284 + doy) / 365)) ) dr = 1 + 0.033 * np.cos(2 * np.pi * doy / 365) ws = np.arccos( np.clip(-np.tan(lat_rad) * np.tan(delta), -1.0, 1.0) ) return doy, lat_rad, delta, dr, ws

逻辑说明:doy由dayofyear直接提取,numpy 数组参与后续运算。delta的公式在外层转了np.radians,保证sin和cos收到的参数是弧度;如果漏掉这一步,赤纬会被当成弧度参与计算,结果偏差可达 20% 以上。ws的np.clip是对 arccos 定义域的兜底——高纬度地区冬季-tan(phi)*tan(delta)可能略小于 -1,不 clip 的话会输出 NaN,处理 840 个站点时这种边缘值一定会遇到。

参数说明:返回的doy范围是 1~366,lat_rad单位弧度,delta单位弧度,ws单位弧度。这些中间量全部保持弧度制,直到最后一步输出才转换单位,避免混用。

3.3 天文辐射 Ra 与日照百分率批量计算

天文辐射 Ra 的计算是核心。公式24*60/π * Gsc * dr * (ws * sin(phi) * sin(delta) + cos(phi) * cos(delta) * sin(ws))本质是对整个白天太阳辐照度的时间积分,所以24*60是把每分钟的辐照度累加成一整天。

GSC = 0.0820 # MJ/m2/min def calc_radiation(df): doy, lat_rad, delta, dr, ws = calc_solar_params(df) ra = ( 24 * 60 / np.pi * GSC * dr * ( ws * np.sin(lat_rad) * np.sin(delta) + np.cos(lat_rad) * np.cos(delta) * np.sin(ws) ) ) max_sunshine = 24 / np.pi * ws sunshine_frac = np.clip( df["sunshine_hours"].to_numpy() / max_sunshine, 0.0, 1.0 ) rs = (0.25 + 0.50 * sunshine_frac) * ra return pd.DataFrame({ "station_id": df["station_id"], "date": df["date"], "longitude": df["longitude"], "latitude": df["latitude"], "sunshine_hours": df["sunshine_hours"], "day_total_radiation_MJ": np.round(rs, 3), "day_total_radiation_kWh": np.round(rs / 3.6, 4), })

逻辑说明:ra的计算整列向量化,840 个站点 30 万行数据在一秒内完成。max_sunshine是当天理论日照上限,用24/π * ws把弧度折算成小时。sunshine_frac是实际日照时数与上限的比值,clip 到 0~1 是因为仪器误差会导致个别记录超过理论值。最终rs = (0.25 + 0.50 * sunshine_frac) * ra,晴天sunshine_frac=1时 Rs 等于 0.75 倍 Ra,符合晴空辐射经验上限。

参数说明:输出同时给 MJ/m²/day 和 kWh/m²/day 两列,光伏设计常用后者,省得二次换算。rs / 3.6是 MJ 到 kWh 的换算关系,因为 1 kWh = 3.6 MJ。

3.4 结果导出:保留原始字段与编码处理

输出 CSV 时把原始站点信息一并保留,方便后续按站点分组统计。编码用utf-8-sig是为了让 Excel 打开时中文表头不乱码,这是经常被忽略但实际很影响交付体验的细节。

def main(input_path, output_path): df = load_data(input_path) result = calc_radiation(df) result.to_csv(output_path, index=False, encoding="utf-8-sig") print(f"处理完成: {len(result)} 条记录") print(f"日总辐射范围: {result['day_total_radiation_MJ'].min():.2f} " f"~ {result['day_total_radiation_MJ'].max():.2f} MJ/m2/day")

逻辑说明:main函数把数据加载、辐射计算、导出串成完整流程,print输出的最小值和最大辐射值是最快的 sanity check——如果最大值超过 40 MJ/m²/day,大概率是纬度或日序计算出了问题,这个阈值在中国区域基本不会误报。

4. 运行与调试流程:命令行、输出文件与结果验证

4.1 环境准备与最小依赖

这个脚本只依赖 pandas 和 numpy,没有其他第三方库,Python 3.9 以上直接跑。虚拟环境安装命令:

python -m venv .venv source .venv/bin/activate # Windows 下是 .venv\Scripts\activate pip install pandas numpy

pandas 负责读取和输出,numpy 负责向量化数学运算。如果你的源数据是 Excel 格式,再加一个pip install openpyxl就行,读取时pd.read_excel替代pd.read_csv。整个环境装下来不到 200MB,和动不动就装全套地理库的方案比,这份脚本在普通办公笔记本上就能跑。

4.2 命令行参数设计

脚本入口支持四个关键参数,列名映射在代码里改,日常运行不需要动代码。

python sunshine_to_radiation.py \ --input 全国840站点_日照时数_2023.csv \ --date-col date \ --lat-col latitude \ --sun-col sunshine_hours \ --output out/daily_radiation_2023.csv

逻辑说明:--input指向原始 CSV 路径;--date-col、--lat-col、--sun-col指定源文件里的日期、纬度、日照时数列名,适配不同来源的数据集;--output是结果路径。这样做的好处是换一批数据只需要改命令行参数,不需要打开脚本改代码——对于经常换数据源的气象数据处理场景来说,这个设计能省下大量重复劳动。参数字段按argparse实现,缺省值分别是date、latitude、sunshine_hours,通常不用传。

4.3 输出文件长什么样

结果 CSV 每行是一个站点一天的数据,列结构如下:

字段类型说明
station_idstr站点编号
datedatetime日期
longitude / latitudefloat经纬度
sunshine_hoursfloat原始日照时数
day_total_radiation_MJfloat日总辐射,MJ/m²/day
day_total_radiation_kWhfloat日总辐射,kWh/m²/day

day_total_radiation_MJ是算法直接输出,第二个 kWh 列是换算值。用 Excel 打开后先看最大值和最小值——中国区域内日总辐射的合理范围大致是 0~35 MJ/m²/day,如果出现 50 甚至 100 的数字,说明公式里某个环节出了问题。

4.4 结果验证三步走

拿到输出别急着用,先做三件事验证。

第一步,单站抽检。挑一个纬度居中的站点,把这一站全年的日总辐射求和得到年总量,单位 MJ/m²,应该在 4000~7000 之间。低于 3000 说明日照百分率被严重低估,可能原始日照时数单位是分钟;高于 8000 则要查天文辐射计算。

第二步,季节趋势检查。北半球站点 6 月的日均辐射应显著高于 12 月,如果出现冬季反而高的倒挂现象,大概率是日序公式里加减天数出了错。

第三步,与公开参考数据对照。选 3~5 个站点,把多年平均的月总辐射和某再分析数据集或气候公报里的数值比对,误差在 10% 以内算正常。气候态的 Angstrom 系数本身有区域差异,超过 15% 的偏差优先怀疑系数选取是否适合本地。

5. 避坑指南与常见问题排查:单位、角度、缺失值与高原站点

5.1 数据读取类问题:NaN 与全零输出

现象:脚本跑完没有报错,但输出文件里的辐射值全是 NaN,或者某一站全为 0。

原因:最常见的是日期字段解析失败,pd.to_datetime遇到2023/1/1 00:00这类半小时粒度时间戳时会产生 NaT,dayofyear随之变成 NaN,整行乘出来全是 NaN。另一个常见原因是日照时数列存在"---"或"9999"这类缺失值标记,pd.to_numeric如果没加errors="coerce",整个列会变成字符串类型,运算结果全部是 NaN。

解决:load_data里强制pd.to_numeric(errors="coerce"),并在读取后打印缺失统计。对于“全为 0”的情况,直接检查原始 CSV 里该站点是否确实有观测,部分站点冬季日照时数记录为 0 是正常的,不一定有错。可以在输出前加一行:

print(df.groupby("station_id")["sunshine_hours"].agg(["count", "min", "max"]))

这个打印能快速发现哪些站点缺数据、哪些站点数值范围异常。

5.2 公式计算类问题:辐射值反常的大

现象:某几个站点的日总辐射超过 50 MJ/m²/day,明显超出物理上限。

原因:纬度没有转弧度。np.tan(lat_rad)和np.sin(lat_rad)用的必须是弧度,如果直接用df["latitude"]的度数参与运算,在纬度 30°~50° 时误差能达到 30% 以上。另一种可能是太阳赤纬公式里外层少了np.radians,delta变成了一组很小的数值,天文辐射 Ra 虚高。

解决:把calc_solar_params里所有三角函数输入统一打印出来检查量级。sin(delta)应该在 -0.4~0.4 之间,ws应该在 0~3.2 之间,如果ws出现复数或大于 π 的值,基本就是角度和弧度混用了。我在代码里会把lat_rad、delta、ws三个量各打印一行 min/max,跑完扫一眼心理就有底。

5.3 高纬度站点的边界问题

现象:纬度高于 66° 的站点在冬季出现 NaN,夏季辐射值却比邻近站点高出一截。

原因:日落时角公式ws = arccos(-tan(phi) * tan(delta))在极昼极夜区域会出现定义域越界——冬天-tan(phi)*tan(delta)可能小于 -1,arccos直接返回 NaN;夏天它又可能大于 1。没有 clip 的版本会静默产出空值,不会报错,这最坑人。

解决:np.clip(-np.tan(lat_rad) * np.tan(delta), -1.0, 1.0)这行代码就是在兜这个底。极夜期间日照时数为 0,辐射本身应该是 0,clip 后 arccos 返回 π,N 和 Ra 算出来是一个合理的小值,再乘以 0 的日照百分率得到 0,逻辑自洽。

5.4 云南贵州等低日照区域系统性偏低

现象:某几个站点的年总辐射比气候公报低 20% 以上,但不是所有站点都这样。

原因:Angstrom 系数 as=0.25、bs=0.50 是在特定区域的多年平均拟合结果,对四川盆地、贵州这些多云地区并不适用。这些区域日照时数普遍偏低,但实际散射辐射占比高,总辐射未必像日照时数显示的那么低。经验公式在这种气候区先天吃亏,不是代码 bug。

解决:把 as 和 bs 暴露成命令行参数--as-coef 0.26 --bs-coef 0.42,用本地站的多年实测辐射率定系数。如果没有实测数据,至少换用高原地带常用的 as=0.18、bs=0.55 试跑一遍对比差异。这类调整属于参数校准问题,不该在代码里硬编码。

5.5 840 个站点跑得极慢的问题

现象:脚本跑了几小时才出结果,CPU 占用还不到 30%。

原因:用 for 循环逐行遍历 DataFrame 计算三角函数。30 万行数据在 Python 的纯循环里要跑 20 分钟以上,而同样的计算用 numpy 向量化一秒钟内完成。

解决:脚本里calc_solar_params和calc_radiation全部用 numpy 数组整列运算,没有任何一层 Python 循环。如果你拿到别的版本写的循环实现,重构时只需要记住一条原则——所有涉及三角函数、幂运算、乘除的逐行计算,一律先.to_numpy()再整列操作,最后拼回 DataFrame。从apply改成向量化,840 个站点一年的数据从 20 分钟压缩到 3 秒,这个提升立竿见影。

6. 二次开发思路:把批量脚本改造成可配置的辐射计算工具

项目源码只是个能跑通的最小实现,实际开发里 840 个站点的数据往往是按月分文件存放,还可能有几千个文件需要批量处理,需要考虑把脚本封装成可扩展的工具形态。

我的建议是把核心函数重构成一个类,把 as/bs 系数和单位换算都作为实例属性,这样二次开发时只需实例化不同配置,就能服务多种数据源:

class RadiationConverter: def __init__(self, as_coef=0.25, bs_coef=0.50, output_unit="MJ"): self.as_coef = as_coef self.bs_coef = bs_coef self.output_unit = output_unit def convert_file(self, input_path, output_path, date_col="date", lat_col="latitude", sun_col="sunshine_hours"): df = load_data(input_path) result = calc_radiation(df, self.as_coef, self.bs_coef) if self.output_unit == "kWh": result["day_total_radiation"] = result["day_total_radiation_MJ"] / 3.6 result.to_csv(output_path, index=False, encoding="utf-8-sig")

这个封装把硬编码的 0.25 和 0.50 变成了构造参数,把年份、区域、数据源差异全部隔离在实例配置里。再往下可以加一个config.yaml描述各数据源的列名映射,用yaml.safe_load读取后自动适配不同机构提供的数据,省得每次拿到新数据都改代码。

另一个值得投入的改造方向是增加质量诊断模式。现在脚本只管算,不管数据质量,实际跑气象数据时,缺测、异常值、站点漂移才是真正的血泪来源。加一个--validate开关,输出每个站点的有效记录数、日照时数年总量、平均日辐射,跑完数据回头看一眼就能定位问题,这比算完再检查要高效得多。

我还要加一个功能:多文件批量处理。840 个站点如果按站点分文件存放,外层用一个for循环遍历目录,每个文件计算完追加写出,避免一次性加载到内存里卡死。最后再加个--skip-errors参数,某个文件格式有问题时跳过并记录日志,不让整个批次中断——这在实际生产中比任何算法优化都更实用。

从那以后,我每次拿到气象数据都会先跑一遍describe()和isna().sum(),确认单位再开始算,算完立刻看最大最小值范围,不再急着把结果交给下游。这套流程救过我好几次,希望帮到你。

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