1. Carga de los datasets

En primer lugar, realizamos la carga de los ficheros csv obtenidos del articulo con el que vamos a trabajar con los datos obtenidos de los sensores.

# Realizamos la carga del csv.
df_LAlbea <-read.csv("CEIP_LAlbea_ValldAbav2.csv")

# Mostramos la estructura del dataframe cargado.
str(df_LAlbea)
## 'data.frame':    34569 obs. of  14 variables:
##  $ published_at: chr  "2021-06-01T14:30:06.735Z" "2021-06-01T14:15:06.784Z" "2021-06-01T14:20:06.733Z" "2021-06-01T14:35:06.728Z" ...
##  $ date_time   : chr  "2021-06-01T14:30:06" "2021-06-01T14:15:06" "2021-06-01T14:20:06" "2021-06-01T14:35:06" ...
##  $ date        : chr  "2021-06-01" "2021-06-01" "2021-06-01" "2021-06-01" ...
##  $ month       : chr  "Jun" "Jun" "Jun" "Jun" ...
##  $ w_day       : chr  "Tue" "Tue" "Tue" "Tue" ...
##  $ time        : chr  "14:30:06" "14:15:06" "14:20:06" "14:35:06" ...
##  $ hour        : int  14 14 14 14 14 14 14 14 14 14 ...
##  $ mint        : int  30 15 20 35 40 10 55 0 45 50 ...
##  $ sec         : int  6 6 6 6 6 6 6 6 6 6 ...
##  $ temp        : num  29.8 29.8 29.8 29.9 30 ...
##  $ hum         : num  46.3 47.5 47.2 46.1 45.9 ...
##  $ co2         : int  390 391 391 393 395 399 400 401 402 402 ...
##  $ bat         : num  85.2 81.1 82.5 86.5 87.9 ...
##  $ sensor_id   : chr  " CO2_01" " CO2_01" " CO2_01" " CO2_01" ...
# Mostramos una muestra de 10 registros del dataset cargado.
df_LAlbea[sample(nrow(df_LAlbea), 10), ]

Para cargar este csv eliminamos el caracter ” (doble comilla) ya que hemos comprobado previamente el formato del fichero.

# Se define el carácter a eliminar (comillas dobles).
caracter_a_eliminar <- "\\\""

# Leemos el contenido del segundo csv.
csv <- readLines('CEIP_SantMiquel_Vilafames.csv')

# Reemplazamos el carácter con una cadena vacía.
csv_modificado <- gsub(caracter_a_eliminar, "", csv)

# Escribe el contenido modificado de nuevo en el fichero.
writeLines(csv_modificado, 'CEIP_SantMiquel_Vilafames_new.csv')

# Realizamos la carga del csv modificado.
df_SantMiquel <-read.csv("CEIP_SantMiquel_Vilafames_new.csv")

# Mostramos la estructura del dataframe cargado.
str(df_SantMiquel)
## 'data.frame':    38890 obs. of  13 variables:
##  $ published_at: chr  "2021-05-03T07:00:05.241Z" "2021-05-03T07:05:05.348Z" "2021-05-03T07:20:05.162Z" "2021-05-03T07:25:05.047Z" ...
##  $ date_time   : chr  "2021-05-03T07:00:05" "2021-05-03T07:05:05" "2021-05-03T07:20:05" "2021-05-03T07:25:05" ...
##  $ date        : chr  "5/3/2021" "5/3/2021" "5/3/2021" "5/3/2021" ...
##  $ month       : chr  "May" "May" "May" "May" ...
##  $ w_day       : chr  "Mon" "Mon" "Mon" "Mon" ...
##  $ hour        : int  7 7 7 7 7 7 7 8 8 8 ...
##  $ mint        : int  0 5 20 25 35 40 55 0 0 5 ...
##  $ sec         : int  5 5 5 5 4 4 5 4 5 5 ...
##  $ temp        : num  26.6 26.5 26.6 26.7 27.2 ...
##  $ hum         : num  39.4 39.6 40.1 40.3 39.4 ...
##  $ co2         : int  302 301 376 520 393 439 385 395 730 386 ...
##  $ bat         : num  99.5 99.5 99.5 99.5 99.5 ...
##  $ sensor_id   : chr  " CO2_06" " CO2_06" " CO2_06" " CO2_06" ...
# Mostramos una muestra de 10 registros del dataset generado.
df_SantMiquel[sample(nrow(df_SantMiquel), 10), ]

Observamos que ambos dataset son practicamente iguales salvo que el primero contiene una obserbación más, por lo que se decide que trabajar con este primer dataset para emplearlo en el estudio del proyecto.

2. CEIP_LAlbea_ValldAbav2

2.1 Transformación de los datos

Con el dataset ya cargado, lo primero que haremos será examinar su estructura y comprobar si cada una de las variables se ajustan al tipo de dato del valor que informan.

Form.Basic = c("striped", "hover", "condensed", "responsive")
kbl(cbind(sapply(df_LAlbea, class)),
    caption="Tipo de datos de las variable") %>%
  kable_styling(bootstrap_options = Form.Basic) %>%
  scroll_box(width = "100%", 
             height = "400px")
Tipo de datos de las variable
published_at character
date_time character
date character
month character
w_day character
time character
hour integer
mint integer
sec integer
temp numeric
hum numeric
co2 integer
bat numeric
sensor_id character

Eliminamos los espacios en blanco de las variables de tipo character que no son fechas.

# Eliminamos los espacios en blanco de los valores de las variables character.
df_LAlbea$month <- trimws(df_LAlbea$month)
df_LAlbea$w_day <- trimws(df_LAlbea$w_day)
df_LAlbea$sensor_id <- trimws(df_LAlbea$sensor_id)

Las variables sensor_id, month y w_day son variables cualitativas, por lo que modificamos el tipo de datos de estas para corregirlo.

df_LAlbea <- df_LAlbea %>%
  dplyr::mutate(sensor_id = as.factor(sensor_id),
                month = as.factor(month),
                w_day = as.factor(w_day))

Las variables published_at y date_time son de tipo timestamp, por lo que también corregimos el tipo de dato.

# Se convierte date_time a timestamp sin los segundos
df_LAlbea$date_time <- as.POSIXct(strptime(df_LAlbea$date_time, format="%Y-%m-%dT%H:%M:%OS", tz= "UTC"))
df_LAlbea$date_time <- format(df_LAlbea$date_time, "%Y-%m-%d %H:%M")
df_LAlbea$date_time <- as.POSIXct(df_LAlbea$date_time, format="%Y-%m-%d %H:%M", tz= "UTC")

# Se convierte published_at a timestamp
df_LAlbea$published_at <- as.POSIXct(df_LAlbea$published_at, format="%Y-%m-%dT%H:%M:%OS", tz="UTC")

Las variables date y time son de tipo fecha, por lo que también corregimos el tipo de dato.

# Se convierten a tipo date
df_LAlbea$date <- as.Date(df_LAlbea$date, format="%Y-%m-%d")
df_LAlbea$time <- strptime(df_LAlbea$time, format="%H:%M:%S")

2.2 Registros duplicados

Vamos a comprobar si existen registros duplicados. En caso afirmativo, eliminaremos dichos registros del dataset.

# Obtenemos los registros duplicados que pudieran existir en el dataset.
duplicados_LAlbea <- duplicated(df_LAlbea)

# Mostramos los registros duplicados.
df_LAlbea[duplicados_LAlbea, ]
# Contamos los registros duplicados.
sum(duplicados_LAlbea)
## [1] 169
# Eliminamos los registros duplicados.
df_LAlbea <- df_LAlbea[!duplicados_LAlbea, ]

2.3 Valores nulos

Comprobamos si existen valores nulos en el dataset.

# Comprobamos la existencia de valores nulos.
sapply(df_LAlbea, function(x) sum(is.na(x)))
## published_at    date_time         date        month        w_day         time 
##            0            0            0            0            0            0 
##         hour         mint          sec         temp          hum          co2 
##            0            0            0            0            0            0 
##          bat    sensor_id 
##            0            0

2.4 Valores extremos

Comprobamos si existen valores extremos en el dataset.

# Generamos los valores mínimos y máximos de las variables cuantitativas.
num_vars_LAlbea <- c('hour', 'mint', 'sec', 'temp', 'hum', 'co2','bat')
min_var <- as.vector(sapply(df_LAlbea[, num_vars_LAlbea], min,na.rm = TRUE))
max_var <- as.vector(sapply(df_LAlbea[, num_vars_LAlbea], max,na.rm = TRUE))

# Convertimos los valores a formato no exponencial
min_var.formatted <- format(min_var, scientific = FALSE)
max_var.formatted <- format(max_var, scientific = FALSE)

# Mostramos la tabla con los resultados
kable(data.frame(Variables= names(df_LAlbea[, num_vars_LAlbea]),
                 min = min_var.formatted,
                 max = max_var.formatted),
      digits=2, caption="Mínimo y máximo") %>%
  kable_styling(bootstrap_options = Form.Basic)
Mínimo y máximo
Variables min max
hour 0.00 23.00
mint 0.00 55.00
sec 3.00 38.00
temp -13600000000000000.00 44.46
hum 21.04 64.18
co2 358.00 1989.00
bat 0.00 100.00

Se observa que existe algún valor o valores erróneos en la variable temp, decidimos sustituir dichos valores por la media de la temperatura por sensor_id, date y hour, ya que el resto de valores de las variables restantes son correctos. A continuación, comprobamos si se soluciona dicho problema.

# Mostramos todos los registros por debajo del valor 0.
print(df_LAlbea[df_LAlbea$temp < 0, ])
##              published_at           date_time       date month w_day
## 15972 2021-06-14 04:50:05 2021-06-14 04:50:00 2021-06-14   Jun   Mon
##                      time hour mint sec      temp   hum co2   bat sensor_id
## 15972 2024-05-21 04:50:05    4   50   5 -1.36e+16 34.17 445 98.83    CO2_03
# Sustituimos el valor de la temperatura por la media
df_LAlbea <- df_LAlbea %>%
  group_by(sensor_id, date, hour) %>%
  mutate(
    temp_media = mean(temp[temp > 0]),
    temp = ifelse(temp < 0, temp_media, temp)
  ) %>%
  ungroup() %>%
  select(-temp_media)

# Mostramos estadísticas de la variable temp.
summary(df_LAlbea$temp)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   22.60   28.72   30.00   29.96   31.29   44.46
# Crear un gráfico para cada variable
fig <- plot_ly(y = df_LAlbea$hour, type = "box", name="Hora")
fig <- fig %>% add_trace(y = df_LAlbea$mint, name="Minuto")
fig <- fig %>% add_trace(y = df_LAlbea$sec, name="Segundo")
fig <- fig %>% add_trace(y = df_LAlbea$temp, name="Temperatura")
fig <- fig %>% add_trace(y = df_LAlbea$hum, name="Humedad")
fig <- fig %>% add_trace(y = df_LAlbea$bat, name="Batería")
fig <- fig %>% layout(title = "Distribución de las variables", yaxis = list(title = 'Valores'))
fig
fig <- plot_ly(y = df_LAlbea$co2, type = "box", name="CO2")
fig <- fig %>% layout(title = "Distribución de las variables", yaxis = list(title = 'Valores'))
fig

En este nuevo análisis se obserba que todas las variables contienen valores dentro de un rango aceptable y no existen valores outlier.

2.5 Estudio de las variables

Ahora examinaremos las variables del dataset mostrando los datos resumen de cada una de ellas.

options(knitr.kable.NA = '')
kable(summary(df_LAlbea),
      digits=2, 
      align='l', 
      caption="Datos resumen") %>%
  kable_styling(bootstrap_options = Form.Basic) %>%
    scroll_box(width = "100%", height = "470px")
Datos resumen
published_at date_time date month w_day time hour mint sec temp hum co2 bat sensor_id
Min. :2021-06-01 00:00:05.73 Min. :2021-06-01 00:00:00.00 Min. :2021-06-01 Jun:34400 Fri:4560 Min. :2024-05-21 00:00:04.00 Min. : 0.00 Min. : 0.00 Min. : 3.0 Min. :22.60 Min. :21.04 Min. : 358.0 Min. : 0.00 CO2_01:6164
1st Qu.:2021-06-06 17:23:50.61 1st Qu.:2021-06-06 17:23:45.00 1st Qu.:2021-06-06 Mon:4636 1st Qu.:2024-05-21 05:55:06.00 1st Qu.: 5.00 1st Qu.:10.00 1st Qu.: 5.0 1st Qu.:28.72 1st Qu.:40.82 1st Qu.: 415.0 1st Qu.: 95.57 CO2_02:6101
Median :2021-06-12 00:55:05.50 Median :2021-06-12 00:55:00.00 Median :2021-06-12 Sat:4634 Median :2024-05-21 12:10:05.00 Median :12.00 Median :30.00 Median : 5.0 Median :30.00 Median :45.30 Median : 429.0 Median : 96.15 CO2_03:6218
Mean :2021-06-12 03:58:31.88 Mean :2021-06-12 03:58:26.08 Mean :2021-06-11 Sun:4631 Mean :2024-05-21 12:00:22.73 Mean :11.55 Mean :27.52 Mean : 5.3 Mean :29.96 Mean :44.57 Mean : 464.9 Mean : 94.45 CO2_04:4216
3rd Qu.:2021-06-17 11:05:06.52 3rd Qu.:2021-06-17 11:05:00.00 3rd Qu.:2021-06-17 Thu:4622 3rd Qu.:2024-05-21 18:00:06.00 3rd Qu.:18.00 3rd Qu.:45.00 3rd Qu.: 6.0 3rd Qu.:31.29 3rd Qu.:49.35 3rd Qu.: 448.0 3rd Qu.: 98.07 CO2_05:5582
Max. :2021-06-23 23:55:05.89 Max. :2021-06-23 23:55:00.00 Max. :2021-06-23 Tue:5624 Max. :2024-05-21 23:55:08.00 Max. :23.00 Max. :55.00 Max. :38.0 Max. :44.46 Max. :64.18 Max. :1989.0 Max. :100.00 CO2_06:6119
Wed:5693

2.6 Comprobación serie temporal

Dado que el estudio que vamos a realizar se basa en una serie temporal, vamos a comprobar que no tenemos ningún gap en los datos. En primer lugar, vamos a comprobar que el rango de fechas en la toma de datos de los sensores es la misma para todos.

# Agrupamos los datos por 'sensor_id' y luego resumimos para obtener las fechas mínima y máxima
tabla_fechas <- df_LAlbea %>%
  group_by(sensor_id) %>%
  summarise(
    fecha_minima = min(date_time),
    fecha_maxima = max(date_time)
  )

# Mostramos La tabla resumen
print(tabla_fechas)
## # A tibble: 6 × 3
##   sensor_id fecha_minima        fecha_maxima       
##   <fct>     <dttm>              <dttm>             
## 1 CO2_01    2021-06-01 00:00:00 2021-06-23 23:55:00
## 2 CO2_02    2021-06-01 00:00:00 2021-06-23 23:55:00
## 3 CO2_03    2021-06-01 00:00:00 2021-06-23 23:55:00
## 4 CO2_04    2021-06-01 00:00:00 2021-06-22 23:30:00
## 5 CO2_05    2021-06-01 00:05:00 2021-06-22 05:05:00
## 6 CO2_06    2021-06-01 00:00:00 2021-06-23 23:55:00
# Calculamos el número de registros por sensor por día
conteo_sensores <- df_LAlbea %>%
  group_by(sensor_id, date) %>%
  summarise(conteo = n())

# Crea un gráfico para cada sensor
ggplot(conteo_sensores, aes(x=date, y=conteo)) +
  geom_line() +
  facet_wrap(~sensor_id, scales = "free_y") +
  theme_minimal() +
  labs(x="Fecha", y="Número de Registros", title="Distribución del Número de Registros por Sensor por Día") +
  theme(axis.text.x = element_text(angle = 90, hjust = 1))

Podemos comprobar como el sensor CO2_04 no registra datos a partir del día 15-06-2021 y como el sensor CO2_05 carece de datos para los días 22 y 23-06-2021. Dado que el resto de sensores mantiene una distribución correcta de los datos, se decide prescindir de los datos de estos dos sensores para utilizarlos en el proyecto, ya que el alto número de registros faltantes puede desvirtuar el resultado final.

Ahora generaremos la secuencia completa cada 5 minutos para la frecuencia determinada y comporbaremos si tenemos algún gap para cada uno de los sensores.

# Eliminamos los registros de los sensores 'CO2_04' y 'CO2_05'
df_LAlbea_filtrado <- df_LAlbea %>%
  filter(sensor_id != 'CO2_04' & sensor_id != 'CO2_05')

# Generamos una secuencia completa de fechas y horas según el rango definido
fecha_min <- min(df_LAlbea_filtrado$date_time)
fecha_max <- max(df_LAlbea_filtrado$date_time)
secuencia_completa <- seq(fecha_min, fecha_max, by = "5 mins")

# Generamos un dataset con la secuencia completa para cada uno de los sensores
df_sec_completo <- expand.grid(sensor_id = unique(df_LAlbea_filtrado$sensor_id), date_time = secuencia_completa)

df_LAlbea_completo <- left_join(df_sec_completo, df_LAlbea_filtrado, by = c("sensor_id", "date_time"))

# Identificamos los registros faltantes
registros_faltantes <- df_LAlbea_completo %>% filter(is.na(df_LAlbea_completo$co2))

# Contamos los registros faltantes por sensor_id
conteo_faltantes <- registros_faltantes %>% group_by(sensor_id) %>% summarise(registros_faltantes = n())

# Mostramos los resultados
print(conteo_faltantes)
## # A tibble: 4 × 2
##   sensor_id registros_faltantes
##   <fct>                   <int>
## 1 CO2_01                    460
## 2 CO2_02                    523
## 3 CO2_03                    407
## 4 CO2_06                    505

Dado que los valores que nos interesan calcular son los de temperatura, humedad, co2 y bateria, eliminamos el resto de variables innecesarias para este proceso de rellenar los valores faltantes.

# Eliminamos las columnas que no participan en este proceso
df_LAlbea_completo <- subset(df_LAlbea_completo, select = -c(published_at, month, w_day, time, hour, mint, sec))

# Rellenamos los valores faltantes del campo date apoyandonos en date_time
df_LAlbea_completo$date[is.na(df_LAlbea_completo$date)] <- as.Date(df_LAlbea_completo$date_time[is.na(df_LAlbea_completo$date)], tz= "UTC")

# Calculamos las medias por sensor_id y fecha
medias <- df_LAlbea_completo %>%
  group_by(sensor_id, date) %>%
  summarise(
    temp_media = mean(temp, na.rm = TRUE),
    hum_media = mean(hum, na.rm = TRUE),
    co2_media = mean(co2, na.rm = TRUE),
    bat_media = mean(bat, na.rm = TRUE)
  )

# Ajustamos el formato de los datos obtenidos de las medias a nuestro dataset
medias$temp_media <- round(medias$temp_media, digits = 2)
medias$hum_media <- round(medias$hum_media, digits = 2)
medias$co2_media <- as.integer(medias$co2_media)
medias$bat_media <- round(medias$bat_media, digits = 2)

# Unimos las medias al dataset original
df_LAlbea_completo <- df_LAlbea_completo %>%
  left_join(medias, by = c("sensor_id", "date"))

# Reemplazamos los valores NA con las medias correspondientes
df_LAlbea_completo$temp[is.na(df_LAlbea_completo$temp)] <- df_LAlbea_completo$temp_media[is.na(df_LAlbea_completo$temp)]
df_LAlbea_completo$hum[is.na(df_LAlbea_completo$hum)] <- df_LAlbea_completo$hum_media[is.na(df_LAlbea_completo$hum)]
df_LAlbea_completo$co2[is.na(df_LAlbea_completo$co2)] <- df_LAlbea_completo$co2_media[is.na(df_LAlbea_completo$co2)]
df_LAlbea_completo$bat[is.na(df_LAlbea_completo$bat)] <- df_LAlbea_completo$bat_media[is.na(df_LAlbea_completo$bat)]

# Eliminamos las columnas de medias adicionales y fecha empleadas en el proceso
df_LAlbea_completo <- subset(df_LAlbea_completo, select = -c(date, temp_media, hum_media, co2_media, bat_media))

# Mostramos los resultados
df_LAlbea[sample(nrow(df_LAlbea_completo), 10), ]

3. CEIP_LAlbea_ValldAbav2 - Classroom

A continuación, cargamos los datos de las caracteristicas de las aulas donde se instalaron los sensores.

# Realizamos la carga del csv con las caracteristicas de las aulas.
df_classroom_LAlbea <-read.csv("Classroom_ValldAbav2.csv")

# Mostramos una muestra de 10 registros del dataset.
df_classroom_LAlbea[sample(nrow(df_classroom_LAlbea)), ]

3.1 Transformación de los datos

Examinamos el tipo de dato de cada una de las variables para comprobar si se ajustan al tipo de datos de cada una de ellas.

Form.Basic = c("striped", "hover", "condensed", "responsive")
kbl(cbind(sapply(df_classroom_LAlbea, class)),
    caption="Tipo de datos de las variable") %>%
  kable_styling(bootstrap_options = Form.Basic) %>%
  scroll_box(width = "100%", 
             height = "400px")
Tipo de datos de las variable
sensor_id character
grade character
number_students integer
metres numeric
windows integer
doors integer
orientation character

Eliminamos los espacios en blanco de las variables de tipo character que no son fechas.

# Eliminamos los espacios en blanco de los valores de las variables character.
df_classroom_LAlbea$sensor_id <- trimws(df_classroom_LAlbea$sensor_id)
df_classroom_LAlbea$grade <- trimws(df_classroom_LAlbea$grade)
df_classroom_LAlbea$orientation <- trimws(df_classroom_LAlbea$orientation)

A pesar de que tenemos variables numericas en este dataset, todas las variables que este recoge son categóricas, puesto que nos estan indicando una característica del aula donde ha sido instalado el sensor de la toma de datos, por lo que modificamos el tipo de datos de estas para corregirlo.

df_classroom_LAlbea <- df_classroom_LAlbea %>%
  dplyr::mutate(sensor_id = as.factor(sensor_id),
                grade = as.factor(grade),
                number_students = as.factor(number_students),
                metres = as.factor(metres),
                windows = as.factor(windows),
                doors = as.factor(doors),
                orientation = as.factor(orientation))

Ahora examinaremos las variables del dataset mostrando los datos resumen de cada una de ellas.

options(knitr.kable.NA = '')
kable(summary(df_classroom_LAlbea),
      digits=2, 
      align='l', 
      caption="Datos resumen Classroom ") %>%
  kable_styling(bootstrap_options = Form.Basic) %>%
    scroll_box(width = "100%", height = "470px")
Datos resumen Classroom
sensor_id grade number_students metres windows doors orientation
S_01:1 1st:1 14:1 35.8 :1 2:3 1:6 Playground:6
S_02:1 2nd:1 16:2 40.9 :1 3:1
S_03:1 3rd:1 20:1 44.56:1 4:2
S_04:1 4th:1 24:1 65.3 :1
S_05:1 5th:1 25:1 67 :1
S_06:1 6th:1 69.17:1

4. Generación de dataset final

Como vamos a unir ambos datasets por la variable sensor_id, comprobamos que el valor de ambos sea igual.

# Revisamos la variable
table(df_LAlbea_completo$sensor_id)
## 
## CO2_01 CO2_02 CO2_03 CO2_04 CO2_05 CO2_06 
##   6624   6624   6624      0      0   6624
table(df_classroom_LAlbea$sensor_id)
## 
## S_01 S_02 S_03 S_04 S_05 S_06 
##    1    1    1    1    1    1

Dado que el valor de los sensores no es el mismo en ambos datasets, modificamos el valor del campo sensor_id con la nomencaltura del contenido del dataset df_LAlbea.

# Reemplazamos los valores en el dataset df_classroom_LAlbea
df_classroom_LAlbea$sensor_id <- gsub("S_01", "CO2_01", df_classroom_LAlbea$sensor_id)
df_classroom_LAlbea$sensor_id <- gsub("S_02", "CO2_02", df_classroom_LAlbea$sensor_id)
df_classroom_LAlbea$sensor_id <- gsub("S_03", "CO2_03", df_classroom_LAlbea$sensor_id)
df_classroom_LAlbea$sensor_id <- gsub("S_04", "CO2_04", df_classroom_LAlbea$sensor_id)
df_classroom_LAlbea$sensor_id <- gsub("S_05", "CO2_05", df_classroom_LAlbea$sensor_id)
df_classroom_LAlbea$sensor_id <- gsub("S_06", "CO2_06", df_classroom_LAlbea$sensor_id)

En este punto realizamos la unión del dataset que contiene los valores obtenidos de los sensores con el dataset con las caracteristicas de las aulas donde se utilizó cada uno de los sensores, empleando como clave común la variable sensor_id.

# Realizamos la unión de ambos datasets utilizando la variable sensor_id como clave común.
dfu_LAlbea <- merge(df_LAlbea_completo, df_classroom_LAlbea, by = "sensor_id")

# Mostramos el detalle del dataframe generado.
dfu_LAlbea[sample(nrow(dfu_LAlbea), 10), ]

Por último, exportamos el dataset final generado a un fichero csv.

# Exportamos nuestro dataset unificado a CSV.
write.csv(dfu_LAlbea, file = "CEIP_LAlbea_co2.csv", row.names = FALSE)