Danialebrat commited on
Commit
a28ed6e
·
1 Parent(s): 6e08a38

Updating Historical Analytics

Browse files
visualization/pages/5_Historical_Analytics.py CHANGED
@@ -1,6 +1,7 @@
1
  """
2
- Page 5: Historical Analytics
3
- View performance metrics and insights from all previous experiments stored in Snowflake.
 
4
  """
5
 
6
  import streamlit as st
@@ -25,6 +26,7 @@ else:
25
  try:
26
  import plotly.express as px
27
  import plotly.graph_objects as go
 
28
  except ModuleNotFoundError:
29
  st.error("""
30
  ⚠️ **Missing Dependency: plotly**
@@ -68,6 +70,25 @@ def create_snowflake_session():
68
  st.error(f"Traceback: {traceback.format_exc()}")
69
  return None
70
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  # Page configuration
72
  st.set_page_config(
73
  page_title="Historical Analytics",
@@ -91,12 +112,14 @@ theme = get_brand_theme(brand)
91
 
92
  # Page Header
93
  emoji = get_brand_emoji(brand)
94
- st.title(f"📚 Historical Analytics - {emoji} {brand.title()}")
95
- st.markdown("**View performance metrics from all previous experiments stored in Snowflake**")
96
 
97
  st.markdown("---")
98
 
99
- # Load Data Button
 
 
100
  col1, col2, col3 = st.columns([1, 2, 1])
101
  with col2:
102
  if st.button("📊 Load Historical Data from Snowflake", use_container_width=True, type="primary"):
@@ -108,21 +131,28 @@ with col2:
108
  try:
109
  db_manager = UIDatabaseManager(session)
110
 
111
- # Load experiment summary (joins metadata + feedback)
 
 
 
112
  experiments_df = db_manager.get_experiment_summary(brand=brand)
113
 
114
  # Store in session_state
 
115
  st.session_state['historical_experiments'] = experiments_df
116
  st.session_state['historical_data_loaded'] = True
 
117
 
118
  # Close session
119
  db_manager.close()
120
 
121
- st.success(f"✅ Loaded {len(experiments_df)} experiments from Snowflake!")
122
  st.rerun()
123
 
124
  except Exception as e:
125
  st.error(f"Error loading historical data: {e}")
 
 
126
  if session:
127
  session.close()
128
  else:
@@ -132,250 +162,882 @@ st.markdown("---")
132
 
133
  # Check if data is loaded
134
  if not st.session_state.get('historical_data_loaded', False):
135
- st.info("👆 Click the button above to load historical experiment data from Snowflake")
 
 
 
 
 
 
 
 
136
  st.stop()
137
 
138
  # Get loaded data
 
139
  experiments_df = st.session_state.get('historical_experiments', pd.DataFrame())
140
 
141
- if experiments_df is None or len(experiments_df) == 0:
142
- st.warning("⚠️ No historical experiments found in Snowflake. Generate and store experiments first using Campaign Builder.")
143
  st.stop()
144
 
145
- # Sidebar - Filters
 
 
146
  with st.sidebar:
147
- st.header("🔍 Filters")
148
-
149
- # Date range filter
150
- st.subheader("Date Range")
151
- show_all = st.checkbox("Show All Experiments", value=True)
152
-
153
- start_date = None
154
- end_date = None
155
-
156
- if not show_all:
157
- # Use actual timestamps from data
158
- if 'start_time' in experiments_df.columns:
159
- experiments_df['start_time'] = pd.to_datetime(experiments_df['start_time'])
160
- min_date = experiments_df['start_time'].min().date()
161
- max_date = experiments_df['start_time'].max().date()
162
-
163
- start_date = st.date_input("Start Date", min_date)
164
- end_date = st.date_input("End Date", max_date)
165
 
166
- # Filter by date range
167
- experiments_df = experiments_df[
168
- (experiments_df['start_time'].dt.date >= start_date) &
169
- (experiments_df['start_time'].dt.date <= end_date)
170
- ]
171
 
172
  st.markdown("---")
173
 
174
  # Refresh button
175
  if st.button("🔄 Reload Data", use_container_width=True):
176
  st.session_state['historical_data_loaded'] = False
 
177
  st.rerun()
178
 
 
 
 
179
  # ============================================================================
180
- # OVERVIEW STATISTICS
181
  # ============================================================================
182
- st.header("📊 Overview Statistics")
183
 
184
- col1, col2, col3, col4 = st.columns(4)
 
 
 
 
185
 
186
- with col1:
187
- st.metric("Total Experiments", len(experiments_df))
188
-
189
- with col2:
190
- total_messages = experiments_df['total_messages'].sum() if 'total_messages' in experiments_df.columns else 0
191
- st.metric("Total Messages", f"{total_messages:,}")
192
 
193
- with col3:
194
- total_rejects = experiments_df['total_rejects'].sum() if 'total_rejects' in experiments_df.columns else 0
195
- st.metric("Total Rejections", f"{total_rejects:,}")
196
 
197
- with col4:
198
- avg_rejection_rate = experiments_df['rejection_rate'].mean() if 'rejection_rate' in experiments_df.columns else 0
199
- st.metric("Avg Rejection Rate", f"{avg_rejection_rate:.1f}%")
200
 
201
- st.markdown("---")
 
 
202
 
203
- # ============================================================================
204
- # EXPERIMENTS SUMMARY TABLE
205
- # ============================================================================
206
- st.header("📈 All Experiments")
207
-
208
- # Display summary table
209
- display_df = experiments_df.copy()
210
-
211
- # Format columns for display
212
- if 'start_time' in display_df.columns:
213
- display_df['start_time'] = pd.to_datetime(display_df['start_time']).dt.strftime('%Y-%m-%d %H:%M')
214
-
215
- # Select columns to display
216
- display_columns = ['campaign_name', 'config_name', 'start_time', 'total_messages',
217
- 'total_users', 'total_stages', 'total_rejects', 'rejection_rate']
218
-
219
- # Only show columns that exist
220
- display_columns = [col for col in display_columns if col in display_df.columns]
221
-
222
- st.dataframe(
223
- display_df[display_columns],
224
- use_container_width=True,
225
- hide_index=True,
226
- column_config={
227
- "campaign_name": "Campaign",
228
- "config_name": "Config",
229
- "start_time": "Date",
230
- "total_messages": "Messages",
231
- "total_users": "Users",
232
- "total_stages": "Stages",
233
- "total_rejects": "Rejects",
234
- "rejection_rate": st.column_config.NumberColumn(
235
- "Reject Rate (%)",
236
- format="%.1f%%"
237
- )
238
- }
239
- )
240
 
241
- st.markdown("---")
242
 
243
- # ============================================================================
244
- # REJECTION RATE TREND OVER TIME
245
- # ============================================================================
246
- st.header("📉 Rejection Rate Trend Over Time")
247
-
248
- if 'start_time' in experiments_df.columns and 'rejection_rate' in experiments_df.columns:
249
- # Sort by time
250
- trend_df = experiments_df.copy()
251
- trend_df['start_time'] = pd.to_datetime(trend_df['start_time'])
252
- trend_df = trend_df.sort_values('start_time')
253
-
254
- fig = go.Figure()
255
-
256
- fig.add_trace(go.Scatter(
257
- x=trend_df['start_time'],
258
- y=trend_df['rejection_rate'],
259
- mode='lines+markers',
260
- name='Rejection Rate',
261
- line=dict(color=theme['accent'], width=3),
262
- marker=dict(size=10),
263
- text=trend_df['campaign_name'],
264
- hovertemplate='<b>%{text}</b><br>Date: %{x}<br>Rejection Rate: %{y:.1f}%<extra></extra>'
265
- ))
266
-
267
- fig.update_layout(
268
- xaxis_title="Experiment Date",
269
- yaxis_title="Rejection Rate (%)",
270
- template="plotly_dark",
271
- paper_bgcolor='rgba(0,0,0,0)',
272
- plot_bgcolor='rgba(0,0,0,0)',
273
- hovermode='closest'
274
  )
275
 
276
- st.plotly_chart(fig, use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
  else:
278
- st.info("Not enough data for trend visualization")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
 
280
- st.markdown("---")
 
281
 
282
- # ============================================================================
283
- # PERFORMANCE COMPARISON
284
- # ============================================================================
285
- st.header("📊 Performance Comparison")
286
 
287
- if len(experiments_df) > 1 and 'config_name' in experiments_df.columns:
288
- # Group by config and compare
289
- config_summary = experiments_df.groupby('config_name').agg({
290
- 'rejection_rate': 'mean',
291
- 'total_messages': 'sum',
292
- 'total_rejects': 'sum'
293
- }).reset_index()
294
 
295
- config_summary = config_summary.sort_values('rejection_rate')
 
296
 
297
- col1, col2 = st.columns(2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
298
 
299
  with col1:
300
- # Bar chart comparing configs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
  fig = px.bar(
302
- config_summary,
303
- x='config_name',
304
- y='rejection_rate',
305
- title="Average Rejection Rate by Configuration",
306
- color='rejection_rate',
307
- color_continuous_scale='RdYlGn_r',
308
- text='rejection_rate'
309
  )
310
-
311
- fig.update_traces(texttemplate='%{text:.1f}%', textposition='outside')
312
  fig.update_layout(
313
  template="plotly_dark",
314
  paper_bgcolor='rgba(0,0,0,0)',
315
  plot_bgcolor='rgba(0,0,0,0)',
316
- xaxis_title="Configuration",
317
- yaxis_title="Rejection Rate (%)"
 
 
318
  )
319
-
320
  st.plotly_chart(fig, use_container_width=True)
321
 
322
- with col2:
323
- # Summary metrics
324
- best_config = config_summary.iloc[0]
325
- worst_config = config_summary.iloc[-1]
326
-
327
- st.markdown("### 🏆 Best Performing Config")
328
- st.metric(
329
- best_config['config_name'],
330
- f"{best_config['rejection_rate']:.1f}%",
331
- delta=f"{worst_config['rejection_rate'] - best_config['rejection_rate']:.1f}% better than worst"
332
- )
333
 
334
- st.markdown("### 📊 Config Comparison")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
  st.dataframe(
336
- config_summary,
337
  use_container_width=True,
338
  hide_index=True,
339
  column_config={
340
- "config_name": "Configuration",
341
- "rejection_rate": st.column_config.NumberColumn(
342
- "Avg Rejection Rate (%)",
343
- format="%.1f%%"
344
- ),
345
- "total_messages": st.column_config.NumberColumn(
346
- "Total Messages",
347
- format="%d"
348
- ),
349
- "total_rejects": st.column_config.NumberColumn(
350
- "Total Rejects",
351
- format="%d"
352
- )
353
  }
354
  )
355
- else:
356
- st.info("Need at least 2 experiments for performance comparison")
357
 
358
- st.markdown("---")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
359
 
360
- # Export options
361
- st.header("💾 Export Historical Data")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
 
363
- col1, col2 = st.columns(2)
364
 
365
  with col1:
 
 
 
 
 
 
 
 
 
 
 
366
  if st.button("📥 Export Experiments Summary", use_container_width=True):
367
  csv = experiments_df.to_csv(index=False, encoding='utf-8')
368
  st.download_button(
369
- label="⬇️ Download Summary CSV",
370
  data=csv,
371
  file_name=f"{brand}_experiments_summary_{datetime.now().strftime('%Y%m%d')}.csv",
372
  mime="text/csv",
373
  use_container_width=True
374
  )
375
 
376
- with col2:
377
- st.button("📥 Export Detailed Feedback", use_container_width=True, disabled=True)
378
- st.caption("💡 Detailed feedback export coming soon. Use Snowflake queries to access feedback data directly.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
 
380
  st.markdown("---")
381
- st.markdown("**💡 Tip:** Use historical analytics to track improvements over time and identify which configurations perform best!")
 
1
  """
2
+ Page 5: Historical Analytics (Enhanced)
3
+ View comprehensive performance metrics and insights from all previous experiments stored in Snowflake.
4
+ Includes campaign-level analysis, model performance, and detailed rejection analytics.
5
  """
6
 
7
  import streamlit as st
 
26
  try:
27
  import plotly.express as px
28
  import plotly.graph_objects as go
29
+ from plotly.subplots import make_subplots
30
  except ModuleNotFoundError:
31
  st.error("""
32
  ⚠️ **Missing Dependency: plotly**
 
70
  st.error(f"Traceback: {traceback.format_exc()}")
71
  return None
72
 
73
+ # Rejection reason mapping
74
+ REJECTION_REASON_LABELS = {
75
+ "poor_header": "Poor Header",
76
+ "poor_body": "Poor Body/Content",
77
+ "grammar_issues": "Grammar Issues",
78
+ "emoji_problems": "Emoji Problems",
79
+ "recommendation_issues": "Recommendation Issues",
80
+ "wrong_information": "Wrong/Inaccurate Information",
81
+ "tone_issues": "Tone Issues",
82
+ "similarity": "Similar To Previous",
83
+ "other": "Other"
84
+ }
85
+
86
+ def get_rejection_label(reason_key):
87
+ """Get human-readable label for rejection reason."""
88
+ if reason_key is None:
89
+ return "Unknown"
90
+ return REJECTION_REASON_LABELS.get(reason_key, reason_key)
91
+
92
  # Page configuration
93
  st.set_page_config(
94
  page_title="Historical Analytics",
 
112
 
113
  # Page Header
114
  emoji = get_brand_emoji(brand)
115
+ st.title(f"📚 Enhanced Historical Analytics - {emoji} {brand.title()}")
116
+ st.markdown("**Comprehensive insights across campaigns, models, stages, and rejection patterns**")
117
 
118
  st.markdown("---")
119
 
120
+ # ============================================================================
121
+ # LOAD DATA SECTION
122
+ # ============================================================================
123
  col1, col2, col3 = st.columns([1, 2, 1])
124
  with col2:
125
  if st.button("📊 Load Historical Data from Snowflake", use_container_width=True, type="primary"):
 
131
  try:
132
  db_manager = UIDatabaseManager(session)
133
 
134
+ # Load campaigns summary (new enhanced view)
135
+ campaigns_df = db_manager.get_all_campaigns_summary(brand=brand)
136
+
137
+ # Load experiment summary (original view for backward compatibility)
138
  experiments_df = db_manager.get_experiment_summary(brand=brand)
139
 
140
  # Store in session_state
141
+ st.session_state['historical_campaigns'] = campaigns_df
142
  st.session_state['historical_experiments'] = experiments_df
143
  st.session_state['historical_data_loaded'] = True
144
+ st.session_state['selected_campaign'] = None # Reset selection
145
 
146
  # Close session
147
  db_manager.close()
148
 
149
+ st.success(f"✅ Loaded {len(campaigns_df)} campaigns and {len(experiments_df)} experiments from Snowflake!")
150
  st.rerun()
151
 
152
  except Exception as e:
153
  st.error(f"Error loading historical data: {e}")
154
+ import traceback
155
+ st.error(f"Traceback: {traceback.format_exc()}")
156
  if session:
157
  session.close()
158
  else:
 
162
 
163
  # Check if data is loaded
164
  if not st.session_state.get('historical_data_loaded', False):
165
+ st.info("👆 Click the button above to load historical data from Snowflake")
166
+ st.markdown("""
167
+ ### What's in Historical Analytics:
168
+ - **Campaign-Level Analysis**: View aggregated metrics for all experiments in a campaign
169
+ - **Model Performance**: Compare LLM models with detailed rejection metrics
170
+ - **Stage Analysis**: Identify which stages have the highest rejection rates
171
+ - **Rejection Reasons**: Interactive breakdown of why messages get rejected
172
+ - **Filtering**: Drill down by campaign, stage, and model
173
+ """)
174
  st.stop()
175
 
176
  # Get loaded data
177
+ campaigns_df = st.session_state.get('historical_campaigns', pd.DataFrame())
178
  experiments_df = st.session_state.get('historical_experiments', pd.DataFrame())
179
 
180
+ if campaigns_df is None or len(campaigns_df) == 0:
181
+ st.warning("⚠️ No historical campaigns found in Snowflake. Generate and store experiments first using Campaign Builder.")
182
  st.stop()
183
 
184
+ # ============================================================================
185
+ # SIDEBAR - CAMPAIGN SELECTOR
186
+ # ============================================================================
187
  with st.sidebar:
188
+ st.header("🎯 Campaign Selector")
189
+
190
+ # Campaign selection
191
+ campaign_options = ["-- All Campaigns Overview --"] + list(campaigns_df['campaign_name'].unique())
192
+ selected_campaign = st.selectbox(
193
+ "Select Campaign",
194
+ campaign_options,
195
+ index=0,
196
+ key='campaign_selector'
197
+ )
 
 
 
 
 
 
 
 
198
 
199
+ # Store selected campaign
200
+ if selected_campaign != "-- All Campaigns Overview --":
201
+ st.session_state['selected_campaign'] = selected_campaign
202
+ else:
203
+ st.session_state['selected_campaign'] = None
204
 
205
  st.markdown("---")
206
 
207
  # Refresh button
208
  if st.button("🔄 Reload Data", use_container_width=True):
209
  st.session_state['historical_data_loaded'] = False
210
+ st.session_state['selected_campaign'] = None
211
  st.rerun()
212
 
213
+ st.markdown("---")
214
+ st.markdown("**💡 Tip:** Select a campaign to see detailed analytics including model performance and rejection patterns.")
215
+
216
  # ============================================================================
217
+ # MAIN CONTENT - CONDITIONAL BASED ON SELECTION
218
  # ============================================================================
 
219
 
220
+ if st.session_state.get('selected_campaign') is None:
221
+ # ========================================================================
222
+ # ALL CAMPAIGNS OVERVIEW
223
+ # ========================================================================
224
+ st.header("🌐 All Campaigns Overview")
225
 
226
+ # Overview metrics
227
+ col1, col2, col3, col4 = st.columns(4)
 
 
 
 
228
 
229
+ with col1:
230
+ st.metric("Total Campaigns", len(campaigns_df))
 
231
 
232
+ with col2:
233
+ total_experiments = campaigns_df['total_experiments'].sum() if 'total_experiments' in campaigns_df.columns else 0
234
+ st.metric("Total Experiments", f"{int(total_experiments):,}")
235
 
236
+ with col3:
237
+ total_messages = campaigns_df['total_messages'].sum() if 'total_messages' in campaigns_df.columns else 0
238
+ st.metric("Total Messages", f"{int(total_messages):,}")
239
 
240
+ with col4:
241
+ total_rejections = campaigns_df['total_rejections'].sum() if 'total_rejections' in campaigns_df.columns else 0
242
+ st.metric("Total Rejections", f"{int(total_rejections):,}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
 
244
+ st.markdown("---")
245
 
246
+ # Campaigns summary table
247
+ st.subheader("📊 Campaigns Summary")
248
+
249
+ display_df = campaigns_df.copy()
250
+
251
+ # Format dates
252
+ if 'first_experiment' in display_df.columns:
253
+ display_df['first_experiment'] = pd.to_datetime(display_df['first_experiment']).dt.strftime('%Y-%m-%d')
254
+ if 'last_experiment' in display_df.columns:
255
+ display_df['last_experiment'] = pd.to_datetime(display_df['last_experiment']).dt.strftime('%Y-%m-%d')
256
+
257
+ # Select and order columns for display
258
+ display_columns = ['campaign_name', 'total_experiments', 'total_messages', 'total_rejections',
259
+ 'rejection_rate', 'models_used', 'first_experiment', 'last_experiment']
260
+ display_columns = [col for col in display_columns if col in display_df.columns]
261
+
262
+ st.dataframe(
263
+ display_df[display_columns],
264
+ use_container_width=True,
265
+ hide_index=True,
266
+ column_config={
267
+ "campaign_name": "Campaign Name",
268
+ "total_experiments": st.column_config.NumberColumn("Experiments", format="%d"),
269
+ "total_messages": st.column_config.NumberColumn("Messages", format="%d"),
270
+ "total_rejections": st.column_config.NumberColumn("Rejections", format="%d"),
271
+ "rejection_rate": st.column_config.NumberColumn("Rejection Rate", format="%.1f%%"),
272
+ "models_used": "Models Used",
273
+ "first_experiment": "First Run",
274
+ "last_experiment": "Last Run"
275
+ }
 
276
  )
277
 
278
+ st.markdown("---")
279
+
280
+ # Campaign comparison charts
281
+ col1, col2 = st.columns(2)
282
+
283
+ with col1:
284
+ st.subheader("📉 Rejection Rate by Campaign")
285
+ if 'rejection_rate' in campaigns_df.columns and len(campaigns_df) > 0:
286
+ fig = px.bar(
287
+ campaigns_df.sort_values('rejection_rate', ascending=False),
288
+ x='campaign_name',
289
+ y='rejection_rate',
290
+ color='rejection_rate',
291
+ color_continuous_scale='RdYlGn_r',
292
+ text='rejection_rate'
293
+ )
294
+ fig.update_traces(texttemplate='%{text:.1f}%', textposition='outside')
295
+ fig.update_layout(
296
+ template="plotly_dark",
297
+ paper_bgcolor='rgba(0,0,0,0)',
298
+ plot_bgcolor='rgba(0,0,0,0)',
299
+ xaxis_title="Campaign",
300
+ yaxis_title="Rejection Rate (%)",
301
+ showlegend=False
302
+ )
303
+ st.plotly_chart(fig, use_container_width=True)
304
+ else:
305
+ st.info("No rejection data available")
306
+
307
+ with col2:
308
+ st.subheader("📊 Messages vs Rejections")
309
+ if 'total_messages' in campaigns_df.columns and 'total_rejections' in campaigns_df.columns:
310
+ fig = go.Figure()
311
+ fig.add_trace(go.Bar(
312
+ x=campaigns_df['campaign_name'],
313
+ y=campaigns_df['total_messages'],
314
+ name='Total Messages',
315
+ marker_color=theme['primary']
316
+ ))
317
+ fig.add_trace(go.Bar(
318
+ x=campaigns_df['campaign_name'],
319
+ y=campaigns_df['total_rejections'],
320
+ name='Rejections',
321
+ marker_color='#ff4444'
322
+ ))
323
+ fig.update_layout(
324
+ template="plotly_dark",
325
+ paper_bgcolor='rgba(0,0,0,0)',
326
+ plot_bgcolor='rgba(0,0,0,0)',
327
+ xaxis_title="Campaign",
328
+ yaxis_title="Count",
329
+ barmode='group'
330
+ )
331
+ st.plotly_chart(fig, use_container_width=True)
332
+ else:
333
+ st.info("No message data available")
334
+
335
  else:
336
+ # ========================================================================
337
+ # SELECTED CAMPAIGN DETAILED ANALYSIS
338
+ # ========================================================================
339
+ selected_campaign = st.session_state['selected_campaign']
340
+
341
+ st.header(f"🎯 Campaign Analysis: {selected_campaign}")
342
+
343
+ # Load detailed campaign analysis
344
+ with st.spinner(f"Loading detailed analytics for '{selected_campaign}'..."):
345
+ session = create_snowflake_session()
346
+ if session:
347
+ try:
348
+ db_manager = UIDatabaseManager(session)
349
+
350
+ # Get detailed feedback analysis
351
+ analysis = db_manager.get_campaign_feedback_analysis(selected_campaign, brand=brand)
352
+
353
+ # Store in session state
354
+ st.session_state['campaign_analysis'] = analysis
355
+
356
+ db_manager.close()
357
+ except Exception as e:
358
+ st.error(f"Error loading campaign analysis: {e}")
359
+ import traceback
360
+ st.error(f"Traceback: {traceback.format_exc()}")
361
+ if session:
362
+ session.close()
363
+ st.stop()
364
+ else:
365
+ st.error("Failed to connect to Snowflake")
366
+ st.stop()
367
+
368
+ analysis = st.session_state.get('campaign_analysis', {})
369
+
370
+ # Get campaign summary
371
+ campaign_summary = campaigns_df[campaigns_df['campaign_name'] == selected_campaign].iloc[0]
372
+
373
+ # Campaign overview metrics
374
+ col1, col2, col3, col4, col5 = st.columns(5)
375
 
376
+ with col1:
377
+ st.metric("Experiments", int(campaign_summary.get('total_experiments', 0)))
378
 
379
+ with col2:
380
+ st.metric("Total Messages", f"{int(campaign_summary.get('total_messages', 0)):,}")
 
 
381
 
382
+ with col3:
383
+ st.metric("Total Rejections", f"{int(campaign_summary.get('total_rejections', 0)):,}")
 
 
 
 
 
384
 
385
+ with col4:
386
+ st.metric("Rejection Rate", f"{campaign_summary.get('rejection_rate', 0):.1f}%")
387
 
388
+ with col5:
389
+ st.metric("Models Used", int(len(str(campaign_summary.get('models_used', '')).split(', '))))
390
+
391
+ st.markdown("---")
392
+
393
+ # ========================================================================
394
+ # OVERALL FEEDBACK ANALYSIS
395
+ # ========================================================================
396
+ st.header("📊 Overall Feedback Analysis")
397
+
398
+ # Get data for overall analysis
399
+ reasons_df = analysis.get('rejection_reasons', pd.DataFrame())
400
+ stage_df = analysis.get('by_stage', pd.DataFrame())
401
+ model_df = analysis.get('by_model', pd.DataFrame())
402
+
403
+ col1, col2, col3 = st.columns(3)
404
 
405
  with col1:
406
+ st.subheader("📋 Top Rejection Reasons")
407
+ if not reasons_df.empty:
408
+ top_reasons = reasons_df.nlargest(5, 'count')
409
+ for idx, row in top_reasons.iterrows():
410
+ reason_label = get_rejection_label(row['rejection_reason'])
411
+ st.metric(reason_label, f"{int(row['count']):,}",
412
+ help=f"Affected {int(row.get('experiments_affected', 0))} experiments")
413
+ else:
414
+ st.info("No rejection data available")
415
+
416
+ with col2:
417
+ st.subheader("⚠️ Highest Rejection Rate Stages")
418
+ if not stage_df.empty and 'rejection_rate' in stage_df.columns:
419
+ top_stages = stage_df.nlargest(5, 'rejection_rate')
420
+ for idx, row in top_stages.iterrows():
421
+ st.metric(
422
+ f"Stage {int(row['stage'])}",
423
+ f"{row['rejection_rate']:.1f}%",
424
+ help=f"{int(row.get('total_rejections', 0))} rejections out of {int(row.get('total_messages', 0))} messages"
425
+ )
426
+ else:
427
+ st.info("No stage rejection data available")
428
+
429
+ with col3:
430
+ st.subheader("🤖 Highest Rejection Rate Models")
431
+ if not model_df.empty and 'rejection_rate' in model_df.columns:
432
+ top_models = model_df.nlargest(5, 'rejection_rate')
433
+ for idx, row in top_models.iterrows():
434
+ model_name = row['llm_model']
435
+ # Truncate long model names
436
+ display_name = model_name if len(model_name) <= 20 else model_name[:17] + "..."
437
+ st.metric(
438
+ display_name,
439
+ f"{row['rejection_rate']:.1f}%",
440
+ help=f"{model_name}: {int(row.get('total_rejections', 0))} rejections"
441
+ )
442
+ else:
443
+ st.info("No model rejection data available")
444
+
445
+ # Overall rejection reasons chart
446
+ if not reasons_df.empty:
447
+ st.subheader("🔍 Rejection Reasons Distribution")
448
+ reasons_display = reasons_df.copy()
449
+ reasons_display['reason_label'] = reasons_display['rejection_reason'].apply(get_rejection_label)
450
+
451
  fig = px.bar(
452
+ reasons_display.sort_values('count', ascending=False),
453
+ x='reason_label',
454
+ y='count',
455
+ color='count',
456
+ color_continuous_scale='Reds',
457
+ text='count'
 
458
  )
459
+ fig.update_traces(textposition='outside')
 
460
  fig.update_layout(
461
  template="plotly_dark",
462
  paper_bgcolor='rgba(0,0,0,0)',
463
  plot_bgcolor='rgba(0,0,0,0)',
464
+ xaxis_title="Rejection Reason",
465
+ yaxis_title="Count",
466
+ showlegend=False,
467
+ height=350
468
  )
469
+ fig.update_xaxes(tickangle=-45)
470
  st.plotly_chart(fig, use_container_width=True)
471
 
472
+ st.markdown("---")
 
 
 
 
 
 
 
 
 
 
473
 
474
+ # ========================================================================
475
+ # MODEL PERFORMANCE ANALYSIS
476
+ # ========================================================================
477
+ st.header("🤖 Model Performance Analysis")
478
+
479
+ model_df = analysis.get('by_model', pd.DataFrame())
480
+
481
+ if not model_df.empty:
482
+ col1, col2 = st.columns(2)
483
+
484
+ with col1:
485
+ st.subheader("📊 Rejection Rate by Model")
486
+ fig = px.bar(
487
+ model_df.sort_values('rejection_rate', ascending=True),
488
+ y='llm_model',
489
+ x='rejection_rate',
490
+ orientation='h',
491
+ color='rejection_rate',
492
+ color_continuous_scale='RdYlGn_r',
493
+ text='rejection_rate'
494
+ )
495
+ fig.update_traces(texttemplate='%{text:.1f}%', textposition='outside')
496
+ fig.update_layout(
497
+ template="plotly_dark",
498
+ paper_bgcolor='rgba(0,0,0,0)',
499
+ plot_bgcolor='rgba(0,0,0,0)',
500
+ xaxis_title="Rejection Rate (%)",
501
+ yaxis_title="Model",
502
+ showlegend=False
503
+ )
504
+ st.plotly_chart(fig, use_container_width=True)
505
+
506
+ with col2:
507
+ st.subheader("📈 Messages vs Rejections by Model")
508
+ fig = go.Figure()
509
+ fig.add_trace(go.Bar(
510
+ x=model_df['llm_model'],
511
+ y=model_df['total_messages_generated'],
512
+ name='Total Messages',
513
+ marker_color=theme['primary']
514
+ ))
515
+ fig.add_trace(go.Bar(
516
+ x=model_df['llm_model'],
517
+ y=model_df['total_rejections'],
518
+ name='Rejections',
519
+ marker_color='#ff4444'
520
+ ))
521
+ fig.update_layout(
522
+ template="plotly_dark",
523
+ paper_bgcolor='rgba(0,0,0,0)',
524
+ plot_bgcolor='rgba(0,0,0,0)',
525
+ xaxis_title="Model",
526
+ yaxis_title="Count",
527
+ barmode='group'
528
+ )
529
+ st.plotly_chart(fig, use_container_width=True)
530
+
531
+ # Model performance table
532
+ st.subheader("📋 Detailed Model Statistics")
533
  st.dataframe(
534
+ model_df,
535
  use_container_width=True,
536
  hide_index=True,
537
  column_config={
538
+ "llm_model": "Model",
539
+ "total_rejections": st.column_config.NumberColumn("Rejections", format="%d"),
540
+ "unique_users_rejected": st.column_config.NumberColumn("Unique Users", format="%d"),
541
+ "total_messages_generated": st.column_config.NumberColumn("Total Messages", format="%d"),
542
+ "rejection_rate": st.column_config.NumberColumn("Rejection Rate", format="%.2f%%")
 
 
 
 
 
 
 
 
543
  }
544
  )
545
+ else:
546
+ st.info("No model performance data available for this campaign")
547
 
548
+ st.markdown("---")
549
+
550
+ # ========================================================================
551
+ # COMPREHENSIVE STAGE ANALYSIS
552
+ # ========================================================================
553
+ st.header("📍 Comprehensive Stage Analysis")
554
+
555
+ stage_df = analysis.get('by_stage', pd.DataFrame())
556
+ reasons_by_stage_df = analysis.get('reasons_by_stage', pd.DataFrame())
557
+
558
+ if not stage_df.empty:
559
+ # Messages vs Rejections Chart
560
+ st.subheader("📊 Messages vs Rejections by Stage")
561
+ fig = go.Figure()
562
+ fig.add_trace(go.Bar(
563
+ x=stage_df['stage'],
564
+ y=stage_df['total_messages'],
565
+ name='Total Messages',
566
+ marker_color=theme['primary'],
567
+ text=stage_df['total_messages'],
568
+ textposition='outside'
569
+ ))
570
+ fig.add_trace(go.Bar(
571
+ x=stage_df['stage'],
572
+ y=stage_df['total_rejections'],
573
+ name='Rejections',
574
+ marker_color='#ff4444',
575
+ text=stage_df['total_rejections'],
576
+ textposition='outside'
577
+ ))
578
+ fig.update_layout(
579
+ template="plotly_dark",
580
+ paper_bgcolor='rgba(0,0,0,0)',
581
+ plot_bgcolor='rgba(0,0,0,0)',
582
+ xaxis_title="Stage",
583
+ yaxis_title="Count",
584
+ barmode='group',
585
+ height=400
586
+ )
587
+ st.plotly_chart(fig, use_container_width=True)
588
+
589
+ # Comprehensive Stage Statistics Table
590
+ st.subheader("📋 Detailed Stage Metrics")
591
+
592
+ # Prepare display dataframe
593
+ stage_display = stage_df.copy()
594
+
595
+ # Column configuration for display
596
+ column_config = {
597
+ "stage": "Stage",
598
+ "total_messages": st.column_config.NumberColumn("Messages", format="%d"),
599
+ "total_rejections": st.column_config.NumberColumn("Rejections", format="%d"),
600
+ "rejection_rate": st.column_config.NumberColumn("Rejection Rate", format="%.2f%%"),
601
+ "unique_users_rejected": st.column_config.NumberColumn("Unique Users", format="%d"),
602
+ "experiments_count": st.column_config.NumberColumn("Experiments", format="%d"),
603
+ "models_used": "Models Used"
604
+ }
605
+
606
+ # Only show columns that exist
607
+ available_columns = [col for col in ['stage', 'total_messages', 'total_rejections', 'rejection_rate',
608
+ 'unique_users_rejected', 'experiments_count', 'models_used']
609
+ if col in stage_display.columns]
610
 
611
+ st.dataframe(
612
+ stage_display[available_columns],
613
+ use_container_width=True,
614
+ hide_index=True,
615
+ column_config=column_config
616
+ )
617
+
618
+ st.markdown("---")
619
+
620
+ # ========================================================================
621
+ # INTERACTIVE STAGE SELECTOR
622
+ # ========================================================================
623
+ st.subheader("🎯 Detailed Stage-by-Stage Analysis")
624
+ st.markdown("Select a stage to see detailed breakdown of models, rejection reasons, and performance metrics.")
625
+
626
+ # Stage selector
627
+ available_stages = sorted(stage_df['stage'].unique())
628
+
629
+ if len(available_stages) > 0:
630
+ selected_stage_detail = st.selectbox(
631
+ "Select Stage for Detailed View",
632
+ available_stages,
633
+ key='detailed_stage_selector'
634
+ )
635
+
636
+ # Get selected stage data
637
+ stage_info = stage_df[stage_df['stage'] == selected_stage_detail].iloc[0]
638
+
639
+ # Display stage metrics
640
+ col1, col2, col3, col4 = st.columns(4)
641
+
642
+ with col1:
643
+ st.metric("Total Messages", f"{int(stage_info.get('total_messages', 0)):,}")
644
+
645
+ with col2:
646
+ st.metric("Total Rejections", f"{int(stage_info.get('total_rejections', 0)):,}")
647
+
648
+ with col3:
649
+ st.metric("Rejection Rate", f"{stage_info.get('rejection_rate', 0):.2f}%")
650
+
651
+ with col4:
652
+ st.metric("Experiments", int(stage_info.get('experiments_count', 0)))
653
+
654
+ # Models used at this stage
655
+ st.markdown(f"**🤖 Models Used at Stage {selected_stage_detail}:**")
656
+ models_used = stage_info.get('models_used', 'N/A')
657
+ st.info(models_used)
658
+
659
+ # Rejection reasons for this stage
660
+ if not reasons_by_stage_df.empty:
661
+ stage_reasons = reasons_by_stage_df[reasons_by_stage_df['stage'] == selected_stage_detail].copy()
662
+
663
+ if not stage_reasons.empty:
664
+ st.markdown(f"**🔍 Rejection Reasons for Stage {selected_stage_detail}:**")
665
+
666
+ # Apply labels
667
+ stage_reasons['reason_label'] = stage_reasons['rejection_reason'].apply(get_rejection_label)
668
+
669
+ col1, col2 = st.columns(2)
670
+
671
+ with col1:
672
+ # Pie chart
673
+ fig = px.pie(
674
+ stage_reasons,
675
+ values='count',
676
+ names='reason_label',
677
+ title=f"Rejection Reasons Distribution - Stage {selected_stage_detail}",
678
+ color_discrete_sequence=px.colors.qualitative.Set3
679
+ )
680
+ fig.update_layout(
681
+ template="plotly_dark",
682
+ paper_bgcolor='rgba(0,0,0,0)',
683
+ plot_bgcolor='rgba(0,0,0,0)',
684
+ height=350
685
+ )
686
+ st.plotly_chart(fig, use_container_width=True)
687
+
688
+ with col2:
689
+ # Bar chart
690
+ fig = px.bar(
691
+ stage_reasons.sort_values('count', ascending=False),
692
+ x='reason_label',
693
+ y='count',
694
+ color='count',
695
+ color_continuous_scale='Oranges',
696
+ text='count',
697
+ title=f"Top Rejection Reasons - Stage {selected_stage_detail}"
698
+ )
699
+ fig.update_traces(textposition='outside')
700
+ fig.update_layout(
701
+ template="plotly_dark",
702
+ paper_bgcolor='rgba(0,0,0,0)',
703
+ plot_bgcolor='rgba(0,0,0,0)',
704
+ xaxis_title="Rejection Reason",
705
+ yaxis_title="Count",
706
+ showlegend=False,
707
+ height=350
708
+ )
709
+ fig.update_xaxes(tickangle=-45)
710
+ st.plotly_chart(fig, use_container_width=True)
711
+
712
+ # Comparison to other stages
713
+ st.markdown(f"**📊 Stage {selected_stage_detail} Comparison:**")
714
+
715
+ # Calculate average rejection rate across all stages
716
+ avg_rejection_rate = stage_df['rejection_rate'].mean()
717
+ stage_rejection_rate = stage_info.get('rejection_rate', 0)
718
+
719
+ if stage_rejection_rate > avg_rejection_rate:
720
+ delta_text = f"{stage_rejection_rate - avg_rejection_rate:.1f}% above average"
721
+ st.warning(f"⚠️ This stage has a rejection rate **{stage_rejection_rate:.1f}%**, which is **{delta_text}**")
722
+ elif stage_rejection_rate < avg_rejection_rate:
723
+ delta_text = f"{avg_rejection_rate - stage_rejection_rate:.1f}% below average"
724
+ st.success(f"✅ This stage has a rejection rate **{stage_rejection_rate:.1f}%**, which is **{delta_text}**")
725
+ else:
726
+ st.info(f"ℹ️ This stage has an average rejection rate of **{stage_rejection_rate:.1f}%**")
727
+
728
+ # Show detailed breakdown table
729
+ st.markdown("**📋 Detailed Rejection Breakdown:**")
730
+ st.dataframe(
731
+ stage_reasons[['reason_label', 'count']].rename(columns={'reason_label': 'Rejection Reason', 'count': 'Count'}),
732
+ use_container_width=True,
733
+ hide_index=True
734
+ )
735
+ else:
736
+ st.info(f"No rejection reasons data available for Stage {selected_stage_detail}")
737
+ else:
738
+ st.info("No rejection reasons data available")
739
+ else:
740
+ st.info("No stage-level data available for this campaign")
741
+
742
+ st.markdown("---")
743
+
744
+ # ========================================================================
745
+ # REJECTION REASONS ANALYSIS (Interactive)
746
+ # ========================================================================
747
+ st.header("🔍 Rejection Reasons Analysis")
748
+
749
+ # Get rejection reasons data
750
+ reasons_df = analysis.get('rejection_reasons', pd.DataFrame())
751
+ reasons_by_stage_df = analysis.get('reasons_by_stage', pd.DataFrame())
752
+ reasons_by_model_df = analysis.get('reasons_by_model', pd.DataFrame())
753
+
754
+ if not reasons_df.empty:
755
+ # Overall rejection reasons
756
+ st.subheader("📊 Overall Rejection Reasons Distribution")
757
+
758
+ # Apply labels to reasons
759
+ reasons_display = reasons_df.copy()
760
+ reasons_display['reason_label'] = reasons_display['rejection_reason'].apply(get_rejection_label)
761
+
762
+ col1, col2 = st.columns(2)
763
+
764
+ with col1:
765
+ # Pie chart
766
+ fig = px.pie(
767
+ reasons_display,
768
+ values='count',
769
+ names='reason_label',
770
+ title="Rejection Reasons Breakdown",
771
+ color_discrete_sequence=px.colors.qualitative.Set3
772
+ )
773
+ fig.update_layout(
774
+ template="plotly_dark",
775
+ paper_bgcolor='rgba(0,0,0,0)',
776
+ plot_bgcolor='rgba(0,0,0,0)'
777
+ )
778
+ st.plotly_chart(fig, use_container_width=True)
779
+
780
+ with col2:
781
+ # Bar chart
782
+ fig = px.bar(
783
+ reasons_display.sort_values('count', ascending=False),
784
+ x='reason_label',
785
+ y='count',
786
+ color='count',
787
+ color_continuous_scale='Reds',
788
+ text='count'
789
+ )
790
+ fig.update_traces(textposition='outside')
791
+ fig.update_layout(
792
+ template="plotly_dark",
793
+ paper_bgcolor='rgba(0,0,0,0)',
794
+ plot_bgcolor='rgba(0,0,0,0)',
795
+ xaxis_title="Rejection Reason",
796
+ yaxis_title="Count",
797
+ showlegend=False
798
+ )
799
+ fig.update_xaxes(tickangle=-45)
800
+ st.plotly_chart(fig, use_container_width=True)
801
+
802
+ st.markdown("---")
803
+
804
+ # Interactive filtering section
805
+ st.subheader("🎛️ Interactive Rejection Analysis")
806
+
807
+ tab1, tab2 = st.tabs(["📍 By Stage", "🤖 By Model"])
808
+
809
+ with tab1:
810
+ # Rejection reasons by stage
811
+ if not reasons_by_stage_df.empty:
812
+ # Apply labels
813
+ reasons_by_stage_display = reasons_by_stage_df.copy()
814
+ reasons_by_stage_display['reason_label'] = reasons_by_stage_display['rejection_reason'].apply(get_rejection_label)
815
+
816
+ # Stage selector
817
+ available_stages = sorted(reasons_by_stage_display['stage'].unique())
818
+
819
+ if len(available_stages) > 0:
820
+ selected_stage = st.selectbox(
821
+ "Select Stage to Analyze",
822
+ available_stages,
823
+ key='stage_selector'
824
+ )
825
+
826
+ # Filter by selected stage
827
+ stage_reasons = reasons_by_stage_display[reasons_by_stage_display['stage'] == selected_stage]
828
+
829
+ col1, col2 = st.columns(2)
830
+
831
+ with col1:
832
+ # Pie chart for selected stage
833
+ fig = px.pie(
834
+ stage_reasons,
835
+ values='count',
836
+ names='reason_label',
837
+ title=f"Rejection Reasons - Stage {selected_stage}",
838
+ color_discrete_sequence=px.colors.qualitative.Pastel
839
+ )
840
+ fig.update_layout(
841
+ template="plotly_dark",
842
+ paper_bgcolor='rgba(0,0,0,0)',
843
+ plot_bgcolor='rgba(0,0,0,0)'
844
+ )
845
+ st.plotly_chart(fig, use_container_width=True)
846
+
847
+ with col2:
848
+ # Bar chart for selected stage
849
+ fig = px.bar(
850
+ stage_reasons.sort_values('count', ascending=False),
851
+ x='reason_label',
852
+ y='count',
853
+ color='count',
854
+ color_continuous_scale='Oranges',
855
+ text='count'
856
+ )
857
+ fig.update_traces(textposition='outside')
858
+ fig.update_layout(
859
+ template="plotly_dark",
860
+ paper_bgcolor='rgba(0,0,0,0)',
861
+ plot_bgcolor='rgba(0,0,0,0)',
862
+ xaxis_title="Rejection Reason",
863
+ yaxis_title="Count",
864
+ showlegend=False
865
+ )
866
+ fig.update_xaxes(tickangle=-45)
867
+ st.plotly_chart(fig, use_container_width=True)
868
+
869
+ # Heatmap across all stages
870
+ st.markdown("#### 🔥 Rejection Heatmap Across All Stages")
871
+ pivot_data = reasons_by_stage_display.pivot_table(
872
+ index='reason_label',
873
+ columns='stage',
874
+ values='count',
875
+ fill_value=0
876
+ )
877
+
878
+ fig = px.imshow(
879
+ pivot_data,
880
+ labels=dict(x="Stage", y="Rejection Reason", color="Count"),
881
+ x=pivot_data.columns,
882
+ y=pivot_data.index,
883
+ color_continuous_scale='Reds',
884
+ aspect='auto'
885
+ )
886
+ fig.update_layout(
887
+ template="plotly_dark",
888
+ paper_bgcolor='rgba(0,0,0,0)',
889
+ plot_bgcolor='rgba(0,0,0,0)'
890
+ )
891
+ st.plotly_chart(fig, use_container_width=True)
892
+ else:
893
+ st.info("No stage-level rejection reason data available")
894
+
895
+ with tab2:
896
+ # Rejection reasons by model
897
+ if not reasons_by_model_df.empty:
898
+ # Apply labels
899
+ reasons_by_model_display = reasons_by_model_df.copy()
900
+ reasons_by_model_display['reason_label'] = reasons_by_model_display['rejection_reason'].apply(get_rejection_label)
901
+
902
+ # Model selector
903
+ available_models = sorted(reasons_by_model_display['llm_model'].unique())
904
+
905
+ if len(available_models) > 0:
906
+ selected_model = st.selectbox(
907
+ "Select Model to Analyze",
908
+ available_models,
909
+ key='model_selector'
910
+ )
911
+
912
+ # Filter by selected model
913
+ model_reasons = reasons_by_model_display[reasons_by_model_display['llm_model'] == selected_model]
914
+
915
+ col1, col2 = st.columns(2)
916
+
917
+ with col1:
918
+ # Pie chart for selected model
919
+ fig = px.pie(
920
+ model_reasons,
921
+ values='count',
922
+ names='reason_label',
923
+ title=f"Rejection Reasons - {selected_model}",
924
+ color_discrete_sequence=px.colors.qualitative.Set2
925
+ )
926
+ fig.update_layout(
927
+ template="plotly_dark",
928
+ paper_bgcolor='rgba(0,0,0,0)',
929
+ plot_bgcolor='rgba(0,0,0,0)'
930
+ )
931
+ st.plotly_chart(fig, use_container_width=True)
932
+
933
+ with col2:
934
+ # Bar chart for selected model
935
+ fig = px.bar(
936
+ model_reasons.sort_values('count', ascending=False),
937
+ x='reason_label',
938
+ y='count',
939
+ color='count',
940
+ color_continuous_scale='Blues',
941
+ text='count'
942
+ )
943
+ fig.update_traces(textposition='outside')
944
+ fig.update_layout(
945
+ template="plotly_dark",
946
+ paper_bgcolor='rgba(0,0,0,0)',
947
+ plot_bgcolor='rgba(0,0,0,0)',
948
+ xaxis_title="Rejection Reason",
949
+ yaxis_title="Count",
950
+ showlegend=False
951
+ )
952
+ fig.update_xaxes(tickangle=-45)
953
+ st.plotly_chart(fig, use_container_width=True)
954
+
955
+ # Heatmap across all models
956
+ st.markdown("#### 🔥 Rejection Heatmap Across All Models")
957
+ pivot_data = reasons_by_model_display.pivot_table(
958
+ index='reason_label',
959
+ columns='llm_model',
960
+ values='count',
961
+ fill_value=0
962
+ )
963
+
964
+ fig = px.imshow(
965
+ pivot_data,
966
+ labels=dict(x="Model", y="Rejection Reason", color="Count"),
967
+ x=pivot_data.columns,
968
+ y=pivot_data.index,
969
+ color_continuous_scale='Blues',
970
+ aspect='auto'
971
+ )
972
+ fig.update_layout(
973
+ template="plotly_dark",
974
+ paper_bgcolor='rgba(0,0,0,0)',
975
+ plot_bgcolor='rgba(0,0,0,0)'
976
+ )
977
+ st.plotly_chart(fig, use_container_width=True)
978
+ else:
979
+ st.info("No model-level rejection reason data available")
980
+ else:
981
+ st.info("No rejection reason data available for this campaign")
982
+
983
+ # ============================================================================
984
+ # EXPORT SECTION
985
+ # ============================================================================
986
+ st.markdown("---")
987
+ st.header("💾 Export Data")
988
 
989
+ col1, col2, col3 = st.columns(3)
990
 
991
  with col1:
992
+ if st.button("📥 Export Campaigns Summary", use_container_width=True):
993
+ csv = campaigns_df.to_csv(index=False, encoding='utf-8')
994
+ st.download_button(
995
+ label="⬇️ Download Campaigns CSV",
996
+ data=csv,
997
+ file_name=f"{brand}_campaigns_summary_{datetime.now().strftime('%Y%m%d')}.csv",
998
+ mime="text/csv",
999
+ use_container_width=True
1000
+ )
1001
+
1002
+ with col2:
1003
  if st.button("📥 Export Experiments Summary", use_container_width=True):
1004
  csv = experiments_df.to_csv(index=False, encoding='utf-8')
1005
  st.download_button(
1006
+ label="⬇️ Download Experiments CSV",
1007
  data=csv,
1008
  file_name=f"{brand}_experiments_summary_{datetime.now().strftime('%Y%m%d')}.csv",
1009
  mime="text/csv",
1010
  use_container_width=True
1011
  )
1012
 
1013
+ with col3:
1014
+ if st.session_state.get('selected_campaign') and st.session_state.get('campaign_analysis'):
1015
+ if st.button("📥 Export Campaign Analysis", use_container_width=True):
1016
+ # Combine all analysis dataframes
1017
+ analysis = st.session_state['campaign_analysis']
1018
+
1019
+ # Create a writer object
1020
+ from io import BytesIO
1021
+ output = BytesIO()
1022
+ with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
1023
+ if not analysis['by_stage'].empty:
1024
+ analysis['by_stage'].to_excel(writer, sheet_name='By Stage', index=False)
1025
+ if not analysis['by_model'].empty:
1026
+ analysis['by_model'].to_excel(writer, sheet_name='By Model', index=False)
1027
+ if not analysis['rejection_reasons'].empty:
1028
+ analysis['rejection_reasons'].to_excel(writer, sheet_name='Rejection Reasons', index=False)
1029
+ if not analysis['reasons_by_stage'].empty:
1030
+ analysis['reasons_by_stage'].to_excel(writer, sheet_name='Reasons by Stage', index=False)
1031
+ if not analysis['reasons_by_model'].empty:
1032
+ analysis['reasons_by_model'].to_excel(writer, sheet_name='Reasons by Model', index=False)
1033
+
1034
+ st.download_button(
1035
+ label="⬇️ Download Analysis Excel",
1036
+ data=output.getvalue(),
1037
+ file_name=f"{brand}_{selected_campaign}_analysis_{datetime.now().strftime('%Y%m%d')}.xlsx",
1038
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1039
+ use_container_width=True
1040
+ )
1041
 
1042
  st.markdown("---")
1043
+ st.markdown("**💡 Pro Tip:** Use the campaign selector to drill down into specific campaigns and identify patterns in model performance and rejection reasons!")
visualization/utils/db_manager.py CHANGED
@@ -496,6 +496,201 @@ class UIDatabaseManager:
496
  st.error(f"Error generating experiment summary: {e}")
497
  return pd.DataFrame()
498
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
499
  def close(self):
500
  """Close the Snowflake session."""
501
  try:
 
496
  st.error(f"Error generating experiment summary: {e}")
497
  return pd.DataFrame()
498
 
499
+ def get_campaign_feedback_analysis(self, campaign_name: str, brand: Optional[str] = None) -> Dict:
500
+ """
501
+ Get detailed feedback analysis for a specific campaign.
502
+ Returns comprehensive statistics including model performance, stage analysis, and rejection reasons.
503
+
504
+ Args:
505
+ campaign_name: Campaign name to analyze
506
+ brand: Optional brand filter
507
+
508
+ Returns:
509
+ dict: Dictionary containing multiple analysis dataframes
510
+ """
511
+ try:
512
+ brand_filter = f"AND m.BRAND = '{brand}'" if brand else ""
513
+
514
+ # Query 1: Enhanced rejection distribution by stage (with messages and models)
515
+ stage_query = f"""
516
+ SELECT
517
+ m.STAGE,
518
+ SUM(m.TOTAL_MESSAGES) as TOTAL_MESSAGES,
519
+ COUNT(f.EXPERIMENT_ID) as TOTAL_REJECTIONS,
520
+ ROUND((COUNT(f.EXPERIMENT_ID) * 100.0 / NULLIF(SUM(m.TOTAL_MESSAGES), 0)), 2) as REJECTION_RATE,
521
+ COUNT(DISTINCT f.USER_ID) as UNIQUE_USERS_REJECTED,
522
+ COUNT(DISTINCT m.EXPERIMENT_ID) as EXPERIMENTS_COUNT,
523
+ LISTAGG(DISTINCT m.LLM_MODEL, ', ') WITHIN GROUP (ORDER BY m.LLM_MODEL) as MODELS_USED
524
+ FROM {self.METADATA_TABLE} m
525
+ LEFT JOIN {self.FEEDBACK_TABLE} f
526
+ ON m.EXPERIMENT_ID = f.EXPERIMENT_ID AND m.STAGE = f.STAGE
527
+ WHERE m.CAMPAIGN_NAME = '{campaign_name}' {brand_filter}
528
+ GROUP BY m.STAGE
529
+ ORDER BY m.STAGE
530
+ """
531
+
532
+ # Query 2: Rejection distribution by LLM model
533
+ model_query = f"""
534
+ SELECT
535
+ m.LLM_MODEL,
536
+ COUNT(f.EXPERIMENT_ID) as TOTAL_REJECTIONS,
537
+ COUNT(DISTINCT f.USER_ID) as UNIQUE_USERS_REJECTED,
538
+ SUM(m.TOTAL_MESSAGES) as TOTAL_MESSAGES_GENERATED,
539
+ ROUND((COUNT(f.EXPERIMENT_ID) * 100.0 / NULLIF(SUM(m.TOTAL_MESSAGES), 0)), 2) as REJECTION_RATE
540
+ FROM {self.METADATA_TABLE} m
541
+ LEFT JOIN {self.FEEDBACK_TABLE} f
542
+ ON m.EXPERIMENT_ID = f.EXPERIMENT_ID AND m.STAGE = f.STAGE
543
+ WHERE m.CAMPAIGN_NAME = '{campaign_name}' {brand_filter}
544
+ GROUP BY m.LLM_MODEL
545
+ ORDER BY REJECTION_RATE DESC
546
+ """
547
+
548
+ # Query 3: Rejection reasons breakdown
549
+ reasons_query = f"""
550
+ SELECT
551
+ f.REJECTION_REASON,
552
+ COUNT(*) as COUNT,
553
+ COUNT(DISTINCT f.EXPERIMENT_ID) as EXPERIMENTS_AFFECTED
554
+ FROM {self.FEEDBACK_TABLE} f
555
+ JOIN {self.METADATA_TABLE} m
556
+ ON f.EXPERIMENT_ID = m.EXPERIMENT_ID
557
+ WHERE m.CAMPAIGN_NAME = '{campaign_name}' {brand_filter}
558
+ GROUP BY f.REJECTION_REASON
559
+ ORDER BY COUNT DESC
560
+ """
561
+
562
+ # Query 4: Rejection reasons by stage
563
+ reasons_by_stage_query = f"""
564
+ SELECT
565
+ f.STAGE,
566
+ f.REJECTION_REASON,
567
+ COUNT(*) as COUNT
568
+ FROM {self.FEEDBACK_TABLE} f
569
+ JOIN {self.METADATA_TABLE} m
570
+ ON f.EXPERIMENT_ID = m.EXPERIMENT_ID
571
+ WHERE m.CAMPAIGN_NAME = '{campaign_name}' {brand_filter}
572
+ GROUP BY f.STAGE, f.REJECTION_REASON
573
+ ORDER BY f.STAGE, COUNT DESC
574
+ """
575
+
576
+ # Query 5: Rejection reasons by model
577
+ reasons_by_model_query = f"""
578
+ SELECT
579
+ m.LLM_MODEL,
580
+ f.REJECTION_REASON,
581
+ COUNT(*) as COUNT
582
+ FROM {self.FEEDBACK_TABLE} f
583
+ JOIN {self.METADATA_TABLE} m
584
+ ON f.EXPERIMENT_ID = m.EXPERIMENT_ID AND f.STAGE = m.STAGE
585
+ WHERE m.CAMPAIGN_NAME = '{campaign_name}' {brand_filter}
586
+ GROUP BY m.LLM_MODEL, f.REJECTION_REASON
587
+ ORDER BY m.LLM_MODEL, COUNT DESC
588
+ """
589
+
590
+ # Execute all queries
591
+ stage_df = self.session.sql(stage_query).to_pandas()
592
+ stage_df.columns = stage_df.columns.str.lower()
593
+
594
+ model_df = self.session.sql(model_query).to_pandas()
595
+ model_df.columns = model_df.columns.str.lower()
596
+
597
+ reasons_df = self.session.sql(reasons_query).to_pandas()
598
+ reasons_df.columns = reasons_df.columns.str.lower()
599
+
600
+ reasons_by_stage_df = self.session.sql(reasons_by_stage_query).to_pandas()
601
+ reasons_by_stage_df.columns = reasons_by_stage_df.columns.str.lower()
602
+
603
+ reasons_by_model_df = self.session.sql(reasons_by_model_query).to_pandas()
604
+ reasons_by_model_df.columns = reasons_by_model_df.columns.str.lower()
605
+
606
+ print(f"✅ Generated feedback analysis for campaign '{campaign_name}'")
607
+
608
+ return {
609
+ 'by_stage': stage_df,
610
+ 'by_model': model_df,
611
+ 'rejection_reasons': reasons_df,
612
+ 'reasons_by_stage': reasons_by_stage_df,
613
+ 'reasons_by_model': reasons_by_model_df
614
+ }
615
+
616
+ except Exception as e:
617
+ st.error(f"Error generating campaign feedback analysis: {e}")
618
+ import traceback
619
+ st.error(f"Traceback: {traceback.format_exc()}")
620
+ return {
621
+ 'by_stage': pd.DataFrame(),
622
+ 'by_model': pd.DataFrame(),
623
+ 'rejection_reasons': pd.DataFrame(),
624
+ 'reasons_by_stage': pd.DataFrame(),
625
+ 'reasons_by_model': pd.DataFrame()
626
+ }
627
+
628
+ def get_all_campaigns_summary(self, brand: Optional[str] = None) -> pd.DataFrame:
629
+ """
630
+ Get summary statistics for all campaigns (aggregating experiments by campaign).
631
+
632
+ Args:
633
+ brand: Optional brand filter
634
+
635
+ Returns:
636
+ pd.DataFrame: Summary statistics per campaign
637
+ """
638
+ try:
639
+ brand_filter = f"WHERE m.BRAND = '{brand}'" if brand else ""
640
+
641
+ query = f"""
642
+ WITH CampaignStats AS (
643
+ SELECT
644
+ m.CAMPAIGN_NAME,
645
+ m.BRAND,
646
+ COUNT(DISTINCT m.EXPERIMENT_ID) as TOTAL_EXPERIMENTS,
647
+ COUNT(DISTINCT m.STAGE) as TOTAL_STAGES,
648
+ SUM(m.TOTAL_MESSAGES) as TOTAL_MESSAGES,
649
+ MAX(m.TOTAL_USERS) as TOTAL_USERS,
650
+ LISTAGG(DISTINCT m.LLM_MODEL, ', ') WITHIN GROUP (ORDER BY m.LLM_MODEL) as MODELS_USED,
651
+ MIN(m.TIMESTAMP) as FIRST_EXPERIMENT,
652
+ MAX(m.TIMESTAMP) as LAST_EXPERIMENT
653
+ FROM {self.METADATA_TABLE} m
654
+ {brand_filter}
655
+ GROUP BY m.CAMPAIGN_NAME, m.BRAND
656
+ ),
657
+ FeedbackStats AS (
658
+ SELECT
659
+ m.CAMPAIGN_NAME,
660
+ COUNT(*) as TOTAL_FEEDBACK,
661
+ COUNT(DISTINCT f.REJECTION_REASON) as UNIQUE_REJECTION_REASONS
662
+ FROM {self.FEEDBACK_TABLE} f
663
+ JOIN {self.METADATA_TABLE} m
664
+ ON f.EXPERIMENT_ID = m.EXPERIMENT_ID
665
+ {brand_filter.replace('m.', 'm.')}
666
+ GROUP BY m.CAMPAIGN_NAME
667
+ )
668
+ SELECT
669
+ c.*,
670
+ COALESCE(f.TOTAL_FEEDBACK, 0) as TOTAL_REJECTIONS,
671
+ COALESCE(f.UNIQUE_REJECTION_REASONS, 0) as UNIQUE_REJECTION_REASONS,
672
+ CASE
673
+ WHEN c.TOTAL_MESSAGES > 0
674
+ THEN ROUND((COALESCE(f.TOTAL_FEEDBACK, 0) * 100.0 / c.TOTAL_MESSAGES), 2)
675
+ ELSE 0
676
+ END as REJECTION_RATE
677
+ FROM CampaignStats c
678
+ LEFT JOIN FeedbackStats f ON c.CAMPAIGN_NAME = f.CAMPAIGN_NAME
679
+ ORDER BY c.LAST_EXPERIMENT DESC
680
+ """
681
+
682
+ result_df = self.session.sql(query).to_pandas()
683
+ result_df.columns = result_df.columns.str.lower()
684
+
685
+ print(f"✅ Generated summary for {len(result_df)} campaigns")
686
+ return result_df
687
+
688
+ except Exception as e:
689
+ st.error(f"Error generating campaigns summary: {e}")
690
+ import traceback
691
+ st.error(f"Traceback: {traceback.format_exc()}")
692
+ return pd.DataFrame()
693
+
694
  def close(self):
695
  """Close the Snowflake session."""
696
  try: