akhil-vaidya commited on
Commit
3b9c5ff
·
verified ·
1 Parent(s): 5ad3d90

Update app/app.py

Browse files
Files changed (1) hide show
  1. app/app.py +916 -916
app/app.py CHANGED
@@ -1,916 +1,916 @@
1
- import streamlit as st
2
- import pandas as pd
3
- import numpy as np
4
- import matplotlib.pyplot as plt
5
- import seaborn as sns
6
- import tempfile
7
- import os
8
- import sys
9
- from io import StringIO
10
- import plotly.express as px
11
- import plotly.graph_objects as go
12
- from plotly.subplots import make_subplots
13
-
14
- # Add the parent directory to sys.path to import the module
15
- sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
16
-
17
- from src.qualivec.data import DataLoader
18
- from src.qualivec.embedding import EmbeddingModel
19
- from src.qualivec.matching import SemanticMatcher
20
- from src.qualivec.classification import Classifier
21
- from src.qualivec.evaluation import Evaluator
22
- from src.qualivec.optimization import ThresholdOptimizer
23
-
24
- # Set page config
25
- st.set_page_config(
26
- page_title="QualiVec Demo",
27
- page_icon="🔍",
28
- layout="wide",
29
- initial_sidebar_state="expanded"
30
- )
31
-
32
- # Custom CSS for better styling
33
- st.markdown("""
34
- <style>
35
- .main-header {
36
- font-size: 2.5rem;
37
- font-weight: bold;
38
- color: #2E4057;
39
- text-align: center;
40
- margin-bottom: 2rem;
41
- }
42
- .section-header {
43
- font-size: 1.5rem;
44
- font-weight: bold;
45
- color: #048A81;
46
- margin-top: 2rem;
47
- margin-bottom: 1rem;
48
- }
49
- .metric-card {
50
- background-color: #f0f2f6;
51
- padding: 1rem;
52
- border-radius: 0.5rem;
53
- margin: 0.5rem 0;
54
- }
55
- .success-message {
56
- background-color: #d4edda;
57
- color: #155724;
58
- padding: 1rem;
59
- border-radius: 0.5rem;
60
- margin: 1rem 0;
61
- }
62
- .warning-message {
63
- background-color: #fff3cd;
64
- color: #856404;
65
- padding: 1rem;
66
- border-radius: 0.5rem;
67
- margin: 1rem 0;
68
- }
69
- </style>
70
- """, unsafe_allow_html=True)
71
-
72
- def main():
73
- st.markdown('<div class="main-header">🔍 QualiVec Demo</div>', unsafe_allow_html=True)
74
- st.markdown("""
75
- <div style="text-align: center; margin-bottom: 2rem;">
76
- <p style="font-size: 1.2rem; color: #666;">
77
- Qualitative Content Analysis with LLM Embeddings
78
- </p>
79
- </div>
80
- """, unsafe_allow_html=True)
81
-
82
- # Sidebar for navigation
83
- st.sidebar.title("Navigation")
84
- page = st.sidebar.selectbox(
85
- "Choose a page",
86
- ["🏠 Home", "📊 Data Upload", "🔧 Configuration", "🎯 Classification", "📈 Results"]
87
- )
88
-
89
- # Initialize session state
90
- if 'classifier' not in st.session_state:
91
- st.session_state.classifier = None
92
- if 'reference_data' not in st.session_state:
93
- st.session_state.reference_data = None
94
- if 'labeled_data' not in st.session_state:
95
- st.session_state.labeled_data = None
96
- if 'optimization_results' not in st.session_state:
97
- st.session_state.optimization_results = None
98
- if 'evaluation_results' not in st.session_state:
99
- st.session_state.evaluation_results = None
100
-
101
- # Route to different pages
102
- if page == "🏠 Home":
103
- show_home_page()
104
- elif page == "📊 Data Upload":
105
- show_data_upload_page()
106
- elif page == "🔧 Configuration":
107
- show_configuration_page()
108
- elif page == "🎯 Classification":
109
- show_classification_page()
110
- elif page == "📈 Results":
111
- show_results_page()
112
-
113
- def show_home_page():
114
- st.markdown('<div class="section-header">Welcome to QualiVec</div>', unsafe_allow_html=True)
115
-
116
- col1, col2, col3 = st.columns([1, 2, 1])
117
-
118
- with col2:
119
- st.markdown("""
120
- ### What is QualiVec?
121
-
122
- QualiVec is a Python library that uses Large Language Model (LLM) embeddings for qualitative content analysis. It helps researchers and analysts classify text data by comparing it against reference examples.
123
-
124
- ### Key Features:
125
- - **Semantic Matching**: Uses advanced embedding models to find semantic similarity
126
- - **Threshold Optimization**: Automatically finds the best similarity threshold
127
- - **Comprehensive Evaluation**: Provides detailed metrics and visualizations
128
- - **Bootstrap Analysis**: Confidence intervals for robust evaluation
129
-
130
- ### How It Works:
131
- 1. **Upload Data**: Provide reference examples and data to classify
132
- 2. **Configure**: Set up embedding models and parameters
133
- 3. **Optimize**: Find the best threshold for classification
134
- 4. **Classify**: Apply the model to your data
135
- 5. **Evaluate**: Get detailed performance metrics
136
-
137
- ### Getting Started:
138
- Use the sidebar to navigate through the demo. Start with **Data Upload** to begin your analysis.
139
- """)
140
-
141
- # Add sample data info
142
- st.markdown('<div class="section-header">Sample Data Format</div>', unsafe_allow_html=True)
143
-
144
- col1, col2 = st.columns(2)
145
-
146
- with col1:
147
- st.markdown("**Reference Data Format:**")
148
- sample_ref = pd.DataFrame({
149
- 'tag': ['Positive', 'Negative', 'Neutral'],
150
- 'sentence': ['This is great!', 'This is terrible', 'This is okay']
151
- })
152
- st.dataframe(sample_ref, use_container_width=True)
153
-
154
- with col2:
155
- st.markdown("**Labeled Data Format:**")
156
- sample_labeled = pd.DataFrame({
157
- 'sentence': ['I love this product', 'Not very good', 'Average quality'],
158
- 'Label': ['Positive', 'Negative', 'Neutral']
159
- })
160
- st.dataframe(sample_labeled, use_container_width=True)
161
-
162
- def show_data_upload_page():
163
- st.markdown('<div class="section-header">Data Upload</div>', unsafe_allow_html=True)
164
-
165
- col1, col2 = st.columns(2)
166
-
167
- with col1:
168
- st.markdown("### Reference Data")
169
- st.markdown("Upload a CSV file containing reference examples with columns: `tag` (class) and `sentence` (example text)")
170
-
171
- reference_file = st.file_uploader(
172
- "Choose reference data file",
173
- type=['csv'],
174
- key='reference_file'
175
- )
176
-
177
- if reference_file is not None:
178
- try:
179
- reference_df = pd.read_csv(reference_file)
180
- st.success("Reference data loaded successfully!")
181
- st.dataframe(reference_df.head(), use_container_width=True)
182
-
183
- # Validate columns
184
- required_cols = ['tag', 'sentence']
185
- missing_cols = [col for col in required_cols if col not in reference_df.columns]
186
-
187
- if missing_cols:
188
- st.error(f"Missing required columns: {missing_cols}")
189
- else:
190
- # Prepare reference data
191
- reference_df = reference_df.rename(columns={
192
- 'tag': 'class',
193
- 'sentence': 'matching_node'
194
- })
195
- st.session_state.reference_data = reference_df
196
-
197
- # Show statistics
198
- st.markdown("**Data Statistics:**")
199
- st.write(f"- Total examples: {len(reference_df)}")
200
- st.write(f"- Unique classes: {reference_df['class'].nunique()}")
201
- st.write(f"- Class distribution:")
202
- st.write(reference_df['class'].value_counts())
203
-
204
- except Exception as e:
205
- st.error(f"Error loading reference data: {str(e)}")
206
-
207
- with col2:
208
- st.markdown("### Labeled Data")
209
- st.markdown("Upload a CSV file containing data to classify with columns: `sentence` (text) and `Label` (true class)")
210
-
211
- labeled_file = st.file_uploader(
212
- "Choose labeled data file",
213
- type=['csv'],
214
- key='labeled_file'
215
- )
216
-
217
- if labeled_file is not None:
218
- try:
219
- labeled_df = pd.read_csv(labeled_file)
220
- st.success("Labeled data loaded successfully!")
221
- st.dataframe(labeled_df.head(), use_container_width=True)
222
-
223
- # Validate columns
224
- required_cols = ['sentence', 'Label']
225
- missing_cols = [col for col in required_cols if col not in labeled_df.columns]
226
-
227
- if missing_cols:
228
- st.error(f"Missing required columns: {missing_cols}")
229
- else:
230
- # Prepare labeled data
231
- labeled_df = labeled_df.rename(columns={'Label': 'label'})
232
- labeled_df['label'] = labeled_df['label'].replace('0', 'Other')
233
- st.session_state.labeled_data = labeled_df
234
-
235
- # Show statistics
236
- st.markdown("**Data Statistics:**")
237
- st.write(f"- Total samples: {len(labeled_df)}")
238
- st.write(f"- Unique labels: {labeled_df['label'].nunique()}")
239
- st.write(f"- Label distribution:")
240
- st.write(labeled_df['label'].value_counts())
241
-
242
- except Exception as e:
243
- st.error(f"Error loading labeled data: {str(e)}")
244
-
245
- # Show data compatibility check
246
- if st.session_state.reference_data is not None and st.session_state.labeled_data is not None:
247
- st.markdown('<div class="section-header">Data Compatibility Check</div>', unsafe_allow_html=True)
248
-
249
- ref_classes = set(st.session_state.reference_data['class'].unique())
250
- labeled_classes = set(st.session_state.labeled_data['label'].unique())
251
-
252
- # Check for unknown classes
253
- unknown_classes = labeled_classes - ref_classes
254
-
255
- if unknown_classes:
256
- st.warning(f"Warning: Labels in labeled data not found in reference data: {unknown_classes}")
257
- else:
258
- st.success("✅ Data compatibility check passed!")
259
-
260
- # Show class overlap
261
- st.markdown("**Class Overlap Analysis:**")
262
- col1, col2, col3 = st.columns(3)
263
-
264
- with col1:
265
- st.metric("Reference Classes", len(ref_classes))
266
- with col2:
267
- st.metric("Labeled Classes", len(labeled_classes))
268
- with col3:
269
- st.metric("Common Classes", len(ref_classes.intersection(labeled_classes)))
270
-
271
- def show_configuration_page():
272
- st.markdown('<div class="section-header">Model Configuration</div>', unsafe_allow_html=True)
273
-
274
- # Check if data is loaded
275
- if st.session_state.reference_data is None or st.session_state.labeled_data is None:
276
- st.warning("Please upload both reference and labeled data first.")
277
- return
278
-
279
- col1, col2 = st.columns(2)
280
-
281
- with col1:
282
- st.markdown("### Embedding Model")
283
-
284
- # Model type selection
285
- model_type = st.selectbox(
286
- "Choose model type",
287
- ["HuggingFace", "Gemini"],
288
- help="Select the type of embedding model to use"
289
- )
290
-
291
- # Model selection based on type
292
- if model_type == "HuggingFace":
293
- model_options = [
294
- "sentence-transformers/all-MiniLM-L6-v2",
295
- "sentence-transformers/all-mpnet-base-v2",
296
- "sentence-transformers/distilbert-base-nli-mean-tokens"
297
- ]
298
-
299
- selected_model = st.selectbox(
300
- "Choose HuggingFace model",
301
- model_options,
302
- help="Select the pre-trained HuggingFace model for generating embeddings"
303
- )
304
- else: # Gemini
305
- gemini_models = [
306
- "gemini-embedding-001",
307
- "text-embedding-004"
308
- ]
309
-
310
- selected_model = st.selectbox(
311
- "Choose Gemini model",
312
- gemini_models,
313
- help="Select the Gemini embedding model for generating embeddings"
314
- )
315
-
316
- # Calculate total texts to process
317
- total_texts = 0
318
- if st.session_state.reference_data is not None:
319
- total_texts += len(st.session_state.reference_data)
320
- if st.session_state.labeled_data is not None:
321
- total_texts += len(st.session_state.labeled_data)
322
-
323
- st.warning(
324
- f"⚠️ **Gemini API Rate Limits (Free Tier)**\\n\\n"
325
- f"- 1,500 requests per day\\n"
326
- f"- Each batch of 100 texts = 1 request\\n"
327
- f"- Your current dataset: ~{total_texts} texts\\n"
328
- f"- Estimated requests needed: ~{(total_texts // 100) + 1}\\n\\n"
329
- f"If you exceed quota, consider:\\n"
330
- f"1. Using a smaller dataset\\n"
331
- f"2. Switching to HuggingFace models (no limits)\\n"
332
- f"3. Upgrading to a paid API plan"
333
- )
334
-
335
- st.info("💡 Note: Using Gemini embeddings requires GOOGLE_API_KEY environment variable to be set.")
336
-
337
- st.markdown("### Initial Threshold")
338
- initial_threshold = st.slider(
339
- "Initial similarity threshold",
340
- min_value=0.0,
341
- max_value=1.0,
342
- value=0.7,
343
- step=0.05,
344
- help="Cosine similarity threshold for classification"
345
- )
346
-
347
- with col2:
348
- st.markdown("### Optimization Parameters")
349
-
350
- optimize_threshold = st.checkbox(
351
- "Enable threshold optimization",
352
- value=True,
353
- help="Automatically find the best threshold"
354
- )
355
-
356
- if optimize_threshold:
357
- col2_1, col2_2 = st.columns(2)
358
-
359
- with col2_1:
360
- start_threshold = st.slider(
361
- "Start threshold",
362
- min_value=0.0,
363
- max_value=1.0,
364
- value=0.5,
365
- step=0.05
366
- )
367
-
368
- end_threshold = st.slider(
369
- "End threshold",
370
- min_value=0.0,
371
- max_value=1.0,
372
- value=0.9,
373
- step=0.05
374
- )
375
-
376
- with col2_2:
377
- step_size = st.slider(
378
- "Step size",
379
- min_value=0.005,
380
- max_value=0.05,
381
- value=0.01,
382
- step=0.005
383
- )
384
-
385
- optimization_metric = st.selectbox(
386
- "Optimization metric",
387
- ["f1_macro", "accuracy", "precision_macro", "recall_macro"]
388
- )
389
-
390
- # Load models button
391
- if st.button("Initialize Models", type="primary"):
392
- with st.spinner("Loading models... This may take a few minutes."):
393
- try:
394
- # Initialize classifier
395
- classifier = Classifier(verbose=False)
396
-
397
- # Determine model type parameter
398
- model_type_param = "gemini" if model_type == "Gemini" else "huggingface"
399
-
400
- classifier.load_models(
401
- model_name=selected_model,
402
- model_type=model_type_param,
403
- threshold=initial_threshold
404
- )
405
-
406
- # Prepare reference vectors
407
- with tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False) as tmp_ref:
408
- tmp_ref_path = tmp_ref.name
409
- st.session_state.reference_data.to_csv(tmp_ref_path, index=False)
410
-
411
- try:
412
- reference_data = classifier.prepare_reference_vectors(
413
- reference_path=tmp_ref_path,
414
- class_column='class',
415
- node_column='matching_node'
416
- )
417
- finally:
418
- # Ensure file is deleted even if an error occurs
419
- try:
420
- os.unlink(tmp_ref_path)
421
- except (OSError, PermissionError):
422
- pass # File might already be deleted or locked
423
-
424
- st.session_state.classifier = classifier
425
- st.session_state.reference_vectors = reference_data
426
- st.session_state.config = {
427
- 'model_type': model_type,
428
- 'model_name': selected_model,
429
- 'initial_threshold': initial_threshold,
430
- 'optimize_threshold': optimize_threshold,
431
- 'start_threshold': start_threshold if optimize_threshold else None,
432
- 'end_threshold': end_threshold if optimize_threshold else None,
433
- 'step_size': step_size if optimize_threshold else None,
434
- 'optimization_metric': optimization_metric if optimize_threshold else None
435
- }
436
-
437
- st.success("✅ Models initialized successfully!")
438
-
439
- except Exception as e:
440
- st.error(f"Error initializing models: {str(e)}")
441
-
442
- # Show current configuration
443
- if st.session_state.classifier is not None:
444
- st.markdown('<div class="section-header">Current Configuration</div>', unsafe_allow_html=True)
445
-
446
- config = st.session_state.config
447
-
448
- col1, col2, col3 = st.columns(3)
449
-
450
- with col1:
451
- st.markdown("**Model Settings:**")
452
- st.write(f"- Model type: {config['model_type']}")
453
- st.write(f"- Model: {config['model_name']}")
454
- st.write(f"- Initial threshold: {config['initial_threshold']}")
455
-
456
- with col2:
457
- st.markdown("**Optimization:**")
458
- st.write(f"- Enabled: {config['optimize_threshold']}")
459
- if config['optimize_threshold']:
460
- st.write(f"- Range: {config['start_threshold']:.2f} - {config['end_threshold']:.2f}")
461
- st.write(f"- Step: {config['step_size']:.3f}")
462
-
463
- with col3:
464
- st.markdown("**Data:**")
465
- st.write(f"- Reference examples: {len(st.session_state.reference_data)}")
466
- st.write(f"- Labeled samples: {len(st.session_state.labeled_data)}")
467
-
468
- def show_classification_page():
469
- st.markdown('<div class="section-header">Classification & Optimization</div>', unsafe_allow_html=True)
470
-
471
- # Check if models are loaded
472
- if st.session_state.classifier is None:
473
- st.warning("Please configure and initialize models first.")
474
- return
475
-
476
- # Run classification
477
- if st.button("Run Classification", type="primary"):
478
- with st.spinner("Running classification and optimization..."):
479
- try:
480
- # Save labeled data to temporary file
481
- with tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False) as tmp_labeled:
482
- tmp_labeled_path = tmp_labeled.name
483
- st.session_state.labeled_data.to_csv(tmp_labeled_path, index=False)
484
-
485
- try:
486
- # Run optimization if enabled
487
- if st.session_state.config['optimize_threshold']:
488
- optimization_results = st.session_state.classifier.evaluate_classification(
489
- labeled_path=tmp_labeled_path,
490
- reference_data=st.session_state.reference_vectors,
491
- sentence_column='sentence',
492
- label_column='label',
493
- optimize_threshold=True,
494
- start=st.session_state.config['start_threshold'],
495
- end=st.session_state.config['end_threshold'],
496
- step=st.session_state.config['step_size']
497
- )
498
-
499
- st.session_state.optimization_results = optimization_results
500
- optimal_threshold = optimization_results["optimal_threshold"]
501
-
502
- # Update classifier with optimal threshold
503
- st.session_state.classifier.matcher = SemanticMatcher(
504
- threshold=optimal_threshold,
505
- verbose=False
506
- )
507
-
508
- st.success(f"✅ Optimization completed! Optimal threshold: {optimal_threshold:.4f}")
509
-
510
- else:
511
- optimal_threshold = st.session_state.config['initial_threshold']
512
-
513
- # Run evaluation
514
- embedding_model = st.session_state.classifier.embedding_model
515
- data_loader = DataLoader(verbose=False)
516
- full_df = data_loader.load_labeled_data(tmp_labeled_path, label_column='label')
517
-
518
- # Generate embeddings
519
- full_embeddings = embedding_model.embed_dataframe(full_df, text_column='sentence')
520
-
521
- # Classify
522
- match_results = st.session_state.classifier.matcher.match(
523
- full_embeddings,
524
- st.session_state.reference_vectors
525
- )
526
- predicted_labels = match_results["predicted_class"].tolist()
527
- true_labels = full_df['label'].tolist()
528
-
529
- # Evaluate
530
- evaluator = Evaluator(verbose=False)
531
- eval_results = evaluator.evaluate(
532
- true_labels=true_labels,
533
- predicted_labels=predicted_labels,
534
- class_names=list(set(true_labels) | set(predicted_labels))
535
- )
536
-
537
- # Bootstrap evaluation
538
- bootstrap_results = evaluator.bootstrap_evaluate(
539
- true_labels=true_labels,
540
- predicted_labels=predicted_labels,
541
- n_iterations=100
542
- )
543
-
544
- st.session_state.evaluation_results = eval_results
545
- st.session_state.bootstrap_results = bootstrap_results
546
- st.session_state.predictions = {
547
- 'true_labels': true_labels,
548
- 'predicted_labels': predicted_labels,
549
- 'match_results': match_results,
550
- 'full_df': full_df
551
- }
552
-
553
- finally:
554
- # Ensure temporary file is deleted
555
- try:
556
- os.unlink(tmp_labeled_path)
557
- except (OSError, PermissionError):
558
- pass # File might already be deleted or locked
559
-
560
- st.success("✅ Classification completed successfully!")
561
-
562
- except Exception as e:
563
- st.error(f"Error during classification: {str(e)}")
564
-
565
- # Show optimization results if available
566
- if st.session_state.optimization_results is not None:
567
- st.markdown('<div class="section-header">Optimization Results</div>', unsafe_allow_html=True)
568
-
569
- results = st.session_state.optimization_results
570
-
571
- col1, col2, col3, col4 = st.columns(4)
572
-
573
- with col1:
574
- st.metric(
575
- "Optimal Threshold",
576
- f"{results['optimal_threshold']:.4f}"
577
- )
578
-
579
- with col2:
580
- st.metric(
581
- "Accuracy",
582
- f"{results['optimal_metrics']['accuracy']:.4f}"
583
- )
584
-
585
- with col3:
586
- st.metric(
587
- "F1 Score",
588
- f"{results['optimal_metrics']['f1_macro']:.4f}"
589
- )
590
-
591
- with col4:
592
- st.metric(
593
- "Precision",
594
- f"{results['optimal_metrics']['precision_macro']:.4f}"
595
- )
596
-
597
- # Plot optimization curve
598
- st.markdown("### Optimization Curve")
599
-
600
- opt_results = results["results_by_threshold"]
601
-
602
- fig = make_subplots(
603
- rows=2, cols=2,
604
- subplot_titles=('Accuracy', 'F1 Score', 'Precision', 'Recall'),
605
- vertical_spacing=0.1
606
- )
607
-
608
- thresholds = opt_results["thresholds"]
609
-
610
- # Add traces
611
- fig.add_trace(
612
- go.Scatter(x=thresholds, y=opt_results["accuracy"], name="Accuracy"),
613
- row=1, col=1
614
- )
615
- fig.add_trace(
616
- go.Scatter(x=thresholds, y=opt_results["f1_macro"], name="F1 Score"),
617
- row=1, col=2
618
- )
619
- fig.add_trace(
620
- go.Scatter(x=thresholds, y=opt_results["precision_macro"], name="Precision"),
621
- row=2, col=1
622
- )
623
- fig.add_trace(
624
- go.Scatter(x=thresholds, y=opt_results["recall_macro"], name="Recall"),
625
- row=2, col=2
626
- )
627
-
628
- # Add optimal threshold line to each subplot using shapes
629
- optimal_thresh = results['optimal_threshold']
630
-
631
- # Add vertical line as shapes to each subplot
632
- shapes = []
633
- for row in range(1, 3):
634
- for col in range(1, 3):
635
- # Calculate the subplot domain
636
- xaxis = f'x{(row-1)*2 + col}' if (row-1)*2 + col > 1 else 'x'
637
- shapes.append(
638
- dict(
639
- type="line",
640
- x0=optimal_thresh, x1=optimal_thresh,
641
- y0=0, y1=1,
642
- yref=f"y{(row-1)*2 + col} domain" if (row-1)*2 + col > 1 else "y domain",
643
- xref=xaxis,
644
- line=dict(color="red", width=2, dash="dash")
645
- )
646
- )
647
-
648
- fig.update_layout(shapes=shapes)
649
-
650
- fig.update_layout(
651
- title="Threshold Optimization Results",
652
- showlegend=False,
653
- height=600
654
- )
655
-
656
- st.plotly_chart(fig, use_container_width=True)
657
-
658
- def show_results_page():
659
- st.markdown('<div class="section-header">Results & Evaluation</div>', unsafe_allow_html=True)
660
-
661
- # Check if evaluation results are available
662
- if st.session_state.evaluation_results is None:
663
- st.warning("Please run classification first to see results.")
664
- return
665
-
666
- eval_results = st.session_state.evaluation_results
667
-
668
- # Performance metrics
669
- st.markdown("### Performance Metrics")
670
-
671
- col1, col2, col3, col4 = st.columns(4)
672
-
673
- with col1:
674
- st.metric(
675
- "Overall Accuracy",
676
- f"{eval_results['accuracy']:.4f}"
677
- )
678
-
679
- with col2:
680
- st.metric(
681
- "Macro F1 Score",
682
- f"{eval_results['f1_macro']:.4f}"
683
- )
684
-
685
- with col3:
686
- st.metric(
687
- "Macro Precision",
688
- f"{eval_results['precision_macro']:.4f}"
689
- )
690
-
691
- with col4:
692
- st.metric(
693
- "Macro Recall",
694
- f"{eval_results['recall_macro']:.4f}"
695
- )
696
-
697
- # Class-wise metrics
698
- st.markdown("### Class-wise Performance")
699
-
700
- class_metrics_df = pd.DataFrame({
701
- 'Class': list(eval_results['class_metrics']['precision'].keys()),
702
- 'Precision': list(eval_results['class_metrics']['precision'].values()),
703
- 'Recall': list(eval_results['class_metrics']['recall'].values()),
704
- 'F1-Score': list(eval_results['class_metrics']['f1'].values()),
705
- 'Support': list(eval_results['class_metrics']['support'].values())
706
- })
707
-
708
- st.dataframe(class_metrics_df, use_container_width=True)
709
-
710
- # Confusion Matrix
711
- st.markdown("### Confusion Matrix")
712
-
713
- cm = eval_results['confusion_matrix']
714
- class_names = eval_results['confusion_matrix_labels']
715
-
716
- fig = px.imshow(
717
- cm,
718
- labels=dict(x="Predicted", y="True", color="Count"),
719
- x=class_names,
720
- y=class_names,
721
- color_continuous_scale='Blues',
722
- text_auto=True,
723
- title="Confusion Matrix"
724
- )
725
-
726
- fig.update_layout(
727
- width=600,
728
- height=600
729
- )
730
-
731
- st.plotly_chart(fig, use_container_width=True)
732
-
733
- # Bootstrap Results
734
- if st.session_state.bootstrap_results is not None:
735
- st.markdown("### Bootstrap Confidence Intervals")
736
-
737
- bootstrap_results = st.session_state.bootstrap_results
738
-
739
- # Debug: show available keys
740
- if 'confidence_intervals' in bootstrap_results:
741
- metrics = ['accuracy', 'precision_macro', 'recall_macro', 'f1_macro']
742
-
743
- for metric in metrics:
744
- if metric in bootstrap_results['confidence_intervals']:
745
- ci_data = bootstrap_results['confidence_intervals'][metric]
746
- st.markdown(f"**{metric.replace('_', ' ').title()}:**")
747
-
748
- col1, col2, col3 = st.columns(3)
749
-
750
- # Check available confidence levels
751
- available_levels = list(ci_data.keys())
752
-
753
- with col1:
754
- if '0.95' in ci_data:
755
- ci_95 = ci_data['0.95']
756
- if isinstance(ci_95, dict):
757
- st.write(f"95% CI: [{ci_95['lower']:.4f}, {ci_95['upper']:.4f}]")
758
- elif isinstance(ci_95, (list, tuple)) and len(ci_95) >= 2:
759
- st.write(f"95% CI: [{ci_95[0]:.4f}, {ci_95[1]:.4f}]")
760
- else:
761
- st.write("95% CI: Format not recognized")
762
- elif 0.95 in ci_data:
763
- ci_95 = ci_data[0.95]
764
- if isinstance(ci_95, dict):
765
- st.write(f"95% CI: [{ci_95['lower']:.4f}, {ci_95['upper']:.4f}]")
766
- elif isinstance(ci_95, (list, tuple)) and len(ci_95) >= 2:
767
- st.write(f"95% CI: [{ci_95[0]:.4f}, {ci_95[1]:.4f}]")
768
- else:
769
- st.write("95% CI: Format not recognized")
770
- else:
771
- st.write("95% CI: Not available")
772
-
773
- with col2:
774
- if '0.99' in ci_data:
775
- ci_99 = ci_data['0.99']
776
- if isinstance(ci_99, dict):
777
- st.write(f"99% CI: [{ci_99['lower']:.4f}, {ci_99['upper']:.4f}]")
778
- elif isinstance(ci_99, (list, tuple)) and len(ci_99) >= 2:
779
- st.write(f"99% CI: [{ci_99[0]:.4f}, {ci_99[1]:.4f}]")
780
- else:
781
- st.write("99% CI: Format not recognized")
782
- elif 0.99 in ci_data:
783
- ci_99 = ci_data[0.99]
784
- if isinstance(ci_99, dict):
785
- st.write(f"99% CI: [{ci_99['lower']:.4f}, {ci_99['upper']:.4f}]")
786
- elif isinstance(ci_99, (list, tuple)) and len(ci_99) >= 2:
787
- st.write(f"99% CI: [{ci_99[0]:.4f}, {ci_99[1]:.4f}]")
788
- else:
789
- st.write("99% CI: Format not recognized")
790
- else:
791
- st.write("99% CI: Not available")
792
-
793
- with col3:
794
- if 'point_estimates' in bootstrap_results and metric in bootstrap_results['point_estimates']:
795
- st.write(f"Point Estimate: {bootstrap_results['point_estimates'][metric]:.4f}")
796
- else:
797
- st.write("Point Estimate: Not available")
798
- else:
799
- st.info("Bootstrap confidence intervals not available.")
800
-
801
- # Bootstrap Distribution Plot
802
- st.markdown("### Bootstrap Distributions")
803
-
804
- if 'bootstrap_distribution' in bootstrap_results:
805
- fig = make_subplots(
806
- rows=2, cols=2,
807
- subplot_titles=('Accuracy', 'F1 Score', 'Precision', 'Recall')
808
- )
809
-
810
- distributions = bootstrap_results['bootstrap_distribution']
811
-
812
- if 'accuracy' in distributions:
813
- fig.add_trace(
814
- go.Histogram(x=distributions['accuracy'], name="Accuracy", nbinsx=30),
815
- row=1, col=1
816
- )
817
- if 'f1_macro' in distributions:
818
- fig.add_trace(
819
- go.Histogram(x=distributions['f1_macro'], name="F1 Score", nbinsx=30),
820
- row=1, col=2
821
- )
822
- if 'precision_macro' in distributions:
823
- fig.add_trace(
824
- go.Histogram(x=distributions['precision_macro'], name="Precision", nbinsx=30),
825
- row=2, col=1
826
- )
827
- if 'recall_macro' in distributions:
828
- fig.add_trace(
829
- go.Histogram(x=distributions['recall_macro'], name="Recall", nbinsx=30),
830
- row=2, col=2
831
- )
832
-
833
- fig.update_layout(
834
- title="Bootstrap Distributions",
835
- showlegend=False,
836
- height=600
837
- )
838
-
839
- st.plotly_chart(fig, use_container_width=True)
840
- else:
841
- st.info("Bootstrap distributions not available.")
842
-
843
- # Sample predictions
844
- if 'predictions' in st.session_state:
845
- st.markdown("### Sample Predictions")
846
-
847
- predictions = st.session_state.predictions
848
- sample_df = predictions['full_df'].copy()
849
- sample_df['predicted_class'] = predictions['predicted_labels']
850
- sample_df['true_class'] = predictions['true_labels']
851
- sample_df['similarity_score'] = predictions['match_results']['similarity_score']
852
- sample_df['correct'] = sample_df['predicted_class'] == sample_df['true_class']
853
-
854
- # Filter options
855
- col1, col2 = st.columns(2)
856
-
857
- with col1:
858
- show_correct = st.checkbox("Show correct predictions", value=True)
859
-
860
- with col2:
861
- show_incorrect = st.checkbox("Show incorrect predictions", value=True)
862
-
863
- # Filter data
864
- if show_correct and show_incorrect:
865
- filtered_df = sample_df
866
- elif show_correct:
867
- filtered_df = sample_df[sample_df['correct'] == True]
868
- elif show_incorrect:
869
- filtered_df = sample_df[sample_df['correct'] == False]
870
- else:
871
- filtered_df = pd.DataFrame()
872
-
873
- if not filtered_df.empty:
874
- # Sample random rows
875
- n_samples = min(20, len(filtered_df))
876
- sample_rows = filtered_df.sample(n=n_samples) if len(filtered_df) > n_samples else filtered_df
877
-
878
- display_df = sample_rows[['sentence', 'true_class', 'predicted_class', 'similarity_score', 'correct']].reset_index(drop=True)
879
-
880
- st.dataframe(display_df, use_container_width=True)
881
- else:
882
- st.info("No predictions to show with current filters.")
883
-
884
- # Download results
885
- st.markdown("### Download Results")
886
-
887
- col1, col2 = st.columns(2)
888
-
889
- with col1:
890
- # Download class-wise metrics
891
- csv_metrics = class_metrics_df.to_csv(index=False)
892
- st.download_button(
893
- label="Download Class Metrics",
894
- data=csv_metrics,
895
- file_name="class_metrics.csv",
896
- mime="text/csv"
897
- )
898
-
899
- with col2:
900
- # Download predictions
901
- if 'predictions' in st.session_state:
902
- predictions = st.session_state.predictions
903
- results_df = predictions['full_df'].copy()
904
- results_df['predicted_class'] = predictions['predicted_labels']
905
- results_df['similarity_score'] = predictions['match_results']['similarity_score']
906
-
907
- csv_results = results_df.to_csv(index=False)
908
- st.download_button(
909
- label="Download Predictions",
910
- data=csv_results,
911
- file_name="predictions.csv",
912
- mime="text/csv"
913
- )
914
-
915
- if __name__ == "__main__":
916
- main()
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+ import numpy as np
4
+ import matplotlib.pyplot as plt
5
+ import seaborn as sns
6
+ import tempfile
7
+ import os
8
+ import sys
9
+ from io import StringIO
10
+ import plotly.express as px
11
+ import plotly.graph_objects as go
12
+ from plotly.subplots import make_subplots
13
+
14
+ # Add the parent directory to sys.path to import the module
15
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
16
+
17
+ from qualivec.data import DataLoader
18
+ from qualivec.embedding import EmbeddingModel
19
+ from qualivec.matching import SemanticMatcher
20
+ from qualivec.classification import Classifier
21
+ from qualivec.evaluation import Evaluator
22
+ from qualivec.optimization import ThresholdOptimizer
23
+
24
+ # Set page config
25
+ st.set_page_config(
26
+ page_title="QualiVec Demo",
27
+ page_icon="🔍",
28
+ layout="wide",
29
+ initial_sidebar_state="expanded"
30
+ )
31
+
32
+ # Custom CSS for better styling
33
+ st.markdown("""
34
+ <style>
35
+ .main-header {
36
+ font-size: 2.5rem;
37
+ font-weight: bold;
38
+ color: #2E4057;
39
+ text-align: center;
40
+ margin-bottom: 2rem;
41
+ }
42
+ .section-header {
43
+ font-size: 1.5rem;
44
+ font-weight: bold;
45
+ color: #048A81;
46
+ margin-top: 2rem;
47
+ margin-bottom: 1rem;
48
+ }
49
+ .metric-card {
50
+ background-color: #f0f2f6;
51
+ padding: 1rem;
52
+ border-radius: 0.5rem;
53
+ margin: 0.5rem 0;
54
+ }
55
+ .success-message {
56
+ background-color: #d4edda;
57
+ color: #155724;
58
+ padding: 1rem;
59
+ border-radius: 0.5rem;
60
+ margin: 1rem 0;
61
+ }
62
+ .warning-message {
63
+ background-color: #fff3cd;
64
+ color: #856404;
65
+ padding: 1rem;
66
+ border-radius: 0.5rem;
67
+ margin: 1rem 0;
68
+ }
69
+ </style>
70
+ """, unsafe_allow_html=True)
71
+
72
+ def main():
73
+ st.markdown('<div class="main-header">🔍 QualiVec Demo</div>', unsafe_allow_html=True)
74
+ st.markdown("""
75
+ <div style="text-align: center; margin-bottom: 2rem;">
76
+ <p style="font-size: 1.2rem; color: #666;">
77
+ Qualitative Content Analysis with LLM Embeddings
78
+ </p>
79
+ </div>
80
+ """, unsafe_allow_html=True)
81
+
82
+ # Sidebar for navigation
83
+ st.sidebar.title("Navigation")
84
+ page = st.sidebar.selectbox(
85
+ "Choose a page",
86
+ ["🏠 Home", "📊 Data Upload", "🔧 Configuration", "🎯 Classification", "📈 Results"]
87
+ )
88
+
89
+ # Initialize session state
90
+ if 'classifier' not in st.session_state:
91
+ st.session_state.classifier = None
92
+ if 'reference_data' not in st.session_state:
93
+ st.session_state.reference_data = None
94
+ if 'labeled_data' not in st.session_state:
95
+ st.session_state.labeled_data = None
96
+ if 'optimization_results' not in st.session_state:
97
+ st.session_state.optimization_results = None
98
+ if 'evaluation_results' not in st.session_state:
99
+ st.session_state.evaluation_results = None
100
+
101
+ # Route to different pages
102
+ if page == "🏠 Home":
103
+ show_home_page()
104
+ elif page == "📊 Data Upload":
105
+ show_data_upload_page()
106
+ elif page == "🔧 Configuration":
107
+ show_configuration_page()
108
+ elif page == "🎯 Classification":
109
+ show_classification_page()
110
+ elif page == "📈 Results":
111
+ show_results_page()
112
+
113
+ def show_home_page():
114
+ st.markdown('<div class="section-header">Welcome to QualiVec</div>', unsafe_allow_html=True)
115
+
116
+ col1, col2, col3 = st.columns([1, 2, 1])
117
+
118
+ with col2:
119
+ st.markdown("""
120
+ ### What is QualiVec?
121
+
122
+ QualiVec is a Python library that uses Large Language Model (LLM) embeddings for qualitative content analysis. It helps researchers and analysts classify text data by comparing it against reference examples.
123
+
124
+ ### Key Features:
125
+ - **Semantic Matching**: Uses advanced embedding models to find semantic similarity
126
+ - **Threshold Optimization**: Automatically finds the best similarity threshold
127
+ - **Comprehensive Evaluation**: Provides detailed metrics and visualizations
128
+ - **Bootstrap Analysis**: Confidence intervals for robust evaluation
129
+
130
+ ### How It Works:
131
+ 1. **Upload Data**: Provide reference examples and data to classify
132
+ 2. **Configure**: Set up embedding models and parameters
133
+ 3. **Optimize**: Find the best threshold for classification
134
+ 4. **Classify**: Apply the model to your data
135
+ 5. **Evaluate**: Get detailed performance metrics
136
+
137
+ ### Getting Started:
138
+ Use the sidebar to navigate through the demo. Start with **Data Upload** to begin your analysis.
139
+ """)
140
+
141
+ # Add sample data info
142
+ st.markdown('<div class="section-header">Sample Data Format</div>', unsafe_allow_html=True)
143
+
144
+ col1, col2 = st.columns(2)
145
+
146
+ with col1:
147
+ st.markdown("**Reference Data Format:**")
148
+ sample_ref = pd.DataFrame({
149
+ 'tag': ['Positive', 'Negative', 'Neutral'],
150
+ 'sentence': ['This is great!', 'This is terrible', 'This is okay']
151
+ })
152
+ st.dataframe(sample_ref, use_container_width=True)
153
+
154
+ with col2:
155
+ st.markdown("**Labeled Data Format:**")
156
+ sample_labeled = pd.DataFrame({
157
+ 'sentence': ['I love this product', 'Not very good', 'Average quality'],
158
+ 'Label': ['Positive', 'Negative', 'Neutral']
159
+ })
160
+ st.dataframe(sample_labeled, use_container_width=True)
161
+
162
+ def show_data_upload_page():
163
+ st.markdown('<div class="section-header">Data Upload</div>', unsafe_allow_html=True)
164
+
165
+ col1, col2 = st.columns(2)
166
+
167
+ with col1:
168
+ st.markdown("### Reference Data")
169
+ st.markdown("Upload a CSV file containing reference examples with columns: `tag` (class) and `sentence` (example text)")
170
+
171
+ reference_file = st.file_uploader(
172
+ "Choose reference data file",
173
+ type=['csv'],
174
+ key='reference_file'
175
+ )
176
+
177
+ if reference_file is not None:
178
+ try:
179
+ reference_df = pd.read_csv(reference_file)
180
+ st.success("Reference data loaded successfully!")
181
+ st.dataframe(reference_df.head(), use_container_width=True)
182
+
183
+ # Validate columns
184
+ required_cols = ['tag', 'sentence']
185
+ missing_cols = [col for col in required_cols if col not in reference_df.columns]
186
+
187
+ if missing_cols:
188
+ st.error(f"Missing required columns: {missing_cols}")
189
+ else:
190
+ # Prepare reference data
191
+ reference_df = reference_df.rename(columns={
192
+ 'tag': 'class',
193
+ 'sentence': 'matching_node'
194
+ })
195
+ st.session_state.reference_data = reference_df
196
+
197
+ # Show statistics
198
+ st.markdown("**Data Statistics:**")
199
+ st.write(f"- Total examples: {len(reference_df)}")
200
+ st.write(f"- Unique classes: {reference_df['class'].nunique()}")
201
+ st.write(f"- Class distribution:")
202
+ st.write(reference_df['class'].value_counts())
203
+
204
+ except Exception as e:
205
+ st.error(f"Error loading reference data: {str(e)}")
206
+
207
+ with col2:
208
+ st.markdown("### Labeled Data")
209
+ st.markdown("Upload a CSV file containing data to classify with columns: `sentence` (text) and `Label` (true class)")
210
+
211
+ labeled_file = st.file_uploader(
212
+ "Choose labeled data file",
213
+ type=['csv'],
214
+ key='labeled_file'
215
+ )
216
+
217
+ if labeled_file is not None:
218
+ try:
219
+ labeled_df = pd.read_csv(labeled_file)
220
+ st.success("Labeled data loaded successfully!")
221
+ st.dataframe(labeled_df.head(), use_container_width=True)
222
+
223
+ # Validate columns
224
+ required_cols = ['sentence', 'Label']
225
+ missing_cols = [col for col in required_cols if col not in labeled_df.columns]
226
+
227
+ if missing_cols:
228
+ st.error(f"Missing required columns: {missing_cols}")
229
+ else:
230
+ # Prepare labeled data
231
+ labeled_df = labeled_df.rename(columns={'Label': 'label'})
232
+ labeled_df['label'] = labeled_df['label'].replace('0', 'Other')
233
+ st.session_state.labeled_data = labeled_df
234
+
235
+ # Show statistics
236
+ st.markdown("**Data Statistics:**")
237
+ st.write(f"- Total samples: {len(labeled_df)}")
238
+ st.write(f"- Unique labels: {labeled_df['label'].nunique()}")
239
+ st.write(f"- Label distribution:")
240
+ st.write(labeled_df['label'].value_counts())
241
+
242
+ except Exception as e:
243
+ st.error(f"Error loading labeled data: {str(e)}")
244
+
245
+ # Show data compatibility check
246
+ if st.session_state.reference_data is not None and st.session_state.labeled_data is not None:
247
+ st.markdown('<div class="section-header">Data Compatibility Check</div>', unsafe_allow_html=True)
248
+
249
+ ref_classes = set(st.session_state.reference_data['class'].unique())
250
+ labeled_classes = set(st.session_state.labeled_data['label'].unique())
251
+
252
+ # Check for unknown classes
253
+ unknown_classes = labeled_classes - ref_classes
254
+
255
+ if unknown_classes:
256
+ st.warning(f"Warning: Labels in labeled data not found in reference data: {unknown_classes}")
257
+ else:
258
+ st.success("✅ Data compatibility check passed!")
259
+
260
+ # Show class overlap
261
+ st.markdown("**Class Overlap Analysis:**")
262
+ col1, col2, col3 = st.columns(3)
263
+
264
+ with col1:
265
+ st.metric("Reference Classes", len(ref_classes))
266
+ with col2:
267
+ st.metric("Labeled Classes", len(labeled_classes))
268
+ with col3:
269
+ st.metric("Common Classes", len(ref_classes.intersection(labeled_classes)))
270
+
271
+ def show_configuration_page():
272
+ st.markdown('<div class="section-header">Model Configuration</div>', unsafe_allow_html=True)
273
+
274
+ # Check if data is loaded
275
+ if st.session_state.reference_data is None or st.session_state.labeled_data is None:
276
+ st.warning("Please upload both reference and labeled data first.")
277
+ return
278
+
279
+ col1, col2 = st.columns(2)
280
+
281
+ with col1:
282
+ st.markdown("### Embedding Model")
283
+
284
+ # Model type selection
285
+ model_type = st.selectbox(
286
+ "Choose model type",
287
+ ["HuggingFace", "Gemini"],
288
+ help="Select the type of embedding model to use"
289
+ )
290
+
291
+ # Model selection based on type
292
+ if model_type == "HuggingFace":
293
+ model_options = [
294
+ "sentence-transformers/all-MiniLM-L6-v2",
295
+ "sentence-transformers/all-mpnet-base-v2",
296
+ "sentence-transformers/distilbert-base-nli-mean-tokens"
297
+ ]
298
+
299
+ selected_model = st.selectbox(
300
+ "Choose HuggingFace model",
301
+ model_options,
302
+ help="Select the pre-trained HuggingFace model for generating embeddings"
303
+ )
304
+ else: # Gemini
305
+ gemini_models = [
306
+ "gemini-embedding-001",
307
+ "text-embedding-004"
308
+ ]
309
+
310
+ selected_model = st.selectbox(
311
+ "Choose Gemini model",
312
+ gemini_models,
313
+ help="Select the Gemini embedding model for generating embeddings"
314
+ )
315
+
316
+ # Calculate total texts to process
317
+ total_texts = 0
318
+ if st.session_state.reference_data is not None:
319
+ total_texts += len(st.session_state.reference_data)
320
+ if st.session_state.labeled_data is not None:
321
+ total_texts += len(st.session_state.labeled_data)
322
+
323
+ st.warning(
324
+ f"⚠️ **Gemini API Rate Limits (Free Tier)**\\n\\n"
325
+ f"- 1,500 requests per day\\n"
326
+ f"- Each batch of 100 texts = 1 request\\n"
327
+ f"- Your current dataset: ~{total_texts} texts\\n"
328
+ f"- Estimated requests needed: ~{(total_texts // 100) + 1}\\n\\n"
329
+ f"If you exceed quota, consider:\\n"
330
+ f"1. Using a smaller dataset\\n"
331
+ f"2. Switching to HuggingFace models (no limits)\\n"
332
+ f"3. Upgrading to a paid API plan"
333
+ )
334
+
335
+ st.info("💡 Note: Using Gemini embeddings requires GOOGLE_API_KEY environment variable to be set.")
336
+
337
+ st.markdown("### Initial Threshold")
338
+ initial_threshold = st.slider(
339
+ "Initial similarity threshold",
340
+ min_value=0.0,
341
+ max_value=1.0,
342
+ value=0.7,
343
+ step=0.05,
344
+ help="Cosine similarity threshold for classification"
345
+ )
346
+
347
+ with col2:
348
+ st.markdown("### Optimization Parameters")
349
+
350
+ optimize_threshold = st.checkbox(
351
+ "Enable threshold optimization",
352
+ value=True,
353
+ help="Automatically find the best threshold"
354
+ )
355
+
356
+ if optimize_threshold:
357
+ col2_1, col2_2 = st.columns(2)
358
+
359
+ with col2_1:
360
+ start_threshold = st.slider(
361
+ "Start threshold",
362
+ min_value=0.0,
363
+ max_value=1.0,
364
+ value=0.5,
365
+ step=0.05
366
+ )
367
+
368
+ end_threshold = st.slider(
369
+ "End threshold",
370
+ min_value=0.0,
371
+ max_value=1.0,
372
+ value=0.9,
373
+ step=0.05
374
+ )
375
+
376
+ with col2_2:
377
+ step_size = st.slider(
378
+ "Step size",
379
+ min_value=0.005,
380
+ max_value=0.05,
381
+ value=0.01,
382
+ step=0.005
383
+ )
384
+
385
+ optimization_metric = st.selectbox(
386
+ "Optimization metric",
387
+ ["f1_macro", "accuracy", "precision_macro", "recall_macro"]
388
+ )
389
+
390
+ # Load models button
391
+ if st.button("Initialize Models", type="primary"):
392
+ with st.spinner("Loading models... This may take a few minutes."):
393
+ try:
394
+ # Initialize classifier
395
+ classifier = Classifier(verbose=False)
396
+
397
+ # Determine model type parameter
398
+ model_type_param = "gemini" if model_type == "Gemini" else "huggingface"
399
+
400
+ classifier.load_models(
401
+ model_name=selected_model,
402
+ model_type=model_type_param,
403
+ threshold=initial_threshold
404
+ )
405
+
406
+ # Prepare reference vectors
407
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False) as tmp_ref:
408
+ tmp_ref_path = tmp_ref.name
409
+ st.session_state.reference_data.to_csv(tmp_ref_path, index=False)
410
+
411
+ try:
412
+ reference_data = classifier.prepare_reference_vectors(
413
+ reference_path=tmp_ref_path,
414
+ class_column='class',
415
+ node_column='matching_node'
416
+ )
417
+ finally:
418
+ # Ensure file is deleted even if an error occurs
419
+ try:
420
+ os.unlink(tmp_ref_path)
421
+ except (OSError, PermissionError):
422
+ pass # File might already be deleted or locked
423
+
424
+ st.session_state.classifier = classifier
425
+ st.session_state.reference_vectors = reference_data
426
+ st.session_state.config = {
427
+ 'model_type': model_type,
428
+ 'model_name': selected_model,
429
+ 'initial_threshold': initial_threshold,
430
+ 'optimize_threshold': optimize_threshold,
431
+ 'start_threshold': start_threshold if optimize_threshold else None,
432
+ 'end_threshold': end_threshold if optimize_threshold else None,
433
+ 'step_size': step_size if optimize_threshold else None,
434
+ 'optimization_metric': optimization_metric if optimize_threshold else None
435
+ }
436
+
437
+ st.success("✅ Models initialized successfully!")
438
+
439
+ except Exception as e:
440
+ st.error(f"Error initializing models: {str(e)}")
441
+
442
+ # Show current configuration
443
+ if st.session_state.classifier is not None:
444
+ st.markdown('<div class="section-header">Current Configuration</div>', unsafe_allow_html=True)
445
+
446
+ config = st.session_state.config
447
+
448
+ col1, col2, col3 = st.columns(3)
449
+
450
+ with col1:
451
+ st.markdown("**Model Settings:**")
452
+ st.write(f"- Model type: {config['model_type']}")
453
+ st.write(f"- Model: {config['model_name']}")
454
+ st.write(f"- Initial threshold: {config['initial_threshold']}")
455
+
456
+ with col2:
457
+ st.markdown("**Optimization:**")
458
+ st.write(f"- Enabled: {config['optimize_threshold']}")
459
+ if config['optimize_threshold']:
460
+ st.write(f"- Range: {config['start_threshold']:.2f} - {config['end_threshold']:.2f}")
461
+ st.write(f"- Step: {config['step_size']:.3f}")
462
+
463
+ with col3:
464
+ st.markdown("**Data:**")
465
+ st.write(f"- Reference examples: {len(st.session_state.reference_data)}")
466
+ st.write(f"- Labeled samples: {len(st.session_state.labeled_data)}")
467
+
468
+ def show_classification_page():
469
+ st.markdown('<div class="section-header">Classification & Optimization</div>', unsafe_allow_html=True)
470
+
471
+ # Check if models are loaded
472
+ if st.session_state.classifier is None:
473
+ st.warning("Please configure and initialize models first.")
474
+ return
475
+
476
+ # Run classification
477
+ if st.button("Run Classification", type="primary"):
478
+ with st.spinner("Running classification and optimization..."):
479
+ try:
480
+ # Save labeled data to temporary file
481
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False) as tmp_labeled:
482
+ tmp_labeled_path = tmp_labeled.name
483
+ st.session_state.labeled_data.to_csv(tmp_labeled_path, index=False)
484
+
485
+ try:
486
+ # Run optimization if enabled
487
+ if st.session_state.config['optimize_threshold']:
488
+ optimization_results = st.session_state.classifier.evaluate_classification(
489
+ labeled_path=tmp_labeled_path,
490
+ reference_data=st.session_state.reference_vectors,
491
+ sentence_column='sentence',
492
+ label_column='label',
493
+ optimize_threshold=True,
494
+ start=st.session_state.config['start_threshold'],
495
+ end=st.session_state.config['end_threshold'],
496
+ step=st.session_state.config['step_size']
497
+ )
498
+
499
+ st.session_state.optimization_results = optimization_results
500
+ optimal_threshold = optimization_results["optimal_threshold"]
501
+
502
+ # Update classifier with optimal threshold
503
+ st.session_state.classifier.matcher = SemanticMatcher(
504
+ threshold=optimal_threshold,
505
+ verbose=False
506
+ )
507
+
508
+ st.success(f"✅ Optimization completed! Optimal threshold: {optimal_threshold:.4f}")
509
+
510
+ else:
511
+ optimal_threshold = st.session_state.config['initial_threshold']
512
+
513
+ # Run evaluation
514
+ embedding_model = st.session_state.classifier.embedding_model
515
+ data_loader = DataLoader(verbose=False)
516
+ full_df = data_loader.load_labeled_data(tmp_labeled_path, label_column='label')
517
+
518
+ # Generate embeddings
519
+ full_embeddings = embedding_model.embed_dataframe(full_df, text_column='sentence')
520
+
521
+ # Classify
522
+ match_results = st.session_state.classifier.matcher.match(
523
+ full_embeddings,
524
+ st.session_state.reference_vectors
525
+ )
526
+ predicted_labels = match_results["predicted_class"].tolist()
527
+ true_labels = full_df['label'].tolist()
528
+
529
+ # Evaluate
530
+ evaluator = Evaluator(verbose=False)
531
+ eval_results = evaluator.evaluate(
532
+ true_labels=true_labels,
533
+ predicted_labels=predicted_labels,
534
+ class_names=list(set(true_labels) | set(predicted_labels))
535
+ )
536
+
537
+ # Bootstrap evaluation
538
+ bootstrap_results = evaluator.bootstrap_evaluate(
539
+ true_labels=true_labels,
540
+ predicted_labels=predicted_labels,
541
+ n_iterations=100
542
+ )
543
+
544
+ st.session_state.evaluation_results = eval_results
545
+ st.session_state.bootstrap_results = bootstrap_results
546
+ st.session_state.predictions = {
547
+ 'true_labels': true_labels,
548
+ 'predicted_labels': predicted_labels,
549
+ 'match_results': match_results,
550
+ 'full_df': full_df
551
+ }
552
+
553
+ finally:
554
+ # Ensure temporary file is deleted
555
+ try:
556
+ os.unlink(tmp_labeled_path)
557
+ except (OSError, PermissionError):
558
+ pass # File might already be deleted or locked
559
+
560
+ st.success("✅ Classification completed successfully!")
561
+
562
+ except Exception as e:
563
+ st.error(f"Error during classification: {str(e)}")
564
+
565
+ # Show optimization results if available
566
+ if st.session_state.optimization_results is not None:
567
+ st.markdown('<div class="section-header">Optimization Results</div>', unsafe_allow_html=True)
568
+
569
+ results = st.session_state.optimization_results
570
+
571
+ col1, col2, col3, col4 = st.columns(4)
572
+
573
+ with col1:
574
+ st.metric(
575
+ "Optimal Threshold",
576
+ f"{results['optimal_threshold']:.4f}"
577
+ )
578
+
579
+ with col2:
580
+ st.metric(
581
+ "Accuracy",
582
+ f"{results['optimal_metrics']['accuracy']:.4f}"
583
+ )
584
+
585
+ with col3:
586
+ st.metric(
587
+ "F1 Score",
588
+ f"{results['optimal_metrics']['f1_macro']:.4f}"
589
+ )
590
+
591
+ with col4:
592
+ st.metric(
593
+ "Precision",
594
+ f"{results['optimal_metrics']['precision_macro']:.4f}"
595
+ )
596
+
597
+ # Plot optimization curve
598
+ st.markdown("### Optimization Curve")
599
+
600
+ opt_results = results["results_by_threshold"]
601
+
602
+ fig = make_subplots(
603
+ rows=2, cols=2,
604
+ subplot_titles=('Accuracy', 'F1 Score', 'Precision', 'Recall'),
605
+ vertical_spacing=0.1
606
+ )
607
+
608
+ thresholds = opt_results["thresholds"]
609
+
610
+ # Add traces
611
+ fig.add_trace(
612
+ go.Scatter(x=thresholds, y=opt_results["accuracy"], name="Accuracy"),
613
+ row=1, col=1
614
+ )
615
+ fig.add_trace(
616
+ go.Scatter(x=thresholds, y=opt_results["f1_macro"], name="F1 Score"),
617
+ row=1, col=2
618
+ )
619
+ fig.add_trace(
620
+ go.Scatter(x=thresholds, y=opt_results["precision_macro"], name="Precision"),
621
+ row=2, col=1
622
+ )
623
+ fig.add_trace(
624
+ go.Scatter(x=thresholds, y=opt_results["recall_macro"], name="Recall"),
625
+ row=2, col=2
626
+ )
627
+
628
+ # Add optimal threshold line to each subplot using shapes
629
+ optimal_thresh = results['optimal_threshold']
630
+
631
+ # Add vertical line as shapes to each subplot
632
+ shapes = []
633
+ for row in range(1, 3):
634
+ for col in range(1, 3):
635
+ # Calculate the subplot domain
636
+ xaxis = f'x{(row-1)*2 + col}' if (row-1)*2 + col > 1 else 'x'
637
+ shapes.append(
638
+ dict(
639
+ type="line",
640
+ x0=optimal_thresh, x1=optimal_thresh,
641
+ y0=0, y1=1,
642
+ yref=f"y{(row-1)*2 + col} domain" if (row-1)*2 + col > 1 else "y domain",
643
+ xref=xaxis,
644
+ line=dict(color="red", width=2, dash="dash")
645
+ )
646
+ )
647
+
648
+ fig.update_layout(shapes=shapes)
649
+
650
+ fig.update_layout(
651
+ title="Threshold Optimization Results",
652
+ showlegend=False,
653
+ height=600
654
+ )
655
+
656
+ st.plotly_chart(fig, use_container_width=True)
657
+
658
+ def show_results_page():
659
+ st.markdown('<div class="section-header">Results & Evaluation</div>', unsafe_allow_html=True)
660
+
661
+ # Check if evaluation results are available
662
+ if st.session_state.evaluation_results is None:
663
+ st.warning("Please run classification first to see results.")
664
+ return
665
+
666
+ eval_results = st.session_state.evaluation_results
667
+
668
+ # Performance metrics
669
+ st.markdown("### Performance Metrics")
670
+
671
+ col1, col2, col3, col4 = st.columns(4)
672
+
673
+ with col1:
674
+ st.metric(
675
+ "Overall Accuracy",
676
+ f"{eval_results['accuracy']:.4f}"
677
+ )
678
+
679
+ with col2:
680
+ st.metric(
681
+ "Macro F1 Score",
682
+ f"{eval_results['f1_macro']:.4f}"
683
+ )
684
+
685
+ with col3:
686
+ st.metric(
687
+ "Macro Precision",
688
+ f"{eval_results['precision_macro']:.4f}"
689
+ )
690
+
691
+ with col4:
692
+ st.metric(
693
+ "Macro Recall",
694
+ f"{eval_results['recall_macro']:.4f}"
695
+ )
696
+
697
+ # Class-wise metrics
698
+ st.markdown("### Class-wise Performance")
699
+
700
+ class_metrics_df = pd.DataFrame({
701
+ 'Class': list(eval_results['class_metrics']['precision'].keys()),
702
+ 'Precision': list(eval_results['class_metrics']['precision'].values()),
703
+ 'Recall': list(eval_results['class_metrics']['recall'].values()),
704
+ 'F1-Score': list(eval_results['class_metrics']['f1'].values()),
705
+ 'Support': list(eval_results['class_metrics']['support'].values())
706
+ })
707
+
708
+ st.dataframe(class_metrics_df, use_container_width=True)
709
+
710
+ # Confusion Matrix
711
+ st.markdown("### Confusion Matrix")
712
+
713
+ cm = eval_results['confusion_matrix']
714
+ class_names = eval_results['confusion_matrix_labels']
715
+
716
+ fig = px.imshow(
717
+ cm,
718
+ labels=dict(x="Predicted", y="True", color="Count"),
719
+ x=class_names,
720
+ y=class_names,
721
+ color_continuous_scale='Blues',
722
+ text_auto=True,
723
+ title="Confusion Matrix"
724
+ )
725
+
726
+ fig.update_layout(
727
+ width=600,
728
+ height=600
729
+ )
730
+
731
+ st.plotly_chart(fig, use_container_width=True)
732
+
733
+ # Bootstrap Results
734
+ if st.session_state.bootstrap_results is not None:
735
+ st.markdown("### Bootstrap Confidence Intervals")
736
+
737
+ bootstrap_results = st.session_state.bootstrap_results
738
+
739
+ # Debug: show available keys
740
+ if 'confidence_intervals' in bootstrap_results:
741
+ metrics = ['accuracy', 'precision_macro', 'recall_macro', 'f1_macro']
742
+
743
+ for metric in metrics:
744
+ if metric in bootstrap_results['confidence_intervals']:
745
+ ci_data = bootstrap_results['confidence_intervals'][metric]
746
+ st.markdown(f"**{metric.replace('_', ' ').title()}:**")
747
+
748
+ col1, col2, col3 = st.columns(3)
749
+
750
+ # Check available confidence levels
751
+ available_levels = list(ci_data.keys())
752
+
753
+ with col1:
754
+ if '0.95' in ci_data:
755
+ ci_95 = ci_data['0.95']
756
+ if isinstance(ci_95, dict):
757
+ st.write(f"95% CI: [{ci_95['lower']:.4f}, {ci_95['upper']:.4f}]")
758
+ elif isinstance(ci_95, (list, tuple)) and len(ci_95) >= 2:
759
+ st.write(f"95% CI: [{ci_95[0]:.4f}, {ci_95[1]:.4f}]")
760
+ else:
761
+ st.write("95% CI: Format not recognized")
762
+ elif 0.95 in ci_data:
763
+ ci_95 = ci_data[0.95]
764
+ if isinstance(ci_95, dict):
765
+ st.write(f"95% CI: [{ci_95['lower']:.4f}, {ci_95['upper']:.4f}]")
766
+ elif isinstance(ci_95, (list, tuple)) and len(ci_95) >= 2:
767
+ st.write(f"95% CI: [{ci_95[0]:.4f}, {ci_95[1]:.4f}]")
768
+ else:
769
+ st.write("95% CI: Format not recognized")
770
+ else:
771
+ st.write("95% CI: Not available")
772
+
773
+ with col2:
774
+ if '0.99' in ci_data:
775
+ ci_99 = ci_data['0.99']
776
+ if isinstance(ci_99, dict):
777
+ st.write(f"99% CI: [{ci_99['lower']:.4f}, {ci_99['upper']:.4f}]")
778
+ elif isinstance(ci_99, (list, tuple)) and len(ci_99) >= 2:
779
+ st.write(f"99% CI: [{ci_99[0]:.4f}, {ci_99[1]:.4f}]")
780
+ else:
781
+ st.write("99% CI: Format not recognized")
782
+ elif 0.99 in ci_data:
783
+ ci_99 = ci_data[0.99]
784
+ if isinstance(ci_99, dict):
785
+ st.write(f"99% CI: [{ci_99['lower']:.4f}, {ci_99['upper']:.4f}]")
786
+ elif isinstance(ci_99, (list, tuple)) and len(ci_99) >= 2:
787
+ st.write(f"99% CI: [{ci_99[0]:.4f}, {ci_99[1]:.4f}]")
788
+ else:
789
+ st.write("99% CI: Format not recognized")
790
+ else:
791
+ st.write("99% CI: Not available")
792
+
793
+ with col3:
794
+ if 'point_estimates' in bootstrap_results and metric in bootstrap_results['point_estimates']:
795
+ st.write(f"Point Estimate: {bootstrap_results['point_estimates'][metric]:.4f}")
796
+ else:
797
+ st.write("Point Estimate: Not available")
798
+ else:
799
+ st.info("Bootstrap confidence intervals not available.")
800
+
801
+ # Bootstrap Distribution Plot
802
+ st.markdown("### Bootstrap Distributions")
803
+
804
+ if 'bootstrap_distribution' in bootstrap_results:
805
+ fig = make_subplots(
806
+ rows=2, cols=2,
807
+ subplot_titles=('Accuracy', 'F1 Score', 'Precision', 'Recall')
808
+ )
809
+
810
+ distributions = bootstrap_results['bootstrap_distribution']
811
+
812
+ if 'accuracy' in distributions:
813
+ fig.add_trace(
814
+ go.Histogram(x=distributions['accuracy'], name="Accuracy", nbinsx=30),
815
+ row=1, col=1
816
+ )
817
+ if 'f1_macro' in distributions:
818
+ fig.add_trace(
819
+ go.Histogram(x=distributions['f1_macro'], name="F1 Score", nbinsx=30),
820
+ row=1, col=2
821
+ )
822
+ if 'precision_macro' in distributions:
823
+ fig.add_trace(
824
+ go.Histogram(x=distributions['precision_macro'], name="Precision", nbinsx=30),
825
+ row=2, col=1
826
+ )
827
+ if 'recall_macro' in distributions:
828
+ fig.add_trace(
829
+ go.Histogram(x=distributions['recall_macro'], name="Recall", nbinsx=30),
830
+ row=2, col=2
831
+ )
832
+
833
+ fig.update_layout(
834
+ title="Bootstrap Distributions",
835
+ showlegend=False,
836
+ height=600
837
+ )
838
+
839
+ st.plotly_chart(fig, use_container_width=True)
840
+ else:
841
+ st.info("Bootstrap distributions not available.")
842
+
843
+ # Sample predictions
844
+ if 'predictions' in st.session_state:
845
+ st.markdown("### Sample Predictions")
846
+
847
+ predictions = st.session_state.predictions
848
+ sample_df = predictions['full_df'].copy()
849
+ sample_df['predicted_class'] = predictions['predicted_labels']
850
+ sample_df['true_class'] = predictions['true_labels']
851
+ sample_df['similarity_score'] = predictions['match_results']['similarity_score']
852
+ sample_df['correct'] = sample_df['predicted_class'] == sample_df['true_class']
853
+
854
+ # Filter options
855
+ col1, col2 = st.columns(2)
856
+
857
+ with col1:
858
+ show_correct = st.checkbox("Show correct predictions", value=True)
859
+
860
+ with col2:
861
+ show_incorrect = st.checkbox("Show incorrect predictions", value=True)
862
+
863
+ # Filter data
864
+ if show_correct and show_incorrect:
865
+ filtered_df = sample_df
866
+ elif show_correct:
867
+ filtered_df = sample_df[sample_df['correct'] == True]
868
+ elif show_incorrect:
869
+ filtered_df = sample_df[sample_df['correct'] == False]
870
+ else:
871
+ filtered_df = pd.DataFrame()
872
+
873
+ if not filtered_df.empty:
874
+ # Sample random rows
875
+ n_samples = min(20, len(filtered_df))
876
+ sample_rows = filtered_df.sample(n=n_samples) if len(filtered_df) > n_samples else filtered_df
877
+
878
+ display_df = sample_rows[['sentence', 'true_class', 'predicted_class', 'similarity_score', 'correct']].reset_index(drop=True)
879
+
880
+ st.dataframe(display_df, use_container_width=True)
881
+ else:
882
+ st.info("No predictions to show with current filters.")
883
+
884
+ # Download results
885
+ st.markdown("### Download Results")
886
+
887
+ col1, col2 = st.columns(2)
888
+
889
+ with col1:
890
+ # Download class-wise metrics
891
+ csv_metrics = class_metrics_df.to_csv(index=False)
892
+ st.download_button(
893
+ label="Download Class Metrics",
894
+ data=csv_metrics,
895
+ file_name="class_metrics.csv",
896
+ mime="text/csv"
897
+ )
898
+
899
+ with col2:
900
+ # Download predictions
901
+ if 'predictions' in st.session_state:
902
+ predictions = st.session_state.predictions
903
+ results_df = predictions['full_df'].copy()
904
+ results_df['predicted_class'] = predictions['predicted_labels']
905
+ results_df['similarity_score'] = predictions['match_results']['similarity_score']
906
+
907
+ csv_results = results_df.to_csv(index=False)
908
+ st.download_button(
909
+ label="Download Predictions",
910
+ data=csv_results,
911
+ file_name="predictions.csv",
912
+ mime="text/csv"
913
+ )
914
+
915
+ if __name__ == "__main__":
916
+ main()