Python for signals
The dozen lines of NumPy, Matplotlib, and SciPy you need to load a recording, plot it properly, filter it, and see its spectrum. Plus enough Git to not lose your work.
On this page1 hour
You do not need to be a programmer for Phase 1. You need to load a CSV, plot it with labelled axes, apply a band-pass, and compute a spectrumSpectrum A plot of how much of a signal lives at each frequency. Related: Fast Fourier transform (FFT)Fast Fourier transform (FFT) The algorithm that breaks a stretch of signal into the sinusoids that would add up to it, giving amplitude and phase at each frequency. , Power spectral density (PSD)Power spectral density (PSD) The spectrum expressed as power per hertz, the standard way to quantify how strong alpha is. . Glossary entry. That is about a dozen lines and they are all on this page. Everything else in Python you will pick up as you need it.
Install
Python 3 from the official installer or Miniforge. Then in a terminal:
pip install numpy scipy matplotlib mne
Use a plain text editor and the terminal, or VS Code, or a Jupyter notebook. All three are fine. Notebooks are good for exploring and bad for anything you want to run again; when a notebook works, copy the code into a .py file.
Load a recording from the plotter
import numpy as np
d = np.loadtxt('recording.csv', delimiter=',', skiprows=1)
t = d[:, 0] / 1000.0 # first column: milliseconds -> seconds
x = d[:, 1] # second column: channel 1, ADC counts
fs = 1.0 / np.median(np.diff(t))
print(f'{len(x)} samples, about {fs:.1f} Hz')
Convert counts to microvolts once, at the top, and never think about it again. For an Arduino on a 5 V reference and a total gainGain How many times an amplifier multiplies its input; 1000× turns 50 microvolts into 50 millivolts, enough for an ADC to see. Related: Instrumentation amplifierInstrumentation amplifier A precision differential amplifier with very high input impedance and high CMRR, built from three op-amps and sold as a single chip such as the INA128 or AD8232. . Glossary entry of 2000: one count is 5 V / 1024 / 2000, about 2.44 µV.
uV = (x - x.mean()) * (5.0 / 1024 / 2000) * 1e6
Plot it properly
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(10, 3))
ax.plot(t, uV, lw=0.6)
ax.set(xlabel='time (s)', ylabel='µV', title='O1 - A1, eyes closed, 2026-09-14')
fig.tight_layout(); fig.savefig('eyes-closed-2026-09-14.png', dpi=150)
Axes with units. A title that says what and when. Saved with a name that sorts by date. Those are the notebook rules from Phase 0 and they apply to every figure you ever make.
Filter it
from scipy import signal
sos = signal.butter(4, [0.5, 40], btype='bandpass', fs=fs, output='sos')
clean = signal.sosfiltfilt(sos, uV) # zero-phase: forward and backward
sosfiltfilt runs the filterFilter An operation that passes some frequencies and attenuates others; every filter also delays or distorts the timing of what it passes. Related: High-pass filterHigh-pass filter Removes slow drifts and DC offsets, passing everything above a cutoff; set it too high and you erase slow brain activity. , Low-pass filterLow-pass filter Removes fast content above a cutoff; the anti-alias filter is one, and so is the smoothing you apply to a noisy trace. , Notch filterNotch filter A very narrow filter that removes one frequency, usually 60 Hz mains hum; it also rings and distorts nearby content, so fix the hum at the source first. , Phase distortionPhase distortion Different frequencies being delayed by different amounts, which smears and shifts waveforms such as an ERP peak. . Glossary entry forward and backward so it does not shift your peaks in time. That is only possible after the fact, on a recording; the Arduino cannot do it live. Phase 2’s filters explainer is about exactly this.
See its spectrum
f, pxx = signal.welch(clean, fs=fs, nperseg=int(2 * fs)) # 2 s windows
fig, ax = plt.subplots()
ax.semilogy(f, pxx); ax.set(xlim=(0, 60), xlabel='Hz', ylabel='µV²/Hz', title='Welch PSD')
Two-second windows give 0.5 Hz resolution, which is enough to see alpha’s peak and read off its frequency. Overlay eyes-open and eyes-closed on the same axes with a legend; that overlay is the exhibit for Project B.
Six things to know about NumPy
Arrays are the unit; loops over samples are slow and almost never needed. x[a:b] slices. x.mean(), x.std(), np.abs(x), np.argmax(x). Boolean masks: x[(f > 8) & (f < 12)].mean() is alpha band power. Broadcasting: x - x.mean() subtracts from every element. Read the error message; it says which line and usually why.
Git, the minimum
Your code and your CSVs are part of the notebook. Put each project in a folder and, once:
git init
git add .
git commit -m "first working single-channel EEG script"
Then after every working change: git add -A && git commit -m "what changed". Push to a free GitHub repository so a dead laptop does not end the project. That repository is also where your build logs’ code lives, and recruiters look at it. Do not commit anything with another person’s name or recording in it.
Where this goes
Phase 2’s software pipeline project moves from these scripts to MNE-PythonMNE-Python The standard open-source Python library for EEG and MEG analysis: filtering, epoching, ICA, topographies, source localization. Related: Independent component analysis (ICA)Independent component analysis (ICA) A method that unmixes multichannel data into statistically independent sources, so blinks and heartbeat can be identified and removed. . Glossary entry, which handles channels, montagesMontage The particular arrangement of which electrodes are subtracted from which, such as referential, bipolar, or average reference. Related: Reference electrodeReference electrode The electrode every other channel is measured against; there is no neutral spot on the head, so the choice shapes every trace. , Common average referenceCommon average reference Re-referencing every channel to the mean of all channels, which approximates a neutral reference when electrodes cover the head well. , Surface LaplacianSurface Laplacian Subtracting the average of neighbouring electrodes from each electrode, which sharpens spatial detail and suppresses distant sources. . Glossary entry, epochsEpoch A fixed-length slice of the recording locked to an event, such as one second starting 200 milliseconds before each stimulus. Related: Trial averagingTrial averaging Adding many time-locked epochs together so the consistent response grows and the random noise shrinks by the square root of the number of trials. , Baseline correctionBaseline correction Subtracting the mean of the pre-stimulus interval from each epoch so trials line up. . Glossary entry, and topographies for you and is what every lab uses. Phase 3 adds scikit-learn for classifiersClassifier An algorithm that assigns each trial a label, such as left hand or right hand, after being trained on labelled examples. Related: Linear discriminant analysis (LDA)Linear discriminant analysis (LDA) A simple, robust classifier that draws a straight boundary between classes; the workhorse of EEG decoding for two decades. , Support vector machine (SVM)Support vector machine (SVM) A classifier that finds the boundary with the widest margin between classes; strong on small datasets. , Cross-validationCross-validation Testing a model on data it never saw during training, repeatedly, so the reported accuracy means something. . Glossary entry. None of it is harder than this page; there is just more of it.