-
Notifications
You must be signed in to change notification settings - Fork 118
Add global option to skip households on simulation failure #1023
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
a0ec6a8
8623516
397f250
fa08f11
f26cc80
76fd833
96e73ec
cf98cd2
baa47fa
6cffd9b
0e34b7d
5da9715
50034fd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -234,6 +234,11 @@ def location_sample( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # FIXME - MEMORY HACK - only include columns actually used in spec | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| chooser_columns = model_settings.SIMULATE_CHOOSER_COLUMNS | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Drop this when PR #1017 is merged | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ("household_id" not in chooser_columns) and ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "household_id" in persons_merged.columns | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| chooser_columns = chooser_columns + ["household_id"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| choosers = persons_merged[chooser_columns] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # create wrapper with keys for this lookup - in this case there is a home_zone_id in the choosers | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -390,6 +395,11 @@ def location_presample( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # FIXME maybe we should add it for multi-zone (from maz_taz) if missing? | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| chooser_columns = model_settings.SIMULATE_CHOOSER_COLUMNS | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| chooser_columns = [HOME_TAZ if c == HOME_MAZ else c for c in chooser_columns] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Drop this when PR #1017 is merged | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ("household_id" not in chooser_columns) and ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "household_id" in persons_merged.columns | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| chooser_columns = chooser_columns + ["household_id"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| choosers = persons_merged[chooser_columns] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # create wrapper with keys for this lookup - in this case there is a HOME_TAZ in the choosers | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -620,6 +630,11 @@ def run_location_simulate( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # FIXME - MEMORY HACK - only include columns actually used in spec | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| chooser_columns = model_settings.SIMULATE_CHOOSER_COLUMNS | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Drop this when PR #1017 is merged | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ("household_id" not in chooser_columns) and ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "household_id" in persons_merged.columns | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| chooser_columns = chooser_columns + ["household_id"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| choosers = persons_merged[chooser_columns] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| alt_dest_col_name = model_settings.ALT_DEST_COL_NAME | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1072,6 +1087,23 @@ def iterate_location_choice( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| choices_df = choices_df_ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # drop choices that belong to the failed households: state.skipped_household_ids | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # so that their choices are not considered in shadow price calculations | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # first append household_id to choices_df | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| choices_df = choices_df.merge( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| persons_merged_df[["household_id"]], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| left_index=True, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| right_index=True, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| how="left", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if len(choices_df) > 0: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| choices_df = choices_df[ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ~choices_df["household_id"].isin( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| state.get("skipped_household_ids", set()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| choices_df = choices_df.drop(columns=["household_id"]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1092
to
+1105
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # first append household_id to choices_df | |
| choices_df = choices_df.merge( | |
| persons_merged_df[["household_id"]], | |
| left_index=True, | |
| right_index=True, | |
| how="left", | |
| ) | |
| if len(choices_df) > 0: | |
| choices_df = choices_df[ | |
| ~choices_df["household_id"].isin( | |
| state.get("skipped_household_ids", set()) | |
| ) | |
| ] | |
| choices_df = choices_df.drop(columns=["household_id"]) | |
| skip_failed_choices = state.get("skip_failed_choices", False) | |
| if skip_failed_choices: | |
| # first append household_id to choices_df | |
| choices_df = choices_df.merge( | |
| persons_merged_df[["household_id"]], | |
| left_index=True, | |
| right_index=True, | |
| how="left", | |
| ) | |
| if len(choices_df) > 0: | |
| choices_df = choices_df[ | |
| ~choices_df["household_id"].isin( | |
| state.get("skipped_household_ids", set()) | |
| ) | |
| ] | |
| choices_df = choices_df.drop(columns=["household_id"]) |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -307,6 +307,12 @@ def write_trip_matrices( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .TAZ.tolist() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # print out number of households skipped due to failed choices | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if state.settings.skip_failed_choices: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logging level for this should be higher than "info"; at least warning if not "error" level. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| f"\n!!!\nATTENTION: Skipped households with failed choices during simulation. Number of households skipped: {state.get('num_skipped_households', 0)}.\n!!!" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def annotate_trips( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| state: workflow.State, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -393,6 +399,21 @@ def write_matrices( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not matrix_settings: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.error("Missing MATRICES setting in write_trip_matrices.yaml") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hh_weight_col = model_settings.HH_EXPANSION_WEIGHT_COL | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if hh_weight_col: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if state.get("num_skipped_households", 0) > 0: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| f"Adjusting household expansion weights in {hh_weight_col} to account for {state.get('num_skipped_households', 0)} skipped households." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # adjust the hh expansion weights to account for skipped households | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| adjustment_factor = state.get_dataframe("households").shape[0] / ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should the adjustment to expansion weights be based not on the number of skipped households, but instead on the original expansion weight total and the total expansion weight of the households we dropped? E.g. if we drop a household with a giant expansion weight that's a bigger thing than dropping a household with a smaller one. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| state.get_dataframe("households").shape[0] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| + state.get("num_skipped_households", 0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| aggregate_trips[hh_weight_col] = ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| aggregate_trips[hh_weight_col] * adjustment_factor | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+404
to
+415
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if state.get("num_skipped_households", 0) > 0: | |
| logger.info( | |
| f"Adjusting household expansion weights in {hh_weight_col} to account for {state.get('num_skipped_households', 0)} skipped households." | |
| ) | |
| # adjust the hh expansion weights to account for skipped households | |
| adjustment_factor = state.get_dataframe("households").shape[0] / ( | |
| state.get_dataframe("households").shape[0] | |
| + state.get("num_skipped_households", 0) | |
| ) | |
| aggregate_trips[hh_weight_col] = ( | |
| aggregate_trips[hh_weight_col] * adjustment_factor | |
| ) | |
| num_skipped_households = state.get("num_skipped_households", 0) | |
| if num_skipped_households > 0: | |
| num_households = state.get_dataframe("households").shape[0] | |
| if num_households > 0: | |
| logger.info( | |
| f"Adjusting household expansion weights in {hh_weight_col} to account for {num_skipped_households} skipped households." | |
| ) | |
| # adjust the hh expansion weights to account for skipped households | |
| denominator = num_households + num_skipped_households | |
| if denominator > 0: | |
| adjustment_factor = num_households / denominator | |
| aggregate_trips[hh_weight_col] = ( | |
| aggregate_trips[hh_weight_col] * adjustment_factor | |
| ) | |
| else: | |
| logger.warning( | |
| "Skipping household expansion weight adjustment because the computed denominator is not positive." | |
| ) | |
| else: | |
| logger.warning( | |
| "Skipping household expansion weight adjustment because there are no households." | |
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -366,7 +366,14 @@ def trip_mode_choice( | |
| "trip_mode_choice choices", trips_df[mode_column_name], value_counts=True | ||
| ) | ||
|
|
||
| assert not trips_df[mode_column_name].isnull().any() | ||
| # if we're skipping failed choices, the trip modes for failed simulations will be null | ||
| if state.settings.skip_failed_choices: | ||
| mask_skipped = trips_df["household_id"].isin( | ||
| state.get("skipped_household_ids", set()) | ||
| ) | ||
| assert not trips_df.loc[~mask_skipped, mode_column_name].isnull().any() | ||
| else: | ||
| assert not trips_df[mode_column_name].isnull().any() | ||
|
|
||
| state.add_table("trips", trips_df) | ||
|
|
||
|
|
@@ -382,6 +389,11 @@ def trip_mode_choice( | |
| # need to update locals_dict to access skims that are the same .shape as trips table | ||
| locals_dict = {} | ||
| locals_dict.update(constants) | ||
| if state.settings.skip_failed_choices: | ||
| mask_skipped = trips_merged["household_id"].isin( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. mask_skipped should be the same for trips_df and trips_merged, right? We only need to calculate the mask once. |
||
| state.get("skipped_household_ids", set()) | ||
| ) | ||
| trips_merged = trips_merged.loc[~mask_skipped] | ||
| simulate.set_skim_wrapper_targets(trips_merged, skims) | ||
| locals_dict.update(skims) | ||
| locals_dict["timeframe"] = "trip" | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -1043,6 +1043,10 @@ def force_escortee_trip_modes_to_match_chauffeur(state: workflow.State, trips): | |||||||||||||||||||||||||||
| f"Changed {diff.sum()} trip modes of school escortees to match their chauffeur" | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| # trip_mode can be na if the run allows skipping failed choices and the trip mode choice has failed | ||||||||||||||||||||||||||||
| if state.settings.skip_failed_choices: | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
| if state.settings.skip_failed_choices: | |
| if state.settings.skip_failed_choices: | |
| null_modes = trips.trip_mode.isna() | |
| if null_modes.any(): | |
| null_count = int(null_modes.sum()) | |
| # log a limited sample of trip_ids with null modes to avoid excessive log size | |
| sample_ids = trips[null_modes].index.to_list()[:10] | |
| logger.warning( | |
| "Returning trips with %d missing trip_mode values due to skip_failed_choices; " | |
| "example trip_ids with null modes: %s", | |
| null_count, | |
| sample_ids, | |
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than just being OK here, code should:
- emit a warning that N choices have failed, and
- check if N exceeds configured failure thresholds (e.g. not more than 1% of all choices), in which case an error should be raised.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -781,6 +781,13 @@ def _check_store_skims_in_shm(self): | |
| should catch many common errors early, including missing required configurations or specified coefficient labels without defined values. | ||
|
||
| """ | ||
|
|
||
| skip_failed_choices: bool = True | ||
| """ | ||
| Skip households that cause errors during processing instead of failing the model run. | ||
| .. versionadded:: 1.6 | ||
| """ | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need additional setting[s] to set thresholds for how many skips are OK and when it's too many and should be an error. |
||
| other_settings: dict[str, Any] = None | ||
|
|
||
| def _get_attr(self, attr): | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -30,6 +30,7 @@ def report_bad_choices( | |||||
| state: workflow.State, | ||||||
| bad_row_map, | ||||||
| df, | ||||||
| skip_failed_choices, | ||||||
| trace_label, | ||||||
| msg, | ||||||
| trace_choosers=None, | ||||||
|
|
@@ -87,6 +88,27 @@ def report_bad_choices( | |||||
|
|
||||||
| logger.warning(row_msg) | ||||||
|
|
||||||
| if skip_failed_choices: | ||||||
| # update counter in state | ||||||
| num_skipped_households = state.get("num_skipped_households", 0) | ||||||
| skipped_household_ids = state.get("skipped_household_ids", set()) | ||||||
| for hh_id in df[trace_col].unique(): | ||||||
| if hh_id is None: | ||||||
| continue | ||||||
| if hh_id not in skipped_household_ids: | ||||||
| skipped_household_ids.add(hh_id) | ||||||
| num_skipped_households += 1 | ||||||
| else: | ||||||
| continue | ||||||
|
Comment on lines
+101
to
+102
|
||||||
| else: | |
| continue |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logger level here should be warn not debug
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation states "When skip_failed_choices is True, overflow_protection will be reverted to False", but "reverted" is not the correct word here. It should be "set" or "forced" since it's not reverting to a previous state but rather being disabled.
| When `skip_failed_choices` is True, `overflow_protection` will be reverted to False to avoid conflicts. | |
| When `skip_failed_choices` is True, `overflow_protection` will be set to False to avoid conflicts. |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When skip_failed_choices is False or not set, the code will use an uninitialized skip_failed_choices variable on line 370, causing a NameError. The variable should be initialized before the conditional check or have a default value.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1325,7 +1325,9 @@ def eval_mnl( | |
| if custom_chooser: | ||
| choices, rands = custom_chooser(state, probs, choosers, spec, trace_label) | ||
| else: | ||
| choices, rands = logit.make_choices(state, probs, trace_label=trace_label) | ||
| choices, rands = logit.make_choices( | ||
| state, probs, trace_label=trace_label, trace_choosers=choosers | ||
| ) | ||
|
|
||
| del probs | ||
| chunk_sizer.log_df(trace_label, "probs", None) | ||
|
|
@@ -1485,11 +1487,15 @@ def eval_nl( | |
| BAD_PROB_THRESHOLD = 0.001 | ||
| no_choices = (base_probabilities.sum(axis=1) - 1).abs() > BAD_PROB_THRESHOLD | ||
|
|
||
| if state.settings.skip_failed_choices is not None: | ||
| skip_failed_choices = state.settings.skip_failed_choices | ||
|
|
||
| if no_choices.any(): | ||
| logit.report_bad_choices( | ||
| state, | ||
| no_choices, | ||
| base_probabilities, | ||
| skip_failed_choices, | ||
|
Comment on lines
+1490
to
+1498
|
||
| trace_label=tracing.extend_trace_label(trace_label, "bad_probs"), | ||
| trace_choosers=choosers, | ||
| msg="base_probabilities do not sum to one", | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should check here how many choices are getting dropped, and