Skip to content

Commit 54c8413

Browse files
authored
Merge pull request #87 from ttngu207/main
QC metrics
2 parents efedd37 + 7ce35ab commit 54c8413

File tree

4 files changed

+145
-1
lines changed

4 files changed

+145
-1
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
Observes [Semantic Versioning](https://semver.org/spec/v2.0.0.html) standard and
44
[Keep a Changelog](https://keepachangelog.com/en/1.0.0/) convention.
55

6+
## [0.1.5] - 2022-07-11
7+
8+
+ Add - New `QualityMetrics` table to store clusters' and waveforms' metrics after the spike sorting analysis.
9+
610
## [0.1.4] - 2022-07-11
711

812
+ Bugfix - Handle case where `spike_depths` data is present.

element_array_ephys/ephys_acute.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import numpy as np
55
import inspect
66
import importlib
7+
import pandas as pd
8+
79
from element_interface.utils import find_root_directory, find_full_path, dict_to_uuid
810

911
from .readers import spikeglx, kilosort, openephys
@@ -662,6 +664,74 @@ def yield_unit_waveforms():
662664
self.Waveform.insert(unit_electrode_waveforms, ignore_extra_fields=True)
663665

664666

667+
@schema
668+
class QualityMetrics(dj.Imported):
669+
definition = """
670+
# Clusters and waveforms metrics
671+
-> CuratedClustering
672+
"""
673+
674+
class Cluster(dj.Part):
675+
definition = """
676+
# Cluster metrics for a particular unit
677+
-> master
678+
-> CuratedClustering.Unit
679+
---
680+
firing_rate=null: float # (Hz) firing rate for a unit
681+
snr=null: float # signal-to-noise ratio for a unit
682+
presence_ratio=null: float # fraction of time in which spikes are present
683+
isi_violation=null: float # rate of ISI violation as a fraction of overall rate
684+
number_violation=null: int # total number of ISI violations
685+
amplitude_cutoff=null: float # estimate of miss rate based on amplitude histogram
686+
isolation_distance=null: float # distance to nearest cluster in Mahalanobis space
687+
l_ratio=null: float #
688+
d_prime=null: float # Classification accuracy based on LDA
689+
nn_hit_rate=null: float # Fraction of neighbors for target cluster that are also in target cluster
690+
nn_miss_rate=null: float # Fraction of neighbors outside target cluster that are in target cluster
691+
silhouette_score=null: float # Standard metric for cluster overlap
692+
max_drift=null: float # Maximum change in spike depth throughout recording
693+
cumulative_drift=null: float # Cumulative change in spike depth throughout recording
694+
contamination_rate=null: float #
695+
"""
696+
697+
class Waveform(dj.Part):
698+
definition = """
699+
# Waveform metrics for a particular unit
700+
-> master
701+
-> CuratedClustering.Unit
702+
---
703+
amplitude: float # (uV) absolute difference between waveform peak and trough
704+
duration: float # (ms) time between waveform peak and trough
705+
halfwidth=null: float # (ms) spike width at half max amplitude
706+
pt_ratio=null: float # absolute amplitude of peak divided by absolute amplitude of trough relative to 0
707+
repolarization_slope=null: float # the repolarization slope was defined by fitting a regression line to the first 30us from trough to peak
708+
recovery_slope=null: float # the recovery slope was defined by fitting a regression line to the first 30us from peak to tail
709+
spread=null: float # (um) the range with amplitude above 12% of the maximum amplitude along the probe
710+
velocity_above=null: float # (s/m) inverse velocity of waveform propagation from the soma toward the top of the probe
711+
velocity_below=null: float # (s/m) inverse velocity of waveform propagation from the soma toward the bottom of the probe
712+
"""
713+
714+
def make(self, key):
715+
output_dir = (ClusteringTask & key).fetch1('clustering_output_dir')
716+
kilosort_dir = find_full_path(get_ephys_root_data_dir(), output_dir)
717+
718+
metric_fp = kilosort_dir / 'metrics.csv'
719+
720+
if not metric_fp.exists():
721+
raise FileNotFoundError(f'QC metrics file not found: {metric_fp}')
722+
723+
metrics_df = pd.read_csv(metric_fp)
724+
metrics_df.set_index('cluster_id', inplace=True)
725+
726+
metrics_list = [
727+
dict(metrics_df.loc[unit_key['unit']], **unit_key)
728+
for unit_key in (CuratedClustering.Unit & key).fetch('KEY')]
729+
730+
self.insert1(key)
731+
self.Cluster.insert(metrics_list, ignore_extra_fields=True)
732+
self.Waveform.insert(metrics_list, ignore_extra_fields=True)
733+
734+
665735
# ---------------- HELPER FUNCTIONS ----------------
666736

667737
def get_spikeglx_meta_filepath(ephys_recording_key):

element_array_ephys/ephys_chronic.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import numpy as np
55
import inspect
66
import importlib
7+
import pandas as pd
8+
79
from element_interface.utils import find_root_directory, find_full_path, dict_to_uuid
810

911
from .readers import spikeglx, kilosort, openephys
@@ -661,6 +663,74 @@ def yield_unit_waveforms():
661663
self.Waveform.insert(unit_electrode_waveforms, ignore_extra_fields=True)
662664

663665

666+
@schema
667+
class QualityMetrics(dj.Imported):
668+
definition = """
669+
# Clusters and waveforms metrics
670+
-> CuratedClustering
671+
"""
672+
673+
class Cluster(dj.Part):
674+
definition = """
675+
# Cluster metrics for a particular unit
676+
-> master
677+
-> CuratedClustering.Unit
678+
---
679+
firing_rate=null: float # (Hz) firing rate for a unit
680+
snr=null: float # signal-to-noise ratio for a unit
681+
presence_ratio=null: float # fraction of time in which spikes are present
682+
isi_violation=null: float # rate of ISI violation as a fraction of overall rate
683+
number_violation=null: int # total number of ISI violations
684+
amplitude_cutoff=null: float # estimate of miss rate based on amplitude histogram
685+
isolation_distance=null: float # distance to nearest cluster in Mahalanobis space
686+
l_ratio=null: float #
687+
d_prime=null: float # Classification accuracy based on LDA
688+
nn_hit_rate=null: float # Fraction of neighbors for target cluster that are also in target cluster
689+
nn_miss_rate=null: float # Fraction of neighbors outside target cluster that are in target cluster
690+
silhouette_score=null: float # Standard metric for cluster overlap
691+
max_drift=null: float # Maximum change in spike depth throughout recording
692+
cumulative_drift=null: float # Cumulative change in spike depth throughout recording
693+
contamination_rate=null: float #
694+
"""
695+
696+
class Waveform(dj.Part):
697+
definition = """
698+
# Waveform metrics for a particular unit
699+
-> master
700+
-> CuratedClustering.Unit
701+
---
702+
amplitude: float # (uV) absolute difference between waveform peak and trough
703+
duration: float # (ms) time between waveform peak and trough
704+
halfwidth=null: float # (ms) spike width at half max amplitude
705+
pt_ratio=null: float # absolute amplitude of peak divided by absolute amplitude of trough relative to 0
706+
repolarization_slope=null: float # the repolarization slope was defined by fitting a regression line to the first 30us from trough to peak
707+
recovery_slope=null: float # the recovery slope was defined by fitting a regression line to the first 30us from peak to tail
708+
spread=null: float # (um) the range with amplitude above 12% of the maximum amplitude along the probe
709+
velocity_above=null: float # (s/m) inverse velocity of waveform propagation from the soma toward the top of the probe
710+
velocity_below=null: float # (s/m) inverse velocity of waveform propagation from the soma toward the bottom of the probe
711+
"""
712+
713+
def make(self, key):
714+
output_dir = (ClusteringTask & key).fetch1('clustering_output_dir')
715+
kilosort_dir = find_full_path(get_ephys_root_data_dir(), output_dir)
716+
717+
metric_fp = kilosort_dir / 'metrics.csv'
718+
719+
if not metric_fp.exists():
720+
raise FileNotFoundError(f'QC metrics file not found: {metric_fp}')
721+
722+
metrics_df = pd.read_csv(metric_fp)
723+
metrics_df.set_index('cluster_id', inplace=True)
724+
725+
metrics_list = [
726+
dict(metrics_df.loc[unit_key['unit']], **unit_key)
727+
for unit_key in (CuratedClustering.Unit & key).fetch('KEY')]
728+
729+
self.insert1(key)
730+
self.Cluster.insert(metrics_list, ignore_extra_fields=True)
731+
self.Waveform.insert(metrics_list, ignore_extra_fields=True)
732+
733+
664734
# ---------------- HELPER FUNCTIONS ----------------
665735

666736
def get_spikeglx_meta_filepath(ephys_recording_key):

element_array_ephys/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
"""Package metadata."""
2-
__version__ = '0.1.4'
2+
__version__ = '0.1.5'

0 commit comments

Comments
 (0)