Bab 38 — GIS 1: Data Vektor: Peta yang Bisa Dihitung
Seri I5 — Aplikasi Pertama: Web, Peta, dan Dasbor
Semua skrip bab ini ada di folder kode/bab38/. Di versi web, Anda juga bisa mengunduh semuanya sekaligus (zip).
Sampai sekarang, “lokasi” di buku ini hanya nama: kolam A, petak H1. Padahal pertanyaan lapangan sering menyangkut letak: Berapa hektare sawah kita? Kolam mana yang paling dekat ke sungai? Petak mana yang masuk zona penyangga? GIS (Geographic Information System, sistem informasi geografis) menjawabnya dengan menyimpan bentuk dan letak benda di bumi sebagai data, lalu menghitungnya. Bab ini membahas data vektor, yaitu benda yang digambar dengan koordinat. (Data raster, yaitu kisi piksel seperti foto satelit, ada di Bab 39.)
Seluruh data bab ini tiruan buatan skrip dan letaknya fiktif, jadi angkanya hanya untuk belajar. Isinya enam petak (hutan, kebun, sawah), empat kolam, tiga kandang, satu sungai, dan dua dusun, tersimpan di kode/bab38/data/ sebagai berkas GeoJSON.

Titik, garis, poligon: Shapely
Shapely adalah pustaka untuk membuat dan menghitung bentuk geometri. Ia tidak tahu apa-apa tentang bumi: ia hanya melihat angka pada bidang datar.
01_geometri_shapely.py
# Bab 38 - Titik, garis, dan poligon dengan Shapely
from shapely.geometry import LineString, Point, Polygon
# Koordinat di bawah ini dalam meter (bidang datar, satuan sama di kedua sumbu)
kolam = Point(120, 285) # titik: satu lokasi
sungai = LineString([(0, 330), (200, 310), (400, 290)]) # garis: urutan titik
petak = Polygon([(50, 100), (250, 100), (250, 250), (50, 250)]) # poligon: area tertutup
print(kolam.geom_type, sungai.geom_type, petak.geom_type)
print("panjang sungai (m):", round(sungai.length, 1))
print("luas petak (m2) :", petak.area)
print("keliling petak (m):", petak.length)
print("titik tengah petak:", petak.centroid)
print("kolam di dalam petak? ", petak.contains(kolam))
print("sungai memotong petak? ", sungai.intersects(petak))
print("jarak kolam ke sungai (m):", round(kolam.distance(sungai), 1))
print("batas luar (minx, miny, maxx, maxy):", petak.bounds)Point LineString Polygon
panjang sungai (m): 402.0
luas petak (m2) : 30000.0
keliling petak (m): 700.0
titik tengah petak: POINT (150 175)
kolam di dalam petak? False
sungai memotong petak? False
jarak kolam ke sungai (m): 32.8
batas luar (minx, miny, maxx, maxy): (50.0, 100.0, 250.0, 250.0)
Pointuntuk satu lokasi (kolam, kandang, pohon),LineStringuntuk urutan titik (sungai, jalan),Polygonuntuk area tertutup (petak).- Luas petak 30000 m² (200 m kali 150 m) dan kelilingnya 700 m. Sungai (dari tiga titik) panjangnya 402 m.
contains,intersects,distancemenjawab pertanyaan letak: kolam tidak di dalam petak, sungai tidak memotong petak, dan jarak kolam ke sungai 32,8 m.
Shapely menghitung dalam satuan angka koordinat. Bila koordinat dalam meter, luasnya meter persegi. Bila dalam derajat, “luas”-nya derajat persegi, yang tidak berguna. Itu persoalan CRS, dibahas sebentar lagi.
GeoPandas: tabel yang punya bentuk
GeoPandas menambahkan satu kolom khusus, geometry, ke DataFrame pandas (Bab 23). Semua yang Anda tahu tentang pandas (menyaring, groupby, menggabung) tetap berlaku. Mesin yang membaca dan menulis berkas peta di belakang layar adalah pyogrio (pengganti fiona, yang belum tersedia untuk Python 3.14 saat bab ini ditulis).
02_geodataframe_format.py
# Bab 38 - GeoDataFrame dan tiga format berkas: GeoJSON, GeoPackage, Shapefile
import warnings
from pathlib import Path
import geopandas as gpd
from shapely.geometry import Point
tmp = Path(__file__).parent / "_tmp"
tmp.mkdir(exist_ok=True)
# GeoDataFrame = tabel pandas + satu kolom bernama geometry + CRS
kandang = gpd.GeoDataFrame(
{"kode": ["D1", "D2"], "jenis": ["sapi", "ayam"], "populasi_ekor_hari_ini": [12, 200]},
geometry=[Point(106.8168, -6.9955), Point(106.8118, -6.9945)], # (bujur, lintang)
crs="EPSG:4326",
)
print(kandang)
print("CRS:", kandang.crs)
kandang.to_file(tmp / "kandang.geojson", driver="GeoJSON")
kandang.to_file(tmp / "kandang.gpkg", layer="kandang", driver="GPKG")
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
kandang.to_file(tmp / "kandang.shp") # Shapefile
print("peringatan Shapefile:", str(w[0].message))
print("berkas Shapefile:", sorted(p.name for p in tmp.glob("kandang.*") if p.suffix not in (".geojson", ".gpkg")))
print("kolom dari Shapefile :", list(gpd.read_file(tmp / "kandang.shp").columns))
print("kolom dari GeoPackage :", list(gpd.read_file(tmp / "kandang.gpkg").columns)) kode jenis populasi_ekor_hari_ini geometry
0 D1 sapi 12 POINT (106.8168 -6.9955)
1 D2 ayam 200 POINT (106.8118 -6.9945)
CRS: EPSG:4326
peringatan Shapefile: Column names longer than 10 characters will be truncated when saved to ESRI Shapefile.
berkas Shapefile: ['kandang.cpg', 'kandang.dbf', 'kandang.prj', 'kandang.shp', 'kandang.shx']
kolom dari Shapefile : ['kode', 'jenis', 'populasi_e', 'geometry']
kolom dari GeoPackage : ['kode', 'jenis', 'populasi_ekor_hari_ini', 'geometry']
Tiga format yang paling sering Anda temui:
| Format | Berkas | Kelebihan | Kekurangan |
|---|---|---|---|
| GeoJSON | satu .geojson (teks) |
Mudah dibaca manusia dan web; selalu EPSG:4326 | Boros ruang; lambat untuk data besar |
| GeoPackage | satu .gpkg (SQLite) |
Satu berkas, banyak lapisan, nama kolom bebas, standar terbuka | Tidak terbaca manusia |
| Shapefile | lima berkas atau lebih (.shp, .shx, .dbf, .prj, .cpg) |
Dibaca hampir semua perangkat lunak lama | Nama kolom maksimum 10 huruf |
Keluarannya menunjukkan batasan Shapefile: kolom populasi_ekor_hari_ini dipotong menjadi populasi_e (dan Anda diberi peringatan), sedangkan GeoPackage menyimpannya utuh. Untuk data baru, pilih GeoPackage.
CRS: derajat atau meter?
Bumi bulat, peta datar. CRS (Coordinate Reference System) menjelaskan arti angka koordinat. Dua yang paling penting:
- EPSG:4326 (WGS 84): koordinat bujur dan lintang dalam derajat. Dipakai GPS dan web.
- Proyeksi UTM, misalnya EPSG:32748 (UTM zona 48 Selatan): koordinat dalam meter di bidang datar. Cocok untuk luas dan jarak di satu wilayah. Indonesia terbagi dalam beberapa zona UTM.
03_crs_proyeksi.py
# Bab 38 - Sistem koordinat (CRS): derajat atau meter?
import geopandas as gpd
from pyproj import CRS, Transformer
petak = gpd.read_file("data/petak.geojson")
print("CRS berkas:", petak.crs)
print("satuan sumbu:", [s.unit_name for s in petak.crs.axis_info])
print("pojok petak H1 (bujur, lintang):", [round(v, 5) for v in petak.geometry.iloc[0].exterior.coords[0]])
print("'luas' H1 dalam derajat persegi:", petak.geometry.iloc[0].area) # tidak ada artinya!
utm = petak.estimate_utm_crs() # menebak zona UTM dari letak data
print("zona UTM yang cocok:", utm.to_string(), "|", utm.name)
datar = petak.to_crs(utm)
print("satuan sumbu UTM:", [s.unit_name for s in datar.crs.axis_info])
print("luas H1 (m2):", round(datar.geometry.iloc[0].area))
# Pyproj: pindah satu titik dari 4326 ke UTM. always_xy=True = urutan (bujur, lintang).
pindah = Transformer.from_crs(CRS.from_epsg(4326), CRS.from_epsg(32748), always_xy=True)
timur, utara = pindah.transform(106.8125, -6.9950)
print("titik di UTM 48S: timur =", round(timur, 1), "| utara =", round(utara, 1))CRS berkas: EPSG:4326
satuan sumbu: ['degree', 'degree']
pojok petak H1 (bujur, lintang): [106.81361, -6.99516]
'luas' H1 dalam derajat persegi: 7.363732839988365e-06
zona UTM yang cocok: EPSG:32748 | WGS 84 / UTM zone 48S
satuan sumbu UTM: ['metre', 'metre']
luas H1 (m2): 90001
titik di UTM 48S: timur = 700227.2 | utara = 9226418.1
- File kita berasal dari EPSG:4326, sumbunya derajat. “Luas” H1 di situ
7.36e-06derajat persegi: angka tanpa arti praktis. estimate_utm_crs()menebak zona UTM yang cocok dari letak data: EPSG:32748.- Setelah
to_crs(...), sumbu berubah menjadi meter dan luas H1 menjadi 90001 m² (kira-kira 9 hektare; selisih 1 m² berasal dari pembulatan koordinat GeoJSON ke 7 desimal, sekitar 1 cm). pyprojbisa memindahkan satu titik sendiri.always_xy=Truememastikan urutannya (bujur, lintang), karena EPSG:4326 resminya berurutan (lintang, bujur) dan sering membingungkan.
Jangan menghitung luas atau jarak langsung dari data berderajat. Ubah dulu ke CRS yang berunit meter dengan to_crs. Ini kesalahan paling sering di GIS. Setiap berkas membawa CRS-nya sendiri (gdf.crs): periksa sebelum menggabung.
Luas petak dan jarak kolam ke sungai
04_luas_jarak.py
# Bab 38 - Luas petak dan jarak kolam ke sungai
import geopandas as gpd
petak = gpd.read_file("data/petak.geojson").to_crs(32748) # meter
kolam = gpd.read_file("data/kolam.geojson").to_crs(32748)
sungai = gpd.read_file("data/sungai.geojson").to_crs(32748)
petak["luas_ha"] = (petak.area / 10_000).round(2)
print(petak[["kode", "sektor", "komoditas", "luas_ha"]].to_string(index=False))
print()
print("luas per sektor (ha):")
print(petak.groupby("sektor")["luas_ha"].sum().to_string())
garis_sungai = sungai.geometry.iloc[0]
kolam["jarak_sungai_m"] = kolam.distance(garis_sungai).round(1)
print()
print(kolam[["kode", "jenis", "jumlah_ikan", "jarak_sungai_m"]].to_string(index=False))
dekat = kolam[kolam["jarak_sungai_m"] < 25]
print("kolam kurang dari 25 m dari sungai:", list(dekat["kode"]))kode sektor komoditas luas_ha
H1 hutan Jati 9.00
H2 hutan Meranti 6.93
K1 kebun kopi 4.00
K2 kebun kakao 3.00
S1 sawah padi 3.00
S2 sawah padi 2.25
luas per sektor (ha):
sektor
hutan 15.93
kebun 7.00
sawah 5.25
kode jenis jumlah_ikan jarak_sungai_m
A lele 800 15.4
B nila 500 29.8
C lele 650 20.0
D nila 300 116.4
kolam kurang dari 25 m dari sungai: ['A', 'C']
Hutan 15,93 ha (9,00 + 6,93), kebun 7,00 ha, sawah 5,25 ha. kolam.distance(garis) menghitung jarak terdekat dari tiap kolam ke sungai: A dan C kurang dari 25 m, kolam D jauh (116,4 m). Pada contoh nyata, kolam dekat sungai berguna untuk air, tetapi berisiko banjir; angka batas 25 m di sini hanya contoh.
Buffer dan overlay
Buffer membuat area di sekitar suatu bentuk dengan jarak tertentu. Overlay menumpuk dua lapisan dan menghitung potongannya: intersection (bagian yang beririsan) atau difference (bagian yang tidak).
05_buffer_overlay.py
# Bab 38 - Buffer (zona penyangga) dan overlay (tumpang susun)
import geopandas as gpd
petak = gpd.read_file("data/petak.geojson").to_crs(32748)
sungai = gpd.read_file("data/sungai.geojson").to_crs(32748)
# Buffer: area dalam jarak 60 m dari sungai (angka 60 hanya contoh belajar)
zona = gpd.GeoDataFrame({"zona": ["penyangga"]}, geometry=sungai.buffer(60), crs=sungai.crs)
print("luas zona penyangga (ha):", round(zona.area.iloc[0] / 10_000, 2))
# Overlay intersection: potongan petak yang masuk zona
masuk = gpd.overlay(petak, zona, how="intersection")
masuk["luas_masuk_m2"] = masuk.area.round(0)
print("\npetak yang masuk zona penyangga:")
print(masuk[["kode", "sektor", "luas_masuk_m2"]].to_string(index=False))
# Overlay difference: bagian petak yang di luar zona
aman = gpd.overlay(petak, zona, how="difference")
aman["luas_aman_ha"] = (aman.area / 10_000).round(2)
print("\nluas petak di luar zona (ha):")
print(aman[["kode", "luas_aman_ha"]].to_string(index=False))luas zona penyangga (ha): 13.24
petak yang masuk zona penyangga:
kode sektor luas_masuk_m2
K2 kebun 4.0
S1 sawah 141.0
S2 sawah 2333.0
luas petak di luar zona (ha):
kode luas_aman_ha
H1 9.00
H2 6.93
K1 4.00
K2 3.00
S1 2.99
S2 2.02
Zona 60 m di kiri-kanan sungai luasnya 13,24 ha. Tiga petak menjorok masuk: S2 paling banyak (2333 m²), S1 hanya 141 m², dan K2 cuma 4 m². Baris terakhir menunjukkan sisanya: S2 tinggal 2,02 ha (dari 2,25) dan S1 2,99 ha. Angka 60 m hanyalah contoh; aturan sempadan sungai yang sebenarnya ditentukan peraturan setempat.

Memasang GIS: pip atau conda-forge?
Di komputer penulis, semua pustaka GIS terpasang dengan pip karena tersedia sebagai wheel (paket jadi) yang sudah membawa pustaka C di dalamnya. Tidak selalu begitu. Pustaka dasarnya, GDAL, GEOS, dan PROJ, ditulis dalam C/C++, dan di beberapa sistem pip tidak menemukan wheel yang cocok sehingga pemasangan gagal. conda-forge (saluran paket komunitas untuk conda atau mamba) memasang pustaka C itu bersama paket Python-nya sehingga biasanya lebih mulus:
| pip | conda-forge | |
|---|---|---|
| Perintah | pip install geopandas rasterio |
conda install -c conda-forge geopandas rasterio |
| Pustaka C (GDAL, PROJ) | Ikut di wheel, bila ada untuk sistem Anda | Dipasang sebagai paket sendiri, versinya serasi |
| Kelebihan | Ringan, satu cara dengan paket lain | Paling andal untuk GDAL, banyak alat GIS lain |
| Kekurangan | Bisa gagal bila tidak ada wheel | Perlu memasang conda/mamba; lingkungan terpisah dari venv |
Perintah conda di atas tidak saya jalankan: conda belum terpasang di komputer penulis, jadi bagian conda-forge belum diuji di sini. Perbandingan ini berdasar cara kerja umum kedua alat. Bila pip install geopandas berhasil di komputer Anda, Anda tidak perlu conda.
Proyek Bersambung I5: peta lokasi usaha
Kita satukan semuanya: membaca lima lapisan, mengubahnya ke UTM, menyimpannya sebagai satu GeoPackage berisi lima lapisan, merangkum luas per sektor, dan membuat peta cepat dengan .plot(). GeoPackage ini bahan untuk Bab 40 (PostGIS dan QGIS).
07_proyek_peta_lokasi.py
# Bab 38 - Proyek bersambung I5: peta lokasi usaha, disimpan ke GeoPackage
from pathlib import Path
import geopandas as gpd
import matplotlib.pyplot as plt
tmp = Path(__file__).parent / "_tmp"
tmp.mkdir(exist_ok=True)
lapisan = ["petak", "kolam", "kandang", "sungai", "dusun"]
data = {nama: gpd.read_file(f"data/{nama}.geojson").to_crs(32748) for nama in lapisan}
# Satu berkas GeoPackage berisi banyak lapisan (layer)
gpkg = tmp / "lokasi_usaha.gpkg"
gpkg.unlink(missing_ok=True)
for nama, gdf in data.items():
gdf.to_file(gpkg, layer=nama, driver="GPKG")
print("lapisan di dalam GeoPackage:", gpd.list_layers(gpkg)["name"].tolist())
# Ringkasan per sektor: luas petak dan jumlah titik
petak = data["petak"]
print("\nluas per sektor (ha):")
luas_ha = petak.assign(luas_ha=petak.area / 10_000)
print(luas_ha.groupby("sektor")["luas_ha"].sum().round(2).to_string())
print("jumlah kolam:", len(data["kolam"]), "| jumlah kandang:", len(data["kandang"]))
# Peta cepat dengan .plot()
fig, ax = plt.subplots(figsize=(7, 5.6))
petak.plot(ax=ax, column="sektor", legend=True, alpha=0.6, edgecolor="black")
data["sungai"].plot(ax=ax, color="tab:blue", linewidth=2)
data["kolam"].plot(ax=ax, color="navy")
data["kandang"].plot(ax=ax, color="red", markersize=40)
ax.set_title("Peta lokasi usaha (data tiruan)")
fig.savefig(tmp / "peta_cepat.png", dpi=100)
print("peta tersimpan:", (tmp / "peta_cepat.png").name)lapisan di dalam GeoPackage: ['petak', 'kolam', 'kandang', 'sungai', 'dusun']
luas per sektor (ha):
sektor
hutan 15.93
kebun 7.00
sawah 5.25
jumlah kolam: 4 | jumlah kandang: 3
peta tersimpan: peta_cepat.png
gpd.list_layers membuktikan satu berkas memuat lima lapisan. Peta cepatnya tersimpan di _tmp/peta_cepat.png (peta yang lebih rapi kita buat di Bab 40).
Latihan
- Di
01_geometri_shapely.py, buat poligon segitiga dengan titik (0,0), (100,0), (0,60) dan cetak luasnya. (Cocokkan dengan rumus setengah alas kali tinggi.) - Di
04_luas_jarak.py, tambahkan kolomjarak_sungai_muntuk kandang dan cetak kandang yang paling jauh dari sungai. - Di
05_buffer_overlay.py, ganti 60 m menjadi 100 m. Petak mana saja yang masuk, dan berapa luas yang masuk di S2? - Tantangan: hitung, untuk setiap dusun, luas total petak yang berada di dalamnya memakai
overlay(petak, dusun, how="intersection")dangroupby. Petak H2 melintasi batas dusun: periksa bahwa jumlah luasnya tetap 6,93 ha.
Ringkasan Bab
- Data vektor = titik, garis, poligon. Shapely menghitung bentuknya; GeoPandas menaruhnya dalam tabel.
- Pilih GeoPackage untuk data baru; Shapefile memotong nama kolom dan butuh banyak berkas.
- CRS menentukan arti koordinat. Untuk luas dan jarak, ubah ke CRS bermeter (mis. UTM) dengan
to_crs. area,distance,buffer,overlay,sjoin, dansjoin_nearestmenjawab pertanyaan lapangan tentang letak.- CRS berbeda pada
sjoinatauoverlayhanya memberi peringatan dan hasil keliru: waspada. - pip cukup bila ada wheel; conda-forge lebih andal untuk GDAL (belum diuji di sini).
Selanjutnya, di Bab 39: data raster: foto satelit tiruan, piksel, dan NDVI.