| name | spikeinterface-electrophysiology |
| description | Unified Python framework for extracellular electrophysiology. Load 20+ formats (SpikeGLX, OpenEphys, NWB, Intan, Maxwell, Blackrock), preprocess, run 10+ sorters (Kilosort4, SpykingCircus2, Tridesclous, MountainSort5) via one API, compute quality metrics (SNR, ISI, firing rate), compare sorters, export NWB/Phy. For format-agnostic multi-sorter workflows. For Neuropixels-specific PSTH/decoding use neuropixels. |
| license | MIT |
SpikeInterface — Unified Extracellular Electrophysiology Framework
Overview
SpikeInterface provides a common Python API to read extracellular recordings from 20+ file formats, preprocess raw voltage traces, run 10+ spike sorters, postprocess and quality-control sorted units, and export results — all without format-specific code. Its modular design lets users swap sorters, formats, and preprocessing steps without rewriting pipelines. SpikeInterface is built around lazy, chainable objects: a Recording holds raw data, a Sorting holds spike times, and a SortingAnalyzer ties them together for waveform and metric computation.
When to Use
- Loading recordings from multiple acquisition systems (SpikeGLX, OpenEphys, Intan, NWB, Maxwell MEA, Blackrock) with a unified API rather than format-specific parsers
- Running the same preprocessing and sorting pipeline across experiments recorded on different hardware
- Comparing two or more spike sorters on the same recording to assess agreement and choose the best output
- Running containerized sorters (Kilosort, IronClust, MountainSort5) via Docker or Singularity without local installation
- Computing standard quality metrics (SNR, ISI violations, firing rate, presence ratio, amplitude cutoff) and applying threshold-based curation
- Validating spike-sorting accuracy against synthetic or hybrid ground-truth recordings
- Exporting sorted results to NWB for data sharing or to Phy for manual curation
- Use
neuropixels-analysis instead for a complete Neuropixels-specific Kilosort4 workflow including PSTH computation, tuning curves, and population decoding
- For EEG, ECG, or other biosignal processing (not spike sorting), use
neurokit2 instead
Prerequisites
- Python packages:
spikeinterface[full]>=0.101, probeinterface, numpy, matplotlib
- Optional sorter deps:
kilosort (pip), or Docker/Singularity for containerized sorters
- Data requirements: raw binary recording files plus probe geometry (
.prb, .json, or auto-detected from format)
- Hardware: GPU required for Kilosort4; all other sorters run on CPU
pip install "spikeinterface[full]>=0.101" probeinterface
pip install kilosort
pip install phy
Quick Start
import spikeinterface.full as si
import spikeinterface.preprocessing as spre
import spikeinterface.sorters as ss
import spikeinterface.qualitymetrics as sqm
recording = si.read_openephys("/data/session_001", stream_name="Signals CH")
recording_pp = spre.bandpass_filter(
spre.common_reference(recording, reference="global", operator="median"),
freq_min=300, freq_max=6000,
)
sorting = ss.run_sorter("spykingcircus2", recording_pp, output_folder="./sc2_out")
analyzer = si.create_sorting_analyzer(sorting, recording_pp, folder="./analyzer")
analyzer.compute(["random_spikes", "waveforms", "templates", "noise_levels"])
metrics = sqm.compute_quality_metrics(analyzer, metric_names=["snr", "firing_rate", "isi_violation"])
print(metrics.describe())
Core API
Module 1: Recording I/O
SpikeInterface wraps every acquisition format behind a common BaseRecording interface. Once loaded, all objects expose the same methods regardless of origin format.
import spikeinterface.full as si
recording_sglx = si.read_spikeglx("/data/session_001", stream_name="imec0.ap")
recording_oe = si.read_openephys("/data/oe_session", stream_name="Signals CH")
recording_nwb = si.read_nwb_recording("/data/recording.nwb",
electrical_series_name="ElectricalSeries")
recording_intan = si.read_intan("/data/session.rhd", stream_name="RHn")
print(f"Format: {type(recording_sglx).__name__}")
print(f"Channels: {recording_sglx.get_num_channels()}")
print(f"Sampling rate:{recording_sglx.get_sampling_frequency()} Hz")
print(f"Duration: {recording_sglx.get_total_duration():.1f} s")
print(f"Probe: {recording_sglx.get_probe().name}")
streams = si.get_neo_streams("spikeglx", "/data/session_001")
print("Available streams:", streams)
recording_slice = recording_sglx.frame_slice(
start_frame=0,
end_frame=int(60 * recording_sglx.get_sampling_frequency()),
)
print(f"Sliced duration: {recording_slice.get_total_duration():.1f} s")
Module 2: Preprocessing
Preprocessing functions return new Recording objects wrapping the original; the chain is applied lazily when data is read. This keeps memory usage low even for multi-hour recordings.
import spikeinterface.preprocessing as spre
recording_cmr = spre.common_reference(recording_sglx,
reference="global",
operator="median")
recording_filt = spre.bandpass_filter(recording_cmr,
freq_min=300,
freq_max=6000)
recording_clean, removed_ids = spre.remove_bad_channels(recording_filt,
method="coherence+psd")
print(f"Removed {len(removed_ids)} bad channels: {removed_ids}")
print(f"Clean channels: {recording_clean.get_num_channels()}")
recording_white = spre.whiten(recording_clean, mode="local")
recording_shifted = spre.phase_shift(recording_clean)
traces = recording_white.get_traces(start_frame=0, end_frame=3000, segment_index=0)
print(f"Trace snippet shape: {traces.shape}")
print(f"Trace range: [{traces.min():.2f}, {traces.max():.2f}] µV")
Module 3: Spike Sorting
ss.run_sorter() wraps every supported sorter behind a uniform call signature. Sorter-specific parameters are passed as keyword arguments; all other pipeline steps are identical.
import spikeinterface.sorters as ss
from pathlib import Path
available = ss.available_sorters()
print("Available sorters:", available)
installed = ss.installed_sorters()
print("Installed locally:", installed)
sorting_sc2 = ss.run_sorter(
"spykingcircus2",
recording_clean,
output_folder=Path("./sorter_output/sc2"),
remove_existing_folder=True,
verbose=True,
)
print(f"SpykingCircus2 units: {len(sorting_sc2.get_unit_ids())}")
sorting_ks4 = ss.run_sorter(
"kilosort4",
recording_clean,
output_folder=Path("./sorter_output/ks4"),
singularity_image=False,
docker_image=True,
remove_existing_folder=True,
nblocks=5,
Th_learned=8,
do_correction=True,
)
print(f"Kilosort4 units: {len(sorting_ks4.get_unit_ids())}")
sorting_ms5 = ss.run_sorter(
"mountainsort5",
recording_clean,
output_folder=Path("./sorter_output/ms5"),
scheme="2",
detect_threshold=5.5,
)
print(f"MountainSort5 units: {len(sorting_ms5.get_unit_ids())}")
Module 4: Postprocessing (SortingAnalyzer)
SortingAnalyzer is the central postprocessing object in SpikeInterface >= 0.101. It replaces the older WaveformExtractor and provides a unified interface for waveforms, templates, PCAs, and downstream metrics.
import spikeinterface.full as si
import spikeinterface.postprocessing as spost
analyzer = si.create_sorting_analyzer(
sorting_sc2,
recording_clean,
folder="./analyzer_sc2",
format="binary_folder",
overwrite=True,
sparse=True,
ms_before=1.0,
ms_after=2.0,
)
analyzer.compute([
"random_spikes",
"waveforms",
"templates",
"noise_levels",
])
templates = analyzer.get_extension("templates").get_data(outputs="Templates")
print(f"Templates object: {templates}")
print(f"Unit 0 template shape: {templates.get_one_template_dense(0).shape}")
analyzer.compute([
"spike_amplitudes",
"principal_components",
"template_similarity",
"correlograms",
"unit_locations",
])
ext_amp = analyzer.get_extension("spike_amplitudes")
unit_ids = analyzer.unit_ids
amps = ext_amp.get_data()[analyzer.sorting.ids_to_indices([unit_ids[0]])]
print(f"Unit {unit_ids[0]} — median amplitude: {abs(amps).median():.1f} µV")
Module 5: Quality Metrics
Quality metrics summarize unit isolation quality. Metrics requiring only spike times (ISI violations, firing rate) are fast; metrics requiring waveforms (SNR, amplitude cutoff) need the SortingAnalyzer to be populated first.
import spikeinterface.qualitymetrics as sqm
metrics = sqm.compute_quality_metrics(
analyzer,
metric_names=[
"snr",
"isi_violation",
"firing_rate",
"presence_ratio",
"amplitude_cutoff",
"nearest_neighbor",
"silhouette_score",
],
)
print(metrics.head())
print(f"\nShape: {metrics.shape}")
import pandas as pd
thresholds = {
"snr": (">=", 5.0),
"isi_violations_ratio": ("<=", 0.1),
"firing_rate": (">=", 0.1),
"presence_ratio": (">=", 0.9),
"amplitude_cutoff": ("<=", 0.1),
}
keep = pd.Series(True, index=metrics.index)
for col, (op, val) in thresholds.items():
if col not in metrics.columns:
continue
if op == ">=":
keep &= metrics[col] >= val
else:
keep &= metrics[col] <= val
good_unit_ids = metrics[keep].index.tolist()
print(f"Total units: {len(metrics)}")
print(f"Curated units: {len(good_unit_ids)} ({100*len(good_unit_ids)/len(metrics):.0f}%)")
sorting_curated = sorting_sc2.select_units(good_unit_ids)
Module 6: Comparison and Export
Compare sorters against each other or against ground truth, then export results in shareable formats.
import spikeinterface.comparison as sc
comparison = sc.compare_two_sorters(
sorting_sc2,
sorting_ks4,
sorting1_name="SpykingCircus2",
sorting2_name="Kilosort4",
match_score=0.5,
delta_time=0.4,
)
perf = comparison.get_performance(method="by_unit")
print(perf.head(10))
agreement_matrix = comparison.get_agreement_fraction_table()
print(f"Agreement matrix shape: {agreement_matrix.shape}")
import spikeinterface.exporters as sexp
sexp.export_to_nwb(
sorting_curated,
nwb_file_path="./session_sorted.nwb",
overwrite=True,
)
print("Exported to NWB: session_sorted.nwb")
sexp.export_to_phy(
analyzer,
output_folder="./phy_export",
compute_pc_features=True,
copy_binary=True,
remove_if_exists=True,
)
print("Phy export ready at: ./phy_export")
print("Launch Phy with: phy template-gui phy_export/params.py")
Common Workflows
Workflow 1: Multi-Sorter Comparison on OpenEphys Data
Goal: Load an OpenEphys recording, preprocess, run two sorters, compare their agreement, curate the higher-yield output, and export to NWB.
import spikeinterface.full as si
import spikeinterface.preprocessing as spre
import spikeinterface.sorters as ss
import spikeinterface.comparison as sc
import spikeinterface.qualitymetrics as sqm
import spikeinterface.exporters as sexp
from pathlib import Path
data_dir = Path("/data/oe_recording")
streams = si.get_neo_streams("openephys", data_dir)
print("Streams:", streams)
recording = si.read_openephys(data_dir, stream_name="Signals CH")
print(f"Loaded: {recording.get_num_channels()} ch, "
f"{recording.get_sampling_frequency()} Hz, "
f"{recording.get_total_duration():.1f} s")
rec = spre.bandpass_filter(recording, freq_min=300, freq_max=6000)
rec = spre.common_reference(rec, reference="global", operator="median")
rec, bad_ids = spre.remove_bad_channels(rec, method="coherence+psd")
print(f"Preprocessing complete. Removed channels: {bad_ids}")
out = Path("./sorting_outputs")
sorting_sc2 = ss.run_sorter("spykingcircus2", rec,
output_folder=out / "sc2",
remove_existing_folder=True)
sorting_tdc = ss.run_sorter(, rec,
output_folder=out / ,
remove_existing_folder=)
(
)
cmp = sc.compare_two_sorters(sorting_sc2, sorting_tdc,
sorting1_name=,
sorting2_name=,
match_score=)
perf = cmp.get_performance(method=)
()
analyzer = si.create_sorting_analyzer(sorting_sc2, rec,
folder=,
overwrite=, sparse=)
analyzer.compute([, , ,
, ])
metrics = sqm.compute_quality_metrics(
analyzer,
metric_names=[, , ,
, ],
)
keep = (metrics[] >= ) & (metrics[] <= ) \
& (metrics[] >= ) & (metrics[] >= )
sorting_curated = sorting_sc2.select_units(metrics[keep].index.tolist())
()
sexp.export_to_nwb(sorting_curated,
nwb_file_path=,
overwrite=)
()
Workflow 2: Ground Truth Validation with Synthetic Recordings
Goal: Generate a synthetic recording with known spike trains, run a sorter, and measure true accuracy (recall, precision) against ground truth — for benchmarking sorters or testing preprocessing pipelines.
import spikeinterface.full as si
import spikeinterface.preprocessing as spre
import spikeinterface.sorters as ss
import spikeinterface.comparison as sc
import numpy as np
recording_gt, sorting_gt = si.generate_ground_truth_recording(
durations=[120.0],
sampling_frequency=30000.0,
num_channels=32,
num_units=10,
noise_kwargs={"noise_level": 10.0, "dtype": "float32"},
seed=42,
)
print(f"GT recording: {recording_gt.get_num_channels()} ch, "
f"{recording_gt.get_total_duration():.0f} s")
print(f"GT units: {len(sorting_gt.unit_ids)}")
print(f"GT firing rates: "
f"{[round(len(sorting_gt.get_unit_spike_train(u, 0))/120, 1) for u in sorting_gt.unit_ids]} Hz")
rec_pp = spre.bandpass_filter(recording_gt, freq_min=300, freq_max=6000)
rec_pp = spre.common_reference(rec_pp, reference=, operator=)
sorting_sc2 = ss.run_sorter(, rec_pp,
output_folder=,
remove_existing_folder=)
sorting_ms5 = ss.run_sorter(, rec_pp,
output_folder=,
remove_existing_folder=,
scheme=)
name, sorting_test [(, sorting_sc2), (, sorting_ms5)]:
cmp = sc.compare_sorter_to_ground_truth(sorting_gt, sorting_test,
exhaustive_gt=)
perf = cmp.get_performance(method=)
()
()
()
()
()
Workflow 3: Batch Processing Multiple Sessions
Goal: Apply the same preprocessing + sorting pipeline to multiple recording sessions and collect quality metrics across all sessions.
import spikeinterface.full as si
import spikeinterface.preprocessing as spre
import spikeinterface.sorters as ss
import spikeinterface.qualitymetrics as sqm
import pandas as pd
from pathlib import Path
sessions = list(Path("/data/experiment").glob("session_*/"))
all_metrics = []
for session_dir in sessions:
print(f"Processing {session_dir.name} ...")
try:
streams = si.get_neo_streams("spikeglx", session_dir)
ap_stream = [s for s in streams if "ap" in s][0]
rec = si.read_spikeglx(session_dir, stream_name=ap_stream)
rec = spre.bandpass_filter(
spre.common_reference(rec, reference="global", operator="median"),
freq_min=300, freq_max=6000,
)
rec, _ = spre.remove_bad_channels(rec)
out_dir = session_dir / "sorting"
sorting = ss.run_sorter("spykingcircus2", rec,
output_folder=out_dir,
remove_existing_folder=True)
analyzer = si.create_sorting_analyzer(
sorting, rec, folder=session_dir / "analyzer", overwrite=True, sparse=True
)
analyzer.compute([, , ,
, ])
m = sqm.compute_quality_metrics(
analyzer, metric_names=[, , ]
)
m[] = session_dir.name
all_metrics.append(m)
Exception e:
()
combined = pd.concat(all_metrics)
combined.to_csv()
()
(combined.groupby()[[, ]].median())
Key Parameters
| Parameter | Module / Function | Default | Range / Options | Effect |
|---|
freq_min / freq_max | spre.bandpass_filter | 300 / 6000 Hz | 150–500 / 3000–10000 Hz | Spike band; use 300–6000 Hz for AP activity |
reference | spre.common_reference | "global" | "global", "local", "single" | Channel subset used for median reference subtraction |
method | spre.remove_bad_channels | "coherence+psd" | "coherence+psd", "std", "mad" | Algorithm for bad channel detection |
scheme | ss.run_sorter("mountainsort5") | "2" | "1", "2", "3" | Sorting scheme; scheme 2 recommended for high-density probes |
nblocks | ss.run_sorter("kilosort4") | 5 | 0–10 | Number of drift correction blocks; 0 disables drift correction |
Th_learned | ss.run_sorter("kilosort4") | 8 | 6–12 | Detection threshold (× noise); lower = more units, more noise |
match_score | sc.compare_two_sorters | 0.5 | 0.1–0.9 | Minimum spike-train overlap to declare a unit match |
sparse | si.create_sorting_analyzer | True | True, False | Limit waveform extraction to channels near each unit; reduces memory |
Best Practices
-
Always inspect available streams before loading: Different acquisition systems save AP data, LFP data, and auxiliary channels as separate streams. Loading the wrong stream silently yields valid-looking but incorrect data.
streams = si.get_neo_streams("spikeglx", data_dir)
print(streams)
recording = si.read_spikeglx(data_dir, stream_name="imec0.ap")
-
Chain preprocessing lazily; do not load to memory early: Preprocessing objects are lazy and apply transformations at read time. Calling get_traces() on the raw recording before preprocessing will load unfiltered data into RAM unnecessarily. Build the full chain before any data access.
-
Use sparse=True when creating a SortingAnalyzer: For high-channel-count probes (64–384 channels), dense waveform extraction is 10–50× more expensive in RAM and disk than sparse. Sparse mode extracts waveforms only on the channels nearest each unit.
-
Run containerized sorters to avoid dependency conflicts: Kilosort2/3 (MATLAB), IronClust, and other sorters have complex dependencies. Use docker_image=True in run_sorter() to pull the official container and run the sorter in isolation:
sorting = ss.run_sorter("kilosort2_5", recording_clean,
output_folder="./ks25_out",
docker_image=True)
-
Compute metrics extensions in dependency order: Extensions depend on each other. The canonical order is: random_spikes → waveforms → templates → noise_levels → spike_amplitudes → principal_components. Skipping an earlier step causes a MissingExtensionError when a downstream step is requested.
-
Save the SortingAnalyzer to disk for large recordings: In-memory analyzers (format="memory") are lost when the process exits. For recordings longer than 30 minutes or with many units, always specify a folder path so the analyzer can be reloaded:
Common Recipes
Recipe: Load and Inspect a Multi-Stream Recording
When to use: Quickly check what streams are available in an unfamiliar recording and confirm channel counts and duration before committing to a full sort.
import spikeinterface.full as si
data_dir = "/data/recording_session"
try:
streams = si.get_neo_streams("spikeglx", data_dir)
fmt = "spikeglx"
except Exception:
streams = si.get_neo_streams("openephys", data_dir)
fmt = "openephys"
print(f"Format: {fmt}")
print(f"Streams: {streams}")
for stream in streams:
try:
rec = si.read_spikeglx(data_dir, stream_name=stream) if fmt == "spikeglx" \
else si.read_openephys(data_dir, stream_name=stream)
print(f" {stream}: {rec.get_num_channels()} ch, "
f"{rec.get_sampling_frequency()} Hz, "
f"{rec.get_total_duration():.1f} s")
except Exception as e:
print(f" {stream}: could not load ({e})")
Recipe: Export Quality Metrics Report to CSV
When to use: After running quality metrics, save a tidy CSV summarizing all units with their metrics and a pass/fail column for downstream analysis or sharing with collaborators.
import spikeinterface.qualitymetrics as sqm
import pandas as pd
metrics = sqm.compute_quality_metrics(
analyzer,
metric_names=["snr", "firing_rate", "isi_violation",
"presence_ratio", "amplitude_cutoff"],
)
metrics["pass_qc"] = (
(metrics["snr"] >= 5) &
(metrics["isi_violations_ratio"] <= 0.1) &
(metrics["firing_rate"] >= 0.1) &
(metrics["presence_ratio"] >= 0.9) &
(metrics["amplitude_cutoff"] <= 0.1)
)
metrics.to_csv("unit_quality_metrics.csv")
n_pass = metrics["pass_qc"].sum()
print(f"QC report saved: {len(metrics)} total units, {n_pass} pass ({100*n_pass/len(metrics):.0f}%)")
print(metrics[metrics["pass_qc"]].describe())
Recipe: Probe Geometry Visualization
When to use: Verify that the probe channel map loaded correctly before sorting. Incorrect channel maps silently degrade sorting quality on high-density probes.
import spikeinterface.full as si
import matplotlib.pyplot as plt
import probeinterface.plotting as pp
recording = si.read_spikeglx("/data/session_001", stream_name="imec0.ap")
probe = recording.get_probe()
print(f"Probe name: {probe.name}")
print(f"N contacts: {probe.get_contact_count()}")
print(f"Contact positions (first 5):\n{probe.contact_positions[:5]}")
fig, ax = plt.subplots(figsize=(3, 10))
pp.plot_probe(probe, ax=ax, with_channel_index=True)
ax.set_title(f"{probe.name} — channel map")
plt.tight_layout()
plt.savefig("probe_geometry.png", dpi=150)
print("Saved probe_geometry.png")
Troubleshooting
| Problem | Cause | Solution |
|---|
ValueError: stream_name not found | Recording has multiple streams; none is specified | Run si.get_neo_streams(format, path) to list available streams; pass the correct one to the reader |
| Sorter output has zero units | Detection threshold too high, or preprocessing removed all signal | Verify recording_clean.get_traces() returns non-zero data; lower detection threshold (e.g. Th_learned=6 for Kilosort4) |
MissingExtensionError | Analyzer extension depends on an uncomputed prerequisite | Follow the canonical compute order: random_spikes → waveforms → templates → noise_levels → spike_amplitudes |
| Docker sorter hangs at startup | Docker daemon not running or image not pulled | Run docker ps to confirm Docker is running; pull image manually with docker pull spikeinterface/kilosort4-compiled-base |
MemoryError during waveform extraction | Dense extraction on high-channel-count probe | Use sparse=True in create_sorting_analyzer; reduce max_spikes_per_unit (default 500) |
| Bad channel detection removes too many channels | Threshold too aggressive or short recording | Set method="std" for a simpler threshold; increase bad_threshold parameter |
| Unit comparison shows 0% agreement between sorters | Delta time window too narrow or match score too strict | Increase delta_time (default 0.4 ms) and lower match_score (try 0.3) |
NWB export raises TypeError on unit properties | Sorting contains non-serializable properties from sorter | Remove problematic properties: sorting.remove_unit_property("property_name") before export |
read_spikeglx fails on LF stream | LFP stream uses different file suffix (.lf.bin) | Specify explicitly; confirm file exists with |
Related Skills
- neuropixels-analysis — Neuropixels-specific pipeline using SpikeInterface + Kilosort4 with PSTH, tuning curves, and population decoding for rodent and primate experiments
- neurokit2 — For biosignal processing (ECG, EEG, EDA, EMG, PPG) rather than spike sorting; use when data is not extracellular electrophysiology
References