日照時數(shù)轉(zhuǎn)日總太陽輻射的Python腳本)
簡介一份面向科研人員與數(shù)據(jù)分析愛好者的Python代碼包可將全國840個氣象站點(diǎn)的日照時數(shù)SSD轉(zhuǎn)換為日總太陽輻射SR適用于氣候變化研究、太陽能資源評估與氣候模型構(gòu)建等場景。資源包為rar壓縮包共3個文件包含1個Python腳本和2個CSV數(shù)據(jù)文件站點(diǎn)輸入數(shù)據(jù)與輻射輸出結(jié)果整體大小275KB輕量易用。腳本覆蓋數(shù)據(jù)讀取、預(yù)處理、基于林格曼方程或經(jīng)驗公式的轉(zhuǎn)換計算、結(jié)果保存及可視化等完整流程并包含錯誤處理與優(yōu)化。已有6343人學(xué)習(xí)下載適合需要批量處理氣象站點(diǎn)輻射數(shù)據(jù)的科研人員也適合想學(xué)習(xí)pandas數(shù)據(jù)處理與自動化腳本的Python愛好者。通過這套代碼讀者可直接套用模板完成日照時數(shù)到日總太陽輻射的批量轉(zhuǎn)換也可借此理解大規(guī)模氣象數(shù)據(jù)的清洗、計算與輸出思路。1. 840 個氣象站點(diǎn)日照時數(shù)轉(zhuǎn)日總太陽輻射一份可以直接運(yùn)行的 Python 腳本某天我拿到一張全國 840 個氣象站點(diǎn)的日照時數(shù)表字段只有站點(diǎn)編號、經(jīng)緯度、日期和日照小時數(shù)任務(wù)是把它們轉(zhuǎn)成日總太陽輻射。當(dāng)時的直覺是“找個公式套進(jìn)去不就行了”真正動手才發(fā)現(xiàn)坑比想象中多緯度要轉(zhuǎn)弧度、日序不能按 365 天拍腦袋、天文輻射公式里那串三角函數(shù)寫錯一個符號輸出就全是 NaN。這篇文章就把這套 Python 腳本的完整實(shí)現(xiàn)拆開講清楚——算法怎么選、代碼怎么寫、840 個站點(diǎn)批量跑起來怎么不出錯、拿到結(jié)果怎么驗證。適合正在做太陽能資源評估、農(nóng)業(yè)氣象或水文模型的開發(fā)者這也是一個可以直接拿去跑、按自己數(shù)據(jù)改字段名就能用的腳本。2. 算法與數(shù)據(jù)結(jié)構(gòu)從日照時數(shù)到天文輻射再到日總輻射的換算鏈路2.1 840 個站點(diǎn)數(shù)據(jù)集的真實(shí)形態(tài)先明確我們處理的是什么。全國 840 個氣象站點(diǎn)的日照時數(shù)數(shù)據(jù)常見的落盤格式是長表 CSV一行代表一個站點(diǎn)某一天的觀測記錄典型字段是station_id、longitude、latitude、date、sunshine_hours。有些源數(shù)據(jù)會用寬表格式——每個站點(diǎn)一列、日期在行上這種先melt成長表再處理。sunshine_hours的物理含義要特別確認(rèn)它是一天中太陽直接輻照度超過 120 W/m2 的累計時長單位通常是小時。有些數(shù)據(jù)集會把分鐘數(shù)當(dāng)小時數(shù)存如果不做單位校驗后面算出來的日總輻射會整體偏大這類問題很難從單一數(shù)值上發(fā)現(xiàn)必須做整站總量復(fù)核。提示拿到數(shù)據(jù)第一步永遠(yuǎn)是df.describe()加df.isna().sum()先了解每一列的缺失情況和量綱再談計算。2.2 日照時數(shù)轉(zhuǎn)日總輻射的四步換算整個轉(zhuǎn)換并不復(fù)雜核心是經(jīng)驗公式 Angstrom-Prescott日總太陽輻射 Rs 與太陽天文輻射 Ra 成線性關(guān)系斜率由日照百分率決定。Rs (as bs * n / N) * Ra其中n是實(shí)際日照時數(shù)N是當(dāng)天最大可能日照時數(shù)as、bs是經(jīng)驗系數(shù)。這公式看起來簡單麻煩的是N和Ra都要從天文參數(shù)推導(dǎo)由日序求太陽赤緯 delta進(jìn)而求日落時角 ws由 ws 算出最大可能日照時數(shù) N由日地距離修正因子和 ws 算出天文輻射 Ra最后套 Angstrom-Prescott 公式得到日總輻射。這四步對應(yīng)的物理量必須一一算對。很多教程只給最終公式把太陽赤緯、時角這些中間量直接跳過導(dǎo)致新手把緯度、角度混著用算出的 Ra 數(shù)值離譜。下面逐個參數(shù)拆開講。2.3 關(guān)鍵天文參數(shù)與 Angstrom-Prescott 系數(shù)的經(jīng)驗取值日序doy直接用date.dt.dayofyear獲取注意閏年。太陽赤緯用 Cooper 近似公式角度制下是23.45 * sin(2π * (284 doy) / 365)這里極易踩坑的是 sin 內(nèi)部的表達(dá)式用角度單位還是弧度單位。日地距離修正因子 dr 用1 0.033 * cos(2π * doy / 365)。日落時角 ws 是arccos(-tan(phi) * tan(delta))其中 phi 必須轉(zhuǎn)成弧度。天文輻射 Ra 的單位是 MJ/m2/day這是后續(xù)所有計算的基準(zhǔn)單位。太陽常數(shù) Gsc 取值 0.0820 MJ/m2/min公式為24 * 60 / π * Gsc * dr * (ws * sin(phi) * sin(delta) cos(phi) * cos(delta) * sin(ws))。最大可能日照時數(shù) N 用24 / π * ws換算成小時。as 和 bs 的經(jīng)驗值水文模擬里常用 as0.25、bs0.50兩者之和接近 0.75代表晴空條件下大氣透射率上限部分區(qū)域會用 as0.18、bs0.55適用于高原清潔大氣。腳本里把 as、bs 作為命令行參數(shù)暴露換區(qū)域不必改代碼。參數(shù)含義典型值 / 單位Gsc太陽常數(shù)0.0820 MJ/m2/mindr日地距離修正因子0.97~1.03delta太陽赤緯-23.45°~23.45°ws日落時角0~πN最大可能日照時數(shù)0~24 hasAngstrom 截距0.25bsAngstrom 斜率0.503. 核心代碼拆解天文輻射 Ra 與日照百分率批量計算的實(shí)現(xiàn)3.1 讀取數(shù)據(jù)與字段歸一化不管源數(shù)據(jù)列名是什么第一步統(tǒng)一映射。我用一個RENAME_MAP字典把不同命名風(fēng)格的字段歸一到固定名稱這樣后續(xù)計算函數(shù)不用關(guān)心原始數(shù)據(jù)長什么樣。import numpy as np import pandas as pd RENAME_MAP { station: station_id, site_id: station_id, lat: latitude, lon: longitude, 經(jīng)度: longitude, 緯度: latitude, 日照時數(shù): sunshine_hours, } def load_data(path): df pd.read_csv(path, encodingutf-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], errorscoerce) return df邏輯說明RENAME_MAP負(fù)責(zé)把中英文混用的表頭統(tǒng)一成內(nèi)部字段名pd.to_datetime把日期字符串解析成 datetime 類型解析失敗會自動變 NaT后面再統(tǒng)一處理。pd.to_numeric加errorscoerce很關(guān)鍵——不這么寫一個臟字符就能讓整列變成 object 類型后續(xù)運(yùn)算直接報錯。參數(shù)說明path是 CSV 路徑返回的 DataFrame 保證含五個必需字段。3.2 一次性算出日序、赤緯、日落時角接下來用 numpy 向量化計算天文參數(shù)。840 個站點(diǎn) × 365 天數(shù)據(jù)量在 30 萬行級別用 for 循環(huán)逐行算在 Python 里是災(zāi)難numpy 列運(yùn)算才是正解。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 數(shù)組參與后續(xù)運(yùn)算。delta的公式在外層轉(zhuǎn)了np.radians保證sin和cos收到的參數(shù)是弧度如果漏掉這一步赤緯會被當(dāng)成弧度參與計算結(jié)果偏差可達(dá) 20% 以上。ws的np.clip是對 arccos 定義域的兜底——高緯度地區(qū)冬季-tan(phi)*tan(delta)可能略小于 -1不 clip 的話會輸出 NaN處理 840 個站點(diǎn)時這種邊緣值一定會遇到。參數(shù)說明返回的doy范圍是 1~366lat_rad單位弧度delta單位弧度ws單位弧度。這些中間量全部保持弧度制直到最后一步輸出才轉(zhuǎn)換單位避免混用。3.3 天文輻射 Ra 與日照百分率批量計算天文輻射 Ra 的計算是核心。公式24*60/π * Gsc * dr * (ws * sin(phi) * sin(delta) cos(phi) * cos(delta) * sin(ws))本質(zhì)是對整個白天太陽輻照度的時間積分所以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 個站點(diǎn) 30 萬行數(shù)據(jù)在一秒內(nèi)完成。max_sunshine是當(dāng)天理論日照上限用24/π * ws把弧度折算成小時。sunshine_frac是實(shí)際日照時數(shù)與上限的比值clip 到 0~1 是因為儀器誤差會導(dǎo)致個別記錄超過理論值。最終rs (0.25 0.50 * sunshine_frac) * ra晴天sunshine_frac1時 Rs 等于 0.75 倍 Ra符合晴空輻射經(jīng)驗上限。參數(shù)說明輸出同時給 MJ/m2/day 和 kWh/m2/day 兩列光伏設(shè)計常用后者省得二次換算。rs / 3.6是 MJ 到 kWh 的換算關(guān)系因為 1 kWh 3.6 MJ。3.4 結(jié)果導(dǎo)出保留原始字段與編碼處理輸出 CSV 時把原始站點(diǎn)信息一并保留方便后續(xù)按站點(diǎn)分組統(tǒng)計。編碼用utf-8-sig是為了讓 Excel 打開時中文表頭不亂碼這是經(jīng)常被忽略但實(shí)際很影響交付體驗的細(xì)節(jié)。def main(input_path, output_path): df load_data(input_path) result calc_radiation(df) result.to_csv(output_path, indexFalse, encodingutf-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函數(shù)把數(shù)據(jù)加載、輻射計算、導(dǎo)出串成完整流程print輸出的最小值和最大輻射值是最快的 sanity check——如果最大值超過 40 MJ/m2/day大概率是緯度或日序計算出了問題這個閾值在中國區(qū)域基本不會誤報。4. 運(yùn)行與調(diào)試流程命令行、輸出文件與結(jié)果驗證4.1 環(huán)境準(zhǔn)備與最小依賴這個腳本只依賴 pandas 和 numpy沒有其他第三方庫Python 3.9 以上直接跑。虛擬環(huán)境安裝命令python -m venv .venv source .venv/bin/activate # Windows 下是 .venv\Scripts\activate pip install pandas numpypandas 負(fù)責(zé)讀取和輸出numpy 負(fù)責(zé)向量化數(shù)學(xué)運(yùn)算。如果你的源數(shù)據(jù)是 Excel 格式再加一個pip install openpyxl就行讀取時pd.read_excel替代pd.read_csv。整個環(huán)境裝下來不到 200MB和動不動就裝全套地理庫的方案比這份腳本在普通辦公筆記本上就能跑。4.2 命令行參數(shù)設(shè)計腳本入口支持四個關(guān)鍵參數(shù)列名映射在代碼里改日常運(yùn)行不需要動代碼。python sunshine_to_radiation.py \ --input 全國840站點(diǎn)_日照時數(shù)_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指定源文件里的日期、緯度、日照時數(shù)列名適配不同來源的數(shù)據(jù)集--output是結(jié)果路徑。這樣做的好處是換一批數(shù)據(jù)只需要改命令行參數(shù)不需要打開腳本改代碼——對于經(jīng)常換數(shù)據(jù)源的氣象數(shù)據(jù)處理場景來說這個設(shè)計能省下大量重復(fù)勞動。參數(shù)字段按argparse實(shí)現(xiàn)缺省值分別是date、latitude、sunshine_hours通常不用傳。4.3 輸出文件長什么樣結(jié)果 CSV 每行是一個站點(diǎn)一天的數(shù)據(jù)列結(jié)構(gòu)如下字段類型說明station_idstr站點(diǎn)編號datedatetime日期longitude / latitudefloat經(jīng)緯度sunshine_hoursfloat原始日照時數(shù)day_total_radiation_MJfloat日總輻射MJ/m2/dayday_total_radiation_kWhfloat日總輻射kWh/m2/dayday_total_radiation_MJ是算法直接輸出第二個 kWh 列是換算值。用 Excel 打開后先看最大值和最小值——中國區(qū)域內(nèi)日總輻射的合理范圍大致是 0~35 MJ/m2/day如果出現(xiàn) 50 甚至 100 的數(shù)字說明公式里某個環(huán)節(jié)出了問題。4.4 結(jié)果驗證三步走拿到輸出別急著用先做三件事驗證。第一步單站抽檢。挑一個緯度居中的站點(diǎn)把這一站全年的日總輻射求和得到年總量單位 MJ/m2應(yīng)該在 4000~7000 之間。低于 3000 說明日照百分率被嚴(yán)重低估可能原始日照時數(shù)單位是分鐘高于 8000 則要查天文輻射計算。第二步季節(jié)趨勢檢查。北半球站點(diǎn) 6 月的日均輻射應(yīng)顯著高于 12 月如果出現(xiàn)冬季反而高的倒掛現(xiàn)象大概率是日序公式里加減天數(shù)出了錯。第三步與公開參考數(shù)據(jù)對照。選 3~5 個站點(diǎn)把多年平均的月總輻射和某再分析數(shù)據(jù)集或氣候公報里的數(shù)值比對誤差在 10% 以內(nèi)算正常。氣候態(tài)的 Angstrom 系數(shù)本身有區(qū)域差異超過 15% 的偏差優(yōu)先懷疑系數(shù)選取是否適合本地。5. 避坑指南與常見問題排查單位、角度、缺失值與高原站點(diǎn)5.1 數(shù)據(jù)讀取類問題NaN 與全零輸出現(xiàn)象腳本跑完沒有報錯但輸出文件里的輻射值全是 NaN或者某一站全為 0。原因最常見的是日期字段解析失敗pd.to_datetime遇到2023/1/1 00:00這類半小時粒度時間戳?xí)r會產(chǎn)生 NaTdayofyear隨之變成 NaN整行乘出來全是 NaN。另一個常見原因是日照時數(shù)列存在---或9999這類缺失值標(biāo)記pd.to_numeric如果沒加errorscoerce整個列會變成字符串類型運(yùn)算結(jié)果全部是 NaN。解決load_data里強(qiáng)制pd.to_numeric(errorscoerce)并在讀取后打印缺失統(tǒng)計。對于“全為 0”的情況直接檢查原始 CSV 里該站點(diǎn)是否確實(shí)有觀測部分站點(diǎn)冬季日照時數(shù)記錄為 0 是正常的不一定有錯??梢栽谳敵銮凹右恍衟rint(df.groupby(station_id)[sunshine_hours].agg([count, min, max]))這個打印能快速發(fā)現(xiàn)哪些站點(diǎn)缺數(shù)據(jù)、哪些站點(diǎn)數(shù)值范圍異常。5.2 公式計算類問題輻射值反常的大現(xiàn)象某幾個站點(diǎn)的日總輻射超過 50 MJ/m2/day明顯超出物理上限。原因緯度沒有轉(zhuǎn)弧度。np.tan(lat_rad)和np.sin(lat_rad)用的必須是弧度如果直接用df[latitude]的度數(shù)參與運(yùn)算在緯度 30°~50° 時誤差能達(dá)到 30% 以上。另一種可能是太陽赤緯公式里外層少了np.radiansdelta變成了一組很小的數(shù)值天文輻射 Ra 虛高。解決把calc_solar_params里所有三角函數(shù)輸入統(tǒng)一打印出來檢查量級。sin(delta)應(yīng)該在 -0.4~0.4 之間ws應(yīng)該在 0~3.2 之間如果ws出現(xiàn)復(fù)數(shù)或大于 π 的值基本就是角度和弧度混用了。我在代碼里會把lat_rad、delta、ws三個量各打印一行 min/max跑完掃一眼心理就有底。5.3 高緯度站點(diǎn)的邊界問題現(xiàn)象緯度高于 66° 的站點(diǎn)在冬季出現(xiàn) NaN夏季輻射值卻比鄰近站點(diǎn)高出一截。原因日落時角公式ws arccos(-tan(phi) * tan(delta))在極晝極夜區(qū)域會出現(xiàn)定義域越界——冬天-tan(phi)*tan(delta)可能小于 -1arccos直接返回 NaN夏天它又可能大于 1。沒有 clip 的版本會靜默產(chǎn)出空值不會報錯這最坑人。解決np.clip(-np.tan(lat_rad) * np.tan(delta), -1.0, 1.0)這行代碼就是在兜這個底。極夜期間日照時數(shù)為 0輻射本身應(yīng)該是 0clip 后 arccos 返回 πN 和 Ra 算出來是一個合理的小值再乘以 0 的日照百分率得到 0邏輯自洽。5.4 云南貴州等低日照區(qū)域系統(tǒng)性偏低現(xiàn)象某幾個站點(diǎn)的年總輻射比氣候公報低 20% 以上但不是所有站點(diǎn)都這樣。原因Angstrom 系數(shù) as0.25、bs0.50 是在特定區(qū)域的多年平均擬合結(jié)果對四川盆地、貴州這些多云地區(qū)并不適用。這些區(qū)域日照時數(shù)普遍偏低但實(shí)際散射輻射占比高總輻射未必像日照時數(shù)顯示的那么低。經(jīng)驗公式在這種氣候區(qū)先天吃虧不是代碼 bug。解決把 as 和 bs 暴露成命令行參數(shù)--as-coef 0.26 --bs-coef 0.42用本地站的多年實(shí)測輻射率定系數(shù)。如果沒有實(shí)測數(shù)據(jù)至少換用高原地帶常用的 as0.18、bs0.55 試跑一遍對比差異。這類調(diào)整屬于參數(shù)校準(zhǔn)問題不該在代碼里硬編碼。5.5 840 個站點(diǎn)跑得極慢的問題現(xiàn)象腳本跑了幾小時才出結(jié)果CPU 占用還不到 30%。原因用 for 循環(huán)逐行遍歷 DataFrame 計算三角函數(shù)。30 萬行數(shù)據(jù)在 Python 的純循環(huán)里要跑 20 分鐘以上而同樣的計算用 numpy 向量化一秒鐘內(nèi)完成。解決腳本里calc_solar_params和calc_radiation全部用 numpy 數(shù)組整列運(yùn)算沒有任何一層 Python 循環(huán)。如果你拿到別的版本寫的循環(huán)實(shí)現(xiàn)重構(gòu)時只需要記住一條原則——所有涉及三角函數(shù)、冪運(yùn)算、乘除的逐行計算一律先.to_numpy()再整列操作最后拼回 DataFrame。從apply改成向量化840 個站點(diǎn)一年的數(shù)據(jù)從 20 分鐘壓縮到 3 秒這個提升立竿見影。6. 二次開發(fā)思路把批量腳本改造成可配置的輻射計算工具項目源碼只是個能跑通的最小實(shí)現(xiàn)實(shí)際開發(fā)里 840 個站點(diǎn)的數(shù)據(jù)往往是按月分文件存放還可能有幾千個文件需要批量處理需要考慮把腳本封裝成可擴(kuò)展的工具形態(tài)。我的建議是把核心函數(shù)重構(gòu)成一個類把 as/bs 系數(shù)和單位換算都作為實(shí)例屬性這樣二次開發(fā)時只需實(shí)例化不同配置就能服務(wù)多種數(shù)據(jù)源class RadiationConverter: def __init__(self, as_coef0.25, bs_coef0.50, output_unitMJ): self.as_coef as_coef self.bs_coef bs_coef self.output_unit output_unit def convert_file(self, input_path, output_path, date_coldate, lat_collatitude, sun_colsunshine_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, indexFalse, encodingutf-8-sig)這個封裝把硬編碼的 0.25 和 0.50 變成了構(gòu)造參數(shù)把年份、區(qū)域、數(shù)據(jù)源差異全部隔離在實(shí)例配置里。再往下可以加一個config.yaml描述各數(shù)據(jù)源的列名映射用yaml.safe_load讀取后自動適配不同機(jī)構(gòu)提供的數(shù)據(jù)省得每次拿到新數(shù)據(jù)都改代碼。另一個值得投入的改造方向是增加質(zhì)量診斷模式?,F(xiàn)在腳本只管算不管數(shù)據(jù)質(zhì)量實(shí)際跑氣象數(shù)據(jù)時缺測、異常值、站點(diǎn)漂移才是真正的血淚來源。加一個--validate開關(guān)輸出每個站點(diǎn)的有效記錄數(shù)、日照時數(shù)年總量、平均日輻射跑完數(shù)據(jù)回頭看一眼就能定位問題這比算完再檢查要高效得多。我還要加一個功能多文件批量處理。840 個站點(diǎn)如果按站點(diǎn)分文件存放外層用一個for循環(huán)遍歷目錄每個文件計算完追加寫出避免一次性加載到內(nèi)存里卡死。最后再加個--skip-errors參數(shù)某個文件格式有問題時跳過并記錄日志不讓整個批次中斷——這在實(shí)際生產(chǎn)中比任何算法優(yōu)化都更實(shí)用。從那以后我每次拿到氣象數(shù)據(jù)都會先跑一遍describe()和isna().sum()確認(rèn)單位再開始算算完立刻看最大最小值范圍不再急著把結(jié)果交給下游。這套流程救過我好幾次希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取