01 — Data Foundation & Quality¶
Identitas kanonik, integritas join, missingness, dan lineage
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
Mengapa tahap ini penting¶
Nama desa dapat berubah ejaan, kode antar-sumber belum tentu setara, dan nilai kosong tidak boleh otomatis dianggap nol. Karena itu, unit analisis dikunci lebih dahulu menggunakan master Kemendagri dan seluruh sumber dipetakan ke village_id yang sama.
master = pd.read_csv(ROOT / "data/reference/village_master.csv", dtype={"village_id": str})
features = pd.read_csv(ROOT / "data/processed/village_features.csv", dtype={"village_id": str})
lineage = pd.read_csv(ROOT / "data/processed/village_feature_lineage.csv", dtype={"village_id": str})
pd.DataFrame({
"rows": [len(master), len(features), len(lineage)],
"columns": [master.shape[1], features.shape[1], lineage.shape[1]],
"unique_village_id": [master.village_id.nunique(), features.village_id.nunique(), lineage.village_id.nunique()],
}, index=["canonical_master", "processed_features", "value_lineage"])
| rows | columns | unique_village_id | |
|---|---|---|---|
| canonical_master | 365 | 13 | 365 |
| processed_features | 365 | 98 | 365 |
| value_lineage | 17922 | 12 | 365 |
POC 1 — Validasi identitas dan grain¶
identity_checks = pd.Series({
"365 unit aktif": len(master) == 365 and master["is_active"].astype(str).str.lower().eq("true").all(),
"village_id unik": master.village_id.is_unique,
"24 kecamatan": master.district_id.nunique() == 24,
"341 desa": master.administrative_type.eq("DESA").sum() == 341,
"24 kelurahan": master.administrative_type.eq("KELURAHAN").sum() == 24,
"fitur tidak kehilangan unit": set(features.village_id) == set(master.village_id),
"lineage hanya memakai ID kanonik": set(lineage.village_id).issubset(set(master.village_id)),
})
identity_checks.to_frame("lulus")
| lulus | |
|---|---|
| 365 unit aktif | True |
| village_id unik | True |
| 24 kecamatan | True |
| 341 desa | True |
| 24 kelurahan | True |
| fitur tidak kehilangan unit | True |
| lineage hanya memakai ID kanonik | True |
POC 2 — Missingness bukan zero¶
analysis_columns = [
"population", "area_km2", "population_density", "pustu_count",
"banking_facility_count", "flood_event_presence_2024",
"temporary_waste_collection_presence",
]
quality_profile = pd.DataFrame({
"dtype": features[analysis_columns].dtypes.astype(str),
"non_null": features[analysis_columns].notna().sum(),
"missing": features[analysis_columns].isna().sum(),
"zero": features[analysis_columns].eq(0).sum(),
"unique": features[analysis_columns].nunique(dropna=True),
})
quality_profile["coverage_pct"] = (quality_profile["non_null"] / len(features) * 100).round(1)
quality_profile.sort_values(["coverage_pct", "unique"])
| dtype | non_null | missing | zero | unique | coverage_pct | |
|---|---|---|---|---|---|---|
| pustu_count | float64 | 118 | 247 | 112 | 2 | 32.3 |
| flood_event_presence_2024 | int64 | 365 | 0 | 301 | 2 | 100.0 |
| temporary_waste_collection_presence | int64 | 365 | 0 | 191 | 2 | 100.0 |
| banking_facility_count | int64 | 365 | 0 | 310 | 9 | 100.0 |
| area_km2 | float64 | 365 | 0 | 0 | 270 | 100.0 |
| population | int64 | 365 | 0 | 0 | 359 | 100.0 |
| population_density | float64 | 365 | 0 | 0 | 365 | 100.0 |
example = quality_profile.loc[["pustu_count", "flood_event_presence_2024"]]
assert example.loc["pustu_count", "missing"] > 0
assert example.loc["flood_event_presence_2024", "missing"] == 0
print("Pustu: sebagian null karena skema sumber tidak lengkap.")
print("Hazard biner: cakupan lengkap; 0 berarti tidak tercatat pada periode referensi, bukan 'aman'.")
Pustu: sebagian null karena skema sumber tidak lengkap. Hazard biner: cakupan lengkap; 0 berarti tidak tercatat pada periode referensi, bukan 'aman'.
POC 3 — Audit lineage untuk satu desa dan fitur¶
sample_id = master.loc[0, "village_id"]
audit = lineage.loc[
lineage.village_id.eq(sample_id)
& lineage.feature_name.isin(["population", "area_km2", "population_density", "flood_event_presence_2024"]),
["village_id", "feature_name", "value", "source_id", "source_variable", "reference_year", "raw_table", "validation_status", "lineage_expression"],
].sort_values("feature_name")
audit
| village_id | feature_name | value | source_id | source_variable | reference_year | raw_table | validation_status | lineage_expression | |
|---|---|---|---|---|---|---|---|---|---|
| 1 | 35.14.01.2001 | area_km2 | 13.11 | SRC-BPS-KDA-2025-TABLE-1.1 | Luas Total Area (km2/sq.km) | 2024 | 1.1 | VALID | NaN |
| 10 | 35.14.01.2001 | flood_event_presence_2024 | 0 | SRC-BPS-KDA-2025-TABLE-4.4.2 | Floods Ada/Tidak Ada | 2024 | 4.4.2 | VALID | DIRECT_SOURCE_VALUE |
| 21 | 35.14.01.2001 | population | 9387 | SRC-BPS-KDA-2025-TABLE-3.1 | Penduduk/Population - Jumlah/Total | 2024 | 3.1 | VALID | NaN |
| 22 | 35.14.01.2001 | population_density | 716.0183066361557 | SRC-DERIVED-TASK03B | population / area_km2 | 2024 | DERIVED | VALID | population / area_km2 |
Keputusan metodologis¶
- Join produksi menggunakan kode kanonik, bukan fuzzy matching nama.
- Agregat kecamatan tidak disalin menjadi nilai desa.
- Missing tetap missing kecuali sumber secara eksplisit mendefinisikan zero.
- Setiap nilai yang dipromosikan memiliki sumber, periode, status validasi, dan—untuk fitur turunan—rumus lineage.