Proyek Akhir: Sistem Informasi Hutan, Kebun, Pertanian, Perikanan, Peternakan
Semua skrip bab ini ada di folder kode/bab69/. Di versi web, Anda juga bisa mengunduh semuanya sekaligus (zip).
Anda sudah menanam banyak benih: dari program “Halo, Lahan!” di Bab 4 sampai pemroses citra terdistribusi di Bab 67. Kini saatnya panen: merakit semuanya menjadi satu kebun yang utuh. Bayangkan Proyek Akhir sebagai membangun stasiun lapangan terpadu: data masuk dari lapangan dan sensor, dirapikan dan disimpan, dianalisis, dipelajari oleh model, ditanyai lewat asisten AI, lalu disajikan lewat API dan dasbor yang dijaga tes dan dikemas rapi.
Catatan kejujuran. Bab ini tidak mengajarkan hal baru; ia menyambungkan hal-hal lama. Bagian “kerangka proyek kecil” di bawah benar-benar dijalankan dan keluarannya asli. Tetapi itu kerangka awal untuk belajar, bukan sistem produksi: datanya tiruan (fiktif), basisnya SQLite, dan tidak ada keamanan, pemantauan, atau skala sungguhan. Bagian besar lain (citra satelit, model ML, agent AI, Docker, CI) hanya ditunjuk ke bab asalnya dan tidak dijalankan ulang di sini; Docker bahkan tidak terpasang di komputer penulis.
Gambaran besar: arsitektur

Bacalah dari atas ke bawah. Sumber (lapangan, citra, dokumen) masuk ke lapisan validasi dan penyimpanan: tempat setiap data diperiksa sebelum disimpan, karena data yang salah di awal akan merusak segalanya di akhir. Dari penyimpanan, data mengalir ke tiga pengolah: analisis, model ML, dan agent AI. Hasilnya disajikan lewat API dan dasbor. Di bawah semuanya ada pita dasar: tes, Docker, CI, dan keamanan.
Pilih jalur dan sektor fokus
Anda tidak harus mengerjakan semuanya dengan kedalaman sama. Pilih satu jalur sebagai tulang punggung, lalu satu sektor sebagai bahan.
| Jalur fokus | Anda mendalami | Bab utama |
|---|---|---|
| Web | API, dasbor, aplikasi, pengemasan, deployment | 21, 34, 36, 37, 42, 43, 45, 48, 49 |
| Data dan statistik | pembersihan, basis data, statistik, regresi, data besar | 22–29, 50, 53 |
| GIS | peta vektor dan raster, citra, geospasial lanjut | 38–40, 51, 52 |
| ML dan AI | model klasik, deep learning, deteksi objek, LLM, agent | 54–62 |
| Sektor fokus | Indikator contoh di kerangka | Bab bahan khusus |
|---|---|---|
| Kehutanan | diameter pohon (cm) | 38, 39, 51, 56, 59 |
| Pertanian/perkebunan | hasil panen (kg) | 26, 27, 39, 54, 59 |
| Perikanan | bobot rata-rata ikan (g), kualitas air | 26, 52, 56, 59 |
| Peternakan | bobot ternak (kg) | 27, 52, 57, 59 |
Jalur dan sektor tidak mengunci Anda. Anda boleh mulai dari jalur web dengan sektor perikanan, lalu bulan depan menambah jalur ML. Yang penting: selesaikan satu alur ujung ke ujung dulu, baru perluas.
Struktur folder proyek
Susunlah proyek Anda sendiri seperti ini (kerangka di bab ini mengikuti sebagian besar susunan ini; Bab 18, 19, 45 menjelaskan alasannya):
sistem-informasi-lapangan/
README.md apa ini, cara menjalankan, status
pyproject.toml nama, versi, dependensi (Bab 17, 45)
src/sistem_lapangan/ kode utama dalam modul kecil
skema.py aturan data (Pydantic)
validasi.py baca dan periksa data mentah
basis_data.py simpan dan baca (SQLite, nanti PostGIS)
analisis.py pandas dan statistik
laporan.py laporan teks, Excel, PDF
api.py FastAPI
tes/ pytest (Bab 18)
data/ contoh data tiruan, bukan data rahasia
notebook/ eksplorasi (Jupyter)
docs/ catatan desain dan keputusan
Dockerfile kemasan (Bab 48)
.github/workflows/ CI (Bab 20, 48)
.gitignore jangan unggah rahasia dan hasil sementara (Bab 20, 49)
Daftar tahap dan bab bahannya
Kerjakan berurutan; tiap tahap menghasilkan sesuatu yang bisa diperiksa.
| Tahap | Yang dibuat | Bab bahan |
|---|---|---|
| 1. Rancang dan siapkan | tujuan, indikator, struktur folder, environment, git | 2, 17, 19, 20 |
| 2. Data lapangan dan IoT | CSV/JSON hasil survei, logger sensor | 13, 14, 15, 52 |
| 3. Validasi | aturan data, penolakan baris salah | 12, 22, 34 |
| 4. Penyimpanan | basis data dan aturan unik | 24, 40, 50 |
| 5. Pipeline citra | citra satelit/drone menjadi peta dan indeks | 38, 39, 51, 53 |
| 6. Analisis dan statistik | ringkasan, uji, regresi, laporan | 23, 25, 26, 27, 29 |
| 7. Model ML dan deteksi objek | klasifikasi, deteksi pohon/ikan/ternak | 54, 55, 56, 58, 59 |
| 8. Agent AI asisten | tanya-jawab dokumen, pemanggilan alat | 60, 61, 62, 63 |
| 9. API dan dasbor | layanan data, tampilan per sektor | 36, 42, 43 |
| 10. Kemas dan jaga | tes, kualitas, Docker, CI, keamanan | 18, 19, 44, 45, 48, 49 |
| 11. Etalase | README, portofolio | 63, 68 |
Kerangka proyek kecil yang bisa dijalankan
Sekarang bagian praktiknya. Kerangka ini memotong alur ujung ke ujung menjadi delapan skrip di atas satu paket kecil bernama sistem_lapangan. Alurnya:
CSV tiruan → validasi Pydantic → SQLite → analisis pandas → API FastAPI (diuji lewat TestClient) → laporan teks → tes pytest → pemeriksaan alur.
Setiap skrip memakai hasil skrip sebelumnya (berkas disimpan di subfolder _tmp/), jadi jalankan berurutan dari 01 sampai 08. Skrip 01 selalu memulai dari nol, sehingga hasilnya sama setiap kali.
Semua data di sini fiktif: delapan unit (dua per sektor), lima pengamatan mingguan tiap unit, ditambah beberapa baris yang sengaja kotor. Angka (diameter, hasil, bobot) hanyalah contoh latihan, bukan anjuran teknis. Sektor memakai satu indikator utama masing-masing: hutan diameter_cm, pertanian hasil_kg, perikanan bobot_rata_g, peternakan bobot_kg.
Aturan data: skema.py
Inilah jantung kerangka: satu kelas yang menyatakan apa itu pengamatan yang sah. Pydantic (Bab 34) memeriksa tipe, dan model_validator memeriksa hubungan antarkolom (indikator harus cocok dengan sektor, nilai harus dalam rentang wajar).
skema.py
"""Aturan data: satu pengamatan lapangan dan batas wajar nilainya (angka contoh, fiktif)."""
from datetime import date
from typing import Literal
from pydantic import BaseModel, Field, model_validator
# Setiap sektor punya satu indikator utama.
INDIKATOR = {
"hutan": "diameter_cm",
"pertanian": "hasil_kg",
"perikanan": "bobot_rata_g",
"peternakan": "bobot_kg",
}
# Batas wajar (bawah, atas) per indikator. Angka contoh, bukan anjuran teknis.
RENTANG = {
"diameter_cm": (1.0, 300.0),
"hasil_kg": (0.0, 5000.0),
"bobot_rata_g": (1.0, 3000.0),
"bobot_kg": (1.0, 1500.0),
}
class Pengamatan(BaseModel):
unit: str = Field(min_length=1, max_length=10)
sektor: Literal["hutan", "pertanian", "perikanan", "peternakan"]
tanggal: date
indikator: str
nilai: float
@model_validator(mode="after")
def periksa_silang(self):
harus = INDIKATOR[self.sektor]
if self.indikator != harus:
raise ValueError(f"sektor {self.sektor} memakai indikator {harus}, bukan {self.indikator}")
bawah, atas = RENTANG[self.indikator]
if not bawah <= self.nilai <= atas:
raise ValueError(f"{self.indikator} harus {bawah} sampai {atas}, bukan {self.nilai}")
return selfTahap 1: membuat data tiruan
01_buat_data_tiruan.py
# Bab 69 - Proyek Akhir, tahap 1: membuat data lapangan tiruan
from sistem_lapangan import KERJA
from sistem_lapangan.data_tiruan import tulis_csv
# Mulai dari nol: hapus sisa hasil lama supaya setiap jalankan menghasilkan hal yang sama.
for nama in ("lapangan.db", "laporan.txt"):
(KERJA / nama).unlink(missing_ok=True)
jumlah = tulis_csv(KERJA / "lapangan_mentah.csv")
print("baris data (tanpa judul kolom):", jumlah)
print("-- lima baris pertama --")
with (KERJA / "lapangan_mentah.csv").open(encoding="utf-8") as f:
for _ in range(6):
print(f.readline().rstrip())
print("-- enam baris terakhir (sengaja kotor atau ganda) --")
isi = (KERJA / "lapangan_mentah.csv").read_text(encoding="utf-8").splitlines()
for baris in isi[-6:]:
print(baris)baris data (tanpa judul kolom): 46
-- lima baris pertama --
unit,sektor,tanggal,indikator,nilai
H1,hutan,2026-01-05,diameter_cm,32.5
H1,hutan,2026-01-12,diameter_cm,32.8
H1,hutan,2026-01-19,diameter_cm,33.2
H1,hutan,2026-01-26,diameter_cm,33.6
H1,hutan,2026-02-02,diameter_cm,34.2
-- enam baris terakhir (sengaja kotor atau ganda) --
H1,hutan,2026-02-09,diameter_cm,-5
K1,perikanan,2026-02-09,bobot_kg,540
P2,pertanian,31-02-2026,hasil_kg,250
T1,empang,2026-02-09,bobot_kg,400
H2,hutan,2026-02-09,diameter_cm,900
H1,hutan,2026-01-05,diameter_cm,32.5
Ada 46 baris data: 40 baris wajar (4 sektor × 2 unit × 5 pekan), lalu 5 baris sengaja salah (nilai negatif, indikator tidak cocok, tanggal tidak ada, sektor tidak dikenal, nilai di luar rentang), lalu 1 baris yang ganda (salinan persis baris pertama). Data nyata pun begini: campur aduk.
Tahap 2: validasi
02_validasi_pydantic.py
# Bab 69 - Proyek Akhir, tahap 2: validasi dengan Pydantic
from sistem_lapangan import KERJA
from sistem_lapangan.validasi import validasi_csv
sah, ditolak = validasi_csv(KERJA / "lapangan_mentah.csv")
print("sah:", len(sah), "| ditolak:", len(ditolak))
for nomor, alasan in ditolak:
print(f"baris {nomor} di berkas: {alasan}")
print("contoh objek sah:", sah[0])sah: 41 | ditolak: 5
baris 42 di berkas: baris: Value error, diameter_cm harus 1.0 sampai 300.0, bukan -5.0
baris 43 di berkas: baris: Value error, sektor perikanan memakai indikator bobot_rata_g, bukan bobot_kg
baris 44 di berkas: tanggal: Input should be a valid date or datetime, invalid character in year
baris 45 di berkas: sektor: Input should be 'hutan', 'pertanian', 'perikanan' or 'peternakan'
baris 46 di berkas: baris: Value error, diameter_cm harus 1.0 sampai 300.0, bukan 900.0
contoh objek sah: unit='H1' sektor='hutan' tanggal=datetime.date(2026, 1, 5) indikator='diameter_cm' nilai=32.5
Dari 46 baris, 41 lolos dan 5 ditolak, masing-masing dengan alasan yang jelas dan nomor baris di berkas (baris 1 adalah judul kolom, jadi data mulai di baris 2). Perhatikan baris ganda lolos validasi: wajar, karena secara isi ia sah. Menolaknya adalah tugas tahap berikutnya.
Tahap 3: penyimpanan di SQLite
basis_data.py
"""Penyimpanan SQLite: satu tabel pengamatan dengan aturan unik."""
import sqlite3
from pathlib import Path
from .skema import Pengamatan
SKEMA_SQL = """
CREATE TABLE IF NOT EXISTS pengamatan (
id INTEGER PRIMARY KEY,
unit TEXT NOT NULL,
sektor TEXT NOT NULL,
tanggal TEXT NOT NULL,
indikator TEXT NOT NULL,
nilai REAL NOT NULL CHECK (nilai >= 0),
UNIQUE (unit, tanggal, indikator)
)
"""
def sambung(jalur: Path) -> sqlite3.Connection:
jalur.parent.mkdir(parents=True, exist_ok=True)
koneksi = sqlite3.connect(jalur)
koneksi.row_factory = sqlite3.Row
koneksi.execute(SKEMA_SQL)
return koneksi
def simpan(jalur: Path, daftar: list[Pengamatan]) -> tuple[int, int]:
"""Mengembalikan (jumlah baru, jumlah ganda yang dilewati)."""
koneksi = sambung(jalur)
baru = 0
try:
with koneksi: # with = transaksi: semua tersimpan atau tidak sama sekali
for p in daftar:
kursor = koneksi.execute(
"INSERT OR IGNORE INTO pengamatan (unit, sektor, tanggal, indikator, nilai) "
"VALUES (?, ?, ?, ?, ?)",
(p.unit, p.sektor, p.tanggal.isoformat(), p.indikator, p.nilai),
)
baru += kursor.rowcount
finally:
koneksi.close()
return baru, len(daftar) - baru
def baca(jalur: Path, sektor: str | None = None) -> list[dict]:
sql = "SELECT unit, sektor, tanggal, indikator, nilai FROM pengamatan"
argumen: tuple = ()
if sektor:
sql += " WHERE sektor = ?"
argumen = (sektor,)
sql += " ORDER BY sektor, unit, tanggal"
koneksi = sambung(jalur)
try:
return [dict(b) for b in koneksi.execute(sql, argumen)]
finally:
koneksi.close()03_simpan_sqlite.py
# Bab 69 - Proyek Akhir, tahap 3: simpan ke SQLite
from sistem_lapangan import KERJA
from sistem_lapangan.basis_data import baca, simpan
from sistem_lapangan.validasi import validasi_csv
sah, ditolak = validasi_csv(KERJA / "lapangan_mentah.csv")
baru, ganda = simpan(KERJA / "lapangan.db", sah)
print(f"tersimpan baru: {baru} | dilewati karena ganda: {ganda}")
# Dijalankan dua kali, hasilnya tetap sama: tidak ada baris bertambah.
baru2, ganda2 = simpan(KERJA / "lapangan.db", sah)
print(f"simpan ulang -> baru: {baru2} | ganda: {ganda2}")
semua = baca(KERJA / "lapangan.db")
print("jumlah baris di basis data:", len(semua))
print("baris pertama:", semua[0])tersimpan baru: 40 | dilewati karena ganda: 1
simpan ulang -> baru: 0 | ganda: 41
jumlah baris di basis data: 40
baris pertama: {'unit': 'H1', 'sektor': 'hutan', 'tanggal': '2026-01-05', 'indikator': 'diameter_cm', 'nilai': 32.5}
Aturan UNIQUE (unit, tanggal, indikator) membuat basis data menolak duplikat; INSERT OR IGNORE melewatinya tanpa error. Hasilnya: 40 baru, 1 ganda dilewati. Menyimpan kedua kali menambah 0 baris: operasi ini aman diulang (idempoten). Itu sifat penting bagi pipeline yang berjalan terjadwal.
Tahap 4: analisis dengan pandas
04_analisis_pandas.py
# Bab 69 - Proyek Akhir, tahap 4: analisis dengan pandas
import pandas as pd
from sistem_lapangan import KERJA
from sistem_lapangan.analisis import laju_per_unit, muat, ringkasan_sektor, tandai_anomali
SEKTOR_FOKUS = "perikanan" # ganti dengan sektor pilihan Anda: hutan, pertanian, perikanan, peternakan
pd.set_option("display.width", 100)
df = muat(KERJA / "lapangan.db")
print("bentuk tabel (baris, kolom):", df.shape)
print()
print(ringkasan_sektor(df).to_string(index=False))
print()
laju = laju_per_unit(df)
print(laju.to_string(index=False))
print()
print(f"-- fokus: {SEKTOR_FOKUS} --")
print(df[df["sektor"] == SEKTOR_FOKUS].pivot(index="tanggal", columns="unit", values="nilai"))
print()
anomali = tandai_anomali(df, batas_z=1.5)
print("nilai yang ditandai (|z| > 1,5):", len(anomali))bentuk tabel (baris, kolom): (40, 5)
sektor indikator banyak rata_rata terkecil terbesar
hutan diameter_cm 10 34.03 32.5 35.5
perikanan bobot_rata_g 10 597.15 539.4 659.8
pertanian hasil_kg 10 256.53 242.2 270.5
peternakan bobot_kg 10 369.87 351.8 388.0
sektor unit naik_per_minggu
hutan H1 0.42
hutan H2 0.35
perikanan K1 27.61
perikanan K2 30.07
pertanian P1 6.55
pertanian P2 5.53
peternakan T1 8.64
peternakan T2 9.06
-- fokus: perikanan --
unit K1 K2
tanggal
2026-01-05 545.4 539.4
2026-01-12 571.7 563.5
2026-01-19 593.9 591.5
2026-01-26 619.0 631.6
2026-02-02 659.8 655.7
nilai yang ditandai (|z| > 1,5): 3
Tabel berisi 40 baris dan 5 kolom. Ringkasan per sektor menunjukkan, misalnya, rata-rata bobot ikan 597,15 g dari 10 pengamatan. Tabel kedua adalah laju naik per minggu tiap unit (kemiringan garis lurus; bobot ikan di kolam K2 naik sekitar 30,07 g per pekan pada data tiruan ini). Variabel SEKTOR_FOKUS di atas skrip bisa Anda ganti ke sektor pilihan Anda. Terakhir, 3 nilai ditandai karena menyimpang lebih dari 1,5 simpangan baku dari rata-rata unitnya; ambang ini sekadar contoh.
Tahap 5: API FastAPI diuji dengan TestClient
api.py
"""API FastAPI di atas basis data SQLite."""
from pathlib import Path
from fastapi import FastAPI, HTTPException
from . import basis_data
from .analisis import muat, ringkasan_sektor
from .skema import Pengamatan
def buat_app(jalur_db: Path) -> FastAPI:
app = FastAPI(title="Sistem Informasi Lapangan (tiruan)")
@app.get("/sehat")
def sehat():
return {"status": "ok", "jumlah": len(basis_data.baca(jalur_db))}
@app.get("/pengamatan", response_model=list[Pengamatan])
def daftar(sektor: str | None = None):
return basis_data.baca(jalur_db, sektor)
@app.post("/pengamatan", response_model=Pengamatan, status_code=201)
def tambah(p: Pengamatan):
baru, _ = basis_data.simpan(jalur_db, [p])
if not baru:
raise HTTPException(409, "pengamatan itu sudah ada")
return p
@app.get("/ringkasan")
def ringkasan():
return ringkasan_sektor(muat(jalur_db)).to_dict(orient="records")
return app05_api_testclient.py
# Bab 69 - Proyek Akhir, tahap 5: API FastAPI diuji lewat TestClient
from fastapi.testclient import TestClient
from sistem_lapangan import KERJA
from sistem_lapangan.api import buat_app
klien = TestClient(buat_app(KERJA / "lapangan.db"))
print("sehat:", klien.get("/sehat").json())
perikanan = klien.get("/pengamatan", params={"sektor": "perikanan"}).json()
print("pengamatan perikanan:", len(perikanan), "| pertama:", perikanan[0])
baru = {"unit": "K1", "sektor": "perikanan", "tanggal": "2026-03-02",
"indikator": "bobot_rata_g", "nilai": 700.0}
print("tambah baru:", klien.post("/pengamatan", json=baru).status_code)
print("tambah lagi (sama):", klien.post("/pengamatan", json=baru).status_code)
salah = dict(baru, tanggal="2026-03-09", nilai=-1)
jawab = klien.post("/pengamatan", json=salah)
print("nilai tidak masuk akal:", jawab.status_code, "->", jawab.json()["detail"][0]["msg"])
for baris in klien.get("/ringkasan").json():
print(baris["sektor"], baris["indikator"], baris["banyak"], baris["rata_rata"])sehat: {'status': 'ok', 'jumlah': 40}
pengamatan perikanan: 10 | pertama: {'unit': 'K1', 'sektor': 'perikanan', 'tanggal': '2026-01-05', 'indikator': 'bobot_rata_g', 'nilai': 545.4}
tambah baru: 201
tambah lagi (sama): 409
nilai tidak masuk akal: 422 -> Value error, bobot_rata_g harus 1.0 sampai 3000.0, bukan -1.0
hutan diameter_cm 10 34.03
perikanan bobot_rata_g 11 606.5
pertanian hasil_kg 10 256.53
peternakan bobot_kg 10 369.87
TestClient memanggil aplikasi tanpa menyalakan server, jadi cepat dan bisa dipakai di tes. Perhatikan tiga kode status: 201 (berhasil dibuat), 409 (konflik: sudah ada), dan 422 (data tidak masuk akal; aturan skema.py yang sama dipakai ulang oleh API). Setelah menambah satu baris, perikanan punya 11 pengamatan dan rata-rata berubah menjadi 606,5 g. Data di basis sekarang 41 baris, dan itu yang akan dibaca tahap berikutnya.
Tahap 6: laporan
laporan.py
"""Menyusun laporan teks dari hasil analisis."""
from pathlib import Path
import pandas as pd
def susun(ringkasan: pd.DataFrame, laju: pd.DataFrame, jumlah_ditolak: int) -> str:
baris = ["LAPORAN SISTEM INFORMASI LAPANGAN (data tiruan)", "=" * 48]
baris.append(f"Baris mentah ditolak saat validasi: {jumlah_ditolak}")
baris.append("")
baris.append("Ringkasan per sektor:")
for _, b in ringkasan.iterrows():
baris.append(
f"- {b['sektor']:<10} {b['indikator']:<13} rata-rata {b['rata_rata']:>7.2f} "
f"(dari {b['banyak']} pengamatan)"
)
baris.append("")
baris.append("Unit dengan kenaikan tercepat per sektor:")
for sektor, grup in laju.groupby("sektor"):
teratas = grup.sort_values("naik_per_minggu", ascending=False).iloc[0]
baris.append(f"- {sektor:<10} {teratas['unit']}: +{teratas['naik_per_minggu']} per minggu")
baris.append("")
baris.append("Catatan: semua angka adalah data tiruan untuk latihan.")
return "\n".join(baris) + "\n"
def tulis(jalur: Path, teks: str) -> None:
jalur.parent.mkdir(parents=True, exist_ok=True)
jalur.write_text(teks, encoding="utf-8")06_laporan.py
# Bab 69 - Proyek Akhir, tahap 6: laporan teks
from sistem_lapangan import KERJA
from sistem_lapangan.analisis import laju_per_unit, muat, ringkasan_sektor
from sistem_lapangan.laporan import susun, tulis
from sistem_lapangan.validasi import validasi_csv
_, ditolak = validasi_csv(KERJA / "lapangan_mentah.csv")
df = muat(KERJA / "lapangan.db") # sudah memuat satu baris tambahan dari tahap 5
teks = susun(ringkasan_sektor(df), laju_per_unit(df), len(ditolak))
tulis(KERJA / "laporan.txt", teks)
print(teks)
print("laporan ditulis ke berkas laporan.txt di folder _tmp")LAPORAN SISTEM INFORMASI LAPANGAN (data tiruan)
================================================
Baris mentah ditolak saat validasi: 5
Ringkasan per sektor:
- hutan diameter_cm rata-rata 34.03 (dari 10 pengamatan)
- perikanan bobot_rata_g rata-rata 606.50 (dari 11 pengamatan)
- pertanian hasil_kg rata-rata 256.53 (dari 10 pengamatan)
- peternakan bobot_kg rata-rata 369.87 (dari 10 pengamatan)
Unit dengan kenaikan tercepat per sektor:
- hutan H1: +0.42 per minggu
- perikanan K2: +30.07 per minggu
- pertanian P1: +6.55 per minggu
- peternakan T2: +9.06 per minggu
Catatan: semua angka adalah data tiruan untuk latihan.
laporan ditulis ke berkas laporan.txt di folder _tmp
Laporan menyusun hasil analisis menjadi teks yang bisa dibaca atasan, lengkap dengan jumlah baris yang ditolak dan catatan bahwa angkanya tiruan. Anda bisa menggantinya menjadi Excel atau PDF dengan teknik Bab 15.
Tahap 7: tes pytest
test_sistem.py
"""Tes kerangka Sistem Informasi Lapangan. Memakai folder sementara, tidak menyentuh _tmp."""
import pytest
from fastapi.testclient import TestClient
from sistem_lapangan.api import buat_app
from sistem_lapangan.basis_data import baca, simpan
from sistem_lapangan.data_tiruan import tulis_csv
from sistem_lapangan.skema import Pengamatan
from sistem_lapangan.validasi import validasi_csv
@pytest.fixture
def siap(tmp_path):
csv = tmp_path / "mentah.csv"
tulis_csv(csv)
sah, ditolak = validasi_csv(csv)
db = tmp_path / "lapangan.db"
simpan(db, sah)
return db, sah, ditolak
def test_validasi_menolak_baris_kotor(siap):
_, sah, ditolak = siap
assert len(sah) == 41 # 40 sah + 1 salinan ganda
assert len(ditolak) == 5
def test_nilai_negatif_ditolak():
with pytest.raises(ValueError):
Pengamatan(unit="H1", sektor="hutan", tanggal="2026-01-05", indikator="diameter_cm", nilai=-5)
def test_indikator_harus_cocok_sektor():
with pytest.raises(ValueError, match="memakai indikator"):
Pengamatan(unit="K1", sektor="perikanan", tanggal="2026-01-05", indikator="bobot_kg", nilai=500)
def test_simpan_dua_kali_tidak_menggandakan(siap):
db, sah, _ = siap
assert len(baca(db)) == 40
assert simpan(db, sah) == (0, 41)
assert len(baca(db)) == 40
def test_api_tambah_dan_konflik(siap):
db, _, _ = siap
klien = TestClient(buat_app(db))
baru = {"unit": "T1", "sektor": "peternakan", "tanggal": "2026-03-02",
"indikator": "bobot_kg", "nilai": 400.0}
assert klien.post("/pengamatan", json=baru).status_code == 201
assert klien.post("/pengamatan", json=baru).status_code == 409
assert klien.get("/sehat").json()["jumlah"] == 41
def test_api_menolak_nilai_salah(siap):
db, _, _ = siap
klien = TestClient(buat_app(db))
salah = {"unit": "H1", "sektor": "hutan", "tanggal": "2026-03-02",
"indikator": "diameter_cm", "nilai": 900}
assert klien.post("/pengamatan", json=salah).status_code == 42207_jalankan_tes.py
# Bab 69 - Proyek Akhir, tahap 7: menjalankan tes pytest
import os
import re
import subprocess
import sys
from pathlib import Path
folder = Path(__file__).resolve().parent
env = dict(os.environ, PYTHONPATH=str(folder), NO_COLOR="1")
hasil = subprocess.run(
[sys.executable, "-m", "pytest", "tes", "-v", "-p", "no:cacheprovider", "--color=no"],
cwd=folder, env=env, text=True, encoding="utf-8",
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
)
for baris in hasil.stdout.splitlines():
if baris.startswith(("platform ", "rootdir:", "configfile:", "plugins:", "cachedir:")):
continue
print(re.sub(r" in \d+\.\d+s( \(.*?\))?", "", baris))
print("kode keluar:", hasil.returncode)============================= test session starts =============================
collecting ... collected 6 items
tes/test_sistem.py::test_validasi_menolak_baris_kotor PASSED [ 16%]
tes/test_sistem.py::test_nilai_negatif_ditolak PASSED [ 33%]
tes/test_sistem.py::test_indikator_harus_cocok_sektor PASSED [ 50%]
tes/test_sistem.py::test_simpan_dua_kali_tidak_menggandakan PASSED [ 66%]
tes/test_sistem.py::test_api_tambah_dan_konflik PASSED [ 83%]
tes/test_sistem.py::test_api_menolak_nilai_salah PASSED [100%]
============================== 6 passed ==============================
kode keluar: 0
Enam tes lulus, dan tes memakai folder sementara sehingga tidak mengganggu hasil tahap-tahap sebelumnya. Pegang prinsip ini: kalau sesuatu penting, tulis tesnya. Baris yang tertera di keluaran berasal dari pytest sungguhan; skrip hanya membuang baris yang berubah-ubah (jalur dan waktu).
Tahap 8: memeriksa alur ujung ke ujung
08_periksa_alur.py
# Bab 69 - Proyek Akhir, tahap 8: pemeriksaan otomatis alur ujung ke ujung
from sistem_lapangan import KERJA
from sistem_lapangan.basis_data import baca
from sistem_lapangan.validasi import validasi_csv
sah, ditolak = validasi_csv(KERJA / "lapangan_mentah.csv")
di_basis = len(baca(KERJA / "lapangan.db"))
laporan = (KERJA / "laporan.txt").read_text(encoding="utf-8")
pemeriksaan = [
("CSV mentah ada", (KERJA / "lapangan_mentah.csv").exists()),
("ada baris yang ditolak validasi", len(ditolak) == 5),
("baris di basis data = sah - ganda + 1 tambahan API", di_basis == len(sah) - 1 + 1),
("laporan menyebut semua empat sektor",
all(s in laporan for s in ("hutan", "pertanian", "perikanan", "peternakan"))),
("laporan memuat catatan data tiruan", "data tiruan" in laporan),
]
for nama, lulus in pemeriksaan:
print(("LULUS " if lulus else "GAGAL ") + nama)
print(f"{sum(l for _, l in pemeriksaan)} dari {len(pemeriksaan)} pemeriksaan lulus")LULUS CSV mentah ada
LULUS ada baris yang ditolak validasi
LULUS baris di basis data = sah - ganda + 1 tambahan API
LULUS laporan menyebut semua empat sektor
LULUS laporan memuat catatan data tiruan
5 dari 5 pemeriksaan lulus
Pemeriksaan terakhir ini mencocokkan angka antar-tahap: berkas ada, 5 baris ditolak, basis data berisi 41 baris, laporan memuat keempat sektor. Kalau salah satu “GAGAL”, Anda tahu tahap mana yang perlu ditelusuri. Kebiasaan membuktikan, bukan mengira, adalah ciri sistem yang bisa dipercaya.
Kerangka ini memakai SQLite agar bisa jalan di mana saja tanpa pemasangan. Untuk data spasial dan banyak pengguna, ganti lapisan basis_data.py dengan PostgreSQL + PostGIS (Bab 24 dan 40). Karena akses data hanya lewat satu modul, penggantian itu tidak perlu menyentuh skema, analisis, atau tes. Itulah gunanya memisahkan modul.
Dari kerangka ke sistem utuh
Kerangka di atas baru menyentuh tahap 1 sampai 4, 6, 9 (API), dan 10 (tes). Untuk melengkapinya:
- Citra dan model (tahap 5 dan 7): tambahkan modul yang membaca citra (Bab 39, 51), menghitung indeks, dan menyimpan hasilnya ke basis data; lalu latih model sederhana (Bab 54) dan, bila sektor Anda cocok, deteksi objek (Bab 56, 59). Lihat dulu apakah komputer Anda punya GPU (Bab 55).
- Agent AI (tahap 8): bungkus fungsi
ringkasandan pencarian data sebagai alat bagi agent (Bab 62), dengan batas izin yang ketat dan tanpa menaruh kunci API di kode (Bab 49, 60). - Dasbor (tahap 9): tampilkan
/ringkasandan peta di Streamlit atau Dash (Bab 43). - Kemas dan jaga (tahap 10): tulis
Dockerfiledan alur CI mengikuti Bab 48, jalankan audit dependensi (Bab 49), dan periksa lisensi data dan pustaka (Bab 63).
Rubrik penilaian mandiri
Nilai diri Anda jujur. Beri 0 (belum), 1 (sebagian), atau 2 (ya, dengan bukti) pada tiap baris. Bukti berarti sesuatu yang bisa ditunjukkan: keluaran perintah, berkas tes, atau tangkapan layar.
| Kriteria | Bukti yang dicari |
|---|---|
| Alur ujung ke ujung berjalan | satu perintah atau urutan skrip dari data mentah sampai laporan |
| Data divalidasi | baris salah ditolak dengan alasan jelas, bukan diam-diam |
| Penyimpanan aman diulang | menjalankan dua kali tidak menggandakan data |
| Analisis benar dan bisa dijelaskan | angka cocok dengan hitungan manual pada contoh kecil |
| API punya tes | tes untuk jalur sukses dan jalur gagal (201, 409, 422) |
| Kode rapi | lolos ruff, ada docstring dan type hint pada fungsi penting (Bab 19, 33) |
| Rahasia dan data pribadi aman | tidak ada kunci di repo; .gitignore benar (Bab 20, 49) |
| Dapat dipakai orang lain | README yang jelas, dependensi tercatat, bisa dijalankan dari nol (Bab 17, 68) |
| Jujur soal batas | README menyebut data tiruan/nyata, apa yang diuji dan tidak |
| Lisensi dan etika dicek | lisensi data dan pustaka tercatat (Bab 63) |
Jumlahkan: 0–7 = masih rangka, kerjakan alurnya dulu; 8–14 = sudah berjalan, perkuat tes dan dokumentasi; 15–20 = siap dipamerkan di portofolio. Batas-batas ini sekadar anjuran penulis, bukan standar resmi.
Latihan
- Jalankan kerangka dari
01sampai08. Lalu ubahSEKTOR_FOKUSdi04_analisis_pandas.pyke sektor lain dan catat apa yang berubah. - Tambahkan satu baris kotor baru di
data_tiruan.py(misalnya unit kosong). Jalankan ulang dan pastikan jumlah baris ditolak berubah menjadi 6 di tahap 2. Perbarui tes yang menyebut angka 5. - Tambahkan titik akhir
GET /unit/{kode}diapi.pyyang mengembalikan pengamatan satu unit (dan 404 bila tidak ada), lengkap dengan dua tes. - Tantangan: ganti
basis_data.pyagar memakai SQLAlchemy (Bab 24) tanpa mengubah skrip01sampai08selain impor. Tes harus tetap lulus.
Ringkasan Bab
- Proyek Akhir merakit seluruh buku: data → validasi → penyimpanan → analisis/model/agent → API dan dasbor → tes, kemasan, dan CI.
- Pilih satu jalur (web, data/statistik, GIS, ML/AI) dan satu sektor, lalu selesaikan satu alur ujung ke ujung dulu.
- Kerangka kecil di bab ini dijalankan sungguhan pada data tiruan: 46 baris mentah, 41 lolos validasi, 40 tersimpan, satu ditambah lewat API, dan 6 tes lulus.
- Ini kerangka belajar, bukan sistem produksi.
- Nilai diri dengan rubrik yang menuntut bukti.
Selanjutnya, di Bagian Sepuluh: daftar-daftar sepuluh butir yang ringkas untuk dibaca ulang: kesalahan pemula, kebiasaan baik, pustaka penting, sumber data gratis, ide proyek lanjutan, dan cara terus belajar.