import%20marimo%0A%0A__generated_with%20%3D%20%220.24.2%22%0Aapp%20%3D%20marimo.App()%0A%0A%0A%40app.cell%0Adef%20_()%3A%0A%20%20%20%20import%20marimo%20as%20mo%0A%0A%20%20%20%20return%20(mo%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%20Introducing%20BEAHIV%0A%0A%20%20%20%20%5BBEAHIV%5D(https%3A%2F%2Fgithub.com%2Fsafer-streets%2Fbeahiv)%20is%20a%20hexagonal%20grid%20system.%20This%20notebook%20is%20a%20walkthough%20of%20its%20features%0A%0A%20%20%20%20-%20BNG-native%0A%20%20%20%20-%20equal-area%20cells%0A%20%20%20%20-%20hexagonal%20geometry%0A%20%20%20%20-%20indexed%20to%20avoid%2Freduce%20expensive%20spatial%20queries%0A%20%20%20%20-%20variable%20size%20and%20orientation%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_()%3A%0A%20%20%20%20import%20beahiv%20as%20bh%0A%20%20%20%20import%20contextily%20as%20cx%0A%20%20%20%20import%20duckdb%0A%20%20%20%20import%20geopandas%20as%20gpd%0A%20%20%20%20import%20matplotlib.pyplot%20as%20plt%0A%20%20%20%20import%20pandas%20as%20pd%0A%20%20%20%20import%20pyarrow%20as%20pa%0A%20%20%20%20from%20dotenv%20import%20load_dotenv%0A%20%20%20%20from%20duckdb.sqltypes%20import%20BIGINT%2C%20BLOB%2C%20DOUBLE%2C%20VARCHAR%0A%20%20%20%20from%20safer_streets_core.database%20import%20duckdb_connector%2C%20get_gdf%0A%20%20%20%20from%20shapely%20import%20from_wkb%2C%20to_wkb%0A%0A%20%20%20%20return%20(%0A%20%20%20%20%20%20%20%20BIGINT%2C%0A%20%20%20%20%20%20%20%20BLOB%2C%0A%20%20%20%20%20%20%20%20DOUBLE%2C%0A%20%20%20%20%20%20%20%20VARCHAR%2C%0A%20%20%20%20%20%20%20%20bh%2C%0A%20%20%20%20%20%20%20%20cx%2C%0A%20%20%20%20%20%20%20%20duckdb%2C%0A%20%20%20%20%20%20%20%20duckdb_connector%2C%0A%20%20%20%20%20%20%20%20from_wkb%2C%0A%20%20%20%20%20%20%20%20get_gdf%2C%0A%20%20%20%20%20%20%20%20gpd%2C%0A%20%20%20%20%20%20%20%20load_dotenv%2C%0A%20%20%20%20%20%20%20%20pa%2C%0A%20%20%20%20%20%20%20%20pd%2C%0A%20%20%20%20%20%20%20%20plt%2C%0A%20%20%20%20%20%20%20%20to_wkb%2C%0A%20%20%20%20)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%23%20Define%20the%20grid%20parameters%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(bh)%3A%0A%20%20%20%20orient%20%3D%20bh.Orientation.FLAT%20%20%23%20POINTY%20is%20the%20other%20option%0A%20%20%20%20side_length%20%3D%20200%20%20%23%20metres%2C%20any%20integer%20value%20between%201%20and%20100%2C000%20is%20valid%0A%20%20%20%20return%20orient%2C%20side_length%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%23%20Initialise%20the%20database%2C%20registering%20vectorised%20UDFs%20for%20beahiv%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(%0A%20%20%20%20BIGINT%2C%0A%20%20%20%20BLOB%2C%0A%20%20%20%20DOUBLE%2C%0A%20%20%20%20VARCHAR%2C%0A%20%20%20%20bh%2C%0A%20%20%20%20duckdb%2C%0A%20%20%20%20duckdb_connector%2C%0A%20%20%20%20from_wkb%2C%0A%20%20%20%20load_dotenv%2C%0A%20%20%20%20pa%2C%0A%20%20%20%20to_wkb%2C%0A)%3A%0A%20%20%20%20load_dotenv()%0A%20%20%20%20con%20%3D%20duckdb_connector(azure%3DTrue)%0A%0A%20%20%20%20GEOMETRY%20%3D%20duckdb.type(%22GEOMETRY%22)%0A%0A%20%20%20%20def%20_bng_to_cell(x%2C%20y%2C%20side_length%2C%20orientation)%3A%0A%20%20%20%20%20%20%20%20%22%22%22Vectorised%20bng_to_cell%20for%20duckdb.%0A%0A%20%20%20%20%20%20%20%20An%20arrow%20UDF%20gets%20*every*%20argument%20as%20a%20ChunkedArray%2C%20a%20SQL%20constant%20included%2C%20so%20the%20grid%0A%20%20%20%20%20%20%20%20parameters%20have%20to%20be%20unwrapped%20back%20into%20python%20scalars%20--%20beahiv%20vectorises%20over%20the%20points%0A%20%20%20%20%20%20%20%20but%20takes%20one%20side_length%2Forientation%20for%20the%20whole%20batch.%20duckdb%20also%20calls%20the%20UDF%20with%0A%20%20%20%20%20%20%20%20empty%20chunks%2C%20where%20there%20is%20no%20row%200%20to%20unwrap%2C%20hence%20the%20guard.%0A%20%20%20%20%20%20%20%20%22%22%22%0A%20%20%20%20%20%20%20%20if%20len(x)%20%3D%3D%200%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20pa.array(%5B%5D%2C%20type%3Dpa.int64())%0A%20%20%20%20%20%20%20%20return%20bh.bng_to_cell(x%2C%20y%2C%20side_length%5B0%5D.as_py()%2C%20bh.Orientation(orientation%5B0%5D.as_py()))%0A%0A%20%20%20%20%23%20beahiv%20speaks%20shapely%2C%20duckdb%20speaks%20WKB%2C%20so%20the%20UDFs%20translate%20at%20the%20boundary.%0A%20%20%20%20%23%20Both%20need%20their%20parameter%20and%20return%20types%20spelling%20out%3A%20inferring%20them%20from%20the%0A%20%20%20%20%23%20signature%20picks%20up%20polyfill's%20defaulted%20args%20and%20loses%20the%20list%20return%20type.%0A%20%20%20%20con.create_function(%0A%20%20%20%20%20%20%20%20%22bh_polyfill%22%2C%0A%20%20%20%20%20%20%20%20lambda%20wkb%2C%20side_length%2C%20orientation%2C%20predicate%3A%20bh.polyfill(%0A%20%20%20%20%20%20%20%20%20%20%20%20from_wkb(wkb)%2C%20side_length%2C%20bh.Orientation(orientation)%2C%20predicate%0A%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20%5BGEOMETRY%2C%20BIGINT%2C%20BIGINT%2C%20VARCHAR%5D%2C%0A%20%20%20%20%20%20%20%20duckdb.list_type(BIGINT)%2C%0A%20%20%20%20)%0A%20%20%20%20con.create_function(%0A%20%20%20%20%20%20%20%20%22bh_cell_polygon%22%2C%0A%20%20%20%20%20%20%20%20lambda%20cell_ids%3A%20to_wkb(bh.cell_polygons(cell_ids))%2C%0A%20%20%20%20%20%20%20%20%5BBIGINT%5D%2C%0A%20%20%20%20%20%20%20%20BLOB%2C%0A%20%20%20%20%20%20%20%20type%3D%22arrow%22%2C%0A%20%20%20%20)%0A%20%20%20%20con.create_function(%0A%20%20%20%20%20%20%20%20%22bh_bng_to_cell%22%2C%0A%20%20%20%20%20%20%20%20_bng_to_cell%2C%0A%20%20%20%20%20%20%20%20%5BDOUBLE%2C%20DOUBLE%2C%20BIGINT%2C%20BIGINT%5D%2C%0A%20%20%20%20%20%20%20%20BIGINT%2C%0A%20%20%20%20%20%20%20%20type%3D%22arrow%22%2C%0A%20%20%20%20)%0A%20%20%20%20return%20(con%2C)%0A%0A%0A%40app.cell%0Adef%20_(con%2C%20get_gdf)%3A%0A%20%20%20%20pfas%20%3D%20get_gdf(%0A%20%20%20%20%20%20%20%20con%2C%0A%20%20%20%20%20%20%20%20%22SELECT%20*%20EXCLUDE%20geom%2C%20ST_AsText(geom)%20AS%20wkt%20FROM%20read_parquet('az%3A%2F%2Fphase2%2Fextract%2Fpolice_force_areas.parquet')%22%2C%0A%20%20%20%20).set_index(%22spatial_id%22)%0A%20%20%20%20pfas.head()%0A%20%20%20%20return%20(pfas%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%23%20Pick%20a%20PFA%20and%20fill%20with%20BEAHIV%20cells%0A%0A%20%20%20%20%60polyfill%60%20has%203%20options%20for%20dealing%20with%20boundary%20overlaps%3A%0A%20%20%20%20-%20%60%22overlap%22%60%20(default)%20any%20cell%20that%20overlaps%20the%20polygon%0A%20%20%20%20-%20%60%22centre%22%60%20any%20cell%20with%20a%20centroid%20in%20the%20polygon%0A%20%20%20%20-%20%60%22full%22%60%20any%20cell%20fully%20within%20the%20polygon%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(bh%2C%20con%2C%20get_gdf%2C%20gpd%2C%20orient%2C%20pfas%2C%20plt%2C%20side_length)%3A%0A%20%20%20%20_fig%2C%20_axs%20%3D%20plt.subplots(1%2C%203%2C%20figsize%3D(18%2C%206))%0A%20%20%20%20pfa%20%3D%20%22London%2C%20City%20of%22%0A%20%20%20%20pointy_city_cells%20%3D%20get_gdf(%0A%20%20%20%20%20%20%20%20con%2C%0A%20%20%20%20%20%20%20%20%22%5Cn%20%20%20%20SELECT%20spatial_id%2C%20ST_AsText(ST_GeomFromWKB(bh_cell_polygon(spatial_id)))%20AS%20wkt%5Cn%20%20%20%20FROM%20(%5Cn%20%20%20%20%20%20%20%20SELECT%20unnest(bh_polyfill(geom%2C%20%3F%2C%20%3F%2C%20'centre'))%20AS%20spatial_id%5Cn%20%20%20%20%20%20%20%20FROM%20read_parquet('az%3A%2F%2Fphase2%2Fextract%2Fpolice_force_areas.parquet')%5Cn%20%20%20%20%20%20%20%20WHERE%20pfa24nm%20%3D%20%3F%5Cn%20%20%20%20)%5Cn%20%20%20%20%22%2C%0A%20%20%20%20%20%20%20%20params%3D(side_length%2C%20bh.Orientation.POINTY%2C%20pfa)%2C%0A%20%20%20%20)%0A%20%20%20%20pointy_city_cells.plot(ax%3D_axs%5B0%5D%2C%20facecolor%3D%22none%22%2C%20edgecolor%3D%22b%22)%0A%20%20%20%20%23%20use%20duckdb%20with%20UDFs...%20polyfill%20returns%20a%20list%20per%20row%2C%20so%20unnest%20it%20to%20one%20cell%20per%20row%0A%20%20%20%20pfas.loc%5Bpfas.pfa24nm%20%3D%3D%20pfa%5D.plot(ax%3D_axs%5B0%5D%2C%20facecolor%3D%22none%22%2C%20edgecolor%3D%22r%22)%0A%20%20%20%20_axs%5B0%5D.set_title(f%22%7Bside_length%7Dm%20pointy%2C%20centre%3A%20%7Blen(pointy_city_cells)%7D%20cells%22)%0A%20%20%20%20_axs%5B0%5D.set_axis_off()%0A%20%20%20%20_cell_ids%20%3D%20bh.polyfill(pfas.loc%5Bpfas.pfa24nm%20%3D%3D%20pfa%2C%20%22geometry%22%5D.item()%2C%20side_length%20%2F%2F%202%2C%20orient%2C%20%22full%22)%0A%20%20%20%20city_cells%20%3D%20gpd.GeoDataFrame(index%3D_cell_ids%2C%20geometry%3Dbh.cell_polygons(_cell_ids)%2C%20crs%3D%22epsg%3A27700%22)%0A%20%20%20%20city_cells.plot(ax%3D_axs%5B2%5D%2C%20facecolor%3D%22none%22%2C%20edgecolor%3D%22b%22)%0A%20%20%20%20pfas.loc%5Bpfas.pfa24nm%20%3D%3D%20pfa%5D.plot(ax%3D_axs%5B2%5D%2C%20facecolor%3D%22none%22%2C%20edgecolor%3D%22r%22)%0A%20%20%20%20_axs%5B2%5D.set_title(f%22%7Bside_length%20%2F%2F%202%7Dm%20flat%2C%20full%3A%20%7Blen(_cell_ids)%7D%20cells%22)%0A%20%20%20%20_axs%5B2%5D.set_axis_off()%0A%20%20%20%20_cell_ids%20%3D%20bh.polyfill(pfas.loc%5Bpfas.pfa24nm%20%3D%3D%20pfa%2C%20%22geometry%22%5D.item()%2C%20side_length%2C%20orient)%0A%20%20%20%20city_cells%20%3D%20gpd.GeoDataFrame(index%3D_cell_ids%2C%20geometry%3Dbh.cell_polygons(_cell_ids)%2C%20crs%3D%22epsg%3A27700%22)%0A%20%20%20%20city_cells.plot(ax%3D_axs%5B1%5D%2C%20facecolor%3D%22none%22%2C%20edgecolor%3D%22b%22)%0A%20%20%20%20pfas.loc%5Bpfas.pfa24nm%20%3D%3D%20pfa%5D.plot(ax%3D_axs%5B1%5D%2C%20facecolor%3D%22none%22%2C%20edgecolor%3D%22r%22)%0A%20%20%20%20_axs%5B1%5D.set_title(f%22%7Bside_length%7Dm%20flat%2C%20overlap%3A%20%7Blen(_cell_ids)%7D%20cells%22)%0A%20%20%20%20%23%20...or%20use%20geopandas%0A%20%20%20%20_axs%5B1%5D.set_axis_off()%0A%20%20%20%20_fig%0A%20%20%20%20return%20city_cells%2C%20pfa%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%23%20How%20does%20indexing%20work%3F%0A%0A%20%20%20%20Flat%3A%20q%20is%2030%20degree%20(to%20horizontal)%20axis%2C%20r%20is%20vertical%20axis%0A%0A%20%20%20%20Pointy%3A%20q%20is%20horizontal%20axis%2C%20r%20is%2060%20degree%20axis.%20Move%20the%20cell%20index%20along%20the%20axes%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(bh%2C%20pfa%2C%20pfas%2C%20side_length)%3A%0A%20%20%20%20city_centroid%20%3D%20bh.bng_to_cell(*pfas.loc%5Bpfas.pfa24nm%20%3D%3D%20pfa%2C%20%22geometry%22%5D.centroid.item().xy%2C%20side_length)%5B0%5D%0A%20%20%20%20pointy_city_centroid%20%3D%20bh.bng_to_cell(%0A%20%20%20%20%20%20%20%20*pfas.loc%5Bpfas.pfa24nm%20%3D%3D%20pfa%2C%20%22geometry%22%5D.centroid.item().xy%2C%20side_length%2C%20orientation%3Dbh.Orientation.POINTY%0A%20%20%20%20)%5B0%5D%0A%20%20%20%20bh.decode(city_centroid)%2C%20bh.decode(pointy_city_centroid)%0A%0A%20%20%20%20type(city_centroid)%0A%20%20%20%20return%20city_centroid%2C%20pointy_city_centroid%0A%0A%0A%40app.cell%0Adef%20_(bh%2C%20city_centroid%2C%20gpd%2C%20pd%2C%20pfa%2C%20pfas%2C%20plt%2C%20pointy_city_centroid)%3A%0A%20%20%20%20_fig%2C%20_axs%20%3D%20plt.subplots(1%2C%202%2C%20figsize%3D(18%2C%206))%0A%20%20%20%20for%20i%2C%20c%20in%20enumerate((city_centroid%2C%20pointy_city_centroid))%3A%0A%20%20%20%20%20%20%20%20cells%20%3D%20pd.DataFrame(index%3Dpd.MultiIndex.from_product((%5B-2%2C%20-1%2C%200%2C%201%2C%202%5D%2C%20%5B-2%2C%20-1%2C%200%2C%201%2C%202%5D)))%0A%20%20%20%20%20%20%20%20cells%5B%22cell_id%22%5D%20%3D%20cells.index.map(lambda%20qr%2C%20c%3Dc%3A%20bh.decode(c).dq(qr%5B0%5D).dr(qr%5B1%5D).encode())%0A%20%20%20%20%20%20%20%20cells%5B%22delta%22%5D%20%3D%20cells.index.map(%0A%20%20%20%20%20%20%20%20%20%20%20%20lambda%20qr%3A%20(f%22q%7Bqr%5B0%5D%3A%2B%7D%22%20if%20qr%5B0%5D%20else%20%22q%22)%20%2B%20%22%2C%20%22%20%2B%20(f%22r%7Bqr%5B1%5D%3A%2B%7D%22%20if%20qr%5B1%5D%20else%20%22r%22)%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20cells%20%3D%20gpd.GeoDataFrame(cells%2C%20geometry%3Dbh.cell_polygons(cells.cell_id)%2C%20crs%3D%22epsg%3A27700%22)%0A%20%20%20%20%20%20%20%20cells.plot(ax%3D_axs%5Bi%5D%2C%20facecolor%3D%22none%22%2C%20edgecolor%3D%22blue%22%2C%20figsize%3D(8%2C%208))%0A%20%20%20%20%20%20%20%20pfas.loc%5Bpfas.pfa24nm%20%3D%3D%20pfa%5D.plot(ax%3D_axs%5Bi%5D%2C%20facecolor%3D%22none%22%2C%20edgecolor%3D%22r%22)%0A%20%20%20%20%20%20%20%20for%20delta%2C%20centre%20in%20zip(cells.delta%2C%20cells.geometry.centroid%2C%20strict%3DTrue)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20_axs%5Bi%5D.annotate(delta%2C%20(centre.x%2C%20centre.y)%2C%20ha%3D%22center%22%2C%20va%3D%22center%22%2C%20fontsize%3D8)%0A%20%20%20%20%20%20%20%20_axs%5Bi%5D.set_axis_off()%0A%20%20%20%20_fig%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%23%20K-rings%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(bh%2C%20city_centroid%2C%20gpd%2C%20pfa%2C%20pfas)%3A%0A%20%20%20%20_ax%20%3D%20gpd.GeoDataFrame(index%3D%5Bcity_centroid%5D%2C%20geometry%3D%5Bbh.cell_polygon(city_centroid)%5D).plot(%0A%20%20%20%20%20%20%20%20alpha%3D0.2%2C%20figsize%3D(8%2C%208)%0A%20%20%20%20)%0A%20%20%20%20for%20k%20in%20range(1%2C%205)%3A%0A%20%20%20%20%20%20%20%20_cell_ids%20%3D%20bh.k_ring(city_centroid%2C%20k)%0A%20%20%20%20%20%20%20%20gpd.GeoDataFrame(index%3D_cell_ids%2C%20geometry%3Dbh.cell_polygons(_cell_ids)).plot(ax%3D_ax%2C%20alpha%3D0.2)%0A%20%20%20%20pfas.loc%5Bpfas.pfa24nm%20%3D%3D%20pfa%5D.plot(ax%3D_ax%2C%20facecolor%3D%22none%22%2C%20edgecolor%3D%22r%22)%0A%20%20%20%20_ax.set_axis_off()%0A%20%20%20%20_ax%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%23%20Neigbours%2C%20Distance%2C%20Centroid%0A%0A%20%20%20%20All%20%24O(1)%24%20non-spatial%20operations.%0A%0A%20%20%20%20-%20%60get_neighbours%60%20returns%20the%201%20k-ring%20not%20including%20the%20original%20cell%0A%20%20%20%20-%20%60distance%60%20returns%20the%20number%20of%20hops%20between%20cells%20(must%20be%20same%20size%20and%20orientation)%0A%20%20%20%20-%20%60centroid%60%20returns%20the%20centroid%2C%20as%20BNG%20(easting%2C%20northing)%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(bh%2C%20city_centroid%2C%20mo%2C%20pd)%3A%0A%20%20%20%20neighbours%20%3D%20bh.get_neighbours(city_centroid)%0A%20%20%20%20distance%20%3D%20bh.distance(neighbours%5B0%5D%2C%20neighbours%5B1%5D)%0A%20%20%20%20centroid%20%3D%20bh.centroid(city_centroid)%0A%0A%20%20%20%20_origin%20%3D%20bh.decode(city_centroid)%0A%20%20%20%20_rows%20%3D%20%5B%5D%0A%20%20%20%20for%20_n%20in%20neighbours%3A%0A%20%20%20%20%20%20%20%20_d%20%3D%20bh.decode(_n)%0A%20%20%20%20%20%20%20%20_x%2C%20_y%20%3D%20bh.centroid(_n)%0A%20%20%20%20%20%20%20%20_rows.append(%0A%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22cell_id%22%3A%20str(_n)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22%CE%94q%22%3A%20_d.q%20-%20_origin.q%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22%CE%94r%22%3A%20_d.r%20-%20_origin.r%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22hops%22%3A%20bh.distance(city_centroid%2C%20_n)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22easting%22%3A%20round(float(_x)%2C%201)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22northing%22%3A%20round(float(_y)%2C%201)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20)%0A%0A%20%20%20%20mo.vstack(%0A%20%20%20%20%20%20%20%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20mo.hstack(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20mo.stat(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20str(city_centroid)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20label%3D%22Cell%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20caption%3Df%22q%3D%7B_origin.q%7D%2C%20r%3D%7B_origin.r%7D%2C%20size%3D%7B_origin.side_length%7Dm%2C%20%7B_origin.orientation.name.lower()%7D%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20bordered%3DTrue%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20mo.stat(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20f%22%7Bfloat(centroid%5B0%5D)%3A%2C.1f%7D%2C%20%7Bfloat(centroid%5B1%5D)%3A%2C.1f%7D%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20label%3D%22Centroid%20(E%2C%20N)%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20caption%3D%22BNG%20metres%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20bordered%3DTrue%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20mo.stat(distance%2C%20label%3D%22Distance%22%2C%20caption%3D%22hops%20between%20neighbours%200%20and%201%22%2C%20bordered%3DTrue)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20widths%3D%22equal%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20mo.md(%22**Neighbours**%20(1%20k-ring%2C%20excluding%20the%20cell%20itself)%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20mo.ui.table(pd.DataFrame(_rows)%2C%20selection%3DNone%2C%20show_column_summaries%3DFalse)%2C%0A%20%20%20%20%20%20%20%20%5D%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%23%20Hierarchy%0A%0A%20%20%20%20Hex%20cells%20cannot%20be%20truly%20hierarchical%20since%20no%20combination%20of%20hexagons%20makes%20a%20larger%20hexagon.%20e.g.%20H3%20defines%20a%20parent%20hex%0A%20%20%20%20by%20taking%20a%20vertex%20from%20each%20of%20the%20k-ring%201%20cells.%20This%20means%20a%20point%20in%20a%20cell%20may%20not%20actually%20be%20contained%20by%20the%20parent%20cell.%0A%0A%20%20%20%20BEAHIV%20doesn't%20claim%20to%20be%20hierarchical%2C%20but%20has%20some%20functionality%20to%20move%20between%20resolutions.%0A%0A%20%20%20%20%23%23%23%23%20Parents%0A%0A%20%20%20%20Parent%20are%20overlapping%20cells%20that%20are%20exactly%20twice%20the%20%60side_length%60.%20Each%20cell%20has%20either%20one%20exclusive%0A%20%20%20%20(when%20%24q%2C%20r%24%20both%20even)%20or%20two%20shared%20parents.%20Exclusive%20parents%20have%20the%20same%20centroid.%0A%0A%20%20%20%20%60get_parent%60%20will%20return%20the%20exclusive%20parent%20(if%20there%20is%20one)%3B%20%60get_parents%60%20returns%20one%20or%20both.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(bh%2C%20city_centroid%2C%20gpd%2C%20pfa%2C%20pfas)%3A%0A%20%20%20%20other%20%3D%20bh.decode(city_centroid).dq(3).dr(-4).encode()%0A%0A%20%20%20%20_ax%20%3D%20gpd.GeoDataFrame(index%3D%5Bcity_centroid%2C%20other%5D%2C%20geometry%3Dbh.cell_polygons(%5Bcity_centroid%2C%20other%5D)).plot(%0A%20%20%20%20%20%20%20%20figsize%3D(8%2C%208)%2C%20alpha%3D0.5%0A%20%20%20%20)%0A%20%20%20%20parents%20%3D%20bh.get_parents(city_centroid)%0A%20%20%20%20gpd.GeoDataFrame(index%3Dparents%2C%20geometry%3Dbh.cell_polygons(parents)).plot(ax%3D_ax%2C%20facecolor%3D%22none%22)%0A%20%20%20%20parents%20%3D%20bh.get_parents(other)%0A%20%20%20%20gpd.GeoDataFrame(index%3Dparents%2C%20geometry%3Dbh.cell_polygons(parents)).plot(ax%3D_ax%2C%20facecolor%3D%22none%22)%0A%20%20%20%20pfas.loc%5Bpfas.pfa24nm%20%3D%3D%20pfa%5D.plot(ax%3D_ax%2C%20facecolor%3D%22none%22%2C%20edgecolor%3D%22r%22)%0A%20%20%20%20_ax.set_axis_off()%0A%20%20%20%20_ax%0A%20%20%20%20return%20(other%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%23%23%20Children%0A%0A%20%20%20%20Children%20are%20overlapping%20cells%20that%20are%20exactly%20half%20the%20side_length%2C%20*for%20even%20side%20lengths%20only*.%0A%0A%20%20%20%20Each%20even-sided%20cell%20has%20one%20exclusive%20child%20which%20shares%20its%20centroid%20-%20%60get_child%60%20will%20return%20this.%20%60get_children%60%20returns%20exclusive%20and%20shared%20children%20-%20the%201%20k-ring%20of%20the%20exclusive%20child.%20Shared%20children%20overlap%2050%25%20with%20each%20of%20their%20two%20parents.%0A%0A%20%20%20%20Since%20BEAHIV%20side%20lengths%20must%20be%20integers%2C%20calling%20%60get_child%60%2F%60get_children%60%20on%20an%20odd-sized%20cell%20will%20raise%20a%20%60ValueError%60%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(bh%2C%20city_centroid%2C%20gpd%2C%20other%2C%20pfa%2C%20pfas)%3A%0A%20%20%20%20_ax%20%3D%20gpd.GeoDataFrame(index%3D%5Bcity_centroid%2C%20other%5D%2C%20geometry%3Dbh.cell_polygons(%5Bcity_centroid%2C%20other%5D)).plot(%0A%20%20%20%20%20%20%20%20figsize%3D(8%2C%208)%2C%20alpha%3D0.5%0A%20%20%20%20)%0A%20%20%20%20child%20%3D%20bh.get_child(city_centroid)%0A%20%20%20%20gpd.GeoDataFrame(index%3D%5Bchild%5D%2C%20geometry%3D%5Bbh.cell_polygon(child)%5D).plot(ax%3D_ax%2C%20facecolor%3D%22none%22)%0A%20%20%20%20children%20%3D%20bh.get_children(other)%0A%20%20%20%20gpd.GeoDataFrame(index%3Dchildren%2C%20geometry%3Dbh.cell_polygons(children)).plot(ax%3D_ax%2C%20facecolor%3D%22none%22)%0A%20%20%20%20pfas.loc%5Bpfas.pfa24nm%20%3D%3D%20pfa%5D.plot(ax%3D_ax%2C%20facecolor%3D%22none%22%2C%20edgecolor%3D%22r%22)%0A%20%20%20%20_ax.set_axis_off()%0A%20%20%20%20_ax%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%23%20Performance%0A%0A%20%20%20%20Since%20BEAHIV%20cells%20may%20straddle%20multiple%20PFAs%2C%20the%20%22falls_within%22%20field%20is%20used%20to%20attribute%20counts%20to%20the%20correct%20PFA%0A%0A%20%20%20%20How%20long%20to%20spatially%20aggregate%203%20years%20of%20England%20%26%20Wales%20public%20crime%20data%20(~18%20million%20crimes)%20into%20200m%20BEAHIV%20cells%2C%0A%20%20%20%20grouped%20by%20PFA%2C%20category%20and%20month%3F%0A%0A%20%20%20%20...%20about%2010s.%20Overhead%20of%20pulling%20data%20from%20the%20cloud%20is%20insignificant%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(con)%3A%0A%20%20%20%20counts%20%3D%20(%0A%20%20%20%20%20%20%20%20con.sql(%22%22%22%0A%20%20%20%20%20%20%20%20WITH%20c%20AS%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20SELECT%20bh_bng_to_cell(ST_X(geom)%2C%20ST_Y(geom)%2C%20200%2C%201)%20AS%20spatial_id%2C%20crime_type%2C%20falls_within%2C%20_month%20AS%20month%0A%20%20%20%20%20%20%20%20%20%20%20%20FROM%20read_parquet('az%3A%2F%2Fphase2%2Fextract%2Fcrime_data.parquet')%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20SELECT%20spatial_id%2C%20crime_type%2C%20falls_within%2C%20month%2C%20COUNT(*)%20AS%20count%20FROM%20c%0A%20%20%20%20%20%20%20%20GROUP%20BY%20spatial_id%2C%20crime_type%2C%20falls_within%2C%20month%0A%20%20%20%20%20%20%20%20%22%22%22)%0A%20%20%20%20%20%20%20%20.df()%0A%20%20%20%20%20%20%20%20.set_index(%5B%22crime_type%22%2C%20%22month%22%2C%20%22falls_within%22%2C%20%22spatial_id%22%5D)%0A%20%20%20%20)%0A%20%20%20%20return%20(counts%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%23%20Look%20at%20that%20on%20a%20map%0A%0A%20%20%20%20Select%20all%20crime%20cell%20totals%20in%20City%20of%20London%20in%202026-06%2C%20ignoring%20any%20that%20fall%20within%20the%20Met%0A%0A%20%20%20%20We%20clip%20the%20cells%20on%20the%20boundary%20for%20the%20map%20(only).%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(city_cells%2C%20counts%2C%20cx%2C%20pfa%2C%20pfas)%3A%0A%20%20%20%20city_counts%20%3D%20city_cells.join(%0A%20%20%20%20%20%20%20%20counts.loc%5B(slice(None)%2C%20%222026-06%22%2C%20%22City%20of%20London%20Police%22%2C%20city_cells.index)%2C%20%3A%5D.groupby(%22spatial_id%22).sum()%0A%20%20%20%20).fillna(0)%0A%20%20%20%20city_counts%20%3D%20city_counts.overlay(pfas.loc%5Bpfas.pfa24nm%20%3D%3D%20pfa%5D)%0A%20%20%20%20_ax%20%3D%20city_counts.plot(%22count%22%2C%20cmap%3D%22Blues%22%2C%20alpha%3D0.7%2C%20figsize%3D(12%2C%208))%0A%20%20%20%20pfas.loc%5Bpfas.pfa24nm%20%3D%3D%20pfa%5D.plot(ax%3D_ax%2C%20facecolor%3D%22none%22%2C%20edgecolor%3D%22r%22)%0A%20%20%20%20cx.add_basemap(_ax%2C%20crs%3Dcity_counts.crs%2C%20source%3Dcx.providers.Esri.WorldGrayCanvas)%0A%20%20%20%20_ax.set_axis_off()%0A%20%20%20%20_ax%0A%20%20%20%20return%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20app.run()%0A
7550714869512f85825d63c43f8be080