Skip to content

Commit 367cbac

Browse files
mxndrwgrdnrdhensleDavid Hensle
authored
🚀 [feat] vehicle type model (#486)
* vehicle type model, first commit * vehicle type model, first commit * option 2 spec changes * got vehicle choice model running w probabilistic dimension * update example mtc configs to run vehicle choice * got vehicle choice model running w probabilistic dimension * pycodestyle fixes and mi to meters conversion * add veh choice to estimation example * implemented option 2 and 4 as MNL * simply veh type choice alts/utilities creation * fix alt file storage bug * pycodestyle fixes * pycodestyle fixes * pycodestyle fixes * debug estimation example * estimation mode debug * docstrings, etc * typo * veh typ choice yaml updates * pycodestyle fixes * added vehicle type data, implemented option 4 minus owned veh interactions * implemented option4 * implemented vehicle type option 2 and vehicle allocation models * tied vehicle ids to household ids, added fleet_year option * added options to include vehicle data in vehicle table, documentation * added annotate tours functionality * cleanup & documentation * restore example_mtc * created example_mtc_extended * vehicle type model documentation * documentation * adding missed vehicle allocation spec * updating regress tables with new auto ownership costs * fixed bug where chargers were applied to all alts and not just evs * fixing incorrect coefficients * responses to pull request comments * small variable name change * log_alt_losers setting * pycodestyle * no vehicle type estimation * adding MTC Extended Example to TravisCI * removing unused expressions * replaced table with correct column order Co-authored-by: David Hensle <david.hensle@rsginc.com> Co-authored-by: David Hensle <davidh@sandag.org> Co-authored-by: dhensle <hensle93@gmail.com> Co-authored-by: dhensle <51132108+dhensle@users.noreply.github.com>
1 parent 91930b5 commit 367cbac

54 files changed

Lines changed: 4941 additions & 195 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.travis.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ jobs:
2121
- stage: Examples
2222
name: "MTC Example"
2323
env: TEST_SUITE=activitysim/examples/example_mtc/test
24+
- name: "MTC Extended Example"
25+
env: TEST_SUITE=activitysim/examples/example_mtc_extended/test
2426
- name: "Multizone Example"
2527
env: TEST_SUITE=activitysim/examples/example_multiple_zone/test
2628
- name: "Marin Example"

activitysim/abm/models/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,5 @@
3636
from . import trip_scheduling_choice
3737
from . import trip_matrices
3838
from . import summarize
39+
from . import vehicle_allocation
40+
from . import vehicle_type_choice

activitysim/abm/models/atwork_subtour_mode_choice.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from activitysim.core import inject
1111
from activitysim.core import pipeline
1212
from activitysim.core import simulate
13+
from activitysim.core import expressions
1314

1415
from activitysim.core import los
1516
from activitysim.core.pathbuilder import TransitVirtualPathBuilder
@@ -166,6 +167,15 @@ def atwork_subtour_mode_choice(
166167
assign_in_place(tours, choices_df)
167168
pipeline.replace_table("tours", tours)
168169

170+
# - annotate tours table
171+
if model_settings.get('annotate_tours'):
172+
tours = inject.get_table('tours').to_frame()
173+
expressions.assign_columns(
174+
df=tours,
175+
model_settings=model_settings.get('annotate_tours'),
176+
trace_label=tracing.extend_trace_label(trace_label, 'annotate_tours'))
177+
pipeline.replace_table("tours", tours)
178+
169179
if trace_hh_id:
170180
tracing.trace_df(tours[tours.tour_category == 'atwork'],
171181
label=tracing.extend_trace_label(trace_label, mode_column_name),

activitysim/abm/models/tour_mode_choice.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from activitysim.core import config
1111
from activitysim.core import inject
1212
from activitysim.core import pipeline
13+
from activitysim.core import expressions
1314
from activitysim.core import simulate
1415
from activitysim.core import logit
1516
from activitysim.core.util import assign_in_place, reindex
@@ -353,6 +354,15 @@ def tour_mode_choice_simulate(tours, persons_merged,
353354

354355
pipeline.replace_table("tours", all_tours)
355356

357+
# - annotate tours table
358+
if model_settings.get('annotate_tours'):
359+
tours = inject.get_table('tours').to_frame()
360+
expressions.assign_columns(
361+
df=tours,
362+
model_settings=model_settings.get('annotate_tours'),
363+
trace_label=tracing.extend_trace_label(trace_label, 'annotate_tours'))
364+
pipeline.replace_table("tours", tours)
365+
356366
if trace_hh_id:
357367
tracing.trace_df(primary_tours,
358368
label=tracing.extend_trace_label(trace_label, mode_column_name),

activitysim/abm/models/util/canonical_ids.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,17 @@
1010
logger = logging.getLogger(__name__)
1111

1212

13-
RANDOM_CHANNELS = ['households', 'persons', 'tours', 'joint_tour_participants', 'trips']
14-
TRACEABLE_TABLES = ['households', 'persons', 'tours', 'joint_tour_participants', 'trips']
13+
RANDOM_CHANNELS = ['households', 'persons', 'tours', 'joint_tour_participants', 'trips', 'vehicles']
14+
TRACEABLE_TABLES = ['households', 'persons', 'tours', 'joint_tour_participants', 'trips', 'vehicles']
1515

1616
CANONICAL_TABLE_INDEX_NAMES = {
1717
'households': 'household_id',
1818
'persons': 'person_id',
1919
'tours': 'tour_id',
2020
'joint_tour_participants': 'participant_id',
2121
'trips': 'trip_id',
22-
'land_use': 'zone_id'
22+
'land_use': 'zone_id',
23+
'vehicles': 'vehicle_id'
2324
}
2425

2526
# unfortunately the two places this is needed (joint_tour_participation and estimation.infer
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
# ActivitySim
2+
# See full license in LICENSE.txt.
3+
4+
import logging
5+
6+
import pandas as pd
7+
import numpy as np
8+
import itertools
9+
import os
10+
11+
from activitysim.core.interaction_simulate import interaction_simulate
12+
from activitysim.core import simulate
13+
from activitysim.core import tracing
14+
from activitysim.core import config
15+
from activitysim.core import inject
16+
from activitysim.core import pipeline
17+
from activitysim.core import expressions
18+
from activitysim.core import logit
19+
from activitysim.core import assign
20+
from activitysim.core import los
21+
22+
from activitysim.core.util import assign_in_place
23+
24+
from .util.mode import mode_choice_simulate
25+
from .util import estimation
26+
27+
logger = logging.getLogger(__name__)
28+
29+
30+
def annotate_vehicle_allocation(model_settings, trace_label):
31+
"""
32+
Add columns to the tours table in the pipeline according to spec.
33+
34+
Parameters
35+
----------
36+
model_settings : dict
37+
trace_label : str
38+
"""
39+
tours = inject.get_table('tours').to_frame()
40+
expressions.assign_columns(
41+
df=tours,
42+
model_settings=model_settings.get('annotate_tours'),
43+
trace_label=tracing.extend_trace_label(trace_label, 'annotate_tours'))
44+
pipeline.replace_table("tours", tours)
45+
46+
47+
def get_skim_dict(network_los, choosers):
48+
"""
49+
Returns a dictionary of skim wrappers to use in expression writing.
50+
51+
Skims have origin as home_zone_id and destination as the tour destination.
52+
53+
Parameters
54+
----------
55+
network_los : activitysim.core.los.Network_LOS object
56+
choosers : pd.DataFrame
57+
58+
Returns
59+
-------
60+
skims : dict
61+
index is skim wrapper name, value is the skim wrapper
62+
"""
63+
skim_dict = network_los.get_default_skim_dict()
64+
orig_col_name = 'home_zone_id'
65+
dest_col_name = 'destination'
66+
67+
out_time_col_name = 'start'
68+
in_time_col_name = 'end'
69+
odt_skim_stack_wrapper = skim_dict.wrap_3d(orig_key=orig_col_name, dest_key=dest_col_name,
70+
dim3_key='out_period')
71+
dot_skim_stack_wrapper = skim_dict.wrap_3d(orig_key=dest_col_name, dest_key=orig_col_name,
72+
dim3_key='in_period')
73+
74+
choosers['in_period'] = network_los.skim_time_period_label(choosers[in_time_col_name])
75+
choosers['out_period'] = network_los.skim_time_period_label(choosers[out_time_col_name])
76+
77+
skims = {
78+
"odt_skims": odt_skim_stack_wrapper.set_df(choosers),
79+
"dot_skims": dot_skim_stack_wrapper.set_df(choosers),
80+
}
81+
return skims
82+
83+
84+
@inject.step()
85+
def vehicle_allocation(
86+
persons,
87+
households,
88+
vehicles,
89+
tours,
90+
tours_merged,
91+
network_los,
92+
chunk_size,
93+
trace_hh_id):
94+
"""Selects a vehicle for each occupancy level for each tour.
95+
96+
Alternatives consist of the up to the number of household vehicles plus one
97+
option for non-household vehicles.
98+
99+
The model will be run once for each tour occupancy defined in the model yaml.
100+
Output tour table will columns added for each occupancy level.
101+
102+
The user may also augment the `tours` tables with new vehicle
103+
type-based fields specified via the annotate_tours option.
104+
105+
Parameters
106+
----------
107+
persons : orca.DataFrameWrapper
108+
households : orca.DataFrameWrapper
109+
vehicles : orca.DataFrameWrapper
110+
vehicles_merged : orca.DataFrameWrapper
111+
tours : orca.DataFrameWrapper
112+
tours_merged : orca.DataFrameWrapper
113+
chunk_size : orca.injectable
114+
trace_hh_id : orca.injectable
115+
"""
116+
trace_label = 'vehicle_allocation'
117+
model_settings_file_name = 'vehicle_allocation.yaml'
118+
model_settings = config.read_model_settings(model_settings_file_name)
119+
120+
logsum_column_name = model_settings.get('MODE_CHOICE_LOGSUM_COLUMN_NAME')
121+
122+
estimator = estimation.manager.begin_estimation('vehicle_allocation')
123+
124+
model_spec_raw = simulate.read_model_spec(file_name=model_settings['SPEC'])
125+
coefficients_df = simulate.read_model_coefficients(model_settings)
126+
model_spec = simulate.eval_coefficients(model_spec_raw, coefficients_df, estimator)
127+
128+
nest_spec = config.get_logit_model_settings(model_settings)
129+
constants = config.get_model_constants(model_settings)
130+
131+
locals_dict = {}
132+
locals_dict.update(constants)
133+
locals_dict.update(coefficients_df)
134+
135+
# ------ constructing alternatives from model spec and joining to choosers
136+
vehicles_wide = vehicles.to_frame().pivot_table(
137+
index='household_id', columns='vehicle_num',
138+
values='vehicle_type', aggfunc=lambda x: ''.join(x))
139+
140+
alts_from_spec = model_spec.columns
141+
# renaming vehicle numbers to alternative names in spec
142+
vehicle_alt_columns_dict = {}
143+
for veh_num in range(1, len(alts_from_spec)):
144+
vehicle_alt_columns_dict[veh_num] = alts_from_spec[veh_num-1]
145+
vehicles_wide.rename(columns=vehicle_alt_columns_dict, inplace=True)
146+
147+
# if the number of vehicles is less than the alternatives, fill with NA
148+
# e.g. all households only have 1 or 2 vehicles because of small sample size,
149+
# still need columns for alternatives 3 and 4
150+
for veh_num, col_name in vehicle_alt_columns_dict.items():
151+
if col_name not in vehicles_wide.columns:
152+
vehicles_wide[col_name] = ''
153+
154+
# last entry in spec is the non-hh-veh option
155+
assert alts_from_spec[-1] == 'non_hh_veh', "Last option in spec needs to be non_hh_veh"
156+
vehicles_wide[alts_from_spec[-1]] = ''
157+
158+
# merging vehicle alternatives to choosers
159+
choosers = tours_merged.to_frame().reset_index()
160+
choosers = pd.merge(choosers, vehicles_wide, how='left', on='household_id')
161+
choosers.set_index('tour_id', inplace=True)
162+
163+
# ----- setup skim keys
164+
skims = get_skim_dict(network_los, choosers)
165+
locals_dict.update(skims)
166+
167+
# ------ preprocessor
168+
preprocessor_settings = model_settings.get('preprocessor', None)
169+
if preprocessor_settings:
170+
expressions.assign_columns(
171+
df=choosers,
172+
model_settings=preprocessor_settings,
173+
locals_dict=locals_dict,
174+
trace_label=trace_label)
175+
176+
logger.info("Running %s with %d tours", trace_label, len(choosers))
177+
178+
if estimator:
179+
estimator.write_model_settings(model_settings, model_settings_file_name)
180+
estimator.write_spec(model_settings)
181+
estimator.write_coefficients(coefficients_df, model_settings)
182+
estimator.write_choosers(choosers)
183+
184+
tours = tours.to_frame()
185+
186+
# ------ running for each occupancy level selected
187+
tours_veh_occup_cols = []
188+
for occup in model_settings.get('OCCUPANCY_LEVELS', [1]):
189+
logger.info("Running for occupancy = %d", occup)
190+
# setting occup for access in spec expressions
191+
locals_dict.update({'occup': occup})
192+
193+
choices = simulate.simple_simulate(
194+
choosers=choosers,
195+
spec=model_spec,
196+
nest_spec=nest_spec,
197+
skims=skims,
198+
locals_d=locals_dict,
199+
chunk_size=chunk_size,
200+
trace_label=trace_label,
201+
trace_choice_name='vehicle_allocation',
202+
estimator=estimator)
203+
204+
# matching alt names to choices
205+
choices = choices.map(dict(enumerate(alts_from_spec))).to_frame()
206+
choices.columns = ['alt_choice']
207+
208+
# last alternative is the non-household vehicle option
209+
for alt in alts_from_spec[:-1]:
210+
choices.loc[choices['alt_choice'] == alt, 'choice'] = \
211+
choosers.loc[choices['alt_choice'] == alt, alt]
212+
choices.loc[choices['alt_choice'] == alts_from_spec[-1], 'choice'] = alts_from_spec[-1]
213+
214+
# creating a column for choice of each occupancy level
215+
tours_veh_occup_col = f'vehicle_occup_{occup}'
216+
tours[tours_veh_occup_col] = choices['choice']
217+
tours_veh_occup_cols.append(tours_veh_occup_col)
218+
219+
if estimator:
220+
estimator.write_choices(choices)
221+
choices = estimator.get_survey_values(choices, 'households', 'vehicle_allocation')
222+
estimator.write_override_choices(choices)
223+
estimator.end_estimation()
224+
225+
pipeline.replace_table("tours", tours)
226+
227+
tracing.print_summary('vehicle_allocation', tours[tours_veh_occup_cols], value_counts=True)
228+
229+
annotate_settings = model_settings.get('annotate_tours', None)
230+
if annotate_settings:
231+
annotate_vehicle_allocation(model_settings, trace_label)
232+
233+
if trace_hh_id:
234+
tracing.trace_df(tours,
235+
label='vehicle_allocation',
236+
warn_if_empty=True)

0 commit comments

Comments
 (0)