{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "b65b6b7d",
   "metadata": {},
   "source": [
    "Imports"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "530e70af",
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd \n",
    "import numpy as np \n",
    "import matplotlib.pyplot as plt \n",
    "import seaborn as sns \n",
    "import random \n",
    "import joblib \n",
    "from pathlib import Path \n",
    "\n",
    "from sklearn.model_selection import GroupKFold, GroupShuffleSplit \n",
    "from sklearn.preprocessing import StandardScaler \n",
    "from sklearn.metrics import ( \n",
    "    precision_score, recall_score,\n",
    "    confusion_matrix, roc_curve, auc, \n",
    "    precision_recall_curve, f1_score, \n",
    "    balanced_accuracy_score, accuracy_score\n",
    ") \n",
    "\n",
    "import tensorflow as tf \n",
    "from tensorflow.keras import Input, layers, models"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0d01127c",
   "metadata": {},
   "source": [
    "Seed"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "67aaf56e",
   "metadata": {},
   "outputs": [],
   "source": [
    "SEED = 42 \n",
    "np.random.seed(SEED) \n",
    "tf.random.set_seed(SEED) \n",
    "random.seed(SEED)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "844e250c",
   "metadata": {},
   "source": [
    "Daten laden "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "73a34b69",
   "metadata": {},
   "outputs": [],
   "source": [
    "data_path = Path(r\"~/data-paulusjafahrsimulator-gpu/new_datasets/50s_25Hz_dataset.parquet\") \n",
    "data = pd.read_parquet(path=data_path)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "325179d3",
   "metadata": {},
   "source": [
    "Daten vorbereiten"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a5ad3126",
   "metadata": {},
   "outputs": [],
   "source": [
    "low_all = data[ \n",
    "    ((data[\"PHASE\"] == \"baseline\") | \n",
    "    ((data[\"STUDY\"] == \"n-back\") & \n",
    "    (data[\"PHASE\"] != \"baseline\") & \n",
    "    (data[\"LEVEL\"].isin([1, 4])))) \n",
    "].copy() \n",
    "\n",
    "high_all = pd.concat([ \n",
    "    data[(data[\"STUDY\"] == \"n-back\") & \n",
    "         (data[\"LEVEL\"].isin([2, 3, 5, 6])) & \n",
    "         (data[\"PHASE\"].isin([\"train\", \"test\"]))], \n",
    "    data[(data[\"STUDY\"] == \"k-drive\") & (data[\"PHASE\"] != \"baseline\")] \n",
    "]).copy() \n",
    "\n",
    "low_all[\"label\"] = 0 \n",
    "high_all[\"label\"] = 1 \n",
    "\n",
    "data = pd.concat([low_all, high_all], ignore_index=True).drop_duplicates()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fd843b62",
   "metadata": {},
   "source": [
    "Features"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5f10e6ca",
   "metadata": {},
   "outputs": [],
   "source": [
    "au_columns = [col for col in data.columns if \"face\" in col.lower()] \n",
    "\n",
    "eye_columns = [ \n",
    "    'Fix_count_short_66_150','Fix_count_medium_300_500','Fix_count_long_gt_1000', \n",
    "    'Fix_count_100','Fix_mean_duration','Fix_median_duration', \n",
    "    'Sac_count','Sac_mean_amp','Sac_mean_dur','Sac_median_dur', \n",
    "    'Blink_count','Blink_mean_dur','Blink_median_dur', \n",
    "    'Pupil_mean','Pupil_IPA' \n",
    "] \n",
    "\n",
    "# NaNs entfernen \n",
    "data = data.dropna(subset=au_columns + eye_columns + [\"label\"]) \n",
    "\n",
    "# Arrays \n",
    "X_au = data[au_columns].values[..., np.newaxis] \n",
    "X_eye = data[eye_columns].values \n",
    "y = data[\"label\"].values \n",
    "groups = data[\"subjectID\"].values"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cabe09af",
   "metadata": {},
   "source": [
    "Train/Test Split"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "52d3b7cf",
   "metadata": {},
   "outputs": [],
   "source": [
    "gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42)\n",
    "train_idx, test_idx = next(gss.split(X_au, y, groups))\n",
    "\n",
    "X_au_train, X_au_test = X_au[train_idx], X_au[test_idx]\n",
    "X_eye_train, X_eye_test = X_eye[train_idx], X_eye[test_idx]\n",
    "y_train, y_test = y[train_idx], y[test_idx]\n",
    "groups_train, groups_test = groups[train_idx], groups[test_idx]\n",
    "\n",
    "print(\"Train:\", len(y_train), \" | Test:\", len(y_test))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6dedded5",
   "metadata": {},
   "source": [
    "Hybrid CNN-Modell"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "41cc1b30",
   "metadata": {},
   "outputs": [],
   "source": [
    "def build_hybrid_model(n_aus, n_eye, lr=1e-4): \n",
    "    input_au = Input(shape=(n_aus, 1), name=\"au_input\") \n",
    "    x = layers.Conv1D(32, 3, activation=\"relu\")(input_au) \n",
    "    x = layers.BatchNormalization()(x) \n",
    "    x = layers.MaxPooling1D(2)(x) \n",
    "    x = layers.Conv1D(64, 3, activation=\"relu\")(x) \n",
    "    x = layers.BatchNormalization()(x) \n",
    "    x = layers.GlobalAveragePooling1D()(x) \n",
    "\n",
    "    input_eye = Input(shape=(n_eye,), name=\"eye_input\") \n",
    "    e = layers.Dense(32, activation=\"relu\")(input_eye) \n",
    "    e = layers.Dropout(0.3)(e) \n",
    "    e = layers.Dense(16, activation=\"relu\")(e) \n",
    "\n",
    "    fused = layers.concatenate([x, e]) \n",
    "    z = layers.Dense(32, activation=\"relu\")(fused) \n",
    "    z = layers.Dropout(0.4)(z) \n",
    "    output = layers.Dense(1, activation=\"sigmoid\")(z) \n",
    "\n",
    "    model = models.Model(inputs=[input_au, input_eye], outputs=output) \n",
    "    model.compile( \n",
    "        optimizer=tf.keras.optimizers.Adam(learning_rate=lr), \n",
    "        loss=\"binary_crossentropy\", \n",
    "        metrics=[\"accuracy\", tf.keras.metrics.AUC(name=\"auc\")] \n",
    "        ) \n",
    "        \n",
    "    return model"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cea6d0d0",
   "metadata": {},
   "source": [
    "Cross Validation (nur Trainingsdaten)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9c390b46",
   "metadata": {},
   "outputs": [],
   "source": [
    "gkf = GroupKFold(n_splits=5) \n",
    "cv_histories = [] \n",
    "cv_results = [] \n",
    "all_conf_matrices = [] \n",
    "\n",
    "for fold, (tr_idx, va_idx) in enumerate(gkf.split(X_au_train, y_train, groups_train)): \n",
    "    print(f\"\\n===== FOLD {fold+1} =====\") \n",
    "    \n",
    "    X_tr_au, X_va_au = X_au_train[tr_idx], X_au_train[va_idx] \n",
    "    X_tr_eye, X_va_eye = X_eye_train[tr_idx], X_eye_train[va_idx] \n",
    "    y_tr, y_va = y_train[tr_idx], y_train[va_idx] \n",
    "    \n",
    "    # Scaler pro Fold \n",
    "    scaler_au = StandardScaler() \n",
    "    scaler_eye = StandardScaler() \n",
    "    \n",
    "    X_tr_au = scaler_au.fit_transform(X_tr_au.reshape(len(X_tr_au), -1)).reshape(X_tr_au.shape) \n",
    "    X_va_au = scaler_au.transform(X_va_au.reshape(len(X_va_au), -1)).reshape(X_va_au.shape) \n",
    "    \n",
    "    X_tr_eye = scaler_eye.fit_transform(X_tr_eye) \n",
    "    X_va_eye = scaler_eye.transform(X_va_eye) \n",
    "    \n",
    "    # Modell \n",
    "    model_cv = build_hybrid_model(len(au_columns), len(eye_columns)) \n",
    "    \n",
    "    callbacks = [ \n",
    "        tf.keras.callbacks.EarlyStopping(monitor=\"val_loss\", patience=10, restore_best_weights=True), \n",
    "        tf.keras.callbacks.ReduceLROnPlateau(monitor=\"val_loss\", factor=0.5, patience=5, min_lr=1e-6) \n",
    "    ] \n",
    "    \n",
    "    history = model_cv.fit( \n",
    "        [X_tr_au, X_tr_eye], y_tr, \n",
    "        validation_data=([X_va_au, X_va_eye], y_va), \n",
    "        epochs=100, \n",
    "        batch_size=16, \n",
    "        verbose=0 \n",
    "    ) \n",
    "    \n",
    "    cv_histories.append(history.history) \n",
    "    \n",
    "    # Evaluation \n",
    "    scores = model_cv.evaluate([X_va_au, X_va_eye], y_va, verbose=0) \n",
    "    cv_results.append(scores) \n",
    "    print(f\"Val Loss={scores[0]:.4f} | Val Acc={scores[1]:.4f} | Val AUC={scores[2]:.4f}\") \n",
    "        \n",
    "    # Konfusionsmatrix pro Fold \n",
    "    y_pred_va = (model_cv.predict([X_va_au, X_va_eye]) > 0.5).astype(int) \n",
    "    cm = confusion_matrix(y_va, y_pred_va) \n",
    "    all_conf_matrices.append(cm) \n",
    "        \n",
    "    plt.figure(figsize=(6,5)) \n",
    "    sns.heatmap(cm, annot=True, fmt=\"d\", cmap=\"Blues\", \n",
    "                xticklabels=[\"Pred 0\", \"Pred 1\"], \n",
    "                yticklabels=[\"True 0\", \"True 1\"]) \n",
    "    plt.title(f\"Konfusionsmatrix - Fold {fold+1}\") \n",
    "    plt.show() \n",
    "    \n",
    "# Aggregierte Konfusionsmatrix \n",
    "agg_cm = sum(all_conf_matrices) \n",
    "\n",
    "plt.figure(figsize=(6,5)) \n",
    "sns.heatmap(agg_cm, annot=True, fmt=\"d\", cmap=\"Purples\", \n",
    "            xticklabels=[\"Pred 0\", \"Pred 1\"], \n",
    "            yticklabels=[\"True 0\", \"True 1\"]) \n",
    "plt.title(\"Aggregierte Konfusionsmatrix - alle Folds\") \n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "97df9df1",
   "metadata": {},
   "source": [
    "Results"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9eae5c0f",
   "metadata": {},
   "outputs": [],
   "source": [
    "#results\n",
    "cv_results = np.array(cv_results) \n",
    "print(\"\\n=== Cross-Validation Ergebnisse ===\") \n",
    "print(f\"Durchschnittlicher Val-Loss: {cv_results[:,0].mean():.4f}\") \n",
    "print(f\"Durchschnittliche Val-Accuracy: {cv_results[:,1].mean():.4f}\") \n",
    "print(f\"Durchschnittliche Val-AUC: {cv_results[:,2].mean():.4f}\")\n",
    "\n",
    "#Ergebnis-Tabelle erstellen\n",
    "results_table = pd.DataFrame({ \n",
    "    \"Fold\": np.arange(1, len(cv_results)+1), \n",
    "    \"Val Loss\": cv_results[:,0], \n",
    "    \"Val Accuracy\": cv_results[:,1], \n",
    "    \"Val AUC\": cv_results[:,2] }) \n",
    "\n",
    "# Durchschnittszeile hinzufügen \n",
    "avg_row = pd.DataFrame({ \n",
    "    \"Fold\": [\"Ø\"], \n",
    "    \"Val Loss\": [cv_results[:,0].mean()], \n",
    "    \"Val Accuracy\": [cv_results[:,1].mean()], \n",
    "    \"Val AUC\": [cv_results[:,2].mean()] \n",
    "}) \n",
    "\n",
    "results_table = pd.concat([results_table, avg_row], ignore_index=True) \n",
    "\n",
    "print(\"\\n=== Ergebnis-Tabelle ===\") \n",
    "print(results_table) \n",
    "\n",
    "#Tabelle speichern \n",
    "results_table.to_csv(\"cnn_crossVal_results.csv\", index=False) \n",
    "print(\"Ergebnisse gespeichert als 'cnn_crossVal_results.csv'\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7e564308",
   "metadata": {},
   "source": [
    "Speichern des Modells"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9afc926b",
   "metadata": {},
   "outputs": [],
   "source": [
    "model_cv.save(\"hybrid_fusion_model_Test_group_split.keras\") \n",
    "joblib.dump(scaler_au, \"scaler_au_Test_group_split.joblib\") \n",
    "joblib.dump(scaler_eye, \"scaler_eye_Test_group_split.joblib\") \n",
    "\n",
    "print(\"Finales Modell gespeichert.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "391af5d5",
   "metadata": {},
   "source": [
    "Test"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0bb8c14c",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Preprocessing Testdaten \n",
    "X_au_test_scaled = scaler_au.transform( \n",
    "    X_au_test.reshape(len(X_au_test), -1) \n",
    ").reshape(X_au_test.shape) \n",
    "\n",
    "X_eye_test_scaled = scaler_eye.transform(X_eye_test) \n",
    "\n",
    "# Vorhersagen \n",
    "y_prob_test = model_cv.predict([X_au_test_scaled, X_eye_test_scaled]).flatten() \n",
    "y_pred_test = (y_prob_test > 0.5).astype(int) \n",
    "\n",
    "# Konfusionsmatrix \n",
    "cm_test = confusion_matrix(y_test, y_pred_test) \n",
    "\n",
    "plt.figure(figsize=(6,5)) \n",
    "sns.heatmap(cm_test, annot=True, fmt=\"d\", cmap=\"Greens\", \n",
    "            xticklabels=[\"Pred 0\", \"Pred 1\"], \n",
    "            yticklabels=[\"True 0\", \"True 1\"]) \n",
    "plt.title(\"Konfusionsmatrix - Testdaten\") \n",
    "plt.show() \n",
    "\n",
    "# ROC \n",
    "fpr, tpr, _ = roc_curve(y_test, y_prob_test) \n",
    "roc_auc = auc(fpr, tpr) \n",
    "\n",
    "plt.figure(figsize=(7,6)) \n",
    "plt.plot(fpr, tpr, label=f\"AUC = {roc_auc:.3f}\") \n",
    "plt.plot([0,1], [0,1], \"k--\") \n",
    "plt.title(\"ROC - Testdaten\") \n",
    "plt.legend() \n",
    "plt.grid(True) \n",
    "plt.show() \n",
    "\n",
    "# Precision-Recall \n",
    "precision, recall, _ = precision_recall_curve(y_test, y_prob_test) \n",
    "plt.figure(figsize=(7,6)) \n",
    "plt.plot(recall, precision) \n",
    "plt.title(\"Precision-Recall - Testdaten\") \n",
    "plt.grid(True) \n",
    "plt.show() \n",
    "\n",
    "# Metriken \n",
    "print(\"Accuracy:\", accuracy_score(y_test, y_pred_test))\n",
    "print(\"F1-Score:\", f1_score(y_test, y_pred_test)) \n",
    "print(\"Balanced Accuracy:\", balanced_accuracy_score(y_test, y_pred_test)) \n",
    "print(\"Precision:\", precision_score(y_test, y_pred_test)) \n",
    "print(\"Recall:\", recall_score(y_test, y_pred_test)) \n",
    "print(\"AUC:\", roc_auc)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.10"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
