An electrode pop is an artifact caused by a sudden change of the electrode-skin contact: a loose electrode, drying gel, cable movement or pressure on the electrode. The half-cell potential at the electrode changes abruptly and then slowly returns to its previous value as the contact stabilizes. In the recording, this appears as:
a step: an almost instantaneous jump of the signal (often tens to thousands of μV), usually in a single channel
a recovery: an exponential return to the previous level over tens to hundreds of milliseconds
Pops are larger and faster than any brain activity and, if not removed, distort amplitude measures, spectra (the step has a broad 1/f spectrum) and ICA decompositions.
A pop differs from other fast artifacts:
spikes (e.g. a single bad sample) return to the previous level immediately
eye blinks and movements rise and fall gradually
amplifier saturation or a lost electrode produces a flat segment that does not recover exponentially
If a pop affects all channels at the same time, its source is usually the reference (or ground) electrode.
Method
remove_pops() models a pop starting at sample \(t_0\) as a step followed by an exponential decay:
\[
\mathrm{pop}(t) = a \, e^{-(t - t_0) / \tau}, \quad t \ge t_0
\]
where \(a\) is the pop amplitude and \(\tau\) is the recovery time constant (after \(\tau\) the pop has decayed to 37%, after \(3\tau\) to 5% of its amplitude).
t_m =collect(0:(1/256):(1-1/256))plot_signal(t_m, 300.*exp.(-t_m ./0.15); ylabel ="Amplitude [μV]", title ="Pop model: a = 300 μV, τ = 0.15 s")
The algorithm works on each channel separately:
Candidates: the first difference of the signal (the sample-to-sample change) is converted to a robust z-score (using the median and the MAD calculated within window seconds, so that changes of the signal amplitude during the recording do not matter). Jumps with z > z (default: 10) are candidates; within r samples only the largest jump is kept.
Step test: the mean level after the jump must differ from the level before by at least half of the jump. Single-sample spikes, which return immediately, are rejected.
Model fit: \(a\) and \(\tau\) are fitted (least squares) to the r samples after the jump, relative to the level before the jump. For each \(\tau\) the best \(a\) is calculated analytically; \(\tau\) is found by a grid search refined by golden-section search.
Acceptance: a candidate is accepted as a pop if it is large (\(|a| \ge\)a_min × robust SD of the signal, default: 5) and the model explains the signal (\(R^2 \ge\)r2_min over the first \(3\tau\), default: 0.8). This rejects blinks, muscle activity and other fast activity that is not a step with an exponential recovery.
Repair: the fitted model is subtracted from the signal (from the pop to the next pop or the end of the signal). Because only the artifact is subtracted, the underlying EEG is preserved: there is no interpolation, smoothing or zeroing.
Parameters:
Parameter
Default
Meaning
z
10
detection threshold (robust z-score of the first difference)
a_min
5
minimum pop amplitude relative to the robust SD of the signal
r2_min
0.8
minimum goodness of fit of the exponential model
r
sr(obj) ÷ 2
fitting segment (samples) after the jump; also the minimum distance between pops
window
10
window (seconds) for the robust statistics
Synthetic data
A 10 Hz rhythm (20 μV) with pink noise and two pops: +300 μV with τ = 40 samples (156 ms) at 4.68 s and −200 μV with τ = 20 samples (78 ms) at 7.42 s. The second channel contains the same signal without pops.
Both pops are found at the correct samples, with amplitudes and time constants (in seconds) close to the true values; no pop is found in the clean channel.
Original (blue) and repaired (black) signal:
NeuroAnalyzer.plot(syn, syn_clean; ch ="Pop", seg = (4, 9), gui =false)
The difference between the repaired signal and the original EEG (without pops) shows that only the artifact was removed:
residual = syn_clean.data[1, :, 1] .- eeg_signalplot_signal(t, residual; ylabel ="Residual [μV]", title ="Repaired signal - original EEG")
println("RMS of the residual: ",round(sqrt(mean(abs2, residual)); digits =2), " μV")
RMS of the residual: 2.09 μV
Real EEG
Pops are best detected after removing the DC offset, the power line noise and slow drifts: the power line noise increases the sample-to-sample changes and slow drifts make the level before and after the pop uncertain.
The added pop (at 30 s) is found with its amplitude and time constant; two other pops are already present in this recording (see below).
NeuroAnalyzer.plot(eeg_pop, eeg_rep; ch ="O1", seg = (28, 33), gui =false)
Repaired (black) and original EEG without the added pop (blue): the background activity is preserved. A small difference (up to about 10 μV) remains during the first 0.4 s after the pop, because the fitted amplitude and time constant (154 μV, 0.09 s) differ slightly from the true ones (150 μV, 0.1 s).
NeuroAnalyzer.plot(eeg, eeg_rep; ch ="O1", seg = (29.5, 31.5), gui =false)
Most pops occur in almost all channels at the same time (9.1 s and 62.5 s): these are pops of the reference electrode. The remaining pops are single-channel pops.
At 61.4 s, the raw signal of Fp2 drops by about 5.5 mV and stays clipped at about −5 mV for about 1 s (the amplifier saturates), then recovers over about 2 s. After high-pass filtering, the clipped segment becomes a large slow wave, which the two-pass filter also spreads backward in time (the ramp before 61.4 s). This is not a pop: remove_pops() removes the drop at 61.4 s as a pop of −5.3 mV, but the rest of the artifact does not follow the exponential model, and the “repaired” signal is not better than the original:
# the signal plots scale channels robustly, so the -5 mV step would be# off scale; plotted in μV insteadfp2 =get_channel(eeg; ch ="Fp2")[1]idx =round(Int, 59*sr(eeg)):round(Int, 67*sr(eeg))fig =Figure(size = fig_size)ax =Axis(fig[1, 1]; ax_style..., title ="Fp2", xlabel ="Time [s]", ylabel ="Amplitude [μV]")lines!(ax, eeg.time_pts[idx], eeg.data[fp2, idx, 1]; color = (:blue, 0.5), label ="original")lines!(ax, eeg.time_pts[idx], eeg_clean.data[fp2, idx, 1]; color =:black, label ="repaired")axislegend(ax; position =:rb)fig
Such segments should be inspected and rejected (or the channel interpolated). A large step that does not recover at all within r samples (the fitted τ reaches its upper limit) is not treated as a pop: it is not repaired and remove_pops() shows a warning with its sample number.
Options
Lower detection thresholds (more, smaller pops) and a longer fitting segment (pops with slower recovery):
eeg_clean, pops =remove_pops(eeg; ch ="eeg", z =8, a_min =3, r =sr(eeg))
Accept pops that follow the exponential model less closely:
remove_pops() and remove_pops!() must be applied to a continuous (non-epoched) signal.
Tip: Remove the DC offset, the power line noise and slow drifts (high-pass filter) before removing pops.
Tip: Always inspect the detected pops (plot(obj, obj_clean)): pops of the reference electrode appear in all channels, saturated or flat segments should be rejected instead of repaired.
Tip: If one electrode pops repeatedly, the channel should be marked as bad and interpolated.