TANGERANG SELATAN WEATHER

Sabtu, 16 Mei 2026

Memvisualisasikan Angin Permukaan dari ERA5

ERA5 menyimpan angin permukaan dalam dua komponen terpisah: u10 (komponen zonal, ke arah timur) dan v10 (komponen meridional, ke arah utara), keduanya pada ketinggian 10 meter di atas permukaan. Dari dua angka ini kita bisa menurunkan kecepatan angin maupun arah angin meteorologi — dan ketika kita agregasikan per musim, pola monsun Indonesia yang terkenal itu muncul dengan sendirinya dari data.

Tutorial ini memandu kita membaca file cached ERA5 2024 untuk wilayah Indonesia, menghitung kecepatan dan arah angin, lalu membandingkan musim DJF (Desember–Februari, musim hujan) dengan JJA (Juni–Agustus, musim kemarau). Tidak ada plotting — sandbox hanya menangkap stdout — tapi angka yang kita print cukup untuk melihat pembalikan angin monsun secara langsung.

Full-disk GeoColor view of Indonesia from Himawari-8 AHI showing rain clouds during the monsoon season close in March 2020 Sumber: NOAA NESDIS / JMA — Himawari-8 merekam awan hujan di atas Indonesia saat transisi musim monsun, Maret 2020. (halaman sumber)

Mengapa Angin Permukaan ERA5 Penting untuk Indonesia

Indonesia terletak di persimpangan dua sistem monsun besar: angin barat laut yang basah dari November hingga Maret dan angin tenggara yang kering dari April hingga Oktober. Pembalikan arah angin inilah yang menyebabkan perbedaan curah hujan dramatis antara musim hujan dan kemarau di hampir seluruh kepulauan.

ERA5 merekam pembalikan ini dengan sangat baik karena menggunakan data assimilation — menggabungkan pengamatan dari ribuan stasiun cuaca, radiosonde, satelit, dan pelampung laut ke dalam satu grid global yang konsisten secara fisika, menurut dokumentasi Copernicus Climate Change Service. Resolusi horizontal ERA5 sekitar 31 km (grid 0,25°), dan coverage temporalnya mencakup dari 1940 hingga mendekati real-time dengan lag sekitar 5 hari.

Komponen u10 dan v10 ERA5 adalah diagnostic fields: nilainya dihitung dari angin model pada blending height 40 meter, kemudian diekstrapolasi turun ke 10 meter menggunakan roughness length tetap sebesar 0,03 m (padang rumput pendek, sesuai konvensi WMO). Menurut dokumentasi ECMWF Forecast User Guide, karena roughness ini diasumsikan seragam, u10/v10 tidak merepresentasikan rata-rata grid-cell yang sebenarnya — di atas pegunungan Papua atau punggung Barisan Sumatera, angin 10 meter ERA5 bisa terlalu lemah karena orografi model dihaluskan ke resolusi 31 km.

Meski begitu, untuk laut terbuka dan dataran rendah, ERA5 u10/v10 adalah salah satu estimasi angin permukaan gridded terbaik yang tersedia.

Memuat Komponen Angin u10 dan v10

File cached untuk Indonesia sudah tersedia di /data/era5/ — tidak perlu autentikasi CDS atau cdsapi.retrieve(). Kita buka keduanya langsung dengan xarray.

import xarray as xr
import numpy as np

ds_u = xr.open_dataset("/data/era5/era5_u10_indonesia_2024_6h.nc")
ds_v = xr.open_dataset("/data/era5/era5_v10_indonesia_2024_6h.nc")

print("=== Dataset u10 ===")
print(ds_u)
print("\n=== Dataset v10 ===")
print(ds_v)
=== Dataset u10 ===
<xarray.Dataset> Size: 75MB
Dimensions:     (valid_time: 1464, latitude: 69, longitude: 185)
Coordinates:
  * valid_time  (valid_time) datetime64[ns] 12kB 2024-01-01 ... 2024-12-31T18...
    expver      (valid_time) <U4 23kB ...
  * 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:
    u10         (valid_time, latitude, longitude) float32 75MB ...
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:59 GRIB to CDM+CF via cfgrib-0.9.1...

=== Dataset v10 ===
<xarray.Dataset> Size: 75MB
Dimensions:     (valid_time: 1464, latitude: 69, longitude: 185)
Coordinates:
  * valid_time  (valid_time) datetime64[ns] 12kB 2024-01-01 ... 2024-12-31T18...
    expver      (valid_time) <U4 23kB ...
  * 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:
    v10         (valid_time, latitude, longitude) float32 75MB ...
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-10T04:02 GRIB to CDM+CF via cfgrib-0.9.1...

Output di atas menampilkan struktur xarray Dataset lengkap: dimensi time, latitude, longitude; koordinatnya; dan atribut metadata. Perhatikan bahwa variabel utama bernama u10 dan v10 (bukan U10 atau eastward_wind) — nama ini penting untuk langkah berikutnya.

Selanjutnya kita inspect koordinat secara lebih detail dan cetak sample nilai untuk memverifikasi data sudah terbaca dengan benar.

# Inspect dimensi dan koordinat
u10 = ds_u["u10"]
v10 = ds_v["v10"]

print("Dimensi u10:", dict(u10.sizes))
print("Latitude  :", float(u10.latitude.min().values), "...", float(u10.latitude.max().values),
      " | n =", u10.latitude.size)
print("Longitude :", float(u10.longitude.min().values), "...", float(u10.longitude.max().values),
      " | n =", u10.longitude.size)
print("Waktu     :", str(u10.valid_time.values[0])[:16], "...", str(u10.valid_time.values[-1])[:16],
      " | n =", u10.valid_time.size)
print("Unit u10  :", u10.attrs.get("units", "tidak tersedia"))
print("Unit v10  :", v10.attrs.get("units", "tidak tersedia"))

# Sample snapshot: titik tengah domain pada 2024-01-15 00:00 UTC
t_sample = "2024-01-15T00:00:00"
lat_mid = float(u10.latitude.values[u10.latitude.size // 2])
lon_mid = float(u10.longitude.values[u10.longitude.size // 2])

u_sample = float(u10.sel(valid_time=t_sample, latitude=lat_mid, longitude=lon_mid, method="nearest").values)
v_sample = float(v10.sel(valid_time=t_sample, latitude=lat_mid, longitude=lon_mid, method="nearest").values)
spd_sample = float(np.sqrt(u_sample**2 + v_sample**2))

print(f"\nSample @ {t_sample}, lat={lat_mid:.2f}, lon={lon_mid:.2f}")
print(f"  u10 = {u_sample:.3f} m/s")
print(f"  v10 = {v_sample:.3f} m/s")
print(f"  |V| = {spd_sample:.3f} m/s")
Dimensi u10: {'valid_time': 1464, 'latitude': 69, 'longitude': 185}
Latitude  : -11.0 ... 6.0  | n = 69
Longitude : 95.0 ... 141.0  | n = 185
Waktu     : 2024-01-01T00:00 ... 2024-12-31T18:00  | n = 1464
Unit u10  : m s**-1
Unit v10  : m s**-1

Sample @ 2024-01-15T00:00:00, lat=-2.50, lon=118.00
  u10 = 2.652 m/s
  v10 = -5.843 m/s
  |V| = 6.417 m/s

Dataset ini mencakup sekitar 1460 timestep (365 hari × 4 pengamatan per hari) pada ~70 titik latitude dan ~185 titik longitude — total sekitar 19 juta grid-point per variabel. xarray membuka file secara lazy sehingga data belum dimuat ke memori sampai kita benar-benar mengaksesnya.

Menghitung Kecepatan Angin dari Komponen u10 dan v10

Kecepatan angin \(|V|\) dihitung dari kedua komponen dengan formula Pythagoras standar:

$$|V| = \sqrt{u_{10}^2 + v_{10}^2}$$

Satu catatan penting yang sering diabaikan: rata-rata kecepatan angin tidak sama dengan kecepatan yang dihitung dari rata-rata komponen. Dengan kata lain, \(\overline{\sqrt{u^2 + v^2}} \neq \sqrt{\bar{u}^2 + \bar{v}^2}\) karena operasi akar kuadrat bersifat nonlinear. Urutan operasi ini penting: hitung kecepatan dari u dan v instantaneous terlebih dahulu, baru kemudian rata-ratakan hasilnya.

Kita hitung kecepatan untuk seluruh tahun 2024, lalu bandingkan statistik musiman DJF (Desember–Februari) dan JJA (Juni–Agustus).

import numpy as np

# Hitung kecepatan angin dari komponen instantaneous
speed = np.sqrt(u10**2 + v10**2)
speed.name = "wind_speed"
speed.attrs["units"] = "m/s"

# Seleksi musim berdasarkan bulan
months = speed.valid_time.dt.month

djf_mask = (months == 12) | (months == 1) | (months == 2)
jja_mask = (months == 6) | (months == 7) | (months == 8)

speed_djf = speed.isel(valid_time=djf_mask)
speed_jja = speed.isel(valid_time=jja_mask)

def print_stats(label, da):
    # Load ke numpy untuk komputasi percentile
    vals = da.values.ravel()
    vals = vals[~np.isnan(vals)]
    print(f"\n{label}  (n={len(vals):,} grid-point samples)")
    print(f"  Min    : {vals.min():.2f} m/s")
    print(f"  P10    : {np.percentile(vals, 10):.2f} m/s")
    print(f"  Median : {np.percentile(vals, 50):.2f} m/s")
    print(f"  Mean   : {vals.mean():.2f} m/s")
    print(f"  P90    : {np.percentile(vals, 90):.2f} m/s")
    print(f"  Max    : {vals.max():.2f} m/s")
    print(f"  Std    : {vals.std():.2f} m/s")

print("=== Statistik Kecepatan Angin 10m (ERA5 2024, Indonesia bbox) ===")
print_stats("DJF (Des–Feb, musim hujan)", speed_djf)
print_stats("JJA (Jun–Agu, musim kemarau)", speed_jja)

# Rata-rata spasial per musim (mean atas seluruh domain)
mean_djf = float(speed_djf.mean().values)
mean_jja = float(speed_jja.mean().values)
print(f"\nRata-rata domain DJF : {mean_djf:.2f} m/s")
print(f"Rata-rata domain JJA : {mean_jja:.2f} m/s")
=== Statistik Kecepatan Angin 10m (ERA5 2024, Indonesia bbox) ===

DJF (Des–Feb, musim hujan)  (n=4,646,460 grid-point samples)
  Min    : 0.00 m/s
  P10    : 0.92 m/s
  Median : 3.62 m/s
  Mean   : 3.99 m/s
  P90    : 7.65 m/s
  Max    : 18.61 m/s
  Std    : 2.59 m/s

JJA (Jun–Agu, musim kemarau)  (n=4,697,520 grid-point samples)
  Min    : 0.00 m/s
  P10    : 0.96 m/s
  Median : 3.99 m/s
  Mean   : 4.38 m/s
  P90    : 8.40 m/s
  Max    : 14.83 m/s
  Std    : 2.79 m/s

Rata-rata domain DJF : 3.99 m/s
Rata-rata domain JJA : 4.38 m/s

Dari output di atas, rata-rata domain JJA (4,38 m/s) sedikit lebih tinggi dibanding DJF (3,99 m/s) untuk tahun 2024. Kontribusi utama datang dari trade winds tenggara yang persisten di atas Laut Banda, Laut Arafura, dan perairan timur Indonesia — sektor yang luas dan terus berangin sepanjang JJA. DJF tetap berangin di Laut Cina Selatan akibat dorongan monsun barat laut, tetapi sebagian besar wilayah lain mengalami angin lebih lemah dan lebih variabel sehingga rata-rata domain ikut turun.

Menghitung Arah Angin Meteorologi

Konvensi meteorologi mendefinisikan arah angin sebagai arah dari mana angin bertiup (bukan ke mana ia pergi), dinyatakan dalam derajat searah jarum jam dari utara. Jadi angin utara = 0°, angin timur = 90°, angin selatan = 180°, angin barat = 270°.

Formula berdasarkan atan2 adalah:

$$\Phi = \left(180 + \frac{180}{\pi} \cdot \arctan2(u_{10},\, v_{10})\right) \bmod 360$$

Perhatikan urutan argumen: atan2(u, v) — bukan atan2(v, u) seperti dalam konvensi matematika standar. Ini sumber kesalahan paling umum saat menghitung arah angin meteorologi.

Kita kelompokkan angin ke empat kuadran: NE (0–90°), SE (90–180°), SW (180–270°), NW (270–360°), lalu hitung persentasenya per musim.

import numpy as np

# Hitung arah angin meteorologi
# atan2(u, v): argumen u dulu, lalu v — ini konvensi meteorologi
wdir = (180.0 + (180.0 / np.pi) * np.arctan2(u10.values, v10.values)) % 360.0

# Konversi ke xarray DataArray dengan koordinat yang sama
wdir_da = xr.DataArray(wdir, coords=u10.coords, dims=u10.dims, name="wind_dir",
                        attrs={"units": "degrees", "convention": "FROM, clockwise from north"})

months_da = wdir_da.valid_time.dt.month

djf_mask = (months_da == 12) | (months_da == 1) | (months_da == 2)
jja_mask = (months_da == 6) | (months_da == 7) | (months_da == 8)

wdir_djf = wdir_da.isel(valid_time=djf_mask).values.ravel()
wdir_jja = wdir_da.isel(valid_time=jja_mask).values.ravel()

def quadrant_stats(label, arr):
    arr = arr[~np.isnan(arr)]
    n = len(arr)
    ne = np.sum((arr >= 0) & (arr < 90)) / n * 100    # angin dari NE
    se = np.sum((arr >= 90) & (arr < 180)) / n * 100   # angin dari SE
    sw = np.sum((arr >= 180) & (arr < 270)) / n * 100  # angin dari SW
    nw = np.sum((arr >= 270) & (arr < 360)) / n * 100  # angin dari NW
    dominant = ["NE", "SE", "SW", "NW"][np.argmax([ne, se, sw, nw])]
    print(f"\n{label}  (n={n:,})")
    print(f"  Dari NE (0–90°)   : {ne:.1f}%")
    print(f"  Dari SE (90–180°) : {se:.1f}%")
    print(f"  Dari SW (180–270°): {sw:.1f}%")
    print(f"  Dari NW (270–360°): {nw:.1f}%")
    print(f"  Kuadran dominan   : {dominant}")

print("=== Distribusi Kuadran Arah Angin 10m (ERA5 2024, Indonesia bbox) ===")
quadrant_stats("DJF (Des–Feb, musim hujan)", wdir_djf)
quadrant_stats("JJA (Jun–Agu, musim kemarau)", wdir_jja)

# Distribusi arah angin setiap 45 derajat untuk resolusi lebih tinggi
print("\n--- Distribusi per sektor 45° ---")
sectors = ["N (337.5–22.5°)", "NE (22.5–67.5°)", "E (67.5–112.5°)", "SE (112.5–157.5°)",
           "S (157.5–202.5°)", "SW (202.5–247.5°)", "W (247.5–292.5°)", "NW (292.5–337.5°)"]
bounds = [(337.5, 22.5), (22.5, 67.5), (67.5, 112.5), (112.5, 157.5),
          (157.5, 202.5), (202.5, 247.5), (247.5, 292.5), (292.5, 337.5)]

for label_s, (lo, hi) in zip(sectors, bounds):
    if lo > hi:  # wrap around north
        djf_cnt = np.sum((wdir_djf >= lo) | (wdir_djf < hi)) / len(wdir_djf) * 100
        jja_cnt = np.sum((wdir_jja >= lo) | (wdir_jja < hi)) / len(wdir_jja) * 100
    else:
        djf_cnt = np.sum((wdir_djf >= lo) & (wdir_djf < hi)) / len(wdir_djf) * 100
        jja_cnt = np.sum((wdir_jja >= lo) & (wdir_jja < hi)) / len(wdir_jja) * 100
    print(f"  {label_s:<22} DJF={djf_cnt:.1f}%  JJA={jja_cnt:.1f}%")
=== Distribusi Kuadran Arah Angin 10m (ERA5 2024, Indonesia bbox) ===

DJF (Des–Feb, musim hujan)  (n=4,646,460)
  Dari NE (0–90°)   : 27.8%
  Dari SE (90–180°) : 10.0%
  Dari SW (180–270°): 18.0%
  Dari NW (270–360°): 44.2%
  Kuadran dominan   : NW

JJA (Jun–Agu, musim kemarau)  (n=4,697,520)
  Dari NE (0–90°)   : 11.3%
  Dari SE (90–180°) : 63.4%
  Dari SW (180–270°): 18.7%
  Dari NW (270–360°): 6.6%
  Kuadran dominan   : SE

--- Distribusi per sektor 45° ---
  N (337.5–22.5°)        DJF=13.6%  JJA=2.8%
  NE (22.5–67.5°)        DJF=16.6%  JJA=4.6%
  E (67.5–112.5°)        DJF=7.0%  JJA=21.2%
  SE (112.5–157.5°)      DJF=5.2%  JJA=39.6%
  S (157.5–202.5°)       DJF=4.1%  JJA=14.3%
  SW (202.5–247.5°)      DJF=7.3%  JJA=9.4%
  W (247.5–292.5°)       DJF=23.1%  JJA=5.1%
  NW (292.5–337.5°)      DJF=23.0%  JJA=3.1%

Output di atas mengkonfirmasi secara numerik pembalikan monsun yang kita harapkan: kuadran NW (angin dari barat laut) mendominasi DJF, sementara kuadran SE (angin dari tenggara) mendominasi JJA. Distribusi 45° memberikan gambaran yang lebih halus — misalnya komponen NE yang lebih kuat di JJA mencerminkan trade winds dari Pasifik di atas Kalimantan dan Sulawesi utara.

NASA SVS visualization showing the seasonal science of monsoons over the Asian and Southeast Asian region with wind and rainfall patterns annotated Sumber: NASA Scientific Visualization Studio, SVS #12303 (halaman sumber) — mekanisme monsun Asia Tenggara: kontras suhu darat-laut membalik arah aliran angin antara musim panas dan musim dingin belahan utara.

Interpretasi dan Konteks untuk Indonesia

Angka-angka dari snippet sebelumnya mencerminkan dua regime angin permukaan yang berbeda secara fundamental di wilayah Indonesia.

Selama DJF, antisiklon Siberia mendorong massa udara dingin dan kering ke selatan melalui Laut Cina Selatan. Ketika udara ini melintasi lautan tropis yang hangat, ia mengambil uap air dan tiba di Indonesia sebagai angin barat laut yang lembap — inilah pemicu utama musim hujan. Kecepatan angin 10 meter di atas laut terbuka (Laut Jawa, Selat Karimata, Laut Banda) umumnya berkisar antara 5–8 m/s; di selat antarapulau yang sempit bisa lebih tinggi akibat efek channeling. Di atas daratan, terutama dataran rendah Kalimantan dan Sumatera, kecepatan turun ke 2–4 m/s.

Selama JJA, pola berbalik. Tekanan tinggi Mascarene di selatan Samudera Hindia mendorong aliran tenggara melewati Australia utara dan Nusa Tenggara, lalu menyebar luas ke wilayah Indonesia timur dan tengah. Angin ini relatif kering — itulah sebab musim kemarau paling intens dirasakan di Jawa dan Nusa Tenggara. Trade winds yang persisten di atas Laut Banda dan Laut Arafura cenderung lebih konstan dibanding monsun barat laut DJF yang lebih sporadis, sehingga rata-rata domain JJA dalam data 2024 ini justru sedikit lebih tinggi dibanding DJF.

Beberapa catatan penting saat menginterpretasikan ERA5 u10/v10:

  • Orografi ERA5 dihaluskan ke resolusi 31 km. Di daerah pegunungan (Pegunungan Jayawijaya Papua, punggung Barisan Sumatera), u10 dan v10 ERA5 bisa terlalu lemah dan tidak merepresentasikan variasi lokal yang sesungguhnya.
  • Karena roughness length yang digunakan tetap 0,03 m (padang rumput pendek, bukan hutan tropis atau kota), kecepatan angin di atas vegetasi lebat atau area urban mungkin diperkirakan terlalu tinggi.
  • Konvensi arah angin meteorologi berlawanan dengan konvensi vektor matematika. Selalu verifikasi: angin utara dalam konvensi meteorologi berarti bertiup dari utara ke selatan, sehingga u10 bernilai negatif dan v10 bernilai negatif.
  • Untuk studi angin ekstrem (badai, squall), data 6-hourly yang kita gunakan akan meremehkan puncak kecepatan angin. ERA5 tersedia dalam resolusi hourly melalui CDS jika diperlukan.

Pola monsun yang teridentifikasi dari data ERA5 2024 ini konsisten dengan dokumentasi NOAA NESDIS mengenai monsun Indonesia dan visualisasi ilmiah NASA SVS tentang mekanisme monsun Asia Tenggara yang didorong oleh kontras suhu darat-laut.

Langkah Berikutnya dan Pengembangan Lanjutan

Tutorial ini hanya menggunakan data tahun 2024. Untuk analisis klimatologis yang lebih kuat — misalnya anomali angin terkait ENSO atau tren jangka panjang — kita bisa menambahkan lebih banyak tahun ke cache dengan menjalankan bin/era5-fetch.sh setelah menambahkan parameter tahun di helpers/era5_fetch.py.

Beberapa ekstensi yang layak dicoba berikutnya:

  • Bulanan vs musiman: menghitung rata-rata bulanan (bukan hanya DJF/JJA) akan memperlihatkan transisi monsun bulan per bulan secara lebih halus, terutama periode peralihan April–Mei dan Oktober–November.
  • Menggabungkan variabel lain: ERA5 cached juga menyediakan suhu udara 2 m (era5_t2m_*), tekanan permukaan laut (era5_msl_*), dan curah hujan (era5_tp_*) — semuanya bisa digabungkan dengan u10/v10 untuk analisis komposit.
  • Verifikasi dengan data observasi: membandingkan u10/v10 ERA5 dengan data scatterometer (ASCAT, QuikSCAT) atau BMKG open data memberikan gambaran seberapa baik reanalysis mereproduksi kondisi aktual.
  • Resolusi hourly: untuk studi angin di area tertentu (misalnya potensi energi angin lepas pantai), ERA5 hourly yang diunduh via CDS akan memberikan variasi diurnal yang lebih lengkap.

Eksplorasi artikel meteorologi lainnya di meteo.my.id — mulai dari analisis ENSO, pemrosesan data BMKG, hingga tutorial NWP dengan xarray di https://meteo.my.id.

Referensi

Tidak ada komentar:

Posting Komentar