Tutorial per lo scaricamento dei dati sulla qualità dell'aria (CAMS) da Copernicus¶
Questo tutorial permette di scaricare i dati sulla qualità dell'aria (CAMS) dal sito di Copernicus
import cdsapi
import geopandas as gpd
import xarray as xr
import os
import yaml
import hashlib
import plotly
from shapely import geometry
from datetime import datetime, timedelta
from math import dist
from scipy.stats import zscore
import plotly.graph_objects as go
plotly.offline.init_notebook_mode()
def get_dates(start_month, n_month=1):
start_date = datetime.strptime(start_month, '%Y-%m')
end_date = start_date + timedelta(days=n_month*31)
return start_date.strftime('%Y-%m-01'), end_date.strftime('%Y-%m-01')
def sanitize_string(string_in):
delete_chars = '\\/:*?"<>|,'
for char in delete_chars:
string_in = string_in.replace(char,'')
ugly_chars = " '-"
for char in ugly_chars:
string_in = string_in.replace(char,'_')
return string_in
Per ottenere le credenziali per utilizzare cdsapi, seguire le istruzioni a questo link: https://ads.atmosphere.copernicus.eu/api-how-to
Per selezionare tutti i parametri di interesse (produce un json in fondo): https://ads.atmosphere.copernicus.eu/cdsapp#!/dataset/cams-europe-air-quality-forecasts?tab=form
Noi abbiamo analizzato i dati della Sardegna partendo dai confini amministrativi forniti dall'ISTAT: https://www.istat.it/storage/cartografia/confini_amministrativi/non_generalizzati/2024/Limiti01012024.zip
Abbiamo anche selezionato il mese di gennaio 2024 con tutte le ore del giorno
regione_sel = 'Sardegna'
month = '2024-06'
hours = [f'{i:02}:00' for i in range(0,24)]
start_date, end_date = get_dates(month, n_month = 1)
month_out = month.replace("-","")
regione_sel_out = sanitize_string(regione_sel.lower())
cams_data_fname = f"{os.path.join(os.path.expanduser('~'), 'ILAB_DATA', 'COPERNICUS')}/NC/data_{regione_sel_out}_{month_out}.nc"
comuni = gpd.read_parquet(os.path.join(os.path.expanduser('~'), 'ILAB_DATA','ISTAT', 'DATA', 'comuni_pop.parquet'))
regione = comuni[comuni['cod_reg']==20].copy()
regione_bb = list(regione['geometry'].to_crs(4326).total_bounds)
bb = [regione_bb[3],regione_bb[0],regione_bb[1],regione_bb[2]]
with open ('../../.adsapirc', 'r') as f:
credentials= yaml.safe_load(f)
c = cdsapi.Client(url=credentials['url'], key=credentials['key'])
Scarico i dati CAMS (raster)
c.retrieve(
'cams-europe-air-quality-forecasts',
{
'model': 'ensemble',
'date': f'{start_date}/{end_date}',
'format': 'netcdf',
'variable': [
'ammonia', 'carbon_monoxide', 'nitrogen_dioxide',
'ozone', 'particulate_matter_10um', 'particulate_matter_2.5um',
'pm10_wildfires', 'pm2.5_total_organic_matter', 'residential_elementary_carbon',
'sulphur_dioxide', 'total_elementary_carbon',
],
'level': '0',
'type': 'analysis',
'time': hours,
'leadtime_hour': '0',
'area': bb
},cams_data_fname)
Apro il raster e lo trasformo in un dataframe
ds = xr.open_dataset(cams_data_fname)
df = ds.to_dataframe().reset_index()
ds.close()
Rinomino le colonne delle variabili eliminando '_conc'
df.columns = [col.replace('_conc','') for col in df.columns]
df.head()
Seleziono le colonne che contengono le variabili, sulle quali verranno calcolati i valori medi
variables = list(df.columns)
variables = [element for element in variables if element not in ['longitude','latitude','level','time']]
aggs_dict = {element: 'mean' for element in variables}
Ottengo un valore di tempo assoluto
timestart = datetime.strptime(start_date, '%Y-%m-%d')
df['time_abs'] = timestart + df['time']
df['date'] = df['time_abs'].dt.date
df['time_abs'] = df['time_abs'].astype('string')
df['time'] = df['time'].astype('string')
df['date'] = df['date'].astype('string')
df.head()
Ora trasformiamo le celle del file raster in vettoriale¶
Per calcolare il passo della griglia (le dimensioni della cella), seleziono le prime due celle relative al primo intervallo temporale
lons = df[df['time']==f'0 days {hours[0]}:00'].sort_values(by=['latitude','longitude']).head(2)['longitude'].values
lats = df[df['time']==f'0 days {hours[0]}:00'].sort_values(by=['latitude','longitude']).head(2)['latitude'].values
distance = dist([lons[1]], [lons[0]])
Definisco una chiave univoca per ogni cella della griglia, data dal passo e dai valori di latitudine e longitudine
def get_id_cell(row, distance_in):
grid_distance = str(distance_in)[:3].replace('.','_')
id_cell = f'{grid_distance}__{row["latitude"]}_{row["longitude"]}'
result = hashlib.md5(id_cell.encode()).hexdigest()
return result
df['id_cell'] = df.apply(get_id_cell, distance_in=distance, axis='columns')
Seleziono un dataframe relativo al primo valore temporale immesso, che userò come base per le geometrie (non ha senso rifare le geometrie per ogni ora del giorno)
df_geom = df[df['time']==f'0 days {hours[0]}:00'].copy()
df_geom = df_geom[['longitude','latitude','id_cell']].copy()
Creo il campo geometria per ogni cella e lo trasformo in un geodataframe. Così otteniamo le geometrie di tutte le celle della Sardegna
def make_cell(row, distance_in):
xmin = row['longitude'] - (distance_in/2)
ymin = row['latitude'] - (distance_in/2)
xmax = row['longitude'] + (distance_in/2)
ymax = row['latitude'] + (distance_in/2)
geom = geometry.Polygon(((xmin,ymin), (xmin,ymax), (xmax,ymax), (xmax,ymin)))
return geom
df_geom['geometry'] = df_geom.apply(make_cell, distance_in=distance, axis='columns')
gdf = gpd.GeoDataFrame(df_geom, geometry=df_geom['geometry'])
gdf = gdf[['id_cell', 'geometry']].set_crs(4326)
Uniamo l'info sulla geometria ai dati CAMS precedentemente scaricati
gdf_out = gdf.merge(df, on='id_cell')
gdf_out.to_parquet(os.path.join(os.path.expanduser('~'), 'ILAB_DATA', 'COPERNICUS','DATA', f'cams_grid_{regione_sel_out}_{month_out}.parquet'))
gdf_out.head()
# # cella a Roma
# fig = go.Figure()
# for var in variables:
# temp = gdf_out[(gdf_out['id_cell']=='53d7e08a8fae6e9b00f5f516945b6a37')].copy()# & (gdf_out['date']=='2024-01-01')
# temp['zscore'] = temp[[var]].apply(zscore)
# fig.add_trace(go.Scatter(x=temp['time_abs'], y=temp['zscore'], name=var))
# fig.show()
Visualizziamo i dati¶
Vediamo l'andamento mensile di tutte le variabili scaricate per una specifica cella sul comune di Cagliari
fig = go.Figure()
for var in variables:
temp = gdf_out[(gdf_out['id_cell']=='54cfb985fad9555c04e4b4135a471259')].copy()# & (gdf_out['date']=='2024-01-01')
temp['zscore'] = temp[[var]].apply(zscore)
fig.add_trace(go.Scatter(x=temp['time_abs'], y=temp['zscore'], name=var))
fig.show()
Possiamo anche calcolare i valori medi comunali¶
Eseguo l'intersezione tra i poligoni dei comuni e le celle della griglia (facciamo una sola volta l'intersezione sulle geometrie e poi riaggiungiamo tutte le info dei comuni e di CAMS che sono ripetute per giorno e per ora)
intersection = gdf.overlay(regione[['geometry','pro_com_t','comune']].to_crs(4326), how="intersection")
intersection = intersection[['pro_com_t','id_cell']].copy()
m = intersection.merge(regione[['geometry','pro_com_t','comune']], on = 'pro_com_t')
m = m.merge(df, on='id_cell')
Eseguo il groupby per ogni comune per ottenere i valori mediati delle grandezze
g = m.groupby(['pro_com_t','comune'], as_index=False).agg(aggs_dict)
regione_stats = regione.merge(g)
regione_stats = gpd.GeoDataFrame(regione_stats, geometry=regione_stats['geometry'])
regione_stats.to_parquet(os.path.join(os.path.expanduser('~'), 'ILAB_DATA', 'DATA', f'cams_comuni_{regione_sel_out}_{month_out}.parquet'))
g.shape, regione_stats.shape
regione_stats[regione_stats['comune']=='Cagliari'].head()