2
votes

Comment convertir une liste de points spatiaux SF en un graphique routable

J'ai un objet sf dataframe avec une série de points représentant la forme d'un itinéraire de bus. Je voudrais transformer cet objet en un graphique routable afin que je puisse estimer le temps qu'il faut pour traverser du point c au t .

Voici ce que j'ai essayé d'utiliser le dodgr package mais je ne suis pas sûr de ce que je fais faux ici:

mydata <- structure(list(shape_id = c(52421L, 52421L, 52421L, 52421L, 52421L, 
                              52421L, 52421L, 52421L, 52421L, 52421L, 52421L, 52421L, 52421L, 
                              52421L, 52421L, 52421L, 52421L, 52421L, 52421L, 52421L), length = structure(c(0.191422504106197, 
                              0.191422504106197, 0.191422504106197, 0.191422504106197, 0.191422504106197, 
                              0.191422504106197, 0.191422504106197, 0.191422504106197, 0.191422504106197, 
                              0.191422504106197, 0.191422504106197, 0.191422504106197, 0.191422504106197, 
                              0.191422504106197, 0.191422504106197, 0.191422504106197, 0.191422504106197, 
                              0.191422504106197, 0.191422504106197, 0.191422504106197), units = structure(list(
                              numerator = "km", denominator = character(0)), class = "symbolic_units"), class = "units"), 
                              geometry = structure(list(structure(c(-46.5623281998182, 
                              -23.5213458001468), class = c("XY", "POINT", "sfg")), structure(c(-46.562221, 
                              -23.52129), class = c("XY", "POINT", "sfg")), structure(c(-46.562121, 
                              -23.521235), class = c("XY", "POINT", "sfg")), structure(c(-46.5620233332577, 
                              -23.5211840000609), class = c("XY", "POINT", "sfg")), structure(c(-46.561925666591, 
                              -23.5211330000609), class = c("XY", "POINT", "sfg")), structure(c(-46.561828, 
                              -23.521082), class = c("XY", "POINT", "sfg")), structure(c(-46.5618098335317, 
                              -23.5212126666783), class = c("XY", "POINT", "sfg")), structure(c(-46.5617916670273, 
                              -23.5213433333544), class = c("XY", "POINT", "sfg")), structure(c(-46.5617735004869, 
                              -23.5214740000284), class = c("XY", "POINT", "sfg")), structure(c(-46.5617553339104, 
                              -23.5216046667004), class = c("XY", "POINT", "sfg")), structure(c(-46.5617371672978, 
                              -23.5217353333702), class = c("XY", "POINT", "sfg")), structure(c(-46.5617190006492, 
                              -23.5218660000379), class = c("XY", "POINT", "sfg")), structure(c(-46.5617008339645, 
                              -23.5219966667036), class = c("XY", "POINT", "sfg")), structure(c(-46.5616826672438, 
                              -23.5221273333671), class = c("XY", "POINT", "sfg")), structure(c(-46.5616645004869, 
                              -23.5222580000284), class = c("XY", "POINT", "sfg")), structure(c(-46.5616463336941, 
                              -23.5223886666877), class = c("XY", "POINT", "sfg")), structure(c(-46.5616281668651, 
                              -23.5225193333449), class = c("XY", "POINT", "sfg")), structure(c(-46.56161, 
                              -23.52265), class = c("XY", "POINT", "sfg")), structure(c(-46.5617355000207, 
                              -23.5226427501509), class = c("XY", "POINT", "sfg")), structure(c(-46.5618610000276, 
                              -23.5226355002012), class = c("XY", "POINT", "sfg"))), class = c("sfc_POINT", 
                              "sfc"), precision = 0, bbox = structure(c(xmin = -46.5623281998182, 
                              ymin = -23.52265, xmax = -46.56161, ymax = -23.521082), class = "bbox"), crs = structure(list(
                              epsg = 4326L, proj4string = "+proj=longlat +datum=WGS84 +no_defs"), class = "crs"), n_empty = 0L), 
                              id = c("a", "b", "c", "d", "e", "f", "g", "h", "i", "j", 
                              "k", "l", "m", "n", "o", "p", "q", "r", "s", "t"), speed_kmh = c(11, 
                              11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 
                              11, 11, 11, 11)), sf_column = "geometry", agr = structure(c(shape_id = NA_integer_, 
                              length = NA_integer_, id = NA_integer_, speed_kmh = NA_integer_
                              ), class = "factor", .Label = c("constant", "aggregate", "identity"
                              )), row.names = c("1.13", "1.14", "1.15", "1.16", "1.17", "1.18", 
                              "1.19", "1.20", "1.21", "1.22", "1.23", "1.24", "1.25", "1.26", 
                              "1.27", "1.28", "1.29", "1.30", "1.31", "1.32"), class = c("sf", 
                              "data.table", "data.frame"))

Erreur dans check_highway_osmid (x, wt_profile): Veuillez spécifier type_col à utiliser pour pondérer streetnet

Données reproductibles

Les données ressemblent à l'image ci-dessous

library(dodgr)
graph <- weight_streetnet(mydata, wt_profile = "motorcar", type_col="highway" , id_col = "id")

 entrez la description de l'image ici a >


1 commentaires

Juste un commentaire: un exemple de code minimal facilite la vie des développeurs. Problème intéressant cependant, hâte de voir et de réfléchir à des solutions!


3 Réponses :


1
votes

Je pense que vous pouvez le résoudre en transformant vos données en un objet igraph et en utilisant les fonctionnalités de la bibliothèque igraph . Vous devez définir les valeurs Arêtes et Vertex ainsi que les valeurs de poids . Dans igraph un Edge est un lien représentant une connexion entre deux nœuds (Source et Target). Dans ce cas, un lien est une "rue" et les points sont les nœuds.

library(igraph)
GraphResult <- data.frame(Source = c(NULL), 
                      Target = c(NULL), 
                      weight  = c(NULL))

for (i in 1:(dim(mydata)[1] - 1)) {

  TempGraphResult <- data.frame(Source = c(0), 
                                Target = c(0), 
                                weight  = c(0))

  TempGraphResult$Source[1] <- mydata$id[i]
  TempGraphResult$Target[1] <- mydata$id[i + 1]
  TempGraphResult$weight[1] <- mydata$length[i]

  GraphResult <- rbind(GraphResult, TempGraphResult) }

MyIgraph <- graph_from_data_frame(GraphResult) 

#In this case works perfectly. But if you have more weight variables and even
#additional variables for the nodes, igraph have functions for constructing the
#igraph object

distances(MyIgraph, "c", "t") #returns 3.254183. Seems correct (0.1914225*17)
SquareMatrix <- distances(MyIgraph)

#*distances() is a function from igraph that performs the routing calculations.

Il est possible de réaliser des réseaux plus complexes et de calculer des itinéraires. Par exemple, vous pouvez définir la direction des routes.

Peut-être que dodger peut gérer le problème, mais je ne suis pas sûr.


2 commentaires

C'est très utile, @Orlando. Merci encore!


J'ai un intérêt particulier pour le sujet. Plus précisément sur la façon d'utiliser R pour calculer les temps de trajet et les itinéraires pour différents formats. Par exemple, il serait utile de faire des calculs en utilisant des lignes au format .shp (sans rapport avec OSM). Mais cela devrait être quelque chose de "facile" à faire et avec beaucoup de flexibilité; cela est nécessaire pour de nombreux problèmes de transport et dans de nombreuses agences d'analyse des transports. Je vois quelque chose comme Network Analyst d'ARCGIS en cours de développement pour R dans un proche avenir. Peut-être que Dodger est sur cette voie. S'il vous plaît laissez-moi savoir lorsque vous arrivez à une réponse plus robuste à votre problème.



1
votes

La fonction weight_streetnet n'est en réalité conçue que pour gérer les réseaux routiers réels, généralement tels que produits par les fonctions osmdata :: osmdata_sf / sp / sc () . Il peut néanmoins être modifié pour gérer des cas comme celui-ci. La chose principale nécessaire est de convertir les points en quelque chose qui connaît les arêtes entre eux, comme un objet sf :: LINESTRING :

net$d <- net$d_weighted
dodgr_dists (net, from = "c", to = "t") # 3.254183

Cela donne un seul- objet de ligne qui peut ensuite être converti au format dodgr , et les valeurs id mises en correspondance avec les bords

net$d_weighted <- as.numeric (mydata$length [1])
dodgr_dists (net, from = "c", to = "t") # 236.0481

À ce stade, dodgr aura calculé et inséré les distances directement à partir des coordonnées géographiques. Vos distances peuvent alors également être insérées et utilisées pour le routage en remplaçant les valeurs d_weighted :

net <- weight_streetnet (x, type_col = "shape_id", id_col = "id", wt_profile = 1)
net$from_id <- mydata$id [as.integer (net$from_id)]
net$to_id <- mydata$id [as.integer (net$to_id)]

Si vous voulez vraiment que vos distances représentent les distances absolues utilisées pour calculer le résultat final, puis remplacez simplement les valeurs de $ d

x <- sf::st_combine (mydata) %>%
    sf::st_cast ("LINESTRING") %>%
    sf::st_sf ()

Notez que pour des problèmes "simples" comme celui-ci, igraph sera généralement plus rapide, car il calcule les itinéraires en utilisant un seul ensemble de poids. Le seul réel avantage de dodgr dans ce contexte est la possibilité d’utiliser des "doubles pondérations" - les valeurs $ d_weighted et $ d - telles que l'itinéraire est calculé selon $ d_weighted , et les distances finales selon $d.


0 commentaires

2
votes

Si vous souhaitez l'inclure dans un flux de travail «ordonné», vous pouvez également envisager d'utiliser un mélange entre sf et tidygraph . Ce dernier offre un cadre soigné pour les réseaux / graphes, sous la forme d'une classe tbl_graph , qui sous-classe igraph (par conséquent, vous pouvez utiliser tbl_graph objets à l'intérieur de toutes les fonctions igraph comme étant un objet igraph ). Cependant, vous pouvez analyser vos nœuds et arêtes comme étant des tibbles, et utiliser des fonctions comme filter () , select () , mutate () , etc. Bien sûr, ces tibbles peuvent également contenir une colonne de liste de géométrie que nous connaissons de sf , ajoutant des informations géographiques aux nœuds et aux arêtes.

L'approche est encore loin d'être parfaite, et des améliorations seraient très Bienvenue, mais cela montre quand même une autre façon de gérer le problème.

ggplot() +
  geom_sf(data = graph %>% activate(edges) %>% as_tibble() %>% st_as_sf(), col = 'darkgrey') +
  geom_sf(data = graph %>% activate(nodes) %>% as_tibble() %>% st_as_sf(), col = 'darkgrey', size = 0.5) +
  geom_sf(data = path_graph %>% activate(edges) %>% as_tibble() %>% st_as_sf(), lwd = 1, col = 'firebrick') +
  geom_sf(data = path_graph %>% activate(nodes) %>% filter(id %in% c(from_node, to_node)) %>% as_tibble() %>% st_as_sf(), size = 2)

Tout comme dans les autres réponses, nous devons créer des arêtes entre les nœuds. Pour l'instant, je suppose que les points sont simplement reliés par ordre alphabétique. Pour l'approche tidygraph , cependant, nous semblons avoir besoin d'identifiants numériques plutôt que de caractères.

# A tibble: 1 x 1
  total_time
         [h]
1  0.2958348

Cela nous donne le tbl_graph code> objet:

path_graph %>%
    activate(edges) %>%
    as_tibble() %>%
    summarise(total_time = sum(time))

Maintenant, nous avons tout dans une structure graphique, nous pouvons sélectionner le nœud à partir duquel nous voulons acheminer, et le nœud vers lequel nous voulons acheminer, et trouver le chemin le plus court entre eux avec le temps de trajet comme variable de poids, en utilisant la fonction shortest_path de igraph . Nous travaillons maintenant simplement avec une route un-à-un ('c' à 't'), mais ce serait exactement la même chose pour un-à-plusieurs, plusieurs-à-un ou plusieurs-à-plusieurs. p>

# A tbl_graph: 18 nodes and 17 edges
#
# An undirected simple graph with 1 component
#
# Node Data: 18 x 4 (active)
     id id_chr shape_id              geometry
  <int> <chr>     <int>           <POINT [°]>
1     3 c         52421 (-46.56212 -23.52124)
2     4 d         52421 (-46.56202 -23.52118)
3     5 e         52421 (-46.56193 -23.52113)
4     6 f         52421 (-46.56183 -23.52108)
5     7 g         52421 (-46.56181 -23.52121)
6     8 h         52421 (-46.56179 -23.52134)
# … with 12 more rows
#
# Edge Data: 17 x 6
   from    to    length   speed                               geometry      time
  <int> <int>      [km]  [km/h]                       <LINESTRING [°]>       [h]
1     1     2 0.1914225      11 (-46.56212 -23.52124, -46.56202 -23.5… 0.017402…
2     2     3 0.1914225      11 (-46.56202 -23.52118, -46.56193 -23.5… 0.017402…
3     3     4 0.1914225      11 (-46.56193 -23.52113, -46.56183 -23.5… 0.017402…
# … with 14 more rows

Le chemin résultant est une liste avec les nœuds et les arêtes qui composent le chemin.

path_graph <- graph %>%
    subgraph.edges(eids = path$epath %>% unlist()) %>%
    as_tbl_graph()

Nous pouvons créer un sous-graphe du graphe original qui ne contient que les nœuds et les arêtes du chemin le plus court.

$vpath
$vpath[[1]]
+ 18/20 vertices, from e43a089:
 [1]  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20


$epath
$epath[[1]]
+ 17/19 edges from e43a089:
 [1]  3-- 4  4-- 5  5-- 6  6-- 7  7-- 8  8-- 9  9--10 10--11 11--12 12--13
[11] 13--14 14--15 15--16 16--17 17--18 18--19 19--20
# Select the node from which and to which the shortest path should be found.
from_node <- graph %>%
  activate(nodes) %>%
  filter(id_chr == "c") %>%
  pull(id)

to_node <- graph %>%
  activate(nodes) %>%
  filter(id_chr == "t") %>%
  pull(id)

# Find the shortest path between these nodes
path <- shortest_paths(
  graph = graph,
  from = from_node,
  to = to_node,
  output = 'both',
  weights = graph %>% activate(edges) %>% pull(time)
)

Ici, il se passe quelque chose que je n'aime pas. Tidygraph / igraph semble avoir une structure d'ID de nœud interne, et vous voyez que dans le sous-graphe, les colonnes from et to dans les données egdes ne correspondent pas à notre id dans les données des nœuds, mais faites simplement référence aux numéros de ligne des données des nœuds. Je ne sais pas comment résoudre ce problème.

Quoi qu'il en soit, nous avons maintenant notre chemin de «c» à «t» en tant que sous-graphe, et pouvons facilement l'analyser. Par exemple, en calculant le temps de trajet total du chemin (comme c'était la question).

# A tbl_graph: 20 nodes and 19 edges
#
# An undirected simple graph with 1 component
#
# Node Data: 20 x 4 (active)
     id id_chr shape_id              geometry
  <int> <chr>     <int>           <POINT [°]>
1     1 a         52421 (-46.56233 -23.52135)
2     2 b         52421 (-46.56222 -23.52129)
3     3 c         52421 (-46.56212 -23.52124)
4     4 d         52421 (-46.56202 -23.52118)
5     5 e         52421 (-46.56193 -23.52113)
6     6 f         52421 (-46.56183 -23.52108)
# … with 14 more rows
#
# Edge Data: 19 x 6
   from    to    length   speed                               geometry      time
  <int> <int>      [km]  [km/h]                       <LINESTRING [°]>       [h]
1     1     2 0.1914225      11 (-46.56233 -23.52135, -46.56222 -23.5… 0.017402…
2     2     3 0.1914225      11 (-46.56222 -23.52129, -46.56212 -23.5… 0.017402…
3     3     4 0.1914225      11 (-46.56212 -23.52124, -46.56202 -23.5… 0.017402…
# … with 16 more rows
# Add a numeric ID column to the nodes.
nodes <- mydata %>%
    rename(id_chr = id) %>%
    rowid_to_column("id") %>%
    select(id, id_chr, everything())

# Define the source node of each edge, and the target node of each edge.
sources <- nodes %>% slice(-n())
targets <- nodes %>% slice(-1)

# Write a function to create lines between data frames of source and target points.
pt2l <- function(x, y) { st_linestring(rbind(st_coordinates(x), st_coordinates(y))) }

# Create the edges.
edges <- tibble(
        from = sources %>% pull(id), 
        to = targets %>% pull(id), 
        length = sources %>% pull(length), 
        speed = sources %>% pull(speed_kmh),
        geometry = map2(st_geometry(sources), st_geometry(targets), pt2l)
    ) %>% st_as_sf() %>% st_set_crs(st_crs(nodes))

# Add a time column to the edges.
edges <- edges %>%
    mutate(speed = set_units(speed, "km/h")) %>%
    mutate(time = length / speed)

# Clean up the nodes data.
nodes <- nodes %>%
    select(-length, -speed_kmh)

# Create the tbl_graph object out of the nodes and edges.
# Providing the edges as sf object is problematic for tidygraph, unfortunately.
# Therefore, we have to provide them as a tibble.
graph <- tbl_graph(nodes = nodes, edges = as_tibble(edges), directed = FALSE)

Mais il est également facile de le tracer , avec des informations géographiques préservées (simplement en exportant les nœuds et les arêtes en tant qu'objets sf).

# Load libraries.
library(tidyverse)
library(sf)
library(tidygraph)
library(igraph)
library(units)

plot

Un article de blog r-spatial pourrait être publié sur cette approche tidygraph-sf;)


0 commentaires