城市通常被视为复杂的系统——或许是人类创造的最复杂的系统之一。走在任何一条街道上,你都会被层层叠叠的互联网络所包围:脚下的道路连接着高速公路和自行车道,头顶的输电线路汇入庞大的电网,地下的管道输送着水和数据。然而,在这些有形的基础设施之外,还存在着更为丰富的社会互动网络。朋友们在社区咖啡馆相聚,形成社交中心;专业人士在联合办公空间拓展人脉,形成商业集群;社区围绕着当地的学校和公园组织起来,构建起定义街区的无形纽带;社交媒体上的帖子都带有位置标签,每一笔信用卡交易都在顾客和商家之间建立起联系,每一次通勤都勾勒出一条连接家和工作场所的日常线索。

城市之间这种相互关联的特性使它们成为图论的理想对象。用计算机科学术语来说,图就是由节点(事物)和边(关系)连接的集合。仔细想想,城市本身就是图:
·形态学
交叉点(节点)由街道段(边)连接
·交通
枢纽(节点)通过公交/火车服务(边)连接
·流动
区域(节点)通过移动/迁移次数(边)连接
·邻近性:
对象(节点)彼此靠近(边);
边界(节点)彼此相邻(边)。
正如托布勒地理学第一定律所提醒我们的那样,“万物皆相互关联,但近的事物比远的事物关联更密切”——这一原则实际上要求我们采用基于图的建模方法。
机遇与挑战
基于图的城市建模前景诱人,但要实现这一目标,既需要把握激动人心的机遇,也需要克服棘手的挑战。一方面,我们从未像现在这样拥有如此丰富多样的城市数据。OpenStreetMap和Overture Maps 等开源项目提供了详细的街道拓扑结构和建筑轮廓。各地方政府和公交公司的GTFS数据源提供了实时公交时刻表,可以将静态的道路网络转化为动态的多模式图。兴趣点 (POI) 揭示了各种设施如何聚集并争夺用户注意力。人口普查划定的行政边界定义了塑造治理和资源分配的无形界限。此外,来自手机和智能卡的匿名化出行数据也日益丰富,揭示了人们在城市中实际的出行路径——将理论上的连接转化为加权的定向流量。

一个简单的异构图,展示了各种便利设施和街道。图的底部描述了便利设施通过街道跳跃的元路径。
这些多元化的数据源为构建异构图提供了可能,从而更好地捕捉城市的复杂性。我们不再孤立地分析街道,而是将城市建模为相互关联的系统,其中单个地点可能同时是街道网络中的一个节点、交通图中的一个站点以及社会结构中的一个点。咖啡馆不再仅仅是一个目的地,而是一个连接点——通过人行道、自行车道以及常客的社交网络,将住宅区与商业区连接起来。这种多层次的方法使得在城市尺度上进行复杂的网络分析成为可能:计算考虑实际人行道网络的步行友好度评分,衡量新的公交线路如何改变可达性模式,或者模拟街道封闭如何影响多式联运系统。

Google Research 于 2024 年发布的 GNN(消息传递框架)可视化
或许最令人兴奋的是图神经网络(GNN)从这些城市图中学习的潜力。GNN 可以捕捉城市网络的拓扑结构及其空间背景,学习编码某些路口拥堵原因或某些社区繁荣而另一些社区衰落原因的表征。与将位置视为独立数据点的传统模型不同,GNN 理解一个地方的特征源于其连接——这使得它们天然适合预测从交通模式到中产阶级化风险等各种情况。
尽管存在诸多机遇,但仍存在重大挑战。城市数据支离破碎——街道网络采用一种格式,公交时刻表采用另一种格式,人口统计数据又采用另一种格式,每种格式都有其自身的坐标系、标识符和更新周期。虽然OSMnx可以优雅地处理 OpenStreetMap 街道网络,但整合非街道数据集仍然十分繁琐。即使在街道数据领域,像 Overture Maps 这样前景广阔的替代方案也仍然无法通过便捷的程序访问,需要自定义解析器和转换,而很少有研究人员有时间构建这些工具。
城市连通性的定义在实践中往往难以界定。我们如何判断一个公交车站与人行道“连接”——是最近的点、缓冲区内的距离,还是实际的步行路径?这些并非简单的技术细节,而是影响后续所有分析的根本性建模决策,然而,目前对于如何一致地编码这些关系却缺乏共识。每个研究项目都在重复造轮子,使得结果的比较和借鉴他人的工作几乎成为不可能。
或许最大的障碍在于数据结构之间频繁的转换。一会儿需要GeoPandas来获取人口普查区的区域统计数据,一会儿又需要NetworkX来计算街道网络的最短路径,一会儿又需要PyTorch 的几何张量来训练基于图神经网络的 GeoAI——每一次转换都需要精心的数据处理,这反而掩盖了实际的分析过程。这种计算上的繁琐操作造成了陡峭的学习曲线,使得城市图分析只能被少数技术专家掌握,而它本应面向那些最了解城市却可能缺乏编程经验的城市规划者、政策制定者和社区倡导者开放。
在此背景下,city2graph应运而生。它是一款可以将任何地理空间数据集转换为图的工具。根据分析目的的不同,它提供了一个集成且可互换的接口——GeoPandas用于空间分析,NetworkX用于网络分析,PyTorch Geometric用于图神经网络(GNN)。

这个新软件包并非要取代像 OSMnx 和 libpysal 这样现有的优秀工具,而是一个全面的封装器。它提供了一个连接来自各种来源的不同类型图表的网关。
快速入门
您可以通过PyPI和conda-forge安装 city2graph 。
如果您使用 PyTorch(几何)GNN 依赖项,您可以根据硬件进行指定。pip install "city2graph[cpu]"
GPU:
pip install "city2graph[cu128]"
# 支持:cu118、cu124、cu126、cu128
conda-forge 已不再官方支持 PyTorch,因此您需要通过 conda-forge 单独安装它。
conda 安装 city2graph -c conda-forge
中央处理器:
# 安装 city2graph
conda install -c conda-forge city2graph
# 然后安装 PyTorch 和 PyTorch Geometric
conda install -c conda-forge pytorch pytorch_geometric
GPU:
# 安装 city2graph conda install -c conda -
forge city2graph
# 然后安装支持 CUDA 的 PyTorch
conda install -c conda-forge pytorch= 2.7.1 =*cuda128* conda install -c conda-forge pytorch_geometric
验证安装:
导入city2graph为c2g
打印(c2g.__version__)
核心功能
city2graph 的核心功能是实现 gdf / nx / pyg 之间数据对象的互换转换。
作为准备工作,我们将生成 1000 个随机点:
import geopandas as gpd
from shapely import Point
import numpy as np
import city2graph as c2g
# 1) 创建一些示例点(EPSG:3857 用于度量距离)
np.random.seed( 42 ) # 为了可复现性
num_points = 1000
x_coords = np.random.rand(num_points) * 1000 # 随机生成 0 到 1000 之间的 x 坐标
y_coords = np.random.rand(num_points) * 1000 # 随机生成 0 到 1000 之间的 y 坐标
gdf = gpd.GeoDataFrame(
{ "name" : [ f"Point_ {i} " for i in range (num_points)]},
geometry=[Point(x, y) for x, y in zip (x_coords, y_coords)],
crs=然后,让我们用 KNN 构建一个简单的邻近图:
然后,我们创建一个k=10的KNN图。
# 2) 邻近图(基于 KNN)
节点、边 = c2g.knn_graph(
gdf,
distance_metric= "欧氏距离" ,
k= 10 ,
as_nx= False )
现在我们有两个包含节点和边的 GeoDataFrame:



边中的多级索引对应于节点的索引。节点中的原始数据列(包括几何形状)将保持不变。
我们可以将此对象转换为 nx 和 pyg 格式:
# 3) 转换为您喜欢的框架
# NetworkX
nx_graph = c2g.gdf_to_nx(nodes, edges)
# PyTorch Geometric
pyg_graph = c2g.gdf_to_pyg(nodes, edges)
对于异构图,数据将由 nodes_dict(dict[str, geopandas.GeoDataFrame])和 edges_dict(dict[tuple[str, str, str], geopandas.GeoDataFrame])组成。
从异构图中,可以利用几何形状生成元路径:
# 定义元路径:amenity -> segment -> segment -> amenity(本例中为 3 跳)
metapaths = [[( "amenity" , "is_nearby" , "segment" ),
( "segment" , "connects " , "segment" ),
( "segment" , "connects" , "segment" ),
( "segment" , "is_nearby" , "amenity" )]]
# 添加元路径派生的边,通过街道网络连接设施
nodes_with_metapaths, edges_with_metapaths = c2g.add_metapaths(
(combined_nodes, combined_edges),
metapaths,
edge_attr= "distance_m" ,
edge_attr_agg= "sum"
)

真实案例
目前,city2graph 支持以下数据域:
形态学
morphological_nodes, morphological_edges = c2g.morphological_graph(
buildings_gdf,
segments_gdf,
center_point,
distance= 500
)

运输
sample_gtfs_path = Path( "./itm_london_gtfs.zip" )
gtfs_data = c2g.load_gtfs(sample_gtfs_path)
travel_summary_nodes, travel_summary_edges = c2g.travel_summary_graph(
gtfs_data, calendar_start= "20250601" , calendar_end= "20250601" )

流动性
# 加载区域(例如,MSOA 边界)和 OD 矩阵数据
od_data = pd.read_csv( "od_matrix.csv" )
zones_gdf = gpd.read_file( "zones.gpkg" )
# 将 OD 矩阵转换为图
od_nodes, od_edges = c2g.od_matrix_to_graph(
od_data,
zones_gdf,
source_col= "origin" ,
target_col= "destination" ,
weight_cols=[ "flow" ],
zone_id_col= "zone_id" ,
directed= False
)

邻近性
wax_l1_nodes, wax_l1_edges = c2g.waxman_graph(poi_gdf, distance_metric= " manhattan" , r0 = 100 , beta = 0.5 ) wax_l2_nodes, wax_l2_edges = c2g.waxman_graph(poi_gdf , distance_metric= "euclidean" , r0= 100 , beta= 0.5 ) wax_net_nodes, wax_net_edges = c2g.waxman_graph(poi_gdf, distance_metric= "network" , r0= 100 , beta= 0.5 , network_gdf=segments_gdf.to_crs(epsg= 6677 ))

nodes_dict = {
"restaurants" : poi_gdf,
"hospitals" : hospital_gdf,
"commercial" : commercial_gdf
}
# 使用 KNN 方法生成层间邻近边
proximity_nodes, proximity_edges = c2g.bridge_nodes(
nodes_dict,
proximity_method= "knn" ,
k= 5 ,
distance_metric= "euclidean"
)

# 从多边形区域构建邻接图(皇后图或车图)
wn_q_nodes, wn_q_edges = c2g.contiguity_graph(
wards_gdf,
contiguity= "queen"
, # 或 "rook"
distance_metric= "euclidean" # 或 "manhattan", "network"
)
# 将点要素(例如,兴趣点、站点)链接到包含它们的多边形(例如,选区)
nodes_dict, edges_dict = c2g.group_nodes(
polygons_gdf=wards_gdf,
points_gdf=poi_gdf,
predicate= "covered_by" # 包含边界点;可选值:"within", "contains"
)
# 将邻接边和分组边合并成一个异构图
combined_nodes = {
"wards" : wn_q_nodes,
"poi" : nodes_dict[ "poi" ]
}
combined_edges = {
( "wards" , "is_contiguous_with" , "wards" ): wn_q_edges[( "wards" , "is_contiguous_with" , "wards" )],
( "wards" , "covers" , "poi" ): edges_dict[( "wards" , "covers" , "poi" )]
}
# 转换为 PyG 异构数据
hetero_graph = c2g.gdf_to_pyg(combined_nodes, combined_edges)

伦敦公交车站分组节点的邻接图