简介:这份资源面向气象、海洋与气候数据分析的入门学习者,提供来自Met Office Hadley Centre观测数据集的全球海水表面温度与海冰浓度数据,均为nc格式,并附带一份入门级Python处理代码,帮助读者快速了解变量结构与数据组织方式,解决初次接触HadISST数据时不知从何下手的问题。压缩包为7z格式,共3个文件,包含2个nc数据文件与1个py脚本,整体约167.98MB,nc文件分别对应海冰浓度与海表温度,py脚本用于读取并查看变量情况,简单易懂。目前已有4930人学习下载,说明该数据在气候分析入门场景中较受关注。读者可借助脚本直接查看变量维度、时间范围与数值分布,理解数据构造,为后续做时间序列分析、空间分布绘图或区域气候研究打下基础,也适合作为课程作业与练习素材。
1. 全球海水表面温度和海冰浓度数据集(2020a专用):一份被低估的极地研究底座
2020 年前后做极地或海气耦合方向的人,大概率绕不开一套全球尺度的海水表面温度(SST)与海冰浓度(SIC)栅格数据集,圈内常按年份版本叫它「2020a」。它解决的核心问题很朴素:把逐日的海表温度场和海冰密集度场对齐到同一套经纬网格上,让做气候态分析、海冰边缘区识别、模式验证的人不用再自己拼接多源产品。适合谁?做极地海冰趋势、海洋热浪、海气通量估算的研究生和工程师,以及需要一份稳定底图数据做下游任务(比如边缘区提取、时间序列异常检测)的开发者。它不花哨,但胜在时空覆盖完整、变量定义清晰,是那种「用起来不惊艳、缺了又不行」的基础数据。这一章先把这份数据到底装了什么、边界在哪讲清楚,后面几章再落到怎么读、怎么算、怎么避坑。
2. 读懂 2020a 的数据组织:变量、网格与时间轴
2.1 变量命名与物理含义
这份数据集通常以 NetCDF 格式分发,核心变量就两个:SST(单位摄氏度,部分版本用开尔文)和 SIC(海冰浓度,0 到 1 的无量纲分数,也有版本用百分比 0 到 100)。别小看单位这件事,我见过不止一个人把 SIC 当成百分比直接乘 100,结果海冰范围算出来大了一百倍。判断方法很简单:读一个北极夏季的格点,如果值在 0.8 附近,那是分数;如果在 80 附近,那是百分比。SST 同理,先看数值范围,热带海表大约 28 到 30,如果读到 300 上下,那就是开尔文,减 273.15 即可。
除了这两个主变量,元数据里一般还带经纬度坐标变量(lon、lat)、时间变量(time,多为距 1970-01-01 或 1900-01-01 的天数偏移),以及一些全局属性记录数据来源和版本。读之前先ncdump -h看一眼头信息,比直接上代码省事得多。
2.2 网格类型与分辨率选择
2020a 常见的网格是规则经纬度网格,分辨率在 0.25° 到 1° 之间。0.25° 的版本细节丰富但文件大,做全球多年逐日分析时内存吃紧;1° 的版本适合做气候态和长期趋势,跑起来轻快。选哪个取决于你的下游任务:如果要做海冰边缘区(Marginal Ice Zone)的精细识别,0.25° 更合适;如果只是算区域平均时间序列,1° 足够且不容易翻车。
需要注意网格的经度范围。有的版本经度是 0 到 360,有的是 -180 到 180。做区域裁剪时如果没对齐,会出现「明明选了白令海峡却裁出一片空白」的玄学现象。统一转成 -180 到 180 再操作,能省掉大量调试时间。
2.3 时间轴的坑:逐日、月均与闰年
时间变量是这份数据最容易踩坑的地方。逐日数据里,不同版本对闰年的处理不一致:有的用 365 天固定日历(noleap),有的用标准日历(standard)。如果你用 pandas 直接按天数偏移转日期,遇到闰年就会整体错位一天。稳妥做法是先读time.units和time.calendar属性,再决定用哪种转换方式。
月均版本相对省心,但要注意月份标记是月初还是月中。做时间序列拼接时,月初标记和月中标记混用会导致曲线出现锯齿。统一成月初标记,或者干脆转成年积日(DOY)再对齐。
3. 用 Python 把 2020a 读进来并做基础质检
3.1 最小读取与变量检查
先装好依赖,xarray 加 netCDF4 是标配。下面这段是最小可运行版本,读一个文件、打印变量、检查范围。
import xarray as xr import numpy as np # 打开单个 NetCDF 文件,decode_times 让 xarray 自动解析时间 ds = xr.open_dataset("sst_sic_2020a_20200101.nc", decode_times=True) # 打印数据集结构,先看清楚有什么 print(ds) # 提取核心变量 sst = ds["sst"] # 海水表面温度 sic = ds["sic"] # 海冰浓度 # 基础质检:范围、缺失值比例 print("SST range:", float(sst.min()), float(sst.max())) print("SIC range:", float(sic.min()), float(sic.max())) print("SST NaN ratio:", float(np.isnan(sst).mean())) print("SIC NaN ratio:", float(np.isnan(sic).mean()))逻辑说明:decode_times=True让 xarray 自动把时间偏移转成 datetime64,省去手动换算。质检三步走——看范围判断单位、看缺失比例判断数据完整性、看变量名确认没有拼写差异。参数上,如果sst.min()出现 -999 之类的值,说明缺失值没被识别成 NaN,需要手动设置mask_and_scale或检查_FillValue属性。
3.2 缺失值与陆海掩膜的区分
SST 在陆地上是缺失值,SIC 在开阔水域是 0 而不是缺失。这两者的处理逻辑完全不同。做全球平均时,SST 必须跳过陆地格点,否则会被 NaN 污染;SIC 则可以直接平均,因为 0 是有物理意义的。常见做法是先生成一个陆海掩膜,用 SST 的非缺失位置作为海洋掩膜,再把这个掩膜应用到 SIC 上做一致性分析。
# 用 SST 非缺失位置构造海洋掩膜 ocean_mask = ~np.isnan(sst.isel(time=0)) # 对 SIC 做海洋区域平均时应用掩膜 sic_ocean_mean = sic.where(ocean_mask).mean(dim=["lat", "lon"]) print(sic_ocean_mean.values)参数说明:where会把掩膜为 False 的位置设为 NaN,再mean就自动跳过。注意dim里写的是经纬度维度名,不同数据集可能叫latitude/longitude,先print(ds.dims)确认。
3.3 时间序列聚合与区域裁剪
做区域分析时,用sel按经纬度切片比用索引安全。下面示例裁出北极区域并算逐日平均。
# 裁出北极区域(纬度大于 60) arctic = ds.sel(lat=slice(60, 90)) # 算区域平均海冰浓度时间序列 sic_ts = arctic["sic"].mean(dim=["lat", "lon"]) # 转成 pandas 方便画图和存 CSV sic_df = sic_ts.to_dataframe(name="sic") sic_df.to_csv("arctic_sic_timeseries.csv")逻辑说明:slice(60, 90)要求纬度是升序排列,如果数据是降序会返回空,先ds = ds.sortby("lat")一下。to_dataframe会把时间作为索引,存 CSV 时注意时区无关,纯日期即可。
4. 海冰浓度与 SST 的联合分析:从边缘区提取到趋势计算
4.1 海冰边缘区的判定阈值怎么定
海冰边缘区一般定义为 SIC 在 0.15 到 0.8 之间的过渡带。这个阈值不是拍脑袋来的,0.15 以下算开阔水,0.8 以上算密集冰,中间是边缘区。但不同研究用的阈值略有差异,做对比时要统一。下面代码提取某一天的边缘区掩膜。
# 取某一天,提取边缘区掩膜 day = ds.isel(time=0) miz_mask = (day["sic"] >= 0.15) & (day["sic"] <= 0.8) # 统计边缘区面积占比(按纬度加权) weights = np.cos(np.deg2rad(ds["lat"])) miz_area = (miz_mask * weights).sum() / weights.sum() print("MIZ area fraction:", float(miz_area))参数说明:纬度加权用cos(lat)是因为高纬度格点实际面积小,不加权会高估极区贡献。阈值 0.15 和 0.8 可按研究惯例调整,但一旦定了就全文统一。
4.2 SST 异常与海冰退缩的对应关系
做趋势分析时,先算 SST 异常(减去气候态),再看它和海冰浓度的相关。气候态一般用多年同期的均值。如果只有 2020a 一年数据,可以用月均值作为基准做距平。
# 按月分组算气候态,再算距平 sst_clim = ds["sst"].groupby("time.month").mean(dim="time") sst_anom = ds["sst"].groupby("time.month") - sst_clim # 算北极区域 SST 异常与 SIC 的相关系数 arctic_anom = sst_anom.sel(lat=slice(60, 90)).mean(dim=["lat", "lon"]) arctic_sic = ds["sic"].sel(lat=slice(60, 90)).mean(dim=["lat", "lon"]) corr = np.corrcoef(arctic_anom.values, arctic_sic.values)[0, 1] print("Correlation:", corr)逻辑说明:groupby("time.month")按月份分组,减去对应月均值得到距平。相关系数为负说明 SST 升高对应海冰减少,符合物理预期。如果算出正相关,先检查时间对齐和缺失值处理。
4.3 趋势计算的注意事项
算线性趋势时,别直接对原始值做最小二乘,先去掉季节循环。否则趋势会被季节波动淹没。用距平序列算趋势,斜率单位是「每单位时间的异常变化」。另外,自相关会低估趋势显著性,做检验时用有效自由度而不是样本数。
from scipy import stats # 用距平序列算趋势 x = np.arange(len(arctic_anom)) slope, intercept, r, p, se = stats.linregress(x, arctic_anom.values) print(f"Trend: {slope:.4f} per day, p={p:.4f}")参数说明:linregress返回斜率、截距、相关系数、p 值和标准误。p 值小于 0.05 算显著,但自相关强时这个 p 值偏乐观,严谨做法是用块自举(block bootstrap)重估。
5. 避坑与排查:2020a 使用中最容易翻车的五件事
5.1 现象:区域裁剪返回空数组
原因:经度范围是 0 到 360,而切片用了 -180 到 180。解决:先ds = ds.assign_coords(lon=(ds.lon + 180) % 360 - 180)再排序,或者统一转成 0 到 360 再裁。
5.2 现象:SIC 平均值超过 1
原因:单位是百分比而不是分数,或者缺失值被当成了有效值。解决:先看sic.max(),超过 1 就除以 100;再检查_FillValue是否被正确识别为 NaN。
5.3 现象:时间序列出现周期性尖刺
原因:月均数据用了月中标记,和月初标记的数据拼接后错位。解决:统一转成年积日再对齐,或者全部重采样到月初。
5.4 现象:趋势显著性异常高
原因:没去季节循环,或者自相关导致自由度被高估。解决:先用距平序列,再用块自举做显著性检验。
5.5 现象:内存溢出
原因:一次性读入多年逐日 0.25° 全球数据。解决:用chunks参数做分块读取,或者先裁剪区域再加载。
# 分块读取,避免一次性加载 ds = xr.open_dataset("large_file.nc", chunks={"time": 30, "lat": 200, "lon": 200})参数说明:chunks按维度分块,time 设 30 表示一次处理 30 个时间步。分块后计算会自动惰性执行,最后compute()才真正算。
6. 进阶技巧:把 2020a 做成可复用的分析流水线
6.1 用 xarray 的 apply_ufunc 做自定义计算
当你需要对每个格点做复杂运算(比如拟合非线性趋势),apply_ufunc比循环快得多。下面示例对每个格点算线性趋势。
def linear_trend(y): x = np.arange(len(y)) mask = ~np.isnan(y) if mask.sum() < 10: return np.nan return np.polyfit(x[mask], y[mask], 1)[0] trend = xr.apply_ufunc( linear_trend, ds["sst"].chunk({"time": -1}), input_core_dims=[["time"]], vectorize=True, dask="parallelized", output_dtypes=[float] )逻辑说明:input_core_dims指定沿 time 维度计算,vectorize=True让函数逐格点调用,dask="parallelized"配合分块并行。参数上,chunk({"time": -1})表示时间维度不分块,保证每个格点的时间序列完整。
6.2 验证方法:用已知事件做交叉检查
做完分析别急着下结论,拿一个已知事件验证。比如 2020 年北极海冰范围在 9 月达到历史低值,如果你的数据算出来 9 月 SIC 均值明显低于其他年份,说明流程没问题。反过来,如果算出来 9 月海冰比 3 月还多,那一定是哪里错了。
6.3 一个具体技巧:用掩膜做条件平均
做海冰边缘区的 SST 分析时,不要直接对全区域平均,而是用 MIZ 掩膜做条件平均。这样得到的 SST 才是边缘区的真实热力状态。
# 用 MIZ 掩膜做条件平均 sst_miz = ds["sst"].where(miz_mask).mean(dim=["lat", "lon"]) print(sst_miz.values)参数说明:where把非 MIZ 区域设为 NaN,mean自动跳过。注意掩膜要和时间对齐,逐日分析时每天都要重新算掩膜。
我自己用这份数据最大的教训是:别信默认设置,先看元数据。单位、日历、经度范围这三样每次都要确认,花五分钟省五小时。还有,做趋势之前一定去季节循环,不然算出来的「显著趋势」可能只是季节波动。希望帮到你。
本文还有配套的精品资源,点击获取