clockclock commited on
Commit
eadab79
·
verified ·
1 Parent(s): ab34e07

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +128 -137
app.py CHANGED
@@ -31,7 +31,6 @@ class EnhancedAIvsRealGazeAnalyzer:
31
  self.feature_names = []
32
 
33
  def _find_and_standardize_participant_col(self, df, filename):
34
- """Finds, renames, and type-converts the participant ID column."""
35
  participant_col = next((c for c in df.columns if 'participant' in str(c).lower()), None)
36
  if not participant_col:
37
  raise ValueError(f"Could not find a 'participant' column in the file: {filename}")
@@ -41,85 +40,164 @@ class EnhancedAIvsRealGazeAnalyzer:
41
 
42
  def load_and_process_data(self, base_path, response_file_path):
43
  print("--- Starting Robust Data Loading ---")
44
- # 1. Load and Standardize Response Data
45
- print("Loading response sheet...")
46
  response_df = pd.read_excel(response_file_path)
47
  response_df = self._find_and_standardize_participant_col(response_df, "GenAI Response.xlsx")
48
  for pair, ans in self.correct_answers.items():
49
  if pair in response_df.columns:
50
  response_df[f'{pair}_Correct'] = (response_df[pair].astype(str).str.strip().str.upper() == ans)
51
-
52
  response_long = response_df.melt(id_vars=['participant_id'], value_vars=self.correct_answers.keys(), var_name='Pair')
53
  correctness_long = response_df.melt(id_vars=['participant_id'], value_vars=[f'{p}_Correct' for p in self.correct_answers.keys()], var_name='Pair_Correct_Col', value_name='Correct')
54
  correctness_long['Pair'] = correctness_long['Pair_Correct_Col'].str.replace('_Correct', '')
55
  response_long = response_long.merge(correctness_long[['participant_id', 'Pair', 'Correct']], on=['participant_id', 'Pair'])
56
 
57
- # 2. Load and Standardize Metrics & Fixation Data
58
  all_metrics_dfs = []
59
  for q in self.questions:
60
- file_path = f"{base_path}/Filtered_GenAI_Metrics_cleaned_{q}.xlsx"
61
- print(f"Processing {file_path}...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  if os.path.exists(file_path):
63
  xls = pd.ExcelFile(file_path)
64
-
65
- # Metrics Data
66
  metrics_df = pd.read_excel(xls, sheet_name=0)
67
  metrics_df = self._find_and_standardize_participant_col(metrics_df, f"{q} Metrics")
68
  metrics_df['Question'] = q
69
  all_metrics_dfs.append(metrics_df)
70
 
71
- # Fixation Data
72
- if 'Fixation-based AOI' in xls.sheet_names:
73
- fix_df = pd.read_excel(xls, sheet_name='Fixation-based AOI')
74
- fix_df = self._find_and_standardize_participant_col(fix_df, f"{q} Fixations")
75
- fix_df.dropna(subset=['Fixation point X', 'Fixation point Y', 'Gaze event duration (ms)'], inplace=True)
76
- fix_df['Question'] = q
77
- for participant, group in fix_df.groupby('participant_id'):
78
- self.fixation_data[(participant, q)] = group.reset_index(drop=True)
 
79
 
80
  if not all_metrics_dfs: raise ValueError("No aggregated metrics files were found.")
81
  self.combined_data = pd.concat(all_metrics_dfs, ignore_index=True)
82
-
83
- # 3. Merge with Confidence
84
- print("Merging all data sources...")
85
- q_to_pair = {f'Q{i+1}': f'Pair{i+1}' for i in range(6)}
 
 
 
 
 
 
 
86
  self.combined_data['Pair'] = self.combined_data['Question'].map(q_to_pair)
87
  self.combined_data = self.combined_data.merge(response_long, on=['participant_id', 'Pair'], how='left')
88
- self.combined_data['Answer_Correctness'] = self.combined_data['Correct'].map({True: 'Correct', False: 'Incorrect'})
89
-
90
- # 4. Finalize class attributes
 
 
 
 
 
 
 
91
  self.numeric_cols = self.combined_data.select_dtypes(include=np.number).columns.tolist()
92
- self.time_metrics = [c for c in self.numeric_cols if any(k in c.lower() for k in ['time', 'duration', 'fixation'])]
93
- self.participant_list = sorted(self.combined_data['participant_id'].unique().tolist())
94
- print("--- Data Loading Successful ---")
 
 
 
 
 
 
 
95
  return self
96
 
97
- def run_prediction_model(self, test_size, n_estimators):
98
- leaky_features = ['Total_Correct', 'Overall_Accuracy', 'Correct', 'participant_id']
99
- self.feature_names = [col for col in self.numeric_cols if col not in leaky_features and col in self.combined_data.columns]
 
 
 
 
 
100
  features = self.combined_data[self.feature_names].copy()
101
- target = self.combined_data['Answer_Correctness'].map({'Correct': 1, 'Incorrect': 0})
102
- valid_indices = target.notna()
103
- features, target = features[valid_indices], target[valid_indices]
 
 
 
 
104
  features = features.fillna(features.median()).fillna(0)
105
- if len(target.unique()) < 2: return "Not enough data to train model.", None, None
106
- X_train, X_test, y_train, y_test = train_test_split(features, target, test_size=test_size, random_state=42, stratify=target)
107
- self.scaler = StandardScaler().fit(X_train)
108
- X_train_scaled = self.scaler.transform(X_train)
109
- self.model = RandomForestClassifier(n_estimators=n_estimators, random_state=42, class_weight='balanced').fit(X_train_scaled, y_train)
110
- report = classification_report(y_test, self.model.predict(self.scaler.transform(X_test)), target_names=['Incorrect', 'Correct'], output_dict=True)
111
- auc_score = roc_auc_score(y_test, self.model.predict_proba(self.scaler.transform(X_test))[:, 1])
112
- summary_md = f"### Model Performance\n- **AUC Score:** **{auc_score:.4f}**\n- **Overall Accuracy:** {report['accuracy']:.3f}"
 
 
 
 
 
 
 
 
113
  report_df = pd.DataFrame(report).transpose().round(3)
114
- feature_importance = pd.DataFrame({'Feature': self.feature_names, 'Importance': self.model.feature_importances_}).sort_values('Importance', ascending=False).head(15)
115
- fig, ax = plt.subplots(figsize=(10, 8)); sns.barplot(data=feature_importance, x='Importance', y='Feature', ax=ax, palette='viridis'); ax.set_title(f'Top 15 Predictive Features (n_estimators={n_estimators})', fontsize=14); plt.tight_layout()
 
 
 
 
 
 
116
  return summary_md, report_df, fig
117
 
118
- def _recalculate_features_from_fixations(self, fixations_df):
 
119
  feature_vector = pd.Series(0.0, index=self.feature_names)
120
  if fixations_df.empty: return feature_vector.fillna(0).values.reshape(1, -1)
121
  if 'AOI name' in fixations_df.columns:
122
- for aoi_name, group in fixations_df.groupby('AOI name'):
123
  col_name = f'Total fixation duration on {aoi_name}'
124
  if col_name in feature_vector.index:
125
  feature_vector[col_name] = group['Gaze event duration (ms)'].sum()
@@ -129,107 +207,20 @@ class EnhancedAIvsRealGazeAnalyzer:
129
  def generate_gaze_playback(self, participant, question, fixation_num):
130
  trial_key = (str(participant), question)
131
  if not participant or not question or trial_key not in self.fixation_data:
132
- return "Please select a valid trial with fixation data.", None, gr.Slider(interactive=False)
 
133
  all_fixations = self.fixation_data[trial_key]
134
  fixation_num = int(fixation_num)
135
  slider_max = len(all_fixations)
136
  if fixation_num > slider_max: fixation_num = slider_max
137
  current_fixations = all_fixations.iloc[:fixation_num]
 
138
  partial_features = self._recalculate_features_from_fixations(current_fixations)
139
  prediction_prob = self.model.predict_proba(self.scaler.transform(partial_features))[0]
140
  prob_correct = prediction_prob[1]
 
141
  fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8), gridspec_kw={'height_ratios': [4, 1]})
142
- fig.suptitle(f"Gaze Playback for {participant} - {question}", fontsize=16, weight='bold')
143
- ax1.set_title(f"Displaying Fixations 1 through {fixation_num}/{slider_max}")
144
- ax1.set_xlim(0, 1920); ax1.set_ylim(1080, 0)
145
- ax1.set_aspect('equal'); ax1.tick_params(left=False, right=False, bottom=False, top=False, labelleft=False, labelbottom=False)
146
- ax1.add_patch(patches.Rectangle((0, 0), 1920/2, 1080, facecolor='black', alpha=0.05))
147
- ax1.add_patch(patches.Rectangle((1920/2, 0), 1920/2, 1080, facecolor='blue', alpha=0.05))
148
- ax1.text(1920*0.25, 50, "Image A", ha='center', fontsize=14, alpha=0.5)
149
- ax1.text(1920*0.75, 50, "Image B", ha='center', fontsize=14, alpha=0.5)
150
- if not current_fixations.empty:
151
- points = current_fixations[['Fixation point X', 'Fixation point Y']]
152
- ax1.plot(points['Fixation point X'], points['Fixation point Y'], marker='o', color='grey', alpha=0.5, linestyle='-')
153
- ax1.scatter(points.iloc[-1]['Fixation point X'], points.iloc[-1]['Fixation point Y'], s=150, c='red', zorder=10, edgecolors='black')
154
- ax2.set_xlim(0, 1); ax2.set_yticks([])
155
- ax2.set_title("Live Prediction Confidence (Answer is 'Correct')")
156
- bar_color = 'green' if prob_correct > 0.5 else 'red'
157
- ax2.barh([0], [prob_correct], color=bar_color, height=0.5)
158
- ax2.axvline(0.5, color='black', linestyle='--', linewidth=1)
159
- ax2.text(prob_correct, 0, f" {prob_correct:.1%} ", va='center', ha='left' if prob_correct < 0.9 else 'right', color='white', weight='bold')
160
- plt.tight_layout(rect=[0, 0, 1, 0.95])
161
- trial_info = self.combined_data[(self.combined_data['participant_id'] == str(participant)) & (self.combined_data['Question'] == question)].iloc[0]
162
- summary_text = f"**Actual Answer:** `{trial_info['Answer_Correctness']}`"
163
- return summary_text, fig, gr.Slider(maximum=slider_max, value=fixation_num, interactive=True)
164
-
165
- def analyze_rq1_metric(self, metric): # Added this back just in case
166
- if not metric or metric not in self.combined_data.columns: return None, "Metric not found."
167
- correct = self.combined_data.loc[self.combined_data['Answer_Correctness'] == 'Correct', metric].dropna()
168
- incorrect = self.combined_data.loc[self.combined_data['Answer_Correctness'] == 'Incorrect', metric].dropna()
169
- if len(correct) < 2 or len(incorrect) < 2: return None, "Not enough data for both groups to compare."
170
- t_stat, p_val = stats.ttest_ind(incorrect, correct, equal_var=False, nan_policy='omit')
171
- fig, ax = plt.subplots(figsize=(8, 6)); sns.boxplot(data=self.combined_data, x='Answer_Correctness', y=metric, ax=ax, palette=['#66b3ff','#ff9999']); ax.set_title(f'Comparison of "{metric}" by Answer Correctness', fontsize=14); ax.set_xlabel("Answer Correctness"); ax.set_ylabel(metric); plt.tight_layout()
172
- summary = f"""### Analysis for: **{metric}**\n- **Mean (Correct Answers):** {correct.mean():.4f}\n- **Mean (Incorrect Answers):** {incorrect.mean():.4f}\n- **T-test p-value:** {p_val:.4f}\n\n**Conclusion:**\n- {'There is a **statistically significant** difference (p < 0.05).' if p_val < 0.05 else 'There is **no statistically significant** difference (p >= 0.05).'}"""
173
- return fig, summary
174
-
175
-
176
- # --- DATA SETUP & GRADIO APP ---
177
- def setup_and_load_data():
178
- repo_url = "https://github.com/RextonRZ/GenAIEyeTrackingCleanedDataset"
179
- repo_dir = "GenAIEyeTrackingCleanedDataset"
180
- if not os.path.exists(repo_dir): git.Repo.clone_from(repo_url, repo_dir)
181
- else: print("Data repository already exists.")
182
- base_path = repo_dir
183
- response_file_path = os.path.join(repo_dir, "GenAI Response.xlsx")
184
- analyzer = EnhancedAIvsRealGazeAnalyzer().load_and_process_data(base_path, response_file_path)
185
- return analyzer
186
-
187
- analyzer = setup_and_load_data()
188
-
189
- with gr.Blocks(theme=gr.themes.Soft()) as demo:
190
- gr.Markdown("# Interactive Dashboard: AI vs. Real Gaze Analysis")
191
- with gr.Tabs():
192
- with gr.TabItem("📊 RQ1: Viewing Time vs. Correctness"):
193
- with gr.Row():
194
- with gr.Column(scale=1):
195
- rq1_metric_dropdown=gr.Dropdown(choices=analyzer.time_metrics, label="Select a Time-Based Metric", value=analyzer.time_metrics[0] if analyzer.time_metrics else None)
196
- rq1_summary_output=gr.Markdown(label="Statistical Summary")
197
- with gr.Column(scale=2):
198
- rq1_plot_output=gr.Plot(label="Metric Comparison")
199
- with gr.TabItem("🤖 RQ2: Predicting Correctness from Gaze"):
200
- with gr.Row():
201
- with gr.Column(scale=1):
202
- gr.Markdown("#### Tune Model Hyperparameters")
203
- rq2_test_size_slider=gr.Slider(minimum=0.1, maximum=0.5, step=0.05, value=0.3, label="Test Set Size")
204
- rq2_estimators_slider=gr.Slider(minimum=10, maximum=200, step=10, value=100, label="Number of Trees")
205
- with gr.Column(scale=2):
206
- rq2_summary_output=gr.Markdown(label="Model Performance Summary")
207
- rq2_table_output=gr.Dataframe(label="Classification Report", interactive=False)
208
- rq2_plot_output=gr.Plot(label="Feature Importance")
209
- with gr.TabItem("👁️ Gaze Playback & Real-Time Prediction"):
210
- gr.Markdown("### See the Prediction Evolve with Every Glance!")
211
- with gr.Row():
212
- with gr.Column(scale=1):
213
- playback_participant=gr.Dropdown(choices=analyzer.participant_list, label="Select Participant")
214
- playback_question=gr.Dropdown(choices=analyzer.questions, label="Select Question")
215
- gr.Markdown("Use the slider to play back fixations one by one.")
216
- playback_slider=gr.Slider(minimum=0, maximum=1, step=1, value=0, label="Fixation Number", interactive=False)
217
- playback_summary=gr.Markdown(label="Trial Info")
218
- with gr.Column(scale=2):
219
- playback_plot=gr.Plot(label="Gaze Playback & Live Prediction")
220
-
221
- outputs_rq2 = [rq2_summary_output, rq2_table_output, rq2_plot_output]
222
- outputs_playback = [playback_summary, playback_plot, playback_slider]
223
- rq1_metric_dropdown.change(fn=analyzer.analyze_rq1_metric, inputs=rq1_metric_dropdown, outputs=[rq1_plot_output, rq1_summary_output])
224
- rq2_test_size_slider.release(fn=analyzer.run_prediction_model, inputs=[rq2_test_size_slider, rq2_estimators_slider], outputs=outputs_rq2)
225
- rq2_estimators_slider.release(fn=analyzer.run_prediction_model, inputs=[rq2_test_size_slider, rq2_estimators_slider], outputs=outputs_rq2)
226
- playback_inputs = [playback_participant, playback_question, playback_slider]
227
- playback_participant.change(lambda: 0, None, playback_slider).then(fn=analyzer.generate_gaze_playback, inputs=playback_inputs, outputs=outputs_playback)
228
- playback_question.change(lambda: 0, None, playback_slider).then(fn=analyzer.generate_gaze_playback, inputs=playback_inputs, outputs=outputs_playback)
229
- playback_slider.release(fn=analyzer.generate_gaze_playback, inputs=playback_inputs, outputs=outputs_playback)
230
-
231
- demo.load(fn=analyzer.analyze_rq1_metric, inputs=rq1_metric_dropdown, outputs=[rq1_plot_output, rq1_summary_output])
232
- demo.load(fn=analyzer.run_prediction_model, inputs=[rq2_test_size_slider, rq2_estimators_slider], outputs=outputs_rq2)
233
 
234
  if __name__ == "__main__":
235
  demo.launch()
 
31
  self.feature_names = []
32
 
33
  def _find_and_standardize_participant_col(self, df, filename):
 
34
  participant_col = next((c for c in df.columns if 'participant' in str(c).lower()), None)
35
  if not participant_col:
36
  raise ValueError(f"Could not find a 'participant' column in the file: {filename}")
 
40
 
41
  def load_and_process_data(self, base_path, response_file_path):
42
  print("--- Starting Robust Data Loading ---")
 
 
43
  response_df = pd.read_excel(response_file_path)
44
  response_df = self._find_and_standardize_participant_col(response_df, "GenAI Response.xlsx")
45
  for pair, ans in self.correct_answers.items():
46
  if pair in response_df.columns:
47
  response_df[f'{pair}_Correct'] = (response_df[pair].astype(str).str.strip().str.upper() == ans)
 
48
  response_long = response_df.melt(id_vars=['participant_id'], value_vars=self.correct_answers.keys(), var_name='Pair')
49
  correctness_long = response_df.melt(id_vars=['participant_id'], value_vars=[f'{p}_Correct' for p in self.correct_answers.keys()], var_name='Pair_Correct_Col', value_name='Correct')
50
  correctness_long['Pair'] = correctness_long['Pair_Correct_Col'].str.replace('_Correct', '')
51
  response_long = response_long.merge(correctness_long[['participant_id', 'Pair', 'Correct']], on=['participant_id', 'Pair'])
52
 
 
53
  all_metrics_dfs = []
54
  for q in self.questions:
55
+ file_path = f"{base_path summary_text, fig, gr.Slider(maximum=slider_max, value=fixation_num, interactive=True)
56
+
57
+ def analyze_rq1_metric(self, metric):
58
+ if not metric or metric not in self.combined_data.columns: return None, "Metric not found."
59
+ correct = self.combined_data.loc[self.combined_data['Answer_Correctness'] == 'Correct', metric].dropna()
60
+ incorrect = self.combined_data.loc[self.combined_data['Answer_Correctness'] == 'Incorrect', metric].dropna()
61
+ if len(correct) < 2 or len(incorrect) < 2: return None, "Not enough data for both groups to compare."
62
+ t_stat, p_val = stats.ttest_ind(incorrect, correct, equal_var=False, nan_policy='omit')
63
+ fig, ax = plt.subplots(figsize=(8, 6)); sns.boxplot(data=self.combined_data, x='Answer_Correctness', y=metric, ax=ax, palette=['#66b3ff','#ff9999']); ax.set_title(f'Comparison of "{metric}" by Answer Correctness', fontsize=14); ax.set_xlabel("Answer Correctness"); ax.set_ylabel(metric); plt.tight_layout()
64
+ summary = f"""### Analysis for: **{metric}**\n- **Mean (Correct Answers):** {correct.mean():.4f}\n- **Mean (Incorrect Answers):** {incorrect.mean():.4f}\n- **T-test p-value:** {p_val:.4f}\n\n**Conclusion:**\n- {'There is a **statistically significant** difference (p < 0.05).' if p_val < 0.05 else 'There is **no statistically significant** difference (p >= 0.05).'}"""
65
+ return fig, summary
66
+
67
+ # --- DATA SETUP & GRADIO APP ---
68
+ def setup_and_load_data():
69
+ repo_url = "https://github.com/RextonRZ/GenAIEyeTrackingCleanedDataset"
70
+ repo_dir = "GenAIEyeTrackingCleanedDataset"
71
+ if not os.path.exists(repo_dir): git.Repo.clone_from(repo_url, repo_dir)
72
+ else: print("Data repository already exists.")
73
+ base_path = repo_dir
74
+ response_file_path = os.path.join(repo_dir, "GenAI Response.xlsx")
75
+ analyzer = EnhancedAIvsRealGazeAnalyzer().load_and_process_data(base_path, response_file_path)
76
+ return analyzer
77
+
78
+ analyzer = setup_and_load_data()
79
+
80
+ with gr.Blocks(theme=gr.themes.Soft()) as demo:
81
+ gr.Markdown("# Interactive Dashboard: AI vs. Real Gaze Analysis")
82
+ with gr.Tabs() as tabs:
83
+ with gr.TabItem("📊 RQ1: Viewing Time vs. Correctness", id=0):
84
+ # ... (UI is the same)
85
+ with gr.TabItem("🤖 RQ2: Predicting Correctness from Gaze", id=1):
86
+ with gr.Row():
87
+ with gr.Column(scale=1):
88
+ gr.Markdown("#### Tune Model Hyperparameters")
89
+ rq2_test_size_slider=gr.Slider(minimum=0.1, maximum=0.5, step=0.05, value=0.3, label="Test Set Size")
90
+ rq2_estimators_slider=gr.Slider(minimum=10, maximum=200, step=10, value=100, label="Number of Trees")
91
+ rq2_status = gr.Markdown("Train a model to enable the Gaze Playback tab.")
92
+ with gr.Column(scale=2):
93
+ # ... (UI is the same)
94
+ with gr.TabItem("👁️ Gaze Playback & Real-Time Prediction", id=2):
95
+ }/Filtered_GenAI_Metrics_cleaned_{q}.xlsx"
96
  if os.path.exists(file_path):
97
  xls = pd.ExcelFile(file_path)
 
 
98
  metrics_df = pd.read_excel(xls, sheet_name=0)
99
  metrics_df = self._find_and_standardize_participant_col(metrics_df, f"{q} Metrics")
100
  metrics_df['Question'] = q
101
  all_metrics_dfs.append(metrics_df)
102
 
103
+ if len(xls.sheet_names) > 1:
104
+ try:
105
+ fix_df = pd.read_excel(xls, sheet_name=1)
106
+ fix_df = self._find_and_standardize_participant_col(fix_df, f"{q} Fixations")
107
+ fix_df.dropna(subset=['Fixation point X', 'Fixation point Y', 'Gaze event duration (ms)'], inplace=True)
108
+ for participant, group in fix_df.groupby('participant_id'):
109
+ self.fixation_data[(participant, q)] = group.reset_index(drop=True)
110
+ except Exception as e:
111
+ print(f" -> WARNING: Could not load fixation sheet for {q}. Error: {e}")
112
 
113
  if not all_metrics_dfs: raise ValueError("No aggregated metrics files were found.")
114
  self.combined_data = pd.concat(all_metrics_dfs, ignore_index=True)
115
+ q_to_pair# ... (UI is the same)
116
+
117
+ # The UI structure is identical to before, just add the new status component
118
+ # This is a bit of a rewrite to use the ids for clarity.
119
+ with gr.Blocks(theme=gr.themes.Soft()) as demo:
120
+ gr.Markdown("# Interactive Dashboard: AI vs. Real Gaze Analysis")
121
+ with gr.Tabs() as tabs:
122
+ with gr.TabItem("📊 RQ1: Viewing Time vs. Correctness", id=0):
123
+ with gr.Row():
124
+ with gr.Column(scale=1):
125
+ rq1_metric_dropdown = gr.Dropdown(choices=analyzer.time_metrics = {f'Q{i+1}': f'Pair{i+1}' for i in range(6)}
126
  self.combined_data['Pair'] = self.combined_data['Question'].map(q_to_pair)
127
  self.combined_data = self.combined_data.merge(response_long, on=['participant_id', 'Pair'], how='left')
128
+ self.combined_data['Answer_Correctness, label="Select a Time-Based Metric", value=analyzer.time_metrics[0] if analyzer.time_metrics else None)
129
+ rq1_summary_output = gr.Markdown(label="Statistical Summary")
130
+ with gr.Column(scale=2):
131
+ rq1_plot_output = gr.Plot(label="Metric Comparison")
132
+ with gr.TabItem("🤖 RQ2: Predicting Correctness from Gaze", id=1):
133
+ with gr.Row():
134
+ with gr.Column(scale=1):
135
+ gr'] = self.combined_data['Correct'].map({True: 'Correct', False: 'Incorrect'})
136
+ .Markdown("#### Tune Model Hyperparameters")
137
+ rq2_test_size_slider = gr.Slider(minimum=0.1, maximum=0.5, step=0.05, value=0.3
138
  self.numeric_cols = self.combined_data.select_dtypes(include=np.number).columns.tolist()
139
+ self.time_metrics = [c for c in self.numeric_cols if any, label="Test Set Size")
140
+ rq2_estimators_slider = gr.Slider(minimum=10(k in c.lower() for k in ['time', 'duration', 'fixation'])]
141
+
142
+ , maximum=200, step=10, value=100, label="Number of Trees")# KEY FIX: Participant list is now derived ONLY from trials with valid fixation data.
143
+ self.participant_list
144
+ rq2_status = gr.Markdown("Train a model to enable the Gaze Playback tab.")
145
+ = sorted(list(set([key[0] for key in self.fixation_data.keys()]))) with gr.Column(scale=2):
146
+ rq2_summary_output = gr.Markdown(label
147
+ print(f"--- Data Loading Successful. Found {len(self.participant_list)} participants with fixation data.="Model Performance Summary")
148
+ rq2_table_output = gr.Dataframe(label="Classification Report", ---")
149
  return self
150
 
151
+ def run_prediction_model(self, test_size, n_estimators interactive=False)
152
+ rq2_plot_output = gr.Plot(label="Feature Importance")
153
+ ):
154
+ leaky_features = ['Correct', 'participant_id']
155
+ self.feature_names = [with gr.TabItem("👁️ Gaze Playback & Real-Time Prediction", id=2):
156
+ col for col in self.combined_data.select_dtypes(include=np.number).columns if col not in leaky_with gr.Row():
157
+ with gr.Column(scale=1):
158
+ gr.Markdown("### See the Prediction Efeatures]
159
  features = self.combined_data[self.feature_names].copy()
160
+ target = self.combinedvolve with Every Glance!")
161
+ playback_participant = gr.Dropdown(choices=analyzer.participant_list, label_data['Answer_Correctness'].map({'Correct': 1, 'Incorrect': 0})
162
+ valid="Select Participant")
163
+ playback_question = gr.Dropdown(choices=analyzer.questions, label="Select Question_indices = target.notna()
164
+ features, target = features[valid_indices], target[valid_")
165
+ gr.Markdown("Use the slider to play back fixations one by one.")
166
+ playback_sliderindices]
167
  features = features.fillna(features.median()).fillna(0)
168
+ if len(target = gr.Slider(minimum=0, maximum=1, step=1, value=0, label="Fix.unique()) < 2: return "Not enough data to train.", None, None
169
+ X_train, X_testation Number", interactive=False)
170
+ playback_summary = gr.Markdown(label="Trial Info")
171
+ with gr.Column(scale=2):
172
+ playback_plot = gr.Plot(label="Gaze Play, y_train, y_test = train_test_split(features, target, test_size=test_size, random_state=42, stratify=target)
173
+ self.scaler = StandardScaler().fitback & Live Prediction")
174
+
175
+ outputs_rq2 = [rq2_summary_output, rq2_table_output, rq2_plot_output, rq2_status]
176
+ outputs_playback = [playback_summary, playback(X_train)
177
+ self.model = RandomForestClassifier(n_estimators=int(n_estimators), random_state=42, class_weight='balanced').fit(self.scaler.transform(X_train), y_train)
178
+ _plot, playback_slider]
179
+ rq1_metric_dropdown.change(fn=analyzer.analyze_rq1_metric, inputs=rq1_metric_dropdown, outputs=[rq1_plot_output, rq report = classification_report(y_test, self.model.predict(self.scaler.transform(X_test)), target_names=['Incorrect', 'Correct'], output_dict=True)
180
+ auc_score =1_summary_output])
181
+ rq2_test_size_slider.release(fn=analyzer.run_prediction_model, inputs=[rq2_test_size_slider, rq2_estimators_slider], outputs=outputs roc_auc_score(y_test, self.model.predict_proba(self.scaler.transform(X_test))[:, 1])
182
+ summary_md = f"### Model Performance\n- **AUC_rq2)
183
+ rq2_estimators_slider.release(fn=analyzer.run_prediction_model, inputs=[rq2_test_size_slider, rq2_estimators_slider], outputs=outputs_rq Score:** **{auc_score:.4f}**\n- **Overall Accuracy:** {report['accuracy']:.3f}"
184
  report_df = pd.DataFrame(report).transpose().round(3)
185
+ feature_importance = pd.DataFrame({'Feature': self.feature_names, 'Importance': self.model.feature2)
186
+ playback_inputs = [playback_participant, playback_question, playback_slider]
187
+ playback_participant.change(lambda: 0, None, playback_slider).then(fn=analyzer.generate_gaze_playback, inputs=playback_inputs, outputs=outputs_playback)
188
+ playback_question.change(lambda_importances_}).sort_values('Importance', ascending=False).head(15)
189
+ fig, ax = plt.subplots(figsize=(10, 8)); sns.barplot(data=feature_importance, x='Importance', y='Feature', ax=ax, palette='viridis'); ax.set_title(f': 0, None, playback_slider).then(fn=analyzer.generate_gaze_playback, inputs=playback_inputs, outputs=outputs_playback)
190
+ playback_slider.release(fn=analyzer.generate_gaze_playback, inputs=playback_inputs, outputs=outputs_playback)
191
+
192
+ demo.load(Top 15 Predictive Features', fontsize=14); plt.tight_layout()
193
  return summary_md, report_df, fig
194
 
195
+ def _recalculate_features_from_fixations(self, fixations_df):fn=analyzer.analyze_rq1_metric, inputs=rq1_metric_dropdown, outputs=[rq1_plot_output, rq1_summary_output])
196
+ demo.load(fn=analyzer.run_prediction
197
  feature_vector = pd.Series(0.0, index=self.feature_names)
198
  if fixations_df.empty: return feature_vector.fillna(0).values.reshape(1, -1)
199
  if 'AOI name' in fixations_df.columns:
200
+ for aoi_name,_model, inputs=[rq2_test_size_slider, rq2_estimators_slider], outputs=outputs group in fixations_df.groupby('AOI name'):
201
  col_name = f'Total fixation duration on {aoi_name}'
202
  if col_name in feature_vector.index:
203
  feature_vector[col_name] = group['Gaze event duration (ms)'].sum()
 
207
  def generate_gaze_playback(self, participant, question, fixation_num):
208
  trial_key = (str(participant), question)
209
  if not participant or not question or trial_key not in self.fixation_data:
210
+ return "**No fixation data found for this trial.**", None, gr.Slider(interactive=False, value=0)
211
+
212
  all_fixations = self.fixation_data[trial_key]
213
  fixation_num = int(fixation_num)
214
  slider_max = len(all_fixations)
215
  if fixation_num > slider_max: fixation_num = slider_max
216
  current_fixations = all_fixations.iloc[:fixation_num]
217
+
218
  partial_features = self._recalculate_features_from_fixations(current_fixations)
219
  prediction_prob = self.model.predict_proba(self.scaler.transform(partial_features))[0]
220
  prob_correct = prediction_prob[1]
221
+
222
  fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8), gridspec_kw={'height_ratios': [4, 1]})
223
+ fig.suptitle_rq2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
 
225
  if __name__ == "__main__":
226
  demo.launch()