用Python处理NCDC气象数据:从ISD-Lite原始文件到Pandas DataFrame的保姆级教程
用Python处理NCDC气象数据从ISD-Lite原始文件到Pandas DataFrame的保姆级教程气象数据分析在科研和商业应用中扮演着重要角色而NCDC提供的ISD-Lite格式数据是获取全球气象观测记录的重要来源。对于刚接触这类数据的研究者来说原始文件的固定宽度格式和特殊编码方式常常成为第一道门槛。本文将带你完整走过从数据下载到分析就绪的整个流程使用Python生态中最强大的数据处理工具Pandas把看似晦涩的文本文件转化为结构清晰的DataFrame。1. 环境准备与数据获取在开始处理数据前我们需要配置合适的Python环境。推荐使用Anaconda发行版它已经包含了我们将要用到的大部分科学计算库。以下是必需包的安装命令conda create -n weather python3.9 conda activate weather conda install pandas numpy matplotlib requestsISD-Lite数据可以通过NOAA的FTP服务器公开获取。虽然可以直接用浏览器访问但通过Python脚本下载会更加高效。这里提供一个自动下载指定年份和站点数据的函数import requests from tqdm import tqdm def download_isd_data(station_id, year, save_path): base_url fftp://ftp.ncdc.noaa.gov/pub/data/noaa/isd-lite/{year}/{station_id}-99999-{year}.gz response requests.get(base_url, streamTrue) total_size int(response.headers.get(content-length, 0)) with open(save_path, wb) as f, tqdm( descsave_path, totaltotal_size, unitiB, unit_scaleTrue ) as bar: for data in response.iter_content(chunk_size1024): size f.write(data) bar.update(size)提示完整的站点列表和元数据可以从isd-history.csv获取该文件包含了全球所有气象站的ID、名称、经纬度和海拔信息。2. 解析ISD-Lite文件结构ISD-Lite采用固定宽度格式存储数据每行代表一个小时的观测记录包含12个字段。理解文件结构是正确解析的关键。以下是各字段的详细说明列号字段名起始位置长度单位换算系数缺失值备注1年份14-1-四位数字2月份62-1-01-123日期92-1-01-314小时122-1-00-235气温146℃10-9999需除以106露点温度206℃10-9999需除以107气压266hPa10-9999需除以108风向326度1-99990表示无风9风速386m/s10-9999需除以1010云量446编码1-9999见编码表111小时降水506mm10-9999-1表示微量126小时降水566mm10-9999-1表示微量使用Pandas的read_fwf函数可以轻松读取这种固定宽度格式。以下是配置列宽度的正确方式colspecs [ (0, 4), (5, 7), (8, 10), (11, 13), # 年、月、日、时 (13, 19), (19, 25), (25, 31), # 气温、露点、气压 (31, 37), (37, 43), (43, 49), # 风向、风速、云量 (49, 55), (55, 61) # 1小时降水、6小时降水 ]3. 数据清洗与转换原始数据中存在多种需要特殊处理的情况。首先是缺失值处理ISD-Lite使用-9999表示缺失数据。我们需要将其转换为NaN以便后续分析import numpy as np def clean_data(df): # 替换缺失值 df.replace(-9999, np.nan, inplaceTrue) # 处理微量降水标记 precip_cols [precip_1h, precip_6h] df[precip_cols] df[precip_cols].replace(-1, 0.05) # 微量降水设为0.05mm # 应用单位换算 df[temperature] df[temperature] / 10 df[dew_point] df[dew_point] / 10 df[pressure] df[pressure] / 10 df[wind_speed] df[wind_speed] / 10 return df云量数据采用特殊编码系统我们需要将其转换为更易理解的描述。创建一个映射字典是高效的方法cloud_map { 0: 无云, 1: 微量云(1/10), 2: 少量云(2-3/10), 3: 分散云(4/10), 4: 疏云(5/10), 5: 多云(6/10), 6: 密集云(7-8/10), 7: 厚云(9/10), 8: 全阴, 9: 不可观测 } df[cloud_coverage] df[cloud_code].map(cloud_map)4. 时间序列处理与气象站元数据整合气象数据的价值很大程度上依赖于准确的时间戳。我们可以将分开的年、月、日、时列合并为一个datetime列df[datetime] pd.to_datetime( df[[year, month, day, hour]].rename(columns{ year: year, month: month, day: day, hour: hour }) ) df.set_index(datetime, inplaceTrue)为了丰富数据维度我们通常需要合并气象站的元数据。假设我们已经加载了isd-history.csvstation_meta pd.read_csv(isd-history.csv, parse_dates[BEGIN, END], na_values[, ]) def merge_station_info(weather_df, meta_df, station_id): station meta_df[meta_df[USAF] station_id].iloc[0] for col in [STATION NAME, CTRY, LAT, LON, ELEV(M)]: weather_df[col.lower()] station[col] return weather_df5. 数据分析与可视化现在我们的数据已经清洗完毕可以进行一些基础分析。比如计算月平均气温monthly_avg df.resample(M).agg({ temperature: mean, dew_point: mean, wind_speed: mean, precip_1h: sum })使用Matplotlib可以快速生成气温变化曲线import matplotlib.pyplot as plt plt.figure(figsize(12, 6)) df[temperature].plot(label每小时气温) df[temperature].resample(D).mean().plot( linewidth2, colorred, label日均气温 ) plt.title(f{station_name} {year}年气温变化) plt.ylabel(温度(℃)) plt.legend() plt.grid() plt.show()对于风向数据风玫瑰图是最合适的可视化方式。以下代码使用极坐标系绘制from windrose import WindroseAxes ax WindroseAxes.from_ax() ax.bar(df[wind_direction].dropna(), df[wind_speed].dropna(), normedTrue, opening0.8, edgecolorwhite) ax.set_legend(title风速(m/s)) plt.title(f{station_name} {year}年风向分布) plt.show()6. 高级技巧与性能优化处理多年或多站点数据时性能成为重要考量。以下是几个优化建议使用Dask处理大数据当数据量超过内存容量时Dask提供了类似Pandas的接口但支持分布式计算。import dask.dataframe as dd ddf dd.read_fwf(*.gz, colspecscolspecs, namescolumn_names) monthly_stats ddf.groupby(month).mean().compute()内存优化技巧通过指定合适的数据类型减少内存占用。dtypes { year: int16, month: int8, day: int8, hour: int8, temperature: float32, wind_speed: float32 } df pd.read_fwf(..., dtypedtypes)自动化处理管道将整个流程封装为函数方便批量处理。def process_isd_file(file_path, station_idNone): df pd.read_fwf(file_path, ...) df clean_data(df) if station_id: df merge_station_info(df, station_meta, station_id) df add_derived_vars(df) return df7. 数据导出与共享清洗后的数据可以导出为多种格式供他人使用# 保存为CSV df.to_csv(fprocessed_{station_id}_{year}.csv, indexTrue) # 保存为Excel with pd.ExcelWriter(fweather_{station_id}.xlsx) as writer: df.to_excel(writer, sheet_nameHourly) monthly_avg.to_excel(writer, sheet_nameMonthly) # 保存为HDF5适合大型数据集 df.to_hdf(fweather_{station_id}.h5, keydata, modew)对于团队协作可以将数据存入SQL数据库from sqlalchemy import create_engine engine create_engine(postgresql://user:passlocalhost:5432/weather) df.to_sql(observations, engine, if_existsappend, indexTrue)8. 实际应用案例让我们看一个实际应用场景分析某机场全年的风向分布为跑道规划提供参考。假设我们已经处理了该机场气象站2022年的数据runway_angle 120 # 跑道方向 wind_angle_diff (df[wind_direction] - runway_angle) % 360 crosswind df[wind_speed] * np.sin(np.radians(wind_angle_diff)) headwind df[wind_speed] * np.cos(np.radians(wind_angle_diff)) # 计算顺风频率 tailwind_percent (headwind -5).mean() * 100 print(f顺风频率: {tailwind_percent:.1f}%) # 绘制风分量分布 plt.hist(crosswind.dropna(), bins30, alpha0.7) plt.axvline(x15, colorred, linestyle--) plt.title(侧风分量分布) plt.xlabel(风速(m/s)) plt.ylabel(频率) plt.show()另一个常见应用是计算温度舒适指数。使用温度和露点数据可以计算热指数(HI)def heat_index(T, RH): 计算热指数(℃) T 1.8 * T 32 # 转换为华氏度 HI -42.379 2.04901523*T 10.14333127*RH - 0.22475541*T*RH HI -0.00683783*T*T - 0.05481717*RH*RH 0.00122874*T*T*RH HI 0.00085282*T*RH*RH - 0.00000199*T*T*RH*RH return (HI - 32) / 1.8 # 转换回摄氏度 df[heat_index] heat_index(df[temperature], calculate_rh(df[temperature], df[dew_point]))在处理北京站2021年夏季数据时发现7月有连续5天气温超过35℃热指数更是达到了危险级别。这种极端天气分析对于城市规划部门制定高温应对策略非常有价值。