mcpField
N
neuropixels-analysis

neuropixels-analysis

github.com/jaechang-hitsデータ・データベース38467 回インストール1 週間前に更新
リポジトリ

neuropixels-analysis について

このスキルは、SpikeInterfaceなどのPythonツールを用いたNeuropixels細胞外記録の解析を研究者向けに案内します。スパイクソーティング、ユニット品質評価、イベント同期・集団解析、NWBへのエクスポートを扱います。

主な機能

  1. 01SpikeGLX、OpenEphys、NWBデータの読み込み
  2. 02記録の前処理とスパイクソーティング
  3. 03ユニット品質指標の算出
  4. 04PSTHと集団コーディングの解析
  5. 05ソート結果のNWBエクスポート

活用シーン

  1. 01生記録から単一ユニット活動を抽出
  2. 02品質指標によるソート済みユニットの選別
  3. 03同一記録で複数ソーターを比較
  4. 04共有用にキュレーション結果を準備

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/jaechang-hits/sciagent-skills --skill neuropixels-analysis

インストール後、Claude Code で/neuropixels-analysisを入力して読み込まれたか確認します。

詳細

Neuropixels 解析

概要

Neuropixels プローブは、30 kHz で同時記録された 384(NP 1.0)または 192(NP 2.0)チャネルから細胞外電位を記録します。解析は標準的なパイプラインに従います:生データ → スパイクソーティング(Kilosort)→ 品質キュレーション → ユニット解析。SpikeInterface(Python)は、10 種類以上のスパイクソーターに対応する統一 API を提供し、複数形式(SpikeGLX、OpenEphys、NWB)からのデータ読み込み、品質指標の計算、ソート結果のエクスポートを処理します。ProbeInterface はプローブのジオメトリとチャネルマップを管理します。ソート後の解析(PSTH、発火率、デコーディング)には、標準的な Python の科学技術スタックを使用します。

使用する場面

  • SpikeGLX(.bin)または OpenEphys(.dat)の記録をスパイクソーティングして、単一ユニット活動を抽出する
  • 自動品質管理指標(ISI 違反、SNR、発火率)を適用して、ソート済みユニットをキュレーションする
  • 実験イベントに合わせた刺激周辺時間ヒストグラム(PSTH)を計算する
  • 集団コーディングを解析する:発火率から刺激変数または行動変数をデコードする
  • ソート済みデータを共有用に NWB(Neurodata Without Borders)形式に変換する
  • 手法を検証するため、同じデータセットで複数のスパイクソーターを比較する
  • ユニットの波形、自己相関ヒストグラム、プローブチャネル上の空間分布を可視化する
  • 共通 API と比較ツールを備え、10 種類以上のスパイクソーターに対応する統一フレームワークが必要な場合は、代わりに SpikeInterface を使用する

前提条件

  • Python パッケージ:spikeinterface[full]、probeinterface、numpy、pandas、matplotlib、scipy
  • スパイクソーター:Kilosort4(Python、pip install kilosort)、または MATLAB ベースの Kilosort2/3(MATLAB ライセンスが必要)
  • データ要件:生の .bin(SpikeGLX)または .dat(OpenEphys)記録、プローブチャネルマップファイル(.prb または ProbeInterface から取得)
  • ハードウェア:Kilosort には NVIDIA GPU(VRAM 4 GB 以上)が必要です。CPU による代替処理も可能ですが、約 10 倍遅くなります
pip install spikeinterface[full] probeinterface kilosort
# Optional: Phy for manual curation
pip install phy

ワークフロー

ステップ 1:生記録を読み込む
import spikeinterface.full as si
from pathlib import Path

# SpikeGLX recording (most common Neuropixels format)
data_dir = Path("/data/recording/session_001")
recording = si.read_spikeglx(data_dir, stream_name="imec0.ap")

print(f"Probe type:       {recording.get_probe().name}")
print(f"Channels:         {recording.get_num_channels()}")
print(f"Sampling rate:    {recording.get_sampling_frequency()} Hz")
print(f"Duration:         {recording.get_total_duration():.1f} s")
print(f"Total samples:    {recording.get_total_samples()}")
ステップ 2:共通リファレンスとバンドパスフィルターを適用する
import spikeinterface.preprocessing as spre

# Common median reference (removes common noise across all channels)
recording_cmr = spre.common_reference(recording, reference="global", operator="median")

# Bandpass filter: 300–6000 Hz for spikes
recording_filt = spre.bandpass_filter(recording_cmr, freq_min=300, freq_max=6000)

# Remove bad channels automatically
recording_clean, removed_ids = spre.remove_bad_channels(recording_filt)
print(f"Removed {len(removed_ids)} bad channels: {removed_ids}")
print(f"Clean recording: {recording_clean.get_num_channels()} channels")
ステップ 3:スパイクソーティングを実行する
import spikeinterface.sorters as ss
from pathlib import Path

output_dir = Path("./sorting_output")

# Kilosort4 (recommended for Neuropixels; requires GPU)
sorting = ss.run_sorter(
    "kilosort4",
    recording_clean,
    output_folder=output_dir / "kilosort4",
    remove_existing_folder=True,
    verbose=True,
    # Kilosort4 parameters
    nblocks=5,          # number of drift correction blocks
    Th_learned=8,       # threshold for learned templates
    do_correction=True, # drift correction
)

print(f"Units found: {len(sorting.get_unit_ids())}")
print(f"Spike counts (first 5): {[len(sorting.get_unit_spike_train(u, segment_index=0)) for u in sorting.unit_ids[:5]]}")
ステップ 4:波形と品質指標を計算する
import spikeinterface.full as si
import spikeinterface.qualitymetrics as sqm

# Extract waveforms (snippets around each spike)
waveforms = si.extract_waveforms(
    recording_clean,
    sorting,
    folder="./waveforms",
    ms_before=1.0,      # ms before spike peak
    ms_after=2.0,       # ms after spike peak
    max_spikes_per_unit=1000,
    overwrite=True,
    n_jobs=4,
)

print(f"Waveform shape per unit: {waveforms.get_waveforms(waveforms.unit_ids[0]).shape}")
# (n_spikes, n_samples, n_channels)

# Compute quality metrics
metrics = sqm.compute_quality_metrics(
    waveforms,
    metric_names=["snr", "isi_violation", "firing_rate", "presence_ratio",
                  "amplitude_cutoff", "nearest_neighbor"],
)
print(f"\nQuality metrics summary:")
print(metrics.describe())
ステップ 5:ユニットをキュレーションする
import pandas as pd

# Curation thresholds (Allen Brain Institute defaults)
thresholds = {
    "snr":                (5.0, None),   # SNR ≥ 5
    "isi_violations_ratio": (None, 0.1), # ISI violation ratio ≤ 10%
    "firing_rate":        (0.1, None),   # firing rate ≥ 0.1 Hz
    "presence_ratio":     (0.9, None),   # present ≥ 90% of recording
    "amplitude_cutoff":   (None, 0.1),   # amplitude cutoff ≤ 10%
}

def apply_thresholds(metrics_df, thresholds):
    mask = pd.Series(True, index=metrics_df.index)
    for metric, (low, high) in thresholds.items():
        if metric not in metrics_df.columns:
            continue
        if low is not None:
            mask &= metrics_df[metric] >= low
        if high is not None:
            mask &= metrics_df[metric] <= high
    return mask

good_units_mask = apply_thresholds(metrics, thresholds)
good_unit_ids = metrics[good_units_mask].index.tolist()

print(f"Total units:   {len(metrics)}")
print(f"Good units:    {len(good_unit_ids)} ({100*len(good_unit_ids)/len(metrics):.0f}%)")

# Filter sorting to good units only
sorting_curated = sorting.select_units(good_unit_ids)
ステップ 6:PSTH を計算して可視化する
import numpy as np
import matplotlib.pyplot as plt

def compute_psth(spike_times, event_times, window=(-0.5, 1.0), bin_size=0.01, fs=30000):
    """Compute peri-stimulus time histogram for one unit."""
    bins = np.arange(window[0], window[1] + bin_size, bin_size)
    spike_times_s = spike_times / fs  # samples → seconds
    counts = np.zeros(len(bins) - 1)
    for t_event in event_times:
        rel_times = spike_times_s - t_event
        in_window = rel_times[(rel_times >= window[0]) & (rel_times < window[1])]
        counts += np.histogram(in_window, bins=bins)[0]
    rate = counts / (len(event_times) * bin_size)  # convert to Hz
    return bins[:-1] + bin_size / 2, rate  # bin centers, firing rate

# Example: visual stimulus events at 1.0, 2.5, 4.0, 5.5 s
fs = 30000
event_times_s = np.array([1.0, 2.5, 4.0, 5.5, 7.0, 8.5])

# Plot PSTH for first 4 good units
fig, axes = plt.subplots(2, 2, figsize=(10, 6))
for ax, unit_id in zip(axes.flat, good_unit_ids[:4]):
    spikes = sorting_curated.get_unit_spike_train(unit_id, segment_index=0)
    times, rate = compute_psth(spikes, event_times_s, fs=fs)
    ax.bar(times, rate, width=0.01, color="steelblue", alpha=0.8)
    ax.axvline(0, color="red", lw=1.5, linestyle="--", label="Stimulus")
    ax.set_xlabel("Time from stimulus (s)")
    ax.set_ylabel("Firing rate (Hz)")
    ax.set_title(f"Unit {unit_id}")

plt.tight_layout()
plt.savefig("psth_grid.pdf", bbox_inches="tight")
print("Saved psth_grid.pdf")
ステップ 7:NWB にエクスポートする
import spikeinterface.exporters as sexp

# Export sorted data to Neurodata Without Borders format
nwb_path = "./recording_sorted.nwb"
sexp.export_to_nwb(
    sorting_curated,
    nwb_file_path=nwb_path,
    overwrite=True,
)
print(f"Exported to NWB: {nwb_path}")

# Also export waveforms for Phy manual curation
phy_dir = "./phy_export"
sexp.export_to_phy(
    waveforms,
    output_folder=phy_dir,
    compute_pc_features=True,
    copy_binary=True,
)
print(f"Phy export ready at: {phy_dir}")
print("Launch with: phy template-gui phy_export/params.py")

主なパラメーター

パラメーターモジュール/関数デフォルト範囲/オプション効果
freq_min / freq_maxbandpass_filter300/6000 Hz150–500 / 3000–10000スパイク帯域です。NP 1.0 の標準は 300–6000 Hz です
nblocksKilosort450–10ドリフト補正ブロックの数です。0 にするとドリフト補正が無効になります
Th_learnedKilosort486–12検出しきい値(× ノイズレベル)です。値を下げるとスパイク数が増える一方、ノイズも増えます
ms_before / ms_afterextract_waveforms1.0/2.00.5–2.0/1.0–3.0 msスパイクピーク前後の波形スニペット区間です
max_spikes_per_unitextract_waveforms500100–5000波形抽出に用いるユニットあたりの最大スパイク数です
snr しきい値品質指標—5–10ユニット採用のための信号対雑音比のしきい値です
isi_violations_ratio品質指標—0.05–0.2不応期違反率です。<0.1 なら十分に分離されています
presence_ratio品質指標—0.9ユニットが活動している記録時間の割合です
bin_sizePSTH0.01 s0.001–0.05 sPSTH の時間分解能です。値を小さくすると詳細度が上がりますが、ノイズも増えます

主な概念

スパイクソーティングパイプライン

電圧の生データ → 前処理(共通リファレンス、バンドパス)→ 検出(しきい値交差または学習済みテンプレート)→ クラスタリング(PCA + k-means またはガウス混合)→ テンプレートマッチング → 品質指標 → キュレーション。Kilosort4 ではドリフト補正が追加されており、慢性 NP 記録では非常に重要です(数時間で先端が 5–50 µm ドリフトすると、ユニットの分離性能が低下します)。

品質指標
  • SNR:波形のピーク振幅/ノイズレベルです。SNR >5 は、ユニットが良好に分離されていることを示します
  • ISI 違反率:不応期(約 1.5 ms)より短いスパイク間隔の割合です。比率 <0.1 は、単一ユニットが分離されていることを示します
  • 存在率:ユニットが少なくとも 1 回スパイクを発火した記録エポックの割合です。<0.9 は、ユニットが消失または失われた可能性を示します
  • 振幅カットオフ:検出しきい値によって検出されなかったスパイクの割合です。<0.1 であれば、ほぼ完全な検出が保証されます

よく使うレシピ

レシピ:同じ記録で 2 つのソーターを比較する
import spikeinterface.sorters as ss
import spikeinterface.comparison as sc

# Run two sorters
sorting_ks4 = ss.run_sorter("kilosort4", recording_clean, output_folder="./ks4")
sorting_sc   = ss.run_sorter("spykingcircus2", recording_clean, output_folder="./sc2")

# Compare outputs
comparison = sc.compare_two_sorters(sorting_ks4, sorting_sc,
                                     sorting1_name="Kilosort4",
                                     sorting2_name="SpykingCircus2")
print(comparison.get_performance())
# Shows: agreement fraction, recall, precision per matched unit pair
レシピ:集団発火率ヒートマップ
import numpy as np
import matplotlib.pyplot as plt

fs = 30000
duration_s = sorting_curated.get_total_duration()
bin_edges = np.arange(0, duration_s, 0.05)  # 50 ms bins

# Build firing rate matrix: (n_units, n_bins)
fr_matrix = np.zeros((len(good_unit_ids), len(bin_edges)-1))
for i, uid in enumerate(good_unit_ids[:50]):
    spikes_s = sorting_curated.get_unit_spike_train(uid, segment_index=0) / fs
    counts, _ = np.histogram(spikes_s, bins=bin_edges)
    fr_matrix[i] = counts / 0.05  # Hz

# Normalize each unit
fr_norm = (fr_matrix - fr_matrix.mean(1, keepdims=True)) / (fr_matrix.std(1, keepdims=True) + 1e-6)

fig, ax = plt.subplots(figsize=(12, 5))
im = ax.imshow(fr_norm, aspect="auto", cmap="RdBu_r",
               extent=[0, duration_s, len(good_unit_ids[:50]), 0], vmin=-2, vmax=2)
ax.set_xlabel("Time (s)")
ax.set_ylabel("Unit #")
ax.set_title("Population Activity Heatmap (z-scored FR)")
plt.colorbar(im, ax=ax, label="z-score")
plt.tight_layout()
plt.savefig("population_heatmap.pdf", bbox_inches="tight")

出力例

出力形式一般的な内容
sorting/SpikeInterface フォルダーユニットごとのスパイク時刻、テンプレート波形
waveforms/SpikeInterface フォルダー波形スニペット (n_spikes, n_samples, n_channels)
quality_metrics.csvCSVユニットごとの SNR、ISI 違反、発火率、存在率
psth_grid.pdfPDFキュレーション済みユニットの PSTH プロット
recording_sorted.nwbNWB/HDF5共有用のポータブルなスパイクソート済みデータ
phy_export/Phy 形式手動キュレーション GUI 用の .npy + params.py

トラブルシューティング

問題原因解決策
Kilosort の GPU メモリエラー記録が長すぎる、または VRAM に対してチャンネル数が多すぎるsi.SubRecording を使って時間チャンクごとに記録を処理します。NP 2.0(192 ch)の設定を使用します
ユニットが 0 個見つかるしきい値が高すぎる、または前処理ですべての信号が除去されたTh_learned を 6 に下げます。recording_clean のチャンネルデータがゼロでないことを確認します
ISI 違反が過剰(>50%)マルチユニット活動が単一ユニットとして統合されたPhy で手動キュレーションを行います。より保守的になるよう Th_learned を上げます
波形抽出で OOM が発生するスパイク数 × チャンネル数 × サンプル数が多すぎるmax_spikes_per_unit を減らします。並列抽出のために n_jobs を増やします
ドリフト補正に失敗するスパイクが少なすぎる、または記録が非常に短いnblocks=1(最小限の補正)または nblocks=0(無効化)に設定します
NWB エクスポートに失敗するセッションメタデータ(被験体、日付)が不足しているNWBFile メタデータを指定します。または sexp.export_to_nwb(..., metadata={...}) を使用します
SpikeGLX ファイルが見つからないストリーム名が正しくないストリームを一覧表示します:si.get_neo_streams("spikeglx", data_dir)

参考資料

コメント