How to Load Tabular Data

Custom TsvLoader/CsvLoader, column_specs typed fields, nested-JSON Field access, group_by child timelines

How to Load Tabular Data

The fastest way to get music data into TimeToAlign! is through tabular loaders. If your data is in CSV or TSV format, you’re just 3 lines of code away from analysis.

What you’ll learn: - Load music annotations from TSV/CSV files - Access event counts, coordinate ranges, and metadata - Create timelines from loaded data - Write a custom loader that maps your columns to event fields - Promote selected columns to typed fields with column_specs - Reach nested JSON columns with Field

Time: 15 minutes

This is the gentle introduction. For the full column-to-field mechanism — the Step-1 resolution chain, composite columns, and Step-2 field promotion — see the in-depth CSV/TSV how-to.

TL;DR

from timetoalign.loader.tabular import TsvLoader

loader = TsvLoader()
loader.load("beethoven.notes.tsv")

df = loader.events.to_dataframe()    # Get as DataFrame
timeline = loader.create_timeline()  # Create Timeline

Setup

from timetoalign.testdata import ensure_data

BEETHOVEN = ensure_data("score") / "beethoven_woo71"
THORESEN = ensure_data("thoresen")

# Available files
{
    "Beethoven files": [f.name for f in BEETHOVEN.glob("WoO71.*.tsv")],
    "Thoresen files": [f.name for f in THORESEN.glob("*.tsv")],
}
{'Beethoven files': ['WoO71.chords.tsv',
  'WoO71.measures.tsv',
  'WoO71.notes.tsv'],
 'Thoresen files': ['thoresen_test_h.tsv', 'thoresen_test.tsv']}

Loading Notes from TSV

TsvLoader is the generic starting point for a TSV whose columns you control. Declare the coordinate columns and promote the values you want as typed fields. For a standard ms3 score export, use the score Ms3Loader instead; it understands its notes, measures, chords, and harmonies facets.

Three lines of code:

from timetoalign.core import NumberType, TimeUnit  # noqa: E402
from timetoalign.loader.tabular import TsvLoader  # noqa: E402


class BeethovenNotesLoader(TsvLoader):
    """Generic TSV configuration for the coordinate and pitch columns used here."""

    start_column = "quarterbeats"
    duration_column = "duration_qb"
    _default_unit = TimeUnit.quarters
    coordinate_type = NumberType.fraction
    column_specs = {"midi": int, "staff": int, "voice": int}


loader = BeethovenNotesLoader()
loader.load(BEETHOVEN / "WoO71.notes.tsv")

f"{len(loader.events):,} notes loaded"
/home/laser/miniconda3/envs/timetoalign/lib/python3.11/site-packages/partitura/__init__.py:9: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
  import pkg_resources
'4,753 notes loaded'

Converting to pandas

Use to_dataframe() to get a DataFrame with clean coordinate values:

loader.events.to_dataframe().head()
id name temporal_type event_type start end duration staff voice tpc ... quarterbeats_all_endings midi mc_onset chord_id scalar gracenote timesig octave volta mn_onset
0 e000000 A3 interval Event 0 1.0 1.0 2 1 3 ... 0 57 0 3 1 None 2/4 3 NaN 1/4
1 e000001 E4 interval Event 0 1.0 1.0 1 2 4 ... 0 64 0 2 1 None 2/4 4 NaN 1/4
2 e000002 A4 interval Event 0 0.5 0.5 1 1 3 ... 0 69 0 0 1 None 2/4 4 NaN 1/4
3 e000003 C#5 interval Event 0 0.5 0.5 1 1 7 ... 0 73 0 0 1 None 2/4 5 NaN 1/4
4 e000004 E5 interval Event 1/2 1.0 0.5 1 1 4 ... 1/2 76 1/8 1 1 None 2/4 5 NaN 3/8

5 rows × 24 columns

Quick Statistics

The loader provides immediate access to summary information:

{
    "event_count": len(loader.events),
    "coordinate_range": loader.events.coordinate_range(),
    "unit": str(loader.unit),
    "number_type": str(loader.number_type),
}
{'event_count': 4753,
 'coordinate_range': (Fraction(0, 1), 876.5),
 'unit': 'quarters',
 'number_type': 'fraction'}

Creating Timelines

TimeToAlign! represents temporal data as Timelines:

timeline = loader.create_timeline(uid="beethoven_notes")
timeline
ContinuousLogicalTimeline[beethoven_notes] (4753 events)
                      0 _______________________________ 876.5 quarters

Custom Loaders for Non-Standard Formats

For files that don’t match the ms3 format, create a custom loader by subclassing TsvLoader or CsvLoader. You point the canonical column attributes (start_column, duration_column, …) at the names in your file.

Let’s load the Thoresen annotations file, which has a different column structure:

import pandas as pd  # noqa: E402

pd.read_csv(THORESEN / "thoresen_test.tsv", sep="\t", nrows=3)
event_id alignment_group_id start_time_sec duration_sec event_type graphical_element_id image_filename rect_coords_json text_content text_anchor_xy_json layer_order description
0 annot_cue_001 NaN 0.0 5.0 rectangle rect_a thoresen_2010_form-building-patterns_p90-91_pa... {"x": 10, "y": 90, "width": 148, "height": 55} NaN NaN NaN NaN
1 annot_cue_002 NaN 1.5 4.0 rectangle rect_b thoresen_2010_form-building-patterns_p90-91_pa... {"x": 40, "y": 37, "width": 127, "height": 21} NaN NaN NaN NaN
2 annot_cue_003 NaN 3.5 2.0 rectangle rect_c thoresen_2010_form-building-patterns_p90-91_pa... {"x": 111, "y": 60, "width": 57, "height": 23} NaN NaN NaN NaN

The Simplest Custom Loader

Map the core coordinate columns and nothing else. Every source column that you do not name survives as an opaque property column — carried alongside the event so you never lose data, but left untyped:

from timetoalign.loader.tabular import TsvLoader  # noqa: E402


class ThoresenLoader(TsvLoader):
    """Minimal loader — maps the core event fields."""

    id_column = "event_id"
    start_column = "start_time_sec"
    duration_column = "duration_sec"
    event_type_column = "event_type"
    name_column = "description"

    _default_unit = TimeUnit.seconds
    coordinate_type = NumberType.float


thoresen = ThoresenLoader()
thoresen.load(THORESEN / "thoresen_test.tsv")
thoresen.events.to_dataframe()
id name temporal_type event_type start end duration text_content layer_order text_anchor_xy_json rect_coords_json graphical_element_id image_filename alignment_group_id
0 annot_cue_001 None interval rectangle 0.0 5.00 5.00 NaN NaN NaN {"x": 10, "y": 90, "width": 148, "height": 55} rect_a thoresen_2010_form-building-patterns_p90-91_pa... NaN
1 annot_cue_002 None interval rectangle 1.5 5.50 4.00 NaN NaN NaN {"x": 40, "y": 37, "width": 127, "height": 21} rect_b thoresen_2010_form-building-patterns_p90-91_pa... NaN
2 annot_cue_003 None interval rectangle 3.5 5.50 2.00 NaN NaN NaN {"x": 111, "y": 60, "width": 57, "height": 23} rect_c thoresen_2010_form-building-patterns_p90-91_pa... NaN
3 annot_cue_004 None interval rectangle 34.6 39.80 5.20 NaN NaN NaN {"x": 145, "y": 90, "width": 160, "height": 58} rect_a2 thoresen_2010_form-building-patterns_p90-91_pa... NaN
4 annot_cue_005 None interval rectangle 43.5 48.00 4.50 NaN NaN NaN {"x": 385, "y": 46, "width": 139, "height": 20} rect_h2 thoresen_2010_form-building-patterns_p90-91_pa... NaN
5 annot_cue_006 None interval rectangle 71.0 75.75 4.75 NaN NaN NaN {"x": 310, "y": 93, "width": 154, "height": 18} rect_d3 thoresen_2010_form-building-patterns_p90-91_pa... NaN
6 annot_cue_007 None interval rectangle 76.0 83.50 7.50 NaN NaN NaN {"x": 456, "y": 69, "width": 229, "height": 18} rect_b3 thoresen_2010_form-building-patterns_p90-91_pa... NaN
7 annot_cue_008 None interval rectangle 90.5 94.50 4.00 NaN NaN NaN {"x": 14, "y": 115, "width": 127, "height": 31} rect_i4 thoresen_2010_form-building-patterns_p90-91_pa... NaN
8 annot_cue_009 None interval rectangle 113.4 116.40 3.00 NaN NaN NaN {"x": 663, "y": 82, "width": 97, "height": 23} rect_a4 thoresen_2010_form-building-patterns_p90-91_pa... NaN
9 annot_cue_010 None interval rectangle 121.0 128.50 7.50 NaN NaN NaN {"x": 19, "y": 119, "width": 251, "height": 29} rect_i5 thoresen_2010_form-building-patterns_p90-91_pa... NaN
10 annot_cue_011 None interval rectangle 141.0 142.50 1.50 NaN NaN NaN {"x": 595, "y": 45, "width": 64, "height": 21} rect_f5 thoresen_2010_form-building-patterns_p90-91_pa... NaN

Promoting Columns to Typed Fields with column_specs

When a property column should become a typed field, name it in column_specs. The keys are source-column names; the values are anything the loader can resolve to a field — a bare Python type (int / float / str) is the simplest form. Here we give two extra columns explicit types:

class ThoresenTypedLoader(TsvLoader):
    """Selected columns promoted to typed fields."""

    id_column = "event_id"
    start_column = "start_time_sec"
    duration_column = "duration_sec"
    event_type_column = "event_type"
    name_column = "description"

    _default_unit = TimeUnit.seconds
    coordinate_type = NumberType.float

    # Promote these source columns to typed fields.
    column_specs = {
        "image_filename": str,
        "graphical_element_id": str,
    }


typed = ThoresenTypedLoader()
typed.load(THORESEN / "thoresen_test.tsv")
typed.events.to_dataframe()
id name temporal_type event_type start end duration text_content layer_order text_anchor_xy_json rect_coords_json graphical_element_id image_filename alignment_group_id
0 annot_cue_001 None interval rectangle 0.0 5.00 5.00 NaN NaN NaN {"x": 10, "y": 90, "width": 148, "height": 55} rect_a thoresen_2010_form-building-patterns_p90-91_pa... NaN
1 annot_cue_002 None interval rectangle 1.5 5.50 4.00 NaN NaN NaN {"x": 40, "y": 37, "width": 127, "height": 21} rect_b thoresen_2010_form-building-patterns_p90-91_pa... NaN
2 annot_cue_003 None interval rectangle 3.5 5.50 2.00 NaN NaN NaN {"x": 111, "y": 60, "width": 57, "height": 23} rect_c thoresen_2010_form-building-patterns_p90-91_pa... NaN
3 annot_cue_004 None interval rectangle 34.6 39.80 5.20 NaN NaN NaN {"x": 145, "y": 90, "width": 160, "height": 58} rect_a2 thoresen_2010_form-building-patterns_p90-91_pa... NaN
4 annot_cue_005 None interval rectangle 43.5 48.00 4.50 NaN NaN NaN {"x": 385, "y": 46, "width": 139, "height": 20} rect_h2 thoresen_2010_form-building-patterns_p90-91_pa... NaN
5 annot_cue_006 None interval rectangle 71.0 75.75 4.75 NaN NaN NaN {"x": 310, "y": 93, "width": 154, "height": 18} rect_d3 thoresen_2010_form-building-patterns_p90-91_pa... NaN
6 annot_cue_007 None interval rectangle 76.0 83.50 7.50 NaN NaN NaN {"x": 456, "y": 69, "width": 229, "height": 18} rect_b3 thoresen_2010_form-building-patterns_p90-91_pa... NaN
7 annot_cue_008 None interval rectangle 90.5 94.50 4.00 NaN NaN NaN {"x": 14, "y": 115, "width": 127, "height": 31} rect_i4 thoresen_2010_form-building-patterns_p90-91_pa... NaN
8 annot_cue_009 None interval rectangle 113.4 116.40 3.00 NaN NaN NaN {"x": 663, "y": 82, "width": 97, "height": 23} rect_a4 thoresen_2010_form-building-patterns_p90-91_pa... NaN
9 annot_cue_010 None interval rectangle 121.0 128.50 7.50 NaN NaN NaN {"x": 19, "y": 119, "width": 251, "height": 29} rect_i5 thoresen_2010_form-building-patterns_p90-91_pa... NaN
10 annot_cue_011 None interval rectangle 141.0 142.50 1.50 NaN NaN NaN {"x": 595, "y": 45, "width": 64, "height": 21} rect_f5 thoresen_2010_form-building-patterns_p90-91_pa... NaN

That is the entire idea: columns are a source artefact; fields are a TimeToAlign! artefact. column_specs is the bridge. A bare type is the gentlest entry — the in-depth CSV/TSV how-to covers the full resolution chain (composite columns, semantic pitch/id fields, and the Step-2 field_specs promotion stage) for richer formats.

Nested JSON Column Access with Field

The Thoresen data has a rect_coords_json column containing pixel coordinates as JSON:

{"x": 10, "y": 90, "width": 148, "height": 55}

Use Field("column", "nested_field") to point a coordinate attribute straight at a nested value. TimeToAlign! parses the JSON automatically. ComputedField lets you derive a coordinate from a small formula over those nested values:

from timetoalign.loader import ComputedField, Field  # noqa: E402


class ThoresenGraphicalLoader(TsvLoader):
    """Loader using PIXEL coordinates from a nested JSON column."""

    # Nested fields are addressed directly; JSON is parsed automatically.
    start_column = Field("rect_coords_json", "x")
    end_column = ComputedField(
        "end", formula="rect_coords_json.x + rect_coords_json.width"
    )

    _default_unit = TimeUnit.pixels
    coordinate_type = NumberType.float
    default_event_type = "Rectangle"


graphical = ThoresenGraphicalLoader()
graphical.load(THORESEN / "thoresen_test.tsv")

{
    "unit": str(graphical.unit),
    "coordinate_range": graphical.events.coordinate_range(),
}
{'unit': 'pixels', 'coordinate_range': (10.0, 760.0)}

Two Coordinate Systems from One File

The same TSV file backs timelines in different coordinate systems — seconds from the time columns, pixels from the JSON column:

# Physical timeline (seconds)
typed.events.to_dataframe()
id name temporal_type event_type start end duration text_content layer_order text_anchor_xy_json rect_coords_json graphical_element_id image_filename alignment_group_id
0 annot_cue_001 None interval rectangle 0.0 5.00 5.00 NaN NaN NaN {"x": 10, "y": 90, "width": 148, "height": 55} rect_a thoresen_2010_form-building-patterns_p90-91_pa... NaN
1 annot_cue_002 None interval rectangle 1.5 5.50 4.00 NaN NaN NaN {"x": 40, "y": 37, "width": 127, "height": 21} rect_b thoresen_2010_form-building-patterns_p90-91_pa... NaN
2 annot_cue_003 None interval rectangle 3.5 5.50 2.00 NaN NaN NaN {"x": 111, "y": 60, "width": 57, "height": 23} rect_c thoresen_2010_form-building-patterns_p90-91_pa... NaN
3 annot_cue_004 None interval rectangle 34.6 39.80 5.20 NaN NaN NaN {"x": 145, "y": 90, "width": 160, "height": 58} rect_a2 thoresen_2010_form-building-patterns_p90-91_pa... NaN
4 annot_cue_005 None interval rectangle 43.5 48.00 4.50 NaN NaN NaN {"x": 385, "y": 46, "width": 139, "height": 20} rect_h2 thoresen_2010_form-building-patterns_p90-91_pa... NaN
5 annot_cue_006 None interval rectangle 71.0 75.75 4.75 NaN NaN NaN {"x": 310, "y": 93, "width": 154, "height": 18} rect_d3 thoresen_2010_form-building-patterns_p90-91_pa... NaN
6 annot_cue_007 None interval rectangle 76.0 83.50 7.50 NaN NaN NaN {"x": 456, "y": 69, "width": 229, "height": 18} rect_b3 thoresen_2010_form-building-patterns_p90-91_pa... NaN
7 annot_cue_008 None interval rectangle 90.5 94.50 4.00 NaN NaN NaN {"x": 14, "y": 115, "width": 127, "height": 31} rect_i4 thoresen_2010_form-building-patterns_p90-91_pa... NaN
8 annot_cue_009 None interval rectangle 113.4 116.40 3.00 NaN NaN NaN {"x": 663, "y": 82, "width": 97, "height": 23} rect_a4 thoresen_2010_form-building-patterns_p90-91_pa... NaN
9 annot_cue_010 None interval rectangle 121.0 128.50 7.50 NaN NaN NaN {"x": 19, "y": 119, "width": 251, "height": 29} rect_i5 thoresen_2010_form-building-patterns_p90-91_pa... NaN
10 annot_cue_011 None interval rectangle 141.0 142.50 1.50 NaN NaN NaN {"x": 595, "y": 45, "width": 64, "height": 21} rect_f5 thoresen_2010_form-building-patterns_p90-91_pa... NaN
# Graphical timeline (pixels)
graphical.events.to_dataframe()
id name temporal_type event_type start end duration text_content event_id graphical_element_id layer_order text_anchor_xy_json description duration_sec image_filename alignment_group_id start_time_sec
0 e000000 None interval rectangle 10.0 158.0 148.0 NaN annot_cue_001 rect_a NaN NaN NaN 5.00 thoresen_2010_form-building-patterns_p90-91_pa... NaN 0.0
1 e000001 None interval rectangle 40.0 167.0 127.0 NaN annot_cue_002 rect_b NaN NaN NaN 4.00 thoresen_2010_form-building-patterns_p90-91_pa... NaN 1.5
2 e000002 None interval rectangle 111.0 168.0 57.0 NaN annot_cue_003 rect_c NaN NaN NaN 2.00 thoresen_2010_form-building-patterns_p90-91_pa... NaN 3.5
3 e000003 None interval rectangle 145.0 305.0 160.0 NaN annot_cue_004 rect_a2 NaN NaN NaN 5.20 thoresen_2010_form-building-patterns_p90-91_pa... NaN 34.6
4 e000004 None interval rectangle 385.0 524.0 139.0 NaN annot_cue_005 rect_h2 NaN NaN NaN 4.50 thoresen_2010_form-building-patterns_p90-91_pa... NaN 43.5
5 e000005 None interval rectangle 310.0 464.0 154.0 NaN annot_cue_006 rect_d3 NaN NaN NaN 4.75 thoresen_2010_form-building-patterns_p90-91_pa... NaN 71.0
6 e000006 None interval rectangle 456.0 685.0 229.0 NaN annot_cue_007 rect_b3 NaN NaN NaN 7.50 thoresen_2010_form-building-patterns_p90-91_pa... NaN 76.0
7 e000007 None interval rectangle 14.0 141.0 127.0 NaN annot_cue_008 rect_i4 NaN NaN NaN 4.00 thoresen_2010_form-building-patterns_p90-91_pa... NaN 90.5
8 e000008 None interval rectangle 663.0 760.0 97.0 NaN annot_cue_009 rect_a4 NaN NaN NaN 3.00 thoresen_2010_form-building-patterns_p90-91_pa... NaN 113.4
9 e000009 None interval rectangle 19.0 270.0 251.0 NaN annot_cue_010 rect_i5 NaN NaN NaN 7.50 thoresen_2010_form-building-patterns_p90-91_pa... NaN 121.0
10 e000010 None interval rectangle 595.0 659.0 64.0 NaN annot_cue_011 rect_f5 NaN NaN NaN 1.50 thoresen_2010_form-building-patterns_p90-91_pa... NaN 141.0

Creating Timelines

Use create_timeline() to turn loaded events into a Timeline object:

# Physical timeline (seconds)
physical_tl = typed.create_timeline(uid="thoresen_physical")
physical_tl
ContinuousPhysicalTimeline[thoresen_physical] (11 events)
                      0 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 142.5 seconds
physical_tl.get_timestamp_table()
pyarrow.Table
axis: double
thoresen_physical: double
----
axis: [[0,1.5,3.5,5,5.5,...,116.4,121,128.5,141,142.5]]
thoresen_physical: [[0,1.5,3.5,5,5.5,...,116.4,121,128.5,141,142.5]]
# Graphical timeline (pixels)
graphical_tl = graphical.create_timeline(uid="thoresen_graphical")
graphical_tl
DiscreteGraphicalTimeline[thoresen_graphical] (11 events)
                      0 ∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶ 760 pixels

Note: Both timelines represent the same 11 events in different coordinate systems: - Physical: 0 - 142.5 seconds (audio time) - Graphical: 10 - 760 pixels (image coordinates)

TimeToAlign! uses these dual representations to align graphical annotations with audio.

Child Timelines from Column Values with group_by

When your data carries events from multiple sources (images, pages, tracks), pass group_by to create_timeline() to split the events into one child timeline per unique value. The Thoresen data has events from five different image files:

from timetoalign.timelines import create_timeline  # noqa: E402

grouped_tl = create_timeline(typed, group_by="image_filename")
grouped_tl
ContinuousPhysicalTimeline[cpt1] (11 events, 5 children)
                      0 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 142.5 seconds
  ├─ thoresen_...     0 ~                                5.5 (3 events)
  ├─ thoresen_...     0 ~~~~~~~~~~                       48 (2 events)
  ├─ thoresen_...     0 ~~~~~~~~~~~~~~~~~~               83.5 (2 events)
  ├─ thoresen_...     0 ~~~~~~~~~~~~~~~~~~~~~~~~~~       116.4 (2 events)
  └─ thoresen_...     0 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 142.5 (2 events)
# Each child timeline represents events from one image
{
    "parent_id": grouped_tl.id,
    "n_children": grouped_tl.n_children,
    "children": {
        child.id: len(child._events) if child._events else 0
        for _, child in grouped_tl.iter_children()
    },
}
{'parent_id': 'cpt1',
 'n_children': 5,
 'children': {'thoresen_2010_form-building-patterns_p90-91_page1_1.jpeg': 3,
  'thoresen_2010_form-building-patterns_p90-91_page1_2.jpeg': 2,
  'thoresen_2010_form-building-patterns_p90-91_page1_3.jpeg': 2,
  'thoresen_2010_form-building-patterns_p90-91_page1_4.jpeg': 2,
  'thoresen_2010_form-building-patterns_p90-91_page2_1.jpeg': 2}}

Summary

Goal How
Load an ms3 score TSV Ms3Loader() + loader.load(path)
Map your own columns Subclass TsvLoader / CsvLoader, set start_column etc.
Promote a column to a typed field Name it in column_specs ({"col": int})
Reach a nested JSON value Field("column", "nested") as a coordinate attribute
Derive a coordinate ComputedField("end", formula="...")
Split into child timelines create_timeline(loader, group_by="column")
from timetoalign.loader.tabular import TsvLoader

class MyLoader(TsvLoader):
    start_column = "onset"
    duration_column = "dur"
    column_specs = {"pitch": int, "velocity": int}

Key Takeaway: Tabular loaders map CSV/TSV columns to TimeToAlign! events declaratively. Unnamed columns ride along as property columns; column_specs promotes the ones you want as typed fields; Field reaches nested JSON. For the full column-to-field mechanism — composite columns, semantic field types, and Step-2 promotion — continue to the in-depth CSV/TSV how-to.