|
|
import gradio as gr |
|
|
import pandas as pd |
|
|
import numpy as np |
|
|
from xgboost import XGBClassifier |
|
|
|
|
|
|
|
|
def calculate_angle(x1, y1, x2, y2, x3, y3): |
|
|
v1 = [x2 - x1, y2 - y1] |
|
|
v2 = [x3 - x2, y3 - y2] |
|
|
dot = v1[0]*v2[0] + v1[1]*v2[1] |
|
|
mag1 = np.sqrt(v1[0]**2 + v1[1]**2) |
|
|
mag2 = np.sqrt(v2[0]**2 + v2[1]**2) |
|
|
|
|
|
if mag1 == 0 or mag2 == 0: |
|
|
return 0.0 |
|
|
cos_theta = np.clip(dot / (mag1 * mag2), -1.0, 1.0) |
|
|
angle = np.degrees(np.arccos(cos_theta)) |
|
|
return angle |
|
|
|
|
|
def label_angle(angle): |
|
|
if np.isnan(angle): |
|
|
return 0 |
|
|
if angle < 30: |
|
|
return 0 |
|
|
elif angle < 60: |
|
|
return 1 |
|
|
elif angle < 90: |
|
|
return 2 |
|
|
elif angle < 120: |
|
|
return 3 |
|
|
elif angle < 150: |
|
|
return 4 |
|
|
else: |
|
|
return 5 |
|
|
|
|
|
def extract_features(extracted, time_diff=0.05): |
|
|
extracted['distance_covered'] = 0.0 |
|
|
extracted['idle_time'] = 0.0 |
|
|
|
|
|
for i in range(1, len(extracted)): |
|
|
dx = extracted.loc[i, 'x'] - extracted.loc[i-1, 'x'] |
|
|
dy = extracted.loc[i, 'y'] - extracted.loc[i-1, 'y'] |
|
|
distance = np.sqrt(dx**2 + dy**2) |
|
|
extracted.loc[i, 'distance_covered'] = distance |
|
|
|
|
|
if extracted.loc[i, 'x'] == extracted.loc[i-1, 'x'] and extracted.loc[i, 'y'] == extracted.loc[i-1, 'y']: |
|
|
extracted.loc[i, 'idle_time'] = extracted.loc[i-1, 'idle_time'] + time_diff |
|
|
else: |
|
|
extracted.loc[i, 'idle_time'] = 0.0 |
|
|
|
|
|
extracted['cursor_speed'] = extracted['distance_covered'] / time_diff |
|
|
extracted['acceleration'] = extracted['cursor_speed'] / time_diff |
|
|
|
|
|
angles = [] |
|
|
for i in range(1, len(extracted) - 1): |
|
|
angle = calculate_angle( |
|
|
extracted.loc[i-1, 'x'], extracted.loc[i-1, 'y'], |
|
|
extracted.loc[i, 'x'], extracted.loc[i, 'y'], |
|
|
extracted.loc[i+1, 'x'], extracted.loc[i+1, 'y'] |
|
|
) |
|
|
angles.append(angle) |
|
|
|
|
|
extracted = extracted.iloc[1:-1].copy() |
|
|
extracted['movement_angle'] = angles |
|
|
extracted['prev_movement_angle'] = [0] + angles[1:] |
|
|
|
|
|
extracted['angle_label'] = extracted['movement_angle'].apply(label_angle) |
|
|
extracted['prev_angle_label'] = extracted['prev_movement_angle'].apply(label_angle) |
|
|
return extracted |
|
|
|
|
|
|
|
|
model = XGBClassifier() |
|
|
model.load_model("xgb_confusion_detector.model") |
|
|
|
|
|
|
|
|
def predict_fn(file): |
|
|
|
|
|
raw_df = pd.read_csv(file) |
|
|
|
|
|
extracted = extract_features(raw_df, time_diff=0.05) |
|
|
|
|
|
try: |
|
|
features_to_use = extracted.drop(['isConfused'], axis=1) |
|
|
except: |
|
|
features_to_use = extracted |
|
|
|
|
|
prediction = model.predict(features_to_use) |
|
|
total_predictions = len(prediction) |
|
|
confused_predictions = np.sum(prediction == 1) |
|
|
|
|
|
confusion_ratio = confused_predictions / total_predictions |
|
|
is_user_confused = confusion_ratio > 0.3 |
|
|
|
|
|
return { |
|
|
"Prediction": prediction.tolist(), |
|
|
"Confidence": model.predict_proba(features_to_use).tolist(), |
|
|
"Confusion Ratio": confusion_ratio, |
|
|
"User Confused?": is_user_confused |
|
|
} |
|
|
|
|
|
|
|
|
interface = gr.Interface( |
|
|
fn=predict_fn, |
|
|
inputs=gr.File(label="Upload cursor movement CSV"), |
|
|
outputs=[ |
|
|
gr.JSON(label="Prediction Details") |
|
|
], |
|
|
title="Confusion Detector", |
|
|
description="Upload a CSV file with cursor movements (timestamp,x,y,isClick) to predict if the user is confused." |
|
|
) |
|
|
|
|
|
interface.launch() |
|
|
|