import%20marimo%0A%0A__generated_with%20%3D%20%220.23.13%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%20Elevation%20Effects%20on%20Centrality%0A%0A%20%20%20%20Compare%20centrality%20results%20with%20and%20without%20Z%20coordinates%20(elevation).%20When%20nodes%20have%20%60z%60%20attributes%2C%20cityseer%20automatically%20applies%20Tobler's%20hiking%20function%20during%20pathfinding%3A%20uphill%20segments%20become%20costlier%20and%20gentle%20downhill%20segments%20become%20slightly%20cheaper.%20This%20reshapes%20which%20paths%20are%20optimal%20and%20therefore%20changes%20centrality%20values.%0A%0A%20%20%20%20The%20flat%2Fhilly%20comparison%20needs%20direct%20manipulation%20of%20the%20graph%20geometry%2C%20so%20each%20variant%20is%20prepared%20with%20the%20lower-level%20%60tools%60%20modules%20and%20then%20bridged%20into%20the%20high-level%20%5B%60CityNetwork%60%5D(https%3A%2F%2Fcityseer.benchmarkurbanism.com%2Fapi%2Fnetwork)%20class%20with%20%60from_nx%60%2C%20which%20builds%20the%20dual%20graph%20and%20computes%20the%20centralities.%0A%0A%20%20%20%20%3E%203D%20elevation%20support%20requires%20cityseer%20v4.24.0%20or%20later.%0A%0A%20%20%20%20The%20bundled%20datasets%20and%20their%20source%20attributions%20are%20documented%20on%20the%20%5Bdatasets%20page%5D(https%3A%2F%2Fcityseer.benchmarkurbanism.com%2Fexamples%2Fdatasets).%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%20geopandas%20as%20gpd%0A%20%20%20%20import%20matplotlib.pyplot%20as%20plt%0A%20%20%20%20import%20numpy%20as%20np%0A%20%20%20%20import%20shapely%0A%20%20%20%20from%20cityseer.network%20import%20CityNetwork%0A%20%20%20%20from%20cityseer.tools%20import%20graphs%2C%20io%0A%20%20%20%20from%20scipy.stats%20import%20spearmanr%0A%0A%20%20%20%20return%20CityNetwork%2C%20gpd%2C%20graphs%2C%20io%2C%20np%2C%20plt%2C%20shapely%2C%20spearmanr%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%20Load%20the%20road%20network%0A%0A%20%20%20%20Load%20a%20hilly%20subset%20of%20the%20bundled%20road%20network%20with%20real%203D%20geometries%2C%20from%20mountainous%20terrain%20north%20of%20the%20city.%20The%20dataset%20is%20loaded%20twice%3A%20once%20preserving%20the%20Z%20coordinates%20(hilly)%20and%20once%20stripping%20them%20(flat).%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(gpd%2C%20graphs%2C%20io%2C%20shapely)%3A%0A%20%20%20%20import%20copy%0A%0A%20%20%20%20edges_raw%20%3D%20gpd.read_file(%22..%2F..%2Fdata%2Fmadrid_streets%2Fstreet_network_3d.gpkg%22)%0A%20%20%20%20edges_raw%20%3D%20edges_raw.explode(index_parts%3DFalse)%0A%20%20%20%20print(f%22Loaded%20%7Blen(edges_raw)%7D%20edges%2C%20has_z%3D%7Bedges_raw.geometry.iloc%5B0%5D.has_z%7D%22)%0A%20%20%20%20%23%20build%20the%20primal%20graph%20once%20(with%20Z)%2C%20then%20simplify%0A%20%20%20%20G_primal%20%3D%20io.nx_from_generic_geopandas(edges_raw)%0A%20%20%20%20G_primal%20%3D%20graphs.nx_remove_filler_nodes(G_primal)%0A%20%20%20%20G_primal%20%3D%20graphs.nx_remove_dangling_nodes(G_primal)%0A%20%20%20%20%23%20report%20elevation%20range%0A%20%20%20%20_zs%20%3D%20%5Bd%5B%22z%22%5D%20for%20_%2C%20d%20in%20G_primal.nodes(data%3DTrue)%20if%20%22z%22%20in%20d%5D%0A%20%20%20%20print(f%22Elevation%20range%3A%20%7Bmin(_zs)%3A.0f%7Dm%20to%20%7Bmax(_zs)%3A.0f%7Dm%20(spread%3A%20%7Bmax(_zs)%20-%20min(_zs)%3A.0f%7Dm)%22)%0A%20%20%20%20%23%20flat%20variant%3A%20strip%20Z%20from%20a%20copy%20of%20the%20same%20primal%20graph%0A%20%20%20%20G_flat_primal%20%3D%20copy.deepcopy(G_primal)%0A%20%20%20%20for%20_node_idx%2C%20_node_data%20in%20G_flat_primal.nodes(data%3DTrue)%3A%0A%20%20%20%20%20%20%20%20_node_data.pop(%22z%22%2C%20None)%0A%20%20%20%20for%20_u%2C%20_v%2C%20_edge_data%20in%20G_flat_primal.edges(data%3DTrue)%3A%0A%20%20%20%20%20%20%20%20if%20%22geom%22%20in%20_edge_data%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20_edge_data%5B%22geom%22%5D%20%3D%20shapely.force_2d(_edge_data%5B%22geom%22%5D)%0A%20%20%20%20return%20G_flat_primal%2C%20G_primal%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%20Elevation%20profile%0A%0A%20%20%20%20Visualise%20the%20terrain%20elevation%20across%20the%20network%20using%20contours%20interpolated%20from%20the%20node%20Z%20values.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(G_primal%2C%20np%2C%20plt)%3A%0A%20%20%20%20from%20scipy.interpolate%20import%20griddata%0A%0A%20%20%20%20%23%20extract%20node%20positions%20and%20elevations%20from%20the%20primal%20graph%0A%20%20%20%20_xs%20%3D%20np.array(%5Bd%5B%22x%22%5D%20for%20_%2C%20d%20in%20G_primal.nodes(data%3DTrue)%5D)%0A%20%20%20%20_ys%20%3D%20np.array(%5Bd%5B%22y%22%5D%20for%20_%2C%20d%20in%20G_primal.nodes(data%3DTrue)%5D)%0A%20%20%20%20_zs%20%3D%20np.array(%5Bd%5B%22z%22%5D%20for%20_%2C%20d%20in%20G_primal.nodes(data%3DTrue)%5D)%0A%20%20%20%20_xlim%20%3D%20(437400%2C%20442200)%0A%20%20%20%20_ylim%20%3D%20(4519850%2C%204527150)%0A%20%20%20%20%23%20create%20grid%20and%20interpolate%0A%20%20%20%20_grid_x%2C%20_grid_y%20%3D%20np.mgrid%5B_xlim%5B0%5D%20%3A%20_xlim%5B1%5D%20%3A%20200j%2C%20_ylim%5B0%5D%20%3A%20_ylim%5B1%5D%20%3A%20200j%5D%0A%20%20%20%20_grid_z%20%3D%20griddata((_xs%2C%20_ys)%2C%20_zs%2C%20(_grid_x%2C%20_grid_y)%2C%20method%3D%22cubic%22)%0A%20%20%20%20_fig%2C%20_ax%20%3D%20plt.subplots(1%2C%201%2C%20figsize%3D(7%2C%207)%2C%20dpi%3D150)%0A%20%20%20%20contour_filled%20%3D%20_ax.contourf(_grid_x%2C%20_grid_y%2C%20_grid_z%2C%20levels%3D20%2C%20cmap%3D%22terrain%22%2C%20alpha%3D0.8)%0A%20%20%20%20contour_lines%20%3D%20_ax.contour(_grid_x%2C%20_grid_y%2C%20_grid_z%2C%20levels%3D20%2C%20colors%3D%22k%22%2C%20linewidths%3D0.3%2C%20alpha%3D0.5)%0A%20%20%20%20_ax.clabel(contour_lines%2C%20inline%3DTrue%2C%20fontsize%3D7%2C%20fmt%3D%22%25.0f%20m%22)%0A%20%20%20%20plt.colorbar(contour_filled%2C%20ax%3D_ax%2C%20label%3D%22Elevation%20(m)%22%2C%20shrink%3D0.8)%0A%20%20%20%20_ax.set_xlim(_xlim)%0A%20%20%20%20_ax.set_ylim(_ylim)%0A%20%20%20%20_ax.set_aspect(%22equal%22)%0A%20%20%20%20_ax.set_title(%22Terrain%20elevation%20(Sierra%20de%20Guadarrama%20foothills)%22)%0A%20%20%20%20_ax.axis(False)%0A%20%20%20%20plt.show()%0A%20%20%20%20return%20(griddata%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%20Compute%20centrality%3A%20flat%20vs%20hilly%0A%0A%20%20%20%20Run%20simplest-path%20(angular)%20closeness%20and%20betweenness%20at%20a%201000m%20threshold%20for%20both%20versions.%20Each%20primal%20graph%20variant%20is%20bridged%20into%20the%20high-level%20API%20with%20%5B%60CityNetwork.from_nx%60%5D(https%3A%2F%2Fcityseer.benchmarkurbanism.com%2Fapi%2Fnetwork%23from_nx)%2C%20which%20builds%20the%20dual%20graph%20required%20for%20angular%20centrality%2C%20and%20%5B%60centrality_simplest%60%5D(https%3A%2F%2Fcityseer.benchmarkurbanism.com%2Fapi%2Fnetwork%23centrality_simplest)%20computes%20the%20centralities.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(CityNetwork%2C%20G_flat_primal%2C%20G_primal)%3A%0A%20%20%20%20distances%20%3D%20%5B1000%5D%0A%0A%20%20%20%20%23%20flat%0A%20%20%20%20cn_flat%20%3D%20CityNetwork.from_nx(G_flat_primal)%0A%20%20%20%20cn_flat.centrality_simplest(distances%3Ddistances)%0A%20%20%20%20nodes_flat%20%3D%20cn_flat.to_geopandas()%0A%0A%20%20%20%20%23%20hilly%3A%20the%203D%20geometries%20carry%20the%20elevation%20data%20through%20from_nx%0A%20%20%20%20cn_hilly%20%3D%20CityNetwork.from_nx(G_primal)%0A%20%20%20%20cn_hilly.centrality_simplest(distances%3Ddistances)%0A%20%20%20%20nodes_hilly%20%3D%20cn_hilly.to_geopandas()%0A%20%20%20%20return%20nodes_flat%2C%20nodes_hilly%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%20Compare%20results%0A%0A%20%20%20%20Align%20nodes%20by%20position%20and%20visualise%20the%20flat%20vs%20hilly%20centrality%20results%20side%20by%20side%2C%20along%20with%20the%20difference.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(G_primal%2C%20griddata%2C%20nodes_flat%2C%20nodes_hilly%2C%20np%2C%20plt)%3A%0A%20%20%20%20import%20matplotlib.colors%20as%20mcolors%0A%0A%20%20%20%20d%20%3D%201000%0A%20%20%20%20harm_key%20%3D%20f%22cc_harmonic_%7Bd%7D_ang%22%0A%20%20%20%20betw_key%20%3D%20f%22cc_betweenness_%7Bd%7D_ang%22%0A%20%20%20%20diff_gdf%20%3D%20nodes_flat.copy()%0A%20%20%20%20diff_gdf%5B%22harm_diff%22%5D%20%3D%20nodes_hilly%5Bharm_key%5D.values%20-%20nodes_flat%5Bharm_key%5D.values%0A%20%20%20%20diff_gdf%5B%22betw_diff%22%5D%20%3D%20nodes_hilly%5Bbetw_key%5D.values%20-%20nodes_flat%5Bbetw_key%5D.values%0A%20%20%20%20_xlim%20%3D%20(438200%2C%20441600)%0A%20%20%20%20_ylim%20%3D%20(4521000%2C%204526000)%0A%20%20%20%20_xs%20%3D%20np.array(%5Bnd%5B%22x%22%5D%20for%20_%2C%20nd%20in%20G_primal.nodes(data%3DTrue)%5D)%0A%20%20%20%20_ys%20%3D%20np.array(%5Bnd%5B%22y%22%5D%20for%20_%2C%20nd%20in%20G_primal.nodes(data%3DTrue)%5D)%0A%20%20%20%20_zs%20%3D%20np.array(%5Bnd%5B%22z%22%5D%20for%20_%2C%20nd%20in%20G_primal.nodes(data%3DTrue)%5D)%0A%20%20%20%20_grid_x%2C%20_grid_y%20%3D%20np.mgrid%5B_xlim%5B0%5D%20%3A%20_xlim%5B1%5D%20%3A%20200j%2C%20_ylim%5B0%5D%20%3A%20_ylim%5B1%5D%20%3A%20200j%5D%0A%20%20%20%20_grid_z%20%3D%20griddata((_xs%2C%20_ys)%2C%20_zs%2C%20(_grid_x%2C%20_grid_y)%2C%20method%3D%22cubic%22)%0A%20%20%20%20_fig%2C%20axes%20%3D%20plt.subplots(3%2C%202%2C%20figsize%3D(13%2C%2019.5)%2C%20dpi%3D150%2C%20gridspec_kw%3D%7B%22hspace%22%3A%200.02%2C%20%22wspace%22%3A%200.02%7D)%0A%20%20%20%20for%20_ax%20in%20axes.flat%3A%0A%20%20%20%20%20%20%20%20_ax.contour(_grid_x%2C%20_grid_y%2C%20_grid_z%2C%20levels%3D15%2C%20colors%3D%22k%22%2C%20linewidths%3D0.3%2C%20alpha%3D0.3)%0A%20%20%20%20%23%20sequential%20centrality%20panels%3A%20OrRd%2C%20edge%20width%20by%20percentile%20rank%2C%20no%20colour%20bar%0A%20%20%20%20for%20_ax%2C%20_gdf%2C%20_col%2C%20_title%20in%20%5B%0A%20%20%20%20%20%20%20%20(axes%5B0%2C%200%5D%2C%20nodes_flat%2C%20harm_key%2C%20f%22Harmonic%20closeness%20%7Bd%7Dm%20(flat)%22)%2C%0A%20%20%20%20%20%20%20%20(axes%5B0%2C%201%5D%2C%20nodes_flat%2C%20betw_key%2C%20f%22Betweenness%20%7Bd%7Dm%20(flat)%22)%2C%0A%20%20%20%20%20%20%20%20(axes%5B1%2C%200%5D%2C%20nodes_hilly%2C%20harm_key%2C%20f%22Harmonic%20closeness%20%7Bd%7Dm%20(hilly)%22)%2C%0A%20%20%20%20%20%20%20%20(axes%5B1%2C%201%5D%2C%20nodes_hilly%2C%20betw_key%2C%20f%22Betweenness%20%7Bd%7Dm%20(hilly)%22)%2C%0A%20%20%20%20%5D%3A%0A%20%20%20%20%20%20%20%20g%20%3D%20_gdf.copy()%0A%20%20%20%20%20%20%20%20if%20%22live%22%20in%20g.columns%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20g%20%3D%20g%5Bg.live%5D.copy()%0A%20%20%20%20%20%20%20%20g%5B%22_r%22%5D%20%3D%20g%5B_col%5D.rank(pct%3DTrue)%0A%20%20%20%20%20%20%20%20g%20%3D%20g.sort_values(%22_r%22)%0A%20%20%20%20%20%20%20%20g.plot(ax%3D_ax%2C%20color%3Dplt.get_cmap(%22OrRd%22)(g%5B%22_r%22%5D)%2C%20linewidth%3D0.15%20%2B%202.25%20*%20g%5B%22_r%22%5D)%0A%20%20%20%20%20%20%20%20_ax.set_title(_title%2C%20loc%3D%22left%22)%0A%20%20%20%20%23%20difference%20panels%3A%20signed%2C%20diverging%2C%20colour%20bar%20kept%0A%20%20%20%20for%20_ax%2C%20_col%2C%20_title%20in%20%5B%0A%20%20%20%20%20%20%20%20(axes%5B2%2C%200%5D%2C%20%22harm_diff%22%2C%20f%22Harmonic%20closeness%20%7Bd%7Dm%20difference%20(hilly%20minus%20flat)%22)%2C%0A%20%20%20%20%20%20%20%20(axes%5B2%2C%201%5D%2C%20%22betw_diff%22%2C%20f%22Betweenness%20%7Bd%7Dm%20difference%20(hilly%20minus%20flat)%22)%2C%0A%20%20%20%20%5D%3A%0A%20%20%20%20%20%20%20%20try%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20_norm%20%3D%20mcolors.TwoSlopeNorm(vcenter%3D0)%0A%20%20%20%20%20%20%20%20%20%20%20%20diff_gdf.plot(ax%3D_ax%2C%20column%3D_col%2C%20cmap%3D%22coolwarm%22%2C%20norm%3D_norm%2C%20legend%3DTrue%2C%20legend_kwds%3D%7B%22shrink%22%3A%200.6%7D)%0A%20%20%20%20%20%20%20%20except%20ValueError%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20diff_gdf.plot(ax%3D_ax%2C%20column%3D_col%2C%20cmap%3D%22coolwarm%22%2C%20legend%3DTrue%2C%20legend_kwds%3D%7B%22shrink%22%3A%200.6%7D)%0A%20%20%20%20%20%20%20%20_ax.set_title(_title%2C%20loc%3D%22left%22)%0A%20%20%20%20for%20_ax%20in%20axes.flat%3A%0A%20%20%20%20%20%20%20%20_ax.set_xlim(_xlim)%0A%20%20%20%20%20%20%20%20_ax.set_ylim(_ylim)%0A%20%20%20%20%20%20%20%20_ax.set_aspect(%22equal%22)%0A%20%20%20%20%20%20%20%20_ax.set_axis_off()%0A%20%20%20%20plt.show()%0A%20%20%20%20return%20(d%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%20Rank%20correlation%0A%0A%20%20%20%20Compute%20Spearman%20rank%20correlations%20and%20mean%20percentage%20changes%20to%20quantify%20how%20much%20elevation%20affects%20the%20centrality%20rankings.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(d%2C%20nodes_flat%2C%20nodes_hilly%2C%20np%2C%20spearmanr)%3A%0A%20%20%20%20print(f%22%7B'Metric'%3A%3C35%7D%20%7B'Spearman%20%CF%81'%3A%3E10%7D%20%7B'Mean%20%25%20change'%3A%3E15%7D%22)%0A%20%20%20%20print(%22-%22%20*%2065)%0A%0A%20%20%20%20for%20metric%2C%20label%20in%20%5B%0A%20%20%20%20%20%20%20%20(%22harmonic%22%2C%20%22Angular%20harmonic%20closeness%22)%2C%0A%20%20%20%20%20%20%20%20(%22betweenness%22%2C%20%22Angular%20betweenness%22)%2C%0A%20%20%20%20%5D%3A%0A%20%20%20%20%20%20%20%20key%20%3D%20f%22cc_%7Bmetric%7D_%7Bd%7D_ang%22%0A%20%20%20%20%20%20%20%20flat_vals%20%3D%20nodes_flat%5Bkey%5D.values%0A%20%20%20%20%20%20%20%20hilly_vals%20%3D%20nodes_hilly%5Bkey%5D.values%0A%0A%20%20%20%20%20%20%20%20mask%20%3D%20(flat_vals%20%3E%200)%20%26%20(hilly_vals%20%3E%200)%0A%20%20%20%20%20%20%20%20rho%2C%20_%20%3D%20spearmanr(flat_vals%5Bmask%5D%2C%20hilly_vals%5Bmask%5D)%0A%20%20%20%20%20%20%20%20pct_change%20%3D%20np.mean(np.abs(hilly_vals%5Bmask%5D%20-%20flat_vals%5Bmask%5D)%20%2F%20flat_vals%5Bmask%5D)%20*%20100%0A%0A%20%20%20%20%20%20%20%20print(f%22%7Blabel%7D%20%7Bd%7Dm%7B''%3A%3C5%7D%20%7Brho%3A%3E10.4f%7D%20%7Bpct_change%3A%3E14.1f%7D%25%22)%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%20Summary%0A%0A%20%20%20%20This%20notebook%20demonstrated%20how%20elevation%20(Z%20coordinates)%20affects%20centrality%20results.%20When%203D%20geometries%20are%20present%2C%20cityseer%20automatically%20applies%20Tobler's%20hiking%20function%2C%20which%20penalises%20uphill%20travel%20and%20slightly%20rewards%20gentle%20downhill%20slopes.%20The%20effect%20is%20asymmetric%2C%20since%20walking%20uphill%20costs%20more%20than%20walking%20downhill%2C%20and%20this%20shifts%20betweenness%20patterns%20in%20hilly%20terrain.%0A%0A%20%20%20%20**Next%20steps%3A**%0A%0A%20%20%20%20-%20To%20calculate%20angular%20centralities%20without%20elevation%2C%20see%20%5BAngular%20Centrality%5D(https%3A%2F%2Fcityseer.benchmarkurbanism.com%2Fexamples%2Fcentrality%2Fgpd-angular-centrality).%0A%20%20%20%20-%20To%20calculate%20metric%20(shortest-path)%20centralities%2C%20see%20%5BMetric%20Centrality%5D(https%3A%2F%2Fcityseer.benchmarkurbanism.com%2Fexamples%2Fcentrality%2Fgpd-metric-centrality).%0A%20%20%20%20-%20To%20compute%20centralities%20directly%20from%20OpenStreetMap%20data%2C%20see%20%5BOSM%20Centrality%5D(https%3A%2F%2Fcityseer.benchmarkurbanism.com%2Fexamples%2Fcentrality%2Fosm-centrality).%0A%20%20%20%20%22%22%22)%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
3f1b8e2e0b48e5f2c27c8cb8828aed98