| """ |
| WESAD Quality Metrics Dashboard - Adjustable Thresholds Version |
| Combined features + UI-adjustable thresholds for dynamic analysis |
| """ |
|
|
| import streamlit as st |
| import pandas as pd |
| import plotly.express as px |
| import plotly.graph_objects as go |
| from pathlib import Path |
| import json |
| from datetime import datetime |
| import time |
| from typing import Dict, List, Any, Optional, Tuple |
| import base64 |
| import yaml |
|
|
| |
| from utils.results_loader import ( |
| find_latest_full_report, |
| load_full_results, |
| summarize_users, |
| get_layer_details, |
| get_report_metadata |
| ) |
|
|
| |
| st.set_page_config( |
| page_title="WESAD Quality Dashboard - Adjustable Thresholds", |
| page_icon="ποΈ", |
| layout="wide", |
| initial_sidebar_state="expanded" |
| ) |
|
|
| |
| st.markdown(""" |
| <style> |
| /* Main container styling */ |
| .main { |
| padding: 1rem; |
| } |
| |
| /* Metric cards with gradient */ |
| .stMetric { |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| padding: 20px; |
| border-radius: 15px; |
| color: white; |
| box-shadow: 0 10px 20px rgba(0,0,0,0.1); |
| } |
| |
| .stMetric label { |
| color: white !important; |
| font-weight: 600; |
| } |
| |
| /* Alert styling */ |
| .alert-high { |
| background: linear-gradient(to right, #ffebee, #ffffff); |
| border-left: 5px solid #f44336; |
| padding: 15px; |
| margin: 10px 0; |
| border-radius: 5px; |
| } |
| |
| .alert-medium { |
| background: linear-gradient(to right, #fff3e0, #ffffff); |
| border-left: 5px solid #ff9800; |
| padding: 15px; |
| margin: 10px 0; |
| border-radius: 5px; |
| } |
| |
| /* Quality badges */ |
| .quality-excellent { |
| background: #4caf50; |
| color: white; |
| padding: 5px 10px; |
| border-radius: 20px; |
| font-weight: bold; |
| } |
| |
| /* Expander styling */ |
| .streamlit-expanderHeader { |
| background: #f8f9fa; |
| border-radius: 10px; |
| font-weight: 600; |
| } |
| |
| /* Button styling */ |
| .stButton > button { |
| background: linear-gradient(45deg, #2196F3 30%, #21CBF3 90%); |
| color: white; |
| border: none; |
| padding: 0.5rem 1rem; |
| border-radius: 5px; |
| font-weight: 600; |
| transition: all 0.3s; |
| } |
| |
| .stButton > button:hover { |
| transform: translateY(-2px); |
| box-shadow: 0 5px 10px rgba(0,0,0,0.2); |
| } |
| |
| /* Threshold sliders */ |
| .threshold-section { |
| background: #f8f9fa; |
| padding: 10px; |
| border-radius: 10px; |
| margin: 10px 0; |
| } |
| </style> |
| """, unsafe_allow_html=True) |
|
|
|
|
| def load_default_thresholds() -> Dict: |
| """Load default thresholds from YAML or use hardcoded defaults""" |
| config_path = Path("config/alerts.example.yaml") |
| |
| if config_path.exists(): |
| with open(config_path, 'r') as f: |
| config = yaml.safe_load(f) |
| return config |
| else: |
| |
| return { |
| 'global': {'overall_score_min': 0.80}, |
| 'layer1': {'any_validator_score_min': 0.80}, |
| 'layer2': { |
| 'dlr_max': 0.10, |
| 'mdr_max': 0.20, |
| 'scr_min': 0.80, |
| 'wear_time_pct_min': 60.0, |
| 'plausibility_pct_min': 80.0 |
| }, |
| 'layer3': { |
| 'ppg_quality_min': 0.80, |
| 'eda_wrist_quality_min': 0.80, |
| 'eda_chest_quality_min': 0.80, |
| 'acc_quality_min': 0.80, |
| 'temp_wrist_quality_min': 0.70, |
| 'temp_chest_quality_min': 0.85, |
| 'cross_modality_quality_min': 0.80 |
| } |
| } |
|
|
|
|
| def initialize_threshold_state(): |
| """Initialize threshold values in session state if not present""" |
| if 'thresholds' not in st.session_state: |
| st.session_state.thresholds = load_default_thresholds() |
| |
| if 'threshold_preset' not in st.session_state: |
| st.session_state.threshold_preset = 'Custom' |
|
|
|
|
| def evaluate_alerts_with_custom_thresholds(summary: Dict, layer_details: Dict, thresholds: Dict) -> List[Dict]: |
| """Evaluate alerts using custom thresholds from UI""" |
| alerts = [] |
| user_id = summary['user_id'] |
| |
| |
| if summary.get('overall_mean_score'): |
| min_score = thresholds['global']['overall_score_min'] |
| if summary['overall_mean_score'] < min_score: |
| alerts.append({ |
| 'user_id': user_id, |
| 'layer': 'Global', |
| 'metric': 'Overall Score', |
| 'value': summary['overall_mean_score'], |
| 'threshold': min_score, |
| 'severity': 'HIGH', |
| 'message': f"Overall score {summary['overall_mean_score']:.3f} below threshold {min_score}" |
| }) |
| |
| |
| if summary.get('layer1_score'): |
| min_score = thresholds['layer1']['any_validator_score_min'] |
| if summary['layer1_score'] < min_score: |
| alerts.append({ |
| 'user_id': user_id, |
| 'layer': 'Layer 1', |
| 'metric': 'Data Integrity', |
| 'value': summary['layer1_score'], |
| 'threshold': min_score, |
| 'severity': 'HIGH', |
| 'message': f"Layer 1 score {summary['layer1_score']:.3f} below threshold {min_score}" |
| }) |
| |
| |
| if 'layer2' in layer_details and user_id in layer_details['layer2']: |
| l2_data = layer_details['layer2'][user_id] |
| |
| |
| dlr = l2_data.get('data_loss_ratio', {}).get('overall_dlr', 0) |
| if dlr > thresholds['layer2']['dlr_max']: |
| alerts.append({ |
| 'user_id': user_id, |
| 'layer': 'Layer 2', |
| 'metric': 'Data Loss Ratio', |
| 'value': dlr, |
| 'threshold': thresholds['layer2']['dlr_max'], |
| 'severity': 'HIGH', |
| 'message': f"DLR {dlr:.2%} exceeds threshold {thresholds['layer2']['dlr_max']:.0%}" |
| }) |
| |
| |
| mdr = l2_data.get('missing_data_ratio', {}).get('overall_mdr', 0) |
| if mdr > thresholds['layer2']['mdr_max']: |
| alerts.append({ |
| 'user_id': user_id, |
| 'layer': 'Layer 2', |
| 'metric': 'Missing Data Ratio', |
| 'value': mdr, |
| 'threshold': thresholds['layer2']['mdr_max'], |
| 'severity': 'MEDIUM', |
| 'message': f"MDR {mdr:.2%} exceeds threshold {thresholds['layer2']['mdr_max']:.0%}" |
| }) |
| |
| |
| wear_time = l2_data.get('wear_time', {}).get('wear_time_percentage', 100) |
| if wear_time < thresholds['layer2']['wear_time_pct_min']: |
| alerts.append({ |
| 'user_id': user_id, |
| 'layer': 'Layer 2', |
| 'metric': 'Wear Time', |
| 'value': wear_time, |
| 'threshold': thresholds['layer2']['wear_time_pct_min'], |
| 'severity': 'MEDIUM', |
| 'message': f"Wear time {wear_time:.1f}% below threshold {thresholds['layer2']['wear_time_pct_min']:.0f}%" |
| }) |
| |
| |
| if 'layer3' in layer_details and 'users' in layer_details['layer3']: |
| if user_id in layer_details['layer3']['users']: |
| l3_data = layer_details['layer3']['users'][user_id] |
| signal_quality = l3_data.get('signal_quality', {}) |
| |
| |
| if 'ppg' in signal_quality and 'summary' in signal_quality['ppg']: |
| ppg_score = signal_quality['ppg']['summary'].get('overall_quality_score', 0) |
| if ppg_score < thresholds['layer3']['ppg_quality_min']: |
| alerts.append({ |
| 'user_id': user_id, |
| 'layer': 'Layer 3', |
| 'metric': 'PPG Quality', |
| 'value': ppg_score, |
| 'threshold': thresholds['layer3']['ppg_quality_min'], |
| 'severity': 'MEDIUM', |
| 'message': f"PPG quality {ppg_score:.3f} below threshold {thresholds['layer3']['ppg_quality_min']}" |
| }) |
| |
| |
| if 'acc' in signal_quality and 'summary' in signal_quality['acc']: |
| acc_score = signal_quality['acc']['summary'].get('quality_score', 0) |
| if acc_score < thresholds['layer3']['acc_quality_min']: |
| alerts.append({ |
| 'user_id': user_id, |
| 'layer': 'Layer 3', |
| 'metric': 'ACC Quality', |
| 'value': acc_score, |
| 'threshold': thresholds['layer3']['acc_quality_min'], |
| 'severity': 'MEDIUM', |
| 'message': f"ACC quality {acc_score:.3f} below threshold {thresholds['layer3']['acc_quality_min']}" |
| }) |
| |
| return alerts |
|
|
|
|
| @st.cache_data(ttl=30) |
| def load_dashboard_data(force_refresh: bool = False): |
| """Load and cache the quality assessment data""" |
| if force_refresh: |
| st.cache_data.clear() |
| |
| report_path = find_latest_full_report() |
| if not report_path: |
| return None, None, None |
| |
| results = load_full_results(report_path) |
| summaries = summarize_users(results) |
| metadata = get_report_metadata(report_path, results) |
| |
| return results, summaries, metadata |
|
|
|
|
| def render_threshold_controls() -> Dict: |
| """Render threshold adjustment controls in sidebar""" |
| st.sidebar.divider() |
| st.sidebar.subheader("ποΈ Adjustable Thresholds") |
| |
| |
| preset = st.sidebar.selectbox( |
| "Threshold Preset:", |
| ["Custom", "Strict (High Quality)", "Standard (Default)", "Lenient (Low Bar)"], |
| index=0, |
| help="Select a preset or customize individual thresholds" |
| ) |
| |
| |
| if preset == "Strict (High Quality)": |
| st.session_state.thresholds = { |
| 'global': {'overall_score_min': 0.90}, |
| 'layer1': {'any_validator_score_min': 0.95}, |
| 'layer2': { |
| 'dlr_max': 0.05, |
| 'mdr_max': 0.10, |
| 'scr_min': 0.90, |
| 'wear_time_pct_min': 80.0, |
| 'plausibility_pct_min': 90.0 |
| }, |
| 'layer3': { |
| 'ppg_quality_min': 0.90, |
| 'eda_wrist_quality_min': 0.85, |
| 'eda_chest_quality_min': 0.85, |
| 'acc_quality_min': 0.90, |
| 'temp_wrist_quality_min': 0.80, |
| 'temp_chest_quality_min': 0.90, |
| 'cross_modality_quality_min': 0.85 |
| } |
| } |
| elif preset == "Standard (Default)": |
| st.session_state.thresholds = load_default_thresholds() |
| elif preset == "Lenient (Low Bar)": |
| st.session_state.thresholds = { |
| 'global': {'overall_score_min': 0.60}, |
| 'layer1': {'any_validator_score_min': 0.60}, |
| 'layer2': { |
| 'dlr_max': 0.25, |
| 'mdr_max': 0.35, |
| 'scr_min': 0.60, |
| 'wear_time_pct_min': 40.0, |
| 'plausibility_pct_min': 60.0 |
| }, |
| 'layer3': { |
| 'ppg_quality_min': 0.60, |
| 'eda_wrist_quality_min': 0.60, |
| 'eda_chest_quality_min': 0.60, |
| 'acc_quality_min': 0.60, |
| 'temp_wrist_quality_min': 0.50, |
| 'temp_chest_quality_min': 0.65, |
| 'cross_modality_quality_min': 0.60 |
| } |
| } |
| |
| |
| with st.sidebar.expander("π― Global Threshold", expanded=False): |
| st.session_state.thresholds['global']['overall_score_min'] = st.slider( |
| "Overall Score Min", |
| min_value=0.0, |
| max_value=1.0, |
| value=st.session_state.thresholds['global']['overall_score_min'], |
| step=0.05, |
| format="%.2f" |
| ) |
| |
| with st.sidebar.expander("π Layer 1 Thresholds", expanded=False): |
| st.session_state.thresholds['layer1']['any_validator_score_min'] = st.slider( |
| "Validator Score Min", |
| min_value=0.0, |
| max_value=1.0, |
| value=st.session_state.thresholds['layer1']['any_validator_score_min'], |
| step=0.05, |
| format="%.2f" |
| ) |
| |
| with st.sidebar.expander("π Layer 2 Thresholds", expanded=False): |
| col1, col2 = st.columns(2) |
| |
| with col1: |
| st.session_state.thresholds['layer2']['dlr_max'] = st.number_input( |
| "DLR Max", |
| min_value=0.0, |
| max_value=1.0, |
| value=st.session_state.thresholds['layer2']['dlr_max'], |
| step=0.05, |
| format="%.2f" |
| ) |
| |
| st.session_state.thresholds['layer2']['mdr_max'] = st.number_input( |
| "MDR Max", |
| min_value=0.0, |
| max_value=1.0, |
| value=st.session_state.thresholds['layer2']['mdr_max'], |
| step=0.05, |
| format="%.2f" |
| ) |
| |
| st.session_state.thresholds['layer2']['scr_min'] = st.number_input( |
| "SCR Min", |
| min_value=0.0, |
| max_value=1.0, |
| value=st.session_state.thresholds['layer2']['scr_min'], |
| step=0.05, |
| format="%.2f" |
| ) |
| |
| with col2: |
| st.session_state.thresholds['layer2']['wear_time_pct_min'] = st.number_input( |
| "Wear Time %", |
| min_value=0.0, |
| max_value=100.0, |
| value=st.session_state.thresholds['layer2']['wear_time_pct_min'], |
| step=5.0, |
| format="%.0f" |
| ) |
| |
| st.session_state.thresholds['layer2']['plausibility_pct_min'] = st.number_input( |
| "Plausibility %", |
| min_value=0.0, |
| max_value=100.0, |
| value=st.session_state.thresholds['layer2']['plausibility_pct_min'], |
| step=5.0, |
| format="%.0f" |
| ) |
| |
| with st.sidebar.expander("π‘ Layer 3 Thresholds", expanded=False): |
| |
| col1, col2 = st.columns(2) |
| with col1: |
| st.session_state.thresholds['layer3']['ppg_quality_min'] = st.slider( |
| "PPG Min", |
| 0.0, 1.0, |
| st.session_state.thresholds['layer3']['ppg_quality_min'], |
| 0.05, format="%.2f", key="ppg_thresh" |
| ) |
| st.session_state.thresholds['layer3']['acc_quality_min'] = st.slider( |
| "ACC Min", |
| 0.0, 1.0, |
| st.session_state.thresholds['layer3']['acc_quality_min'], |
| 0.05, format="%.2f", key="acc_thresh" |
| ) |
| |
| |
| with col2: |
| st.session_state.thresholds['layer3']['eda_wrist_quality_min'] = st.slider( |
| "EDA Wrist", |
| 0.0, 1.0, |
| st.session_state.thresholds['layer3']['eda_wrist_quality_min'], |
| 0.05, format="%.2f", key="eda_w_thresh" |
| ) |
| st.session_state.thresholds['layer3']['eda_chest_quality_min'] = st.slider( |
| "EDA Chest", |
| 0.0, 1.0, |
| st.session_state.thresholds['layer3']['eda_chest_quality_min'], |
| 0.05, format="%.2f", key="eda_c_thresh" |
| ) |
| |
| |
| st.session_state.thresholds['layer3']['temp_wrist_quality_min'] = st.slider( |
| "Temp Wrist Min", |
| 0.0, 1.0, |
| st.session_state.thresholds['layer3']['temp_wrist_quality_min'], |
| 0.05, format="%.2f", key="temp_w_thresh" |
| ) |
| st.session_state.thresholds['layer3']['temp_chest_quality_min'] = st.slider( |
| "Temp Chest Min", |
| 0.0, 1.0, |
| st.session_state.thresholds['layer3']['temp_chest_quality_min'], |
| 0.05, format="%.2f", key="temp_c_thresh" |
| ) |
| |
| |
| st.sidebar.divider() |
| col1, col2 = st.sidebar.columns(2) |
| |
| with col1: |
| if st.button("πΎ Save Config"): |
| config_file = Path("config") / f"thresholds_{datetime.now().strftime('%Y%m%d_%H%M%S')}.yaml" |
| config_file.parent.mkdir(exist_ok=True) |
| with open(config_file, 'w') as f: |
| yaml.dump(st.session_state.thresholds, f) |
| st.success(f"Saved to {config_file.name}") |
| |
| with col2: |
| if st.button("π₯ Export Config"): |
| yaml_str = yaml.dump(st.session_state.thresholds) |
| b64 = base64.b64encode(yaml_str.encode()).decode() |
| href = f'<a href="data:application/yaml;base64,{b64}" download="thresholds.yaml">Download YAML</a>' |
| st.sidebar.markdown(href, unsafe_allow_html=True) |
| |
| return st.session_state.thresholds |
|
|
|
|
| def render_enhanced_sidebar(metadata: Dict, summaries: List[Dict]) -> tuple: |
| """Enhanced sidebar with threshold controls""" |
| st.sidebar.title("ποΈ Dashboard Controls") |
| |
| |
| st.sidebar.markdown(""" |
| <div style="text-align: center; padding: 10px;"> |
| <h4 style="color: #667eea;">WESAD Quality Metrics</h4> |
| <p style="font-size: 12px;">Adjustable Thresholds Edition</p> |
| </div> |
| """, unsafe_allow_html=True) |
| |
| |
| st.sidebar.subheader("π Report Information") |
| if metadata: |
| st.sidebar.info(f""" |
| **File:** {metadata['filename']} |
| **Modified:** {metadata['modified_time'][:19]} |
| **Size:** {metadata['file_size_mb']:.1f} MB |
| **Users:** {metadata['user_count']} |
| """) |
| |
| |
| if st.sidebar.button("π Refresh Data", help="Reload the latest quality assessment"): |
| st.cache_data.clear() |
| st.rerun() |
| |
| st.sidebar.divider() |
| |
| |
| st.sidebar.subheader("π₯ User Selection") |
| all_users = [s['user_id'] for s in summaries] |
| |
| |
| col1, col2 = st.sidebar.columns(2) |
| with col1: |
| if st.button("Select All"): |
| st.session_state['selected_users'] = all_users |
| with col2: |
| if st.button("Clear All"): |
| st.session_state['selected_users'] = [] |
| |
| selected_users = st.sidebar.multiselect( |
| "Select users to analyze:", |
| options=all_users, |
| default=st.session_state.get('selected_users', all_users), |
| key='user_selector' |
| ) |
| st.session_state['selected_users'] = selected_users |
| |
| |
| thresholds = render_threshold_controls() |
| |
| |
| st.sidebar.divider() |
| st.sidebar.subheader("βοΈ Display Settings") |
| show_raw_data = st.sidebar.checkbox("Show raw data tables", value=False) |
| expand_all = st.sidebar.checkbox("Expand all user sections", value=False) |
| show_threshold_lines = st.sidebar.checkbox("Show threshold lines in charts", value=True) |
| |
| return selected_users, thresholds, show_raw_data, expand_all, show_threshold_lines |
|
|
|
|
| def render_overview_with_thresholds(summaries: List[Dict], selected_users: List[str], thresholds: Dict, show_lines: bool): |
| """Overview tab with threshold visualization""" |
| st.header("π Quality Metrics Overview") |
| |
| |
| min_quality = thresholds['global']['overall_score_min'] |
| filtered_summaries = [ |
| s for s in summaries |
| if s['user_id'] in selected_users |
| and (s['overall_mean_score'] or 0) >= min_quality |
| ] |
| |
| |
| st.info(f"ποΈ Current Overall Quality Threshold: **{min_quality:.2f}**") |
| |
| if not filtered_summaries: |
| st.warning(f"No users meet the current threshold ({min_quality:.2f})") |
| st.write(f"Users below threshold: {len([s for s in summaries if s['user_id'] in selected_users])} total") |
| else: |
| |
| col1, col2, col3, col4 = st.columns(4) |
| |
| with col1: |
| st.metric( |
| label="Meeting Threshold", |
| value=len(filtered_summaries), |
| delta=f"of {len([s for s in summaries if s['user_id'] in selected_users])} selected" |
| ) |
| |
| with col2: |
| avg_score = sum(s['overall_mean_score'] for s in filtered_summaries) / len(filtered_summaries) |
| st.metric( |
| label="Mean Score (Above Threshold)", |
| value=f"{avg_score:.3f}", |
| delta=f"Threshold: {min_quality:.2f}" |
| ) |
| |
| with col3: |
| below_threshold = len([s for s in summaries if s['user_id'] in selected_users and (s['overall_mean_score'] or 0) < min_quality]) |
| st.metric( |
| label="Below Threshold", |
| value=below_threshold, |
| delta="Need attention" if below_threshold > 0 else "All pass", |
| delta_color="inverse" if below_threshold > 0 else "normal" |
| ) |
| |
| with col4: |
| |
| total_alerts = 0 |
| for s in summaries: |
| if s['user_id'] in selected_users: |
| if s['overall_mean_score'] and s['overall_mean_score'] < min_quality: |
| total_alerts += 1 |
| st.metric( |
| label="Active Alerts", |
| value=total_alerts, |
| delta="Based on current thresholds" |
| ) |
| |
| st.divider() |
| |
| |
| if len([s for s in summaries if s['user_id'] in selected_users]) > 0: |
| col1, col2 = st.columns(2) |
| |
| with col1: |
| st.subheader("π Score Distribution vs Threshold") |
| |
| |
| all_selected = [s for s in summaries if s['user_id'] in selected_users] |
| scores = [s['overall_mean_score'] for s in all_selected if s['overall_mean_score']] |
| |
| fig = go.Figure() |
| |
| |
| fig.add_trace(go.Histogram( |
| x=scores, |
| nbinsx=20, |
| name='Score Distribution', |
| marker_color='lightblue', |
| opacity=0.7 |
| )) |
| |
| |
| if show_lines: |
| fig.add_vline( |
| x=min_quality, |
| line_dash="dash", |
| line_color="red", |
| annotation_text=f"Threshold ({min_quality:.2f})" |
| ) |
| |
| fig.update_layout( |
| xaxis_title="Overall Quality Score", |
| yaxis_title="Count", |
| showlegend=True, |
| height=400 |
| ) |
| |
| st.plotly_chart(fig, use_container_width=True, key="overview_histogram_adjustable") |
| |
| with col2: |
| st.subheader("π Layer Scores vs Thresholds") |
| |
| |
| layer_data = [] |
| for s in all_selected: |
| if s['layer1_score']: |
| layer_data.append({'Layer': 'Layer 1', 'Score': s['layer1_score']}) |
| if s['layer2_score']: |
| layer_data.append({'Layer': 'Layer 2', 'Score': s['layer2_score']}) |
| if s['layer3_score']: |
| layer_data.append({'Layer': 'Layer 3', 'Score': s['layer3_score']}) |
| |
| if layer_data: |
| df_box = pd.DataFrame(layer_data) |
| fig = px.box(df_box, x='Layer', y='Score', color='Layer') |
| |
| |
| if show_lines: |
| fig.add_hline( |
| y=thresholds['layer1']['any_validator_score_min'], |
| line_dash="dash", |
| line_color="gray", |
| annotation_text="L1 Threshold" |
| ) |
| |
| l2_approx = 1 - thresholds['layer2']['dlr_max'] |
| fig.add_hline( |
| y=l2_approx, |
| line_dash="dot", |
| line_color="gray", |
| annotation_text="L2 Approx" |
| ) |
| fig.add_hline( |
| y=thresholds['layer3']['ppg_quality_min'], |
| line_dash="dashdot", |
| line_color="gray", |
| annotation_text="L3 Threshold" |
| ) |
| |
| fig.update_layout(showlegend=False, height=400) |
| st.plotly_chart(fig, use_container_width=True, key="overview_box_adjustable") |
| |
| |
| st.subheader("π₯ User Quality Summary") |
| |
| if len([s for s in summaries if s['user_id'] in selected_users]) > 0: |
| df_display = pd.DataFrame([{ |
| 'User': s['user_id'], |
| 'Layer 1': f"{s['layer1_score']:.3f}" if s['layer1_score'] else 'N/A', |
| 'Layer 2': f"{s['layer2_score']:.3f}" if s['layer2_score'] else 'N/A', |
| 'Layer 3': f"{s['layer3_score']:.3f}" if s['layer3_score'] else 'N/A', |
| 'Overall': f"{s['overall_mean_score']:.3f}" if s['overall_mean_score'] else 'N/A', |
| 'Status': 'β
Pass' if s['overall_mean_score'] and s['overall_mean_score'] >= min_quality else 'β Fail', |
| 'Margin': f"{(s['overall_mean_score'] - min_quality):.3f}" if s['overall_mean_score'] else 'N/A' |
| } for s in summaries if s['user_id'] in selected_users]) |
| |
| |
| st.dataframe( |
| df_display, |
| use_container_width=True, |
| hide_index=True, |
| column_config={ |
| "Status": st.column_config.TextColumn( |
| "Threshold Status", |
| help=f"Based on threshold {min_quality:.2f}" |
| ), |
| "Margin": st.column_config.TextColumn( |
| "Margin", |
| help="Distance from threshold (positive = pass)" |
| ) |
| } |
| ) |
|
|
|
|
| def render_layer1_with_thresholds(results: Dict, selected_users: List[str], thresholds: Dict, expand_all: bool): |
| """Layer 1 with threshold indicators""" |
| st.header("π Layer 1: Data Integrity") |
| |
| threshold = thresholds['layer1']['any_validator_score_min'] |
| st.info(f"ποΈ Current Layer 1 Threshold: **{threshold:.2f}**") |
| |
| if 'layer1' not in results: |
| st.warning("No Layer 1 data available") |
| return |
| |
| layer1_data = results['layer1'] |
| |
| |
| col1, col2, col3, col4 = st.columns(4) |
| with col1: |
| avg_score = sum(layer1_data[u].get('overall_score', 0) for u in selected_users if u in layer1_data) / len(selected_users) |
| st.metric("Average Layer 1 Score", f"{avg_score:.3f}") |
| with col2: |
| perfect_count = sum(1 for u in selected_users if u in layer1_data and layer1_data[u].get('overall_score', 0) == 1.0) |
| st.metric("Perfect Scores", f"{perfect_count}/{len(selected_users)}") |
| with col3: |
| above_threshold = sum(1 for u in selected_users if u in layer1_data and layer1_data[u].get('overall_score', 0) >= threshold) |
| st.metric("Above Threshold", f"{above_threshold}/{len(selected_users)}") |
| with col4: |
| issues_count = sum(len(layer1_data[u].get('issues', [])) for u in selected_users if u in layer1_data) |
| st.metric("Total Issues", issues_count) |
| |
| st.divider() |
| |
| |
| for user_id in selected_users: |
| if user_id not in layer1_data: |
| continue |
| |
| user_data = layer1_data[user_id] |
| overall_score = user_data.get('overall_score', 0) |
| |
| |
| if overall_score >= threshold: |
| status_icon = "β
" |
| status_text = "Pass" |
| else: |
| status_icon = "β" |
| status_text = f"Below threshold ({threshold:.2f})" |
| |
| with st.expander(f"{status_icon} User: {user_id} - Score: {overall_score:.3f} - {status_text}", expanded=expand_all): |
| col1, col2 = st.columns([1, 2]) |
| |
| with col1: |
| st.metric("Overall Score", f"{overall_score:.3f}") |
| st.metric("Distance from Threshold", f"{overall_score - threshold:+.3f}") |
| |
| |
| st.write("**π Validator Scores:**") |
| validators = ['readme_validator', 'quest_validator', 'pkl_metadata_validator', |
| 'file_format_validator', 'structural_validator'] |
| |
| for validator in validators: |
| if validator in user_data: |
| v_data = user_data[validator] |
| score = v_data.get('score', 0) |
| passed = v_data.get('passed', False) |
| |
| status = "β
" if score >= threshold else "β οΈ" |
| validator_name = validator.replace('_', ' ').title() |
| st.write(f"{status} {validator_name}: {score:.2f}") |
| |
| with col2: |
| st.write("**π Validation Details:**") |
| |
| total_issues = 0 |
| for validator in validators: |
| if validator in user_data: |
| issues = user_data[validator].get('issues', []) |
| if issues: |
| total_issues += len(issues) |
| st.warning(f"{validator}: {len(issues)} issues") |
| for issue in issues[:3]: |
| st.text(f" β’ {issue}") |
| if len(issues) > 3: |
| st.text(f" ... and {len(issues)-3} more") |
| |
| if total_issues == 0: |
| st.success("β
No validation issues found!") |
| else: |
| st.error(f"β οΈ Total issues: {total_issues}") |
|
|
|
|
| def render_layer2_with_thresholds(results: Dict, selected_users: List[str], thresholds: Dict, expand_all: bool): |
| """Layer 2 with threshold indicators""" |
| st.header("π Layer 2: Data Completeness") |
| |
| |
| st.info(f"""ποΈ **Current Layer 2 Thresholds:** |
| β’ DLR Max: {thresholds['layer2']['dlr_max']:.0%} | MDR Max: {thresholds['layer2']['mdr_max']:.0%} |
| β’ SCR Min: {thresholds['layer2']['scr_min']:.0%} | Wear Time Min: {thresholds['layer2']['wear_time_pct_min']:.0f}% |
| β’ Plausibility Min: {thresholds['layer2']['plausibility_pct_min']:.0f}%""") |
| |
| if 'layer2' not in results: |
| st.warning("No Layer 2 data available") |
| return |
| |
| layer2_data = results['layer2'] |
| |
| |
| col1, col2, col3, col4 = st.columns(4) |
| with col1: |
| avg_score = sum(layer2_data[u].get('overall_score', 0) for u in selected_users if u in layer2_data) / len(selected_users) |
| st.metric("Average Layer 2 Score", f"{avg_score:.3f}") |
| with col2: |
| avg_dlr = sum(layer2_data[u].get('data_loss_ratio', {}).get('overall_dlr', 0) for u in selected_users if u in layer2_data) / len(selected_users) |
| color = "normal" if avg_dlr <= thresholds['layer2']['dlr_max'] else "inverse" |
| st.metric("Avg Data Loss Ratio", f"{avg_dlr:.2%}", delta="Pass" if avg_dlr <= thresholds['layer2']['dlr_max'] else "Fail", delta_color=color) |
| with col3: |
| avg_mdr = sum(layer2_data[u].get('missing_data_ratio', {}).get('overall_mdr', 0) for u in selected_users if u in layer2_data) / len(selected_users) |
| color = "normal" if avg_mdr <= thresholds['layer2']['mdr_max'] else "inverse" |
| st.metric("Avg Missing Data Ratio", f"{avg_mdr:.2%}", delta="Pass" if avg_mdr <= thresholds['layer2']['mdr_max'] else "Fail", delta_color=color) |
| with col4: |
| avg_wear = sum(layer2_data[u].get('wear_time', {}).get('wear_time_percentage', 0) for u in selected_users if u in layer2_data) / len(selected_users) |
| color = "normal" if avg_wear >= thresholds['layer2']['wear_time_pct_min'] else "inverse" |
| st.metric("Avg Wear Time", f"{avg_wear:.1f}%", delta="Pass" if avg_wear >= thresholds['layer2']['wear_time_pct_min'] else "Fail", delta_color=color) |
| |
| st.divider() |
| |
| |
| for user_id in selected_users: |
| if user_id not in layer2_data: |
| continue |
| |
| user_data = layer2_data[user_id] |
| overall_score = user_data.get('overall_score', 0) |
| |
| |
| violations = [] |
| dlr = user_data.get('data_loss_ratio', {}).get('overall_dlr', 0) |
| mdr = user_data.get('missing_data_ratio', {}).get('overall_mdr', 0) |
| scr = user_data.get('sensor_channel_ratio', {}).get('overall_scr', 1.0) |
| wear_time = user_data.get('wear_time', {}).get('wear_time_percentage', 100) |
| plausibility = user_data.get('plausibility', {}).get('average_plausible_percentage', 100) |
| |
| if dlr > thresholds['layer2']['dlr_max']: |
| violations.append(f"DLR ({dlr:.0%} > {thresholds['layer2']['dlr_max']:.0%})") |
| if mdr > thresholds['layer2']['mdr_max']: |
| violations.append(f"MDR ({mdr:.0%} > {thresholds['layer2']['mdr_max']:.0%})") |
| if scr < thresholds['layer2']['scr_min']: |
| violations.append(f"SCR ({scr:.0%} < {thresholds['layer2']['scr_min']:.0%})") |
| if wear_time < thresholds['layer2']['wear_time_pct_min']: |
| violations.append(f"Wear ({wear_time:.0f}% < {thresholds['layer2']['wear_time_pct_min']:.0f}%)") |
| if plausibility < thresholds['layer2']['plausibility_pct_min']: |
| violations.append(f"Plaus ({plausibility:.0f}% < {thresholds['layer2']['plausibility_pct_min']:.0f}%)") |
| |
| status_icon = "β
" if not violations else "β" |
| status_text = "All Pass" if not violations else f"{len(violations)} violations" |
| |
| with st.expander(f"{status_icon} User: {user_id} - Score: {overall_score:.3f} - {status_text}", expanded=expand_all): |
| if violations: |
| st.error(f"**Threshold Violations:** {', '.join(violations)}") |
| |
| |
| col1, col2, col3 = st.columns(3) |
| |
| with col1: |
| st.metric( |
| "Data Loss Ratio", |
| f"{dlr:.2%}", |
| delta=f"Threshold: {thresholds['layer2']['dlr_max']:.0%}", |
| delta_color="normal" if dlr <= thresholds['layer2']['dlr_max'] else "inverse" |
| ) |
| |
| st.metric( |
| "Missing Data Ratio", |
| f"{mdr:.2%}", |
| delta=f"Threshold: {thresholds['layer2']['mdr_max']:.0%}", |
| delta_color="normal" if mdr <= thresholds['layer2']['mdr_max'] else "inverse" |
| ) |
| |
| with col2: |
| st.metric( |
| "Sensor Channel Ratio", |
| f"{scr:.2f}", |
| delta=f"Min: {thresholds['layer2']['scr_min']:.2f}", |
| delta_color="normal" if scr >= thresholds['layer2']['scr_min'] else "inverse" |
| ) |
| |
| st.metric( |
| "Wear Time", |
| f"{wear_time:.1f}%", |
| delta=f"Min: {thresholds['layer2']['wear_time_pct_min']:.0f}%", |
| delta_color="normal" if wear_time >= thresholds['layer2']['wear_time_pct_min'] else "inverse" |
| ) |
| |
| with col3: |
| st.metric( |
| "Data Plausibility", |
| f"{plausibility:.1f}%", |
| delta=f"Min: {thresholds['layer2']['plausibility_pct_min']:.0f}%", |
| delta_color="normal" if plausibility >= thresholds['layer2']['plausibility_pct_min'] else "inverse" |
| ) |
| |
| st.metric( |
| "Overall Layer 2", |
| f"{overall_score:.3f}", |
| delta="Pass" if not violations else f"{len(violations)} issues" |
| ) |
|
|
|
|
| def render_layer3_with_thresholds(results: Dict, selected_users: List[str], thresholds: Dict, expand_all: bool): |
| """Layer 3 with threshold indicators and FIXED extraction""" |
| st.header("π‘ Layer 3: Signal Quality") |
| |
| |
| st.info(f"""ποΈ **Current Layer 3 Thresholds:** |
| β’ PPG: {thresholds['layer3']['ppg_quality_min']:.2f} | ACC: {thresholds['layer3']['acc_quality_min']:.2f} |
| β’ EDA Wrist: {thresholds['layer3']['eda_wrist_quality_min']:.2f} | EDA Chest: {thresholds['layer3']['eda_chest_quality_min']:.2f} |
| β’ Temp Wrist: {thresholds['layer3']['temp_wrist_quality_min']:.2f} | Temp Chest: {thresholds['layer3']['temp_chest_quality_min']:.2f}""") |
| |
| if 'layer3' not in results or 'users' not in results['layer3']: |
| st.warning("No Layer 3 data available") |
| return |
| |
| layer3_users = results['layer3']['users'] |
| |
| |
| col1, col2, col3 = st.columns(3) |
| |
| valid_scores = [] |
| for user_id in selected_users: |
| if user_id in layer3_users: |
| score = layer3_users[user_id].get('overall_score', None) |
| if score: |
| valid_scores.append(score) |
| |
| with col1: |
| if valid_scores: |
| avg_score = sum(valid_scores) / len(valid_scores) |
| st.metric("Average Layer 3 Score", f"{avg_score:.3f}") |
| with col2: |
| |
| above_threshold = sum(1 for s in valid_scores if s > thresholds['layer3']['ppg_quality_min']) |
| st.metric("Above Threshold", f"{above_threshold}/{len(valid_scores)}") |
| with col3: |
| st.metric("Modalities Analyzed", "6") |
| |
| st.divider() |
| |
| |
| for user_id in selected_users: |
| if user_id not in layer3_users: |
| continue |
| |
| user_data = layer3_users[user_id] |
| overall_score = user_data.get('overall_score', 0) |
| |
| |
| signal_quality = user_data.get('signal_quality', {}) |
| |
| |
| violations = [] |
| scores = [] |
| labels = [] |
| details = [] |
| |
| |
| if 'ppg' in signal_quality and 'summary' in signal_quality['ppg']: |
| summary = signal_quality['ppg']['summary'] |
| score = summary.get('overall_quality_score', None) |
| if score is not None: |
| scores.append(score) |
| labels.append('PPG') |
| if score < thresholds['layer3']['ppg_quality_min']: |
| violations.append(f"PPG ({score:.2f} < {thresholds['layer3']['ppg_quality_min']:.2f})") |
| details.append({ |
| 'Windows': summary.get('total_windows_analyzed', 0), |
| 'SNR': summary.get('mean_snr', 0), |
| 'Threshold': thresholds['layer3']['ppg_quality_min'] |
| }) |
| |
| |
| if 'acc' in signal_quality and 'summary' in signal_quality['acc']: |
| summary = signal_quality['acc']['summary'] |
| score = summary.get('quality_score', None) |
| if score is not None: |
| scores.append(score) |
| labels.append('ACC') |
| if score < thresholds['layer3']['acc_quality_min']: |
| violations.append(f"ACC ({score:.2f} < {thresholds['layer3']['acc_quality_min']:.2f})") |
| details.append({ |
| 'Issues': summary.get('total_issues_detected', 0), |
| 'MAI': summary.get('mai_assessment', 'N/A'), |
| 'Threshold': thresholds['layer3']['acc_quality_min'] |
| }) |
| |
| |
| if 'eda' in signal_quality: |
| if 'wrist' in signal_quality['eda'] and 'summary' in signal_quality['eda']['wrist']: |
| summary = signal_quality['eda']['wrist']['summary'] |
| score = summary.get('overall_quality_score', None) |
| if score is not None: |
| scores.append(score) |
| labels.append('EDA WRIST') |
| if score < thresholds['layer3']['eda_wrist_quality_min']: |
| violations.append(f"EDA Wrist ({score:.2f} < {thresholds['layer3']['eda_wrist_quality_min']:.2f})") |
| details.append({ |
| 'SCR Peaks': summary.get('total_scr_peaks', 0), |
| 'Threshold': thresholds['layer3']['eda_wrist_quality_min'] |
| }) |
| |
| if 'chest' in signal_quality['eda'] and 'summary' in signal_quality['eda']['chest']: |
| summary = signal_quality['eda']['chest']['summary'] |
| score = summary.get('overall_quality_score', None) |
| if score is not None: |
| scores.append(score) |
| labels.append('EDA CHEST') |
| if score < thresholds['layer3']['eda_chest_quality_min']: |
| violations.append(f"EDA Chest ({score:.2f} < {thresholds['layer3']['eda_chest_quality_min']:.2f})") |
| details.append({ |
| 'SCR Peaks': summary.get('total_scr_peaks', 0), |
| 'Threshold': thresholds['layer3']['eda_chest_quality_min'] |
| }) |
| |
| |
| if 'temperature' in signal_quality: |
| if 'wrist' in signal_quality['temperature'] and 'summary' in signal_quality['temperature']['wrist']: |
| summary = signal_quality['temperature']['wrist']['summary'] |
| score = summary.get('overall_quality_score', None) |
| if score is not None: |
| scores.append(score) |
| labels.append('TEMP WRIST') |
| if score < thresholds['layer3']['temp_wrist_quality_min']: |
| violations.append(f"Temp Wrist ({score:.2f} < {thresholds['layer3']['temp_wrist_quality_min']:.2f})") |
| details.append({ |
| 'Mean Temp': f"{summary.get('mean_temperature', 0):.1f}Β°C", |
| 'Threshold': thresholds['layer3']['temp_wrist_quality_min'] |
| }) |
| |
| if 'chest' in signal_quality['temperature'] and 'summary' in signal_quality['temperature']['chest']: |
| summary = signal_quality['temperature']['chest']['summary'] |
| score = summary.get('overall_quality_score', None) |
| if score is not None: |
| scores.append(score) |
| labels.append('TEMP CHEST') |
| if score < thresholds['layer3']['temp_chest_quality_min']: |
| violations.append(f"Temp Chest ({score:.2f} < {thresholds['layer3']['temp_chest_quality_min']:.2f})") |
| mean_temp = summary.get('mean_temperature', 'N/A') |
| if isinstance(mean_temp, str): |
| temp_str = mean_temp |
| else: |
| temp_str = f"{float(mean_temp):.1f}Β°C" |
| details.append({ |
| 'Mean Temp': temp_str, |
| 'Threshold': thresholds['layer3']['temp_chest_quality_min'] |
| }) |
| |
| status_icon = "β
" if not violations else "β" |
| status_text = "All Pass" if not violations else f"{len(violations)} below threshold" |
| |
| with st.expander(f"{status_icon} User: {user_id} - Score: {overall_score:.3f} - {status_text}", expanded=expand_all): |
| st.metric("Overall Signal Quality", f"{overall_score:.3f}") |
| |
| if violations: |
| st.error(f"**Threshold Violations:** {', '.join(violations[:3])}") |
| if len(violations) > 3: |
| st.error(f"... and {len(violations)-3} more") |
| |
| st.divider() |
| |
| if scores: |
| |
| fig = go.Figure() |
| |
| |
| colors = [] |
| for i, (label, score) in enumerate(zip(labels, scores)): |
| |
| if 'PPG' in label: |
| thresh = thresholds['layer3']['ppg_quality_min'] |
| elif 'ACC' in label: |
| thresh = thresholds['layer3']['acc_quality_min'] |
| elif 'EDA WRIST' in label: |
| thresh = thresholds['layer3']['eda_wrist_quality_min'] |
| elif 'EDA CHEST' in label: |
| thresh = thresholds['layer3']['eda_chest_quality_min'] |
| elif 'TEMP WRIST' in label: |
| thresh = thresholds['layer3']['temp_wrist_quality_min'] |
| elif 'TEMP CHEST' in label: |
| thresh = thresholds['layer3']['temp_chest_quality_min'] |
| else: |
| thresh = 0.8 |
| |
| color = 'green' if score >= thresh else 'red' |
| colors.append(color) |
| |
| fig.add_trace(go.Bar( |
| x=labels, |
| y=scores, |
| marker_color=colors, |
| text=[f"{s:.3f}" for s in scores], |
| textposition='outside' |
| )) |
| |
| |
| for i, (label, detail) in enumerate(zip(labels, details)): |
| if 'Threshold' in detail: |
| fig.add_shape( |
| type="line", |
| x0=i-0.4, x1=i+0.4, |
| y0=detail['Threshold'], y1=detail['Threshold'], |
| line=dict(color="gray", width=2, dash="dash") |
| ) |
| |
| fig.update_layout( |
| title=f"Signal Quality vs Thresholds - {user_id}", |
| yaxis=dict(range=[0, 1.1], title="Quality Score"), |
| xaxis=dict(title="Modality"), |
| showlegend=False, |
| height=350 |
| ) |
| |
| st.plotly_chart(fig, use_container_width=True, key=f"layer3_bar_thresh_{user_id}") |
| |
| |
| st.subheader("π Detailed Metrics") |
| cols = st.columns(3) |
| |
| for idx, (label, score, detail) in enumerate(zip(labels, scores, details)): |
| col_idx = idx % 3 |
| with cols[col_idx]: |
| thresh = detail.get('Threshold', 0.8) |
| status = "β
" if score >= thresh else "β" |
| st.write(f"**{status} {label}**") |
| st.write(f"Score: {score:.3f}") |
| st.write(f"Threshold: {thresh:.2f}") |
| st.write(f"Margin: {score - thresh:+.3f}") |
| for key, value in detail.items(): |
| if key != 'Threshold': |
| st.write(f"β’ {key}: {value}") |
| else: |
| st.info("No signal quality data available for this user") |
|
|
|
|
| def render_alerts_with_thresholds(summaries: List[Dict], results: Dict, selected_users: List[str], thresholds: Dict): |
| """Alerts tab showing violations based on current thresholds""" |
| st.header("β οΈ Quality Alerts - Dynamic Thresholds") |
| |
| |
| with st.expander("π Current Threshold Settings", expanded=True): |
| col1, col2, col3 = st.columns(3) |
| |
| with col1: |
| st.write("**Global:**") |
| st.write(f"β’ Overall Min: {thresholds['global']['overall_score_min']:.2f}") |
| st.write("**Layer 1:**") |
| st.write(f"β’ Validator Min: {thresholds['layer1']['any_validator_score_min']:.2f}") |
| |
| with col2: |
| st.write("**Layer 2:**") |
| st.write(f"β’ DLR Max: {thresholds['layer2']['dlr_max']:.0%}") |
| st.write(f"β’ MDR Max: {thresholds['layer2']['mdr_max']:.0%}") |
| st.write(f"β’ Wear Time Min: {thresholds['layer2']['wear_time_pct_min']:.0f}%") |
| |
| with col3: |
| st.write("**Layer 3:**") |
| st.write(f"β’ PPG Min: {thresholds['layer3']['ppg_quality_min']:.2f}") |
| st.write(f"β’ ACC Min: {thresholds['layer3']['acc_quality_min']:.2f}") |
| st.write(f"β’ EDA Min: {thresholds['layer3']['eda_wrist_quality_min']:.2f}") |
| |
| st.divider() |
| |
| |
| all_alerts = [] |
| for summary in summaries: |
| if summary['user_id'] not in selected_users: |
| continue |
| |
| user_alerts = evaluate_alerts_with_custom_thresholds(summary, results, thresholds) |
| all_alerts.extend(user_alerts) |
| |
| |
| col1, col2, col3, col4 = st.columns(4) |
| |
| with col1: |
| st.metric("Total Alerts", len(all_alerts)) |
| with col2: |
| high_alerts = sum(1 for a in all_alerts if a['severity'] == 'HIGH') |
| st.metric("High Severity", high_alerts, delta_color="inverse" if high_alerts > 0 else "off") |
| with col3: |
| medium_alerts = sum(1 for a in all_alerts if a['severity'] == 'MEDIUM') |
| st.metric("Medium Severity", medium_alerts) |
| with col4: |
| users_with_alerts = len(set(a['user_id'] for a in all_alerts)) |
| st.metric("Users Affected", f"{users_with_alerts}/{len(selected_users)}") |
| |
| st.divider() |
| |
| if all_alerts: |
| st.subheader("π¨ Active Alerts") |
| |
| |
| alerts_df = pd.DataFrame(all_alerts) |
| |
| |
| for severity in ['HIGH', 'MEDIUM']: |
| severity_alerts = alerts_df[alerts_df['severity'] == severity] |
| if not severity_alerts.empty: |
| if severity == 'HIGH': |
| st.error(f"**High Severity Alerts ({len(severity_alerts)})**") |
| else: |
| st.warning(f"**Medium Severity Alerts ({len(severity_alerts)})**") |
| |
| for _, alert in severity_alerts.iterrows(): |
| col1, col2, col3 = st.columns([2, 3, 2]) |
| with col1: |
| st.write(f"**{alert['user_id']}** - {alert['layer']}") |
| with col2: |
| st.write(alert['message']) |
| with col3: |
| st.write(f"Value: {alert['value']:.3f}") |
| |
| |
| st.divider() |
| st.subheader("π Alert Statistics") |
| |
| col1, col2 = st.columns(2) |
| |
| with col1: |
| |
| layer_counts = alerts_df['layer'].value_counts() |
| fig = px.pie(values=layer_counts.values, names=layer_counts.index, title="Alerts by Layer") |
| st.plotly_chart(fig, use_container_width=True, key="alerts_by_layer") |
| |
| with col2: |
| |
| metric_counts = alerts_df['metric'].value_counts().head(5) |
| fig = px.bar(x=metric_counts.values, y=metric_counts.index, orientation='h', title="Top 5 Metrics with Alerts") |
| st.plotly_chart(fig, use_container_width=True, key="alerts_by_metric") |
| |
| else: |
| st.success("β
No alerts triggered with current thresholds!") |
| st.balloons() |
| |
| |
| st.info("π‘ Adjust thresholds in the sidebar to explore different quality criteria") |
|
|
|
|
| def main(): |
| """Main dashboard application with adjustable thresholds""" |
| |
| |
| initialize_threshold_state() |
| |
| st.title("ποΈ WESAD Quality Metrics Dashboard - Adjustable Thresholds") |
| st.markdown("**Interactive threshold adjustment for dynamic quality assessment**") |
| |
| |
| with st.spinner("Loading quality assessment data..."): |
| results, summaries, metadata = load_dashboard_data() |
| |
| if not results: |
| st.error("β No quality assessment report found!") |
| st.info("Please run: `python3 run_full.py --save` to generate a report") |
| return |
| |
| |
| selected_users, thresholds, show_raw_data, expand_all, show_threshold_lines = render_enhanced_sidebar(metadata, summaries) |
| |
| |
| tab1, tab2, tab3, tab4, tab5, tab6 = st.tabs([ |
| "π Overview", |
| "π Layer 1: Integrity", |
| "π Layer 2: Completeness", |
| "π‘ Layer 3: Signal Quality", |
| "β οΈ Dynamic Alerts", |
| "π Threshold Analysis" |
| ]) |
| |
| with tab1: |
| render_overview_with_thresholds(summaries, selected_users, thresholds, show_threshold_lines) |
| |
| with tab2: |
| render_layer1_with_thresholds(results, selected_users, thresholds, expand_all) |
| |
| with tab3: |
| render_layer2_with_thresholds(results, selected_users, thresholds, expand_all) |
| |
| with tab4: |
| render_layer3_with_thresholds(results, selected_users, thresholds, expand_all) |
| |
| with tab5: |
| render_alerts_with_thresholds(summaries, results, selected_users, thresholds) |
| |
| with tab6: |
| st.header("π Threshold Sensitivity Analysis") |
| |
| |
| st.subheader("Impact of Current Thresholds") |
| |
| |
| threshold_ranges = { |
| 'Strict (0.90)': 0.90, |
| 'High (0.85)': 0.85, |
| 'Current': thresholds['global']['overall_score_min'], |
| 'Standard (0.80)': 0.80, |
| 'Moderate (0.70)': 0.70, |
| 'Lenient (0.60)': 0.60 |
| } |
| |
| pass_rates = [] |
| for name, thresh in threshold_ranges.items(): |
| passing = sum(1 for s in summaries if s['user_id'] in selected_users and s.get('overall_mean_score', 0) >= thresh) |
| total = len([s for s in summaries if s['user_id'] in selected_users]) |
| pass_rates.append({ |
| 'Threshold': f"{name} ({thresh:.2f})", |
| 'Pass Rate': (passing / total * 100) if total > 0 else 0, |
| 'Passing': passing, |
| 'Failing': total - passing |
| }) |
| |
| df_pass = pd.DataFrame(pass_rates) |
| |
| col1, col2 = st.columns(2) |
| |
| with col1: |
| fig = px.bar(df_pass, x='Threshold', y='Pass Rate', title="Pass Rate by Threshold Level") |
| fig.add_hline(y=80, line_dash="dash", annotation_text="80% target") |
| st.plotly_chart(fig, use_container_width=True, key="pass_rate_analysis") |
| |
| with col2: |
| fig = px.bar(df_pass, x='Threshold', y=['Passing', 'Failing'], |
| title="Users Passing vs Failing", barmode='stack') |
| st.plotly_chart(fig, use_container_width=True, key="pass_fail_stack") |
| |
| |
| st.subheader("Detailed Threshold Impact") |
| st.dataframe(df_pass, use_container_width=True, hide_index=True) |
| |
| |
| st.divider() |
| st.caption(f"Adjustable Thresholds Dashboard | Data: {metadata['filename']} | Updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|