Data Understanding (NO₂ Medan)#
Studi Kasus: Konsentrasi NO₂ di Kota Medan#
1.1 Sumber Data#
Data konsentrasi gas Nitrogen Dioksida (NO₂) diperoleh dari platform Copernicus Data Space Ecosystem (CDSE) melalui layanan OpenEO API pada alamat openeo.dataspace.copernicus.eu.
Data diambil dari produk citra satelit Sentinel-5P yang menyediakan informasi kualitas udara berbasis pengamatan atmosfer.
Secara umum, alur pengambilan data dilakukan dengan langkah-langkah berikut:
Koleksi Data:
SENTINEL_5P_L2, yaitu produk Level-2 dari satelit Sentinel-5P yang berisi pengukuran konsentrasi berbagai gas atmosfer, termasuk NO₂.Rentang Waktu: Data diunduh untuk periode 20 September 2024 hingga 20 Oktober 2024 sebagai contoh periode pengamatan selama satu bulan.
Wilayah Pengamatan (Area of Interest / AOI): Ditentukan dalam bentuk poligon (GeoJSON) yang mencakup wilayah Kota Medan, dengan koordinat geografis sekitar longitude 98.48825704511182 dan latitude 3.7073584868121117.
Band Data yang Digunakan:
NO₂— merepresentasikan kolom atau kanal pengamatan konsentrasi Nitrogen Dioksida.
1.2 Proses Pengambilan dan Agregasi Data#
Tahapan pengambilan data dilakukan melalui skrip Python menggunakan OpenEO dengan pipeline sebagai berikut:
Penyaringan Nilai Tidak Valid: Nilai NO₂ yang bersifat negatif atau tidak logis ditandai sebagai invalid dan dikeluarkan dari proses perhitungan.
Agregasi Temporal: Data diolah menggunakan fungsi
aggregate_temporal_period(period="day", reducer="mean")untuk menghasilkan rata-rata harian dari pengamatan citra satelit selama periode yang dipilih.Agregasi Spasial: Fungsi
aggregate_spatial(geometries=aoi, reducer="mean")digunakan untuk menghitung rata-rata spasial nilai NO₂ di seluruh area poligon Kota Medan.Ekspor Data: Hasil pengolahan disimpan dalam format CSV melalui proses batch export agar mudah digunakan untuk analisis lanjutan (misalnya untuk preprocessing dan modeling).
1.3 Area Pengamatan#
Wilayah analisis mencakup area Kota Medan, Sumatera Utara, yang d idefinisikan melalui GeoJSON polygon sebagai batas wilayah pengamatan. Area ini dipilih karena merupakan salah satu kota besar dengan kepadatan lalu lintas dan aktivitas industri yang cukup tinggi, sehingga cocok dijadikan lokasi studi kasus pemantauan kualitas udara berbasis citra satelit.
Import Library python yang digunakan#
import openeo
import pandas as pd
import matplotlib.pyplot as plt
import os
!pip install openeo
Requirement already satisfied: openeo in /usr/local/lib/python3.12/dist-packages (0.45.0)
Requirement already satisfied: requests>=2.26.0 in /usr/local/lib/python3.12/dist-packages (from openeo) (2.32.4)
Requirement already satisfied: urllib3>=1.9.0 in /usr/local/lib/python3.12/dist-packages (from openeo) (2.5.0)
Requirement already satisfied: shapely>=1.6.4 in /usr/local/lib/python3.12/dist-packages (from openeo) (2.1.2)
Requirement already satisfied: numpy>=1.17.0 in /usr/local/lib/python3.12/dist-packages (from openeo) (2.0.2)
Requirement already satisfied: xarray<2025.01.2,>=0.12.3 in /usr/local/lib/python3.12/dist-packages (from openeo) (2025.1.1)
Requirement already satisfied: pandas>0.20.0 in /usr/local/lib/python3.12/dist-packages (from openeo) (2.2.2)
Requirement already satisfied: pystac>=1.5.0 in /usr/local/lib/python3.12/dist-packages (from openeo) (1.14.1)
Requirement already satisfied: deprecated>=1.2.12 in /usr/local/lib/python3.12/dist-packages (from openeo) (1.2.18)
Requirement already satisfied: wrapt<2,>=1.10 in /usr/local/lib/python3.12/dist-packages (from deprecated>=1.2.12->openeo) (1.17.3)
Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.12/dist-packages (from pandas>0.20.0->openeo) (2.9.0.post0)
Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.12/dist-packages (from pandas>0.20.0->openeo) (2025.2)
Requirement already satisfied: tzdata>=2022.7 in /usr/local/lib/python3.12/dist-packages (from pandas>0.20.0->openeo) (2025.2)
Requirement already satisfied: charset_normalizer<4,>=2 in /usr/local/lib/python3.12/dist-packages (from requests>=2.26.0->openeo) (3.4.4)
Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.12/dist-packages (from requests>=2.26.0->openeo) (3.11)
Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.12/dist-packages (from requests>=2.26.0->openeo) (2025.10.5)
Requirement already satisfied: packaging>=23.2 in /usr/local/lib/python3.12/dist-packages (from xarray<2025.01.2,>=0.12.3->openeo) (25.0)
Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.12/dist-packages (from python-dateutil>=2.8.2->pandas>0.20.0->openeo) (1.17.0)
Koneksi ke Copernicus Data Space#
connection = openeo.connect("openeo.dataspace.copernicus.eu").authenticate_oidc()
Authenticated using refresh token.
Scraping Data#
aoi = {
"type": "Polygon",
"coordinates": [
[
[98.48825704511182, 3.7073584868121117],
[98.48825704511182, 3.424509844415482],
[98.842944131384, 3.424509844415482],
[98.842944131384, 3.7073584868121117],
[98.48825704511182, 3.7073584868121117],
]
],
}
s5p = connection.load_collection(
"SENTINEL_5P_L2",
spatial_extent={
"west": 98.48825704511182,
"south": 3.424509844415482,
"east": 98.842944131384,
"north": 3.7073584868121117,
},
temporal_extent=["2024-10-02", "2025-11-01"],
bands=["NO2"],
)
# 4. Mask nilai negatif (data invalid)
def mask_invalid(x):
return x < 0
s5p_masked = s5p.mask(s5p.apply(mask_invalid))
# 5. Agregasi temporal harian
daily_mean = s5p_masked.aggregate_temporal_period(period="day", reducer="mean")
# 6. Agregasi spasial (rata-rata dalam AOI)
daily_mean_aoi = daily_mean.aggregate_spatial(geometries=aoi, reducer="mean")
# 7. Jalankan batch job dan hasilkan file CSV
job = daily_mean_aoi.execute_batch(out_format="CSV",)
# 8. Unduh hasil job
results = job.get_results()
results.download_files("no2_results")
0:00:00 Job 'j-25102910020342ffb40536626dc1ef06': send 'start'
0:00:13 Job 'j-25102910020342ffb40536626dc1ef06': queued (progress 0%)
0:00:18 Job 'j-25102910020342ffb40536626dc1ef06': queued (progress 0%)
0:00:24 Job 'j-25102910020342ffb40536626dc1ef06': queued (progress 0%)
0:00:32 Job 'j-25102910020342ffb40536626dc1ef06': queued (progress 0%)
0:00:42 Job 'j-25102910020342ffb40536626dc1ef06': queued (progress 0%)
0:00:55 Job 'j-25102910020342ffb40536626dc1ef06': queued (progress 0%)
0:01:10 Job 'j-25102910020342ffb40536626dc1ef06': running (progress N/A)
0:01:30 Job 'j-25102910020342ffb40536626dc1ef06': running (progress N/A)
0:01:54 Job 'j-25102910020342ffb40536626dc1ef06': running (progress N/A)
0:02:24 Job 'j-25102910020342ffb40536626dc1ef06': running (progress N/A)
0:03:01 Job 'j-25102910020342ffb40536626dc1ef06': running (progress N/A)
0:03:48 Job 'j-25102910020342ffb40536626dc1ef06': running (progress N/A)
0:04:46 Job 'j-25102910020342ffb40536626dc1ef06': running (progress N/A)
0:05:47 Job 'j-25102910020342ffb40536626dc1ef06': running (progress N/A)
0:06:47 Job 'j-25102910020342ffb40536626dc1ef06': finished (progress 100%)
[PosixPath('no2_results/timeseries.csv'),
PosixPath('no2_results/job-results.json')]
import os
import shutil
old_name = "no2_results/timeseries.csv"
new_name = "no2_results/no2_medan_data.csv"
if os.path.exists(old_name):
shutil.move(old_name, new_name)
print(f"File renamed from 'timeseries.csv' to 'no2_medan_data.csv'")
else:
print("File timeseries.csv not found")
File renamed from 'timeseries.csv' to 'no2_medan_data.csv'
# 9. Baca file CSV hasil
import os
df =pd.read_csv(os.path.join("no2_results", "no2_medan_data.csv"))
# 10. Pastikan kolom tanggal benar
df["date"] = pd.to_datetime(df["date"])
# 11. Buat kolom bulan (YYYY-MM)
df["month"] = df["date"].dt.to_period("M")
# 12. Hitung rata-rata NO2 per bulan
df_monthly = df.groupby("month", as_index=False)["NO2"].mean()
df
/tmp/ipython-input-3399061483.py:5: UserWarning: Converting to PeriodArray/Index representation will drop timezone information.
df["month"] = df["date"].dt.to_period("M")
| date | feature_index | NO2 | month | |
|---|---|---|---|---|
| 0 | 2025-10-18 00:00:00+00:00 | 0 | 0.000025 | 2025-10 |
| 1 | 2025-10-19 00:00:00+00:00 | 0 | 0.000042 | 2025-10 |
| 2 | 2025-10-22 00:00:00+00:00 | 0 | NaN | 2025-10 |
| 3 | 2025-10-16 00:00:00+00:00 | 0 | 0.000038 | 2025-10 |
| 4 | 2025-10-20 00:00:00+00:00 | 0 | NaN | 2025-10 |
| ... | ... | ... | ... | ... |
| 391 | 2025-10-04 00:00:00+00:00 | 0 | 0.000026 | 2025-10 |
| 392 | 2025-10-03 00:00:00+00:00 | 0 | NaN | 2025-10 |
| 393 | 2025-10-05 00:00:00+00:00 | 0 | 0.000032 | 2025-10 |
| 394 | 2025-09-29 00:00:00+00:00 | 0 | NaN | 2025-09 |
| 395 | 2025-10-31 00:00:00+00:00 | 0 | NaN | 2025-10 |
396 rows × 4 columns
1.4 Struktur Kolom#
Setelah proses batch job selesai dijalankan dan hasilnya diekspor ke format CSV, data kemudian dibaca menggunakan pandas.DataFrame. Berdasarkan hasil cuplikan data dan kode pemrosesan yang digunakan, terdapat tiga kolom utama dalam dataset hasil ekstraksi, yaitu:
1. Kolom date#
Tipe Data:
datetime
(dikonversi menggunakanpd.to_datetime(df["date"])pada kode).Format Contoh:
2025-10-01 00:00:00+00:00
Format tersebut menunjukkan waktu dalam zona waktu UTC (+00:00).Makna: Kolom ini berisi tanggal representatif dari periode agregasi, di mana setiap baris mencerminkan nilai rata-rata harian hasil perhitungan NO₂.
Karena proses agregasi dilakukan denganperiod="day", maka setiap baris pada kolomdatemerepresentasikan satu hari penuh hasil pengamatan.
2. Kolom feature_index#
Tipe Data:
integer
(umumnya bernilai0untuk setiap baris).Makna: Kolom ini menunjukkan indeks fitur spasial (feature tile) yang dihasilkan oleh koleksi OpenEO.
Pada kasus ini, karena dilakukan agregasi spasial terhadap satu AOI (Area of Interest), maka nilaifeature_indexhampir selalu0.
Namun, pada situasi lain di mana pipeline menghasilkan lebih dari satu area atau spatial tile, kolom ini berfungsi untuk membedakan setiap geometri spasial yang diolah.
3. Kolom NO2#
Tipe Data:
float
(nilai numerik desimal kecil, dapat berupaNaNbila tidak ada data valid).Makna: Kolom ini berisi nilai rata-rata konsentrasi NO₂ hasil gabungan antara agregasi spasial dan temporal di area pengamatan (AOI) untuk setiap tanggal.
NilaiNO2mewakili tingkat kepadatan gas NO₂ yang diukur oleh sensor Sentinel-5P.Rentang Nilai: Biasanya sangat kecil (misalnya
0.000033,0.000054), yang menunjukkan unit kepadatan gas (perlu dikonfirmasi dengan metadata produk Sentinel-5P).Nilai
NaN: Menunjukkan tidak ada pengamatan valid untuk hari tersebut. Hal ini dapat terjadi apabila seluruh nilai piksel pada hari tersebut tidak lolos filter (misalnya karena nilai negatif atau tertutup awan).
Secara keseluruhan, struktur data yang dihasilkan cukup sederhana namun kaya informasi, terdiri dari:
Kolom |
Tipe |
Deskripsi |
|---|---|---|
|
datetime |
Tanggal pengamatan harian NO₂ |
|
integer |
Indeks fitur spasial (biasanya 0) |
|
float |
Nilai rata-rata NO₂ harian hasil agregasi spasial-temporal |
Dataset dengan struktur seperti ini sangat sesuai untuk analisis deret waktu (time series) dan tahap berikutnya yaitu pembuatan model prediksi kualitas udara menggunakan pendekatan supervised learning.
1.5 Indentifikasi Missing Value#
NO2 = pd.read_csv('no2_results/no2_medan_data.csv')
NO2 = NO2.sort_values(by='date')
NO2.isnull().sum()
| 0 | |
|---|---|
| date | 0 |
| feature_index | 0 |
| NO2 | 143 |
missing_count = NO2.isnull().sum()
missing_percent = (missing_count / len(NO2)) * 100
missing_table = pd.DataFrame({
'Missing Count': missing_count,
'Missing Percent (%)': missing_percent.round(2)
})
missing_table
| Missing Count | Missing Percent (%) | |
|---|---|---|
| date | 0 | 0.00 |
| feature_index | 0 | 0.00 |
| NO2 | 143 | 36.11 |
Jumlah Missing Value#
missing_count = NO2.isnull().sum()
missing_percent = (missing_count / len(NO2)) * 100
fig, ax = plt.subplots(figsize=(7,5))
bars = ax.bar(missing_count.index, missing_count)
ax.set_title("Frekuensi Missing Values per Kolom")
ax.set_ylabel("Jumlah Missing")
for i, v in enumerate(missing_count):
ax.text(i, v + 0.5, f"{v} ({missing_percent[i]:.1f}%)",
ha='center', va='bottom', fontsize=9)
plt.tight_layout()
plt.show()
/tmp/ipython-input-3930139431.py:12: FutureWarning: Series.__getitem__ treating keys as positions is deprecated. In a future version, integer keys will always be treated as labels (consistent with DataFrame behavior). To access a value by position, use `ser.iloc[pos]`
ax.text(i, v + 0.5, f"{v} ({missing_percent[i]:.1f}%)",
Tabel Informasi Dataset#
info_table = pd.DataFrame({
"Kolom": df.columns,
"Tipe Data": df.dtypes.astype(str),
"Jumlah Data Tidak Kosong": len(df) - df.isna().sum(),
"Jumlah Missing": df.isna().sum()
})
info_table
| Kolom | Tipe Data | Jumlah Data Tidak Kosong | Jumlah Missing | |
|---|---|---|---|---|
| date | date | datetime64[ns, UTC] | 396 | 0 |
| feature_index | feature_index | int64 | 396 | 0 |
| NO2 | NO2 | float64 | 253 | 143 |
| month | month | period[M] | 396 | 0 |
1.6 Visualisasi Time Series NO₂#
# Baca data
df = pd.read_csv("no2_results/no2_medan_data.csv")
df["date"] = pd.to_datetime(df["date"])
df["month"] = df["date"].dt.to_period("M")
df_monthly = df.groupby("month", as_index=False)["NO2"].mean()
# 13. Visualisasi hasil
plt.figure(figsize=(10,5))
plt.plot(df_monthly["month"].astype(str), df_monthly["NO2"], marker="o", color="blue", label="NO₂ Rata-rata per Bulan")
plt.title("Rata-rata Bulanan NO₂ (Sentinel-5P) di Bangkalan")
plt.xlabel("Bulan")
plt.ylabel("Konsentrasi NO₂")
plt.xticks(rotation=45)
plt.grid(True)
plt.legend()
plt.tight_layout()
plt.show()
/tmp/ipython-input-2664469499.py:6: UserWarning: Converting to PeriodArray/Index representation will drop timezone information.
df["month"] = df["date"].dt.to_period("M")