diff --git a/docs/users-guide/application-analysis-guide/application-analysis-guide.md b/docs/users-guide/application-analysis-guide/application-analysis-guide.md new file mode 100644 index 000000000..e41cf4115 --- /dev/null +++ b/docs/users-guide/application-analysis-guide/application-analysis-guide.md @@ -0,0 +1,32 @@ +# ActivitySim Application and Analysis Guide + +This guide is to demonstrate to modelers how to apply ActivitySim for the analysis of various projects. It is intended to both demonstrate exactly how to change the inputs to test a particular project or policy, process the outputs to best answer the question that was asked, and provide a general understanding of what ActivitySim can and can't do. There are presently three example scenarios using the [SANDAG ABM3 Example model](https://github.com/activitysim/sandag-abm3-example), though more may be added in the future. All of these scenarios are designed to be comparable to the example as is. + +## General Configuration + +Every ActivitySim model component has a settings file, that is typically hardcoded in the model code as being the component name followed by .yaml. For example, the workplace location model is configured with the `workplace_location.yaml` file. Within these files there are several key settings for each model component, which can include file definitions. Further filenames are defined in this file with the following settings: +- `SPEC`: The spec file defines how the utilities are calculated for each model alternative. +- `COEFFICIENTS`: The coefficients file defines coefficient files that are references in the spec file. +- `COEFFICIENTS TEMPLATE`: For models that are segmented (such as how trip mode choice is segmented by purpose), this defines how to name each coefficient for each segment. As an example from the SANDAG ABM3 Example, the trip mode choice spec file refers to [`coef_ivt`](https://github.com/ActivitySim/sandag-abm3-example/tree/main/configs/resident) in multiple places, but if one were to look at the coefficient file they would see a different value of `coef_ivt` [for each purpose](https://github.com/ActivitySim/sandag-abm3-example/blob/main/configs/resident/trip_mode_choice_coefficients.csv#L17-L22). The association between `coef_ivt` and `coef_ivt_[purpose]` is defined in the [coefficient template file](https://github.com/ActivitySim/sandag-abm3-example/blob/main/configs/resident/trip_mode_choice_coefficients_template.csv#L15). + +The user can also define preprocessor to add fields to a choosers table before a component is run or annotators to add that afterward. The fields added in a preprocessor can then be used in utility calculations to simplify the calculations and reduce the total amount of memory usage. They can be added by specifying `preprocessor` or `annotator` in the settings (there are other options as well), with the following additional options: +- `SPEC`: The name of the file (.csv extension does not need to be included) +- `DF`: The table in memory that the fields will be added to. This can be set to `choosers` to be the same model that the component will be run on. +- `TABLES`: Tables to include in memory in case data from other tables are to be added to `DF`. + +The files that define these are CSV files with three fields: +- `Description`: A description of the field being added. This is only for the users and is not read by ActivitySim. +- `Target`: The name of the field to be added. +- `Expression`: An expression defining how to calculate the field. +In the case where an output variable name is hardcoded (such as the outputs of the [trip mode choice](https://github.com/ActivitySim/activitysim/blob/main/activitysim/abm/models/trip_mode_choice.py#L81) model), one can use an annotator to get around that by creating a variable on the same table with a custom name by just setting that field to be equivalent to the hardcoded field via the expression. For example, after running trip mode choice, if one were to create a field called `modeTrip` that was a copy of the hardcoded `trip_mode` field, they would need to have a line in the annotator that's run after trip mode choice as follows: + +| Description | Target | Expression | +| ---------------------------- | -------- | ---------- | +| New name for trip mode field | modeTrip | trip_mode | + +There are further settings regarding the structure of any logit model that's used. One setting, `LOGIT_TYPE` will be `MNL` for multinomial logit and `NL` for nested logit. If a nested logit structure is used, then there needs to be a setting called `NESTS` that define how each alternative nests. Further, there is an additional setting called `CONSTANTS` that contains constants that can be refered to in the model component's preprocessor and spec files. If a variable is used in multiple components, then it should be specified in the global settings file `constants.yaml`, which contains constants that can be accessed by all model components. + +## Example scenarios +- [Land Use Change](land-use-change\land_use_change.md) +- [Network Change](network-change\network_change.md) +- [Telecommuting Change](telecommute-change\telecommute_change.md) \ No newline at end of file diff --git a/docs/users-guide/application-analysis-guide/land-use-change/land_use_change.md b/docs/users-guide/application-analysis-guide/land-use-change/land_use_change.md new file mode 100644 index 000000000..a0d46b697 --- /dev/null +++ b/docs/users-guide/application-analysis-guide/land-use-change/land_use_change.md @@ -0,0 +1,221 @@ +# Land Use Change Guide + +## Introduction + +Many contemporary urban planners are encouraging developers to build denser housing, particularly around transit stops. Naturally, planners will want to gauge what the impact of such a development would be on their jurisdiction's transportation system, particularly regarding metrics such as VMT (and subsequently greenhouse gas emission) and transit boardings (and subsequently farebox revenue). To demonstrate this, we will be analyzing a hypothetical development in the San Diego Region that will add 2000 households and 1000 retail jobs near a light rail station. These will be added to the existing population and jobs in the region. The guide will show how to make changes to the ActivitySim inputs, how to run the test, and how to calculate some of the key metrics such as VMT and changes in mode share. + +**NOTE: The example provided is a hypothetical project that demonstrates how one would use ActivitySim to model the effects of a land use change and does not necessarily reflect any real planned developments.** + +## Setting Up the Scenario + +Three input files need to be changed in order to run this test: the files defining the synthetic population (households.csv and persons.csv) and the land use file (land_use.csv). Because Activity-based models use synthetic populations, those input files will need to be updated to reflect the increase in the number of households within the study area. There are multiple ways that this could be done. The ActivitySim consortium maintains the PopulationSim population synthesis software, which includes a `repop` mode that can be used to add households to an existing synthetic population. This demonstration will show how to do this, though there are many ways one could add the additional households. In addition to PopulationSim's `repop` mode, one could shift existing population from elsewhere, or even generating a wholly new synthetic population. + +While updating the land use file may seem very straightforward, it is very easy to overlook some necessary changes that could result in the model understating the impact of the change. A modeler doesn't need to just edit the household and employment fields in the study area--they also need to edit any field derived from those fields. The following fields land use fields in the SANDAG ABM3 example reflect the increase in the number of multi-family households (descriptions of the fields can be found at SANDAG's [ABM3 Documentation](https://sandag.github.io/ABM/inputs.html#land-use)): +- pop +- hhp +- hs +- hs_mf +- hh +- hh_mf +- i1 +- i2 +- i3 +- i4 +- i5 +- i6 +- i7 +- i8 +- i9 +- i10 +- duden +- popden +- dudenbin +- PopEmpDenPerMi + +Further, the following fields would need to be updated to reflect the increase in retail employment: +- emp_ret +- emp_total +- empden +- PopEmpDenPerMi + +### Instructions + +1. The first thing to do would be to update the new synthetic population. This can be done using PopulationSim's repop mode, which adds additional population on top of existing PopulationSim outputs (a pipeline file is needed). A more detailed explanation of PopulationSim's repop mode can be found in [PopulationSim's documentation](https://activitysim.github.io/populationsim/application_configuration.html#configuring-settings-file-for-repop-mode), but this guide will briefly provide some examples of how PopulationSim can be configured for this particular scenario. + +First, the `run_list` in the settings file (settings.yaml) needs to be adjusted to have PopulationSim run the repop steps: +``` +run_list: + steps: + - input_pre_processor.repop + - repop_setup_data_structures + - initial_seed_balancing.final=true;repop + - integerize_final_seed_weights.repop + - repop_balancing + # expand_households options are append or replace + - expand_households.repop;append + - summarize.repop + - write_synthetic_population.repop + - write_tables.repop +``` + +Next, `repop_control_file_name: repop_controls.csv` should be added to the settings file. This tells PopulationSim which file to configure what the control totals will be within the configs directory (configs\repop_controls.csv). The following configuration will help control for characteristic of the population within a TOD area. For example, TOD is more likely to attract smaller households who are more likely to be workers, more likely to be held by younger adults, and less likely to have children than the general population. +| target | geography | seed_table | importance | control_field | expression | +|-----------------------|-----------|------------|------------|----------------------|-------------------------------------------------------------------| +| num_hh | mgra | households | 1000000000 | Total_HH | (households.WGTP > 0) & (households.WGTP < np.inf) | +| HHSize_1 | mgra | households | 250000 | HHSize_1 | households.NP == 1 | +| HHSize_2 | mgra | households | 250000 | HHSize_2 | households.NP == 2 | +| HHSize_3 | mgra | households | 250000 | HHSize_4 | households.NP == 3 | +| HHSize_4Plus | mgra | households | 250000 | HHSize_2 | households.NP == 2 | +| HHInc_0to14999 | mgra | households | 100000 | HHInc_0to14999 | (households.HHADJINC >= 0) & (households.HHADJINC <= 14999) | +| HHInc_15000to29999 | mgra | households | 100000 | HHInc_15000to29999 | (households.HHADJINC >= 15000) & (households.HHADJINC <= 29999) | +| HHInc_30000to59999 | mgra | households | 100000 | HHInc_30000to59999 | (households.HHADJINC >= 30000) & (households.HHADJINC <= 59999) | +| HHInc_60000to99999 | mgra | households | 100000 | HHInc_60000to99999 | (households.HHADJINC >= 60000) & (households.HHADJINC <= 99999) | +| HHInc_100000to149999 | mgra | households | 100000 | HHInc_100000to149999 | (households.HHADJINC >= 100000) & (households.HHADJINC <= 149999) | +| HHInc_150000to199999 | mgra | households | 100000 | HHInc_150000to199999 | (households.HHADJINC >= 150000) & (households.HHADJINC <= 199999) | +| HHInc_200000Plus | mgra | households | 100000 | HHInc_200000Plus | (households.HHADJINC >= 200000) & (households.HHADJINC <= np.inf) | +| HHWork_0 | mgra | households | 100000 | HHWork_0 | households.workers == 0 | +| HHWork_1 | mgra | households | 100000 | HHWork_1 | households.workers == 1 | +| HHWork_2 | mgra | households | 100000 | HHWork_2 | households.workers == 2 | +| HHWork_3Plus | mgra | households | 100000 | HHWork_3Plus | households.workers >= 3 | +| Age_LT5 | mgra | persons | 100000 | Age_LT5 | (persons.AGEP >= 0) & (persons.AGEP <= 4) | +| Age_5to9 | mgra | persons | 100000 | Age_5to9 | (persons.AGEP >= 5) & (persons.AGEP <= 9) | +| Age_10to14 | mgra | persons | 100000 | Age_10to14 | (persons.AGEP >= 10) & (persons.AGEP <= 14) | +| Age_15to17 | mgra | persons | 100000 | Age_15to17 | (persons.AGEP >= 15) & (persons.AGEP <= 17) | +| Age_18to24 | mgra | persons | 100000 | Age_18to24 | (persons.AGEP >= 18) & (persons.AGEP <= 24) | +| Age_25to34 | mgra | persons | 100000 | Age_25to34 | (persons.AGEP >= 25) & (persons.AGEP <= 34) | +| Age_35to44 | mgra | persons | 100000 | Age_35to44 | (persons.AGEP >= 35) & (persons.AGEP <= 44) | +| Age_45to54 | mgra | persons | 100000 | Age_45to54 | (persons.AGEP >= 45) & (persons.AGEP <= 54) | +| Age_55to64 | mgra | persons | 100000 | Age_55to64 | (persons.AGEP >= 55) & (persons.AGEP <= 64) | +| Age_65to74 | mgra | persons | 100000 | Age_65to74 | (persons.AGEP >= 65) & (persons.AGEP <= 74) | +| Age_75to84 | mgra | persons | 100000 | Age_75to84 | (persons.AGEP >= 75) & (persons.AGEP <= 84) | +| Age_85Plus | mgra | persons | 100000 | Age_85Plus | (persons.AGEP >= 85) | + +The totals in each of the zones are then defined in the control total file, which is defined in settings.yaml within the `input_table_list` setting. Within that setting, there are two subsettings: `filename` which defines the name of the file within the PopulationSim run's data directory (in this case data\repop_control_totals.csv), and `tablename`, which defines what the table will be called in memory. +``` +input_table_list: + - filename : repop_control_totals.csv + tablename: mgra_control_data +``` + +These values can be set to add a population to the study area that is characteristic of a typical transit-oriented development in the San Diego area. +| mgra | num_hh | HHSize_1 | HHSize_2 | HHSize_3 | HHSize_4Plus | HHInc_0to14999 | HHInc_15000to29999 | HHInc_30000to59999 | HHInc_60000to99999 | HHInc_100000to149999 | HHInc_150000to199999 | HHInc_200000Plus | HHWork_0 | HHWork_1 | HHWork_2 | HHWork_3Plus | Age_LT5 | Age_5to9 | Age_10to14 | Age_15to17 | Age_18to24 | Age_25to34 | Age_35to44 | Age_45to54 | Age_55to64 | Age_65to74 | Age_75to84 | Age_85Plus | +|----------------------|----------------------|----------------------|----------------------|----------------------|----------------------|----------------------|----------------------|----------------------|----------------------|----------------------|----------------------|----------------------|----------------------|----------------------|----------------------|----------------------|----------------------|----------------------|----------------------|----------------------|----------------------|----------------------|----------------------|----------------------|----------------------|----------------------|----------------------|----------------------| +| 579 | 1000 | 383 | 388 | 134 | 95 | 67 | 127 | 298 | 227 | 150 | 70 | 61 | 606 | 114 | 199 | 81 | 93 | 121 | 100 | 55 | 250 | 481 | 289 | 208 | 203 | 205 | 124 | 44 | +| 4502 | 1000 | 383 | 388 | 134 | 95 | 67 | 127 | 298 | 227 | 150 | 70 | 61 | 606 | 114 | 199 | 81 | 93 | 121 | 100 | 55 | 250 | 481 | 289 | 208 | 203 | 205 | 124 | 44 | + +The output synthetic population files, output\synthetic_households.csv and output\synthetic_persons.csv, then need to be placed in the `data` directory for the ActivitySim run and renamed households.csv and persons.csv, respectively. It should be noted that SANDAG does have an additional processing step to conver the PopulationSim outputs into ActivitySim inputs, which is available in their [Land Use Prep Tool](https://github.com/SANDAG/landuse_prep_tool) repository. + +To check that the number of households is correct in the updated synthetic population, it may be useful to check that the population was added correctly. One can do so by reading households.csv into a Pandas DataFrame and checking that the values of `len(households.query("mgra == 579")) and len(households.query("mgra == 4502"))` are both equal to 1000 (SANDAG's MAZs are based on a geography used by SANDAG staff known as the Master Geographic Reference Area). + +2. The land use file now needs to be updated to reflect the updated population. This will be demonstrated using code blocks, that have the ActivitySim input files read in as Pandas DataFrames as follows: +``` +households = pd.DataFrame(r"data-full\households.csv") +persons = pd.DataFrame(r"data-full\persons.csv") +land_use = pd.DataFrame(r"data-full\land_use.csv") +``` +These lines of code update the household and population values within the land use file. +``` +land_use["hh"] = households.groupby("home_zone_id").count()["household_id"] +persons["home_zone_id"] = persons["household_id"].map(households.set_index("household_id")["home_zone_id"]) +land_use["pop"] = persons.groupby("home_zone_id").count()["person_id"] +del persons["home_zone_id"] +``` +Further variables will need to be edited as well (a full list of all variables is shown at the beginning of the section on setting up the scenario). For example, the SANDAG example contains variables on the number of housing units in each MAZ, including those that are vacant (so this will be higher than the number of households). There are also variables for population density. It may initially seem that one could just calculate them by dividing the updated population by the total area. While some models may use population density calculated in that way, the population density variables in the SANDAG model are actually the population within a buffer and are calculated via a preprocessing step that's external to ActivitySim (which should be rerun in this particular scenario). One should pay close attention to how each of the variables are defined to reduce the risk of a misunderstanding causing incorrect model results. + +3. The land use data needs to again be adjusted for the retail jobs. This is overall more straightforward than adjusting the population, as ActivitySim models don't typically have a set of synthetic set of establishments. As this particular scenario add 1000 additional retail jobs to the TOD development, the values of the retail and total employment fields simply need to be updated for the zones within the study area: +``` +# Adjust employment +land_use = land_use.set_index("MAZ") +land_use.loc[579, "emp_ret"] += 500 +land_use.loc[579, "emp_total"] += 500 +land_use.loc[4502, "emp_ret"] += 500 +land_use.loc[4502, "emp_total"] += 500 +land_use = land_use.reset_index() # Not necessary, but helpful if further operations use the MAZ field this could prevent an error +``` +It should be noted that the same caveat applies to fields derived from employment data, such as employment density or any aggregated fields that may be present. One should be careful to update all fields that are relevant to the total employment. + +The following lines of code then write the updated ActivitySim inputs to file (the `index = False` keyword argument prevents a superfluous index column from being written): +``` +households.to_csv(r"data-full\households.csv", index = False) +persons.to_csv(r"data-full\persons.csv", index = False) +land_use.to_csv(r"data-full\land_use.csv", index = False) +``` + +## Running the Test + +To run the test, run the following command line argument: +``` +uv run activitysim run -c configs\common -c configs\resident -d data_full -o output --ext extensions +``` + +## Analyzing the Results + +The following code blocks demonstrate how to calculate key metrics from the model outputs. They all assume that the ActivitySim output files will be read in as a data frame where the name will be the same as the file name but without the prefix or the file extension (e.g. final_trips.csv will be read as trips). + +### Auto Ownership +The outputs of most ActivitySim model components are stored as a single field in that component's choosers table. This means that when the table is read in as a Pandas DataFrame, the share of choosers choosing each alternative can be calculated via the `pandas.Series.value_counts()` function by setting the `normalize` argument to `True` (it is `False` by default). For the auto ownership model, the choosers are the households, and thus the number of autos is an attribute on the households table. + +The calculation of the reigional auto ownership rates is very straightforward: +``` +auto_ownership = households["auto_ownership"].value_counts(normalize = True) +``` +However, when comparing to a baseline run, there won't be much change to these values when looking at the whole region as the development in question is very localized. For that reason, analysts may want to consider looking at just the area around the station, which should show a more pronounced change than just looking at the regionwide numbers: +``` +station_area = [579, 4502, 8524, 7714, 12170, 12171, 5455, 8457, 846, 8232, 7831, 12172, 12173, 12174, 12175, 12176, 12177, 12178] +station_area_households = households.query("home_zone_id in @station_area") +station_area_auto_ownership = station_area_households["auto_ownership"].value_counts(normalize = True) +``` +One could also compare the auto ownership values compared to the regionwide values. + +### Transit Pass Ownership +Where auto ownership was a household-level choice, transit pass ownership is a person-level choice, so the transit pass ownership field is in the persons table. This is how to calculate the transit pass ownership share: +``` +transit_pass_ownership_rate = persons["transit_pass_ownership"].value_counts(normalize = True) +``` +This variable is 1 for those who own a transit pass and 0 who do not, so the share of people with a value of 1 will be the transit pass owership rate. It should be noted that this variable would also be more sensitive for the station area than the whole region when comparing to a baseline run: +``` +station_area_persons = persons.query("home_zone_id in @station_area") +station_area_transit_pass_ownership = station_area_persons["transit_pass_ownership"].value_counts(normalize = True) +``` + +### Average Distance To Work +For every worker with an out-of-home work location, ActivitySim models what that location is. While the zone ID is the only output of the workplace location model, the SANDAG ABM3 Example is configured to add the [distance to work](https://github.com/ActivitySim/sandag-abm3-example/blob/main/configs/resident/annotate_persons_workplace.csv#L3) to the persons table, meaning that the average distance to work can be calculated using the `pandas.Series.mean()` function with that field (Pandas will ignore the null values for people who don't work outside their home): +``` +avg_dist_to_work = persons["distance_to_work"].mean() +``` +Further, one can put the work distances into bins by using NumPy's `histogram()` method. This would allow for one to look at the whole distribution of the distances, not just the average. The following describes 5-mile bins up to 30 miles (with one large bin for 30 miles and above assuming 100 miles is greater than the maximum possible distance), though any analyst is free to define their bins to suit their needs: +``` +import numpy as np +bins = list(range(0, 35, 5) + [100]) +workers_by_distance_bin = np.histogram(persons["distance_to_work"], bins = bins)[0] +``` +One could also filter for the study area using a method similar to the method used when demonstrating the calculation of transit pass ownership rates. + +### Mode Share +Calculating the mode share of ActivitySim is fairly straightforward as the modes are reported in the output. However, as this is an activity-based model, one needs to ask the questions of *which* mode share they'd like to know. For example, the simplest is the regional mode share, which can just be directly calculated from the trips file: +``` +mode_share = trips["trip_mode"].value_counts(normalize = True) +``` +This will return the percentage of trips that use each mode. However, for similar reasons as described above, one may want to only focus on the study area and not just look at the trip mode share for all trips. The following metric computes the *tour* mode share to work for workers living within the station area. +``` +station_area = [579, 4502, 8524, 7714, 12170, 12171, 5455, 8457, 846, 8232, 7831, 12172, 12173, 12174, 12175, 12176, 12177, 12178] +station_area_tours = tours[["home_maz", "tour_mode", "tour_purpose"]].query("origin in @station_area and tour_purpose == 'work'") +tour_mode_share_to_work = station_area_tours["tour_mode"].value_counts(normalize = True) +``` + +### Vehicle Miles Traveled +While the true modeled VMT requires assignment to be run, one can get a reasonable estimate via the ActivitySim outputs. The output trips table in the SANDAG ABM3 example actually includes fields called `distance` and `weightTrip`, which are created in the preprocessor for writing the outputs (write_trip_matrices_annotate_trips_preprocessor.csv). The `distance` field is created by [reading in the distance skim value](https://github.com/ActivitySim/sandag-abm3-example/blob/main/configs/resident/write_trip_matrices_annotate_trips_preprocessor.csv#L5) and the `weightTrip` field is a weight that [factors in the occupancy](https://github.com/ActivitySim/sandag-abm3-example/blob/main/configs/resident/write_trip_matrices_annotate_trips_preprocessor.csv#L7). The following lines of code compute the VMT using those particular fields: +``` +auto_modes = ["DRIVEALONE", "SHARED2", "SHARED3", "TNC_SINGLE", "TNC_SHARED", "TAXI"] +auto_trips = trips[["trip_mode", "distance", "weightTrip"]].query("trip_mode in @auto_modes") +vmt = (auto_trips["distance"] * auto_trips["weightTrip"]()).sum() +``` +Now, not every ActivitySim implementation will have such a field in their outputs, so the calculation may not be as simple. If the distance field isn't added to the outputs, one will need to read in the skims in order to perform the calculation. One will also need to remember to factor in the occupancy, as an individual who is carpooling has less of an impact on VMT than a person who is driving alone. + +Further, if one wants to normalize the VMT by capita, they simply need to divide the VMT value by the number of persons: +``` +vmt_per_capita = vmt / len(persons) +``` + +## Summary +This guide demonstrated how to set up, run, and analyze a TOD scenario in ActivitySim. Users were shown how to configure PopulationSim in repop mode, perform QA on the updated synthetic population, run the scenario, and analyze some key metrics. Some alternate ways that scenarios could be set up were mentioned, such as adding the population through a script or changing utility coefficients to better reflect different behavior when living in such a development. Nuances on the metrics were described, such has how impacts from localized projects may not be as noticable in metrics that describe network performance at a regional level. \ No newline at end of file diff --git a/docs/users-guide/application-analysis-guide/network-change/network_change.md b/docs/users-guide/application-analysis-guide/network-change/network_change.md new file mode 100644 index 000000000..47511c0d2 --- /dev/null +++ b/docs/users-guide/application-analysis-guide/network-change/network_change.md @@ -0,0 +1,76 @@ +# Network Change + +The most influential policy choice that transportation planners can make is changing the network itself. Evaluating the impact of building additional roadways were why travel demand models were initially developed in the first place. As transportation planning has expanded to be reflective of other modes, models of people's daily travel behavior needed to be adjusted so they could model the affects of changes to the transit or nonmotorized networks as well. ActivitySim itself does not directly model network changes at the present. In fact, ActivitySim doesn't actually take network files as inputs. This doesn't mean that ActivitySim isn't sensitive to network changes. ActivitySim models the demand for travel, so it uses the skim matrices as those contain the information that travelers use when making their decisions. In most cases travelers aren't concerned about the specific route when deciding if and where to travel. They're more concerned with things such as how long it will take them to get there or what the cost will be, and this information is stored in the skim matrices. + +This guide will demonstrate how to model and analyze the addition of a BRT line using ActivitySim. The line will run from the San Diego suburb of La Mesa to the neighborhood of Ocean Beach via the Hillcrest neighborhood. Please note that the network change itself needs to be made in whichever assignment software that is being used, which will not be discussed in this guide as many ActivitySim users use different assignment software, and network editing needs to be done differently in each. SANDAG uses custom software that was developed by ESRI for editing and managing their model networks. + +## Setting up the Scenario + +1. In the software system that manages the model networks, add the BRT line to the transit network. + +2. Run transit assignment and skimming with the updated network. + +3. Move the updated transit skim files into the `data` folder for the ActivitySim run. + +If one were to make a change to the roadway network instead of the transit network, they would need to run highway assignment and skimming instead of transit assignment and skimming. Further, for any network change that is more temporary (such as a closure of a bridge or transit line), it is possible to freeze the results of the longer-term choice models and only model how people will change their shorter-term behavior. To do so, one must first have a completed run for the desired scenario year, and save all of the pipeline results (`cleanup_pipeline_after_run` must be set to `False` in the ActivitySim settings file). Then, in the settings file, set `resume_after` to be the last model step that you wish to freeze. In the SANDAG ABM3 example, if one were to want to only run the short-term choices, they would set `resume_after: telecommute_frequency` in settings_mp.yaml which would only run the model steps from `cdap_simulate` on, reading the upstream results from the saved pipeline. + +## Running the Test + +To run the test, run the following command line argument: +``` +uv run activitysim run -c configs\common -c configs\resident -d data_full -o output --ext extensions +``` + +## Analyzing the Results + +The following code blocks demonstrate how to calculate key metrics from the model outputs. They all assume that the ActivitySim output files will be read in as a data frame where the name will be the same as the file name but without the prefix or the file extension (e.g. final_trips.csv will be read as trips). + +### Mode Share by Purpose + +The following code calculates the tour mode share by tour purpose: +``` +tour_mode_share_by_purpose = tours[["tour_mode", "primary_purpose"]].value_counts().reset_index().pivot(index = "tour_mode", columns = "primary_purpose", values = "count").fillna(0) +for col in tour_mode_share_by_purpose.columns: + tour_mode_share_by_purpose[col] /= tour_mode_share_by_purpose[col].sum() +``` +To see a stronger contrast with the baseline scenario, one can filter the tours if the origin and/or destination is along the BRT line's corridor. Which specific zones are along the corridor would be determined via a GIS exercise. + +### Share of Trips by Time Period + +ActivitySim codes the time of day with 48 half-hour periods starting at 3 AM. This means that period 1 is from 3-3:30 AM, period 2 is 3:30-4 AM, and so forth. Many planners are interested in measuring how many people shift the time of their travel from peak periods to off-peak periods (known as peak spreading), which puts less strain on the transportation system as a whole. The following code creates a field that categorizes trips into categories for each hour of the AM peak or the rest of the day based on the departure period, and then groups the number of trips by that category. By comparing the results of the test run to the baseline run, the analyst can directly caluclate how many trips moved away from the AM peak. +``` +trips["AM Peak Hour"] = np.where( + (trips["depart"] == 7) | (trips["depart"] == 8), + "6-7 AM", + np.where( + (trips["depart"] == 9) | (trips["depart"] == 10), + "7-8 AM", + np.where( + (trips["depart"] == 11) | (trips["depart"] == 12), + "8-9 AM", + "Rest of Day" + ) + ) +) +trips_by_am_peak_hour = trips["AM Peak Hour"].value_counts() +``` + +### Congestion Impact +There are many ways one could measure the effects of traffic congestion. Most of these are directly the result of highway assignment, of which ActivitySim does not presently have a model component for. For example, ActivitySim does not report any network-level results, so one would not be able to calculate link volumes directly from ActivitySim outputs. + +### Vehicle Miles Traveled +While the true modeled VMT requires assignment to be run, one can get a reasonable estimate via the ActivitySim outputs. The output trips table in the SANDAG ABM3 example actually includes fields called `distance` and `weightTrip`, which are created in the preprocessor for writing the outputs (write_trip_matrices_annotate_trips_preprocessor.csv). The `distance` field is created by [reading in the distance skim value](https://github.com/ActivitySim/sandag-abm3-example/blob/main/configs/resident/write_trip_matrices_annotate_trips_preprocessor.csv#L5) and the `weightTrip` field is a weight that [factors in the occupancy](https://github.com/ActivitySim/sandag-abm3-example/blob/main/configs/resident/write_trip_matrices_annotate_trips_preprocessor.csv#L7). The following lines of code compute the VMT using those particular fields: +``` +auto_modes = ["DRIVEALONE", "SHARED2", "SHARED3", "TNC_SINGLE", "TNC_SHARED", "TAXI"] +auto_trips = trips[["trip_mode", "distance", "weightTrip"]].query("trip_mode in @auto_modes") +vmt = (auto_trips["distance"] * auto_trips["weightTrip"]()).sum() +``` +Now, not every ActivitySim implementation will have such a field in their outputs, so the calculation may not be as simple. If the distance field isn't added to the outputs, one will need to read in the skims in order to perform the calculation. One will also need to remember to factor in the occupancy, as an individual who is carpooling has less of an impact on VMT than a person who is driving alone. + +Further, if one wants to normalize the VMT by capita, they simply need to divide the VMT value by the number of persons: +``` +vmt_per_capita = vmt / len(persons) +``` + +## Summary +Network changes are the most important and noticible transportation policy change that planning agencies will make. While ActivitySim does not directly model how people will adjust the route changes they make in response to the network change, it can model how they adjust their overall travel behavior such as whether or not transit use increases if a transit line serving their needs is added to the network. Setting up the scenario doesn't require much within ActivitySim configuration other than updating the appropriate skim files. However, those changes in the skim files could have a significant change on the outputs, many relevant summaries of which were demonstrated here. \ No newline at end of file diff --git a/docs/users-guide/application-analysis-guide/telecommute-change/telecommute_change.md b/docs/users-guide/application-analysis-guide/telecommute-change/telecommute_change.md new file mode 100644 index 000000000..dcba8ec9e --- /dev/null +++ b/docs/users-guide/application-analysis-guide/telecommute-change/telecommute_change.md @@ -0,0 +1,122 @@ +# Telecommuting Change + +Telecommuting has been on planners' minds since the start of the COVID-19 Pandemic in 2020, as the rapid increase in telecommuting at the onset of the pandemic resulted in drastic reductions in VMT and transit boardings, the latter of which hasn't recovered in some places. At the present many employers are pushing for their employees to return to the office, and thus planners would like to test the impacts of such on the transportation network. This guide will demonstrate how to set up an increased return-to-office scenario in ActivitySim. + +## Setting up the Scenario + +ActivitySim has a telecommute frequency model, which is run fairly early on in the model stream as whether or not one telecommutes has a major impact on their travel behavior in a given day. If one were to look at the telecommute frequency results for the SANDAG ABM3 Example model (which was estimated and calibrated using a combination of 2016 and 2022 data), they would see a share along the lines of this (note that 5 days/week is not an option as those workers are determined in the work from home model): + +| Alternative | Share of Workers | +| ------------------------- | ---------------- | +| No Telecommuting | 75.8% | +| Telecommuting 1 Day/Week | 4.9% | +| Telecommuting 2 Days/Week | 5.9% | +| Telecommuting 3 Days/Week | 5.9% | +| Telecommuting 4 Days/Week | 7.4% | + +Because the output of the share, the model outputs would need to be calibrated. The telecommute frequency in the SANDAG ABM3 Model has three calibration coefficients: one for telecommuting 1 day per week, one for telecommuting 2-3 days per week, and one for telecommuting 4 days per week (These values are set in the file configs\resident\telecommute_frequency_coeffs.csv). This means that the 2 days per week and 3 days per week categories need to be added together when calibrating: + +| Alternative | Share of Workers | +| --------------------------- | ---------------- | +| No Telecommuting | 75.8% | +| Telecommuting 1 Day/Week | 4.9% | +| Telecommuting 2-3 Days/Week | 11.8% | +| Telecommuting 4 Days/Week | 7.4% | + +The guide will calibrate the telecommute frequencies to the following shares: +| Alternative | Share of Workers | +| --------------------------- | ---------------- | +| No Telecommuting | 90.0% | +| Telecommuting 1 Day/Week | 7.0% | +| Telecommuting 2-3 Days/Week | 2.0% | +| Telecommuting 4 Days/Week | 1.0% | + +The coefficients then need to be adjusted based on the natural logarithm of the ratio of the target share to the modeled share. It should be noted that the shares won't match at first, so the process needs to be run iteratively. The next iteration's coefficient $$c_{n+1}$$ on a given iteration $$n$$ is calculated as follows: + +$$c_{n+1}=c_{n}+\alpha\ln{(\frac{\hat{s}}{s_{n}})}$$ + +Where $$\hat{s}$$ is the target share, $$s_{n}$$ is the share for calibration iteration $$n$$, and $$\alpha$$ is an optional factor to control how quickly convergence is reached (in this example it will be set to 1). This means that the coefficients for the first calibration iteration would be as follows: + +| Alternative | Target Share | Modeled Share | Adjustment | Old Coefficient | New Coefficient | +| --------------------------- | ------------ | ------------- | ---------- | --------------- | --------------- | +| No Telecommuting | 90.0% | 75.8% | | | | +| Telecommuting 1 Day/Week | 7.0% | 4.9% | 0.357 | -2.549 | -2.192 | +| Telecommuting 2-3 Days/Week | 2.0% | 11.8% | -1.775 | -1.534 | -3.309 | +| Telecommuting 4 Days/Week | 1.0% | 7.4% | -2.001 | -1.948 | -3.949 | + +The user would then need to run ActivitySim with the following values set in configs\resident\telecommute_frequency_coeffs.csv: + +| coefficient_name | value | constrain | +| ---------------- | ------ | --------- | +| asc_1day | -2.192 | F | +| asc_23day | -3.309 | F | +| asc_4day | -3.949 | F | + +As the telecommute frequency model is relatively early in the model stream, one can save time while calibrating by skipping the remaining model steps. This can be done by commenting out all everything from `cdap_simulate` to `parking_location` along with `write_trip_matrices` in the `models` list in configs\resident\settings_mp.yaml along with commenting out the blocks defining the output `tours` and `trips` tables in the `output_tables` section of configs\resident\settings.yaml. To speed up each calibration even further, one can set `resume_after: free_parking` in settings_mp.yaml to skip all of the prior steps and read them in from the saved pipeline (assuming `cleanup_pipeline_after_run` is set to be `False`). + +After enough calibration iterations have been run so that the model shares are sufficiently close enough to the target shares, the user can run the full model to estimate the impact of increased returning to the office on the transportation system and people's overall travel behavior. + +## Running the Test + +To run the test, run the following command line argument: +``` +uv run activitysim run -c configs\common -c configs\resident -d data_full -o output --ext extensions +``` + +## Analyzing the Results + +The following code blocks demonstrate how to calculate key metrics from the model outputs. They all assume that the ActivitySim output files will be read in as a data frame where the name will be the same as the file name but without the prefix or the file extension (e.g. final_trips.csv will be read as trips). + +### Daily Activity Pattern +One model result that one would expect to change from a decrease in teleworking is the daily activity pattern. One would expect the use of the "Mandatory" activity pattern to increase and the "Nonmandatory" and "Home" patterns to decrease. For both the baseline and the test runs, one can calculate the share of people choosing each day pattern with the following block of code: +``` +dap_share = persons["cdap_activity"].value_counts(normalize = True) +``` +However, one may want to only look at the workers, as the activity pattern of nonworkers should be generally the same between the two scenarios (though intra-household interactions may change the patterns of some of the non-workers): +``` +worker_dap_share = persons.query("is_worker")["cdap_activity"].value_counts(normalize = True) +``` + +### Trips by Purpose for Workers +A change in telecommute frequency would likely result in a change in the number of tours by purpose for workers. If a worker is teleworking, that gives them more flexibility in their ability to make nonmandatory travel, so one could expect trips within those purposes to increase when comparing to a baseline run. The following calculates the number of trips by purpose for workers: +``` +trips["is_worker"] = persons.set_index("person_id")["is_worker"].reindex(trips["person_id"]) +trips_by_workers = trips.query("is_worker") +worker_trips_by_purpose = trips_by_workers["purpose"].value_counts() +``` + +### Time of Day Distribution +Decreased telecommuting should have a strong impact on the time of day distribution. It was observed in multiple places that the AM travel peak effectively disappeared in the years immediately following the onset of the COVID-19 pandemic, so one could reasonably guess that 9-5 workers largely returning to the office would result in that peak reemerging. The time of the trip is stored in the `depart` field of the trips file and is coded in a half-hour bin starting at 3 AM, with time period 1 being 3-3:30 AM, time period 2 being 3:30-4 AM, and so forth. One can group the number of trips by time period and sort them in order. Comparing the `trips_by_time_period` series between the baseline run and the test run will allow the analyst to see if the AM peak returned. +``` +trips_by_time_period = trips["depart"].value_counts(normalize = True).sort_index() +``` + +### Average Distance to Work +The work location model is run before the telecommute frequency, so one should expect it should not change. However, one may want to check its results to ensure that it doesn't change. The following code block calculates that: +``` +avg_dist_to_work = persons["distance_to_work"].mean() +``` + +### Vehicle Miles Traveled +While the true modeled VMT requires assignment to be run, one can get a reasonable estimate via the ActivitySim outputs. The output trips table in the SANDAG ABM3 example actually includes fields called `distance` and `weightTrip`, which are created in the preprocessor for writing the outputs (write_trip_matrices_annotate_trips_preprocessor.csv). The `distance` field is created by [reading in the distance skim value](https://github.com/ActivitySim/sandag-abm3-example/blob/main/configs/resident/write_trip_matrices_annotate_trips_preprocessor.csv#L5) and the `weightTrip` field is a weight that [factors in the occupancy](https://github.com/ActivitySim/sandag-abm3-example/blob/main/configs/resident/write_trip_matrices_annotate_trips_preprocessor.csv#L7). The following lines of code compute the VMT using those particular fields: +``` +auto_modes = ["DRIVEALONE", "SHARED2", "SHARED3", "TNC_SINGLE", "TNC_SHARED", "TAXI"] +auto_trips = trips[["trip_mode", "distance", "weightTrip"]].query("trip_mode in @auto_modes") +vmt = (auto_trips["distance"] * auto_trips["weightTrip"]()).sum() +``` + +### Number of Transit Trips +As previously mentioned, teleworking has generally seen a decrease in transit usage. Analysts may want to estimate the impact of return to office on the number of transit trips for purposes such as revenue forecasting. The following code block demonstrates how to calculate the number of transit trips for a given run. One can compare the value of the `transit_trips` series between the test run and a baseline run to get an estimate in the increase, though results from transit assignment would be needed for more detailed calculation, such as boardings on specific lines. +``` +transit_modes = [ + "WALK_LOC", "WALK_PRM", "WALK_MIX", + "PNR_LOC", "PNR_PRM", "PNR_MIX", + "KNR_LOC", "KNR_PRM", "KNR_MIX", + "TNC_LOC", "TNC_PRM", "TNC_MIX" +] +transit_trips = len(trips.query("trip_mode in @transit_modes")) +``` + +## Summary + +The impacts of returning to the office are something that is on the mind of many transportation planners as we move into the late 2020s. Many employers want to have their employees work from an office more which will result in an increase in travel to work. This will result in an increased strain on the transportation network that needs to be planned for. ActivitySim is capable of modeling an decrease in telecommuting with its Telecommute Frequency model component, though it takes more effort than just simply changing a few model inputs here and there. Because how often people telecommute is an output of that model, it must be calibrated to match the desired telecommuting shares, which will involve iteratively running the component and adjusting calibration coefficients until the results are sufficiently close to the desired targets. After that is done, the full modeling system can be run to estimate the impact of people telecommuting fewer days, which based on the structure of the model will impact things such as the daily activity pattern and number of trips taken by transit, but not where people choose to work as that model component is typically run earlier. \ No newline at end of file