Spaces:
Running
Running
| """ | |
| benchmarks/equity_comparison.py — Baseline Comparison + Equity Sensitivity Harness. | |
| Action 2 (baseline comparison) + Action 6 (sensitivity) from recommendation.md. | |
| Generates two pitch tables and writes them to benchmarks/output/equity_comparison.md. | |
| Five strategies: | |
| 1. pure_greedy — equity_fn=lambda _: 1.0, force_strategy="greedy" | |
| Efficiency anchor (honest frontier: no equity). | |
| 2. agriflow — default equity_fn + default dispatch | |
| Production behavior. | |
| 3. uniform — equal split per surplus across viable deficits | |
| Equity anchor (ignores score entirely). | |
| 4. proportional — allocation proportional to deficit magnitude per kab | |
| Status-quo Bapanas foil (attack #7 answer). | |
| 5. agriflow_smoothed — linear-interpolation closure over existing tier knots | |
| (1.30/1.15/1.05/1.00 at IPM 68/72/78), no new params. | |
| Demonstrates smoothed ≈ step (attack #2 answer). | |
| All five strategies receive IDENTICAL inputs: same supply/deficit pool, same | |
| logistics context, same hard constraints (generate_candidates). Only | |
| scoring/prioritisation differs. This is the apple-to-apple requirement. | |
| Run: | |
| python benchmarks/equity_comparison.py | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import sys | |
| from collections import defaultdict | |
| from typing import Dict, List, Tuple | |
| if sys.platform == "win32": | |
| try: | |
| sys.stdout.reconfigure(encoding="utf-8") | |
| except (AttributeError, OSError): | |
| pass | |
| sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | |
| from matching_engine import run_matching | |
| from matching_engine.allocation import equity_multiplier_value | |
| from matching_engine.constraints import generate_candidates | |
| from matching_engine.models import LogisticsContext | |
| from sample_data.loader import load_all_sample_data | |
| from benchmarks._metrics import ( | |
| atkinson, | |
| fulfillment_by_node, | |
| gini, | |
| kab_fulfillment, | |
| min_fulfillment, | |
| total_deficit_covered, | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # Key type: (kab_id, commodity_code, segment_value) -> float | |
| # --------------------------------------------------------------------------- | |
| _Key = Tuple[str, str, str] | |
| SAMPANG_ID = "3527" | |
| BANGKALAN_ID = "3526" | |
| # --------------------------------------------------------------------------- | |
| # HELPER: extract demand_tons dict from loaded deficit nodes | |
| # --------------------------------------------------------------------------- | |
| def _build_demand_tons(deficit_nodes) -> Dict[_Key, float]: | |
| """Build demand_tons dict from a list of DemandNode objects.""" | |
| out: Dict[_Key, float] = {} | |
| for d in deficit_nodes: | |
| key = (d.kabupaten.id, d.commodity.code, d.segment.value) | |
| out[key] = out.get(key, 0.0) + d.volume_tons | |
| return out | |
| # --------------------------------------------------------------------------- | |
| # HELPER: extract matched_tons dict from a MatchingReport | |
| # --------------------------------------------------------------------------- | |
| def _report_to_matched_tons(report) -> Dict[_Key, float]: | |
| """Adapt MatchingReport.matches -> matched_tons dict.""" | |
| out: Dict[_Key, float] = {} | |
| for m in report.matches: | |
| key = (m.deficit.kabupaten.id, m.deficit.commodity.code, m.deficit.segment.value) | |
| out[key] = out.get(key, 0.0) + m.matched_volume_tons | |
| return out | |
| # --------------------------------------------------------------------------- | |
| # STRATEGY 3: UNIFORM ALLOCATION | |
| # Uses only Layer 1 candidate generation; ignores score entirely. | |
| # Each surplus splits its volume equally across its viable deficits, | |
| # capping at each deficit's actual need. | |
| # --------------------------------------------------------------------------- | |
| def uniform_allocate(surplus, deficit, logistics) -> Dict[_Key, float]: | |
| """ | |
| Equity baseline: each surplus splits volume equally across its viable | |
| deficit partners (post hard-constraint). Score is ignored entirely. | |
| Returns matched_tons dict: (kab_id, commodity_code, segment) -> tons matched. | |
| This is a foil only — not a product feature, implemented in benchmarks/. | |
| """ | |
| candidates = generate_candidates(surplus, deficit, logistics=logistics) | |
| # Group: surplus_key -> list of deficit nodes that can receive it | |
| surplus_to_deficits: Dict[str, List] = defaultdict(list) | |
| for s, d in candidates: | |
| s_key = s.kabupaten.id + "_" + s.commodity.code | |
| surplus_to_deficits[s_key].append((s, d)) | |
| # Build demand lookup: (kab_id, commodity, segment) -> total_demand | |
| demand_lookup: Dict[_Key, float] = _build_demand_tons(deficit) | |
| # remaining demand per key | |
| remaining_demand: Dict[_Key, float] = dict(demand_lookup) | |
| matched: Dict[_Key, float] = defaultdict(float) | |
| # For each surplus, distribute its volume equally across eligible deficits | |
| # Iterate multiple passes to handle partial fills (demand may already be | |
| # partially filled from another surplus). Simple single-pass equal split: | |
| # divide surplus volume by number of eligible deficit keys. | |
| for s_key, pairs in surplus_to_deficits.items(): | |
| s = pairs[0][0] | |
| remaining_supply = s.volume_tons | |
| eligible = [ | |
| (s_, d) for s_, d in pairs | |
| if remaining_demand.get( | |
| (d.kabupaten.id, d.commodity.code, d.segment.value), 0.0 | |
| ) > 0 | |
| ] | |
| if not eligible: | |
| continue | |
| n = len(eligible) | |
| share = remaining_supply / n # equal share per eligible deficit | |
| for _, d in eligible: | |
| d_key = (d.kabupaten.id, d.commodity.code, d.segment.value) | |
| allocation = min(share, remaining_demand.get(d_key, 0.0)) | |
| matched[d_key] += allocation | |
| remaining_demand[d_key] = max(0.0, remaining_demand[d_key] - allocation) | |
| return dict(matched) | |
| # --------------------------------------------------------------------------- | |
| # STRATEGY 4: PROPORTIONAL-TO-DEFICIT (Bapanas status-quo foil) | |
| # Allocates each surplus proportionally to the deficit magnitude of each | |
| # eligible kab. Larger deficit gets larger share. | |
| # --------------------------------------------------------------------------- | |
| def proportional_allocate(surplus, deficit, logistics) -> Dict[_Key, float]: | |
| """ | |
| Status-quo Bapanas foil: allocation proportional to deficit magnitude. | |
| Each surplus distributes volume to eligible deficits weighted by | |
| their remaining demand volume. Ignores score, distance ranking, equity. | |
| Represents a simple pro-rata rule: 'give more to those who need more'. | |
| Returns matched_tons dict: (kab_id, commodity_code, segment) -> tons. | |
| """ | |
| candidates = generate_candidates(surplus, deficit, logistics=logistics) | |
| surplus_to_deficits: Dict[str, List] = defaultdict(list) | |
| for s, d in candidates: | |
| s_key = s.kabupaten.id + "_" + s.commodity.code | |
| surplus_to_deficits[s_key].append((s, d)) | |
| demand_lookup: Dict[_Key, float] = _build_demand_tons(deficit) | |
| remaining_demand: Dict[_Key, float] = dict(demand_lookup) | |
| matched: Dict[_Key, float] = defaultdict(float) | |
| for s_key, pairs in surplus_to_deficits.items(): | |
| s = pairs[0][0] | |
| remaining_supply = s.volume_tons | |
| eligible = [ | |
| (s_, d) for s_, d in pairs | |
| if remaining_demand.get( | |
| (d.kabupaten.id, d.commodity.code, d.segment.value), 0.0 | |
| ) > 0 | |
| ] | |
| if not eligible: | |
| continue | |
| # Weight = remaining demand magnitude | |
| d_keys = [ | |
| (d.kabupaten.id, d.commodity.code, d.segment.value) | |
| for _, d in eligible | |
| ] | |
| weights = [remaining_demand[k] for k in d_keys] | |
| total_weight = sum(weights) | |
| if total_weight == 0: | |
| continue | |
| for d_key, w in zip(d_keys, weights): | |
| share = remaining_supply * (w / total_weight) | |
| allocation = min(share, remaining_demand[d_key]) | |
| matched[d_key] += allocation | |
| remaining_demand[d_key] = max(0.0, remaining_demand[d_key] - allocation) | |
| return dict(matched) | |
| # --------------------------------------------------------------------------- | |
| # STRATEGY 5: AGRIFLOW-SMOOTHED (linear interpolation over existing knots) | |
| # This is a closure ONLY — do not modify equity_multiplier_value production. | |
| # Knots: (68, 1.30), (72, 1.15), (78, 1.05), (inf, 1.00) | |
| # Linear interpolation between knots; outside range uses boundary values. | |
| # --------------------------------------------------------------------------- | |
| def _equity_smoothed(ipm: float) -> float: | |
| """ | |
| Linear interpolation over the existing tier knots. | |
| Knot values match equity_multiplier_value exactly at knot boundaries: | |
| IPM=68 -> 1.30, IPM=72 -> 1.15, IPM=78 -> 1.05, IPM>=85 -> 1.00 | |
| Below 68: returns 1.30 (same as step-function). | |
| Between knots: linear interpolation. | |
| Above 85: returns 1.00. | |
| This demonstrates that smoothed behaviour approximates the step-function | |
| (attack #2 answer) without changing any production code. | |
| """ | |
| # Knot points: (ipm, multiplier) | |
| knots = [(68.0, 1.30), (72.0, 1.15), (78.0, 1.05), (85.0, 1.00)] | |
| if ipm <= knots[0][0]: | |
| return knots[0][1] | |
| if ipm >= knots[-1][0]: | |
| return knots[-1][1] | |
| for i in range(len(knots) - 1): | |
| x0, y0 = knots[i] | |
| x1, y1 = knots[i + 1] | |
| if x0 <= ipm <= x1: | |
| t = (ipm - x0) / (x1 - x0) | |
| return y0 + t * (y1 - y0) | |
| return 1.00 # unreachable | |
| # --------------------------------------------------------------------------- | |
| # ACTION 6 — SENSITIVITY: three threshold variants | |
| # --------------------------------------------------------------------------- | |
| def equity_strict(ipm: float) -> float: | |
| """Strict thresholds: 1.50/1.25/1.10/1.00 at IPM <65/<70/<75/>=75.""" | |
| if ipm < 65: | |
| return 1.50 | |
| elif ipm < 70: | |
| return 1.25 | |
| elif ipm < 75: | |
| return 1.10 | |
| else: | |
| return 1.00 | |
| def equity_current(ipm: float) -> float: | |
| """Current (production) thresholds — delegates to equity_multiplier_value. | |
| Single source of truth: this column in the sensitivity table IS the | |
| shipped behavior, not a re-typed copy that could drift.""" | |
| return equity_multiplier_value(ipm) | |
| def equity_lenient(ipm: float) -> float: | |
| """Lenient thresholds: 1.15/1.08/1.03/1.00 at IPM <70/<75/<80/>=80.""" | |
| if ipm < 70: | |
| return 1.15 | |
| elif ipm < 75: | |
| return 1.08 | |
| elif ipm < 80: | |
| return 1.03 | |
| else: | |
| return 1.00 | |
| # --------------------------------------------------------------------------- | |
| # BOUNDARY PERTURBATION — how many Jatim kabs change tier under ±1/±2 shifts | |
| # --------------------------------------------------------------------------- | |
| def boundary_perturbation(kabupaten_dict: dict) -> dict: | |
| """ | |
| Shift each IPM threshold by ±1 and ±2 points and count how many | |
| Jatim kabupaten change tier (i.e. receive a different multiplier). | |
| Thresholds being perturbed: 68, 72, 78 (the three break points in | |
| equity_multiplier_value for Jatim 2024). | |
| Returns a dict with counts per shift magnitude for each threshold. | |
| """ | |
| ipm_values = [k.ipm for k in kabupaten_dict.values()] | |
| def count_different(delta: float) -> int: | |
| """Count kabs whose multiplier changes when ALL thresholds shift by delta.""" | |
| changed = 0 | |
| for ipm in ipm_values: | |
| orig = equity_multiplier_value(ipm) | |
| # Shifted function: thresholds 68->68+delta, 72->72+delta, 78->78+delta | |
| t68 = 68.0 + delta | |
| t72 = 72.0 + delta | |
| t78 = 78.0 + delta | |
| if ipm < t68: | |
| shifted = 1.30 | |
| elif ipm < t72: | |
| shifted = 1.15 | |
| elif ipm < t78: | |
| shifted = 1.05 | |
| else: | |
| shifted = 1.00 | |
| if abs(orig - shifted) > 1e-9: | |
| changed += 1 | |
| return changed | |
| result = {} | |
| for delta in (-2.0, -1.0, +1.0, +2.0): | |
| result[delta] = count_different(delta) | |
| return result | |
| # --------------------------------------------------------------------------- | |
| # COMPUTE METRICS ROW for a strategy | |
| # --------------------------------------------------------------------------- | |
| def compute_row( | |
| strategy_name: str, | |
| matched_tons: Dict[_Key, float], | |
| demand_tons: Dict[_Key, float], | |
| ) -> dict: | |
| """Compute all 5 metric columns for one strategy.""" | |
| return { | |
| "strategy": strategy_name, | |
| "total_deficit_covered": total_deficit_covered(matched_tons, demand_tons), | |
| "gini": gini(matched_tons, demand_tons), | |
| "atkinson_05": atkinson(matched_tons, demand_tons, epsilon=0.5), | |
| "atkinson_10": atkinson(matched_tons, demand_tons, epsilon=1.0), | |
| "min_fulfillment": min_fulfillment(matched_tons, demand_tons), | |
| "sampang": kab_fulfillment(matched_tons, demand_tons, SAMPANG_ID), | |
| "bangkalan": kab_fulfillment(matched_tons, demand_tons, BANGKALAN_ID), | |
| } | |
| # --------------------------------------------------------------------------- | |
| # MAIN | |
| # --------------------------------------------------------------------------- | |
| def main(): | |
| print("=" * 80) | |
| print(" AGRIFLOW EQUITY COMPARISON HARNESS") | |
| print("=" * 80) | |
| print(" Loading Jatim sample data ...") | |
| data = load_all_sample_data() | |
| surplus = data["surplus"] | |
| deficit = data["deficit"] | |
| weather = data["weather"] | |
| historical = data["historical_prices"] | |
| kabupaten_dict = data["kabupaten"] | |
| logistics = LogisticsContext() | |
| demand_tons = _build_demand_tons(deficit) | |
| print(f" Surplus nodes: {len(surplus)}") | |
| print(f" Deficit nodes: {len(deficit)}") | |
| print(f" Demand keys: {len(demand_tons)}") | |
| print() | |
| # ----------------------------------------------------------------------- | |
| # RUN FIVE STRATEGIES | |
| # All use same surplus/deficit/logistics — only equity_fn / prioritisation differs | |
| # ----------------------------------------------------------------------- | |
| print(" Running strategies ...") | |
| # 1. Pure greedy — efficiency anchor | |
| report_greedy = run_matching( | |
| surplus, deficit, | |
| logistics=logistics, | |
| weather_forecasts=weather, | |
| historical_prices=historical, | |
| force_strategy="greedy", | |
| equity_fn=lambda _ipm: 1.0, | |
| ) | |
| matched_greedy = _report_to_matched_tons(report_greedy) | |
| print(" [1/5] pure_greedy done") | |
| # 2. AgriFlow — default (production behavior, equity_fn omitted) | |
| report_agriflow = run_matching( | |
| surplus, deficit, | |
| logistics=logistics, | |
| weather_forecasts=weather, | |
| historical_prices=historical, | |
| ) | |
| matched_agriflow = _report_to_matched_tons(report_agriflow) | |
| print(" [2/5] agriflow done") | |
| # 3. Uniform — equity anchor (ignores score, equal split) | |
| matched_uniform = uniform_allocate(surplus, deficit, logistics) | |
| print(" [3/5] uniform done") | |
| # 4. Proportional-to-deficit (Bapanas status-quo foil) | |
| matched_proportional = proportional_allocate(surplus, deficit, logistics) | |
| print(" [4/5] proportional done") | |
| # 5. AgriFlow-smoothed — linear interpolation over existing knots | |
| report_smoothed = run_matching( | |
| surplus, deficit, | |
| logistics=logistics, | |
| weather_forecasts=weather, | |
| historical_prices=historical, | |
| equity_fn=_equity_smoothed, | |
| ) | |
| matched_smoothed = _report_to_matched_tons(report_smoothed) | |
| print(" [5/5] agriflow_smoothed done") | |
| print() | |
| # ----------------------------------------------------------------------- | |
| # TABLE 1 — BASELINE COMPARISON (5 strategies × 5 metrics + kab spotlight) | |
| # ----------------------------------------------------------------------- | |
| rows = [ | |
| compute_row("pure_greedy", matched_greedy, demand_tons), | |
| compute_row("agriflow", matched_agriflow, demand_tons), | |
| compute_row("uniform", matched_uniform, demand_tons), | |
| compute_row("proportional", matched_proportional, demand_tons), | |
| compute_row("agriflow_smoothed", matched_smoothed, demand_tons), | |
| ] | |
| table1_header = ( | |
| "| Strategy | Coverage | Gini | Atk(0.5) | Atk(1.0) | " | |
| "MinFulfill | Sampang | Bangkalan |" | |
| ) | |
| table1_sep = ( | |
| "|-------------------|----------|-------|----------|----------|" | |
| "-----------|---------|-----------|" | |
| ) | |
| table1_lines = [table1_header, table1_sep] | |
| for r in rows: | |
| line = ( | |
| f"| {r['strategy']:<17s} " | |
| f"| {r['total_deficit_covered']:.4f} " | |
| f"| {r['gini']:.4f}" | |
| f"| {r['atkinson_05']:.4f} " | |
| f"| {r['atkinson_10']:.4f} " | |
| f"| {r['min_fulfillment']:.4f} " | |
| f"| {r['sampang']:.4f} " | |
| f"| {r['bangkalan']:.4f} |" | |
| ) | |
| table1_lines.append(line) | |
| print("=" * 80) | |
| print(" TABLE 1 — BASELINE COMPARISON (5 strategies)") | |
| print(" Coverage = volume-weighted tons fulfilled / tons demanded") | |
| print(" Gini / Atkinson = weighted by demand volume (lower = more equitable)") | |
| print(" MinFulfill = fulfillment ratio of worst-served demand node (higher = better)") | |
| print(" Sampang=3527 (IPM 66.72), Bangkalan=3526 (IPM 67.70)") | |
| print("=" * 80) | |
| for line in table1_lines: | |
| print(line) | |
| print() | |
| # ----------------------------------------------------------------------- | |
| # ACTION 6 — SENSITIVITY (strict / current / lenient threshold variants) | |
| # ----------------------------------------------------------------------- | |
| print(" Running Action 6 sensitivity ...") | |
| sens_variants = [ | |
| ("strict", equity_strict, "IPM <65→1.50, <70→1.25, <75→1.10, >=75→1.00"), | |
| ("current", equity_current, "IPM <68→1.30, <72→1.15, <78→1.05, >=78→1.00 [PROD]"), | |
| ("lenient", equity_lenient, "IPM <70→1.15, <75→1.08, <80→1.03, >=80→1.00"), | |
| ] | |
| sens_rows = [] | |
| for name, fn, desc in sens_variants: | |
| rep = run_matching( | |
| surplus, deficit, | |
| logistics=logistics, | |
| weather_forecasts=weather, | |
| historical_prices=historical, | |
| equity_fn=fn, | |
| ) | |
| mt = _report_to_matched_tons(rep) | |
| sens_rows.append({ | |
| "variant": name, | |
| "desc": desc, | |
| "total_deficit_covered": total_deficit_covered(mt, demand_tons), | |
| "gini": gini(mt, demand_tons), | |
| "sampang": kab_fulfillment(mt, demand_tons, SAMPANG_ID), | |
| "bangkalan": kab_fulfillment(mt, demand_tons, BANGKALAN_ID), | |
| }) | |
| table2_header = ( | |
| "| Variant | Coverage | Gini | Sampang | Bangkalan | Description |" | |
| ) | |
| table2_sep = ( | |
| "|---------|----------|-------|---------|-----------|-------------|" | |
| ) | |
| table2_lines = [table2_header, table2_sep] | |
| for r in sens_rows: | |
| line = ( | |
| f"| {r['variant']:<7s} " | |
| f"| {r['total_deficit_covered']:.4f} " | |
| f"| {r['gini']:.4f}" | |
| f"| {r['sampang']:.4f} " | |
| f"| {r['bangkalan']:.4f} " | |
| f"| {r['desc']} |" | |
| ) | |
| table2_lines.append(line) | |
| print() | |
| print("=" * 80) | |
| print(" TABLE 2 — ACTION 6 SENSITIVITY (threshold variants)") | |
| print(" 'current' delegates to equity_multiplier_value — single source of truth.") | |
| print("=" * 80) | |
| for line in table2_lines: | |
| print(line) | |
| print() | |
| # ----------------------------------------------------------------------- | |
| # BOUNDARY PERTURBATION | |
| # ----------------------------------------------------------------------- | |
| perturb = boundary_perturbation(kabupaten_dict) | |
| print("=" * 80) | |
| print(" BOUNDARY PERTURBATION — kabs changing tier when thresholds shift ±1/±2") | |
| print(" (Attack #2: 'cliff effect' — how sensitive is tier assignment to threshold?)") | |
| print("=" * 80) | |
| print(" Jatim IPM values (sorted):") | |
| ipm_sorted = sorted(k.ipm for k in kabupaten_dict.values()) | |
| print(" " + " ".join(f"{v:.2f}" for v in ipm_sorted)) | |
| print() | |
| print(" | Threshold shift | Kabs changing tier |") | |
| print(" |-----------------|---------------------|") | |
| for delta in (-2.0, -1.0, +1.0, +2.0): | |
| label = f"{delta:+.0f} pts" | |
| print(f" | {label:<15s} | {perturb[delta]:2d} |") | |
| print() | |
| print(" Interpretation:") | |
| print(" - 0-1 kabs per ±1pt shift = cliff effect negligible.") | |
| print(" - More kabs = higher sensitivity (attack #2 stronger).") | |
| print() | |
| # ----------------------------------------------------------------------- | |
| # WRITE OUTPUT | |
| # ----------------------------------------------------------------------- | |
| output_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "output") | |
| os.makedirs(output_dir, exist_ok=True) | |
| output_path = os.path.join(output_dir, "equity_comparison.md") | |
| with open(output_path, "w", encoding="utf-8") as f: | |
| f.write("# AgriFlow Equity Comparison\n\n") | |
| f.write("Generated by `benchmarks/equity_comparison.py`.\n\n") | |
| f.write("## Table 1 — Baseline Comparison (5 strategies)\n\n") | |
| f.write("Coverage = volume-weighted tons fulfilled / tons demanded. \n") | |
| f.write("Gini / Atkinson = weighted by demand volume; lower = more equitable. \n") | |
| f.write("MinFulfill = fulfillment ratio of worst-served demand node; higher = better. \n") | |
| f.write("Sampang=3527 (IPM 66.72), Bangkalan=3526 (IPM 67.70). \n\n") | |
| for line in table1_lines: | |
| f.write(line + "\n") | |
| f.write("\n") | |
| f.write("## Table 2 — Action 6 Sensitivity (threshold variants)\n\n") | |
| f.write("`current` delegates to `equity_multiplier_value` — single source of truth.\n\n") | |
| for line in table2_lines: | |
| f.write(line + "\n") | |
| f.write("\n") | |
| f.write("## Boundary Perturbation\n\n") | |
| f.write("Shift all IPM thresholds simultaneously by ±1 or ±2 points.\n") | |
| f.write("Counts kabupaten in Jatim (38 total) that move to a different tier.\n\n") | |
| f.write("| Threshold shift | Kabs changing tier |\n") | |
| f.write("|-----------------|---------------------|\n") | |
| for delta in (-2.0, -1.0, +1.0, +2.0): | |
| label = f"{delta:+.0f} pts" | |
| f.write(f"| {label:<15s} | {perturb[delta]:2d} |\n") | |
| f.write("\n") | |
| print(f" Tables written to: {output_path}") | |
| print() | |
| # ----------------------------------------------------------------------- | |
| # QUICK SANITY CHECK — report potential ordering violations | |
| # ----------------------------------------------------------------------- | |
| greedy_cov = rows[0]["total_deficit_covered"] | |
| agriflow_cov = rows[1]["total_deficit_covered"] | |
| greedy_gini = rows[0]["gini"] | |
| agriflow_gini = rows[1]["gini"] | |
| uniform_gini = rows[2]["gini"] | |
| sampang_agriflow = rows[1]["sampang"] | |
| sampang_greedy = rows[0]["sampang"] | |
| print(" ORDERING CHECKS (expected by pitch claims):") | |
| check = lambda ok, msg: print(f" {'PASS' if ok else 'FAIL'} {msg}") | |
| check(greedy_cov >= agriflow_cov, | |
| f"greedy coverage ({greedy_cov:.4f}) >= agriflow coverage ({agriflow_cov:.4f})") | |
| check(agriflow_gini <= greedy_gini, | |
| f"agriflow gini ({agriflow_gini:.4f}) <= greedy gini ({greedy_gini:.4f})") | |
| check(uniform_gini <= agriflow_gini, | |
| f"uniform gini ({uniform_gini:.4f}) <= agriflow gini ({agriflow_gini:.4f})") | |
| check(sampang_agriflow >= sampang_greedy, | |
| f"Sampang: agriflow ({sampang_agriflow:.4f}) >= greedy ({sampang_greedy:.4f})") | |
| strict_sampang = sens_rows[0]["sampang"] | |
| lenient_sampang = sens_rows[2]["sampang"] | |
| check(strict_sampang >= lenient_sampang, | |
| f"strict Sampang ({strict_sampang:.4f}) >= lenient Sampang ({lenient_sampang:.4f})") | |
| smoothed_cov = rows[4]["total_deficit_covered"] | |
| smoothed_gini = rows[4]["gini"] | |
| check(abs(smoothed_cov - agriflow_cov) < 0.05, | |
| f"smoothed coverage ({smoothed_cov:.4f}) approx agriflow ({agriflow_cov:.4f})") | |
| check(abs(smoothed_gini - agriflow_gini) < 0.05, | |
| f"smoothed gini ({smoothed_gini:.4f}) approx agriflow gini ({agriflow_gini:.4f})") | |
| print() | |
| print("=" * 80) | |
| print(" DONE. Copy tables from benchmarks/output/equity_comparison.md into pitch deck.") | |
| print("=" * 80) | |
| if __name__ == "__main__": | |
| main() | |