Rerun parquet ingestion
ParquetReader is a pure reader: it maps a flat table onto the Rerun
model by turning raw columns into grouped, time-indexed chunks of struct and
scalar components. Column-name prefixes become entities, grouped columns
become a single struct component, designated columns become timelines. The
reader does not assemble archetypes anymore — mapping struct fields into
typed Rerun components (Transform3D, Scalars, Points3D) is done with lenses on
the reader's .stream(). The whole reader job is configuration; fill in the
rerun-data-model mapping table first, then express it through the
constructor. Stream mechanics after .stream() are in
rerun-chunk-processing.
The whole table is configuration, not code. If you find yourself building
Chunk.from_columns from a parquet, or munging it in pandas first, stop —
ParquetReader plus a lens almost certainly expresses it. Anything the reader
cannot express (per-row entity routing, derived values, unit conversion)
belongs in lenses downstream, not in pre-pandas munging; keep the pipeline
columnar.
The API
from rerun.experimental import ParquetReader, DeriveLens
reader = ParquetReader(
table_path,
entity_path_prefix="/world", # prepended to every entity path
column_grouping="prefix", # "prefix" | "individual" | "explicit_prefixes"
delimiter="_", # split for column_grouping="prefix"
prefixes=None, # required for "explicit_prefixes"
use_structs=True, # pack grouped columns into one struct component
static_columns=["robot_type"], # constant-per-file values, logged static
index_columns=[("timestamp", "timestamp", "us"), ("frame_index", "sequence")],
)
stream = reader.stream()
Every parameter after path is keyword-only. There is no column_rules
kwarg — typed-component assembly moved to lenses (below).
What the reader emits
The reader turns the table into chunks, one chunk per group, then leaves the
data as generic struct/scalar components for lenses to map. The naming is the
key thing the rest of the pipeline keys off:
A grouped multi-column prefix X → entity /X, with a single struct
component named data. The struct's fields are the column names with the
prefix (and delimiter, for "prefix") stripped. So A_pos_x, A_quat_w
under prefix A land as struct data with fields pos_x, quat_w on
entity /A.
A lone column with no group → its own entity named after the column, and
a raw component named after the column — not a data struct. So a speed
column becomes entity /speed, component speed.
A /__properties metadata chunk built from the parquet file's schema
metadata. You typically drop it right after .stream():
stream = reader.stream().drop(content="/__properties/**")
Column grouping: which columns share an entity
"prefix" (default): split each column name on delimiter, group by the
first segment. gripper_pos_x, gripper_pos_y → entity /gripper, struct
data{pos_x, pos_y}.
"explicit_prefixes": group by the exact strings in prefixes, tried
longest-first; the prefix is stripped from each struct field name (a raw
string match, no delimiter — foo + a → field a). Columns matching no
prefix become individual groups. Use this when names contain the delimiter
ambiguously (observation.state vs observation.images.top: pass the full
prefixes).
"individual": every column is its own chunk/entity with a raw component
named after the column — no struct packing at all, even for columns sharing a
prefix. use_structs is ignored here. Rarely the model you want; reach for
it only as a debugging baseline.
use_structs=True (default) packs a group's columns into a single Arrow
struct component (the data field) for "prefix"/"explicit_prefixes";
False emits one component per column (the pre-struct flat layout, what
queries see as separate columns).
Timelines: index_columns
Each entry is (name, type) or (name, type, unit):
type: "timestamp" (since epoch), "duration" (elapsed), "sequence"
(ordinal int).
unit describes what the raw integers in the column are ("ns" default,
"us", "ms", "s"); Rerun rescales to ns internally. Ignored for
"sequence".
If omitted, a synthetic row_index sequence timeline is generated. That
is almost never the timeline you want to query or align against; always name
the real time columns. Stamp both a timestamp and a sequence timeline when the
table has both (multi-rate alignment, see rerun-data-model).
Static columns: static_columns
Listed columns are constant across all rows; they are emitted once as a single
static (timeless) chunk, separate from the temporal data. A listed column that
actually varies raises an error when the stream runs — that error is a
data-quality signal, not a reason to drop the static declaration.
Typed components via lenses
The reader's grouped output is generic struct (data) and scalar data. A
DeriveLens reads that struct's fields, packs and casts them into real Rerun
components, and writes them to an output entity — this is what the old
column_rules API used to do, now done downstream on the stream.
Construct a lens against the reader's struct component ("data" for grouped
prefixes, or the column name for a lone/individual column), then add one or
more .to_* builder methods. Each builder returns a fresh lens, so they chain.
| Builder |
Produces |
Argument order |
to_translation(x, y, z) |
Transform3D:translation |
x, y, z |
to_quaternion(x, y, z, w) |
Transform3D:quaternion |
x, y, z, w (xyzw) |
to_scale(x, y, z) |
Transform3D:scale |
x, y, z |
to_rotation_axis_angle(axis_x, axis_y, axis_z, angle) |
Transform3D:rotation_axis_angle |
axis_x, axis_y, axis_z, angle (radians) |
to_scalars(*fields) |
Scalars:scalars |
one or more field names |
to_packed_component(component, *fields) |
the given component |
descriptor, then field names |
to_component(component, selector) |
the given component |
descriptor, then a Selector |
to_timeline(name, type, selector) |
a timeline (not a component) |
name, "sequence"/"duration_ns"/"timestamp_ns", selector |
to_packed_component packs the named struct fields (in order, at least one
required) into the fixed-size list the component expects, and by default
auto-casts f64→f32 to match component types. The to_translation,
to_quaternion, to_scale helpers are convenience wrappers over it, so they
all auto-cast. to_rotation_axis_angle builds a Struct{axis, angle} and
hard-casts axis and angle to f32 internally. to_scalars with a single field
emits a plain scalar per row (not a 1-element list); with multiple fields it
emits one scalar series per field at the same entity.
Apply lenses with .stream().lenses([lens], content="/A", output_mode="drop_unmatched"):
content is a pre-filter on the source entity path — it scopes which
chunks the lens may touch. Out-of-scope chunks pass through unchanged. Set it
to the reader's grouped entity (e.g. "/A").
output_mode decides the fate of in-scope-but-unmatched chunks:
"drop_unmatched" (default, keep only lens output), "forward_unmatched"
(output replaces matched, other originals survive), or "forward_all"
(output plus all originals).
- The lens's own
output_entity= sets the destination entity — independent
of content, which gates the input side.
End-to-end Transform3D example. The reader groups A_* columns into a data
struct at /A; the lens reads the prefix-stripped field names (pos_x,
quat_w), packs and casts them, and writes a full Transform3D to /pose:
from rerun.experimental import DeriveLens, ParquetReader
lens = (
DeriveLens("data", output_entity="/pose")
.to_translation("pos_x", "pos_y", "pos_z")
.to_quaternion("quat_x", "quat_y", "quat_z", "quat_w")
)
chunks = (
ParquetReader(table_path, index_columns=[("frame_index", "sequence")])
.stream()
.lenses([lens], content="/A", output_mode="drop_unmatched")
.to_chunks()
)
Chaining several .to_* on one lens with a shared output_entity
accumulates multiple component columns into the same archetype at that entity —
above, both Transform3D:translation and Transform3D:quaternion land on
/pose, forming a complete Transform3D. For a generic fixed-size-list
component, pass the descriptor to to_packed_component:
import rerun as rr
from rerun.experimental import DeriveLens, ParquetReader
lens = DeriveLens("data", output_entity="/points").to_packed_component(
rr.Points3D.descriptor_positions(), "x", "y", "z"
)
Selectors
Lens field paths use Selector, a jq-like grammar over Arrow columns
(.field to access a struct field, [] to iterate a list, [N] to index, ?
to suppress errors on absent fields, ! to assert non-null, | to pipe, and
pack(.x, .y, .z) to zip paths into a fixed-size list). The to_* helpers
build these selectors for you; reach for to_component(component, Selector(".x"))
when you need a custom field path. Field paths reference the
prefix-stripped struct field names — the lens sees pos_x, not A_pos_x.
Gotchas
- No
index_columns → synthetic row_index timeline only. Queries that
expect a timestamp timeline find nothing.
- The
unit is the raw column's unit, not a desired output unit; a
microsecond column declared "ns" lands 1000x in the past.
static_columns raises if a listed column actually varies; that error is a
data-quality signal, not a reason to drop the static declaration. It is
raised lazily when the stream runs, not at construction.
- A grouped prefix's struct component is named
data — that is the
input_component string a DeriveLens matches against. A lone or
"individual" column is instead a raw component named after the column.
- Selector field paths reference the prefix-stripped struct field names
(
pos_x, not gripper_pos_x).
- Drop the
/__properties metadata chunk the reader emits from parquet schema
metadata: .stream().drop(content="/__properties/**").
- Quaternion column order is x, y, z, w in
to_quaternion; check the source's
convention before wiring fields.
to_packed_component (and the transform helpers built on it) auto-casts
f64→f32 to match component types; this is usually what you want for
parquet's double columns.
- Anything the reader cannot express (per-row entity routing, derived values,
unit conversion) belongs in lenses downstream, not in pre-pandas munging;
keep the pipeline columnar (
rerun-chunk-processing).
References
- Lens builder source with full docstrings:
rerun/experimental/_lens.py in
the installed rerun-sdk package (to_translation, to_quaternion,
to_scale, to_rotation_axis_angle, to_scalars, to_packed_component,
to_component, to_timeline).
- Reader source:
rerun/experimental/_parquet_reader.py, or
python -c "from rerun.experimental import ParquetReader; help(ParquetReader)"
- Canonical worked examples: the integration tests
rerun_py/tests/integration/test_parquet_reader.py (grouping, index/static
columns, and the Transform3D / Points3D / Scalars lens flows) and
rerun_py/tests/integration/test_lazy_chunk_stream.py (lens application,
content/output_mode, selectors).
rerun-lerobot — LeRobot datasets store episodes as parquet; that skill
covers the built-in importer route vs reading the parquet directly with
this reader.
rerun-data-model (mapping decisions), rerun-chunk-processing (stream
mechanics after .stream())
1---2name: rerun-parquet-33description: Ingest tabular Parquet files into Rerun chunk streams with rerun.experimental.ParquetReader. Read when converting trajectory or sensor tables (LeRobot-style parquet, exported logs) into entities and components — column grouping, timeline/index columns, static columns, and lenses (DeriveLens) that assemble the typed components (Transform3D, Scalars) from the reader's grouped struct/scalar output. Builds on rerun-chunk-processing and rerun-data-model.4---56# Rerun parquet ingestion78`ParquetReader` is a **pure reader**: it maps a flat table onto the Rerun9model by turning raw columns into grouped, time-indexed chunks of struct and10scalar components. Column-name prefixes become entities, grouped columns11become a single struct component, designated columns become timelines. The12reader does **not** assemble archetypes anymore — mapping struct fields into13typed Rerun components (Transform3D, Scalars, Points3D) is done with lenses on14the reader's `.stream()`. The whole reader job is configuration; fill in the15`rerun-data-model` mapping table first, then express it through the16constructor. Stream mechanics after `.stream()` are in17`rerun-chunk-processing`.1819**The whole table is configuration, not code.** If you find yourself building20`Chunk.from_columns` from a parquet, or munging it in pandas first, stop —21`ParquetReader` plus a lens almost certainly expresses it. Anything the reader22cannot express (per-row entity routing, derived values, unit conversion)23belongs in lenses downstream, not in pre-pandas munging; keep the pipeline24columnar.2526## The API2728```python29from rerun.experimental import ParquetReader, DeriveLens3031reader = ParquetReader(32 table_path,33 entity_path_prefix="/world", # prepended to every entity path34 column_grouping="prefix", # "prefix" | "individual" | "explicit_prefixes"35 delimiter="_", # split for column_grouping="prefix"36 prefixes=None, # required for "explicit_prefixes"37 use_structs=True, # pack grouped columns into one struct component38 static_columns=["robot_type"], # constant-per-file values, logged static39 index_columns=[("timestamp", "timestamp", "us"), ("frame_index", "sequence")],40)41stream = reader.stream()42```4344Every parameter after `path` is keyword-only. There is no `column_rules`45kwarg — typed-component assembly moved to lenses (below).4647## What the reader emits4849The reader turns the table into chunks, one chunk per group, then leaves the50data as generic struct/scalar components for lenses to map. The naming is the51key thing the rest of the pipeline keys off:5253- **A grouped multi-column prefix `X`** → entity `/X`, with a single struct54 component named **`data`**. The struct's fields are the column names with the55 prefix (and delimiter, for `"prefix"`) stripped. So `A_pos_x`, `A_quat_w`56 under prefix `A` land as struct `data` with fields `pos_x`, `quat_w` on57 entity `/A`.58- **A lone column with no group** → its own entity named after the column, and59 a raw component named after the column — *not* a `data` struct. So a `speed`60 column becomes entity `/speed`, component `speed`.61- **A `/__properties` metadata chunk** built from the parquet file's schema62 metadata. You typically drop it right after `.stream()`:6364 ```python65 stream = reader.stream().drop(content="/__properties/**")66 ```6768## Column grouping: which columns share an entity6970- `"prefix"` (default): split each column name on `delimiter`, group by the71 first segment. `gripper_pos_x`, `gripper_pos_y` → entity `/gripper`, struct72 `data{pos_x, pos_y}`.73- `"explicit_prefixes"`: group by the exact strings in `prefixes`, tried74 longest-first; the prefix is stripped from each struct field name (a raw75 string match, no delimiter — `foo` + `a` → field `a`). Columns matching no76 prefix become individual groups. Use this when names contain the delimiter77 ambiguously (`observation.state` vs `observation.images.top`: pass the full78 prefixes).79- `"individual"`: every column is its own chunk/entity with a raw component80 named after the column — no struct packing at all, even for columns sharing a81 prefix. `use_structs` is ignored here. Rarely the model you want; reach for82 it only as a debugging baseline.8384`use_structs=True` (default) packs a group's columns into a single Arrow85struct component (the `data` field) for `"prefix"`/`"explicit_prefixes"`;86`False` emits one component per column (the pre-struct flat layout, what87queries see as separate columns).8889## Timelines: `index_columns`9091Each entry is `(name, type)` or `(name, type, unit)`:9293- `type`: `"timestamp"` (since epoch), `"duration"` (elapsed), `"sequence"`94 (ordinal int).95- `unit` describes what the raw integers in the column *are* (`"ns"` default,96 `"us"`, `"ms"`, `"s"`); Rerun rescales to ns internally. Ignored for97 `"sequence"`.9899**If omitted, a synthetic `row_index` sequence timeline is generated.** That100is almost never the timeline you want to query or align against; always name101the real time columns. Stamp both a timestamp and a sequence timeline when the102table has both (multi-rate alignment, see `rerun-data-model`).103104## Static columns: `static_columns`105106Listed columns are constant across all rows; they are emitted once as a single107static (timeless) chunk, separate from the temporal data. A listed column that108actually varies raises an error when the stream runs — that error is a109data-quality signal, not a reason to drop the static declaration.110111## Typed components via lenses112113The reader's grouped output is generic struct (`data`) and scalar data. A114`DeriveLens` reads that struct's fields, packs and casts them into real Rerun115components, and writes them to an output entity — this is what the old116`column_rules` API used to do, now done downstream on the stream.117118Construct a lens against the reader's struct component (`"data"` for grouped119prefixes, or the column name for a lone/individual column), then add one or120more `.to_*` builder methods. Each builder returns a fresh lens, so they chain.121122| Builder | Produces | Argument order |123|---|---|---|124| `to_translation(x, y, z)` | `Transform3D:translation` | x, y, z |125| `to_quaternion(x, y, z, w)` | `Transform3D:quaternion` | x, y, z, w (xyzw) |126| `to_scale(x, y, z)` | `Transform3D:scale` | x, y, z |127| `to_rotation_axis_angle(axis_x, axis_y, axis_z, angle)` | `Transform3D:rotation_axis_angle` | axis_x, axis_y, axis_z, angle (radians) |128| `to_scalars(*fields)` | `Scalars:scalars` | one or more field names |129| `to_packed_component(component, *fields)` | the given component | descriptor, then field names |130| `to_component(component, selector)` | the given component | descriptor, then a `Selector` |131| `to_timeline(name, type, selector)` | a timeline (not a component) | name, `"sequence"`/`"duration_ns"`/`"timestamp_ns"`, selector |132133`to_packed_component` packs the named struct fields (in order, at least one134required) into the fixed-size list the component expects, and by default135**auto-casts `f64`→`f32`** to match component types. The `to_translation`,136`to_quaternion`, `to_scale` helpers are convenience wrappers over it, so they137all auto-cast. `to_rotation_axis_angle` builds a `Struct{axis, angle}` and138hard-casts axis and angle to `f32` internally. `to_scalars` with a single field139emits a plain scalar per row (not a 1-element list); with multiple fields it140emits one scalar series per field at the same entity.141142Apply lenses with `.stream().lenses([lens], content="/A", output_mode="drop_unmatched")`:143144- `content` is a pre-filter on the *source* entity path — it scopes which145 chunks the lens may touch. Out-of-scope chunks pass through unchanged. Set it146 to the reader's grouped entity (e.g. `"/A"`).147- `output_mode` decides the fate of in-scope-but-unmatched chunks:148 `"drop_unmatched"` (default, keep only lens output), `"forward_unmatched"`149 (output replaces matched, other originals survive), or `"forward_all"`150 (output plus all originals).151- The lens's own `output_entity=` sets the *destination* entity — independent152 of `content`, which gates the input side.153154End-to-end Transform3D example. The reader groups `A_*` columns into a `data`155struct at `/A`; the lens reads the prefix-stripped field names (`pos_x`,156`quat_w`), packs and casts them, and writes a full `Transform3D` to `/pose`:157158```python159from rerun.experimental import DeriveLens, ParquetReader160161lens = (162 DeriveLens("data", output_entity="/pose")163 .to_translation("pos_x", "pos_y", "pos_z")164 .to_quaternion("quat_x", "quat_y", "quat_z", "quat_w")165)166167chunks = (168 ParquetReader(table_path, index_columns=[("frame_index", "sequence")])169 .stream()170 .lenses([lens], content="/A", output_mode="drop_unmatched")171 .to_chunks()172)173```174175Chaining several `.to_*` on **one lens with a shared `output_entity`**176accumulates multiple component columns into the same archetype at that entity —177above, both `Transform3D:translation` and `Transform3D:quaternion` land on178`/pose`, forming a complete `Transform3D`. For a generic fixed-size-list179component, pass the descriptor to `to_packed_component`:180181```python182import rerun as rr183from rerun.experimental import DeriveLens, ParquetReader184185lens = DeriveLens("data", output_entity="/points").to_packed_component(186 rr.Points3D.descriptor_positions(), "x", "y", "z"187)188```189190## Selectors191192Lens field paths use `Selector`, a jq-like grammar over Arrow columns193(`.field` to access a struct field, `[]` to iterate a list, `[N]` to index, `?`194to suppress errors on absent fields, `!` to assert non-null, `|` to pipe, and195`pack(.x, .y, .z)` to zip paths into a fixed-size list). The `to_*` helpers196build these selectors for you; reach for `to_component(component, Selector(".x"))`197when you need a custom field path. Field paths reference the198**prefix-stripped** struct field names — the lens sees `pos_x`, not `A_pos_x`.199200## Gotchas2012021. No `index_columns` → synthetic `row_index` timeline only. Queries that203 expect a timestamp timeline find nothing.2042. The `unit` is the raw column's unit, not a desired output unit; a205 microsecond column declared `"ns"` lands 1000x in the past.2063. `static_columns` raises if a listed column actually varies; that error is a207 data-quality signal, not a reason to drop the static declaration. It is208 raised lazily when the stream runs, not at construction.2094. A grouped prefix's struct component is named **`data`** — that is the210 `input_component` string a `DeriveLens` matches against. A lone or211 `"individual"` column is instead a raw component named after the column.2125. Selector field paths reference the **prefix-stripped** struct field names213 (`pos_x`, not `gripper_pos_x`).2146. Drop the `/__properties` metadata chunk the reader emits from parquet schema215 metadata: `.stream().drop(content="/__properties/**")`.2167. Quaternion column order is x, y, z, w in `to_quaternion`; check the source's217 convention before wiring fields.2188. `to_packed_component` (and the transform helpers built on it) auto-casts219 `f64`→`f32` to match component types; this is usually what you want for220 parquet's double columns.2219. Anything the reader cannot express (per-row entity routing, derived values,222 unit conversion) belongs in lenses downstream, not in pre-pandas munging;223 keep the pipeline columnar (`rerun-chunk-processing`).224225## References226227- Lens builder source with full docstrings: `rerun/experimental/_lens.py` in228 the installed `rerun-sdk` package (`to_translation`, `to_quaternion`,229 `to_scale`, `to_rotation_axis_angle`, `to_scalars`, `to_packed_component`,230 `to_component`, `to_timeline`).231- Reader source: `rerun/experimental/_parquet_reader.py`, or232 `python -c "from rerun.experimental import ParquetReader; help(ParquetReader)"`233- Canonical worked examples: the integration tests234 `rerun_py/tests/integration/test_parquet_reader.py` (grouping, index/static235 columns, and the Transform3D / Points3D / Scalars lens flows) and236 `rerun_py/tests/integration/test_lazy_chunk_stream.py` (lens application,237 `content`/`output_mode`, selectors).238- `rerun-lerobot` — LeRobot datasets store episodes as parquet; that skill239 covers the built-in importer route vs reading the parquet directly with240 this reader.241- `rerun-data-model` (mapping decisions), `rerun-chunk-processing` (stream242 mechanics after `.stream()`)