Add config-driven model evaluation pipeline (rework of #17)#28
Add config-driven model evaluation pipeline (rework of #17)#28KarimAED wants to merge 7 commits into
Conversation
|
@rapsoj , could you confirm that you are happy with this to replace your PR #17 ? If so, please approve this PR and close #17. If not, let us know what the sticking points are, please. Thanks! I assigned @KarimAED to this PR, so once approved, he can make any final commits, merge, and delete the branch. |
Reworked version of the evaluation pipeline from PR #17, rebuilt on top of current main: - run-evaluation console script runs the full analysis suite from a JSON config (paths resolved relative to the config file); the new-model join step is optional so the CSV's existing model column can be evaluated directly - shared helpers in evaluation/scripts/common.py (annotation loading, CI summaries, bar plots, hex-grid aggregation) replace the load/metrics/ plot blocks that were duplicated across the per-analysis scripts - add_new_model_results joins per-date prediction GPKGs onto the manual annotations by reconstructing 100 m tiles around the stored centroids - h_merge_geojsons gains the --process-by-date batch mode (already documented in the README) built on util.thresholding, and no longer requires OUTPUT_GPKG in that mode - total_error outputs renamed to hex_analytic_* so they no longer collide with spatial_bootstrap_hex outputs in a shared output directory - spatial context layers (agriculture, h3_density, destruction) are NOT committed; they are provisioned locally into the gitignored spatial_data/layers/ directory (see README); the municipal boundary shapefile is reused from gaza_boundaries/ - checked-in results/ outputs removed and gitignored; manual-eval assets moved under evaluation/manual_eval/ Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XjFy5GC8fK74DeV5mcEtBc
…face Adds displacement_tracker/evaluation/annotation_reference.py: - ManualAnnotationReferenceSource implements ReferenceSource from util/reference_data.py (PR #29): each annotated tile's count is rasterized into the master-grid cell containing the tile centroid, with UNOSAT-style explicit date selection (required whenever the CSV spans more than one acquisition date) - registered as reference type 'manual_eval' in SOURCE_TYPES on import - annotation-reference console script materializes one date's annotations as a counts GeoTIFF on a master grid, consumable by the built-in 'raster' reference type without importing this module - guarded import raises a clear error on checkouts that predate the reference-data interface; nothing else in the evaluation suite imports this module, so the pipeline keeps working without PR #29 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XjFy5GC8fK74DeV5mcEtBc
Failure modes found by fault-injection testing, all previously fatal or
silently lossy:
- an all-filtered date crashed the whole batch (empty GeoDataFrame
to_file raises); save_merged_gpkg now clears any stale output and
skips the write with a warning
- uppercase .JSON files were moved into date folders by the sorter but
never picked up by the case-sensitive glob; both sides now share a
case-insensitive list_geojson_files helper
- a corrupt/unreadable GeoJSON aborted every remaining date; it is now
logged and skipped, a folder where every file fails raises, and the
batch loop isolates per-date failures, processes the rest, and exits
non-zero with a summary of failed dates
- shutil.move silently overwrote an existing file in the date folder;
collisions now leave the root copy in place with an explicit warning
that it will not be merged
- _99999999_ passed the \d{8} pattern and created a junk folder;
dates are now validated as real calendar dates in both the sorter
and iter_date_folders
- dates immediately before the extension (pred_20241014.json) were not
recognized; the pattern now accepts _YYYYMMDD followed by _ or .
Single-folder mode and re-run idempotency regression-tested.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjFy5GC8fK74DeV5mcEtBc
The three spatial context layers (~20 MB minified) are no longer part of the repository: spatial_data/layers/ is gitignored, the README documents how to obtain them (team share, or restored from the original eval-postprocessing-pipeline branch), and run-evaluation now preflights all input paths and lists anything missing with a pointer to the README instead of failing with a GDAL traceback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XjFy5GC8fK74DeV5mcEtBc
d25b710 to
6a67902
Compare
Resolves the h_merge_geojsons.py conflict by keeping main's config-driven CLI (#27) and folding the --process-by-date batch mode into it as a merge.process_by_date config key on merge_geojsons(); docs updated to the config-driven syntax. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XjFy5GC8fK74DeV5mcEtBc
|
@claude review Generated by Claude Code |
|
Claude finished @KarimAED's task in 7m 0s —— View job Thermo-Nuclear Code Quality Review
SummaryThe core of this PR is genuinely good structural work. Nine copy-pasted standalone scripts with hardcoded module-level config became parametrized functions over a shared But the PR stops one step short of its own standard in several places, and one claim in the description doesn't hold. Findings in priority order: Must fix
Should fix (finish the consolidation this PR started)
Question
Minor, no inline: new evaluation code mixes bare Items 1 and 2 are correctness-adjacent, item 3 ships a dead registration and a broken CLI entry point, and items 4–6 are the same consolidation discipline the PR itself established, applied one level further. All are small, targeted changes. Thermo-Nuclear Verdict: CHANGES REQUIRED |
| # Derive per-region spread and extrapolated totals from the CI summary. | ||
| n = results_df["num_tiles"] | ||
| se = (results_df["mean_tile_error"] - results_df["ci_lower"]) / Z_95 | ||
| results_df["std_tile_error"] = (se * np.sqrt(n)).where(n > 1, 0.0) | ||
| results_df["se_tile_error"] = se | ||
| results_df["s_tile_error"] = results_df["std_tile_error"] | ||
| results_df["total_regional_error"] = n * results_df["mean_tile_error"] | ||
| results_df["total_regional_se"] = n * se | ||
| results_df["total_regional_ci_lower"] = ( | ||
| results_df["total_regional_error"] - Z_95 * results_df["total_regional_se"] | ||
| ) | ||
| results_df["total_regional_ci_upper"] = ( | ||
| results_df["total_regional_error"] + Z_95 * results_df["total_regional_se"] | ||
| ) |
There was a problem hiding this comment.
Magical inversion of the CI to recover the standard error. This block reconstructs se by algebraically inverting group_error_summary's output ((mean - ci_lower) / Z_95) and then rebuilds std from it. That couples this script to the implementation detail that the summary's CI is a symmetric normal-approximation interval at exactly Z_95 — if mean_error_ci ever grows a t-interval, a different z, or a bootstrap variant, this silently produces wrong std/se values instead of failing.
mean_error_ci already computes np.std(errors, ddof=1) and the se internally — it just throws them away. The direct fix is to include std_tile_error/se_tile_error (or just std) in the dict mean_error_ci returns; every consumer gets them for free through group_error_summary and this whole back-derivation block collapses to the two total_regional_* lines.
Also: results_df["s_tile_error"] = results_df["std_tile_error"] writes the same value under two names. If s_tile_error is only there to preserve a legacy CSV schema, say so in a comment or drop it — as written it reads like an accident.
| PREWAR_TIF = "tif_files/prewar_gaza.tif" | ||
|
|
||
| OUTPUT_CSV = "evaluation/manual_annotation_results.csv" | ||
| OUTPUT_CSV = "evaluation/manual_eval/manual_annotation_results.csv" |
There was a problem hiding this comment.
This path is still stale relative to the repo root. GAZA_BOUNDARY_SHP = "gaza_boundaries/..." (line 19) implies the script is run from the repo root, but from there evaluation/manual_eval/ doesn't exist — the file now lives at displacement_tracker/evaluation/manual_eval/. df.to_csv doesn't create directories, so the annotation session's final save raises FileNotFoundError and (looking at lines 381–388) the collected annotations are lost. The PR description says this path was updated for the move, but it's missing the displacement_tracker/ prefix:
| OUTPUT_CSV = "evaluation/manual_eval/manual_annotation_results.csv" | |
| OUTPUT_CSV = "displacement_tracker/evaluation/manual_eval/manual_annotation_results.csv" |
| return selected | ||
|
|
||
|
|
||
| SOURCE_TYPES["manual_eval"] = ManualAnnotationReferenceSource |
There was a problem hiding this comment.
This registration has no importer, so the manual_eval reference type is unreachable. Nothing in this repo imports displacement_tracker.evaluation.annotation_reference (I checked), and the module deliberately isn't imported by the evaluation suite. So the documented config usage (reference: {type: manual_eval, ...}) can only work if #29's build_reference_source — or the user — happens to import this module first. Side-effect registration with no import hook is a dead code path wearing a working one's docs.
When #29 lands, either its reference layer needs an explicit plugin/import mechanism that pulls this module in, or the registration should move to wherever SOURCE_TYPES is populated. Please state which of those is the plan (and where the import will live); otherwise only the CLI/raster path actually functions.
Relatedly, pyproject.toml:60 registers the annotation-reference console script now, while this module raises ImportError on every checkout until #29 merges — poetry run annotation-reference --help today is a traceback. The guarded import with a clear message is good, but shipping a wired-up entry point that cannot run is the wrong sequencing: add the entry point (and this module) in or after #29, or land #29 first.
There was a problem hiding this comment.
Fixed in 79deb07 by making the interface optional instead of a hard import: the module now imports (and the annotation-reference CLI works) on every checkout — the raster-export path has no real dependency on the ABC. When util/reference_data.py is present, the class subclasses ReferenceSource and registers as manual_eval at import; when absent, registration is skipped with a debug log. The runtime-import question is documented in the module docstring and README: config-only flows should use the exported counts raster (raster type) until #29's flow imports this module (or #29 adds the import itself once both are merged).
Generated by Claude Code
| df = pd.read_csv(csv_path) | ||
| required = {lat_column, lon_column, date_column, count_column} | ||
| missing = required - set(df.columns) | ||
| if missing: | ||
| raise ValueError( | ||
| f"Annotation CSV missing required columns: {sorted(missing)}" | ||
| ) | ||
|
|
||
| df = _select_date(df, date, date_column, csv_path) | ||
| df = df.dropna(subset=[count_column]) | ||
|
|
||
| self._count_column = count_column | ||
| self._gdf = gpd.GeoDataFrame( | ||
| df[[count_column]], | ||
| geometry=gpd.points_from_xy(df[lon_column], df[lat_column]), | ||
| crs="EPSG:4326", | ||
| ) |
There was a problem hiding this comment.
This re-implements the annotation loading that scripts/common.py just centralized (read CSV → validate required columns → build EPSG:4326 points from lat/lon = load_annotation_points). Two loaders for the same CSV in the same package will drift.
I suspect the reason you didn't reuse it is that importing common drags in matplotlib and force-sets the Agg backend at import time — which you rightly don't want in the tuning flow. That's the actual structural smell: common.py bundles data loading, statistics, and plotting into one module. Splitting it (e.g. common.py keeps loading/stats, plotting helpers move to a plots.py) lets this class call the canonical loader with a date/count_column filter on top, and deletes the duplicate column-validation/GeoDataFrame-construction here.
| agriculture_gdf = gpd.read_file(agriculture_geojson) | ||
|
|
||
| # Ensure CRS alignment | ||
| if agriculture_gdf.crs != tiles_gdf.crs: | ||
| agriculture_gdf = agriculture_gdf.to_crs(tiles_gdf.crs) | ||
|
|
||
| # Dissolve to single multipolygon | ||
| agriculture_union = agriculture_gdf.geometry.union_all() |
There was a problem hiding this comment.
This read-layer → check-required-column → align-CRS preamble is repeated near-verbatim in four scripts (evaluate_agriculture.py:34-37, evaluate_destruction.py:41-46, evaluate_density.py:44-48, evaluate_municipal_bounds.py:39-43). Since common.py exists precisely to hold these shared steps, add one helper there, e.g. load_layer(path, target_crs, required_columns=()), and each script's preamble becomes one line. evaluate_destruction and evaluate_density also share the sjoin-then-groupby(joined.index).agg(...) collapse for multi-polygon matches — worth folding into a small sjoin_collapse helper at the same time if it stays this parallel.
| if exclusion_geom is not None: | ||
| before_exclusion = len(pts) | ||
| pts = filter_points_by_exclusion(pts, exclusion_geom) | ||
| LOGGER.info( | ||
| " %s: %d kept after exclusion filtering", | ||
| path.name, | ||
| len(pts), | ||
| ) | ||
| if before_exclusion != len(pts): | ||
| LOGGER.info( | ||
| " %s: %d points removed by exclusion zones", | ||
| path.name, | ||
| before_exclusion - len(pts), | ||
| ) | ||
|
|
||
| if inclusion_geom is not None: | ||
| before_inclusion = len(pts) | ||
| pts = filter_points_by_inclusion(pts, inclusion_geom) | ||
| LOGGER.info( | ||
| " %s: %d kept after inclusion filtering", | ||
| path.name, | ||
| len(pts), | ||
| ) | ||
| if before_inclusion != len(pts): | ||
| LOGGER.info( | ||
| " %s: %d points removed outside inclusion zone", | ||
| path.name, | ||
| before_inclusion - len(pts), | ||
| ) |
There was a problem hiding this comment.
The extraction into process_geojson_folder is the right move, but it carried these two structurally identical 14-line zone-filter blocks along verbatim — and they sit on top of filter_points_by_exclusion/filter_points_by_inclusion, which are themselves the same function differing by a not. Since this PR already owns the restructuring, collapse the whole thing: one filter_points_by_zone(points, geom, keep_inside: bool) plus a loop over [("exclusion", exclusion_geom, False), ("inclusion", inclusion_geom, True)] replaces two functions and both blocks, and the double logging (kept after ... filtering immediately followed by removed ...) can become a single line per zone.
Minor, same file: sort_preds_by_date returns sorted(touched_dates) but the only caller (merge_geojsons:433) ignores it and re-derives folders via iter_date_folders. Drop the return value (or use it) so the function's contract matches reality.
| PATH_KEYS = ( | ||
| "annotation_csv", | ||
| "output_csv", | ||
| "prediction_dir", | ||
| "sample_tif", | ||
| "output_dir", | ||
| "boundary_shp", | ||
| "agriculture_geojson", | ||
| "h3_geojson", | ||
| "destruction_geojson", | ||
| ) | ||
|
|
||
| NEW_MODEL_KEYS = ("prediction_dir", "sample_tif", "new_model_column") | ||
|
|
||
|
|
||
| def load_config(config_path: Path) -> dict: | ||
| with config_path.open("r", encoding="utf-8") as f: | ||
| cfg = json.load(f) | ||
|
|
||
| # Resolve relative paths against the config file's directory. | ||
| base = config_path.parent | ||
| for key in PATH_KEYS: | ||
| if cfg.get(key): | ||
| cfg[key] = str((base / cfg[key]).resolve()) | ||
| return cfg | ||
|
|
||
|
|
||
| def require(cfg: dict, key: str): | ||
| if not cfg.get(key): | ||
| raise click.ClickException(f"Missing required config key: {key}") | ||
| return cfg[key] |
There was a problem hiding this comment.
Boundary question: why a second, parallel config mechanism? The repo's canonical configuration is config.yaml + util/config.load_flow_config (which merge-geojsons in this very PR uses). This introduces a JSON sidecar with its own bespoke machinery — PATH_KEYS path resolution, a local require(), key-presence conventions. Users now maintain two config dialects, and evaluation settings can't live next to the pipeline settings they relate to (e.g. the merge.process_by_date output dir that feeds prediction_dir here).
If there's a deliberate reason (per-run experiment configs that shouldn't touch the flow config, decoupling from ${DATA_DIR} env substitution), document it in the module docstring. Otherwise an evaluation: section in config.yaml read through load_flow_config would delete this loader and keep one config story for the repo.
There was a problem hiding this comment.
Deliberate, now documented in the README (79deb07): the evaluation suite is a standalone analysis layer rather than a pipeline stage — it's typically run with several parallel configs (one per evaluated model), and its paths resolve against the config file's own directory (annotation CSV, local gitignored layers) rather than ${DATA_DIR}. Folding it into config.yaml's flow sections would force exactly one evaluation config per checkout and route standalone analysis assets through pipeline config resolution. Happy to revisit if the team prefers one dialect everywhere.
Generated by Claude Code
- mean_error_ci now returns the sample std it computes; every group summary carries std_tile_error, and evaluate_municipal_bounds derives se/totals from it directly instead of algebraically inverting the CI; the duplicate s_tile_error column is dropped (values unchanged, verified against the previous run) - manual_eval.py OUTPUT_CSV gets the missing displacement_tracker/ prefix so saves work from the repo root like its other paths - annotation_reference imports cleanly on every checkout: the ReferenceSource base and SOURCE_TYPES registration are optional (skipped with a debug log when util/reference_data.py is absent), so the annotation-reference console script always works; registration and its import requirement are documented in the module docstring and README - common.py split: loaders/statistics stay in common.py (no matplotlib), plotting moves to plots.py; annotation_reference reuses the canonical read_annotations/as_points loaders instead of its own copy - new load_layer() helper replaces the read/validate/align-CRS preamble repeated in the agriculture, destruction, density and municipal scripts - h_merge_geojsons: single filter_points_by_zone replaces the twin exclusion/inclusion functions and their twin logging stanzas; sort_preds_by_date's dead return value removed - print() replaced with the repo's setup_logging logger across the evaluation scripts; README documents why the evaluation suite keeps its own JSON config rather than the pipeline config.yaml Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XjFy5GC8fK74DeV5mcEtBc
|
@claude review Generated by Claude Code |
|
Claude finished @KarimAED's task in 3m 36s —— View job Thermo-Nuclear Code Quality Review — Round 2
SummaryCommit 79deb07 addresses every round-1 finding, and it addresses them the right way — at the source, not with patches on top:
I re-read the full diff against Against the skill's approval bar: no structural regression, no missed simplification with a visible path, no file over 234 lines in the evaluation package (largest touched file overall is Thermo-Nuclear Verdict: APPROVED |
| import numpy as np | ||
| import pandas as pd | ||
|
|
||
| import matplotlib.pyplot as plt |
There was a problem hiding this comment.
Minor, non-blocking: before the common.py/plots.py split, every evaluation script inherited the forced Agg backend from common.py's import. Now the matplotlib.use("Agg") call lives only in plots.py, and this script (and evaluate_tile_correlation.py) imports pyplot directly without importing plots — so standalone use of these two modules no longer forces Agg and relies on matplotlib's headless fallback. Through run-evaluation it's fine (evaluate_agriculture's plots import fires first), but that's an incidental import-order dependency. A one-line from displacement_tracker.evaluation.scripts import plots # noqa: F401 — or moving these scripts' bespoke figures into plots.py alongside the other plotting helpers — would make it explicit.
There was a problem hiding this comment.
Fixed in 6c3cd94, taking the second option: the hexbin and scatter-with-1:1 figures moved into plots.py alongside the other plotting helpers (with the shared finite_xy cleaner in common.py), so neither script imports matplotlib at all and the backend policy lives in exactly one place. Verified matplotlib.get_backend() == "Agg" after a standalone import of each module.
Generated by Claude Code
evaluate_spatial_points and evaluate_tile_correlation imported pyplot directly after the common/plots split, relying on import order for the forced Agg backend. Their figures (error hexbin, scatter with 1:1 line) now live in plots.py with the other plotting helpers, the shared finite_xy cleaner moves to common.py, and neither script imports matplotlib at all. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XjFy5GC8fK74DeV5mcEtBc
|
@claude review Generated by Claude Code |
|
Claude finished @KarimAED's task in 2m 8s —— View job Thermo-Nuclear Code Quality Review — Round 3
SummaryThe only change since the round-2 approval is commit 6c3cd94, which resolves the one non-blocking observation from that round — and it takes the option I'd have picked:
I could not run the import-time backend check myself (executing Python isn't permitted in this environment), but the change is statically unambiguous — the scripts have no matplotlib import to mis-order — and the author reports verifying Nothing else in the diff moved, so the round-2 assessment stands: all round-1 findings fixed at the source, no file-size concerns (largest evaluation file is No new inline comments; there is nothing left to flag at the bar this skill sets. Thermo-Nuclear Verdict: APPROVED |
Reworked version of the evaluation pipeline from #17, rebuilt on top of current
mainso it merges cleanly and keeps the repo small. Same functionality: a config-driven suite that scores model predictions against the manual tile annotations (total/hex-aggregated error with analytic and bootstrap CIs, error by municipality / month / building density / agriculture / destruction, and manual-vs-model correlations), plus the per-date prediction merge and annotation-join steps needed to evaluate a new model.Review status: approved by the repo's thermo-nuclear review workflow at the current tip (
6c3cd94), after three rounds; all findings fixed at the source (sharedcommon.py/plots.pysplit, direct std instead of CI inversion, import-safe reference module, consolidated zone filters, unified logging).What's here
run-evaluationconsole script (displacement_tracker/evaluation/run_all_analyses.py) runs all nine analyses fromanalysis_config.json. Relative config paths resolve against the config file, so it runs from anywhere, and all input paths are preflighted with a helpful error listing anything missing. The new-model join step is optional: leaveprediction_dir/sample_tif/new_model_columnnull to evaluate themodel_columnalready in the annotation CSV. (The suite keeps its own JSON config deliberately — it's a standalone analysis layer typically run with several parallel per-model configs; documented in the README.)evaluation/scripts/common.py(annotation/layer loading, validation, mean-error CI summaries with std, hex-grid aggregation) andevaluation/scripts/plots.py(all figure rendering plus the singleAggbackend policy). Eachevaluate_*module now only expresses how tiles are grouped.add_new_model_results.pyjoins per-date prediction GPKGs onto the annotations by reconstructing the 100 m tiles around the stored centroids; counting is vectorized with a spatial join per date.merge-geojsonsper-date batch mode (merge.process_by_date: truein the unifiedconfig.yaml; the README documented this capability butmaindidn't have it), built onutil/thresholding.filter_points_by_adjusted_peakso thresholding semantics stay unified with prediction and validation.merge.outputis optional in this mode. Hardened against bad inputs: corrupt files are logged and skipped, per-date failures are isolated (remaining dates still process, the run exits non-zero with a summary), all-filtered dates skip the write instead of crashing, move collisions warn instead of silently overwriting, case-insensitive suffix handling prevents sorted-but-never-merged files, and dates are validated as real calendar dates.total_errorandspatial_bootstrap_hexboth wrotehex_with_cis.*/hex_mean_error_map.png; the analytic variant now writeshex_analytic_*so both can share an output directory.results/outputs are deleted and gitignored; manual-eval assets move underevaluation/manual_eval/(withmanual_eval.py's output path updated to match);.gitignoregains the missing trailing newline plus entries forsample.tif,results/,predictions/,spatial_data/layers/and.Rhistory.Reference-data interface integration (#29)
evaluation/annotation_reference.pyexposes the manual annotations to the generic validation/tuning reference interface (util/reference_data.py, introduced by #29):ManualAnnotationReferenceSourceimplementsReferenceSource.counts_on_grid: each annotated tile'smanual_tent_countis rasterized into the master-grid cell containing the tile centroid (count-weightedMergeAlg.add), with CRS reprojection andclip_geomsupport. Date selection is explicit and UNOSAT-style: the CSV spans 11 acquisition dates, sodateis required whenever more than one is present, and the error lists the available dates.manual_evalinSOURCE_TYPESwhen the interface is present, so it works throughbuild_reference_source/tuning.referenceconfig after importing this module.annotation-referenceconsole script materializes one date's annotations as a counts GeoTIFF on a master grid — consumable by the built-inrasterreference type without importing this module.util/reference_data.py(i.e. until feat: hyperparameter tuning pipeline with a generic reference-data interface #29 lands) the module still imports and the CLI still works — only themanual_evaltype registration is skipped (debug-logged). No merge-order dependency with feat: hyperparameter tuning pipeline with a generic reference-data interface #29 in either direction.Data — nothing large is committed
This PR adds no data blobs to git history. The three spatial context layers the analyses read (
agriculture,h3_density,destruction, ~20 MB even minified) are provisioned locally into the gitignoredspatial_data/layers/directory; the README documents how to obtain them (team share, or restored from the still-existingeval-postprocessing-pipelinebranch), andrun-evaluationpreflights them with a clear error when absent. The branch history was rewritten to confirm no commit ever contained them. The municipal-boundary shapefile is referenced from the existing rootgaza_boundaries/; the only data files touched are the manual-annotation CSV (moved, ~190 KB, pre-existing) and the deletion of ~30 previously committedresults/artifacts. Net repo growth is code only.Verification
agriculture,month, and municipal summaries match the previously committed results to the last decimal, and the bootstrap point estimates match exactly (CI bounds shift slightly because the RNG moved from legacynp.random.seedtodefault_rng).add_new_model_resultsexercised end-to-end on synthetic per-date GeoJSONs + a synthetic raster, including the column-name-collision fallback.reference_data.py(checked out temporarily from that branch): the 1,316 annotated tents for 2024-10-14 resolve to exactly 1,316 on a 100 m UTM master grid;build_reference_source({"type": "manual_eval", ...})returns identical counts; the CLI-exported GeoTIFF round-trips bit-identically through feat: hyperparameter tuning pipeline with a generic reference-data interface #29'sRasterReferenceSource; module import and CLI verified working on checkouts without feat: hyperparameter tuning pipeline with a generic reference-data interface #29, withmatplotlib/backend policy confirmed (Agg) on standalone imports.🤖 Generated with Claude Code
https://claude.ai/code/session_01XjFy5GC8fK74DeV5mcEtBc