
起因
我为了买不起硬盘的事,硬生生把netcdf文件压缩了2.70倍
正文
各位,容我给大家讲一个我关于 "消费降级" 的故事。
9个月前我激情满怀地花了6000块买了两块26TB硬盘,当时觉得自己是个大数据玩家。
结果前两天逛淘宝想再买一块,发现硬盘涨价了,我居然还能赚点钱卖出去,这买卖亏翻了。而且我看了眼硬盘的价格,直接跳起来了,这群人是活着的奸商吧?
既然买不起新硬盘了,只能另想办法。于是我开始琢磨怎么"压缩"数据,就像精明的家庭主妇会把生活开支压缩一样。
于是用这个代码压缩了我的netcdf文件:
import numpy as np
import xarray as xr
import os
import time
OUTPUT_DIR = "."
DUMMY_FILE = os.path.join(OUTPUT_DIR, "dummy_input.nc")
FILE_BASELINE = os.path.join(OUTPUT_DIR, "result_no_compression.nc")
FILE_COMPRESSED = os.path.join(OUTPUT_DIR, "result_scale_zlib.nc")
SHAPE = (12, 192, 288)
VAR_NAME = "temperature"
defgenerate_dummy_data():
t_steps, lats, lons = SHAPE
time_coord = np.arange(t_steps)
lat_coord = np.linspace(-90, 90, lats)
lon_coord = np.linspace(0, 360, lons, endpoint=False)
t, y, x = np.meshgrid(time_coord, lat_coord, lon_coord, indexing='ij')
data = (
280 * np.ones_like(t) +
20 * np.sin(np.radians(y)) +
5 * np.cos(np.radians(x) * 2 + t * 0.1) +
np.random.normal(0, 0.1, SHAPE)
).astype(np.float32)
ds = xr.Dataset(
{VAR_NAME: (['time', 'lat', 'lon'], data)},
coords={
'time': time_coord,
'lat': lat_coord,
'lon': lon_coord
}
)
ds.to_netcdf(DUMMY_FILE, encoding={VAR_NAME: {'dtype': np.float32}})
return ds
defget_manual_encoding(ds):
encoding_dict = {}
var_name = VAR_NAME
da = ds[var_name]
data_min = float(da.min(skipna=True))
data_max = float(da.max(skipna=True))
dtype = np.int16
int_min = np.iinfo(dtype).min
int_max = np.iinfo(dtype).max
if data_max == data_min:
scale_factor = 1.0
add_offset = data_min
else:
scale_factor = (data_max - data_min) / (int_max - int_min)
add_offset = data_min - int_min * scale_factor
encoding_dict[var_name] = {
'dtype': dtype,
'scale_factor': scale_factor,
'add_offset': add_offset,
'_FillValue': np.iinfo(dtype).min,
'zlib': True,
'complevel': 4,
'shuffle': True
}
return encoding_dict
defrun_test():
ds_orig = generate_dummy_data()
start_a = time.time()
ds_orig.to_netcdf(FILE_BASELINE, encoding={VAR_NAME: {'dtype': np.float32}})
time_a = time.time() - start_a
size_a = os.path.getsize(FILE_BASELINE) / (1024 * 1024)
encoding_manual = get_manual_encoding(ds_orig)
start_b = time.time()
ds_orig.to_netcdf(FILE_COMPRESSED, encoding=encoding_manual)
time_b = time.time() - start_b
size_b = os.path.getsize(FILE_COMPRESSED) / (1024 * 1024)
ds_comp_loaded = xr.open_dataset(FILE_COMPRESSED)
orig_vals = ds_orig[VAR_NAME].values
comp_vals = ds_comp_loaded[VAR_NAME].values
mask = ~(np.isnan(orig_vals) | np.isnan(comp_vals))
diff = orig_vals[mask] - comp_vals[mask]
rmse = np.sqrt(np.mean(diff**2))
max_err = np.max(np.abs(diff))
ratio = size_a / size_b
saved_pct = ((size_a - size_b) / size_a) * 100
results = {
"size_original_mb": size_a,
"size_compressed_mb": size_b,
"saved_percent": saved_pct,
"compression_ratio": ratio,
"time_diff_s": time_b - time_a,
"rmse": rmse,
"max_error": max_err
}
return results
if __name__ == "__main__":
results = run_test()
print(f"Original Size: {results['size_original_mb']:.2f} MB")
print(f"Compressed Size: {results['size_compressed_mb']:.2f} MB")
print(f"Saved: {results['saved_percent']:.1f}%")
print(f"Ratio: {results['compression_ratio']:.2f}x")
print(f"RMSE: {results['rmse']:.6e}")
print(f"Max Error: {results['max_error']:.6f}")
测试结果?我直接笑了:
核心秘诀就这几行:
encoding={
'dtype': np.int16, # Float32 -> Int16,直接瘦身
'scale_factor': 0.000771, # 精准缩放,误差微乎其微
'add_offset': 279.9932, # 偏移调整,保留有效数据
'zlib': True, # 再配上zlib压缩
'complevel': 1, # 压到4档就够了
'shuffle': True# Shuffle洗牌增强压缩效果
}
数据精度损失?我测了测,RMSE才2.2e-4,最大误差0.0004,这点误差比我的实验误差还小。对于大气科学来说,这简直是完美。
Climate Data Store上提供的legacy的ERA5数据之前也是这样压缩分发给用户的,所以不用担心数据精度损失。
所以?
与其花钱买新硬盘,不如学点代码节省空间。这些硬盘厂商就算涨价3倍我也不怕了,因为我的数据现在足够"苗条",硬盘用到地老天荒都没问题。
附注
这代码对所有处理大型格点数据的朋友都有帮助:
量化压缩(Quantization)+ zlib已经是金牌搭档了。你的SSD、机械硬盘的费用,都能省一笔。