TripAI / modules /mode_choice.py
mahbubchula's picture
Upload 8 files
a58f1b7 verified
# modules/mode_choice.py
# TripAI – Multinomial Logit Mode Choice
from __future__ import annotations
from dataclasses import dataclass
from typing import Dict
import numpy as np
import pandas as pd
@dataclass
class ModeChoiceResult:
"""
Container for mode choice outputs.
Attributes
----------
total_od : pd.DataFrame
OD matrix summed over all purposes.
volumes : Dict[str, pd.DataFrame]
Mode-specific OD matrices (Car/Metro/Bus).
probabilities : Dict[str, pd.DataFrame]
Mode choice probabilities per OD pair.
"""
total_od: pd.DataFrame
volumes: Dict[str, pd.DataFrame]
probabilities: Dict[str, pd.DataFrame]
def mode_choice(
od_matrices: Dict[str, pd.DataFrame],
taz: pd.DataFrame,
travel_time: pd.DataFrame,
beta_time: float = -0.06,
beta_cost: float = -0.03,
beta_car_own: float = 0.5,
) -> ModeChoiceResult:
"""
Apply a simple Multinomial Logit (MNL) mode choice model for
Car / Metro / Bus.
U_m = β_time * time_m + β_cost * cost_m + γ * car_ownership (for car only)
Parameters
----------
od_matrices : dict[str, pd.DataFrame]
OD matrices by purpose (HBW, HBE, HBS).
taz : pd.DataFrame
TAZ attributes with 'car_ownership_rate', 'x_km', 'y_km'.
travel_time : pd.DataFrame
Base car travel time matrix (minutes).
beta_time : float
Coefficient on in-vehicle travel time.
beta_cost : float
Coefficient on generalized cost.
beta_car_own : float
Additional utility for Car associated with car ownership rate.
Returns
-------
ModeChoiceResult
Aggregated OD, volumes by mode, and probabilities by mode.
"""
zones = travel_time.index
# 1. Aggregate OD across purposes
total_od = None
for mat in od_matrices.values():
if total_od is None:
total_od = mat.copy()
else:
total_od += mat
total_od = total_od.loc[zones, zones]
# 2. Build time and cost matrices for each mode
tt_car = travel_time.loc[zones, zones].astype(float)
tt_metro = tt_car * 0.8 # metro faster
tt_bus = tt_car * 1.3 # bus slower
# Distance proxy (km)
dist_proxy = tt_car / 60.0 * 30.0 # 30 km/h
cost_car = 2.0 + 0.12 * dist_proxy
cost_metro = 15.0
cost_bus = 8.0 + 0.03 * dist_proxy
# 3. Car ownership matrix
car_own = taz["car_ownership_rate"].reindex(zones).to_numpy()
n = len(zones)
car_own_matrix = np.repeat(car_own[:, None], n, axis=1)
# 4. Utilities
modes = ["car", "metro", "bus"]
utilities = {}
# Car
U_car = (
beta_time * tt_car.to_numpy()
+ beta_cost * cost_car.to_numpy()
+ beta_car_own * car_own_matrix
)
utilities["car"] = U_car
# Metro
U_metro = beta_time * tt_metro.to_numpy() + beta_cost * cost_metro.to_numpy()
utilities["metro"] = U_metro
# Bus
U_bus = beta_time * tt_bus.to_numpy() + beta_cost * cost_bus.to_numpy()
utilities["bus"] = U_bus
# 5. Probabilities via softmax
exp_sum = np.zeros_like(U_car)
for U in utilities.values():
exp_sum += np.exp(U)
probabilities: Dict[str, pd.DataFrame] = {}
for mode, U in utilities.items():
P = np.exp(U) / np.maximum(exp_sum, 1e-12)
probabilities[mode] = pd.DataFrame(P, index=zones, columns=zones)
# 6. Mode-specific OD flows
volumes: Dict[str, pd.DataFrame] = {}
total_np = total_od.to_numpy()
for mode in modes:
volumes[mode] = pd.DataFrame(
total_np * probabilities[mode].to_numpy(),
index=zones,
columns=zones,
)
return ModeChoiceResult(
total_od=total_od,
volumes=volumes,
probabilities=probabilities,
)