Skip to content
Back to skills

Graph Conversion

ASecurity

"Convert city2graph GeoDataFrame graph tables to and from NetworkX,

  • 247 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 8, 2026
toolspythonrustgobashnodeapi

Works with

  • terminal
  • api

Security analysis

A100/100

Pro scans all 4 files and shows the line behind each finding

Scanned September 8, 2026

npx -y skills add VectorSpaceLab/AREX-Skill --skill graph-conversion --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Graph Conversion?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Graph Conversion
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/vectorspacelab-graph-conversion/badge)](https://www.skillsdirectory.com/skills/vectorspacelab-graph-conversion)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: "graph-conversion"
description: "Convert city2graph GeoDataFrame graph tables to and from NetworkX,
  rustworkx, and optional PyTorch Geometric representations, including
  heterogeneous relations, metadata, features, labels, directionality,
  multigraph keys, and metapaths."
metadata:
  disco-role: operating
disable-model-invocation: true
license: BSD 3-Clause
---

# Graph conversion

Use this skill when the task is to move a city2graph graph between its
GeoDataFrame-first representation and NetworkX, rustworkx, or PyTorch
Geometric (PyG), or when constructing derived heterogeneous relations with
metapaths. Keep the GeoDataFrame tables as the source of truth: graph objects
use integer/positional IDs and attached metadata, while the GeoDataFrame
indexes carry the original node and edge identifiers.

## Fast route

1. **Classify the input.** Use one GeoDataFrame for a homogeneous graph, or
   dictionaries for a heterogeneous graph. The heterogeneous node dictionary
   is keyed by node type strings; the edge dictionary is keyed by
   `(source_type, relation_type, target_type)` tuples.
2. **Validate the table contract before conversion.** Node IDs are the node
   GeoDataFrame index values. Edge endpoints are the first two levels of an
   edge `MultiIndex`; a third level is an edge key. All non-empty tables in a
   graph must use a consistent CRS.
3. **Choose the target.** Use `gdf_to_nx`/`nx_to_gdf` for graph algorithms and
   attribute-preserving spatial round trips; use `gdf_to_pyg`/`pyg_to_gdf` for
   tensor workflows; use `nx_to_pyg` or `pyg_to_nx` when bridging formats; use
   `nx_to_rx`/`rx_to_nx` for rustworkx algorithms.
4. **Make semantics explicit.** Decide `directed`, `multigraph`, `keep_geom`,
   feature/label columns, and (for heterogeneous PyG) reverse edge types before
   converting. The default PyG conversion is undirected and therefore
   symmetrizes safe edges.
5. **Verify the result.** Check graph class, node/edge counts, edge-type stores,
   tensor shapes, `graph_metadata`, CRS, index names, and whether a round trip
   is expected to preserve exact geometry or only centroid/straight-line
   geometry.

Detailed signatures and return-shape rules are in
[references/api-reference.md](references/api-reference.md). Heterogeneous and
PyG-specific semantics are in
[references/heterogeneous-and-pyg.md](references/heterogeneous-and-pyg.md).
Error diagnosis and minimal probes are in
[references/troubleshooting.md](references/troubleshooting.md).

## Canonical GeoDataFrame contract

### Homogeneous tables

```python
nodes: geopandas.GeoDataFrame | None
edges: geopandas.GeoDataFrame | None
```

- A node table's index is the node identifier space. A named `Index` is
  preserved by the NetworkX/PyG metadata; a `MultiIndex` is also supported.
- A non-empty edge table used by PyG must have a `pandas.MultiIndex` with at
  least two levels: `(source_id, target_id)`. The first two values are looked
  up in `nodes.index`. A third level `(source, target, key)` identifies a
  multigraph edge and is preserved.
- NetworkX conversion can infer endpoints from the first and last coordinates
  of LineString geometries. If edges have no geometry, use a two-level edge
  MultiIndex and provide the node table; endpoint IDs are then resolved from
  the node index. Unknown endpoint IDs are dropped with a warning in this
  path.
- Node geometries may be Points, polygons, or other valid geometries; node
  positions are represented by centroids when a graph/tensor format requires
  coordinates. Edge geometries normally are LineString or MultiLineString.
  Homogeneous NetworkX conversion can accept geometryless edges when their
  first two index levels identify endpoints and a node frame is supplied;
  geometryless nodes cannot provide positions.
- Empty tables are allowed by the conversion validators. In particular, an
  empty edge frame may lack a MultiIndex for PyG; a non-empty one may not.
- CRS is metadata, not a node/edge attribute. Non-empty node and edge tables
  must have equal CRS values. A missing CRS is accepted, but distances,
  lengths, and geographic-coordinate computations need caller judgment.

### Heterogeneous tables

```python
nodes = {"building": building_nodes, "road": road_nodes}
edges = {
    ("building", "connects_to", "road"): building_road_edges,
    ("road", "links_to", "road"): road_road_edges,
}
```

- Every node-type key is a string. Every edge-type key is a three-string tuple.
- Each edge frame's first index level contains IDs from its source node-type
  frame; its second level contains IDs from its destination node-type frame.
- Node IDs may overlap between types because type namespaces distinguish them.
- Heterogeneous NetworkX nodes receive `node_type`; heterogeneous edges receive
  `edge_type`. Heterogeneous PyG uses `HeteroData` stores keyed by node and
  edge types.
- Validate all table CRSs together. Do not silently combine frames in
  different CRSs merely because their index values happen to match.

## NetworkX and rustworkx route

### GeoDataFrame ↔ NetworkX

```python
from city2graph import gdf_to_nx, nx_to_gdf

G = gdf_to_nx(nodes=nodes, edges=edges,
              keep_geom=True, multigraph=False, directed=False)
nodes_back, edges_back = nx_to_gdf(G)
```

`gdf_to_nx` chooses `Graph`, `DiGraph`, `MultiGraph`, or `MultiDiGraph` from
`directed` and `multigraph`. It assigns internal integer node IDs when a node
frame is supplied, stores the original index in `_original_index`, stores
centroid coordinates in `pos`, and carries ordinary columns as attributes.
Edge attributes include `_original_edge_index`; multigraph edges use their
third index level as the NetworkX key, or generated row keys when needed.
Heterogeneous node IDs are offset into one NetworkX namespace, but the original
IDs and type metadata make reconstruction possible.

`nx_to_gdf` expects graph-level metadata and node positions or geometry. It
returns a homogeneous GeoDataFrame (or a pair of node/edge GeoDataFrames) for
homogeneous metadata, and typed dictionaries for heterogeneous metadata. With
`nodes=False` or `edges=False`, homogeneous output can be a single frame;
heterogeneous output remains a `(nodes_dict, edges_dict)` pair with an empty
side. Requesting neither raises `ValueError`.

When an ordinary NetworkX graph lacks `pos`, `nx_to_gdf` can populate it from
`x` and `y` (the default `set_missing_pos_from=("x", "y")`) or from one
2-element coordinate attribute. `validate_nx` is stricter: it rejects empty
node/edge graphs, requires `crs` and `is_hetero` metadata, and requires every
node to have `pos` or `geometry`; heterogeneous graphs additionally need type
metadata and per-node/per-edge type attributes.

### NetworkX ↔ rustworkx

```python
from city2graph import nx_to_rx, rx_to_nx

rx_graph = nx_to_rx(G)
G_again = rx_to_nx(rx_graph)
```

`nx_to_rx` returns `rustworkx.PyGraph` or `PyDiGraph`, preserving directed and
multigraph status and copying `G.graph` to `rx_graph.attrs`. It stores each
original NetworkX node ID in the node payload key `__nx_node_id__`; for a
multigraph it stores the NetworkX edge key as `__nx_edge_key__` in the edge
payload. `rx_to_nx` uses those sentinels to restore IDs and keys, and restores
graph attributes. Raw rustworkx payloads without sentinels are retained under
`payload`; this is a compatibility conversion, not a GeoDataFrame conversion.

## PyG route

PyG is optional. Before invoking any PyG function, use
`city2graph.is_torch_available()` (or `city2graph.graph.is_torch_available()`).
If either PyTorch or `torch_geometric` is absent, the public PyG functions raise
an `ImportError` with the message that PyTorch and PyTorch Geometric are
required. Install/test a CPU-compatible PyTorch + PyG environment separately;
do not make a graph conversion skill depend on CUDA. A minimal gate is:

```bash
python -c "import torch, torch_geometric; print(torch.__version__, torch_geometric.__version__)"
```

Then use:

```python
from city2graph import gdf_to_pyg, pyg_to_gdf

data = gdf_to_pyg(
    nodes, edges,
    node_feature_cols=["population"],
    node_label_cols=["class_id"],
    edge_feature_cols=["length"],
    device="cpu",
    keep_geom=True,
    directed=True,
)
nodes_back, edges_back = pyg_to_gdf(data)
```

A homogeneous input produces `torch_geometric.data.Data`; a dictionary input
produces `HeteroData`. PyG maps each node index value to a sequential integer
position. Endpoint IDs are resolved against the node index; duplicate node IDs
resolve to the last occurrence for connectivity. Edges whose endpoints are
absent are omitted from the tensor. `graph_metadata` is mandatory for
reconstruction and validation; call `validate_pyg(data)` after constructing or
mutating a PyG object.

Features and labels are column-driven. Homogeneous specs must be lists;
heterogeneous specs must be dictionaries keyed by node type or full edge-type
tuple. Only existing numeric columns are included, in requested order;
nonexistent and nonnumeric requested columns are ignored, so a zero-width
`x`, `y`, or `edge_attr` is a valid result. Positions are centroid coordinates.
`device=None` selects CUDA when available, otherwise CPU; explicitly selecting
unavailable CUDA raises a clear `ValueError`. `dtype` controls floating tensor
creation and position tensors.

## Geometry and round-trip policy

- `keep_geom=True` (the default for PyG) serializes original geometries as WKB
  in `graph_metadata`. A PyG → GeoDataFrame round trip can therefore preserve
  curved or non-straight edge geometries and null geometry values when metadata
  remains intact.
- `keep_geom=False`, or a graph created without stored geometry metadata, falls
  back to positions. Nodes become Points and edges become straight LineStrings
  between source and destination positions. This is not an exact geometry
  round trip; an L-shaped or curved input edge will be simplified.
- NetworkX conversion stores geometry attributes when `keep_geom=True`; when
  absent, reconstructed edge geometry is derived from endpoint `pos`.
- Geographic CRS positions are computed through an estimated projected CRS for
  centroid calculation and transformed back to the source CRS. This improves
  centroid calculation but does not make geographic distances metrically valid.
- Preserve `graph_metadata`/`G.graph` when serializing or passing objects
  between stages. A manually constructed bare NetworkX or PyG object may not
  contain enough index, CRS, type, or geometry information to reconstruct the
  original tables.

## Directionality, undirected canonicalization, and multigraphs

### PyG directionality

For a homogeneous PyG conversion, `directed=False` is the default. Each
non-self-loop edge `(u, v)` is stored as `(u, v)` and `(v, u)`; self-loops are
not duplicated. Edge attributes and serialized edge geometries are duplicated
in the same order. Reconstruction uses metadata to deduplicate the generated
reverse rows back to the original edge table.

For `directed=False`, a non-empty edge frame must not already contain both
`(u, v)` and `(v, u)` for the same undirected identity, and it must not contain
parallel rows with the same unordered identity unless it has an explicit key
level or `multigraph=True`. Such inputs raise `ValueError` rather than silently
losing attributes or geometry. If a directed source exported reciprocal rows,
choose one of:

```python
from city2graph import canonicalize_edges
edges_one_per_pair = canonicalize_edges(edges, duplicates="first")
data = gdf_to_pyg(nodes, edges_one_per_pair, directed=False)
```

or use `directed=True` to preserve both orientations. `canonicalize_edges`
changes index orientation only and does not reverse geometries. For a keyed
multigraph, use `duplicates="key"` or provide a three-level index, and pass
`multigraph=True` when two-level parallel rows need generated keys.

### Heterogeneous directionality

For HeteroData, `directed` may be one bool or a **complete** dictionary with
exactly one bool per supplied edge-type key. Missing or extra keys raise
`ValueError`.

- Same-type undirected relation `(type, relation, type)`: symmetrized in its
  store; reconstruction deduplicates it using metadata.
- Cross-type undirected relation `(src, relation, dst)`: cannot be symmetrized
  in place because source and destination stores differ. city2graph creates a
  generated reverse store, by default `(dst, "rev_<relation>", src)`, with a
  flipped `edge_index` and cloned edge attributes. Generated stores are PyG
  message-passing artifacts and are skipped by `pyg_to_gdf`.
- `reverse_edge_types=None` is strict and raises for any non-empty undirected
  cross-type relation. A mapping supplies explicit reverse types; endpoints
  must be exactly reversed, and the result must not collide with a user edge
  type.
- `graph_metadata.original_edge_types`, `reverse_edge_types`, and
  `generated_reverse_edge_types` record this provenance. Do not treat every
  `data.edge_types` entry as an original input table.

`nx_to_pyg` follows NetworkX class directionality by default: Graph/MultiGraph
become undirected PyG (and are symmetrized), while DiGraph/MultiDiGraph remain
directed. Its `directed` argument is an explicit override. `pyg_to_nx` must
collapse per-edge-type directionality into one NetworkX graph class; mixed
heterogeneous directionality emits a warning and uses an undirected graph.

## Metapaths

Metapaths operate on heterogeneous tables or a city2graph-compatible
heterogeneous NetworkX graph. The primary form is:

```python
from city2graph import add_metapaths

nodes_out, edges_out = add_metapaths(
    (nodes_dict, edges_dict),
    sequence=[
        ("building", "connects_to", "road"),
        ("road", "links_to", "road"),
    ],
    edge_attr="travel_time",
    edge_attr_agg="sum",       # "sum", "mean", or a callable
    directed=False,
)
```

The sequence must contain at least two edge-type tuples. Consecutive hop
endpoints must join. If `directed=False` and an exact hop type is absent, the
reverse endpoint type with the same relation name is considered. A missing hop
then raises `KeyError`. The result key is
`(start_type, "metapath_0", end_type)`, or uses `new_relation_name`.
`weight` counts joined paths; requested numeric edge attributes are reduced
along each path and then across terminal pairs. The result gets a straight
terminal-to-terminal geometry and normalized index names. Undirected path
materialization canonicalizes path orientation and edge identity, so mirrored
traversals are not counted twice; distinct keyed parallel edges remain distinct.
`trace_path=True` is retained for compatibility but is currently a no-op apart
from debug logging. `as_nx=True` returns NetworkX and records generated
metapath metadata under `G.graph["metapath_dict"]`.

For threshold-based connectivity, use `add_metapaths_by_weight`. It requires
`weight`, `threshold`, and an `endpoint_type`; it uses weighted Dijkstra over
all or selected edge types and emits a same-type relation named
`connected_within_<min>_<max>` unless customized. The threshold band is
inclusive, self-connections are excluded, and undirected output canonicalizes
terminal pairs. Missing/invalid endpoint or filtered edge data can return the
original graph unchanged; validate the result key before assuming a new table
was created.

## Completion checklist

- [ ] Input shape is homogeneous or heterogeneous consistently.
- [ ] Node indexes are the intended stable IDs; edge first two index levels
      reference them exactly.
- [ ] All non-empty frames have the same CRS.
- [ ] `directed`, reciprocal rows, self-loops, and multigraph keys agree with
      the intended semantics.
- [ ] Feature/label specs match homogeneous list versus heterogeneous dict
      rules and requested columns are numeric.
- [ ] PyG availability/device was probed before a PyG conversion.
- [ ] `keep_geom` was chosen with the exact-versus-straight geometry trade-off
      in mind.
- [ ] Result class, counts, metadata, edge types, tensor shapes, and a
      round-trip sample were checked.

Files in this skill

  • SKILL.md15.7 KB
  • references/api-reference.md9.3 KB
  • references/heterogeneous-and-pyg.md8.9 KB
  • references/troubleshooting.md11.9 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.