Skip to content

[EPIC] Push pandas to the seams: views-frames migration for sample-bearing predictions #85

Closed

Description

@Polichinel

Problem

The FAO delivery is moving from point estimates to predictions-with-uncertainty (S samples per cell). As long as pandas sits on the data path, samples cannot propagate: a distribution becomes an object-dtype list-in-cell (the inherited PGMDataset representation) — the memory / encode-decode wall captured in register C-40 (and pipeline-core's own ~18 GB OOM, their #181). views-frames stores a distribution natively as a contiguous (N, S) float32 PredictionFrame.

This epic maps every pandas site in views-postprocessing and sequences the swap to native views-frames, one site at a time, each with a parity proof, disturbing cross-repo interconnectivity as little as possible.

Key enabling fact: the samples already arrive inside the forecast PGMDataset's object-dtype cells — they're collapsed to point estimates today only because unfao/frames.py hardcodes S=1 and the wires carry object-dtype parquet. So vpp can make its interior sample-aware unilaterally, while the two wire boundaries stay frozen until cross-repo coordination.

Why it matters

The map — three concentric rings (grounded in file:line)

Ring 0 — already pandas-free (do not touch): views_postprocessing/delivery/{coverage,identity,observed_range,provenance}.py, unfao/gaul_schema.py, unfao/source_metadata.py.

Ring 1 — vpp-owned seams (UNILATERAL):

  • unfao/extraction.py — the pandas→primitives seam (cells_of/months_of/drop_months_above/unmapped_cell_count).
  • unfao/enrichment.py — pd.read_parquet(lookup) (:48) + .merge(...) (:117-119): a keyed metadata-attach join.
  • unfao/frames.py — the unused views-frames adapter; _column_2d(...).reshape(-1,1) hardcodes S=1 (:85).
  • scripts/build_gaul_lookup.py — offline lookup build (off the delivery path).

Ring 2 — forced / contract boundaries (CROSS-REPO-GATED):

Desired end state

A forecast flows through vpp's interior as a PredictionFrame carrying (N, S), producing byte-identical deliveries to today on S=1 inputs (proving the interior is sample-ready) behind frozen wires. The final outbound hop that actually ships (N,S) to FAO (S6) and the historical inbound retirement (S7) are landed only when their cross-repo gates clear.

Scope

In: the 7 sequenced steps below (S1–S7), each independently shippable with a parity proof.
Out: Ring 0 (already pandas-free); pushing the enrichment metadata join into views-frames (S4 moves it off pandas, not into a frame — it is not frame algebra); changing the outbound format ahead of faoapi.

Stories (in sequence)

  1. S1 — frames.py: generalize to_prediction_frame to S>1 [UNILATERAL] ← recommended first
  2. S2 — extraction.py: frame-native sibling readers [UNILATERAL]
  3. S3 — forecast "convert at the door" (internal PredictionFrame) [UNILATERAL]
  4. S4 — enrichment.py: numpy/pyarrow keyed gather [UNILATERAL, low priority]
  5. S5 — build_gaul_lookup.py: pyarrow-native build [UNILATERAL, lowest priority]
  6. S6 — forecast outbound → arrow sample-frame wire [CROSS-REPO-GATED: faoapi #45 + C-40]
  7. S7 — historical inbound read_dataframe/PGMDataset retirement [CROSS-REPO-GATED: pipeline-core C-40] — do not move yet

Dependency sequence

S1 ──► S2 ──► S3 ──► S6 (gated: faoapi #45)
        S4 (independent, low pri)
        S5 (independent, lowest pri)
S7 (gated: pipeline-core C-40) — independent of S1–S6; never blocks samples

Epic acceptance criteria

  • S1–S3 merged: a forecast is represented internally as a (N,S) PredictionFrame; deliveries are byte-identical to today on S=1 inputs (parity).
  • Each landed step carries a parity proof (build pandas → convert → assert arrays equal, and/or head-to-head old-vs-new) and keeps ruff + the full suite green.
  • delivery/ remains pandas-free throughout (the design-contract test stays green).
  • S6/S7 remain explicitly blocked until their gates (faoapi FAO delivery path is point/DataFrame-based — won't carry rusty_bucket's pooled draws #45 / pipeline-core C-40) clear; no unilateral wire change.
  • Tracking issue complete.

Refs

Register C-40 (the pandas gate), #45 (the delivery-side draw carrier / faoapi wire), pipeline-core #186/#207/#161/#159/#211 (their DataFrame retirement). Parity pattern: tests/test_views_frames_conformance.py.


Issues

Activity

  1. added
    epicA large capability spanning multiple stories
    on Jun 28, 2026
  2. Polichinel commented on Jul 2, 2026

    @Polichinel
    CollaboratorAuthor

    Cross-linking from views-pipeline-core: two of its new epics interlock with this one. (1) Publish train (pipeline-core#261): pipeline-core 3.0 will be cut to PyPI, at which point your views-pipeline-core <3.0.0 cap lifts (pipeline-core#222) — the tracking issue pipeline-core#264 will signal. (2) God-class gate: your S7 (#92, PGMDataset/read_dataframe retirement) is one of the two remaining consumer-side gates for pipeline-core's _ViewsDataset decomposition (their register C-36/C-135; the other is views-reporting#203). No new asks — this epic already covers your side; just making the dependency graph visible.

  3. added 4 commits that reference this issue on Aug 3, 2026
  4. Polichinel commented on Aug 4, 2026

    @Polichinel
    CollaboratorAuthor

    Done — and the claim is literal now, which is why it closes here rather than at #90.

    Every story is merged: #86, #87, #88, #89, #91, #92, and finally #90 (arrow-native lookup builder, PR #220). The last step was #221, retiring GaulLookupEnricher — the module that held the package's final pandas reference, a TYPE_CHECKING import in a class with zero production callers.

    $ grep -rn "^import pandas\|^from pandas" views_postprocessing/ scripts/
    $
    

    Nothing. Not a runtime import, not a type-only one. tests/test_doc_accuracy.py's assertion changed from "exactly one type-only pandas importer" to "none", so the property is now pinned rather than described.

    Why #90 did not close this epic. The builder became arrow-native there, but contract/enrichment.py still existed and still imported pandas. Closing then would have been a claim that did not match the code — the thing ADR-014 exists about. The register's C-75 recorded the retire decision in #220 and #221 executed it; the epic closes on the second.

    What the arc actually delivered: the delivery interior carries (N, S) behind frozen wires, the representation seam is one module, the historical artifact builds frame-native, and the GAUL lookup is built and read in arrow end to end — with the committed artifact proven value-identical across the builder rewrite (all 10 columns, every metadata key byte-for-byte).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    epicA large capability spanning multiple stories

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions