Skip to content

Commit 6c94c0a

Browse files
aman-coder03pre-commit-ci[bot]larsonerdrammock
authored
Add example for Python/R interoperability using mass univariate t-test (#13729)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Eric Larson <larson.eric.d@gmail.com> Co-authored-by: Daniel McCloy <dan@mccloy.info>
1 parent 6d49f69 commit 6c94c0a

4 files changed

Lines changed: 147 additions & 1 deletion

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add example showing how to interoperate with R using ``rpy2``, by `Aman Srivastava`_.

examples/stats/r_interop.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
"""
2+
.. _r-interop:
3+
4+
============================
5+
Integrating with R via rpy2
6+
============================
7+
8+
This example shows how to run a mass-univariate 2-sample t-test on
9+
:class:`~mne.Epochs` data in Python using :func:`scipy.stats.ttest_ind`,
10+
then run the equivalent test in R via `rpy2 <https://rpy2.github.io>`__,
11+
and confirm that both approaches give identical results.
12+
13+
``rpy2`` is probably most useful for leveraging statistical functionality in R
14+
that is unavailable (or hard to use) in Python, but in principle it can be
15+
used for anything the R ecosystem has to offer.
16+
17+
.. note::
18+
This example requires ``rpy2`` to be installed (``pip install rpy2``)
19+
and a working R installation with the ``stats`` package (included by
20+
default in R).
21+
"""
22+
# Authors: The MNE-Python contributors.
23+
# License: BSD-3-Clause
24+
# Copyright the MNE-Python contributors.
25+
26+
# %%
27+
# Load sample data and create Epochs
28+
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
29+
#
30+
# We use the MNE sample dataset and create epochs for two conditions:
31+
# auditory/left and auditory/right.
32+
33+
import rpy2.robjects as ro
34+
from rpy2.robjects import default_converter, numpy2ri
35+
from rpy2.robjects.conversion import localconverter
36+
from scipy import stats
37+
38+
import mne
39+
40+
data_path = mne.datasets.sample.data_path()
41+
raw_fname = data_path / "MEG" / "sample" / "sample_audvis_filt-0-40_raw.fif"
42+
event_fname = data_path / "MEG" / "sample" / "sample_audvis_filt-0-40_raw-eve.fif"
43+
44+
raw = mne.io.read_raw_fif(raw_fname, preload=True)
45+
events = mne.read_events(event_fname)
46+
47+
event_id = {"auditory/left": 1, "auditory/right": 2}
48+
tmin, tmax = -0.2, 0.5
49+
50+
epochs = mne.Epochs(
51+
raw,
52+
events,
53+
event_id=event_id,
54+
tmin=tmin,
55+
tmax=tmax,
56+
baseline=(None, 0),
57+
preload=True,
58+
)
59+
60+
# %%
61+
# Visualize the evoked responses
62+
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
63+
#
64+
# We first plot the evoked responses to motivate the statistical test.
65+
# Auditory left vs right stimuli should differ over lateral temporal sensors.
66+
67+
evoked_left = epochs["auditory/left"].average()
68+
evoked_right = epochs["auditory/right"].average()
69+
70+
mne.viz.plot_compare_evokeds(
71+
{"auditory/left": evoked_left, "auditory/right": evoked_right},
72+
picks="MEG 1323",
73+
)
74+
75+
# %%
76+
# Extract ROI data and run t-test in Python
77+
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
78+
#
79+
# We pick a few lateral temporal sensors as our ROI and average over them
80+
# and a typical N1 time window (80–120 ms). This gives one value per epoch,
81+
# which is a plausible neuroscience analysis.
82+
83+
roi_channels = ["MEG 1323"]
84+
tmin_roi, tmax_roi = 0.08, 0.12
85+
86+
epochs.crop(tmin_roi, tmax_roi).pick(roi_channels)
87+
epochs_left = epochs["auditory/left"].get_data().mean(axis=(1, 2))
88+
epochs_right = epochs["auditory/right"].get_data().mean(axis=(1, 2))
89+
90+
t_python, p_python = stats.ttest_ind(epochs_left, epochs_right)
91+
print(f"Python → t = {t_python:.4f}, p = {p_python:.4f}")
92+
93+
# %%
94+
# Run the same t-test in R via rpy2
95+
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
96+
#
97+
# We pass the same NumPy arrays to R using ``rpy2`` and call R's built-in
98+
# ``t.test()``. A few things to note about the ``rpy2`` API:
99+
#
100+
# **Accessing R functions:**
101+
# ``rpy2.robjects`` (imported as ``ro``) has an attribute ``r`` that acts as
102+
# a proxy to the R global namespace. You can access any R function by name
103+
# using a dictionary-like interface, e.g. ``ro.r["t.test"]`` retrieves R's
104+
# ``t.test`` function as a callable Python object.
105+
#
106+
# **Converting NumPy arrays to R vectors:**
107+
# R functions expect R objects as input, not raw NumPy arrays.
108+
# ``rpy2.robjects.FloatVector`` converts a 1-D NumPy array of floats
109+
# into an R numeric vector. The ``localconverter`` context manager
110+
# together with ``numpy2ri.converter`` handles the conversion
111+
# automatically inside the ``with`` block.
112+
#
113+
# **Passing arguments with dots in their names:**
114+
# Unlike Python, R allows function parameter names to contain ``.``, such as
115+
# ``var.equal``. Since ``var.equal`` is not a valid Python keyword argument
116+
# name, you must pass it inside a dictionary and unpack it with ``**``.
117+
#
118+
# **Extracting results from R objects:**
119+
# R's ``t.test()`` returns a list-like object. The ``rx2`` method extracts
120+
# a named element from it - this is equivalent to the ``$`` operator in R
121+
# (e.g. ``result$statistic``). The extracted value is still an R vector, so
122+
# we index with ``[0]`` to get the first (and only) element as a Python scalar,
123+
# and wrap it in ``float()`` to ensure it is a plain Python float.
124+
125+
with localconverter(default_converter + numpy2ri.converter):
126+
r_left = ro.FloatVector(epochs_left)
127+
r_right = ro.FloatVector(epochs_right)
128+
129+
r_ttest = ro.r["t.test"]
130+
result = r_ttest(r_left, r_right, **{"var.equal": True})
131+
132+
t_r = float(result.rx2("statistic")[0])
133+
p_r = float(result.rx2("p.value")[0])
134+
print(f"R → t = {t_r:.4f}, p = {p_r:.4f}")
135+
136+
# %%
137+
# Compare results
138+
# ^^^^^^^^^^^^^^^^
139+
#
140+
# Both approaches give identical t and p values (up to floating point
141+
# precision), confirming that R and Python produce equivalent results.
142+
143+
print(f"\nt difference: {abs(t_python - t_r):.2e}")
144+
print(f"p difference: {abs(p_python - p_r):.2e}")

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ doc = [
2424
"pyvistaqt >= 0.11", # released 2023-06-30, no newer version available
2525
"pyxdf",
2626
"pyzmq != 24.0.0",
27+
"rpy2",
2728
"scikit-learn >= 1.4", # released 2024-01-18, will become 1.5 on 2026-05-21
2829
"seaborn >= 0.5, != 0.11.2",
2930
"selenium >= 4.27.1",

tools/circleci_bash_env.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ set -e
44
set -o pipefail
55

66
./tools/setup_xvfb.sh
7-
sudo apt install -qq graphviz optipng python3.12-venv python3-venv libxft2 ffmpeg
7+
sudo apt install -qq graphviz optipng python3.12-venv python3-venv libxft2 ffmpeg r-base libtirpc-dev
88
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
99
sudo apt install ./google-chrome-stable_current_amd64.deb
1010
python3.12 -m venv ~/python_env

0 commit comments

Comments
 (0)