Análisis geográfico de líneas de autobús en Barcelona (2/2)

Objetivo: Determinar solapamientos de líneas de autobús con códigos postales en la ciudad de Barcelona (España)

[Continuación del artículo Análisis geográfico de líneas de autobús en Barcelona (1/2)]

Líneas de autobús

Este ejercicio se centra en tres líneas: 7, 21 y 92. La línea 7, que cruza casi la totalidad de la ciudad por la Diagonal. La 21 conecta la Zona Franca con el Paral·lel. La línea 92 disecciona la ciudad desde el Tibidabo a la Barceloneta. La elección de estas líneas surge de una diferenciación implícita entre las mismas. La línea 7 toca zonas residenciales, universitarias y de oficinas. La 21 conecta zona industrial con zona residencial. La línea 92 cruza zonas residenciales con alto interés turístico.
Las líneas de autobús se muestran directamente en la página de TMB. Rebuscando un poco en las llamadas que se realizan, se puede obtener la URL con la que emular las peticiones.

Para la descarga de las líneas de autobús, se opta por un bucle que descarga y crea variables de forma dinámica. La gestión dinámica de variables se hace a través de la función assign para crear la variable y get para recuperar la variable. De forma dinámica, pues, se descargan las tres líneas en ficheros JSON y se crean sendas variables JSON y SLDF.

La lectura y carga de los ficheros JSON en R puede hacerse alternativamente con fromJSON {rjson} o con geojson {geojsonio} aunque los objetos de salida no son exactamente iguales. En este ejemplo, fromJSON cumple con las necesidades del ejercicio.

Los archivos del interfaz de TMB son de tipo JSON y deben convertirse en Spatial Lines para poder combinarlos con los códigos postales. En cambio, el formato JSON permite fácilmente la representación mediante la librería HighCharter, que es un wrapper de HighCharts.

lines_num<-c(7,21,92) #Las tres líneas en estudio

for (i in lines_num){
  #Descarga en línea
  #the_url<-paste('https://api.tmb.cat/v1/transit/linies/bus/',i,'/horaris/?app_key=a828910cef5a0376607986191db19d14&app_id=4c132798&sortBy=ID_TIPUS_DIA,ID_SENTIT',sep='')
  #assign(paste('l', i, '.json',sep=''),content(GET(the_url))) #Guardamos en un archivo

  #Alternativamente se pueden leer los ficheros
  assign(paste('l', i, '.json',sep=''), fromJSON(file=paste('data/linia_',i,'.json',sep='')))
  print(class(get(paste('l', i, '.json',sep=''))))

  assign(paste('l', i, '.sldf',sep=''),readOGR(paste('data/linia_',i,'.json',sep=''),"OGRGeoJSON")) #Carga en formato Spatial Lines Data Frame
  assign(paste('l', i, '.sldf',sep=''),spTransform(get(paste('l', i, '.sldf',sep='')), projection_value)) #Nueva proyección
}
## [1] "list"
## OGR data source with driver: GeoJSON 
## Source: "data/linia_7.json", layer: "OGRGeoJSON"
## with 6 features
## It has 22 fields
## [1] "list"
## OGR data source with driver: GeoJSON 
## Source: "data/linia_21.json", layer: "OGRGeoJSON"
## with 6 features
## It has 22 fields
## [1] "list"
## OGR data source with driver: GeoJSON 
## Source: "data/linia_92.json", layer: "OGRGeoJSON"
## with 6 features
## It has 22 fields

En el caso de las líneas de autobús se observa que hay *6 * objetos por cada línea y que los metadatos son relevantes. Los datos en @data nos sirven para desentrañar la razón por la que hay más de un objeto Spatial Line Data Frame para cada línea de autobús. Finalmente se comprueba que las proyecciones de los códigos postales y las líneas de autobús coinciden a través de identicalCRS.

class(l7.sldf) #Tipo de objeto
## [1] "SpatialLinesDataFrame"
## attr(,"package")
## [1] "sp"
slotNames(l7.sldf) #Objetos que contiene cada línea
## [1] "data"        "lines"       "bbox"        "proj4string"
names(l7.sldf) #Metadatos de cada línea
##  [1] "ID_LINIA"        "CODI_LINIA"      "NOM_LINIA"      
##  [4] "DESC_LINIA"      "ORDRE_LINIA"     "ID_OPERADOR"    
##  [7] "NOM_OPERADOR"    "CODI_FAMILIA"    "NOM_FAMILIA"    
## [10] "ORDRE_FAMILIA"   "ID_HORARI"       "HORARI"         
## [13] "ID_SENTIT"       "SENTIT"          "DESC_SENTIT"    
## [16] "PRIMERA_SORTIDA" "ULTIMA_SORTIDA"  "ID_TIPUS_TIRA"  
## [19] "TIPUS_TIRA"      "ID_TIPUS_DIA"    "DESC_TIPUS_DIA" 
## [22] "DATA"
print(l7.sldf@data) #Contenido de los metadatos @data
##   ID_LINIA CODI_LINIA NOM_LINIA                 DESC_LINIA ORDRE_LINIA
## 0      169          7         7 Fòrum / Zona Universitària           7
## 1      169          7         7 Fòrum / Zona Universitària           7
## 2      169          7         7 Fòrum / Zona Universitària           7
## 3      169          7         7 Fòrum / Zona Universitària           7
## 4      169          7         7 Fòrum / Zona Universitària           7
## 5      169          7         7 Fòrum / Zona Universitària           7
##   ID_OPERADOR NOM_OPERADOR CODI_FAMILIA   NOM_FAMILIA ORDRE_FAMILIA
## 0           2           TB            1 Convencionals            13
## 1           2           TB            1 Convencionals            13
## 2           2           TB            1 Convencionals            13
## 3           2           TB            1 Convencionals            13
## 4           2           TB            1 Convencionals            13
## 5           2           TB            1 Convencionals            13
##   ID_HORARI                    HORARI ID_SENTIT SENTIT DESC_SENTIT
## 0      2002 007.007.FH35.00.00.TB09V0         1      A       Anada
## 1      2002 007.007.FH35.00.00.TB09V0         2      T     Tornada
## 2      2003 007.007.SH29.00.05.TB09V0         1      A       Anada
## 3      2003 007.007.SH29.00.05.TB09V0         2      T     Tornada
## 4      2001 007.007.EH29.00.07.TB09V0         1      A       Anada
## 5      2001 007.007.EH29.00.07.TB09V0         2      T     Tornada
##   PRIMERA_SORTIDA ULTIMA_SORTIDA ID_TIPUS_TIRA TIPUS_TIRA ID_TIPUS_DIA
## 0           05:50          22:25             1 FREQÜÈNCIA            1
## 1           06:35          23:05             1 FREQÜÈNCIA            1
## 2           06:00          22:25             1 FREQÜÈNCIA            2
## 3           06:45          23:05             1 FREQÜÈNCIA            2
## 4           08:00          22:25             1 FREQÜÈNCIA            3
## 5           08:45          23:05             1 FREQÜÈNCIA            3
##        DESC_TIPUS_DIA        DATA
## 0             Feiners 2016-12-15Z
## 1             Feiners 2016-12-15Z
## 2           Dissabtes 2016-12-16Z
## 3           Dissabtes 2016-12-16Z
## 4 Festius i diumenges 2016-12-17Z
## 5 Festius i diumenges 2016-12-17Z
identicalCRS(grouped_pc_polygons.sp,l7.sldf) #Comprobación de idénticas proyecciones
## [1] TRUE
highchart(type = "map") %>% 
  hc_title(text = "Líneas de autobús (Barcelona)" %>% 
  hc_chart(backgroundColor = "#D9E9FF") %>% 
  hc_add_series(data = l7.json, type = "mapline",  lineWidth = 2,
                name = "Línea 7 Ida", color = 'rgba(0, 0, 80, 0.33)',
                states = list(hover = list(color = "#BADA55"))) %>%
  hc_add_series(data = l21.json, type = "mapline",  lineWidth = 2,
                name = "Línea 21", color = 'rgba(0, 10, 0, 0.33)',
                states = list(hover = list(color = "#BADA55"))) %>%
  hc_add_series(data = l92.json, type = "mapline",  lineWidth = 2,
                name = "Línea 92", color = 'rgba(80, 0, 80, 0.33)',
                states = list(hover = list(color = "#BADA55")),
                tooltip = list(pointFormat = "Linia 92")) %>%
  hc_mapNavigation(enabled = TRUE)

Hay 22 categorías de metadatos para la línea de autobús. Algunas de ellas van emparejadas como el identificativo del sentido de la marcha (ID_SENTIT) y el significado de su valor (SENTIT y DESC_SENTIT). Se observa los 6 SpatialLines se agrupan en tipo de dia (ID_TIPUS_DIA, Dissabtes, Feiners, Festius i diumenges) y por cada tipo de día por el sentido de la marcha (ID_SENTIT, Anada, Tornada). Los tres tipos de días son laborables, sábados y domingos y festivos. Los tipos de sentido de la marcha son ida y vuelta.

Es interesante darse cuenta de que las últimas salidas no varían según el tipo de día pero sí las primeras salidas, más tempranas en días laborables y tardías en domingos. Eso puede empezar a dindicarnos el tipo de usuarios que tiene esa línea.

line_att<-c("ID_LINIA","CODI_LINIA","DESC_LINIA","ORDRE_LINIA","NOM_OPERADOR","NOM_FAMILIA","ORDRE_FAMILIA","DESC_SENTIT","DESC_TIPUS_DIA")
for (i in lines_num){
  the_line<-paste('l',i,'.sldf',sep='') #Definimos el nombre de la variable que con se recupera con get() 
  plot(main=paste('Línea',i), get(the_line)[get(the_line)$ID_TIPUS_DIA=="1" & get(the_line)$ID_SENTIT=="1",], col="blue") #Ida en día laborable (azul)
  plot(get(the_line)[get(the_line)$ID_TIPUS_DIA %in%"1" & get(the_line)$ID_SENTIT %in% "2",], add=T, col="green") #Vuelta en día laborable (verde)
  print(gLength(get(the_line), byid=T)) #Longitudes de cada uno de los objetos SL {rgeos}
  print(SpatialLinesLengths(get(the_line))) #Alternativa de cálculo de distancias {sp}
  #gLength(get(the_line)) #Longitud total del objeto SLDF
  #sum(gLength(get(the_line), byid=T)) #Equivale a gLength(get(the_line))
  assign(the_line, subset(get(the_line),select=line_att)) #Alternativamente get(the_line)[,line_att]
  assign(the_line, `[[<-`(get(the_line), 'DISTANCIA', value = SpatialLinesLengths(get(the_line)))) #Asignamos las distancias de cada línea equivalente a l7.sldf$DISTANCIA<-SpatialLinesLengths(l7.sldf)
}

plot of chunk detalles_autobus_distancia

##        0        1        2        3        4        5 
## 10717.19 10483.66 10717.19 10483.66 10717.19 10483.66 
## [1] 10717.19 10483.66 10717.19 10483.66 10717.19 10483.66

plot of chunk detalles_autobus_distancia

##        0        1        2        3        4        5 
## 17876.96 15930.23 17876.96 15930.23 17876.96 15930.23 
## [1] 17876.96 15930.23 17876.96 15930.23 17876.96 15930.23

plot of chunk detalles_autobus_distancia

##        0        1        2        3        4        5 
## 11187.57 11740.47 11187.57 11740.47 11187.57 11740.47 
## [1] 11187.57 11740.47 11187.57 11740.47 11187.57 11740.47
print(l7.sldf@data) #Metadatos
##   ID_LINIA CODI_LINIA                 DESC_LINIA ORDRE_LINIA NOM_OPERADOR
## 0      169          7 Fòrum / Zona Universitària           7           TB
## 1      169          7 Fòrum / Zona Universitària           7           TB
## 2      169          7 Fòrum / Zona Universitària           7           TB
## 3      169          7 Fòrum / Zona Universitària           7           TB
## 4      169          7 Fòrum / Zona Universitària           7           TB
## 5      169          7 Fòrum / Zona Universitària           7           TB
##     NOM_FAMILIA ORDRE_FAMILIA DESC_SENTIT      DESC_TIPUS_DIA DISTANCIA
## 0 Convencionals            13       Anada             Feiners  10717.19
## 1 Convencionals            13     Tornada             Feiners  10483.66
## 2 Convencionals            13       Anada           Dissabtes  10717.19
## 3 Convencionals            13     Tornada           Dissabtes  10483.66
## 4 Convencionals            13       Anada Festius i diumenges  10717.19
## 5 Convencionals            13     Tornada Festius i diumenges  10483.66

Haciendo un bucle sobre las tres líneas y con la función get() se recupera el valor de la variable del entorno con el nombre asignado. Se debe tener en cuenta que la función y las variables estén en el mismo entorno.

Como es de esperar, las trayectorias de ida y vuelta de las líneas de autobús son similares pero no idénticas. Se observa también que las distancias de los trayectos son idénticos con lo que se puede asumir que los trayectos no cambian según el día. El filtro se realiza con %in% en vez de == ya que en el segundo caso podrían colarse valores NA.

Para el análisis de los trayectos y su solapamiento con los códigos postales se podría optar por reducir el SLDF a los días laborables pero al desconocer el comportamiento con el resto de líneas con respecto al tipo de día, es preferible mantener toda la información.

La función gLength (de la librería rgeos) con el parámetro byid=T calcula la distancia de cada una de las líneas contenidas en el SLDF. Para calcular el total de distancias de los objetos contenidos, se puede bien ejecutar la suma de cada objeto o bien directamente utilizar el parámetro byid=F (por defecto). En este caso, la suma de las distancias de cada uno de los objetos no aporta relevante información en comparación con la de cada uno de los objetos. Las unidades de las medidas son metros según la proyección UTM EPSG 2062.

De cada una de las líneas podemos prescindir de algunos de los atributos que bien están repetidos o que no son de interés. La forma de filtrar los SPDF/SLDF es talmente como con los data.frames. De esta forma se observa lo fácil que es tratar con objetos SP ya que se asemejan a los data.frames con lo que bien podemos seleccionar determinadas filas (linea[linea$nombre_de_columna %in% c(“valor”)]) o bien columnas (linea[,c(“nombre_de_columna”)]).

Se pretende dilucidar cuanto solapamiento hay entre las líneas de autobús y los códigos postales. Esos datos pueden conformar interesantes características (o atributos) para modelos predictivos. Es por eso que se añade dicha información en la capa de metadatos de los objetos espaciales. En este caso se puede optar bien por incluir para cada línea de autobús, los valores de solapamiento con códigos postales. Eso conlleva crear 45 variables cada una con los 6 subtipos de línea de autobús.

En el paquete rgeos podemos utilizar la función gIntersects, que retorna un boleano (TRUE/FALSE) según si hay solapamientos y gIntersection que retorna las geometrías de solapamiento.

Al comprobar las intersecciones entre las líneas de autobús y los códigos postales, se deben tener en cuenta los valores nulos (sin solapamiento) para que no devuelva errores. Al intentar calcular la longitud de solapamiento en caso de que no exista tal retorna un error. Es por eso que para hacer los cálculos de solapamientos se debe generar un gestor de errores. En caso de que no haya intersección entre los elementos, se asigna el valor cero (0).

plot(main="Líneas 7, 21 y 92 y código 08028", l7.sldf[l7.sldf$DESC_TIPUS_DIA=="Feiners",], col="blue", xlim=c(1082000,1093200), ylim=c(762200,773700))
plot(l92.sldf[l92.sldf$DESC_TIPUS_DIA=="Feiners",], col="green", add=T) 
plot(l21.sldf[l21.sldf$DESC_TIPUS_DIA=="Feiners",], col="red", add=T) 
plot(grouped_pc_polygons.sp[c("08028"),], add=T)

plot of chunk solapamientos

gIntersects(l7.sldf,l92.sldf) #Exite solapamiento entre las líneas 7 y 92
## [1] TRUE
gIntersects(l7.sldf,l92.sldf, byid=T) #Todos los trayectos tienen solapamiento con 6x6 cruces analizados
##      0    1    2    3    4    5
## 0 TRUE TRUE TRUE TRUE TRUE TRUE
## 1 TRUE TRUE TRUE TRUE TRUE TRUE
## 2 TRUE TRUE TRUE TRUE TRUE TRUE
## 3 TRUE TRUE TRUE TRUE TRUE TRUE
## 4 TRUE TRUE TRUE TRUE TRUE TRUE
## 5 TRUE TRUE TRUE TRUE TRUE TRUE
overGeomGeom(l7.sldf,l21.sldf) #Alternativa con rgeos
## [1] NA
overGeomGeom(l7.sldf,l92.sldf) #Alternativa con rgeos
## 0.0 1.0 2.0 3.0 4.0 5.0 
##   1   1   1   1   1   1
gIntersects(l7.sldf,grouped_pc_polygons.sp[c("08028"),], byid=T) #Todos los trayectos de la línea 7 tienen solapamiento con 6 cruces analizados sobre el código postal 08028
##          0    1    2    3    4    5
## 08028 TRUE TRUE TRUE TRUE TRUE TRUE
gLength(gIntersection(l7.sldf,grouped_pc_polygons.sp[c("08028"),], byid=T), byid=T) #Distancias de solapamientos con 6 cruces analizados sobre el código postal 08028
##   0 08028   1 08028   2 08028   3 08028   4 08028   5 08028 
##  438.0803 1731.3528  438.0803 1731.3528  438.0803 1731.3528
#gLength(gIntersection(l92.sldf,grouped_pc_polygons.sp[c("08028"),], byid=T), byid=T) #Da un error al no poder calcular distancias si no hay solapamientos

#Se deben gestionar los solapamientos y para aquellos, asignar la distancia, al resto ceros
the_codes<-laply(grouped_pc_polygons.sp@polygons, slot, "ID")

for (line_num in lines_num){
  the_line<-paste('l',line_num,'.sldf',sep='') #Definimos el nombre de la variable que con se recupera con get() 

for (cp in the_codes)
{
  the_inter_len<-c()
  the_inter_tf<-c()
  cp_label<-paste('PC',cp,sep='') #Etiqueta para los atributos
  the_cp<-grouped_pc_polygons.sp[c(cp),]
  the_inter_tf<-gIntersects(get(the_line),the_cp, byid=T) #Boleano de intersecciones

  #Filtrar los que no
  #Ejecutar las intersecciones sólo si se sabe que intersectan
  if(length(which(the_inter_tf==TRUE)) > 0)   the_inter_len<-gLength(gIntersection(get(the_line)[which(the_inter_tf==TRUE),],the_cp, byid=T),byid=T)

  n=1 #Definiendo el vector con distancias de solapamiento
  the_vector<-c()
  for(i in 1:length(the_inter_tf)){
    if(the_inter_tf[i]==TRUE){
      the_vector<-c(the_vector,the_inter_len[n]) #Asignando los valores de longitud de intersección
      n<-n+1
    }else{
      the_vector<-c(the_vector,0) #Rellenando aquellos casos sin intersección
    }
  }

assign(the_line, `[[<-`(get(the_line), paste(cp_label), value = the_vector)) #Asignamos las distancias de cada línea como atributo

  } #for (cp in the_codes)
  #Mostramos los primeros códigos postales por línea para ver su diferente solapamiento
print(paste('Línea',line_num))
  print(head(get(the_line)@data[grep('PC0800',names(get(the_line)))])) #Los primeros códigos postales

} #for (i in lines_num){
## [1] "Línea 7"
##   PC08001 PC08002 PC08003 PC08004  PC08005  PC08006   PC08007  PC08008
## 0       0       0       0       0 247.8864 656.4355  702.4742 572.3953
## 1       0       0       0       0 300.2889   0.0000 1071.7318 849.5873
## 2       0       0       0       0 247.8864 656.4355  702.4742 572.3953
## 3       0       0       0       0 300.2889   0.0000 1071.7318 849.5873
## 4       0       0       0       0 247.8864 656.4355  702.4742 572.3953
## 5       0       0       0       0 300.2889   0.0000 1071.7318 849.5873
##   PC08009
## 0 6.86003
## 1 0.00000
## 2 6.86003
## 3 0.00000
## 4 6.86003
## 5 0.00000
## [1] "Línea 21"
##    PC08001 PC08002 PC08003  PC08004 PC08005 PC08006 PC08007 PC08008
## 0 236.3212       0       0 837.7951       0       0       0       0
## 1 794.2614       0       0   0.0000       0       0       0       0
## 2 236.3212       0       0 837.7951       0       0       0       0
## 3 794.2614       0       0   0.0000       0       0       0       0
## 4 236.3212       0       0 837.7951       0       0       0       0
## 5 794.2614       0       0   0.0000       0       0       0       0
##   PC08009
## 0       0
## 1       0
## 2       0
## 3       0
## 4       0
## 5       0
## [1] "Línea 92"
##   PC08001 PC08002  PC08003 PC08004  PC08005 PC08006 PC08007 PC08008
## 0       0       0 253.7074       0 1973.837       0       0       0
## 1       0       0 186.4626       0 1854.645       0       0       0
## 2       0       0 253.7074       0 1973.837       0       0       0
## 3       0       0 186.4626       0 1854.645       0       0       0
## 4       0       0 253.7074       0 1973.837       0       0       0
## 5       0       0 186.4626       0 1854.645       0       0       0
##   PC08009
## 0       0
## 1       0
## 2       0
## 3       0
## 4       0
## 5       0
print(l92.sldf@data[names(l92.sldf) %in% c('PC08013','PC08026','PC08041')]) #La línea 92 no siempre cruza los mismos códigos postales
##    PC08013  PC08026 PC08041
## 0    0.000 1081.199 135.645
## 1 1244.928    0.000   0.000
## 2    0.000 1081.199 135.645
## 3 1244.928    0.000   0.000
## 4    0.000 1081.199 135.645
## 5 1244.928    0.000   0.000
gIntersects(l92.sldf,grouped_pc_polygons.sp[c("08028"),], byid=T) #Todos los trayectos tienen solapamiento con 6 cruces analizados sobre el código postal 08028
##           0     1     2     3     4     5
## 08028 FALSE FALSE FALSE FALSE FALSE FALSE
gIntersects(l7.sldf,grouped_pc_polygons.sp[c("08028"),], byid=T) #Todos los trayectos tienen solapamiento con 6 cruces analizados sobre el código postal 08028
##          0    1    2    3    4    5
## 08028 TRUE TRUE TRUE TRUE TRUE TRUE
head(gArea(grouped_pc_polygons.sp, byid=T)) #Mostrando las áreas de los primeros elementos
##     08001     08002     08003     08004     08005     08006 
##  962346.5  720293.8 1689963.2 1360613.2 3214080.6  966020.4
sapply(slot(grouped_pc_polygons.sp, "polygons"), function(i) slot(i, "area")) #Extrayendo las áreas de los códigos postales
##  [1]   962346.46   720293.77  1689963.21  1360613.21  3214080.59
##  [6]   966020.44   520237.79   411961.75   475685.11   474730.67
## [11]   543965.06   856524.19  1450039.52  1949590.33  1277848.71
## [16]  1642589.30    90332.11 12115735.92  2119825.90  2117937.09
## [21]  2556314.12   913048.52  1308703.20  1581853.36  1873436.83
## [26]  1284397.41   817809.10  1701567.05  3063905.54  1317503.64
## [31]  3635273.46  1437412.51  2196722.17  4538768.77  3700317.26
## [36]  9043834.19   628758.44   466751.66  6002371.40  3914262.76
## [41] 10233462.45  1082275.48  1796553.93    37143.47    11197.88

Mirando las tres líneas de autobús, en el caso de añadir el análisis de solapamiento con el argumento byid=T (por defecto es FALSE) deben salir 36 combinaciones al haber 6 elementos por cada línea.

Las líneas de autobús pueden cruzarse entre ellas. rgeos permite analizar dichas solapamientos tanto con gIntersects como con overGeomGeom.

Analizando la línea de autobús 7 contra el código postal 08028, salen seis combinaciones y en todas ellas hay solapamiento. No sucede así con la línea 92. Es interesante ver que la línea 92 cruza en los trayectos de ida el código postal 08026 o el 08041 per no de vuelta mientras que sólo cruza de vuelta el 08013.

Las funciones para el cálculo de áreas, longitudes, perímetros, etc pueden realizarse en cualquier momento sobre objetos SP aunque es cómo tenerlos cargados dentro de los metadatos ya que éstos se extraerán para realizar modelados de datos.

Llegados a este punto se han conseguido cargar de forma dinámica tanto los códigos postales de una ciudad, en este caso Barcelona, como algunas líneas de autobús y determinar los solapamientos entre dichos objetos. Fácilmente se puede realizar estudio sobre todas las líneas de autobús de la ciudad o hasta realizar dicho estudio en otra urbe de interés al tratarse de una solución perfectamente parametrizable.

A partir de ahí, entrarían los datos para la creación de los modelos predictivos y descriptivos tales como categorías poblacionales, número de comercios/establecimientos turísticos, ocupación de las líneas de autobús, etc.

En foros de datos abiertos como Barcelona Open Data o realizando una petición directamente a TMB debería esperarse en un futuro cercano la presencia de esos datos para desplegar todo el potencial de este ejercicio.

One thought on “Análisis geográfico de líneas de autobús en Barcelona (2/2)

Comentarios Cerrados