Skip to content

Commit 8c9d449

Browse files
authored
[DOC] Improve example quality and API doc clarity (#124)
## Summary This PR improves the documentation quality of the package in two ways: - rewrites and extends several example scripts to make them more didactic - tightens public API docstrings so parameters/returns are clearer and more consistent with the code ## What changed - improved the teaching narrative of the example gallery, with clearer goals, interpretation hints, and simple quantitative checks - added a new ASR example focused on calibration sensitivity - regenerated the corresponding example notebooks - cleaned up docstrings across core public utilities and methods
1 parent 2c09b8f commit 8c9d449

42 files changed

Lines changed: 2143 additions & 4195 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

doc/conf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@
8888
"logo": {
8989
"image_light": "_static/logo.png",
9090
"image_dark": "_static/logo-dark.png",
91-
"text": "meegkit",
91+
"text": f"meegkit v{version}",
9292
},
9393
"show_toc_level": 1,
9494
"external_links": [
@@ -126,7 +126,7 @@
126126
"examples_dirs": "../examples", # path to your example scripts
127127
"gallery_dirs": "auto_examples", # path to where to save gallery generated output
128128
"filename_pattern": "/example_",
129-
"ignore_pattern": "config.py",
129+
"ignore_pattern": r"(config|run_all_notebooks)\.py",
130130
"run_stale_examples": False,
131131
}
132132

doc/index.rst

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,31 @@ in mind that this is mostly development code, and as such is likely to change
1515
without any notice. Also, while most of the methods have been fairly robustly
1616
tested, bugs can (and should!) be expected.
1717

18+
The package is most useful for readers who want practical reference
19+
implementations of denoising and component-analysis methods, together with
20+
worked examples that show how to interpret the outputs.
21+
1822
The source code of the project is hosted on Github at the following address:
1923
https://github.com/nbara/python-meegkit
2024

21-
To get started, follow the installation instructions `in the README <https://github.com/nbara/python-meegkit#installation>`_.
25+
Quick start
26+
-----------
27+
28+
Install the package with ``pip``:
29+
30+
.. code-block:: bash
31+
32+
pip install meegkit
33+
34+
Some ASR-related functionality requires optional dependencies. To install those
35+
as well, use:
36+
37+
.. code-block:: bash
38+
39+
pip install 'meegkit[extra]'
40+
41+
For development, documentation building, or testing, see the fuller
42+
installation guidance `in the README <https://github.com/nbara/python-meegkit#installation>`_.
2243

2344
Available modules
2445
-----------------
@@ -49,7 +70,16 @@ Here is a list of the methods and techniques available in ``meegkit``:
4970
Examples gallery
5071
----------------
5172

52-
A number of example scripts and notebooks are available:
73+
A number of example scripts and notebooks are available.
74+
75+
If you are new to the package, a good starting sequence is:
76+
77+
1. ``example_asr`` for a full artifact-removal workflow.
78+
2. ``example_dss`` for a simple synthetic component-recovery example.
79+
3. ``example_trca`` or ``example_ress`` for task-oriented spatial filtering.
80+
81+
Many examples are synthetic sanity checks with known ground truth, which makes
82+
them useful for understanding what each method is expected to recover.
5383

5484

5585
.. toctree::

examples/README.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,22 @@ Examples gallery
44
Below is a list of example scripts showing basic usage for most methods in
55
MEEGkit.
66

7+
The examples are intentionally mixed between two roles:
8+
9+
1. synthetic sanity checks, where the ground truth is known and the method can
10+
be judged directly,
11+
2. workflow-style demonstrations, where the goal is to show how to interpret
12+
outputs on more realistic data.
13+
14+
Suggested starting points:
15+
16+
1. ``example_asr`` for artifact-subspace reconstruction.
17+
2. ``example_dss`` for a compact repeated-signal example.
18+
3. ``example_dss_line`` for line-noise removal.
19+
4. ``example_trca`` for block-wise classification performance.
20+
21+
Examples such as ``example_trca`` and the notebook-executed gallery pages may
22+
take longer to run than the smaller synthetic demos.
23+
724
The examples can be browsed directly on `Github <https://github.com/nbara/python-meegkit/tree/master/examples>`_.
825

examples/example_asr.ipynb

Lines changed: 68 additions & 244 deletions
Large diffs are not rendered by default.

examples/example_asr.py

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,26 @@
22
ASR example
33
===========
44
5-
Denoise data using Artifact Subspace Reconstruction.
5+
This example demonstrates a full ASR workflow on short EEG data:
6+
7+
1. Calibrate ASR on a mostly clean segment.
8+
2. Apply ASR in 1-second windows.
9+
3. Compare raw and cleaned traces and quantify amplitude reduction.
10+
11+
This is intended as a first-pass inspection example rather than a benchmark:
12+
it shows how the calibration choice propagates to the cleaned output.
13+
14+
The most useful outputs are the calibration retention mask and the channel-wise
15+
RMS attenuation summary.
616
717
Uses meegkit.ASR().
18+
19+
References
20+
----------
21+
.. [1] Mullen, T., Kothe, C., Chi, Y., Ojeda, A., Kerth, T., Makeig, S.,
22+
Jung, T. P., & Cauwenberghs, G. (2015). Real-time neuroimaging and
23+
cognitive monitoring using wearable dry EEG. IEEE Transactions on
24+
Biomedical Engineering, 62(11), 2553-2567.
825
"""
926
import os
1027

@@ -21,11 +38,15 @@
2138
###############################################################################
2239
# Calibration and processing
2340
# -----------------------------------------------------------------------------
41+
# We use the first 30 seconds as a calibration segment. In practice, this
42+
# segment should be as artifact-free as possible because ASR thresholds are
43+
# derived from it.
2444

2545
# Train on a clean portion of data
2646
asr = ASR(method="euclid")
2747
train_idx = np.arange(0 * sfreq, 30 * sfreq, dtype=int)
2848
_, sample_mask = asr.fit(raw[:, train_idx])
49+
selected_fraction = np.mean(sample_mask)
2950

3051
# Apply filter using sliding (non-overlapping) windows
3152
X = sliding_window(raw, window=int(sfreq), step=int(sfreq))
@@ -36,16 +57,26 @@
3657
raw = X.reshape(8, -1) # reshape to (n_chans, n_times)
3758
clean = Y.reshape(8, -1)
3859

60+
# A simple quality metric: root-mean-square attenuation per channel.
61+
rms_before = np.sqrt(np.mean(raw ** 2, axis=1))
62+
rms_after = np.sqrt(np.mean(clean ** 2, axis=1))
63+
rms_ratio = rms_after / np.maximum(rms_before, np.finfo(float).eps)
64+
3965
###############################################################################
4066
# Plot the results
4167
# -----------------------------------------------------------------------------
4268
#
43-
# Data was trained on a 40s window from 5s to 45s onwards (gray filled area).
44-
# The algorithm then removes portions of this data with high amplitude
45-
# artifacts before running the calibration (hatched area = good).
69+
# The gray overlay marks the 30-second calibration region actually used by the
70+
# code. The hatched overlay shows the subset of that region that ASR kept while
71+
# estimating its clean-data statistics.
72+
#
73+
# What to look for:
74+
# - After ASR, sharp bursts should be attenuated in many channels.
75+
# - The RMS ratio (after/before) should generally be below 1.
76+
# - Strong attenuation everywhere would suggest over-aggressive calibration.
4677

4778
times = np.arange(raw.shape[-1]) / sfreq
48-
f, ax = plt.subplots(8, sharex=True, figsize=(8, 5))
79+
f, ax = plt.subplots(8, sharex=True, figsize=(9, 6))
4980
for i in range(8):
5081
ax[i].fill_between(train_idx / sfreq, 0, 1, color="grey", alpha=.3,
5182
transform=ax[i].get_xaxis_transform(),
@@ -59,8 +90,24 @@
5990
ax[i].set_ylim([-50, 50])
6091
ax[i].set_ylabel(f"ch{i}")
6192
ax[i].set_yticks([])
93+
ax[0].set_title("Raw and cleaned EEG traces")
6294
ax[i].set_xlabel("Time (s)")
6395
ax[0].legend(fontsize="small", bbox_to_anchor=(1.04, 1), borderaxespad=0)
6496
plt.subplots_adjust(hspace=0, right=0.75)
6597
plt.suptitle("Before/after ASR")
98+
99+
fig, axm = plt.subplots(1, 1, figsize=(7, 3))
100+
axm.bar(np.arange(raw.shape[0]), rms_ratio)
101+
axm.axhline(1.0, color="k", ls=":", lw=1)
102+
axm.set_xlabel("Channel")
103+
axm.set_ylabel("RMS ratio (after / before)")
104+
axm.set_title("Channel-wise attenuation summary")
105+
axm.set_xticks(np.arange(raw.shape[0]))
106+
axm.grid(True, axis="y", ls=":", alpha=.4)
107+
plt.tight_layout()
108+
109+
print(f"Median RMS ratio across channels: {np.median(rms_ratio):.3f}")
110+
print(f"Fraction of calibration samples retained: {selected_fraction:.3f}")
111+
print("Interpretation: if only a small fraction of the calibration window is")
112+
print("retained, the chosen segment may not be clean enough for stable ASR.")
66113
plt.show()
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"\n# ASR calibration sensitivity\n\nThis tutorial-style example illustrates how ASR results depend on the\ncalibration segment used during fitting.\n\nWe fit ASR on three candidate calibration windows and compare the resulting\nchannel-wise RMS attenuation. This helps users choose a calibration strategy in\nreal datasets.\n\nThe main question is whether the cleaning result is stable across plausible\ncalibration windows, or whether one window yields much stronger attenuation.\n\nUses `meegkit.asr.ASR()`.\n\n## References\n.. [1] Mullen, T., Kothe, C., Chi, Y., Ojeda, A., Kerth, T., Makeig, S.,\n Jung, T. P., & Cauwenberghs, G. (2015). Real-time neuroimaging and\n cognitive monitoring using wearable dry EEG. IEEE Transactions on\n Biomedical Engineering, 62(11), 2553-2567.\n"
8+
]
9+
},
10+
{
11+
"cell_type": "code",
12+
"execution_count": null,
13+
"metadata": {
14+
"collapsed": false
15+
},
16+
"outputs": [],
17+
"source": [
18+
"import os\n\nimport matplotlib.pyplot as plt\nimport numpy as np\n\nfrom meegkit.asr import ASR\nfrom meegkit.utils.matrix import sliding_window\n\nraw = np.load(os.path.join(\"..\", \"tests\", \"data\", \"eeg_raw.npy\"))\nsfreq = 250"
19+
]
20+
},
21+
{
22+
"cell_type": "markdown",
23+
"metadata": {},
24+
"source": [
25+
"## Define candidate calibration windows\nIn practice, calibration quality strongly affects how aggressively ASR\nsuppresses artifacts. Here we compare three 20-second windows.\n\n"
26+
]
27+
},
28+
{
29+
"cell_type": "code",
30+
"execution_count": null,
31+
"metadata": {
32+
"collapsed": false
33+
},
34+
"outputs": [],
35+
"source": [
36+
"windows_sec = {\n \"early (0-20s)\": (0, 20),\n \"middle (10-30s)\": (10, 30),\n \"late (20-40s)\": (20, 40),\n}\n\nX = sliding_window(raw, window=int(sfreq), step=int(sfreq))\nraw_win = X.reshape(raw.shape[0], -1)\nrms_before = np.sqrt(np.mean(raw_win ** 2, axis=1))\n\nratios = {}"
37+
]
38+
},
39+
{
40+
"cell_type": "markdown",
41+
"metadata": {},
42+
"source": [
43+
"## Fit and apply ASR for each candidate window\n\n"
44+
]
45+
},
46+
{
47+
"cell_type": "code",
48+
"execution_count": null,
49+
"metadata": {
50+
"collapsed": false
51+
},
52+
"outputs": [],
53+
"source": [
54+
"for label, (start_s, stop_s) in windows_sec.items():\n train_idx = np.arange(start_s * sfreq, stop_s * sfreq, dtype=int)\n\n asr = ASR(method=\"euclid\")\n asr.fit(raw[:, train_idx])\n\n Y = np.zeros_like(X)\n for i in range(X.shape[1]):\n Y[:, i, :] = asr.transform(X[:, i, :])\n\n clean = Y.reshape(raw.shape[0], -1)\n rms_after = np.sqrt(np.mean(clean ** 2, axis=1))\n ratios[label] = rms_after / np.maximum(rms_before, np.finfo(float).eps)"
55+
]
56+
},
57+
{
58+
"cell_type": "markdown",
59+
"metadata": {},
60+
"source": [
61+
"## Visualize channel-wise attenuation by calibration choice\nWhat to look for:\n- Ratios below 1 indicate attenuation.\n- Large differences across windows suggest sensitivity to calibration period.\n\n"
62+
]
63+
},
64+
{
65+
"cell_type": "code",
66+
"execution_count": null,
67+
"metadata": {
68+
"collapsed": false
69+
},
70+
"outputs": [],
71+
"source": [
72+
"fig, ax = plt.subplots(1, 1, figsize=(8, 4))\nchannels = np.arange(raw.shape[0])\n\nfor label, ratio in ratios.items():\n ax.plot(channels, ratio, \"o-\", lw=1.5, label=label)\n\nax.axhline(1.0, color=\"k\", ls=\":\", lw=1)\nax.set_xticks(channels)\nax.set_xlabel(\"Channel\")\nax.set_ylabel(\"RMS ratio (after / before)\")\nax.set_title(\"ASR sensitivity to calibration window\")\nax.grid(True, axis=\"y\", ls=\":\", alpha=.4)\nax.legend(fontsize=\"small\")\nplt.tight_layout()\n\nfor label, ratio in ratios.items():\n print(f\"{label:>16}: median ratio = {np.median(ratio):.3f}\")\n\nbest_label = min(ratios, key=lambda key: np.median(ratios[key]))\nprint(f\"Most aggressive calibration in this example: {best_label}\")\nprint(\"Interpretation: large separation between curves means the ASR result\")\nprint(\"depends strongly on which segment is treated as clean calibration data.\")\n\nplt.show()"
73+
]
74+
}
75+
],
76+
"metadata": {
77+
"kernelspec": {
78+
"display_name": "Python 3",
79+
"language": "python",
80+
"name": "python3"
81+
},
82+
"language_info": {
83+
"codemirror_mode": {
84+
"name": "ipython",
85+
"version": 3
86+
},
87+
"file_extension": ".py",
88+
"mimetype": "text/x-python",
89+
"name": "python",
90+
"nbconvert_exporter": "python",
91+
"pygments_lexer": "ipython3",
92+
"version": "3.13.11"
93+
}
94+
},
95+
"nbformat": 4,
96+
"nbformat_minor": 0
97+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""
2+
ASR calibration sensitivity
3+
===========================
4+
5+
This tutorial-style example illustrates how ASR results depend on the
6+
calibration segment used during fitting.
7+
8+
We fit ASR on three candidate calibration windows and compare the resulting
9+
channel-wise RMS attenuation. This helps users choose a calibration strategy in
10+
real datasets.
11+
12+
The main question is whether the cleaning result is stable across plausible
13+
calibration windows, or whether one window yields much stronger attenuation.
14+
15+
Uses `meegkit.asr.ASR()`.
16+
17+
References
18+
----------
19+
.. [1] Mullen, T., Kothe, C., Chi, Y., Ojeda, A., Kerth, T., Makeig, S.,
20+
Jung, T. P., & Cauwenberghs, G. (2015). Real-time neuroimaging and
21+
cognitive monitoring using wearable dry EEG. IEEE Transactions on
22+
Biomedical Engineering, 62(11), 2553-2567.
23+
"""
24+
import os
25+
26+
import matplotlib.pyplot as plt
27+
import numpy as np
28+
29+
from meegkit.asr import ASR
30+
from meegkit.utils.matrix import sliding_window
31+
32+
raw = np.load(os.path.join("..", "tests", "data", "eeg_raw.npy"))
33+
sfreq = 250
34+
35+
###############################################################################
36+
# Define candidate calibration windows
37+
# -----------------------------------------------------------------------------
38+
# In practice, calibration quality strongly affects how aggressively ASR
39+
# suppresses artifacts. Here we compare three 20-second windows.
40+
windows_sec = {
41+
"early (0-20s)": (0, 20),
42+
"middle (10-30s)": (10, 30),
43+
"late (20-40s)": (20, 40),
44+
}
45+
46+
X = sliding_window(raw, window=int(sfreq), step=int(sfreq))
47+
raw_win = X.reshape(raw.shape[0], -1)
48+
rms_before = np.sqrt(np.mean(raw_win ** 2, axis=1))
49+
50+
ratios = {}
51+
52+
###############################################################################
53+
# Fit and apply ASR for each candidate window
54+
# -----------------------------------------------------------------------------
55+
for label, (start_s, stop_s) in windows_sec.items():
56+
train_idx = np.arange(start_s * sfreq, stop_s * sfreq, dtype=int)
57+
58+
asr = ASR(method="euclid")
59+
asr.fit(raw[:, train_idx])
60+
61+
Y = np.zeros_like(X)
62+
for i in range(X.shape[1]):
63+
Y[:, i, :] = asr.transform(X[:, i, :])
64+
65+
clean = Y.reshape(raw.shape[0], -1)
66+
rms_after = np.sqrt(np.mean(clean ** 2, axis=1))
67+
ratios[label] = rms_after / np.maximum(rms_before, np.finfo(float).eps)
68+
69+
###############################################################################
70+
# Visualize channel-wise attenuation by calibration choice
71+
# -----------------------------------------------------------------------------
72+
# What to look for:
73+
# - Ratios below 1 indicate attenuation.
74+
# - Large differences across windows suggest sensitivity to calibration period.
75+
fig, ax = plt.subplots(1, 1, figsize=(8, 4))
76+
channels = np.arange(raw.shape[0])
77+
78+
for label, ratio in ratios.items():
79+
ax.plot(channels, ratio, "o-", lw=1.5, label=label)
80+
81+
ax.axhline(1.0, color="k", ls=":", lw=1)
82+
ax.set_xticks(channels)
83+
ax.set_xlabel("Channel")
84+
ax.set_ylabel("RMS ratio (after / before)")
85+
ax.set_title("ASR sensitivity to calibration window")
86+
ax.grid(True, axis="y", ls=":", alpha=.4)
87+
ax.legend(fontsize="small")
88+
plt.tight_layout()
89+
90+
for label, ratio in ratios.items():
91+
print(f"{label:>16}: median ratio = {np.median(ratio):.3f}")
92+
93+
best_label = min(ratios, key=lambda key: np.median(ratios[key]))
94+
print(f"Most aggressive calibration in this example: {best_label}")
95+
print("Interpretation: large separation between curves means the ASR result")
96+
print("depends strongly on which segment is treated as clean calibration data.")
97+
98+
plt.show()

0 commit comments

Comments
 (0)