Skip to content

对 NetCDF 格式数据进行 short to float 解压操作 ​

本文档以 CDO 工具和 Python 脚本语言两种处理方式为例,详细说明将 NetCDF 数据中以 short(int16)整型存储、经 scale_factor / add_offset 编码的变量还原为 float 浮点型数据的操作方法。


前置说明 ​

  • 待处理数据格式:NetCDF(*.nc)
  • 适用场景:数据集为节省存储空间,将物理量以 16 位整型(short/int16)打包存储,真实值需按下式还原:
text
真实值 = 原始整型值 × scale_factor + add_offset
  • 判断依据:用 ncdump -h file.nc 查看变量属性,若同时存在以下属性,即为打包存储的 short 数据:
    • scale_factor:缩放比例(浮点数)
    • add_offset:偏移量(浮点数)
    • _FillValue 或 missing_value:缺测值标记为 short
    • 变量类型显示为 short
  • 解压规则:解压后的 float 变量应移除 scale_factor、add_offset、_FillValue 等打包属性,缺测点转为 NaN 或按目标约定保留。
  • 注意:解压后文件体积可能约为原文件的 2 倍(int16 → float32),请预留足够磁盘空间。

方法一:使用 CDO 进行解压 ​

  • 环境配置:可详见 使用CDO工具处理天机NetCDF4数据文件
  • 转换命令:CDO 读取数据时会自动按 CF 规范应用 scale_factor 和 add_offset;使用 -b 32 强制输出为 32 位浮点型(-b 64 为双精度)。
bash
# 解压为 32 位 float,输出 NetCDF4 格式
cdo -b 32 -f nc4 copy input.nc output.nc

参数说明:

  • -b 32:输出精度为 32 位浮点(float)
  • -b 64:输出精度为 64 位浮点(double)
  • copy:原样复制并完成解码/类型转换
  • -f nc4:输出 NetCDF4 格式(可省略,默认即为 NetCDF3/4 由环境决定)
  • 验证结果:
bash
# 变量类型应由 short 变为 float,且不再有 scale_factor/add_offset 属性
ncdump -h output.nc

方法二:使用 Python 进行解压 ​

  • 环境与依赖:
    • Python 3.x
    • 依赖库:xarray、netCDF4(任一后端)

2.1 自动解压(推荐) ​

xarray 默认按 CF 约定解码(自动应用 scale_factor/add_offset、将 _FillValue 转为 NaN),读入即为 float,直接写出即可:

python
import xarray as xr


def unpack_nc(input_nc, output_nc):
    ds = xr.open_dataset(input_nc)          # 默认 decode_cf=True,读入即为 float
    ds.to_netcdf(output_nc)                 # 写出后打包属性已被移除
    ds.close()


if __name__ == "__main__":
    input_nc  = "./u100m.nc"          # 需要解压的 nc 数据
    output_nc = "./u100m_float.nc"    # 解压后的输出文件

    unpack_nc(input_nc, output_nc)

2.2 手动解压(基于 netCDF4 库,不依赖 xarray) ​

若需要自行控制缺测值处理方式,可用 netCDF4 库直接读取原始 short 数据,手工套用公式还原后写入新文件:

python
from netCDF4 import Dataset
import numpy as np


def unpack_nc_manual(input_nc, output_nc, var_name):
    with Dataset(input_nc, "r") as src, \
         Dataset(output_nc, "w", format="NETCDF4") as dst:

        # 复制全局属性
        dst.setncatts({
            k: src.getncattr(k)
            for k in src.ncattrs()
        })

        # 复制维度
        for name, dim in src.dimensions.items():
            dst.createDimension(
                name,
                None if dim.isunlimited() else len(dim)
            )

        for name, var in src.variables.items():
            attrs = {
                k: var.getncattr(k)
                for k in var.ncattrs()
            }

            # 关闭自动 mask 和 scale,确保读取原始存储值
            var.set_auto_maskandscale(False)
            data = var[:]

            if name == var_name:
                # 从原始打包数据中识别缺测值
                missing_values = []
                for key in ("_FillValue", "missing_value"):
                    if key in attrs:
                        missing_values.extend(
                            np.asarray(attrs[key]).ravel().tolist()
                        )

                valid = np.ones(data.shape, dtype=bool)
                for value in missing_values:
                    valid &= data != value

                # 读取打包参数
                scale = attrs.pop("scale_factor", 1.0)
                offset = attrs.pop("add_offset", 0.0)
                attrs.pop("_FillValue", None)
                attrs.pop("missing_value", None)

                # 转换为浮点值并解包
                unpacked = data.astype(np.float32)
                unpacked *= scale
                unpacked += offset

                # 缺测值直接设为 NaN
                unpacked[~valid] = np.nan

                # 创建浮点变量,显式将 NaN 设为填充值
                new_var = dst.createVariable(
                    name,
                    "f4",
                    var.dimensions,
                    fill_value=np.float32(np.nan)
                )
                new_var.setncatts(attrs)
                new_var[:] = unpacked

            else:
                # 其他变量按原始存储值复制
                fill_value = attrs.pop("_FillValue", None)

                kwargs = {}
                if fill_value is not None:
                    kwargs["fill_value"] = fill_value

                new_var = dst.createVariable(
                    name,
                    var.dtype,
                    var.dimensions,
                    **kwargs
                )
                new_var.setncatts(attrs)
                new_var[:] = data


if __name__ == "__main__":
    unpack_nc_manual(
        input_nc="./u100m.nc",
        output_nc="./u100m_manual.nc",
        var_name="u100m",
    )

说明:

  • netCDF4 读取时不做任何 CF 解码,var[:] 返回的即原始 short 整型值,因此乘加公式是手动执行
  • "f4" 表示 32 位双精度,如需 64 位浮点可改为 "f8"
  • var.dimensions 沿用了原变量的维度名,配合前面复制的维度,坐标一致性不受影响

2.3 解压结果验证 ​

python
import xarray as xr

src = xr.open_dataset("./u100m.nc")            # 解压前
dst = xr.open_dataset("./u100m_manual.nc")     # 解压后

print(src["u100m"].dtype)   # int16 (short)
print(dst["u100m"].dtype)   # float32 / float64
print(dst["u100m"].attrs)   # 不再包含 scale_factor / add_offset

注意事项 ​

  • 先 concat 再解压有风险——各文件打包参数不一致时慎重直接拼接:scale_factor / add_offset 是逐文件写入变量属性的,不同批次/时次的文件完全可能取值不同(例如前期文件用 0.1/0,后期文件用 0.02/-50)。若用 NCO ncrcat 等方式先把多个 short 文件合并成一个,再统一解压,最终文件只会保留一份属性,先拼进来的打包参数会"覆盖"其他文件的——除第一批数据外,其余时次的数值都会被错误还原。

    • 正确顺序:先逐文件解压为 float,再 concat 合并;或使用支持的工具在 concat 时逐文件显式指定各自的 scale/offset。
    • 合并前可用脚本核对各文件属性是否一致,不一致时不要偷懒:
    python
    from netCDF4 import Dataset
    
    for f in file_list:
        with Dataset(f, "r") as ds:
            v = ds["u100m"]
            print(f, v.scale_factor, v.add_offset, getattr(v, "_FillValue", None))
    • 若必须在 short 状态下合并(例如数据量太大),需在合并时为每个文件块分别乘加还原,并保证拼接后属性中不再残留 scale_factor / add_offset。
  • 不要重复解压:若文件属性中已没有 scale_factor / add_offset,不要再执行解压,否则等于对已还原的数据再次乘加,结果会出错。

  • 缺测值处理:2.1 节自动解码会把 _FillValue 转为 NaN;若下游工具不接受 NaN,可用 ds.fillna(-9999) 等方式替换后再写出。

  • 只解压部分变量:xarray 可用 ds[[var_name]].to_netcdf(...) 仅转换指定变量,其余变量保持 short 不变。

  • 大文件处理:数据量大时建议先裁剪区域/时次再解压,或使用 xr.open_dataset(..., chunks={...}) 配合 dask 分块处理,避免内存不足。

  • 属性保留:解压后 units、long_name 等描述性属性会原样保留,无需额外处理。