From 4d5e8ae154f445bddf18a9b6e70138f901d97dfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benedikt=20Kl=C3=B6ckl?= Date: Thu, 12 Dec 2024 14:47:30 +0100 Subject: [PATCH 1/9] add new view dialog to display channel stats from raw.describe() --- src/mnelab/dialogs/channel_stats.py | 46 +++++++++++++++++++++++++++++ src/mnelab/mainwindow.py | 13 ++++++++ 2 files changed, 59 insertions(+) create mode 100644 src/mnelab/dialogs/channel_stats.py diff --git a/src/mnelab/dialogs/channel_stats.py b/src/mnelab/dialogs/channel_stats.py new file mode 100644 index 00000000..ecf1088d --- /dev/null +++ b/src/mnelab/dialogs/channel_stats.py @@ -0,0 +1,46 @@ +# © MNELAB developers +# +# License: BSD (3-clause) +from PySide6.QtWidgets import ( + QDialog, + QPushButton, + QTableWidget, + QTableWidgetItem, + QVBoxLayout, +) + + +class ChannelStats(QDialog): + def __init__(self, parent, data): + super().__init__(parent=parent) + + # window + self.setWindowTitle("Channel Stats") + self.resize(690, 800) + self.setMinimumSize(200, 150) + self.setMaximumWidth(690) + layout = QVBoxLayout(self) + + # table widget + self.table = QTableWidget() + self.table.setRowCount(len(data)) + self.table.setColumnCount(len(data.columns)) + self.table.setHorizontalHeaderLabels(data.columns) + + # populate table + for row in range(len(data)): + for col in range(len(data.columns)): + item = QTableWidgetItem(str(data.iloc[row, col])) + self.table.setItem(row, col, item) + + self.table.resizeColumnToContents(0) # name + self.table.resizeColumnToContents(1) # type + self.table.resizeColumnToContents(2) # unit + layout.addWidget(self.table) + + # add close button + close_button = QPushButton("Close") + close_button.clicked.connect(self.close) + layout.addWidget(close_button) + + self.setLayout(layout) diff --git a/src/mnelab/mainwindow.py b/src/mnelab/mainwindow.py index 34733549..3268fa5c 100644 --- a/src/mnelab/mainwindow.py +++ b/src/mnelab/mainwindow.py @@ -29,6 +29,7 @@ from pyxdf import resolve_streams from mnelab.dialogs import * # noqa: F403 +from mnelab.dialogs.channel_stats import ChannelStats from mnelab.io import writers from mnelab.io.mat import parse_mat from mnelab.io.npy import parse_npy @@ -315,6 +316,10 @@ def __init__(self, model: Model): self.show_history, QKeySequence(Qt.CTRL | Qt.Key_Y), ) + self.actions["channel_stats"] = view_menu.addAction( + "&Channel Stats", + self.show_channel_stats, + ) self.actions["toolbar"] = view_menu.addAction("&Toolbar", self._toggle_toolbar) self.actions["toolbar"].setCheckable(True) self.actions["statusbar"] = view_menu.addAction( @@ -542,6 +547,9 @@ def data_changed(self): self.actions["epoch_data"].setEnabled( enabled and events and self.model.current["dtype"] == "raw" ) + self.actions["channel_stats"].setEnabled( + enabled and self.model.current["dtype"] == "raw" + ) self.actions["drop_bad_epochs"].setEnabled( enabled and events and self.model.current["dtype"] == "epochs" ) @@ -1248,6 +1256,11 @@ def show_history(self): dialog = HistoryDialog(self, "\n".join(self.model.history)) dialog.exec() + def show_channel_stats(self): + """Show channel stats.""" + dialog = ChannelStats(self, self.model.current["data"].describe(True)) + dialog.exec_() + def show_about(self): """Show About dialog.""" from . import __version__ From afe172e972b00f70c19fadb0132634ccdab7da7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benedikt=20Kl=C3=B6ckl?= Date: Thu, 12 Dec 2024 14:51:05 +0100 Subject: [PATCH 2/9] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78f9668a..1a743e23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - XDF reader now parses measurement date ([#470](https://github.com/cbrnr/mnelab/pull/470) by [Stefan Appelhoff](https://stefanappelhoff.com)) - Add support for loading custom montage files ([#468](https://github.com/cbrnr/mnelab/pull/468) by [Benedikt Klöckl](https://github.com/bkloeckl) and [Clemens Brunner](https://github.com/cbrnr)) - Add new filter dialog option for notch filter and improve UI ([#469](https://github.com/cbrnr/mnelab/pull/469) by [Benedikt Klöckl](https://github.com/bkloeckl)) +- Add functionality to display channel stats ([#462](https://github.com/cbrnr/mnelab/pull/462) by [Benedikt Klöckl](https://github.com/bkloeckl)) ### 🌀 Changed - Change the append dialog appearance to include original indices used for identifying the data ([#449](https://github.com/cbrnr/mnelab/pull/449) by [Benedikt Klöckl](https://github.com/bkloeckl)) From 5902a5bf8c856c785e2ae3d8a93d09212d001a7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benedikt=20Kl=C3=B6ckl?= Date: Fri, 13 Dec 2024 10:47:04 +0100 Subject: [PATCH 3/9] Update dependencies to include pandas >= 2.2.3 and sort alphabetically --- pyproject.toml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 421e74a2..d9c75792 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,16 +22,18 @@ classifiers = [ ] keywords = ["EEG", "MEG", "MNE", "GUI", "electrophysiology"] dependencies = [ - "mne >= 1.7.0", - "PySide6 >= 6.7.1", "edfio >= 0.4.2", "matplotlib >= 3.8.0", + "mne >= 1.7.0", "numpy >= 2.0.0", - "scipy >= 1.14.1", + "pandas >= 2.2.3", "pybv >= 0.7.4", - "pyxdf >= 1.16.4", "pyobjc-framework-Cocoa >= 10.0; platform_system=='Darwin'", + "pyxdf >= 1.16.4", + "PySide6 >= 6.7.1", + "scipy >= 1.14.1", ] + dynamic = ["version"] [project.optional-dependencies] From a497c10132948a1b9d76303dcb5a0e799ee41ab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benedikt=20Kl=C3=B6ckl?= Date: Fri, 13 Dec 2024 10:51:45 +0100 Subject: [PATCH 4/9] Update dependencies to lower case letters --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d9c75792..a784704e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,9 +28,9 @@ dependencies = [ "numpy >= 2.0.0", "pandas >= 2.2.3", "pybv >= 0.7.4", - "pyobjc-framework-Cocoa >= 10.0; platform_system=='Darwin'", + "pyobjc-framework-cocoa >= 10.0; platform_system=='darwin'", "pyxdf >= 1.16.4", - "PySide6 >= 6.7.1", + "pyside6 >= 6.7.1", "scipy >= 1.14.1", ] From 76620216c5a7c5216addaee9c052de6f383f3654 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benedikt=20Kl=C3=B6ckl?= Date: Tue, 21 Jan 2025 14:51:15 +0100 Subject: [PATCH 5/9] better visalization of data with automatic unit scaling, allow sorting of table --- src/mnelab/dialogs/channel_stats.py | 113 +++++++++++++++++++++++----- src/mnelab/mainwindow.py | 2 +- 2 files changed, 96 insertions(+), 19 deletions(-) diff --git a/src/mnelab/dialogs/channel_stats.py b/src/mnelab/dialogs/channel_stats.py index ecf1088d..69a4b193 100644 --- a/src/mnelab/dialogs/channel_stats.py +++ b/src/mnelab/dialogs/channel_stats.py @@ -1,6 +1,13 @@ # © MNELAB developers # # License: BSD (3-clause) + +from collections import defaultdict + +import numpy as np +from mne import channel_type +from mne.defaults import _handle_default +from PySide6.QtCore import Qt from PySide6.QtWidgets import ( QDialog, QPushButton, @@ -10,33 +17,36 @@ ) +class SortableTableWidgetItem(QTableWidgetItem): + def __init__(self, value): + super().__init__(str(value)) + self.value = value + + def __lt__(self, other): + try: + return float(self.value) < float(other.value) + except ValueError: + return str(self.value) < str(other.value) + + class ChannelStats(QDialog): - def __init__(self, parent, data): + def __init__(self, parent, raw): super().__init__(parent=parent) # window self.setWindowTitle("Channel Stats") - self.resize(690, 800) - self.setMinimumSize(200, 150) - self.setMaximumWidth(690) + self.resize(630, 550) + self.setMinimumSize(400, 300) layout = QVBoxLayout(self) - # table widget - self.table = QTableWidget() - self.table.setRowCount(len(data)) - self.table.setColumnCount(len(data.columns)) - self.table.setHorizontalHeaderLabels(data.columns) + # create table + self.table = QTableWidget(self) + self.table.setSortingEnabled(True) + layout.addWidget(self.table) # populate table - for row in range(len(data)): - for col in range(len(data.columns)): - item = QTableWidgetItem(str(data.iloc[row, col])) - self.table.setItem(row, col, item) - - self.table.resizeColumnToContents(0) # name - self.table.resizeColumnToContents(1) # type - self.table.resizeColumnToContents(2) # unit - layout.addWidget(self.table) + self.populate_table(raw) + self.table.resizeColumnsToContents() # add close button close_button = QPushButton("Close") @@ -44,3 +54,70 @@ def __init__(self, parent, data): layout.addWidget(close_button) self.setLayout(layout) + + def populate_table(self, raw): + # extract channel stats (logic from raw.describe()) + nchan = raw.info["nchan"] + cols = defaultdict(list) + cols["name"] = raw.ch_names + for i in range(nchan): + ch = raw.info["chs"][i] + data = raw[i][0] + cols["type"].append(channel_type(raw.info, i)) + cols["unit"].append(ch.get("unit", "")) + cols["min"].append(np.min(data)) + cols["Q1"].append(np.percentile(data, 25)) + cols["median"].append(np.median(data)) + cols["Q3"].append(np.percentile(data, 75)) + cols["max"].append(np.max(data)) + + # unit scaling + scalings = _handle_default("scalings") + units = _handle_default("units") + for i in range(nchan): + unit = units.get(cols["type"][i]) + scaling = scalings.get(cols["type"][i], 1) + if scaling != 1: + cols["unit"][i] = unit + for col in ["min", "Q1", "median", "Q3", "max"]: + cols[col][i] *= scaling + + # set up tabel + headers = [ + "Channel", + "Name", + "Type", + "Unit", + "Min", + "Q1", + "Median", + "Q3", + "Max", + ] + self.table.setColumnCount(len(headers)) + self.table.setRowCount(nchan) + self.table.setHorizontalHeaderLabels(headers) + + # populate + for i in range(nchan): + item = SortableTableWidgetItem(i) + item.setFlags(item.flags() & ~Qt.ItemIsEditable) + self.table.setItem(i, 0, item) + + item = SortableTableWidgetItem(cols["name"][i]) + item.setFlags(item.flags() & ~Qt.ItemIsEditable) + self.table.setItem(i, 1, item) + + item = SortableTableWidgetItem(cols["type"][i].upper()) + item.setFlags(item.flags() & ~Qt.ItemIsEditable) + self.table.setItem(i, 2, item) + + item = SortableTableWidgetItem(cols["unit"][i]) + item.setFlags(item.flags() & ~Qt.ItemIsEditable) + self.table.setItem(i, 3, item) + + for j, key in enumerate(["min", "Q1", "median", "Q3", "max"], start=4): + formatted_value = f"{cols[key][i]:.2f}" + item = SortableTableWidgetItem(float(formatted_value)) + item.setFlags(item.flags() & ~Qt.ItemIsEditable) + self.table.setItem(i, j, item) diff --git a/src/mnelab/mainwindow.py b/src/mnelab/mainwindow.py index 3268fa5c..b4256e26 100644 --- a/src/mnelab/mainwindow.py +++ b/src/mnelab/mainwindow.py @@ -1258,7 +1258,7 @@ def show_history(self): def show_channel_stats(self): """Show channel stats.""" - dialog = ChannelStats(self, self.model.current["data"].describe(True)) + dialog = ChannelStats(self, self.model.current["data"]) dialog.exec_() def show_about(self): From 7d9cb8f9ca85444e57d8ae10921f650ad61aeb20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benedikt=20Kl=C3=B6ckl?= Date: Tue, 21 Jan 2025 18:50:59 +0100 Subject: [PATCH 6/9] removed pandas dependency, decouple calculations from dialog, right-align numeric columns, added column mean --- pyproject.toml | 1 - src/mnelab/dialogs/channel_stats.py | 78 ++++++------------- src/mnelab/utils/channelstats_calculations.py | 39 ++++++++++ 3 files changed, 64 insertions(+), 54 deletions(-) create mode 100644 src/mnelab/utils/channelstats_calculations.py diff --git a/pyproject.toml b/pyproject.toml index a784704e..4cdaaaac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,6 @@ dependencies = [ "matplotlib >= 3.8.0", "mne >= 1.7.0", "numpy >= 2.0.0", - "pandas >= 2.2.3", "pybv >= 0.7.4", "pyobjc-framework-cocoa >= 10.0; platform_system=='darwin'", "pyxdf >= 1.16.4", diff --git a/src/mnelab/dialogs/channel_stats.py b/src/mnelab/dialogs/channel_stats.py index 69a4b193..5ca85844 100644 --- a/src/mnelab/dialogs/channel_stats.py +++ b/src/mnelab/dialogs/channel_stats.py @@ -1,12 +1,6 @@ # © MNELAB developers # # License: BSD (3-clause) - -from collections import defaultdict - -import numpy as np -from mne import channel_type -from mne.defaults import _handle_default from PySide6.QtCore import Qt from PySide6.QtWidgets import ( QDialog, @@ -16,6 +10,8 @@ QVBoxLayout, ) +from mnelab.utils.channelstats_calculations import calculate_channel_stats + class SortableTableWidgetItem(QTableWidgetItem): def __init__(self, value): @@ -35,7 +31,7 @@ def __init__(self, parent, raw): # window self.setWindowTitle("Channel Stats") - self.resize(630, 550) + self.resize(650, 550) self.setMinimumSize(400, 300) layout = QVBoxLayout(self) @@ -56,68 +52,44 @@ def __init__(self, parent, raw): self.setLayout(layout) def populate_table(self, raw): - # extract channel stats (logic from raw.describe()) - nchan = raw.info["nchan"] - cols = defaultdict(list) - cols["name"] = raw.ch_names - for i in range(nchan): - ch = raw.info["chs"][i] - data = raw[i][0] - cols["type"].append(channel_type(raw.info, i)) - cols["unit"].append(ch.get("unit", "")) - cols["min"].append(np.min(data)) - cols["Q1"].append(np.percentile(data, 25)) - cols["median"].append(np.median(data)) - cols["Q3"].append(np.percentile(data, 75)) - cols["max"].append(np.max(data)) - - # unit scaling - scalings = _handle_default("scalings") - units = _handle_default("units") - for i in range(nchan): - unit = units.get(cols["type"][i]) - scaling = scalings.get(cols["type"][i], 1) - if scaling != 1: - cols["unit"][i] = unit - for col in ["min", "Q1", "median", "Q3", "max"]: - cols[col][i] *= scaling - - # set up tabel - headers = [ - "Channel", - "Name", - "Type", - "Unit", - "Min", - "Q1", - "Median", - "Q3", - "Max", - ] - self.table.setColumnCount(len(headers)) + cols, nchan = calculate_channel_stats(raw) + self.table.setColumnCount(9) self.table.setRowCount(nchan) - self.table.setHorizontalHeaderLabels(headers) + self.table.setHorizontalHeaderLabels( + [ + "Channel", + "Name", + "Type", + "Unit", + "Min", + "Q1", + "Mean", + "Median", + "Q3", + "Max", + ] + ) - # populate for i in range(nchan): - item = SortableTableWidgetItem(i) + item = QTableWidgetItem(str(i)) item.setFlags(item.flags() & ~Qt.ItemIsEditable) self.table.setItem(i, 0, item) - item = SortableTableWidgetItem(cols["name"][i]) + item = QTableWidgetItem(cols["name"][i]) item.setFlags(item.flags() & ~Qt.ItemIsEditable) self.table.setItem(i, 1, item) - item = SortableTableWidgetItem(cols["type"][i].upper()) + item = QTableWidgetItem(cols["type"][i].upper()) item.setFlags(item.flags() & ~Qt.ItemIsEditable) self.table.setItem(i, 2, item) - item = SortableTableWidgetItem(cols["unit"][i]) + item = QTableWidgetItem(cols["unit"][i]) item.setFlags(item.flags() & ~Qt.ItemIsEditable) self.table.setItem(i, 3, item) for j, key in enumerate(["min", "Q1", "median", "Q3", "max"], start=4): formatted_value = f"{cols[key][i]:.2f}" - item = SortableTableWidgetItem(float(formatted_value)) + item = QTableWidgetItem(formatted_value) item.setFlags(item.flags() & ~Qt.ItemIsEditable) + item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter) self.table.setItem(i, j, item) diff --git a/src/mnelab/utils/channelstats_calculations.py b/src/mnelab/utils/channelstats_calculations.py new file mode 100644 index 00000000..6c088681 --- /dev/null +++ b/src/mnelab/utils/channelstats_calculations.py @@ -0,0 +1,39 @@ +# © MNELAB developers +# +# License: BSD (3-clause) + +from collections import defaultdict + +import numpy as np +from mne import channel_type +from mne.defaults import _handle_default + + +def calculate_channel_stats(raw): + nchan = raw.info["nchan"] + cols = defaultdict(list) + cols["name"] = raw.ch_names + + for i in range(nchan): + ch = raw.info["chs"][i] + data = raw[i][0] + cols["type"].append(channel_type(raw.info, i)) + cols["unit"].append(ch.get("unit", "")) + cols["min"].append(np.min(data)) + cols["Q1"].append(np.percentile(data, 25)) + cols["mean"].append(np.mean(data)) + cols["median"].append(np.median(data)) + cols["Q3"].append(np.percentile(data, 75)) + cols["max"].append(np.max(data)) + + scalings = _handle_default("scalings") + units = _handle_default("units") + for i in range(nchan): + unit = units.get(cols["type"][i]) + scaling = scalings.get(cols["type"][i], 1) + if scaling != 1: + cols["unit"][i] = unit + for col in ["min", "Q1", "mean", "median", "Q3", "max"]: + cols[col][i] *= scaling + + return cols, nchan From 99af8350a125d0261421d4713919c2beda923c76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benedikt=20Kl=C3=B6ckl?= Date: Sun, 26 Jan 2025 19:06:46 +0100 Subject: [PATCH 7/9] moved calculations to utils.py, fixed platform_system typo, corrected import, added dynamic window width calculation to always hide horizontal scrolling, vectorize calculations --- pyproject.toml | 2 +- src/mnelab/dialogs/channel_stats.py | 10 ++++- src/mnelab/utils/__init__.py | 8 +++- src/mnelab/utils/channelstats_calculations.py | 39 ------------------- src/mnelab/utils/utils.py | 34 ++++++++++++++++ 5 files changed, 50 insertions(+), 43 deletions(-) delete mode 100644 src/mnelab/utils/channelstats_calculations.py diff --git a/pyproject.toml b/pyproject.toml index 4cdaaaac..a36c3865 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ dependencies = [ "mne >= 1.7.0", "numpy >= 2.0.0", "pybv >= 0.7.4", - "pyobjc-framework-cocoa >= 10.0; platform_system=='darwin'", + "pyobjc-framework-cocoa >= 10.0; platform_system=='Darwin'", "pyxdf >= 1.16.4", "pyside6 >= 6.7.1", "scipy >= 1.14.1", diff --git a/src/mnelab/dialogs/channel_stats.py b/src/mnelab/dialogs/channel_stats.py index 5ca85844..2cb3dc24 100644 --- a/src/mnelab/dialogs/channel_stats.py +++ b/src/mnelab/dialogs/channel_stats.py @@ -10,7 +10,7 @@ QVBoxLayout, ) -from mnelab.utils.channelstats_calculations import calculate_channel_stats +from mnelab.utils import calculate_channel_stats class SortableTableWidgetItem(QTableWidgetItem): @@ -31,7 +31,6 @@ def __init__(self, parent, raw): # window self.setWindowTitle("Channel Stats") - self.resize(650, 550) self.setMinimumSize(400, 300) layout = QVBoxLayout(self) @@ -49,6 +48,13 @@ def __init__(self, parent, raw): close_button.clicked.connect(self.close) layout.addWidget(close_button) + # calculate table width + table_width = self.table.verticalHeader().width() + table_width += sum( + self.table.columnWidth(i) for i in range(self.table.columnCount()) + ) + table_width += self.table.verticalScrollBar().width() + self.resize(table_width - 30, 550) self.setLayout(layout) def populate_table(self, raw): diff --git a/src/mnelab/utils/__init__.py b/src/mnelab/utils/__init__.py index e6d159db..d800079c 100644 --- a/src/mnelab/utils/__init__.py +++ b/src/mnelab/utils/__init__.py @@ -4,4 +4,10 @@ from mnelab.utils.dependencies import have from mnelab.utils.syntax import PythonHighlighter -from mnelab.utils.utils import Montage, count_locations, image_path, natural_sort +from mnelab.utils.utils import ( + Montage, + calculate_channel_stats, + count_locations, + image_path, + natural_sort, +) diff --git a/src/mnelab/utils/channelstats_calculations.py b/src/mnelab/utils/channelstats_calculations.py deleted file mode 100644 index 6c088681..00000000 --- a/src/mnelab/utils/channelstats_calculations.py +++ /dev/null @@ -1,39 +0,0 @@ -# © MNELAB developers -# -# License: BSD (3-clause) - -from collections import defaultdict - -import numpy as np -from mne import channel_type -from mne.defaults import _handle_default - - -def calculate_channel_stats(raw): - nchan = raw.info["nchan"] - cols = defaultdict(list) - cols["name"] = raw.ch_names - - for i in range(nchan): - ch = raw.info["chs"][i] - data = raw[i][0] - cols["type"].append(channel_type(raw.info, i)) - cols["unit"].append(ch.get("unit", "")) - cols["min"].append(np.min(data)) - cols["Q1"].append(np.percentile(data, 25)) - cols["mean"].append(np.mean(data)) - cols["median"].append(np.median(data)) - cols["Q3"].append(np.percentile(data, 75)) - cols["max"].append(np.max(data)) - - scalings = _handle_default("scalings") - units = _handle_default("units") - for i in range(nchan): - unit = units.get(cols["type"][i]) - scaling = scalings.get(cols["type"][i], 1) - if scaling != 1: - cols["unit"][i] = unit - for col in ["min", "Q1", "mean", "median", "Q3", "max"]: - cols[col][i] *= scaling - - return cols, nchan diff --git a/src/mnelab/utils/utils.py b/src/mnelab/utils/utils.py index 2910583d..e75d20d0 100644 --- a/src/mnelab/utils/utils.py +++ b/src/mnelab/utils/utils.py @@ -3,11 +3,14 @@ # License: BSD (3-clause) import re +from collections import defaultdict from dataclasses import dataclass from pathlib import Path import numpy as np +from mne import channel_type from mne.channels import DigMontage +from mne.defaults import _handle_default def count_locations(info): @@ -33,6 +36,37 @@ def key(s): return sorted(lst, key=key) +def calculate_channel_stats(raw): + # extract channel info + nchan = raw.info["nchan"] + data = raw.get_data() + cols = defaultdict(list) + cols["name"] = raw.ch_names + cols["type"] = [channel_type(raw.info, i) for i in range(nchan)] + + # vectorized calculations + cols["min"] = np.min(data, axis=1).tolist() + cols["Q1"] = np.percentile(data, 25, axis=1).tolist() + cols["mean"] = np.mean(data, axis=1).tolist() + cols["median"] = np.median(data, axis=1).tolist() + cols["Q3"] = np.percentile(data, 75, axis=1).tolist() + cols["max"] = np.max(data, axis=1).tolist() + + # scaling and units + scalings = _handle_default("scalings") + units = _handle_default("units") + cols["unit"] = [] + for i in range(nchan): + unit = units.get(cols["type"][i]) + scaling = scalings.get(cols["type"][i], 1) + cols["unit"].append(unit if scaling != 1 else "") + if scaling != 1: + for col in ["min", "Q1", "mean", "median", "Q3", "max"]: + cols[col][i] *= scaling + + return cols, nchan + + @dataclass class Montage: montage: DigMontage From 98cf4491613cc80b105c152d2532cb81c3eccdbc Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Mon, 27 Jan 2025 13:18:11 +0100 Subject: [PATCH 8/9] Remove .tolist() --- src/mnelab/utils/utils.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/mnelab/utils/utils.py b/src/mnelab/utils/utils.py index e75d20d0..cfd75dc2 100644 --- a/src/mnelab/utils/utils.py +++ b/src/mnelab/utils/utils.py @@ -45,12 +45,12 @@ def calculate_channel_stats(raw): cols["type"] = [channel_type(raw.info, i) for i in range(nchan)] # vectorized calculations - cols["min"] = np.min(data, axis=1).tolist() - cols["Q1"] = np.percentile(data, 25, axis=1).tolist() - cols["mean"] = np.mean(data, axis=1).tolist() - cols["median"] = np.median(data, axis=1).tolist() - cols["Q3"] = np.percentile(data, 75, axis=1).tolist() - cols["max"] = np.max(data, axis=1).tolist() + cols["min"] = np.min(data, axis=1) + cols["Q1"] = np.percentile(data, 25, axis=1) + cols["mean"] = np.mean(data, axis=1) + cols["median"] = np.median(data, axis=1) + cols["Q3"] = np.percentile(data, 75, axis=1) + cols["max"] = np.max(data, axis=1) # scaling and units scalings = _handle_default("scalings") From c7be862a673914fc04feb602b0cafd5348aaf846 Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Mon, 27 Jan 2025 13:44:46 +0100 Subject: [PATCH 9/9] Speed up by ~25% --- src/mnelab/dialogs/channel_stats.py | 1 + src/mnelab/utils/utils.py | 8 +++----- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/mnelab/dialogs/channel_stats.py b/src/mnelab/dialogs/channel_stats.py index 2cb3dc24..d8485cd0 100644 --- a/src/mnelab/dialogs/channel_stats.py +++ b/src/mnelab/dialogs/channel_stats.py @@ -1,6 +1,7 @@ # © MNELAB developers # # License: BSD (3-clause) + from PySide6.QtCore import Qt from PySide6.QtWidgets import ( QDialog, diff --git a/src/mnelab/utils/utils.py b/src/mnelab/utils/utils.py index cfd75dc2..bc07939b 100644 --- a/src/mnelab/utils/utils.py +++ b/src/mnelab/utils/utils.py @@ -45,12 +45,10 @@ def calculate_channel_stats(raw): cols["type"] = [channel_type(raw.info, i) for i in range(nchan)] # vectorized calculations - cols["min"] = np.min(data, axis=1) - cols["Q1"] = np.percentile(data, 25, axis=1) + cols["min"], cols["Q1"], cols["median"], cols["Q3"], cols["max"] = np.percentile( + data, [0, 25, 50, 75, 100], axis=1 + ) cols["mean"] = np.mean(data, axis=1) - cols["median"] = np.median(data, axis=1) - cols["Q3"] = np.percentile(data, 75, axis=1) - cols["max"] = np.max(data, axis=1) # scaling and units scalings = _handle_default("scalings")