Skip to content
Open
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
79 changes: 63 additions & 16 deletions activitysim/abm/models/school_escorting.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,17 @@ def determine_escorting_participants(
)

chaperones["chaperone_num"] = (
chaperones.sort_values("chaperone_weight", ascending=False)
chaperones.sort_values(
["chaperone_weight", "person_id"], ascending=[False, True]
)
.groupby("household_id")
.cumcount()
+ 1
)
escortees["escortee_num"] = (
escortees.sort_values("age", ascending=True).groupby("household_id").cumcount()
escortees.sort_values([age_col, "person_id"], ascending=[True, True])
.groupby("household_id")
.cumcount()
+ 1
)

Expand Down Expand Up @@ -247,7 +251,7 @@ def create_school_escorting_bundles_table(choosers, tours, stage):
)

# each chauffeur option has ride share or pure escort
bundles["chauf_num"] = np.ceil(bundles["chauf_type_num"].div(2)).astype(int)
bundles["chauf_num"] = ((bundles["chauf_type_num"] + 1) // 2).astype("int64")

# getting bundle chauffeur id based on the chauffeur num
bundles["chauf_id"] = -1
Expand All @@ -257,7 +261,7 @@ def create_school_escorting_bundles_table(choosers, tours, stage):
choosers["chauf_id" + str(i)],
bundles["chauf_id"],
)
bundles["chauf_id"] = bundles["chauf_id"].astype(int)
bundles["chauf_id"] = bundles["chauf_id"].astype("int64")
assert (
bundles["chauf_id"] > 0
).all(), "Invalid chauf_id's for school escort bundles!"
Expand All @@ -278,9 +282,13 @@ def create_school_escorting_bundles_table(choosers, tours, stage):
school_time_cols = [
"time_home_to_school" + str(i) for i in range(1, NUM_ESCORTEES + 1)
]
bundles["outbound_order"] = list(bundles[school_time_cols].values.argsort() + 1)
# Child number is the deterministic tie-breaker when siblings have the same
# home-to-school time, so preserve the order of the child-number columns.
bundles["outbound_order"] = list(
bundles[school_time_cols].values.argsort(kind="stable") + 1
)
bundles["inbound_order"] = list(
(-1 * bundles[school_time_cols]).values.argsort() + 1
(-1 * bundles[school_time_cols]).values.argsort(kind="stable") + 1
) # inbound gets reverse order
bundles["child_order"] = np.where(
bundles["school_escort_direction"] == "outbound",
Expand Down Expand Up @@ -314,6 +322,54 @@ def create_school_escorting_bundles_table(choosers, tours, stage):
return bundles


def assign_school_escort_bundle_ids(escort_bundles: pd.DataFrame) -> pd.DataFrame:
"""Sort bundles by semantic keys and assign deterministic, unique IDs."""
bundle_key_columns = [
"household_id",
"school_escort_direction",
"bundle_num",
]
duplicate_keys = escort_bundles.duplicated(bundle_key_columns, keep=False)
if duplicate_keys.any():
duplicates = escort_bundles.loc[duplicate_keys, bundle_key_columns]
raise ValueError(f"Duplicate school escort bundle keys:\n{duplicates}")

# Inbound bundles were historically appended first and therefore received
# the lower IDs. Use an explicit direction rank to preserve that behavior
# without depending on categorical or input row ordering.
direction = escort_bundles["school_escort_direction"]
direction_order = np.select(
[direction == "inbound", direction == "outbound"], [0, 1], default=-1
)
if (direction_order < 0).any():
invalid_directions = direction[direction_order < 0].unique().tolist()
raise ValueError(
f"Invalid school escort bundle directions: {invalid_directions}"
)

escort_bundles = (
escort_bundles.assign(_school_escort_direction_order=direction_order)
.sort_values(
by=[
"household_id",
"_school_escort_direction_order",
"bundle_num",
]
)
.drop(columns="_school_escort_direction_order")
)
escort_bundles["bundle_id"] = (
escort_bundles["household_id"].astype("int64") * 10
+ escort_bundles.groupby("household_id").cumcount()
+ 1
).astype("int64")

if not escort_bundles["bundle_id"].is_unique:
raise ValueError("Generated school escort bundle IDs are not unique")

return escort_bundles


class SchoolEscortSettings(BaseLogitComponentSettings, extra="forbid"):
"""
Settings for the `telecommute_frequency` component.
Expand Down Expand Up @@ -577,16 +633,7 @@ def school_escorting(

# Only want to create bundles and tours and trips if at least one household has school escorting
if len(escort_bundles) > 0:
escort_bundles["bundle_id"] = (
escort_bundles["household_id"] * 10
+ escort_bundles.groupby("household_id").cumcount()
+ 1
)
escort_bundles.sort_values(
by=["household_id", "school_escort_direction"],
ascending=[True, False],
inplace=True,
)
escort_bundles = assign_school_escort_bundle_ids(escort_bundles)

school_escort_tours = school_escort_tours_trips.create_pure_school_escort_tours(
state, escort_bundles
Expand Down
13 changes: 9 additions & 4 deletions activitysim/abm/models/util/school_escort_tours_trips.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def join_attributes(df, column_names):
series = (
df[col]
.fillna(-1)
.astype(int)
.astype("int64")
.astype(str)
.replace("-1", "", regex=False)
)
Expand Down Expand Up @@ -331,7 +331,7 @@ def create_chauf_trip_table(bundles):

def create_chauf_escort_trips(bundles):
chauf_trip_bundles = create_chauf_trip_table(bundles.copy())
chauf_trip_bundles["tour_id"] = bundles["chauf_tour_id"].astype(int)
chauf_trip_bundles["tour_id"] = bundles["chauf_tour_id"].astype("int64")

# departure time is the first school start in the outbound school_escort_direction and the last school end in the inbound school_escort_direction
starts = (
Expand Down Expand Up @@ -651,7 +651,7 @@ def process_tours_after_escorting_model(state: workflow.State, escort_bundles, t
num_escortees = (
escort_bundles.drop_duplicates("chauf_tour_id")
.set_index("chauf_tour_id")["num_escortees"]
.astype(int)
.astype("int64")
)
tours.loc[num_escortees.index, "num_escortees"] = num_escortees

Expand Down Expand Up @@ -921,7 +921,12 @@ def create_pure_school_escort_tours(state: workflow.State, bundles):
pe_tours["school_escort_direction"] == "inbound", "pure_escort", pd.NA
)

pe_tours = pe_tours.sort_values(by=["household_id", "person_id", "start"])
if not pe_tours["bundle_id"].is_unique:
raise ValueError("Pure school escort bundle IDs are not unique")

pe_tours = pe_tours.sort_values(
by=["household_id", "person_id", "start", "bundle_id"]
)

# finding what the next start time for that person for scheduling
pe_tours["next_pure_escort_start"] = (
Expand Down
Loading