Summary
Make the un_fao postprocessor's historical/actuals path pandas-free, so the FAO delivery runs entirely on views-frames. This is the root fix for the run-0 OOM and the gate that unblocks shipping the global-land FAO forecast.
Why (the incident)
Delivering the global-land forecast (region=land_gaul, 64,742 cells × ~438 months = 28,356,996 rows) OOM-killed the postprocessor at ~23.8 GB:
Out of memory: Killed process (python) total-vm:32146116kB, anon-rss:23778224kB
Root cause: _read_historical_data fetches the actuals as a pandas DataFrame (get_data() → read_dataframe() → PGMDataset). The region scope is correct and proven (datafactory served exactly 64,742 cells); the wall is pandas.
Decisive context: pipeline-core's pandas-free fetch (get_feature_frame → views_frames.FeatureFrame) is fully built and tested but has zero production consumers (a pipeline-core falsification test says the frame path is "capability-on-paper, exercised by zero real runs"). This issue makes the FAO product the first real consumer.
The forecast side is already frame-native — extend that seam to the actuals
The wire_contract path is already pandas-free for the forecast (wire/, delivery/, frame_extraction.py, track_a_source.py — several modules explicitly forbid importing pandas). We extend the same seam to the historical/actuals side and retire the pandas historical path — we do not migrate PGMDataset, we bypass it exactly as the forecast path already does.
Scope
- Fetch actuals as a FeatureFrame. In
unfao.py:_read_historical_data (~:88-104), call self._data_loader.get_feature_frame(...) and read cached_frame_path instead of get_data()/read_dataframe()/PGMDataset. Consume the views_frames.FeatureFrame directly. (Requires the descriptor to declare data_format: feature_frame — that companion change is staged in views-models postprocessors/un_fao/configs/config_queryset.py; land the two together.)
- Route actuals through the existing frame-native seam the forecast already uses:
frame_extraction, the pyarrow GAUL join in wire/sidecar.py:build_sidecar, and delivery/coverage. Retire PGMDataset, enrichment.py (pandas merge), and the pandas extraction.py seam for the historical path.
- Build the three named frame-native gaps (self-documented in
frame_extraction.py:14-21):
- Confirm the fetch works off-the-shelf: un_fao's historical queryset is a frame-capable datafactory descriptor (
source: views-datafactory; FRAME_CAPABLE_SOURCES gate at feature_frame_path.py:160) — expected yes; verify.
Acceptance criteria
un_fao runs global-land (land_gaul, 64,742) end-to-end without OOM (peak RSS well under host RAM).
- The coverage gate passes at 64,742 for both the forecast and the actuals companion frame.
grep -rn "import pandas" on the un_fao delivery path (unfao/, delivery/, wire/) returns nothing.
- The forecast lands in
unfao_bucket; python -m tools.liveness shows unfao_delivery fresh.
Coordination
- Companion change in views-models (
config_queryset.py data_format: feature_frame + region land_gaul, README) — must land in lockstep.
- Part of the platform-wide "pandas out of the FAO product path" effort (see the companion issues in views-faoapi, views-frames, views-pipeline-core).
- This is the critical path: run-0 ships as soon as this + the views-models companion land.
Filed from the views-models seat during run-0 (2026-07-27), with a full three-repo code sweep behind it. Receipts available.
Summary
Make the
un_faopostprocessor's historical/actuals path pandas-free, so the FAO delivery runs entirely onviews-frames. This is the root fix for the run-0 OOM and the gate that unblocks shipping the global-land FAO forecast.Why (the incident)
Delivering the global-land forecast (
region=land_gaul, 64,742 cells × ~438 months = 28,356,996 rows) OOM-killed the postprocessor at ~23.8 GB:Root cause:
_read_historical_datafetches the actuals as a pandas DataFrame (get_data()→read_dataframe()→PGMDataset). The region scope is correct and proven (datafactory served exactly 64,742 cells); the wall is pandas.Decisive context: pipeline-core's pandas-free fetch (
get_feature_frame→views_frames.FeatureFrame) is fully built and tested but has zero production consumers (a pipeline-core falsification test says the frame path is "capability-on-paper, exercised by zero real runs"). This issue makes the FAO product the first real consumer.The forecast side is already frame-native — extend that seam to the actuals
The
wire_contractpath is already pandas-free for the forecast (wire/,delivery/,frame_extraction.py,track_a_source.py— several modules explicitly forbid importing pandas). We extend the same seam to the historical/actuals side and retire the pandas historical path — we do not migratePGMDataset, we bypass it exactly as the forecast path already does.Scope
unfao.py:_read_historical_data(~:88-104), callself._data_loader.get_feature_frame(...)and readcached_frame_pathinstead ofget_data()/read_dataframe()/PGMDataset. Consume theviews_frames.FeatureFramedirectly. (Requires the descriptor to declaredata_format: feature_frame— that companion change is staged in views-modelspostprocessors/un_fao/configs/config_queryset.py; land the two together.)frame_extraction, the pyarrow GAUL join inwire/sidecar.py:build_sidecar, anddelivery/coverage. RetirePGMDataset,enrichment.py(pandasmerge), and the pandasextraction.pyseam for the historical path.frame_extraction.py:14-21):drop_months_above(the_clip_observed_historymonth clip; S7/S7 — historical inbound read_dataframe/PGMDataset retirement #92),unmapped_cell_count(provenance; S4/S4 — enrichment.py: numpy/pyarrow keyed gather (drop pandas merge) #89),cross_level_aligntakes a consumer-supplied mapping).source: views-datafactory;FRAME_CAPABLE_SOURCESgate atfeature_frame_path.py:160) — expected yes; verify.Acceptance criteria
un_faoruns global-land (land_gaul, 64,742) end-to-end without OOM (peak RSS well under host RAM).grep -rn "import pandas"on the un_fao delivery path (unfao/,delivery/,wire/) returns nothing.unfao_bucket;python -m tools.livenessshowsunfao_deliveryfresh.Coordination
config_queryset.pydata_format: feature_frame+ regionland_gaul, README) — must land in lockstep.Filed from the views-models seat during run-0 (2026-07-27), with a full three-repo code sweep behind it. Receipts available.