{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# HW-III: Longitudinal MI Training and Plasticity\n",
    "## ECE-374N/385J Neural Engineering — Spring 2024\n",
    "\n",
    "Analysis of neuro-plastic effects of longitudinal Motor Imagery (MI) BCI training for two healthy subjects.\n",
    "- 7 sessions each: 1 offline + 6 online\n",
    "- 3 runs per session, 20 trials per run\n",
    "- 32-channel EEG, fs = 512 Hz"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Setup & Data Loading"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "id": "30ca5d4a",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Setup complete.\n"
     ]
    }
   ],
   "source": [
    "import numpy as np\n",
    "import scipy.io as sio\n",
    "from scipy import signal, stats\n",
    "import matplotlib\n",
    "import matplotlib.pyplot as plt\n",
    "import matplotlib.gridspec as gridspec\n",
    "from matplotlib.patches import Circle\n",
    "import pymannkendall as mk\n",
    "import warnings\n",
    "warnings.filterwarnings('ignore')\n",
    "\n",
    "# MNE for topoplots\n",
    "import mne\n",
    "from mne.viz import plot_topomap\n",
    "mne.set_log_level('WARNING')\n",
    "\n",
    "%matplotlib inline\n",
    "plt.rcParams['figure.dpi'] = 120\n",
    "plt.rcParams['font.size'] = 10\n",
    "\n",
    "DATA_DIR = 'HW_3_sp23_longitudinalTraining_Plasticity_ECE374N-385J_Neural_Eng/'\n",
    "print('Setup complete.')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "id": "04600b31",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "fs = 512 Hz\n",
      "Channels (32): ['Fp1' 'Fpz' 'Fp2' 'F7' 'F3' 'Fz' 'F4' 'F8' 'FC5' 'FC1' 'FC2' 'FC6' 'M1'\n",
      " 'T7' 'C3' 'Cz' 'C4' 'T8' 'M2' 'CP5' 'CP1' 'CP2' 'CP6' 'P7' 'P3' 'Pz' 'P4'\n",
      " 'P8' 'POz' 'O1' 'Oz' 'O2']\n",
      "Subjects loaded.\n",
      "\n",
      "Subject 1 | Online Session 1 | Run 1 — Event sequence:\n",
      "  #       POS (sample)       TYP\n",
      "  ----- --------------  --------\n",
      "  0               6922     32766\n",
      "  1               7183      1000\n",
      "  2               8653       768\n",
      "  3               9681       769\n",
      "  4              10706      7691\n",
      "  5              10938      7692\n",
      "  6              11710      1000\n",
      "  7              12936       768\n",
      "  8              13971       770\n",
      "  9              14998      7701\n",
      "  10             15324      7702\n",
      "  11             16097      1000\n",
      "  12             17186       768\n",
      "  13             18218       769\n",
      "  14             19243      7691\n",
      "  15             21083      7692\n",
      "  16             21861      1000\n",
      "  17             23325       768\n",
      "  18             24359       770\n",
      "  19             25384      7701\n",
      "  20             27483      7703\n",
      "  21             28256      1000\n",
      "  22             29559       768\n",
      "  23             30595       770\n",
      "  24             31621      7701\n",
      "  25             32476      7703\n",
      "  26             33249      1000\n",
      "  27             34367       768\n",
      "  28             35399       769\n",
      "  29             36425      7691\n",
      "  30             40022      7692\n",
      "  31             40800      1000\n",
      "  32             42312       768\n",
      "  33             43340       769\n",
      "  34             44367      7691\n",
      "  35             47611      7692\n",
      "  36             48389      1000\n",
      "  37             49764       768\n",
      "  38             50795       770\n",
      "  39             51820      7701\n",
      "  40             53306      7703\n",
      "  41             54084      1000\n",
      "  42             55161       768\n",
      "  43             56189       770\n",
      "  44             57214      7701\n",
      "  45             59802      7703\n",
      "  46             60577      1000\n",
      "  47             62050       768\n",
      "  48             63078       770\n",
      "  49             64104      7701\n",
      "  50             66621      7703\n",
      "  51             67400      1000\n",
      "  52             68635       768\n",
      "  53             69662       769\n",
      "  54             70688      7691\n",
      "  55             71708      7692\n",
      "  56             72486      1000\n",
      "  57             73741       768\n",
      "  58             74776       770\n",
      "  59             75803      7701\n",
      "  60             78173      7703\n",
      "  61             78949      1000\n",
      "  62             80456       768\n",
      "  63             81488       769\n",
      "  64             82515      7691\n",
      "  65             85560      7693\n",
      "  66             86334      1000\n",
      "  67             87487       768\n",
      "  68             88519       769\n",
      "  69             89545      7691\n",
      "  70             93152      7692\n",
      "  71             93926      1000\n",
      "  72             95056       768\n",
      "  73             96084       769\n",
      "  74             97109      7691\n",
      "  75             97694      7693\n",
      "  76             98467      1000\n",
      "  77             99977       768\n",
      "  78            101009       770\n",
      "  79            102035      7701\n",
      "  80            102973      7702\n",
      "  81            103750      1000\n",
      "  82            105135       768\n",
      "  83            106166       770\n",
      "  84            107193      7701\n",
      "  85            110782      7702\n",
      "  86            111556      1000\n",
      "  87            112982       768\n",
      "  88            114010       770\n",
      "  89            115035      7701\n",
      "  90            116185      7703\n",
      "  91            116957      1000\n",
      "  92            118476       768\n",
      "  93            119510       769\n",
      "  94            120537      7691\n",
      "  95            122398      7693\n",
      "  96            123177      1000\n",
      "  97            124555       768\n",
      "  98            125590       769\n",
      "  99            126618      7691\n",
      "  100           128381      7693\n",
      "  101           129155     32766\n"
     ]
    }
   ],
   "source": [
    "# ── Load subject data ───────────────────────────────────────────────────────\n",
    "\n",
    "def load_subject(mat_path):\n",
    "    \"\"\"Load a subject .mat file and return the struct.\"\"\"\n",
    "    data = sio.loadmat(mat_path, squeeze_me=True, struct_as_record=False)\n",
    "    key = [k for k in data if not k.startswith('_')][0]\n",
    "    return data[key]\n",
    "\n",
    "subj1 = load_subject(DATA_DIR + 'subj1.mat')\n",
    "subj2 = load_subject(DATA_DIR + 'subj2.mat')\n",
    "subjects = [subj1, subj2]\n",
    "subj_names = ['Subject-1', 'Subject-2']\n",
    "N_SESSIONS = 6   # online sessions\n",
    "N_RUNS = 3\n",
    "N_TRIALS = 20    # per run\n",
    "FS = subj1.offline.run[0].header.fs  # 512 Hz\n",
    "\n",
    "# ── Load channel locations ──────────────────────────────────────────────────\n",
    "loc_data = sio.loadmat('ch32Locations.mat', squeeze_me=True, struct_as_record=False)\n",
    "ch_locs = loc_data['ch32Locations']\n",
    "CH_LABELS = np.array([ch_locs[i].labels for i in range(32)])\n",
    "# 3-D Cartesian (EEGLAB convention: X=nose, Y=left, Z=up)\n",
    "X_eeg = np.array([ch_locs[i].X for i in range(32)])\n",
    "Y_eeg = np.array([ch_locs[i].Y for i in range(32)])\n",
    "# Convert to 2-D topomap coords: mne expects (right, front)\n",
    "pos2d = np.column_stack([-Y_eeg, X_eeg])\n",
    "pos2d = pos2d / np.max(np.linalg.norm(pos2d, axis=1))  # normalise to unit circle\n",
    "\n",
    "print(f'fs = {FS} Hz')\n",
    "print(f'Channels ({len(CH_LABELS)}):', CH_LABELS)\n",
    "print('Subjects loaded.')\n",
    "\n",
    "# ── Event sequence: Subject 1, Session 1, Run 1 ─────────────────────────────\n",
    "typs_s1_s1_r1 = subj1.online[0].run[0].header.triggers.TYP\n",
    "pos_s1_s1_r1  = subj1.online[0].run[0].header.triggers.POS\n",
    "print('\\nSubject 1 | Online Session 1 | Run 1 — Event sequence:')\n",
    "print(f'  {\"#\":<5} {\"POS (sample)\":>14}  {\"TYP\":>8}')\n",
    "print(f'  {\"-\"*5} {\"-\"*14}  {\"-\"*8}')\n",
    "for i, (p, t) in enumerate(zip(pos_s1_s1_r1, typs_s1_s1_r1)):\n",
    "    print(f'  {i:<5} {p:>14}  {t:>8}')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c8f067be",
   "metadata": {},
   "source": [
    "---\n",
    "# Section 2.1 — BCI Command Delivery"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 39,
   "id": "b0811a77",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Subject-1\n",
      "  ------------------------------------------------\n",
      "                  Run 1    Run 2    Run 3     Mean\n",
      "  ------------------------------------------------\n",
      "  Session 1       55.0%    45.0%    55.0%    51.7%   acc\n",
      "                  15.0%    30.0%    25.0%    23.3%   timeout\n",
      "  Session 2       60.0%    50.0%    45.0%    51.7%   acc\n",
      "                  35.0%    25.0%    50.0%    36.7%   timeout\n",
      "  Session 3       45.0%    65.0%    60.0%    56.7%   acc\n",
      "                  35.0%    35.0%    40.0%    36.7%   timeout\n",
      "  Session 4       50.0%    60.0%    30.0%    46.7%   acc\n",
      "                  45.0%    40.0%    65.0%    50.0%   timeout\n",
      "  Session 5       65.0%    80.0%    70.0%    71.7%   acc\n",
      "                  25.0%    20.0%    15.0%    20.0%   timeout\n",
      "  Session 6       80.0%    50.0%    35.0%    55.0%   acc\n",
      "                  15.0%    40.0%    45.0%    33.3%   timeout\n",
      "  ------------------------------------------------\n",
      "  Overall acc     55.6%\n",
      "  Overall t/o     33.3%\n",
      "\n",
      "Subject-2\n",
      "  ------------------------------------------------\n",
      "                  Run 1    Run 2    Run 3     Mean\n",
      "  ------------------------------------------------\n",
      "  Session 1       40.0%    45.0%    50.0%    45.0%   acc\n",
      "                  25.0%     0.0%     0.0%     8.3%   timeout\n",
      "  Session 2        5.0%    25.0%    30.0%    20.0%   acc\n",
      "                  65.0%    60.0%    45.0%    56.7%   timeout\n",
      "  Session 3       60.0%    55.0%    20.0%    45.0%   acc\n",
      "                  25.0%    35.0%    70.0%    43.3%   timeout\n",
      "  Session 4       25.0%    45.0%    35.0%    35.0%   acc\n",
      "                  75.0%    55.0%    65.0%    65.0%   timeout\n",
      "  Session 5       75.0%    55.0%    35.0%    55.0%   acc\n",
      "                  25.0%    45.0%    65.0%    45.0%   timeout\n",
      "  Session 6       75.0%    45.0%    50.0%    56.7%   acc\n",
      "                  25.0%    55.0%    50.0%    43.3%   timeout\n",
      "  ------------------------------------------------\n",
      "  Overall acc     42.8%\n",
      "  Overall t/o     43.6%\n",
      "\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 197572 characters]",
      "text/plain": [
       "<Figure size 960x1291.2 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Saved table_accuracy_timeout.png\n"
     ]
    }
   ],
   "source": [
    "TIMEOUT_THRESH_SAMP = int(6.9 * FS)  # 6.9 s in samples\n",
    "\n",
    "accuracy     = []   # 2 x 6 x 3\n",
    "timeout_rate = []   # 2 x 6 x 3\n",
    "\n",
    "for subj in [subj1, subj2]:\n",
    "    subj_session_run_accuracy     = []\n",
    "    subj_session_run_timeout_rate = []\n",
    "    for session in subj.online:\n",
    "        session_run_accuracy     = []\n",
    "        session_run_timeout_rate = []\n",
    "        for run in session.run:\n",
    "            trial_start_samp = 0\n",
    "            run_hits     = 0\n",
    "            run_timeouts = 0\n",
    "            run_total    = 0\n",
    "            for typ, pos in zip(run.header.triggers.TYP, run.header.triggers.POS):\n",
    "                if (typ == 7691) or (typ == 7701):\n",
    "                    trial_start_samp = pos\n",
    "                elif (typ == 7692) or (typ == 7702):\n",
    "                    run_total += 1\n",
    "                    if (pos - trial_start_samp) >= TIMEOUT_THRESH_SAMP:\n",
    "                        run_timeouts += 1\n",
    "                elif (typ == 7693) or (typ == 7703):\n",
    "                    run_hits  += 1\n",
    "                    run_total += 1\n",
    "            session_run_accuracy.append(run_hits     / run_total)\n",
    "            session_run_timeout_rate.append(run_timeouts / run_total)\n",
    "        subj_session_run_accuracy.append(session_run_accuracy)\n",
    "        subj_session_run_timeout_rate.append(session_run_timeout_rate)\n",
    "    accuracy.append(subj_session_run_accuracy)\n",
    "    timeout_rate.append(subj_session_run_timeout_rate)\n",
    "\n",
    "accuracy     = np.array(accuracy)      # (2, 6, 3)\n",
    "timeout_rate = np.array(timeout_rate)  # (2, 6, 3)\n",
    "\n",
    "# ── Pretty-print ─────────────────────────────────────────────────────────────\n",
    "header = f\"  {\"\":<12}  {\"Run 1\":>7}  {\"Run 2\":>7}  {\"Run 3\":>7}  {\"Mean\":>7}\"\n",
    "sep = \"  \" + \"-\" * (len(header) - 2)\n",
    "for si, name in enumerate([\"Subject-1\", \"Subject-2\"]):\n",
    "    print(f\"{name}\")\n",
    "    print(sep)\n",
    "    print(header)\n",
    "    print(sep)\n",
    "    for sess_idx in range(6):\n",
    "        accs = accuracy[si, sess_idx]\n",
    "        tos  = timeout_rate[si, sess_idx]\n",
    "        runs_acc = \"\".join([f\"  {a*100:6.1f}%\" for a in accs])\n",
    "        runs_to  = \"\".join([f\"  {t*100:6.1f}%\" for t in tos])\n",
    "        print(f\"  Session {sess_idx+1:<4}{runs_acc}  {accs.mean()*100:6.1f}%   acc\")\n",
    "        print(f\"  {\"\":<12}{runs_to}  {tos.mean()*100:6.1f}%   timeout\")\n",
    "    print(sep)\n",
    "    print(f\"  {\"Overall acc\":<12}{\"\"*24}  {accuracy[si].mean()*100:6.1f}%\")\n",
    "    print(f\"  {\"Overall t/o\":<12}{\"\"*24}  {timeout_rate[si].mean()*100:6.1f}%\")\n",
    "    print()\n",
    "\n",
    "# ── Save table as PNG ─────────────────────────────────────────────────────────\n",
    "col_labels = ['Run 1', 'Run 2', 'Run 3', 'Mean']\n",
    "row_labels, cell_vals, row_colors = [], [], []\n",
    "for si, name in enumerate(['Subject-1', 'Subject-2']):\n",
    "    if si > 0:\n",
    "        row_labels.append(''); cell_vals.append([''] * 4); row_colors.append(['#ffffff'] * 4)\n",
    "    row_labels.append(name); cell_vals.append([''] * 4); row_colors.append(['#d0d8e8'] * 4)\n",
    "    for sess_idx in range(6):\n",
    "        accs = accuracy[si, sess_idx]\n",
    "        tos  = timeout_rate[si, sess_idx]\n",
    "        row_labels.append(f'  Session {sess_idx+1}  acc')\n",
    "        cell_vals.append([f'{a*100:.1f}%' for a in accs] + [f'{accs.mean()*100:.1f}%'])\n",
    "        bg = '#f7f7f7' if sess_idx % 2 == 0 else '#ffffff'\n",
    "        row_colors.append([bg] * 4)\n",
    "        row_labels.append(f'  Session {sess_idx+1}  t/o')\n",
    "        cell_vals.append([f'{t*100:.1f}%' for t in tos] + [f'{tos.mean()*100:.1f}%'])\n",
    "        row_colors.append([bg] * 4)\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(8, len(row_labels) * 0.38 + 0.5))\n",
    "ax.axis('off')\n",
    "tbl = ax.table(\n",
    "    cellText=cell_vals,\n",
    "    rowLabels=row_labels,\n",
    "    colLabels=col_labels,\n",
    "    cellLoc='center',\n",
    "    rowLoc='left',\n",
    "    loc='center',\n",
    ")\n",
    "tbl.auto_set_font_size(False)\n",
    "tbl.set_fontsize(10)\n",
    "tbl.scale(1, 1.4)\n",
    "\n",
    "# Apply row background colours\n",
    "for (row, col), cell in tbl.get_celld().items():\n",
    "    if row == 0:\n",
    "        cell.set_facecolor('#4472c4')\n",
    "        cell.set_text_props(color='white', fontweight='bold')\n",
    "    elif col == -1:\n",
    "        cell.set_facecolor('#e8ecf4')\n",
    "    elif row - 1 < len(row_colors):\n",
    "        cell.set_facecolor(row_colors[row - 1][col])\n",
    "    if col == 3:\n",
    "        cell.set_text_props(fontweight='bold')\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.savefig('table_accuracy_timeout.png', bbox_inches='tight', dpi=150)\n",
    "plt.show()\n",
    "print('Saved table_accuracy_timeout.png')\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 56,
   "id": "a0e66b29",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 57956 characters]",
      "text/plain": [
       "<Figure size 840x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 57508 characters]",
      "text/plain": [
       "<Figure size 840x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "sessions = range(1, 7)\n",
    "acc_s1 = [sum(r) / len(r) for r in accuracy[0]]\n",
    "acc_s2 = [sum(r) / len(r) for r in accuracy[1]]\n",
    "\n",
    "plt.figure(figsize=(7, 4))\n",
    "plt.plot(sessions, [a * 100 for a in acc_s1], \"o-\", label=\"Subject-1\")\n",
    "plt.plot(sessions, [a * 100 for a in acc_s2], \"s-\", label=\"Subject-2\")\n",
    "plt.axhline(50, color=\"gray\", linestyle=\"--\", linewidth=1, label=\"Chance (50%)\")\n",
    "plt.xlabel(\"Session\")\n",
    "plt.ylabel(\"Accuracy (%)\")\n",
    "plt.title(\"BCI Accuracy per Session\")\n",
    "plt.xticks(sessions)\n",
    "plt.legend()\n",
    "plt.tight_layout()\n",
    "plt.show()\n",
    "\n",
    "to_s1 = [sum(r) / len(r) for r in timeout_rate[0]]\n",
    "to_s2 = [sum(r) / len(r) for r in timeout_rate[1]]\n",
    "\n",
    "plt.figure(figsize=(7, 4))\n",
    "plt.plot(sessions, [t * 100 for t in to_s1], \"o-\", label=\"Subject-1\")\n",
    "plt.plot(sessions, [t * 100 for t in to_s2], \"s-\", label=\"Subject-2\")\n",
    "plt.xlabel(\"Session\")\n",
    "plt.ylabel(\"Timeout Rate (%)\")\n",
    "plt.title(\"Timeout Rate per Session\")\n",
    "plt.xticks(sessions)\n",
    "plt.legend()\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 41,
   "id": "e177376b",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "====================================================================\n",
      "PEARSON CORRELATION  —  session number vs accuracy\n",
      "====================================================================\n",
      "  Group           r = +0.590   p = 0.2173  ns   (mean 49.2%)\n",
      "  Subject-1       r = +0.414   p = 0.4144  ns   (mean 55.6%)\n",
      "  Subject-2       r = +0.600   p = 0.2075  ns   (mean 42.8%)\n",
      "\n",
      "====================================================================\n",
      "WILCOXON RANK-SUM  —  sessions 1-3 vs 4-6  (accuracy)\n",
      "====================================================================\n",
      "  Group           U = 126   p = 0.2655  ns   early 45.0%  ->  late 53.3%\n",
      "  Subject-1       U = 32   p = 0.4494  ns   early 53.3%  ->  late 57.8%\n",
      "  Subject-2       U = 28   p = 0.2675  ns   early 36.7%  ->  late 48.9%\n",
      "\n",
      "====================================================================\n",
      "PEARSON CORRELATION  —  session number vs timeout rate\n",
      "====================================================================\n",
      "  Group           r = +0.334   p = 0.5175  ns   (mean 38.5%)\n",
      "  Subject-1       r = +0.066   p = 0.9007  ns   (mean 33.3%)\n",
      "  Subject-2       r = +0.446   p = 0.3750  ns   (mean 43.6%)\n",
      "\n",
      "====================================================================\n",
      "WILCOXON RANK-SUM  —  sessions 1-3 vs 4-6  (timeout rate)\n",
      "====================================================================\n",
      "  Group           U = 124   p = 0.2263  ns   early 34.2%  ->  late 42.8%\n",
      "  Subject-1       U = 37   p = 0.7893  ns   early 32.2%  ->  late 34.4%\n",
      "  Subject-2       U = 28   p = 0.2655  ns   early 36.1%  ->  late 51.1%\n",
      "\n",
      "====================================================================\n",
      "MANN-KENDALL TREND TEST  —  accuracy\n",
      "====================================================================\n",
      "  Group           tau = +0.467   p = 0.2597  ns   trend: no trend\n",
      "  Subject-1       tau = +0.267   p = 0.5661  ns   trend: no trend\n",
      "  Subject-2       tau = +0.467   p = 0.2597  ns   trend: no trend\n",
      "\n",
      "====================================================================\n",
      "MANN-KENDALL TREND TEST  —  timeout rate\n",
      "====================================================================\n",
      "  Group           tau = +0.067   p = 1.0000  ns   trend: no trend\n",
      "  Subject-1       tau = +0.000   p = 1.0000  ns   trend: no trend\n",
      "  Subject-2       tau = +0.200   p = 0.7071  ns   trend: no trend\n"
     ]
    }
   ],
   "source": [
    "from scipy.stats import pearsonr, mannwhitneyu\n",
    "\n",
    "sessions = np.arange(1, 7)\n",
    "\n",
    "def sig_stars(p):\n",
    "    return '***' if p < 0.001 else ('**' if p < 0.01 else ('*' if p < 0.05 else 'ns'))\n",
    "\n",
    "# ── Pearson: session number vs accuracy ──────────────────────────────────────\n",
    "print('=' * 68)\n",
    "print('PEARSON CORRELATION  —  session number vs accuracy')\n",
    "print('=' * 68)\n",
    "\n",
    "acc_group_mean = accuracy.mean(axis=(0, 2))          # (6,) mean over subjects & runs\n",
    "r, p = pearsonr(sessions, acc_group_mean)\n",
    "print(f'  {\"Group\":<14}  r = {r:+.3f}   p = {p:.4f}  {sig_stars(p)}'\n",
    "      f'   (mean {acc_group_mean.mean()*100:.1f}%)')\n",
    "\n",
    "for si, name in enumerate(['Subject-1', 'Subject-2']):\n",
    "    sess_mean = accuracy[si].mean(axis=1)            # (6,)\n",
    "    r, p = pearsonr(sessions, sess_mean)\n",
    "    print(f'  {name:<14}  r = {r:+.3f}   p = {p:.4f}  {sig_stars(p)}'\n",
    "          f'   (mean {sess_mean.mean()*100:.1f}%)')\n",
    "\n",
    "# ── Wilcoxon rank-sum: sessions 1-3 vs 4-6, accuracy ────────────────────────\n",
    "print()\n",
    "print('=' * 68)\n",
    "print('WILCOXON RANK-SUM  —  sessions 1-3 vs 4-6  (accuracy)')\n",
    "print('=' * 68)\n",
    "\n",
    "early_g = accuracy[:, :3, :].flatten()\n",
    "late_g  = accuracy[:, 3:, :].flatten()\n",
    "stat, p = mannwhitneyu(early_g, late_g, alternative='two-sided')\n",
    "print(f'  {\"Group\":<14}  U = {stat:.0f}   p = {p:.4f}  {sig_stars(p)}'\n",
    "      f'   early {early_g.mean()*100:.1f}%  ->  late {late_g.mean()*100:.1f}%')\n",
    "\n",
    "for si, name in enumerate(['Subject-1', 'Subject-2']):\n",
    "    early = accuracy[si, :3, :].flatten()\n",
    "    late  = accuracy[si, 3:, :].flatten()\n",
    "    stat, p = mannwhitneyu(early, late, alternative='two-sided')\n",
    "    print(f'  {name:<14}  U = {stat:.0f}   p = {p:.4f}  {sig_stars(p)}'\n",
    "          f'   early {early.mean()*100:.1f}%  ->  late {late.mean()*100:.1f}%')\n",
    "\n",
    "# ── Pearson: session number vs timeout rate ──────────────────────────────────\n",
    "print()\n",
    "print('=' * 68)\n",
    "print('PEARSON CORRELATION  —  session number vs timeout rate')\n",
    "print('=' * 68)\n",
    "\n",
    "to_group_mean = timeout_rate.mean(axis=(0, 2))       # (6,)\n",
    "r, p = pearsonr(sessions, to_group_mean)\n",
    "print(f'  {\"Group\":<14}  r = {r:+.3f}   p = {p:.4f}  {sig_stars(p)}'\n",
    "      f'   (mean {to_group_mean.mean()*100:.1f}%)')\n",
    "\n",
    "for si, name in enumerate(['Subject-1', 'Subject-2']):\n",
    "    sess_mean = timeout_rate[si].mean(axis=1)\n",
    "    r, p = pearsonr(sessions, sess_mean)\n",
    "    print(f'  {name:<14}  r = {r:+.3f}   p = {p:.4f}  {sig_stars(p)}'\n",
    "          f'   (mean {sess_mean.mean()*100:.1f}%)')\n",
    "\n",
    "# ── Wilcoxon rank-sum: sessions 1-3 vs 4-6, timeout rate ────────────────────\n",
    "print()\n",
    "print('=' * 68)\n",
    "print('WILCOXON RANK-SUM  —  sessions 1-3 vs 4-6  (timeout rate)')\n",
    "print('=' * 68)\n",
    "\n",
    "early_g = timeout_rate[:, :3, :].flatten()\n",
    "late_g  = timeout_rate[:, 3:, :].flatten()\n",
    "stat, p = mannwhitneyu(early_g, late_g, alternative='two-sided')\n",
    "print(f'  {\"Group\":<14}  U = {stat:.0f}   p = {p:.4f}  {sig_stars(p)}'\n",
    "      f'   early {early_g.mean()*100:.1f}%  ->  late {late_g.mean()*100:.1f}%')\n",
    "\n",
    "for si, name in enumerate(['Subject-1', 'Subject-2']):\n",
    "    early = timeout_rate[si, :3, :].flatten()\n",
    "    late  = timeout_rate[si, 3:, :].flatten()\n",
    "    stat, p = mannwhitneyu(early, late, alternative='two-sided')\n",
    "    print(f'  {name:<14}  U = {stat:.0f}   p = {p:.4f}  {sig_stars(p)}'\n",
    "          f'   early {early.mean()*100:.1f}%  ->  late {late.mean()*100:.1f}%')\n",
    "\n",
    "# ── Mann-Kendall: trend over sessions, accuracy ───────────────────────────────\n",
    "import pymannkendall as mk\n",
    "\n",
    "print()\n",
    "print('=' * 68)\n",
    "print('MANN-KENDALL TREND TEST  —  accuracy')\n",
    "print('=' * 68)\n",
    "\n",
    "acc_group_mean = accuracy.mean(axis=(0, 2))\n",
    "res = mk.original_test(acc_group_mean)\n",
    "print(f'  {\"Group\":<14}  tau = {res.Tau:+.3f}   p = {res.p:.4f}  {sig_stars(res.p)}   trend: {res.trend}')\n",
    "\n",
    "for si, name in enumerate(['Subject-1', 'Subject-2']):\n",
    "    sess_mean = accuracy[si].mean(axis=1)\n",
    "    res = mk.original_test(sess_mean)\n",
    "    print(f'  {name:<14}  tau = {res.Tau:+.3f}   p = {res.p:.4f}  {sig_stars(res.p)}   trend: {res.trend}')\n",
    "\n",
    "# ── Mann-Kendall: trend over sessions, timeout rate ───────────────────────────\n",
    "print()\n",
    "print('=' * 68)\n",
    "print('MANN-KENDALL TREND TEST  —  timeout rate')\n",
    "print('=' * 68)\n",
    "\n",
    "to_group_mean = timeout_rate.mean(axis=(0, 2))\n",
    "res = mk.original_test(to_group_mean)\n",
    "print(f'  {\"Group\":<14}  tau = {res.Tau:+.3f}   p = {res.p:.4f}  {sig_stars(res.p)}   trend: {res.trend}')\n",
    "\n",
    "for si, name in enumerate(['Subject-1', 'Subject-2']):\n",
    "    sess_mean = timeout_rate[si].mean(axis=1)\n",
    "    res = mk.original_test(sess_mean)\n",
    "    print(f'  {name:<14}  tau = {res.Tau:+.3f}   p = {res.p:.4f}  {sig_stars(res.p)}   trend: {res.trend}')\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 42,
   "id": "3e5a19d4",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "========================================================================\n",
      "WILCOXON RANK-SUM  —  each session vs session 1  (accuracy)\n",
      "                          stat         p  sig   sess1 mean  sess_s mean\n",
      "  --------------------------------------------------------------------\n",
      "  Group                    3.0    0.1562  ns     48.3%  ->    35.8%   [sess 1 vs 2]\n",
      "  Subject-1                3.0    1.0000  ns     51.7%  ->    51.7%\n",
      "  Subject-2                0.0    0.2500  ns     45.0%  ->    20.0%\n",
      "\n",
      "  Group                    8.5    0.7500  ns     48.3%  ->    50.8%   [sess 1 vs 3]\n",
      "  Subject-1                2.0    0.7500  ns     51.7%  ->    56.7%\n",
      "  Subject-2                3.0    1.0000  ns     45.0%  ->    45.0%\n",
      "\n",
      "  Group                    2.0    0.1875  ns     48.3%  ->    40.8%   [sess 1 vs 4]\n",
      "  Subject-1                2.0    0.7500  ns     51.7%  ->    46.7%\n",
      "  Subject-2                0.0    0.5000  ns     45.0%  ->    35.0%\n",
      "\n",
      "  Group                    4.0    0.2188  ns     48.3%  ->    63.3%   [sess 1 vs 5]\n",
      "  Subject-1                0.0    0.2500  ns     51.7%  ->    71.7%\n",
      "  Subject-2                2.0    0.7500  ns     45.0%  ->    55.0%\n",
      "\n",
      "  Group                    2.0    0.3750  ns     48.3%  ->    55.8%   [sess 1 vs 6]\n",
      "  Subject-1                2.0    0.7500  ns     51.7%  ->    55.0%\n",
      "  Subject-2                0.0    1.0000  ns     45.0%  ->    56.7%\n",
      "\n",
      "\n",
      "========================================================================\n",
      "WILCOXON RANK-SUM  —  each session vs session 1  (timeout rate)\n",
      "                          stat         p  sig   sess1 mean  sess_s mean\n",
      "  --------------------------------------------------------------------\n",
      "  Group                    1.0    0.0625  ns     15.8%  ->    46.7%   [sess 1 vs 2]\n",
      "  Subject-1                1.0    0.5000  ns     23.3%  ->    36.7%\n",
      "  Subject-2                0.0    0.2500  ns      8.3%  ->    56.7%\n",
      "\n",
      "  Group                    0.0    0.0625  ns     15.8%  ->    40.0%   [sess 1 vs 3]\n",
      "  Subject-1                0.0    0.2500  ns     23.3%  ->    36.7%\n",
      "  Subject-2                0.0    0.5000  ns      8.3%  ->    43.3%\n",
      "\n",
      "  Group                    0.0    0.0312  *     15.8%  ->    57.5%   [sess 1 vs 4]\n",
      "  Subject-1                0.0    0.2500  ns     23.3%  ->    50.0%\n",
      "  Subject-2                0.0    0.2500  ns      8.3%  ->    65.0%\n",
      "\n",
      "  Group                    3.5    0.3750  ns     15.8%  ->    32.5%   [sess 1 vs 5]\n",
      "  Subject-1                2.5    1.0000  ns     23.3%  ->    20.0%\n",
      "  Subject-2                0.0    0.5000  ns      8.3%  ->    45.0%\n",
      "\n",
      "  Group                    0.0    0.1250  ns     15.8%  ->    38.3%   [sess 1 vs 6]\n",
      "  Subject-1                0.0    0.5000  ns     23.3%  ->    33.3%\n",
      "  Subject-2                0.0    0.5000  ns      8.3%  ->    43.3%\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "from scipy.stats import wilcoxon\n",
    "\n",
    "# ── Session-vs-session-1 comparison ──────────────────────────────────────────\n",
    "# Paired by run index within subject; group level pools both subjects (6 pairs).\n",
    "# Wilcoxon signed-rank used throughout (matched pairs, non-parametric).\n",
    "\n",
    "for arr, label in [\n",
    "        (accuracy,     'accuracy'),\n",
    "        (timeout_rate, 'timeout rate'),\n",
    "]:\n",
    "    print('=' * 72)\n",
    "    print(f'WILCOXON RANK-SUM  —  each session vs session 1  ({label})')\n",
    "    print(f'  {\"\":20}  {\"stat\":>6}  {\"p\":>8}  sig   sess1 mean  sess_s mean')\n",
    "    print('  ' + '-' * 68)\n",
    "    for sess_idx in range(1, 6):                  # sessions 2-6 (0-indexed 1-5)\n",
    "        # Group level: pool both subjects -> 6 pairs\n",
    "        ref_g = arr[:, 0, :].flatten()            # session 1, all runs, both subj\n",
    "        cmp_g = arr[:, sess_idx, :].flatten()\n",
    "        try:\n",
    "            stat, p = wilcoxon(cmp_g, ref_g)\n",
    "        except ValueError:\n",
    "            stat, p = np.nan, 1.0\n",
    "        print(f'  {\"Group\":<20}  {stat:>6.1f}  {p:>8.4f}  {sig_stars(p)}'\n",
    "              f'   {ref_g.mean()*100:6.1f}%  ->  {cmp_g.mean()*100:6.1f}%'\n",
    "              f'   [sess 1 vs {sess_idx+1}]')\n",
    "\n",
    "        for si, name in enumerate(['Subject-1', 'Subject-2']):\n",
    "            ref_s = arr[si, 0, :]                 # 3 runs from session 1\n",
    "            cmp_s = arr[si, sess_idx, :]          # 3 runs from session s\n",
    "            try:\n",
    "                stat, p = wilcoxon(cmp_s, ref_s)\n",
    "            except ValueError:\n",
    "                stat, p = np.nan, 1.0\n",
    "            print(f'  {name:<20}  {stat:>6.1f}  {p:>8.4f}  {sig_stars(p)}'\n",
    "                  f'   {ref_s.mean()*100:6.1f}%  ->  {cmp_s.mean()*100:6.1f}%')\n",
    "        print()\n",
    "    print()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "97f4b056",
   "metadata": {},
   "source": [
    "---\n",
    "# Section 2.2 — PSD Feature Extraction"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 45,
   "id": "e3a96c27",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Frequency bins (14): [ 4  6  8 10 12 14 16 18 20 22 24 26 28 30] Hz\n",
      "Features per trial: 32 channels × 14 bins = 448\n",
      "\n",
      "Subject       Session     Run     Trials    Feature shape\n",
      "----------------------------------------------------------\n",
      "Subject-1     Session 1     Run 1   20        (20, 448)\n",
      "Subject-1     Session 1     Run 2   20        (20, 448)\n",
      "Subject-1     Session 1     Run 3   20        (20, 448)\n",
      "Subject-1     Session 2     Run 1   20        (20, 448)\n",
      "Subject-1     Session 2     Run 2   20        (20, 448)\n",
      "Subject-1     Session 2     Run 3   20        (20, 448)\n",
      "Subject-1     Session 3     Run 1   20        (20, 448)\n",
      "Subject-1     Session 3     Run 2   20        (20, 448)\n",
      "Subject-1     Session 3     Run 3   20        (20, 448)\n",
      "Subject-1     Session 4     Run 1   20        (20, 448)\n",
      "Subject-1     Session 4     Run 2   20        (20, 448)\n",
      "Subject-1     Session 4     Run 3   20        (20, 448)\n",
      "Subject-1     Session 5     Run 1   20        (20, 448)\n",
      "Subject-1     Session 5     Run 2   20        (20, 448)\n",
      "Subject-1     Session 5     Run 3   20        (20, 448)\n",
      "Subject-1     Session 6     Run 1   20        (20, 448)\n",
      "Subject-1     Session 6     Run 2   20        (20, 448)\n",
      "Subject-1     Session 6     Run 3   20        (20, 448)\n",
      "Subject-2     Session 1     Run 1   20        (20, 448)\n",
      "Subject-2     Session 1     Run 2   20        (20, 448)\n",
      "Subject-2     Session 1     Run 3   20        (20, 448)\n",
      "Subject-2     Session 2     Run 1   20        (20, 448)\n",
      "Subject-2     Session 2     Run 2   20        (20, 448)\n",
      "Subject-2     Session 2     Run 3   20        (20, 448)\n",
      "Subject-2     Session 3     Run 1   20        (20, 448)\n",
      "Subject-2     Session 3     Run 2   20        (20, 448)\n",
      "Subject-2     Session 3     Run 3   20        (20, 448)\n",
      "Subject-2     Session 4     Run 1   20        (20, 448)\n",
      "Subject-2     Session 4     Run 2   20        (20, 448)\n",
      "Subject-2     Session 4     Run 3   20        (20, 448)\n",
      "Subject-2     Session 5     Run 1   20        (20, 448)\n",
      "Subject-2     Session 5     Run 2   20        (20, 448)\n",
      "Subject-2     Session 5     Run 3   20        (20, 448)\n",
      "Subject-2     Session 6     Run 1   20        (20, 448)\n",
      "Subject-2     Session 6     Run 2   20        (20, 448)\n",
      "Subject-2     Session 6     Run 3   20        (20, 448)\n",
      "\n",
      "psd_flat shape : (720, 448)   (expected n_trials × 448)\n",
      "lbl_flat shape : (720,)\n",
      "Classes        : [769 770]  (769=left, 770=right)\n"
     ]
    }
   ],
   "source": [
    "# ── PSD Feature Extraction via Welch's Method ─────────────────────────────\n",
    "# Task period: from MI cue onset (TYP 769/770) to trial-end trigger\n",
    "# (TYP 7692/7693 for left, 7702/7703 for right).\n",
    "#\n",
    "# Frequency bins: 4, 6, 8, ..., 30 Hz  →  14 bins\n",
    "# Channels: 32\n",
    "# Features per trial: 32 × 14 = 448\n",
    "\n",
    "FREQ_BINS  = np.arange(4, 32, 2)        # [4, 6, 8, ..., 30] Hz — 14 bins\n",
    "N_FREQ     = len(FREQ_BINS)             # 14\n",
    "N_CH       = 32\n",
    "N_FEATURES = N_CH * N_FREQ             # 448\n",
    "\n",
    "# Welch parameters: nperseg = FS/2 → frequency resolution = FS/nperseg = 2 Hz\n",
    "NPERSEG  = FS // 2                      # 256 samples\n",
    "NOVERLAP = NPERSEG // 2                 # 128 samples (50 % overlap)\n",
    "\n",
    "# ── Trigger constants ────────────────────────────────────────────────────────\n",
    "CUE_TYPES = {769, 770}                   # MI cue onset (class label)\n",
    "TRIAL_END = {7692, 7693, 7702, 7703}    # hit / timeout / miss\n",
    "\n",
    "# psd_all[subj_idx][sess_idx][run_idx] → ndarray (n_trials, 448)\n",
    "# lbl_all[subj_idx][sess_idx][run_idx] → ndarray (n_trials,)  {769, 770}\n",
    "psd_all = []\n",
    "lbl_all = []\n",
    "\n",
    "for subj in subjects:\n",
    "    psd_subj, lbl_subj = [], []\n",
    "\n",
    "    for sess in subj.online:                     # 6 online sessions\n",
    "        psd_sess, lbl_sess = [], []\n",
    "\n",
    "        for run in sess.run:                     # 3 runs per session\n",
    "            typs = run.header.triggers.TYP\n",
    "            pos  = run.header.triggers.POS\n",
    "            eeg  = run.eeg                       # (n_samples, 32)\n",
    "\n",
    "            run_feats, run_lbls = [], []\n",
    "            cue_samp  = None\n",
    "            cue_class = None\n",
    "\n",
    "            for typ, p in zip(typs, pos):\n",
    "                if typ in CUE_TYPES:\n",
    "                    cue_samp  = p\n",
    "                    cue_class = typ\n",
    "                elif typ in TRIAL_END and cue_samp is not None:\n",
    "                    seg = eeg[cue_samp:p, :]     # (n_samp, 32)\n",
    "\n",
    "                    # Ensure segment is long enough for at least one Welch window\n",
    "                    nperseg_eff  = min(NPERSEG, len(seg) - 1) if len(seg) <= NPERSEG else NPERSEG\n",
    "                    noverlap_eff = nperseg_eff // 2\n",
    "\n",
    "                    feats = []\n",
    "                    for ch in range(N_CH):\n",
    "                        f, pxx = signal.welch(seg[:, ch], fs=FS,\n",
    "                                              nperseg=nperseg_eff,\n",
    "                                              noverlap=noverlap_eff)\n",
    "                        # Pick power at each target frequency bin\n",
    "                        # (with nperseg=256 & fs=512, f[k] = 2k Hz exactly)\n",
    "                        idx = np.searchsorted(f, FREQ_BINS)\n",
    "                        feats.append(pxx[idx])\n",
    "\n",
    "                    run_feats.append(np.concatenate(feats))   # (448,)\n",
    "                    run_lbls.append(cue_class)\n",
    "                    cue_samp = cue_class = None\n",
    "\n",
    "            psd_sess.append(np.array(run_feats))   # (n_trials, 448)\n",
    "            lbl_sess.append(np.array(run_lbls))    # (n_trials,)\n",
    "\n",
    "        psd_subj.append(psd_sess)\n",
    "        lbl_subj.append(lbl_sess)\n",
    "\n",
    "    psd_all.append(psd_subj)\n",
    "    lbl_all.append(lbl_subj)\n",
    "\n",
    "# ── Verification ─────────────────────────────────────────────────────────────\n",
    "print(f'Frequency bins ({N_FREQ}): {FREQ_BINS} Hz')\n",
    "print(f'Features per trial: {N_CH} channels × {N_FREQ} bins = {N_FEATURES}')\n",
    "print()\n",
    "print(f'{\"Subject\":<12}  {\"Session\":<10}  {\"Run\":<6}  {\"Trials\":<8}  {\"Feature shape\"}')\n",
    "print('-' * 58)\n",
    "for si, name in enumerate(subj_names):\n",
    "    for se in range(6):\n",
    "        for ri in range(3):\n",
    "            arr = psd_all[si][se][ri]\n",
    "            print(f'{name:<12}  Session {se+1:<3}   Run {ri+1}   '\n",
    "                  f'{arr.shape[0]:<8}  {arr.shape}')\n",
    "\n",
    "# Flat view: (total_trials, 448)\n",
    "psd_flat = np.vstack([psd_all[si][se][ri]\n",
    "                      for si in range(2)\n",
    "                      for se in range(6)\n",
    "                      for ri in range(3)])\n",
    "lbl_flat = np.concatenate([lbl_all[si][se][ri]\n",
    "                           for si in range(2)\n",
    "                           for se in range(6)\n",
    "                           for ri in range(3)])\n",
    "\n",
    "print()\n",
    "print(f'psd_flat shape : {psd_flat.shape}   (expected n_trials × 448)')\n",
    "print(f'lbl_flat shape : {lbl_flat.shape}')\n",
    "print(f'Classes        : {np.unique(lbl_flat)}  (769=left, 770=right)')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4189d952",
   "metadata": {},
   "source": [
    "---\n",
    "## Fisher Score per Feature per Session"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 46,
   "id": "24ac8274",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "fisher_scores shape: (2, 6, 448)  (subjects × sessions × features)\n",
      "\n",
      "Subject       Session      mean FS    max FS    min FS\n",
      "----------------------------------------------------\n",
      "Subject-1     Session 1       0.1208    0.8984    0.0009\n",
      "Subject-1     Session 2       0.1135    0.8981    0.0002\n",
      "Subject-1     Session 3       0.1551    0.5961    0.0003\n",
      "Subject-1     Session 4       0.1804    0.9310    0.0006\n",
      "Subject-1     Session 5       0.1402    0.7288    0.0000\n",
      "Subject-1     Session 6       0.1590    0.7643    0.0006\n",
      "\n",
      "Subject-2     Session 1       0.2412    0.7762    0.0003\n",
      "Subject-2     Session 2       0.1958    0.7303    0.0001\n",
      "Subject-2     Session 3       0.2200    0.7668    0.0003\n",
      "Subject-2     Session 4       0.3368    1.0675    0.0024\n",
      "Subject-2     Session 5       0.4006    1.5490    0.0017\n",
      "Subject-2     Session 6       0.5356    1.7666    0.0071\n",
      "\n"
     ]
    }
   ],
   "source": [
    "# ── Fisher Score  ─────────────────────────────────────────────────────────────\n",
    "# fisher_score(f) = |μ_c1(f) - μ_c2(f)| / sqrt(σ²_c1(f) + σ²_c2(f))\n",
    "#\n",
    "# Computed per subject × session, pooling all 3 runs (60 trials).\n",
    "# Result shape: (2, 6, 448)\n",
    "\n",
    "def fisher_score(X, y):\n",
    "    \"\"\"\n",
    "    X : (n_trials, n_features)\n",
    "    y : (n_trials,) with two unique class labels\n",
    "    Returns fisher_score vector of shape (n_features,).\n",
    "    \"\"\"\n",
    "    classes = np.unique(y)\n",
    "    c1, c2  = X[y == classes[0]], X[y == classes[1]]\n",
    "    mu1,  mu2  = c1.mean(axis=0),  c2.mean(axis=0)\n",
    "    var1, var2 = c1.var(axis=0),   c2.var(axis=0)\n",
    "    denom = np.sqrt(var1 + var2)\n",
    "    # Guard against zero denominator (constant feature)\n",
    "    denom[denom == 0] = np.nan\n",
    "    return np.abs(mu1 - mu2) / denom\n",
    "\n",
    "# fisher_scores[si, se, :] → (448,) scores for subject si, session se\n",
    "fisher_scores = np.full((2, 6, N_FEATURES), np.nan)\n",
    "\n",
    "for si in range(2):\n",
    "    for se in range(6):\n",
    "        # Pool all 3 runs for this session\n",
    "        X_sess = np.vstack([psd_all[si][se][ri] for ri in range(3)])   # (60, 448)\n",
    "        y_sess = np.concatenate([lbl_all[si][se][ri] for ri in range(3)])  # (60,)\n",
    "        fisher_scores[si, se] = fisher_score(X_sess, y_sess)\n",
    "\n",
    "print(f'fisher_scores shape: {fisher_scores.shape}  (subjects × sessions × features)')\n",
    "print()\n",
    "\n",
    "# ── Summary statistics ────────────────────────────────────────────────────────\n",
    "print(f'{\"Subject\":<12}  {\"Session\":<10}  {\"mean FS\":>8}  {\"max FS\":>8}  {\"min FS\":>8}')\n",
    "print('-' * 52)\n",
    "for si, name in enumerate(subj_names):\n",
    "    for se in range(6):\n",
    "        fs_vec = fisher_scores[si, se]\n",
    "        print(f'{name:<12}  Session {se+1:<3}   '\n",
    "              f'{np.nanmean(fs_vec):8.4f}  '\n",
    "              f'{np.nanmax(fs_vec):8.4f}  '\n",
    "              f'{np.nanmin(fs_vec):8.4f}')\n",
    "    print()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 47,
   "id": "8940a750",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Top-10 discriminative features — Subject-2 (mean Fisher score across sessions)\n",
      "  Rank   Feat#   Channel   Freq (Hz)     Mean FS\n",
      "  ----------------------------------------------\n",
      "  1      111     F8        30             0.9232\n",
      "  2      167     FC6       30             0.9024\n",
      "  3      166     FC6       28             0.8835\n",
      "  4      165     FC6       26             0.8601\n",
      "  5      110     F8        28             0.8476\n",
      "  6      98      F8        4              0.8023\n",
      "  7      39      Fp2       26             0.7492\n",
      "  8      108     F8        24             0.7477\n",
      "  9      109     F8        26             0.7421\n",
      "  10     163     FC6       22             0.7373\n"
     ]
    }
   ],
   "source": [
    "# ── Top-10 features for Subject-2 (averaged across all 6 sessions) ───────────\n",
    "# Feature index i  →  channel: i // N_FREQ,  freq bin: FREQ_BINS[i % N_FREQ]\n",
    "\n",
    "fs_s2_mean = np.nanmean(fisher_scores[1], axis=0)   # (448,) mean over sessions\n",
    "\n",
    "top10_idx = np.argsort(fs_s2_mean)[::-1][:10]\n",
    "\n",
    "print('Top-10 discriminative features — Subject-2 (mean Fisher score across sessions)')\n",
    "print(f'  {\"Rank\":<5}  {\"Feat#\":<6}  {\"Channel\":<8}  {\"Freq (Hz)\":<11}  {\"Mean FS\":>8}')\n",
    "print('  ' + '-' * 46)\n",
    "for rank, fi in enumerate(top10_idx, 1):\n",
    "    ch_idx  = fi // N_FREQ\n",
    "    freq_hz = FREQ_BINS[fi % N_FREQ]\n",
    "    print(f'  {rank:<5}  {fi:<6}  {CH_LABELS[ch_idx]:<8}  {freq_hz:<11}  '\n",
    "          f'{fs_s2_mean[fi]:8.4f}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 49,
   "id": "02c84725",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 204972 characters]",
      "text/plain": [
       "<Figure size 1680x936 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Saved fig_top10_fisher_scores.png\n"
     ]
    }
   ],
   "source": [
    "# ── Per-session Fisher scores for the top-10 Subject-2 features — PNG table ──\n",
    "\n",
    "col_labels = ['Feature', 'Subject', 'S1', 'S2', 'S3', 'S4', 'S5', 'S6', 'Mean', 'Min', 'Max']\n",
    "\n",
    "# Subject colour bands\n",
    "COLORS = {\n",
    "    'Subject-1': '#dce6f1',   # light blue\n",
    "    'Subject-2': '#e2efda',   # light green\n",
    "    'header':    '#4472c4',\n",
    "    'feat_hdr':  '#d0d8e8',\n",
    "}\n",
    "\n",
    "cell_text  = []\n",
    "cell_color = []\n",
    "\n",
    "for rank, fi in enumerate(top10_idx, 1):\n",
    "    ch_lbl  = CH_LABELS[fi // N_FREQ]\n",
    "    freq_hz = FREQ_BINS[fi % N_FREQ]\n",
    "    feat_label = f'#{rank}  {ch_lbl}@{freq_hz}Hz'\n",
    "\n",
    "    for si, name in enumerate(subj_names):\n",
    "        sc  = fisher_scores[si, :, fi]            # (6,)\n",
    "        row = [feat_label if si == 0 else '',\n",
    "               name,\n",
    "               *[f'{v:.3f}' for v in sc],\n",
    "               f'{np.nanmean(sc):.3f}',\n",
    "               f'{np.nanmin(sc):.3f}',\n",
    "               f'{np.nanmax(sc):.3f}']\n",
    "        cell_text.append(row)\n",
    "\n",
    "        bg = COLORS['Subject-1'] if si == 0 else COLORS['Subject-2']\n",
    "        cell_color.append([bg] * len(col_labels))\n",
    "\n",
    "n_rows = len(cell_text)\n",
    "n_cols = len(col_labels)\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(14, n_rows * 0.35 + 0.8))\n",
    "ax.axis('off')\n",
    "\n",
    "tbl = ax.table(\n",
    "    cellText=cell_text,\n",
    "    colLabels=col_labels,\n",
    "    cellLoc='center',\n",
    "    loc='center',\n",
    ")\n",
    "tbl.auto_set_font_size(False)\n",
    "tbl.set_fontsize(8.5)\n",
    "tbl.scale(1, 1.35)\n",
    "\n",
    "# Style header row\n",
    "for col in range(n_cols):\n",
    "    cell = tbl[0, col]\n",
    "    cell.set_facecolor(COLORS['header'])\n",
    "    cell.set_text_props(color='white', fontweight='bold')\n",
    "\n",
    "# Style data rows\n",
    "for row in range(1, n_rows + 1):\n",
    "    for col in range(n_cols):\n",
    "        tbl[row, col].set_facecolor(cell_color[row - 1][col])\n",
    "        # Bold the summary columns\n",
    "        if col >= 8:\n",
    "            tbl[row, col].set_text_props(fontweight='bold')\n",
    "        # Left-align feature label and subject name\n",
    "        if col <= 1:\n",
    "            tbl[row, col].set_text_props(ha='left')\n",
    "\n",
    "# Wider columns for feature label and subject\n",
    "tbl.auto_set_column_width(list(range(n_cols)))\n",
    "\n",
    "ax.set_title('Top-10 Subject-2 Features — Fisher Score per Session (both subjects)',\n",
    "             fontsize=11, fontweight='bold', pad=10)\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.savefig('fig_top10_fisher_scores.png', bbox_inches='tight', dpi=150)\n",
    "plt.show()\n",
    "print('Saved fig_top10_fisher_scores.png')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 51,
   "id": "ff9180d4",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 882844 characters]",
      "text/plain": [
       "<Figure size 1800x720 with 14 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Saved fig_topoplot_fisher_per_session.png\n"
     ]
    }
   ],
   "source": [
    "# ── Topoplot: summed Fisher score per channel per session ─────────────────────\n",
    "# Top of each topoplot = NOSE (front of head).\n",
    "# pos2d was built as [-Y_eeg, X_eeg]; X_eeg points toward the nose in EEGLAB\n",
    "# convention, so the +y axis of the plot = forward = nose.\n",
    "\n",
    "channel_fs = fisher_scores.reshape(2, 6, N_CH, N_FREQ).sum(axis=-1)  # (2, 6, 32)\n",
    "\n",
    "def add_nose_marker(ax):\n",
    "    \"\"\"Draw a filled triangle + 'Nose' label at the top-centre of a topomap axes.\"\"\"\n",
    "    from matplotlib.patches import FancyArrowPatch\n",
    "    # Filled upward-pointing triangle in axis fraction coords\n",
    "    tri_x = [0.47, 0.53, 0.50, 0.47]\n",
    "    tri_y = [0.96, 0.96, 1.03, 0.96]\n",
    "    ax.fill(tri_x, tri_y, transform=ax.transAxes,\n",
    "            color='black', clip_on=False, zorder=5)\n",
    "    ax.text(0.50, 1.06, 'Nose', transform=ax.transAxes,\n",
    "            ha='center', va='bottom', fontsize=7,\n",
    "            fontweight='bold', color='black', clip_on=False)\n",
    "\n",
    "fig, axes = plt.subplots(2, 6, figsize=(15, 6))\n",
    "fig.suptitle('Summed Fisher Score per EEG Channel — per Session\\n'\n",
    "             '(▲ = Nose / front of head)',\n",
    "             fontsize=12, fontweight='bold', y=1.02)\n",
    "\n",
    "for si, name in enumerate(subj_names):\n",
    "    vmin = channel_fs[si].min()\n",
    "    vmax = channel_fs[si].max()\n",
    "\n",
    "    for se in range(6):\n",
    "        ax = axes[si, se]\n",
    "        data = channel_fs[si, se]\n",
    "\n",
    "        im, _ = plot_topomap(\n",
    "            data, pos2d,\n",
    "            axes=ax,\n",
    "            vlim=(vmin, vmax),\n",
    "            cmap='RdBu_r',\n",
    "            show=False,\n",
    "            sensors=True,\n",
    "            contours=4,\n",
    "        )\n",
    "        add_nose_marker(ax)\n",
    "        ax.set_title(f'Session {se + 1}', fontsize=9, pad=18)\n",
    "        if se == 0:\n",
    "            ax.set_ylabel(name, fontsize=10, fontweight='bold', labelpad=8)\n",
    "\n",
    "    cbar = fig.colorbar(im, ax=axes[si, :], shrink=0.75, pad=0.02)\n",
    "    cbar.set_label('Σ Fisher Score', fontsize=9)\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.savefig('fig_topoplot_fisher_per_session.png', bbox_inches='tight', dpi=150)\n",
    "plt.show()\n",
    "print('Saved fig_topoplot_fisher_per_session.png')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 52,
   "id": "48ff768e",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "====================================================================\n",
      "BEST FEATURE (highest Fisher score in Session 6) per subject\n",
      "====================================================================\n",
      "  Subject-1: feat #227  →  C4 @ 10 Hz\n",
      "           FS per session: S1=0.518  S2=0.764  S3=0.540  S4=0.931  S5=0.603  S6=0.764\n",
      "\n",
      "  Subject-2: feat #165  →  FC6 @ 26 Hz\n",
      "           FS per session: S1=0.356  S2=0.537  S3=0.215  S4=1.051  S5=1.235  S6=1.767\n",
      "\n",
      "====================================================================\n",
      "SUBJECT-LEVEL TREND  (Mann-Kendall + Spearman, n=6 sessions)\n",
      "====================================================================\n",
      "  Subject-1\n",
      "    Mann-Kendall : tau = +0.467   p = 0.2597  ns   trend: no trend\n",
      "    Spearman ρ   : rho = +0.600   p = 0.2080  ns\n",
      "\n",
      "  Subject-2\n",
      "    Mann-Kendall : tau = +0.733   p = 0.0603  ns   trend: no trend\n",
      "    Spearman ρ   : rho = +0.829   p = 0.0416  *\n",
      "\n",
      "====================================================================\n",
      "GROUP-LEVEL  Session-1 vs Session-6  (Wilcoxon signed-rank, n=2)\n",
      "NOTE: n=2 → minimum achievable p = 0.500; interpret descriptively\n",
      "====================================================================\n",
      "  Session-1 values : Subject-1=0.518  Subject-2=0.356\n",
      "  Session-6 values : Subject-1=0.764  Subject-2=1.767\n",
      "  Δ (S6 − S1)      : Subject-1=+0.246  Subject-2=+1.411\n",
      "  Wilcoxon (S6>S1) : W = 3.0   p = 0.2500  ns\n",
      "\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 69852 characters]",
      "text/plain": [
       "<Figure size 840x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Saved fig_best_feature_trajectory.png\n"
     ]
    }
   ],
   "source": [
    "# ── Best feature per subject (highest FS in session 6) tracked across sessions\n",
    "# Statistical analysis:\n",
    "#   Subject level  — Mann-Kendall trend test (monotonic trend, n=6, non-parametric)\n",
    "#                    + Spearman ρ as effect size\n",
    "#   Group level    — Wilcoxon signed-rank session-1 vs session-6 (paired, non-parametric)\n",
    "#                    NOTE: n=2 subjects → min achievable p = 0.5; interpret descriptively\n",
    "\n",
    "from scipy.stats import spearmanr, wilcoxon, ttest_rel\n",
    "\n",
    "sessions_arr = np.arange(1, 7)\n",
    "\n",
    "best_feat_idx  = []   # per subject: feature index with max FS in session 6\n",
    "best_feat_traj = []   # per subject: (6,) FS values across sessions\n",
    "\n",
    "print('=' * 68)\n",
    "print('BEST FEATURE (highest Fisher score in Session 6) per subject')\n",
    "print('=' * 68)\n",
    "for si, name in enumerate(subj_names):\n",
    "    fi   = np.nanargmax(fisher_scores[si, 5, :])        # session 6 = index 5\n",
    "    ch   = CH_LABELS[fi // N_FREQ]\n",
    "    freq = FREQ_BINS[fi % N_FREQ]\n",
    "    traj = fisher_scores[si, :, fi]                     # (6,)\n",
    "    best_feat_idx.append(fi)\n",
    "    best_feat_traj.append(traj)\n",
    "    print(f'  {name}: feat #{fi}  →  {ch} @ {freq} Hz')\n",
    "    print(f'           FS per session: ' +\n",
    "          '  '.join([f'S{s+1}={v:.3f}' for s, v in enumerate(traj)]))\n",
    "    print()\n",
    "\n",
    "# ── Subject-level trend: Mann-Kendall + Spearman ─────────────────────────────\n",
    "print('=' * 68)\n",
    "print('SUBJECT-LEVEL TREND  (Mann-Kendall + Spearman, n=6 sessions)')\n",
    "print('=' * 68)\n",
    "for si, name in enumerate(subj_names):\n",
    "    traj = best_feat_traj[si]\n",
    "    mk_res  = mk.original_test(traj)\n",
    "    rho, p_sp = spearmanr(sessions_arr, traj)\n",
    "    print(f'  {name}')\n",
    "    print(f'    Mann-Kendall : tau = {mk_res.Tau:+.3f}   p = {mk_res.p:.4f}'\n",
    "          f'  {sig_stars(mk_res.p)}   trend: {mk_res.trend}')\n",
    "    print(f'    Spearman ρ   : rho = {rho:+.3f}   p = {p_sp:.4f}'\n",
    "          f'  {sig_stars(p_sp)}')\n",
    "    print()\n",
    "\n",
    "# ── Group-level: session 1 vs session 6, Wilcoxon signed-rank ────────────────\n",
    "print('=' * 68)\n",
    "print('GROUP-LEVEL  Session-1 vs Session-6  (Wilcoxon signed-rank, n=2)')\n",
    "print('NOTE: n=2 → minimum achievable p = 0.500; interpret descriptively')\n",
    "print('=' * 68)\n",
    "s1_vals = np.array([best_feat_traj[si][0] for si in range(2)])\n",
    "s6_vals = np.array([best_feat_traj[si][5] for si in range(2)])\n",
    "try:\n",
    "    stat, p_wx = wilcoxon(s6_vals, s1_vals, alternative='greater')\n",
    "except ValueError:\n",
    "    stat, p_wx = np.nan, 1.0\n",
    "\n",
    "print(f'  Session-1 values : ' +\n",
    "      '  '.join([f'{n}={v:.3f}' for n, v in zip(subj_names, s1_vals)]))\n",
    "print(f'  Session-6 values : ' +\n",
    "      '  '.join([f'{n}={v:.3f}' for n, v in zip(subj_names, s6_vals)]))\n",
    "print(f'  Δ (S6 − S1)      : ' +\n",
    "      '  '.join([f'{n}={d:+.3f}' for n, d in zip(subj_names, s6_vals - s1_vals)]))\n",
    "print(f'  Wilcoxon (S6>S1) : W = {stat}   p = {p_wx:.4f}  {sig_stars(p_wx)}')\n",
    "print()\n",
    "\n",
    "# ── Plot: trajectories of the best feature per subject ────────────────────────\n",
    "fig, ax = plt.subplots(figsize=(7, 4))\n",
    "markers = ['o', 's']\n",
    "colors  = ['#2176ae', '#e05c2a']\n",
    "\n",
    "for si, name in enumerate(subj_names):\n",
    "    fi   = best_feat_idx[si]\n",
    "    ch   = CH_LABELS[fi // N_FREQ]\n",
    "    freq = FREQ_BINS[fi % N_FREQ]\n",
    "    traj = best_feat_traj[si]\n",
    "    ax.plot(sessions_arr, traj, marker=markers[si], color=colors[si],\n",
    "            linewidth=2, markersize=7,\n",
    "            label=f'{name}  ({ch} @ {freq} Hz)')\n",
    "\n",
    "ax.set_xlabel('Online Session', fontsize=11)\n",
    "ax.set_ylabel('Fisher Score', fontsize=11)\n",
    "ax.set_title('Best-feature Fisher Score Trajectory\\n'\n",
    "             '(feature selected by highest FS in Session 6)', fontsize=11)\n",
    "ax.set_xticks(sessions_arr)\n",
    "ax.set_xticklabels([f'S{s}' for s in sessions_arr])\n",
    "ax.legend(fontsize=9)\n",
    "ax.grid(True, alpha=0.3)\n",
    "plt.tight_layout()\n",
    "plt.savefig('fig_best_feature_trajectory.png', bbox_inches='tight', dpi=150)\n",
    "plt.show()\n",
    "print('Saved fig_best_feature_trajectory.png')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "eaa3ec97",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ── Subject-2: top-10 features in session 6, correlated with accuracy ─────────\n",
    "#\n",
    "# Statistical approach:\n",
    "#   Spearman ρ  — primary test: non-parametric rank correlation, robust for n=6,\n",
    "#                 appropriate when normality/linearity can't be assumed.\n",
    "#   Pearson r   — reported alongside as reference.\n",
    "#   Multiple comparisons (10 tests) corrected with Benjamini-Hochberg FDR.\n",
    "#   Note: with n=6 the two-tailed Spearman significance threshold is |ρ|≈0.886\n",
    "#   (p<0.05, uncorrected), so only very strong correlations can reach significance.\n",
    "\n",
    "from scipy.stats import spearmanr, pearsonr\n",
    "\n",
    "def bh_fdr(p_values, alpha=0.05):\n",
    "    \"\"\"Benjamini-Hochberg FDR correction. Returns (reject, p_adjusted).\"\"\"\n",
    "    p = np.asarray(p_values, dtype=float)\n",
    "    m = len(p)\n",
    "    order = np.argsort(p)\n",
    "    p_sorted = p[order]\n",
    "    # Adjusted p-values (monotone-enforced from right to left)\n",
    "    p_adj_sorted = np.minimum(1.0,\n",
    "        np.minimum.accumulate((p_sorted * m / np.arange(1, m + 1))[::-1])[::-1])\n",
    "    reject_sorted = p_adj_sorted <= alpha\n",
    "    # Restore original order\n",
    "    p_adj = np.empty(m)\n",
    "    reject = np.empty(m, dtype=bool)\n",
    "    p_adj[order] = p_adj_sorted\n",
    "    reject[order] = reject_sorted\n",
    "    return reject, p_adj\n",
    "\n",
    "# ── Subject-2 accuracy per session (mean over 3 runs) ─────────────────────────\n",
    "acc_s2 = accuracy[1].mean(axis=1)           # (6,)\n",
    "\n",
    "# ── Top-10 features for Subject-2 in session 6 specifically ───────────────────\n",
    "s2_s6_fs  = fisher_scores[1, 5, :]          # (448,) FS in session 6\n",
    "top10_s6  = np.argsort(s2_s6_fs)[::-1][:10]\n",
    "\n",
    "# ── Compute correlations ───────────────────────────────────────────────────────\n",
    "results = []\n",
    "for fi in top10_s6:\n",
    "    fs_traj        = fisher_scores[1, :, fi]   # (6,) FS across sessions\n",
    "    rho,  p_sp     = spearmanr(fs_traj, acc_s2)\n",
    "    r,    p_pe     = pearsonr(fs_traj,  acc_s2)\n",
    "    results.append(dict(fi=fi,\n",
    "                        ch=CH_LABELS[fi // N_FREQ],\n",
    "                        freq=FREQ_BINS[fi % N_FREQ],\n",
    "                        fs_s6=s2_s6_fs[fi],\n",
    "                        rho=rho, p_sp=p_sp,\n",
    "                        r=r,     p_pe=p_pe))\n",
    "\n",
    "# ── Benjamini-Hochberg FDR correction on Spearman p-values ────────────────────\n",
    "p_raw = np.array([d['p_sp'] for d in results])\n",
    "reject, p_fdr = bh_fdr(p_raw, alpha=0.05)\n",
    "for i, d in enumerate(results):\n",
    "    d['p_fdr']   = p_fdr[i]\n",
    "    d['sig_fdr'] = reject[i]\n",
    "\n",
    "# ── Sign test: are all ρ in the same direction? ────────────────────────────────\n",
    "from scipy.stats import binomtest\n",
    "n_pos = sum(d['rho'] > 0 for d in results)\n",
    "n_neg = len(results) - n_pos\n",
    "binom_res = binomtest(n_pos, len(results), p=0.5, alternative='two-sided')\n",
    "\n",
    "# ── Print table ────────────────────────────────────────────────────────────────\n",
    "print('Subject-2 — Top-10 Session-6 Features: Spearman ρ with accuracy')\n",
    "print('n=6 sessions; two-tailed Spearman significance threshold ≈ |ρ|>0.886 (p<0.05)')\n",
    "print()\n",
    "print(f'  {\"Rk\":<3} {\"Feature\":<11} {\"FS@S6\":>6}  '\n",
    "      f'{\"ρ\":>6}  {\"p(raw)\":>8}  {\"sig\":>4}  {\"p(FDR)\":>8}  {\"FDR\":>5}  {\"r(Pearson)\":>10}')\n",
    "print('  ' + '-' * 72)\n",
    "for rank, d in enumerate(results, 1):\n",
    "    fdr_tag = 'FDR*' if d['sig_fdr'] else ''\n",
    "    print(f'  {rank:<3} {d[\"ch\"]:<4}@{d[\"freq\"]:>2}Hz  {d[\"fs_s6\"]:>6.3f}  '\n",
    "          f'{d[\"rho\"]:>+6.3f}  {d[\"p_sp\"]:>8.4f}  {sig_stars(d[\"p_sp\"]):>4}  '\n",
    "          f'{d[\"p_fdr\"]:>8.4f}  {fdr_tag:>5}  {d[\"r\"]:>+10.3f}')\n",
    "\n",
    "print()\n",
    "n_sig_raw = sum(d['p_sp'] < 0.05 for d in results)\n",
    "n_sig_fdr = sum(d['sig_fdr'] for d in results)\n",
    "print(f'  Significant uncorrected (p<0.05) : {n_sig_raw} / 10 feature(s)')\n",
    "print(f'  Significant FDR-corrected        : {n_sig_fdr} / 10 feature(s)')\n",
    "print()\n",
    "print(f'  Sign test (binomial): {n_pos} positive / {n_neg} negative ρ values')\n",
    "print(f'  H0: ρ direction random (p=0.5 each) → p = {binom_res.pvalue:.4f}  {sig_stars(binom_res.pvalue)}')\n",
    "print(f'  Interpretation: {\"All features trend in the same direction (positive)\" if n_pos == len(results) else f\"{n_pos}/{len(results)} features trend positively\"}')\n",
    "\n",
    "# ── Plot: dual-axis FS trajectory vs accuracy for each feature ─────────────────\n",
    "fig, axes = plt.subplots(2, 5, figsize=(15, 6))\n",
    "axes = axes.flatten()\n",
    "\n",
    "for rank, (d, ax) in enumerate(zip(results, axes), 1):\n",
    "    fs_traj = fisher_scores[1, :, d['fi']]\n",
    "    ax2 = ax.twinx()\n",
    "\n",
    "    l1, = ax.plot(sessions_arr, fs_traj,      'o-',  color='#2176ae', lw=2,   ms=5)\n",
    "    l2, = ax2.plot(sessions_arr, acc_s2 * 100, 's--', color='#e05c2a', lw=1.5, ms=5)\n",
    "\n",
    "    # Green border = uncorrected significant; gold = FDR significant; grey = ns\n",
    "    if d['sig_fdr']:\n",
    "        bcol, bw = '#d4a000', 3.0\n",
    "    elif d['p_sp'] < 0.05:\n",
    "        bcol, bw = '#228b22', 2.5\n",
    "    else:\n",
    "        bcol, bw = '#aaaaaa', 1.5\n",
    "    for spine in ax.spines.values():\n",
    "        spine.set_edgecolor(bcol); spine.set_linewidth(bw)\n",
    "\n",
    "    sig_str = f\"ρ={d['rho']:+.2f}  p={d['p_sp']:.3f}\"\n",
    "    if d['sig_fdr']:        sig_str += ' ★FDR'\n",
    "    elif d['p_sp'] < 0.05:  sig_str += ' *'\n",
    "    ax.set_title(f'#{rank} {d[\"ch\"]}@{d[\"freq\"]}Hz\\n{sig_str}', fontsize=8)\n",
    "    ax.set_xticks(sessions_arr)\n",
    "    ax.set_xticklabels([f'S{s}' for s in sessions_arr], fontsize=7)\n",
    "    ax.set_ylabel('Fisher Score', fontsize=7, color='#2176ae')\n",
    "    ax2.set_ylabel('Acc %',       fontsize=7, color='#e05c2a')\n",
    "    ax.tick_params(axis='y',  labelcolor='#2176ae', labelsize=7)\n",
    "    ax2.tick_params(axis='y', labelcolor='#e05c2a', labelsize=7)\n",
    "\n",
    "from matplotlib.lines import Line2D\n",
    "legend_handles = [\n",
    "    Line2D([0],[0], color='#2176ae', marker='o',          label='Fisher Score (FS)'),\n",
    "    Line2D([0],[0], color='#e05c2a', marker='s', ls='--', label='Accuracy (%)'),\n",
    "    Line2D([0],[0], color='#228b22', lw=3,                label='p<0.05 uncorrected'),\n",
    "    Line2D([0],[0], color='#d4a000', lw=3,                label='FDR significant'),\n",
    "    Line2D([0],[0], color='#aaaaaa', lw=3,                label='ns'),\n",
    "]\n",
    "fig.legend(handles=legend_handles, loc='lower center', ncol=5,\n",
    "           fontsize=9, bbox_to_anchor=(0.5, -0.04))\n",
    "fig.suptitle('Subject-2: Fisher Score vs Accuracy — Top-10 Session-6 Features\\n'\n",
    "             'Border colour = significance level',\n",
    "             fontsize=11, fontweight='bold')\n",
    "plt.tight_layout()\n",
    "plt.savefig('fig_s2_fs_vs_accuracy.png', bbox_inches='tight', dpi=150)\n",
    "plt.show()\n",
    "print('Saved fig_s2_fs_vs_accuracy.png')"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "venv",
   "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.13.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
