Code
# rode isso apenas se voce estiver no colab!
# git clone https://github.com/ftl-brazil-2026/ebook.git
# pip install -r requirements.txtDataArray e DatasetDataset. Como várias variáveis estão agrupadas..isel vs .selgroupby e resample: climatologia e agregações temporaischunksAté agora trabalhamos com vetores e rasters. Esses dados até agora eram normalmente bidimensionais ou tridimensionais, as vezes imagens espectrais contendo mais de uma banda. Contudo, quando falamos em Cubo de Dados boa parte dos dados de sensoriamento remoto e, principalmente, de reanálises climáticas (ERA5, CMIP, previsão numérica do tempo) tem várias dimensões ao mesmo tempo: tempo, latitude, longitude e às vezes nível vertical (altitude/pressão).
O xarray estende o numpy e o pandas para esse mundo N-dimensional, permitindo a agregação das coordenadas espaciais para um série temporal; pesquisa no eixo do tempo e coordenação entre multivariáveis.

Link Colab
dados[3, 12, 40] não diz nada sobre o que é o eixo 0, 1 ou 2.DataArray e DatasetDataArray: um array N-D + dims (nomes dos eixos) + coords (os valores de cada eixo: datas, latitudes…) + attrs (metadados livres: unidade, nome longo, etc).Dataset: um dicionário de DataArrays que compartilham as mesmas coordenadas — por exemplo, temperatura e precipitação vivendo no mesmo grid de tempo/lat/lon.Vamos criar dados sintéticos (não é ERA5 ainda) só para explorar a mecânica do xarray sem depender de rede.
rng = np.random.default_rng(42)
time = pd.date_range("2015-01-01", periods=24, freq="MS")
lat = np.linspace(-9.6, -9.2, 5)
lon = np.linspace(-40.7, -40.3, 5)
ciclo_sazonal = np.sin(2 * np.pi * (np.arange(24) / 12))[:, None, None]
temp_valores = 27 + 3 * ciclo_sazonal + rng.normal(0, 0.5, (24, 5, 5))
temp = xr.DataArray(
temp_valores,
dims=["time", "lat", "lon"],
coords={"time": time, "lat": lat, "lon": lon},
name="t2m",
attrs={"units": "degC", "long_name": "Temperatura do ar a 2m (sintético)"},
)
temp<xarray.DataArray 't2m' (time: 24, lat: 5, lon: 5)> Size: 5kB
array([[[27.15235854, 26.48000795, 27.3752256 , 27.47028236,
26.02448241],
[26.34891025, 27.0639202 , 26.8418787 , 26.99159942,
26.57347804],
[27.43969899, 27.38889597, 27.03301535, 27.5636206 ,
27.23375467],
[26.57035377, 27.18437539, 26.5205587 , 27.43922515,
26.97503704],
[26.90756882, 26.65953523, 27.61127067, 26.92273526,
26.78583609]],
[[28.32393322, 28.76615459, 28.68272203, 28.70636631,
28.7154105 ],
[29.5708238 , 28.29679249, 28.24387864, 28.09311364,
28.80798971],
[29.06448615, 28.44302627, 28.07992176, 28.08775939,
28.82529639],
[28.87162709, 28.77157713, 28.16724515, 28.61608066,
28.5583429 ],
[28.6093443 , 28.93571439, 28.61179777, 28.83945678,
...
24.3583767 ],
[24.24838218, 24.02516 , 23.88579245, 23.77968789,
23.95752513],
[24.3665836 , 24.56907135, 24.42749482, 24.01915615,
24.85201607],
[24.77163012, 24.32209963, 24.07546572, 24.67613775,
24.49591058],
[23.67786016, 24.36793366, 24.53294169, 23.9520764 ,
24.49684549]],
[[24.77258876, 26.16809305, 26.12397499, 25.37374133,
25.68172717],
[24.29503902, 24.92182617, 25.35311054, 24.96393349,
25.85719824],
[26.49864827, 24.91169264, 25.0812683 , 25.61772418,
26.30555807],
[24.88881284, 25.62451806, 26.41064943, 24.67412043,
24.8594654 ],
[25.2881967 , 25.23970579, 25.90630064, 25.62082986,
24.61251897]]])
Coordinates:
* time (time) datetime64[us] 192B 2015-01-01 2015-02-01 ... 2016-12-01
* lat (lat) float64 40B -9.6 -9.5 -9.4 -9.3 -9.2
* lon (lon) float64 40B -40.7 -40.6 -40.5 -40.4 -40.3
Attributes:
units: degC
long_name: Temperatura do ar a 2m (sintético)array([[[27.15235854, 26.48000795, 27.3752256 , 27.47028236,
26.02448241],
[26.34891025, 27.0639202 , 26.8418787 , 26.99159942,
26.57347804],
[27.43969899, 27.38889597, 27.03301535, 27.5636206 ,
27.23375467],
[26.57035377, 27.18437539, 26.5205587 , 27.43922515,
26.97503704],
[26.90756882, 26.65953523, 27.61127067, 26.92273526,
26.78583609]],
[[28.32393322, 28.76615459, 28.68272203, 28.70636631,
28.7154105 ],
[29.5708238 , 28.29679249, 28.24387864, 28.09311364,
28.80798971],
[29.06448615, 28.44302627, 28.07992176, 28.08775939,
28.82529639],
[28.87162709, 28.77157713, 28.16724515, 28.61608066,
28.5583429 ],
[28.6093443 , 28.93571439, 28.61179777, 28.83945678,
...
24.3583767 ],
[24.24838218, 24.02516 , 23.88579245, 23.77968789,
23.95752513],
[24.3665836 , 24.56907135, 24.42749482, 24.01915615,
24.85201607],
[24.77163012, 24.32209963, 24.07546572, 24.67613775,
24.49591058],
[23.67786016, 24.36793366, 24.53294169, 23.9520764 ,
24.49684549]],
[[24.77258876, 26.16809305, 26.12397499, 25.37374133,
25.68172717],
[24.29503902, 24.92182617, 25.35311054, 24.96393349,
25.85719824],
[26.49864827, 24.91169264, 25.0812683 , 25.61772418,
26.30555807],
[24.88881284, 25.62451806, 26.41064943, 24.67412043,
24.8594654 ],
[25.2881967 , 25.23970579, 25.90630064, 25.62082986,
24.61251897]]])array(['2015-01-01T00:00:00.000000', '2015-02-01T00:00:00.000000',
'2015-03-01T00:00:00.000000', '2015-04-01T00:00:00.000000',
'2015-05-01T00:00:00.000000', '2015-06-01T00:00:00.000000',
'2015-07-01T00:00:00.000000', '2015-08-01T00:00:00.000000',
'2015-09-01T00:00:00.000000', '2015-10-01T00:00:00.000000',
'2015-11-01T00:00:00.000000', '2015-12-01T00:00:00.000000',
'2016-01-01T00:00:00.000000', '2016-02-01T00:00:00.000000',
'2016-03-01T00:00:00.000000', '2016-04-01T00:00:00.000000',
'2016-05-01T00:00:00.000000', '2016-06-01T00:00:00.000000',
'2016-07-01T00:00:00.000000', '2016-08-01T00:00:00.000000',
'2016-09-01T00:00:00.000000', '2016-10-01T00:00:00.000000',
'2016-11-01T00:00:00.000000', '2016-12-01T00:00:00.000000'],
dtype='datetime64[us]')array([-9.6, -9.5, -9.4, -9.3, -9.2])
array([-40.7, -40.6, -40.5, -40.4, -40.3])
Repare no repr acima. Ele mostra as dimensões, o tamanho de cada uma, as coordenadas e os atributos. Sendo muito mais informativo que um array.shape puro.
dims: ('time', 'lat', 'lon')
shape: (24, 5, 5)
atributos: {'units': 'degC', 'long_name': 'Temperatura do ar a 2m (sintético)'}
Coordinates:
* time (time) datetime64[us] 192B 2015-01-01 2015-02-01 ... 2016-12-01
* lat (lat) float64 40B -9.6 -9.5 -9.4 -9.3 -9.2
* lon (lon) float64 40B -40.7 -40.6 -40.5 -40.4 -40.3
Dataset. Como várias variáveis estão agrupadas.Se quisermos analisar um regime de dado bioma, como o semiárido Brasileiro, diante de uma perspectiva climática, podemos procurar por correlações entre as variáveis como temperatura e precipitação. Entender um pouco da regionalidade climática que caracteriza aquela região.
Para nosso primeiro contanto com o Xarray, vamos criar uma precipitação sintética (chuva concentrada em poucos meses) e juntar tudo em um Dataset.
array([-0.66666667, -0.58333333, -0.5 , -0.41666667, -0.33333333,
-0.25 , -0.16666667, -0.08333333, 0. , 0.08333333,
0.16666667, 0.25 , 0.33333333, 0.41666667, 0.5 ,
0.58333333, 0.66666667, 0.75 , 0.83333333, 0.91666667,
1. , 1.08333333, 1.16666667, 1.25 ])
# imitando uma estacao chuvosa
estacao_chuvosa = np.sin(2 * np.pi * ((np.arange(24) - 8) / 12))[:, None, None]
precip_valores = np.clip(50 + 120 * estacao_chuvosa + rng.normal(0, 15, (24, 5, 5)), 0, None)
# criando um dataset em xarray
precip = xr.DataArray(
precip_valores,
dims=["time", "lat", "lon"],
coords={"time": time, "lat": lat, "lon": lon},
name="tp",
attrs={"units": "mm", "long_name": "Precipitação mensal (sintético)"},
)
ds_sintetico = xr.Dataset({"t2m": temp, "tp": precip})
ds_sintetico<xarray.Dataset> Size: 10kB
Dimensions: (time: 24, lat: 5, lon: 5)
Coordinates:
* time (time) datetime64[us] 192B 2015-01-01 2015-02-01 ... 2016-12-01
* lat (lat) float64 40B -9.6 -9.5 -9.4 -9.3 -9.2
* lon (lon) float64 40B -40.7 -40.6 -40.5 -40.4 -40.3
Data variables:
t2m (time, lat, lon) float64 5kB 27.15 26.48 27.38 ... 25.62 24.61
tp (time, lat, lon) float64 5kB 161.7 145.3 173.0 ... 192.3 154.1array(['2015-01-01T00:00:00.000000', '2015-02-01T00:00:00.000000',
'2015-03-01T00:00:00.000000', '2015-04-01T00:00:00.000000',
'2015-05-01T00:00:00.000000', '2015-06-01T00:00:00.000000',
'2015-07-01T00:00:00.000000', '2015-08-01T00:00:00.000000',
'2015-09-01T00:00:00.000000', '2015-10-01T00:00:00.000000',
'2015-11-01T00:00:00.000000', '2015-12-01T00:00:00.000000',
'2016-01-01T00:00:00.000000', '2016-02-01T00:00:00.000000',
'2016-03-01T00:00:00.000000', '2016-04-01T00:00:00.000000',
'2016-05-01T00:00:00.000000', '2016-06-01T00:00:00.000000',
'2016-07-01T00:00:00.000000', '2016-08-01T00:00:00.000000',
'2016-09-01T00:00:00.000000', '2016-10-01T00:00:00.000000',
'2016-11-01T00:00:00.000000', '2016-12-01T00:00:00.000000'],
dtype='datetime64[us]')array([-9.6, -9.5, -9.4, -9.3, -9.2])
array([-40.7, -40.6, -40.5, -40.4, -40.3])
array([[[27.15235854, 26.48000795, 27.3752256 , 27.47028236,
26.02448241],
[26.34891025, 27.0639202 , 26.8418787 , 26.99159942,
26.57347804],
[27.43969899, 27.38889597, 27.03301535, 27.5636206 ,
27.23375467],
[26.57035377, 27.18437539, 26.5205587 , 27.43922515,
26.97503704],
[26.90756882, 26.65953523, 27.61127067, 26.92273526,
26.78583609]],
[[28.32393322, 28.76615459, 28.68272203, 28.70636631,
28.7154105 ],
[29.5708238 , 28.29679249, 28.24387864, 28.09311364,
28.80798971],
[29.06448615, 28.44302627, 28.07992176, 28.08775939,
28.82529639],
[28.87162709, 28.77157713, 28.16724515, 28.61608066,
28.5583429 ],
[28.6093443 , 28.93571439, 28.61179777, 28.83945678,
...
24.3583767 ],
[24.24838218, 24.02516 , 23.88579245, 23.77968789,
23.95752513],
[24.3665836 , 24.56907135, 24.42749482, 24.01915615,
24.85201607],
[24.77163012, 24.32209963, 24.07546572, 24.67613775,
24.49591058],
[23.67786016, 24.36793366, 24.53294169, 23.9520764 ,
24.49684549]],
[[24.77258876, 26.16809305, 26.12397499, 25.37374133,
25.68172717],
[24.29503902, 24.92182617, 25.35311054, 24.96393349,
25.85719824],
[26.49864827, 24.91169264, 25.0812683 , 25.61772418,
26.30555807],
[24.88881284, 25.62451806, 26.41064943, 24.67412043,
24.8594654 ],
[25.2881967 , 25.23970579, 25.90630064, 25.62082986,
24.61251897]]])array([[[161.6542045 , 145.25996514, 173.03975671, 144.50923541,
144.37381921],
[162.04002262, 165.36694569, 160.64453891, 128.63908869,
161.99356505],
[138.40842768, 157.45219013, 132.56703192, 160.61788067,
141.82406489],
[134.6835287 , 164.6303505 , 157.54771628, 144.71339643,
175.69073119],
[147.31326213, 154.40466356, 157.95675017, 144.62805944,
160.99009286]],
[[101.99821479, 103.82542517, 130.4396396 , 94.39120921,
73.80829504],
[134.16405493, 148.23991929, 103.92096102, 80.94742939,
105.34274036],
[105.70665575, 107.15114238, 93.29917937, 118.69341714,
117.86761063],
[ 87.5839157 , 120.48795098, 140.79027473, 112.57940499,
104.94012257],
[107.86995178, 119.2288515 , 84.03992592, 112.46586054,
...
164.39030104],
[155.58224529, 153.94314616, 176.01735204, 117.15996653,
132.6577859 ],
[136.11699206, 148.47413874, 150.10392591, 131.31316374,
139.14567797],
[141.01042328, 190.78440984, 180.94917767, 147.74680719,
148.4692991 ],
[136.6849885 , 125.30109226, 152.16984566, 138.95526871,
152.65029048]],
[[145.99691226, 158.57038313, 172.22940643, 175.49314397,
176.26207938],
[150.19266524, 182.82028595, 157.99682455, 179.49286872,
169.84041985],
[149.35418751, 165.25703443, 175.48045613, 179.19448071,
167.88518154],
[192.97668336, 185.11266156, 166.15018825, 181.25182718,
199.00690228],
[199.40726901, 151.58000179, 156.10200333, 192.2737818 ,
154.12578424]]])Daqui em diante, vamos usar um recorte pequeno e real do ERA5-Land (diário) sobre Petrolina (PE) e Juazeiro (BA). Como analisar a mesma dupla de cidades do nosso estudo de caso mais à frente. A ideia é combinar a mecânica do xarray em um dataset climático.
Para acessar o Earth Data Hub, registre-se em platform.destine.eu e gere um personal access token na seção Quota & API Keys. Guarde-o no seu .env como EARTH_HUB_API_KEY (a chave já está prevista no .env_copy deste repositório).
Atenção à cota: a conta gratuita tem um limite de requisições por mês. Nunca chame .load()/.compute() no dataset inteiro. Recorte com .sel() primeiro (uma cidade, uma pequena caixa delimitadora, um intervalo de datas) e só então materialize.
edh_token = os.environ.get("EARTH_HUB_API_KEY")
assert edh_token, "Defina EARTH_HUB_API_KEY no seu .env (veja earthdatahub.destine.eu)"
# Catálogo completo de datasets ERA5 disponíveis: https://earthdatahub.destine.eu/collections/era5/datasets
# Confira lá o slug exato do dataset que quer usar (hourly, daily, monthly-means...)
ERA5_LAND_DAILY = "era5-land-daily-utc-v1"
era5_url = f"https://edh:{edh_token}@data.earthdatahub.destine.eu/era5/{ERA5_LAND_DAILY}.zarr"<xarray.Dataset> Size: 8TB
Dimensions: (valid_time: 28002, latitude: 1472, longitude: 3600)
Coordinates:
* valid_time (valid_time) datetime64[ns] 224kB 1950-01-01 ... 2026-08-31
* latitude (latitude) float64 12kB 90.0 89.9 89.8 ... -56.9 -57.0 -57.1
* longitude (longitude) float64 29kB 0.0 0.1 0.2 0.3 ... 359.7 359.8 359.9
Data variables: (12/14)
d2m (valid_time, latitude, longitude) float32 594GB dask.array<chunksize=(75, 150, 300), meta=np.ndarray>
e (valid_time, latitude, longitude) float32 594GB dask.array<chunksize=(75, 150, 300), meta=np.ndarray>
pev (valid_time, latitude, longitude) float32 594GB dask.array<chunksize=(75, 150, 300), meta=np.ndarray>
ro (valid_time, latitude, longitude) float32 594GB dask.array<chunksize=(75, 150, 300), meta=np.ndarray>
sp (valid_time, latitude, longitude) float32 594GB dask.array<chunksize=(75, 150, 300), meta=np.ndarray>
ssr (valid_time, latitude, longitude) float32 594GB dask.array<chunksize=(75, 150, 300), meta=np.ndarray>
... ...
swvl1 (valid_time, latitude, longitude) float32 594GB dask.array<chunksize=(75, 150, 300), meta=np.ndarray>
swvl2 (valid_time, latitude, longitude) float32 594GB dask.array<chunksize=(75, 150, 300), meta=np.ndarray>
t2m (valid_time, latitude, longitude) float32 594GB dask.array<chunksize=(75, 150, 300), meta=np.ndarray>
tp (valid_time, latitude, longitude) float32 594GB dask.array<chunksize=(75, 150, 300), meta=np.ndarray>
u10 (valid_time, latitude, longitude) float32 594GB dask.array<chunksize=(75, 150, 300), meta=np.ndarray>
v10 (valid_time, latitude, longitude) float32 594GB dask.array<chunksize=(75, 150, 300), meta=np.ndarray>
Attributes:
GRIB_edition: 1
GRIB_centre: ecmf
GRIB_centreDescription: European Centre for Medium-Range Weather Forecasts
GRIB_subCentre: 0
Conventions: CF-1.7
institution: European Centre for Medium-Range Weather Forecasts
history: 2026-08-19T08:44 GRIB to CDM+CF via cfgrib-0.9.1...array(['1950-01-01T00:00:00.000000000', '1950-01-02T00:00:00.000000000',
'1950-01-03T00:00:00.000000000', ..., '2026-08-29T00:00:00.000000000',
'2026-08-30T00:00:00.000000000', '2026-08-31T00:00:00.000000000'],
shape=(28002,), dtype='datetime64[ns]')array([ 90. , 89.9, 89.8, ..., -56.9, -57. , -57.1], shape=(1472,))
array([0.000e+00, 1.000e-01, 2.000e-01, ..., 3.597e+02, 3.598e+02, 3.599e+02],
shape=(3600,))
|
||||||||||||||||
|
||||||||||||||||
|
||||||||||||||||
|
||||||||||||||||
|
||||||||||||||||
|
||||||||||||||||
|
||||||||||||||||
|
||||||||||||||||
|
||||||||||||||||
|
||||||||||||||||
|
||||||||||||||||
|
||||||||||||||||
|
||||||||||||||||
|
||||||||||||||||
Vamos checar os dois antes de recortar.
lon_name = "longitude" if "longitude" in ds_era5.coords else "lon"
lat_name = "latitude" if "latitude" in ds_era5.coords else "lat"
lon_min_raw = float(ds_era5[lon_name].min())
lon_max_raw = float(ds_era5[lon_name].max())
lat_descendente = float(ds_era5[lat_name][0]) > float(ds_era5[lat_name][-1])
usa_0_360 = lon_max_raw > 180
print(f"{lon_name}: [{lon_min_raw:.2f}, {lon_max_raw:.2f}]")
print(f"{lat_name}: descendente? {lat_descendente}")longitude: [0.00, 359.90]
latitude: descendente? True
Petrolina (PE) & Juazeiro (BA)
Vamos usar uma pequena caixa delimitadora ao redor das duas cidades.
import folium
from shapely.geometry import box
bbox= union.bounds
bounds_polygon = box(*bbox)
centro_lat = (bbox[1] + bbox[3]) / 2
centro_lon = (bbox[0] + bbox[2]) / 2
m = folium.Map(location=[centro_lat, centro_lon], zoom_start=10)
folium.GeoJson(
bounds_polygon.__geo_interface__,
name="Bounds",
style_function=lambda x: {
'fillColor': 'none',
'color': 'blue',
'weight': 3,
'fillOpacity': 0
}
).add_to(m)
mlat_min, lat_max = miny, maxy
lon_min, lon_max = minx, maxx
ANO_INICIO, ANO_FIM = "2015-01-01", "2025-12-31"
def ajusta_lon(lon):
return lon % 360 if float(ds_era5.longitude.max()) else lon
fatia_lat = slice(lat_max, lat_min) if lat_descendente else slice(lat_min, lat_max)
fatia_lon = slice(ajusta_lon(lon_min), ajusta_lon(lon_max))
# selecionando
# temperatura a 2m
# precipitacao total
variaveis = ["t2m", "tp"]<xarray.Dataset> Size: 5MB
Dimensions: (valid_time: 4018, latitude: 13, longitude: 11)
Coordinates:
* valid_time (valid_time) datetime64[ns] 32kB 2015-01-01 ... 2025-12-31
* latitude (latitude) float64 104B -8.7 -8.8 -8.9 -9.0 ... -9.7 -9.8 -9.9
* longitude (longitude) float64 88B 319.1 319.2 319.3 ... 319.9 320.0 320.1
Data variables:
t2m (valid_time, latitude, longitude) float32 2MB dask.array<chunksize=(34, 13, 11), meta=np.ndarray>
tp (valid_time, latitude, longitude) float32 2MB dask.array<chunksize=(34, 13, 11), meta=np.ndarray>
Attributes:
GRIB_edition: 1
GRIB_centre: ecmf
GRIB_centreDescription: European Centre for Medium-Range Weather Forecasts
GRIB_subCentre: 0
Conventions: CF-1.7
institution: European Centre for Medium-Range Weather Forecasts
history: 2026-08-19T08:44 GRIB to CDM+CF via cfgrib-0.9.1...array(['2015-01-01T00:00:00.000000000', '2015-01-02T00:00:00.000000000',
'2015-01-03T00:00:00.000000000', ..., '2025-12-29T00:00:00.000000000',
'2025-12-30T00:00:00.000000000', '2025-12-31T00:00:00.000000000'],
shape=(4018,), dtype='datetime64[ns]')array([-8.7, -8.8, -8.9, -9. , -9.1, -9.2, -9.3, -9.4, -9.5, -9.6, -9.7, -9.8,
-9.9])array([319.1, 319.2, 319.3, 319.4, 319.5, 319.6, 319.7, 319.8, 319.9, 320. ,
320.1])
|
||||||||||||||||
|
||||||||||||||||
Só agora, com o recorte pequeno definido, materializamos os dados de fato.
Se estiver muito grande/lento na sua conexão, reduza ANO_INICIO/ANO_FIM primeiro.
O ERA5 guarda temperatura em Kelvin e precipitação acumulada em metros. Convertendo para °C e mm:
<xarray.Dataset> Size: 5MB
Dimensions: (latitude: 13, longitude: 11, valid_time: 4018)
Coordinates:
* latitude (latitude) float64 104B -8.7 -8.8 -8.9 -9.0 ... -9.7 -9.8 -9.9
* longitude (longitude) float64 88B 319.1 319.2 319.3 ... 319.9 320.0 320.1
* valid_time (valid_time) datetime64[ns] 32kB 2015-01-01 ... 2025-12-31
Data variables:
t2m_celsius (valid_time, latitude, longitude) float32 2MB 26.76 ... 29.69
tp_mm (valid_time, latitude, longitude) float32 2MB 0.1642 ... 0.0...array([-8.7, -8.8, -8.9, -9. , -9.1, -9.2, -9.3, -9.4, -9.5, -9.6, -9.7, -9.8,
-9.9])array([319.1, 319.2, 319.3, 319.4, 319.5, 319.6, 319.7, 319.8, 319.9, 320. ,
320.1])array(['2015-01-01T00:00:00.000000000', '2015-01-02T00:00:00.000000000',
'2015-01-03T00:00:00.000000000', ..., '2025-12-29T00:00:00.000000000',
'2025-12-30T00:00:00.000000000', '2025-12-31T00:00:00.000000000'],
shape=(4018,), dtype='datetime64[ns]')array([[[26.764069, 26.982819, 27.232819, ..., 28.725006, 28.889069,
28.943756],
[26.631256, 26.857819, 27.131256, ..., 28.404694, 28.725006,
28.881256],
[26.545319, 26.818756, 27.154694, ..., 28.256256, 28.592194,
28.701569],
...,
[27.420319, 27.248444, 26.998444, ..., 27.350006, 27.357819,
27.506256],
[26.467194, 26.217194, 26.139069, ..., 26.975006, 27.100006,
27.326569],
[25.029694, 25.435944, 25.787506, ..., 26.764069, 26.873444,
26.975006]],
[[26.951569, 27.107819, 27.295319, ..., 28.217194, 28.279694,
28.326569],
[26.842194, 27.029694, 27.295319, ..., 27.912506, 28.123444,
28.264069],
[26.787506, 26.982819, 27.287506, ..., 27.764069, 28.006256,
28.162506],
...
[31.412506, 31.287506, 31.037506, ..., 31.162506, 31.162506,
31.225006],
[30.443756, 30.318756, 30.193756, ..., 31.006256, 31.037506,
30.975006],
[29.131256, 29.693756, 30.037506, ..., 30.787506, 30.912506,
30.693756]],
[[29.693756, 29.850006, 30.037506, ..., 30.787506, 30.850006,
30.912506],
[29.600006, 29.818756, 30.037506, ..., 30.568756, 30.756256,
30.912506],
[29.537506, 29.787506, 30.068756, ..., 30.506256, 30.725006,
30.787506],
...,
[30.662506, 30.475006, 30.193756, ..., 30.068756, 30.006256,
30.068756],
[29.662506, 29.506256, 29.381256, ..., 29.943756, 29.975006,
29.912506],
[28.350006, 28.756256, 29.100006, ..., 29.787506, 29.881256,
29.693756]]], shape=(4018, 13, 11), dtype=float32)array([[[0.16415119, 0.1475811 , 0.13756752, ..., 0.10818243,
0.08302927, 0.07414818],
[0.23591518, 0.20968914, 0.19049644, ..., 0.10436773,
0.08547306, 0.08010864],
[0.24557114, 0.22256374, 0.20349026, ..., 0.10025501,
0.08803606, 0.0885725 ],
...,
[0.06610155, 0.03260374, 0.03495812, ..., 0.023067 ,
0.02866983, 0.03829598],
[0.11759996, 0.06270409, 0.06094575, ..., 0.01786649,
0.02327561, 0.03123283],
[0.18179417, 0.10734797, 0.09006262, ..., 0.01783669,
0.02332032, 0.02890825]],
[[0.36978722, 0.3077984 , 0.32544136, ..., 0.5340576 ,
0.4901886 , 0.44631958],
[0.32949448, 0.3144741 , 0.36287308, ..., 0.5002022 ,
0.48613548, 0.46777725],
[0.3299713 , 0.341177 , 0.39672852, ..., 0.5083084 ,
0.5116463 , 0.5078316 ],
...
[0.09869784, 0.07747859, 0.0712648 , ..., 0.05670637,
0.06129593, 0.07522851],
[0.32794476, 0.10467321, 0.06415695, ..., 0.05423278,
0.05679578, 0.07014722],
[0.41362643, 0.13697147, 0.06941706, ..., 0.08066744,
0.06050617, 0.06931275]],
[[0.18018484, 0.06400794, 0.06810576, ..., 0.03536791,
0.03456324, 0.03959984],
[0.17669797, 0.07209927, 0.07113069, ..., 0.04496425,
0.0468567 , 0.05237013],
[0.1975447 , 0.08583814, 0.09331852, ..., 0.05101413,
0.05405396, 0.05792826],
...,
[0.05642325, 0.05996972, 0.05622953, ..., 0.03436953,
0.03711134, 0.03724545],
[0.04152209, 0.03794581, 0.03626198, ..., 0.02192706,
0.02514571, 0.02510101],
[0.02674013, 0.02009422, 0.01762062, ..., 0.01021475,
0.0108555 , 0.01052767]]], shape=(4018, 13, 11), dtype=float32).isel vs .selAssim como o pandas e o geopandas possuem suas maneiras de indexar e selecionar alguams fatias, o xarraytambém possui suas manhas.
.isel() seleciona por posição (como array[0]), igual numpy..sel() seleciona por rótulo (uma data, uma coordenada), o jeito que você realmente pensa sobre os dados.method="nearest" acha o ponto de grade mais próximo de uma coordenada que não é exata. Esse método é essencial pois permite buscar um ponto dentro do grid que o xarray dataset está estabelecido.<xarray.DataArray 't2m_celsius' (valid_time: 4018)> Size: 16kB
array([27.748444, 27.670319, 28.467194, ..., 31.600006, 32.006256,
30.787506], shape=(4018,), dtype=float32)
Coordinates:
* valid_time (valid_time) datetime64[ns] 32kB 2015-01-01 ... 2025-12-31
latitude float64 8B -9.4
longitude float64 8B 319.5
Attributes:
units: degC
long_name: Temperatura do ar a 2marray([27.748444, 27.670319, 28.467194, ..., 31.600006, 32.006256,
30.787506], shape=(4018,), dtype=float32)array(['2015-01-01T00:00:00.000000000', '2015-01-02T00:00:00.000000000',
'2015-01-03T00:00:00.000000000', ..., '2025-12-29T00:00:00.000000000',
'2025-12-30T00:00:00.000000000', '2025-12-31T00:00:00.000000000'],
shape=(4018,), dtype='datetime64[ns]')array(-9.4)
array(319.5)
No numpy puro, somar dois arrays exige que as formas “batam” na ordem certa. No xarray, o alinhamento é feito pelo nome das dimensões e pelos valores das coordenadas
temp_fahrenheit = dados["t2m_celsius"] * 9 / 5 + 32 # transformando em fahrenheit
# opa, aqui as coisas ficam interessante
# amplitude termica: diferença entre o dia mais quente e o mais frio do período, célula a célula
amplitude_termica = dados["t2m_celsius"].max("valid_time") - dados["t2m_celsius"].min("valid_time")
amplitude_termica<xarray.DataArray 't2m_celsius' (latitude: 13, longitude: 11)> Size: 572B
array([[11.21875 , 11.2578125, 11.1953125, 11.1484375, 11.1328125,
11.03125 , 10.9140625, 10.984375 , 10.875 , 10.875 ,
10.75 ],
[11.21875 , 11.2265625, 11.25 , 11.1328125, 11.09375 ,
11.0703125, 10.9296875, 10.9453125, 10.890625 , 10.8984375,
10.8515625],
[11.2578125, 11.25 , 11.2265625, 11.1796875, 11.1015625,
11.1015625, 11.03125 , 11. , 10.9453125, 10.9609375,
11.0234375],
[11.28125 , 11.2890625, 11.28125 , 11.25 , 11.2265625,
11.2265625, 11.1796875, 11.1015625, 10.9453125, 11. ,
10.9921875],
[11.3359375, 11.3671875, 11.3203125, 11.34375 , 11.3515625,
11.3671875, 11.3203125, 11.21875 , 11.0625 , 11. ,
11.046875 ],
[11.2890625, 11.4140625, 11.46875 , 11.5234375, 11.5390625,
11.5 , 11.4609375, 11.375 , 11.2421875, 11.1015625,
11.125 ],
[11.0625 , 11.515625 , 11.5625 , 11.6015625, 11.75 ,
11.71875 , 11.625 , 11.59375 , 11.4765625, 11.390625 ,
11.4375 ],
[11.03125 , 11.3203125, 11.75 , 11.75 , 11.9765625,
12.03125 , 12.0078125, 11.9140625, 11.7578125, 11.671875 ,
11.78125 ],
[11.5078125, 11.6953125, 11.7890625, 11.9609375, 12.125 ,
12.1171875, 12.15625 , 12.125 , 11.9921875, 11.8984375,
11.9921875],
[11.53125 , 11.703125 , 11.984375 , 12.078125 , 12.265625 ,
12.2109375, 12.3125 , 12.296875 , 12.140625 , 12.1484375,
12.109375 ],
[11.7890625, 11.9609375, 12. , 12.15625 , 12.390625 ,
12.3515625, 12.40625 , 12.359375 , 12.3125 , 12.2734375,
12.171875 ],
[11.9765625, 11.8828125, 11.9609375, 12.125 , 12.34375 ,
12.375 , 12.3828125, 12.4375 , 12.5 , 12.40625 ,
12.21875 ],
[11.90625 , 11.9765625, 12.1015625, 12.0625 , 12.3203125,
12.3984375, 12.53125 , 12.4921875, 12.4921875, 12.5234375,
12.28125 ]], dtype=float32)
Coordinates:
* latitude (latitude) float64 104B -8.7 -8.8 -8.9 -9.0 ... -9.7 -9.8 -9.9
* longitude (longitude) float64 88B 319.1 319.2 319.3 ... 319.9 320.0 320.1
Attributes:
units: degC
long_name: Temperatura do ar a 2marray([[11.21875 , 11.2578125, 11.1953125, 11.1484375, 11.1328125,
11.03125 , 10.9140625, 10.984375 , 10.875 , 10.875 ,
10.75 ],
[11.21875 , 11.2265625, 11.25 , 11.1328125, 11.09375 ,
11.0703125, 10.9296875, 10.9453125, 10.890625 , 10.8984375,
10.8515625],
[11.2578125, 11.25 , 11.2265625, 11.1796875, 11.1015625,
11.1015625, 11.03125 , 11. , 10.9453125, 10.9609375,
11.0234375],
[11.28125 , 11.2890625, 11.28125 , 11.25 , 11.2265625,
11.2265625, 11.1796875, 11.1015625, 10.9453125, 11. ,
10.9921875],
[11.3359375, 11.3671875, 11.3203125, 11.34375 , 11.3515625,
11.3671875, 11.3203125, 11.21875 , 11.0625 , 11. ,
11.046875 ],
[11.2890625, 11.4140625, 11.46875 , 11.5234375, 11.5390625,
11.5 , 11.4609375, 11.375 , 11.2421875, 11.1015625,
11.125 ],
[11.0625 , 11.515625 , 11.5625 , 11.6015625, 11.75 ,
11.71875 , 11.625 , 11.59375 , 11.4765625, 11.390625 ,
11.4375 ],
[11.03125 , 11.3203125, 11.75 , 11.75 , 11.9765625,
12.03125 , 12.0078125, 11.9140625, 11.7578125, 11.671875 ,
11.78125 ],
[11.5078125, 11.6953125, 11.7890625, 11.9609375, 12.125 ,
12.1171875, 12.15625 , 12.125 , 11.9921875, 11.8984375,
11.9921875],
[11.53125 , 11.703125 , 11.984375 , 12.078125 , 12.265625 ,
12.2109375, 12.3125 , 12.296875 , 12.140625 , 12.1484375,
12.109375 ],
[11.7890625, 11.9609375, 12. , 12.15625 , 12.390625 ,
12.3515625, 12.40625 , 12.359375 , 12.3125 , 12.2734375,
12.171875 ],
[11.9765625, 11.8828125, 11.9609375, 12.125 , 12.34375 ,
12.375 , 12.3828125, 12.4375 , 12.5 , 12.40625 ,
12.21875 ],
[11.90625 , 11.9765625, 12.1015625, 12.0625 , 12.3203125,
12.3984375, 12.53125 , 12.4921875, 12.4921875, 12.5234375,
12.28125 ]], dtype=float32)array([-8.7, -8.8, -8.9, -9. , -9.1, -9.2, -9.3, -9.4, -9.5, -9.6, -9.7, -9.8,
-9.9])array([319.1, 319.2, 319.3, 319.4, 319.5, 319.6, 319.7, 319.8, 319.9, 320. ,
320.1])Reduzir ao longo de ["lat", "lon"] dá uma série temporal (1 valor por tempo).
Reduzir ao longo de "time" dá um mapa (1 valor por célula).
media_espacial = dados.mean(dim=[lat_name, lon_name]) # série temporal
media_temporal = dados.mean(dim="valid_time") # mapa médio
media_espacial["t2m_celsius"].plot(marker="o", figsize=(10, 3))
plt.title("Temperatura média espacial ao longo do tempo — ERA5-Land (Petrolina/Juazeiro)")
plt.tight_layout()
plt.show()
groupby e resample: climatologia e agregações temporaisgroupby("time.month") agrupa todos os janeiros juntos, todos os fevereiros juntos, por aí vai. Essa é a nossa base para calcular uma climatologia (ciclo sazonal médio).resample("YE") (ou "1D", "1MS"…) são diferentes formas de reagrupar essa dada frequência temporal em uma nova frequência que faça sentido para nossa análise.climatologia_fundamentos = dados.groupby("valid_time.month").mean()
climatologia_fundamentos["t2m_celsius"].mean(dim=[lat_name, lon_name]).hvplot.line(
x="month", marker="o", height=300, width=650,
title="Ciclo sazonal médio de temperatura — ERA5-Land (Petrolina/Juazeiro)",
xlabel="Mês", ylabel="Temperatura (°C)",
)chunksQuando possuimos dados que não cabem na memória (o que de fato é a maioria dos dados geoespacias e análise para um país de dimensão continental) é impossível carregar tudo isso na memória RAM do seu computador. Dessa forma, temos algumas maneiras bem inteligentes de lidar com esses dados. Uma dessas maneiras é utilizando chunks. Quando você abre um dataset com chunks={...} (ou chunks="auto"), o xarray usa Dask por baixo, assim as operações ficam preguiçosas (lazy) e nada é lido de fato até você chamar .load() ou .compute(). Essa mesma estratégia que existe no xarray é utilizada por exemplo no GEE. Isso permite selecionar (.sel) um recorte pequeno de um dataset gigante e só então materializá-lo.
“Zarr is like parquet for arrays”
Zarr is an open-source protocol for storing multi-dimensional-array datasets, like measurements over time, space, or other variables, as N-dimensional arrays. Think of it as a lightweight, modular alternative to older formats like HDF5, designed to work especially well with cloud storage and large-scale computing.
O NetCDF tradicional é um único arquivo binário, para ler um pedacinho, muitas vezes é preciso baixar o arquivo inteiro. O Zarr quebra o array em chunks (blocos) guardados como objetos separados, o que combina perfeitamente com armazenamento em nuvem (S3, Azure Blob) e acesso via HTTP. No fim, ele permite que você leia apenas os blocos que precisa. É por isso que provedores modernos de reanálise, como o Earth Data Hub da DestinE, que distribuem o ERA5 em Zarr ao invés de NetCDF.
beleza, já vimos o básico do xarray com dado de verdade. Bora aprofundar a análise no estudo de caso completo.
Petrolina e Juazeiro são cidades-irmãs separadas pelo rio São Francisco, no coração do semiárido nordestino. O clima é quente e com chuvas escassas e concentradas e, ainda assim, a região é um dos maiores polos de fruticultura irrigada de exportação do Brasil (manga, uva), graças à irrigação a partir do rio São Francisco. Essa tensão entre clima semiárido e agricultura irrigada é um ótimo motivo para olhar com cuidado para os dados climáticos da região.
Vamos usar o ERA5-Land (reanálise da ECMWF, ~9 km de resolução), acessado via Earth Data Hub (EDH) da plataforma DestinE, um espelho do ERA5 em Zarr na nuvem, que permite abrir o dataset inteiro de forma preguiçosa (lazy) e baixar só o recorte espaço-temporal de que precisamos.
Dado o espelho de zarr que escolhemos do Earth Data Hub, os dados estão já agregados por dia. Podemos tanto fazer os plots a nível de dia ou podemos rearranjar eles para plotar a nível de mês.
dados_diarios = xr.Dataset()
dados_diarios["t2m_celsius"] = dados["t2m_celsius"]
dados_diarios["tp_mm"] = dados["tp_mm"]
dados_mensais = xr.Dataset()
dados_mensais["t2m_celsius"] = dados_diarios["t2m_celsius"].resample(valid_time="1MS").mean()
dados_mensais["tp_mm"] = dados_diarios["tp_mm"].resample(valid_time="1MS").sum()
dados_mensais<xarray.Dataset> Size: 152kB
Dimensions: (latitude: 13, longitude: 11, valid_time: 132)
Coordinates:
* latitude (latitude) float64 104B -8.7 -8.8 -8.9 -9.0 ... -9.7 -9.8 -9.9
* longitude (longitude) float64 88B 319.1 319.2 319.3 ... 319.9 320.0 320.1
* valid_time (valid_time) datetime64[ns] 1kB 2015-01-01 ... 2025-12-01
spatial_ref int64 8B 0
Data variables:
t2m_celsius (valid_time, latitude, longitude) float32 76kB 27.98 ... 27.87
tp_mm (valid_time, latitude, longitude) float32 76kB 14.81 ... 7.867array([-8.7, -8.8, -8.9, -9. , -9.1, -9.2, -9.3, -9.4, -9.5, -9.6, -9.7, -9.8,
-9.9])array([319.1, 319.2, 319.3, 319.4, 319.5, 319.6, 319.7, 319.8, 319.9, 320. ,
320.1])array(['2015-01-01T00:00:00.000000000', '2015-02-01T00:00:00.000000000',
'2015-03-01T00:00:00.000000000', '2015-04-01T00:00:00.000000000',
'2015-05-01T00:00:00.000000000', '2015-06-01T00:00:00.000000000',
'2015-07-01T00:00:00.000000000', '2015-08-01T00:00:00.000000000',
'2015-09-01T00:00:00.000000000', '2015-10-01T00:00:00.000000000',
'2015-11-01T00:00:00.000000000', '2015-12-01T00:00:00.000000000',
'2016-01-01T00:00:00.000000000', '2016-02-01T00:00:00.000000000',
'2016-03-01T00:00:00.000000000', '2016-04-01T00:00:00.000000000',
'2016-05-01T00:00:00.000000000', '2016-06-01T00:00:00.000000000',
'2016-07-01T00:00:00.000000000', '2016-08-01T00:00:00.000000000',
'2016-09-01T00:00:00.000000000', '2016-10-01T00:00:00.000000000',
'2016-11-01T00:00:00.000000000', '2016-12-01T00:00:00.000000000',
'2017-01-01T00:00:00.000000000', '2017-02-01T00:00:00.000000000',
'2017-03-01T00:00:00.000000000', '2017-04-01T00:00:00.000000000',
'2017-05-01T00:00:00.000000000', '2017-06-01T00:00:00.000000000',
'2017-07-01T00:00:00.000000000', '2017-08-01T00:00:00.000000000',
'2017-09-01T00:00:00.000000000', '2017-10-01T00:00:00.000000000',
'2017-11-01T00:00:00.000000000', '2017-12-01T00:00:00.000000000',
'2018-01-01T00:00:00.000000000', '2018-02-01T00:00:00.000000000',
'2018-03-01T00:00:00.000000000', '2018-04-01T00:00:00.000000000',
'2018-05-01T00:00:00.000000000', '2018-06-01T00:00:00.000000000',
'2018-07-01T00:00:00.000000000', '2018-08-01T00:00:00.000000000',
'2018-09-01T00:00:00.000000000', '2018-10-01T00:00:00.000000000',
'2018-11-01T00:00:00.000000000', '2018-12-01T00:00:00.000000000',
'2019-01-01T00:00:00.000000000', '2019-02-01T00:00:00.000000000',
'2019-03-01T00:00:00.000000000', '2019-04-01T00:00:00.000000000',
'2019-05-01T00:00:00.000000000', '2019-06-01T00:00:00.000000000',
'2019-07-01T00:00:00.000000000', '2019-08-01T00:00:00.000000000',
'2019-09-01T00:00:00.000000000', '2019-10-01T00:00:00.000000000',
'2019-11-01T00:00:00.000000000', '2019-12-01T00:00:00.000000000',
'2020-01-01T00:00:00.000000000', '2020-02-01T00:00:00.000000000',
'2020-03-01T00:00:00.000000000', '2020-04-01T00:00:00.000000000',
'2020-05-01T00:00:00.000000000', '2020-06-01T00:00:00.000000000',
'2020-07-01T00:00:00.000000000', '2020-08-01T00:00:00.000000000',
'2020-09-01T00:00:00.000000000', '2020-10-01T00:00:00.000000000',
'2020-11-01T00:00:00.000000000', '2020-12-01T00:00:00.000000000',
'2021-01-01T00:00:00.000000000', '2021-02-01T00:00:00.000000000',
'2021-03-01T00:00:00.000000000', '2021-04-01T00:00:00.000000000',
'2021-05-01T00:00:00.000000000', '2021-06-01T00:00:00.000000000',
'2021-07-01T00:00:00.000000000', '2021-08-01T00:00:00.000000000',
'2021-09-01T00:00:00.000000000', '2021-10-01T00:00:00.000000000',
'2021-11-01T00:00:00.000000000', '2021-12-01T00:00:00.000000000',
'2022-01-01T00:00:00.000000000', '2022-02-01T00:00:00.000000000',
'2022-03-01T00:00:00.000000000', '2022-04-01T00:00:00.000000000',
'2022-05-01T00:00:00.000000000', '2022-06-01T00:00:00.000000000',
'2022-07-01T00:00:00.000000000', '2022-08-01T00:00:00.000000000',
'2022-09-01T00:00:00.000000000', '2022-10-01T00:00:00.000000000',
'2022-11-01T00:00:00.000000000', '2022-12-01T00:00:00.000000000',
'2023-01-01T00:00:00.000000000', '2023-02-01T00:00:00.000000000',
'2023-03-01T00:00:00.000000000', '2023-04-01T00:00:00.000000000',
'2023-05-01T00:00:00.000000000', '2023-06-01T00:00:00.000000000',
'2023-07-01T00:00:00.000000000', '2023-08-01T00:00:00.000000000',
'2023-09-01T00:00:00.000000000', '2023-10-01T00:00:00.000000000',
'2023-11-01T00:00:00.000000000', '2023-12-01T00:00:00.000000000',
'2024-01-01T00:00:00.000000000', '2024-02-01T00:00:00.000000000',
'2024-03-01T00:00:00.000000000', '2024-04-01T00:00:00.000000000',
'2024-05-01T00:00:00.000000000', '2024-06-01T00:00:00.000000000',
'2024-07-01T00:00:00.000000000', '2024-08-01T00:00:00.000000000',
'2024-09-01T00:00:00.000000000', '2024-10-01T00:00:00.000000000',
'2024-11-01T00:00:00.000000000', '2024-12-01T00:00:00.000000000',
'2025-01-01T00:00:00.000000000', '2025-02-01T00:00:00.000000000',
'2025-03-01T00:00:00.000000000', '2025-04-01T00:00:00.000000000',
'2025-05-01T00:00:00.000000000', '2025-06-01T00:00:00.000000000',
'2025-07-01T00:00:00.000000000', '2025-08-01T00:00:00.000000000',
'2025-09-01T00:00:00.000000000', '2025-10-01T00:00:00.000000000',
'2025-11-01T00:00:00.000000000', '2025-12-01T00:00:00.000000000'],
dtype='datetime64[ns]')array(0)
array([[[27.980288, 28.063957, 28.212898, ..., 28.87948 , 28.946266,
28.974743],
[27.930387, 28.093191, 28.267586, ..., 28.640066, 28.825802,
28.958866],
[27.922323, 28.090166, 28.342182, ..., 28.571012, 28.785984,
28.900902],
...,
[28.026407, 27.87318 , 27.610329, ..., 27.691477, 27.662746,
27.790771],
[27.142838, 26.960379, 26.817738, ..., 27.286236, 27.33941 ,
27.478271],
[25.870913, 26.254229, 26.517334, ..., 27.002718, 27.074541,
27.106548]],
[[26.779964, 26.860043, 27.02773 , ..., 27.805075, 27.932028,
28.022152],
[26.827118, 27.034708, 27.250387, ..., 27.66473 , 27.88934 ,
27.98839 ],
[26.872599, 27.101671, 27.385433, ..., 27.6731 , 27.893246,
27.957977],
...
[28.722912, 28.52812 , 28.29062 , ..., 28.033329, 27.935413,
27.972912],
[27.82083 , 27.719788, 27.517704, ..., 27.807287, 27.77812 ,
27.706245],
[26.766663, 27.10208 , 27.262495, ..., 27.50312 , 27.573954,
27.372913]],
[[28.577818, 28.671568, 28.803625, ..., 29.34193 , 29.421568,
29.517334],
[28.506245, 28.66451 , 28.845963, ..., 29.181648, 29.372173,
29.551609],
[28.443745, 28.646366, 28.900398, ..., 29.202818, 29.416527,
29.474995],
...,
[28.872173, 28.715923, 28.51935 , ..., 28.59294 , 28.496164,
28.555641],
[27.911488, 27.82076 , 27.712898, ..., 28.318745, 28.295559,
28.245157],
[26.774391, 27.177616, 27.454834, ..., 27.97701 , 28.057657,
27.869148]]], shape=(132, 13, 11), dtype=float32)array([[[14.806369 , 14.631644 , 14.095271 , ..., 8.908914 ,
6.6579933 , 5.0783834 ],
[16.228367 , 16.724293 , 16.810562 , ..., 10.938635 ,
7.986067 , 5.8771977 ],
[15.801462 , 16.572668 , 16.91387 , ..., 11.243633 ,
8.181398 , 6.0351553 ],
...,
[ 7.605346 , 7.1450744 , 7.168586 , ..., 3.3280332 ,
3.3599796 , 3.4116857 ],
[ 9.990854 , 7.66756 , 7.3019247 , ..., 3.6115737 ,
3.412163 , 3.389129 ],
[12.978937 , 8.815142 , 7.5760117 , ..., 4.005123 ,
3.4859235 , 3.3815677 ]],
[[78.40914 , 75.38811 , 74.04284 , ..., 75.51008 ,
72.346756 , 69.89222 ],
[81.449104 , 78.35495 , 75.379906 , ..., 72.61705 ,
69.63991 , 67.206406 ],
[82.299675 , 79.392 , 75.97528 , ..., 69.47513 ,
66.94797 , 64.50139 ],
...
[13.299793 , 12.425421 , 14.5791025 , ..., 23.978027 ,
22.086264 , 19.73352 ],
[15.798021 , 15.174266 , 19.331335 , ..., 28.35074 ,
26.059748 , 23.725597 ],
[19.229073 , 19.178658 , 23.215675 , ..., 37.281033 ,
30.918491 , 27.781723 ]],
[[ 4.676309 , 4.8057485 , 6.3073015 , ..., 12.030514 ,
10.7441 , 9.926463 ],
[ 4.73625 , 5.1100388 , 6.5727806 , ..., 9.567623 ,
9.266831 , 8.678206 ],
[ 5.011552 , 5.324618 , 6.853131 , ..., 7.691553 ,
7.705172 , 7.7718573 ],
...,
[ 7.13689 , 6.934534 , 6.60666 , ..., 5.7411017 ,
5.836086 , 6.28549 ],
[10.711933 , 7.623354 , 6.421022 , ..., 6.2875457 ,
6.4381375 , 6.8748302 ],
[12.119015 , 8.147898 , 6.5570116 , ..., 6.65641 ,
7.319363 , 7.8667884 ]]], shape=(132, 13, 11), dtype=float32)Ciclo médio mensal, tirando a média espacial sobre a bbox. Como podemos entender o semiárido brasileiro?
# aqui reduzimos o espaço(x,y) para uma dimensão temporal
media_espacial_mensal = dados_mensais.mean(dim=[lat_name, lon_name])
# já aqui agregamos todos os meses, mes a mes, para calcular a média de cada mes
# dessa forma, cada pixel contem a media no eixo temporal, entre 2015-2023
grupo = media_espacial_mensal["t2m_celsius"].groupby("valid_time.month")
clim_media = grupo.mean()
clim_max = grupo.max()
clim_min = grupo.min()
climatologia = media_espacial_mensal.groupby("valid_time.month").mean()
temp_plot = climatologia["t2m_celsius"].hvplot.line(
x="month", color="firebrick", height=300, width=420,
title="Temperatura média mensal", xlabel="Mês", ylabel="°C",
)
precip_plot = climatologia["tp_mm"].hvplot.bar(
x="month", color="steelblue", height=300, width=420,
title="Precipitação média mensal", xlabel="Mês", ylabel="mm",
)
opts = dict(x="month",
height=300,
width=420)
temp_plot = (
clim_max.hvplot.line(**opts, color="darkred", line_dash="dashed", label="Máxima")
* clim_media.hvplot.line(**opts, color="firebrick", line_width=2.5, label="Média")
* clim_min.hvplot.line(**opts, color="darkorange", line_dash="dashed", label="Mínima")
).opts(title="Temperatura mensal", xlabel="Mês", ylabel="°C", legend_position="bottom_right")
precip_plot = climatologia["tp_mm"].hvplot.bar(
x="month", color="steelblue", height=300, width=420,
title="Precipitação média mensal", xlabel="Mês", ylabel="mm",
)
temp_plot + precip_plotUma anomalia é o desvio de cada mês em relação à sua climatologia (o normal esperado para aquele mês). Anomalias negativas de chuva persistentes indicam períodos de seca.
<xarray.Dataset> Size: 288B
Dimensions: (month: 12)
Coordinates:
* month (month) int64 96B 1 2 3 4 5 6 7 8 9 10 11 12
spatial_ref (month) int64 96B 0 0 0 0 0 0 0 0 0 0 0 0
Data variables:
t2m_celsius (month) float32 48B 27.56 27.4 27.56 ... 27.91 28.25 28.28
tp_mm (month) float32 48B 68.14 57.23 63.67 ... 10.54 33.26 32.66array([ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
array([27.559742, 27.402514, 27.563099, 26.884632, 25.751867, 24.429482,
23.76684 , 24.48817 , 26.114685, 27.907818, 28.249249, 28.27881 ],
dtype=float32)array([68.141235, 57.233395, 63.66849 , 47.192966, 19.679893, 9.470023,
6.706355, 3.938633, 2.439172, 10.544956, 33.255283, 32.658604],
dtype=float32)anomalia = media_espacial_mensal.groupby("valid_time.month") - climatologia
linha_zero = hv.HLine(0).opts(color="black", line_width=0.8)
(
anomalia["tp_mm"].hvplot.line(
x="valid_time", color="steelblue", height=300, width=850,
title="Anomalia mensal de precipitação — Petrolina/Juazeiro",
xlabel="Data", ylabel="Anomalia (mm)",
)
* linha_zero
)t2m_media_simples = dados_mensais["t2m_celsius"].max(dim="valid_time")
# mesma conversão 0-360 - -180/180 de sempre
t2m_media_simples = t2m_media_simples.assign_coords(
**{lon_name: (((t2m_media_simples[lon_name] + 180) % 360) - 180)}
).sortby(lon_name)
t2m_media_simples.hvplot.image(
x=lon_name, y=lat_name, cmap="YlOrRd", colorbar=True,
title="Temperatura minima (2015-2025) Petrolina & Juazeiro",
height=420, width=460,
)import matplotlib.pyplot as plt
import cartopy.crs as ccrs
proj = ccrs.PlateCarree()
t2m_media = dados_mensais["t2m_celsius"].mean(dim="valid_time")
# a grade do ERA5-Land aqui ainda está na convenção 0-360° (ex: 319.9),
# como vimos lá em "Convenções do ERA5". Convertendo para -180/180 antes
t2m_media = t2m_media.assign_coords(
**{lon_name: (((t2m_media[lon_name] + 180) % 360) - 180)}
).sortby(lon_name)
lon_vals = t2m_media[lon_name].values
lat_vals = t2m_media[lat_name].values
p = t2m_media.plot(
subplot_kws=dict(projection=proj,
facecolor="lightgray"),
transform=proj,
cmap="YlOrRd",
cbar_kwargs={"label": "Temperatura média 2015-2025 (°C)"},
figsize=(7, 6),
)
ax = p.axes
for cidade, cor, nome in [(petrolina, "black", "Petrolina"), (juazeiro, "black", "Juazeiro")]:
ax.add_geometries(cidade.geometry, crs=proj, facecolor="none", edgecolor=cor, linewidth=1.5, label=nome)
# extent a partir da própria grade ERA5
margem = 0.1
ax.set_extent(
[lon_vals.min() - margem, lon_vals.max() + margem, lat_vals.min() - margem, lat_vals.max() + margem],
crs=proj,
)
ax.set_title("Petrolina e Juazeiro. Temperatura media entre 2015-2025")
plt.show()
Podemos também criar um facet grid plot para reproduzir um painel de mapas, passando diferente valores por dimensão. Apenas passando col="month". Pra isso precisamos da climatologia por célula do grid, não a espacialmente-agregada que já calculamos lá em cima (aquela virou 1 valor por mês; essa aqui mantém latitude/longitude).
# climatologia mensal por célula do grid (mantém latitude/longitude, a media do mês)
climatologia_espacial = dados_mensais.groupby("valid_time.month").mean()
t2m_climatologia = climatologia_espacial["t2m_celsius"].assign_coords(
**{lon_name: (((climatologia_espacial[lon_name] + 180) % 360) - 180)}
).sortby(lon_name)
fg = t2m_climatologia.plot(
col="month",
col_wrap=4,
transform=proj,
subplot_kws={"projection": proj},
cmap="YlOrRd",
cbar_kwargs={"orientation": "horizontal",
"shrink": 0.8,
"aspect": 40,
"label": "Temperatura média (°C)"},
robust=True,
)
for ax in fg.axs.flat:
for cidade, cor in [(petrolina, "black"), (juazeiro, "navy")]:
ax.add_geometries(cidade.geometry, crs=proj, facecolor="none", edgecolor=cor, linewidth=1)
ax.set_extent(
[t2m_climatologia[lon_name].min() - 0.1, t2m_climatologia[lon_name].max() + 0.1,
t2m_climatologia[lat_name].min() - 0.1, t2m_climatologia[lat_name].max() + 0.1],
crs=proj,
)
plt.show()
Aqui, utilizarei apenas 1 ponto ao inves de pegar uma malha para poder analisar uma série temporal maior sem depender de acessar uma parcela muito grande de dados. Minha idéia por aqui é entender como as mudanças climáticas estão impactando nas temperaturas minimas extremas. Isso é, quando uma noite fica acima de 25C, é considerado como extreme-heat wave, onda extrema de calor. Isso porque uma noite de 25C é excessivamente quente.
Entao para conseguir esse resultado, vou primeiro escolher uma cidade da regiao de goias, recalcular o dado do ERA5 para a minima diaria, se a minima diária excede 25 entao conto 1, e somo para o ano. Isso implicará em uma gráfica de barras com 25 anos contando quantas noites excederem 25C
edh_token = os.environ.get("EARTH_HUB_API_KEY")
assert edh_token, "Defina EARTH_HUB_API_KEY no seu .env (veja earthdatahub.destine.eu)"
# Catálogo completo de datasets ERA5 disponíveis: https://earthdatahub.destine.eu/collections/era5/datasets
# Diferente da seção anterior, aqui precisamos do dado HORÁRIO (não do diário já
# agregado), porque só com valores por hora dá pra calcular a mínima de cada dia.
ERA5_LAND_HOURLY = "era5-land-v0"
era5_url_horario = f"https://edh:{edh_token}@data.earthdatahub.destine.eu/era5/{ERA5_LAND_HOURLY}.zarr"Esse são os dados do espelho do ERA5 em horas. Aqui temos um volume de dados muito maior porque estamos discretizando por horas.
Vamos aproveitar pra checar as mesmas duas convenções (longitude e ordem da latitude) que vimos antes, já que cada dataset do EDH pode vir com uma convenção diferente.
ds_era5_horario = xr.open_dataset(era5_url_horario, chunks={}, engine="zarr")
lon_name_h = "longitude" if "longitude" in ds_era5_horario.coords else "lon"
lat_name_h = "latitude" if "latitude" in ds_era5_horario.coords else "lat"
usa_0_360_h = float(ds_era5_horario[lon_name_h].max()) > 180
lat_descendente_h = float(ds_era5_horario[lat_name_h][0]) > float(ds_era5_horario[lat_name_h][-1])
print(f"{lon_name_h}: usa 0-360? {usa_0_360_h}")
print(f"{lat_name_h}: descendente? {lat_descendente_h}")longitude: usa 0-360? True
latitude: descendente? True
Em vez de uma caixa delimitadora, aqui vamos pegar só um ponto, diferente do que já buscamos um slice do da area.
Goiânia (GO): lat=-16.643, lon=-49.274
Como é um ponto só (não uma malha), mesmo pegando dado horário por décadas o volume final não é significativo, por isso dá pra usar uma janela bem mais longa do que na análise em grade lá em cima. Continua valendo a mesma regra: .sel() primeiro (ponto + intervalo de tempo), preguiçoso, e só depois .load().
def ajusta_lon_h(lon):
return lon % 360 if usa_0_360_h else lon
ANO_INICIO_PONTO, ANO_FIM_PONTO = "1985-01-01", "2024-12-31" # 40 anos
ponto_goiania = (
ds_era5_horario["t2m"]
.sel(valid_time=slice(ANO_INICIO_PONTO, ANO_FIM_PONTO))
.sel(
latitude=lat_goiania,
longitude=ajusta_lon_h(lon_goiania),
method="nearest",
)
)
ponto_goiania<xarray.DataArray 't2m' (valid_time: 350640)> Size: 1MB
dask.array<getitem, shape=(350640,), dtype=float32, chunksize=(1440,), chunktype=numpy.ndarray>
Coordinates:
* valid_time (valid_time) datetime64[ns] 3MB 1985-01-01 ... 2024-12-31T23:...
latitude float64 8B -16.6
longitude float64 8B 310.7
Attributes: (12/32)
GRIB_paramId: 167
GRIB_dataType: fc
GRIB_numberOfPoints: 5299200
GRIB_typeOfLevel: surface
GRIB_stepUnits: 1
GRIB_stepType: instant
... ...
GRIB_totalNumber: 0
GRIB_units: K
long_name: 2 metre temperature
units: K
standard_name: unknown
last_ingested_valid_time_index: 672047
|
||||||||||||||||
array(['1985-01-01T00:00:00.000000000', '1985-01-01T01:00:00.000000000',
'1985-01-01T02:00:00.000000000', ..., '2024-12-31T21:00:00.000000000',
'2024-12-31T22:00:00.000000000', '2024-12-31T23:00:00.000000000'],
shape=(350640,), dtype='datetime64[ns]')array(-16.6)
array(310.7)
CPU times: user 20.9 s, sys: 2.94 s, total: 23.8 s
Wall time: 6min 13s
<xarray.DataArray 't2m' (valid_time: 350640)> Size: 1MB
array([21.350006, 20.850006, 20.850006, ..., 23.350006, 22.975006,
22.131256], shape=(350640,), dtype=float32)
Coordinates:
* valid_time (valid_time) datetime64[ns] 3MB 1985-01-01 ... 2024-12-31T23:...
latitude float64 8B -16.6
longitude float64 8B 310.7
Attributes:
units: degC
long_name: Temperatura do ar a 2m em Goiâniaarray([21.350006, 20.850006, 20.850006, ..., 23.350006, 22.975006,
22.131256], shape=(350640,), dtype=float32)array(['1985-01-01T00:00:00.000000000', '1985-01-01T01:00:00.000000000',
'1985-01-01T02:00:00.000000000', ..., '2024-12-31T21:00:00.000000000',
'2024-12-31T22:00:00.000000000', '2024-12-31T23:00:00.000000000'],
shape=(350640,), dtype='datetime64[ns]')array(-16.6)
array(310.7)
Existe até um índice climático parecido com esse já batizado na literatura: noites tropicais (tropical nights), normalmente definidas como dias em que a temperatura mínima não cai abaixo de 20°C. Aqui usamos um limiar mais rígido, 24°C, pra marcar as noites de calor realmente extremo.
resample pra 1 valor de mínima por dia
<xarray.DataArray 't2m' (year: 40)> Size: 320B
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 2, 0, 0, 0, 0, 2, 0, 1, 0, 0,
4, 2, 0, 1, 0, 2, 0, 1, 2, 1, 3, 0, 5, 3, 2, 1, 6, 8])
Coordinates:
* year (year) int64 320B 1985 1986 1987 1988 ... 2021 2022 2023 2024
latitude float64 8B -16.6
longitude float64 8B 310.7
Attributes:
units: degC
long_name: Temperatura do ar a 2m em Goiâniaarray([0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 2, 0, 0, 0, 0, 2, 0, 1, 0, 0,
4, 2, 0, 1, 0, 2, 0, 1, 2, 1, 3, 0, 5, 3, 2, 1, 6, 8])array([1985, 1986, 1987, 1988, 1989, 1990, 1991, 1992, 1993, 1994, 1995, 1996,
1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008,
2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020,
2021, 2022, 2023, 2024])array(-16.6)
array(310.7)