Lewati ke isi
Profil penulisSeri Buku GIS Kehutanan dan Pertanian/ I4
Tampilkan bagian untuk:

BAB 5: Ekspresi: Bahasa Rumus yang Dipakai di Mana-Mana

#Studi kasus: "Kalimat laporan yang menulis dirinya sendiri"

Kepala Seksi ingin satu kalimat ringkas untuk tiap petak, misalnya "KPH Beta, 4 ha, 5 titik panas". Setiap bulan angkanya berubah, dan Anda mengetik ulang semuanya. Bulan ini Anda mencoba cara lain: menuliskan rumusnya sekali. QGIS yang menghitung dan menyusun kalimatnya untuk setiap petak.

#Konsep: ekspresi dalam tiga kalimat

Ekspresi adalah rumus tertulis yang dihitung QGIS untuk setiap fitur, mirip rumus di sel lembar kerja. Rumus yang sama dipakai di banyak tempat: seleksi, kalkulator field, label, atlas, dan model. Agregasi menambah satu kemampuan: menghitung ringkasan dari layer lain, misalnya jumlah titik panas yang jatuh di dalam tiap petak.

Istilah baru bab ini:

  • Ekspresi: rumus yang dihitung per fitur.
  • Fungsi ekspresi: perintah kecil di dalam rumus, seperti round() atau upper().
  • Konteks: "lingkungan" tempat rumus dihitung, yaitu fitur yang sedang dibaca dan variabel proyek.
  • Agregasi (aggregate): ringkasan dari layer lain, seperti jumlah, rata-rata, atau gabungan teks.
  • `@parent`: fitur yang sedang dibaca di layer luar, dipakai di dalam agregasi.
Ilustrasi 5.1: Agregasi: hitung isi layer lain untuk tiap fitur
Skema: tiap KPH menghitung titik panas yang jatuh di dalamnya, dan titik di luar tidak ikut dihitung

#Bagian A: QGIS

#Bagian A: Ekspresi di QGIS

#A.1 Ekspresi dasar

Dua aturan tanda kutip harus dihafal. Tanda kutip ganda ("NAMA_KPH") menunjuk nama kolom. Tanda kutip tunggal ('KPH Beta') menulis teks. Operator || menyambung teks. Hasil uji di QGIS 4.0.2 untuk KPH Alpha:

EkspresiHasil
round($area / 10000, 2)4
upper("NAMA_KPH")KPH ALPHA
replace("NAMA_KPH", 'KPH ', '')Alpha
if($area > 50000, 'besar', 'kecil')kecil
CASE WHEN $area > 50000 THEN 'besar' ELSE 'kecil' ENDkecil

Gabungan teks ditulis dengan ||. Contoh yang menghasilkan KPH Alpha (4 ha):

EKSPRESI QGIS
"NAMA_KPH" || ' (' || round($area / 10000, 1) || ' ha)'

#A.2 Menghitung ekspresi dari Python

Skrip 5.1 membangun "lingkungan" (konteks) lalu menghitung ekspresi untuk tiap petak. Skrip ini juga menunjukkan dua jenis galat ekspresi: galat tulis (rumus tidak lengkap) dan galat hitung (rumus benar tetapi nilainya tidak bisa dihitung).

PYTHON (PYQGIS) 5.1
# [SKRIP 5.1: Mengevaluasi ekspresi QGIS dari Python, termasuk agregasi]
# Penulis: Badar Mubarok Yogaswara
from qgis.core import (QgsProject, QgsVectorLayer, QgsExpression, QgsExpressionContext,
                       QgsExpressionContextUtils, QgsCoordinateReferenceSystem)

DATA = "D:/Latihan/paket-i4"
HASIL = "D:/Latihan/hasil_i4"

proyek = QgsProject.instance()
batas = QgsVectorLayer(DATA + "/Batas_KPH.gpkg", "Batas_KPH", "ogr")
hotspot = QgsVectorLayer(DATA + "/Hotspot_Nasional.gpkg", "Hotspot_Nasional", "ogr")
survei = QgsVectorLayer(DATA + "/Survei.gpkg", "Titik_Survei", "ogr")
for lyr in (batas, hotspot, survei):
    proyek.addMapLayer(lyr)         # fungsi aggregate() mencari layer lain berdasarkan NAMA di proyek


def konteks(layer, fitur):
    """Membuat 'lingkungan' tempat ekspresi dihitung: variabel proyek, layer, dan fitur saat ini."""
    c = QgsExpressionContext()
    c.appendScopes(QgsExpressionContextUtils.globalProjectLayerScopes(layer))
    c.setFeature(fitur)
    return c


def hitung(teks, layer, fitur):
    e = QgsExpression(teks)
    if e.hasParserError():                      # salah tulis ekspresi
        return "GALAT TULIS: " + e.parserErrorString()
    nilai = e.evaluate(konteks(layer, fitur))
    if e.hasEvalError():                        # salah saat dihitung
        return "GALAT HITUNG: " + e.evalErrorString()
    return nilai


# 1. Ekspresi dasar: rumus, teks, dan pilihan (CASE)
dasar = {
    "luas (ha)": "round($area / 10000, 2)",
    "nama huruf besar": 'upper("NAMA_KPH")',
    "ukuran": "CASE WHEN $area > 50000 THEN 'besar' ELSE 'kecil' END",
}
for fitur in batas.getFeatures():
    print(fitur["NAMA_KPH"], {k: hitung(v, batas, fitur) for k, v in dasar.items()})

# 2. Agregasi: menghitung dari LAYER LAIN. @parent = fitur KPH yang sedang dibaca.
titik_panas = ("aggregate(layer:='Hotspot_Nasional', aggregate:='count', expression:=\"ID_Titik\", "
               "filter:=intersects($geometry, geometry(@parent)))")
tinggi_rata = ("round(aggregate(layer:='Titik_Survei', aggregate:='mean', expression:=\"Tinggi_Phn\", "
               "filter:=intersects($geometry, geometry(@parent))), 1)")
print("--- agregasi per KPH ---")
for fitur in batas.getFeatures():
    print("%-10s titik panas = %s | tinggi pohon rata-rata = %s" % (
        fitur["NAMA_KPH"], hitung(titik_panas, batas, fitur), hitung(tinggi_rata, batas, fitur)))

# 3. Agregasi tanpa filter: satu angka untuk seluruh layer
fitur = next(batas.getFeatures())
total = hitung("aggregate('Hotspot_Nasional', 'count', \"ID_Titik\")", batas, fitur)
print("Total titik panas (semua KPH):", total)

# 4. Ekspresi yang salah dikenali dengan aman
print(hitung("round($area / 10000, ", batas, fitur))
print(hitung("to_int('abc') + 1", batas, fitur))

# 5. Dua cara lain menghitung titik: daftar dan gabungan teks
lain = {
    "jumlah lewat overlay": "array_length(overlay_intersects('Hotspot_Nasional', \"ID_Titik\"))",
    "daftar ID": ("aggregate('Hotspot_Nasional', 'concatenate', \"ID_Titik\", "
                  "filter:=intersects($geometry, geometry(@parent)), concatenator:=', ')"),
}
for fitur in batas.getFeatures():
    print(fitur["NAMA_KPH"], {k: hitung(v, batas, fitur) for k, v in lain.items()})

# 6. Awas: $area mengikuti pengaturan ellipsoid proyek, area($geometry) memakai bidang datar peta
fitur = next(batas.getFeatures())
print("Tanpa ellipsoid  :", hitung("$area", batas, fitur), hitung("area($geometry)", batas, fitur))
proyek.setCrs(QgsCoordinateReferenceSystem("EPSG:32749"))
proyek.setEllipsoid("EPSG:7030")                 # WGS 84, pengaturan umum pada proyek baru
print("Dengan ellipsoid :", round(hitung("$area", batas, fitur), 2), hitung("area($geometry)", batas, fitur))

Hasil uji di QGIS 4.0.2:

HASIL UJI SKRIP 5.1
KPH Alpha {'luas (ha)': 4.0, 'nama huruf besar': 'KPH ALPHA', 'ukuran': 'kecil'}
KPH Beta {'luas (ha)': 4.0, 'nama huruf besar': 'KPH BETA', 'ukuran': 'kecil'}
KPH Gamma {'luas (ha)': 8.0, 'nama huruf besar': 'KPH GAMMA', 'ukuran': 'besar'}
--- agregasi per KPH ---
KPH Alpha  titik panas = 2 | tinggi pohon rata-rata = 11.3
KPH Beta   titik panas = 5 | tinggi pohon rata-rata = 9.6
KPH Gamma  titik panas = 5 | tinggi pohon rata-rata = 8.9
Total titik panas (semua KPH): 12
GALAT TULIS:
Incomplete expression. You might not have finished the full expression.
GALAT HITUNG: Cannot convert 'abc' to int
KPH Alpha {'jumlah lewat overlay': 2, 'daftar ID': 'HS-008, HS-012'}
KPH Beta {'jumlah lewat overlay': 5, 'daftar ID': 'HS-004, HS-005, HS-006, HS-009, HS-011'}
KPH Gamma {'jumlah lewat overlay': 5, 'daftar ID': 'HS-001, HS-002, HS-003, HS-007, HS-010'}
Tanpa ellipsoid  : 40000.0 40000.0
Dengan ellipsoid : 39997.03 40000.0

#A.3 Agregasi: menghitung dari layer lain

Perhatikan satu petak, misalnya KPH Beta. Rumusnya berbunyi: "dari semua titik panas, hitung hanya yang bersinggungan dengan petak ini". Bagian "petak ini" ditulis geometry(@parent). Bagian "bersinggungan" ditulis intersects($geometry, ...), di mana $geometry adalah titik panas yang sedang diperiksa.

Hasil uji: KPH Alpha 2 titik, KPH Beta 5 titik, dan KPH Gamma 5 titik. Totalnya 12 titik, sama dengan jumlah titik di layer. Dua cara lain yang memberi hasil sama: array_length(overlay_intersects('Hotspot_Nasional', "ID_Titik")) untuk jumlah, dan agregasi 'concatenate' untuk daftar ID titik.

#A.4 Ekspresi di dalam alat Processing

Ekspresi tidak hanya untuk label. Skrip 5.2 memakainya di Field calculator (menambah kolom luas dan jumlah titik panas) dan di Extract by expression (memilih petak dengan titik panas minimal 5). Terakhir, skrip menyusun teks laporan untuk tiap petak.

PYTHON (PYQGIS) 5.2
# [SKRIP 5.2: Ekspresi di dalam alat Processing: Field calculator, Extract by expression, label]
# Penulis: Badar Mubarok Yogaswara
import processing
from qgis.core import (QgsProject, QgsVectorLayer, QgsExpression, QgsExpressionContext,
                       QgsExpressionContextUtils)

DATA = "D:/Latihan/paket-i4"
HASIL = "D:/Latihan/hasil_i4"

proyek = QgsProject.instance()
batas = QgsVectorLayer(DATA + "/Batas_KPH.gpkg", "Batas_KPH", "ogr")
hotspot = QgsVectorLayer(DATA + "/Hotspot_Nasional.gpkg", "Hotspot_Nasional", "ogr")
proyek.addMapLayer(batas)
proyek.addMapLayer(hotspot)         # wajib ada di proyek agar aggregate() menemukannya

# 1. Menambah dua kolom baru dengan ekspresi
tahap1 = processing.run("native:fieldcalculator", {
    "INPUT": batas, "FIELD_NAME": "LUAS_HA", "FIELD_TYPE": 0,          # 0 = desimal
    "FIELD_LENGTH": 10, "FIELD_PRECISION": 2,
    "FORMULA": "round($area / 10000, 2)", "OUTPUT": "TEMPORARY_OUTPUT"})["OUTPUT"]
tahap2 = processing.run("native:fieldcalculator", {
    "INPUT": tahap1, "FIELD_NAME": "JML_HOTSPOT", "FIELD_TYPE": 1,     # 1 = bilangan bulat
    "FIELD_LENGTH": 5, "FIELD_PRECISION": 0,
    "FORMULA": "aggregate('Hotspot_Nasional', 'count', \"ID_Titik\", "
               "intersects($geometry, geometry(@parent)))",
    "OUTPUT": "TEMPORARY_OUTPUT"})["OUTPUT"]
tahap2.setName("Batas_KPH_Ringkas")
proyek.addMapLayer(tahap2)
print("Kolom:", [f.name() for f in tahap2.fields()])
for f in tahap2.getFeatures():
    print("  ", f["NAMA_KPH"], f["LUAS_HA"], "ha;", f["JML_HOTSPOT"], "titik panas")

# 2. Memilih fitur dengan ekspresi lewat alat
rawan = processing.run("native:extractbyexpression", {
    "INPUT": tahap2, "EXPRESSION": '"JML_HOTSPOT" >= 5', "OUTPUT": "TEMPORARY_OUTPUT"})["OUTPUT"]
print("KPH dengan titik panas >= 5:", sorted(f["NAMA_KPH"] for f in rawan.getFeatures()))

# 3. Teks laporan satu KPH, dibentuk dengan ekspresi (cara yang sama dipakai label di layout)
teks = ("'LAPORAN ' || upper(\"NAMA_KPH\") || '\n' || 'Luas: ' || \"LUAS_HA\" || ' ha\n' || "
        "'Titik panas: ' || \"JML_HOTSPOT\" || ' titik'")
ekspresi = QgsExpression(teks)
for f in tahap2.getFeatures():
    c = QgsExpressionContext()
    c.appendScopes(QgsExpressionContextUtils.globalProjectLayerScopes(tahap2))
    c.setFeature(f)
    print(ekspresi.evaluate(c))
    print("---")

Hasil uji di QGIS 4.0.2:

HASIL UJI SKRIP 5.2
Kolom: ['fid', 'NAMA_KPH', 'LUAS_HA', 'JML_HOTSPOT']
   KPH Alpha 4.0 ha; 2 titik panas
   KPH Beta 4.0 ha; 5 titik panas
   KPH Gamma 8.0 ha; 5 titik panas
KPH dengan titik panas >= 5: ['KPH Beta', 'KPH Gamma']
LAPORAN KPH ALPHA
Luas: 4 ha
Titik panas: 2 titik
---
LAPORAN KPH BETA
Luas: 4 ha
Titik panas: 5 titik
---
LAPORAN KPH GAMMA
Luas: 8 ha
Titik panas: 5 titik
---

Hasilnya: kolom LUAS_HA dan JML_HOTSPOT terbentuk, KPH Beta dan KPH Gamma terpilih, dan teks laporan tersusun sendiri untuk tiap petak.

#Bagian B: ArcGIS Pro

#Bagian B: Ekspresi di ArcGIS Pro

ArcGIS Pro memakai Arcade untuk ekspresi bergaya QGIS. Alat Calculate Field menerima ekspresi Arcade, dan lewat ArcPy jenis ekspresinya ditulis "ARCADE". [CEK] Untuk menghitung titik dalam poligon, Arcade memakai FeatureSetByName, Intersects, dan Count. Pada Calculate Field, pencarian layer lain dilaporkan memakai $datastore, bukan $map. Bentuk persisnya perlu diuji pada geodatabase Anda. [CEK]

PYTHON (ARCPY, PYTHON 3) [CEK]
import arcpy
arcpy.management.CalculateField("Batas_KPH", "LUAS_HA", "Round(Area($feature, 'hectares'), 2)", "ARCADE")
arcpy.management.CalculateField("Batas_KPH", "HOTSPOT",
    "Count(Intersects($feature, FeatureSetByName($datastore, 'Hotspot_Nasional')))", "ARCADE")

Cara yang lebih pasti adalah Spatial Join, yang menambah kolom Join_Count (lihat Skrip 2.1-Pro). Ekspresi Python pada Calculate Field juga tersedia, misalnya !shape.area@hectares!. [CEK]

#Bagian C: ArcMap 10.8

#Bagian C: Ekspresi di ArcMap 10.8

ArcMap tidak punya Arcade. Field Calculator menerima ekspresi VB Script atau Python. Untuk luas hektare, ekspresi Python-nya !shape.area@hectares!. [CEK] Agregasi lintas layer tidak ada padanan langsung di kalkulator field. Pakai Spatial Join (kolom Join_Count) atau Summary Statistics, seperti pada Skrip 2.1-ArcMap.

PYTHON (ARCPY, PYTHON 2.7) [CEK]
import arcpy
arcpy.CalculateField_management("Batas_KPH", "LUAS_HA", "!shape.area@hectares!", "PYTHON_9.3")

#Cek paham

  1. Apa arti @parent di dalam aggregate?
  2. Mengapa layer Hotspot_Nasional harus ada di proyek saat rumus agregasi dihitung?
  3. Apa beda $area dan area($geometry)?

Jawaban:

  1. Fitur yang sedang dibaca pada layer luar, misalnya petak KPH yang sedang dihitung.
  2. Karena aggregate mencari layer lain berdasarkan nama di proyek. Bila tidak ketemu, hasilnya NULL dengan pesan galat.
  3. $area mengikuti ellipsoid pengaturan proyek. area($geometry) memakai bidang datar peta.

#Kesalahan umum

  • Tertukar tanda kutip. "KPH Alpha" dibaca sebagai nama kolom dan menghasilkan galat Field 'KPH Alpha' not found. Teks ditulis 'KPH Alpha'.
  • Menyambung dengan nilai kosong. 'a' || NULL menghasilkan NULL, bukan a. Bungkus dengan coalesce(nilai, 0).
  • Layer agregasi tidak ada di proyek. Tambahkan layer ke proyek dengan nama yang sama persis.
  • Lupa memeriksa galat ekspresi. Pakai hasParserError() dan hasEvalError() sebelum memakai hasil.

#Ringkasan dan latihan

Ringkasan: ekspresi adalah rumus per fitur. Kutip ganda untuk kolom, kutip tunggal untuk teks. aggregate menghitung dari layer lain dengan filter intersects($geometry, geometry(@parent)). Ekspresi dapat dihitung dari Python (QgsExpression) maupun di dalam alat (Field calculator, Extract by expression).

Latihan:

  1. Tulis ekspresi yang menghasilkan "KPH Gamma: 8 ha, 5 titik panas". Hasil yang benar untuk Gamma memakai luas 8 dan jumlah titik 5.
  2. Pakai aggregate dengan 'mean' pada Survei.gpkg (layer Titik_Survei) untuk menghitung tinggi pohon rata-rata tiap petak. Hasil pada Skrip 5.1: Alpha 11,3, Beta 9,6, dan Gamma 8,9.

#Tabel perbandingan: ekspresi

HalQGISArcGIS ProArcMap 10.8
Bahasa ekspresiEkspresi QGISArcade atau Python [CEK]VB Script atau Python [CEK]
Kolom hitunganField calculatorCalculate Field [CEK]Field Calculator [CEK]
Agregasi layer lainaggregate(...)Arcade FeatureSetByName [CEK] atau Spatial JoinTidak ada padanan langsung; Spatial Join
Hitung dari PythonQgsExpressionCalculateField dengan "ARCADE" [CEK]CalculateField_management [CEK]
Dipakai di label dan atlasYaLabel Arcade [CEK]Tidak seperti QGIS [CEK]