Add debug st.info before st.plotly_chart to diagnose invisible chart
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
---
|
||||
date: 2026-03-30
|
||||
topic: "compass-trajectory-consistency"
|
||||
status: validated
|
||||
---
|
||||
|
||||
## Problem Statement
|
||||
|
||||
What we're solving and why
|
||||
|
||||
We must ensure the political compass (single-window snapshot) and the Explorer trajectories use the same numeric coordinate frame for the first two SVD axes so the compass numbers match the trajectory centroids exactly.
|
||||
|
||||
**Key issue:** Component 1 already matched, but component 2 shows persistent mismatches due to an API/shape ambiguity and occasional fallback logic differences. Fixing this prevents confusing, inconsistent numbers in the UI.
|
||||
|
||||
|
||||
## Constraints
|
||||
|
||||
Non-negotiables and limitations
|
||||
|
||||
- The canonical coordinate frame is the Procrustes-aligned output of **compute_2d_axes** (the repo artifact that produces **positions_by_window**).
|
||||
- Keep UI responsiveness and existing cache usage (@st.cache_data where present).
|
||||
- Minimal, focused changes: only update Explorer call sites and the compass renderer API. Do not change the SVD pipeline outputs.
|
||||
- Use the **first chronological party vector** as the fallback when a party has no MPs in a window (user decision).
|
||||
|
||||
|
||||
## Approach
|
||||
|
||||
Chosen approach and why
|
||||
|
||||
We will adopt an explicit API for the compass renderer: pass per-party 2D projected coordinates (party → (x,y)) computed from **positions_by_window** for the target window. This eliminates shape/indexing ambiguity and guarantees numeric equality with trajectory centroids.
|
||||
|
||||
**Why:**
|
||||
- Simpler and less error-prone than synthesizing k-dimensional vectors or changing compute_2d_axes.
|
||||
- Keeps the canonical data source unchanged (positions_by_window) and makes intent explicit at the Explorer surface.
|
||||
- Easy to test: we can assert numeric equality directly on the 2D coordinates.
|
||||
|
||||
|
||||
## Architecture
|
||||
|
||||
High-level structure of the change
|
||||
|
||||
**Key pieces:**
|
||||
- **compute_2d_axes** (unchanged): produces **positions_by_window** which is the canonical frame.
|
||||
- **Explorer: party centroid helper:** new helper that computes per-party (x, y) means from positions_by_window for a window.
|
||||
- **_build_party_axis_figure (changed API):** now accepts **party_coords: Dict[str, Tuple[float,float]]** and a selected component index (1 or 2) and uses the explicit coordinate values for plotting.
|
||||
- **Call-site updates:** update all places that previously passed party SVD vectors to instead compute and pass party_coords (use first-chronological party vector only when no MPs are present for that party in the window).
|
||||
|
||||
|
||||
## Components
|
||||
|
||||
Key pieces and responsibilities
|
||||
|
||||
- **compute_party_coords(positions_by_window, party_map, window_id):**
|
||||
- Input: positions_by_window, party->MP mapping (load_party_map or similar), window id.
|
||||
- Output: party -> (x_mean, y_mean). If no MPs for a party, returns None or uses fallback loader.
|
||||
|
||||
- **_build_party_axis_figure(party_coords, comp_sel, ...):**
|
||||
- Input: explicit 2D coords; **comp_sel** ∈ {1,2}.
|
||||
- Behavior: uses party_coords[p][comp_sel-1] as the axis value, constructs hover text, CIs, and plots. No indexing into long SVD vectors.
|
||||
|
||||
- **Fallback loader:** existing **load_party_axis_scores** (unchanged). When compute_party_coords finds no MPs, we will use the party's first chronological vector from load_party_axis_scores(window) as fallback and indicate fallback in hover text.
|
||||
|
||||
- **Callers to update:**
|
||||
- build_svd_components_tab
|
||||
- any other explorer function that previously passed party-axis vectors into _build_party_axis_figure
|
||||
|
||||
|
||||
## Data Flow
|
||||
|
||||
How data moves through the updated code path
|
||||
|
||||
1. UI requests compass for window W and component C.
|
||||
2. Explorer calls load_positions(db_path) → gets positions_by_window.
|
||||
3. compute_party_coords builds per-party (x,y) means from positions_by_window[W].
|
||||
4. For parties with zero MPs in W, call load_party_axis_scores(window) and take the **first chronological** party vector as fallback; annotate hover that a fallback is used.
|
||||
5. Pass party_coords to _build_party_axis_figure which reads comp_sel and uses the explicit coordinate at index 0 or 1.
|
||||
6. Explorer trajectories tab already computes the same centroids from positions_by_window; therefore numbers match exactly.
|
||||
|
||||
|
||||
## Error Handling
|
||||
|
||||
Strategy for failures and edge cases
|
||||
|
||||
- If positions_by_window is missing or corrupted: surface a clear diagnostic message in the UI recommending running the SVD recompute pipeline, and avoid attempting to plot mismatched values.
|
||||
- If a party has no MPs and load_party_axis_scores also returns no data: omit that party from the compass and add a tooltip note in the UI explaining why.
|
||||
- If any coordinate is NaN/inf: skip plotting that party and log a debug message with the party id and window.
|
||||
- Log a WARN when a fallback is used so we can find parties with no MPs across windows.
|
||||
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
How we will verify correctness
|
||||
|
||||
- Unit tests
|
||||
- Synthetic positions_by_window: build a small fake positions_by_window with known MP coordinates and party→MP mappings. Assert compute_party_coords outputs expected means and that _build_party_axis_figure uses those exact numbers for components 1 and 2.
|
||||
- Fallback behavior: create a window with a party that has no MPs and assert load_party_axis_scores is called and its first chronological vector is used.
|
||||
|
||||
- Integration tests
|
||||
- Run against a small real DB snapshot used in prior verification. Assert for a representative set of parties across several windows that compass numbers equal the trajectory centroids for components 1 and 2.
|
||||
|
||||
- CI
|
||||
- Run full test suite. Known pre-existing failures unrelated to this change may persist; document them separately but do not block this change on them.
|
||||
|
||||
- Manual QA
|
||||
- Run Explorer locally and spot-check compass tooltips vs trajectory hover values for multiple parties and windows.
|
||||
|
||||
|
||||
## Open Questions
|
||||
|
||||
Unresolved items (minor)
|
||||
|
||||
- None critical: the user selected the fallback preference (first chronological party vector) and agreed to update all callers without backward compatibility.
|
||||
|
||||
|
||||
---
|
||||
|
||||
I'm proceeding to create the implementation plan. Interrupt if you want changes to this design.
|
||||
@@ -0,0 +1,113 @@
|
||||
---
|
||||
date: 2026-03-31
|
||||
topic: "diagnose-no-plot-trajectories"
|
||||
status: draft
|
||||
---
|
||||
|
||||
## Problem Statement
|
||||
|
||||
We need to restore visible party trajectories in the Explorer "Partij Trajectories" tab so the Plotly chart shows non-empty traces for realistic windows, and provide opt-in diagnostics that explain why traces are missing.
|
||||
|
||||
**Why:** Users see an empty chart in some environments/windows. This could be caused by upstream data gaps, malformed coordinates, strict filtering in helpers, or unhandled exceptions in the plotting helper. We must gather evidence, fix the actual cause, and avoid changing production behavior unless debug is explicitly enabled.
|
||||
|
||||
|
||||
## Constraints
|
||||
|
||||
- Keep changes minimal and reversible; prefer instrumentation and small helper fixes over large refactors.
|
||||
- Diagnostics must be opt-in (EXPLORER_DEBUG_TRAJECTORIES env var and UI checkbox).
|
||||
- Helpers must be import-safe and pure so unit tests run without heavy GUI/DB dependencies.
|
||||
- Use project's environment management (uv) for local runs and CI — do not call pip directly.
|
||||
|
||||
|
||||
## Approach (chosen)
|
||||
|
||||
I recommend a **diagnostic-first** approach followed by targeted small fixes. Steps:
|
||||
|
||||
- Add a small, dedicated diagnostic writer script that emits a structured JSON diagnostics artifact for representative windows from data/motions.db.
|
||||
- Improve input validation and normalization in load_positions / compute_2d_axes (coerce numeric strings, treat 'nan'/'None' consistently, ignore out-of-range coords) so helpers are robust to malformed rows.
|
||||
- Keep current gates that avoid plotting when inputs are invalid, but record precise diagnostics into module-level _last_trajectories_diagnostics and the CLI JSON output.
|
||||
- Add unit tests for the normalization logic and for inspector behaviors; add a small integration diagnostic test that runs via uv and checks trace_count > 0 for a known-good sample window.
|
||||
|
||||
Reasoning: we already have instrumentation capturing stages (load_positions_empty, no_mp_positions, select_helper_exception, trace_count). Gathering structured evidence will let us pick a minimal fix (data normalization or filter tweak) without risky behaviour changes.
|
||||
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- Aggressive fallback rendering: render approximated centroids when traces are empty. Rejected because it may mask data quality issues and mislead users.
|
||||
- Upstream data repair: fix svd pipeline / DB rows before Explorer. Good long-term, but requires cross-team coordination and longer cycle — we should diagnose first.
|
||||
|
||||
|
||||
## Architecture
|
||||
|
||||
**High-level:** The Explorer plotting pipeline remains the same; we add a diagnostics writer and a small normalization layer.
|
||||
|
||||
- Data source: data/motions.db (svd_vectors and party maps)
|
||||
- Pipeline: get_uniform_dim_windows -> compute_2d_axes -> load_positions -> inspect_positions_for_issues -> compute_party_centroids -> select_trajectory_plot_data -> Plotly fig
|
||||
- Diagnostics: module-level _last_trajectories_diagnostics plus a CLI script that runs representative windows and writes JSON artifacts to thoughts/shared/diagnostics/YYYY-MM-DD-trajectories-diagnostics.json
|
||||
|
||||
|
||||
## Components and responsibilities
|
||||
|
||||
- Diagnostic CLI (scripts/save_trajectories_diagnostics.py):
|
||||
- Run a configurable sample of windows, call compute_2d_axes, load_positions, inspect_positions_for_issues, select_trajectory_plot_data.
|
||||
- Emit structured JSON with per-window diagnostics and aggregated summary.
|
||||
|
||||
- Normalization helpers (explorer_helpers.normalize_positions):
|
||||
- Coerce numeric strings to floats, coerce common null tokens to NaN, clamp improbable values, and return a normalized positions_by_window structure.
|
||||
- Pure, import-safe, and covered by unit tests.
|
||||
|
||||
- Instrumentation (explorer._last_trajectories_diagnostics):
|
||||
- Record stage, window id, counts (n_windows, n_entities per window), mp_positions_count, any helper exceptions/tracebacks, and sample rows.
|
||||
|
||||
- UI changes (pages/2_Explorer.py):
|
||||
- Add an opt-in debug checkbox that enables detailed diagnostics in the UI when checked (or when EXPLORER_DEBUG_TRAJECTORIES=1).
|
||||
- Do not change default plotting or filtering behavior when debug is disabled.
|
||||
|
||||
- Tests
|
||||
- Unit tests for normalization and inspector.
|
||||
- Diagnostic integration test run via uv (non-flaky, uses a small sample or DB fixture).
|
||||
|
||||
|
||||
## Data Flow
|
||||
|
||||
1. Caller requests trajectories tab (build_trajectories_tab).
|
||||
2. call get_uniform_dim_windows(DB) -> returns window descriptors.
|
||||
3. For each sampled window, compute_2d_axes(window) -> returns raw positions_by_window (possibly malformed).
|
||||
4. normalize_positions(positions_by_window) -> cleaned positions_by_window.
|
||||
5. inspect_positions_for_issues(positions_by_window) -> returns diagnostics (missing coords, string values, NaNs, zero-length paths).
|
||||
6. compute_party_centroids(positions_by_window) -> party centroids and mp_positions.
|
||||
7. select_trajectory_plot_data(centroids, mp_positions, options) -> returns fig, trace_count, banner_text. On exception capture diagnostics.
|
||||
8. If trace_count == 0 -> do not call st.plotly_chart; show friendly message and, if debug enabled, show the collected diagnostics and link to the saved JSON artifact.
|
||||
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Capture exceptions at helper boundaries and record to select_trajectory_plot_data._last_diagnostics and module _last_trajectories_diagnostics. Do not raise to Streamlit UI unless debug is enabled.
|
||||
- Normalize inputs proactively to reduce exception surface (avoid type errors from strings/None).
|
||||
- If a helper raises, return a safe empty fig and banner that suggests enabling diagnostics.
|
||||
- JSON diagnostics writer writes atomically (write to a .tmp file then rename) to avoid partial files being consumed.
|
||||
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
- Unit tests (fast, import-safe):
|
||||
- normalize_positions handles strings, 'nan', None, and clamps extremes.
|
||||
- inspect_positions_for_issues detects empty windows, NaNs-only windows, and malformed coordinate types.
|
||||
- select_trajectory_plot_data returns (fig, trace_count>0) for a known-good small sample and sets diagnostics correctly when trace_count==0.
|
||||
|
||||
- Integration tests (run under uv in CI or locally):
|
||||
- Diagnostic CLI can be executed via uv run and creates a JSON diagnostic artifact for a small sample; test asserts artifact exists and is valid JSON with expected fields.
|
||||
|
||||
- Manual verification:
|
||||
- Run EXPLORER_DEBUG_TRAJECTORIES=1 uv run python scripts/save_trajectories_diagnostics.py --db data/motions.db --out thoughts/shared/diagnostics/<date>.json
|
||||
- Open the Explorer locally and reproduce an empty-chart scenario; enable debug checkbox and view diagnostics.
|
||||
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. Do we prefer automatic normalization (silently fixing data) or conservative behavior (report and require upstream fix)? My recommendation: auto-normalize common, unambiguous issues (strings -> numbers, common null tokens) and surface anything ambiguous in diagnostics.
|
||||
2. Where should diagnostic artifacts live long-term? thoughts/shared/diagnostics is fine for short-term; consider a single diagnostics/ bucket for CI artifacts.
|
||||
3. Which windows should the diagnostics CLI sample by default? I propose sampling: 1) first 10 windows, 2) 10 windows evenly spaced, and 3) one window that previously produced empty result if known.
|
||||
|
||||
|
||||
I'm proceeding to create the design doc. Interrupt if you want changes.
|
||||
Reference in New Issue
Block a user