1. 课程导入与科学数据全景

在科学计算中,数据格式从来不是无关紧要的“容器”。同一份温度场数据,存成 CSV 可能只有几十兆,存成 HDF5 却能在毫秒级完成切片读取;同一批气象预报结果,用 NetCDF 交付能被气候社区直接复用,用私有二进制交付则往往需要额外写一份解析器。格式选择直接影响存储成本、读取性能、工具兼容性和协作效率。本课程要解决的,正是“不同格式之间如何正确转换、如何保持语义不丢失、如何让异构工具链协同工作”这一组问题。

一、科学数据格式的四个家族

面对众多扩展名,先按“数据如何被组织和解释”归类,比逐个记忆格式更有效。可以分成四个家族:

  • 纯文本格式:CSV、TSV、JSON、YAML、PLAIN 文本。优点是通用、可读、易调试;缺点是体积大、解析慢、缺乏类型与维度信息。CSV 甚至不规定缺失值写法,空串、NA、-9999 都可能出现。
  • 自描述二进制格式:NetCDF、HDF5、Zarr、Parquet、Arrow。它们把“数据 + 元数据”打包在一起,能表达多维数组、数据类型、维度坐标、单位、缺失值掩码和分块布局。
  • 无描述二进制格式:原始 .bin、Fortran 无格式流、自定义结构体转储。只有读写双方约定好字节序、类型宽度和排列顺序才能解析,跨平台极易出错。
  • 领域专用格式:FITS(天文)、GRIB(气象)、DICOM(医学影像)、LAS/LAZ(点云)。它们在通用容器之上叠加了行业约定的头段与语义。

需要特别区分“自描述”与“自带坐标语义”。Parquet 是自描述列式格式,擅长表格型数据,但它本身不表达“这个维度是时间、单位是小时”;NetCDF 和 HDF5 则可以通过维度变量、units、scale_factor 等属性承载物理语义。转换时若只搬数值不搬属性,数据看似完整,语义却已经丢失。

科学数据格式全景:四大家族与转换通道 文本格式 CSV / TSV JSON / YAML 纯文本表格 可读 · 体积大 · 无维度 自描述二进制 HDF5 / NetCDF Zarr / Parquet Arrow 元数据 · 分块 · 多维 无描述二进制 原始 .bin Fortran 流 结构体转储 依赖约定 · 易错 领域专用 FITS / GRIB DICOM LAS / LAZ 行业头段 · 强语义 转换中间层:统一数据模型(数组 + 维度 + 属性 + 类型) 读入时解析元数据 → 映射为统一模型 → 写出时重建目标格式语义 关键风险点:单位、缺失值、维度顺序、字节序、压缩与分块 分析消费端 Python / R / Julia Pandas / xarray 可视化与共享 Panoply / ParaView 云存储 / 对象存储 归档与长期保存 FAIR 原则 版本化 / 校验和

二、互操作挑战究竟难在哪里

格式转换的困难很少在“读字节、写字节”这一步,而集中在语义映射。以下五类问题几乎贯穿全部课程:

  1. 维度顺序约定不一致。NetCDF 习惯 (time, lat, lon),图像库常按 (H, W, C),Fortran 内存按列优先而 C 按行优先。转置写错不会报错,只会让结果“看起来像噪声”。
  2. 缺失值与填充值。NetCDF 用 _FillValue,HDF5 常用属性约定,Parquet 用 null。若把 -9999 当真实温度参与平均,统计结果会被严重拉低。
  3. 类型与精度。float32 转 float64 无损,反向则可能溢出;整型无符号与有符号混用、字节序(大端/小端)不一致,都会导致数值错乱。
  4. 单位与坐标。开尔文与摄氏度、米与千米、时间原点(days since 1970-01-01 与 hours since 2000-01-01)必须显式换算,否则数据“正确但含义错误”。
  5. 分块与压缩。HDF5 的 chunk 布局、Zarr 的分块目录、Parquet 的 row group,决定了随机访问与顺序扫描的性能差异,转换时若忽略会带来数量级的性能变化。
一个常见误区是“先转成 CSV 再看”。对多维科学数据,CSV 会丢失维度、坐标和单位,且体积膨胀数倍。转换应尽量在自描述格式之间进行,并保留属性。

三、一个可操作的转换示例

假设我们有一份 NetCDF 气温数据 tas.nc,变量 tas 形状为 (time=24, lat=180, lon=360),单位 K,含 _FillValue = -9999。目标是把其中第 0 个时刻转成 Parquet 供表格工具分析。关键步骤不是简单写出,而是先做语义处理:

import xarray as xr
ds = xr.open_dataset("tas.nc")
t0 = ds["tas"].isel(time=0)                 # 取第 0 时刻,形状 (lat, lon)
t0 = t0.where(t0 != ds["tas"].attrs.get("_FillValue", -9999))  # 缺失值转 NaN
t0 = t0 - 273.15                            # K → ℃,单位换算
df = t0.to_dataframe(name="tas_c").reset_index()  # 展开为 lat/lon/tas_c 三列
df.to_parquet("tas_t0.parquet", index=False)

这段代码体现了三个原则:先按维度切片再展开,避免把三维数组硬塞进表格;显式处理缺失值,不让填充值污染统计;显式换算单位并在列名中标注 tas_c。转换完成后应做校验:行数是否等于 180 × 360 = 64800,tas_c 的取值范围是否落在合理区间(例如 -60 至 50 ℃),NaN 比例是否与原始缺失比例一致。只有这三项都对上,才能认为转换在语义上是可信的。

四、课程目标与学习路径

本课程围绕“看懂格式、正确转换、验证结果、工程落地”四条主线展开。学习路径建议如下:先建立格式分类与元数据模型的基本认知(第 1–6 章),再掌握文本与二进制之间的读写和类型处理(第 7–12 章),随后进入 HDF5、NetCDF、Zarr、Parquet 之间的多维与列式互转(第 13–20 章),最后覆盖大规模数据的分块、并行、云存储与流水线工程化(第 21–30 章)。

学习时请始终带着三个问题:这份数据的维度与坐标是什么?它的缺失值、单位、类型如何表达?转换后我用什么指标验证没有丢信息?把这三个问题变成习惯,比记住任何单一 API 都更有价值。