Column types & missing values#
H5Col supports numeric, fixed-length string, boolean, and categorical columns,
each with an optional physical units, a description, and a valid value range
(valid_min/valid_max). This notebook tours them, plus how missing values
work.
import tempfile
from pathlib import Path
import h5py
import numpy as np
from h5col import (
ColumnSpec,
FixedString,
OversizedStringError,
Table,
bool_dtype,
)
path = Path(tempfile.gettempdir()) / "h5col_types.h5"
f = h5py.File(path, "w")
Fixed-length strings — no silent truncation#
HDF5 fixed-length strings are sized in bytes. FixedString enforces the
budget and raises instead of silently truncating (a UTF-8 code point can take
up to 4 bytes).
fs = FixedString(5) # 5 bytes
print("encoded:", fs.encode(["hello", "world"]))
try:
fs.encode(["café ☕"]) # far more than 5 bytes
except OversizedStringError as e:
print("refused oversized value:", e)
encoded: [b'hello' b'world']
refused oversized value: string value at index 0 needs 9 bytes but the column allows at most 5: 'café ☕'
Categorical columns#
Give categories (the label set) instead of a dtype. H5Col stores compact integer codes and a separate categories dataset under a CATEGORIES group; ordered=True records an ordinal scale.
table = Table.create(
f.create_group("obs"),
[
ColumnSpec(name="count", dtype="i4", units="1",
description="event count"),
# flux is non-negative, so we mark missing with a negative sentinel:
# a fill value must lie strictly OUTSIDE [valid_min, valid_max].
ColumnSpec(name="flux", dtype="f8", units="W m-2",
valid_min=0.0, fill_value=-1.0),
ColumnSpec(name="flagged", dtype=bool_dtype()),
ColumnSpec(name="quality",
categories=["good", "suspect", "bad"], ordered=True),
],
)
table.append({
"count": [10, 3, 7, 0],
"flux": [412.5, 0.0, 88.1, 5.0],
"flagged": [False, True, False, True],
"quality": ["good", "suspect", "bad", None], # None -> missing category
})
table.read()
{'count': array([10, 3, 7, 0], dtype=int32),
'flux': array([412.5, 0. , 88.1, 5. ]),
'flagged': array([False, True, False, True]),
'quality': array(['good', 'suspect', 'bad', None], dtype=object)}
q = table["quality"]
print("is_categorical:", q.is_categorical)
print("categories:", list(q.categories))
print("ordered:", q.ordered)
print("integer codes:", q.codes) # -1 marks the missing category
print("decoded:", list(q.read()))
is_categorical: True
categories: ['good', 'suspect', 'bad']
ordered: True
integer codes: [ 0 1 2 -1]
decoded: ['good', 'suspect', 'bad', None]
Missing values#
Every non-boolean column has a fill value marking missing rows. It must lie
outside the valid range — here flux is non-negative, so we chose -1.0. A
column omitted from an append leaves its new rows missing. Boolean columns
cannot be missing, so they must always be supplied.
# 'flux' omitted here -> those two rows are missing.
table.append({
"count": [4, 9],
"flagged": [True, False],
"quality": ["good", "good"],
})
print("flux values:", table["flux"].read())
print("flux fill value:", table["flux"].fill_value)
print("missing mask:", table["flux"].is_missing().tolist())
flux values: [412.5 0. 88.1 5. -1. -1. ]
flux fill value: -1.0
missing mask: [False, False, False, False, True, True]
The canonical missing-value test compares each row against the fill value, so is_missing() cleanly separates real data from absent rows regardless of the sentinel chosen.
present = ~table["flux"].is_missing()
print("mean flux over present rows:",
float(np.mean(table["flux"].read()[present])))
f.close()
mean flux over present rows: 126.4