Spaces:
Sleeping
Sleeping
File size: 17,650 Bytes
b81a86b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 | """
derive_surplus_deficit_2022.py
==============================
Derives surplus_deficit_real.csv for ALL 6 commodities at reference year 2022.
Commodities produced:
beras_premium, beras_medium -- from BPS per-kab produksi + konsumsi + populasi (2022)
cabai_merah -- cabai_besar + cabai_keriting, BPS Hortikultura 2022
cabai_rawit -- BPS Hortikultura 2022
bawang_merah -- BPS Hortikultura 2022
bawang_putih -- BPS Hortikultura 2022
Units & conversions:
Beras production : ton/year (already ton in year_beras.csv)
Hortikultural : KUINTAL/year in source CSVs -> divide by 10 -> TON
Beras consumption : kg/kapita/week -> x52 weeks x populasi / 1000 -> ton/year
Hortikultura cons.: kg/kapita/year (national, from Kementan PDF) x populasi / 1000 -> ton/year
Per-capita consumption sources:
Beras : BPS Susenas per-kab (real), week_konsumsi_beras_perkapita.csv, 2022
Cabai merah : Kementan "Statistik Konsumsi Pangan 2024", Tabel 4.6a, p.30
Cabe merah (Chillies) 2022 = 1.909 kg/kapita/tahun [NATIONAL PROXY]
Cabai rawit : same table, Cabe rawit (Cayenne pepper) 2022 = 2.073 kg/kapita/tahun [NATIONAL PROXY]
Bawang merah : same PDF, Tabel 4.1a, p.25
Bawang merah (Onion) 2022 = 3.024 kg/kapita/tahun [NATIONAL PROXY]
Bawang putih : same PDF, Tabel 4.2a, p.26
Bawang putih (Garlic) 2022 = 2.016 kg/kapita/tahun [NATIONAL PROXY]
Prices - DEFICIT (consumer/wholesale) prices (2022 median from PIHPS / baseline):
Surplus (producer/farmgate) price = 0.75 x deficit price [DEMO ASSUMPTION:
standard producer-to-consumer distribution margin ~25%. In production, per-kab
BPS farmgate prices would replace this ratio.]
beras_premium: deficit 11,500 / surplus 8,625 IDR/kg
beras_medium : deficit 10,325 / surplus 7,744 IDR/kg (rounded 10325*0.75)
cabai_merah : deficit 45,000 / surplus 33,750 IDR/kg [NOT in PIHPS; baseline FLAGGED]
cabai_rawit : deficit 41,000 / surplus 30,750 IDR/kg [PIHPS 2022]
bawang_merah : deficit 32,500 / surplus 24,375 IDR/kg [PIHPS 2022]
bawang_putih : deficit 20,750 / surplus 15,563 IDR/kg [PIHPS 2022, rounded]
Harvest age (days since harvest for SURPLUS nodes) - DEMO ASSUMPTION:
Represents "freshly arrived at collection point, ready for dispatch".
In production: from real-time scraper (field to collection point transit).
cabai_merah & cabai_rawit : 1 day (very perishable; max_fresh_age_days=5)
bawang_merah & bawang_putih: 3 days (semi-perishable; max_fresh_age_days=30/60)
beras_premium & beras_medium: 20 days (stable grain; max_fresh_age_days=180)
DEFICIT nodes: harvest_age_days=0 (not applicable for demand side).
Excluded:
daging_ayam, telur_ayam -- broiler hilang; telur petelur hanya 2 kab. PENDING.
Run:
python sample_data/bps_real/derive_surplus_deficit_2022.py
"""
from __future__ import annotations
import pathlib
import pandas as pd
# ---------------------------------------------------------------------------
# Paths
# ---------------------------------------------------------------------------
ROOT = pathlib.Path(__file__).parent.parent.parent
BPS_DIR = ROOT / "sample_data" / "bps_real"
HORTI_DIR = pathlib.Path("D:/DOWNLOAD/Download 2026/baru-20260531T082645Z-3-001/baru")
PH_DIR = ROOT / "sample_data" / "price_history"
KAB_CSV = ROOT / "sample_data" / "kabupaten_jatim.csv"
OUT_CSV = ROOT / "sample_data" / "surplus_deficit_real.csv"
YEAR = 2022
# ---------------------------------------------------------------------------
# Per-capita consumption (NATIONAL PROXY - Kementan PDF, Tabel 4.6a/4.1a/4.2a)
# Source: Statistik Konsumsi Pangan 2024, Pusat Data Kementan, Desember 2024
# URL: https://satudata.pertanian.go.id/assets/docs/publikasi/Buku_Statistik_Konsumsi_2024.pdf
# ---------------------------------------------------------------------------
PERKAPITA_KG_PER_TAHUN = {
"cabai_merah": 1.909, # Tabel 4.6a p.30: Cabe merah / Chillies, 2022
"cabai_rawit": 2.073, # Tabel 4.6a p.30: Cabe rawit / Cayenne pepper, 2022
"bawang_merah": 3.024, # Tabel 4.1a p.25: Bawang merah / Onion, 2022
"bawang_putih": 2.016, # Tabel 4.2a p.26: Bawang putih / Garlic, 2022
}
# ---------------------------------------------------------------------------
# DEFICIT prices IDR/kg (2022 median from PIHPS or flagged baseline)
# These represent consumer/wholesale market prices.
# ---------------------------------------------------------------------------
PRICES_DEFICIT = {
"beras_premium": 11_500, # median(super1=12000, super2=11000), PIHPS 2022
"beras_medium": 10_325, # median(medium1=10650, medium2=10000), PIHPS 2022
"cabai_merah": 45_000, # komoditas_constraints.csv baseline - NOT in PIHPS [FLAGGED]
"cabai_rawit": 41_000, # cabe_rawit_cleaned.csv 2022 median, PIHPS
"bawang_merah": 32_500, # bawang_merah_cleaned.csv 2022 median, PIHPS
"bawang_putih": 20_750, # bawang_putih_cleaned.csv 2022 median, PIHPS
}
# SURPLUS prices IDR/kg = 0.75 x DEFICIT price (producer/farmgate estimate)
# DEMO ASSUMPTION: 25% distribution margin (producer to consumer). Rounded to
# nearest 1 IDR. In production, replace with per-kab BPS farmgate price data.
PRICES_SURPLUS = {
code: round(price * 0.75)
for code, price in PRICES_DEFICIT.items()
}
# ---------------------------------------------------------------------------
# Harvest age (days since harvest) - per commodity, SURPLUS nodes only
# DEMO ASSUMPTION: "freshly arrived at collection point, ready for dispatch"
# Production system: replace with real-time scraper data (typically 0-2 days).
# ---------------------------------------------------------------------------
HARVEST_AGE_SURPLUS = {
"cabai_merah": 1, # very perishable; max_fresh_age_days=5
"cabai_rawit": 1, # very perishable; max_fresh_age_days=5
"bawang_merah": 3, # semi-perishable; max_fresh_age_days=30
"bawang_putih": 3, # semi-perishable; max_fresh_age_days=60
"beras_premium": 20, # stable grain; max_fresh_age_days=180
"beras_medium": 20, # stable grain; max_fresh_age_days=180
}
HARVEST_AGE_DEFICIT = 0 # not applicable for demand side
# Grade split for beras
PREMIUM_SPLIT = 0.60
MEDIUM_SPLIT = 0.40
# ---------------------------------------------------------------------------
# Load kabupaten reference
# ---------------------------------------------------------------------------
def load_kab_map() -> dict[str, int]:
"""Returns {BPS-full-name -> kab_id} for all 38 Jatim kab/kota."""
kab = pd.read_csv(KAB_CSV)
mapping: dict[str, int] = {}
for _, row in kab.iterrows():
n = row["nama"]
kid = int(row["kab_id"])
if n.startswith("Kota "):
mapping[n] = kid # e.g. "Kota Surabaya" -> 3578
else:
mapping["Kabupaten " + n] = kid # e.g. "Kabupaten Lamongan" -> 3524
return mapping
def load_kab_ids_ordered() -> list[int]:
"""
Returns list of 38 kab_ids in BPS hortikultura row order:
rows 0-28 = Kabupaten (by BPS sequential code 3501-3529),
rows 29-37 = Kota (3571-3579).
"""
kab = pd.read_csv(KAB_CSV)
# Kabupaten (non-kota) sorted by kab_id ascending
non_kota = kab[~kab["nama"].str.startswith("Kota ")].sort_values("kab_id")
# Kota sorted by kab_id ascending: 3571 Kediri, 3572 Blitar, 3573 Malang,
# 3574 Probolinggo, 3575 Pasuruan, 3576 Mojokerto,
# 3577 Madiun, 3578 Surabaya, 3579 Batu
kota = kab[kab["nama"].str.startswith("Kota ")].sort_values("kab_id")
return list(non_kota["kab_id"].astype(int)) + list(kota["kab_id"].astype(int))
# ---------------------------------------------------------------------------
# Load hortikultura CSV (wide format, BPS, kuintal)
# Returns Series indexed by position 0-37, values in TON (after /10)
# Excludes Total row; treats NaN as 0.
# ---------------------------------------------------------------------------
def load_horti_csv(filename: str) -> pd.Series:
df = pd.read_csv(HORTI_DIR / filename, encoding="utf-8-sig")
# Row 38 (or last row) is "Total" - drop it
df = df[df["Kabupaten"] != "Total"].copy()
assert len(df) == 38, (
f"{filename}: expected 38 data rows after excluding Total, got {len(df)}"
)
prod_kuintal = df["Produksi_2022"].fillna(0.0).reset_index(drop=True)
prod_ton = prod_kuintal / 10.0 # KUINTAL -> TON
return prod_ton
# ---------------------------------------------------------------------------
# Derive beras surplus/deficit rows (2022)
# ---------------------------------------------------------------------------
def derive_beras(kab_map: dict[str, int], kab_ids_38: list[int]) -> list[dict]:
prod_df = pd.read_csv(BPS_DIR / "year_beras.csv")
kons_df = pd.read_csv(BPS_DIR / "week_konsumsi_beras_perkapita.csv")
pop_df = pd.read_csv(BPS_DIR / "year_populasi_jatim.csv")
prod22 = prod_df[prod_df["tahun"] == YEAR].copy()
kons22 = kons_df[kons_df["tahun"] == YEAR].copy()
pop22 = pop_df[pop_df["tahun"] == YEAR].copy()
assert len(prod22) == 38, f"Beras produksi 2022: expected 38 rows, got {len(prod22)}"
assert len(kons22) == 38, f"Beras konsumsi 2022: expected 38 rows, got {len(kons22)}"
assert len(pop22) == 38, f"Populasi 2022: expected 38 rows, got {len(pop22)}"
rows: list[dict] = []
for _, prod_row in prod22.iterrows():
kab_name = prod_row["kabupaten"] # e.g. "Kabupaten Lamongan"
kab_id = kab_map[kab_name]
produksi = float(prod_row["produksi"]) # ton
kons_row = kons22[kons22["kabupaten/kota"] == kab_name]
pop_row = pop22[pop22["kabupaten/kota"] == kab_name]
assert len(kons_row) == 1, f"Missing konsumsi for {kab_name}"
assert len(pop_row) == 1, f"Missing populasi for {kab_name}"
kg_per_week = float(kons_row["avg_konsumsi_perkapita_kg"].iloc[0])
populasi = float(pop_row["populasi"].iloc[0])
konsumsi_ton = kg_per_week * 52.0 * populasi / 1000.0
net_ton = produksi - konsumsi_ton
role = "SURPLUS" if net_ton > 0 else "DEFICIT"
vol = abs(net_ton)
for code, split in [("beras_premium", PREMIUM_SPLIT), ("beras_medium", MEDIUM_SPLIT)]:
if role == "SURPLUS":
price = PRICES_SURPLUS[code]
age = HARVEST_AGE_SURPLUS[code]
else:
price = PRICES_DEFICIT[code]
age = HARVEST_AGE_DEFICIT
rows.append({
"kab_id": kab_id,
"commodity_code": code,
"role": role,
"volume_tons": round(vol * split, 2),
"price_idr_per_kg": price,
"harvest_age_days": age,
})
return rows
# ---------------------------------------------------------------------------
# Derive hortikultura surplus/deficit rows (2022)
# ---------------------------------------------------------------------------
def derive_horti(kab_ids_38: list[int], pop22_series: pd.Series) -> list[dict]:
"""
kab_ids_38 : list of 38 kab_ids in BPS row order
pop22_series: pd.Series of 38 populasi values in same row order
"""
# Load production files (ton after conversion)
cabai_besar = load_horti_csv("cabai_besar.csv")
cabai_keriting = load_horti_csv("cabai_keriting.csv")
cabai_rawit = load_horti_csv("cabai_rawit.csv")
bawang_merah = load_horti_csv("bawang_merah.csv")
bawang_putih = load_horti_csv("bawang_putih.csv")
# cabai_merah = besar + keriting
cabai_merah_prod = cabai_besar + cabai_keriting
commodities = {
"cabai_merah": cabai_merah_prod,
"cabai_rawit": cabai_rawit,
"bawang_merah": bawang_merah,
"bawang_putih": bawang_putih,
}
rows: list[dict] = []
for code, prod_series in commodities.items():
per_kapita = PERKAPITA_KG_PER_TAHUN[code] # kg/kapita/tahun (national)
for i in range(38):
kab_id = kab_ids_38[i]
produksi = float(prod_series.iloc[i]) # ton
populasi = float(pop22_series.iloc[i]) # jiwa
konsumsi_ton = per_kapita * populasi / 1000.0
net_ton = produksi - konsumsi_ton
role = "SURPLUS" if net_ton > 0 else "DEFICIT"
vol = abs(net_ton)
if role == "SURPLUS":
price = PRICES_SURPLUS[code]
age = HARVEST_AGE_SURPLUS[code]
else:
price = PRICES_DEFICIT[code]
age = HARVEST_AGE_DEFICIT
rows.append({
"kab_id": kab_id,
"commodity_code": code,
"role": role,
"volume_tons": round(vol, 2),
"price_idr_per_kg": price,
"harvest_age_days": age,
})
return rows
# ---------------------------------------------------------------------------
# Build population series in BPS row order (same as kab_ids_38)
# ---------------------------------------------------------------------------
def build_pop22_series(kab_ids_38: list[int]) -> pd.Series:
"""
Returns a pd.Series of 2022 population values in kab_ids_38 order.
Uses year_populasi_jatim.csv (same file as beras derivation).
"""
pop_df = pd.read_csv(BPS_DIR / "year_populasi_jatim.csv")
pop22 = pop_df[pop_df["tahun"] == YEAR].copy()
# Build id->pop dict using the same kab_map approach
kab = pd.read_csv(KAB_CSV)
name_to_id: dict[str, int] = {}
for _, row in kab.iterrows():
n = row["nama"]
kid = int(row["kab_id"])
if n.startswith("Kota "):
name_to_id[n] = kid
else:
name_to_id["Kabupaten " + n] = kid
id_to_pop = {}
for _, r in pop22.iterrows():
full_name = r["kabupaten/kota"]
kid = name_to_id.get(full_name)
if kid is not None:
id_to_pop[kid] = float(r["populasi"])
assert len(id_to_pop) == 38, f"Expected 38 pop entries, got {len(id_to_pop)}"
values = [id_to_pop[kid] for kid in kab_ids_38]
return pd.Series(values, name="populasi")
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> None:
print(f"Deriving surplus_deficit_real.csv @ year {YEAR} ...")
print(" Fix 1: per-commodity harvest_age (cabai=1, bawang=3, beras=20)")
print(" Fix 2: surplus price = 0.75 x deficit price (distribution margin)")
kab_map = load_kab_map()
kab_ids_38 = load_kab_ids_ordered()
pop22_series = build_pop22_series(kab_ids_38)
beras_rows = derive_beras(kab_map, kab_ids_38)
horti_rows = derive_horti(kab_ids_38, pop22_series)
all_rows = beras_rows + horti_rows
df = pd.DataFrame(all_rows, columns=[
"kab_id", "commodity_code", "role",
"volume_tons", "price_idr_per_kg", "harvest_age_days",
])
# Sort: surplus first (descending volume), then deficit, grouped by commodity
df["_role_order"] = df["role"].map({"SURPLUS": 0, "DEFICIT": 1})
df = df.sort_values(["commodity_code", "_role_order", "volume_tons"],
ascending=[True, True, False])
df = df.drop(columns=["_role_order"]).reset_index(drop=True)
df.to_csv(OUT_CSV, index=False)
print(f"Written {len(df)} rows to {OUT_CSV}")
# Sanity report
print("\n--- Price check: surplus vs deficit prices ---")
for code in sorted(PRICES_DEFICIT.keys()):
print(f" {code:<22} deficit={PRICES_DEFICIT[code]:>7,} "
f"surplus={PRICES_SURPLUS[code]:>7,} "
f"spread={PRICES_DEFICIT[code]-PRICES_SURPLUS[code]:>7,}")
print("\n--- Harvest age check (SURPLUS nodes) ---")
for code in sorted(HARVEST_AGE_SURPLUS.keys()):
print(f" {code:<22} harvest_age={HARVEST_AGE_SURPLUS[code]} days")
print("\n--- Sanity check: top-3 producers per commodity (production ton) ---")
for code in ["cabai_merah", "cabai_rawit", "bawang_merah", "bawang_putih"]:
sub = df[df["commodity_code"] == code].copy()
kab_ref = pd.read_csv(KAB_CSV)
id_to_name = dict(zip(kab_ref["kab_id"].astype(int), kab_ref["nama"]))
surplus = sub[sub["role"] == "SURPLUS"].nlargest(3, "volume_tons")
print(f"\n{code}:")
for _, r in surplus.iterrows():
print(f" kab_id={r['kab_id']} ({id_to_name.get(r['kab_id'],'?')}) "
f"surplus={r['volume_tons']:,.0f} ton "
f"price_surplus={r['price_idr_per_kg']:,}")
for code in ["beras_premium", "beras_medium"]:
sub = df[df["commodity_code"] == code]
kab_ref = pd.read_csv(KAB_CSV)
id_to_name = dict(zip(kab_ref["kab_id"].astype(int), kab_ref["nama"]))
surplus = sub[sub["role"] == "SURPLUS"].nlargest(3, "volume_tons")
print(f"\n{code}:")
for _, r in surplus.iterrows():
print(f" kab_id={r['kab_id']} ({id_to_name.get(r['kab_id'],'?')}) "
f"surplus={r['volume_tons']:,.0f} ton "
f"price_surplus={r['price_idr_per_kg']:,}")
print("\n--- Commodity row counts ---")
print(df.groupby(["commodity_code", "role"]).size().to_string())
print(f"\nTotal rows: {len(df)}")
if __name__ == "__main__":
main()
|