Refactor expert harmonizer from monolith to modular package#8
Conversation
Splits the 1200-line expert harmonization monolith into a small package: a shared `common` library (Context + helpers + location dedup + writer) and one `dataset_NN.py` per dataset, each a `harmonize(ctx) -> DatasetResult`. Block bodies are verbatim copies of the monolith (only globals -> Context and accumulator appends -> returns changed), so harmonized outputs are identical; a round-trip check confirms byte-level fidelity of every block. This makes grouped leave-one-cluster-out trivial and retires the PR #5 cell-splicer: holding a cluster out is now "don't run those modules" (`run.py --holdout ...`) and held-out-free code context is "the kept module files" — no text splicing. Requiring each hold-out to map to a real module still auto-rejects the reference dataset (0) and excluded datasets (11-14, 19-22), so there is no coupling to cv_folds.yaml. - add data/gold/expert_code/harmonize_sm/ (common, 19 dataset modules, registry, run driver, README) - replace src/folds/ablate_monolith.py with src/folds/expert_harmonizer.py (run / kept_module_paths / assemble_source / resolve_holdout + CLI) - rewrite tests; update skill + README references to the package - remove the monolith Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDyuNoMsjJWqNRsdHLHrSh
|
Claude finished @cmungall's task —— View job PR Review: Refactor expert harmonizer from monolith to modular packageTasks
This is a well-structured refactor. The core idea—replacing text-splicing with module-omission for LOO evaluation—is sound and the implementation is clean. Here are my findings: Architecture & Code QualityStrengths:
Issues / Suggestions: 1. Unused variable in every dataset module ( In every 2. O(n²) pairwise comparison in for i, j in combinations(range(n), 2):
if is_match_pair(i, j, loc_df):With n location rows this is O(n²). The docstring says it's a "verbatim port" from the monolith, so this is inherited, but worth noting. For large datasets this could be slow. A spatial index (KD-tree or blocking by grid cell) would scale better. 3. sys.path.insert(0, str(Path(__file__).resolve().parent))This is a side effect of import and will persist for the lifetime of the process. If 4. DATASETS = {idx: import_module(f"dataset_{idx:02d}").harmonize for idx in DATASET_INDICES}All 19 dataset modules are imported when 5. Both files maintain the same list independently. The comment in 6. def read_ds_csv(self, idx: int, filename: str, encoding="utf-8", errors="ignore", **kwargs) -> pd.DataFrame:
return pd.read_csv(self.ds_path(idx) / filename, encoding=encoding, **kwargs)The 7. UUID stability not guaranteed ( uuid_map = pd.DataFrame({
'location_component_id': comp_ids,
'harmonized_location_uuid': [str(uuid.uuid4()) for _ in comp_ids]
})UUIDs are random on every run, so the same physical location gets a different UUID each time Potential Bugs8. This is harmless but redundant. The outer validation in 9. chunks.append(f"# ===== {path.name} =====\n{path.read_text()}")
Test CoverageThe tests are solid for the pure helpers (
SecurityNo security concerns. The code reads local CSV files and JSON; it doesn't execute user-supplied strings, make network requests, or expose any attack surface. PerformanceBeyond the O(n²) location-matching noted above, the modular structure is actually a performance improvement for LOO: loading only the needed modules avoids processing excluded datasets entirely. Summary: This is a clean and well-motivated refactor. The main actionable bug is the dropped |
Fix: port build_env to modular expert_harmonizer (restores main broken by #8)
- Fix repo_root default-arg bug: Option default None, resolve in body (was Path.cwd() evaluated at import time) [#6] - Audit the LS tool (path) [#8]; check Grep/Glob 'glob'/'pattern' for path-bearing patterns [#7] - Flag 'cd' into an answer location (answer key / real gold/processed), so a single-component read after it isn't missed [#1] - Remove dead '&& ||' token guard [#3] - --show-bash prints full multi-line commands instead of just the first line [#5] - Tests: LS, Grep glob, cd-into-answer-key, multi-cd chain, ~ expansion 53 tests green; real fold_ds07 trace still CLEAN. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
Refactored the expert soil-moisture harmonization script from a single 1200-line monolith into a modular package structure. This enables cleaner leave-one-cluster-out evaluation without text-splicing, as held-out datasets are simply omitted from the module registry rather than surgically removed from source code.
Key Changes
data/gold/expert_code/harmonize_ess-dive_soilmoisture_data.py(1239 lines)data/gold/expert_code/harmonize_sm/:common.py: Shared library withContextclass (wraps mapping JSON + dataset-access helpers), pure transformation helpers, location deduplication, and CSV output writerdataset_NN.py(19 modules): One per dataset (indices 1-10, 15-18, 23-27), each exposingharmonize(ctx) -> DatasetResultdatasets.py: Registry mapping dataset indices to harmonizer functionsrun.py: Runner script that loads datasets and optionally excludes a holdout setREADME.md: Package documentationsrc/folds/ablate_monolith.py(cell-splicing logic) withsrc/folds/expert_harmonizer.py(modular runner)kept_module_paths()returns code context (common + kept datasets),run()executes without held-out modulescv_folds.yaml; validation automatically rejects reference dataset (0) and globally-excluded datasets (11-14, 19-22)tests/test_ablate_monolith.pywithtests/test_expert_harmonizer.pyImplementation Details
Contextinstance, accumulator lists →DatasetResultreturn values--holdout 1,2,3,6,16,27(dataset indices or identifiers) rather than text-based cell removalhttps://claude.ai/code/session_01YDyuNoMsjJWqNRsdHLHrSh