Data Validation#

In this demo, we showcase some data validation tools. We’ll start by loading the MTC mini data for testing.

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>
store.table_types()
{'households': mtc.tables.households.Households,
 'tours': mtc.tables.tours.Tours,
 'persons': mtc.tables.persons.Persons,
 'land_use': mtc.tables.landuse.LandUse}
lu = store.land_use
lu.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)
lu.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

Let’s validate the contents of the “tours” table. Before we validate this data, we can see what’s in the table using the info method.

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

We can validate the contents of this table using the “validate” method.

store.tours.validate()

This method has no return value, but it will raise a ValidationError if there is a problem.
If we want to see the details of the validation process, we can turn on verbose output.

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.

You’ll notice that some of the fields in this table were skipped. That’s because they are not “raw” data, but they are expected to come from some model component that has not yet been run. We can tell the validation tool which steps have been run by passing it a sources argument, and then the validation of fields created by those steps will not be skipped.

with pytest.raises(tv.errors.ValidationError) as e:
    store.tours.validate(sources=["tour_preprocessor"])
print(e.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'.

If we manually add the required fields to the table, we can emulate what the step would do, and pass validation, but only if they are the correct dtypes.

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 e:
    store.tours.validate(sources=["tour_preprocessor"])
print(e.value)
Table validation failed:
Field 'is_joint' in table 'tours' has type float32, expected <class 'bool'>.

Let’s try again, using the correct dtypes.

store = store.assign_on(
    "tours",
    is_joint=jnp.asarray(False),
)

store.tours.validate(sources=["tour_preprocessor"])