Validating MTC Model Data#
Traveler table classes describe more than column names: their annotations also define expected dtypes, categorical dimensions, and the model steps responsible for derived fields. This walkthrough uses the bundled MTC mini dataset to show how those contracts are inspected, how incomplete workflow state is handled, and how validation errors help diagnose bad model output.
Load a typed store#
The mini loader reads the small example tables, creates their cross-table indexes, generates synthetic tours, and returns an MTC Store. We also import JAX for constructing deliberately typed test values and pytest for displaying expected validation failures without stopping the notebook.
import jax.numpy as jnp
import pytest
import mtc
import traveler as tv
store = mtc.mini()
store
<Store with keys households, persons, skims, land_use, time_periods, tours>
The store class registers a concrete table type for each model table. This registry is what lets Traveler validate a generic stored dataset against application-specific schemas.
store.table_types()
{'households': mtc.tables.households.Households,
'tours': mtc.tables.tours.Tours,
'persons': mtc.tables.persons.Persons,
'land_use': mtc.tables.landuse.LandUse}
Inspect fields before validating#
Stored tables expose their columns as attributes. Here RESACRE is the land-use table’s residential-acreage array, and info() summarizes all available fields, shapes, and dtypes without dumping the complete data.
land_use = store.land_use
land_use.RESACRE
Array([ 1. , 1. , 1. , 1. , 1. , 7. ,
13. , 8.33042, 9.79332, 10.37666, 12.58862, 1. ,
1. , 1. , 3. , 13.73247, 23.84518, 2.09941,
14.19946, 15.35359, 15.18943, 9. , 4.413 , 2. ,
4. ], dtype=float32)
land_use.info()
<JaxTable id_col=TAZ>
- DISTRICT (25,) int8
- SD (25,) int8
- TOTHH (25,) int32
- TOTPOP (25,) int32
- TOTACRE (25,) float32
- RESACRE (25,) float32
- CIACRE (25,) float32
- TOTEMP (25,) int32
- AGE0519 (25,) int32
- RETEMPN (25,) int32
- FPSEMPN (25,) int32
- HEREMPN (25,) int32
- OTHEMPN (25,) int32
- AGREMPN (25,) int32
- MWTEMPN (25,) int32
- PRKCST (25,) float32
- OPRKCST (25,) float32
- area_type (25,) int32
- HSENROLL (25,) float32
- COLLFTE (25,) float32
- COLLPTE (25,) float32
- TOPOLOGY (25,) int32
- TERMINAL (25,) float32
- TAZ (25,) int32
- county_id (25,) int8
- _prng_key_ (25,) key<fry>
Validate the currently available tour data#
The tour table combines raw identifiers and categories with fields that will be produced later by a tour-preprocessing step. Its summary shows which columns are present before that step runs.
store.tours.info()
<JaxTable id_col=tour_id>
- person_id (20000,) int32
- tour_id (20000,) int32
- otaz (20000,) categorical: (25 categories)
- dtaz (20000,) categorical: (25 categories)
- out_period (20000,) categorical: EA, AM, MD, ... (5 categories)
- in_period (20000,) categorical: EA, AM, MD, ... (5 categories)
- _prng_key_ (20000,) key<fry>
- _person_idx_ (20000,) int32
- _household_idx_ (20000,) int32
- household_id (20000,) int32
A normal validation checks every field required at the store’s current workflow state. It returns nothing when the contract is satisfied; problems are reported by raising ValidationError.
store.tours.validate()
Verbose validation makes that decision process visible. Notice that fields annotated with from_step("tour_preprocessor") are skipped: Traveler does not require step-produced data before its source step has run.
store.tours.validate(verbose=2)
Field 'tour_id' validated as <class 'numpy.int32'>.
Field 'person_id' validated as <class 'numpy.int32'>.
Field 'household_id' validated as <class 'numpy.int32'>.
Field '_household_idx_' validated as <class 'numpy.int32'>.
Field '_person_idx_' validated as <class 'numpy.int32'>.
Skipping validation of field 'is_joint' in table 'tours' because it derives from 'tour_preprocessor' which is not yet completed.
Skipping validation of field 'is_atwork_subtour' in table 'tours' because it derives from 'tour_preprocessor' which is not yet completed.
Skipping validation of field 'hov2_available' in table 'tours' because it derives from 'tour_preprocessor' which is not yet completed.
Skipping validation of field 'work_tour_is_SOV' in table 'tours' because it derives from 'tour_preprocessor' which is not yet completed.
Skipping validation of field 'terminal_time' in table 'tours' because it derives from 'tour_preprocessor' which is not yet completed.
Skipping validation of field 'number_of_participants' in table 'tours' because it derives from 'tour_preprocessor' which is not yet completed.
Skipping validation of field 'daily_parking_cost' in table 'tours' because it derives from 'tour_preprocessor' which is not yet completed.
Validate a later workflow state#
We can ask the validator to behave as though tour_preprocessor has completed. The same table must then contain every field attributed to that step, so the untouched mini data correctly fails validation.
with pytest.raises(tv.errors.ValidationError) as error:
store.tours.validate(sources=["tour_preprocessor"])
print(error.value)
Table validation failed:
Field 'is_joint' is missing in table 'tours'.
Field 'is_atwork_subtour' is missing in table 'tours'.
Field 'hov2_available' is missing in table 'tours'.
Field 'work_tour_is_SOV' is missing in table 'tours'.
Field 'terminal_time' is missing in table 'tours'.
Field 'number_of_participants' is missing in table 'tours'.
Field 'daily_parking_cost' is missing in table 'tours'.
Diagnose an incorrect result dtype#
To emulate the preprocessor, we functionally assign all of its required outputs to a returned store. Most values below have suitable scalar types, but is_joint is intentionally a floating-point value even though the schema requires a boolean. Validation now gets past the missing-field checks and identifies the narrower dtype problem.
store = store.assign_on(
"tours",
is_joint=jnp.asarray(0.0),
is_atwork_subtour=jnp.asarray(False),
daily_parking_cost=jnp.asarray(15.0),
hov2_available=True,
work_tour_is_SOV=False,
terminal_time=jnp.float32(2.0),
number_of_participants=jnp.int8(1),
)
with pytest.raises(tv.errors.ValidationError) as error:
store.tours.validate(sources=["tour_preprocessor"])
print(error.value)
Table validation failed:
Field 'is_joint' in table 'tours' has type float32, expected <class 'bool'>.
Correct the field and validate again#
Because assign_on returns a new store, correcting the bad field is another explicit state transition. Once is_joint is replaced by a boolean value, the tour table satisfies the contract expected after preprocessing.
store = store.assign_on(
"tours",
is_joint=jnp.asarray(False),
)
store.tours.validate(sources=["tour_preprocessor"])
The important point is that validation follows the model workflow. Raw inputs can be valid before derived fields exist, while a store representing a later stage must contain correctly shaped and typed results from every completed step.