fmegahed commited on
Commit
65b5ca1
·
1 Parent(s): 916f17d

Fix flickering, flatten stats, add AI interpretation to all tabs

Browse files

- Add .streamlit/config.toml: headless mode, disable file watcher (stops spurious reruns in Docker)

- Guard session_state writes with _clean_key so objects never change between reruns

- Flatten Trend and Stationarity into Summary Statistics (no nested expander)

- Extract reusable _render_ai_interpretation helper

- Add AI Chart Interpretation to Panel and Spaghetti tabs

- Add Per-series Summary table to Spaghetti tab for consistency

Files changed (2) hide show
  1. .streamlit/config.toml +7 -0
  2. app.py +99 -57
.streamlit/config.toml ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ [server]
2
+ headless = true
3
+ fileWatcherType = "none"
4
+ runOnSave = false
5
+
6
+ [browser]
7
+ gatherUsageStats = false
app.py CHANGED
@@ -159,7 +159,7 @@ def _render_cleaning_report(report: CleaningReport) -> None:
159
 
160
 
161
  def _render_summary_stats(stats) -> None:
162
- """Render SummaryStats as metric cards + expander."""
163
  row1 = st.columns(4)
164
  row1[0].metric("Count", f"{stats.count:,}")
165
  row1[1].metric("Missing", f"{stats.missing_count} ({stats.missing_pct:.1f}%)")
@@ -172,32 +172,58 @@ def _render_summary_stats(stats) -> None:
172
  row2[2].metric("Median", f"{stats.median_val:,.2f}")
173
  row2[3].metric("75th %ile / Max", f"{stats.p75:,.2f} / {stats.max_val:,.2f}")
174
 
175
- with st.expander("Trend & Stationarity"):
176
- tc1, tc2 = st.columns(2)
177
- tc1.metric(
178
- "Trend slope (per period)",
179
- f"{stats.trend_slope:,.4f}" if pd.notna(stats.trend_slope) else "N/A",
180
- help="Slope from OLS on a numeric index.",
181
- )
182
- tc2.metric(
183
- "Trend p-value",
184
- f"{stats.trend_pvalue:.4f}" if pd.notna(stats.trend_pvalue) else "N/A",
185
- )
186
- ac1, ac2 = st.columns(2)
187
- ac1.metric(
188
- "ADF statistic",
189
- f"{stats.adf_statistic:.4f}" if pd.notna(stats.adf_statistic) else "N/A",
190
- help="Augmented Dickey-Fuller test statistic.",
191
- )
192
- ac2.metric(
193
- "ADF p-value",
194
- f"{stats.adf_pvalue:.4f}" if pd.notna(stats.adf_pvalue) else "N/A",
195
- help="p < 0.05 suggests the series is stationary.",
196
- )
 
 
 
 
 
 
 
 
197
  st.caption(
198
- f"Date range: {stats.date_start.date()} to {stats.date_end.date()} "
199
- f"({stats.date_span_days:,} days)"
200
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
 
202
 
203
  # ---------------------------------------------------------------------------
@@ -217,7 +243,7 @@ style_dict = get_miami_mpl_style()
217
  for key in [
218
  "raw_df", "cleaned_df", "cleaning_report", "freq_info",
219
  "date_col", "y_cols", "qc", "qc_hash",
220
- "_upload_id", "_upload_delim", "cleaned_df_hash",
221
  ]:
222
  if key not in st.session_state:
223
  st.session_state[key] = None
@@ -306,16 +332,21 @@ with st.sidebar:
306
  key="sidebar_missing_action",
307
  )
308
 
309
- # Clean
310
  if y_cols:
311
- cleaned_df, report, freq_info = _clean_pipeline(
312
- _df_hash(raw_df), raw_df, date_col, tuple(y_cols),
313
- dup_action, missing_action,
314
- )
315
- st.session_state.cleaned_df = cleaned_df
316
- st.session_state.cleaning_report = report
317
- st.session_state.freq_info = freq_info
 
 
 
 
318
 
 
319
  st.caption(f"Frequency: **{freq_info.label}** "
320
  f"({'regular' if freq_info.is_regular else 'irregular'})")
321
 
@@ -602,29 +633,9 @@ with tab_single:
602
  _render_summary_stats(stats)
603
 
604
  # ---- AI Interpretation ------------------------------------------------
605
- with st.expander("AI Chart Interpretation", expanded=False):
606
- st.caption(
607
- "The chart image (PNG) is sent to OpenAI for interpretation. "
608
- "Do not include sensitive data in your charts."
609
- )
610
- if not check_api_key_available():
611
- st.warning("Set `OPENAI_API_KEY` to enable AI interpretation.")
612
- elif fig is not None:
613
- if st.button("Interpret Chart with AI", key="interpret_a"):
614
- with st.spinner("Analyzing chart..."):
615
- png = fig_to_png_bytes(fig)
616
- date_range_str = (
617
- f"{df_plot[date_col].min().date()} to "
618
- f"{df_plot[date_col].max().date()}"
619
- )
620
- metadata = {
621
- "chart_type": chart_type,
622
- "frequency_label": freq_info.label if freq_info else "Unknown",
623
- "date_range": date_range_str,
624
- "y_column": active_y,
625
- }
626
- interp = interpret_chart(png, metadata)
627
- render_interpretation(interp)
628
 
629
  # ===================================================================
630
  # Tab B — Few Series (Panel)
@@ -654,6 +665,7 @@ with tab_few:
654
  palette_name_b = st.selectbox("Color palette", _PALETTE_NAMES, key="pal_b")
655
  palette_b = get_palette_colors(palette_name_b, len(panel_cols))
656
 
 
657
  try:
658
  fig_panel = plot_panel(
659
  working_df, date_col, panel_cols,
@@ -684,6 +696,12 @@ with tab_few:
684
  use_container_width=True,
685
  )
686
 
 
 
 
 
 
 
687
  # ===================================================================
688
  # Tab C — Many Series (Spaghetti)
689
  # ===================================================================
@@ -720,6 +738,7 @@ with tab_many:
720
  palette_name_c = st.selectbox("Color palette", _PALETTE_NAMES, key="pal_c")
721
  palette_c = get_palette_colors(palette_name_c, len(spag_cols))
722
 
 
723
  try:
724
  fig_spag = plot_spaghetti(
725
  working_df, date_col, spag_cols,
@@ -734,3 +753,26 @@ with tab_many:
734
  st.pyplot(fig_spag, use_container_width=True)
735
  except Exception as exc:
736
  st.error(f"Spaghetti chart error: {exc}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
 
160
 
161
  def _render_summary_stats(stats) -> None:
162
+ """Render SummaryStats as metric cards (flat, no nesting)."""
163
  row1 = st.columns(4)
164
  row1[0].metric("Count", f"{stats.count:,}")
165
  row1[1].metric("Missing", f"{stats.missing_count} ({stats.missing_pct:.1f}%)")
 
172
  row2[2].metric("Median", f"{stats.median_val:,.2f}")
173
  row2[3].metric("75th %ile / Max", f"{stats.p75:,.2f} / {stats.max_val:,.2f}")
174
 
175
+ row3 = st.columns(4)
176
+ row3[0].metric(
177
+ "Trend slope",
178
+ f"{stats.trend_slope:,.4f}" if pd.notna(stats.trend_slope) else "N/A",
179
+ help="Slope from OLS on a numeric index.",
180
+ )
181
+ row3[1].metric(
182
+ "Trend p-value",
183
+ f"{stats.trend_pvalue:.4f}" if pd.notna(stats.trend_pvalue) else "N/A",
184
+ )
185
+ row3[2].metric(
186
+ "ADF statistic",
187
+ f"{stats.adf_statistic:.4f}" if pd.notna(stats.adf_statistic) else "N/A",
188
+ help="Augmented Dickey-Fuller test statistic.",
189
+ )
190
+ row3[3].metric(
191
+ "ADF p-value",
192
+ f"{stats.adf_pvalue:.4f}" if pd.notna(stats.adf_pvalue) else "N/A",
193
+ help="p < 0.05 suggests the series is stationary.",
194
+ )
195
+ st.caption(
196
+ f"Date range: {stats.date_start.date()} to {stats.date_end.date()} "
197
+ f"({stats.date_span_days:,} days)"
198
+ )
199
+
200
+
201
+ def _render_ai_interpretation(fig, chart_type_label, freq_info, df_plot,
202
+ date_col, y_label, button_key):
203
+ """Reusable AI Chart Interpretation block for any tab."""
204
+ with st.expander("AI Chart Interpretation", expanded=False):
205
  st.caption(
206
+ "The chart image (PNG) is sent to OpenAI for interpretation. "
207
+ "Do not include sensitive data in your charts."
208
  )
209
+ if not check_api_key_available():
210
+ st.warning("Set `OPENAI_API_KEY` to enable AI interpretation.")
211
+ elif fig is not None:
212
+ if st.button("Interpret Chart with AI", key=button_key):
213
+ with st.spinner("Analyzing chart..."):
214
+ png = fig_to_png_bytes(fig)
215
+ date_range_str = (
216
+ f"{df_plot[date_col].min().date()} to "
217
+ f"{df_plot[date_col].max().date()}"
218
+ )
219
+ metadata = {
220
+ "chart_type": chart_type_label,
221
+ "frequency_label": freq_info.label if freq_info else "Unknown",
222
+ "date_range": date_range_str,
223
+ "y_column": y_label,
224
+ }
225
+ interp = interpret_chart(png, metadata)
226
+ render_interpretation(interp)
227
 
228
 
229
  # ---------------------------------------------------------------------------
 
243
  for key in [
244
  "raw_df", "cleaned_df", "cleaning_report", "freq_info",
245
  "date_col", "y_cols", "qc", "qc_hash",
246
+ "_upload_id", "_upload_delim", "_clean_key",
247
  ]:
248
  if key not in st.session_state:
249
  st.session_state[key] = None
 
332
  key="sidebar_missing_action",
333
  )
334
 
335
+ # Clean — only recompute and write session_state when inputs change
336
  if y_cols:
337
+ _key = (date_col, tuple(y_cols), dup_action, missing_action,
338
+ st.session_state._upload_id)
339
+ if st.session_state.get("_clean_key") != _key:
340
+ cleaned_df, report, freq_info = _clean_pipeline(
341
+ _df_hash(raw_df), raw_df, date_col, tuple(y_cols),
342
+ dup_action, missing_action,
343
+ )
344
+ st.session_state.cleaned_df = cleaned_df
345
+ st.session_state.cleaning_report = report
346
+ st.session_state.freq_info = freq_info
347
+ st.session_state._clean_key = _key
348
 
349
+ freq_info = st.session_state.freq_info
350
  st.caption(f"Frequency: **{freq_info.label}** "
351
  f"({'regular' if freq_info.is_regular else 'irregular'})")
352
 
 
633
  _render_summary_stats(stats)
634
 
635
  # ---- AI Interpretation ------------------------------------------------
636
+ _render_ai_interpretation(
637
+ fig, chart_type, freq_info, df_plot, date_col, active_y, "interpret_a",
638
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
639
 
640
  # ===================================================================
641
  # Tab B — Few Series (Panel)
 
665
  palette_name_b = st.selectbox("Color palette", _PALETTE_NAMES, key="pal_b")
666
  palette_b = get_palette_colors(palette_name_b, len(panel_cols))
667
 
668
+ fig_panel = None
669
  try:
670
  fig_panel = plot_panel(
671
  working_df, date_col, panel_cols,
 
696
  use_container_width=True,
697
  )
698
 
699
+ # AI Interpretation
700
+ _render_ai_interpretation(
701
+ fig_panel, f"Panel ({panel_chart})", freq_info,
702
+ working_df, date_col, ", ".join(panel_cols), "interpret_b",
703
+ )
704
+
705
  # ===================================================================
706
  # Tab C — Many Series (Spaghetti)
707
  # ===================================================================
 
738
  palette_name_c = st.selectbox("Color palette", _PALETTE_NAMES, key="pal_c")
739
  palette_c = get_palette_colors(palette_name_c, len(spag_cols))
740
 
741
+ fig_spag = None
742
  try:
743
  fig_spag = plot_spaghetti(
744
  working_df, date_col, spag_cols,
 
753
  st.pyplot(fig_spag, use_container_width=True)
754
  except Exception as exc:
755
  st.error(f"Spaghetti chart error: {exc}")
756
+
757
+ # Per-series summary table
758
+ with st.expander("Per-series Summary", expanded=False):
759
+ spag_summary = compute_multi_series_summary(
760
+ working_df, date_col, spag_cols,
761
+ )
762
+ st.dataframe(
763
+ spag_summary.style.format({
764
+ "mean": "{:,.2f}",
765
+ "std": "{:,.2f}",
766
+ "min": "{:,.2f}",
767
+ "max": "{:,.2f}",
768
+ "trend_slope": "{:,.4f}",
769
+ "adf_pvalue": "{:.4f}",
770
+ }),
771
+ use_container_width=True,
772
+ )
773
+
774
+ # AI Interpretation
775
+ _render_ai_interpretation(
776
+ fig_spag, "Spaghetti Plot", freq_info,
777
+ working_df, date_col, ", ".join(spag_cols), "interpret_c",
778
+ )