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
= "https://www.ilmeteo.it/portale/archivio-meteo/Genova/2020/Febbraio"
url
# importazione html
= xml2::read_html(url)
html
# le informazioni storiche si trovano nella tabella 4
= xml2::xml_find_all(html, "//table")[4]
table
# ricerco tutti i tr contenuti nella 4 tabella (i tr rappresentano la singola riga della tabella)
= xml2::xml_find_all(table, "./tr") all_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
<- function(){
NULL_df_meteoIT
# base dataset for importation
::tibble(
dplyr
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
<- function(tr = NULL){
tr_to_row_meteoIT
# initialize the data to have always an output
= NULL_df_meteoIT()
null_df
= xml2::xml_find_all(tr, "./td")
all_td
= xml2::xml_text(all_td[1])
n.day = xml2::xml_text(all_td[2])
mean_temp = xml2::xml_text(all_td[3])
min_temp = xml2::xml_text(all_td[4])
max_temp = xml2::xml_text(all_td[5])
precip = xml2::xml_text(all_td[7])
max_vento = xml2::xml_attr(xml2::xml_find_all(all_td[10], "./a"), "href")
url_info
$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))
null_df
return(null_df)
}
# FUNZIONI Utils
# rende la prima lettera di una parola maiuscola (per controllo imput)
<- function(word){
FirstToupper
= str_split(word, "")
word
1]][1] = toupper(word[[1]][1])
word[[
= paste0(word[[1]], collapse = "")
word
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
<- function(x = NULL, xpath = NULL, attr.names = "", attr.type = "class", ns = xml2::xml_ns(x), exact_match = TRUE){
xml_find_attr
= xml2::xml_find_all(x = x, xpath = xpath, ns = ns)
all_elements
if(exact_match){
= which(xml2::xml_attr(all_elements, attr.type) %in% attr.names)
class_index
else {
}
= which(stringr::str_detect(xml2::xml_attr(all_elements, attr.type), attr.names[1]))
class_index
}
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.
<- function(city = NULL, month = NULL, year = NULL, verbose = TRUE){
city_month_year_MeteoIT
# URL di base
= "https://www.ilmeteo.it/portale/archivio-meteo"
base_url
# 1) Argomento "anno"
= seq(1973, lubridate::year(Sys.Date()), 1) # anni ammessi
anni_validi = match.arg(year, anni_validi) # anno di importazione
imp.year
# 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
= 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 = paste0(city, collapse = " ") # salvo il nome della citta
city_name = paste0(city, collapse = "+") # ricreo la stringa
city
# 3) Argomento "mese"
= c(Gennaio = 1, Febbraio = 2, Marzo = 3, Aprile = 4,
mesi_validi Maggio = 5, Giugno = 6, Luglio = 7, Agosto = 8,
Settembre = 9, Ottobre = 10, Novembre = 11, Dicembre = 12)
# indice per il numero del mese
= match.arg(month, mesi_validi)
month_index # nome del mese per comporre url
= names(month_index)
month.name
# 4) creazione url per l'importazione
= paste0(base_url, "/", city, "/", imp.year, "/", month.name )
url
# 5) lettura html
= xml2::read_html(url)
html
# 6) tabella con informazioni di interesse: n°4
= xml2::xml_find_all(html, "//table")[4]
table
# 7) separo tutte le righe per avere un importazione controllata (ma piu lunga)
= xml2::xml_find_all(table, "./tr")
all_tr
# 8) Mapping "tr_to_row_meteoIT"
= purrr::map_df(all_tr[-1], tr_to_row_meteoIT)
df_importazione
# 9) Estrazione stazione di riferimento
= 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)
station
# aggiunta informazioni sulla citta, mese e anno
= dplyr::mutate(df_importazione,
df_importazione city = city_name,
month = month_index,
year = imp.year,
station = station)
# creazione della data
$date = as.Date(paste0(imp.year,"/", month_index, "/", df_importazione$n.day))
df_importazione
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
<- function(city = NULL, year = NULL, verbose = TRUE){
city_year_MeteoIT
# match argomento anni
= seq(1973, lubridate::year(Sys.Date()), 1)
anni_validi
= as.character(match.arg(year, anni_validi))
imp.year
if(imp.year == lubridate::year(Sys.Date())){
= lubridate::month(Sys.Date())
max_month = as.character(1:max_month)
months
else {
}
= as.character(1:12)
months
}
= purrr::safely(city_month_year_MeteoIT)
safe_city_month_year
::map_df(months, ~safe_city_month_year(city = city,
purrrmonth = .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
<- function(df = NULL, verbose = TRUE ){
add_daily_MeteoIT
# inizializzo output
= NULL_df_meteoIT()
df_output
# funzione per importare le informazioni di un singolo url di una singola riga
<- function(df_row){
DailyInfo
= df_row$url_info[1]
url
= xml2::read_html(url)
html
= xml2::xml_find_all(html, ".//table")[4]
table
= rvest::html_table(table)[[1]]
table
$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]]
df_row
return(df_row)
}
<- nrow(df) # Number of iterations of the loop
n_iter
if(verbose) message("Tempo Stimato: ", round(n_iter), " Secondi")
= Sys.time()
start_time
if(verbose){
# Initializes the progress bar
<- txtProgressBar(min = 0, # Minimum value of the progress bar
pb 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
}
= list()
all_df_info
= purrr::safely(DailyInfo)
safe_import
for(i in 1:n_iter) {
= safe_import(df[i,])$result
result # controllo per eventuali pagine mancanti
if(is.null(result)){
= df[i,]
all_df_info[[i]] else {
} = result
all_df_info[[i]]
}
if(verbose){setTxtProgressBar(pb, i)} # settaggio progress bar
}
if(verbose){close(pb)} # chiusura connesione
= Sys.time()
end_time
if(verbose) message("Tempo Impiegato: ",
round(end_time-start_time, 3),
" Minuti...")
# dataset finale
= dplyr::bind_rows(all_df_info)
df_output
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.
<- function(city = NULL, start = "2021", end = "2022", verbose = TRUE, add_daily_info = FALSE, object.name = NULL){
MeteoIT
= as.numeric(start)
start = as.numeric(end)
end
# creo la sequenza di anni
= seq(start, end, 1)
seq_years = as.character(seq_years)
seq_years
= purrr::safely(city_year_MeteoIT)
safe_city_year
= purrr::map_df(seq_years, ~safe_city_year(city, year = .x, verbose = verbose)$result)
df_importazione
if(verbose){message("Importazione Parziale Completata!")}
# se il salvataggio è attivo salviamo dopo la prima importazione
if(!is.null(object.name)){
= Sys.Date()
data
= stringr::str_replace_all(data, "-", "_")
data
= paste0(object.name, "_", data, "_", ".RData")
filename
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
= add_daily_MeteoIT(df_importazione, verbose = verbose)
df_completo
# 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)){
= Sys.Date()
data
= stringr::str_replace_all(data, "-", "_")
data
= paste0(object.name, "_", data, "_", ".RData")
filename
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:
= MeteoIT(city = "Bologna", start = "2022", end = "2022", verbose = FALSE)
df
1:10, 1:11] %>%
df[::kable(caption = "Esempio parziale di Output (senza informazioni giornaliere)") %>%
knitr::kable_classic() %>%
kableExtrakable_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) %>%
::kable(caption = "Esempio parziale di Output (con informazioni giornaliere)") %>%
knitr::kable_classic() %>%
kableExtrakable_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 |