Skip to content

Commit 739c2e4

Browse files
authored
BayDAG Contribution #11: School Escorting Estimation Updates (#777)
* School escorting estimation updates Most changes are needed to avoid crash if estimation run actually had no school escorting in the input data * blacken * updating to work with Pydantic and State object * adding missed columns necessary for no school escorting * blacken * handling zero escorting cases * removing duplicate code
1 parent bb437b3 commit 739c2e4

2 files changed

Lines changed: 118 additions & 40 deletions

File tree

activitysim/abm/models/school_escorting.py

Lines changed: 101 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,14 @@ def determine_escorting_participants(
5858
& (persons.cdap_activity == "M")
5959
]
6060
households_with_escortees = escortees["household_id"]
61-
choosers = choosers[choosers.index.isin(households_with_escortees)]
61+
if len(households_with_escortees) == 0:
62+
logger.warning("No households with escortees found!")
63+
else:
64+
tot_households = len(choosers)
65+
choosers = choosers[choosers.index.isin(households_with_escortees)]
66+
logger.info(
67+
f"Proceeding with {len(choosers)} households with escortees out of {tot_households} total households"
68+
)
6269

6370
# can specify different weights to determine chaperones
6471
persontype_weight = model_settings.PERSON_WEIGHT
@@ -140,7 +147,7 @@ def add_prev_choices_to_choosers(
140147
stage_alts,
141148
how="left",
142149
left_on=escorting_choice,
143-
right_on=stage_alts.index.name,
150+
right_index=True,
144151
)
145152
.set_index("household_id")
146153
)
@@ -216,8 +223,12 @@ def create_school_escorting_bundles_table(choosers, tours, stage):
216223
bundles : pd.DataFrame
217224
one school escorting bundle per row
218225
"""
219-
# making a table of bundles
220-
choosers = choosers.reset_index()
226+
# want to keep household_id in columns, which is already there if running in estimation mode
227+
if "household_id" in choosers.columns:
228+
choosers = choosers.reset_index(drop=True)
229+
else:
230+
choosers = choosers.reset_index()
231+
# creating a row for every school escorting bundle
221232
choosers = choosers.loc[choosers.index.repeat(choosers["nbundles"])]
222233

223234
bundles = pd.DataFrame()
@@ -460,7 +471,11 @@ def school_escorting(
460471

461472
trace_hh_id = state.settings.trace_hh_id
462473

463-
alts = simulate.read_model_alts(state, model_settings.ALTS, set_index="Alt")
474+
# FIXME setting index as "Alt" causes crash in estimation mode...
475+
# happens in joint_tour_frequency_composition too!
476+
# alts = simulate.read_model_alts(state, model_settings.ALTS, set_index="Alt")
477+
alts = simulate.read_model_alts(state, model_settings.ALTS, set_index=None)
478+
alts.index = alts["Alt"].values
464479

465480
choosers, participant_columns = determine_escorting_participants(
466481
households_merged, persons, model_settings
@@ -478,7 +493,9 @@ def school_escorting(
478493
for stage_num, stage in enumerate(school_escorting_stages):
479494
stage_trace_label = trace_label + "_" + stage
480495
estimator = estimation.manager.begin_estimation(
481-
state, "school_escorting_" + stage
496+
state,
497+
model_name="school_escorting_" + stage,
498+
bundle_name="school_escorting",
482499
)
483500

484501
model_spec_raw = state.filesystem.read_model_spec(
@@ -533,9 +550,26 @@ def school_escorting(
533550

534551
if estimator:
535552
estimator.write_model_settings(model_settings, model_settings_file_name)
536-
estimator.write_spec(model_settings)
537-
estimator.write_coefficients(coefficients_df, model_settings)
553+
estimator.write_spec(model_settings, tag=stage.upper() + "_SPEC")
554+
estimator.write_coefficients(
555+
coefficients_df, file_name=stage.upper() + "_COEFFICIENTS"
556+
)
538557
estimator.write_choosers(choosers)
558+
estimator.write_alternatives(alts, bundle_directory=True)
559+
560+
# FIXME #interaction_simulate_estimation_requires_chooser_id_in_df_column
561+
# shuold we do it here or have interaction_simulate do it?
562+
# chooser index must be duplicated in column or it will be omitted from interaction_dataset
563+
# estimation requires that chooser_id is either in index or a column of interaction_dataset
564+
# so it can be reformatted (melted) and indexed by chooser_id and alt_id
565+
assert choosers.index.name == "household_id"
566+
assert "household_id" not in choosers.columns
567+
choosers["household_id"] = choosers.index
568+
569+
# FIXME set_alt_id - do we need this for interaction_simulate estimation bundle tables?
570+
estimator.set_alt_id("alt_id")
571+
572+
estimator.set_chooser_id(choosers.index.name)
539573

540574
log_alt_losers = state.settings.log_alt_losers
541575

@@ -580,47 +614,74 @@ def school_escorting(
580614

581615
if stage_num >= 1:
582616
choosers["Alt"] = choices
583-
choosers = choosers.join(alts, how="left", on="Alt")
617+
choosers = choosers.join(alts.set_index("Alt"), how="left", on="Alt")
584618
bundles = create_school_escorting_bundles_table(
585619
choosers[choosers["Alt"] > 1], tours, stage
586620
)
587621
escort_bundles.append(bundles)
588622

589623
escort_bundles = pd.concat(escort_bundles)
590-
escort_bundles["bundle_id"] = (
591-
escort_bundles["household_id"] * 10
592-
+ escort_bundles.groupby("household_id").cumcount()
593-
+ 1
594-
)
595-
escort_bundles.sort_values(
596-
by=["household_id", "school_escort_direction"],
597-
ascending=[True, False],
598-
inplace=True,
599-
)
600624

601-
school_escort_tours = school_escort_tours_trips.create_pure_school_escort_tours(
602-
state, escort_bundles
603-
)
604-
chauf_tour_id_map = {
605-
v: k for k, v in school_escort_tours["bundle_id"].to_dict().items()
606-
}
607-
escort_bundles["chauf_tour_id"] = np.where(
608-
escort_bundles["escort_type"] == "ride_share",
609-
escort_bundles["first_mand_tour_id"],
610-
escort_bundles["bundle_id"].map(chauf_tour_id_map),
611-
)
612-
assert (
613-
escort_bundles["chauf_tour_id"].notnull().all()
614-
), f"chauf_tour_id is null for {escort_bundles[escort_bundles['chauf_tour_id'].isna()]}. Check availability conditions."
625+
# Only want to create bundles and tours and trips if at least one household has school escorting
626+
if len(escort_bundles) > 0:
627+
escort_bundles["bundle_id"] = (
628+
escort_bundles["household_id"] * 10
629+
+ escort_bundles.groupby("household_id").cumcount()
630+
+ 1
631+
)
632+
escort_bundles.sort_values(
633+
by=["household_id", "school_escort_direction"],
634+
ascending=[True, False],
635+
inplace=True,
636+
)
615637

616-
tours = school_escort_tours_trips.add_pure_escort_tours(tours, school_escort_tours)
617-
tours = school_escort_tours_trips.process_tours_after_escorting_model(
618-
state, escort_bundles, tours
619-
)
638+
school_escort_tours = school_escort_tours_trips.create_pure_school_escort_tours(
639+
state, escort_bundles
640+
)
641+
chauf_tour_id_map = {
642+
v: k for k, v in school_escort_tours["bundle_id"].to_dict().items()
643+
}
644+
escort_bundles["chauf_tour_id"] = np.where(
645+
escort_bundles["escort_type"] == "ride_share",
646+
escort_bundles["first_mand_tour_id"],
647+
escort_bundles["bundle_id"].map(chauf_tour_id_map),
648+
)
620649

621-
school_escort_trips = school_escort_tours_trips.create_school_escort_trips(
622-
escort_bundles
623-
)
650+
assert (
651+
escort_bundles["chauf_tour_id"].notnull().all()
652+
), f"chauf_tour_id is null for {escort_bundles[escort_bundles['chauf_tour_id'].isna()]}. Check availability conditions."
653+
654+
tours = school_escort_tours_trips.add_pure_escort_tours(
655+
tours, school_escort_tours
656+
)
657+
tours = school_escort_tours_trips.process_tours_after_escorting_model(
658+
state, escort_bundles, tours
659+
)
660+
school_escort_trips = school_escort_tours_trips.create_school_escort_trips(
661+
escort_bundles
662+
)
663+
664+
else:
665+
# create empty school escort tours & trips tables to be used downstream
666+
tours["school_esc_outbound"] = pd.NA
667+
tours["school_esc_inbound"] = pd.NA
668+
tours["school_escort_direction"] = pd.NA
669+
tours["next_pure_escort_start"] = pd.NA
670+
school_escort_tours = pd.DataFrame(columns=tours.columns)
671+
trip_cols = [
672+
"household_id",
673+
"person_id",
674+
"tour_id",
675+
"trip_id",
676+
"outbound",
677+
"depart",
678+
"purpose",
679+
"destination",
680+
"escort_participants",
681+
"chauf_tour_id",
682+
"primary_purpose",
683+
]
684+
school_escort_trips = pd.DataFrame(columns=trip_cols)
624685

625686
school_escort_trips["primary_purpose"] = school_escort_trips[
626687
"primary_purpose"

activitysim/abm/models/util/school_escort_tours_trips.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,19 @@ def merge_school_escort_trips_into_pipeline(state: workflow.State):
405405
tours = state.get_dataframe("tours")
406406
trips = state.get_dataframe("trips")
407407

408+
# checking to see if there are school escort trips to merge in
409+
if len(school_escort_trips) == 0:
410+
# if no trips, fill escorting columns with NA
411+
trips[
412+
[
413+
"escort_participants",
414+
"school_escort_direction",
415+
"school_escort_trip_id",
416+
]
417+
] = pd.NA
418+
state.add_table("trips", trips)
419+
return trips
420+
408421
# want to remove stops if school escorting takes place on that half tour so we can replace them with the actual stops
409422
out_se_tours = tours[
410423
tours["school_esc_outbound"].isin(["pure_escort", "ride_share"])
@@ -643,6 +656,10 @@ def force_escortee_tour_modes_to_match_chauffeur(state: workflow.State, tours):
643656
# Does it even matter if trip modes are getting matched later?
644657
escort_bundles = state.get_dataframe("escort_bundles")
645658

659+
if len(escort_bundles) == 0:
660+
# do not need to do anything if no escorting
661+
return tours
662+
646663
# grabbing the school tour ids for each school escort bundle
647664
se_tours = escort_bundles[["school_tour_ids", "chauf_tour_id"]].copy()
648665
# merging in chauffeur tour mode

0 commit comments

Comments
 (0)