solve() and simulate() accept pandas objects directly. Initial conditions can be a
DataFrame, and parameters can contain pd.Series values with labeled indices.
Simulation results come back as a DataFrame via .to_dataframe(). The typical workflow
is DataFrame in, DataFrame out.
Initial Conditions as a DataFrame¶
Pass a pandas DataFrame directly to simulate() as initial_conditions. One row per
agent, one column per state variable, plus a "regime_name" column:
df = pd.DataFrame(
{
"regime_name": ["working", "working", "retired"],
"wealth": [10.0, 50.0, 30.0],
"health": ["good", "bad", "good"],
"age": [25.0, 25.0, 25.0],
}
)
result = model.simulate(
params=params,
initial_conditions=df,
log_level="debug",
)"regime_name"column is required. Use regime names as strings (e.g.,"working").Discrete states use string labels from the model’s categorical classes (e.g.,
"good"instead of0). Labels are validated and mapped to integer codes automatically.A discrete state declared with values takes those numbers, matched exactly for integers and within the grid’s tolerance for floats; a value on no node raises.
Continuous states pass through as-is.
Categoricaldtype columns are also supported, including the numeric CategoricalsSimulationResult.to_dataframe()publishes for value grids, so rows of a simulated panel are valid initial conditions.simulate(..., use_categorical_codes=True)reads every discrete column as integer codes instead.
The example above is for an age model. A model declared with n_periods requires an
integer "period" column instead of "age". Use exactly the coordinate kind the model
declares; supplying both names or the wrong name fails before simulation.
You can also pass initial conditions as a plain dict of JAX arrays (see Solving and Simulating).
Parameters with pd.Series¶
When parameters include array values — transition probabilities, wage profiles, or any
array indexed by model variables — prepare them as labeled pd.Series with a named
MultiIndex. Pass them directly in the params dict; solve() and simulate() convert
them automatically:
params = {
"discount_factor": 0.95,
"working": {
"next_health": {
"probs_array": health_probs_series, # pd.Series with MultiIndex
},
"utility": {"risk_aversion": 1.5},
},
}
# Series values are converted to JAX arrays transparently
result = model.simulate(
params=params,
initial_conditions=df,
log_level="debug",
)Scalars remain constants. Ordinary arrays retain positional indexing, while declared
temporal parameters require labelled inputs. TimeVarying provides the same time
alignment without pandas; see Periods and temporal parameters.
Series format¶
Each pd.Series must have:
A named
MultiIndex(or namedIndexfor 1-D arrays). Level names must match the function’s indexing parameters.String labels for discrete variables, matching the model’s categorical classes, and the values themselves for a variable declared with values (
0, 4, 10, not"4"). Passuse_categorical_codes=Truetosolveorsimulateto index every discrete level by integer codes instead."age"for an age model, with actual values from itsAgeGrid, or"period"for a period model, with integer computational indices. These names are never interchangeable, even when the numerical labels coincide.For transition functions: an additional outcome level —
"next_health"for a state transition,"next_regime"for a regime-transition law over all targets, whose parameters sit atparams["edges"][source]. A gate or a projection underparams["edges"][source][target]runs on the target regime’s grid, so the target’s categories label its levels.
A discrete state or action labels a level wherever the regime declares it, including a horizon at which no demanded function reads it and the model prunes it: the parameter a dormant law reads keeps its slot, and its Series converts by the same categories.
A function indexes an array by the codes of a discrete variable, its positions in
pandas’ terms. For a variable declared with values, the function reads <name> as the
value (pandas: the category) and <name>_code as the int32 position (pandas: the code),
and indexes with the code:
def wage(*, experience, experience_code, wage_premium, base_wage):
return base_wage * (1 + 0.02 * experience + wage_premium[experience_code])
params = {
"working_life": {
"wage": {
"wage_premium": pd.Series(
[0.25, 0.0, 0.1], index=pd.Index([10, 0, 5], name="experience")
),
"base_wage": 2.0,
}
}
}The level is still named experience and labelled by the values; pylcm places each
label at its code, whatever order the Series is written in. Indexing by the value,
wage_premium[experience], is refused when the model is built, because the value 10 is
no position of a three-node grid. For a category class <name> already holds the code,
so <name>_code equals it and either spelling indexes the same way.
Level order does not matter — levels are reordered to match the function signature automatically.
For temporal data, schema checks precede filtering. Rows outside the model’s age or period grid are silently discarded, including duplicates outside the grid. Duplicate selected full keys and missing required time–category combinations raise errors. Extra rows never substitute for missing required rows. A transition consumes its source period; an unused terminal slot needs no fabricated observation.
With @time_varying_params("wage"), the consumer receives only the current time slice
and indexes any remaining categorical axes normally. The existing manual wage[period]
route also accepts labelled Series. Never index a normalized array with age: its
positions are periods. Known manual temporal reads of unlabelled arrays warn in age
models and fail in period models, independently of log_level.
What happens during conversion¶
Your model functions work with plain JAX arrays and integer indexing — nothing about
pandas enters the model at runtime. The Series is purely an input convenience. Before
any model code runs, the conversion inspects the function signature to determine which
dimensions the array is indexed over, maps each label to an integer position using the
model’s grids (e.g., "good" → 0, "bad" → 1), and scatters the Series values into
a JAX array of the correct shape. The function receives a normal jnp.ndarray and never
sees pandas.
Why Labeled Indices Matter¶
Every discrete variable axis must use string labels from the model’s categorical classes, not raw integer codes. This is a deliberate design choice.
The conversion step validates every label against the model’s grids before building the array. If a label is misspelled, a category is missing, or axes are swapped, you get a clear error before the array enters JAX. Without this validation, a wrong index would silently produce a misshapen array. JAX would then vmap that array over millions of simulated agents — producing garbage results with no error message and no way to trace the problem back to the input.
Labeled indices turn silent data corruption into loud, early errors with actionable messages.
derived_categoricals¶
A derived categorical changes parameter indexing but is not a state grid or transition. Declare it before selecting solver-specific row structure, and return integer category codes rather than Booleans.
When a function indexes its array parameter by a variable that is not a state or action in the model — typically a DAG function output — the model has no grid to validate labels against. You will see an error like:
Unrecognised indexing parameter 'employment_type'. Expected 'age' or a
discrete grid name (['health', 'partner']). If 'employment_type' is a DAG
function output, add derived_categoricals={"employment_type": DiscreteGrid(category_class=EmploymentType)}
to the Regime or Model constructor.Fix this by declaring the grid on the Regime that uses it:
working = Regime(
# Other regime fields are omitted in this fragment.
derived_categoricals={
"employment_type": DiscreteGrid(category_class=EmploymentType)
},
)If the variable has different categories in different regimes, each regime declares its own grid:
working = Regime(
# Other regime fields are omitted in this fragment.
derived_categoricals={
"employment_type": DiscreteGrid(category_class=FullEmploymentType)
},
)
retired = Regime(
# Other regime fields are omitted in this fragment.
derived_categoricals={
"employment_type": DiscreteGrid(category_class=RetiredEmploymentType)
},
)For convenience, model-level derived_categoricals are broadcast to all regimes:
Model(
regimes={"working": working, "retired": retired},
derived_categoricals={
"employment_type": DiscreteGrid(category_class=EmploymentType)
},
ages=ages,
regime_id_class=RegimeId,
edges=edges,
initial_nodes=initial_nodes,
)Integer return types required¶
Functions used as derived categoricals must return integer values, not booleans. JAX
cannot use boolean values as array indices inside JIT-compiled code
(NonConcreteBooleanIndexError). If your derived categorical compares states:
# Wrong — returns bool, fails inside JIT
def is_good_health(health: DiscreteState) -> BoolND:
return health == Health.good
# Correct — returns int32
def is_good_health(health: DiscreteState) -> IntND:
return jnp.int32(health == Health.good)Validating State Transition Probabilities¶
State (and regime) transition probabilities are validated automatically during
model.solve() and model.simulate(): their shape, range, and rows summing to
1 are checked on a sweep over the regime’s grids when runtime_checks=True, the
default. Failures raise independently of console verbosity — see
Solving and Simulating.
Regime transition probabilities are validated automatically during model.solve() and
model.simulate(), so this helper covers only state transitions.
Under the Hood¶
Internally, solve() and simulate() call convert_series_in_params (in
_lcm.pandas_utils) to walk the already-broadcast params and convert each pd.Series
via array_from_series. For initial conditions, initial_conditions_from_dataframe
handles the DataFrame-to-dict conversion. Both are internal helpers — you don’t need to
call them directly.
See Also¶
Solving and Simulating — full
solve()/simulate()APIParameters — where transition probability arrays go in the params dict
Regimes — defining
StochasticTransitionstate transitions