Puntos de interés cultural en Barcelona

CULTURA
BARCELONA
GPKG
Autor/a
Afiliación

Universitat de València

Fecha de publicación

10 de mayo de 2024

Input

Conocer la naturaleza y ubicación de los puntos de interés cultural en Barcelona no solo es importante para el sector turístico y hotelero, sino también para aquellas empresas que buscan identificar nuevas localizaciones estratégicas, planificar campañas de marketing efectivas o comprender mejor cómo está estructurada la ciudad.

Esta información está disponible en forma de dataset, que contiene datos generales y espaciales sobre estas ubicaciones, en el servicio de datos abiertos del Ajuntament de Barcelona, Open Data BCN. Este es un portal en el que cualquier usuario puede acceder al material que posee la institución sin restricciones. Para obtenerlo, debemos consultar el catálogo de datasets. En él, seleccionando Ciudad y Servicios como tema, y especificando la opción Turismo, podemos encontrarlo. Alternativamente, se puede acceder a él y a sus detalles directamente desde aquí.

El archivo en cuestión, cuya licencia de uso es https://creativecommons.org/licenses/by/4.0/, fue publicado el 04/02/2015, aunque se actualiza semanalmente. Además, se puede descargar tanto en formato CSV como en JSON. En este caso, utilizaremos el primero que hemos mencionado para tratar posibles inconsistencias.

Descripción

En primer lugar, cargamos las librerías necesarias.

A continuación, importamos el conjunto de datos con el que vamos a trabajar. Es importante mencionar que añadiremos el argumento show_col_types con la opción FALSE, ya que, en este momento, no queremos analizar los tipos de las columnas que lo conforman.

# Importamos los datos
datos <- read_csv("data/2023_4T_hut_comunicacio_opendata.csv", show_col_types = FALSE)

Es un dataframe formado por 14 120 filas y 21 columnas. En otras palabras, hay información disponible de más de 14 000 puntos de interés cultural almacenada en 21 variables. A priori, parece ser una cantidad considerable de datos.

class(datos)
[1] "spec_tbl_df" "tbl_df"      "tbl"         "data.frame" 
dim(datos)
[1] 14120    21

Las 21 columnas que lo forman han sido nombradas de la siguiente manera:

# Obtenemos los nombres de las columnas
colnames(datos)
 [1] "N_EXPEDIENT"                 "CODI_DISTRICTE"             
 [3] "DISTRICTE"                   "CODI_BARRI"                 
 [5] "BARRI"                       "TIPUS_CARRER"               
 [7] "CARRER"                      "TIPUS_NUM"                  
 [9] "NUM1"                        "LLETRA1"                    
[11] "NUM2"                        "LLETRA2"                    
[13] "BLOC"                        "PORTAL"                     
[15] "ESCALA"                      "PIS"                        
[17] "PORTA"                       "NUMERO_REGISTRE_GENERALITAT"
[19] "NUMERO_PLACES"               "LONGITUD_X"                 
[21] "LATITUD_Y"                  

Ahora, vamos a ver de qué tipo son cada una de ellas.

# Obtenemos sus tipos
spec(datos)
cols(
  N_EXPEDIENT = col_character(),
  CODI_DISTRICTE = col_double(),
  DISTRICTE = col_character(),
  CODI_BARRI = col_double(),
  BARRI = col_character(),
  TIPUS_CARRER = col_character(),
  CARRER = col_character(),
  TIPUS_NUM = col_double(),
  NUM1 = col_double(),
  LLETRA1 = col_character(),
  NUM2 = col_double(),
  LLETRA2 = col_logical(),
  BLOC = col_logical(),
  PORTAL = col_character(),
  ESCALA = col_character(),
  PIS = col_character(),
  PORTA = col_character(),
  NUMERO_REGISTRE_GENERALITAT = col_character(),
  NUMERO_PLACES = col_double(),
  LONGITUD_X = col_double(),
  LATITUD_Y = col_double()
)

Se distinguen 3 tipos distintos de información almacenada:

  • Col_character(): se refiere a columnas que incluyen texto.

  • Col_double(): son variables de tipo numérico; específicamente, números con decimales.

  • Col_logical(): se trata de columnas de tipo lógico. Estas solo toman los valores TRUE, si se cumple una determinada condición, o FALSE, si no se cumple.

Seguidamente, echamos un vistazo a las primeras instancias de cada una de las variables con el objetivo de comprender mejor los datos que estamos tratando.

glimpse(datos)
Rows: 14,120
Columns: 21
$ N_EXPEDIENT                 <chr> "01-2011-0123", "01-2011-0137", "01-2011-0…
$ CODI_DISTRICTE              <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, …
$ DISTRICTE                   <chr> "CIUTAT VELLA", "CIUTAT VELLA", "CIUTAT VE…
$ CODI_BARRI                  <dbl> 4, 2, 4, 4, 1, 2, 2, 2, 2, 4, 1, 1, 1, 1, …
$ BARRI                       <chr> "Sant Pere, Santa Caterina i la Ribera", "…
$ TIPUS_CARRER                <chr> "Carrer", "Carrer", "Carrer", "Plaça", "Ca…
$ CARRER                      <chr> "AGULLERS", "ROCA", "AGULLERS", "OLLES", "…
$ TIPUS_NUM                   <dbl> 2, 2, 2, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 1, …
$ NUM1                        <dbl> 22, 8, 22, 9, 1, 8, 8, 8, 8, 5, 15, 15, 15…
$ LLETRA1                     <chr> NA, NA, NA, "B", NA, NA, NA, NA, NA, NA, N…
$ NUM2                        <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ LLETRA2                     <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ BLOC                        <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ PORTAL                      <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ ESCALA                      <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ PIS                         <chr> "1", "1", "2", "1", "PR", "2", "3", "4", "…
$ PORTA                       <chr> NA, "2", NA, "2", "2", "2", "2", "2", "2",…
$ NUMERO_REGISTRE_GENERALITAT <chr> "HUTB-003654", "HUTB-001678", "HUTB-003650…
$ NUMERO_PLACES               <dbl> 3, 3, 3, 9, 6, 3, 3, 3, 3, 4, 5, 6, 5, 6, …
$ LONGITUD_X                  <dbl> 2.180903, 2.171985, 2.180903, 2.182204, 2.…
$ LATITUD_Y                   <dbl> 41.38104, 41.38038, 41.38104, 41.38184, 41…

Por último, vamos a determinar cuántos valores faltantes hay en todo el dataset. Conocer esto es esencial para saber qué información tenemos realmente.

sum(is.na(datos))
[1] 138919

El conjunto de datos en su totalidad contiene más de 138 900 missing values. Esta es una cantidad bastante grande sobre la que se debería tomar decisiones más adelante.

Una vez conocemos más detalladamente los datos, los representamos gráficamente usando la librería Leaflet. No obstante, antes de hacerlo, debemos de asegurarnos de que las columnas que forman las coordenadas no contienen NAs.

sum(is.na(datos$LONGITUD_X))
[1] 6
sum(is.na(datos$LATITUD_Y))
[1] 6

Existen 6 observaciones sin datos para estas variables. Vamos a ver si tienen información para el resto de columnas.

# Nos quedamos con las filas con NAs en la variable LONGITUD_x
na_long <- subset(datos, is.na(LONGITUD_X))

# Vemos su aspecto
glimpse(na_long)
Rows: 6
Columns: 21
$ N_EXPEDIENT                 <chr> NA, NA, NA, NA, NA, NA
$ CODI_DISTRICTE              <dbl> NA, NA, NA, NA, NA, NA
$ DISTRICTE                   <chr> NA, NA, NA, NA, NA, NA
$ CODI_BARRI                  <dbl> NA, NA, NA, NA, NA, NA
$ BARRI                       <chr> NA, NA, NA, NA, NA, NA
$ TIPUS_CARRER                <chr> NA, NA, NA, NA, NA, NA
$ CARRER                      <chr> NA, NA, NA, NA, NA, NA
$ TIPUS_NUM                   <dbl> NA, NA, NA, NA, NA, NA
$ NUM1                        <dbl> NA, NA, NA, NA, NA, NA
$ LLETRA1                     <chr> NA, NA, NA, NA, NA, NA
$ NUM2                        <dbl> NA, NA, NA, NA, NA, NA
$ LLETRA2                     <lgl> NA, NA, NA, NA, NA, NA
$ BLOC                        <lgl> NA, NA, NA, NA, NA, NA
$ PORTAL                      <chr> NA, NA, NA, NA, NA, NA
$ ESCALA                      <chr> NA, NA, NA, NA, NA, NA
$ PIS                         <chr> NA, NA, NA, NA, NA, NA
$ PORTA                       <chr> NA, NA, NA, NA, NA, NA
$ NUMERO_REGISTRE_GENERALITAT <chr> NA, NA, NA, NA, NA, NA
$ NUMERO_PLACES               <dbl> NA, NA, NA, NA, NA, NA
$ LONGITUD_X                  <dbl> NA, NA, NA, NA, NA, NA
$ LATITUD_Y                   <dbl> NA, NA, NA, NA, NA, NA
# Nos quedamos con las filas con NAs en la variable LATITUD_Y
na_lat <- subset(datos, is.na(LATITUD_Y))

# Vemos su aspecto
glimpse(na_lat)
Rows: 6
Columns: 21
$ N_EXPEDIENT                 <chr> NA, NA, NA, NA, NA, NA
$ CODI_DISTRICTE              <dbl> NA, NA, NA, NA, NA, NA
$ DISTRICTE                   <chr> NA, NA, NA, NA, NA, NA
$ CODI_BARRI                  <dbl> NA, NA, NA, NA, NA, NA
$ BARRI                       <chr> NA, NA, NA, NA, NA, NA
$ TIPUS_CARRER                <chr> NA, NA, NA, NA, NA, NA
$ CARRER                      <chr> NA, NA, NA, NA, NA, NA
$ TIPUS_NUM                   <dbl> NA, NA, NA, NA, NA, NA
$ NUM1                        <dbl> NA, NA, NA, NA, NA, NA
$ LLETRA1                     <chr> NA, NA, NA, NA, NA, NA
$ NUM2                        <dbl> NA, NA, NA, NA, NA, NA
$ LLETRA2                     <lgl> NA, NA, NA, NA, NA, NA
$ BLOC                        <lgl> NA, NA, NA, NA, NA, NA
$ PORTAL                      <chr> NA, NA, NA, NA, NA, NA
$ ESCALA                      <chr> NA, NA, NA, NA, NA, NA
$ PIS                         <chr> NA, NA, NA, NA, NA, NA
$ PORTA                       <chr> NA, NA, NA, NA, NA, NA
$ NUMERO_REGISTRE_GENERALITAT <chr> NA, NA, NA, NA, NA, NA
$ NUMERO_PLACES               <dbl> NA, NA, NA, NA, NA, NA
$ LONGITUD_X                  <dbl> NA, NA, NA, NA, NA, NA
$ LATITUD_Y                   <dbl> NA, NA, NA, NA, NA, NA

Para ambos casos, identificamos que no se tienen datos, es decir, se trata de filas vacías. Por lo tanto, las podemos eliminar de nuestra investigación.

# Eliminamos las filas con NAs para las dos variables
datos <- datos %>% 
  filter(!is.na(LONGITUD_X) & !is.na(LATITUD_Y)) 

Comprobamos que lo hemos hecho correctamente y que, por tanto, no hay valores faltantes en las variables LONGITUD_X y LATITUD_Y.

sum(is.na(datos$LONGITUD_X))
[1] 0
sum(is.na(datos$LATITUD_Y))
[1] 0

Efectivamente, se han descartado las 6 filas vacías del conjunto de datos. Ahora, ya podemos empezar a preparar los datos con tal de plasmarlos en un mapa.

En primer lugar, comprobamos que las coordenadas están almacenadas en una columna de tipo numérico y no de tipo textual.

class(datos$LONGITUD_X)
[1] "numeric"
class(datos$LATITUD_Y)
[1] "numeric"

Como son variables numéricas, no necesitan ser transformadas a cualquier otro tipo.

A continuación, convertimos nuestro conjunto de datos en un objeto sf. Es crucial mencionar que el CRS utilizado es el 4326, es decir, el sistema mundial: WGS 84, dado que así lo especifica la documentación del portal de datos del que hemos extraído la información.

Novedades del portal - OpenDataBCN

datos_sf <- sf::st_as_sf(datos, coords=c("LONGITUD_X","LATITUD_Y"), crs = 4326)

Además, creamos una variable de dirección que combine información de distintas columnas. Esta será utilizada como etiqueta con el objetivo de identificar más fácilmente la ubicación de cada uno de los puntos que se muestren.

datos_sf$DIRECCION <- paste(datos$TIPUS_CARRER, datos$CARRER, datos$NUM1, sep = " ")

Finalmente, representamos las observaciones:

datos_sf %>%
  leaflet() %>% 
  addProviderTiles("CartoDB.Voyager") %>%
  addCircles(color = "orange",
             label = ~DIRECCION, 
             radius = 5) 

En este mapa, se puede ver que hay una observación situada en medio del mar. Dicho de otro modo, en el dataset tenemos un punto de interés cultural cuyas coordenadas están fuera de la ciudad de Barcelona.

Tratamiento

En la sección anterior se ha detectado una observación fuera de los límites de la región de interés. Esta podría distorsionar las investigaciones que se basen en este conjunto de datos, ya que hace que la distribución geográfica de los puntos varíe. Por tanto, lo vamos a eliminar a través de la intersección con el polígono que corresponde a Barcelona.

# Extraemos la información del municipio usando el nombre
municipios <- mapSpain::esp_get_munic()

barcelona_muni <- municipios %>% filter(name == "Barcelona") 

# Obtenemos su contorno
bar_muni_contorno <- barcelona_muni %>%
  group_by() %>% 
  summarise()

# Transformamos a la misma proyección ambos objetos
trans <- st_transform(bar_muni_contorno, st_crs(datos_sf))

# Calculamos la intersección
datos_sf <- st_intersection(datos_sf, trans)

Output

Una vez descartado el punto problemático, tenemos un objeto sf formado por 9 846 observaciones y 21 variables.

class(datos_sf)
[1] "sf"         "tbl_df"     "tbl"        "data.frame"
dim(datos_sf)
[1] 9846   21

Gracias al cambio realizado, todos los puntos que se representen a partir de ahora estarán dentro de los límites de la ciudad que estamos trabajando.

datos_sf %>%
  leaflet() %>% 
  addProviderTiles("CartoDB.Voyager") %>%
  addCircles(color = "orange",
             label = ~DIRECCION, 
             radius = 5) 

Cabe destacar que parece que haya puntos superpuestos en la representación. Esto, posiblemente, se debe a que, tal y como se puede leer en la captura de pantalla adjuntada arriba, los datos que usa Open Data BCN están extraídos de la plataforma OpenStreetMap. En esta, los usuarios introducen de forma altruista información espacial, y, en ocasiones, estos suelen añadir un mismo sitio varias veces.

Como hablamos de puntos de interés cultural, no tendría sentido que hubiese dos de ellos en la misma ubicación. Sin embargo, podría tratarse de un mismo edificio en el que dos pisos distintos son considerados de interés. Si echamos un vistazo a los datos, detectaremos que, en algunas filas, las coordenadas coinciden, pero la información almacenada en el resto de columnas es distinta. Por tanto, no los eliminaremos del análisis.

Para finalizar, podemos observar mejor la distribución de los puntos en la siguiente figura:

ggplot() +
  geom_sf(data = bar_muni_contorno) + 
  geom_sf(data = datos_sf,
          color = "orange") + 
  ggtitle("Distribución de los puntos de interés cultural en Barcelona") + 
  theme_void() 

# Guardamos el output en formato GPKG
sf::st_write(datos_sf, "ptos_cult_bcn.gpkg")

El conjunto de datos modificado a lo largo del estudio puede ser descargado, en formato GeoPackage (gpkg), en este enlace.



Proyecto de Innovación Educativa Emergente (PIEE-2737007)