Librerías usadas

library(readxl)
library(tidyverse)
library(FactoMineR)
library(factoextra)
library(cluster)
library(dplyr)
library(gridExtra)
library(RColorBrewer)
library(leaflet)
library(mapview)

Carga y tratamiento de los datos

Para comenzar este objetivo, necesitaremos cargar los datos necesarios. Estos se encuentran en “df_agrupado”. Este archivo fue obtenido a partir del tratamiento de los datos que se le hizo previamente, así como la adición de gran cantidad de datos para diversos objetivos (aunque no los referidos a este objetivo, al estar estos contenidos en otro dataframe). También vamos a eliminar varias filas, las cuales son erróneas. Estas se caracterizan por tener en 0 tanto la cantidad bornes libres como la cantidad de ocupados, lo cual es ilógico. Supondremos que se trata de algún mantenimiento o algún error, así que eliminaremos esas filas y, al tratarse de medias,los resultados no se verán excesivamente modificados.

load("df_agrupado.RData")
incoherentes = subset(df, avg_av==0 & avg_free==0)
df = anti_join(df, incoherentes)
## Joining with `by = join_by(number_, fecha, hora_hora, gid, name, address, open,
## avg_av, avg_free, avg_total, ticket, tmed, prec, tmin, horatmin, tmax,
## horatmax, dir, velmedia, racha, festivo, vacaciones, fin_de_semana, día_semana,
## latitud, longitud, nom_barrio, codbarrio, coddistrit, interés, metro)`
head(df)
##   number_      fecha hora_hora    gid                  name
## 1       1 01/01/2023         0 901581 001_GUILLEN_DE_CASTRO
## 2       1 01/01/2023         1 901581 001_GUILLEN_DE_CASTRO
## 3       1 01/01/2023         2 901581 001_GUILLEN_DE_CASTRO
## 4       1 01/01/2023         3 901581 001_GUILLEN_DE_CASTRO
## 5       1 01/01/2023         4 901581 001_GUILLEN_DE_CASTRO
## 6       1 01/01/2023         5 901581 001_GUILLEN_DE_CASTRO
##                                        address open avg_av avg_free avg_total
## 1 C/GUILLEM DE CASTRO esquina con C/NA JORDANA TRUE   13.5     11.5        25
## 2 C/GUILLEM DE CASTRO esquina con C/NA JORDANA TRUE   12.0     13.0        25
## 3 C/GUILLEM DE CASTRO esquina con C/NA JORDANA TRUE   11.0     14.0        25
## 4 C/GUILLEM DE CASTRO esquina con C/NA JORDANA TRUE   10.5     14.5        25
## 5 C/GUILLEM DE CASTRO esquina con C/NA JORDANA TRUE    8.5     16.5        25
## 6 C/GUILLEM DE CASTRO esquina con C/NA JORDANA TRUE    7.0     18.0        25
##   ticket tmed prec tmin horatmin tmax horatmax dir velmedia racha festivo
## 1   TRUE 12,1  0,0  7,0    08:10 17,2    13:00   2      0,0   4,2       0
## 2   TRUE 12,1  0,0  7,0    08:10 17,2    13:00   2      0,0   4,2       0
## 3   TRUE 12,1  0,0  7,0    08:10 17,2    13:00   2      0,0   4,2       0
## 4   TRUE 12,1  0,0  7,0    08:10 17,2    13:00   2      0,0   4,2       0
## 5   TRUE 12,1  0,0  7,0    08:10 17,2    13:00   2      0,0   4,2       0
## 6   TRUE 12,1  0,0  7,0    08:10 17,2    13:00   2      0,0   4,2       0
##   vacaciones fin_de_semana día_semana  latitud   longitud nom_barrio codbarrio
## 1          1             1          D 39.48004 -0.3829293   EL CARME         3
## 2          1             1          D 39.48004 -0.3829293   EL CARME         3
## 3          1             1          D 39.48004 -0.3829293   EL CARME         3
## 4          1             1          D 39.48004 -0.3829293   EL CARME         3
## 5          1             1          D 39.48004 -0.3829293   EL CARME         3
## 6          1             1          D 39.48004 -0.3829293   EL CARME         3
##   coddistrit interés metro
## 1          1       2     0
## 2          1       2     0
## 3          1       2     0
## 4          1       2     0
## 5          1       2     0
## 6          1       2     0

Debido al cambio de tratamiento y del enfoque del objetivo, las columnas metro y lugares de interés van a ser eliminadas del dataframe, así como muchas variables como “distrito” o “tmin”, que no son relevantes para nuestro objetivo. Además, la columna “día de la semana” contiene, al mismo tiempo, valores como “V” de viernes y “F” de “Friday”, que serán reemplazados por la “V”. De esta manera, los quedamos con las columnas de id, hora, fecha, nombre, avg_total,avg_free,avg_av, latitud y longitud.

df=df[,c(1:3,5,8:10,24:26)]
df$día_semana[df$día_semana=="F"]="V"

Aprovechando la columna de interés eliminada, se ha vuelto a revisar la ubicación de las estaciones de bici y los monumentos que tienen cerca, mejorando la precisión del número de lugares de interés y añadiendo, además, cuáles son estos para su posterior clasificación. Añadiremos que las estaciones fueron seleccionadas a partir de una exhaustiva búsqueda y revisión de todas las estaciones en un mapa, así como qué tenían alrededor. Lo concantenaremos con nuestro dataframe,

interes=read_xlsx("lugares_de_interes.xlsx")
df_interés=merge(df,interes,by="number_")
head(df_interés)
##   number_      fecha hora_hora                  name avg_av avg_free avg_total
## 1       1 01/01/2023         0 001_GUILLEN_DE_CASTRO   13.5     11.5        25
## 2       1 01/01/2023         1 001_GUILLEN_DE_CASTRO   12.0     13.0        25
## 3       1 01/01/2023         2 001_GUILLEN_DE_CASTRO   11.0     14.0        25
## 4       1 01/01/2023         3 001_GUILLEN_DE_CASTRO   10.5     14.5        25
## 5       1 01/01/2023         4 001_GUILLEN_DE_CASTRO    8.5     16.5        25
## 6       1 01/01/2023         5 001_GUILLEN_DE_CASTRO    7.0     18.0        25
##   día_semana  latitud   longitud num_int Otro CC I U H D T P Categoría
## 1          D 39.48004 -0.3829293       3    1  0 1 1 0 0 0 0 Monumento
## 2          D 39.48004 -0.3829293       3    1  0 1 1 0 0 0 0 Monumento
## 3          D 39.48004 -0.3829293       3    1  0 1 1 0 0 0 0 Monumento
## 4          D 39.48004 -0.3829293       3    1  0 1 1 0 0 0 0 Monumento
## 5          D 39.48004 -0.3829293       3    1  0 1 1 0 0 0 0 Monumento
## 6          D 39.48004 -0.3829293       3    1  0 1 1 0 0 0 0 Monumento

Ahora, hemos añadido el lugar de interés, cuál sería el más priotario en la estación y el número de lugares que tiene alrededor como suma de las columnas binarias que lo indican. Sin embargo, decidimos solamente usar el lugar más priotiario. Sin embargo, antes eliminaremos las columnas binarias que había en este archivo excel que indicaban los lugares de interés cercanos, ya que solo nos va a interesar el lugar que se considera más influyente de los varios que pueda tener.

Al tener diferente número de avg_total (número total de bicicletas que se pueden enganchar en la estación), se va a hacer una conversión para poder tratarlo como porcentaje de ocupación y usarlo para nuestro análisis. Utilizando el porcentaje de ocupación, los datos ya están escalados para futuros análisis. Eliminamos las columnas de avg_free, avg_av y avg_total al dejar de ser necesarias.

df_interés$porc_ocu=df_interés$avg_av*100/df_interés$avg_total
df_interés=df_interés[,c(1:4,8:10,20,21)]
df_interés=df_interés[df_interés$Categoría!="Nada",]
head(df_interés)
##   number_      fecha hora_hora                  name día_semana  latitud
## 1       1 01/01/2023         0 001_GUILLEN_DE_CASTRO          D 39.48004
## 2       1 01/01/2023         1 001_GUILLEN_DE_CASTRO          D 39.48004
## 3       1 01/01/2023         2 001_GUILLEN_DE_CASTRO          D 39.48004
## 4       1 01/01/2023         3 001_GUILLEN_DE_CASTRO          D 39.48004
## 5       1 01/01/2023         4 001_GUILLEN_DE_CASTRO          D 39.48004
## 6       1 01/01/2023         5 001_GUILLEN_DE_CASTRO          D 39.48004
##     longitud Categoría porc_ocu
## 1 -0.3829293 Monumento       54
## 2 -0.3829293 Monumento       48
## 3 -0.3829293 Monumento       44
## 4 -0.3829293 Monumento       42
## 5 -0.3829293 Monumento       34
## 6 -0.3829293 Monumento       28

El objetivo del análisis, que es observar si hay relación entre el comportamiento de las estaciones y los lugares de interés, se llevará a cabo realizando, en primer lugar, un clustering. De esta manera, se agruparán las estaciones que tengan un comportamiento similar.

Sin embargo, con los datos de los que disponemos, es imposible realizar ningún tipo de análisis útil. Por lo tanto, hay que realizar una transformación previa, agrupando todas las filas por horas utilizando su media. Este código se utilizará posteriormente para obtener los días laborables y los fines de semana,transformado en una función.

El propósito de este código es escoger 24 dataframes asociados a las 24 horas que tengan todas las filas que tengan dicha hora indicada. Después, se comprime en las 276 estaciones, haciendo la media de todas los porcentajes de ocupación.

df_horas = subset(df_interés, select = c(number_, hora_hora, porc_ocu))
df_horas = group_by(df_horas, hora_hora, number_) %>% summarise(avg_porc=mean(porc_ocu))
## `summarise()` has grouped output by 'hora_hora'. You can override using the
## `.groups` argument.
medianoche = subset(df_horas, df_horas$hora_hora==0, -c(hora_hora))
una = subset(df_horas, df_horas$hora_hora==1, -c(hora_hora))
dos = subset(df_horas, df_horas$hora_hora==2, -c(hora_hora))
tres = subset(df_horas, df_horas$hora_hora==3, -c(hora_hora))
cuatro = subset(df_horas, df_horas$hora_hora==4, -c(hora_hora))
cinco = subset(df_horas, df_horas$hora_hora==5, -c(hora_hora))
seis = subset(df_horas, df_horas$hora_hora==6, -c(hora_hora))
siete = subset(df_horas, df_horas$hora_hora==7, -c(hora_hora))
ocho = subset(df_horas, df_horas$hora_hora==8, -c(hora_hora))
nueve = subset(df_horas, df_horas$hora_hora==9, -c(hora_hora))
diez = subset(df_horas, df_horas$hora_hora==10, -c(hora_hora))
once = subset(df_horas, df_horas$hora_hora==11, -c(hora_hora))
mediodia = subset(df_horas, df_horas$hora_hora==12, -c(hora_hora))
una_pm = subset(df_horas, df_horas$hora_hora==13, -c(hora_hora))
dos_pm = subset(df_horas, df_horas$hora_hora==14, -c(hora_hora))
tres_pm = subset(df_horas, df_horas$hora_hora==15, -c(hora_hora))
cuatro_pm = subset(df_horas, df_horas$hora_hora==16, -c(hora_hora))
cinco_pm = subset(df_horas, df_horas$hora_hora==17, -c(hora_hora))
seis_pm = subset(df_horas, df_horas$hora_hora==18, -c(hora_hora))
siete_pm = subset(df_horas, df_horas$hora_hora==19, -c(hora_hora))
ocho_pm = subset(df_horas, df_horas$hora_hora==20, -c(hora_hora))
nueve_pm = subset(df_horas, df_horas$hora_hora==21, -c(hora_hora))
diez_pm = subset(df_horas, df_horas$hora_hora==22, -c(hora_hora))
once_pm = subset(df_horas, df_horas$hora_hora==23, -c(hora_hora))

colnames(medianoche) = c("number_", "avg_00")
colnames(una) = c("number_", "avg_01")
colnames(dos) = c("number_", "avg_02")
colnames(tres) = c("number_", "avg_03")
colnames(cuatro) = c("number_", "avg_04")
colnames(cinco) = c("number_", "avg_05")
colnames(seis) = c("number_", "avg_06")
colnames(siete) = c("number_", "avg_07")
colnames(ocho) = c("number_", "avg_08")
colnames(nueve) = c("number_", "avg_09")
colnames(diez) = c("number_", "avg_10")
colnames(once) = c("number_", "avg_11")
colnames(mediodia) = c("number_", "avg_12")
colnames(una_pm) = c("number_", "avg_13")
colnames(dos_pm) = c("number_", "avg_14")
colnames(tres_pm) = c("number_", "avg_15")
colnames(cuatro_pm) = c("number_", "avg_16")
colnames(cinco_pm) = c("number_", "avg_17")
colnames(seis_pm) = c("number_", "avg_18")
colnames(siete_pm) = c("number_", "avg_19")
colnames(ocho_pm) = c("number_", "avg_20")
colnames(nueve_pm) = c("number_", "avg_21")
colnames(diez_pm) = c("number_", "avg_22")
colnames(once_pm) = c("number_", "avg_23")

df_horas2 = list(medianoche,una,dos,tres,cuatro,cinco,seis,siete,ocho,nueve,diez,once,mediodia,una_pm,dos_pm,
                  tres_pm,cuatro_pm,cinco_pm,seis_pm,siete_pm,ocho_pm,nueve_pm,diez_pm,once_pm)
df_horas  = df_horas2 %>% reduce(full_join, by = "number_")

df_horas_todo=merge(df_interés,df_horas,by="number_")

Al final, se concatena con el original para mantener el dataframe original con todos los datos que se han ido utilizando. No hay problema con que estén todas las estaciones a todas las horas, pues todas las estaciones van a tener su respectiva media de horas, indistintamente de su fecha u hora.

A continuación, nos interesa el comportamiento de las estaciones con lugares de interés, así que eliminaremos las que tengan “Nada” en la variable “Categoría”. Además, solo necesitaremos una fila de cada estación. No habrá problema pues, como se ha comentado antes, todas las estaciones tienen valores idénticos en las variables de media por hora. Por lo tanto, eliminamos las columnas de hora, fecha y día de la semana, latitud y longitud, para solamente tener el nombre, el id, latitud, longitud, categoría y sus medias. Cuando necesitemos más datos, los volveremos a tomar.

df_analizar=df_horas_todo[,c(1,4,10:33,8,6,7)]
estaciones_interes=df_interés$Categoría!="Nada"
df_analizar=df_analizar[df_interés$Categoría!="Nada",]
df_analizar=unique(df_analizar)
row.names(df_analizar)=NULL
head(df_analizar)
##   number_                                         name    avg_00    avg_01
## 1       1                        001_GUILLEN_DE_CASTRO 17.554217 17.040161
## 2       4          004_PLAZA_DE_LA_VIRGEN_CALLE_BAILIA 11.855422  9.208835
## 3      11               011_PZA_AYTO_CON_CALLE_COTANDA  8.723197  5.592105
## 4      13 013_PZA. ALFONSO MAGNANIMO_CON_CALLE_LA_NAVE 26.229920 21.490127
## 5      15                                   015_RIBERA  7.831325  5.634302
## 6      16                                  016_COLON I 13.237952 10.391566
##      avg_02    avg_03    avg_04    avg_05    avg_06    avg_07   avg_08   avg_09
## 1 16.421687 16.301205 15.959839 15.542169 15.192771 15.447390 20.89837 23.19512
## 2  7.520482  5.554217  5.437751  5.967871  8.100402 10.181526 14.73577 19.47561
## 3  4.743340  4.690546  4.438759  4.637752  4.459064  7.570434 22.70322 40.12747
## 4 19.436914 18.260542 17.708333 17.394578 17.507530 18.113286 25.80454 42.72104
## 5  4.672218  4.287739  4.134184  4.010158  4.296598  6.095559 14.00646 19.62877
## 6  8.421687  7.595382  7.655622  8.960843 10.075301 13.558233 25.53354 41.55488
##     avg_10   avg_11   avg_12   avg_13   avg_14   avg_15   avg_16   avg_17
## 1 20.63008 19.82114 19.55691 18.75610 17.07317 15.65854 18.53659 16.87398
## 2 21.89431 28.49187 32.89837 30.85366 26.14634 17.77642 15.48780 14.52033
## 3 49.96793 51.81177 53.20419 51.94526 40.35494 29.24691 22.59259 24.86831
## 4 48.01829 49.79251 51.65142 53.34096 49.36907 47.59909 47.75152 46.64211
## 5 23.80739 25.75024 26.70074 24.82066 20.22657 18.29268 17.67396 17.43484
## 6 58.56707 67.46443 71.38720 72.20020 60.37602 55.45732 61.34146 63.04370
##     avg_18   avg_19   avg_20   avg_21   avg_22   avg_23  Categoría  latitud
## 1 16.49593 16.02033 16.39024 16.60976 18.24390 18.59350  Monumento 39.48004
## 2 18.65854 24.02846 24.23577 25.61463 25.73171 19.89024  Monumento 39.47675
## 3 26.94650 26.63786 27.22634 25.81481 21.01235 15.02881  Monumento 39.47119
## 4 46.65481 45.95613 42.31877 36.91565 32.67700 31.33469 Centro_Com 39.47206
## 5 17.08812 18.13725 18.56468 17.23458 14.32329 10.94572  Monumento 39.46909
## 6 63.72967 60.51829 52.81504 33.51524 22.85061 18.26728 Centro_Com 39.47009
##     longitud
## 1 -0.3829293
## 2 -0.3753424
## 3 -0.3767844
## 4 -0.3708744
## 5 -0.3756374
## 6 -0.3704334

Estaciones cercanas a lugares de interés: Clustering.

Una vez obtenidas las estaciones que queremos, así como los datos que necesitamos para los análisis, se procederá a hacer un clustering para agrupar las estaciones por similar comportamiento. Veamos si existe agrupamiento entre las estaciones mediante un mapa de calor.

datos_elegidos=df_analizar[,3:26]
midist <- get_dist(datos_elegidos, stand = TRUE, method = "euclidean")
fviz_dist(midist, show_labels = TRUE, lab_size = 0.3,
          gradient = list(low = "#00AFBB", mid = "white", high = "#FC4E07"))

Se puede observar que existe agrupamiento entre las estaciones. Se pueden ver dos grandes clusters, a la vez que varias agrupaciones más pequeñas. No nos interesa tanto tener dos grupos grandes, si no que resulta más interesante encontrar más grupos que puedan asociarse con las diferentes estaciones según su categoría. Comprobemos la tendencia al agrupamiento más analíticamente.

set.seed(55)
myN = c(30,50,96)  # m
myhopkins = NULL
myseed = sample(1:1000, 3)
for (i in myN) {
  for (j in myseed) {
    tmp = get_clust_tendency(data = datos_elegidos, n = i, graph = FALSE, seed = j)
    myhopkins = c(myhopkins, tmp$hopkins_stat)
  }
}
summary(myhopkins)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##  0.7825  0.7899  0.7938  0.7958  0.7986  0.8107

Según el índice de Hopkings, se observa que existe una agrupamiento considerable entre las diferentes estaciones al estar alrededor del 0’8. Con lo cual, tiene sentido realizar un clustering a estos datos.

Tras utilizar varios métodos de clustering, (jerárquicos como Ward o el método de la media, y de partición como k-means), hemos decidido utilizar el método de k-medoides. Los algortimos de partición devuelven malos resultados, con los clusters solapados y con difícil interpretación si el número de clusteres es diferente de 2. Los buenos resultados de k-medoides y su capacidad de devolver el medoide (la estación que mejor representará a su cluster) lo convierten en el algoritmo más adecuado para estos análisis.

p1 = fviz_nbclust(x = datos_elegidos, FUNcluster = pam, method = "silhouette", 
             k.max = 10, verbose = FALSE) +
  labs(title = "K-Medoides")
p2 = fviz_nbclust(x = datos_elegidos, FUNcluster = pam, method = "wss", 
             k.max = 10, verbose = FALSE) +
  labs(title = "K-Medoides")
grid.arrange(p1, p2, nrow = 1)

Analizando estos gráficos que nos ofrecen la calidad de los clusters en función del número de estos, observamos que el número ideal son 2 clusters. Sin embargo, no tiene demasiado interés utilizar 2 clusters teniendo la cantidad de estaciones que tenemos y los diferentes comportamientos que se infiere tienen las casi 100 estaciones distintas. Cuatro clusters tal vez pueda tener todavía una SCR ligeramente alta, pero un número diferente de clusters tiene un coeficiente de Silhouette comparativamente peor que las SCR, así que este será el número de clusters elegido. Solamente con este número de agrupaciones, empezamos a intuir que los tipos de lugares de interés no van a tener cada uno su propio cluster.

Vamos a visualizar los clusters.

k=4
clust4 <- pam(datos_elegidos, k = k)
tabla=table(clust4$clustering)

p1 = fviz_cluster(object = list(data=datos_elegidos, cluster=clust4$clustering), stand = FALSE,
             ellipse.type = "convex", geom = "point", show.clust.cent = FALSE,
             labelsize = 8)  +
  labs(title = "K-MEDOIDES",
       subtitle = "Dist euclidea, K=4") +
  theme_bw() +
  theme(legend.position = "bottom")
grid.arrange(p1,nrow = 1)

Los clusters, que se visualizan sos muy diferenciados y separados, además de tener, a simple vista, una cantidad de estaciones relativamente balanceadas o, al menos, aceptable y útil para los estudios. Veámoslo.

table(clust4$clustering)
## 
##  1  2  3  4 
## 25 18 21 35

Tal como sospechábamos, las estaciones están medianamente bien repartidas entre los clusters, aunque el segundo cluster es la mitad que el cuarto, pero no son pocas estaciones igualmente y vendrá bien en nuestro estudios (después de todo, supone casi el 20% de nuestros datos).

¿Son los clusters realmente significativos?

Antes de continuar con nuestro trabajo, debemos conocer si verdaderamente están relacionados los lugares de interés de cada estación con el cluster asignado. Para ello, realizaremos y un test de independencia y un análisis factorial de correspondencias.

datos_elegidos$clus_part=clust4$clustering
tablacruzada=table(df_analizar$Categoría,datos_elegidos$clus_part)
chisq.test(tablacruzada)
## Warning in chisq.test(tablacruzada): Chi-squared approximation may be incorrect
## 
##  Pearson's Chi-squared test
## 
## data:  tablacruzada
## X-squared = 70.177, df = 21, p-value = 3.292e-07

El test de independencia de chi-cuadrado arroja conclusiones favorables al estudio, demostrando que existe relación entre los clusters (las agrupaciones de comportamientos) y los lugares de interés que tienen sus respectivas estaciones.

El Análisis Factorial de Correspondencias no tiene como objetivo un modelo, si no ver cómo de relacionados pueden estar los datoss con los clusters, ya que las variables latentes indican la correlación entre los grupos de variables.

res.afc = CA(tablacruzada, graph = FALSE)
eig.val <- get_eigenvalue(res.afc)
Vmedia = 100 * (1/nrow(eig.val))
fviz_eig(res.afc, addlabels = TRUE) +
  geom_hline(yintercept=Vmedia, linetype=2, color="red")

Un 80% de la variabilidad de los datos puede explicarse con solo dos componentes, y un 100% con las tres componentes. Esto indica que las variables proporcionadas están muy relacionadas entre sí, siguiendo con las conclusiones obtenidas del test de independencia.

Para observar cómo influyen las variables de lugar de interés con respecto a sus respectivos clusters, vamos a visualizarlos en un biplot.

fviz_ca_biplot(res.afc, repel = TRUE)

Mediante este gráfico, se puede apreciar la contribución de las variables de interés a los clusters.

Resultados de los análisis a nivel general

Estudiaremos también el comportamiento de los medoides de los clusters mediante gráficos y tablas cruzadas para observar cuántos lugares han caído en cada cluster.

array=clust4$medoids
array=as.data.frame(array)
array_tras=as.data.frame(t(array[1:24]))
colnames(array_tras)=c("c1","c2","c3","c4")
array_tras$hora=as.double(c(0:23))
x <- ggplot(data = array_tras) +
  geom_line(aes(x = hora, y = c1, color = "c1"), linetype = "solid", size = 1) +
  geom_line(aes(x = hora, y = c2, color = "c2"), linetype = "solid", size = 1) +
  geom_line(aes(x = hora, y = c3, color = "c3"), linetype = "solid", size = 1) +
  geom_line(aes(x = hora, y = c4, color = "c4"), linetype = "solid", size = 1) +
  #geom_line(aes(x = hora, y = c5), color = "black", linewidth = 1) +
 # geom_line(aes(x = hora, y = c6), color = "pink", linewidth = 1) +
#  geom_line(aes(x = hora, y = c7), color = "aquamarine", linewidth = 1) +

  theme_minimal()
## Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
## ℹ Please use `linewidth` instead.
x

Aquí se ve el gráfico a lo largo de las horas de media. Para poder estudiar el gráfico y la tabla conjuntamente, los uniremos mediante una librería en una celda.

tabla_grob <- tableGrob(tablacruzada)
grafico_grob <- ggplotGrob(x)
grid.arrange(tabla_grob, grafico_grob, ncol = 2)

Aquí tenemos los resultados del clustering, conjuntamente con la tabla de frecuencias.

Para finalizar con estar parte más general del objetivo, lo visualizaremos en un mapa. Hay que concatenar las columnas de latitud y longitud, pero será algo simple ya que las estaciones están ordenadas en todos los dataframes, así que simplemente habrá que crear un dataframe con sus respectivas columnas de coordenadas y el cluster al que pertenecen.

df_mapa_todo=data.frame(df_analizar$number_, df_analizar$name,df_analizar$latitud, df_analizar$longitud)
df_mapa_todo$clust_part=clust4$clustering
colnames(df_mapa_todo)=c("number_","name","latitud","longitud","clust_part")
palette=colorFactor(palette = rainbow(4), domain = df_mapa_todo$clust_part)
leaflet() %>%
  addTiles() %>%
  addCircleMarkers(data = df_mapa_todo, lng = ~longitud, lat = ~latitud, label=~name,
                  radius = 6, fillOpacity = 1, stroke = FALSE, color=~palette(clust_part)) %>%
  addLegend("bottomright", pal = palette, values = unique(df_mapa_todo$clust_part),
            labels = unique(df_mapa_todo$clust_part), title = "Cluster")

Análisis según los días laborables y los fines de semana: Resultados

Ahora procederemos a estudiar los diferentes días de la semana. El objetivo de esta parte final del estudio es ver si hay un comportamiento diferente entre los días laborales y el fin de semana.

Para ello, necesitamos hacer varias operaciones con los datos nuevamente. Para obtener los datos de cada día de la semana, hacemos uso del código utilizado anteriormente, implementado en una función. Esta función realiza lo mismo que el conjunto de código utilizado previamente, teniendo como parámetro el conjunto de datos al que queremos realizarle la transformación (los días que queremos incluir). Teniendo ya la manera de obtener los datos, empezaremos a realizar las mismas operaciones sobre los días de la semana para ver qué comportamiento tienen las estaciones y cómo varían los clusters y las estaciones a las que se mueven los clusters.

Para agilizar un poco el documento, se mostrarán ya elegidos el número de clusters, así como se ocultará el código de la función (repetimos que es el mismo usado anteriormente pero convertido en una función con parámetro), así como el código del los gráficos y los mapas, que es idéntico al utilizado anteriormente.

## `summarise()` has grouped output by 'hora_hora'. You can override using the
## `.groups` argument.

Visualicemos el mapa.

Por último, vamos con los fines de semana.

## `summarise()` has grouped output by 'hora_hora'. You can override using the
## `.groups` argument.

Visualicemos los resultados en el mapa.

A la vista de los mapas y de todos los gráficos, es el momento de empezar a discutir los resultados de todos nuestros análisis en la memoria final.