{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":59093,"databundleVersionId":7469972,"sourceType":"competition"}],"dockerImageVersionId":30673,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Graphs from EEG data\nThis kernel is inspired by [this paper](https://arxiv.org/abs/2310.02152v2) on GNN-based EEG classification. I wanted to try GNN-based approaches in this competition but unfortunately I had no time for it. Please feel free to leave a comment or add any suggestions!","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19"}},{"cell_type":"code","source":"!pip install torch_geometric\n!pip install torcheeg\n\n!pip install eeg_positions","metadata":{"execution":{"iopub.status.busy":"2024-04-08T22:07:09.456709Z","iopub.execute_input":"2024-04-08T22:07:09.457845Z","iopub.status.idle":"2024-04-08T22:08:11.311338Z","shell.execute_reply.started":"2024-04-08T22:07:09.457802Z","shell.execute_reply":"2024-04-08T22:08:11.309732Z"},"_kg_hide-output":true,"collapsed":true,"jupyter":{"outputs_hidden":true},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nimport torch\n\nimport warnings\nwarnings.filterwarnings(\"ignore\", category=FutureWarning)\n\nimport networkx as nx","metadata":{"execution":{"iopub.status.busy":"2024-04-08T22:15:52.480304Z","iopub.execute_input":"2024-04-08T22:15:52.480758Z","iopub.status.idle":"2024-04-08T22:15:52.487182Z","shell.execute_reply.started":"2024-04-08T22:15:52.480725Z","shell.execute_reply":"2024-04-08T22:15:52.485664Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"EEG_PATH_TRAIN = '/kaggle/input/hms-harmful-brain-activity-classification/train_eegs/'\nEEG_PATH_TEST = '/kaggle/input/hms-harmful-brain-activity-classification/test_eegs'\n\ndf_train = pd.read_csv('/kaggle/input/hms-harmful-brain-activity-classification/train.csv')\ndf_test = pd.read_csv('/kaggle/input/hms-harmful-brain-activity-classification/test.csv')\ndf_train.head(3)","metadata":{"execution":{"iopub.status.busy":"2024-04-08T22:08:13.755389Z","iopub.execute_input":"2024-04-08T22:08:13.756147Z","iopub.status.idle":"2024-04-08T22:08:14.098066Z","shell.execute_reply.started":"2024-04-08T22:08:13.756104Z","shell.execute_reply":"2024-04-08T22:08:14.096573Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Network neuroscience offers an approach to EEG modelling by framing the signals as a graph. This is due to the fact that the brain exhibits a complex network structure, with neurons forming connections and communicating with each other.\n\nAnalysing EEG data as a graph enables he study of network properties, including functional connectivity, providing insights into brain function and dysfunction. Graph-based analysis offers insights into brain organisation and dynamics.\n\nThere are two types of graphs that can be built from the EEG data:\n1. **Structural connectivity (SC)** graphs \n\nSC graphs are pre-defined. SC in the classical sense of physical connections between brain regions is not possible to obtain using EEG signals since these are recorded at the scalp surface. Instead, we use the term to describe methods that construct brain graphs based on the physical distance between EEG electrodes.\n\nSC graph is pre-defined such that electrodes are connected by an edge in the following way:\n\n$$ e_{ij} = \\begin{cases}1\\text{ or }1/d_{ij},&\\text{ if } d_{ij} \\leq t\\\\\n\t  0,&\\text{ otherwise}\n\t  \\end{cases} $$\n      \nwhere $e_{ij}$ is the edge weight connecting nodes $i$ and $j$, and $d_{ij}$ is the measure of distance between EEG electrodes, and $t$ is a manually defined threshold controlling the graph sparsity.\n\n2. **Functional connectivity (FC)** graphs\n\nFC graphs can be both pre-defined and learnable. FC refers to pairwise statistical relationships between EEG signals. It can be obtained from either classical FC measures or learnable methods. Unlike SC, the FC graph is unique for each data sample and can contain both short and long-range edges. But since it is derived directly from EEG signals, it might be sensitive to noise. \n\t  Learnable FC based on node feature distance of feature concatenation are generally computed as \n      \n$$\\begin{align}e_{ij} &= \\theta_1(|h_i - h_j|), \\text{ and }\\\\ e_{ij} &= \\theta_2(h_i || h_j) \\end{align}$$\n      \nwhere $\\theta_1:\\mathbb{R}^d \\to \\mathbb{R}$ and $\\theta_2:\\mathbb{R}^{2\\times d}\\to\\mathbb{R}$ are neural networks and $h_i$ is the node feature/embedding of node $i$.\n\nLet's create both SC and FC graphs.","metadata":{}},{"cell_type":"markdown","source":"## SC graph\nThe EEG data were obtained using the well-known [10-20 system](https://www.kaggle.com/code/seshurajup/eegs-10-20-system). This information allows us to model and construct brain grain on the physical distance between EEG electrodes.","metadata":{}},{"cell_type":"code","source":"from eeg_positions import get_elec_coords, plot_coords","metadata":{"_kg_hide-output":true,"execution":{"iopub.status.busy":"2024-04-08T22:08:14.099963Z","iopub.execute_input":"2024-04-08T22:08:14.100427Z","iopub.status.idle":"2024-04-08T22:08:14.575130Z","shell.execute_reply.started":"2024-04-08T22:08:14.100367Z","shell.execute_reply":"2024-04-08T22:08:14.573392Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Let's take some EEG sample:","metadata":{}},{"cell_type":"code","source":"def get_one_row_data(row_id):\n    row = df_train.iloc[row_id]\n    path = '/kaggle/input/hms-harmful-brain-activity-classification/train_eegs/'\n    \n    #return eeg_from_parquet(f'{path}{row.eeg_id}.parquet')\n    eeg = pd.read_parquet(f'{path}{row.eeg_id}.parquet')\n    eeg_offset = int(row.eeg_label_offset_seconds)\n    eeg = eeg.iloc[eeg_offset*200:(eeg_offset+50)*200]\n    \n    ekg = eeg['EKG']#torch.Tensor(eeg['EKG'].to_numpy())\n    eeg = eeg.drop(columns='EKG')#torch.Tensor(eeg.drop(columns='EKG').to_numpy())\n    \n    return (eeg, ekg)","metadata":{"execution":{"iopub.status.busy":"2024-04-08T22:16:44.328863Z","iopub.execute_input":"2024-04-08T22:16:44.330083Z","iopub.status.idle":"2024-04-08T22:16:44.337696Z","shell.execute_reply.started":"2024-04-08T22:16:44.330021Z","shell.execute_reply":"2024-04-08T22:16:44.336304Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"eegs = get_one_row_data(183)[0]\n#pd.read_parquet('/kaggle/input/hms-harmful-brain-activity-classification/train_eegs/1000913311.parquet')\neegs","metadata":{"execution":{"iopub.status.busy":"2024-04-08T22:16:44.544607Z","iopub.execute_input":"2024-04-08T22:16:44.545062Z","iopub.status.idle":"2024-04-08T22:16:44.596661Z","shell.execute_reply.started":"2024-04-08T22:16:44.545029Z","shell.execute_reply":"2024-04-08T22:16:44.595248Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"color_hash = {'Cz': 'khaki',\n 'Fpz': 'lightgreen',\n 'Fz': 'lightgreen',\n 'Pz': 'lightgreen',\n 'Oz': 'lightgreen',\n 'T7': 'skyblue',\n 'C3': 'skyblue',\n 'C4': 'skyblue',\n 'T8': 'skyblue',\n 'Fp2': 'salmon',\n 'F8': 'salmon',\n 'P8': 'violet',\n 'O2': 'violet',\n 'Fp1': 'salmon',\n 'F7': 'salmon',\n 'P7': 'violet',\n 'O1': 'violet',\n 'F3': 'salmon',\n 'F4': 'salmon',\n 'P3': 'violet',\n 'P4': 'violet'}\n\ncoords = get_elec_coords(\n    system=\"1020\",\n    dim=\"2d\",\n)\nchans = list(coords['label'])\n\ndef egg_index_colors(parse_chans):\n    pos, colrs = [],[]\n    for i, x in enumerate(chans):\n        if x not in parse_chans + ['T7','T8','P7','P8']:\n            pos.append(i)\n            colrs.append('white')\n        else:\n            pos.append(i)\n            colrs.append(color_hash[x])\n    return pos, colrs\n\nparse_chans = list(set(eegs.columns) - set([ 'EKG']))\negg_chans, egg_colors = egg_index_colors(parse_chans)\n#little renamings\nmcn_system = {'T3': 'T7', 'T4': 'T8', 'T5': 'P7', 'T6': 'P8'}\nparse_chans = [name if name not in mcn_system else mcn_system[name] for name in parse_chans]\n\ncolors = (\n    egg_colors\n)\n\nfig, ax = plot_coords(\n    coords,\n    scatter_kwargs={\n        \"s\": 300, \n        \"color\": colors,\n        \"edgecolors\": \"black\", \n        \"linewidths\": 0.5, \n    },\n    text_kwargs={\n        \"ha\": \"center\",  \n        \"va\": \"center\",  \n        \"fontsize\": 8, \n    },\n)","metadata":{"execution":{"iopub.status.busy":"2024-04-08T22:16:47.012548Z","iopub.execute_input":"2024-04-08T22:16:47.013028Z","iopub.status.idle":"2024-04-08T22:16:47.522871Z","shell.execute_reply.started":"2024-04-08T22:16:47.012994Z","shell.execute_reply":"2024-04-08T22:16:47.521168Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Let's create a brain graph structure using the following rule:\n\n$$ e_{ij} = \\begin{cases} 1, &\\text{if } d_{ij} \\leq t,\\\\ 0,&\\text{otherwise.}\\end{cases}$$\n\nwith $t=0.4$","metadata":{"execution":{"iopub.status.busy":"2024-04-08T12:27:32.454632Z","iopub.execute_input":"2024-04-08T12:27:32.455766Z","iopub.status.idle":"2024-04-08T12:27:32.462337Z","shell.execute_reply.started":"2024-04-08T12:27:32.455734Z","shell.execute_reply":"2024-04-08T12:27:32.461035Z"}}},{"cell_type":"code","source":"SC_THRESHOLD = 0.4","metadata":{"execution":{"iopub.status.busy":"2024-04-08T22:16:47.566017Z","iopub.execute_input":"2024-04-08T22:16:47.567064Z","iopub.status.idle":"2024-04-08T22:16:47.577619Z","shell.execute_reply.started":"2024-04-08T22:16:47.567002Z","shell.execute_reply":"2024-04-08T22:16:47.574471Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"eegs_coords = {key:coords.loc[coords.label == key, ['x','y']].to_numpy()[0] for key in parse_chans}\neegs_names = list(eegs_coords.keys())\neegs_coords","metadata":{"execution":{"iopub.status.busy":"2024-04-08T22:16:47.958091Z","iopub.execute_input":"2024-04-08T22:16:47.958833Z","iopub.status.idle":"2024-04-08T22:16:47.991539Z","shell.execute_reply.started":"2024-04-08T22:16:47.958798Z","shell.execute_reply":"2024-04-08T22:16:47.990593Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def euclidean_distance(p1, p2):\n    return np.sqrt((p1[0] - p2[0])**2 + (p1[1] - p2[1])**2)\n\ndef create_distance_matrix(points_dict):\n    point_names = list(points_dict.keys())\n    num_points = len(point_names)\n    distance_matrix = np.zeros((num_points, num_points))\n    \n    for i in range(num_points):\n        for j in range(num_points):\n            distance_matrix[i, j] = euclidean_distance(points_dict[point_names[i]], points_dict[point_names[j]])\n    \n    return distance_matrix\n    \ndistance_matrix = create_distance_matrix(eegs_coords)\ndistance_matrix","metadata":{"execution":{"iopub.status.busy":"2024-04-08T22:16:49.059626Z","iopub.execute_input":"2024-04-08T22:16:49.061154Z","iopub.status.idle":"2024-04-08T22:16:49.079842Z","shell.execute_reply.started":"2024-04-08T22:16:49.061105Z","shell.execute_reply":"2024-04-08T22:16:49.078367Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def create_adjacency_matrix(distance_matrix, threshold, with_loops=False):\n    adjacency_matrix = np.zeros_like(distance_matrix, dtype=int)\n    adjacency_matrix[distance_matrix <= threshold] = 1\n    if not with_loops:\n        np.fill_diagonal(adjacency_matrix, 0)\n        \n    return adjacency_matrix\n\nadj_matrix = create_adjacency_matrix(distance_matrix, threshold=SC_THRESHOLD)\nadj_matrix","metadata":{"execution":{"iopub.status.busy":"2024-04-08T22:16:49.627962Z","iopub.execute_input":"2024-04-08T22:16:49.628400Z","iopub.status.idle":"2024-04-08T22:16:49.640122Z","shell.execute_reply.started":"2024-04-08T22:16:49.628369Z","shell.execute_reply":"2024-04-08T22:16:49.639001Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"graph = nx.from_numpy_array(adj_matrix)\nnx.draw(graph)","metadata":{"execution":{"iopub.status.busy":"2024-04-08T22:16:50.153588Z","iopub.execute_input":"2024-04-08T22:16:50.154257Z","iopub.status.idle":"2024-04-08T22:16:50.401105Z","shell.execute_reply.started":"2024-04-08T22:16:50.154223Z","shell.execute_reply":"2024-04-08T22:16:50.400148Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## FC graph\nFor FC graphs, let's use the `TorchEEG` package. We will use `absolute_pearson_correlation_coefficient` method built-in in the package in order to create a FC graph.","metadata":{}},{"cell_type":"code","source":"from torcheeg.transforms.pyg import ToDynamicG\nfrom torcheeg.transforms import Compose, BandDifferentialEntropy\n\nfrom torch_geometric.utils import to_torch_sparse_tensor","metadata":{"_kg_hide-output":true,"execution":{"iopub.status.busy":"2024-04-08T22:16:52.617811Z","iopub.execute_input":"2024-04-08T22:16:52.618275Z","iopub.status.idle":"2024-04-08T22:16:52.625152Z","shell.execute_reply.started":"2024-04-08T22:16:52.618243Z","shell.execute_reply":"2024-04-08T22:16:52.623371Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def to_dense(edge_index, size=(19,19)):\n    dense = torch.sparse.FloatTensor(edge_index, torch.ones(edge_index.shape[1]), \n                                    torch.Size(size)).to_dense()\n    return dense - np.eye(size[0])","metadata":{"execution":{"iopub.status.busy":"2024-04-08T22:25:58.718493Z","iopub.execute_input":"2024-04-08T22:25:58.719889Z","iopub.status.idle":"2024-04-08T22:25:58.727610Z","shell.execute_reply.started":"2024-04-08T22:25:58.719842Z","shell.execute_reply":"2024-04-08T22:25:58.725719Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"The authors mention the **DifferentialEntropy** features as one of the most popular features (besides raw data) when one trains GNNs over EEG data. The FC graph can also be created via this feature.","metadata":{}},{"cell_type":"code","source":"def get_transforms(add_differential_entropy = False):\n    if add_differential_entropy:\n        return Compose([\n        BandDifferentialEntropy(),\n        ToDynamicG(edge_func='absolute_pearson_correlation_coefficient', \n                   threshold=0.7, binary=True)\n    ])\n\n    return Compose([\n        ToDynamicG(edge_func='absolute_pearson_correlation_coefficient', \n                   threshold=0.7, binary=True)\n    ])\n\ntransforms = get_transforms()","metadata":{"execution":{"iopub.status.busy":"2024-04-08T22:31:55.765338Z","iopub.execute_input":"2024-04-08T22:31:55.765890Z","iopub.status.idle":"2024-04-08T22:31:55.773639Z","shell.execute_reply.started":"2024-04-08T22:31:55.765852Z","shell.execute_reply":"2024-04-08T22:31:55.772025Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"adj_matrix = to_dense(transforms(eeg=eegs.to_numpy().T)['eeg'].edge_index).numpy()\nadj_matrix","metadata":{"execution":{"iopub.status.busy":"2024-04-08T22:31:59.175170Z","iopub.execute_input":"2024-04-08T22:31:59.175668Z","iopub.status.idle":"2024-04-08T22:31:59.282528Z","shell.execute_reply.started":"2024-04-08T22:31:59.175631Z","shell.execute_reply":"2024-04-08T22:31:59.281283Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"graph = nx.from_numpy_array(adj_matrix)\nnx.draw(graph)","metadata":{"execution":{"iopub.status.busy":"2024-04-08T22:31:59.408829Z","iopub.execute_input":"2024-04-08T22:31:59.409291Z","iopub.status.idle":"2024-04-08T22:31:59.696001Z","shell.execute_reply.started":"2024-04-08T22:31:59.409257Z","shell.execute_reply":"2024-04-08T22:31:59.694791Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Differential entropy produces the following FC graph with much more edges:","metadata":{}},{"cell_type":"code","source":"transforms = get_transforms(True)\nadj_matrix = to_dense(transforms(eeg=eegs.to_numpy().T)['eeg'].edge_index).numpy()\ngraph = nx.from_numpy_array(adj_matrix)\nnx.draw(graph)","metadata":{"execution":{"iopub.status.busy":"2024-04-08T22:32:37.307902Z","iopub.execute_input":"2024-04-08T22:32:37.308305Z","iopub.status.idle":"2024-04-08T22:32:37.670284Z","shell.execute_reply.started":"2024-04-08T22:32:37.308275Z","shell.execute_reply":"2024-04-08T22:32:37.668987Z"},"trusted":true},"execution_count":null,"outputs":[]}]}