TANGERANG SELATAN WEATHER

Jumat, 19 Juni 2026

Menghitung Kelembaban Spesifik dari ERA5

Relative humidity mudah dipahami, tapi punya satu kelemahan besar: nilainya berubah hanya karena suhu udara naik — bahkan tanpa ada penambahan atau pengurangan uap air sama sekali. Itu membuat RH kurang ideal sebagai tracer kelembaban ketika udara bergerak secara adiabatik. Specific humidity (\(q\), satuan kg/kg) tidak punya masalah itu. Nilai \(q\) konstan selama parsel udara naik atau turun secara adiabatik tanpa perubahan fase, sehingga \(q\) menjadi variabel pilihan untuk analisis moisture budget, trajectory, dan profil kelembaban vertikal.

ERA5 menyediakan \(q\) langsung di pressure level (500, 850 hPa, dst.), tapi di permukaan ceritanya berbeda. ERA5 tidak mengarsipkan specific humidity permukaan secara langsung — yang tersedia adalah 2m dewpoint temperature (d2m) dan surface pressure (sp). Dari dua variabel itu, kita bisa menurunkan \(q\) permukaan menggunakan persamaan termodinamika standar.

Tutorial ini berjalan dari dua arah: kita buka file \(q\) dari pressure level, lalu kita derivasi \(q\) permukaan dari d2m dan MSLP, kemudian bandingkan keduanya di titik Jakarta dan buat plot temporal.

Latar gradien biru-hijau yang melambangkan analisis kelembaban atmosfer

Memuat Data ERA5

Kita mulai dengan mengunduh dua file ERA5 dari CDS. File pertama berisi \(q\) pada pressure level 500 dan 850 hPa — ini sudah dalam satuan kg/kg, tidak perlu derivasi. File kedua berisi d2m dan MSLP untuk derivasi \(q\) permukaan di bagian berikutnya.

Untuk mengakses CDS, kita perlu akun di cds.climate.copernicus.eu dan file konfigurasi ~/.cdsapirc yang berisi API key. Setelah itu, snippet di bawah akan mengunduh file sekali; jalankan ulang dan cache guard (if not os.path.exists(OUT)) akan melewati download yang sudah ada.

import os
import cdsapi
import xarray as xr

# --- pressure-level specific humidity (500 & 850 hPa, daily 00Z, 2024) ---
OUT_Q = "era5_q_pl500-850_indonesia_2024_d.nc"
if not os.path.exists(OUT_Q):
    c = cdsapi.Client(quiet=True)
    c.retrieve(
        "reanalysis-era5-pressure-levels",
        {
            "product_type": "reanalysis",
            "variable": ["specific_humidity"],
            "pressure_level": ["500", "850"],
            "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"],
            "area":  [6, 95, -11, 141],   # N, W, S, E — Indonesia
            "format": "netcdf",
        },
        OUT_Q,
    )

ds_q = xr.open_dataset(OUT_Q)
print("Pressure-level q dataset:")
print(ds_q)
print("\nDimensions:", dict(ds_q.dims))
print("Variables:", list(ds_q.data_vars))
print("q units:", ds_q["q"].attrs.get("units", "tidak tercantum"))
Pressure-level q dataset:
<xarray.Dataset> Size: 37MB
Dimensions:         (valid_time: 366, pressure_level: 2, latitude: 69,
                     longitude: 185)
Coordinates:
  * valid_time      (valid_time) datetime64[ns] 3kB 2024-01-01 ... 2024-12-31
    expver          (valid_time) <U4 6kB ...
  * pressure_level  (pressure_level) float64 16B 850.0 500.0
  * latitude        (latitude) float64 552B 6.0 5.75 5.5 ... -10.5 -10.75 -11.0
  * longitude       (longitude) float64 1kB 95.0 95.25 95.5 ... 140.8 141.0
    number          int64 8B ...
Data variables:
    q               (valid_time, pressure_level, latitude, longitude) float32 37MB ...
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:16 GRIB to CDM+CF via cfgrib-0.9.1...

Dimensions: {'valid_time': 366, 'pressure_level': 2, 'latitude': 69, 'longitude': 185}
Variables: ['q']
q units: kg kg**-1

ERA5 menyimpan \(q\) pada pressure level dalam satuan kg/kg, dengan 37 level tersedia dari 1 hPa hingga 1000 hPa. Kita hanya meminta 500 dan 850 hPa karena itu rentang paling relevan untuk analisis lapisan troposfer bawah-tengah di wilayah tropis.

Sekarang kita unduh file surface — 2m dewpoint temperature dan mean sea-level pressure (MSLP). Keduanya akan digunakan di bagian berikutnya untuk menurunkan \(q\) permukaan.

# --- surface variables: d2m + MSLP (6-hourly, 2024) ---
OUT_D2M = "era5_d2m_indonesia_2024_6h.nc"
if not os.path.exists(OUT_D2M):
    c = cdsapi.Client(quiet=True)
    c.retrieve(
        "reanalysis-era5-single-levels",
        {
            "product_type": "reanalysis",
            "variable": ["2m_dewpoint_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],
            "format": "netcdf",
        },
        OUT_D2M,
    )

OUT_MSL = "era5_msl_indonesia_2024_6h.nc"
if not os.path.exists(OUT_MSL):
    c = cdsapi.Client(quiet=True)
    c.retrieve(
        "reanalysis-era5-single-levels",
        {
            "product_type": "reanalysis",
            "variable": ["mean_sea_level_pressure"],
            "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],
            "format": "netcdf",
        },
        OUT_MSL,
    )

ds_d2m = xr.open_dataset(OUT_D2M)
ds_msl = xr.open_dataset(OUT_MSL)

print("d2m dataset:")
print(ds_d2m)
print("\nmsl dataset:")
print(ds_msl)
print("\nd2m units:", ds_d2m["d2m"].attrs.get("units", "tidak tercantum"))
print("msl units:", ds_msl["msl"].attrs.get("units", "tidak tercantum"))
d2m dataset:
<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:
    d2m         (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:06 GRIB to CDM+CF via cfgrib-0.9.1...

msl dataset:
<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:
    msl         (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:54 GRIB to CDM+CF via cfgrib-0.9.1...

d2m units: K
msl units: Pa

ERA5 menyimpan d2m dalam Kelvin dan MSLP dalam Pascal. Kita akan mengonversi keduanya ke satuan yang sesuai sebelum memasukkannya ke formula. Catatan penting: kita menggunakan MSLP sebagai proxy tekanan permukaan. Untuk wilayah dataran rendah seperti sebagian besar Jakarta dan pesisir Indonesia, pendekatan ini cukup baik karena perbedaan antara MSLP dan surface pressure di wilayah datar sangat kecil. Untuk analisis di wilayah pegunungan, sebaiknya gunakan surface pressure (sp) yang tersedia terpisah di ERA5.

Menurunkan Kelembaban Spesifik di Permukaan

Karena ERA5 tidak mengarsipkan specific humidity permukaan secara langsung, kita harus menurunkannya dari d2m dan tekanan menggunakan dua persamaan.

Langkah pertama: hitung saturation vapor pressure \(e_s\) dari dewpoint temperature \(T_d\) menggunakan formula Bolton (1980):

$$e_s(T_d) = 6{,}112 \cdot \exp\!\left(\frac{17{,}67 \, T_{d,C}}{T_{d,C} + 243{,}5}\right)$$

di mana \(T_{d,C}\) adalah suhu dewpoint dalam °C dan \(e_s\) dalam hPa. Pada suhu dewpoint sekitar 25°C (khas wilayah pesisir tropis Indonesia), formula ini menghasilkan \(e_s \approx 31{,}7\) hPa.

Langkah kedua: gunakan nilai \(e_s\) ini sebagai estimasi tekanan parsial uap air aktual \(e \approx e_s(T_d)\) — karena di titik dewpoint, udara tepat jenuh sehingga tekanan uap aktual sama dengan \(e_s\). Kemudian masukkan ke formula specific humidity:

$$q = \frac{\varepsilon \, e}{p - (1 - \varepsilon)\, e}, \quad \varepsilon \approx 0{,}622$$

di mana \(p\) adalah tekanan total atmosfer (hPa) dan \(\varepsilon = R_d / R_v \approx 0{,}622\) adalah rasio konstanta gas udara kering terhadap uap air. Nilai eksak yang digunakan dalam IFS ERA5 adalah \(\varepsilon = 0{,}621981\). Formula ini menghasilkan \(q\) dalam kg/kg.

Persamaan ini merupakan formula standar AMS Glossary yang juga digunakan secara internal di model IFS ECMWF. Perhatikan bahwa formula ini sedikit berbeda dari pendekatan sederhana \(q \approx \varepsilon e / p\) — perbedaannya kecil (orde 1–2%) tapi lebih akurat.

import numpy as np

# --- konstanta ---
EPSILON = 0.622   # R_d / R_v

def bolton_es(t_k):
    """Saturation vapor pressure (hPa) menggunakan formula Bolton 1980.
    Input: suhu dalam Kelvin."""
    t_c = t_k - 273.15
    return 6.112 * np.exp(17.67 * t_c / (t_c + 243.5))

def specific_humidity(es_hpa, p_hpa):
    """Specific humidity (kg/kg) dari vapor pressure dan tekanan total, keduanya dalam hPa."""
    return EPSILON * es_hpa / (p_hpa - (1 - EPSILON) * es_hpa)

# --- terapkan ke seluruh grid ---
# Beberapa file ERA5 dari CDS menggunakan koordinat "valid_time" alih-alih "time".
# Standarkan namanya sehingga kode di bawah tetap konsisten.
if "valid_time" in ds_d2m.coords:
    ds_d2m = ds_d2m.rename({"valid_time": "time"})
if "valid_time" in ds_msl.coords:
    ds_msl = ds_msl.rename({"valid_time": "time"})

d2m_k  = ds_d2m["d2m"]                    # Kelvin
msl_pa = ds_msl["msl"]                    # Pascal
msl_hpa = msl_pa / 100.0                  # konversi ke hPa

es_2m = bolton_es(d2m_k)                  # hPa, shape: (time, lat, lon)
q_2m  = specific_humidity(es_2m, msl_hpa) # kg/kg

# Beri atribut yang rapi
q_2m = q_2m.assign_attrs(units="kg/kg", long_name="2m specific humidity (derived)")
q_2m.name = "q_2m"

# --- cek satu timestep: nilai spasial rerata ---
q_sample = float(q_2m.isel(time=0).mean())
es_sample = float(es_2m.isel(time=0).mean())
msl_sample = float(msl_hpa.isel(time=0).mean())
d2m_c_sample = float((d2m_k.isel(time=0) - 273.15).mean())

print(f"Timestep 0 — rata-rata spasial Indonesia:")
print(f"  d2m         = {d2m_c_sample:.2f} °C")
print(f"  e_s(d2m)    = {es_sample:.2f} hPa")
print(f"  MSLP        = {msl_sample:.2f} hPa")
print(f"  q_2m        = {q_sample*1000:.3f} g/kg")
print(f"\nBentuk array q_2m: {q_2m.shape}")
print(f"Rentang nilai:  min={float(q_2m.min())*1000:.2f} g/kg, max={float(q_2m.max())*1000:.2f} g/kg")
Timestep 0 — rata-rata spasial Indonesia:
  d2m         = 24.38 °C
  e_s(d2m)    = 30.57 hPa
  MSLP        = 1011.21 hPa
  q_2m        = 19.024 g/kg

Bentuk array q_2m: (1464, 69, 185)
Rentang nilai:  min=5.19 g/kg, max=26.11 g/kg

Hasilnya menunjukkan \(q\) permukaan rata-rata spasial Indonesia. Nilai yang wajar untuk wilayah tropis basah berada di kisaran 16–20 g/kg di permukaan — lebih tinggi di atas laut dan lebih bervariasi di daratan.

Membandingkan Permukaan dan 850 hPa

Setelah punya \(q\) permukaan dari derivasi, mari kita bandingkan dengan \(q\) di 850 hPa dari dataset pressure level. Kita pilih titik Jakarta (\(-6{,}2°\) LS, \(106{,}85°\) BT) dan hitung rata-rata bulanan untuk melihat pola musiman.

import pandas as pd

# Titik Jakarta
LAT_JKT = -6.2
LON_JKT = 106.85

# Standarkan nama koordinat waktu jika file ERA5 menggunakan "valid_time".
if "valid_time" in ds_q.coords:
    ds_q = ds_q.rename({"valid_time": "time"})

# --- q_850 dari pressure-level dataset ---
q_850 = ds_q["q"].sel(pressure_level=850, method="nearest")

# Pilih titik grid terdekat Jakarta
q_850_jkt = q_850.sel(latitude=LAT_JKT, longitude=LON_JKT, method="nearest")
q_2m_jkt  = q_2m.sel(latitude=LAT_JKT, longitude=LON_JKT, method="nearest")

# Rerata bulanan
q_850_monthly = q_850_jkt.resample(time="1ME").mean()
q_2m_monthly  = q_2m_jkt.resample(time="1ME").mean()

# Tampilkan tabel per bulan
months = ["Jan","Feb","Mar","Apr","Mei","Jun","Jul","Agu","Sep","Okt","Nov","Des"]
print(f"{'Bulan':<6} {'q_2m (g/kg)':>12} {'q_850 (g/kg)':>14} {'Selisih (g/kg)':>15}")
print("-" * 50)
for i in range(len(q_850_monthly.time)):
    q2 = float(q_2m_monthly.isel(time=i)) * 1000
    q8 = float(q_850_monthly.isel(time=i)) * 1000
    print(f"{months[i]:<6} {q2:>12.2f} {q8:>14.2f} {(q2 - q8):>15.2f}")

print()
print(f"Rata-rata tahunan q_2m  : {float(q_2m_monthly.mean())*1000:.2f} g/kg")
print(f"Rata-rata tahunan q_850 : {float(q_850_monthly.mean())*1000:.2f} g/kg")
print(f"Gradien vertikal rata-rata: {(float(q_2m_monthly.mean()) - float(q_850_monthly.mean()))*1000:.2f} g/kg")
Bulan   q_2m (g/kg)   q_850 (g/kg)  Selisih (g/kg)
--------------------------------------------------
Jan           18.77          12.87            5.89
Feb           19.01          12.77            6.24
Mar           18.89          12.82            6.07
Apr           19.68          13.25            6.44
Mei           19.33          12.63            6.70
Jun           18.59          12.20            6.40
Jul           17.03          11.19            5.85
Agu           16.93          10.80            6.12
Sep           17.15          11.49            5.66
Okt           17.52          11.41            6.12
Nov           18.77          12.27            6.50
Des           18.47          12.75            5.73

Rata-rata tahunan q_2m  : 18.35 g/kg
Rata-rata tahunan q_850 : 12.20 g/kg
Gradien vertikal rata-rata: 6.14 g/kg

Output di atas menunjukkan pola yang konsisten dengan fisika atmosfer tropis: \(q\) di permukaan selalu lebih tinggi daripada di 850 hPa karena kandungan uap air berkurang seiring ketinggian (peningkatan tekanan uap dikompensasi oleh penurunan tekanan total, tapi profil kelembaban aktual turun tajam). Nilai absolut \(q\) permukaan tertinggi memang muncul pada musim basah (Desember–Maret), tetapi selisih vertikal permukaan-minus-850 hPa justru relatif datar sepanjang tahun, berkisar 5,7–6,7 g/kg, dengan nilai terbesar pada April–Mei serta November dan terendah pada Desember dan September. Pola ini mengindikasikan bahwa profil kelembaban 850 hPa di Jakarta mengikuti fluktuasi permukaan cukup dekat, sehingga gradien tidak melebar tajam saat musim basah.

Visualisasi Kelembaban Vertikal

Perbandingan tabel berguna, tapi plot timeseries memperlihatkan struktur temporal yang lebih kaya — termasuk variabilitas intraseasonal yang mungkin terkait dengan MJO maupun siklus diurnal yang teraverage ke skala bulanan.

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.dates as mdates

fig, ax = plt.subplots(figsize=(12, 5))

# Konversi ke g/kg untuk tampilan yang lebih intuitif
times = pd.to_datetime(q_2m_monthly.time.values)
y_2m  = q_2m_monthly.values * 1000
y_850 = q_850_monthly.values * 1000

ax.plot(times, y_2m,  color="#1565C0", linewidth=2.0,
        marker="o", markersize=5, label="$q$ permukaan (2 m, derivasi Bolton)")
ax.plot(times, y_850, color="#E64A19", linewidth=2.0,
        marker="s", markersize=5, label="$q$ 850 hPa (ERA5 langsung)")

ax.fill_between(times, y_850, y_2m, alpha=0.12, color="#1565C0",
                label="Selisih vertikal")

ax.set_title("Kelembaban Spesifik Jakarta 2024 — Permukaan vs 850 hPa",
             fontsize=13, fontweight="bold", pad=12)
ax.set_xlabel("Bulan", fontsize=11)
ax.set_ylabel("Specific humidity (g kg$^{-1}$)", fontsize=11)
ax.xaxis.set_major_formatter(mdates.DateFormatter("%b"))
ax.xaxis.set_major_locator(mdates.MonthLocator())
ax.grid(True, linestyle="--", alpha=0.5)
ax.legend(fontsize=10)
ax.set_xlim(times[0], times[-1])

# Anotasi musim basah
ax.axvspan(times[0], times[2],  alpha=0.07, color="blue", label="_DJF")
ax.axvspan(times[11], times[-1], alpha=0.07, color="blue")
ax.text(times[1], ax.get_ylim()[0] + 0.3, "DJF", color="blue", fontsize=9, alpha=0.7)

plt.tight_layout()
plt.savefig("snippet-5-1.png", dpi=150, bbox_inches="tight")
print("Plot disimpan ke snippet-5-1.png")

Grafik deret waktu yang membandingkan kelembaban spesifik di 2 m dan 850 hPa di Jakarta sepanjang tahun 2024, menunjukkan pola musiman dan gradien vertikal kelembaban

Plot memperlihatkan separasi vertikal yang konsisten antara \(q\) permukaan dan \(q\) 850 hPa sepanjang tahun 2024. Pola musiman terlihat jelas: nilai \(q\) lebih tinggi pada bulan-bulan musim basah (Desember–Maret) ketika adveksi uap air dari Samudra Hindia dan Samudra Pasifik meningkat bersamaan dengan konveksi yang lebih aktif. Area biru transparan di antara kedua kurva menggambarkan gradien kelembaban vertikal — semakin lebar area ini, semakin besar penurunan kelembaban dari permukaan ke 850 hPa.

Penutup

Tutorial ini menunjukkan dua jalur untuk mendapatkan specific humidity dari ERA5. Jalur pertama adalah pembacaan langsung: untuk pressure level (500, 850 hPa, dst.), ERA5 sudah menyediakan \(q\) dalam kg/kg sehingga kita cukup memuat file dan memilih dimensi yang diinginkan. Jalur kedua adalah derivasi: untuk permukaan, ERA5 menyimpan d2m dan sp (atau MSLP sebagai pendekatan), lalu kita terapkan formula Bolton untuk \(e_s\) dan formula standard AMS untuk \(q\).

Nilai \(q\) sangat berguna karena bersifat konservatif dalam proses adiabatik — artinya, jika kita mengikuti parsel udara yang naik secara adiabatik tanpa kondensasi, nilai \(q\) tidak berubah. Sifat ini menjadikan \(q\) variabel tracer yang baik untuk analisis moisture budget monsun, trajectory analisis massa udara, dan perbandingan profil vertikal kelembaban antar wilayah. Ketika analisis MJO melibatkan budget uap air, \(q\) pada berbagai pressure level menjadi kuantitas kunci.

Langkah lanjutan yang bisa dicoba: perluas analisis ke grid spasial penuh dan buat peta rata-rata \(q\) per musim, bandingkan \(q\) ERA5 dengan produk satellite moisture seperti MODIS Total Precipitable Water, atau gunakan profil \(q\) sebagai input dalam perhitungan CAPE dan CIN. Coba juga ekstraksi titik yang berbeda di wilayah pegunungan seperti Bandung atau Manado untuk melihat bagaimana topografi mempengaruhi profil kelembaban vertikal, dan bandingkan hasilnya dengan mixing ratio jenuh untuk analisis stabilitas.

Eksplorasi artikel meteorologi lainnya di meteo.my.id melalui tautan resmi di https://meteo.my.id.

Referensi

Tidak ada komentar:

Posting Komentar