You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
"""Creates a NetworkX Graph from GeoDataFrame consisting of LineString objects.
377
+
378
+
Parameters
379
+
----------
380
+
381
+
gdf : GeoDataFrame
382
+
GeoDataFrame containing the LineString data.
383
+
384
+
"""
385
+
386
+
# Create the NetworkX graph
387
+
graph = nx.Graph()
388
+
389
+
# Generate edge dictionary
390
+
for edge in gdf.itertuples():
391
+
coords = edge.geometry.coords
392
+
393
+
# Get first and last node of the edge (excluding vertices)
394
+
first, last = coords[0], coords[-1]
395
+
396
+
# Edge attributes
397
+
edge_attr = edge._asdict()
398
+
399
+
graph.add_edge(first, last, **edge_attr)
400
+
401
+
# Generate node attributes
402
+
node_attrs = {node: {"coords": node, "x": node[0], "y": node[1]} for node in graph.nodes}
403
+
nx.set_node_attributes(graph, node_attrs)
404
+
405
+
# Relabel the indices
406
+
graph = nx.convert_node_labels_to_integers(graph)
407
+
408
+
# Add some useful attributes
409
+
graph.graph['crs'] = gdf.crs
410
+
411
+
return graph
412
+
```
413
+
414
+
Next we will break it down few steps at a time to understand what happens here. Let's start by investigating what happens inside the loop:
415
+
416
+
```python
417
+
graph = nx.Graph()
418
+
419
+
for edge in streets.itertuples():
420
+
# Get the coordinates
421
+
coords = edge.geometry.coords
422
+
423
+
# Get first and last node of the edge (excluding vertices)
424
+
first, last = coords[0], coords[-1]
425
+
426
+
# Get the edge attributes
427
+
edge_attributes = edge._asdict()
428
+
429
+
# Add to the graph
430
+
graph.add_edge(first, last, **edge_attributes)
431
+
break
432
+
```
433
+
434
+
Now we iterated over one edge in our street network and stopped the loop to be able to investigate what our variables contain. The `coords` variable contain the coordinates of all the vertices in the first edge of our street network:
435
+
436
+
```python
437
+
list(coords)
438
+
```
439
+
440
+
As we can see, there are 3 vertices in this `LineString` object representing a given street. When constructing the graph topology, we only care about the `nodes` of the street segment geometry (i.e. the first and last coordinates of the geometry). This means that in case there are vertices between the nodes, those will not be taken into consideration in the topology of the graph (unless specifically needed for some special use case). Thus, the network topology itself does not need to have the full geometry of the street segments for it to work.
441
+
442
+
```python
443
+
# Create a GeoDataFrame out of the vertices
444
+
vertices = gpd.GeoDataFrame(geometry=[Point(coordpair) for coordpair in coords])
445
+
446
+
fig, ax = plt.subplots()
447
+
streets.iloc[0:1].plot(ax=ax)
448
+
vertices.plot(ax=ax, color=["r", "b", "r"])
449
+
```
450
+
451
+
_**Figure 8.X.** Only the nodes (in red) will be used to construct the edge for a given network topology._
452
+
453
+
Considering only the nodes and ignoring the vertices has also benefits as doing this reduces the size of the graph and makes it faster to run any analyses on it. Thus, we only take the first and last coordinate-pair of the edge geometry which we will use as nodes:
454
+
455
+
```python
456
+
print("First node:", first)
457
+
print("Last node:", last)
458
+
```
459
+
460
+
Although the network topology only considers the nodes, this does not mean that you would loose the actual geometries of the street network, as we can still store the full geometry as an edge attribute of our graph. The `edge_attributes` variable contains all the associated information from the given row in our `GeoDataFrame` as a dictionary:
461
+
462
+
```python
463
+
edge_attributes
464
+
```
465
+
466
+
When we call the `graph.add_edge(first, last, **edge_attributes)`, we add this edge to the given `graph` in which the `**edge_attributes` command unpacks the values of the dictionary and inserts them as attributes for the given edge. Thus, when we investigate the contents of the edges at this point in time, we will see that the actual `geometry` is also stored for the edge:
467
+
468
+
```python
469
+
graph.edges.data()
470
+
```
471
+
472
+
At this point, you might wonder what happened with the `nodes` as we did not specifically add them to the graph in a similar manner as in our earlier examples? We can investigate how the `nodes` look like at this stage:
473
+
474
+
```python
475
+
graph.nodes.data()
476
+
```
477
+
478
+
As we can see, `networkx` actually adds the nodes automatically to the graph when we call the `.add_edge()` method based on the nodes provided to construct a given edge. However, as we can see from the nodes' data above, these nodes do not contain any information about the nodes in the nodes attributes as it is only an empty dictionary at this stage. This is something that we can handle afterwards as it is possible to set the node attributes also after the topology has been constructed based on the edges alone. To do this, we can e.g. parse the coordinates of the nodes and store that information as node attributes using the `nx.set_node_attributes()` as follows:
479
+
480
+
```python
481
+
# Create a dictionary that contain the node attributes
482
+
node_attrs = {node: {"coords": node, "x": node[0], "y": node[1]} for node in graph.nodes}
483
+
nx.set_node_attributes(graph, node_attrs)
484
+
```
485
+
486
+
```python
487
+
graph.nodes.data()
488
+
```
489
+
490
+
As we can see, now the `nodes` of our graph includes three attributes that provide information about the location of the nodes: `coords`, `x` and `y`.
491
+
492
+
Finally, you might have noticed that the `ids` for the nodes in our graph are quite cumbersome as they basically represent the exact coordinates of the nodes. Luckily, it is easy to relabel the node ids into a format that is easier to use and understand, using simple integer values as the ids. We can do this by using the `nx.convert_node_labels_to_integers()` function as follows:
493
+
494
+
```python
495
+
graph = nx.convert_node_labels_to_integers(graph)
496
+
```
497
+
498
+
```python
499
+
graph.edges.data()
500
+
```
501
+
502
+
```python
503
+
graph.nodes.data()
504
+
```
505
+
506
+
As we can see, now the ids for the nodes were altered from long coordinate tuples into simple integers, such as `0` and `1`, which are much easier to understand and use if you e.g. want to select specific node from the graph.
507
+
508
+
As a very last thing in our `gdf_to_graph()` function, we add a custom attribute to our graph where we store the coordinate reference system information of the input `GeoDataFrame` which can be useful information when using the given graph for analysis with other datasets:
509
+
510
+
```python
511
+
graph.graph["crs"] = streets.crs
512
+
graph.graph["crs"]
513
+
```
514
+
515
+
That's it! This is how you can create an undirected graph based on a given `GeoDataFrame` that consists of `LineString` objects. The input data we used here represents streets, but the input data can basically be about anything as long as the geometries of the input data are represented as `LineString` objects and the data itself does have a network-like structure. In a similar manner, you could represent e.g. rivers, pipelines, power lines, social networks etc.
516
+
517
+
Let's finally use our `gdf_to_graph()` function and create a full network topology based on our `streets``GeoDataFrame`:
518
+
519
+
```python
520
+
G = gdf_to_graph(streets)
521
+
522
+
positions = {node: (attrs["x"], attrs["y"]) for node, attrs in G.nodes.data()}
523
+
524
+
nx.draw(G,
525
+
pos=positions,
526
+
node_color="red",
527
+
node_size=0.5,
528
+
)
529
+
```
530
+
531
+
### Directed graph using a GeoDataFrame
532
+
533
+
534
+
Now as we have learned how to create a simple undirected graph based on `LineString` geometries, we will continue and expand the previous example to construct a directed graph topology that considers the permitted direction of movement along the streets. When working with street network data and analyzing e.g. the travel times or distances by car, it is necessary to take into consideration one-way streets as those are extremely common especially in larger cities. On these streets, a person can only drive to one direction, and if you would need to travel to opposite direction, making an U-turn is not possible but you would need to find another path using other streets of the network. Thus, understandingly one-way streets can have significant influence on the optimal routes between given locations that need to be considered when doing network analysis. Otherwise, our analyses and results will likely provide incorrect and unrealistic results that could even cause dangerous situations if e.g. a car navigator would guide you to a one-way street where the traffic flows against your travel direction.
535
+
371
536
The `direction` column includes information about the allowed direction of the traffic flow, i.e. whether the traffic is permitted in both directions or whether it is a oneway street. In this street network dataset the values are coded as shown in Table 8.1.
372
537
373
538
@@ -403,18 +568,15 @@ def gdf_to_directed_graph(gdf, direction='direction', both_ways=2, against=3, al
403
568
Value specifying that the road is drivable along the digitizing direction.
404
569
405
570
"""
406
-
import networkx as nx
407
571
408
572
# Create the NetworkX graph
409
573
graph = nx.MultiDiGraph()
410
-
411
-
columns =list(gdf.columns)
412
574
413
575
# Generate edge dictionary
414
576
for edge in gdf.itertuples():
415
577
coords = edge.geometry.coords
416
578
417
-
# Get first and last coordinates (drop possible Z information)
579
+
# Get first and last node of the edge (excluding vertices and possible Z coordinate)
0 commit comments