05 — From Analytics to Decision-Support¶

Development dimensions, hazard context, policy signals, dan batas klaim

Notebook ini adalah POC portfolio yang dapat dijalankan ulang dari artefak lokal Pasuruan Lens. Kode produksi tetap berada di python/scripts/ dan python/src/pasuruan365/; notebook berfungsi sebagai narasi analitik yang ringkas, transparan, dan mudah dipresentasikan.

from pathlib import Path
import json
import sys

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns


def find_project_root(start: Path | None = None) -> Path:
    start = (start or Path.cwd()).resolve()
    for candidate in (start, *start.parents):
        if (candidate / "package.json").exists() and (candidate / "data").exists():
            return candidate
    raise RuntimeError("Root Pasuruan Lens tidak ditemukan. Jalankan notebook dari repository ini.")


ROOT = find_project_root()
sys.path.insert(0, str(ROOT / "python" / "src"))

pd.set_option("display.max_columns", 100)
pd.set_option("display.max_colwidth", 100)
sns.set_theme(style="whitegrid", context="notebook")
COLORS = {"A": "#2563eb", "B": "#f59e0b", "C": "#10b981"}

print(f"Project root: {ROOT}")
print(f"Python: {sys.version.split()[0]}")
Project root: PASURUAN365
Python: 3.12.6

Prinsip produk¶

Tidak semua indikator layak digabung menjadi satu angka. Pasuruan Lens memisahkan tiga pertanyaan: variasi dimensi pembangunan, kejadian bahaya yang terdokumentasi, dan policy opportunity yang dapat ditelaah. Pemisahan ini mencegah cluster, hazard, dan service availability berubah menjadi ranking semu.

dimensions = pd.read_csv(ROOT / "data/analytics/development_dimension_profiles.csv", dtype={"village_id": str})
hazards = pd.read_csv(ROOT / "data/analytics/hazard_exposure_profiles.csv", dtype={"village_id": str})
policy = pd.read_csv(ROOT / "data/analytics/policy/village_policy_profiles.csv", dtype={"village_id": str})
components = pd.read_csv(ROOT / "data/analytics/policy/policy_spatial_components.csv")

pd.DataFrame({
    "rows": [len(dimensions), len(hazards), len(policy), len(components)],
    "villages": [dimensions.village_id.nunique(), hazards.village_id.nunique(), policy.village_id.nunique(), np.nan],
    "categories": [dimensions.dimension_id.nunique(), hazards.hazard_id.nunique(), policy.policy_lens.nunique(), components.policyLens.nunique()],
}, index=["development", "hazard", "policy", "spatial components"])
rows villages categories
development 2555 365.0 7
hazard 1460 365.0 4
policy 1825 365.0 5
spatial components 33 NaN 5

POC 1 — Dimension readiness, bukan composite score¶

dimension_status = (
    dimensions.groupby(["dimension_id", "dimension_status"])
    .agg(villages=("village_id", "nunique"), numeric_values=("dimension_value_if_applicable", "count"))
    .reset_index()
)
dimension_status
dimension_id dimension_status villages numeric_values
0 BASIC_SERVICE_AVAILABILITY INDICATOR_PROFILE 365 0
1 BASIC_UTILITIES INDICATOR_PROFILE 365 0
2 DIGITAL_COMMUNICATION_CONNECTIVITY INSUFFICIENT_INDICATOR_COVERAGE 15 0
3 DIGITAL_COMMUNICATION_CONNECTIVITY PUBLIC_RELATIVE_POSITION 350 350
4 ECONOMIC_SERVICE_INFRASTRUCTURE INDICATOR_PROFILE 365 0
5 EDUCATION_SERVICE_AVAILABILITY NOT_AVAILABLE 365 0
6 INSTITUTIONAL_PUBLIC_SERVICE_CAPACITY DESCRIPTIVE_ONLY 365 0
7 TRANSPORT_SERVICE_AVAILABILITY INDICATOR_PROFILE 365 0
assert not any("overall" in c.lower() or "rank" in c.lower() for c in dimensions.columns)
print("✓ Dataset memublikasikan profil per dimensi tanpa overall development score atau ranking.")
✓ Dataset memublikasikan profil per dimensi tanpa overall development score atau ranking.

POC 2 — Hazard tetap berupa kejadian biner terpisah¶

hazard_counts = (
    hazards.groupby(["hazard_id", "hazard_name"], as_index=False)
    .agg(documented_occurrence=("documented_occurrence", "sum"), coverage=("village_id", "nunique"))
)
hazard_counts
hazard_id hazard_name documented_occurrence coverage
0 EARTHQUAKE Gempa Bumi 30 365
1 FLOOD Banjir 64 365
2 LANDSLIDE Tanah Longsor 24 365
3 TORNADO Angin Puyuh 48 365
ax = hazard_counts.sort_values("documented_occurrence").plot.barh(
    x="hazard_name", y="documented_occurrence", legend=False, figsize=(8, 4), color="#ef4444"
)
ax.set(title="Unit dengan kejadian terdokumentasi (referensi 2024)", xlabel="Jumlah desa/kelurahan", ylabel="")
plt.tight_layout()
Visualisasi output notebook Pasuruan Lens

POC 3 — Policy signal dan konsentrasi spasial¶

policy_summary = (
    policy.groupby("policy_lens", as_index=False)
    .agg(
        signals=("is_opportunity_signal", "sum"),
        spatially_concentrated=("spatial_gap_status", lambda s: s.eq("SPATIALLY_CONCENTRATED_GAP").sum()),
        data_limited=("evidence_strength", lambda s: s.eq("LIMITED_EVIDENCE").sum()),
    )
)
policy_summary
policy_lens signals spatially_concentrated data_limited
0 BASIC_SERVICE_AVAILABILITY 291 278 0
1 BASIC_UTILITIES 289 277 0
2 DIGITAL_COMMUNICATION_CONNECTIVITY 87 56 15
3 ECONOMIC_SERVICE_INFRASTRUCTURE 310 305 7
4 TRANSPORT_SERVICE_AVAILABILITY 91 58 3
component_summary = (
    components.groupby("policyLens", as_index=False)
    .agg(
        components=("componentId", "count"),
        largest_component=("componentSize", "max"),
        cross_district_components=("crossDistrict", "sum"),
    )
    .sort_values("largest_component", ascending=False)
)
component_summary
policyLens components largest_component cross_district_components
3 ECONOMIC_SERVICE_INFRASTRUCTURE 2 308 1
0 BASIC_SERVICE_AVAILABILITY 3 288 1
1 BASIC_UTILITIES 3 286 1
2 DIGITAL_COMMUNICATION_CONNECTIVITY 9 62 3
4 TRANSPORT_SERVICE_AVAILABILITY 16 47 4

POC 4 — Satu desa, evidence yang tetap terpisah¶

sample = policy.loc[policy.is_opportunity_signal].iloc[0]
sample_profile = policy.loc[
    policy.village_id.eq(sample.village_id),
    ["village_name", "district_name", "policy_lens", "signal_basis", "policy_signal", "evidence_strength", "spatial_gap_status", "typology", "hazard_context", "reference_period"],
]
sample_profile
village_name district_name policy_lens signal_basis policy_signal evidence_strength spatial_gap_status typology hazard_context reference_period
0 Gerbo Purwodadi DIGITAL_COMMUNICATION_CONNECTIVITY RELATIVE_PERCENTILE NO_RELATIVE_GAP_DETECTED STRONG_EVIDENCE NOT_APPLICABLE C [] primarily 2024
1 Gerbo Purwodadi TRANSPORT_SERVICE_AVAILABILITY VALIDATED_ABSOLUTE_CONDITION NO_ABSOLUTE_GAP_DETECTED MODERATE_EVIDENCE NOT_APPLICABLE C [] primarily 2024
2 Gerbo Purwodadi BASIC_SERVICE_AVAILABILITY VALIDATED_ABSOLUTE_CONDITION NO_ABSOLUTE_GAP_DETECTED MODERATE_EVIDENCE NOT_APPLICABLE C [] primarily 2024
3 Gerbo Purwodadi ECONOMIC_SERVICE_INFRASTRUCTURE VALIDATED_ABSOLUTE_CONDITION ABSOLUTE_GAP MODERATE_EVIDENCE SPATIALLY_CONCENTRATED_GAP C [] primarily 2024
4 Gerbo Purwodadi BASIC_UTILITIES VALIDATED_ABSOLUTE_CONDITION ABSOLUTE_GAP MODERATE_EVIDENCE SPATIALLY_CONCENTRATED_GAP C [] primarily 2024

Kesimpulan¶

Actionable di sini berarti evidence dapat ditelusuri dan layak ditelaah, bukan instruksi alokasi anggaran. Lima policy lens menggunakan rule yang sesuai dengan semantik indikator; hazard dan tipologi tetap menjadi konteks terpisah. Overall priority memerlukan objective, bobot normatif, data outcome/equity/accessibility, serta proses governance yang belum tersedia.