✦ Day 2 · Matplotlib & MNE Foundations

Plotting for Insights
Turning Arrays into Visual Stories

Master essential visualization patterns for EEG signal analysis — from global figure configuration and time-series overlays to MNE scalp topoplots, raincloud distributions, and publication figures with delta topomaps.

⚙️ Figure rcParams Defaults 📈 Time-Series & SEM 🧠 MNE Scalp Topomaps 🌧️ Raincloud Distributions 🎨 Matplotlib & MNE-Python

Data visualization bridges high-dimensional numerical arrays and physiological understanding. This guide operates directly on the shared 5D dataset loaded from ../day_1/data/neural_data.npy (shape 3×3×30×32×6), delivering precise takeaways for every plot configuration.


1

Figure Configuration Defaults (rcParams)

Publication-grade graphics begin with consistent global defaults. Rather than manually tweaking fonts, tick widths, and margins on every figure, establish a reusable configuration helper that applies system-wide rcParams.

Python
def setup_publication_style():
    """Configures global Matplotlib defaults for publication figures."""
    plt.rcParams.update({
        "font.family"          : "sans-serif",            # Primary font family category
        "font.sans-serif"     : ["Inter", "Helvetica", "Arial"],  # Font fallback stack
        "axes.spines.top"      : False,                  # Hide top spine boundary
        "axes.spines.right"    : False,                  # Hide right spine boundary
        "axes.linewidth"       : 1.2,                    # Line thickness for left/bottom spines
        "axes.edgecolor"       : "#2E3654",              # Dark slate border color
        "axes.labelcolor"      : "#1E293B",              # Axis label text color
        "axes.titlesize"       : 12,                     # Subplot title font size
        "axes.titleweight"     : "bold",                 # Subplot title font weight
        "axes.labelsize"       : 11,                     # X/Y axis label font size
        "axes.labelweight"     : "medium",               # Axis label font weight
        "xtick.major.width"    : 1.2,                    # X-axis tick line thickness
        "ytick.major.width"    : 1.2,                    # Y-axis tick line thickness
        "xtick.labelsize"      : 9,                      # X-axis tick label font size
        "ytick.labelsize"      : 9,                      # Y-axis tick label font size
        "figure.titlesize"     : 14,                     # Main super-title font size
        "figure.titleweight"   : "bold",                 # Main super-title font weight
        "figure.dpi"           : 150,                    # On-screen render DPI
        "savefig.dpi"          : 150,                    # High-resolution export DPI
        "savefig.bbox"         : "tight",   # Strip redundant outer whitespace
    })

Takeaway: Set axes.spines.top = False and axes.spines.right = False globally to eliminate non-data ink. Use savefig.bbox = "tight" to prevent title clipping on export.


2

Single Time-Series Plotting

Visualizing an individual electrode channel trajectory. Extracting channel Cz (index 9) alpha power for Control Subject 0 across time points.

Python
ts = data[0, :, 0, 9, 2]  # Control · sub0 · Cz · alpha

fig, ax = plt.subplots(figsize=(10, 3.5))
ax.plot(t_axis, ts, color=COLORS[0], lw=1.5, marker="o", label="Control · sub0 · Cz · alpha")
ax.set_xticks(t_axis)
ax.set_xticklabels(["Baseline", "Task", "Rest"])
ax.set_xlabel("Timepoint"); ax.set_ylabel("Power (μV²/Hz)")
ax.set_title("Alpha Power — Control Group, Subject 0, Channel Cz")
ax.legend(); ax.grid(True, alpha=0.3)
Single Time-Series Plot
Figure 1: Cz channel alpha power trajectory for Control Subject 0.

Takeaway: Set explicit categorical tick labels (ax.set_xticks([0, 1, 2]); ax.set_xticklabels(["Baseline", "Task", "Rest"])) for discrete experimental timepoints. Prefer object-oriented axes (ax.plot) over procedural module calls.


3

Multi-Group Time-Series Overlay

Overlaying grand-average cohort signals (Control, Patient, Treatment) to evaluate group-level temporal differences.

Python
fig, ax = plt.subplots(figsize=(11, 4))
for g in range(G):
    ts_g = data[g, :, :, :, 2].mean(axis=(1, 2))   # Mean over subjects & channels
    ax.plot(t_axis, ts_g, color=COLORS[g], lw=2, marker="o", label=gnames[g])

ax.set_xticks(t_axis)
ax.set_xticklabels(["Baseline", "Task", "Rest"])
ax.set_xlabel("Timepoint"); ax.set_ylabel("Mean Alpha Power")
ax.set_title("Alpha Power Over Time — All Groups")
ax.legend(); ax.grid(True, alpha=0.3)
Multi-Group Time-Series
Figure 2: Mean alpha power trajectory comparison across Control, Patient, and Treatment cohorts.

Takeaway: Store an explicit color palette array (e.g. COLORS = ["#2196F3", "#F44336", "#4CAF50"]) to enforce fixed cohort colors across all figures in your study.


4

Confidence Bands (fill_between ±SEM)

Communicating statistical variance. Shading standard error of the mean (SEM across subjects) reveals whether cohort trajectories are reliably separated.

Python
for g in range(G):
    mu, sem = _mean_sem(g, fi=2)
    ax.plot(t_axis, mu, color=COLORS[g], lw=2, marker="o", label=gnames[g])
    ax.fill_between(t_axis, mu - sem, mu + sem, color=COLORS[g], alpha=0.20)

ax.set_xticks(t_axis)
ax.set_xticklabels(["Baseline", "Task", "Rest"])
ax.set_xlabel("Timepoint"); ax.set_ylabel("Alpha Power ± SEM")

# Place legend at the bottom below the plot frame
ax.legend(loc="upper center", bbox_to_anchor=(0.5, -0.22), ncol=4, frameon=False)
Confidence Bands Plot
Figure 3: Mean alpha power with ±1 SEM shaded confidence bands and vertical reference mark.

Takeaway: Position legends at the bottom below the plot frame (bbox_to_anchor=(0.5, -0.22)) so labels never obscure signal trajectories or shaded SEM confidence ribbons.


5

Subplots Grid Layout

Organizing multi-feature matrices across groups into a synchronized subfigure grid using plt.subplots(2, 3, sharex=True).

Python
fig, axes = plt.subplots(2, 3, figsize=(15, 6), sharex=True)
feat_cols = [("Alpha", 2), ("Beta", 3), ("Gamma", 4)]

for row, g in enumerate([0, 1]):
    for col, (fname, fi) in enumerate(feat_cols):
        ax = axes[row, col]
        mu, sem = _mean_sem(g, fi)
        ax.plot(t_axis, mu, color=COLORS[g], lw=1.8, marker="o", label=gnames[g])
        ax.fill_between(t_axis, mu - sem, mu + sem, color=COLORS[g], alpha=0.20)
        ax.set_title(f"{fname} Band", fontsize=10, fontweight="bold")
        ax.set_xticks(t_axis)
        ax.set_xticklabels(["Baseline", "Task", "Rest"])
        if row == 1:
            ax.set_xlabel("Timepoint", fontsize=9)
        
        # Position legend outside plot area to avoid obscuring signal curves
        if col == 2:
            ax.legend(loc="center left", bbox_to_anchor=(1.04, 0.5), frameon=False)
Subplots Grid
Figure 4: 2×3 matrix comparing alpha, beta, and gamma frequency power between Control and Patient cohorts.

Takeaway: Always place legend boxes outside the plot area (using bbox_to_anchor=(1.04, 0.5)) so labels never obscure signal trajectories or confidence ribbons.


6

MNE Scalp Topography (mne.viz.plot_topomap)

Projecting 32-channel electrode power onto anatomically accurate 2D scalp topomaps using MNE-Python and standard 10-20 sensor montages.

Python
import mne
from matplotlib.colors import LinearSegmentedColormap

# Benedikt Ehinger's EEG Topographic Colormap (becp)
colors = [(0.2706, 0.4588, 0.7059), (0.5686, 0.7490, 0.8588),
          (0.8784, 0.9529, 0.9725), (1.0000, 1.0000, 0.7490),
          (0.9961, 0.8784, 0.5647), (0.9882, 0.5529, 0.3490),
          (0.8431, 0.1882, 0.1529)]
cmap_becp = LinearSegmentedColormap.from_list("becp", colors, N=256)

# Create MNE Info with standard 10-20 montage
info = mne.create_info(ch_names=chnames, sfreq=hz, ch_types="eeg")
info.set_montage(mne.channels.make_standard_montage("standard_1020"))

fig, axes = plt.subplots(1, 3, figsize=(14, 4))

for g in range(G):
    alpha_power = data[g, :, :, :, 2].mean(axis=(0, 1))  # (32,) channel mean
    vmin, vmax = alpha_power.min(), alpha_power.max()
    im, _ = mne.viz.plot_topomap(
        alpha_power, info, axes=axes[g], show=False, cmap=cmap_becp, vlim=(vmin, vmax)
    )
    axes[g].set_title(f"{gnames[g]} — Alpha Topography")
    fig.colorbar(im, ax=axes[g], shrink=0.75, pad=0.04)
MNE Scalp Topomap
Figure 5: MNE scalp topography rendering 32-channel alpha power across Control, Patient, and Treatment cohorts.

Takeaway: Prefer MNE topomaps over rectangular heatmaps for spatial channel data — polar 2D head geometry provides true anatomical spatial interpolation.


7

Raincloud Distributions (Cloud + Box + Rain)

Replacing bar graphs with Raincloud Plots. Combining half-KDE density shapes, narrow boxplots, and raw data scatter points delivers complete distribution transparency.

Python
import scipy.stats as stats

for g in range(G):
    sub_vals = data[g, :, :, :, fi].mean(axis=(0, 2))  # per-subject mean
    color = COLORS[g]
    
    # 1. Cloud: Half-KDE density plot
    kde = stats.gaussian_kde(sub_vals)
    y_grid = np.linspace(sub_vals.min() - 0.04, sub_vals.max() + 0.04, 100)
    density = (kde(y_grid) / kde(y_grid).max()) * 0.28
    ax.fill_betweenx(y_grid, g + 0.06, g + 0.06 + density, color=color, alpha=0.45)
    
    # 2. Umbrella: Narrow box plot
    ax.boxplot(sub_vals, positions=[g], widths=0.08, patch_artist=True,
               boxprops=dict(facecolor=color, alpha=0.8),
               medianprops=dict(color="white", lw=1.8))
    
    # 3. Rain: Jittered raw subject scatter points
    jitter = RNG.uniform(-0.06, 0.06, size=len(sub_vals))
    ax.scatter(g - 0.18 + jitter, sub_vals, color=color, alpha=0.65, s=28)
Raincloud Plots
Figure 6: Raincloud plots displaying individual subject power distributions across Alpha, Beta, and Gamma frequency bands.

Takeaway: Bar charts hide multimodal shapes and subject sample sizes. Rainclouds reveal true raw subject data points alongside summary statistics.


8

Annotated Correlation Matrix

Evaluating feature co-activation across frequency bands by computing Pearson correlation matrices (np.corrcoef).

Data Pooling: By reshaping data[g] from (T=3, S=30, C=32, F=6) into (T*S*C, F)(2880, 6), we pool subjects across all timepoints (Baseline, Task, Rest) and all 32 electrode channels into $2{,}880$ total observations to estimate robust cross-frequency coupling for each cohort.

Python
for g in range(G):
    # Flatten cohort into (T*S*C, F) = (2880, 6) -> pools subjects across all timepoints & channels
    mat  = data[g].reshape(-1, F)
    corr = np.corrcoef(mat.T)
    im   = axes[g].imshow(corr, cmap="RdBu_r", vmin=-1, vmax=1)
    
    # Overlay annotated text with dynamic contrast
    for i in range(F):
        for j in range(F):
            col = "w" if abs(corr[i, j]) > 0.60 else "k"
            axes[g].text(j, i, f"{corr[i,j]:.2f}", ha="center", va="center", color=col)
Correlation Matrix
Figure 7: Feature-to-feature correlation matrices for Control, Patient, and Treatment cohorts (pooling subjects across all timepoints and channels).

Takeaway: Always center diverging colormaps (e.g. RdBu_r) at zero with vmin=-1, vmax=1 for correlation matrices, and switch text color dynamically for contrast.


9

Journal-Ready Figure & MNE Delta Topomap

Assembling a two-panel publication figure combining subject-level scatter relationships in Panel A with an MNE Patient − Control delta scalp topomap in Panel B.

Python
setup_publication_style()

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 5))

# Panel A: Alpha vs Beta scatter per subject
for g in [0, 1]:
    sub_alpha = data[g, :, :, :, 2].mean(axis=(0, 2))
    sub_beta  = data[g, :, :, :, 3].mean(axis=(0, 2))
    ax1.scatter(sub_alpha, sub_beta, c=COLORS[g], alpha=0.72, s=65, label=gnames[g])

# Panel B: Patient - Control Delta Topomap (Benedikt Ehinger becp colormap)
diff_alpha = data[1, :, :, :, 2].mean(axis=(0, 1)) - data[0, :, :, :, 2].mean(axis=(0, 1))
vlim = np.abs(diff_alpha).max()

im, _ = mne.viz.plot_topomap(
    diff_alpha, info, axes=ax2, show=False, cmap=cmap_becp, vlim=(-vlim, vlim)
)
ax2.set_title("(B) Patient − Control Δ Alpha Topography")
fig.colorbar(im, ax=ax2, shrink=0.75, pad=0.04, label="Δ Alpha Power")
Journal-Ready Figure with Delta Topomap
Figure 8: Journal-ready two-panel figure combining subject scatter relationships with MNE Patient − Control delta scalp topography.

Takeaway: On difference topomaps (Patient - Control), set symmetric limits vlim=(-vlim, vlim) with a zero-centered diverging colormap so blue indicates power reduction and red indicates power enhancement.


Homework Challenge: High Alpha Electrode Topography

🎯 Objective: Recreate the Highlighted Scalp Topography Below

Your task is to write a Python script using mne.viz.plot_topomap that reproduces this target figure. You need to identify channels with high alpha power during baseline rest in healthy controls and visually highlight them with white indicator markers.

  • Cohort & Condition: Select the Control group (g=0), Baseline timepoint (t=0), and Alpha power feature (fi=2).
  • Channel Mean: Compute the average alpha power across all 30 subjects for each of the 32 electrode channels (result shape: (32,)).
  • Thresholding: Identify channels where alpha power exceeds the 75th percentile threshold (hint: np.percentile(alpha_base, 75)).
  • MNE Visualization & Limits: Render the 2D scalp topography with the Ehinger (becp) colormap. Explicitly set vmin and vmax to the exact minimum and maximum values of the data array being plotted (e.g. vmin = alpha_base.min() and vmax = alpha_base.max(), passed via vlim=(vmin, vmax)) so the colormap spans the full data range.
  • Electrode Highlighting: Pass a boolean mask and custom mask_params to plot_topomap so that channels exceeding the threshold are overlaid with prominent white dots with a crisp border.
Homework High Alpha Electrode Topography
Homework Target Figure: Control cohort baseline alpha power topography with high-power electrodes (> 75th percentile) highlighted using white sensor markers.

Day 2 Cheat-Sheet

Visualization Technique Primary API Call EEG Use Case
Figure Defaults plt.rcParams.update({...}) Global typography & spine cleanup
Single Line ax.plot(t, signal) Raw time series & ERP traces
Group Overlay for g in G: ax.plot(...) Cohort signal trajectory comparison
Confidence Ribbon ax.fill_between(t, mu-sem, mu+sem) Group mean ± SEM variance representation
Subplot Grid plt.subplots(r, c, sharex=True) Multi-channel / multi-band comparison matrix
MNE Scalp Topomap mne.viz.plot_topomap(data, info) Anatomical 2D head scalp spatial topography
Raincloud Plot KDE + Boxplot + Jitter Scatter Complete subject-level distribution summary
Correlation Matrix np.corrcoef() + ax.text() Feature co-activation & functional coupling
Journal Figure Scatter + MNE Δ Topomap Clean publication graphics & delta maps
Homework Topo plot_topomap(..., mask=mask) Highlighting thresholded electrode channels
🎯

Homework Challenges

Hands-on problem sets targeting custom error representations, shared axis normalization, topographic scaling, higher-dimensional subplot grids, and composite biomarker discovery. Python solution scripts are available under day_2/hw/.

HW 1 • Figure 3 Solution: day_2/hw/01_fig3_errorbars.py

Discrete Error Bars Instead of Confidence Ribbon (Mean ± SEM)

In s03_confidence_bands.png, group variance was depicted using continuous shaded ribbons (ax.fill_between). For this challenge, modify the visualization to plot discrete error bars (ax.errorbar) at each discrete timepoint (Baseline, Task, Rest) displaying mean ± SEM without ribbon shading.

ax.errorbar(t_axis, mu, yerr=sem, fmt='-o', capsize=6, capthick=1.8, label=gnames[g])
HW 2 • Figure 4 Solution: day_2/hw/02_fig4_shared_y.py

Shared Y-Axis Scale & Overlayed Cohorts Across Frequency Bands

Transform the 2x3 subplot matrix from Figure 4 into a single 1x3 row (Alpha, Beta, Gamma). Plot both Control and Patient cohorts overlayed on the same axes within each frequency band, and enforce a unified Y-axis scale (sharey=True) across all three subplots to facilitate direct power amplitude comparison across frequency bands.

fig, axes = plt.subplots(1, 3, figsize=(14, 4.5), sharey=True)
HW 3 • Figure 5 Solution: day_2/hw/03_fig5_shared_clim.py

Standardized Global Color Limits Across MNE Topoplots

In Figure 5, each scalp topoplot dynamically computed its own vmin and vmax, resulting in misaligned color scales. Calculate a single global minimum (vmin) and maximum (vmax) across all three cohorts combined (Control, Patient, Treatment) and apply vlim=(global_vmin, global_vmax) across all three topoplots for fair visual spatial comparison.

mne.viz.plot_topomap(alpha_power, info, axes=ax, vlim=(global_vmin, global_vmax))
HW 4 • Figure 7 Solution: day_2/hw/04_fig7_grid3x3.py

3×3 Dynamic Feature Correlation Matrix Grid (Timepoint × Cohort)

Expand Figure 7 into a 3×3 matrix of correlation plots. Each row represents a timepoint (Row 0: Baseline, Row 1: Task, Row 2: Rest) and each column represents a cohort (Col 0: Control, Col 1: Patient, Col 2: Treatment). Calculate feature correlation matrices per cell and annotate each cell with correlation values.

fig, axes = plt.subplots(3, 3, figsize=(14, 12)); mat = data[g, t].reshape(-1, F)
HW 5 • Figure 8 Solution: day_2/hw/05_fig8_composite.py

Biomarker Discovery Composite Figure & Scientific Inference

Analyze neural_data.npy to discover electrophysiological biomarkers separating Control, Patient, and Treatment cohorts. Create a 2-panel composite figure (Panel A: Theta/Alpha Ratio across cohorts; Panel B: Alpha vs Theta subject-level cluster space with convex hull boundaries) and provide a written scientific inference detailing patient alpha suppression, theta elevation, and treatment normalization.

tar = theta_power / alpha_power # Panel A: TAR distribution | Panel B: Joint cluster space
📚

Further reading

  1. Customizing Matplotlib with Style Sheets and rcParams 📖
    Official Matplotlib guide on setting up global figure defaults, font families, spine removal, and custom style files.
  2. MNE-Python Official Overview & Topomap Guide 📖
    Comprehensive tutorials on MNE Info structures, sensor montages, and 2D scalp topographic plotting.
  3. EEG Topographic Colormaps in Matplotlib 📖
    Rahul Venugopal's repository demonstrating custom topographic colormaps and scalp projection techniques in Python.
  4. Prof. Dr. Benedikt Ehinger's Science Blog & EEG Resources 📖
    Personal science blog featuring EEG analysis guides, linear modeling tutorials, and custom colormaps.
  5. Raincloud Plots: A Multi-Platform Tool for Robust Visualization 📖
    The landmark Allen et al. (2019) paper introducing raincloud plots for transparent scientific data presentation.
  6. Scientific Visualization: Python + Matplotlib by Nicolas P. Rougier 📖
    An open-access masterclass on crafting elegant, publication-grade figures.