Τα δεδομένα για το κλίμα έρχονται συχνά στην επικαιρότητα λόγω σημαντικών μετεωρολογικών φαινομένων. Μπορείτε να αναλύσετε τα στοιχεία του Copernicus εύκολα με Python, χωρίς να χρειαστεί να εγκαταστήσετε λογισμικό ή να κατεβάσετε αρχεία στον υπολογιστή σας.
Το ευρωπαϊκό καλοκαίρι του 2026 κατέρριψε προηγούμενα μετεωρολογικά ρεκόρ, με πολλαπλά κύματα καύσωνα και επίμονη ξηρασία. Πόλεις της Δυτικής Ευρώπης κατέγραψαν θερμοκρασίες άνω των 40 βαθμών Κελσίου, με τη Γαλλία να έρχεται αντιμέτωπη με 4 κύματα θερμών εισβολών, για συνολικό διάστημα 45 ημερών, σύμφωνα με την κλιματική υπηρεσία του Παρισιού. Τα καταπράσινα πάρκα του Λονδίνου θύμιζαν περισσότερο εικόνες από μεσογειακή χώρα, καθώς η ξηρασία κατέκαψε τη χλωρίδα, αυξάνοντας τις δασικές πυρκαγιές σε «επίπεδα ρεκόρ» στην Αγγλία και την Ουαλία, ενώ στη Γαλλία και την Ισπανία οι πυρκαγιές κατέκαψαν εκατοντάδες χιλιάδες στρέμματα.
Η αύξηση της θερμοκρασίας του πλανήτη αποτυπώνεται στα δεδομένα του Copernicus ERA5, με την υπηρεσία να παρέχει ιστορικά στοιχεία, από το 1940 και εφεξής, και να αποτελεί μια σημαντική πηγή για τα κλιματικά δεδομένα. To ERA5 αποτελεί την πέμπτη γενιά «reanalysis» («επανανάλυσης») του ερευνητικού ινστιτούτου ECMWF (European Centre for Medium-Range Weather Forecasts) και, χρησιμοποιώντας τους νόμους της φυσικής, συνδυάζει μοντέλα δεδομένων με πραγματικές καταγραφές μετεωρολογικών σταθμών σε ένα ενιαίο, παγκόσμιο dataset.
Όπως σημειώνεται στην ιστοσελίδα του Copernicus ERA5, αυτή η αρχή, που ονομάζεται «αφομοίωση δεδομένων», βασίζεται στη μέθοδο που χρησιμοποιείται από τα κέντρα πρόγνωσης καιρού, όπου ανά τακτά χρονικά διαστήματα (κάθε 12 ώρες στο ECMWF) μια προηγούμενη πρόγνωση συνδυάζεται με νέες διαθέσιμες παρατηρήσεις για την παραγωγή μιας νέας, βελτιωμένης εκτίμησης της κατάστασης της ατμόσφαιρας. Αυτή η τελευταία ονομάζεται ανάλυση, και από αυτήν εκδίδεται μια ενημερωμένη, βελτιωμένη πρόγνωση. Η «reanalysis» λειτουργεί με τον ίδιο τρόπο αλλά με χαμηλότερη ακρίβεια, καθιστώντας δυνατή την παροχή ενός συνόλου ιστορικών δεδομένων.
Πρόσβαση στα δεδομένα μέσω κώδικα

To Copernicus Climate Data Store παρέχει ανοιχτά δεδομένα για πολλές κατηγορίες ζητημάτων, από πιο «γενικής κατανάλωσης», όπως η θερμοκρασία και οι βροχοπτώσεις, έως πιο ειδικευμένες, όπως η ταχύτητα του πάγου στην Ανταρκτική και τη Γροιλανδία, και η υγρασία στην ανώτερη τροπόσφαιρα. Η πρόσβαση στα δεδομένα μπορεί να γίνει με την πιο συμβατική μέθοδο της ανάκτησης των στοιχείων μέσω της φόρμας που διαθέτει η υπηρεσία, κάνοντας διάφορες επιλογές που συγκεκριμενοποιούν τις πληροφορίες οι οποίες θα ενταχθούν στο τελικό αρχείο.
Ωστόσο, η προσέγγιση που ακολουθήσαμε στο παρόν άρθρο ήταν διαφορετική: χρησιμοποιήσαμε τα δεδομένα ARCO (Analysis Ready Cloud Optimized), μια repackaged εκδοχή των αρχικών δεδομένων του ERA5 η οποία παρέχει άμεση προγραμματιστική πρόσβαση, μέσω API, σε μια –πιο περιορισμένη– επιλογή από dataset, χωρίς να είναι απαραίτητο να τα κατεβάσουμε στη συσκευή μας, καθιστώντας έτσι ευκολότερη την πρόσβαση σε μεγάλο εύρος στοιχείων.
Για να έχουμε πρόσβαση στα δεδομένα αυτά, απαιτείται να έχουμε δημιουργήσει έναν –δωρεάν– λογαριασμό στο Copernicus, ώστε να διαθέτουμε κλειδί.
Στη δική μας περίπτωση, επιλέξαμε να αναλύσουμε τα δεδομένα στο Google Colab, μια ανοιχτή πλατφόρμα επεξεργασίας κώδικα Python που βρίσκεται στο cloud και «τρέχει» στον browser, χωρίς να απαιτείται η εγκατάσταση περαιτέρω λογισμικού.
Στον κώδικα Python που συντάξαμε χρησιμοποιήσαμε τα δεδομένα του ERA5, το οποίο διαθέτει κλιματικά στοιχεία σε γεωχωρική ανάλυση πλέγματος περίπου 31 χιλιομέτρων. Επιλέχθηκε το συγκεκριμένο dataset και όχι το ανάλογο ERA5-Land, που διέθετε πιο λεπτομερή ανάλυση σε πλέγμα 9 χιλιομέτρων, λόγω της συχνής ανάγκης να οπτικοποιούνται περιοχές με έντονη ακτογραμμή (νησιά, χερσόνησοι).
Οι παράμετροι που μπορούμε να επιλέξουμε κυμαίνονται από τον άνεμο και την πίεση, στη θερμοκρασία και τον υετό (το νερό που πέφτει συνολικά ως βροχή, χαλάζι ή χιόνι), ενώ ανάμεσα στις επιλογές είναι και η θερμοκρασία της θάλασσας. Στο παρόν άρθρο, εστιάσαμε στον εντοπισμό των δεδομένων θερμοκρασίας και υετού, καθώς αυτές οι δύο κατηγορίες μάς επηρεάζουν πιο άμεσα στην καθημερινότητά μας.
| Όνομα | Μονάδα μέτρησης | Όνομα μεταβλητής | Σημείωση |
| 10 metre U wind component | m s-1 | u10 | |
| 100 metre U wind component | m s-1 | u100 | |
| 10 metre V wind component | m s-1 | v10 | |
| 100 metre V wind component | m s-1 | v100 | |
| 10m wind gust since previous post-processing | m s-1 | fg10 | |
| 2 metre dewpoint temperature | K | d2m | |
| 2 metre temperature | K | t2m | |
| Mean sea level pressure | Pa | msl | |
| Sea surface temperature | K | sst | Sea surface temperature is available at the usual times, eg hourly, but its content is only updated once daily. |
| Skin temperature | K | skt | |
| Surface pressure | Pa | sp | |
| Surface latent heat flux | J m-2 | slhf | |
| Surface sensible heat flux | J m-2 | sshf | |
| Surface solar radiation downwards | J m-2 | ssrd | |
| Surface thermal radiation downwards | J m-2 | strd | |
| Total precipitation | m | tp | |
| Total cloud cover | (0 – 1) | tcc | |
| Total sky direct solar radiation at surface | J m-2 | fdir | |
| Cloud base height | m | cbh | |
| Boundary layer height | m | blh |
Διαλέξαμε, επίσης, τη γεωχωρική (geochunked) εκδοχή του συνόλου δεδομένου, καθώς η προσέγγισή μας είναι να εντοπίσουμε τα δεδομένα σύμφωνα με την τοποθεσία, είτε είναι πόλη είτε χώρα. Έπειτα χρησιμοποιήσαμε το κλειδί API για να λάβουμε πρόσβαση στα στοιχεία. Το κλειδί μπορεί να εντοπιστεί στο προφίλ σας στη σελίδα του Copernicus.
Στον παρακάτω κώδικα, έχουμε διαλέξει ως παράδειγμα τη Γερμανία και τη μέση θερμοκρασία κάθε έτους από το 1970 έως το 2025. Τα σύνορα της χώρας εντοπίζονται μέσω των ανοιχτών δεδομένων του OpenStreetMaps, και οπτικοποιούνται σε διαδραστικό χάρτη όπου ο χρήστης μπορεί να διαλέξει το έτος που επιθυμεί να δει.
Δεν είναι, όμως, απαραίτητο να περιοριστούμε στο παρόν παράδειγμα. Με λίγες αλλαγές στις βασικές μεταβλητές μπορείτε να αλλάξετε τις παραμέτρους και να αναλύσετε διαφορετικά στοιχεία.
Κώδικας χαρτογράφησης μέσης ετήσιας θερμοκρασίας στη Γερμανία από το 1970
# –––––––––– SETTINGS - Change the main parameters here ––––––––––––––
variable_name = "t2m" # the variable that we want to use
location = "Germany" # the country/region that we want to fetch (if not found, check the proper name in OpenStreetMaps)
function = "mean" # Options: "mean", "max", "min"
months_list = [] # Insert months of interest
months = months_list or list(range(1, 13)) # If 'months_list' is empty, it defaults to range(1, 13) covering all 12 months
time_range = range(1970, 2026) # years of interest
cdsapi_key = "" # Insert your key
# –––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
# 0. import libraries
!pip install -q osmnx regionmask flox zarr aiohttp
import os, json
import numpy as np, xarray as xr, geopandas as gpd
import osmnx as ox, regionmask, shapely
import plotly.express as px
from google.colab import userdata
geochunked_url = "https://arco.datastores.ecmwf.int/cadl-arco-geo-002/arco/reanalysis_era5_single_levels/sfc/geoChunked.zarr"
ds = xr.open_zarr(
geochunked_url,
consolidated=True,
storage_options={"headers": {"Authorization": f"Bearer {cdsapi_key}"}},
)
# 1. Boundary
gdf = ox.geocode_to_gdf(location).to_crs("EPSG:4326")
x0, y0, x1, y1 = gdf.total_bounds
# 2. Aggregate per year
cache = f"{location}_{variable_name}_{function}.nc"
if os.path.exists(cache):
ann = xr.open_dataarray(cache)
else:
da = ds[variable_name].sel(
latitude=slice(y1, y0) if ds.latitude[0] > ds.latitude[-1] else slice(y0, y1),
longitude=slice(x0, x1),
time=slice(str(time_range[0]), str(time_range[-1])),
)
if months_list: # skip the filter when all months are used
da = da.sel(time=da.time.dt.month.isin(months))
ann = getattr(da.groupby("time.year"), function)().compute() - 273.15
ann.to_netcdf(cache)
# 3. Mask to boundary
mask = regionmask.mask_geopandas(gdf, ann.longitude, ann.latitude)
df = ann.where(mask.notnull()).to_dataframe("temp_c").reset_index().dropna(subset=["temp_c"])
df["temp_c"] = df["temp_c"].round(1)
# 4. Grid cells, clipped to boundary
dy = np.diff(np.unique(df.latitude)).min() / 2
dx = np.diff(np.unique(df.longitude)).min() / 2
cells = df[["latitude", "longitude"]].drop_duplicates().reset_index(drop=True)
cells["cell_id"] = cells.index.astype(str)
grid = gpd.GeoDataFrame(
cells,
geometry=shapely.box(cells.longitude - dx, cells.latitude - dy,
cells.longitude + dx, cells.latitude + dy),
crs="EPSG:4326",
)
grid = gpd.clip(grid, gdf)
data = (df.merge(cells, on=["latitude", "longitude"])[["cell_id", "year", "temp_c"]]
.query("cell_id in @grid.cell_id")
.sort_values("year"))
data["year"] = data["year"].astype(str)
# 5. Compact geojson + view that fits all polygons
g = grid[["cell_id", "geometry"]].copy()
g["geometry"] = shapely.set_precision(g.geometry.values, 1e-4)
geo = json.loads(g.set_index("cell_id").to_json())
bx0, by0, bx1, by1 = grid.total_bounds
lat_c = (by0 + by1) / 2
zoom = min(np.log2(1000 / 512 * 360 / (bx1 - bx0)),
np.log2(650 / 512 * 180 / ((by1 - by0) / np.cos(np.radians(lat_c))))) - 0.2
# 6. Figure
unit = function.capitalize()
fig = px.choropleth_map(
data, geojson=geo, locations="cell_id", color="temp_c", animation_frame="year",
color_continuous_scale="RdBu_r",
range_color=[df.temp_c.min() - 5, df.temp_c.max() + 5],
map_style="carto-positron", zoom=zoom,
center={"lat": lat_c, "lon": (bx0 + bx1) / 2},
opacity=0.5,
labels={"temp_c": f"{unit} Temp (°C)"},
title=f"{location}: {unit} Temperature by Year (ERA5)",
)
fig.update_traces(marker_line_width=0)
fig.update_layout(margin={"r": 0, "t": 40, "l": 0, "b": 0}, height=650)
for fr in fig.frames:
for t in fr.data:
t.geojson = None
OUT = f"/content/{location.lower()}_era5.html"
fig.write_html(OUT, include_plotlyjs="cdn", full_html=True, auto_play=False)
fig.show()
Όπως φαίνεται και στην οπτικοποίηση (1970-2025), οι υψηλότερες θερμοκρασίες γίνονται ολοένα και συχνότερες όσο πλησιάζουμε στο 2025, με πολλές περιοχές να σημειώνουν μέσες ετήσιες θερμοκρασίες άνω των 10 βαθμών Κελσίου, σύμφωνα με το ERA5.
Χαρτογράφηση της μέσης ετήσιας θερμοκρασίας στη Γερμανία από το 1970 έως το 2025
Πέρα από το επίπεδο χώρας, πολιτείας ή περιφέρειας, τα δεδομένα μπορούν να δώσουν εικόνα για το κλιματικό μοτίβο και σε μικρότερο γεωγραφικό επίπεδο, όπως μια συγκεκριμένη αστική περιοχή. Έτσι, στον παρακάτω κώδικα-παράδειγμα, χρησιμοποιήσαμε μια λίστα από τυχαίες πόλεις και τη χώρα/πολιτεία στην οποία βρίσκονται και οπτικοποιήσαμε τη μέση θερμοκρασία που έχει καταγραφεί τους καλοκαιρινούς μήνες (Ιούνιος-Ιούλιος-Αύγουστος) σε αυτές.
Εντοπισμός μέσης θερμοκρασίας καλοκαιρινών μηνών σε διάφορες πόλεις του πλανήτη
# —————————————————————————————————————————————————————
# SETTINGS — everything a user should change
variable_name = "t2m" # ERA5 variable, e.g. "t2m", "tp"
function = "mean" # "mean", "max", "min", "sum", "median", "std"
months_list = [6, 7, 8] # e.g. [6, 7, 8] for summer only
months = months_list or list(range(1, 13)) # if empty, then gets full year
time_range = range(1970, 2026) # years of interest
cities = { # city and country/state to find
"Athens": "Greece",
"Belgrade": "Serbia",
"Bologna": "Italy",
"Burgas": "Bulgaria",
"Topeka": "Kansas"
}
valid_functions = ["mean", "max", "min", "sum", "median", "std"]
function_labels = {
"mean": "Mean", "max": "Maximum", "min": "Minimum",
"sum": "Total", "median": "Median", "std": "Std. dev. of",
}
cdsapi_key = "" # insert your key
# —————————————————————————————————————————————————————
# SETUP: install, import, open the ARCO store
!pip install -q zarr fsspec aiohttp requests plotly xarray
import importlib
import requests
import numpy as np
import pandas as pd
import xarray as xr
import plotly.graph_objects as go
from google.colab import userdata
importlib.invalidate_caches()
xr.backends.plugins.list_engines.cache_clear()
assert "zarr" in xr.backends.list_engines(), "zarr backend still missing, restart the runtime once"
geochunked_url = "https://arco.datastores.ecmwf.int/cadl-arco-geo-002/arco/reanalysis_era5_single_levels/sfc/geoChunked.zarr"
ds = xr.open_zarr(
geochunked_url,
consolidated=True,
storage_options={"headers": {"Authorization": f"Bearer {cdsapi_key}"}},
)
# Unit conversion per variable: (function applied to raw value, display unit)
unit_conversions = {
"t2m": (lambda x: x - 273.15, "°C"),
"2m_dewpoint_temperature": (lambda x: x - 273.15, "°C"),
"skin_temperature": (lambda x: x - 273.15, "°C"),
"sea_surface_temperature": (lambda x: x - 273.15, "°C"),
"tp": (lambda x: x * 1000, "mm"), # metres -> mm
}
convert_fn, unit_label = unit_conversions.get(variable_name, (lambda x: x, ""))
# Validation
if function not in valid_functions:
raise ValueError(f"function must be one of: {valid_functions}")
if not months:
raise ValueError("At least one month must be selected.")
if any(m < 1 or m > 12 for m in months):
raise ValueError("months_list must contain values between 1 and 12.")
# OPENSTREETMAP
def geocode_city(city, country):
"""Find city coordinates using OpenStreetMap/Nominatim."""
response = requests.get(
"https://nominatim.openstreetmap.org/search",
params={"q": f"{city}, {country}", "format": "jsonv2", "limit": 1},
headers={"User-Agent": "ERA5-city-analysis/1.0"},
timeout=10,
)
response.raise_for_status()
results = response.json()
if not results:
raise ValueError(f"Could not find {city}, {country} using OpenStreetMap.")
return float(results[0]["lat"]), float(results[0]["lon"])
def normalize_longitude(lon):
"""Convert longitude to the convention used by ds (-180..180 or 0..360)."""
ds_min_lon = float(ds["longitude"].min())
ds_max_lon = float(ds["longitude"].max())
if ds_min_lon >= 0 and ds_max_lon > 180:
return lon % 360
return ((lon + 180) % 360) - 180
# Find complete calendar years
times = pd.to_datetime(ds["time"].values)
time_df = pd.DataFrame({"time": times})
time_df["year"] = time_df["time"].dt.year
year_start = time_df.groupby("year")["time"].min()
year_end = time_df.groupby("year")["time"].max()
complete_years = [
year for year in sorted(time_df["year"].unique())
if year_start.loc[year].normalize() <= pd.Timestamp(year=year, month=1, day=1)
and year_end.loc[year].normalize() >= pd.Timestamp(year=year, month=12, day=31)
]
selected_years = [y for y in complete_years if y in time_range]
if not selected_years:
raise ValueError("No complete calendar years found within the selected time_range.")
# Per city
city_results = {}
for city_name, country in cities.items():
target_lat, target_lon = geocode_city(city_name, country)
dataset_lon = normalize_longitude(target_lon)
series = ds[variable_name].sel(latitude=target_lat, longitude=dataset_lon, method="nearest")
df = series.to_dataframe(name=variable_name).reset_index()
df["year"] = df["time"].dt.year
df["month"] = df["time"].dt.month
df = df[df["year"].isin(selected_years) & df["month"].isin(months)]
annual = df.groupby("year")[variable_name].agg(function).reset_index()
annual["value"] = convert_fn(annual[variable_name])
city_results[city_name] = {"country": country, "annual": annual[["year", "value"]]}
# INTERACTIVE CHART — annual values per city
function_label = function_labels[function]
period_label = "All months" if months == list(range(1, 13)) else "Months: " + ", ".join(str(m) for m in months)
fig = go.Figure()
for city_name, r in city_results.items():
annual_df = r["annual"]
fig.add_trace(go.Scatter(
x=annual_df["year"], y=annual_df["value"], mode="lines", name=city_name,
hovertemplate=f"Year: %{{x}}<br>{city_name}: %{{y:.2f}}{unit_label}<extra></extra>",
))
fig.update_layout(
title=f"{function_label} annual {variable_name} ({unit_label}) — ERA5<br><sup>{period_label}</sup>",
xaxis_title="Year",
yaxis_title=f"{variable_name} ({unit_label})",
template="plotly_white",
height=500,
)
fig.show()
Με λίγες αλλαγές στις μεταβλητές μπορούμε να εντοπίσουμε και τον υετό κάθε έτους για κάθε περιοχή. Στον παραπάνω κώδικα αλλάζουμε τρεις μεταβλητές:
- Το variable_name = από «t2m» σε «tp»
- Το function = «mean» σε «sum», για να λάβουμε τον συνολικό όγκο νερού που έχει πέσει ετησίως από βροχή, χαλάζι, χιόνι
- Αφήνουμε κενή τη λίστα μηνών για να λάβουμε όλους τους μήνες του έτους

Σημειωτέον, στον κώδικα η επιλογή των συντεταγμένων γίνεται αυτόματα μέσω της ονομασίας της πόλης και της χώρας/πολιτείας. Για να επαληθεύσουμε ότι εντοπίσαμε δεδομένα πράγματι για τις περιοχές που θέλαμε, οπτικοποιήσαμε σε χάρτη τις συντεταγμένες των περιοχών για τις οποίες πήραμε ιστορικά στοιχεία νωρίτερα.

Oλόκληρος ο κώδικας που γράφτηκε και χρησιμοποιήθηκε για τους σκοπούς του παρόντος είναι διαθέσιμος εδώ. Σε περίπτωση που επιθυμείτε απλώς να εξερευνήσετε τα δεδομένα και όχι να τα αναλύσετε, πιθανώς θα σας φανεί χρήσιμο το ERA Explorer, ένα διαδικτυακό εργαλείο του Copernicus που οπτικοποιεί τα ιστορικά στοιχεία για κάθε περιοχή του πλανήτη.
