OSCR

Personalized adaptive virtual reality experience driven by electroencephalography-based pain recognition.

Code ↔ Paper

12 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 12 matches
  1. [1] § Materials and methods › Data analysis ↔ EEG_XGBoost_Pain_Classification.ipynb, lines 521–541 · score 0.75 · max_depth, learning_rate, n_estimators, metrics, Hyperparameter, classification
  2. [2] § Materials and methods › Sample technique and feature extraction ↔ EEG_LightGBM_Pain_Classification.ipynb, lines 260–327 · score 0.63 · wavelet transform, Daubechies, coefficient, db4, wavedec, energy
  3. [3] § Materials and methods › Sample technique and feature extraction ↔ EEG_XGBoost_Pain_Classification.ipynb, lines 258–325 · score 0.63 · wavelet transform, Daubechies, coefficient, db4, wavedec, energy
  4. [4] § Materials and methods › Sample technique and feature extraction ↔ EEG_LightGBM_Pain_Classification.ipynb, lines 163–182 · score 0.62 · find bad eog, ICA component, eye, scores, channels, EEG
  5. [5] § Materials and methods › Sample technique and feature extraction ↔ EEG_XGBoost_Pain_Classification.ipynb, lines 161–180 · score 0.62 · find bad eog, ICA component, eye, scores, channels, EEG
  6. [6] § Materials and methods › Data analysis ↔ EEG_XGBoost_Pain_Classification.ipynb, lines 521–541 · score 0.61 · unique pain, XGBoost, softmax, metrics, objective, overfitting
  7. [7] § Materials and methods › Model validity and reliability ↔ EEG_LightGBM_Pain_Classification.ipynb, lines 508–522 · score 0.61 · StandardScaler, XGBoost, partition, stratified, split, classes
  8. [8] § Materials and methods › Model validity and reliability ↔ EEG_XGBoost_Pain_Classification.ipynb, lines 505–519 · score 0.61 · StandardScaler, XGBoost, partition, stratified, split, classes
  9. [9] § Materials and methods › Data analysis ↔ EEG_LightGBM_Pain_Classification.ipynb, lines 524–540 · score 0.57 · learning_rate, n_estimators, Hyperparameter, classification, model
  10. [10] § Materials and methods › Population and dataset ↔ EEG_LightGBM_Pain_Classification.ipynb, lines 45–60 · score 0.53 · raw EEG, BrainVision, vhdr, Pain
  11. [11] § Materials and methods › Population and dataset ↔ EEG_XGBoost_Pain_Classification.ipynb, lines 43–58 · score 0.53 · raw EEG, BrainVision, vhdr, Pain
  12. [12] § Materials and methods › Setting and system architecture ↔ EEG_LightGBM_Pain_Classification.ipynb, lines 1–32 · score 0.51 · PyWavelets, libraries, gradient, LightGBM, MNE, boosting

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Jupyter notebook · 605 lines · 23 KB · no license · 6 matches

  1. # %% [markdown]
  2. # # EEG Pain-Level Classification with XGBoost
  3. #
  4. # A full pipeline for classifying subjective pain levels from EEG data using Discrete Wavelet Transform (DWT) feature extraction and an XGBoost classifier.
  5. #
  6. # **Pipeline overview:**
  7. # 1. [Environment Setup](#1-environment-setup)
  8. # 2. [EEG Data Loading & Inspection](#2-eeg-data-loading--inspection)
  9. # 3. [Preprocessing & Artifact Removal (ICA)](#3-preprocessing--artifact-removal-ica)
  10. # 4. [Epoching & Visualisation](#4-epoching--visualisation)
  11. # 5. [DWT Feature Extraction](#5-dwt-feature-extraction)
  12. # 6. [Merging Per-Participant CSV Files](#6-merging-per-participant-csv-files)
  13. # 7. [Dataset Preparation](#7-dataset-preparation)
  14. # 8. [XGBoost Model Training & Evaluation](#8-xgboost-model-training--evaluation)
  15. # 9. [Model Saving](#9-model-saving)
  16. # %% [markdown]
  17. # ## 1. Environment Setup
  18. # %%
  19. # Install required packages
  20. # MNE: EEG/MEG analysis library
  21. # autoreject: automatic rejection threshold estimation for EEG epochs
  22. # PyWavelets: Discrete Wavelet Transform implementation
  23. %pip install mne autoreject PyWavelets
  24. # %%
  25. # Mount Google Drive to access EEG data files and save outputs
  26. from google.colab import drive
  27. drive.mount('/content/drive')
  28. # %%
  29. import os
  30. import numpy as np
  31. import pandas as pd
  32. import matplotlib.pyplot as plt
  33. import seaborn as sns
  34. import mne
  35. # Suppress verbose MNE logs — set to 'warning' or 'info' for more detail
  36. mne.set_log_level('error')
  37. # %% [markdown]
  38. # ## 2. EEG Data Loading & Inspection
  39. #
  40. # Load a single BrainVision `.vhdr` file and inspect its annotations.
  41. # Each annotation encodes a pain stimulus level (e.g. `Comment/10` = pain level 10).
  42. # %%
  43. # ── Configuration ──────────────────────────────────────────────────────────
  44. # Update p_id to match the participant you want to process.
  45. p_id = 'vp51'
  46. data_dir = '/content/drive/My Drive/EEG_Data/'
  47. raw_file = os.path.join(data_dir, f'Exp_Mediation_Paradigm1_Perception_{p_id}.vhdr')
  48. # Load raw EEG recording; preload=True reads all data into memory up front
  49. raw = mne.io.read_raw_brainvision(raw_file, preload=True, verbose='error')
  50. print(raw.info)
  51. # %%
  52. # Inspect annotations to understand which pain levels are present in this file.
  53. # Run this before defining event_mapping — the available Comment/* codes vary
  54. # between participants.
  55. mne.events_from_annotations(raw)
  56. # %%
  57. events, event_dict = mne.events_from_annotations(raw)
  58. # Map annotation strings to integer event IDs.
  59. # Adjust this dictionary based on the output of the cell above.
  60. event_mapping = {
  61. 'Comment/10': 10001,
  62. 'Comment/20': 10002,
  63. 'Comment/30': 10003,
  64. 'Comment/40': 10004,
  65. 'Comment/50': 10006,
  66. 'Comment/60': 10007,
  67. }
  68. # Plot the event timeline to verify the mapping visually
  69. fig, ax = plt.subplots(figsize=(15, 5))
  70. mne.viz.plot_events(events, raw.info['sfreq'], event_id=event_mapping, axes=ax)
  71. plt.title(f'Event Timeline — Participant {p_id}')
  72. plt.show()
  73. # %% [markdown]
  74. # ## 3. Preprocessing & Artifact Removal (ICA)
  75. #
  76. # Steps performed here:
  77. # 1. Assign correct channel types (EOG, ECG, misc) so MNE can apply them correctly.
  78. # 2. Set the standard 10-05 electrode montage.
  79. # 3. Band-pass filter the raw signal (0.1 – 30 Hz for general analysis; 1 – 30 Hz for ICA).
  80. # 4. Create fixed-length epochs, automatically estimate an amplitude rejection threshold,
  81. # then fit ICA and exclude eye-movement (EOG) components.
  82. # %%
  83. # ── Channel type assignment ─────────────────────────────────────────────────
  84. # MNE needs to know which channels are EEG vs. EOG vs. ECG so that it can
  85. # apply artifact detection and montage correctly.
  86. channel_types = {
  87. 'LE': 'eog', # Left eye electrode
  88. 'RE': 'eog', # Right eye electrode
  89. 'ECG': 'ecg', # Cardiac channel
  90. 'Ne': 'misc', # Non-EEG / miscellaneous
  91. 'Ma': 'misc',
  92. 'Ext': 'misc',
  93. }
  94. raw.set_channel_types(channel_types)
  95. # Apply the standard 10-05 electrode position system
  96. montage = mne.channels.make_standard_montage('standard_1005')
  97. raw.set_montage(montage)
  98. # %%
  99. # ── Band-pass filtering (for downstream epoching & ERPs) ───────────────────
  100. # 0.1 Hz high-pass removes slow drifts; 30 Hz low-pass removes high-frequency
  101. # noise and line interference above the gamma band of interest.
  102. low_cut = 0.1
  103. hi_cut = 30.0
  104. raw_filt = raw.copy().filter(low_cut, hi_cut)
  105. # Visualise the power spectral density to confirm the filter worked
  106. raw_filt.plot_psd(fmax=40, show=False)
  107. plt.show()
  108. # %%
  109. # ── ICA preparation ─────────────────────────────────────────────────────────
  110. # ICA works best when low-frequency power is removed more aggressively (≥1 Hz).
  111. # We create a separate filtered copy solely for ICA fitting.
  112. ica_low_cut = 1.0
  113. raw_ica = raw.copy().filter(ica_low_cut, hi_cut)
  114. # Segment the ICA copy into 1-second fixed-length epochs.
  115. # These are used only for fitting ICA — not for downstream analysis.
  116. tstep = 1.0
  117. events_ica = mne.make_fixed_length_events(raw_ica, duration=tstep)
  118. epochs_ica = mne.Epochs(
  119. raw_ica, events_ica,
  120. tmin=0.0, tmax=tstep,
  121. baseline=None, preload=True
  122. )
  123. # %%
  124. from autoreject import get_rejection_threshold
  125. # Automatically compute a peak-to-peak amplitude rejection threshold.
  126. # Epochs exceeding this threshold are excluded from ICA fitting,
  127. # preventing large artefacts from corrupting the decomposition.
  128. reject = get_rejection_threshold(epochs_ica)
  129. print('Rejection threshold:', reject)
  130. # %%
  131. # ── Fit ICA ─────────────────────────────────────────────────────────────────
  132. random_state = 42 # Fix seed for reproducibility
  133. ica_n_components = 0.99 # Retain components explaining 99 % of variance
  134. ica = mne.preprocessing.ICA(n_components=ica_n_components, random_state=random_state)
  135. ica.fit(epochs_ica, reject=reject, tstep=tstep)
  136. # %%
  137. # ── Identify and exclude EOG artefact components ───────────────────────────
  138. # Uses frontal channels (Fp1, F8) as EOG proxies via z-score correlation.
  139. # Components with |z| > 1.96 (≈ 95 % CI) are flagged as eye-movement artefacts.
  140. ica_z_thresh = 1.96
  141. eog_indices, eog_scores = ica.find_bads_eog(
  142. raw_ica,
  143. ch_name=['Fp1', 'F8'],
  144. threshold=ica_z_thresh
  145. )
  146. ica.exclude = eog_indices
  147. print(f'Excluded ICA components (EOG): {eog_indices}')
  148. # Visualise the z-scores — flagged components shown in red
  149. ica.plot_scores(eog_scores)
  150. plt.show()
  151. # Inspect the spatial topographies of all ICA components
  152. ica.plot_components()
  153. plt.show()
  154. # %% [markdown]
  155. # ## 4. Epoching & Visualisation
  156. #
  157. # Create stimulus-locked epochs around each pain-level event, apply ICA artefact
  158. # correction, re-reference to mastoid electrodes, and visualise the resulting ERPs.
  159. # %%
  160. # ── Epoch the filtered (non-ICA) data around stimulus events ───────────────
  161. tmin = -0.200 # 200 ms pre-stimulus baseline
  162. tmax = 1.000 # 1 000 ms post-stimulus
  163. baseline = (None, 0) # Baseline-correct using the pre-stimulus window
  164. epochs = mne.Epochs(
  165. raw_filt,
  166. events, event_mapping,
  167. tmin, tmax,
  168. baseline=baseline,
  169. preload=True
  170. )
  171. print(epochs)
  172. # %%
  173. # Quick sanity check: visualise the grand-average ERP before artefact removal
  174. epochs.average().plot(spatial_colors=True, show=False)
  175. plt.title('Grand-average ERP — before ICA correction')
  176. plt.show()
  177. # Scalp topography at 100 ms intervals across the epoch window
  178. times = np.arange(0, tmax, 0.1)
  179. epochs.average().plot_topomap(times=times, average=0.050)
  180. plt.show()
  181. # %%
  182. # ── Apply ICA to the stimulus-locked epochs ─────────────────────────────────
  183. # Removes the previously identified EOG components from the data.
  184. epochs_postica = ica.apply(epochs.copy())
  185. # Visualise grand-average ERP after artefact correction
  186. epochs_postica.average().plot(spatial_colors=True, show=False)
  187. plt.title('Grand-average ERP — after ICA correction')
  188. plt.show()
  189. times = np.arange(0, tmax, 0.1)
  190. epochs_postica.average().plot_topomap(times=times, average=0.050)
  191. plt.show()
  192. # %%
  193. # ── Re-reference to linked mastoids ─────────────────────────────────────────
  194. # TP9 and TP10 correspond to the left and right mastoid electrodes.
  195. # Mastoid referencing is standard in ERP and pain research.
  196. epochs_mastoidref = epochs_postica.set_eeg_reference(ref_channels=['TP9', 'TP10'])
  197. times = np.arange(0, tmax, 0.1)
  198. epochs_mastoidref.average().plot_topomap(times=times, average=0.050)
  199. plt.title('Grand-average topography — mastoid reference')
  200. plt.show()
  201. # %%
  202. # ── Per-condition ERP plots ──────────────────────────────────────────────────
  203. # Create one Evoked object per pain-level condition for comparison
  204. conditions = [
  205. 'Comment/10', 'Comment/20', 'Comment/30',
  206. 'Comment/40', 'Comment/50', 'Comment/60',
  207. ]
  208. evokeds = {c: epochs_mastoidref[c].average() for c in conditions}
  209. # Assign condition name as comment so it appears in plot titles
  210. for condition, evoked in evokeds.items():
  211. evoked.comment = condition
  212. # Joint plot shows butterfly + topomaps at key latencies
  213. key_times = [0.150, 0.250, 0.400, 0.600, 0.800]
  214. for condition, evoked in evokeds.items():
  215. evoked.plot_joint(times=key_times, title=condition)
  216. plt.show()
  217. # %% [markdown]
  218. # ## 5. DWT Feature Extraction
  219. #
  220. # For each epoch and each EEG channel, apply a 5-level Daubechies-4 Discrete
  221. # Wavelet Transform and extract 8 statistical features from the resulting
  222. # approximation (cA) and detail (cD) coefficients:
  223. #
  224. # | Feature | Description |
  225. # |---|---|
  226. # | `cD_Energy` | Mean energy across detail sub-bands |
  227. # | `cA_Energy` | Energy of the approximation coefficients |
  228. # | `D_Entropy` | Mean wavelet entropy across detail sub-bands |
  229. # | `A_Entropy` | Wavelet entropy of the approximation coefficients |
  230. # | `D_mean` | Mean amplitude across detail sub-bands |
  231. # | `A_mean` | Mean amplitude of the approximation coefficients |
  232. # | `D_std` | Mean std deviation across detail sub-bands |
  233. # | `A_std` | Std deviation of the approximation coefficients |
  234. #
  235. # This yields **8 features × N channels** per epoch, saved to a per-participant CSV.
  236. # %%
  237. from pywt import wavedec
  238. # EEG channels used for feature extraction.
  239. # These 8 channels span frontal, central, parietal, and occipital regions.
  240. CHANNELS = ['Fz', 'C3', 'Cz', 'C4', 'Pz', 'PO7', 'Oz', 'PO8']
  241. def wavelet_avg_features(data, type_wav='db4'):
  242. """
  243. Compute 8 DWT-based features from a single EEG channel signal.
  244. Parameters
  245. ----------
  246. data : array-like, shape (n_samples,)
  247. Single-channel EEG time series.
  248. type_wav : str
  249. Wavelet family to use. Default is 'db4' (Daubechies-4).
  250. Returns
  251. -------
  252. list of float
  253. [cD_Energy, cA_Energy, D_Entropy, A_Entropy,
  254. D_mean, A_mean, D_std, A_std]
  255. """
  256. # Decompose signal into 5 detail sub-bands + 1 approximation sub-band
  257. coeffs = wavedec(data, type_wav, level=5)
  258. # coeffs[0] → approximation (cA5)
  259. # coeffs[1..5] → detail sub-bands (cD5 … cD1, fine → coarse)
  260. # Energy: sum of squared coefficients (Parseval's theorem analog)
  261. cD_Energy = np.mean([np.sum(np.square(coeffs[i])) for i in range(1, 6)])
  262. cA_Energy = np.sum(np.square(coeffs[0]))
  263. # Wavelet entropy: captures signal complexity / information content
  264. D_Entropy = np.mean([
  265. np.sum(np.square(coeffs[i]) * np.log(np.square(coeffs[i]) + 1e-10))
  266. for i in range(1, 6)
  267. ])
  268. A_Entropy = np.sum(np.square(coeffs[0]) * np.log(np.square(coeffs[0]) + 1e-10))
  269. # Statistical moments
  270. D_mean = np.mean([np.mean(coeffs[i]) for i in range(1, 6)])
  271. A_mean = np.mean(coeffs[0])
  272. D_std = np.mean([np.std(coeffs[i]) for i in range(1, 6)])
  273. A_std = np.std(coeffs[0])
  274. return [cD_Energy, cA_Energy, D_Entropy, A_Entropy, D_mean, A_mean, D_std, A_std]
  275. # %%
  276. # ── Step 1: Extract pain-level labels from annotations ──────────────────────
  277. # Each 'Comment/XX' annotation encodes a pain stimulus intensity (10–60).
  278. # We divide by 10 so labels become integers 1–6.
  279. print('Extracting labels from annotations...')
  280. labels = []
  281. for ann in raw.annotations:
  282. if 'Comment/' in ann['description']:
  283. try:
  284. raw_level = ann['description'].split('/')[-1]
  285. # Remove any non-digit characters (e.g. 'ß') before converting
  286. pain_level = int(raw_level.replace('ß', '')) // 10
  287. labels.append(pain_level)
  288. except ValueError:
  289. print(f'Warning: skipping invalid annotation — {ann["description"]}')
  290. print(f'Extracted {len(labels)} labels.')
  291. # %%
  292. # ── Step 2: Extract wavelet features for every epoch ───────────────────────
  293. features = []
  294. for epoch in epochs.iter_evoked():
  295. epoch_features = []
  296. for ch_idx in range(len(CHANNELS)):
  297. # wavelet_avg_features returns 8 values per channel
  298. ch_features = wavelet_avg_features(epoch.data[ch_idx, :])
  299. epoch_features.extend(ch_features)
  300. features.append(epoch_features)
  301. # ── Step 3: Align features and labels (lengths may differ slightly) ─────────
  302. min_length = min(len(features), len(labels))
  303. features = features[:min_length]
  304. labels = labels[:min_length]
  305. print(f'Aligned features and labels: {min_length} rows.')
  306. # %%
  307. # ── Step 4: Save features + labels to a per-participant CSV ─────────────────
  308. output_dir = '/content/drive/My Drive/discrete_wavelet/8channels/'
  309. # Build column names: one set of 8 feature names per channel
  310. columns = []
  311. feature_names = ['cD_Energy', 'cA_Energy', 'D_Entropy', 'A_Entropy',
  312. 'D_mean', 'A_mean', 'D_std', 'A_std']
  313. for ch in CHANNELS:
  314. columns.extend([f'{ch}_{feat}' for feat in feature_names])
  315. df_participant = pd.DataFrame(features, columns=columns)
  316. df_participant['Label'] = labels
  317. csv_path = os.path.join(output_dir, f'wavelet_{p_id}.csv')
  318. df_participant.to_csv(csv_path, index=False)
  319. print(f'Saved {df_participant.shape[0]} epochs × {df_participant.shape[1]} columns → {csv_path}')
  320. # %% [markdown]
  321. # ## 6. Merging Per-Participant CSV Files
  322. #
  323. # After running the extraction above for every participant, merge all individual
  324. # CSVs into a single dataset file ready for model training.
  325. # %%
  326. # ── Merge all per-participant CSVs ──────────────────────────────────────────
  327. folder_path = '/content/drive/My Drive/discrete_wavelet/8channels'
  328. output_file = '/content/drive/My Drive/discrete_wavelet/merged/merged_data_8channels.csv'
  329. csv_files = [f for f in os.listdir(folder_path) if f.endswith('.csv')]
  330. print(f'Found {len(csv_files)} participant file(s): {csv_files}')
  331. df_list = [pd.read_csv(os.path.join(folder_path, f)) for f in csv_files]
  332. merged_df = pd.concat(df_list, ignore_index=True)
  333. merged_df.to_csv(output_file, index=False)
  334. print(f'Merged dataset saved → {output_file}')
  335. print(f'Shape: {merged_df.shape}')
  336. # %% [markdown]
  337. # ## 7. Dataset Preparation
  338. #
  339. # Load the merged dataset, verify integrity, and apply optional moving-average
  340. # smoothing within each pain-level condition before model training.
  341. # %%
  342. # ── Load merged dataset ─────────────────────────────────────────────────────
  343. csv_path = '/content/drive/My Drive/discrete_wavelet/merged/merged_data_8channels.csv'
  344. df = pd.read_csv(csv_path)
  345. print(df.head())
  346. print(f'\nDataset shape: {df.shape}')
  347. print(f'Label distribution:\n{df["Label"].value_counts().sort_index()}')
  348. # Check for missing values — none expected, but good practice
  349. missing_X = df.drop(columns=['Label']).isna().sum().sum()
  350. missing_Y = df['Label'].isna().sum()
  351. print(f'\nMissing values — Features: {missing_X} | Labels: {missing_Y}')
  352. # %%
  353. # ── Feature correlation heatmap ─────────────────────────────────────────────
  354. # Helps identify highly correlated features that may be redundant.
  355. # Large figures are needed due to the number of feature columns.
  356. plt.figure(figsize=(50, 50))
  357. cor_matrix = df.corr()
  358. sns.heatmap(cor_matrix, annot=True, cmap='coolwarm', fmt='.2f')
  359. plt.title('Feature Correlation Heatmap')
  360. plt.tight_layout()
  361. plt.show()
  362. # %%
  363. # ── Optional: moving-average smoothing per pain-level class ─────────────────
  364. # Smoothing reduces sample-to-sample noise within each condition, which can
  365. # improve the signal-to-noise ratio seen by the classifier.
  366. def moving_average(x, w=10):
  367. """Apply a simple uniform moving average with window size w."""
  368. return np.convolve(x, np.ones(w), 'valid') / w
  369. df_true = df.copy()
  370. df_true['predefinedlabel'] = df_true['Label']
  371. feature_columns = CHANNELS # The 8 channels defined in Section 5
  372. feature_suffixes = ['cD_Energy', 'cA_Energy', 'D_Entropy', 'A_Entropy',
  373. 'D_mean', 'A_mean', 'D_std', 'A_std']
  374. df_ma_rows = []
  375. for k in df_true['predefinedlabel'].unique():
  376. df_k = df_true[df_true['predefinedlabel'] == k]
  377. features_smoothed = {}
  378. for col in feature_columns:
  379. for suffix in feature_suffixes:
  380. feat_col = f'{col}_{suffix}'
  381. features_smoothed[feat_col] = moving_average(df_k[feat_col].values)
  382. n = len(next(iter(features_smoothed.values())))
  383. label_arr = np.full(n, k)
  384. timepoint_arr = np.arange(n)
  385. row_df = pd.DataFrame(features_smoothed)
  386. row_df['timepoint'] = timepoint_arr
  387. row_df['Label'] = label_arr
  388. df_ma_rows.append(row_df)
  389. df_ma = pd.concat(df_ma_rows, ignore_index=True)
  390. print(f'Smoothed dataset shape: {df_ma.shape}')
  391. print(df_ma.head())
  392. # %% [markdown]
  393. # ## 8. XGBoost Model Training & Evaluation
  394. #
  395. # Train an XGBoost multi-class classifier on the smoothed feature dataset.
  396. # Two complementary evaluations are used:
  397. # - **Hold-out test set** (80/20 split): gives a single accuracy estimate.
  398. # - **Stratified 5-fold cross-validation**: gives a more robust, variance-aware estimate.
  399. # %%
  400. import xgboost as xgb
  401. from sklearn.model_selection import train_test_split, StratifiedKFold, cross_val_score
  402. from sklearn.metrics import (accuracy_score, f1_score,
  403. classification_report, confusion_matrix)
  404. from sklearn.preprocessing import StandardScaler
  405. from xgboost import plot_importance
  406. # %%
  407. # ── Prepare features and labels ─────────────────────────────────────────────
  408. X = df_ma.drop(columns=['Label'])
  409. Y = df_ma['Label']
  410. # Ensure all feature columns are numeric
  411. X = X.select_dtypes(include=['number']).astype('float32')
  412. print(f'Feature matrix: {X.shape}')
  413. print(f'Label distribution:\n{Y.value_counts().sort_index()}')
  414. # %%
  415. # ── Train / test split ──────────────────────────────────────────────────────
  416. # stratify=Y ensures each pain-level class is proportionally represented
  417. # in both the training and test partitions.
  418. X_train, X_test, y_train, y_test = train_test_split(
  419. X, Y, test_size=0.2, random_state=42, stratify=Y
  420. )
  421. # Feature scaling — XGBoost is tree-based and does not strictly require
  422. # scaling, but it can help convergence when combined with regularisation.
  423. scaler = StandardScaler()
  424. X_train = scaler.fit_transform(X_train)
  425. X_test = scaler.transform(X_test)
  426. print(f'Train size: {X_train.shape[0]} | Test size: {X_test.shape[0]}')
  427. # %%
  428. # ── Define and train the XGBoost classifier ─────────────────────────────────
  429. # Key hyperparameters:
  430. # objective : 'multi:softmax' for discrete class outputs
  431. # num_class : number of unique pain-level labels (set to max label + 1)
  432. # learning_rate : step size shrinkage to prevent overfitting
  433. # max_depth : maximum tree depth (controls model complexity)
  434. # n_estimators : number of boosting rounds
  435. model = xgb.XGBClassifier(
  436. objective='multi:softmax',
  437. num_class=int(Y.max()) + 1, # Automatically inferred from data
  438. learning_rate=0.1,
  439. max_depth=5,
  440. n_estimators=200,
  441. random_state=42,
  442. use_label_encoder=False,
  443. eval_metric='mlogloss',
  444. )
  445. model.fit(X_train, y_train)
  446. print('Training complete.')
  447. # %%
  448. # ── Evaluate on the hold-out test set ───────────────────────────────────────
  449. y_pred = model.predict(X_test)
  450. accuracy_test = accuracy_score(y_test, y_pred)
  451. f1_test = f1_score(y_test, y_pred, average='weighted')
  452. print(f'Test Accuracy : {accuracy_test * 100:.2f}%')
  453. print(f'Test F1 Score : {f1_test:.4f}')
  454. print()
  455. print('Classification Report:')
  456. print(classification_report(y_test, y_pred))
  457. # %%
  458. # ── Confusion matrix ────────────────────────────────────────────────────────
  459. conf_matrix = confusion_matrix(y_test, y_pred)
  460. class_labels = sorted(Y.unique())
  461. plt.figure(figsize=(8, 6))
  462. sns.heatmap(
  463. conf_matrix, annot=True, fmt='d', cmap='Blues',
  464. xticklabels=class_labels, yticklabels=class_labels
  465. )
  466. plt.xlabel('Predicted Label')
  467. plt.ylabel('True Label')
  468. plt.title('Confusion Matrix — Hold-out Test Set')
  469. plt.tight_layout()
  470. plt.show()
  471. # %%
  472. # ── Stratified 5-fold cross-validation ─────────────────────────────────────
  473. # Cross-validation gives a more reliable accuracy estimate by training and
  474. # evaluating on all folds of the data, reducing dependence on a single split.
  475. skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
  476. cv_results = cross_val_score(model, X, Y, cv=skf, scoring='accuracy')
  477. print(f'Cross-validation Accuracy: {cv_results.mean() * 100:.2f}% '
  478. f'(± {cv_results.std() * 100:.2f}%)')
  479. print(f'Per-fold scores: {[f"{s*100:.1f}%" for s in cv_results]}')
  480. # %%
  481. # ── Feature importance ──────────────────────────────────────────────────────
  482. # 'weight' counts the number of times a feature is used to split a node.
  483. # High-importance features are the most discriminative for pain classification.
  484. plt.figure(figsize=(12, 6))
  485. plot_importance(model, importance_type='weight', max_num_features=20)
  486. plt.title('Top-20 Feature Importances (split count)')
  487. plt.tight_layout()
  488. plt.show()
  489. # %% [markdown]
  490. # ## 9. Model Saving
  491. #
  492. # Export the trained XGBoost model to Google Drive so it can be loaded later
  493. # for inference or fine-tuning without retraining.
  494. # %%
  495. # ── Save model weights ──────────────────────────────────────────────────────
  496. # The .json format is portable across XGBoost versions and languages.
  497. # Use .txt if you need compatibility with older XGBoost (< 1.6) versions.
  498. model_path = '/content/drive/My Drive/xgboost_model_8channels.json'
  499. model.get_booster().save_model(model_path)
  500. print(f'Model saved → {model_path}')

EEG_XGBoost_Pain_Classification.ipynb at commit 7f55aff, no license · at the source

Overview

Authors: Sabrina Al Bukhari1, Ahmad Zahran1, Anzif Anvaj1, Muhammed Hamdan1, Ahmed Atif1, Jinane Mounsef1, Yacine Hadjiat2
  1. Department of Electrical and Computing Engineering, Rochester Institute of Technology, Dubai, United Arab Emirates
  2. College of Medicine, Mohammed Bin Rashid University of Medicine and Health Sciences, Dubai, United Arab Emirates
Journal: PloS one, volume 21, issue 8, article e0354510
Dates: received 10 January 2026; accepted 9 July 2026; published online 24 August 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1371/journal.pone.0354510 · PMID 42636251 · PMCID PMC13502692 · OpenAlex W7204078881
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: EEG (modality), human (organism), pain (population)
Methods: Spectral & time-frequency, Preprocessing, Smoothing, state filtering, decompositions, Machine learning, Statistics, Physiology & signal measures
MeSH: Electroencephalography*, Pain*, Pain Management*, Pain Measurement*, Virtual Reality*, Adult, Artificial Intelligence, Avatar, Boosting Machine Learning Algorithms, Female, Humans, Male, Young Adult (* major topic)
Topic: Pediatric Pain Management Techniques (Pediatrics, Perinatology and Child Health, Medicine), according to OpenAlex
Citations: not cited yet (Europe PMC); 31 references in the paper

Abstract

Background: Non-pharmacological pain management represents an urgent clinical need. Emerging technologies such as virtual reality (VR) and electroencephalography (EEG)-based artificial intelligence (AI) offer promising avenues for objective pain assessment and adaptive therapeutic intervention.

Purpose: This study aims to develop and validate a real-time, closed-loop EEG-driven VR therapy system that classifies pain levels from brain signals and delivers personalized, avatar-guided therapeutic responses.

Methods: An open-source EEG dataset (51 participants; perception condition; laser-induced pain stimuli rated 0–100) was preprocessed using bandpass filtering, Independent Component Analysis (ICA), and AutoReject. Wavelet-based features (Daubechies-4, 5 levels) were extracted from 1-second epochs and used to train two gradient-boosting classifiers: XGBoost and LightGBM. Predicted pain levels were transmitted via HTTP POST requests to Unreal Engine 5.3.2, where a MetaHuman avatar delivered adaptive therapeutic responses.

Results: LightGBM achieved 97.89% classification accuracy (cross-validation: 95.78% ± 0.82%) and XGBoost achieved 97.25% (cross-validation: 96.09% ± 0.70%) across 11 pain classes (0–10), outperforming all comparable studies in the literature. Real-time avatar responses were demonstrated across three pain categories: Slight (1–3), Moderate (4–6), and Severe (7–10).

Conclusion: The study successfully demonstrates the technical feasibility of a closed-loop EEG-VR pain management system using lightweight machine learning models. The system achieves state-of-the-art pain classification accuracy with fine-grained 11-class granularity.

Implications: This system offers a scalable, drug-free alternative for pain management applicable in clinical and rehabilitation settings. The modular design facilitates future extensions, including emotional state tracking, haptic feedback, and reinforcement learning-based personalization.

Reproduced under the paper's license (CC BY), from the paper cited above.

Repositories

Its files are read in the Code ↔ Paper reader above, with 12 matches between paragraphs and lines of code.

Zenodo 20846022

License: CC-BY-4.0
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Size: 1 file
Software Heritage: not checked
Found in: “Data Availability”
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: autoreject (2 files), Matplotlib (2 files), MNE-Python (2 files), NumPy (2 files), pandas (2 files), PyWavelets (2 files), scikit-learn (2 files), seaborn (2 files), LightGBM (1 file), XGBoost (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers (HTTP 200)
  • 27 September 2026: the link answers (HTTP 200)
3 files

sabrinamkb/eeg-vr-pain-adaptation

License: none: the authors keep all their rights
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: 7f55aff51f03e1ff56906ba3624f2ad6f868c1ae, 25 June 2026
Languages: Jupyter (2)
Size: 4 files, 2 scripts
Software Heritage: not archived
Found in: “Data Availability”
Holds: README, CITATION.cff, 2 notebooks
Not found: license file, environment file, tests, continuous integration, documentation
Tools: autoreject (2 files), Matplotlib (2 files), MNE-Python (2 files), NumPy (2 files), pandas (2 files), PyWavelets (2 files), scikit-learn (2 files), seaborn (2 files), LightGBM (1 file), XGBoost (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
3 files

The paper's code and data availability statement is in the Data section.

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 2 repositories of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 4 scripts, each with its path and the digest of its content;
  • 12 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

No dataset and no data link were found in the paper.

Data Availability

The author-generated code and software pipelines underlying the findings of this study have been made publicly available without restriction. The permanent, reproducible archive can be accessed via Zenodo under DOI: 10.5281/zenodo.20846022 (https://doi.org/10.5281/zenodo.20846022) and the live development repository is hosted on GitHub at https://github.com/sabrinamkb/eeg-vr-pain-adaptation.git.

Reproduced under the paper's license (CC BY), from the paper cited above.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 2, 28 September 2026

  • Funding: added Mohammed Bin Rashid University of Medicine and Health Sciences

Version 1, 27 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 7 authors, 13 MeSH terms, 25 references.

Cite

This paper

Al Bukhari, S., Zahran, A., Anvaj, A., Hamdan, M., Atif, A., Mounsef, J., & Hadjiat, Y. (2026). Personalized adaptive virtual reality experience driven by electroencephalography-based pain recognition. PloS one, 21(8), e0354510. https://doi.org/10.1371/journal.pone.0354510

BibTeX

@article{albukhari2026personalized,
author = {Al Bukhari, Sabrina and Zahran, Ahmad and Anvaj, Anzif and Hamdan, Muhammed and Atif, Ahmed and Mounsef, Jinane and Hadjiat, Yacine},
title = {{Personalized adaptive virtual reality experience driven by electroencephalography-based pain recognition}},
journal = {PloS one},
year = {2026},
month = aug,
volume = {21},
number = {8},
pages = {e0354510},
publisher = {PLOS},
issn = {1932-6203},
doi = {10.1371/journal.pone.0354510},
url = {https://doi.org/10.1371/journal.pone.0354510},
pmid = {42636251},
pmcid = {PMC13502692}
}

RIS

TY - JOUR
AU - Al Bukhari, Sabrina
AU - Zahran, Ahmad
AU - Anvaj, Anzif
AU - Hamdan, Muhammed
AU - Atif, Ahmed
AU - Mounsef, Jinane
AU - Hadjiat, Yacine
TI - Personalized adaptive virtual reality experience driven by electroencephalography-based pain recognition
T2 - PloS one
J2 - PLoS One
PY - 2026
DA - 2026/08/24
VL - 21
IS - 8
SP - e0354510
SN - 1932-6203
PB - PLOS
DO - 10.1371/journal.pone.0354510
UR - https://doi.org/10.1371/journal.pone.0354510
LA - en
ER -

CSL-JSON

{
"id": "10.1371/journal.pone.0354510",
"type": "article-journal",
"title": "Personalized adaptive virtual reality experience driven by electroencephalography-based pain recognition",
"container-title": "PloS one",
"author": [
{
"family": "Al Bukhari",
"given": "Sabrina"
},
{
"family": "Zahran",
"given": "Ahmad"
},
{
"family": "Anvaj",
"given": "Anzif"
},
{
"family": "Hamdan",
"given": "Muhammed"
},
{
"family": "Atif",
"given": "Ahmed"
},
{
"family": "Mounsef",
"given": "Jinane"
},
{
"family": "Hadjiat",
"given": "Yacine"
}
],
"container-title-short": "PLoS One",
"volume": "21",
"issue": "8",
"page": "e0354510",
"DOI": "10.1371/journal.pone.0354510",
"PMID": "42636251",
"PMCID": "PMC13502692",
"ISSN": "1932-6203",
"publisher": "PLOS",
"URL": "https://doi.org/10.1371/journal.pone.0354510",
"language": "en",
"issued": {
"date-parts": [
[
2026,
8,
24
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1371/journal.pcbi.1014302 [code]
Trial-level sequence modeling reveals hidden dynamics of dual-task interference.
Journal: PLoS computational biology
In common: autoreject, MNE-Python, seaborn, 4 other tools, EEG, 2 references
[2] doi:10.1162/imag.a.1321 [code]
Phase similarity between similar objects indicates representational merging across retrieval training but not sleep.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: autoreject, MNE-Python, seaborn, 4 other tools, EEG, 2 references
[3] doi:10.1162/imag.a.1269 [code]
From early to contemporary normative modeling: Mapping individual differences in neurophysiological signals.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: autoreject, MNE-Python, seaborn, 4 other tools, EEG, 2 references
[4] doi:10.3389/fpsyg.2026.1774068 [code]
Analysis of cognitive mechanisms in phoneme perception and pronunciation errors among Korean language learners.
Journal: Frontiers in psychology
In common: LightGBM, XGBoost, MNE-Python, 5 other tools, EEG
[5] doi:10.7554/elife.108023 [code]
Challenges in replay detection by TDLM in post-encoding resting state.
Journal: eLife
In common: autoreject, MNE-Python, seaborn, 4 other tools, 2 references
[6] doi:10.1002/hbm.70368 [code]
The Mismatch Negativity Compared: EEG, SQUID‐MEG, and Novel 4 Helium‐OPMs
Journal: n/a
In common: autoreject, MNE-Python, seaborn, 3 other tools, EEG, 2 references
[7] doi:10.1186/s11689-026-09706-z [code]
Neural oscillatory dynamics reveal altered top-down and integrative mechanisms during face processing in autistic children and unaffected siblings of autistic children.
Journal: Journal of neurodevelopmental disorders
In common: autoreject, MNE-Python, seaborn, 3 other tools, EEG, 2 references
[8] doi:10.1038/s41597-026-07350-9 [code]
An open multi-center MEG-EEG dataset for studying conscious visual perception.
Journal: Scientific data
In common: autoreject, MNE-Python, seaborn, 4 other tools, EEG, 1 reference
[9] doi:10.3389/fnhum.2026.1869918 [code]
Single-subject auditory ERP-BCI performance enhancement in ALS via an AI coding assistant prompt.
Journal: Frontiers in human neuroscience
In common: LightGBM, XGBoost, MNE-Python, 4 other tools, EEG
[10] doi:10.1162/imag.a.1040 [code]
Novel 4 He-OPMs support waveform-specific beta burst analysis comparable to SQUID-MEG
Journal: n/a
In common: autoreject, MNE-Python, scikit-learn, 3 other tools, 2 references

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.