pyquity 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pyquity-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,28 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyquity
3
+ Version: 1.0.0
4
+ Summary: PyQuity is a compact Python toolkit for building and analyzing multimodal street and transit networks with a focus on accessibility and distributive equity
5
+ Author: pannawatr
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/pannawatr/pyquity
8
+ Classifier: Programming Language :: Python :: 3.9
9
+ Requires-Python: >=3.9
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: osmnx>=2.0.0
12
+ Requires-Dist: geopandas>=1.0.0
13
+ Requires-Dist: shapely>=2.0.0
14
+ Requires-Dist: networkx>=3.2
15
+ Requires-Dist: numpy>=1.23
16
+ Requires-Dist: pandas>=2.0
17
+ Requires-Dist: scipy>=1.10
18
+ Requires-Dist: matplotlib>=3.7
19
+ Requires-Dist: pyproj>=3.5
20
+
21
+ ## PyQuity
22
+ PyQuity is a compact Python toolkit for building and analyzing multimodal street and transit networks with a focus on accessibility and distributive equity. Quickly generate graphs and grids, attach POIs/GTFS, compute route-based accessibility, and evaluate equity (sufficientarianism, egalitarianism) with seamless GeoPandas/NetworkX integration.
23
+
24
+ ## Installation
25
+ PyQuity can be installed via PyPI:
26
+ ```bash
27
+ pip install pyquity
28
+ ```
@@ -0,0 +1,8 @@
1
+ ## PyQuity
2
+ PyQuity is a compact Python toolkit for building and analyzing multimodal street and transit networks with a focus on accessibility and distributive equity. Quickly generate graphs and grids, attach POIs/GTFS, compute route-based accessibility, and evaluate equity (sufficientarianism, egalitarianism) with seamless GeoPandas/NetworkX integration.
3
+
4
+ ## Installation
5
+ PyQuity can be installed via PyPI:
6
+ ```bash
7
+ pip install pyquity
8
+ ```
@@ -0,0 +1,17 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pyquity"
7
+ version = "1.0.0"
8
+ description = "PyQuity is a compact Python toolkit for building and analyzing multimodal street and transit networks with a focus on accessibility and distributive equity"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name="pannawatr"}]
13
+ classifiers = ["Programming Language :: Python :: 3.9"]
14
+ dependencies = ["osmnx>=2.0.0", "geopandas>=1.0.0", "shapely>=2.0.0", "networkx>=3.2", "numpy>=1.23", "pandas>=2.0", "scipy>=1.10", "matplotlib>=3.7", "pyproj>=3.5"]
15
+
16
+ [project.urls]
17
+ Homepage = "https://github.com/pannawatr/pyquity"
@@ -0,0 +1,4 @@
1
+ from .plot import *
2
+ from .graph import *
3
+ from .utils import *
4
+ from .equity import *
@@ -0,0 +1,125 @@
1
+ import pyquity
2
+ import osmnx as ox
3
+ import numpy as np
4
+ import networkx as nx
5
+
6
+ class Equity:
7
+ def __init__(self, G_walk, G_micromobility, grid, amenity):
8
+ self.G_walk = G_walk
9
+ self.G_micromobility = G_micromobility
10
+ self.grid = grid
11
+ self.amenity = amenity
12
+
13
+ # Ensure amenities are point geometries
14
+ if not all(self.amenity.geometry.geom_type == "Point"):
15
+ self.amenity["geometry"] = self.amenity.geometry.centroid
16
+
17
+ def sufficientarianism(self, served_time: int = 15):
18
+ # Map amenities to nearest nodes and map grid centroids to nearest nodes with micromobility
19
+ self.micromobility_amenity_nodes = ox.distance.nearest_nodes(self.G_micromobility, self.amenity.geometry.centroid.x.values, self.amenity.geometry.centroid.y.values)
20
+ self.micromobility_grid_nodes = ox.distance.nearest_nodes(self.G_micromobility, self.grid.geometry.centroid.x.values, self.grid.geometry.centroid.y.values)
21
+
22
+ # Map amenities to nearest nodes and map grid centroids to nearest nodes with walk
23
+ self.walk_amenity_nodes = ox.distance.nearest_nodes(self.G_walk, self.amenity.geometry.centroid.x.values, self.amenity.geometry.centroid.y.values)
24
+ self.walk_grid_nodes = ox.distance.nearest_nodes(self.G_walk, self.grid.geometry.centroid.x.values, self.grid.geometry.centroid.y.values)
25
+
26
+ # Assign the nearest network node ID to each grid cell based on micromobility count
27
+ self.grid["grid_id"] = np.where(self.grid['micromobility_count'] > 0, self.micromobility_grid_nodes, self.walk_grid_nodes)
28
+ self.grid["served"] = 0
29
+
30
+ # Iterate over each grid row to calculate serviceability
31
+ for idx, grid_row in self.grid.iterrows():
32
+ grid_node = grid_row['grid_id']
33
+
34
+ # Check if the grid has micromobility services or not and select the appropriate graph and nodes
35
+ if grid_row['micromobility_count'] > 0:
36
+ G_current = self.G_micromobility
37
+ amenity_nodes = self.micromobility_amenity_nodes
38
+ else:
39
+ G_current = self.G_walk
40
+ amenity_nodes = self.walk_amenity_nodes
41
+
42
+ # Compute shortest paths from the selected grid node to all other nodes
43
+ paths = nx.single_source_dijkstra_path(G_current, source=grid_node, weight='length', cutoff=served_time * (22 * 1000 / 3600) * 60)
44
+
45
+ # Iterate over each amenity node to check if it's reachable within the time limit
46
+ for amenity_node in amenity_nodes:
47
+ # If this grid node is already served, no need to check further amenities
48
+ if self.grid.loc[self.grid["grid_id"] == grid_node, "served"].values[0] == 1:
49
+ break
50
+
51
+ # Check if the amenity is reachable in the computed paths
52
+ if amenity_node in paths:
53
+ try:
54
+ # Get the route as a list of node IDs and calculate distance and travel time using pyquity
55
+ route = [int(node) for node in paths[int(amenity_node)]]
56
+ distance, travel_time = pyquity.route_length_by_mode(G_current, route)
57
+ total_time = sum(travel_time.values())
58
+
59
+ # If total travel time is within the served_time threshold
60
+ if total_time <= served_time:
61
+ self.grid.loc[self.grid["grid_id"] == grid_node, "served"] = 1
62
+ break
63
+ except:
64
+ continue
65
+
66
+ # Return GeoDataFrame of grid
67
+ return self.grid
68
+
69
+ def egalitarianism(self, grid):
70
+ # Extract 'served' data and compute Lorenz curve and Gini coefficient
71
+ served_data = grid['served'].values
72
+
73
+ # Calculate Lorenz curve and Gini coefficient
74
+ lorenz_curve = self._lorenz_curve(served_data)
75
+ gini_index = self._gini_coefficient(lorenz_curve)
76
+ return gini_index, lorenz_curve
77
+
78
+ def _lorenz_curve(self, data):
79
+ # Calculate Lorenz curve
80
+ sorted_data = np.sort(data)
81
+ lorenz_curve = np.insert((np.cumsum(sorted_data) / np.sum(sorted_data)), 0, 0)
82
+ return lorenz_curve
83
+
84
+ def _gini_coefficient(self, lorenz_curve):
85
+ # Calculate Gini coefficient from Lorenz curve
86
+ gini_index = ((0.5 - (np.sum(lorenz_curve) / (len(lorenz_curve) - 1))) * 2)
87
+ return gini_index
88
+
89
+ def utilitarianism(self, served_time, network_type: str='walk'):
90
+ # Initialize a column to track how many amenities serve each grid cell
91
+ self.grid["count_served"] = 0
92
+
93
+ # Select the correct network, nodes, and grid ID mapping depending on travel mode
94
+ if network_type == 'walk':
95
+ self.grid['grid_id'] = self.walk_grid_nodes
96
+ G_current = self.G_walk
97
+ amenity_nodes = self.walk_amenity_nodes
98
+ elif network_type == 'bike':
99
+ self.grid['grid_id'] = self.micromobility_grid_nodes
100
+ G_current = self.G_micromobility
101
+ amenity_nodes = self.micromobility_amenity_nodes
102
+
103
+ # Iterate over each grid cell
104
+ for idx, grid_row in self.grid.iterrows():
105
+ grid_node = grid_row['grid_id']
106
+
107
+ # Compute shortest paths (Dijkstra) from the grid node to all reachable nodes
108
+ paths = nx.single_source_dijkstra_path(G_current, source=grid_node, weight='length', cutoff=served_time * (22 * 1000 / 3600) * 60)
109
+
110
+ # Check each amenity node to see if it is reachable
111
+ for amenity_node in amenity_nodes:
112
+ if amenity_node in paths:
113
+ try:
114
+ # Get the route as a list of node IDs and calculate distance and travel time using pyquity
115
+ route = [int(node) for node in paths[int(amenity_node)]]
116
+ distance, travel_time = pyquity.route_length_by_mode(G_current, route)
117
+ total_time = sum(travel_time.values())
118
+
119
+ # If total travel time is within the served_time threshold
120
+ if total_time <= served_time:
121
+ self.grid.loc[self.grid["grid_id"] == grid_node, "count_served"] += 1
122
+ except:
123
+ continue
124
+ # Return the updated grid with count_served values
125
+ return self.grid
@@ -0,0 +1,215 @@
1
+ import osmnx as ox
2
+ import numpy as np
3
+ import pandas as pd
4
+ import networkx as nx
5
+ import geopandas as gpd
6
+ import partridge as ptg
7
+ from shapely import ops as sops
8
+ from scipy.spatial import cKDTree
9
+ from geopy.distance import distance
10
+ from shapely.geometry import Point, Polygon, LineString
11
+
12
+ def multimodal_graph(G_osm: nx.MultiDiGraph, G_gtfs: nx.MultiDiGraph, k: int=1):
13
+ # Relabel GTFS nodes with unique integers to avoid ID collisions with OSM graph
14
+ G_gtfs = nx.relabel_nodes(G_gtfs, {node: i for i, node in enumerate(G_gtfs.nodes())}, copy=True)
15
+
16
+ # Get OSM node coordinates
17
+ osm_nodes, _ = ox.graph_to_gdfs(G_osm)
18
+ osm_coords = np.array(list(zip(osm_nodes["y"], osm_nodes["x"])))
19
+
20
+ # Get GTFS stop coordinates
21
+ gtfs_nodes = [(n, data["x"], data["y"]) for n, data in G_gtfs.nodes(data=True)]
22
+ gtfs_coords = np.array([(y, x) for _, x, y in gtfs_nodes])
23
+
24
+ # Build KD-tree for nearest-neighbor search
25
+ tree = cKDTree(osm_coords)
26
+
27
+ # Combine OSM and GTFS graphs
28
+ G = nx.compose(G_osm, G_gtfs)
29
+
30
+ # Connect each GTFS stop to its nearest OSM node(s)
31
+ for (stop_id, x, y), (dist, idx) in zip(gtfs_nodes, zip(*tree.query(gtfs_coords, k=k))):
32
+ osm_node = osm_nodes.iloc[idx].name
33
+ point_u = (y, x)
34
+ point_v = (osm_nodes.iloc[idx].y, osm_nodes.iloc[idx].x)
35
+
36
+ length = distance(point_u, point_v).m
37
+ G.add_edge(stop_id, osm_node, mode="transfer", length=length)
38
+ G.add_edge(osm_node, stop_id, mode="transfer", length=length)
39
+
40
+ # Return the graph
41
+ G.graph["crs"] = "EPSG:4326"
42
+ return G
43
+
44
+ def graph_from_gtfs(gtfs: str) -> nx.MultiDiGraph:
45
+ # Read available service dates
46
+ date = ptg.read_service_ids_by_date(gtfs)
47
+ if not date:
48
+ raise ValueError("No valid service date found in GTFS.")
49
+
50
+ # Select the first available service date
51
+ target_date = sorted(date.keys())[0]
52
+ feed = ptg.load_geo_feed(gtfs, view={'trips.txt': {'service_id': date[target_date]}, 'shapes.txt': {}})
53
+
54
+ # Extract GTFS tables
55
+ stop_times = feed.stop_times
56
+ trips = feed.trips
57
+ stops = feed.stops
58
+ shapes = feed.shapes
59
+
60
+ # Initialize a multidirected graph
61
+ G = nx.MultiDiGraph()
62
+
63
+ # Add each transit stop as a node in the graph
64
+ for _, row in stops.iterrows():
65
+ G.add_node(row['stop_id'], name=row['stop_name'], x=row['geometry'].x, y=row['geometry'].y)
66
+
67
+ # Ensure all shape geometries are LineString
68
+ shapes['geometry'] = shapes['geometry'].apply( lambda geoms: geoms if isinstance(geoms, LineString) else LineString(geoms))
69
+ shapes = shapes.set_index('shape_id').geometry
70
+
71
+ # Create edges from trips, grouped by shape_id
72
+ for shape_id, group in trips.groupby('shape_id'):
73
+ if shape_id not in shapes.index or shapes[shape_id] is None:
74
+ continue
75
+ geoms = shapes[shape_id]
76
+
77
+ # Take the first trip in the group as representative
78
+ trip_id = group.iloc[0]['trip_id']
79
+
80
+ # Get the stop sequence for the trip
81
+ stop_sequence = stop_times[stop_times.trip_id == trip_id].sort_values('stop_sequence')
82
+ stop_id = stop_sequence['stop_id'].tolist()
83
+
84
+ # Add edges between consecutive stops
85
+ for i in range(len(stop_id) - 1):
86
+ u, v = stop_id[i], stop_id[i + 1]
87
+
88
+ # Retrieve the coordinates of the two stops
89
+ point_u = stops.loc[stops.stop_id == u, 'geometry'].values[0]
90
+ point_v = stops.loc[stops.stop_id == v, 'geometry'].values[0]
91
+ length = distance((point_u.y, point_u.x), (point_v.y, point_v.x)).m
92
+
93
+ # Get geometry for the edge
94
+ try:
95
+ orig = geoms.project(point_u)
96
+ dest = geoms.project(point_v)
97
+ low, high = sorted([orig, dest])
98
+ segment = sops.substring(geoms, low, high, normalized=False)
99
+ if segment.is_empty or segment.length == 0:
100
+ segment = LineString([pu, pv])
101
+ except:
102
+ LineString([point_u, point_v])
103
+
104
+ # Add edge only if it does not already exist, and add geometry
105
+ if not G.has_edge(u, v, key=trip_id):
106
+ G.add_edge(
107
+ u, v,
108
+ trip_id=trip_id,
109
+ geometry=segment,
110
+ length=length,
111
+ mode='transit'
112
+ )
113
+
114
+ # Return the graph
115
+ G.graph['crs'] = "EPSG:4326"
116
+ return G
117
+
118
+ def graph_from_place(place_name: str, network_type: str):
119
+ # Return a graph from OSMnx
120
+ G = ox.graph_from_place(place_name, network_type=network_type)
121
+
122
+ # Add mode to each edge in the graph
123
+ for u, v, k, data in G.edges(keys=True, data=True):
124
+ data['mode'] = network_type
125
+
126
+ return G
127
+
128
+ def grid_from_place(place_name: str, grid_size: float) -> gpd.GeoDataFrame:
129
+ # Read boundary from OpenStreetMap(OSM)
130
+ boundary = ox.geocode_to_gdf(place_name)
131
+ boundary = boundary.to_crs(epsg=3857) # Project boundary to Web Mercator(EPSG:3857) from default(EPSG:4326)
132
+ minx, miny, maxx, maxy = boundary.total_bounds
133
+
134
+ # Initialize a grid and Add into GeoDataFrame
135
+ polygons = [Polygon([(x, y), (x + grid_size, y), (x + grid_size, y + grid_size), (x, y + grid_size)]) for x in np.arange(minx, maxx, grid_size) for y in np.arange(miny, maxy, grid_size)]
136
+ gdf = gpd.GeoDataFrame(geometry=polygons, crs=boundary.crs)
137
+ grid = gpd.overlay(gdf, boundary, how='intersection')
138
+
139
+ # Reproject the grid back to WGS84 (EPSG:4326) and return it
140
+ return grid.to_crs(epsg=4326)
141
+
142
+ def amenity_from_place(place_name: str, amenity_type: str) -> gpd.GeoDataFrame:
143
+ # Define categories of amenities with their corresponding OSM tags
144
+ amenity = {
145
+ 'education': ['college', 'dancing_school', 'driving_school', 'first_aid_school', 'kindergarten', 'language_school', 'library', 'surf_school', 'toy_library', 'research_institute', 'training', 'music_school', 'school', 'traffic_park', 'university'],
146
+ 'financial': ['atm', 'bank', 'bureau_de_change'],
147
+ 'healthcare': ['baby_hatch', 'clinic', 'dentist', 'doctors', 'hospital', 'nursing_home', 'pharmacy', 'social_facility', 'veterinary' ]
148
+ }
149
+
150
+ # Get the list of amenity tags for the selected amenity type
151
+ amenity = amenity[amenity_type.lower()]
152
+
153
+ # Query OpenStreetMap for features matching the selected amenity tags within the specified place
154
+ gdf = ox.features.features_from_place(place_name, tags={'amenity': amenity})
155
+
156
+ # Return the resulting GeoDataFrame
157
+ return gdf
158
+
159
+ def amenity_in_grid(grid: gpd.GeoDataFrame, amenity: gpd.GeoDataFrame) -> gpd.GeoDataFrame:
160
+ # Reproject amenities to Web Mercator (EPSG:3857) and convert geometry to centroids (points)
161
+ amenity = amenity.to_crs(epsg=3857)
162
+ amenity['geometry'] = amenity.geometry.centroid
163
+
164
+ # Reproject grid to Web Mercator
165
+ grid = grid.to_crs(epsg=3857)
166
+
167
+ # Ensure both layers have the same CRS before spatial operations
168
+ if grid.crs == amenity.crs:
169
+ grid['amenity_count'] = 0 # Initialize 'amenity_count' column in the grid
170
+
171
+ # Count amenities within each grid cell
172
+ for index, x in grid.iterrows():
173
+ for _, y in amenity.iterrows():
174
+ if x.geometry.contains(y.geometry):
175
+ grid.at[index, 'amenity_count'] += 1
176
+
177
+ # Reproject the grid back to WGS84 (EPSG:4326) and return it
178
+ return grid.to_crs(epsg=4326)
179
+
180
+ def micromobility_in_grid(grid: gpd.GeoDataFrame, amenity: gpd.GeoDataFrame, micromobility_size: int, alpha: float = 1.0) -> gpd.GeoDataFrame:
181
+ # If amenity counts are not present, initialize counts and return zeros
182
+ if 'amenity_count' not in grid.columns:
183
+ grid['micromobility_count'] = 0
184
+ return grid
185
+
186
+ # Prepare counts and ensure numeric
187
+ counts = grid['amenity_count'].fillna(0).astype(float)
188
+
189
+ # Compute weights: raise counts to the power of alpha to control emphasis
190
+ weights = counts ** alpha
191
+
192
+ total_weight = weights.sum()
193
+ # If there are no amenities at all, assign zero everywhere
194
+ if total_weight == 0 or micromobility_size <= 0:
195
+ grid['micromobility_count'] = 0
196
+ return grid
197
+
198
+ # Normalize weights to form a probability distribution
199
+ probs = weights / total_weight
200
+
201
+ # Expected (real-valued) allocation per cell
202
+ expected = probs * micromobility_size
203
+
204
+ # Assign integer allocations by flooring expected values
205
+ grid['micromobility_count'] = np.floor(expected).astype(int)
206
+
207
+ # Distribute the remaining units by largest fractional parts (prioritizes high-amenity cells)
208
+ remainder = int(micromobility_size - grid['micromobility_count'].sum())
209
+ if remainder > 0:
210
+ fractional = expected - np.floor(expected)
211
+ indices = fractional.sort_values(ascending=False).index[:remainder]
212
+ for idx in indices:
213
+ grid.at[idx, 'micromobility_count'] += 1
214
+
215
+ return grid
@@ -0,0 +1,40 @@
1
+ import osmnx as ox
2
+ import networkx as nx
3
+ import matplotlib.pyplot as plt
4
+
5
+ def plot_grid(grid, ax=None, linewidth: float=0.5, figsize: tuple[float, float]=(10, 10), legend: bool=True, show: bool=True, **kwargs):
6
+ # Create a new figure and axes if none provided
7
+ if ax is None:
8
+ fig, ax = plt.subplots(figsize=figsize)
9
+
10
+ # Plot the GeoDataFrame on the axes
11
+ ax = grid.plot(ax=ax, edgecolor='k', color='white', linewidth=linewidth, legend=legend, **kwargs)
12
+
13
+ # Display the plot if requested
14
+ if show:
15
+ plt.show()
16
+
17
+ return ax
18
+
19
+ def plot_graph_route_by_mode(G, route, ax=None, show: bool=True, legend: bool=True, node_size: float=0, edge_color: str='lightgray'):
20
+ mode_colors = {'walk': 'green', 'transit': 'red', 'bike': 'blue', 'transfer': 'yellow'}
21
+
22
+ # Convert the route into a GeoDataFrame (one row per edge) using OSMnx
23
+ gdf = ox.routing.route_to_gdf(G, route)
24
+
25
+ # Create a new figure and axes if none provided
26
+ if ax is None:
27
+ fig, ax = ox.plot_graph(G, show=False, close=False, node_size=node_size, edge_color=edge_color)
28
+
29
+ # Plot each segment of the route, color-coded by its mode
30
+ for mode, data in gdf.groupby('mode'):
31
+ color = mode_colors.get(mode, 'gray')
32
+ data.plot(ax=ax, linewidth=3.5, edgecolor=color, label=mode)
33
+
34
+ # Show the legend and plot if requested
35
+ if show:
36
+ plt.show()
37
+ if legend:
38
+ ax.legend()
39
+
40
+ return ax
@@ -0,0 +1,28 @@
1
+ import osmnx as ox
2
+ import geopandas as gpd
3
+
4
+ def route_length_by_mode(G, route, verbose: bool = True):
5
+ # Convert route to GeoDataFrame and project to metric CRS
6
+ gdf = ox.routing.route_to_gdf(G, route)
7
+ gdf = gdf.to_crs(epsg=3857)
8
+
9
+ # Calculate total distance by mode (km)
10
+ distance = gdf.groupby('mode')['length'].sum() / 1000
11
+ distance = round(distance, 2).to_dict()
12
+
13
+ # Average speeds by mode (m/s)
14
+ speed = {
15
+ 'walk': 5 * 1000 / 3600,
16
+ 'bike': 20 * 1000 / 3600,
17
+ 'transfer': 5 * 1000 / 3600,
18
+ 'transit': 22 * 1000 / 3600
19
+ }
20
+
21
+ # Calculate travel time by mode (minutes)
22
+ travel_time = {}
23
+ for mode, dist_km in distance.items():
24
+ if mode in speed:
25
+ time_sec = (dist_km * 1000) / speed[mode]
26
+ travel_time[mode] = round(time_sec / 60, 2)
27
+
28
+ return distance, travel_time
@@ -0,0 +1,28 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyquity
3
+ Version: 1.0.0
4
+ Summary: PyQuity is a compact Python toolkit for building and analyzing multimodal street and transit networks with a focus on accessibility and distributive equity
5
+ Author: pannawatr
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/pannawatr/pyquity
8
+ Classifier: Programming Language :: Python :: 3.9
9
+ Requires-Python: >=3.9
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: osmnx>=2.0.0
12
+ Requires-Dist: geopandas>=1.0.0
13
+ Requires-Dist: shapely>=2.0.0
14
+ Requires-Dist: networkx>=3.2
15
+ Requires-Dist: numpy>=1.23
16
+ Requires-Dist: pandas>=2.0
17
+ Requires-Dist: scipy>=1.10
18
+ Requires-Dist: matplotlib>=3.7
19
+ Requires-Dist: pyproj>=3.5
20
+
21
+ ## PyQuity
22
+ PyQuity is a compact Python toolkit for building and analyzing multimodal street and transit networks with a focus on accessibility and distributive equity. Quickly generate graphs and grids, attach POIs/GTFS, compute route-based accessibility, and evaluate equity (sufficientarianism, egalitarianism) with seamless GeoPandas/NetworkX integration.
23
+
24
+ ## Installation
25
+ PyQuity can be installed via PyPI:
26
+ ```bash
27
+ pip install pyquity
28
+ ```
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ pyquity/__init__.py
4
+ pyquity/equity.py
5
+ pyquity/graph.py
6
+ pyquity/plot.py
7
+ pyquity/utils.py
8
+ pyquity.egg-info/PKG-INFO
9
+ pyquity.egg-info/SOURCES.txt
10
+ pyquity.egg-info/dependency_links.txt
11
+ pyquity.egg-info/requires.txt
12
+ pyquity.egg-info/top_level.txt
@@ -0,0 +1,9 @@
1
+ osmnx>=2.0.0
2
+ geopandas>=1.0.0
3
+ shapely>=2.0.0
4
+ networkx>=3.2
5
+ numpy>=1.23
6
+ pandas>=2.0
7
+ scipy>=1.10
8
+ matplotlib>=3.7
9
+ pyproj>=3.5
@@ -0,0 +1 @@
1
+ pyquity
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+