Skip to content

Commit 89eb899

Browse files
committed
Expose agreement_method ark in basepairsortercomparison
1 parent 050e5f9 commit 89eb899

5 files changed

Lines changed: 120 additions & 68 deletions

File tree

src/spikeinterface/comparison/collision.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ class CollisionGTComparison(GroundTruthComparison):
1515
1616
Parameters
1717
----------
18-
gt_sorting : SortingExtractor
18+
gt_sorting : BaseSorting
1919
The first sorting for the comparison
2020
collision_lag : float, default 2.0
2121
Collision lag in ms.
22-
tested_sorting : SortingExtractor
22+
tested_sorting : BaseSorting
2323
The second sorting for the comparison
2424
nbins : int, default : 11
2525
Number of collision bins

src/spikeinterface/comparison/comparisontools.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def do_count_event(sorting):
7272
7373
Parameters
7474
----------
75-
sorting : SortingExtractor
75+
sorting : BaseSorting
7676
A sorting extractor
7777
7878
Returns
@@ -345,9 +345,9 @@ def make_agreement_scores(
345345
346346
Parameters
347347
----------
348-
sorting1 : SortingExtractor
348+
sorting1 : BaseSorting
349349
The first sorting extractor
350-
sorting2 : SortingExtractor
350+
sorting2 : BaseSorting
351351
The second sorting extractor
352352
delta_frames : int
353353
Number of frames to consider spikes coincident
@@ -549,9 +549,9 @@ def do_score_labels(sorting1, sorting2, delta_frames, unit_map12, label_misclass
549549
550550
Parameters
551551
----------
552-
sorting1 : SortingExtractor instance
552+
sorting1 : BaseSorting
553553
The ground truth sorting
554-
sorting2 : SortingExtractor instance
554+
sorting2 : BaseSorting
555555
The tested sorting
556556
delta_frames : int
557557
Number of frames to consider spikes coincident
@@ -902,7 +902,7 @@ def make_collision_events(sorting, delta):
902902
903903
Parameters
904904
----------
905-
sorting : SortingExtractor
905+
sorting : BaseSorting
906906
The sorting extractor object for counting collision events
907907
delta : int
908908
Number of frames for considering collision events
@@ -1297,9 +1297,10 @@ def compute_distance_matrix(sorting1: BaseSorting, sorting2: BaseSorting, delta_
12971297
squared_norm_1_vector[:, np.newaxis] + squared_norm_2_vector[np.newaxis, :] - 2 * dot_product_matrix
12981298
)
12991299

1300-
distance_metrix = np.sqrt(squared_distance_matrix)
1300+
distance_matrix = np.sqrt(squared_distance_matrix)
1301+
agreement_matrix = 1 / ((distance_matrix**2 / dot_product_matrix) + 1)
13011302

1302-
return distance_metrix
1303+
return distance_matrix, agreement_matrix
13031304

13041305

13051306
def calculate_generalized_comparison_metrics(

src/spikeinterface/comparison/correlogram.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ class CorrelogramGTComparison(GroundTruthComparison):
1717
1818
Parameters
1919
----------
20-
gt_sorting : SortingExtractor
20+
gt_sorting : BaseSorting
2121
The first sorting for the comparison
22-
tested_sorting : SortingExtractor
22+
tested_sorting : BaseSorting
2323
The second sorting for the comparison
2424
bin_ms : float, default: 1.0
2525
Size of bin for correlograms

src/spikeinterface/comparison/paircomparisons.py

Lines changed: 82 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
import numpy as np
44

5-
from spikeinterface.core.core_tools import define_function_from_class
5+
from ..core import BaseSorting
6+
from ..core.core_tools import define_function_from_class
67
from .basecomparison import BasePairComparison, MixinSpikeTrainComparison, MixinTemplateComparison
78
from .comparisontools import (
89
do_count_event,
@@ -12,6 +13,7 @@
1213
do_confusion_matrix,
1314
do_count_score,
1415
compute_performance,
16+
compute_distance_matrix,
1517
)
1618
from ..postprocessing import compute_template_similarity_by_pair
1719

@@ -23,16 +25,17 @@ class BasePairSorterComparison(BasePairComparison, MixinSpikeTrainComparison):
2325

2426
def __init__(
2527
self,
26-
sorting1,
27-
sorting2,
28-
sorting1_name=None,
29-
sorting2_name=None,
30-
delta_time=0.4,
31-
match_score=0.5,
32-
chance_score=0.1,
33-
ensure_symmetry=False,
34-
n_jobs=1,
35-
verbose=False,
28+
sorting1: BaseSorting,
29+
sorting2: BaseSorting,
30+
sorting1_name: str | None = None,
31+
sorting2_name: str | None = None,
32+
delta_time: float = 0.4,
33+
match_score: float = 0.5,
34+
chance_score: float = 0.1,
35+
ensure_symmetry: bool = False,
36+
agreement_method: str = "from_count",
37+
n_jobs: int = 1,
38+
verbose: bool = False,
3639
):
3740
if sorting1_name is None:
3841
sorting1_name = "sorting1"
@@ -59,6 +62,7 @@ def __init__(
5962
self.unit2_ids = self.sorting2.get_unit_ids()
6063

6164
self.ensure_symmetry = ensure_symmetry
65+
self.agreement_method = agreement_method
6266

6367
self._do_agreement()
6468
self._do_matching()
@@ -85,18 +89,30 @@ def _do_agreement(self):
8589

8690
# common to GroundTruthComparison and SymmetricSortingComparison
8791
# spike count for each spike train
88-
self.event_counts1 = do_count_event(self.sorting1)
89-
self.event_counts2 = do_count_event(self.sorting2)
92+
if self.agreement_method == "from_count":
93+
self.event_counts1 = do_count_event(self.sorting1)
94+
self.event_counts2 = do_count_event(self.sorting2)
9095

91-
# matrix of event match count for each pair
92-
self.match_event_count = make_match_count_matrix(
93-
self.sorting1, self.sorting2, self.delta_frames, ensure_symmetry=self.ensure_symmetry
94-
)
96+
# matrix of event match count for each pair
97+
self.match_event_count = make_match_count_matrix(
98+
self.sorting1, self.sorting2, self.delta_frames, ensure_symmetry=self.ensure_symmetry
99+
)
95100

96-
# agreement matrix score for each pair
97-
self.agreement_scores = make_agreement_scores_from_count(
98-
self.match_event_count, self.event_counts1, self.event_counts2
99-
)
101+
# agreement matrix score for each pair
102+
self.agreement_scores = make_agreement_scores_from_count(
103+
self.match_event_count, self.event_counts1, self.event_counts2
104+
)
105+
elif self.agreement_method == "distance_function":
106+
import pandas as pd
107+
108+
_, agreement_matrix = compute_distance_matrix(self.sorting1, self.sorting2, self.delta_frames)
109+
110+
self.agreement_scores = pd.DataFrame(
111+
agreement_matrix, index=self.sorting1.unit_ids, columns=self.sorting2.unit_ids
112+
)
113+
114+
else:
115+
raise ValueError("agreement_method must be 'from_count' or 'distance_matrix'")
100116

101117

102118
class SymmetricSortingComparison(BasePairSorterComparison):
@@ -112,9 +128,9 @@ class SymmetricSortingComparison(BasePairSorterComparison):
112128
113129
Parameters
114130
----------
115-
sorting1 : SortingExtractor
131+
sorting1 : BaseSorting
116132
The first sorting for the comparison
117-
sorting2 : SortingExtractor
133+
sorting2 : BaseSorting
118134
The second sorting for the comparison
119135
sorting1_name : str, default: None
120136
The name of sorter 1
@@ -126,6 +142,9 @@ class SymmetricSortingComparison(BasePairSorterComparison):
126142
Minimum agreement score to match units
127143
chance_score : float, default: 0.1
128144
Minimum agreement score to for a possible match
145+
agreement_method : "from_count" | "distance_function", default: "from_count"
146+
The method to compute agreement scores. The "from_count" method computes agreement scores from spike counts.
147+
The "distance_function" method computes agreement scores from spike time distance functions.
129148
n_jobs : int, default: -1
130149
Number of cores to use in parallel. Uses all available if -1
131150
verbose : bool, default: False
@@ -139,15 +158,16 @@ class SymmetricSortingComparison(BasePairSorterComparison):
139158

140159
def __init__(
141160
self,
142-
sorting1,
143-
sorting2,
144-
sorting1_name=None,
145-
sorting2_name=None,
146-
delta_time=0.4,
147-
match_score=0.5,
148-
chance_score=0.1,
149-
n_jobs=-1,
150-
verbose=False,
161+
sorting1: BaseSorting,
162+
sorting2: BaseSorting,
163+
sorting1_name: str | None = None,
164+
sorting2_name: str | None = None,
165+
delta_time: float = 0.4,
166+
match_score: float = 0.5,
167+
chance_score: float = 0.1,
168+
agreement_method: str = "from_count",
169+
n_jobs: int = -1,
170+
verbose: bool = False,
151171
):
152172
BasePairSorterComparison.__init__(
153173
self,
@@ -159,6 +179,7 @@ def __init__(
159179
match_score=match_score,
160180
chance_score=chance_score,
161181
ensure_symmetry=True,
182+
agreement_method=agreement_method,
162183
n_jobs=n_jobs,
163184
verbose=verbose,
164185
)
@@ -167,10 +188,13 @@ def get_matching(self):
167188
return self.hungarian_match_12, self.hungarian_match_21
168189

169190
def get_matching_event_count(self, unit1, unit2):
170-
if (unit1 is not None) and (unit2 is not None):
171-
return self.match_event_count.at[unit1, unit2]
191+
if self.agreement_method == "from_count":
192+
if (unit1 is not None) and (unit2 is not None):
193+
return self.match_event_count.at[unit1, unit2]
194+
else:
195+
raise Exception("get_matching_event_count: unit1 and unit2 must not be None.")
172196
else:
173-
raise Exception("get_matching_event_count: unit1 and unit2 must not be None.")
197+
raise Exception("get_matching_event_count is valid only if agreement_method='from_count'")
174198

175199
def get_best_unit_match1(self, unit1):
176200
return self.best_match_12[unit1]
@@ -215,9 +239,9 @@ class GroundTruthComparison(BasePairSorterComparison):
215239
216240
Parameters
217241
----------
218-
gt_sorting : SortingExtractor
242+
gt_sorting : BaseSorting
219243
The first sorting for the comparison
220-
tested_sorting : SortingExtractor
244+
tested_sorting : BaseSorting
221245
The second sorting for the comparison
222246
gt_name : str, default: None
223247
The name of sorter 1
@@ -243,6 +267,9 @@ class GroundTruthComparison(BasePairSorterComparison):
243267
For instance, MEArec simulated dataset have exhaustive_gt=True
244268
match_mode : "hungarian" | "best", default: "hungarian"
245269
The method to match units
270+
agreement_method : "from_count" | "distance_function", default: "from_count"
271+
The method to compute agreement scores. The "from_count" method computes agreement scores from spike counts.
272+
The "distance_function" method computes agreement scores from spike time distance functions.
246273
n_jobs : int, default: -1
247274
Number of cores to use in parallel. Uses all available if -1
248275
compute_labels : bool, default: False
@@ -260,22 +287,23 @@ class GroundTruthComparison(BasePairSorterComparison):
260287

261288
def __init__(
262289
self,
263-
gt_sorting,
264-
tested_sorting,
265-
gt_name=None,
266-
tested_name=None,
267-
delta_time=0.4,
268-
match_score=0.5,
269-
well_detected_score=0.8,
270-
redundant_score=0.2,
271-
overmerged_score=0.2,
272-
chance_score=0.1,
273-
exhaustive_gt=False,
274-
n_jobs=-1,
275-
match_mode="hungarian",
276-
compute_labels=False,
277-
compute_misclassifications=False,
278-
verbose=False,
290+
gt_sorting: BaseSorting,
291+
tested_sorting: BaseSorting,
292+
gt_name: str | None = None,
293+
tested_name: str | None = None,
294+
delta_time: float = 0.4,
295+
match_score: float = 0.5,
296+
well_detected_score: float = 0.8,
297+
redundant_score: float = 0.2,
298+
overmerged_score: float = 0.2,
299+
chance_score: float = 0.1,
300+
exhaustive_gt: bool = False,
301+
agreement_method: str = "from_count",
302+
n_jobs: int = -1,
303+
match_mode: str = "hungarian",
304+
compute_labels: bool = False,
305+
compute_misclassifications: bool = False,
306+
verbose: bool = False,
279307
):
280308
import pandas as pd
281309

@@ -293,6 +321,7 @@ def __init__(
293321
match_score=match_score,
294322
chance_score=chance_score,
295323
ensure_symmetry=False,
324+
agreement_method=agreement_method,
296325
n_jobs=n_jobs,
297326
verbose=verbose,
298327
)

src/spikeinterface/comparison/tests/test_symmetricsortingcomparison.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import numpy as np
22

3-
from spikeinterface.core import generate_sorting
3+
from spikeinterface.core import generate_sorting, aggregate_units
44
from spikeinterface.extractors import NumpySorting
55
from spikeinterface.comparison import compare_two_sorters
66

@@ -24,9 +24,13 @@ def test_compare_two_sorters():
2424
],
2525
[0, 0, 5],
2626
)
27-
sc = compare_two_sorters(sorting1, sorting2)
27+
sc_from_counts = compare_two_sorters(sorting1, sorting2, agreement_method="from_count")
28+
sc_from_distance = compare_two_sorters(sorting1, sorting2, agreement_method="distance_function")
2829

29-
print(sc.agreement_scores)
30+
np.testing.assert_array_equal(
31+
sc_from_counts.hungarian_match_12.to_numpy(),
32+
sc_from_distance.hungarian_match_12.to_numpy(),
33+
)
3034

3135

3236
def test_compare_multi_segment():
@@ -37,6 +41,24 @@ def test_compare_multi_segment():
3741
assert np.allclose(np.diag(cmp_multi.agreement_scores), np.ones(len(sort.unit_ids)))
3842

3943

44+
def test_agreements():
45+
"""
46+
Test that the agreement scores are the same when using from_count and distance_matrix
47+
"""
48+
sorting1 = generate_sorting(num_units=100)
49+
sorting_extra = generate_sorting(num_units=50)
50+
sorting2 = aggregate_units([sorting1, sorting_extra])
51+
sorting2 = sorting2.select_units(unit_ids=sorting2.unit_ids[np.random.permutation(len(sorting2.unit_ids))])
52+
53+
sc_from_counts = compare_two_sorters(sorting1, sorting2, agreement_method="from_count")
54+
sc_from_distance = compare_two_sorters(sorting1, sorting2, agreement_method="distance_function")
55+
56+
np.testing.assert_array_equal(
57+
sc_from_counts.hungarian_match_12.to_numpy(),
58+
sc_from_distance.hungarian_match_12.to_numpy(),
59+
)
60+
61+
4062
if __name__ == "__main__":
4163
test_compare_two_sorters()
4264
test_compare_multi_segment()

0 commit comments

Comments
 (0)