--- name: debug description: Use when debugging a pytrendy bug, reproducing a regression, or inspecting intermediate pipeline stage output. Covers the tests/test.py sandbox convention, phase 1 (process_signals debug=True plots), phase 2 (segments_refine stage-by-stage bisect), and handoff to the test skill for formal regression tests. --- # Debugging in pytrendy ## Scratch sandbox: tests/test.py `tests/test.py` is the committed reproduction sandbox. Key facts: - **Not pytest-collected** — filename `test.py` doesn't match the `test_*.py` glob, so `pytest` ignores it. Its module docstring states this explicitly. - **`# %%` cell markers throughout** → VSCode treats it as an interactive notebook. Run cells inline with Shift+Enter; plots render inline via the interactive backend. - **Magic setup at top**: `%load_ext autoreload` / `%autoreload 2` — edits to `pytrendy/` reload without restarting the kernel. - **`# TODONE:` / `# TODO:` inline notes** track fix status per cell — the file's distinctive convention (see line 30, 53, 89, etc. for examples). - **Existing sections are specific bug reproductions** (synth data + `detect_trends()` call + inspect plot), not generic templates. Agent-added sections must match: one `# %%` cell per bug, specific scenario/data/params. ### When adding a section - `# ---------- ` header (matches existing convention — see line 325 "New Reproduction: abrupt padding all-flat fallback on zero-baseline market entry"). - `# %%` cell loading the data source **once**. Do not copy or rename the DataFrame per cell. - **CSV fixture**: load via `pd.read_csv(...)`. Keep columns as-is; pass the actual column name to `value_col` in each `detect_trends()` call (e.g. `value_col='zero_baseline_market_entry_2'`). - **pytrendy built-in data** (`pt.load_data(...)`) or inline synth: load once and reuse the reference across cells. Only reload if the scenario genuinely requires different data modifications. - `# %%` cell calling `detect_trends(...)` — with `debug=True` for phase-1 bugs, plain for phase-2 bugs. - `# TODONE:` note on the cell once the bug is fixed (matches the file's tracking convention). - Cell stays in place after migration to formal tests — not deleted. ### After the fix Mark the reproduction cell's note `# TODONE:`. Leave the cell in place; it stays useful for re-running after future changes. Then migrate the reproduction into a formal regression test — see the `test` skill ("Adding a regression test from a debug reproduction"). ## Phase 1 — process_signals (rolling window flags) Bugs in flagging propagate everywhere downstream. Diagnose here first. Pass `debug=True` to `detect_trends()`. `process_signals.py:196` emits 7 diagnostic plots in sequence, each showing the value column on the left y-axis and the metric/flag on the right: 1. **SNR** + `THRESHOLD_NOISE` line (2.5) — noise detection input. 2. **noise_flag** — where SNR fell below threshold. 3. **smoothed** — Savitzky-Golay output (window=15). 4. **smoothed_std** — rolling std (window=7) for flat detection. 5. **flat_flag** — where smoothed_std ≤ min non-zero rolling std. 6. **smoothed_deriv** + `±derivative_limit` lines — trend direction input. 7. **trend_flag** — final per-day classification (1=Up, -1=Down, -2=Flat, -3=Noise). If a flag is wrong here (e.g. `noise_flag` fires on a legitimate abrupt shift, or `trend_flag` misses an obvious uptrend), the bug is in `process_signals.py`. The knobs: `THRESHOLD_NOISE=2.5`, `THRESHOLD_SMOOTH=0.001`, `WINDOW_SMOOTH=15`, `WINDOW_FLAT=7`. Reproduce with a single series + `debug=True` in a `# %%` cell of `tests/test.py` and iterate. ## Phase 2 — segments_refine (post-processing bisect) If `process_signals` flags look correct but the final segments are wrong, the bug is in the post-processing chain. `pytrendy/post_processing/segments_refine/__init__.py:refine_segments()` runs ~14 stage calls in a specific order, and bugs compound — a misclassification at `classify_trends` propagates through grouping, shaving, cleanup, and re-classification. **Stage order from `__init__.py`:** 1. `classify_trends` — DTW gradual/abrupt labels. 2. `group_segments` (1st pass — sporadic flats/noises). 3. `expand_contract_segments` — gradual boundary adjust (±7d extrema). 4. `shave_abrupt_trends` — abrupt changepoint z-score shaving. 5. `clean_artifacts` — remove overlaps from expand/contract. 6. `group_segments` (2nd pass). 7. `clean_artifacts` (again). 8. `classify_trends` (reclassify — some graduals → abrupts). 9. If changed: `shave_abrupt_trends` (2nd pass, `second_pass=True, init_segments=...`). 10. `group_segments` + `clean_artifacts`. 11. `fill_in_flats` — fill gaps with flats. 12. `group_segments` (3rd pass, final) + `clean_artifacts` (`inverse_only=True`). ### Primary bisect — comment in source Open `refine_segments()` in `pytrendy/post_processing/segments_refine/__init__.py`. Comment out stages one at a time (working from the bottom of the chain upward, or from `classify_trends` onward). After each edit, re-run the `tests/test.py` reproduction cell (Shift+Enter in VSCode) to see the effect on the plot/segments. The first uncomment that reintroduces the wrong behavior isolates the culprit stage. ### Alternative bisect — snapshot helper (exploration only, not persisted) For faster iteration without touching source, run this loop outside `tests/test.py` (REPL, separate scratch notebook, inline eval). Prints segment state after each stage; the divergence point is the culprit. **Throwaway — never persist this in `tests/test.py`.** ```python import pytrendy as pt from pytrendy.post_processing.segments_get import get_segments from pytrendy.post_processing.segments_refine import ( classify_trends, group_segments, expand_contract_segments, shave_abrupt_trends, clean_artifacts, fill_in_flats, ) df = pt.load_data('series_synthetic')[['date', 'gradual']].set_index('date') value_col = 'gradual' method_params = dict(abrupt_padding=0) segs = get_segments(df) print('pre-refine', len(segs), [(s['direction'], str(s['start']), str(s['end'])) for s in segs]) for name, fn in [ ('classify_trends', lambda s: classify_trends(df, value_col, s)), ('group_segments#1', lambda s: group_segments(s)), ('expand_contract', lambda s: expand_contract_segments(df, value_col, s)), ('shave_abrupt', lambda s: shave_abrupt_trends(df, value_col, s, method_params)), ('clean_artifacts#1', lambda s: clean_artifacts(df, value_col, s, method_params)), ('group_segments#2', lambda s: group_segments(s)), ('clean_artifacts#2', lambda s: clean_artifacts(df, value_col, s, method_params)), ('classify_trends#2', lambda s: classify_trends(df, value_col, s)), ('fill_in_flats', lambda s: fill_in_flats(df, s)), ('group_segments#3', lambda s: group_segments(s)), ('clean_artifacts#3', lambda s: clean_artifacts(df, value_col, s, method_params, inverse_only=True)), ]: segs = fn(segs) print(name, len(segs), [(s['direction'], str(s['start']), str(s['end'])) for s in segs]) ``` ### Re-classification loop gotcha Steps 8-10 only fire if `classify_trends` (reclassify) changes anything (`if segments_refined != init_segments` at `__init__.py:52`). If your bug only appears on series with mixed gradual/abrupt, the 2nd-pass `shave_abrupt_trends` with `second_pass=True, init_segments=...` is a likely culprit — it has different behavior than the 1st pass. ## Handoff to formal tests Once fixed, the `test` skill takes over — migrate the reproduction into `tests/tests_crashes_edgecases/test_*.py` using `assert_segments_match` / `assert_segments_in_a_haystack` from `tests/conftest.py`. The `tests/test.py` cell stays in place.