BAB 15: Lemari Data yang Patuh: Atribut, Seleksi, Digitasi, dan Laporan dengan Skrip
Studi kasus: "Ada berapa petak, dan jenis apa saja?" #
Kepala Seksi ingin ringkasan cepat: berapa petak di KPH Contoh, dan tanaman apa saja yang tumbuh di sana? Anda bisa membuka tabel atribut lalu menghitung sendiri. Dengan skrip, jawabannya muncul dalam sekali jalan. Pekan depan permintaannya bertambah: pilih jati yang sudah tua, gambar jalan baru, gabungkan hasil inventarisasi, lalu buat rekap. Tujuh skrip di bab ini menjawab semuanya, dan Anda tahu bagian mana yang diganti untuk data lain.
Konsep: tabel atribut dalam tiga kalimat #
Setiap layer vektor punya tabel di belakangnya, seperti lemari arsip. Tiap laci adalah satu fitur (satu petak, satu titik, atau satu jalan di peta), dan tiap lembar keterangan di dalam laci adalah satu kolom. Skrip PyQGIS membuka lemari itu, membaca laci satu per satu, lalu mengambil kolom yang Anda minta.

Empat istilah yang perlu Anda kenal sekarang:
- Fitur (feature): satu objek di layer, misalnya satu petak.
- Kolom (field): satu jenis keterangan, misalnya
JENISatauLUAS_LAMA_HA. QgsVectorLayer: perkakas QGIS yang membuka sebuah layer vektor dari berkas.import: perintah untuk memanggil perkakas yang tidak ikut otomatis. Cara kerjanya dibahas lengkap di Bab 8.
Setelah bisa membaca lemari, Anda akan memerintahnya. Enam istilah berikut muncul bertahap di skrip berikutnya, dan masing-masing dijelaskan di tempatnya:
- Seleksi: menandai sebagian fitur, seperti memberi stabilo pada beberapa baris.
- Digitasi: menambah atau mengubah gambar fitur, di sini lewat koordinat yang diketik.
- Topologi: aturan "rapi" antar bentuk, misalnya petak tidak boleh tumpang tindih.
- Kalkulasi kolom: mengisi kolom baru dengan rumus.
- Join: menempelkan kolom dari tabel lain berdasarkan kolom kunci yang sama.
- Rekap: ringkasan akhir untuk dibaca atasan.
Peta jalan bab ini:
| Skrip | Berkas | Pekerjaannya |
|---|---|---|
| 15.1 | b2_01_atribut.py | Membaca tabel atribut |
| 15.2 | b2_02_seleksi.py | Memilih fitur dengan ekspresi dan lokasi |
| 15.3 | b2_03_digitasi.py | Menggambar jalan dan persemaian, membelah petak |
| 15.4 | b2_04_topologi.py | Memeriksa dan memperbaiki geometri |
| 15.5 | b2_05_kalkulasi.py | Menambah kolom hitungan |
| 15.6 | b2_06_join.py | Menggabungkan tabel inventarisasi |
| 15.7 | b2_07_laporan.py | Membuat rekap dan mengekspor |
Skrip 15.1 sampai 15.4 berdiri sendiri. Skrip 15.5, 15.6, dan 15.7 harus dijalankan berurutan, karena hasil yang satu menjadi bahan yang berikutnya.
Satu catatan jalur sebelum mulai. Ketujuh skrip membaca lokasi data dari pengaturan B2_DATA, dan skrip yang menulis berkas membaca B2_KELUAR. Bila pengaturan itu tidak ada, jalur cadangan C:/KPH_Contoh/paket-b2 dan C:/KPH_Contoh/keluaran yang dipakai. Anda bisa membuat folder itu, atau mengganti jalur di skripnya.
Skrip 15.1: Membaca tabel atribut #
Berkas b2_01_atribut.py sama dengan yang dipakai di Buku B2. Di dalam berkasnya tertulis "Skrip 1.1" mengikuti penomoran buku itu.
Tujuan. Memeriksa bahwa layer terbaca, menampilkan nama kolom dan jenisnya, melihat tiga petak pertama, lalu merangkum luas dan jenis tanaman.
Bahan. Paket data paket-b2, berkas Petak_KPH.gpkg, layer Petak (16 petak). Skrip dijalankan dari editor skrip QGIS atau dari terminal QGIS, seperti yang Anda siapkan di Bab 3.
Skrip ini bekerja dalam enam tahap. Gambar berikut menunjukkan urutannya, lengkap dengan nomor baris.

Skrip utuh:
# Skrip 1.1: membaca tabel atribut lapisan Petak (QGIS Python Console)
# Penulis: Badar Mubarok Yogaswara
import os
from qgis.core import QgsVectorLayer, QgsProject, QgsDistanceArea, QgsCoordinateTransformContext
DATA = os.environ.get("B2_DATA", r"C:/KPH_Contoh/paket-b2")
petak = QgsVectorLayer(DATA + "/Petak_KPH.gpkg|layername=Petak", "Petak", "ogr")
if not petak.isValid():
raise SystemExit("Lapisan Petak tidak terbaca. Periksa DATA: " + DATA)
print("Valid:", petak.isValid(), "| Jumlah baris:", petak.featureCount())
# Kolom (field) dan tipenya
for fld in petak.fields():
print(fld.name(), "-", fld.typeName())
# Tiga baris pertama
for i, fitur in enumerate(petak.getFeatures()):
print(fitur["KODE_PETAK"], fitur["NAMA_KPH"], fitur["JENIS"], fitur["TAHUN_TANAM"], fitur["STATUS"])
if i == 2:
break
# Ringkasan dasar: jumlah dan rata-rata LUAS_LAMA_HA
nilai = [f["LUAS_LAMA_HA"] for f in petak.getFeatures()]
print("Jumlah LUAS_LAMA_HA:", round(sum(nilai), 2), "| Rata-rata:", round(sum(nilai) / len(nilai), 4))
print("Jenis unik:", sorted(set(f["JENIS"] for f in petak.getFeatures())))Penjelasan baris demi baris. Nomor baris mengikuti berkas di atas.
| Baris | Tulisan | Artinya |
|---|---|---|
| 1-2 | # Skrip 1.1 ... | Komentar: judul dan nama penulis. Python melewatinya. |
| 3 | import os | Panggil perkakas os, yang mengurus hal-hal komputer seperti pengaturan dan jalur folder. |
| 4 | from qgis.core import ... | Panggil empat perkakas dari QGIS. Hanya QgsVectorLayer yang dipakai di skrip ini. Tiga lainnya sisa dan tidak berpengaruh. |
| 6 | DATA = os.environ.get("B2_DATA", r"C:/KPH_Contoh/paket-b2") | Isi kotak DATA dengan lokasi folder data. Python mencari pengaturan bernama B2_DATA lebih dulu. Bila tidak ada, ia memakai jalur cadangan di ujung. Huruf r di depan kutip berarti teks mentah, agar garis miring pada jalur Windows tidak disalahartikan. |
| 7 | petak = QgsVectorLayer(...) | Buka layer dan simpan di kotak petak. Ada tiga isian di dalam kurung. Isian pertama adalah sumber data: lokasi folder disambung dengan + ke nama berkas, lalu tanda garis tegak dan nama layer di dalam berkas (layername=Petak). Isian kedua adalah nama layer di QGIS. Isian ketiga, "ogr", adalah pembaca untuk data vektor. |
| 8-9 | if not petak.isValid(): lalu raise SystemExit(...) | Pengaman. Bila layer gagal dibuka, skrip berhenti di sini dan mencetak pesan beserta isi DATA. SystemExit artinya "hentikan skrip sekarang". Tanpa baris ini, skrip terus berjalan dengan layer kosong dan baru gagal di baris 24. |
| 10 | print("Valid:", ...) | Tampilkan apakah layer berhasil dibuka dan berapa jumlah barisnya. |
| 13-14 | for fld in petak.fields(): | Ulangi satu kali untuk tiap kolom. Di setiap putaran, kotak fld berisi satu kolom, dan baris di bawahnya (yang menjorok ke dalam) mencetak nama serta jenisnya. |
| 17-20 | for i, fitur in enumerate(...) | Ulangi untuk tiap petak. enumerate menambahkan nomor urut i, mulai dari 0. Baris 19-20 berarti: bila i sudah 2 (petak ketiga), berhenti (break). |
| 18 | fitur["KODE_PETAK"] | Ambil isi kolom KODE_PETAK dari petak yang sedang dibaca. Nama kolom ditulis di dalam kurung siku dan kutip. |
| 23 | nilai = [f["LUAS_LAMA_HA"] for f in ...] | Kumpulkan isi kolom LUAS_LAMA_HA dari semua petak ke dalam satu daftar, hanya dengan satu baris. Cara penulisan ini disebut list comprehension (dibahas di Bab 6). |
| 24 | round(sum(nilai), 2) | sum menjumlahkan isi daftar. len menghitung banyak isinya. round(..., 4) membulatkan hasil bagi menjadi empat desimal. |
| 25 | sorted(set(...)) | set membuang isi yang kembar, dan sorted mengurutkannya menurut abjad. |
Hasil. Layar menampilkan (berdasarkan uji pada QGIS 4.0.2):
Valid: True | Jumlah baris: 16
fid - Integer64
KODE_PETAK - String
NAMA_KPH - String
JENIS - String
TAHUN_TANAM - Integer
STATUS - String
LUAS_LAMA_HA - Real
P-01 KPH Alpha Jati 2008 Produksi
P-02 KPH Alpha Jati 2008 Produksi
P-03 KPH Beta Mahoni 2012 Produksi
Jumlah LUAS_LAMA_HA: 16.0 | Rata-rata: 1.0
Jenis unik: ['Akasia', 'Jati', 'Mahoni', 'Sengon']Dari hasil ini Anda tahu: ada 16 petak, ada tujuh kolom, dan empat jenis tanaman.
Bagian yang boleh diubah.
- Baris 6, lokasi data. Ini yang paling sering Anda ubah. Ganti jalur di ujung dengan lokasi folder
paket-b2di komputer Anda, misalnyar"D:/Latihan/paket-b2". Gunakan garis miring biasa/agar aman di semua sistem. - Baris 7, nama berkas dan layer. Ganti
Petak_KPH.gpkgdanlayername=Petakuntuk membuka layer lain. Contoh:Titik_Plot.gpkg|layername=Titik_Plot. - Baris 19, angka 2. Angka ini menentukan berapa petak yang dicetak. Ganti menjadi
4untuk menampilkan lima petak, karena nomor urut mulai dari 0. - Baris 18, nama kolom. Ganti dengan kolom lain dari hasil baris 13-14. Salin nama persis seperti di daftar itu. Di QGIS 4.0.2,
fitur["jenis"]tetap terbaca walau hurufnya kecil semua, tetapifields().indexOf("jenis")mengembalikan -1. Menyalin nama persis menghindari kejutan itu. - Baris 23-25, kolom hitungan.
LUAS_LAMA_HAboleh diganti kolom angka lain, danJENISboleh diganti kolom teks lain.
Jebakan.
- Jalur salah. Bila baris 6 menunjuk folder yang tidak ada, baris 8-9 menghentikan skrip dengan pesan
Lapisan Petak tidak terbaca. Periksa DATA: ..., diikuti jalur yang Anda isi. Pesan itu muncul sebelum ada hasil apa pun. Pelajarannya: baca pesan paling atas lebih dulu, lalu periksa jalur di baris 6. Bila baris 8-9 dihapus, layer yang gagal dibuka melaporkan jumlah baris -2 (artinya "tidak diketahui"). Skrip lalu terus berjalan dan berhenti di baris 24 denganZeroDivisionError, pesan yang tidak menunjuk ke penyebab sebenarnya. - Satuan tidak dicek. Angka
16.0dan1.0hanyalah isi kolom. Skrip tidak tahu apakah satuannya hektare atau meter persegi. Cek arti kolom pada dokumentasi data. - Nama kolom salah ketik. Nama yang benar-benar salah, misalnya
JNS, menimbulkanKeyError. Salin nama kolom langsung dari hasil baris 13-14, termasuk huruf besarnya.
Latihan variasi.
- Tampilkan lima petak pertama, bukan tiga.
- Tampilkan daftar status yang ada (
STATUS), bukan jenis tanaman.
Contoh jawaban, hanya bagian yang berubah:
if i == 4:
break
print("Status unik:", sorted(set(f["STATUS"] for f in petak.getFeatures())))Layar menampilkan lima petak, lalu Status unik: ['Lindung', 'Produksi', 'Rehabilitasi'].
Skrip 15.2: Memilih fitur dengan seleksi #
Seleksi itu seperti memberi stabilo kuning pada baris-baris tertentu di tabel. Fitur yang disorot ikut tampil terpilih di peta. Skrip ini mencoba tujuh cara menyorot, dari yang paling sederhana sampai yang memakai lokasi. Berkasnya b2_02_seleksi.py; di dalamnya tertulis "Skrip 2.1".
Tujuan. Memilih petak dan plot dengan syarat, membalik pilihan, memilih menurut lokasi, mempersempit pilihan, lalu menyaring layer.
Bahan. Paket data paket-b2: layer Petak (16 petak) di Petak_KPH.gpkg dan layer Titik_Plot (24 titik pengukuran) di Titik_Plot.gpkg. Skrip memakai processing, alat bawaan Konsol Python QGIS. Cara menjalankannya ada di Bab 3.
Istilah baru di skrip ini:
- seleksi.
- ekspresi: Kalimat syarat yang dipahami QGIS.
- fungsi:
def, dibahas di Bab 7. - alat:
processing.run. - filter layer: Menyembunyikan baris yang tidak memenuhi syarat.

Skrip utuh:
# Skrip 2.1: query dan seleksi pada lapisan Petak dan Titik_Plot (QGIS Python Console)
# Penulis: Badar Mubarok Yogaswara
import os
import processing
from qgis.core import Qgis, QgsVectorLayer
DATA = os.environ.get("B2_DATA", r"C:/KPH_Contoh/paket-b2")
petak = QgsVectorLayer(DATA + "/Petak_KPH.gpkg|layername=Petak", "Petak", "ogr")
plot = QgsVectorLayer(DATA + "/Titik_Plot.gpkg|layername=Titik_Plot", "Titik_Plot", "ogr")
def kode_terpilih(lyr):
return sorted(f["KODE_PETAK"] for f in lyr.selectedFeatures())
# 1. Seleksi dengan ekspresi: jati yang ditanam sebelum 2010
petak.selectByExpression("\"JENIS\" = 'Jati' AND \"TAHUN_TANAM\" < 2010")
print("Jati sebelum 2010:", petak.selectedFeatureCount(), kode_terpilih(petak))
# 2. Seleksi gabungan: status Produksi DAN bukan Jati
petak.selectByExpression("\"STATUS\" = 'Produksi' AND \"JENIS\" <> 'Jati'")
print("Produksi bukan Jati:", petak.selectedFeatureCount())
# 3. Memakai LIKE dan ATAU
petak.selectByExpression("\"JENIS\" LIKE 'S%' OR \"STATUS\" = 'Lindung'")
print("Jenis berawalan S atau Lindung:", petak.selectedFeatureCount(), kode_terpilih(petak))
# 4. Balikkan seleksi
petak.invertSelection()
print("Setelah dibalik:", petak.selectedFeatureCount())
petak.removeSelection()
# 5. Seleksi menurut lokasi: plot yang berada di dalam petak
hasil = processing.run("native:selectbylocation", {
"INPUT": plot, "PREDICATE": [6], "INTERSECT": petak, "METHOD": 0}) # 6 = within
print("Plot di dalam petak:", plot.selectedFeatureCount(), "dari", plot.featureCount())
plot.selectByExpression("\"ID_PLOT\" IN ('PL-23','PL-24')")
print("Plot di luar (cek):", sorted(f["ID_PLOT"] for f in plot.selectedFeatures()))
# 6. Plot sehat dan berdiameter 30 cm ke atas, di dalam petak
plot.removeSelection()
processing.run("native:selectbylocation", {"INPUT": plot, "PREDICATE": [6], "INTERSECT": petak, "METHOD": 0})
dalam = plot.selectedFeatureCount()
plot.selectByExpression("\"KONDISI\" = 'Sehat' AND \"DIAMETER_CM\" >= 30", Qgis.SelectBehavior.IntersectSelection) # persempit seleksi yang ada
print("Sehat, diameter >= 30, di dalam petak:", plot.selectedFeatureCount(), "(dari", dalam, "plot di dalam petak)")
# 7. Filter lapisan (Provider Feature Filter): hanya Jati yang tampil
petak.removeSelection()
petak.setSubsetString("\"JENIS\" = 'Jati'")
print("Setelah filter Jati, baris yang tampil:", petak.featureCount())
petak.setSubsetString("")
print("Filter dilepas, baris:", petak.featureCount())Penjelasan baris demi baris.
| Baris | Tulisan | Artinya |
|---|---|---|
| 1-2 | # Skrip 2.1 ... | Komentar: judul dan penulis. |
| 3-5 | import os, import processing, from qgis.core import ... | Panggil perkakas: os untuk pengaturan, processing untuk alat QGIS, serta Qgis dan QgsVectorLayer dari QGIS. |
| 7 | DATA = os.environ.get(...) | Sama dengan Skrip 15.1: lokasi data dari pengaturan B2_DATA, atau jalur cadangan. |
| 8-9 | petak = ..., plot = ... | Buka dua layer dan simpan di kotak petak dan plot. |
| 12-13 | def kode_terpilih(lyr): | Buat fungsi kecil bernama kode_terpilih. Ia mengambil kode petak dari fitur yang terpilih, lalu mengurutkannya. Fungsi ini dipakai di baris 18 dan 26 supaya tidak menulis hal yang sama dua kali. |
| 17 | petak.selectByExpression(...) | Pilih petak yang memenuhi syarat: jenis Jati dan tahun tanam sebelum 2010. Nama kolom diapit kutip ganda, nilai teks diapit kutip tunggal. Tanda \" adalah cara menulis kutip ganda di dalam teks yang juga berkutip ganda. |
| 18 | print("Jati sebelum 2010:", ...) | Tampilkan banyaknya petak terpilih dan kodenya. |
| 21-22 | ... <> 'Jati' | Syarat gabungan: status Produksi dan jenis bukan Jati. Tanda <> berarti "tidak sama dengan". |
| 25-26 | LIKE 'S%' OR ... | LIKE 'S%' berarti "diawali huruf S" (Sengon). OR berarti "atau": yang berstatus Lindung ikut terpilih. |
| 29-31 | invertSelection(), removeSelection() | Balikkan pilihan (yang terpilih jadi tidak, dan sebaliknya), cetak jumlahnya, lalu lepas semua pilihan. |
| 34-35 | processing.run("native:selectbylocation", {...}) | Jalankan alat "pilih menurut lokasi". Kamus di dalam kurung berisi isian alat: INPUT (yang dipilih), PREDICATE (hubungan lokasi), INTERSECT (pembanding), dan METHOD (cara memilih). Angka [6] berarti "berada di dalam" (within). Angka 0 berarti pilihan baru. |
| 36 | plot.selectedFeatureCount() | Hitung plot yang terpilih, yaitu plot yang berada di dalam petak. |
| 37-38 | plot.selectByExpression(... IN (...)) | Pilih dua plot dengan kodenya, lalu cetak: inilah plot yang berada di luar petak. |
| 41-43 | plot.removeSelection(), lalu alat yang sama | Lepas pilihan, ulangi pemilihan menurut lokasi, lalu simpan jumlah plot di dalam petak ke kotak dalam. |
| 44 | ..., Qgis.SelectBehavior.IntersectSelection | Pilih lagi dengan syarat sehat dan diameter 30 cm ke atas, tetapi hanya di antara yang sudah terpilih. Itulah arti IntersectSelection: irisan. |
| 45 | print(... , "(dari", dalam, ...) | Tampilkan hasil dan pembandingnya. |
| 48 | petak.removeSelection() | Lepas pilihan petak. |
| 49 | petak.setSubsetString(...) | Pasang filter: hanya baris Jati yang tampil. Berbeda dari seleksi, filter benar-benar menyembunyikan baris lain. |
| 51 | petak.setSubsetString("") | Kosongkan filter, sehingga semua baris tampil lagi. |
Hasil. Layar menampilkan (uji pada QGIS 4.0.2):
Jati sebelum 2010: 4 ['P-01', 'P-02', 'P-05', 'P-13']
Produksi bukan Jati: 8
Jenis berawalan S atau Lindung: 6 ['P-04', 'P-06', 'P-07', 'P-11', 'P-13', 'P-15']
Setelah dibalik: 10
Plot di dalam petak: 22 dari 24
Plot di luar (cek): ['PL-23', 'PL-24']
Sehat, diameter >= 30, di dalam petak: 5 (dari 22 plot di dalam petak)
Setelah filter Jati, baris yang tampil: 5
Filter dilepas, baris: 16Cocokkan: 6 terpilih dibalik menjadi 10, dan 6 + 10 = 16 petak. Plot di dalam petak 22, ditambah 2 di luar, sama dengan 24 plot.
Bagian yang boleh diubah.
- Baris 17, 21, 25, dan 44: syarat seleksi. Ganti jenis, tahun, status, atau ambang diameter. Contoh:
"JENIS" = 'Mahoni'. - Baris 35 dan 42, angka predikat. Angka 6 berarti "di dalam". Dari uji di komputer ini:
0(bersinggungan atau beririsan) memilih 22 plot, dan2(terpisah) memilih 2 plot, yaitu plot yang di luar petak. Nomor ini hanya berlaku untuk alatselectbylocation. - Baris 35,
METHOD. Dari uji:0pilihan baru,1tambahkan ke pilihan,2ambil irisan dengan pilihan sekarang,3buang dari pilihan. [CEK] pada versi QGIS Anda. - Baris 49, filter. Ganti
'Jati'dengan jenis lain.
Jebakan.
- Nama kolom salah tidak menimbulkan galat. Menulis
"TAHUN" < 2010padahal kolomnyaTAHUN_TANAMtidak memunculkan pesan apa pun. Hasilnya hanya0petak terpilih. Bila hasil seleksi 0 yang tidak Anda duga, periksa dulu ejaan kolom. - Huruf besar dan kecil pada nilai.
"JENIS" = 'jati'memilih 0 petak, karena nilainya tertulisJati. - Kode plot tertulis tetap. Baris 37 memakai
PL-23danPL-24, yang cocok hanya dengan data latihan ini. Untuk data lain, ambil plot di luar petak dengan alat yang sama dan predikat2. - Filter menempel. Bila skrip berhenti sebelum baris 51, filter Jati masih terpasang pada layer (uji:
featureCounttinggal 5 dari 16). Selalu kosongkan filter setelah selesai. - Hasil
processing.runbaris 34 tidak dipakai. Variabelhasilhanya sisa. Pilihannya sudah menempel pada layerplot.
Latihan variasi.
- Pilih petak berjenis Mahoni dengan status Produksi, lalu cetak kodenya.
- Cetak kode plot yang berada di luar petak, tanpa menulis
PL-23danPL-24.
Contoh jawaban, hanya bagian yang berubah:
petak.selectByExpression("\"JENIS\" = 'Mahoni' AND \"STATUS\" = 'Produksi'")
print(kode_terpilih(petak))
processing.run("native:selectbylocation", {"INPUT": plot, "PREDICATE": [2], "INTERSECT": petak, "METHOD": 0})
print(sorted(f["ID_PLOT"] for f in plot.selectedFeatures()))Layar menampilkan ['P-03', 'P-10', 'P-14'] dan ['PL-23', 'PL-24'].
Skrip 15.3: Menggambar dan membelah dengan koordinat #
Biasanya Anda menggambar jalan dengan mouse. Skrip ini menggambarnya dengan koordinat yang diketik, seperti memberi tahu tukang gambar titik demi titik. Berkasnya b2_03_digitasi.py; di dalamnya tertulis "Skrip 3.1".
Tujuan. Menggambar satu jalan (garis) dan satu persemaian (poligon), lalu membelah petak P-12 menjadi dua, pada salinan data agar berkas asli aman.
Bahan. Paket data paket-b2: Jalan_Baru.gpkg, Persemaian_Baru.gpkg, Petak_KPH.gpkg, dan citra Citra_Sketsa.tif. Skrip membuat folder keluaran sendiri (B2_KELUAR) dan menulis salinannya di sana.
Istilah baru di skrip ini:
- digitasi.
- simpul: vertex, titik sudut sebuah garis atau poligon.
- geometri: Bentuk fitur.
- commit: Menyimpan hasil edit ke berkas.
- koordinat UTM: Angka posisi dalam meter.


Skrip utuh:
# Skrip 3.1: menggambar (digitasi) jalan dan persemaian lewat Python, lalu membelah satu petak (QGIS Python Console)
# Penulis: Badar Mubarok Yogaswara
# Hasilnya sama dengan yang Anda kerjakan dengan mouse di Bab 3, tetapi simpul diketik sebagai koordinat.
import os
import shutil
from qgis.core import (QgsVectorLayer, QgsRasterLayer, QgsFeature, QgsGeometry, QgsPointXY, QgsPoint, QgsLineString)
DATA = os.environ.get("B2_DATA", r"C:/KPH_Contoh/paket-b2")
KELUAR = os.environ.get("B2_KELUAR", r"C:/KPH_Contoh/keluaran")
os.makedirs(KELUAR, exist_ok=True)
X0, Y0 = 312000.0, 9996000.0 # sudut barat-daya area contoh
# Salin lapisan kosong agar berkas asli di paket data tidak berubah
for nama in ("Jalan_Baru.gpkg", "Persemaian_Baru.gpkg", "Petak_KPH.gpkg"):
shutil.copy(os.path.join(DATA, nama), os.path.join(KELUAR, nama))
jalan = QgsVectorLayer(KELUAR + "/Jalan_Baru.gpkg|layername=Jalan_Baru", "Jalan_Baru", "ogr")
semai = QgsVectorLayer(KELUAR + "/Persemaian_Baru.gpkg|layername=Persemaian_Baru", "Persemaian_Baru", "ogr")
petak = QgsVectorLayer(KELUAR + "/Petak_KPH.gpkg|layername=Petak", "Petak", "ogr")
citra = QgsRasterLayer(DATA + "/Citra_Sketsa.tif", "Citra_Sketsa")
def titik(x, y):
return QgsPointXY(X0 + x, Y0 + y)
# 1. Jalan (garis): 6 simpul, diukur dari sudut barat-daya (meter)
simpul_jalan = [(0, 150), (90, 160), (180, 210), (260, 215), (330, 260), (400, 275)]
# Pemeriksaan: warna citra di tiap simpul harus cokelat (jalan tanah), bukan hijau
for x, y in simpul_jalan:
xs = min(x, 399) # simpul di tepi citra digeser 1 m ke dalam agar terbaca
nilai = [citra.dataProvider().sample(titik(xs, y), b)[0] for b in (1, 2, 3)]
print("Citra di simpul", (x, y), "RGB =", [int(v) for v in nilai])
jalan.startEditing()
f = QgsFeature(jalan.fields())
f.setGeometry(QgsGeometry.fromPolylineXY([titik(x, y) for x, y in simpul_jalan]))
f["NAMA_JALAN"] = "Jalan Inspeksi Timur"
f["KELAS"] = "Jalan tanah"
f["PANJANG_M"] = round(f.geometry().length(), 1)
jalan.addFeature(f)
print("Commit jalan:", jalan.commitChanges(), "| panjang (m):", round(f.geometry().length(), 1))
# 2. Persemaian (poligon): 5 simpul
simpul_semai = [(220, 25), (280, 20), (290, 70), (250, 85), (215, 65)]
semai.startEditing()
g = QgsGeometry.fromPolygonXY([[titik(x, y) for x, y in simpul_semai]])
f = QgsFeature(semai.fields())
f.setGeometry(g)
f["NAMA"] = "Persemaian Utara"
f["LUAS_M2"] = round(g.area(), 1)
semai.addFeature(f)
print("Commit persemaian:", semai.commitChanges(), "| luas (m2):", round(g.area(), 1), "| ha:", round(g.area() / 10000, 4))
# 3. Membelah petak P-12 dengan garis tegak x = 350 (pilih P-12 lebih dulu supaya petak lain tidak ikut terbelah)
petak.selectByExpression("\"KODE_PETAK\" = 'P-12'")
if petak.selectedFeatureCount() == 0:
raise SystemExit("P-12 tidak ditemukan; pembelahan dibatalkan agar petak lain tidak ikut terbelah.")
luas_awal = sum(ft.geometry().area() for ft in petak.selectedFeatures())
petak.startEditing()
garis_belah = QgsLineString([QgsPoint(X0 + 350, Y0 + 190), QgsPoint(X0 + 350, Y0 + 310)])
hasil, _ = petak.splitFeatures(garis_belah, False, False)
print("Hasil belah:", hasil)
print("Jumlah petak setelah belah (sebelum commit):", petak.featureCount())
print("Commit petak:", petak.commitChanges())
print("Jumlah petak setelah commit:", petak.featureCount())
for ft in petak.getFeatures():
if ft["KODE_PETAK"] == "P-12":
print("P-12 bagian:", ft.id(), round(ft.geometry().area(), 1), "m2")
sisa = sum(ft.geometry().area() for ft in petak.getFeatures() if ft["KODE_PETAK"] == "P-12")
print("Luas P-12 sebelum:", round(luas_awal, 1), "| jumlah dua bagian:", round(sisa, 1))Penjelasan baris demi baris.
| Baris | Tulisan | Artinya |
|---|---|---|
| 1-3 | # Skrip 3.1 ... | Komentar: judul, penulis, dan catatan bahwa hasilnya sama dengan kerja manual, hanya simpulnya diketik. |
| 4-6 | import os, import shutil, from qgis.core import (...) | Panggil os dan shutil (menyalin berkas), lalu tujuh perkakas dari QGIS. Tanda kurung membuat daftar impor boleh ditulis di beberapa baris. |
| 8-9 | DATA = ..., KELUAR = ... | Dua lokasi: folder data asli dan folder hasil. Keduanya dari pengaturan, dengan jalur cadangan. |
| 10 | os.makedirs(KELUAR, exist_ok=True) | Buat folder hasil. exist_ok=True berarti: bila sudah ada, tidak apa-apa. |
| 11 | X0, Y0 = 312000.0, 9996000.0 | Dua kotak diisi sekaligus: titik acuan di sudut barat-daya area contoh. Semua simpul dihitung dari titik ini. |
| 14-15 | for nama in (...): shutil.copy(...) | Salin tiga berkas GeoPackage dari folder data ke folder hasil. Setelah ini, semua edit terjadi di salinan. |
| 17-19 | jalan, semai, petak | Buka tiga layer dari salinan di folder hasil. |
| 20 | QgsRasterLayer(...) | Buka citra sketsa. Citra adalah raster, bukan vektor. Bab 16 membahasnya. |
| 23-24 | def titik(x, y): | Fungsi pembantu: ubah jarak dari titik acuan (meter) menjadi titik koordinat sebenarnya. |
| 28 | simpul_jalan = [(0, 150), ...] | Daftar enam simpul jalan. Tiap simpul adalah pasangan (jarak ke timur, jarak ke utara). Pasangan seperti ini disebut tuple. |
| 30-33 | for x, y in simpul_jalan: | Untuk tiap simpul, baca tiga angka warna citra di sana (merah, hijau, biru) lalu cetak. Baris 31 menggeser simpul di tepi paling kanan 1 meter ke dalam, supaya masih terbaca. |
| 35 | jalan.startEditing() | Buka mode edit pada layer jalan. |
| 36-37 | QgsFeature(...), setGeometry(...) | Buat fitur kosong, lalu isi bentuknya dengan garis dari keenam simpul. |
| 38-40 | f["NAMA_JALAN"] = ... | Isi tiga kolom: nama, kelas, dan panjang (dibulatkan satu desimal). |
| 41-42 | jalan.addFeature(f), commitChanges() | Tambahkan fitur ke layer, lalu simpan. commitChanges() mengembalikan True bila berhasil, dan nilainya dicetak. |
| 45-53 | simpul_semai sampai commitChanges() | Pola yang sama untuk persemaian, tetapi bentuknya poligon dari lima simpul. Kolom LUAS_M2 diisi dari luas bentuk. |
| 56 | petak.selectByExpression(...) | Pilih petak P-12 saja. Pilihan ini penting, karena pembelahan hanya berlaku untuk fitur yang terpilih. |
| 57-58 | if petak.selectedFeatureCount() == 0: lalu raise SystemExit(...) | Pengaman. Bila tidak ada petak terpilih (misalnya kode P-12 tidak ada di data), skrip berhenti dengan pesan. Tanpa baris ini, garis belah bisa membelah petak lain yang dilewatinya. SystemExit artinya "hentikan skrip sekarang" (lihat Skrip 15.1). |
| 59 | luas_awal = sum(...) | Jumlahkan luas fitur terpilih sebelum dibelah. |
| 60-61 | petak.startEditing(), garis_belah = ... | Buka mode edit petak. Siapkan garis belah tegak lurus pada jarak 350 meter ke timur. |
| 62 | hasil, _ = petak.splitFeatures(...) | Belah fitur yang terpilih dengan garis itu. Alat ini mengembalikan dua nilai. Nilai pertama 0 berarti berhasil. Garis bawah _ menampung nilai kedua yang tidak dipakai. |
| 63-66 | print(...), commitChanges() | Cetak hasil belah dan jumlah petak sebelum simpan. Lalu simpan (baris 65) dan cetak hasilnya, True bila berhasil. Terakhir, cetak jumlah petak sesudah simpan. |
| 67-69 | for ft in petak.getFeatures(): | Cetak nomor dan luas dua bagian P-12. |
| 70-71 | sisa = ..., print(...) | Jumlahkan kedua bagian dan bandingkan dengan luas awal. |
Hasil. Layar menampilkan (uji pada QGIS 4.0.2):
Citra di simpul (0, 150) RGB = [150, 118, 78]
Citra di simpul (90, 160) RGB = [150, 118, 78]
Citra di simpul (180, 210) RGB = [150, 118, 78]
Citra di simpul (260, 215) RGB = [150, 118, 78]
Citra di simpul (330, 260) RGB = [150, 118, 78]
Citra di simpul (400, 275) RGB = [150, 118, 78]
Commit jalan: True | panjang (m): 428.5
Commit persemaian: True | luas (m2): 3700.0 | ha: 0.37
Hasil belah: 0
Jumlah petak setelah belah (sebelum commit): 17
Commit petak: True
Jumlah petak setelah commit: 17
P-12 bagian: 12 4906.9 m2
P-12 bagian: 17 4443.1 m2
Luas P-12 sebelum: 9350.0 | jumlah dua bagian: 9350.0Keenam warna sama, [150, 118, 78], yaitu cokelat: jalan tanah, bukan hijau. Luas dua bagian P-12 bila dijumlah sama dengan luas awal, 9350 meter persegi. Itu bukti pembelahan tidak menghilangkan bagian apa pun.
Bagian yang boleh diubah.
- Baris 11, titik acuan. Harus cocok dengan lokasi data Anda. Salah angka berarti gambar jatuh di tempat lain.
- Baris 28 dan 45, daftar simpul. Tambah, kurangi, atau ubah pasangan angkanya. Jalan butuh minimal dua simpul, poligon minimal tiga.
- Baris 38-39 dan 50, isi kolom. Ganti nama jalan atau nama persemaian.
- Baris 56 dan 61, petak dan garis belah. Ganti
'P-12'dan angka350. Garis belah harus benar-benar memotong petak yang dipilih. Bila Anda mengganti kode petak, pesan di baris 58 masih menyebut P-12; ubah juga agar tidak membingungkan.
Jebakan.
- Jalur data salah. Bila
B2_DATAmenunjuk folder yang tidak ada, skrip berhenti di baris 15:FileNotFoundError: [Errno 2] No such file or directory: 'C:/tidak_ada\\Jalan_Baru.gpkg'Perhatikan jalur dalam pesan itu: garis miring biasa dan terbalik bercampur. Itu normal, jangan bingung. - Salinan ditimpa tiap kali dijalankan.
shutil.copymenimpa tanpa bertanya. Dari uji: dijalankan dua kali berturut-turut, hasilnya tetap 17 petak, karena salinan dibuat ulang dari data asli. Bahayanya muncul bila skrip lain memakai folder hasil yang sama. - Tidak ada petak terpilih. Bila kode di baris 56 tidak ditemukan (misalnya salah ketik atau data lain), baris 57-58 menghentikan skrip dengan pesan
P-12 tidak ditemukan; pembelahan dibatalkan .... Ini disengaja. Tanpa pengaman itu,luas_awalmenjadi0, dan garis belah bisa membelah setiap petak yang dilewatinya. Pada data bawaan, garis hanya memotong P-12, tetapi pada data lain hasilnya bisa lebih banyak petak yang terbelah. Itulah alasan komentar di baris 55. - Lupa
commitChanges(). Fitur baru hanya ada di memori dan hilang bila QGIS ditutup (uji: berkas dibaca ulang berisi 0 fitur). - Koordinat harus dalam meter. Layer ini memakai UTM (EPSG:32749), jadi luas bernilai meter persegi. Pada koordinat derajat, angka luas tidak berarti.
- Hasil commit tetap perlu dibaca. Baris 42, 53, dan 65 mencetak hasil
commitChanges(). Bila tertulisFalse, perubahan tidak tersimpan. Jangan lanjut sebelum penyebabnya jelas.
Latihan variasi.
- Geser garis belah ke 330 meter. Berapa luas dua bagian P-12?
- Ganti nama jalan menjadi
Jalan Inspeksi Barat, lalu cetak isi kolom jalan dari layer salinan.
Contoh jawaban. Untuk nomor 1, ganti angka 350 di baris 61 menjadi 330 (dua tempat). Layar menampilkan bagian 4906.9 dan 4443.1 berubah menjadi 6817.6 dan 2532.4 meter persegi, dengan jumlah tetap 9350. Untuk nomor 2, ganti teks di baris 38, lalu tambahkan di akhir skrip:
for ft in jalan.getFeatures():
print(ft["NAMA_JALAN"], ft["KELAS"], ft["PANJANG_M"])Layar menampilkan Jalan Inspeksi Barat Jalan tanah 428.5 pada baris terakhir.
Skrip 15.4: Memeriksa dan memperbaiki geometri #
Sebuah peta yang rapi punya aturan: bentuk harus sah, tidak ganda, tidak bertindihan, dan tidak berlubang. Memeriksanya satu per satu dengan mata sangat melelahkan. Skrip ini memeriksanya dengan alat. Berkasnya b2_04_topologi.py; di dalamnya tertulis "Skrip 4.1".
Tujuan. Menemukan dan memperbaiki geometri tidak sah, petak ganda, dan petak tumpang tindih pada layer latihan Petak_Rusak, lalu melaporkan celah.
Bahan. Paket data paket-b2, berkas Petak_Rusak.gpkg, layer Petak_Rusak (18 objek; rusaknya disengaja). Semua hasil disimpan di memori, jadi tidak ada berkas keluaran.
Istilah baru di skrip ini:
- geometri sah: Bentuk yang tidak menyilang dirinya sendiri.
- duplikat: Dua fitur yang sama persis.
- tumpang tindih: overlap.
- celah: gap, lubang di antara petak bertetangga.
**param: Cara fungsi menerima isian dengan nama apa saja.

Skrip utuh:
# Skrip 4.1: memeriksa dan memperbaiki galat topologi dasar pada Petak_Rusak (QGIS Python Console)
# Penulis: Badar Mubarok Yogaswara
import os
import processing
from qgis.core import QgsVectorLayer, QgsWkbTypes
DATA = os.environ.get("B2_DATA", r"C:/KPH_Contoh/paket-b2")
KELUAR = os.environ.get("B2_KELUAR", r"C:/KPH_Contoh/keluaran")
os.makedirs(KELUAR, exist_ok=True)
rusak = QgsVectorLayer(DATA + "/Petak_Rusak.gpkg|layername=Petak_Rusak", "Petak_Rusak", "ogr")
print("Jumlah objek Petak_Rusak:", rusak.featureCount())
def jalankan(alg, **param):
return processing.run(alg, param)
# 1. Geometri tidak sah (mis. garis menyilang sendiri)
v = jalankan("native:checkvalidity", INPUT_LAYER=rusak, METHOD=2,
VALID_OUTPUT="memory:", INVALID_OUTPUT="memory:", ERROR_OUTPUT="memory:")
print("Sah:", v["VALID_COUNT"], "| Tidak sah:", v["INVALID_COUNT"], "| Titik galat:", v["ERROR_COUNT"])
for f in v["INVALID_OUTPUT"].getFeatures():
print(" tidak sah:", f["KODE_PETAK"], "|", f["_errors"])
# 2. Perbaiki geometri tidak sah lebih dulu (alat pemeriksa lain menolak geometri rusak)
perbaiki = jalankan("native:fixgeometries", INPUT=rusak, METHOD=1, OUTPUT="memory:")
fix = perbaiki["OUTPUT"]
print("Setelah perbaiki geometri:", fix.featureCount(), "objek")
v2 = jalankan("native:checkvalidity", INPUT_LAYER=fix, METHOD=2,
VALID_OUTPUT="memory:", INVALID_OUTPUT="memory:", ERROR_OUTPUT="memory:")
print("Ulang cek: sah", v2["VALID_COUNT"], "| tidak sah", v2["INVALID_COUNT"])
for f in fix.getFeatures():
if f["KODE_PETAK"] == "P-18":
print(" P-18 setelah perbaikan:", QgsWkbTypes.displayString(f.geometry().wkbType()), "| luas m2:", round(f.geometry().area(), 1))
# 3. Duplikat
d = jalankan("native:checkgeometryduplicate", INPUT=fix, UNIQUE_ID="fid", ERRORS="memory:")
print("Galat duplikat:", d["ERRORS"].featureCount())
# 4. Tumpang tindih (overlap) dan celah (gap)
o = jalankan("native:checkgeometryoverlap", INPUT=fix, UNIQUE_ID="fid", MIN_OVERLAP_AREA=0, ERRORS="memory:")
print("Galat tumpang tindih:", o["ERRORS"].featureCount())
print(" kolom:", [x.name() for x in o["ERRORS"].fields()])
for f in o["ERRORS"].getFeatures():
print(" ", f.attributes()[1:])
g = jalankan("native:checkgeometrygap", INPUT=fix, UNIQUE_ID="fid", GAP_THRESHOLD=5000,
NEIGHBORS="memory:", ERRORS="memory:", OUTPUT="memory:")
print("Galat celah:", g["ERRORS"].featureCount())
print(" kolom:", [x.name() for x in g["ERRORS"].fields()])
for f in g["ERRORS"].getFeatures():
print(" ", f.attributes()[1:])
# 5. Perbaikan duplikat
bersih = jalankan("native:deleteduplicategeometries", INPUT=fix, OUTPUT="memory:", DUPLICATES="memory:")
print("Setelah hapus duplikat:", bersih["OUTPUT"].featureCount(), "objek | duplikat dibuang:", bersih["DUPLICATE_COUNT"])
# 6. Perbaiki tumpang tindih (alat "Delete overlaps") pada lapisan yang sudah bebas duplikat
bersih_l = bersih["OUTPUT"]
o2 = jalankan("native:checkgeometryoverlap", INPUT=bersih_l, UNIQUE_ID="fid", MIN_OVERLAP_AREA=0, ERRORS="memory:")
tanpa_tumpang = jalankan("native:fixgeometryoverlap", INPUT=bersih_l, ERRORS=o2["ERRORS"], UNIQUE_ID="fid",
OVERLAP_FEATURE_UNIQUE_IDX="gc_overlap_feature_fid", ERROR_VALUE_ID="gc_error",
OUTPUT="memory:", REPORT="memory:")["OUTPUT"]
o3 = jalankan("native:checkgeometryoverlap", INPUT=tanpa_tumpang, UNIQUE_ID="fid", MIN_OVERLAP_AREA=0, ERRORS="memory:")
print("Setelah Delete overlaps | objek:", tanpa_tumpang.featureCount(), "| tumpang tindih tersisa:", o3["ERRORS"].featureCount())
for f in tanpa_tumpang.getFeatures():
if f["KODE_PETAK"] in ("P-06", "P-07"):
print(" ", f["KODE_PETAK"], "luas m2:", round(f.geometry().area(), 1))
# Celah: alat "Fill gaps" dicoba pada data latihan dan melaporkan "Failed to merge with neighbor".
# Celah ini diperbaiki dengan tangan (Vertex Tool + snapping), seperti di Bab 4.Penjelasan baris demi baris.
| Baris | Tulisan | Artinya |
|---|---|---|
| 1-5 | komentar dan import | Judul, penulis, dan perkakas: os, processing, QgsVectorLayer, serta QgsWkbTypes untuk menamai jenis bentuk. |
| 7-9 | DATA, KELUAR, os.makedirs(...) | Lokasi data dan folder hasil. Folder hasil dibuat, tetapi skrip ini tidak memakainya. |
| 10-11 | rusak = QgsVectorLayer(...) | Buka layer latihan dan cetak jumlah objeknya. |
| 14-15 | def jalankan(alg, param): | Fungsi pembungkus. alg adalah nama alat. param menampung semua isian bernama, lalu processing.run(alg, param) meneruskannya sebagai kamus. Hasilnya, pemanggilan alat di bawah jadi lebih pendek. |
| 19-20 | jalankan("native:checkvalidity", ...) | Periksa kesahan geometri. Hasil sah, tidak sah, dan titik galat masing-masing dikirim ke layer sementara (memory:). |
| 21 | print("Sah:", v["VALID_COUNT"], ...) | Cetak tiga hitungan dari kamus hasil. |
| 22-23 | for f in v["INVALID_OUTPUT"].getFeatures(): | Cetak kode petak yang tidak sah dan jenis galatnya (kolom _errors). |
| 26-28 | native:fixgeometries | Perbaiki bentuk yang rusak lebih dulu, karena alat pemeriksa berikutnya menolak bentuk rusak (komentar baris 25). Hasilnya disimpan di kotak fix. |
| 29-31 | checkvalidity lagi | Periksa ulang fix: semua harus sah. |
| 32-34 | if f["KODE_PETAK"] == "P-18": | Cetak jenis bentuk dan luas P-18 setelah diperbaiki. |
| 37-38 | native:checkgeometryduplicate | Cari fitur ganda. UNIQUE_ID="fid" memberi tahu alat kolom mana yang unik untuk tiap fitur. |
| 41-45 | native:checkgeometryoverlap | Cari petak yang bertindihan. Baris 43 mencetak nama kolom hasil, dan baris 44-45 mencetak tiap galat tanpa kolom pertama ([1:], potongan daftar). |
| 46-51 | native:checkgeometrygap | Cari celah. GAP_THRESHOLD=5000 berarti: celah dengan luas sampai 5000 meter persegi dilaporkan. |
| 54-55 | native:deleteduplicategeometries | Hapus duplikat dan cetak jumlah objek serta duplikat yang dibuang. |
| 58-62 | native:fixgeometryoverlap | Perbaiki tumpang tindih pada layer bebas duplikat. Isian gc_overlap_feature_fid dan gc_error adalah nama kolom yang dihasilkan alat pemeriksa tadi. |
| 63-64 | checkgeometryoverlap lagi | Periksa ulang: sisa tumpang tindih harus 0. |
| 65-67 | if f["KODE_PETAK"] in ("P-06", "P-07"): | Cetak luas dua petak yang tadinya bertindihan. |
| 68-69 | komentar | Catatan penulis: alat "Fill gaps" gagal pada data latihan, jadi celah diperbaiki dengan tangan. |
Hasil. Layar menampilkan (uji pada QGIS 4.0.2):
Jumlah objek Petak_Rusak: 18
Sah: 17 | Tidak sah: 1 | Titik galat: 1
tidak sah: P-18 | Self-intersection
Setelah perbaiki geometri: 18 objek
Ulang cek: sah 18 | tidak sah 0
P-18 setelah perbaikan: MultiPolygon | luas m2: 450.0
Galat duplikat: 1
Galat tumpang tindih: 1
kolom: ['gc_layerid', 'gc_layername', 'gc_errorx', 'gc_errory', 'gc_error', 'fid', 'gc_overlap_feature_fid']
['output', 312206.0, 9996165.0, 840.0, 7, 6]
Galat celah: 1
kolom: ['gc_layerid', 'gc_layername', 'gc_partidx', 'gc_ringidx', 'gc_vertidx', 'gc_errorx', 'gc_errory', 'gc_error', 'gc_errorid']
['output', -1, -1, -1, 312101.875, 9996266.25, '265', 0]
Setelah hapus duplikat: 17 objek | duplikat dibuang: 1
Setelah Delete overlaps | objek: 17 | tumpang tindih tersisa: 0
P-07 luas m2: 9350.0
P-06 luas m2: 9900.0Cara membacanya. P-18 menyilang dirinya sendiri (Self-intersection), dan setelah diperbaiki luasnya 450 meter persegi. Kolom gc_error pada galat tumpang tindih bernilai 840.0: itu luas irisan dalam meter persegi, antara petak bernomor fid 7 dan 6. Buktinya, P-06 di data awal berluas 10740 dan setelah diperbaiki 9900. Selisihnya tepat 840. Nilai 265 pada galat celah kemungkinan luas celah. [kemungkinan]
Bagian yang boleh diubah.
- Baris 46,
GAP_THRESHOLD. Ambang luas celah. Dari uji: bila diganti100, hasilnyaGalat celah: 0, karena celah di data ini lebih luas daripada 100. - Baris 41, 59, dan 63,
MIN_OVERLAP_AREA. Tumpang tindih yang lebih kecil dari angka ini diabaikan. Nilai0melaporkan semuanya. - Baris 33 dan 66, kode petak yang dicetak. Ganti sesuai petak yang ingin dilihat.
Jebakan.
- Alat pemeriksa menolak bentuk rusak. Dari uji: menjalankan
checkgeometryduplicatelangsung padaPetak_Rusakmemberi pesan:QgsProcessingException: Feature (18) from “r” has invalid geometry. Please fix the geometry or change the “Invalid features filtering” option for this input or globally in Processing settings.Karena itu perbaikan geometri (baris 26) harus lebih dulu. - Hasil hilang saat QGIS ditutup. Semua hasil bertempat di memori. Bila perlu disimpan, tambahkan alat penyimpan seperti di Skrip 15.5.
- Kode petak tertulis tetap.
P-18,P-06, danP-07hanya cocok dengan data latihan. - Celah tidak diperbaiki skrip. Skrip hanya melaporkannya. Perbaikannya dilakukan dengan tangan, seperti di Buku B2.
fiddangc_....fidadalah nomor urut bawaan GeoPackage. Awalangc_kemungkinan singkatan geometry checker, nama kelompok alat ini. [kemungkinan]
Latihan variasi.
- Cetak hanya kode petak yang tidak sah, tanpa jenis galatnya.
- Naikkan
GAP_THRESHOLDmenjadi 10000. Apakah jumlah celah berubah?
Contoh jawaban. Nomor 1: ubah baris 23 menjadi print(f["KODE_PETAK"]). Layar menampilkan P-18. Nomor 2: ganti angka di baris 46. Hasilnya tetap Galat celah: 1, karena celah di data ini hanya satu dan luasnya jauh di bawah ambang itu.
Skrip 15.5: Menambah kolom dengan rumus #
Menghitung luas, umur, dan kelas untuk 16 petak dengan kalkulator itu membosankan. Skrip ini menyuruh QGIS mengisinya sekaligus. Berkas b2_05_kalkulasi.py; di dalamnya tertulis "Skrip 5.1".
Tujuan. Menambah lima kolom (luas, selisih luas, umur, kelas umur, kode lengkap), memeriksa hasilnya, lalu menyimpannya ke Petak_Hitung.gpkg.
Bahan. Petak_KPH.gpkg, layer Petak, dari paket paket-b2. Hasilnya, Petak_Hitung.gpkg, ditulis ke folder B2_KELUAR dan dipakai oleh Skrip 15.6 dan 15.7.
Istilah baru di skrip ini:
- konstanta: Kotak yang isinya tidak diubah, ditulis huruf besar.
- kamus: dict, pasangan nama dan nilai.
- parameter bawaan: Isian yang sudah punya nilai awal.
- docstring: Catatan di bawah
def. - ekspresi QGIS: Rumus di dalam teks.

Skrip utuh:
# Skrip 5.1: kalkulasi field pada lapisan Petak dengan ekspresi (QGIS Python Console)
# Penulis: Badar Mubarok Yogaswara
import os
import processing
from qgis.core import QgsVectorLayer
DATA = os.environ.get("B2_DATA", r"C:/KPH_Contoh/paket-b2")
KELUAR = os.environ.get("B2_KELUAR", r"C:/KPH_Contoh/keluaran")
os.makedirs(KELUAR, exist_ok=True)
petak = QgsVectorLayer(DATA + "/Petak_KPH.gpkg|layername=Petak", "Petak", "ogr")
TEKS, DESIMAL, BULAT = 2, 0, 1 # kode tipe field untuk alat Field calculator
def tambah(lyr, nama, tipe, rumus, panjang=0, presisi=0):
"""Menjalankan alat Field calculator dan mengembalikan lapisan hasil (sementara di memori)."""
return processing.run("native:fieldcalculator", {
"INPUT": lyr, "FIELD_NAME": nama, "FIELD_TYPE": tipe, "FIELD_LENGTH": panjang,
"FIELD_PRECISION": presisi, "FORMULA": rumus, "OUTPUT": "memory:"})["OUTPUT"]
h = tambah(petak, "LUAS_HA", DESIMAL, "round(area($geometry) / 10000, 3)", 10, 3)
h = tambah(h, "SELISIH_HA", DESIMAL, "round(\"LUAS_HA\" - \"LUAS_LAMA_HA\", 3)", 10, 3)
h = tambah(h, "UMUR", BULAT, "2026 - \"TAHUN_TANAM\"")
h = tambah(h, "KELAS_UMUR", TEKS,
"CASE WHEN \"UMUR\" < 10 THEN 'Muda' WHEN \"UMUR\" < 15 THEN 'Sedang' ELSE 'Tua' END", 10)
h = tambah(h, "KODE_LENGKAP", TEKS, "\"NAMA_KPH\" || ' / ' || \"KODE_PETAK\"", 30)
print("Kolom:", [f.name() for f in h.fields()])
for f in h.getFeatures():
print(f["KODE_PETAK"], f["LUAS_HA"], f["LUAS_LAMA_HA"], f["SELISIH_HA"], f["UMUR"], f["KELAS_UMUR"], f["KODE_LENGKAP"])
# Pemeriksaan: jumlah luas tiap KPH dan jumlah petak tiap kelas umur
luas = {}
kelas = {}
for f in h.getFeatures():
luas[f["NAMA_KPH"]] = round(luas.get(f["NAMA_KPH"], 0) + f["LUAS_HA"], 3)
kelas[f["KELAS_UMUR"]] = kelas.get(f["KELAS_UMUR"], 0) + 1
print("Luas per KPH (ha):", luas)
print("Petak per kelas umur:", kelas)
print("Total LUAS_HA:", round(sum(f["LUAS_HA"] for f in h.getFeatures()), 3))
beda = [f["KODE_PETAK"] for f in h.getFeatures() if abs(f["SELISIH_HA"]) > 0.06]
print("Petak dengan selisih lebih dari 0,06 ha:", beda)
# Simpan hasil agar dipakai di Bab 6 dan 7
processing.run("native:savefeatures", {"INPUT": h, "OUTPUT": KELUAR + "/Petak_Hitung.gpkg", "LAYER_NAME": "Petak_Hitung"})
print("Tersimpan:", os.path.exists(KELUAR + "/Petak_Hitung.gpkg"))Penjelasan baris demi baris.
| Baris | Tulisan | Artinya |
|---|---|---|
| 1-5 | komentar dan import | Judul, penulis, dan tiga perkakas: os, processing, QgsVectorLayer. |
| 7-10 | DATA, KELUAR, os.makedirs, petak | Lokasi data, folder hasil (dibuat bila belum ada), dan layer Petak. |
| 12 | TEKS, DESIMAL, BULAT = 2, 0, 1 | Tiga konstanta sekaligus. Alat kalkulator kolom meminta jenis kolom sebagai angka: 2 untuk teks, 0 untuk desimal, 1 untuk bilangan bulat. Nama yang jelas lebih mudah dibaca daripada angka. |
| 15-19 | def tambah(lyr, nama, tipe, rumus, panjang=0, presisi=0): | Fungsi pembungkus untuk alat native:fieldcalculator. panjang=0 dan presisi=0 adalah parameter bawaan: boleh tidak ditulis saat memanggil. Kalimat di dalam tiga kutip adalah docstring. Alat dipanggil dengan kamus isian, dan yang dikembalikan hanya lapisan hasil (["OUTPUT"]). |
| 22 | h = tambah(petak, "LUAS_HA", DESIMAL, ...) | Kolom pertama: luas dari bentuk, dibagi 10.000 (meter persegi menjadi hektare), dibulatkan tiga desimal. |
| 23 | h = tambah(h, "SELISIH_HA", ...) | Selisih antara luas hitungan dan kolom lama LUAS_LAMA_HA. Perhatikan h dipakai lagi sebagai masukan: tiap kolom ditambahkan ke hasil sebelumnya. |
| 24 | "2026 - \"TAHUN_TANAM\"" | Umur = tahun acuan 2026 dikurangi tahun tanam. |
| 25-26 | CASE WHEN ... END | Kelas umur: di bawah 10 tahun Muda, di bawah 15 Sedang, selain itu Tua. |
| 27 | "NAMA_KPH" ... ' / ' ... "KODE_PETAK" | Rangkai dua kolom dengan tanda garis miring di antaranya. Dalam ekspresi QGIS, dua garis tegak berarti "sambung teks". |
| 29 | print("Kolom:", [f.name() for f in h.fields()]) | Cetak nama semua kolom. |
| 30-31 | for f in h.getFeatures(): | Cetak tujuh isian tiap petak. |
| 34-35 | luas = {}, kelas = {} | Dua kamus kosong untuk menghitung. |
| 36-38 | luas.get(f["NAMA_KPH"], 0) | kamus.get(kunci, 0) mengambil isi bila kunci ada, dan 0 bila belum ada. Dengan itu, penjumlahan dan penghitungan berjalan tanpa menyiapkan kunci lebih dulu. |
| 39-41 | print(...) | Cetak luas tiap KPH, jumlah petak tiap kelas umur, dan total luas. |
| 42-43 | beda = [...] | Daftar petak yang selisih luasnya lebih dari 0,06 hektare. abs membuang tanda minus. |
| 46-47 | native:savefeatures, os.path.exists | Simpan h ke Petak_Hitung.gpkg, lalu periksa berkasnya ada. |
Hasil. Layar menampilkan (uji pada QGIS 4.0.2):
Kolom: ['fid', 'KODE_PETAK', 'NAMA_KPH', 'JENIS', 'TAHUN_TANAM', 'STATUS', 'LUAS_LAMA_HA', 'LUAS_HA', 'SELISIH_HA', 'UMUR', 'KELAS_UMUR', 'KODE_LENGKAP']
P-01 1.01 1.0 0.01 18 Tua KPH Alpha / P-01
P-02 0.93 1.0 -0.07 18 Tua KPH Alpha / P-02
P-03 0.995 0.95 0.045 14 Sedang KPH Beta / P-03
P-04 1.065 1.0 0.065 8 Muda KPH Beta / P-04
P-05 1.07 1.0 0.07 21 Tua KPH Alpha / P-05
P-06 0.99 1.05 -0.06 14 Sedang KPH Alpha / P-06
P-07 0.935 1.0 -0.065 8 Muda KPH Beta / P-07
P-08 1.005 1.0 0.005 11 Sedang KPH Beta / P-08
P-09 1.005 0.98 0.025 16 Tua KPH Gamma / P-09
P-10 1.055 1.0 0.055 14 Sedang KPH Gamma / P-10
P-11 0.995 1.0 -0.005 7 Muda KPH Gamma / P-11
P-12 0.935 1.0 -0.065 11 Sedang KPH Gamma / P-12
P-13 0.945 1.0 -0.055 21 Tua KPH Gamma / P-13
P-14 0.995 1.02 -0.025 12 Sedang KPH Gamma / P-14
P-15 1.065 1.0 0.065 6 Muda KPH Gamma / P-15
P-16 1.005 1.0 0.005 10 Sedang KPH Gamma / P-16
Luas per KPH (ha): {'KPH Alpha': 4.0, 'KPH Beta': 4.0, 'KPH Gamma': 8.0}
Petak per kelas umur: {'Tua': 5, 'Sedang': 7, 'Muda': 4}
Total LUAS_HA: 16.0
Petak dengan selisih lebih dari 0,06 ha: ['P-02', 'P-04', 'P-05', 'P-07', 'P-12', 'P-15']
Tersimpan: TruePemeriksaan: 4 + 4 + 8 = 16 hektare, dan 5 + 7 + 4 = 16 petak. Angka-angka itu cocok dengan kolom lama.
Bagian yang boleh diubah.
- Baris 24, tahun acuan 2026. Ganti dengan tahun Anda. Dari uji: dengan
2030, P-01 berumur 22 dan kelas umur menjadi{'Tua': 11, 'Sedang': 5}. - Baris 26, batas kelas umur. Ganti angka 10 dan 15. Dari uji: dengan 12 dan 20, hasilnya
{'Sedang': 7, 'Muda': 7, 'Tua': 2}. - Baris 22-27, rumus. Tiap rumus adalah ekspresi QGIS yang sama dengan di kalkulator kolom. Ganti nama kolom dan hitungannya.
- Baris 42, ambang 0.06. Ganti sesuai toleransi selisih luas Anda.
- Baris 46, nama berkas hasil. Bila diganti, Skrip 15.6 harus ikut diubah.
Jebakan.
- Tahun 2026 tertulis tetap. Hasil umur berbeda bila skrip dipakai di tahun lain.
- Kode jenis kolom bergantung pada alat. Angka 2, 0, dan 1 di baris 12 adalah urutan pilihan pada alat. [CEK] bila versi QGIS Anda berbeda.
- Satuan luas.
area($geometry)memakai satuan layer. Layer ini UTM (meter), jadi hasilnya meter persegi dan dibagi 10.000 menjadi hektare. Pada layer berkoordinat derajat, angkanya tidak berarti. Skrip tidak mengingatkan hal ini. - Selisih dihitung dari kolom lama. Selisih bisa bernilai
Nonebila kolom lama kosong, danabs(None)menimbulkan galat. [kemungkinan] - Menjalankan ulang aman. Dari uji: dua kali berturut-turut tetap menghasilkan 16 petak di
Petak_Hitung.gpkg, karena berkas ditimpa.
Latihan variasi.
- Ganti tahun acuan menjadi 2030 lalu lihat jumlah petak tiap kelas umur.
- Tambahkan kolom
LUAS_M2(luas dalam meter persegi, tanpa dibagi 10.000).
Contoh jawaban. Nomor 1 hanya mengganti angka di baris 24, hasilnya {'Tua': 11, 'Sedang': 5}. Nomor 2 menambah satu baris sesudah baris 22. Dari uji, dua petak pertama berluas 10100.0 dan 9300.0 meter persegi:
h = tambah(h, "LUAS_M2", DESIMAL, "round(area($geometry), 1)", 12, 1)Skrip 15.6: Menggabungkan tabel dengan join #
Hasil pengukuran di lapangan sering datang sebagai tabel terpisah, misalnya berkas CSV. Join menempelkan kolomnya ke tabel petak, dengan kunci yang sama sebagai pencocok, seperti menyatukan dua daftar nama lewat nomor induk. Berkas b2_06_join.py; di dalamnya tertulis "Skrip 6.1".
Tujuan. Menempelkan jumlah pohon dan volume dari Inventarisasi.csv ke petak dan memperbaiki kunci yang tidak cocok. Lalu menghitung kepadatan dan volume per hektare, mencoba join virtual, dan menyimpan hasilnya.
Bahan. Hasil Skrip 15.5 (Petak_Hitung.gpkg di B2_KELUAR) dan Inventarisasi.csv dari paket paket-b2. Jalankan Skrip 15.5 lebih dulu.
Istilah baru di skrip ini:
- CSV: Tabel teks dengan koma sebagai pemisah.
- kunci join: Kolom pencocok.
- join permanen: Hasilnya layer baru.
- join virtual: Hanya tampilan di layer, tanpa berkas baru.
lambda: Fungsi sekali pakai, dibahas di Bab 7.


Skrip utuh:
# Skrip 6.1: menggabungkan (join) tabel Inventarisasi.csv ke lapisan Petak (QGIS Python Console)
# Penulis: Badar Mubarok Yogaswara
# Jalankan Skrip 5.1 lebih dulu (menghasilkan Petak_Hitung.gpkg di folder keluaran).
import os
import processing
from qgis.core import QgsVectorLayer, QgsVectorLayerJoinInfo
DATA = os.environ.get("B2_DATA", r"C:/KPH_Contoh/paket-b2")
KELUAR = os.environ.get("B2_KELUAR", r"C:/KPH_Contoh/keluaran")
petak = QgsVectorLayer(KELUAR + "/Petak_Hitung.gpkg|layername=Petak_Hitung", "Petak_Hitung", "ogr")
uri = "file:///" + DATA.replace("\\", "/") + "/Inventarisasi.csv?type=csv&delimiter=,&detectTypes=yes&geomType=none"
inv = QgsVectorLayer(uri, "Inventarisasi", "delimitedtext")
print("Tabel valid:", inv.isValid(), "| baris:", inv.featureCount())
print("Tipe kolom:", [(f.name(), f.typeName()) for f in inv.fields()])
# 1. Join permanen (hasilnya lapisan baru): kunci KODE_PETAK di kedua tabel
g = processing.run("native:joinattributestable", {
"INPUT": petak, "FIELD": "KODE_PETAK", "INPUT_2": inv, "FIELD_2": "KODE_PETAK",
"FIELDS_TO_COPY": ["JML_POHON", "VOLUME_M3", "TGL_UKUR"], "METHOD": 1, "DISCARD_NONMATCHING": False, "PREFIX": "inv_",
"OUTPUT": "memory:", "NON_MATCHING": "memory:"})
hasil = g["OUTPUT"]
cocok = [f["KODE_PETAK"] for f in hasil.getFeatures() if f["inv_JML_POHON"] is not None]
kosong = [f["KODE_PETAK"] for f in hasil.getFeatures() if f["inv_JML_POHON"] is None]
print("Join 1 | petak cocok:", len(cocok), "| petak tanpa pasangan:", kosong)
kode_petak = {f["KODE_PETAK"] for f in petak.getFeatures()}
print(" baris tabel yang tidak punya petak:", sorted(f["KODE_PETAK"] for f in inv.getFeatures() if f["KODE_PETAK"] not in kode_petak))
# 2. Perbaiki kuncinya: huruf kecil menjadi huruf besar. Kolom bantu dibuat di tabel, lalu join diulang.
inv2 = processing.run("native:fieldcalculator", {
"INPUT": inv, "FIELD_NAME": "KUNCI", "FIELD_TYPE": 2, "FIELD_LENGTH": 10, "FIELD_PRECISION": 0,
"FORMULA": "upper(trim(\"KODE_PETAK\"))", "OUTPUT": "memory:"})["OUTPUT"]
g2 = processing.run("native:joinattributestable", {
"INPUT": petak, "FIELD": "KODE_PETAK", "INPUT_2": inv2, "FIELD_2": "KUNCI",
"FIELDS_TO_COPY": ["JML_POHON", "VOLUME_M3", "TGL_UKUR"], "METHOD": 1, "DISCARD_NONMATCHING": False, "PREFIX": "inv_",
"OUTPUT": "memory:", "NON_MATCHING": "memory:"})
h2 = g2["OUTPUT"]
cocok2 = [f for f in h2.getFeatures() if f["inv_JML_POHON"] is not None]
kosong2 = [f["KODE_PETAK"] for f in h2.getFeatures() if f["inv_JML_POHON"] is None]
print("Join 2 | petak cocok:", len(cocok2), "| petak tanpa pasangan:", kosong2,
"| baris tabel tanpa petak:", sorted(f["KUNCI"] for f in inv2.getFeatures() if f["KUNCI"] not in kode_petak))
# 3. Hitung kepadatan pohon dan volume per hektare dari hasil join
h3 = processing.run("native:fieldcalculator", {
"INPUT": h2, "FIELD_NAME": "POHON_HA", "FIELD_TYPE": 0, "FIELD_LENGTH": 10, "FIELD_PRECISION": 1,
"FORMULA": "round(\"inv_JML_POHON\" / \"LUAS_HA\", 1)", "OUTPUT": "memory:"})["OUTPUT"]
h3 = processing.run("native:fieldcalculator", {
"INPUT": h3, "FIELD_NAME": "VOL_HA", "FIELD_TYPE": 0, "FIELD_LENGTH": 10, "FIELD_PRECISION": 1,
"FORMULA": "round(\"inv_VOLUME_M3\" / \"LUAS_HA\", 1)", "OUTPUT": "memory:"})["OUTPUT"]
for f in h3.getFeatures():
print(f["KODE_PETAK"], f["LUAS_HA"], f["inv_JML_POHON"], f["POHON_HA"], f["inv_VOLUME_M3"], f["VOL_HA"])
tot_pohon = sum(f["inv_JML_POHON"] for f in cocok2)
tot_vol = round(sum(f["inv_VOLUME_M3"] for f in cocok2), 1)
print("Total pohon (15 petak):", tot_pohon, "| total volume m3:", tot_vol)
tertinggi = max((f for f in h3.getFeatures() if f["VOL_HA"] is not None), key=lambda x: x["VOL_HA"])
print("Volume per ha tertinggi:", tertinggi["KODE_PETAK"], tertinggi["VOL_HA"])
# 4. Join virtual (tidak membuat berkas baru), setara Layer Properties > Joins
info = QgsVectorLayerJoinInfo()
info.setJoinLayer(inv)
info.setJoinFieldName("KODE_PETAK")
info.setTargetFieldName("KODE_PETAK")
info.setPrefix("inv_")
info.setUsingMemoryCache(True)
petak.addJoin(info)
print("Join virtual | kolom setelah join:", [f.name() for f in petak.fields()][-5:])
n = sum(1 for f in petak.getFeatures() if f["inv_JML_POHON"] is not None)
print("Join virtual | petak yang terisi:", n)
# 5. Simpan hasil akhir untuk Bab 7
processing.run("native:savefeatures", {"INPUT": h3, "OUTPUT": KELUAR + "/Petak_Inventarisasi.gpkg", "LAYER_NAME": "Petak_Inventarisasi"})
print("Tersimpan:", os.path.exists(KELUAR + "/Petak_Inventarisasi.gpkg"))Penjelasan baris demi baris.
| Baris | Tulisan | Artinya |
|---|---|---|
| 1-3 | komentar | Judul, penulis, dan pengingat: jalankan Skrip 5.1 lebih dulu (di buku ini, Skrip 15.5). |
| 4-6 | import ... | os, processing, serta QgsVectorLayer dan QgsVectorLayerJoinInfo (pembuat join virtual). |
| 8-9 | DATA, KELUAR | Lokasi data dan lokasi hasil. |
| 10 | petak = QgsVectorLayer(KELUAR + ...) | Buka hasil Skrip 15.5. Bila berkas itu belum ada, layar menampilkan galat di bawah. |
| 11 | uri = "file:///" + ... | Susun alamat CSV. .replace("\\", "/") mengganti garis miring terbalik Windows menjadi garis miring biasa. Ujung alamat memberi tahu QGIS: ini CSV, pemisahnya koma, tipe kolom ditebak, dan tidak ada kolom bentuk. |
| 12-14 | inv = QgsVectorLayer(uri, ..., "delimitedtext") | Buka CSV sebagai layer tabel. Pembacanya delimitedtext, bukan ogr. Cetak status, jumlah baris, dan tipe kolom. |
| 17-21 | native:joinattributestable | Join pertama. FIELD dan FIELD_2 adalah kolom kunci di tabel pertama dan kedua. FIELDS_TO_COPY memilih kolom yang ditempel. PREFIX menambah awalan inv_. DISCARD_NONMATCHING: False berarti petak tanpa pasangan tetap dipertahankan. |
| 22-24 | cocok, kosong | Dua daftar dengan list comprehension: petak yang kolom inv_JML_POHON-nya terisi, dan yang None (kosong). |
| 25-26 | kode_petak = {...} | Himpunan semua kode petak. Lalu cetak baris tabel yang tidak punya petak. Kurung kurawal {} dengan isi seperti ini disebut set comprehension. |
| 29-31 | native:fieldcalculator | Buat kolom bantu KUNCI di tabel CSV: upper(trim(...)). trim membuang spasi, upper membuat huruf besar. Maka p-09 menjadi P-09. |
| 32-40 | join kedua | Ulangi join, kini dengan kunci KUNCI. Baris 39-40 mencetak jumlah yang cocok, yang kosong, dan baris tabel tanpa petak. |
| 43-48 | dua kali fieldcalculator | Hitung POHON_HA (jumlah pohon dibagi luas) dan VOL_HA (volume dibagi luas), dibulatkan satu desimal. |
| 49-50 | for f in h3.getFeatures(): | Cetak enam isian tiap petak. |
| 51-53 | tot_pohon, tot_vol | Jumlahkan pohon dan volume dari petak yang cocok. Tulisan "15 petak" di baris 53 adalah angka tetap. |
| 54-55 | max(..., key=lambda x: x["VOL_HA"]) | Cari petak dengan VOL_HA terbesar. lambda adalah fungsi mini: "diberi x, kembalikan x["VOL_HA"]". Petak yang VOL_HA-nya None disaring lebih dulu. |
| 58-64 | QgsVectorLayerJoinInfo() | Join virtual: tentukan layer yang digabung, kolom kunci kedua sisi, dan awalan, lalu petak.addJoin(info). |
| 65-67 | [-5:], sum(1 for ...) | Cetak lima nama kolom terakhir, lalu hitung petak yang terisi. [-5:] berarti "lima terakhir". |
| 70-71 | native:savefeatures | Simpan h3 ke Petak_Inventarisasi.gpkg, lalu periksa berkasnya ada. |
Hasil. Layar menampilkan (uji pada QGIS 4.0.2):
Tabel valid: True | baris: 16
Tipe kolom: [('KODE_PETAK', 'text'), ('JML_POHON', 'integer'), ('VOLUME_M3', 'double'), ('TGL_UKUR', 'date'), ('REGU', 'text')]
Join 1 | petak cocok: 14 | petak tanpa pasangan: ['P-09', 'P-12']
baris tabel yang tidak punya petak: ['P-17', 'p-09']
Join 2 | petak cocok: 15 | petak tanpa pasangan: ['P-12'] | baris tabel tanpa petak: ['P-17']
P-01 1.01 970 960.4 246.8 244.4
P-02 0.93 842 905.4 235.0 252.7
P-03 0.995 989 994.0 202.7 203.7
P-04 1.065 796 747.4 125.0 117.4
P-05 1.07 920 859.8 275.7 257.7
P-06 0.99 863 871.7 181.7 183.5
P-07 0.935 846 904.8 119.8 128.1
P-08 1.005 844 839.8 148.5 147.8
P-09 1.005 920 915.4 218.1 217.0
P-10 1.055 813 770.6 180.5 171.1
P-11 0.995 892 896.5 113.0 113.6
P-12 0.935 None None None None
P-13 0.945 915 968.3 279.2 295.4
P-14 0.995 845 849.2 179.8 180.7
P-15 1.065 829 778.4 99.6 93.5
P-16 1.005 978 973.1 156.3 155.5
Total pohon (15 petak): 13262 | total volume m3: 2761.7
Volume per ha tertinggi: P-13 295.4
Join virtual | kolom setelah join: ['KODE_LENGKAP', 'inv_JML_POHON', 'inv_VOLUME_M3', 'inv_TGL_UKUR', 'inv_REGU']
Join virtual | petak yang terisi: 14
Tersimpan: TrueTiga pelajaran dari hasil ini. Pertama, join pertama hanya cocok 14: p-09 huruf kecil gagal bertemu P-09. Kedua, setelah kunci dirapikan, 15 cocok. Satu petak (P-12) memang tidak punya data, dan satu baris tabel (P-17) tidak punya petak. Ketiga, join virtual hanya 14 petak terisi, karena ia memakai kunci asli yang belum dirapikan. Tabel tipe kolom juga memuat kolom REGU yang tidak ikut dicopy di join permanen, tetapi ikut tampil pada join virtual.
Bagian yang boleh diubah.
- Baris 19 dan 34,
FIELDS_TO_COPY. Tambah atau kurangi kolom. Dari uji: menambah"REGU"membuat kolominv_REGUikut tersedia, misalnya('P-01', 'Regu 1'). - Baris 19 dan 34,
PREFIX. Ganti awalaninv_, lalu ganti juga di baris 22, 23, 37, 38, 45, 48, dan seterusnya. - Baris 11,
delimiter=,. Ganti bila CSV Anda memakai titik koma. - Baris 31, rumus kunci. Ganti
upper(trim(...))sesuai jenis ketidakcocokan. - Baris 44 dan 47, angka 1 pada pembulatan. Ganti jumlah desimal.
Jebakan.
- Lupa menjalankan Skrip 15.5. Layer
petaktidak valid, dan baris 17 berhenti dengan:_core.QgsProcessingException: Invalid join field from layer 1: “KODE_PETAK” does not existPesannya menyesatkan: kolomnya ada, hanya berkasnya belum ada. Cek Skrip 15.5 dulu. - Kunci tidak cocok tanpa galat. Join tidak pernah protes bila ada yang tidak cocok. Baris 24 dan 26 ada untuk memeriksanya. Selalu baca keduanya.
- Angka tetap. "15 petak" di baris 53 hanya benar untuk data ini.
- Join virtual tidak tersimpan. Join di baris 64 hilang bila QGIS ditutup, dan ia tidak membuat berkas.
- Tipe kolom CSV. Jenis kolom ditebak dari isi (
detectTypes=yes). Kolom dengan isi campur bisa terbaca sebagai teks. - Dua variabel serupa.
hasildanh2dipakai bergantian untuk dua join. Jangan tertukar.
Latihan variasi.
- Cetak kode petak yang kepadatannya (
POHON_HA) di atas 900. - Ikutkan kolom
REGUdalam join permanen kedua, lalu cetak regu empat petak pertama.
Contoh jawaban. Nomor 1, tambahkan di akhir skrip:
padat = [f["KODE_PETAK"] for f in h3.getFeatures() if f["POHON_HA"] is not None and f["POHON_HA"] > 900]
print("Petak dengan POHON_HA di atas 900:", padat)Layar menampilkan ['P-01', 'P-02', 'P-03', 'P-07', 'P-09', 'P-13', 'P-16']. Nomor 2: tambahkan "REGU" di baris 34 dan cetak [(f["KODE_PETAK"], f["inv_REGU"]) for f in h2.getFeatures()][:4]. Layar menampilkan [('P-01', 'Regu 1'), ('P-02', 'Regu 2'), ('P-03', 'Regu 3'), ('P-04', 'Regu 1')].
Skrip 15.7: Membuat rekap untuk Kepala Seksi #
Semua kerja tadi bermuara di sini: ringkasan yang bisa dibaca atasan dan diekspor. Berkas b2_07_laporan.py; di dalamnya tertulis "Skrip 7.1".
Tujuan. Merekap pohon per KPH, mendaftar petak tanpa data, memilih jati kelas umur Tua yang diurutkan menurut volume, lalu mengekspor ke GeoPackage dan CSV.
Bahan. Petak_Inventarisasi.gpkg di B2_KELUAR, hasil Skrip 15.5 dan 15.6. Jalankan keduanya lebih dulu. Skrip ini tidak memakai B2_DATA.
Istilah baru di skrip ini:
- rekap per kategori: Statistik tiap kelompok.
- ekspor: Menyimpan hasil ke berkas lain.
with open: Membuka berkas dan otomatis menutupnya, Bab 9.csv.writer: Penulis baris CSV.


Skrip utuh:
# Skrip 7.1: rekap akhir untuk Kepala Seksi dari lapisan Petak_Inventarisasi (QGIS Python Console)
# Penulis: Badar Mubarok Yogaswara
# Jalankan Skrip 5.1 dan 6.1 lebih dulu (menghasilkan Petak_Inventarisasi.gpkg di folder keluaran).
import csv
import os
import processing
from qgis.core import QgsVectorLayer
KELUAR = os.environ.get("B2_KELUAR", r"C:/KPH_Contoh/keluaran")
lap = QgsVectorLayer(KELUAR + "/Petak_Inventarisasi.gpkg|layername=Petak_Inventarisasi", "Petak_Inventarisasi", "ogr")
print("Baris:", lap.featureCount())
# 1. Rekap per KPH dengan alat "Statistics by categories"
rek = processing.run("qgis:statisticsbycategories", {
"INPUT": lap, "VALUES_FIELD_NAME": "inv_JML_POHON", "CATEGORIES_FIELD_NAME": ["NAMA_KPH"], "OUTPUT": "memory:"})["OUTPUT"]
print("Kolom rekap:", [f.name() for f in rek.fields()])
for f in rek.getFeatures():
print(f["NAMA_KPH"], "| jumlah petak berisi data:", f["count"], "| total pohon:", f["sum"], "| rata-rata:", round(f["mean"], 1))
# 2. Petak yang datanya belum lengkap
kosong = [f["KODE_PETAK"] for f in lap.getFeatures() if f["inv_JML_POHON"] is None]
print("Petak tanpa data inventarisasi:", kosong)
# 3. Jati kelas umur Tua, diurutkan menurut volume per hektare
lap.selectByExpression("\"JENIS\" = 'Jati' AND \"KELAS_UMUR\" = 'Tua'")
print("Jati Tua terpilih:", lap.selectedFeatureCount())
for f in sorted(lap.selectedFeatures(), key=lambda x: -x["VOL_HA"]):
print(" ", f["KODE_PETAK"], f["NAMA_KPH"], f["VOL_HA"], "m3/ha")
# 4. Ekspor yang terpilih ke GeoPackage dan ke CSV
processing.run("native:saveselectedfeatures", {"INPUT": lap, "OUTPUT": KELUAR + "/Jati_Tua.gpkg"})
with open(KELUAR + "/Jati_Tua.csv", "w", newline="", encoding="utf-8") as fh:
w = csv.writer(fh)
w.writerow(["KODE_PETAK", "NAMA_KPH", "LUAS_HA", "UMUR", "POHON_HA", "VOL_HA"])
for f in lap.selectedFeatures():
w.writerow([f["KODE_PETAK"], f["NAMA_KPH"], f["LUAS_HA"], f["UMUR"], f["POHON_HA"], f["VOL_HA"]])
hasil = QgsVectorLayer(KELUAR + "/Jati_Tua.gpkg", "Jati_Tua", "ogr")
print("Berkas Jati_Tua.gpkg:", hasil.featureCount(), "objek | Jati_Tua.csv ada:", os.path.exists(KELUAR + "/Jati_Tua.csv"))Penjelasan baris demi baris.
| Baris | Tulisan | Artinya |
|---|---|---|
| 1-3 | komentar | Judul, penulis, dan pengingat urutan skrip. |
| 4-7 | import csv, os, processing, QgsVectorLayer | csv adalah perkakas untuk menulis berkas CSV. |
| 9-11 | KELUAR, lap = ..., print("Baris:", ...) | Buka layer hasil Skrip 15.6 dan cetak jumlah barisnya. |
| 14-15 | qgis:statisticsbycategories | Hitung statistik inv_JML_POHON per NAMA_KPH. Hasilnya layer sementara berisi kolom count, sum, mean, dan lain-lain. |
| 16 | print("Kolom rekap:", ...) | Cetak nama kolom hasil, agar Anda tahu kolom mana yang tersedia. |
| 17-18 | for f in rek.getFeatures(): | Cetak tiap KPH: banyaknya petak berisi data, total pohon, dan rata-rata (satu desimal). |
| 21-22 | kosong = [...] | Daftar petak yang inv_JML_POHON-nya None. |
| 25-26 | lap.selectByExpression(...) | Pilih jati dengan kelas umur Tua, lalu cetak banyaknya. |
| 27-28 | sorted(..., key=lambda x: -x["VOL_HA"]) | Urutkan terpilih dari volume per hektare terbesar. Tanda minus membalik urutan, jadi yang besar di depan. |
| 31 | native:saveselectedfeatures | Simpan hanya yang terpilih ke Jati_Tua.gpkg. |
| 32 | with open(..., "w", newline="", encoding="utf-8") as fh: | Buka berkas CSV untuk ditulis. newline="" mencegah baris kosong ekstra di Windows. encoding="utf-8" menjaga huruf. |
| 33-34 | w = csv.writer(fh), w.writerow([...]) | Buat penulis, lalu tulis baris judul kolom. |
| 35-36 | for f in lap.selectedFeatures(): | Untuk tiap petak terpilih, tulis satu baris dengan enam isian. Urutannya harus sama dengan judul di baris 34. |
| 37-38 | hasil = QgsVectorLayer(...), print(...) | Baca balik Jati_Tua.gpkg dan cek Jati_Tua.csv ada. |
Hasil. Layar menampilkan (uji pada QGIS 4.0.2):
Baris: 16
Kolom rekap: ['NAMA_KPH', 'count', 'unique', 'min', 'max', 'range', 'sum', 'mean', 'median', 'stddev', 'minority', 'majority', 'q1', 'q3', 'iqr']
KPH Alpha | jumlah petak berisi data: 4 | total pohon: 3595.0 | rata-rata: 898.8
KPH Beta | jumlah petak berisi data: 4 | total pohon: 3475.0 | rata-rata: 868.8
KPH Gamma | jumlah petak berisi data: 7 | total pohon: 6192.0 | rata-rata: 884.6
Petak tanpa data inventarisasi: ['P-12']
Jati Tua terpilih: 5
P-13 KPH Gamma 295.4 m3/ha
P-05 KPH Alpha 257.7 m3/ha
P-02 KPH Alpha 252.7 m3/ha
P-01 KPH Alpha 244.4 m3/ha
P-09 KPH Gamma 217.0 m3/ha
Berkas Jati_Tua.gpkg: 5 objek | Jati_Tua.csv ada: TruePada sebagian komputer, dua baris peringatan DeprecationWarning tentang parameterAsFields() muncul sebelum hasil. Itu peringatan dari QGIS tentang alatnya, bukan galat, dan hasilnya tetap benar. Cek: 3595 + 3475 + 6192 = 13262, sama dengan total pohon di Skrip 15.6.
Bagian yang boleh diubah.
- Baris 25, syarat seleksi. Ganti jenis atau kelas umur.
- Baris 27, tanda minus. Hapus minus untuk mengurutkan dari terkecil.
- Baris 34 dan 36, kolom CSV. Daftar judul dan daftar isian harus sama panjang dan sama urutan.
- Baris 31 dan 32, nama berkas hasil. Ganti
Jati_Tuasesuai isi. - Baris 15, kolom statistik dan kategori. Ganti
inv_JML_POHONmenjadiinv_VOLUME_M3untuk rekap volume.
Jebakan.
- Lupa Skrip 15.5 dan 15.6. Layer tidak valid, dan baris 14 berhenti dengan:
_core.QgsProcessingException: Field "NAMA_KPH" does not exist.Seperti pada Skrip 15.6, pesannya menyesatkan: yang kurang adalah berkasnya. - Nilai kosong mematahkan pengurutan. Bila
VOL_HAkosong (misalnya petak P-12 ikut terpilih), baris 27 berhenti denganTypeError: bad operand type for unary -: 'NoneType'. Dari uji, ini terjadi saat hanya P-12 yang dipilih. - Baris 18 memakai
round(f["mean"], 1). Bilameankosong, ia gagal juga. [kemungkinan] - Baris 37 membuka
Jati_Tua.gpkgtanpa nama layer. Cukup bila hanya ada satu layer di berkas. - CSV memakai titik desimal. Pembaca Indonesia yang membukanya di Excel berbahasa Indonesia mungkin melihat angka terbaca sebagai teks. [CEK]
Latihan variasi.
- Pilih petak berjenis Sengon dan urutkan dari volume per hektare terkecil.
- Ganti rekap per KPH menjadi rekap volume (
inv_VOLUME_M3).
Contoh jawaban. Nomor 1: ganti baris 25 menjadi lap.selectByExpression("\"JENIS\" = 'Sengon'") dan hapus tanda minus di baris 27. Layar menampilkan empat petak: P-15 93.5, P-11 113.6, P-04 117.4, dan P-07 128.1. Tulisan "Jati Tua terpilih" tetap tercetak di baris 26 walau isinya Sengon, jadi ganti juga kata di dalamnya. Nomor 2: ganti "inv_JML_POHON" di baris 15 menjadi "inv_VOLUME_M3". Dari uji, baris pertama rekap menjadi KPH Alpha | jumlah petak berisi data: 4 | total pohon: 939.2 | rata-rata: 234.8. Tulisan "total pohon" tetap tercetak walau isinya volume, jadi ganti juga katanya di baris 18.
Cek paham #
- Apa bedanya seleksi dengan filter layer (
setSubsetString)? - Mengapa Skrip 15.3 menyalin berkas dulu sebelum menggambar?
- Mengapa join pertama di Skrip 15.6 hanya cocok 14 petak, padahal ada 15 petak yang punya data?
- Mengapa Skrip 15.7 gagal bila Skrip 15.5 dan 15.6 belum dijalankan?
Jawaban.
- Seleksi hanya menandai fitur yang terpilih, sedangkan fitur lain tetap ada. Filter menyembunyikan fitur lain sampai filternya dikosongkan.
- Agar berkas asli di paket data tidak berubah. Semua edit terjadi di salinan.
- Kunci
p-09di tabel CSV ditulis huruf kecil, sehingga tidak sama denganP-09. Setelah dirapikan denganupperdantrim, 15 petak cocok. - Skrip 15.7 membaca
Petak_Inventarisasi.gpkg, yang dibuat oleh Skrip 15.6 dari hasil Skrip 15.5. Tanpa berkas itu, layar tidak valid dan alat gagal.
Kesalahan umum #
- Membiarkan jalur contoh. Jalur cadangan menunjuk folder di komputer penulis. Perbaikan: ganti jalur cadangan, atau isi pengaturan
B2_DATAdanB2_KELUAR. - Salah ketik nama kolom di ekspresi. Seleksi tidak galat, hanya memilih 0 fitur. Perbaikan: salin nama kolom dari hasil Skrip 15.1.
- Menjalankan skrip tidak berurutan. Skrip 15.6 dan 15.7 memakai berkas buatan skrip sebelumnya. Perbaikan: jalankan 15.5, 15.6, lalu 15.7.
- Lupa
commitChanges()setelah edit. Perubahan tidak tersimpan. Perbaikan: akhiri tiap edit dengancommitChanges()dan baca hasilnya (TrueatauFalse). - Percaya join tanpa memeriksa kunci. Perbaikan: selalu cetak yang tidak cocok, seperti baris 24 dan 26 di Skrip 15.6.
Ringkasan dan latihan #
- Layer vektor punya tabel atribut. Skrip membacanya per fitur dan per kolom.
selectByExpressionmenandai fitur dengan syarat, danselectbylocationmemilih menurut lokasi.- Mengubah layer lewat skrip: salin,
startEditing, tambah atau ubah,commitChanges. - Alat pemeriksa topologi menolak bentuk rusak. Perbaiki bentuk lebih dulu, lalu periksa ulang.
- Tambah kolom lewat
native:fieldcalculator, tempel tabel lewatnative:joinattributestable. - Rekap akhir: statistik per kelompok, seleksi, urut, lalu ekspor ke GeoPackage dan CSV.
- Tiga hal yang diganti untuk data baru: lokasi data, nama berkas dan layer, nama kolom.
Latihan. Buka Titik_Plot.gpkg dengan layer Titik_Plot. Cetak nama kolomnya saja, tanpa baris datanya. Lalu pilih plot sehat dan berdiameter 40 cm ke atas, dan cetak kodenya. Bagian mana dari Skrip 15.1 yang boleh Anda hapus?
Tabel perbandingan: pekerjaan di menu dan padanannya di skrip #
| Pekerjaan di QGIS | Padanan di skrip | Contoh di KPH Contoh |
|---|---|---|
| Membuka tabel atribut | layer.getFeatures() | Membaca 16 petak |
| Select by Expression | layer.selectByExpression("...") | Jati sebelum 2010: 4 petak |
| Select by Location | native:selectbylocation | 22 plot di dalam petak |
| Toggle Editing, lalu simpan | startEditing(), commitChanges() | Jalan dan persemaian baru |
| Split Features | layer.splitFeatures(garis, ...) | P-12 menjadi dua bagian |
| Geometry Checker | native:checkvalidity dan kawan-kawannya | P-18 menyilang diri |
| Field Calculator | native:fieldcalculator | Kolom LUAS_HA dan UMUR |
| Join Attributes by Field Value | native:joinattributestable | Pohon dan volume per petak |
| Statistics by Categories | qgis:statisticsbycategories | Total pohon per KPH |
| Export Selected Features | native:saveselectedfeatures | Jati_Tua.gpkg |