Senin, 06 Juli 2026
Mengapa Dask untuk Data ERA5?
ERA5 adalah reanalisis generasi kelima ECMWF yang menyediakan data atmosfer per jam sejak tahun 1940 hingga kini, mencakup grid global 0,25°×0,25° dengan 137 level vertikal. Arsip globalnya mencapai ratusan petabyte — jauh melampaui kapasitas RAM server manapun. Bahkan subset regional Indonesia yang kita gunakan di tutorial ini (69 lintang × 185 bujur, 1464 time step 6-hourly sepanjang 2024) menghasilkan file float32 sekitar 75 MB per variabel. Angka itu memang masih muat di RAM, tapi teknik yang kita pelajari di sini berskala identik untuk kasus yang jauh lebih besar: 10 tahun data, 5 variabel, resolusi 0,1° — kita langsung bicara ratusan gigabyte.
Di sinilah dask masuk. Dengan satu argumen tambahan chunks= saat memanggil xr.open_dataset(), xarray mengubah array NumPy biasa menjadi dask array yang bersifat lazy. Operasi seperti .mean(), .groupby(), atau .quantile() tidak langsung dieksekusi — melainkan direkam sebagai task graph. Eksekusi baru terjadi saat kita memanggil .compute(), dan saat itulah dask memproses chunk demi chunk secara paralel menggunakan thread pool lokal.
Tutorial ini menutup siklus penuh: download ERA5 sekali lewat cdsapi, buka sebagai dask array, eksplorasi struktur chunk, bangun komputasi lazy, hitung rata-rata bulanan dan persentil ke-95 dalam °C, lalu visualisasikan hasilnya sebagai peta.
Mengunduh dan Membuka Data ERA5 dengan Chunking
Sebelum menjalankan snippet pertama, pastikan kamu sudah punya akun di cds.climate.copernicus.eu dan file ~/.cdsapirc berisi API key-nya. Download hanya dilakukan satu kali; snippet di bawah memeriksa apakah file sudah ada sebelum menyentuh CDS.
import os, cdsapi, xarray as xr
OUT = "era5_t2m_indonesia_2024_6h.nc"
if not os.path.exists(OUT):
c = cdsapi.Client(quiet=True)
c.retrieve(
"reanalysis-era5-single-levels",
{
"product_type": "reanalysis",
"variable": ["2m_temperature"],
"year": "2024",
"month": [f"{m:02d}" for m in range(1, 13)],
"day": [f"{d:02d}" for d in range(1, 32)],
"time": ["00:00", "06:00", "12:00", "18:00"],
"area": [6, 95, -11, 141], # N, W, S, E — bbox Indonesia
"format": "netcdf",
},
OUT,
)
ds = xr.open_dataset(OUT, chunks={"valid_time": 200})
if "valid_time" in ds.dims:
ds = ds.rename({"valid_time": "time"})
tvar = "t2m" if "t2m" in ds else list(ds.data_vars)[0]
print(ds)
print("\nTipe data array :", type(ds[tvar].data))
print("Chunks :", ds[tvar].chunks)
<xarray.Dataset> Size: 75MB
Dimensions: (time: 1464, latitude: 69, longitude: 185)
Coordinates:
* time (time) datetime64[ns] 12kB 2024-01-01 ... 2024-12-31T18:00:00
expver (time) <U4 23kB dask.array<chunksize=(200,), meta=np.ndarray>
* latitude (latitude) float64 552B 6.0 5.75 5.5 5.25 ... -10.5 -10.75 -11.0
* longitude (longitude) float64 1kB 95.0 95.25 95.5 ... 140.5 140.8 141.0
number int64 8B ...
Data variables:
t2m (time, latitude, longitude) float32 75MB dask.array<chunksize=(200, 35, 93), meta=np.ndarray>
Attributes:
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-05-10T03:51 GRIB to CDM+CF via cfgrib-0.9.1...
Tipe data array : <class 'dask.array.core.Array'>
Chunks : ((200, 200, 200, 200, 200, 200, 200, 64), (35, 34), (93, 92))
Perhatikan dua hal dari output di atas. Pertama, xr.open_dataset dengan chunks={"valid_time": 200} mengembalikan dataset yang dimensi waktunya dibagi menjadi blok-blok 200 time step — belum ada data yang dibaca dari disk. Kedua, type(ds[tvar].data) menampilkan dask.array.core.Array, bukan NumPy. Dataset ini sepenuhnya lazy: xarray hanya membaca metadata file dan menyusun rencana chunk, bukan isinya.
Memahami Chunk Size dan Performa
Sebelum membangun komputasi, penting untuk memahami geometri chunk yang kita pilih.
import numpy as np
chunks = ds[tvar].chunks # tuple-of-tuples: (time_chunks, lat_chunks, lon_chunks)
n_chunks = [len(c) for c in chunks]
# Gunakan chunk pertama sebagai representatif
chunk_shape = tuple(c[0] for c in chunks)
chunk_mb = np.prod(chunk_shape) * 4 / 1e6 # float32 = 4 byte per elemen
total_tasks = np.prod(n_chunks)
total_mb = np.prod(ds[tvar].shape) * 4 / 1e6
print(f"Dimensi array : {ds[tvar].dims}")
print(f"Shape total : {ds[tvar].shape}")
print(f"Chunk shape (awal): {chunk_shape}")
print(f"Jumlah chunk/dim : {n_chunks} → total {total_tasks} tasks")
print(f"MB per chunk : {chunk_mb:.1f} MB")
print(f"Total array size : ~{total_mb:.0f} MB (muat di RAM)")
Dimensi array : ('time', 'latitude', 'longitude')
Shape total : (1464, 69, 185)
Chunk shape (awal): (200, 35, 93)
Jumlah chunk/dim : [8, 2, 2] → total 32 tasks
MB per chunk : 2.6 MB
Total array size : ~75 MB (muat di RAM)
Dengan chunks={"valid_time": 200}, kita mendapat sekitar 8 chunk di dimensi waktu dan masing-masing satu chunk di lat dan lon — total 8 tasks untuk seluruh array 75 MB. Ukuran per chunk sekitar 10 MB, yang memang lebih kecil dari rentang ideal 100 MB–1 GB. Untuk dataset kecil seperti ini, itu tidak masalah; kita pakai angka kecil supaya struktur chunk mudah diperiksa secara manual.
Yang perlu dihindari adalah chunk terlalu kecil. Jika kita menulis chunks={"valid_time": 1}, hasilnya 1464 tasks masing-masing sekitar 0,05 MB. Overhead scheduler dask sekitar 1 ms per task, artinya 1464 tasks menambahkan lebih dari 1 detik latency murni — sebelum satu byte data diproses. Panduan resmi dask tegas: chunk di bawah 1 MB hampir selalu buruk.
Aturan praktis untuk perencanaan memori:
$$\text{RAM yang dikonsumsi} \approx \text{ukuran chunk} \times \text{thread aktif} \times 2$$
Dengan chunk 100 MB dan 8 core aktif, dask berpotensi mengonsumsi hingga 1,6 GB RAM secara bersamaan. Rencanakan chunk size sesuai kapasitas mesin yang kamu gunakan.
Membangun Komputasi Lazy dan Trigger Compute
Inti dask adalah perbedaan antara lazy dan eager. Saat kita menulis ds[tvar].groupby("time.month").mean(), dask tidak langsung berhitung — ia hanya menyusun task graph, yaitu struktur data yang merekam operasi apa yang perlu dikerjakan pada chunk mana, dan dalam urutan apa. Hasilnya masih berupa dask.array. Komputasi nyata baru terjadi saat kita memanggil .compute().
Alur kerja dask: dari open_dataset lazy hingga numpy result setelah .compute().
lazy_monthly = ds[tvar].groupby("time.month").mean()
print("Sebelum compute:", type(lazy_monthly.data))
monthly = lazy_monthly.compute()
print("Setelah compute :", type(monthly.data))
print()
# Tampilkan rata-rata spasial per bulan dalam °C
for m in range(1, 13):
t_c = float(monthly.sel(month=m).mean()) - 273.15
print(f" Bulan {m:2d}: {t_c:.2f} °C (rata-rata spasial Indonesia)")
Sebelum compute: <class 'dask.array.core.Array'>
Setelah compute : <class 'numpy.ndarray'>
Bulan 1: 27.32 °C (rata-rata spasial Indonesia)
Bulan 2: 27.44 °C (rata-rata spasial Indonesia)
Bulan 3: 27.56 °C (rata-rata spasial Indonesia)
Bulan 4: 27.76 °C (rata-rata spasial Indonesia)
Bulan 5: 27.77 °C (rata-rata spasial Indonesia)
Bulan 6: 27.13 °C (rata-rata spasial Indonesia)
Bulan 7: 26.90 °C (rata-rata spasial Indonesia)
Bulan 8: 26.66 °C (rata-rata spasial Indonesia)
Bulan 9: 27.19 °C (rata-rata spasial Indonesia)
Bulan 10: 27.39 °C (rata-rata spasial Indonesia)
Bulan 11: 27.34 °C (rata-rata spasial Indonesia)
Bulan 12: 27.27 °C (rata-rata spasial Indonesia)
Setelah .compute(), monthly kini bertipe DataArray dengan backing NumPy — bukan dask lagi. Nilainya mencerminkan rata-rata suhu 2 m di seluruh domain Indonesia, termasuk lautan yang mendominasi grid. Dokumentasi xarray merekomendasikan .compute() yang mengembalikan objek baru daripada .load() yang mengubah data di tempat — kebiasaan yang lebih aman saat kita ingin mempertahankan versi lazy aslinya.
Operasi Reduksi dan Persentil Lintas Waktu
Dask tidak hanya berguna untuk mean. Operasi seperti quantile() yang secara intuitif membutuhkan pengurutan seluruh distribusi waktu tetap bisa dieksekusi chunk-per-chunk. Di bawah tenda, dask menggunakan pendekatan aproksimasi yang memproses setiap chunk secara independen lalu menggabungkan hasilnya.
import numpy as np
lazy_q95 = ds[tvar].quantile(0.95, dim="time")
print("Tipe sebelum compute:", type(lazy_q95.data))
q95 = lazy_q95.compute()
q95_c = q95.values - 273.15 # konversi ke °C
print(f"\nPersentil ke-95 suhu 2 m sepanjang 2024:")
print(f" Min spasial : {q95_c.min():.2f} °C")
print(f" Max spasial : {q95_c.max():.2f} °C")
print(f" Mean spasial: {q95_c.mean():.2f} °C")
Tipe sebelum compute: <class 'dask.array.core.Array'>
Persentil ke-95 suhu 2 m sepanjang 2024:
Min spasial : 16.31 °C
Max spasial : 35.52 °C
Mean spasial: 29.56 °C
Field persentil ke-95 mewakili ambang atas suhu harian di setiap titik grid sepanjang 2024 — secara kasar, suhu pada hari-hari terpanas. Nilai max spasial yang muncul berasal dari daerah dataran rendah atau perairan dangkal dengan intensitas panas tertinggi. Komputasi ini dikerjakan dask dengan memproses setiap chunk secara independen, persis seperti saat kita hitung mean sebelumnya.
Visualisasi Hasil dan Interpretasi
Terakhir, kita hitung field suhu rata-rata tahunan dengan .compute(), lalu plot sebagai peta filled-contour di atas domain Indonesia menggunakan cartopy.
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import cartopy.feature as cfeature
import numpy as np
annual_mean = ds[tvar].mean("time").compute() - 273.15 # °C
fig, ax = plt.subplots(
figsize=(12, 6),
subplot_kw={"projection": ccrs.PlateCarree()}
)
cf = ax.contourf(
annual_mean.longitude,
annual_mean.latitude,
annual_mean.values,
levels=20,
cmap="RdYlBu_r",
transform=ccrs.PlateCarree()
)
ax.add_feature(cfeature.COASTLINE, linewidth=0.8)
ax.add_feature(cfeature.BORDERS, linewidth=0.5, linestyle="--")
gl = ax.gridlines(draw_labels=True, linewidth=0.4, color="gray", alpha=0.6)
gl.top_labels = False
gl.right_labels = False
ax.set_extent([95, 141, -11, 6], crs=ccrs.PlateCarree())
ax.set_title(
"Suhu 2 m Rata-rata Tahunan 2024 — ERA5, Indonesia",
fontsize=13, pad=10
)
plt.colorbar(cf, ax=ax, label="Suhu rata-rata tahunan (°C)", shrink=0.75, pad=0.02)
plt.tight_layout()
plt.savefig("snippet-5.png", dpi=120, bbox_inches="tight")
plt.close()
print("Peta suhu rata-rata tahunan tersimpan: snippet-5.png")
Peta di atas menunjukkan gradien suhu yang konsisten dengan geografi Indonesia: pesisir laut dangkal dan dataran rendah Sumatera-Kalimantan cenderung lebih hangat (sekitar 27–29 °C), sementara pegunungan Papua dan Sulawesi tengah lebih sejuk karena elevasi. Warna lebih gelap di barat laut mencerminkan Laut Andaman dan Laut Cina Selatan yang relatif lebih dingin dibanding perairan tropis dalam. Pola ini sesuai dengan ekspektasi klimatologis ERA5 di wilayah tropis maritim.
Optimasi Chunk Size dan Pemrosesan Multi-File
Tutorial ini menggunakan satu file dan chunk sederhana di dimensi waktu. Untuk kasus yang lebih besar, dua teknik lanjutan sangat relevan.
Multi-file paralel. xarray menyediakan xr.open_mfdataset("era5_*.nc", parallel=True) yang membuka banyak file NetCDF secara paralel menggunakan dask.delayed. Secara default setiap file menjadi satu chunk, membuat pipeline multi-tahun atau multi-variabel jauh lebih efisien.
Tuning chunk size. Gunakan aturan \(\text{chunk size} \times \text{thread aktif} \times 2 \leq \text{RAM tersedia}\) sebagai batas atas. Untuk array 3D besar berbasis float64, dask docs menyarankan shape sekitar (100, 400, 400) elemen. Pastikan juga batas chunk selaras dengan chunk on-disk agar setiap chunk dask merupakan kelipatan bulat dari chunk penyimpanan — ini menghindari pembacaan ganda dari disk.
Untuk distributed computing (multi-node), eksplorasi dask.distributed dan Client() — tapi itu topik tersendiri. Eksplorasi artikel meteorologi lainnya di meteo.my.id untuk tutorial data iklim lanjutan seperti open_mfdataset multi-tahun dan analisis anomali skala besar. Mulai dari meteo.my.id.
Referensi
- Copernicus Climate Data Store — ERA5 Single Levels Overview — Dokumentasi resmi ERA5: reanalisis generasi kelima ECMWF, data per jam sejak 1940, resolusi 0,25°×0,25°, 137 level vertikal.
- xarray Documentation — Parallel Computing with Dask — Panduan resmi integrasi xarray dengan dask: argumen
chunks=,.compute()vs.load(), danopen_mfdataset(parallel=True). - Dask Blog — Choosing Good Chunk Sizes in Dask — Panduan praktis pemilihan ukuran chunk: rentang 100 MB–1 GB direkomendasikan, di bawah 1 MB hampir selalu buruk, dan jumlah chunk di atas 100.000 dapat menurunkan performa scheduler.
- Dask Documentation — Array Best Practices — Aturan praktis dask array termasuk hubungan ukuran chunk dengan konsumsi memori dan cara menyelaraskan chunk dengan chunk storage on-disk.