4) Urban Network Analysis (UNA) Tools: Alternative Paths and Betweenness
We will use the same layers, origins and destinations setup from our previous example:
[1]:
import madina as md
import madina.una.tools as una
cambridge = md.Zonal()
#Loading sidewalks, buildings and subway geometries.
cambridge.load_layer('sidewalks', 'Cities/Cambridge/Data/sidewalks.geojson')
cambridge.load_layer('buildings', 'Cities/Cambridge/Data/building_entrances.geojson')
cambridge.load_layer('subway', 'Cities/Cambridge/Data/subway.geojson')
# Creating a network, and adding origins and destinations
cambridge.create_street_network(source_layer="sidewalks", node_snapping_tolerance=0.1)
cambridge.insert_node(label='origin', layer_name="subway")
cambridge.insert_node(label='destination', layer_name="buildings")
# Creating graphs
cambridge.create_graph()
una.alternative_paths
This tool finds all possible paths within a detour ratio from the shortest path between a given origins to all reachable destinations within a search radius
[2]:
path_gdf = una.alternative_paths(
cambridge,
origin_id=1,
search_radius=300,
detour_ratio=1.15,
)
cambridge.create_map(
[
{'layer': 'sidewalks', 'color': [125, 125, 125], 'opacity': 0.1},
{'gdf': path_gdf[path_gdf['destination'] == 35].sort_values('distance', ascending=False), 'color_by_attribute': 'distance', 'color_method': 'gradient'},
{'gdf': cambridge['buildings'].gdf.loc[[35]], 'color': [255, 0, 0]},
{'gdf': cambridge['subway'].gdf.loc[[1]], 'color': [255, 0, 0]},
]
)
[2]:
[3]:
print ("path count: ", path_gdf[path_gdf['destination'] == 35].shape[0])
print ("Shortest path: ", path_gdf[path_gdf['destination'] == 35]['distance'].min())
print ("Longest path: ", path_gdf[path_gdf['destination'] == 35]['distance'].max())
path count: 23
Shortest path: 200.79423856491636
Longest path: 230.79936586656325
una.alternative_paths - Turn Penalty
[4]:
cambridge.set_turn_parameters(
turn_penalty_amount=30,
turn_threshold_degree=45,
)
path_gdf = una.alternative_paths(
cambridge,
origin_id=1,
search_radius=300,
detour_ratio=1.15,
turn_penalty=True,
)
cambridge.create_map(
[
{'layer': 'sidewalks', 'color': [125, 125, 125], 'opacity': 0.1},
{'gdf': path_gdf[path_gdf['destination'] == 35].sort_values('distance', ascending=False), 'color_by_attribute': 'distance', 'color_method': 'gradient'},
{'gdf': cambridge['buildings'].gdf.loc[[35]], 'color': [255, 0, 0]},
{'gdf': cambridge['subway'].gdf.loc[[1]], 'color': [255, 0, 0]},
]
)
[4]:
[5]:
print ("path count: ", path_gdf[path_gdf['destination'] == 35].shape[0])
print ("Shortest path: ", path_gdf[path_gdf['destination'] == 35]['distance'].min())
print ("Longest path: ", path_gdf[path_gdf['destination'] == 35]['distance'].max())
path count: 9
Shortest path: 290.8119650520391
Longest path: 331.02084028670606
When enabling turn penalty, we could notice any of the following:
The shortest path could change into one that is geometrically longer, but contains fewer turns. This means that the shortest path would increase in lengtth
If the shortest path increases in length, the allowed detour increases. This mean that additional, longer paths might become eligible as alternative within the same detour ratio
paths with more turns might lose eligibility, and new paths that where geometrically longer than the allowed original detour might become eligible
una.betweenness
The betweenness tool generate trips from origins to destinations along multiple possible paths depending on a set of parameters. Each network segment is then assigned a value that indicates the intensity of trips that passes through it. Trip generation between origins and destinations, and the path assignment of these trips is subject to the following:
Search Radius: Each origin would generate to destinations that are no further away than this search radius, in the same distance unit for the used CRS.
Detour Ratio: Paths that deviates from the shortest path between an origin and a destination by this ratio are considered. If set to 1, only the shortest path between an origin and a destination is considered.
origin weight: if not specified, all origins would generate exactly 1 trip. if an origin attribute is specified, each origin would generate a number of trips that matches the value of the attribute.
Decay if enabled, a decay model would be used to limit the generation of trip between a given origin and a detanation based on a specified decay function.
decay method: a mathimatical function that takes as input the shortest distance between an origin and a destination, and returns a decay ratio between 0 and 1. An exponential decay function is used that takes a beta parameter.
Closest Destination: Are trips only oruted to the closest destination? or are they routed to all reachable destination within the search radius?
Destination Weight: if trips are routed to multiple destinations, a destination weight oculd be used to allocate partial trips using a probability function that takes in distancess to all reachable destination, and their respective weights. This is done through the Huff Model. Total trips assigned to all destination would be exactly equal to the origin weight
Turn Penalty a turn penalty could bem imposed on the distance betweem an origin and a destination based on how many turns each path has. a Turn is defined by a turn angle threshold, and a penalty is specified in the same CRS distance unit.
Percieved Distance: By default, the geometric distance of network segment is used, but it is possible to assign each individual segment a weight that represent its percieved length. This could be based on behavioural analysis.
Elastic Trip Generation: a K-nearest-neighbor approach that bases each origin’s trip generation on how many destinations are accessible, So that origins with higher access are allowed to generate more trips than origins with less access.
We will go over these parameters one by one using a few examples, using one origin. As each origin is independent from other origins, Trips generated by the betweenness tool are additive across origins.
Classical Betweenness Centrality
In graph theory, the edge betweenness centrality is defined as “the sum of the fraction of all-pairs shortest paths that pass through the edge” (NetworkX). This is done by finding the shortest path between each node, and all other nodes in the graph, and for each edge, calculating the ratio of paths that pass through that edge to the total number of paths. While this metric is a good indicator of segment centrality and importance, it doesn’t account for many charateristics of urban transport networks:
Differences between node types, does not distinguish between origin nodes and destination nodes for instance. Paths are generated between all nodes irrespective of their types. In urban applications, this might not reflect reality as same-type trips are minority.
Node importance nodes are weighted equally. Cannot accomidate nodes that generate (or attract) more traffic
Reliance on shortest path, where in many applications in walkabiliy, shortest paths are not necessirally prefereable to pedestrains for instance.
While this measure is able to account for segment-specific percieved distance by using an appropriate network weight, it cannot factor path-specific charactewristics such as turns and elevation.
This definition, while is limited, is still widely used in many urban analysis applications (The Space Syntax is a well known example). Its simplicity means it could be applied with only information about the netwrok structure, and little-to-no information about the built environment and land use.
The UNA Betweenness provides an extension to the classical definition of edge betweenness centrality, addressing some of the limitations that of particular interest in urban networks. In the simplest form, the UNA betweenness is identical to the classical edge betweenness centrality.
Lets create a network with one origin, and one destination
[6]:
one_building_gdf = cambridge['buildings'].gdf.loc[[11]]
cambridge.load_layer('one_building', one_building_gdf)
cambridge.clear_nodes()
cambridge.insert_node(label='origin', layer_name="one_building")
cambridge.insert_node(label='destination', layer_name="subway")
cambridge.create_graph()
cambridge.create_map([{'layer': 'one_building'}, {'layer': 'subway'}, {'layer': 'sidewalks'}])
[6]:
The UNA betweenness definition deviates from the classical edge betweenness definition as it only generate paths from all origins to all reachable destinations, as opposed to generating shortest paths from all nodes regardless of their label (origins, destinations, street intersections). Calling the una.betweenness() function with the default setting, and giving it a large enough search radius such that all origins can reach all destinations, shows the basic version of the betweenness in
UNA.
The parameter save_betweenness_as saves the results as a column in the source layer given to create_street_network(source_layer="sidewalks"), in this case, the sidewalk layer, so we could visualize the result using the sidewalk layer
[7]:
una.betweenness(
cambridge,
search_radius = 300,
save_betweenness_as="betweenness_baseline"
)
cambridge.create_map(
[
{'layer': 'one_building'},
{'layer': 'subway'},
{'layer': 'sidewalks', 'color_by_attribute': 'betweenness_baseline', 'color_method': 'gradient', 'text': "betweenness_baseline"}
]
)
Time spent: 3s [Done 1 of 1 origins (100.00%)]
[7]:
We get a betweenness of 1 for all segments along the shortest path between the origin and the destination. ## Impact of Origin Weight: When we inserted our origin earlier, we did not specify a weight. This means origins are weightwd equally and assigned a weight of one. in urban applications, origins generate trips propotionally to each origin’s trip generation capacity. In this example, buildings could be weighted by how many people live in them.
[8]:
cambridge.clear_nodes()
cambridge.insert_node(label='origin', layer_name="one_building", weight_attribute='people')
cambridge.insert_node(label='destination', layer_name="subway")
una.betweenness(
cambridge,
search_radius = 300,
save_betweenness_as="betweenness_origin_weight"
)
cambridge.create_map(
[
{'layer': 'one_building'},
{'layer': 'subway'},
{'layer': 'sidewalks', 'color_by_attribute': 'betweenness_origin_weight', 'color_method': 'gradient', 'text': "betweenness_origin_weight"}
]
)
Time spent: 3s [Done 1 of 1 origins (100.00%)]
[8]:
The building above has 47 people living in it. When we weigh origins, the number of trips generated from this origin now relfects that weight. We see that there was 47 trips generated from the building to the subway station along the shortest path. Weighing origins by an appropriate attribute, results in a better representation of the actual land use.
Impact of Detour:
When modeling pedestrians, it is unlikely that all pedestrians would follow shortest paths from all origins to all destinations while making trips. This could be a result of path qualities, lack of perfect knowledge, individual preferences and behaviours. The parameter detour_ratio is set to 1 by default. Setting it to a higher number would allow routing trips that deviate from the shortest route by this specified ratio. for instnace, if the shortest path between an origin and a destination
is 100m, and the detour_ratio is set to 1.15, all possible paths between the origin and the destination that are up to 115m would be considered.
[9]:
una.betweenness(
cambridge,
search_radius = 300,
detour_ratio=1.15,
save_betweenness_as="betweenness_detour"
)
cambridge.create_map(
[
{'layer': 'one_building'},
{'layer': 'subway'},
{'layer': 'sidewalks', 'color_by_attribute': 'betweenness_detour', 'color_method': 'gradient', 'text': "betweenness_detour"}
]
)
Time spent: 3s [Done 1 of 1 origins (100.00%)]
[9]:
We notice in this example, two alternative paths are considered. Trips are split equally between these trips, and each of these two paths gets 23.5 trips each. NOtice that these paths overlaps in many segments, these overlapping segments get 47 trips.
Impact of Decay:
It is well established that the distance between an origin and a destination inversly corelate with trip generation probability. the longer the trip, the less likely it would happen. This could be accounted for by using a decay function that reduces the number of trips as distance increases. An exponential decay function is provided as option, and it takes a beta parameter that represent a proxy for the resistance to walk. A higher beta value lead to less trips generated. The beta value of
the ecponential decay function is sensitive to the distance unit, and in using meters, a value between 0.001 and 0.004 is a good approximation for most urban contexts.
[10]:
una.betweenness(
cambridge,
search_radius = 300,
detour_ratio=1.15,
decay=True,
decay_method="exponent",
beta=0.001,
save_betweenness_as="betweenness_decay"
)
cambridge.create_map(
[
{'layer': 'one_building'},
{'layer': 'subway'},
{'layer': 'sidewalks', 'color_by_attribute': 'betweenness_decay', 'color_method': 'gradient', 'text': "betweenness_decay"}
]
)
Time spent: 3s [Done 1 of 1 origins (100.00%)]
[10]:
Using this particular value, reduced trips from 47 to 42.2 trips. The more distance, the less trip likelyhood. The higher the bets value, less trips are generated. The decay method could be one of these options: (“exponent”, “power”). ## Impact of Turn Penalty: Pedestrians tend to prefer more straight paths with less turns. Th ebetweenness tool in UNA can penalize turns by setting the parameter turn_penalty=True. This would impose a distance penalty for a path whenever a turn happens. In
order to account for turn , we have to define two parameters:
turn_penalty_amount: When a path go through a turn, how much distance penalty this path would accrue? This number is in the same units as the network CES.turn_threshold_degree: A threshold in degrees that defines a turn. When two segments meet at an angle greater thn or equal to this threshold, a turn penalty is imoposed.
These parameters are set by default when calling create_street_network(), or could be provided as input to that function. It is possible to update these parameters by calling cambridge.set_turn_parameters(). In this case, we specify a penalty of 70 meters, for any turn that exceeds a 45 degree from a straight path.
[11]:
cambridge.set_turn_parameters(
turn_penalty_amount=75,
turn_threshold_degree = 45,
)
una.betweenness(
cambridge,
search_radius = 300,
detour_ratio=1.15,
decay=True,
decay_method="exponent",
beta=0.001,
turn_penalty=True,
save_betweenness_as="betweenness_turn_penalty"
)
cambridge.create_map(
[
{'layer': 'one_building'},
{'layer': 'subway'},
{'layer': 'sidewalks', 'color_by_attribute': 'betweenness_turn_penalty', 'color_method': 'gradient', 'text': "betweenness_turn_penalty"}
]
)
Time spent: 3s [Done 1 of 1 origins (100.00%)]
[11]:
Using these settings, we notice that one of the two path options is no longer considered, as it has three more turns, causing it to be 225 meter longer than the alternative. The path length together with the turn penalties now exceed the allowed detour. We also notice the total trips generated is now 39.2, a reduction from 42.2 when we did not account for turns. The shortest path had one turn making it feel 75meters longer, and causing less trips to be generated.
Impact of Percieved Distance:
To account for network segment qualities, each segment could be assigned a weight that reflect positive and negative qualities of each segment. For instance, a wider sidewalk that is more comfortable to walk in might be precieved -behaviourally- as shorter than a segment with a narrow sidewalk that’s more physically and psychologically taxing to traverse. A crosswalk might be penalized as there is potential wait time, and less comfort to cross, compared to a sidewalk.
Custom segment weights that account for these qualities should be represented as an attribute/column in the layer used to construct the network, the ‘sidewalks layer in this case. Lets assign the segment with id=18, the crosswalk near the station, a crosswalk penalty of 50m additional to its length by creating an attribute called percieved_length. Making any changes to the sidewalks layer means that we need to reconstruct the network and add origins and destinations. The function
create_street_network() by default, uses the geometric length of each segment as its network weight. We shoudl set the parameter weight_attribute='percieved_length'. Segments that are not assigned a percieved_length values, or assigned a value of 0, would default to the geometric length of the segment as weight.
[12]:
cambridge['sidewalks'].gdf['percieved_length'] = cambridge['sidewalks'].gdf['geometry'].length
cambridge['sidewalks'].gdf.at[18, 'percieved_length'] = cambridge['sidewalks'].gdf.at[18, 'percieved_length'] + 50
cambridge.create_street_network(
source_layer="sidewalks",
node_snapping_tolerance=0.1,
weight_attribute='percieved_length',
turn_penalty_amount=75,
turn_threshold_degree = 45,
)
cambridge.insert_node(label='origin', layer_name="one_building", weight_attribute='people')
cambridge.insert_node(label='destination', layer_name="subway")
cambridge.create_graph()
una.betweenness(
cambridge,
search_radius = 300,
detour_ratio=1.15,
decay=True,
decay_method="exponent",
beta=0.001,
turn_penalty=True,
save_betweenness_as="betweenness_percieved_distance"
)
cambridge.create_map(
[
{'layer': 'one_building'},
{'layer': 'subway'},
{'layer': 'sidewalks', 'color_by_attribute': 'betweenness_percieved_distance', 'color_method': 'gradient', 'text': "betweenness_percieved_distance"}
]
)
Time spent: 3s [Done 1 of 1 origins (100.00%)]
[12]:
We notice two things that happened:
Since we added a penalty to the crosswalk, the shortest distance is still following the same path, but is now 50m longer. Because of the detour ratio allowance, A new potential route become plausable.
As the shortest distance increased, total trip generation dropped from 39.2 to 37.3 as a result of the shortest path now feeling longer due to the crosswalk penalty.
Impact of Many Destinations
For now, we’ve artificially limited the number of destinations to only one subway station. Setting destinations to be all possible stations would allow us to see how different destinations compete to attract more trips. In the una.betweenness() function, setting the parameter closest_destination=False, would allow trips to be routed to all destinations, not only the closest one.
[13]:
cambridge.clear_nodes()
cambridge.insert_node(label='origin', layer_name="one_building", weight_attribute='people')
cambridge.insert_node(label='destination', layer_name="subway")
cambridge.create_graph()
una.betweenness(
cambridge,
search_radius = 500,
detour_ratio=1.15,
decay=True,
decay_method="exponent",
beta=0.001,
turn_penalty=True,
closest_destination=False,
save_betweenness_as="betweenness_many_destinations"
)
cambridge.create_map(
[
{'layer': 'one_building'},
{'layer': 'subway'},
{'layer': 'sidewalks', 'color_by_attribute': 'betweenness_many_destinations', 'color_method': 'gradient', 'text': "betweenness_many_destinations"}
]
)
Time spent: 3s [Done 1 of 1 origins (100.00%)]
[13]:
We notice that there are still 37.3 trips are generated. The number of trips generated would only depends on the shortest distance to the closest destination. But we notice now that the northen station only recieved 20.7 trips, while 16.6 are now routed to the southern station. Trips are assigned to destinations based on distance, through a probability that depends on the exponential gravity function that assigns higher probabilities to destinations that are closer to the origin.
Impact of Destination Weights:
The una.betweenness function allows routing trips to competing destinations based on both: the shortest distance to a destination, and a measure of destination attractiveness (A destination weight). Similar to what we did in section 3, lets assign each of the subway stations an attribute called daily trains. Let’s see what happen when the further station is more attractive
[14]:
cambridge.layers['subway'].gdf.at[0, 'daily_trains'] = 20
cambridge.layers['subway'].gdf.at[1, 'daily_trains'] = 50
cambridge.clear_nodes()
cambridge.insert_node(label='origin', layer_name="one_building", weight_attribute='people')
cambridge.insert_node(label='destination', layer_name="subway", weight_attribute='daily_trains')
cambridge.create_graph()
una.betweenness(
cambridge,
search_radius = 500,
detour_ratio=1.15,
decay=True,
decay_method="exponent",
beta=0.001,
turn_penalty=True,
closest_destination=False,
save_betweenness_as="betweenness_destination_weight"
)
cambridge.create_map(
[
{'layer': 'one_building'},
{'layer': 'subway'},
{'layer': 'sidewalks', 'color_by_attribute': 'betweenness_destination_weight', 'color_method': 'gradient', 'text': "betweenness_destination_weight"}
]
)
Time spent: 3s [Done 1 of 1 origins (100.00%)]
[14]:
We see that the building still generates 37.3 trips, but now the northen station recieved 12.4 trips, compared to 20.7 before assigning destination weights. The southern station gained more trips from 16.6 before assigning weights, to 24.9 after assigning it a higher weight.
Impact of Elastic Trip Generation:
FOr many types of destinations, people tend to generate more trips, the more destinations they have access to. This could be a result for a preference for verieity or other behavioral aspects. The una.betweenness algorithm accomidates this through elastic weights: A factor between 0 and 1 that is multiplied by the origin weight to reduce trip generation when access to destinations is deemed low for the destination type. If this factor is equal to 1, the origin would generate trips equal to its
weight. Setting the parameter elastic_weight=True, enables this behavior. Two additional parameters are needed:
knn_weight:knn_plateau:
[15]:
una.betweenness(
cambridge,
search_radius = 500,
detour_ratio=1.15,
decay=True,
decay_method="exponent",
beta=0.001,
turn_penalty=True,
closest_destination=False,
elastic_weight = True,
knn_weight = [0.5, 0.25, 0.25],
knn_plateau = 400,
save_betweenness_as="betweenness_elastic_weight"
)
cambridge.create_map(
[
{'layer': 'one_building'},
{'layer': 'subway'},
{'layer': 'sidewalks', 'color_by_attribute': 'betweenness_elastic_weight', 'color_method': 'gradient', 'text': "betweenness_elastic_weight"}
]
)
Time spent: 4s [Done 1 of 1 origins (100.00%)]
[15]:
Based on these settings, for the origin to achieve its full trip generation, it has to meet these criteria:
Have access to three destinations
These three destinations are within 400m.
This origin had access to only two destinations, so it lost 25% of its trip generation (weight) because it lacked a third destination. It lost a fraction of its trip generation because the second reachable destination is further than 400m. Applying elastic_weight=True with these settings led to a reduction of trip generation from 37.3 to 27.5 for this origin
Impact of Many Origins
At the beginning of this example, we limited our origins to be one building. The una.betweenness function treats every origin independently. if we set the origin to be all buildings, trips from all origins to all destinations would be accumilated onto the network
[16]:
cambridge.clear_nodes()
cambridge.insert_node(label='origin', layer_name="buildings", weight_attribute='people')
cambridge.insert_node(label='destination', layer_name="subway", weight_attribute='daily_trains')
cambridge.create_graph()
una.betweenness(
cambridge,
search_radius = 500,
detour_ratio=1.15,
decay=True,
decay_method="exponent",
beta=0.001,
turn_penalty=True,
closest_destination=False,
elastic_weight = True,
knn_weight = [0.5, 0.25, 0.25],
knn_plateau = 400,
save_betweenness_as="betweenness_all_origins"
)
cambridge.create_map(
[
{'layer': 'buildings'},
{'layer': 'subway'},
{'layer': 'sidewalks', 'color_by_attribute': 'betweenness_all_origins', 'color_method': 'gradient', 'text': "betweenness_all_origins"}
]
)
Time spent: 12s [Done 118 of 118 origins (100.00%)]
[16]:
Given this context, and these specific settings, it is estimated that 534 trips would be made to the northen station, and 1,039 trips to the southern station. Keep in mind that these are only estimates, and should be
Impact of Parallell Processing
The more origins, destinations and network segment there is in a given analysis zone, the slower the betweenness calculations are. It is possible to utilize multiprocessing, utilizing more cores of a computer’s processor to run calculations in parallel. setting the num_cores parameter to the number of cores you want to utilize would speed up the process propotionally to how many cores you allocate. Setting the number to be equal to the maximum available cores would slow down your computer
considerably. If you still need to be able to use your computer, set this parameter to leave one or two cores free for use. To know how many cores available in your computer:
[17]:
import multiprocessing
print(multiprocessing.cpu_count())
8
When we use the full number of available cores, we get a considerable speedup
[18]:
una.betweenness(
cambridge,
search_radius = 500,
detour_ratio=1.15,
decay=True,
decay_method="exponent",
beta=0.001,
turn_penalty=True,
closest_destination=False,
elastic_weight = True,
knn_weight = [0.5, 0.25, 0.25],
knn_plateau = 400,
num_cores=2,
save_betweenness_as="betweenness_all_origins"
)
cambridge.create_map(
[
{'layer': 'buildings'},
{'layer': 'subway'},
{'layer': 'sidewalks', 'color_by_attribute': 'betweenness_all_origins', 'color_method': 'gradient', 'text': "betweenness_all_origins"}
]
)
Time spent: 7s [Done 118 of 118 origins (100.00%)]
[18]:
Notes on performance.
Testing on an personal computer with 8 cores, The running time was 12 seconds on a single core, and 7 seconds when using 2 cores. There is some overhead to prepare data and aggrigate results when doing multiprocessing, so the time savings are not completely linear. The benifit for multiprocessing increases as the number of origins increases, and the number of used cores increas, causing the overhead operations to be shared by a larger number of origins. In a larger urban setting, with a more complicated network and a larger number of origins and destinations, performance becomes very important. Here are a few recommendations to enhance performance:
Use multiprocessing: The more origin you have in your analysis area, the more time savings you get from multiprocessing
Simplify the network: A street centerline has less segments than a sidewalk network. Sidewalk network offer a more realistic representation of the pedestrian trip making experience, but lead to more running times. Consider cleaning out the network to eliminate any unneccissaey segments, or as mentioned in section 2, try to eliminate degree-2 intersections whenever you can.
Consider aggregating origins or destinations: While the current version is capable of handling larger numbers of origins and destinations, in large analysis zones, or in cases where you need to run a larger number of scenarios, consider a spatial resulution that is appropriate for your analysis that doesn’t lead to significiant accuracy cost. For example, consider clustering origins and destinations on the same side of the street segment into fewer pouints by clustering them evry 40 meters for example. Make sure to assign weights that reflect how many points belong to the cluster, and their importance.
Use a resonable search radius and a detour ratio: A high detour ration leads to generating larger numbers of alternative paths. consider setting this parameter to the lowesr possible value to significiantly increase performance. A lower search radius also leads to performance gains. COnsider setting a realistic search radius, especially if decay is enabled. as further destinations are already heavily penalized and constitute a smaller fraction of the trip generation.