diff --git a/activitysim/abm/models/__init__.py b/activitysim/abm/models/__init__.py index 138366843..44534fa53 100644 --- a/activitysim/abm/models/__init__.py +++ b/activitysim/abm/models/__init__.py @@ -33,6 +33,7 @@ stop_frequency, summarize, telecommute_frequency, + telecommute_status, tour_mode_choice, tour_od_choice, tour_scheduling_probabilistic, diff --git a/activitysim/abm/models/telecommute_status.py b/activitysim/abm/models/telecommute_status.py new file mode 100644 index 000000000..795405259 --- /dev/null +++ b/activitysim/abm/models/telecommute_status.py @@ -0,0 +1,125 @@ +# ActivitySim +# See full license in LICENSE.txt. +from __future__ import annotations + +import logging + +import numpy as np +import pandas as pd + +from activitysim.core import ( + config, + estimation, + expressions, + simulate, + tracing, + workflow, +) +from activitysim.core.configuration.base import PreprocessorSettings, PydanticReadable +from activitysim.core.configuration.logit import LogitComponentSettings + +logger = logging.getLogger("activitysim") + + +class TelecommuteStatusSettings(LogitComponentSettings, extra="forbid"): + """ + Settings for the `telecommute_status` component. + """ + + TELECOMMUTE_ALT: int + """Value that specifies if the worker is telecommuting on the simulation day.""" + + CHOOSER_FILTER_COLUMN_NAME: str = "is_worker" + """Column name in the dataframe to represent worker.""" + + +@workflow.step +def telecommute_status( + state: workflow.State, + persons_merged: pd.DataFrame, + persons: pd.DataFrame, + model_settings: TelecommuteStatusSettings | None = None, + model_settings_file_name: str = "telecommute_status.yaml", + trace_label: str = "telecommute_status", +) -> None: + """ + This model predicts whether a person (worker) telecommutes on the simulation day. + The output from this model is TRUE (if telecommutes) or FALSE (if does not telecommute). + """ + if model_settings is None: + model_settings = TelecommuteStatusSettings.read_settings_file( + state.filesystem, + model_settings_file_name, + ) + + choosers = persons_merged + chooser_filter_column_name = model_settings.CHOOSER_FILTER_COLUMN_NAME + choosers = choosers[(choosers[chooser_filter_column_name])] + logger.info("Running %s with %d persons", trace_label, len(choosers)) + + estimator = estimation.manager.begin_estimation(state, "telecommute_status") + + constants = config.get_model_constants(model_settings) + + # - preprocessor + expressions.annotate_preprocessors( + state, + df=choosers, + locals_dict=constants, + skims=None, + model_settings=model_settings, + trace_label=trace_label, + ) + + model_spec = state.filesystem.read_model_spec(file_name=model_settings.SPEC) + coefficients_df = state.filesystem.read_model_coefficients(model_settings) + model_spec = simulate.eval_coefficients( + state, model_spec, coefficients_df, estimator + ) + nest_spec = config.get_logit_model_settings(model_settings) + + if estimator: + estimator.write_model_settings(model_settings, model_settings_file_name) + estimator.write_spec(model_settings) + estimator.write_coefficients(coefficients_df, model_settings) + estimator.write_choosers(choosers) + + choices = simulate.simple_simulate( + state, + choosers=choosers, + spec=model_spec, + nest_spec=nest_spec, + locals_d=constants, + trace_label=trace_label, + trace_choice_name="is_telecommuting", + estimator=estimator, + compute_settings=model_settings.compute_settings, + ) + + telecommute_alt = model_settings.TELECOMMUTE_ALT + choices = choices == telecommute_alt + + if estimator: + estimator.write_choices(choices) + choices = estimator.get_survey_values(choices, "persons", "is_telecommuting") + estimator.write_override_choices(choices) + estimator.end_estimation() + + persons["is_telecommuting"] = choices.reindex(persons.index).fillna(0).astype(bool) + + state.add_table("persons", persons) + + tracing.print_summary( + "telecommute_status", persons.is_telecommuting, value_counts=True + ) + + if state.settings.trace_hh_id: + state.tracing.trace_df(persons, label=trace_label, warn_if_empty=True) + + expressions.annotate_tables( + state, + locals_dict=constants, + skims=None, + model_settings=model_settings, + trace_label=trace_label, + ) diff --git a/activitysim/estimation/larch/simple_simulate.py b/activitysim/estimation/larch/simple_simulate.py index e608a5008..59c83d345 100644 --- a/activitysim/estimation/larch/simple_simulate.py +++ b/activitysim/estimation/larch/simple_simulate.py @@ -257,6 +257,22 @@ def work_from_home_model( ) +def telecommute_status_model( + name="telecommute_status", + edb_directory="output/estimation_data_bundle/{name}/", + return_data=False, +): + return simple_simulate_model( + name=name, + edb_directory=edb_directory, + return_data=return_data, + choices={ + True: 1, + False: 2, + }, # True is telecommute, false is does not telecommute, names match spec positions + ) + + def mandatory_tour_frequency_model( name="mandatory_tour_frequency", edb_directory="output/estimation_data_bundle/{name}/", diff --git a/docs/dev-guide/components/telecommute_status.md b/docs/dev-guide/components/telecommute_status.md new file mode 100644 index 000000000..c889f7e32 --- /dev/null +++ b/docs/dev-guide/components/telecommute_status.md @@ -0,0 +1,66 @@ +(component-telecommute_status)= +# Telecommute Status + +```{eval-rst} +.. currentmodule:: activitysim.abm.models.telecommute_status +``` + +ActivitySim telecommute representation consists of two long term submodels - +a person [work_from_home](work_from_home) model and +a person [telecommute_frequency](telecommute_frequency) model. +The work from home model predicts if a worker works exclusively from home, +whereas the telecommute frequency model predicts number of days in a week a worker telecommutes, +if they do not exclusively work from home. +However, neither of them predicts whether a worker telecommutes or not on the simulation day. +This telecommute status model extends the previous two models to predict for all workers whether +they telecommute on the simulation day. + +A simple implementation of the telecommute status model can be based on the worker's telecommute frequency. +For example, if a worker telecommutes 4 days a week, then there is a 80% probability for them +to telecommute on the simulation day. +The telecommute status model software can accommodate more complex model forms if needed. + +There have been discussions about where to place the telecommute status model within the model sequence, +particularly regarding its interation with the Coordinated Daily Activity Pattern (CDAP) model. +Some have proposed expanding the CDAP definition of the "Mandatory" day pattern to include commuting, telecommuting and working from home, +and then applying the telecommute status model to workers with a "Mandatory" day pattern. +While this idea had merit, it would require re-defining and re-estimating CDAP for many regions, which presents practical challenges. + +During Phase 9B development, the Consortium collaboratively reached a consensus on a preferred design for explicitly modeling telecommuting. +It was decided that the existing CDAP definitions would remain unchanged. The new design introduces the ability +to model hybrid workers—those who work both in-home and out-of-home on the simulation day. To support this, +the Consortium recommended adding two new models: a Telecommute Arrangement model and an In-Home Work Activity Duration model. + +As of August 2025, these two models remain at the design stage and have not yet been implemented. Once deployed, +they will supersede the current telecommute status model, which will no longer be needed. In the interim, +the telecommute status model can be used to flag telecommuters in the simulation. + +The main interface to the telecommute status model is the +[telecommute_status](activitysim.abm.models.telecommute_status) function. This +function is registered as an Inject step in the example Pipeline. + +## Structure + +- *Configuration File*: `telecommute_status.yaml` +- *Core Table*: `persons` +- *Result Table*: `is_telecommuting` + + +## Configuration + +```{eval-rst} +.. autopydantic_model:: TelecommuteStatusSettings + :inherited-members: BaseModel, PydanticReadable + :show-inheritance: +``` + +### Examples + +- [Example SANDAG ABM3](https://github.com/ActivitySim/sandag-abm3-example/tree/main/configs/resident/telecommute_status.yaml) + + +## Implementation + +```{eval-rst} +.. autofunction:: telecommute_status +```