sameer-graph-lib 0.2.0__tar.gz → 0.3.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.
- sameer_graph_lib-0.3.0/PKG-INFO +376 -0
- sameer_graph_lib-0.3.0/README.md +329 -0
- {sameer_graph_lib-0.2.0 → sameer_graph_lib-0.3.0}/pyproject.toml +45 -43
- sameer_graph_lib-0.3.0/src/sameer_graph_lib/__init__.py +130 -0
- sameer_graph_lib-0.3.0/src/sameer_graph_lib/plotter.py +780 -0
- sameer_graph_lib-0.3.0/src/sameer_graph_lib/route_explorer.py +318 -0
- sameer_graph_lib-0.3.0/src/sameer_graph_lib/route_graph.py +1019 -0
- sameer_graph_lib-0.3.0/src/sameer_graph_lib/route_viz.py +777 -0
- sameer_graph_lib-0.3.0/src/sameer_graph_lib.egg-info/PKG-INFO +376 -0
- {sameer_graph_lib-0.2.0 → sameer_graph_lib-0.3.0}/src/sameer_graph_lib.egg-info/SOURCES.txt +7 -0
- {sameer_graph_lib-0.2.0 → sameer_graph_lib-0.3.0}/src/sameer_graph_lib.egg-info/requires.txt +11 -0
- sameer_graph_lib-0.3.0/tests/test_plotter.py +169 -0
- sameer_graph_lib-0.3.0/tests/test_route_graph.py +246 -0
- sameer_graph_lib-0.3.0/tests/test_route_viz.py +224 -0
- sameer_graph_lib-0.2.0/PKG-INFO +0 -201
- sameer_graph_lib-0.2.0/README.md +0 -163
- sameer_graph_lib-0.2.0/src/sameer_graph_lib/__init__.py +0 -53
- sameer_graph_lib-0.2.0/src/sameer_graph_lib.egg-info/PKG-INFO +0 -201
- {sameer_graph_lib-0.2.0 → sameer_graph_lib-0.3.0}/LICENSE +0 -0
- {sameer_graph_lib-0.2.0 → sameer_graph_lib-0.3.0}/setup.cfg +0 -0
- {sameer_graph_lib-0.2.0 → sameer_graph_lib-0.3.0}/src/sameer_graph_lib/_h3.py +0 -0
- {sameer_graph_lib-0.2.0 → sameer_graph_lib-0.3.0}/src/sameer_graph_lib/affinity_graph.py +0 -0
- {sameer_graph_lib-0.2.0 → sameer_graph_lib-0.3.0}/src/sameer_graph_lib/corridor_extractor.py +0 -0
- {sameer_graph_lib-0.2.0 → sameer_graph_lib-0.3.0}/src/sameer_graph_lib/geometry.py +0 -0
- {sameer_graph_lib-0.2.0 → sameer_graph_lib-0.3.0}/src/sameer_graph_lib/hex_graph.py +0 -0
- {sameer_graph_lib-0.2.0 → sameer_graph_lib-0.3.0}/src/sameer_graph_lib/motion.py +0 -0
- {sameer_graph_lib-0.2.0 → sameer_graph_lib-0.3.0}/src/sameer_graph_lib/plotting.py +0 -0
- {sameer_graph_lib-0.2.0 → sameer_graph_lib-0.3.0}/src/sameer_graph_lib/plylinedecoding.py +0 -0
- {sameer_graph_lib-0.2.0 → sameer_graph_lib-0.3.0}/src/sameer_graph_lib/spatial_ingestor.py +0 -0
- {sameer_graph_lib-0.2.0 → sameer_graph_lib-0.3.0}/src/sameer_graph_lib/topology_analyzer.py +0 -0
- {sameer_graph_lib-0.2.0 → sameer_graph_lib-0.3.0}/src/sameer_graph_lib.egg-info/dependency_links.txt +0 -0
- {sameer_graph_lib-0.2.0 → sameer_graph_lib-0.3.0}/src/sameer_graph_lib.egg-info/top_level.txt +0 -0
- {sameer_graph_lib-0.2.0 → sameer_graph_lib-0.3.0}/tests/test_geometry.py +0 -0
- {sameer_graph_lib-0.2.0 → sameer_graph_lib-0.3.0}/tests/test_h3_cell_plotting.py +0 -0
- {sameer_graph_lib-0.2.0 → sameer_graph_lib-0.3.0}/tests/test_h3_cells_map_plotting.py +0 -0
- {sameer_graph_lib-0.2.0 → sameer_graph_lib-0.3.0}/tests/test_h3_grid_distance.py +0 -0
- {sameer_graph_lib-0.2.0 → sameer_graph_lib-0.3.0}/tests/test_plotting.py +0 -0
- {sameer_graph_lib-0.2.0 → sameer_graph_lib-0.3.0}/tests/test_smoke.py +0 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sameer-graph-lib
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: H3 and NetworkX based route affinity, route-flow and plotting toolkit.
|
|
5
|
+
Author-email: Sameer <sameerkumarroy073@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/iams31/sameer_graph_lib
|
|
8
|
+
Project-URL: Repository, https://github.com/iams31/sameer_graph_lib
|
|
9
|
+
Project-URL: Issues, https://github.com/iams31/sameer_graph_lib/issues
|
|
10
|
+
Keywords: h3,networkx,geospatial,routing,graph,visualization,pandas
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: h3>=4.0.0
|
|
22
|
+
Requires-Dist: networkx>=3.0
|
|
23
|
+
Requires-Dist: polyline>=2.0
|
|
24
|
+
Provides-Extra: plot
|
|
25
|
+
Requires-Dist: matplotlib>=3.7; extra == "plot"
|
|
26
|
+
Provides-Extra: geo
|
|
27
|
+
Requires-Dist: contextily>=1.5; extra == "geo"
|
|
28
|
+
Requires-Dist: geopandas>=0.14; extra == "geo"
|
|
29
|
+
Requires-Dist: shapely>=2.0; extra == "geo"
|
|
30
|
+
Provides-Extra: motion
|
|
31
|
+
Requires-Dist: numpy>=1.24; extra == "motion"
|
|
32
|
+
Requires-Dist: pyproj>=3.5; extra == "motion"
|
|
33
|
+
Provides-Extra: route
|
|
34
|
+
Requires-Dist: pandas>=2.0; extra == "route"
|
|
35
|
+
Requires-Dist: numpy>=1.24; extra == "route"
|
|
36
|
+
Requires-Dist: xarray>=2023.1; extra == "route"
|
|
37
|
+
Provides-Extra: analysis
|
|
38
|
+
Requires-Dist: pandas>=2.0; extra == "analysis"
|
|
39
|
+
Requires-Dist: numpy>=1.24; extra == "analysis"
|
|
40
|
+
Requires-Dist: xarray>=2023.1; extra == "analysis"
|
|
41
|
+
Requires-Dist: matplotlib>=3.7; extra == "analysis"
|
|
42
|
+
Provides-Extra: dev
|
|
43
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
44
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
45
|
+
Requires-Dist: twine>=5.1; extra == "dev"
|
|
46
|
+
Dynamic: license-file
|
|
47
|
+
|
|
48
|
+
# sameer-graph-lib
|
|
49
|
+
|
|
50
|
+
`sameer-graph-lib` is an editable Python library for building H3-based route affinity graphs with NetworkX.
|
|
51
|
+
|
|
52
|
+
It turns H3 arrays, latitude/longitude sequences, and encoded polylines into connected hex chains, inserts them into a weighted graph, extracts high-affinity corridors, and decomposes the graph into a main trunk plus minor branches.
|
|
53
|
+
|
|
54
|
+
## Editable install
|
|
55
|
+
|
|
56
|
+
```powershell
|
|
57
|
+
python -m pip install -e ".[dev,plot]"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Because the install is editable, changes you make inside `src/sameer_graph_lib` are picked up immediately by Python without reinstalling.
|
|
61
|
+
|
|
62
|
+
If your machine uses `uv`, run commands through the managed environment:
|
|
63
|
+
|
|
64
|
+
```powershell
|
|
65
|
+
uv run --extra dev --extra plot python -c "import sameer_graph_lib; print(sameer_graph_lib.__version__)"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Install
|
|
69
|
+
|
|
70
|
+
From PyPI after publication:
|
|
71
|
+
|
|
72
|
+
```powershell
|
|
73
|
+
pip install sameer-graph-lib
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
With optional plotting and geospatial extras:
|
|
77
|
+
|
|
78
|
+
```powershell
|
|
79
|
+
pip install "sameer-graph-lib[plot,geo]"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
For the route flow graph and the DataFrame plotter (pandas, numpy, xarray, matplotlib):
|
|
83
|
+
|
|
84
|
+
```powershell
|
|
85
|
+
pip install "sameer-graph-lib[analysis]"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Quick start
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from sameer_graph_lib import HexGraph
|
|
92
|
+
|
|
93
|
+
graph = HexGraph(hex_resolution=9)
|
|
94
|
+
|
|
95
|
+
route = graph.add_latlng_sequence([
|
|
96
|
+
(12.9716, 77.5946),
|
|
97
|
+
(12.9760, 77.5990),
|
|
98
|
+
])
|
|
99
|
+
|
|
100
|
+
print(graph.get_graph_stats())
|
|
101
|
+
selected = graph.get_appropriate_hexes(cutoff=0.8)
|
|
102
|
+
fig = graph.visualize_graph(title="80% compact cluster", highlight_hexes=selected)
|
|
103
|
+
print(graph.decompose_branches())
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Main APIs
|
|
107
|
+
|
|
108
|
+
- `SpatialIngestor`: converts H3 arrays, lat/lng sequences, and encoded polylines into contiguous H3 chains.
|
|
109
|
+
- `AffinityGraph`: NetworkX wrapper for array-based insertion, nearest attachment, affinity scoring, editing, and JSON persistence.
|
|
110
|
+
- `CorridorExtractor`: uses exact all-node Dijkstra selection to extract the most compact cluster covering a target percentage of graph traversal volume.
|
|
111
|
+
- `TopologyAnalyzer`: separates the main branch from residual minor branches.
|
|
112
|
+
- `HexGraph`: backwards-compatible convenience class for your original code style.
|
|
113
|
+
- `RouteSchema` / `RouteTensor` / `RouteGraph`: grain-aware pickup -> drop graph where each edge carries a metric cube (see [Route flow graphs](#route-flow-graphs-pickup---drop)).
|
|
114
|
+
- `RouteExplorer`: the one-liner front door to the route graph - build from a DataFrame, rank partners, and plot the flow around a cluster.
|
|
115
|
+
- `plotter`: stateless DataFrame plotting - multi-column, multi-axis, multi-group, plus distribution checks.
|
|
116
|
+
|
|
117
|
+
Graph creation follows the original per-node procedure: H3 arrays are normalized, then each hex is inserted with `add_node`/`add_hex`. Lat/lng sequences and encoded polylines are first converted into H3 arrays at the requested resolution, then inserted the same way.
|
|
118
|
+
|
|
119
|
+
For QC, use:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
fig = graph.visualize_graph(highlight_hexes=selected)
|
|
123
|
+
fig.savefig("graph_qc.png", dpi=150, bbox_inches="tight")
|
|
124
|
+
|
|
125
|
+
fig = graph.visualize_step_by_step(route[:5], labels=["A", "B", "C", "D", "E"])
|
|
126
|
+
fig.savefig("insertion_steps.png", dpi=150, bbox_inches="tight")
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
To plot actual H3 hex boundaries as geospatial polygons:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from sameer_graph_lib import plot_h3_cells, plot_h3_cells_map
|
|
133
|
+
|
|
134
|
+
fig = plot_h3_cells("88618c4f29fffff", label_full_hex=True)
|
|
135
|
+
fig.savefig("single_h3_cell.png", dpi=150, bbox_inches="tight")
|
|
136
|
+
|
|
137
|
+
fig = graph.plot_h3_cells(highlight_hexes=selected, show_labels=False)
|
|
138
|
+
fig.savefig("h3_cell_footprint.png", dpi=150, bbox_inches="tight")
|
|
139
|
+
|
|
140
|
+
fig = plot_h3_cells_map(route, selected_cells=selected)
|
|
141
|
+
fig.savefig("h3_cell_basemap.png", dpi=150, bbox_inches="tight")
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`plot_h3_cells_map` uses GeoPandas + Contextily. Install it with:
|
|
145
|
+
|
|
146
|
+
```powershell
|
|
147
|
+
python -m pip install -e ".[plot,geo]"
|
|
148
|
+
uv run --extra plot --extra geo python -c "from sameer_graph_lib import plot_h3_cells_map"
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
To get H3 centers and a convex hull:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
from sameer_graph_lib import getLatLng, h3_convex_hull
|
|
155
|
+
|
|
156
|
+
points = getLatLng(route) # [(lat, lng), ...]
|
|
157
|
+
hull = h3_convex_hull(route) # Shapely geometry in (lng, lat)
|
|
158
|
+
graph_hull = graph.convex_hull() # Same, using graph nodes
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Route flow graphs (pickup -> drop)
|
|
162
|
+
|
|
163
|
+
The second half of the library works on tabular route data rather than H3
|
|
164
|
+
chains: one row per `pickup_cluster x drop_cluster x week_period x hour`, with
|
|
165
|
+
whatever metric columns you export. Every edge stores a **tensor** - a dense
|
|
166
|
+
cube over the grain dimensions - so sums stay exact, averages stay correctly
|
|
167
|
+
weighted, and two tensors can be merged without re-reading the source rows.
|
|
168
|
+
|
|
169
|
+
The metric columns are **yours**: the schema reads whatever numeric columns the
|
|
170
|
+
frame has. Names that look like averages (`avg_`, `_rate`, `pct`, `ratio`, ...)
|
|
171
|
+
are weighted-averaged, the rest are summed, and grain/id/categorical columns are
|
|
172
|
+
never treated as metrics. Pass `sum_metrics=` / `mean_metrics=` to override the
|
|
173
|
+
split, and `metric=` anywhere to choose the variable; with nothing named, calls
|
|
174
|
+
fall back to `schema.default_metric` (the first sum metric).
|
|
175
|
+
|
|
176
|
+
Install the extra:
|
|
177
|
+
|
|
178
|
+
```powershell
|
|
179
|
+
python -m pip install -e ".[analysis]" # pandas, numpy, xarray, matplotlib
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Quick start
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
from sameer_graph_lib import RouteExplorer
|
|
186
|
+
|
|
187
|
+
ex = RouteExplorer(df, metric="orders") # metric= is optional
|
|
188
|
+
|
|
189
|
+
ex.sources("A1", top=5) # where A1 orders come from
|
|
190
|
+
ex.drops("A1", top=5) # where A1 orders go
|
|
191
|
+
ex.route("A1", "B1") # every metric for one route
|
|
192
|
+
ex.summary("A1") # every metric for one cluster
|
|
193
|
+
print(ex.describe())
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`where` and `using` return a new view that shares the same graph, so you can
|
|
197
|
+
narrow the question without rebuilding anything:
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
peak = ex.where(week_period="weekday", hour=[8, 9, 10]).using("speed")
|
|
201
|
+
peak.drops("A1", top=5)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Plotting the flow around a cluster
|
|
205
|
+
|
|
206
|
+
`plot` puts the focus clusters in the middle, the clusters that feed them on
|
|
207
|
+
the left, and the clusters they feed on the right. The per-level top-k is the
|
|
208
|
+
main control: pass an int for one level, or a list for one number per level.
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
# top 5 sources, then the top 3 sources of each of those, then the top 2;
|
|
212
|
+
# and on the other side, the top 4 drops and the top 2 of each.
|
|
213
|
+
fig = ex.plot("A1", upstream=[5, 3, 2], downstream=[4, 2])
|
|
214
|
+
|
|
215
|
+
# several focus clusters, plus the routes between the selected set
|
|
216
|
+
ex.plot(["A1", "C1"], upstream=3, downstream=3, include_cross_edges=True)
|
|
217
|
+
|
|
218
|
+
# rank by one variable, show several: every node and edge carries a table
|
|
219
|
+
ex.plot("A1", metric="orders",
|
|
220
|
+
node_metrics=["orders", "accepted_orders", "requests", "speed"],
|
|
221
|
+
edge_metrics=["orders", "speed", "avg_distance"],
|
|
222
|
+
hour=[8, 9, 10])
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
`node_metrics` and `edge_metrics` take any number of variables. They are drawn
|
|
226
|
+
as a small aligned table under each node and on each edge, with the metric
|
|
227
|
+
names on the left and the values right-aligned; every table in one figure
|
|
228
|
+
shares its column widths, and the figure sizes itself so they do not collide.
|
|
229
|
+
Numbers are formatted per value by default (62,709 and 0.273 both read
|
|
230
|
+
correctly); pass `value_format="{:,.1f}"` for one format, or a dict such as
|
|
231
|
+
`value_format={"speed": "{:.3f}"}` per metric.
|
|
232
|
+
|
|
233
|
+
Other options: `size_metric=` (which variable drives node size),
|
|
234
|
+
`show_metric_names=False` (values only), `per_parent=False` (rank a whole level
|
|
235
|
+
globally instead of per parent), `min_value=`, `layout="layered" | "geo" |
|
|
236
|
+
"spring"` (`geo` places H3 cluster ids at their real coordinates),
|
|
237
|
+
`show_edge_values`, `label_cross_edges`, `curve`, `figsize`, and the colours.
|
|
238
|
+
|
|
239
|
+
The same expansion is available without plotting:
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
ex.flow("A1", upstream=[5, 3], downstream=4) # tidy DataFrame
|
|
243
|
+
ex.subgraph("A1", upstream=2, downstream=2) # nx.DiGraph, levels on nodes
|
|
244
|
+
print(ex.tree("A1", upstream=[5, 3], downstream=4))
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
```text
|
|
248
|
+
A1 (orders)
|
|
249
|
+
sources (orders coming in)
|
|
250
|
+
|-- <- C1 [8,523.4]
|
|
251
|
+
| |-- <- D3 [8,802.4]
|
|
252
|
+
| +-- <- D2 [3,646.5]
|
|
253
|
+
+-- <- C3 [7,041.1]
|
|
254
|
+
drops (orders going out)
|
|
255
|
+
|-- -> B4 [4,574.9]
|
|
256
|
+
+-- -> B5 [4,446.4]
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Other route views
|
|
260
|
+
|
|
261
|
+
| Call | What you get |
|
|
262
|
+
| --- | --- |
|
|
263
|
+
| `ex.plot_partners("A1", top=10)` | top sources and top drops as back-to-back bars on one shared scale |
|
|
264
|
+
| `ex.plot_profile(cluster="A1")` | week_period x hour heatmap for a cluster or a route |
|
|
265
|
+
| `ex.plot_matrix(top=15)` | pickup x drop heatmap |
|
|
266
|
+
| `ex.plot_graph(top_routes=30)` | the whole graph as a ranked ring, sized and coloured by a metric |
|
|
267
|
+
| `ex.clusters_frame()` | per cluster inbound vs outbound volume and net balance |
|
|
268
|
+
| `ex.top_routes(20)` | routes ranked by any metric |
|
|
269
|
+
| `ex.matrix()` / `ex.frame()` | pivot table / long per-bucket frame |
|
|
270
|
+
|
|
271
|
+
The lower-level classes are public too: `RouteSchema` (grains and metric
|
|
272
|
+
columns), `RouteTensor` (one route's metric cube, with `summary`, `totals`,
|
|
273
|
+
`profile`, `merge`) and `RouteGraph` (the NetworkX graph, with `partners`,
|
|
274
|
+
`node_tensor`, `shortest_route`, `flow_subgraph`, `as_dataset`).
|
|
275
|
+
|
|
276
|
+
## DataFrame plotting
|
|
277
|
+
|
|
278
|
+
`sameer_graph_lib.plotter` is a stateless plotting layer for any DataFrame -
|
|
279
|
+
each function takes a frame and returns a figure.
|
|
280
|
+
|
|
281
|
+
```python
|
|
282
|
+
from sameer_graph_lib import plotter
|
|
283
|
+
|
|
284
|
+
# n columns, one panel each, test vs control overlaid in every panel
|
|
285
|
+
plotter.plot_columns(df, ["orders", "speed", "aor"], x="hour", group="variant",
|
|
286
|
+
agg="mean", kind="line", mode="grid")
|
|
287
|
+
|
|
288
|
+
# columns with different scales on one chart, one y-axis each
|
|
289
|
+
plotter.plot_multi_axis(df, ["orders", "speed"], x="hour", agg="mean")
|
|
290
|
+
|
|
291
|
+
# distribution of a column, with count/mean/median/sd/skew in the corner
|
|
292
|
+
plotter.plot_distribution(df, "speed", kind="hist", bins=40)
|
|
293
|
+
|
|
294
|
+
# the same distribution per group, one colour per line, on one chart
|
|
295
|
+
plotter.compare_distributions(df, "speed", group="variant", kind="kde", with_box=True)
|
|
296
|
+
|
|
297
|
+
# A/B readout: aggregate per group, optionally relative to a baseline
|
|
298
|
+
plotter.compare_groups(df, ["orders", "speed"], group="variant", agg="mean",
|
|
299
|
+
normalize_to="control")
|
|
300
|
+
|
|
301
|
+
plotter.describe_distribution(df, "speed", group="variant") # stats table
|
|
302
|
+
plotter.plot_correlation(df, ["orders", "speed", "distance"])
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
`mode` is `grid` (one panel per column), `overlay` (one shared axes) or `twin`
|
|
306
|
+
(one y-axis per column). `kind` is `line`, `step`, `area`, `bar`, `barh`,
|
|
307
|
+
`scatter`, `hist`, `kde`, `box`, `violin` or `ecdf`. `group` splits every
|
|
308
|
+
series by a categorical column and gives each level its own colour. KDE and
|
|
309
|
+
ECDF are computed with numpy, so scipy is not required.
|
|
310
|
+
|
|
311
|
+
The explorer exposes the same helpers over its own long frame:
|
|
312
|
+
|
|
313
|
+
```python
|
|
314
|
+
ex.plot_columns(["orders", "speed"], x="hour", group="week_period", agg="mean")
|
|
315
|
+
ex.compare_distributions("speed", "week_period")
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
## Useful commands
|
|
319
|
+
|
|
320
|
+
```powershell
|
|
321
|
+
python -m pytest
|
|
322
|
+
python -m build
|
|
323
|
+
uv run --extra dev pytest -q
|
|
324
|
+
uv run --extra dev python -m build
|
|
325
|
+
uv run --extra dev python -m twine check dist/*
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Build artifacts will appear in `dist/` after `python -m build`.
|
|
329
|
+
|
|
330
|
+
## Publish To PyPI
|
|
331
|
+
|
|
332
|
+
1. Build the package:
|
|
333
|
+
|
|
334
|
+
```powershell
|
|
335
|
+
uv run --extra dev python -m build
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
2. Validate the package metadata:
|
|
339
|
+
|
|
340
|
+
```powershell
|
|
341
|
+
uv run --extra dev python -m twine check dist/*
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
3. Upload to PyPI:
|
|
345
|
+
|
|
346
|
+
```powershell
|
|
347
|
+
uv run --extra dev python -m twine upload dist/*
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
After upload, users can install it with:
|
|
351
|
+
|
|
352
|
+
```powershell
|
|
353
|
+
pip install sameer-graph-lib
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
## Publish From GitHub
|
|
357
|
+
|
|
358
|
+
This repo also includes a Trusted Publishing workflow in
|
|
359
|
+
[.github/workflows/publish.yml](C:/Users/rrran/Desktop/sameer_graph_lib/.github/workflows/publish.yml:1).
|
|
360
|
+
|
|
361
|
+
To finish that setup:
|
|
362
|
+
|
|
363
|
+
1. Create the project on PyPI, or reserve the name `sameer-graph-lib`.
|
|
364
|
+
2. On PyPI, open the project settings and add a Trusted Publisher for:
|
|
365
|
+
`owner`: `iams31`
|
|
366
|
+
`repository`: `sameer_graph_lib`
|
|
367
|
+
`workflow`: `publish.yml`
|
|
368
|
+
`environment`: `pypi`
|
|
369
|
+
3. Create a GitHub Release, or run the workflow manually from the Actions tab.
|
|
370
|
+
|
|
371
|
+
After that, GitHub Actions can publish without storing a long-lived PyPI token.
|
|
372
|
+
|
|
373
|
+
Official references:
|
|
374
|
+
|
|
375
|
+
- PyPI Trusted Publishing: https://docs.pypi.org/trusted-publishers/
|
|
376
|
+
- Packaging guide upload flow: https://packaging.python.org/tutorials/packaging-projects/
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
# sameer-graph-lib
|
|
2
|
+
|
|
3
|
+
`sameer-graph-lib` is an editable Python library for building H3-based route affinity graphs with NetworkX.
|
|
4
|
+
|
|
5
|
+
It turns H3 arrays, latitude/longitude sequences, and encoded polylines into connected hex chains, inserts them into a weighted graph, extracts high-affinity corridors, and decomposes the graph into a main trunk plus minor branches.
|
|
6
|
+
|
|
7
|
+
## Editable install
|
|
8
|
+
|
|
9
|
+
```powershell
|
|
10
|
+
python -m pip install -e ".[dev,plot]"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Because the install is editable, changes you make inside `src/sameer_graph_lib` are picked up immediately by Python without reinstalling.
|
|
14
|
+
|
|
15
|
+
If your machine uses `uv`, run commands through the managed environment:
|
|
16
|
+
|
|
17
|
+
```powershell
|
|
18
|
+
uv run --extra dev --extra plot python -c "import sameer_graph_lib; print(sameer_graph_lib.__version__)"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
From PyPI after publication:
|
|
24
|
+
|
|
25
|
+
```powershell
|
|
26
|
+
pip install sameer-graph-lib
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
With optional plotting and geospatial extras:
|
|
30
|
+
|
|
31
|
+
```powershell
|
|
32
|
+
pip install "sameer-graph-lib[plot,geo]"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
For the route flow graph and the DataFrame plotter (pandas, numpy, xarray, matplotlib):
|
|
36
|
+
|
|
37
|
+
```powershell
|
|
38
|
+
pip install "sameer-graph-lib[analysis]"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from sameer_graph_lib import HexGraph
|
|
45
|
+
|
|
46
|
+
graph = HexGraph(hex_resolution=9)
|
|
47
|
+
|
|
48
|
+
route = graph.add_latlng_sequence([
|
|
49
|
+
(12.9716, 77.5946),
|
|
50
|
+
(12.9760, 77.5990),
|
|
51
|
+
])
|
|
52
|
+
|
|
53
|
+
print(graph.get_graph_stats())
|
|
54
|
+
selected = graph.get_appropriate_hexes(cutoff=0.8)
|
|
55
|
+
fig = graph.visualize_graph(title="80% compact cluster", highlight_hexes=selected)
|
|
56
|
+
print(graph.decompose_branches())
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Main APIs
|
|
60
|
+
|
|
61
|
+
- `SpatialIngestor`: converts H3 arrays, lat/lng sequences, and encoded polylines into contiguous H3 chains.
|
|
62
|
+
- `AffinityGraph`: NetworkX wrapper for array-based insertion, nearest attachment, affinity scoring, editing, and JSON persistence.
|
|
63
|
+
- `CorridorExtractor`: uses exact all-node Dijkstra selection to extract the most compact cluster covering a target percentage of graph traversal volume.
|
|
64
|
+
- `TopologyAnalyzer`: separates the main branch from residual minor branches.
|
|
65
|
+
- `HexGraph`: backwards-compatible convenience class for your original code style.
|
|
66
|
+
- `RouteSchema` / `RouteTensor` / `RouteGraph`: grain-aware pickup -> drop graph where each edge carries a metric cube (see [Route flow graphs](#route-flow-graphs-pickup---drop)).
|
|
67
|
+
- `RouteExplorer`: the one-liner front door to the route graph - build from a DataFrame, rank partners, and plot the flow around a cluster.
|
|
68
|
+
- `plotter`: stateless DataFrame plotting - multi-column, multi-axis, multi-group, plus distribution checks.
|
|
69
|
+
|
|
70
|
+
Graph creation follows the original per-node procedure: H3 arrays are normalized, then each hex is inserted with `add_node`/`add_hex`. Lat/lng sequences and encoded polylines are first converted into H3 arrays at the requested resolution, then inserted the same way.
|
|
71
|
+
|
|
72
|
+
For QC, use:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
fig = graph.visualize_graph(highlight_hexes=selected)
|
|
76
|
+
fig.savefig("graph_qc.png", dpi=150, bbox_inches="tight")
|
|
77
|
+
|
|
78
|
+
fig = graph.visualize_step_by_step(route[:5], labels=["A", "B", "C", "D", "E"])
|
|
79
|
+
fig.savefig("insertion_steps.png", dpi=150, bbox_inches="tight")
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
To plot actual H3 hex boundaries as geospatial polygons:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from sameer_graph_lib import plot_h3_cells, plot_h3_cells_map
|
|
86
|
+
|
|
87
|
+
fig = plot_h3_cells("88618c4f29fffff", label_full_hex=True)
|
|
88
|
+
fig.savefig("single_h3_cell.png", dpi=150, bbox_inches="tight")
|
|
89
|
+
|
|
90
|
+
fig = graph.plot_h3_cells(highlight_hexes=selected, show_labels=False)
|
|
91
|
+
fig.savefig("h3_cell_footprint.png", dpi=150, bbox_inches="tight")
|
|
92
|
+
|
|
93
|
+
fig = plot_h3_cells_map(route, selected_cells=selected)
|
|
94
|
+
fig.savefig("h3_cell_basemap.png", dpi=150, bbox_inches="tight")
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`plot_h3_cells_map` uses GeoPandas + Contextily. Install it with:
|
|
98
|
+
|
|
99
|
+
```powershell
|
|
100
|
+
python -m pip install -e ".[plot,geo]"
|
|
101
|
+
uv run --extra plot --extra geo python -c "from sameer_graph_lib import plot_h3_cells_map"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
To get H3 centers and a convex hull:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from sameer_graph_lib import getLatLng, h3_convex_hull
|
|
108
|
+
|
|
109
|
+
points = getLatLng(route) # [(lat, lng), ...]
|
|
110
|
+
hull = h3_convex_hull(route) # Shapely geometry in (lng, lat)
|
|
111
|
+
graph_hull = graph.convex_hull() # Same, using graph nodes
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Route flow graphs (pickup -> drop)
|
|
115
|
+
|
|
116
|
+
The second half of the library works on tabular route data rather than H3
|
|
117
|
+
chains: one row per `pickup_cluster x drop_cluster x week_period x hour`, with
|
|
118
|
+
whatever metric columns you export. Every edge stores a **tensor** - a dense
|
|
119
|
+
cube over the grain dimensions - so sums stay exact, averages stay correctly
|
|
120
|
+
weighted, and two tensors can be merged without re-reading the source rows.
|
|
121
|
+
|
|
122
|
+
The metric columns are **yours**: the schema reads whatever numeric columns the
|
|
123
|
+
frame has. Names that look like averages (`avg_`, `_rate`, `pct`, `ratio`, ...)
|
|
124
|
+
are weighted-averaged, the rest are summed, and grain/id/categorical columns are
|
|
125
|
+
never treated as metrics. Pass `sum_metrics=` / `mean_metrics=` to override the
|
|
126
|
+
split, and `metric=` anywhere to choose the variable; with nothing named, calls
|
|
127
|
+
fall back to `schema.default_metric` (the first sum metric).
|
|
128
|
+
|
|
129
|
+
Install the extra:
|
|
130
|
+
|
|
131
|
+
```powershell
|
|
132
|
+
python -m pip install -e ".[analysis]" # pandas, numpy, xarray, matplotlib
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Quick start
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from sameer_graph_lib import RouteExplorer
|
|
139
|
+
|
|
140
|
+
ex = RouteExplorer(df, metric="orders") # metric= is optional
|
|
141
|
+
|
|
142
|
+
ex.sources("A1", top=5) # where A1 orders come from
|
|
143
|
+
ex.drops("A1", top=5) # where A1 orders go
|
|
144
|
+
ex.route("A1", "B1") # every metric for one route
|
|
145
|
+
ex.summary("A1") # every metric for one cluster
|
|
146
|
+
print(ex.describe())
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`where` and `using` return a new view that shares the same graph, so you can
|
|
150
|
+
narrow the question without rebuilding anything:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
peak = ex.where(week_period="weekday", hour=[8, 9, 10]).using("speed")
|
|
154
|
+
peak.drops("A1", top=5)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Plotting the flow around a cluster
|
|
158
|
+
|
|
159
|
+
`plot` puts the focus clusters in the middle, the clusters that feed them on
|
|
160
|
+
the left, and the clusters they feed on the right. The per-level top-k is the
|
|
161
|
+
main control: pass an int for one level, or a list for one number per level.
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
# top 5 sources, then the top 3 sources of each of those, then the top 2;
|
|
165
|
+
# and on the other side, the top 4 drops and the top 2 of each.
|
|
166
|
+
fig = ex.plot("A1", upstream=[5, 3, 2], downstream=[4, 2])
|
|
167
|
+
|
|
168
|
+
# several focus clusters, plus the routes between the selected set
|
|
169
|
+
ex.plot(["A1", "C1"], upstream=3, downstream=3, include_cross_edges=True)
|
|
170
|
+
|
|
171
|
+
# rank by one variable, show several: every node and edge carries a table
|
|
172
|
+
ex.plot("A1", metric="orders",
|
|
173
|
+
node_metrics=["orders", "accepted_orders", "requests", "speed"],
|
|
174
|
+
edge_metrics=["orders", "speed", "avg_distance"],
|
|
175
|
+
hour=[8, 9, 10])
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`node_metrics` and `edge_metrics` take any number of variables. They are drawn
|
|
179
|
+
as a small aligned table under each node and on each edge, with the metric
|
|
180
|
+
names on the left and the values right-aligned; every table in one figure
|
|
181
|
+
shares its column widths, and the figure sizes itself so they do not collide.
|
|
182
|
+
Numbers are formatted per value by default (62,709 and 0.273 both read
|
|
183
|
+
correctly); pass `value_format="{:,.1f}"` for one format, or a dict such as
|
|
184
|
+
`value_format={"speed": "{:.3f}"}` per metric.
|
|
185
|
+
|
|
186
|
+
Other options: `size_metric=` (which variable drives node size),
|
|
187
|
+
`show_metric_names=False` (values only), `per_parent=False` (rank a whole level
|
|
188
|
+
globally instead of per parent), `min_value=`, `layout="layered" | "geo" |
|
|
189
|
+
"spring"` (`geo` places H3 cluster ids at their real coordinates),
|
|
190
|
+
`show_edge_values`, `label_cross_edges`, `curve`, `figsize`, and the colours.
|
|
191
|
+
|
|
192
|
+
The same expansion is available without plotting:
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
ex.flow("A1", upstream=[5, 3], downstream=4) # tidy DataFrame
|
|
196
|
+
ex.subgraph("A1", upstream=2, downstream=2) # nx.DiGraph, levels on nodes
|
|
197
|
+
print(ex.tree("A1", upstream=[5, 3], downstream=4))
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
```text
|
|
201
|
+
A1 (orders)
|
|
202
|
+
sources (orders coming in)
|
|
203
|
+
|-- <- C1 [8,523.4]
|
|
204
|
+
| |-- <- D3 [8,802.4]
|
|
205
|
+
| +-- <- D2 [3,646.5]
|
|
206
|
+
+-- <- C3 [7,041.1]
|
|
207
|
+
drops (orders going out)
|
|
208
|
+
|-- -> B4 [4,574.9]
|
|
209
|
+
+-- -> B5 [4,446.4]
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Other route views
|
|
213
|
+
|
|
214
|
+
| Call | What you get |
|
|
215
|
+
| --- | --- |
|
|
216
|
+
| `ex.plot_partners("A1", top=10)` | top sources and top drops as back-to-back bars on one shared scale |
|
|
217
|
+
| `ex.plot_profile(cluster="A1")` | week_period x hour heatmap for a cluster or a route |
|
|
218
|
+
| `ex.plot_matrix(top=15)` | pickup x drop heatmap |
|
|
219
|
+
| `ex.plot_graph(top_routes=30)` | the whole graph as a ranked ring, sized and coloured by a metric |
|
|
220
|
+
| `ex.clusters_frame()` | per cluster inbound vs outbound volume and net balance |
|
|
221
|
+
| `ex.top_routes(20)` | routes ranked by any metric |
|
|
222
|
+
| `ex.matrix()` / `ex.frame()` | pivot table / long per-bucket frame |
|
|
223
|
+
|
|
224
|
+
The lower-level classes are public too: `RouteSchema` (grains and metric
|
|
225
|
+
columns), `RouteTensor` (one route's metric cube, with `summary`, `totals`,
|
|
226
|
+
`profile`, `merge`) and `RouteGraph` (the NetworkX graph, with `partners`,
|
|
227
|
+
`node_tensor`, `shortest_route`, `flow_subgraph`, `as_dataset`).
|
|
228
|
+
|
|
229
|
+
## DataFrame plotting
|
|
230
|
+
|
|
231
|
+
`sameer_graph_lib.plotter` is a stateless plotting layer for any DataFrame -
|
|
232
|
+
each function takes a frame and returns a figure.
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
from sameer_graph_lib import plotter
|
|
236
|
+
|
|
237
|
+
# n columns, one panel each, test vs control overlaid in every panel
|
|
238
|
+
plotter.plot_columns(df, ["orders", "speed", "aor"], x="hour", group="variant",
|
|
239
|
+
agg="mean", kind="line", mode="grid")
|
|
240
|
+
|
|
241
|
+
# columns with different scales on one chart, one y-axis each
|
|
242
|
+
plotter.plot_multi_axis(df, ["orders", "speed"], x="hour", agg="mean")
|
|
243
|
+
|
|
244
|
+
# distribution of a column, with count/mean/median/sd/skew in the corner
|
|
245
|
+
plotter.plot_distribution(df, "speed", kind="hist", bins=40)
|
|
246
|
+
|
|
247
|
+
# the same distribution per group, one colour per line, on one chart
|
|
248
|
+
plotter.compare_distributions(df, "speed", group="variant", kind="kde", with_box=True)
|
|
249
|
+
|
|
250
|
+
# A/B readout: aggregate per group, optionally relative to a baseline
|
|
251
|
+
plotter.compare_groups(df, ["orders", "speed"], group="variant", agg="mean",
|
|
252
|
+
normalize_to="control")
|
|
253
|
+
|
|
254
|
+
plotter.describe_distribution(df, "speed", group="variant") # stats table
|
|
255
|
+
plotter.plot_correlation(df, ["orders", "speed", "distance"])
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
`mode` is `grid` (one panel per column), `overlay` (one shared axes) or `twin`
|
|
259
|
+
(one y-axis per column). `kind` is `line`, `step`, `area`, `bar`, `barh`,
|
|
260
|
+
`scatter`, `hist`, `kde`, `box`, `violin` or `ecdf`. `group` splits every
|
|
261
|
+
series by a categorical column and gives each level its own colour. KDE and
|
|
262
|
+
ECDF are computed with numpy, so scipy is not required.
|
|
263
|
+
|
|
264
|
+
The explorer exposes the same helpers over its own long frame:
|
|
265
|
+
|
|
266
|
+
```python
|
|
267
|
+
ex.plot_columns(["orders", "speed"], x="hour", group="week_period", agg="mean")
|
|
268
|
+
ex.compare_distributions("speed", "week_period")
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Useful commands
|
|
272
|
+
|
|
273
|
+
```powershell
|
|
274
|
+
python -m pytest
|
|
275
|
+
python -m build
|
|
276
|
+
uv run --extra dev pytest -q
|
|
277
|
+
uv run --extra dev python -m build
|
|
278
|
+
uv run --extra dev python -m twine check dist/*
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Build artifacts will appear in `dist/` after `python -m build`.
|
|
282
|
+
|
|
283
|
+
## Publish To PyPI
|
|
284
|
+
|
|
285
|
+
1. Build the package:
|
|
286
|
+
|
|
287
|
+
```powershell
|
|
288
|
+
uv run --extra dev python -m build
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
2. Validate the package metadata:
|
|
292
|
+
|
|
293
|
+
```powershell
|
|
294
|
+
uv run --extra dev python -m twine check dist/*
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
3. Upload to PyPI:
|
|
298
|
+
|
|
299
|
+
```powershell
|
|
300
|
+
uv run --extra dev python -m twine upload dist/*
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
After upload, users can install it with:
|
|
304
|
+
|
|
305
|
+
```powershell
|
|
306
|
+
pip install sameer-graph-lib
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
## Publish From GitHub
|
|
310
|
+
|
|
311
|
+
This repo also includes a Trusted Publishing workflow in
|
|
312
|
+
[.github/workflows/publish.yml](C:/Users/rrran/Desktop/sameer_graph_lib/.github/workflows/publish.yml:1).
|
|
313
|
+
|
|
314
|
+
To finish that setup:
|
|
315
|
+
|
|
316
|
+
1. Create the project on PyPI, or reserve the name `sameer-graph-lib`.
|
|
317
|
+
2. On PyPI, open the project settings and add a Trusted Publisher for:
|
|
318
|
+
`owner`: `iams31`
|
|
319
|
+
`repository`: `sameer_graph_lib`
|
|
320
|
+
`workflow`: `publish.yml`
|
|
321
|
+
`environment`: `pypi`
|
|
322
|
+
3. Create a GitHub Release, or run the workflow manually from the Actions tab.
|
|
323
|
+
|
|
324
|
+
After that, GitHub Actions can publish without storing a long-lived PyPI token.
|
|
325
|
+
|
|
326
|
+
Official references:
|
|
327
|
+
|
|
328
|
+
- PyPI Trusted Publishing: https://docs.pypi.org/trusted-publishers/
|
|
329
|
+
- Packaging guide upload flow: https://packaging.python.org/tutorials/packaging-projects/
|