Analisis Potensi Likuifaksi Tanah Menggunakan Metode Seed & Idriss Berbasis Data SPT¶
Analisis Potensi Likuifaksi Berdasarkan Data Penyelidikan Tanah (N-SPT)¶
Studi Kasus: Cross Taxiway Timur Bandara Soekarno-Hatta (Mina et al., 2020)
Alur Kerja Umum (Workflow)¶
Notebook ini bertujuan untuk mengevaluasi potensi likuifaksi pada lapisan tanah berdasarkan data Standard Penetration Test (SPT) menggunakan metode empiris Seed & Idriss. Proses analisis dibagi menjadi beberapa tahapan komputasi berikut:
- Inisiasi Data & Parameter Seismik: Memasukkan parameter gempa rata-rata ($a_{max}$), muka air tanah (GWL), dan pembacaan nilai N-SPT lapangan.
- Kalkulasi Parameter Geoteknik: Menghitung tegangan total dan efektif tanah pada setiap kedalaman.
- Perhitungan Rasio Kritis:
- CSR (Cyclic Stress Ratio): Tegangan geser siklik yang diakibatkan oleh gempa.
- CRR (Cyclic Resistance Ratio): Kapasitas perlawanan tanah terhadap likuifaksi berdasarkan nilai N-SPT yang telah dikoreksi menjadi $(N_1)_{60cs}$.
- Penentuan Factor of Safety (FS): Menghitung nilai keamanan (FS = CRR / CSR). Jika FS < 1, lapisan tersebut dinyatakan rentan terlikuifaksi.
- Visualisasi Data: Memetakan nilai FS, CSR, dan CRR terhadap kedalaman tanah secara grafis.
Tahap 1 : Persiapan Modul dan Library¶
Tahap pertama adalah memuat pustaka (library) Python yang dibutuhkan untuk komputasi numerik, manipulasi data tabular, dan visualisasi grafik.
import numpy as np # Digunakan untuk pengolahan angka dan perhitungan matematika
import pandas as pd # Digunakan untuk manipulasi dan analisis data dalam bentuk tabel
import matplotlib.pyplot as plt # Digunakan untuk memvisualisasi data dalam bentuk grafik atau gambar
print("Modul berhasil dimuat.")
Modul berhasil dimuat.
Tahap 2 : Input Parameter Seismik dan Data Lapangan¶
Pada bagian ini, kita mendefinisikan parameter gempa regional (percepatan gempa maksimum/$a_{max}$), kondisi air tanah (GWL), asumsi berat isi tanah, serta mengonversi raw data N-SPT dari titik bor BH-08 menjadi sebuah dataframe [cite: 2].
# Parameter Gempa dan Lingkungan
Amax_g = 0.36 # Percepatan gempa rata-rata (g)
GWL = 8.0 # Muka air tanah (meter)
gamma_w = 9.81 # Berat volume air (kN/m³)
Pa = 98.066 # Tekanan atmosfer (kPa)
# Parameter Tanah (kN/m³)
gamma_t = 18.0
gamma_sat = 23.0
# Data N-SPT Titik BH08
data = {
'Kedalaman_m': [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30],
'N_SPT': [22, 24, 22, 12, 22, 15, 25, 23, 28, 31, 52, 56, 45, 46, 60]
}
df = pd.DataFrame(data)
display(df.head()) # Menampilkan 5 baris pertama data
| Kedalaman_m | N_SPT | |
|---|---|---|
| 0 | 2 | 22 |
| 1 | 4 | 24 |
| 2 | 6 | 22 |
| 3 | 8 | 12 |
| 4 | 10 | 22 |
Tahap 3 : Perhitungan Tegangan Total dan Tegangan Efektif Tanah¶
Kalkulasi tekanan tanah sangat bergantung pada posisi muka air tanah (GWL). Tanah yang berada di bawah muka air tanah akan mengalami gaya angkat hidrostatis (tekanan air pori/$u$), sehingga tegangan efektif ($\sigma'$) menjadi lebih kecil dibandingkan tegangan totalnya ($\sigma$).
# Membuat list kosong untuk menyimpan hasil tegangan total
teg_tot = []
# Membuat list kosong untuk menyimpan hasil tegangan efektif
teg_eff = []
# Loop untuk setiap nilai kedalaman (z) pada kolom 'Kedalaman_m'
for z in df['Kedalaman_m']:
# Mengecek apakah kedalaman masih di atas muka air tanah (GWL)
if z <= GWL:
# Menghitung tegangan total (tanah belum jenuh air)
sigma = z * gamma_t
# Tekanan air pori = 0 karena belum terendam air
u = 0
# Jika kedalaman berada di bawah muka air tanah
else:
# Menghitung tegangan total:
# bagian atas (kering) + bagian bawah (jenuh)
sigma = (GWL * gamma_t) + ((z - GWL) * gamma_sat)
# Menghitung tekanan air pori akibat air tanah
u = (z - GWL) * gamma_w
# Menyimpan nilai tegangan total ke dalam list
teg_tot.append(sigma)
# Menghitung dan menyimpan tegangan efektif (σ' = σ - u)
teg_eff.append(sigma - u)
# Menambahkan hasil tegangan total ke dalam DataFrame sebagai kolom baru
df['Tegangan_Total'] = teg_tot
# Menambahkan hasil tegangan efektif ke dalam DataFrame sebagai kolom baru
df['Tegangan_Efektif'] = teg_eff
# Menampilkan kolom kedalaman, tegangan total, dan tegangan efektif
# Dibulatkan hingga 2 angka di belakang koma agar rapi
display(df[['Kedalaman_m', 'Tegangan_Total', 'Tegangan_Efektif']].round(2))
| Kedalaman_m | Tegangan_Total | Tegangan_Efektif | |
|---|---|---|---|
| 0 | 2 | 36.0 | 36.00 |
| 1 | 4 | 72.0 | 72.00 |
| 2 | 6 | 108.0 | 108.00 |
| 3 | 8 | 144.0 | 144.00 |
| 4 | 10 | 190.0 | 170.38 |
| 5 | 12 | 236.0 | 196.76 |
| 6 | 14 | 282.0 | 223.14 |
| 7 | 16 | 328.0 | 249.52 |
| 8 | 18 | 374.0 | 275.90 |
| 9 | 20 | 420.0 | 302.28 |
| 10 | 22 | 466.0 | 328.66 |
| 11 | 24 | 512.0 | 355.04 |
| 12 | 26 | 558.0 | 381.42 |
| 13 | 28 | 604.0 | 407.80 |
| 14 | 30 | 650.0 | 434.18 |
Tahap 4 : Menghitung Cyclic Stress Ratio (CSR)¶
CSR mewakili beban seismik yang memicu terjadinya tegangan geser pada lapisan tanah akibat gempa. Perhitungan ini juga melibatkan faktor reduksi tegangan ($r_d$) menggunakan persamaan T.F. Blake (1997), yang mengoreksi nilai rambatan gelombang gempa seiring dengan bertambahnya kedalaman [cite: 2].
# Membuat list kosong untuk menyimpan nilai faktor reduksi tegangan (rd)
rd_list = []
# Loop untuk setiap kedalaman (z)
for z in df['Kedalaman_m']:
# Menghitung pembilang (numerator) dari persamaan rd (T.F. Blake, 1997)
num = 1 - 0.4113*(z**0.5) + 0.04052*z + 0.001753*(z**1.5)
# Menghitung penyebut (denominator) dari persamaan rd
den = 1 - 0.4177*(z**0.5) + 0.05729*z - 0.006205*(z**1.5) + 0.001210*(z**2)
# Menyimpan hasil rd = num / den ke dalam list
rd_list.append(num / den)
# Menambahkan hasil rd ke dalam DataFrame sebagai kolom baru
df['rd'] = rd_list
# ---------------------------------------
# Menghitung CSR (Cyclic Stress Ratio)
# ---------------------------------------
# Rumus CSR:
# 0.65 * percepatan gempa maksimum * rasio tegangan * faktor reduksi rd
df['CSR'] = 0.65 * Amax_g * (df['Tegangan_Total'] / df['Tegangan_Efektif']) * df['rd']
# Menampilkan hasil rd dan CSR
# Dibulatkan 3 angka desimal agar lebih rapi
display(df[['Kedalaman_m', 'rd', 'CSR']].round(3))
| Kedalaman_m | rd | CSR | |
|---|---|---|---|
| 0 | 2 | 0.987 | 0.231 |
| 1 | 4 | 0.973 | 0.228 |
| 2 | 6 | 0.958 | 0.224 |
| 3 | 8 | 0.937 | 0.219 |
| 4 | 10 | 0.905 | 0.236 |
| 5 | 12 | 0.857 | 0.240 |
| 6 | 14 | 0.794 | 0.235 |
| 7 | 16 | 0.728 | 0.224 |
| 8 | 18 | 0.667 | 0.212 |
| 9 | 20 | 0.618 | 0.201 |
| 10 | 22 | 0.581 | 0.193 |
| 11 | 24 | 0.553 | 0.187 |
| 12 | 26 | 0.532 | 0.182 |
| 13 | 28 | 0.515 | 0.178 |
| 14 | 30 | 0.501 | 0.176 |
Tahap 5 : Koreksi Nilai N-SPT Menjadi $(N_1)_{60cs}$¶
Nilai pengujian N-SPT di lapangan tidak bisa langsung digunakan. Nilai tersebut harus distandardisasi terlebih dahulu terhadap tekanan regangan penutup vertikal (overburden pressure / $C_N$) serta dikalikan dengan faktor koreksi alat (seperti rasio energi palu dan panjang batang bor)[cite: 2].
# Hitung faktor koreksi overburden (CN)
# Ini buat menyesuaikan pengaruh tegangan efektif terhadap nilai SPT
df['CN'] = 2.2 / (1.2 + (df['Tegangan_Efektif'] / Pa))
# Batasi nilai maksimum CN = 1.7
df['CN'] = df['CN'].clip(upper=1.7)
# Koreksi nilai N-SPT jadi (N1)60cs
# Faktor koreksi alat:
# C_E = 1.0 (energy)
# C_B = 1.0 (diameter borehole)
# C_R = 0.75 (rod length)
# C_S = 1.0 (sampler)
df['N1_60cs'] = df['N_SPT'] * df['CN'] * 1.0 * 1.0 * 0.75 * 1.0
# Tampilkan hasilnya (dibulatkan 2 angka biar rapi)
display(df[['Kedalaman_m', 'N_SPT', 'CN', 'N1_60cs']].round(2))
| Kedalaman_m | N_SPT | CN | N1_60cs | |
|---|---|---|---|---|
| 0 | 2 | 22 | 1.40 | 23.16 |
| 1 | 4 | 24 | 1.14 | 20.47 |
| 2 | 6 | 22 | 0.96 | 15.77 |
| 3 | 8 | 12 | 0.82 | 7.42 |
| 4 | 10 | 22 | 0.75 | 12.36 |
| 5 | 12 | 15 | 0.69 | 7.72 |
| 6 | 14 | 25 | 0.63 | 11.87 |
| 7 | 16 | 23 | 0.59 | 10.14 |
| 8 | 18 | 28 | 0.55 | 11.51 |
| 9 | 20 | 31 | 0.51 | 11.94 |
| 10 | 22 | 52 | 0.48 | 18.85 |
| 11 | 24 | 56 | 0.46 | 19.17 |
| 12 | 26 | 45 | 0.43 | 14.59 |
| 13 | 28 | 46 | 0.41 | 14.16 |
| 14 | 30 | 60 | 0.39 | 17.59 |
Tahap 6 : Kalkulasi Cyclic Resistance Ratio (CRR) dan Factor of Safety (FS)¶
CRR merepresentasikan kapasitas perlawanan tanah terhadap likuifaksi, dihitung berdasarkan kurva dasar Seed & Idriss. Selanjutnya, Nilai Keamanan (Factor of Safety / FS) didapatkan dari pembagian daya tahan tanah (CRR) terhadap beban gempa (CSR) [cite: 2]. Jika nilai FS < 1, lapisan tanah tersebut diklasifikasikan berpotensi likuifaksi.
# Buat list kosong untuk nyimpan nilai CRR
crr_list = []
# Loop tiap nilai (N1)60cs
for n in df['N1_60cs']:
# Kalau nilai SPT sudah tinggi (tanah sangat padat)
if n > 30:
crr_list.append(2.0) # dianggap aman dari likuifaksi
else:
# Hitung CRR pakai persamaan empiris Seed & Idriss
val = np.exp((n/14.1) + (n/126)**2 - (n/23.6)**3 + (n/25.4)**4 - 2.8)
crr_list.append(val)
# Masukkan hasil CRR ke DataFrame
df['CRR'] = crr_list
# Hitung Factor of Safety (FS)
# FS = CRR / CSR
df['FS'] = df['CRR'] / df['CSR']
# Tentukan status:
# FS < 1 → Rentan likuifaksi
# FS >= 1 → Aman
df['Status'] = np.where(df['FS'] < 1.0, 'Rentan', 'Aman')
# Tampilkan hasil akhir (dibulatkan 3 angka)
kolom_final = ['Kedalaman_m', 'CSR', 'CRR', 'FS', 'Status']
display(df[kolom_final].round(3))
| Kedalaman_m | CSR | CRR | FS | Status | |
|---|---|---|---|---|---|
| 0 | 2 | 0.231 | 0.252 | 1.093 | Aman |
| 1 | 4 | 0.228 | 0.212 | 0.930 | Rentan |
| 2 | 6 | 0.224 | 0.163 | 0.726 | Rentan |
| 3 | 8 | 0.219 | 0.101 | 0.460 | Rentan |
| 4 | 10 | 0.236 | 0.135 | 0.572 | Rentan |
| 5 | 12 | 0.240 | 0.103 | 0.428 | Rentan |
| 6 | 14 | 0.235 | 0.131 | 0.560 | Rentan |
| 7 | 16 | 0.224 | 0.119 | 0.532 | Rentan |
| 8 | 18 | 0.212 | 0.129 | 0.609 | Rentan |
| 9 | 20 | 0.201 | 0.132 | 0.657 | Rentan |
| 10 | 22 | 0.193 | 0.193 | 1.000 | Rentan |
| 11 | 24 | 0.187 | 0.196 | 1.052 | Aman |
| 12 | 26 | 0.182 | 0.153 | 0.839 | Rentan |
| 13 | 28 | 0.178 | 0.149 | 0.836 | Rentan |
| 14 | 30 | 0.176 | 0.180 | 1.023 | Aman |
Tahap 7 : Visualisasi Profil Vertikal (FS vs Kedalaman)¶
Grafik pertama ini bertujuan untuk memvisualisasikan kondisi pelapisan tanah secara vertikal.
- Sumbu-Y (Kedalaman) sengaja dibalik (inverted) agar merepresentasikan kondisi di bawah permukaan tanah (nilai 0 adalah elevasi permukaan).
- Garis putus-putus berwarna merah adalah batas kritis keamanan (FS = 1).
- Algoritma akan secara otomatis memberikan arsiran merah (highlight) pada rentang kedalaman di mana kurva FS jatuh di sebelah kiri garis kritis (bernilai kurang dari 1).
# Bikin figure khusus untuk grafik FS vs Kedalaman
plt.figure(figsize=(7, 6))
# Plot garis FS terhadap kedalaman
plt.plot(
df['FS'], # sumbu x = nilai FS
df['Kedalaman_m'], # sumbu y = kedalaman
marker='o', # titik data
color='forestgreen', # warna garis
linewidth=2,
label='Nilai FS'
)
# Garis vertikal di FS = 1 (batas kritis likuifaksi)
plt.axvline(
x=1.0,
color='red',
linestyle='--',
linewidth=2,
label='Batas Kritis (FS=1)'
)
# Ambil data yang termasuk kategori rentan (FS < 1)
rentan = df[df['Status'] == 'Rentan']
# Kasih arsiran di zona kedalaman yang rentan
for _, row in rentan.iterrows():
plt.axhspan(
row['Kedalaman_m'] - 1, # batas bawah area
row['Kedalaman_m'] + 1, # batas atas area
color='red',
alpha=0.2 # transparan biar gak terlalu pekat
)
# Atur tampilan sumbu
plt.ylim(df['Kedalaman_m'].max() + 2, 0) # dibalik biar kedalaman makin ke bawah
plt.xlim(0, max(3, df['FS'].max() + 0.5)) # batas x biar rapi
# Label dan judul
plt.xlabel('Factor of Safety (FS)')
plt.ylabel('Kedalaman (m)')
plt.title('Profil Vertikal FS terhadap Kedalaman')
# Legend dan grid
plt.legend()
plt.grid(True, linestyle=':')
# Rapihin layout dan tampilkan grafik
plt.tight_layout()
plt.show()
Tahap 8 : Visualisasi Sebaran Parameter (CSR vs CRR)¶
Grafik kedua menggunakan metode scatter plot untuk melihat korelasi antara tegangan geser akibat gempa (CSR) dan kapasitas perlawanan tanah (CRR) pada tiap titik kedalaman.
- Garis Diagonal: Merupakan titik ekuilibrium di mana nilai CSR persis sama dengan CRR (FS = 1).
- Zona Hijau (Aman): Area di mana perlawanan tanah lebih besar daripada beban gempa (CRR > CSR).
- Zona Merah (Likuifaksi): Area di mana beban gempa melampaui kapasitas tanah (CSR > CRR). Titik kedalaman yang jatuh di zona ini dipastikan akan mengalami kegagalan.
# Bikin kanvas/area gambar dengan ukuran 7x6 inch
plt.figure(figsize=(7, 6))
# Scatter plot antara CRR (x) dan CSR (y)
# Setiap titik mewakili satu data kedalaman
plt.scatter(
df['CRR'], # nilai CRR sebagai sumbu X (kapasitas/tahanan tanah)
df['CSR'], # nilai CSR sebagai sumbu Y (beban gempa)
c=df['FS'], # warna titik ditentukan oleh nilai FS (biar ada informasi tambahan)
cmap='RdYlGn', # skala warna: merah (kecil) → hijau (besar)
s=100, # ukuran titik (biar jelas kelihatan)
edgecolor='black', # kasih outline hitam di tiap titik
zorder=3, # urutan layer (biar titik muncul paling depan)
label='Titik Elevasi' # label untuk legend
)
# Ambil nilai maksimum dari CRR dan CSR
# Tujuannya biar sumbu X dan Y punya skalanya yang sama (grafik simetris)
max_val = max(df['CRR'].max(), df['CSR'].max()) + 0.1
# Plot garis diagonal dari (0,0) ke (max,max)
# Ini garis kondisi CSR = CRR → FS = 1 (batas kritis)
plt.plot(
[0, max_val], # nilai X dari 0 sampai maksimum
[0, max_val], # nilai Y dari 0 sampai maksimum
'r--', # garis merah putus-putus
linewidth=2,
label='Garis Kritis (FS=1)',
zorder=2 # diletakkan di bawah titik
)
# Mengarsir area di atas garis (CSR > CRR)
# Ini zona dimana beban gempa lebih besar dari kekuatan tanah
plt.fill_between(
[0, max_val], # range X
[0, max_val], # batas bawah = garis diagonal
max_val, # batas atas = nilai maksimum
color='red',
alpha=0.1, # transparan
label='Zona Likuifaksi (CSR > CRR)'
)
# Mengarsir area di bawah garis (CRR > CSR)
# Ini zona aman karena tanah masih lebih kuat dari beban
plt.fill_between(
[0, max_val], # range X
0, # batas bawah = 0
[0, max_val], # batas atas = garis diagonal
color='green',
alpha=0.1,
label='Zona Aman (CRR > CSR)'
)
# Atur batas sumbu X dari 0 sampai max_val
plt.xlim(0, max_val)
# Atur batas sumbu Y dari 0 sampai max_val (biar sama skala)
plt.ylim(0, max_val)
# Label sumbu X
plt.xlabel('Cyclic Resistance Ratio (CRR)')
# Label sumbu Y
plt.ylabel('Cyclic Stress Ratio (CSR)')
# Judul grafik
plt.title('Sebaran Nilai Parameter Beban vs Tahanan')
# Tampilkan legend (keterangan warna & garis)
plt.legend()
# Tambahin grid biar lebih mudah dibaca
plt.grid(True, linestyle=':')
# Rapihin layout biar ga numpuk
plt.tight_layout()
# Tampilkan grafik ke layar
plt.show()
Tahap 9 : Interpretasi dan Kesimpulan Otomatis¶
Code cell terakhir ini difungsikan untuk memindai data dan secara otomatis menuliskan draf kesimpulan rentang kedalaman yang mengalami kerentanan likuifaksi, sehingga mengurangi risiko kesalahan interpretasi manusia (human error).
# Cetak judul kesimpulan analisis
print("=== KESIMPULAN ANALISIS POTENSI LIKUIFAKSI ===")
# Cek apakah ada data yang termasuk kategori "Rentan" (FS < 1)
if not rentan.empty:
# Ambil semua nilai kedalaman yang masuk kategori rentan
kedalaman_kritis = rentan['Kedalaman_m'].tolist()
# Cari kedalaman paling dangkal dari zona rentan
kedalaman_min = min(kedalaman_kritis)
# Cari kedalaman paling dalam dari zona rentan
kedalaman_max = max(kedalaman_kritis)
# Tampilkan status bahwa lokasi rentan likuifaksi
print("STATUS: RENTAN LIKUIFAKSI")
# Penjelasan bahwa FS < 1 berarti tanah gagal menahan beban gempa
print(f"1. Berdasarkan analisis rasio CSR dan CRR, lapisan tanah terindikasi mengalami kegagalan (FS < 1).")
# Menampilkan rentang kedalaman yang berpotensi likuifaksi
print(f"2. Rentang kedalaman kritis yang berpotensi terlikuifaksi terdeteksi pada elevasi {kedalaman_min} meter hingga {kedalaman_max} meter di bawah permukaan tanah.")
# Jika tidak ada data rentan
else:
# Tampilkan status aman
print("STATUS: AMAN")
# Menjelaskan bahwa semua FS >= 1
print("1. Seluruh profil kedalaman memiliki nilai FS >= 1.")
# Menjelaskan bahwa tanah cukup kuat menahan beban gempa
print("2. Kapasitas tanah (CRR) di titik bor ini mampu menahan beban siklik gempa (CSR) secara memadai.")
=== KESIMPULAN ANALISIS POTENSI LIKUIFAKSI === STATUS: RENTAN LIKUIFAKSI 1. Berdasarkan analisis rasio CSR dan CRR, lapisan tanah terindikasi mengalami kegagalan (FS < 1). 2. Rentang kedalaman kritis yang berpotensi terlikuifaksi terdeteksi pada elevasi 4 meter hingga 28 meter di bawah permukaan tanah.
Tahap 10 : Pemetaan Statis Berdasarkan Data Observasi (Excel)¶
Pada tahap ini, kita beralih dari data simulasi ke data observasi riil yang disimpan dalam format Excel (Data_hasil_likuifaksi_decimal.xlsx).
Langkah yang dilakukan:
- Data Cleaning: Mengonversi format desimal Indonesia (koma) menjadi standar internasional (titik) agar dapat diolah secara matematis oleh Python.
- Filter Titik Unik: Menghilangkan duplikasi data pada titik bor yang sama agar visualisasi tidak tumpang tindih.
- Visualisasi Statis: Menggunakan
geopandasuntuk memplot titik-titik observasi beserta gradasi warnanya. Warna merah muda hingga hijau merepresentasikan transisi dari nilai FS rendah (rentan) ke tinggi (aman).
# =========================================================
# 1. PERSIAPAN LIBRARY
# =========================================================
# Hapus tanda pagar (#) jika library belum terinstal di Google Colab
# Library geopandas digunakan untuk analisis data spasial
# Library openpyxl digunakan untuk membaca file Excel (.xlsx)
# !pip install geopandas openpyxl -q
# Mengimpor library pandas untuk membaca dan mengolah data tabel
import pandas as pd
# Mengimpor library geopandas untuk membuat data spasial berbasis koordinat
import geopandas as gpd
# Mengimpor matplotlib untuk visualisasi grafik dan plotting peta
import matplotlib.pyplot as plt
# =========================================================
# 2. MEMBACA & MEMBERSIHKAN DATA EXCEL
# =========================================================
# Membaca file Excel yang berisi data hasil analisis likuifaksi
df = pd.read_excel(
# Nama file Excel
'Data_hasil_likuifaksi_decimal.xlsx',
# Nama sheet yang digunakan
sheet_name='Hasil Perhitungan SPT',
# Header tabel berada pada baris kedua Excel
header=1
)
# Membersihkan kolom fs terkecil
# Mengubah format angka dari:
# "0,672" → 0.672
# agar dapat terbaca sebagai tipe float
df['fs terkecil'] = (
# Mengubah data menjadi string terlebih dahulu
df['fs terkecil']
.astype(str)
# Mengganti tanda koma menjadi titik
.str.replace(',', '.')
# Mengubah hasil menjadi tipe angka float
.astype(float)
)
# Mengambil satu data unik untuk setiap titik borehole
# Tujuannya agar titik pada peta tidak bertumpuk
# karena satu borehole memiliki banyak data kedalaman
df_unique = df.groupby(
# Mengelompokkan berdasarkan nama titik bor
'Titik',
# Index default tidak dijadikan index baru
as_index=False
# Mengambil data pertama dari tiap kelompok
).first()
# =========================================================
# 3. MEMBUAT GEODATAFRAME & PLOTTING
# =========================================================
# Membuat GeoDataFrame dari data biasa menjadi data spasial
gdf = gpd.GeoDataFrame(
# Data utama yang digunakan
df_unique,
# Membuat geometry titik berdasarkan koordinat longitude dan latitude
geometry=gpd.points_from_xy(
# Koordinat X → Longitude
df_unique['Longitude_Decimal'],
# Koordinat Y → Latitude
df_unique['Latitude_Decimal']
),
# Menentukan sistem koordinat WGS84
# EPSG:4326 = standar koordinat global GPS
crs='EPSG:4326'
)
# Membuat figure dan axis untuk plotting peta
fig, ax = plt.subplots(
# Ukuran figure 10 x 8 inch
figsize=(10, 8)
)
# Melakukan plotting titik spasial
gdf.plot(
# Menempatkan plot pada axis yang sudah dibuat
ax=ax,
# Warna titik berdasarkan nilai FS
column='fs terkecil',
# Menggunakan gradasi warna merah-kuning-hijau
# merah = rentan
# hijau = aman
cmap='RdYlGn',
# Menampilkan legenda warna
legend=True,
# Ukuran marker titik
markersize=120,
# Memberi garis tepi hitam pada marker
edgecolor='black'
)
# Menambahkan label nama borehole pada setiap titik
for x, y, label in zip(
# Mengambil koordinat X geometry
gdf.geometry.x,
# Mengambil koordinat Y geometry
gdf.geometry.y,
# Mengambil nama titik bor
gdf['Titik']
):
# Menampilkan teks label di dekat titik
ax.text(
# Posisi X label sedikit digeser agar tidak menimpa titik
x + 0.0001,
# Posisi Y label
y,
# Isi teks label
label,
# Ukuran font label
fontsize=9,
# Membuat teks menjadi bold
fontweight='bold'
)
# Memberikan judul peta
plt.title(
# Isi judul
'Sebaran Spasial Nilai Factor of Safety (FS)',
# Membuat judul bold
fontweight='bold'
)
# Memberikan nama sumbu X
plt.xlabel('Longitude')
# Memberikan nama sumbu Y
plt.ylabel('Latitude')
# Menampilkan grid pada peta
plt.grid(
# Bentuk garis grid putus-putus
True,
linestyle='--'
)
# Mengatur tata letak figure agar lebih rapi
plt.tight_layout()
# Menampilkan hasil plotting peta
plt.show()
Tahap 11 : Interpolasi Spasial dan Peta Interaktif (Folium)¶
Tahap ini merupakan tingkat lanjut (advanced) dari pemetaan geoteknik. Kita melakukan dua hal utama:
- Interpolasi Spasial (Metode Cubic): Menggunakan pustaka
scipy.interpolateuntuk memprediksi nilai FS pada area kosong di antara titik-titik bor. Hasilnya berupa peta kontur yang menunjukkan zona bahaya secara meluas, bukan hanya titik per titik. - Peta Web Interaktif: Menggunakan pustaka
foliumuntuk menempatkan titik bor di atas basemap (seperti Google Maps/OpenStreetMap) yang bisa digeser (pan), diperbesar (zoom), dan diklik (popup) untuk melihat detail nilai FS-nya.
# =========================================================
# BASEMAP INTERAKTIF TITIK BOREHOLE
# =========================================================
# Menginstal library folium dan openpyxl
# folium → digunakan untuk membuat peta interaktif berbasis web
# openpyxl → digunakan untuk membaca file Excel (.xlsx)
!pip install folium openpyxl
# =========================================================
# IMPORT LIBRARY
# =========================================================
# Mengimpor pandas untuk membaca dan mengolah data tabel
import pandas as pd
# Mengimpor folium untuk membuat interactive map
import folium
# =========================================================
# MEMBACA FILE EXCEL
# =========================================================
# Membaca file Excel hasil analisis likuifaksi
df = pd.read_excel(
# Nama file Excel
'Data_hasil_likuifaksi_decimal.xlsx',
# Nama sheet yang digunakan
sheet_name='Hasil Perhitungan SPT',
# Header tabel berada pada baris kedua
header=1
)
# =========================================================
# MEMBERSIHKAN DATA
# =========================================================
# Membersihkan format nilai FS
# karena biasanya data Excel terbaca seperti:
# "0,672" → string
# sehingga perlu diubah menjadi:
# 0.672 → float
df['fs terkecil'] = (
# Mengambil kolom fs terkecil
df['fs terkecil']
# Mengubah seluruh data menjadi string
.astype(str)
# Mengganti tanda koma menjadi titik
.str.replace(',', '.')
# Mengubah hasil akhir menjadi tipe float
.astype(float)
)
# Mengambil hanya kolom yang diperlukan untuk pemetaan
df_map = df[
[
'Titik', # Nama titik borehole
'Latitude_Decimal', # Koordinat latitude
'Longitude_Decimal', # Koordinat longitude
'fs terkecil' # Nilai FS minimum
]
# Menghapus data duplikat
# agar satu borehole hanya muncul satu kali pada peta
].drop_duplicates()
# Membulatkan angka latitude menjadi 6 digit desimal
# agar tampilan koordinat lebih rapi
df_map['Latitude_Decimal'] = (
df_map['Latitude_Decimal']
.round(6)
)
# Membulatkan angka longitude menjadi 6 digit desimal
df_map['Longitude_Decimal'] = (
df_map['Longitude_Decimal']
.round(6)
)
# =========================================================
# MEMBUAT BASEMAP
# =========================================================
# Menghitung rata-rata latitude
# digunakan sebagai pusat tampilan peta
center_lat = df_map['Latitude_Decimal'].mean()
# Menghitung rata-rata longitude
# digunakan sebagai pusat tampilan peta
center_lon = df_map['Longitude_Decimal'].mean()
# Membuat peta interaktif menggunakan folium
m = folium.Map(
# Menentukan lokasi pusat peta
location=[center_lat, center_lon],
# Mengatur tingkat zoom awal peta
zoom_start=14,
# Menentukan jenis basemap
# OpenStreetMap adalah peta jalan standar
tiles='OpenStreetMap'
)
# =========================================================
# MENAMBAHKAN TITIK BOREHOLE
# =========================================================
# Melakukan looping untuk setiap baris data borehole
for _, row in df_map.iterrows():
# Mengecek nilai FS minimum
if row['fs terkecil'] < 1:
# Jika FS < 1 maka warna titik merah
# artinya zona rentan likuifaksi
color = 'red'
else:
# Jika FS >= 1 maka warna titik hijau
# artinya zona relatif aman
color = 'green'
# Membuat marker berbentuk lingkaran pada peta
folium.CircleMarker(
# Menentukan posisi koordinat marker
location=[
# Latitude titik
row['Latitude_Decimal'],
# Longitude titik
row['Longitude_Decimal']
],
# Ukuran radius marker
radius=8,
# Warna garis tepi marker
color=color,
# Mengaktifkan warna isi marker
fill=True,
# Warna isi marker
fill_color=color,
# Tingkat transparansi marker
fill_opacity=0.8,
# Popup yang muncul saat marker diklik
popup=(
f"""
<b>Titik:</b> {row['Titik']}<br>
<b>FS:</b> {row['fs terkecil']:.3f}
"""
)
# Menambahkan marker ke dalam peta
).add_to(m)
# =========================================================
# MENAMPILKAN PETA
# =========================================================
# Menampilkan hasil peta interaktif
m
Requirement already satisfied: folium in /usr/local/lib/python3.12/dist-packages (0.20.0) Requirement already satisfied: openpyxl in /usr/local/lib/python3.12/dist-packages (3.1.5) Requirement already satisfied: branca>=0.6.0 in /usr/local/lib/python3.12/dist-packages (from folium) (0.8.2) Requirement already satisfied: jinja2>=2.9 in /usr/local/lib/python3.12/dist-packages (from folium) (3.1.6) Requirement already satisfied: numpy in /usr/local/lib/python3.12/dist-packages (from folium) (2.0.2) Requirement already satisfied: requests in /usr/local/lib/python3.12/dist-packages (from folium) (2.32.4) Requirement already satisfied: xyzservices in /usr/local/lib/python3.12/dist-packages (from folium) (2026.3.0) Requirement already satisfied: et-xmlfile in /usr/local/lib/python3.12/dist-packages (from openpyxl) (2.0.0) Requirement already satisfied: MarkupSafe>=2.0 in /usr/local/lib/python3.12/dist-packages (from jinja2>=2.9->folium) (3.0.3) Requirement already satisfied: charset_normalizer<4,>=2 in /usr/local/lib/python3.12/dist-packages (from requests->folium) (3.4.7) Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.12/dist-packages (from requests->folium) (3.13) Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/local/lib/python3.12/dist-packages (from requests->folium) (2.5.0) Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.12/dist-packages (from requests->folium) (2026.4.22)
# =========================================================
# INTERPOLASI SPASIAL NILAI FS
# =========================================================
# Menginstal library scipy dan openpyxl
# scipy → digunakan untuk proses interpolasi spasial
# openpyxl → digunakan untuk membaca file Excel
!pip install scipy openpyxl
# =========================================================
# IMPORT LIBRARY
# =========================================================
# Mengimpor pandas untuk membaca dan mengolah data tabel
import pandas as pd
# Mengimpor numpy untuk operasi numerik dan pembuatan grid
import numpy as np
# Mengimpor matplotlib untuk visualisasi grafik
import matplotlib.pyplot as plt
# Mengimpor fungsi griddata dari scipy
# digunakan untuk interpolasi spasial
from scipy.interpolate import griddata
# =========================================================
# MEMBACA FILE EXCEL
# =========================================================
# Membaca file Excel hasil analisis likuifaksi
df = pd.read_excel(
# Nama file Excel
'Data_hasil_likuifaksi_decimal.xlsx',
# Nama sheet yang digunakan
sheet_name='Hasil Perhitungan SPT',
# Header tabel berada pada baris kedua
header=1
)
# =========================================================
# MEMBERSIHKAN DATA
# =========================================================
# Membersihkan format nilai FS
# karena data dari Excel biasanya masih terbaca:
# "0,672" → string
# sehingga perlu diubah menjadi:
# 0.672 → float
df['fs terkecil'] = (
# Mengambil kolom fs terkecil
df['fs terkecil']
# Mengubah data menjadi string
.astype(str)
# Mengganti koma menjadi titik
.str.replace(',', '.')
# Mengubah menjadi float
.astype(float)
)
# Mengambil kolom yang diperlukan untuk interpolasi
df_map = df[
[
'Titik', # Nama borehole
'Latitude_Decimal', # Koordinat latitude
'Longitude_Decimal', # Koordinat longitude
'fs terkecil' # Nilai FS minimum
]
# Menghapus data duplikat
# agar satu titik borehole hanya muncul sekali
].drop_duplicates()
# Membulatkan koordinat latitude
df_map['Latitude_Decimal'] = (
df_map['Latitude_Decimal']
.round(6)
)
# Membulatkan koordinat longitude
df_map['Longitude_Decimal'] = (
df_map['Longitude_Decimal']
.round(6)
)
# =========================================================
# MENGAMBIL KOORDINAT DAN NILAI FS
# =========================================================
# Mengambil seluruh nilai longitude
# sebagai koordinat X
x = df_map['Longitude_Decimal'].values
# Mengambil seluruh nilai latitude
# sebagai koordinat Y
y = df_map['Latitude_Decimal'].values
# Mengambil nilai FS minimum
# sebagai parameter interpolasi
z = df_map['fs terkecil'].values
# =========================================================
# MEMBUAT GRID INTERPOLASI
# =========================================================
# Membuat grid spasial
# Grid ini menjadi area perhitungan interpolasi
grid_x, grid_y = np.mgrid[
# Rentang longitude minimum hingga maksimum
x.min():x.max():200j,
# Rentang latitude minimum hingga maksimum
y.min():y.max():200j
]
# 200j artinya:
# grid dibagi menjadi 200 bagian
# sehingga menghasilkan interpolasi lebih halus
# =========================================================
# PROSES INTERPOLASI
# =========================================================
# Melakukan interpolasi spasial menggunakan griddata
grid_z = griddata(
# Koordinat titik asli
(x, y),
# Nilai FS asli
z,
# Grid tujuan interpolasi
(grid_x, grid_y),
# Metode interpolasi cubic
# menghasilkan transisi warna lebih smooth
method='cubic'
)
# =========================================================
# VISUALISASI INTERPOLASI
# =========================================================
# Membuat figure baru
plt.figure(figsize=(10,8))
# Membuat contour interpolasi berwarna
contour = plt.contourf(
# Grid koordinat longitude
grid_x,
# Grid koordinat latitude
grid_y,
# Nilai hasil interpolasi
grid_z,
# Jumlah level contour
levels=20,
# Colormap merah-kuning-hijau
cmap='RdYlGn'
)
# Menampilkan titik asli borehole
plt.scatter(
# Koordinat longitude
x,
# Koordinat latitude
y,
# Warna titik berdasarkan nilai FS
c=z,
# Colormap yang digunakan
cmap='RdYlGn',
# Warna garis tepi titik
edgecolor='black',
# Ukuran marker titik
s=120
)
# Menambahkan label nama titik pada setiap borehole
for i, txt in enumerate(df_map['Titik']):
plt.text(
# Posisi longitude
x[i],
# Posisi latitude
y[i],
# Isi teks label
txt,
# Ukuran font label
fontsize=8
)
# Membuat colorbar
cbar = plt.colorbar(contour)
# Memberikan nama colorbar
cbar.set_label('Factor of Safety (FS)')
# Memberikan judul grafik
plt.title('Interpolasi Spasial Nilai FS')
# Memberikan label sumbu X
plt.xlabel('Longitude')
# Memberikan label sumbu Y
plt.ylabel('Latitude')
# Menampilkan grid
plt.grid(True)
# Menampilkan hasil visualisasi interpolasi
plt.show()
Requirement already satisfied: scipy in /usr/local/lib/python3.12/dist-packages (1.16.3) Requirement already satisfied: openpyxl in /usr/local/lib/python3.12/dist-packages (3.1.5) Requirement already satisfied: numpy<2.6,>=1.25.2 in /usr/local/lib/python3.12/dist-packages (from scipy) (2.0.2) Requirement already satisfied: et-xmlfile in /usr/local/lib/python3.12/dist-packages (from openpyxl) (2.0.0)
Tahap 12 : Pengembangan Lanjutan (Interpolasi Spasial & Peta Interaktif)¶
Sebagai langkah pengembangan (future work) dari komputasi geoteknik ini, analisis titik bor (diskrit) dapat diekstrapolasi menjadi peta kerentanan likuifaksi yang kontinu (menyeluruh) menggunakan teknik Interpolasi Spasial.
Interpolasi Spasial (Kriging / IDW): Nilai Factor of Safety (FS) atau parameter SPT dari titik-titik bor (BH-01 hingga BH-08) dapat diinterpolasi menggunakan metode Inverse Distance Weighting (IDW) atau Kriging (memanfaatkan pustaka seperti
scipy.interpolateatauPyKrige). Hasil dari interpolasi ini adalah peta kontur atau raster (seperti profil gradasi warna pada Gambar 3 di paper Mina et al., 2020) yang memprediksi nilai kerentanan di area yang tidak memiliki data bor.Overlay pada Peta Interaktif (Web-Mapping): Alih-alih menggunakan peta statis (plot standar Matplotlib), kita dapat membuat peta interaktif langsung di dalam Google Colab menggunakan pustaka
folium. Peta kontur hasil interpolasi spasial (dalam format GeoTIFF atau GeoJSON) kemudian di-overlay (ditumpangkan) di atas basemap satelit interaktif (seperti OpenStreetMap atau Google Satellite). Dengan ini, pengguna dapat melakukan zoom-in/out dan mengklik titik bor untuk melihat pop-up nilai CRS, CRR, dan FS secara real-time.
📌 Penutup & Kesimpulan Akhir¶
Proyek komputasi geologi "Analisis Potensi Likuifaksi Tanah Menggunakan Metode Seed & Idriss" ini membuktikan bahwa integrasi antara metode geoteknik empiris konvensional dan bahasa pemrograman Python (Google Colab) dapat memberikan efisiensi yang masif.
- Otomatisasi: Perhitungan matematis iteratif (tegangan efektif, CSR, koreksi SPT, hingga CRR) dapat diselesaikan dalam hitungan detik untuk profil kedalaman yang sangat panjang.
- Akurasi Visual: Visualisasi Factor of Safety (FS) terhadap kedalaman membantu engineer mengidentifikasi zona kritis dengan cepat dan presisi.
- Integrasi Geospasial: Dukungan ekosistem Python (GeoPandas, SimpleKML) memungkinkan data tabular langsung dikonversi menjadi data geospasial untuk mitigasi bencana yang komprehensif.
Pendekatan numerik dan spasial ini diharapkan dapat menjadi fondasi yang kuat untuk analisis kebencanaan geologi, khususnya dalam memberikan rekomendasi perbaikan tanah (soil improvement) pada infrastruktur vital seperti area Bandar Udara.
# install library yang diperlukan
# pandas → membaca dan mengolah data excel
# numpy → operasi numerik dan pembuatan grid
# matplotlib → visualisasi grafik
# scipy → interpolasi spasial
# geopandas → pengolahan data spasial
# contextily → menambahkan basemap nyata
# openpyxl → membaca file excel .xlsx
# pyproj → konversi sistem koordinat
!pip install pandas numpy matplotlib scipy geopandas contextily openpyxl pyproj
# import pandas untuk manipulasi data tabel
import pandas as pd
# import numpy untuk operasi numerik
import numpy as np
# import matplotlib untuk plotting visualisasi
import matplotlib.pyplot as plt
# import geopandas untuk membuat data spasial
import geopandas as gpd
# import contextily untuk menambahkan basemap nyata
import contextily as ctx
# import fungsi interpolasi griddata dari scipy
from scipy.interpolate import griddata
# import transformer untuk konversi koordinat
from pyproj import Transformer
# membaca file excel hasil analisis likuifaksi
df = pd.read_excel(
# nama file excel
'Data_hasil_likuifaksi_decimal.xlsx',
# nama sheet yang digunakan
sheet_name='Hasil Perhitungan SPT',
# header tabel berada di baris kedua
header=1
)
# membersihkan format nilai FS
# karena biasanya angka dari excel terbaca:
# "0,672" → string
# sehingga perlu diubah menjadi:
# 0.672 → float
df['fs terkecil'] = (
# mengambil kolom fs terkecil
df['fs terkecil']
# mengubah data menjadi string
.astype(str)
# mengganti koma menjadi titik
.str.replace(',', '.')
# mengubah hasil akhir menjadi float
.astype(float)
)
# mengambil hanya kolom yang diperlukan
df_map = df[[
# nama titik borehole
'Titik',
# koordinat latitude
'Latitude_Decimal',
# koordinat longitude
'Longitude_Decimal',
# nilai FS minimum
'fs terkecil'
# menghapus data duplikat
# agar satu titik borehole hanya muncul sekali
]].drop_duplicates()
# membulatkan latitude menjadi 6 digit desimal
df_map['Latitude_Decimal'] = (
df_map['Latitude_Decimal']
.round(6)
)
# membulatkan longitude menjadi 6 digit desimal
df_map['Longitude_Decimal'] = (
df_map['Longitude_Decimal']
.round(6)
)
# membuat GeoDataFrame (data spasial)
gdf = gpd.GeoDataFrame(
# data utama
df_map,
# membuat geometry titik dari longitude dan latitude
geometry=gpd.points_from_xy(
# koordinat X = longitude
df_map['Longitude_Decimal'],
# koordinat Y = latitude
df_map['Latitude_Decimal']
),
# menentukan sistem koordinat WGS84
crs='EPSG:4326'
)
# mengubah sistem koordinat ke Web Mercator
# karena contextily membutuhkan EPSG:3857
gdf_web = gdf.to_crs(epsg=3857)
# mengambil data longitude sebagai sumbu X
x = df_map['Longitude_Decimal'].values
# mengambil data latitude sebagai sumbu Y
y = df_map['Latitude_Decimal'].values
# mengambil nilai FS sebagai parameter interpolasi
z = df_map['fs terkecil'].values
# membuat grid interpolasi
# grid ini menjadi area perhitungan interpolasi
grid_x, grid_y = np.mgrid[
# rentang longitude minimum hingga maksimum
x.min():x.max():300j,
# rentang latitude minimum hingga maksimum
y.min():y.max():300j
]
# 300j artinya grid dibagi menjadi 300 bagian
# semakin besar nilainya → hasil interpolasi semakin halus
# melakukan interpolasi spasial
grid_z = griddata(
# koordinat titik asli
(x, y),
# nilai FS asli
z,
# grid interpolasi tujuan
(grid_x, grid_y),
# metode cubic untuk menghasilkan transisi lebih smooth
method='cubic'
)
# membuat transformer koordinat
transformer = Transformer.from_crs(
# sistem koordinat awal
"EPSG:4326",
# sistem koordinat tujuan
"EPSG:3857",
# memastikan urutan koordinat selalu x,y
always_xy=True
)
# mengubah grid interpolasi ke Web Mercator
grid_x_web, grid_y_web = transformer.transform(
# grid longitude
grid_x,
# grid latitude
grid_y
)
# membuat figure visualisasi
fig, ax = plt.subplots(
# ukuran figure
figsize=(12, 10)
)
# membuat contour interpolasi berwarna
contour = ax.contourf(
# grid longitude
grid_x_web,
# grid latitude
grid_y_web,
# hasil interpolasi
grid_z,
# jumlah level contour
levels=20,
# colormap merah-kuning-hijau
cmap='RdYlGn',
# transparansi overlay
alpha=0.6
)
# menampilkan titik borehole
gdf_web.plot(
# axis plot
ax=ax,
# warna berdasarkan nilai FS
column='fs terkecil',
# colormap
cmap='RdYlGn',
# warna garis tepi marker
edgecolor='black',
# ukuran marker
markersize=100
)
# looping untuk menambahkan label titik
for x_pt, y_pt, label in zip(
# koordinat X titik
gdf_web.geometry.x,
# koordinat Y titik
gdf_web.geometry.y,
# nama titik borehole
gdf_web['Titik']
):
# menambahkan teks label
ax.text(
# posisi X
x_pt,
# posisi Y
y_pt,
# isi label
label,
# ukuran font
fontsize=8
)
# menambahkan basemap nyata OpenStreetMap
ctx.add_basemap(
# axis plot
ax,
# jenis basemap
source=ctx.providers.OpenStreetMap.Mapnik
)
# membuat colorbar
cbar = plt.colorbar(contour)
# memberi nama colorbar
cbar.set_label('Factor of Safety (FS)')
# memberi judul visualisasi
plt.title('Overlay Interpolasi FS terhadap Basemap')
# menghilangkan axis agar tampilan lebih clean
plt.axis('off')
# menampilkan hasil visualisasi
plt.show()
Requirement already satisfied: pandas in /usr/local/lib/python3.12/dist-packages (2.2.2) Requirement already satisfied: numpy in /usr/local/lib/python3.12/dist-packages (2.0.2) Requirement already satisfied: matplotlib in /usr/local/lib/python3.12/dist-packages (3.10.0) Requirement already satisfied: scipy in /usr/local/lib/python3.12/dist-packages (1.16.3) Requirement already satisfied: geopandas in /usr/local/lib/python3.12/dist-packages (1.1.3) Requirement already satisfied: contextily in /usr/local/lib/python3.12/dist-packages (1.7.0) Requirement already satisfied: openpyxl in /usr/local/lib/python3.12/dist-packages (3.1.5) Requirement already satisfied: pyproj in /usr/local/lib/python3.12/dist-packages (3.7.2) Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.12/dist-packages (from pandas) (2.9.0.post0) Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.12/dist-packages (from pandas) (2025.2) Requirement already satisfied: tzdata>=2022.7 in /usr/local/lib/python3.12/dist-packages (from pandas) (2026.1) Requirement already satisfied: contourpy>=1.0.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (1.3.3) Requirement already satisfied: cycler>=0.10 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (0.12.1) Requirement already satisfied: fonttools>=4.22.0 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (4.62.1) Requirement already satisfied: kiwisolver>=1.3.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (1.5.0) Requirement already satisfied: packaging>=20.0 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (26.1) Requirement already satisfied: pillow>=8 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (11.3.0) Requirement already satisfied: pyparsing>=2.3.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (3.3.2) Requirement already satisfied: pyogrio>=0.7.2 in /usr/local/lib/python3.12/dist-packages (from geopandas) (0.12.1) Requirement already satisfied: shapely>=2.0.0 in /usr/local/lib/python3.12/dist-packages (from geopandas) (2.1.2) Requirement already satisfied: geopy in /usr/local/lib/python3.12/dist-packages (from contextily) (2.4.1) Requirement already satisfied: mercantile in /usr/local/lib/python3.12/dist-packages (from contextily) (1.2.1) Requirement already satisfied: rasterio in /usr/local/lib/python3.12/dist-packages (from contextily) (1.5.0) Requirement already satisfied: requests in /usr/local/lib/python3.12/dist-packages (from contextily) (2.32.4) Requirement already satisfied: joblib in /usr/local/lib/python3.12/dist-packages (from contextily) (1.5.3) Requirement already satisfied: xyzservices in /usr/local/lib/python3.12/dist-packages (from contextily) (2026.3.0) Requirement already satisfied: et-xmlfile in /usr/local/lib/python3.12/dist-packages (from openpyxl) (2.0.0) Requirement already satisfied: certifi in /usr/local/lib/python3.12/dist-packages (from pyproj) (2026.4.22) Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.12/dist-packages (from python-dateutil>=2.8.2->pandas) (1.17.0) Requirement already satisfied: geographiclib<3,>=1.52 in /usr/local/lib/python3.12/dist-packages (from geopy->contextily) (2.1) Requirement already satisfied: click>=3.0 in /usr/local/lib/python3.12/dist-packages (from mercantile->contextily) (8.3.3) Requirement already satisfied: affine in /usr/local/lib/python3.12/dist-packages (from rasterio->contextily) (2.4.0) Requirement already satisfied: attrs in /usr/local/lib/python3.12/dist-packages (from rasterio->contextily) (26.1.0) Requirement already satisfied: cligj>=0.5 in /usr/local/lib/python3.12/dist-packages (from rasterio->contextily) (0.7.2) Requirement already satisfied: charset_normalizer<4,>=2 in /usr/local/lib/python3.12/dist-packages (from requests->contextily) (3.4.7) Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.12/dist-packages (from requests->contextily) (3.13) Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/local/lib/python3.12/dist-packages (from requests->contextily) (2.5.0)