Le Fonti dei Dati
I dati metereologici sono una tipologia di dati difficilemnte repereibili velocemente su internet. Tra le fonti piu autorevoli presenti citiamo:
Sul sito dell’ aereonautica militare, è possibile ottenere dati di diverso tipo, tuttavia non sono ad accesso libero, ma a **pagamento*.
Gli open data della pubblica amministrazione, contengono numerose serie storiche per diversi argomenti oltre ai dati metereologici. Tuttavia i dati riportati non sono omogenei e sono spesso inconsistenti in termini di formato tra i vari comuni/regioni che li riportano. Inoltre non è possibile ottenere serie omogenee per tutte le citta/province italiane, ma solo per alcune rendendo l’analisi potenziale comunque parziale in partenza.
Archivio Storico del Meteo.it
L’unico sito italiano che presenta delle serie storiche complete e con numerose variabili riportate è il ilmeteo.it, sezione archivio storico. Come riportato nel sito uffiale le misurazioni sono prese dalle stazioni di ufficiali, i cui dati sono riportati anche negli altri siti. Una differenza che abbiamo trovato è con i dati di Copernicus, ad esempio, è la temperatura media: mentre nei dati di Copernicus spesso è calcolata come \(\frac{min + max}{2}\) mentre nel meteo.it non è sempre cosi.
Costruzione dello scaper
In primo luogo notiamo che l’url per accedere all’archivio presenta una certa regolarità:
- ilmeteo.it/portale/archivio-meteo/, {citta}/{anno}/{mese}.
Possiamo quindi procedere ad individuare un framework consistente e robusto, per farlo procederemo con uno schema top-down per capire la struttura del sito, per poi implementare un framework bottom-up.
Le informazioni disponibili
Abbiamo notato che esistono due livelli di informazione disponibili:
Il primo livello è accessibile tramite un url del tipo {citta}/{anno}/{mese}, tuttavia questo livello, facile da raggiungere e importare contiene solo una parte delle informazioni disponibili.
Un secondo livello del tipo {citta}/{anno}/{mese}/{giorno}, contine molte più informazioni tra cui:
- temperatura (minima, massima e media)
- punto di rugiada (dew_point)
- precipitazioni
- umidità (minima, massima e media)
- visibilità
- velocita del vento (massima e media) e raffiche
- pressione (media e a livello del mare)
- fenomeni atmosferici del giorno (etichette del meteo.it)
- condizioni meteo del giorno (etichette del meteo.it)
# url di esempio
url = "https://www.ilmeteo.it/portale/archivio-meteo/Genova/2020/Febbraio"
# importazione html
html = xml2::read_html(url)
# le informazioni storiche si trovano nella tabella 4
table = xml2::xml_find_all(html, "//table")[4]
# ricerco tutti i tr contenuti nella 4 tabella (i tr rappresentano la singola riga della tabella)
all_tr = xml2::xml_find_all(table, "./tr")Livello 0: tr_to_row_meteoIT
Funzione tr_to_row, è la funzione di base che prende in input un singolo tr dalla lista all_tr e restituisce una riga di una tabella ordinata. Abbiamo optato per questa divisione poiche in questa maniera strutturiamo a priori il formato della tabella che vogliamo avere in output, siamo in grado di controllare ed evitare errori qualora un elemento non venga trovato e in generale rendiamo la procedura facilemente modificabile qualora avvenga una modifica nei codici sorgenti.
# FUNCTION: NULL_df_meteoIT
# output: tabella base per importazione
NULL_df_meteoIT <- function(){
# base dataset for importation
dplyr::tibble(
date = NA_character_, # data in formato %YYYY/%MM/%DD
city = NA_character_, # nome citta
n.day = NA_character_, # numero giorno del mese
month = NA_character_, # numero del mese
year = NA_character_, # anno
mean_temp = NA_integer_, # temperatura media
min_temp = NA_integer_, # temperatura minima
max_temp = NA_integer_, # temperatura massima
dew_point = NA_character_, # punto di rugiada
precip = NA_character_, # precipitazioni
mean_humid = NA_character_, # umidita media
min_humid = NA_character_, # umidita minima
max_humid = NA_character_, # umidita massima
visibility = NA_character_, # visibilita
mean_wind = NA_character_, # velocita vento media
max_wind = NA_character_, # velocita vento massima
burst = NA_character_, # raffiche di vento
mean_pressure = NA_character_, # pressione media
sealevel_pressure = NA_character_, # pressione media sul livello del mare
rain = NA_character_, # pioggia
phenomena = NA_character_, # fenomeni metereologici
weather_condition = NA_character_, # condizioni meteo
url_info = NA_character_ # url infomazioni giornaliere
)
}
# FUNCTION: tr_to_row_meteoIT
# tr: xml node della riga della tabella dell'archivio storico meteo.it
tr_to_row_meteoIT <- function(tr = NULL){
# initialize the data to have always an output
null_df = NULL_df_meteoIT()
all_td = xml2::xml_find_all(tr, "./td")
n.day = xml2::xml_text(all_td[1])
mean_temp = xml2::xml_text(all_td[2])
min_temp = xml2::xml_text(all_td[3])
max_temp = xml2::xml_text(all_td[4])
precip = xml2::xml_text(all_td[5])
max_vento = xml2::xml_text(all_td[7])
url_info = xml2::xml_attr(xml2::xml_find_all(all_td[10], "./a"), "href")
null_df$n.day = ifelse(is.null(n.day), NA_character_, n.day)
null_df$mean_temp = ifelse(is.null(mean_temp), NA_character_, mean_temp)
null_df$min_temp = ifelse(is.null(min_temp), NA_character_, min_temp)
null_df$max_temp = ifelse(is.null(max_temp), NA_character_, max_temp)
null_df$precip = ifelse(is.null(precip), NA_character_, precip)
null_df$max_wind = ifelse(is.null(max_vento), NA_character_, max_vento)
null_df$url_info = ifelse(is.null(url_info), NA_character_, paste0("https://www.ilmeteo.it/portale/", url_info))
return(null_df)
}
# FUNZIONI Utils
# rende la prima lettera di una parola maiuscola (per controllo imput)
FirstToupper <- function(word){
word = str_split(word, "")
word[[1]][1] = toupper(word[[1]][1])
word = paste0(word[[1]], collapse = "")
return(word)
}
# Function: xml_find_attr
# funzione modificata dal pacchetto xml2 che permette di estrarre direttamente gli elementi aventi un determinato
# valore in un attributo (class, id, etc), è possibile selezionare un solo attributo ma piu nomi di classi possibili
xml_find_attr <- function(x = NULL, xpath = NULL, attr.names = "", attr.type = "class", ns = xml2::xml_ns(x), exact_match = TRUE){
all_elements = xml2::xml_find_all(x = x, xpath = xpath, ns = ns)
if(exact_match){
class_index = which(xml2::xml_attr(all_elements, attr.type) %in% attr.names)
} else {
class_index = which(stringr::str_detect(xml2::xml_attr(all_elements, attr.type), attr.names[1]))
}
all_elements[class_index]
}Livello 1: city_month_year_MeteoIT
La funzione city_month_year_MeteoIT, è una funzione che prende in input il nome di una citta (city), il mese da importare (month) e l’anno (imp.year) e restituisce in output una singola tabella della forma del null_df.
city_month_year_MeteoIT <- function(city = NULL, month = NULL, year = NULL, verbose = TRUE){
# URL di base
base_url = "https://www.ilmeteo.it/portale/archivio-meteo"
# 1) Argomento "anno"
anni_validi = seq(1973, lubridate::year(Sys.Date()), 1) # anni ammessi
imp.year = match.arg(year, anni_validi) # anno di importazione
# 2) Argomento "citta"
# la citta deve essere scritta sempre con la lettera maiuscola
# le citta con un nome composto vengono separate dal "+": es Ascoli+Piceno
city = stringr::str_split(city, " ")[[1]] # separo le parole (se sono piu di una)
city = purrr::map_chr(city, FirstToupper) # trasformo tutte le lettere in maiuscole
city_name = paste0(city, collapse = " ") # salvo il nome della citta
city = paste0(city, collapse = "+") # ricreo la stringa
# 3) Argomento "mese"
mesi_validi = c(Gennaio = 1, Febbraio = 2, Marzo = 3, Aprile = 4,
Maggio = 5, Giugno = 6, Luglio = 7, Agosto = 8,
Settembre = 9, Ottobre = 10, Novembre = 11, Dicembre = 12)
# indice per il numero del mese
month_index = match.arg(month, mesi_validi)
# nome del mese per comporre url
month.name = names(month_index)
# 4) creazione url per l'importazione
url = paste0(base_url, "/", city, "/", imp.year, "/", month.name )
# 5) lettura html
html = xml2::read_html(url)
# 6) tabella con informazioni di interesse: n°4
table = xml2::xml_find_all(html, "//table")[4]
# 7) separo tutte le righe per avere un importazione controllata (ma piu lunga)
all_tr = xml2::xml_find_all(table, "./tr")
# 8) Mapping "tr_to_row_meteoIT"
df_importazione = purrr::map_df(all_tr[-1], tr_to_row_meteoIT)
# 9) Estrazione stazione di riferimento
station = xml2::xml_text(xml_find_attr(html, xpath = ".//div", attr.type = "class", attr.names = "smalllinks")[1])
station = stringr::str_remove_all(station, "Dati registrati dalla stazione meteo di ")
station = tm::removePunctuation(station)
# aggiunta informazioni sulla citta, mese e anno
df_importazione = dplyr::mutate(df_importazione,
city = city_name,
month = month_index,
year = imp.year,
station = station)
# creazione della data
df_importazione$date = as.Date(paste0(imp.year,"/", month_index, "/", df_importazione$n.day))
if(verbose) message("Importazione: ", city_name, " (Anno: ", imp.year, ", Mese: ", month, ")")
return(df_importazione)
}Livello Intermedio: city_year_MeteoIT
Funzione city_year_MeteoIT, è la funzione analoga alla precedente, con l’unica differenza che permette di importare tutti i mesi disponibili, la funzione è stata creata con lo scopo di evitare di iterare tutti i mesi dell’anno qualora inserissimo l’anno corrente e per importare solo i mesi dall’inizio dell’anni qualora l’anno (year) corrisponda a quello corrente.
# FUNCTION: Meteo_city_year: map "city_month_year_MeteoIT" su un anno
city_year_MeteoIT <- function(city = NULL, year = NULL, verbose = TRUE){
# match argomento anni
anni_validi = seq(1973, lubridate::year(Sys.Date()), 1)
imp.year = as.character(match.arg(year, anni_validi))
if(imp.year == lubridate::year(Sys.Date())){
max_month = lubridate::month(Sys.Date())
months = as.character(1:max_month)
} else {
months = as.character(1:12)
}
safe_city_month_year = purrr::safely(city_month_year_MeteoIT)
purrr::map_df(months, ~safe_city_month_year(city = city,
month = .x,
year = year,
verbose = verbose)$result)
}Aggiunta delle Informazioni giornaliere
Molte delle informazioni presenti nella pagina principale dell’archivio sono incoplete o non presenti. Per poter ottenere tutte le informazioni si devono richiedere gli url per la pagina corrispondente al singolo giorno. La procedura è particolarmente lunga in termini di tempo (per 10 anni di dati completi circa 1.30 h), ma è l’unica procedura che permette di ottenere senza particolare sforzo delle serie storiche subito analizzabili senza dover effettuare operazioni manuali. Inoltre i dati presenti comprono una vasta gamma di informazioni permettendo diverse applicazioni.
# Aggiunta informazioni giornaliere
add_daily_MeteoIT <- function(df = NULL, verbose = TRUE ){
# inizializzo output
df_output = NULL_df_meteoIT()
# funzione per importare le informazioni di un singolo url di una singola riga
DailyInfo <- function(df_row){
url = df_row$url_info[1]
html = xml2::read_html(url)
table = xml2::xml_find_all(html, ".//table")[4]
table = rvest::html_table(table)[[1]]
df_row$mean_temp = table[1,][[2]]
df_row$min_temp = table[2,][[2]]
df_row$max_temp = table[3,][[2]]
df_row$dew_point = table[4,][[2]]
df_row$mean_humid = table[5,][[2]]
df_row$min_humid = table[6,][[2]]
df_row$max_humid = table[7,][[2]]
df_row$visibility = table[8,][[2]]
df_row$mean_wind = table[9,][[2]]
df_row$max_wind = table[10,][[2]]
df_row$burst = table[11,][[2]]
df_row$mean_pressure = table[12,][[2]]
df_row$sealevel_pressure = table[13,][[2]]
df_row$rain = table[14,][[2]]
df_row$phenomena = table[15,][[2]]
df_row$weather_condition = table[16,][[2]]
return(df_row)
}
n_iter <- nrow(df) # Number of iterations of the loop
if(verbose) message("Tempo Stimato: ", round(n_iter), " Secondi")
start_time = Sys.time()
if(verbose){
# Initializes the progress bar
pb <- txtProgressBar(min = 0, # Minimum value of the progress bar
max = n_iter, # Maximum value of the progress bar
style = 3, # Progress bar style (also available style = 1 and style = 2)
width = 50, # Progress bar width. Defaults to getOption("width")
char = "=") # Character used to create the bar
}
all_df_info = list()
safe_import = purrr::safely(DailyInfo)
for(i in 1:n_iter) {
result = safe_import(df[i,])$result
# controllo per eventuali pagine mancanti
if(is.null(result)){
all_df_info[[i]] = df[i,]
} else {
all_df_info[[i]] = result
}
if(verbose){setTxtProgressBar(pb, i)} # settaggio progress bar
}
if(verbose){close(pb)} # chiusura connesione
end_time = Sys.time()
if(verbose) message("Tempo Impiegato: ",
round(end_time-start_time, 3),
" Minuti...")
# dataset finale
df_output = dplyr::bind_rows(all_df_info)
return(df_output)
}Livello Finale: MeteoIT
Funzione MeteoIT, prende in input una citta, un anno di inizio ed uno di fine e importa tutti i dati creando una tabella unica.
MeteoIT <- function(city = NULL, start = "2021", end = "2022", verbose = TRUE, add_daily_info = FALSE, object.name = NULL){
start = as.numeric(start)
end = as.numeric(end)
# creo la sequenza di anni
seq_years = seq(start, end, 1)
seq_years = as.character(seq_years)
safe_city_year = purrr::safely(city_year_MeteoIT)
df_importazione = purrr::map_df(seq_years, ~safe_city_year(city, year = .x, verbose = verbose)$result)
if(verbose){message("Importazione Parziale Completata!")}
# se il salvataggio è attivo salviamo dopo la prima importazione
if(!is.null(object.name)){
data = Sys.Date()
data = stringr::str_replace_all(data, "-", "_")
filename = paste0(object.name, "_", data, "_", ".RData")
assign(object.name, df_importazione)
save(list = object.name, file = filename)
if(verbose){message("Importazione Parziale dell'oggetto: <", object.name, "> salvata con il nome di: ", filename)}
}
if(add_daily_info){
if(verbose){message("Importazione con informazioni giornaliere in corso...")}
# aggiunta informazioni complete
df_completo = add_daily_MeteoIT(df_importazione, verbose = verbose)
# df_completo = CleanMeteoIT(df_completo)
# se il salvataggio è attivo salviamo tutto dopo la seconda importazione
# se il salvataggio è attivo salviamo dopo la prima importazione
if(!is.null(object.name)){
data = Sys.Date()
data = stringr::str_replace_all(data, "-", "_")
filename = paste0(object.name, "_", data, "_", ".RData")
assign(object.name, df_completo)
save(list = object.name, file = filename)
if(verbose){message("Importazione completa dell'oggetto: <", object.name, "> salvata con il nome di: ", filename)}
}
return(df_completo)
}
return(df_importazione)
}Esempio di Utilizzo
Importazione dei dati senza informazioni giornaliere:
df = MeteoIT(city = "Bologna", start = "2022", end = "2022", verbose = FALSE)
df[1:10, 1:11] %>%
knitr::kable(caption = "Esempio parziale di Output (senza informazioni giornaliere)") %>%
kableExtra::kable_classic() %>%
kable_styling(latex_options = "hold_position")| date | city | n.day | month | year | mean_temp | min_temp | max_temp | dew_point | precip | mean_humid |
|---|---|---|---|---|---|---|---|---|---|---|
| 2022-01-01 | Bologna | 1 | 1 | 2022 | 2 °C | -1 °C | 4 °C | NA |
|
NA |
| 2022-01-02 | Bologna | 2 | 1 | 2022 | 5 °C | -2 °C | 11 °C | NA |
|
NA |
| 2022-01-03 | Bologna | 3 | 1 | 2022 | 4 °C | 3 °C | 5 °C | NA |
|
NA |
| 2022-01-04 | Bologna | 4 | 1 | 2022 | 5 °C | 3 °C | 6 °C | NA |
|
NA |
| 2022-01-05 | Bologna | 5 | 1 | 2022 | 7 °C | 4 °C | 16 °C | NA | n/d | NA |
| 2022-01-06 | Bologna | 6 | 1 | 2022 | 5 °C | 2 °C | 7 °C | NA | n/d | NA |
| 2022-01-07 | Bologna | 7 | 1 | 2022 | 3 °C | 0 °C | 7 °C | NA |
|
NA |
| 2022-01-08 | Bologna | 8 | 1 | 2022 | 2 °C | -1 °C | 6 °C | NA |
|
NA |
| 2022-01-09 | Bologna | 9 | 1 | 2022 | 0 °C | -3 °C | 3 °C | NA | n/d | NA |
| 2022-01-10 | Bologna | 10 | 1 | 2022 | 3 °C | 1 °C | 5 °C | NA | n/d | NA |
Una volta importata il primo strato si puo procedere ad aggiungere i dati giornalieri, utilizzando la funzione meteoIT è possibile farlo automaticamente impostando il parametro add_daily_info = TRUE altrimenti è possibile importare i dati manualmente dando in input il datset importato al punto precedente nel seguente modo:
add_daily_MeteoIT(df[1:10,], verbose = FALSE)[,12:24] %>%
select(-url_info, -sealevel_pressure, -burst, -station) %>%
knitr::kable(caption = "Esempio parziale di Output (con informazioni giornaliere)") %>%
kableExtra::kable_classic() %>%
kable_styling(latex_options = "hold_position")| min_humid | max_humid | visibility | mean_wind | max_wind | mean_pressure | rain | phenomena | weather_condition |
|---|---|---|---|---|---|---|---|---|
| 100 % | 100 % | 1 km | 4 km/h | 9 km/h | 1026 mb |
|
Nebbia | nebbia al mattino |
| 76 % | 100 % | 11 km | 5 km/h | 11 km/h | 1024 mb |
|
Nebbia | nebbia al mattino |
| 100 % | 100 % |
|
5 km/h | 9 km/h | 1020 mb |
|
Nebbia | nebbia |
| 100 % | 100 % |
|
5 km/h | 11 km/h | 1011 mb |
|
Nebbia | nebbia |
| 63 % | 100 % | 5 km | 12 km/h | 24 km/h | 1001 mb | n/d | Pioggia - Nebbia | pioggia debole |
| 81 % | 100 % | 17 km | 11 km/h | 24 km/h | 1013 mb | n/d | Pioggia | pioggia e schiarite |
| 61 % | 100 % | 19 km | 6 km/h | 11 km/h | 1019 mb |
|
Nessuno | sereno |
| 65 % | 100 % | 17 km | 6 km/h | 11 km/h | 1015 mb |
|
Nessuno | sereno |
| 87 % | 100 % | 10 km | 8 km/h | 15 km/h | 1006 mb | n/d | Pioggia - Neve | poco nuvoloso |
| 70 % | 93 % | 19 km | 9 km/h | 15 km/h | 1011 mb | n/d | Pioggia | sereno |