Appearance
npz 格式说明
format="npz" 返回的是 numpy 的 .npz 文件(一个 zip 包,每列一个 .npy 成员)。
先确认你需不需要它
npz 适合行数上万、且取回来要做数值计算的场景——它给的是 numpy 数组, 可以直接喂给 numba 这类 JIT 编译器。只是查出来看看、导出,或者接下来全是 groupby/merge 的话,用 format="pandas" 更省心。写法见示例。
也可以看看 arrow
跨语言消费(C++/CUDA),或者不希望高精度数值被转成字符串的话,用 arrow 通道。 两者只有 DECIMAL 的处理不同,其余列类型逐一对应。
用 Python SDK 的话不必读这一页,NpzResult 已经把下面这些规则处理好了。 如果你要绕开 SDK 自己解析——比如落盘后用别的程序读——那就需要知道这些约定。
这是 FinORM 自己定的约定
.npy 是 numpy 的标准格式,np.load 一定能读出来。但里面的列怎么组织是我们定的: 字符串列拆成两个数组、空值另放一个标记数组、元数据塞在一个叫 __meta__ 的成员里。 通用工具不认识这些规则,只会看到一堆数组。
成员一览
python
import numpy as np
z = np.load("result.npz", allow_pickle=False)
print(z.files)
# ['__meta__', 'stk_cd__codes', 'stk_cd__uniques', 'txn_dt', 'cls_prc', 'vol', 'vol__null']四类成员:
| 成员 | 含义 |
|---|---|
__meta__ | 元数据,一个 JSON 字符串。先读它,它说明了每一列怎么解 |
<列名> | 普通列,直接就是数据 |
<列名>__codes / <列名>__uniques | 字符串列,见下 |
<列名>__null | 空值标记,见下 |
元数据
python
import json
meta = json.loads(str(z["__meta__"][0]))json
{
"request_id": "req_8f3a...",
"table": "cneqa_mkt_sh_sz_bj_ab_dly_dat",
"returned_rows": 482000,
"truncated": false,
"data_version": "gen_20260913_030000",
"warnings": [],
"columns": [
{"name": "stk_cd", "type": "string", "encoding": "DICT_STRING", "dtype": "<U",
"members": ["stk_cd__codes", "stk_cd__uniques"]},
{"name": "cls_prc", "type": "double", "encoding": "FLOAT64", "dtype": "<f8",
"members": ["cls_prc"]},
{"name": "vol", "type": "bigint", "encoding": "INT64", "dtype": "<i8",
"members": ["vol", "vol__null"]}
]
}columns[].members 直接告诉你这一列占了哪几个成员,不用自己按后缀去猜。
truncated 为 true 说明行数达到了上限,返回的是前一部分,不是全部。
字符串列是拆开存的
证券代码这类列重复度很高,直接存会很浪费:numpy 的定长字符串每个元素固定占 宽度 × 4 字节,一百万行的证券代码要占 34 MB。所以我们存成两个数组:
python
codes = z["stk_cd__codes"] # int32,每行一个下标
uniques = z["stk_cd__uniques"] # 去重后的取值,升序
stk_cd = uniques[codes] # 还原成完整的一列拆开存只要 4 MB,省 8 倍多。用不到完整那一列时就别还原,直接拿 codes 做分组、 做映射通常更快。
uniques 是升序的,和 np.unique 的结果一致。
空值怎么表示
numpy 的整数数组装不下"空"这个概念(不像 pandas 有 NaN),所以空值单独放一个数组:
python
vol = z["vol"] # 空的位置是 0
is_null = z["vol__null"] # True 表示那个位置本来是空的
vol[is_null] # 这些 0 不是真的 0只有确实存在空值的列才会有 __null 成员,__meta__ 的 members 里能看到。
浮点列的空位除了标记,还会直接写成 NaN,所以做计算时可以不管标记数组—— np.nanmean() 这类函数会自动跳过。整数列不行,必须看标记。
日期和时间
为了省空间,日期不是字符串:
| 列类型 | 存成 | 怎么还原 |
|---|---|---|
| 日期 | int32,1970-01-01 起的天数 | arr.astype("datetime64[D]") |
| 时间戳 | int64,1970-01-01 起的微秒数 | arr.astype("datetime64[us]") |
做时间序列计算时天数其实更好用——算间隔直接相减就行。
高精度金额会以字符串返回
arrow 通道没有这个问题
本节描述的降级只发生在 npz。arrow 用原生 decimal128 无损承载, 不转字符串。库里 58% 的 decimal 列声明精度超过 15 位,会落进下面这条规则。
这一条最容易踩坑,和 format="json" 的行为不一样。
float64 只能精确表示约 15 位有效数字。总市值、复权因子、高精度因子这类列超过了这个位数, 用 float64 存会悄悄丢精度且不报错。所以这些列我们以字符串形式返回:
python
# meta 里这一列会写明 "encoding": "STRING"
from decimal import Decimal
amt = z["amt"] # dtype 是 <U,不是 <f8
values = [Decimal(x) for x in amt] # 需要精确计算就转 Decimal
values = amt.astype(np.float64) # 不在乎那几位就转 float判断依据看 __meta__ 里这一列的 encoding:
| encoding | 含义 |
|---|---|
FLOAT64 | 普通浮点,精度够用 |
STRING | 精度超了 float64,以字符串给出,要精确计算请自行转 Decimal |
A 股的价格、成交额都在安全范围内,走的是 FLOAT64。
类型对照表
__meta__ 里的 encoding | numpy dtype | 说明 |
|---|---|---|
FLOAT64 | <f8 | 浮点;空位是 NaN |
INT64 | <i8 | 长整数;空位是 0,看 __null |
INT32 | <i4 | 整数;空位同上 |
BOOL | |b1 | 布尔 |
INT32_DAYS | <i4 | 日期,天数 |
INT64_MICROS | <i8 | 时间戳,微秒 |
DICT_STRING | <U | 字符串,拆成 __codes + __uniques |
STRING | <U | 字符串,整列直接存 |
不用 SDK 怎么读
不用 SDK,手工把一份 npz 还原成 DataFrame:
python
import json
import numpy as np
import pandas as pd
z = np.load("result.npz", allow_pickle=False)
meta = json.loads(str(z["__meta__"][0]))
data = {}
for col in meta["columns"]:
name, enc = col["name"], col["encoding"]
if enc == "DICT_STRING":
values = z[f"{name}__uniques"][z[f"{name}__codes"]]
elif enc == "INT32_DAYS":
values = z[name].astype("datetime64[D]")
elif enc == "INT64_MICROS":
values = z[name].astype("datetime64[us]")
else:
values = z[name]
data[name] = values
df = pd.DataFrame(data)
print(meta["returned_rows"], meta["truncated"])延伸阅读:接口参考(query() 的完整参数)、 示例(含 numba 计算写法)、 常见问题(什么时候该用 npz)。