{ "cells": [ { "cell_type": "markdown", "id": "52cf883a", "metadata": {}, "source": [ "# Reading part of a table\n", "\n", "Every other notebook reads whole columns. That is the right default, and on a\n", "table of a few thousand rows it is also the fastest thing to do. But a column\n", "of a few million rows is a different proposition: reading all of it to look at\n", "fifty rows costs the whole column in time and in memory.\n", "\n", "This notebook is about asking for less. `h5col` columns support subscript —\n", "`column[17:98]`, `column[-1]`, `column[mask]` — and only the rows asked for are\n", "read from the file. We will build a table big enough for the difference to be\n", "visible, then measure it." ] }, { "cell_type": "code", "execution_count": 1, "id": "5316c2f5", "metadata": { "execution": { "iopub.execute_input": "2026-08-24T15:35:44.117890Z", "iopub.status.busy": "2026-08-24T15:35:44.117209Z", "iopub.status.idle": "2026-08-24T15:35:44.291119Z", "shell.execute_reply": "2026-08-24T15:35:44.290644Z" } }, "outputs": [], "source": [ "import tempfile\n", "import time\n", "from pathlib import Path\n", "\n", "import h5py\n", "import numpy as np\n", "\n", "import h5col\n", "from h5col import ColumnSpec, LeafValuesSpec, ListColumnSpec, Table, field\n" ] }, { "cell_type": "code", "execution_count": 2, "id": "28334c14", "metadata": { "execution": { "iopub.execute_input": "2026-08-24T15:35:44.292405Z", "iopub.status.busy": "2026-08-24T15:35:44.292291Z", "iopub.status.idle": "2026-08-24T15:35:44.294056Z", "shell.execute_reply": "2026-08-24T15:35:44.293744Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "h5col 0.5.0 | h5py 3.16.0\n" ] } ], "source": [ "print(f\"h5col {h5col.__version__} | h5py {h5py.__version__}\")" ] }, { "cell_type": "markdown", "id": "5e057659", "metadata": {}, "source": [ "## A table worth slicing\n", "\n", "200,000 rows: a station identifier, a temperature that is sometimes missing, and\n", "a list column holding a handful of raw samples per row. The chunk size is set\n", "small enough that a range read touches only a few chunks." ] }, { "cell_type": "code", "execution_count": 3, "id": "c6cc397a", "metadata": { "execution": { "iopub.execute_input": "2026-08-24T15:35:44.295111Z", "iopub.status.busy": "2026-08-24T15:35:44.295043Z", "iopub.status.idle": "2026-08-24T15:35:44.568973Z", "shell.execute_reply": "2026-08-24T15:35:44.568562Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "200,000 rows written\n", "19.7 MB on disk\n" ] } ], "source": [ "path = Path(tempfile.gettempdir()) / \"readings.h5\"\n", "N = 200_000\n", "rng = np.random.default_rng(0)\n", "\n", "with h5py.File(path, \"w\") as f:\n", " table = Table.create(\n", " f.create_group(\"readings\"),\n", " [\n", " ColumnSpec(name=\"station\", dtype=h5col.FixedString(6), chunks=8192),\n", " ColumnSpec(\n", " name=\"t_air\",\n", " dtype=\"f8\",\n", " fill_value=np.nan,\n", " units=\"degC\",\n", " chunks=8192,\n", " ),\n", " ListColumnSpec(\n", " name=\"samples\",\n", " values=LeafValuesSpec(dtype=\"f8\"),\n", " nullable=True,\n", " ),\n", " ],\n", " )\n", " table.append(\n", " {\n", " \"station\": [f\"S{i % 900:05d}\" for i in range(N)],\n", " # Every eleventh reading did not arrive.\n", " \"t_air\": [\n", " None if i % 11 == 0 else float(15 + (i % 200) / 10) for i in range(N)\n", " ],\n", " \"samples\": [\n", " None if i % 97 == 0 else [float(i), float(i + 1), float(i + 2)]\n", " for i in range(N)\n", " ],\n", " }\n", " )\n", " print(f\"{table.nrows:,} rows written\")\n", "\n", "print(f\"{path.stat().st_size / 1e6:.1f} MB on disk\")" ] }, { "cell_type": "markdown", "id": "3e9d1c20", "metadata": {}, "source": [ "## Subscript\n", "\n", "A column reads by subscript, and the key can be whatever describes the rows you\n", "want. An integer gives that row's value on its own; anything else gives an\n", "array." ] }, { "cell_type": "code", "execution_count": 4, "id": "64b915e1", "metadata": { "execution": { "iopub.execute_input": "2026-08-24T15:35:44.570177Z", "iopub.status.busy": "2026-08-24T15:35:44.570035Z", "iopub.status.idle": "2026-08-24T15:35:44.575744Z", "shell.execute_reply": "2026-08-24T15:35:44.574943Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "col[17:22] [16.7 16.8 16.9 17.0 17.1]\n", "col[0] masked <- row 0 is missing\n", "col[1] np.float64(15.1)\n", "col[-1] np.float64(34.9) <- counts back from the end\n", "col[[5, 1, 5]] [15.5 15.1 15.5] <- any order, repeats allowed\n", "len(col) 200,000\n" ] } ], "source": [ "with h5py.File(path, \"r\") as f:\n", " col = Table.open(f[\"readings\"])[\"t_air\"]\n", "\n", " print(\"col[17:22] \", col[17:22])\n", " print(\"col[0] \", repr(col[0]), \" <- row 0 is missing\")\n", " print(\"col[1] \", repr(col[1]))\n", " print(\"col[-1] \", repr(col[-1]), \" <- counts back from the end\")\n", " print(\"col[[5, 1, 5]] \", col[[5, 1, 5]], \" <- any order, repeats allowed\")\n", " print(\"len(col) \", f\"{len(col):,}\")" ] }, { "cell_type": "markdown", "id": "c0661555", "metadata": {}, "source": [ "Missing rows arrive masked, exactly as they do from `read()`. A single missing\n", "row is `numpy.ma.masked`, which you can test with `is`:" ] }, { "cell_type": "code", "execution_count": 5, "id": "5a6c217b", "metadata": { "execution": { "iopub.execute_input": "2026-08-24T15:35:44.576913Z", "iopub.status.busy": "2026-08-24T15:35:44.576832Z", "iopub.status.idle": "2026-08-24T15:35:44.582100Z", "shell.execute_reply": "2026-08-24T15:35:44.581696Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "col[0] is np.ma.masked: True\n", "col[1] is np.ma.masked: False\n", "\n", "18,182 missing rows of 200,000\n", "first few: [-- -- -- --]\n" ] } ], "source": [ "with h5py.File(path, \"r\") as f:\n", " col = Table.open(f[\"readings\"])[\"t_air\"]\n", " print(\"col[0] is np.ma.masked:\", col[0] is np.ma.masked)\n", " print(\"col[1] is np.ma.masked:\", col[1] is np.ma.masked)\n", "\n", " # A boolean array with one entry per row selects the rows it marks.\n", " missing = col.is_missing()\n", " print(f\"\\n{missing.sum():,} missing rows of {len(col):,}\")\n", " print(\"first few:\", col[missing][:4])" ] }, { "cell_type": "markdown", "id": "e6c8fe60", "metadata": {}, "source": [ "Subscript has nowhere to put a keyword, so it always decodes and always masks.\n", "When you want the raw fill values instead, call `read_rows`, which takes the\n", "same keys plus `masked=`:" ] }, { "cell_type": "code", "execution_count": 6, "id": "64569927", "metadata": { "execution": { "iopub.execute_input": "2026-08-24T15:35:44.583618Z", "iopub.status.busy": "2026-08-24T15:35:44.583541Z", "iopub.status.idle": "2026-08-24T15:35:44.586955Z", "shell.execute_reply": "2026-08-24T15:35:44.586665Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "masked [-- 15.1 15.2 15.3]\n", "unmasked [ nan 15.1 15.2 15.3]\n" ] } ], "source": [ "with h5py.File(path, \"r\") as f:\n", " col = Table.open(f[\"readings\"])[\"t_air\"]\n", " print(\"masked \", col.read_rows(slice(0, 4)))\n", " print(\"unmasked \", col.read_rows(slice(0, 4), masked=False))" ] }, { "cell_type": "markdown", "id": "62622039", "metadata": {}, "source": [ "## What it saves\n", "\n", "The point of all this is that the file is not read in full. We can count that\n", "directly by watching every read h5py performs." ] }, { "cell_type": "code", "execution_count": 7, "id": "5be938a0", "metadata": { "execution": { "iopub.execute_input": "2026-08-24T15:35:44.588001Z", "iopub.status.busy": "2026-08-24T15:35:44.587934Z", "iopub.status.idle": "2026-08-24T15:35:44.590229Z", "shell.execute_reply": "2026-08-24T15:35:44.589820Z" } }, "outputs": [], "source": [ "def count_reads(fn):\n", " \"\"\"Total elements *fn* pulls out of HDF5, counted at the h5py level.\"\"\"\n", " real = h5py.Dataset.__getitem__\n", " total = 0\n", "\n", " def logged(self, key):\n", " nonlocal total\n", " block = real(self, key)\n", " total += int(np.size(block))\n", " return block\n", "\n", " h5py.Dataset.__getitem__ = logged\n", " try:\n", " fn()\n", " finally:\n", " h5py.Dataset.__getitem__ = real\n", " return total\n", "\n", "\n", "def timed(fn, repeat=3):\n", " return (\n", " min(\n", " (lambda s=time.perf_counter(): (fn(), time.perf_counter() - s)[1])()\n", " for _ in range(repeat)\n", " )\n", " * 1000\n", " )" ] }, { "cell_type": "code", "execution_count": 8, "id": "e5f5fcd6", "metadata": { "execution": { "iopub.execute_input": "2026-08-24T15:35:44.591155Z", "iopub.status.busy": "2026-08-24T15:35:44.591092Z", "iopub.status.idle": "2026-08-24T15:35:44.596424Z", "shell.execute_reply": "2026-08-24T15:35:44.596102Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " elements read ms\n", "col.read() (whole column) 200,000 0.15\n", "col[100:150] 50 0.05\n", "col[100] 1 0.07\n", "col[[20, 22, 21]] 3 0.07\n" ] } ], "source": [ "with h5py.File(path, \"r\") as f:\n", " col = Table.open(f[\"readings\"])[\"t_air\"]\n", "\n", " cases = [\n", " (\"col.read() (whole column)\", lambda: col.read()),\n", " (\"col[100:150]\", lambda: col[100:150]),\n", " (\"col[100]\", lambda: col[100]),\n", " (\"col[[20, 22, 21]]\", lambda: col[[20, 22, 21]]),\n", " ]\n", " print(f\"{'':30} {'elements read':>14} {'ms':>8}\")\n", " for label, fn in cases:\n", " print(f\"{label:30} {count_reads(fn):>14,} {timed(fn):>8.2f}\")" ] }, { "cell_type": "markdown", "id": "b3a91689", "metadata": {}, "source": [ "Fifty rows cost fifty values instead of two hundred thousand. The wall-clock\n", "saving here is modest — this column is only 1.6 MB, so reading all of it was\n", "never slow — but the elements read are what decides whether a column that does\n", "*not* fit in memory can be worked with at all.\n", "\n", "Scattered positions are fetched with coalesced, chunk-aligned reads: `h5col`\n", "works out which chunks hold the wanted rows and reads those, so two rows at\n", "opposite ends of the column cost two chunks rather than everything between\n", "them." ] }, { "cell_type": "code", "execution_count": 9, "id": "d2bfa522", "metadata": { "execution": { "iopub.execute_input": "2026-08-24T15:35:44.597435Z", "iopub.status.busy": "2026-08-24T15:35:44.597379Z", "iopub.status.idle": "2026-08-24T15:35:44.600932Z", "shell.execute_reply": "2026-08-24T15:35:44.600562Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "chunk length 8,192 rows\n", "clustered [20, 22, 21] 3 elements\n", "spanning [0, N-1] 11,584 elements <- the two chunks those rows land in, not the span\n", "whole column 200,000 elements\n" ] } ], "source": [ "with h5py.File(path, \"r\") as f:\n", " col = Table.open(f[\"readings\"])[\"t_air\"]\n", " chunk = col.dataset.chunks[0]\n", " print(f\"chunk length {chunk:,} rows\")\n", " print(\n", " \"clustered [20, 22, 21] \",\n", " f\"{count_reads(lambda: col[[20, 22, 21]]):>10,} elements\",\n", " )\n", " print(\n", " \"spanning [0, N-1] \",\n", " f\"{count_reads(lambda: col[[0, N - 1]]):>10,} elements\",\n", " \" <- the two chunks those rows land in, not the span\",\n", " )\n", " print(\"whole column \", f\"{count_reads(lambda: col.read()):>10,} elements\")" ] }, { "cell_type": "markdown", "id": "04eb1b8b", "metadata": {}, "source": [ "## List columns\n", "\n", "List columns take the same keys, and this is where asking for less pays best.\n", "Reading a list column into Python has to build an object for every row, which\n", "costs far more than the reading does — so cutting the rows cuts the work\n", "twice over.\n", "\n", "They reach their rows differently, too. A list column finds its values through\n", "an `OFFSETS` array, so a range read looks up where that range's values start\n", "and end and reads only that span, at every level of nesting." ] }, { "cell_type": "code", "execution_count": 10, "id": "928873ac", "metadata": { "execution": { "iopub.execute_input": "2026-08-24T15:35:44.601918Z", "iopub.status.busy": "2026-08-24T15:35:44.601865Z", "iopub.status.idle": "2026-08-24T15:35:44.611552Z", "shell.execute_reply": "2026-08-24T15:35:44.611201Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "col[0] None <- a null row, not an empty one\n", "col[1] [np.float64(1.0), np.float64(2.0), np.float64(3.0)]\n", "col[1:3] [[np.float64(1.0), np.float64(2.0), np.float64(3.0)], [np.float64(2.0), np.float64(3.0), np.float64(4.0)]]\n", "col[-1] [np.float64(199999.0), np.float64(200000.0), np.float64(200001.0)]\n" ] } ], "source": [ "with h5py.File(path, \"r\") as f:\n", " col = Table.open(f[\"readings\"])[\"samples\"]\n", "\n", " print(\"col[0] \", col[0], \" <- a null row, not an empty one\")\n", " print(\"col[1] \", col[1])\n", " print(\"col[1:3] \", col[1:3])\n", " print(\"col[-1] \", col[-1])" ] }, { "cell_type": "code", "execution_count": 11, "id": "79bb870f", "metadata": { "execution": { "iopub.execute_input": "2026-08-24T15:35:44.612551Z", "iopub.status.busy": "2026-08-24T15:35:44.612488Z", "iopub.status.idle": "2026-08-24T15:35:45.016485Z", "shell.execute_reply": "2026-08-24T15:35:45.015986Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " elements read ms\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "col.read() (whole column) 993,815 78.12\n", "col[100:150] 251 1.33\n", "col[100] 6 1.28\n" ] } ], "source": [ "with h5py.File(path, \"r\") as f:\n", " col = Table.open(f[\"readings\"])[\"samples\"]\n", " print(f\"{'':28} {'elements read':>14} {'ms':>8}\")\n", " for label, fn in [\n", " (\"col.read() (whole column)\", lambda: col.read()),\n", " (\"col[100:150]\", lambda: col[100:150]),\n", " (\"col[100]\", lambda: col[100]),\n", " ]:\n", " print(f\"{label:28} {count_reads(fn):>14,} {timed(fn):>8.2f}\")" ] }, { "cell_type": "markdown", "id": "9e375bde", "metadata": {}, "source": [ "Unlike a scalar column, there is no chunk coalescing here: scattered positions\n", "are served from the range that *spans* them, from the lowest wanted row to the\n", "highest. Rows that sit near each other are almost free; rows at opposite ends\n", "of the column cost what the whole column costs, because the span between them\n", "is the column. It is never worse than reading it whole." ] }, { "cell_type": "code", "execution_count": 12, "id": "ca41cce3", "metadata": { "execution": { "iopub.execute_input": "2026-08-24T15:35:45.017616Z", "iopub.status.busy": "2026-08-24T15:35:45.017553Z", "iopub.status.idle": "2026-08-24T15:35:45.125512Z", "shell.execute_reply": "2026-08-24T15:35:45.124927Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "clustered [20, 22, 21] 16 elements\n", "spanning [0, N-1] 993,815 elements\n" ] } ], "source": [ "with h5py.File(path, \"r\") as f:\n", " col = Table.open(f[\"readings\"])[\"samples\"]\n", " print(\n", " \"clustered [20, 22, 21] \",\n", " f\"{count_reads(lambda: col[[20, 22, 21]]):>10,} elements\",\n", " )\n", " print(\n", " \"spanning [0, N-1] \",\n", " f\"{count_reads(lambda: col[[0, N - 1]]):>10,} elements\",\n", " )" ] }, { "cell_type": "markdown", "id": "d98802c5", "metadata": {}, "source": [ "## It composes with queries\n", "\n", "A query already reads only the rows it matched. Now the list columns in that\n", "result are narrowed too, rather than being read whole and then subset." ] }, { "cell_type": "code", "execution_count": 13, "id": "aa48103d", "metadata": { "execution": { "iopub.execute_input": "2026-08-24T15:35:45.126513Z", "iopub.status.busy": "2026-08-24T15:35:45.126447Z", "iopub.status.idle": "2026-08-24T15:35:45.213107Z", "shell.execute_reply": "2026-08-24T15:35:45.212745Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "909 matching rows\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "t_air [34.9 34.9 34.9 34.9]\n", "samples [[np.float64(199.0), np.float64(200.0), np.float64(201.0)], [np.float64(399.0), np.float64(400.0), np.float64(401.0)]]\n" ] } ], "source": [ "with h5py.File(path, \"r\") as f:\n", " table = Table.open(f[\"readings\"])\n", " selection = table.select(field(\"t_air\") > 34.8)\n", " print(f\"{selection.count:,} matching rows\")\n", "\n", " result = selection.read([\"t_air\", \"samples\"])\n", " print(\"t_air \", result[\"t_air\"][:4])\n", " print(\"samples\", result[\"samples\"][:2])" ] }, { "cell_type": "markdown", "id": "751d54f5", "metadata": {}, "source": [ "## One thing to watch\n", "\n", "`column[...]` and `column.dataset[...]` are a letter apart and are not the same\n", "read. The second goes straight to h5py: it skips decoding, ignores missing\n", "values, and can hand back rows the table does not consider part of itself.\n", "\n", "`truncate` lowers the row count but leaves the rows above it in place as\n", "reserved storage, which makes the difference easy to see:" ] }, { "cell_type": "code", "execution_count": 14, "id": "b5e909d3", "metadata": { "execution": { "iopub.execute_input": "2026-08-24T15:35:45.214136Z", "iopub.status.busy": "2026-08-24T15:35:45.214075Z", "iopub.status.idle": "2026-08-24T15:35:45.223482Z", "shell.execute_reply": "2026-08-24T15:35:45.223128Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "NROWS 10\n", "len(col) 10\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "col[:].shape (10,)\n", "col.dataset[:].shape (1000,) <- reserved rows included\n" ] } ], "source": [ "trunc_path = Path(tempfile.gettempdir()) / \"truncated.h5\"\n", "with h5py.File(trunc_path, \"w\") as f:\n", " t = Table.create(\n", " f.create_group(\"t\"),\n", " [ColumnSpec(name=\"v\", dtype=\"i8\", fill_value=-1)],\n", " )\n", " t.append({\"v\": np.arange(1000)})\n", " t.truncate(10)\n", "\n", " col = t[\"v\"]\n", " print(\"NROWS \", t.nrows)\n", " print(\"len(col) \", len(col))\n", " print(\"col[:].shape \", col[:].shape)\n", " print(\"col.dataset[:].shape \", col.dataset[:].shape, \" <- reserved rows included\")" ] }, { "cell_type": "markdown", "id": "f2fe1c57", "metadata": {}, "source": [ "Use `.dataset` when you want the stored bytes and nothing else. For reading a\n", "table, `column[...]` is the one that respects what the table says it contains.\n", "\n", "## Where to go next\n", "\n", "- The [Reading into Python](https://hdfgroup.github.io/h5col/guide/reading-into-python.html)\n", " chapter covers the same ground in prose, including what each column type\n", " hands back.\n", "- The [Exporting to Arrow](https://hdfgroup.github.io/h5col/notebooks/07_arrow_export.html)\n", " notebook shows the other way to read a large list column: it reads the whole\n", " thing, but hands the stored buffers to Arrow instead of building a Python\n", " list per row, which suits wanting most of a column rather than a slice of it.\n", "- The [queries](https://hdfgroup.github.io/h5col/queries/index.html) section\n", " covers selecting rows by predicate rather than by position." ] } ], "metadata": { "kernelspec": { "display_name": "examples", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.14.6" } }, "nbformat": 4, "nbformat_minor": 5 }