Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions activitysim/abm/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
stop_frequency,
summarize,
telecommute_frequency,
telecommute_status,
tour_mode_choice,
tour_od_choice,
tour_scheduling_probabilistic,
Expand Down
125 changes: 125 additions & 0 deletions activitysim/abm/models/telecommute_status.py
Original file line number Diff line number Diff line change
@@ -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,
)
16 changes: 16 additions & 0 deletions activitysim/estimation/larch/simple_simulate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}/",
Expand Down
66 changes: 66 additions & 0 deletions docs/dev-guide/components/telecommute_status.md
Original file line number Diff line number Diff line change
@@ -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
```
Loading