1mod combine;
2
3use ndarray::{Array1,ArrayView,ArrayView2,ArrayView3,ArrayViewMut1,Ix1};
4use num;
5use num::traits::float::TotalOrder;
6use numpy::{Element,PyArray,PyArray1,PyArray2,PyArray3,PyArrayDescrMethods,PyArrayDyn,PyArrayMethods,PyReadonlyArray1,PyUntypedArray,PyUntypedArrayMethods,dtype};
7use pyo3::Bound;
8use pyo3::exceptions::PyTypeError;
9use pyo3::marker::Ungil;
10use pyo3::prelude::*;
11use rayon::prelude::*;
12use rayon::ThreadPoolBuilder;
13use std::cmp::{Ordering,PartialOrd};
14use std::iter::{DoubleEndedIterator,repeat};
15use std::ops::AddAssign;
16
17pub trait TotalCmp: Copy + Send + Sync {
20 fn total_cmp(&self, other: &Self) -> Ordering;
21}
22
23macro_rules! impl_total_cmp_float {
24 ($t:ty) => {
25 impl TotalCmp for $t {
26 fn total_cmp(&self, other: &Self) -> Ordering { <$t>::total_cmp(self, other) }
27 }
28 };
29}
30macro_rules! impl_total_cmp_ord {
31 ($t:ty) => {
32 impl TotalCmp for $t {
33 fn total_cmp(&self, other: &Self) -> Ordering { self.cmp(other) }
34 }
35 };
36}
37
38impl_total_cmp_float!(f32);
39impl_total_cmp_float!(f64);
40impl_total_cmp_ord!(i8);
41impl_total_cmp_ord!(i16);
42impl_total_cmp_ord!(i32);
43impl_total_cmp_ord!(i64);
44impl_total_cmp_ord!(u8);
45impl_total_cmp_ord!(u16);
46impl_total_cmp_ord!(u32);
47impl_total_cmp_ord!(u64);
48impl_total_cmp_ord!(bool);
49
50fn do_argsort<T, D>(data: &D, len: usize, stable: bool, pool: Option<&rayon::ThreadPool>) -> Vec<usize>
52where T: TotalCmp, D: Data<T> + Sync + ?Sized
53{
54 let mut indices: Vec<usize> = (0..len).collect();
55 let cmp = |i: &usize, k: &usize| data.get_at(*i).total_cmp(&data.get_at(*k));
56 match pool {
57 None => {
58 if stable { indices.par_sort_by(cmp); } else { indices.par_sort_unstable_by(cmp); }
60 }
61 Some(p) => {
62 if stable { p.install(|| indices.par_sort_by(cmp)); }
63 else { p.install(|| indices.par_sort_unstable_by(cmp)); }
64 }
65 }
66 indices
67}
68
69fn do_sort<T: TotalCmp>(data: &mut [T], stable: bool, pool: Option<&rayon::ThreadPool>) {
70 let cmp = |a: &T, b: &T| a.total_cmp(b);
71 match pool {
72 None => {
73 if stable { data.par_sort_by(cmp); } else { data.par_sort_unstable_by(cmp); }
75 }
76 Some(p) => {
77 if stable { p.install(|| data.par_sort_by(cmp)); }
78 else { p.install(|| data.par_sort_unstable_by(cmp)); }
79 }
80 }
81}
82
83fn build_pool(num_threads: Option<usize>) -> Option<rayon::ThreadPool> {
84 num_threads.map(|n| ThreadPoolBuilder::new().num_threads(n).build().unwrap())
85}
86
87pub trait IntoF64: Into<f64> + Copy + PartialOrd {}
89impl<T: Into<f64> + Copy + PartialOrd> IntoF64 for T {}
90
91#[derive(Clone, Copy)]
92pub enum Order {
93 ASCENDING,
94 DESCENDING
95}
96
97#[derive(Clone, Copy)]
99struct ConstWeight {
100 value: f64,
101}
102
103impl ConstWeight {
104 fn one() -> Self {
105 return ConstWeight { value: 1.0 };
106 }
107}
108
109impl Iterator for ConstWeight {
110 type Item = f64;
111 fn next(&mut self) -> Option<f64> {
112 return Some(self.value);
113 }
114}
115
116impl DoubleEndedIterator for ConstWeight {
117 fn next_back(&mut self) -> Option<f64> {
118 return Some(self.value);
119 }
120}
121
122impl Data<f64> for ConstWeight {
123 fn get_iterator(&self) -> impl DoubleEndedIterator<Item = f64> + Clone {
124 return *self;
125 }
126
127 fn get_at(&self, _index: usize) -> f64 {
128 return self.value;
129 }
130}
131
132pub trait Data<T: Clone> {
133 fn get_iterator(&self) -> impl DoubleEndedIterator<Item = T> + Clone;
134 fn get_at(&self, index: usize) -> T;
135}
136
137pub trait SortableData<T: TotalCmp + Clone>: Data<T> + Sync {
144 fn as_contiguous_slice(&self) -> Option<&[T]> { None }
147
148 fn argsort_unstable(&self, pool: Option<&rayon::ThreadPool>) -> Vec<usize> {
149 let len = self.get_iterator().count();
150 if let Some(slice) = self.as_contiguous_slice() {
151 do_argsort(slice, slice.len(), false, pool)
152 } else {
153 do_argsort(self, len, false, pool)
154 }
155 }
156}
157
158impl <T: Clone> Data<T> for Vec<T> {
159 fn get_iterator(&self) -> impl DoubleEndedIterator<Item = T> + Clone {
160 return self.iter().cloned();
161 }
162 fn get_at(&self, index: usize) -> T {
163 return self[index].clone();
164 }
165}
166
167impl<F: TotalCmp + num::Float + TotalOrder + Clone + Sync> SortableData<F> for Vec<F> {
168 fn as_contiguous_slice(&self) -> Option<&[F]> { Some(self.as_slice()) }
169}
170
171impl <T: Clone> Data<T> for [T] {
172 fn get_iterator(&self) -> impl DoubleEndedIterator<Item = T> + Clone {
173 return self.iter().cloned();
174 }
175 fn get_at(&self, index: usize) -> T {
176 return self[index].clone();
177 }
178}
179
180impl <T: Clone> Data<T> for &[T] {
181 fn get_iterator(&self) -> impl DoubleEndedIterator<Item = T> + Clone {
182 return self.iter().cloned();
183 }
184 fn get_at(&self, index: usize) -> T {
185 return self[index].clone();
186 }
187}
188
189impl<F: TotalCmp + num::Float + TotalOrder + Clone + Sync> SortableData<F> for &[F] {
190 fn as_contiguous_slice(&self) -> Option<&[F]> { Some(self) }
191}
192
193impl <T: Clone, const N: usize> Data<T> for [T; N] {
194 fn get_iterator(&self) -> impl DoubleEndedIterator<Item = T> + Clone {
195 return self.iter().cloned();
196 }
197 fn get_at(&self, index: usize) -> T {
198 return self[index].clone();
199 }
200}
201
202impl<F: TotalCmp + num::Float + TotalOrder + Clone + Sync, const N: usize> SortableData<F> for [F; N] {
203 fn as_contiguous_slice(&self) -> Option<&[F]> { Some(self.as_slice()) }
204}
205
206impl <T: Clone> Data<T> for ArrayView<'_, T, Ix1> {
207 fn get_iterator(&self) -> impl DoubleEndedIterator<Item = T> + Clone {
208 return self.iter().cloned();
209 }
210 fn get_at(&self, index: usize) -> T {
211 return self[index].clone();
212 }
213}
214
215impl <F> SortableData<F> for ArrayView<'_, F, Ix1>
216where F: TotalCmp + num::Float + TotalOrder + Clone + Sync {
217 fn as_contiguous_slice(&self) -> Option<&[F]> { self.as_slice() }
218}
219
220pub trait BinaryLabel: Clone + Copy {
221 fn get_value(&self) -> bool;
222}
223
224impl BinaryLabel for bool {
225 fn get_value(&self) -> bool {
226 return self.clone();
227 }
228}
229
230impl BinaryLabel for u8 {
231 fn get_value(&self) -> bool {
232 return (self & 1u8) == 1u8;
233 }
234}
235
236impl BinaryLabel for u16 {
237 fn get_value(&self) -> bool {
238 return (self & 1u16) == 1u16;
239 }
240}
241
242impl BinaryLabel for u32 {
243 fn get_value(&self) -> bool {
244 return (self & 1u32) == 1u32;
245 }
246}
247
248impl BinaryLabel for u64 {
249 fn get_value(&self) -> bool {
250 return (self & 1u64) == 1u64;
251 }
252}
253
254impl BinaryLabel for i8 {
255 fn get_value(&self) -> bool {
256 return (self & 1i8) == 1i8;
257 }
258}
259
260impl BinaryLabel for i16 {
261 fn get_value(&self) -> bool {
262 return (self & 1i16) == 1i16;
263 }
264}
265
266impl BinaryLabel for i32 {
267 fn get_value(&self) -> bool {
268 return (self & 1i32) == 1i32;
269 }
270}
271
272impl BinaryLabel for i64 {
273 fn get_value(&self) -> bool {
274 return (self & 1i64) == 1i64;
275 }
276}
277
278fn select<T, I>(slice: &I, indices: &[usize]) -> Vec<T>
279where T: Copy, I: Data<T>
280{
281 let mut selection: Vec<T> = Vec::new();
282 selection.reserve_exact(indices.len());
283 for index in indices {
284 selection.push(slice.get_at(*index));
285 }
286 return selection;
287}
288
289pub trait ScoreSortedDescending {
290 fn _score(&self, labels_with_weights: impl Iterator<Item = (f64, (bool, f64))> + Clone) -> f64;
291 fn score<P, B, W>(&self, labels_with_weights: impl Iterator<Item = (P, (B, W))> + Clone) -> f64
292 where P: IntoF64, B: BinaryLabel, W: IntoF64
293 {
294 return self._score(
295 labels_with_weights.map(|(p, (b, w))| -> (f64, (bool, f64)) { (p.into(), (b.get_value(), w.into()))})
296 )
297 }
298}
299
300
301pub fn score_sorted_iterators<S, P, B, W>(
302 score: S,
303 predictions: impl Iterator<Item = P> + Clone,
304 labels: impl Iterator<Item = B> + Clone,
305 weights: impl Iterator<Item = W> + Clone,
306) -> f64
307where S: ScoreSortedDescending, P: IntoF64, B: BinaryLabel, W: IntoF64 {
308 let zipped = predictions.zip(labels.zip(weights));
309 return score.score(zipped);
310}
311
312
313pub fn score_sorted_sample<S, P, B, W>(
314 score: S,
315 predictions: &impl Data<P>,
316 labels: &impl Data<B>,
317 weights: &impl Data<W>,
318 order: Order,
319) -> f64
320where S: ScoreSortedDescending, P: IntoF64, B: BinaryLabel, W: IntoF64 + Clone {
321 let p = predictions.get_iterator();
322 let l = labels.get_iterator();
323 let w = weights.get_iterator();
324 return match order {
325 Order::ASCENDING => score_sorted_iterators(score, p.rev(), l.rev(), w.rev()),
326 Order::DESCENDING => score_sorted_iterators(score, p, l, w),
327 };
328}
329
330
331pub fn score_maybe_sorted_sample<S, P, B, W>(
332 score: S,
333 predictions: &(impl Data<P> + SortableData<P>),
334 labels: &impl Data<B>,
335 weights: Option<&impl Data<W>>,
336 order: Option<Order>,
337) -> f64
338where S: ScoreSortedDescending, P: IntoF64 + TotalCmp + num::Float + TotalOrder, B: BinaryLabel, W: IntoF64
339{
340 return match order {
341 Some(o) => {
342 match weights {
343 Some(w) => score_sorted_sample(score, predictions, labels, w, o),
344 None => score_sorted_sample(score, predictions, labels, &ConstWeight::one(), o),
345 }
346 }
347 None => {
348 let mut indices = predictions.argsort_unstable(None);
349 indices.reverse(); let sorted_labels = select(labels, &indices);
351 let sorted_predictions = select(predictions, &indices);
352 match weights {
353 Some(w) => {
354 let sorted_weights = select(w, &indices);
355 score_sorted_sample(score, &sorted_predictions, &sorted_labels, &sorted_weights, Order::DESCENDING)
356 }
357 None => score_sorted_sample(score, &sorted_predictions, &sorted_labels, &ConstWeight::one(), Order::DESCENDING)
358 }
359 }
360 };
361}
362
363
364pub fn score_two_sorted_samples<S, P, B, W>(
365 score: S,
366 predictions1: impl Iterator<Item = P> + Clone,
367 label1: impl Iterator<Item = B> + Clone,
368 weight1: impl Iterator<Item = W> + Clone,
369 predictions2: impl Iterator<Item = P> + Clone,
370 label2: impl Iterator<Item = B> + Clone,
371 weight2: impl Iterator<Item = W> + Clone,
372) -> f64
373where S: ScoreSortedDescending, P: IntoF64, B: BinaryLabel + PartialOrd, W: IntoF64
374{
375 return score_two_sorted_samples_zipped(
376 score,
377 predictions1.zip(label1.zip(weight1)),
378 predictions2.zip(label2.zip(weight2)),
379 );
380}
381
382
383pub fn score_two_sorted_samples_zipped<S, P, B, W>(
384 score: S,
385 iter1: impl Iterator<Item = (P, (B, W))> + Clone,
386 iter2: impl Iterator<Item = (P, (B, W))> + Clone,
387) -> f64
388where S: ScoreSortedDescending, P: IntoF64, B: BinaryLabel + PartialOrd, W: IntoF64
389{
390 let combined_iter = combine::combine::CombineIterDescending::new(iter1, iter2);
391 return score.score(combined_iter);
392}
393
394
395struct AveragePrecision;
396
397
398#[derive(Clone,Copy,Debug)]
399struct Positives {
400 tps: f64,
401 fps: f64,
402}
403
404impl Positives {
405 fn new(tps: f64, fps: f64) -> Self {
406 return Positives { tps, fps };
407 }
408
409 fn zero() -> Self {
410 return Positives::new(0.0, 0.0);
411 }
412
413 fn add(&mut self, label: bool, weight: f64) {
414 let tp = weight * (label as u8) as f64;
415 let fp = weight - tp;
416 self.tps += tp;
417 self.fps += fp;
418 }
419
420 fn positives_sum(&self) -> f64 {
421 return self.tps + self.fps;
422 }
423
424 fn precision(&self) -> f64 {
425 return self.tps / self.positives_sum();
426 }
427}
428
429
430impl ScoreSortedDescending for AveragePrecision {
431 fn _score(&self, mut labels_with_weights: impl Iterator<Item = (f64, (bool, f64))> + Clone) -> f64
432 {
433 let mut positives = Positives::zero();
434 let mut last_p = f64::NAN;
435 let mut last_tps = 0.0f64;
436 let mut ap = 0.0f64;
437
438 match labels_with_weights.next() {
440 None => (), Some((p, (label, w))) => {
442 positives.add(label, w);
443 last_p = p;
444 }
445 }
446
447 for (p, (label, w)) in labels_with_weights {
448 if last_p != p {
449 ap += (positives.tps - last_tps) * positives.precision();
450 last_p = p;
451 last_tps = positives.tps;
452 }
453 positives.add(label, w);
454 }
455
456 ap += (positives.tps - last_tps) * positives.precision();
457
458 return if positives.tps == 0.0 { 0.0 } else { ap / positives.tps };
462 }
463}
464
465
466struct RocAuc;
467
468
469impl ScoreSortedDescending for RocAuc {
470 fn _score(&self, mut labels_with_weights: impl Iterator<Item = (f64, (bool, f64))> + Clone) -> f64
471 {
472 let mut positives = Positives::zero();
473 let mut last_p = f64::NAN;
474 let mut last_counted_fp = 0.0f64;
475 let mut last_counted_tp = 0.0f64;
476 let mut area_under_curve = 0.0f64;
477
478 match labels_with_weights.next() {
480 None => (), Some((p, (label, w))) => {
482 positives.add(label, w);
483 last_p = p;
484 }
485 }
486
487 for (p, (label, w)) in labels_with_weights {
488 if last_p != p {
489 area_under_curve += area_under_line_segment(
490 last_counted_fp, positives.fps,
491 last_counted_tp, positives.tps,
492 );
493 last_counted_fp = positives.fps;
494 last_counted_tp = positives.tps;
495 last_p = p;
496 }
497 positives.add(label, w);
498 }
499 area_under_curve += area_under_line_segment(
500 last_counted_fp, positives.fps,
501 last_counted_tp, positives.tps,
502 );
503 return area_under_curve / (positives.tps * positives.fps);
504 }
505}
506
507
508struct RocAucWithMaxFPR {
509 max_fpr: f64,
510}
511
512
513impl RocAucWithMaxFPR {
514 fn new(max_fpr: f64) -> Self {
515 return RocAucWithMaxFPR { max_fpr };
516 }
517
518 fn get_positive_sum(labels_with_weights: impl Iterator<Item = (bool, f64)>) -> Positives {
519 let mut positives = Positives::zero();
520 for (label, weight) in labels_with_weights {
521 positives.add(label, weight);
522 }
523 return positives;
524 }
525}
526
527
528impl ScoreSortedDescending for RocAucWithMaxFPR {
529 fn _score(&self, mut labels_with_weights: impl Iterator<Item = (f64, (bool, f64))> + Clone) -> f64
530 {
531 let total_positives = Self::get_positive_sum(labels_with_weights.clone().map(|(_p, (b, w))| (b, w)));
532 let max_fpr = self.max_fpr;
533 let false_positive_cutoff = max_fpr * total_positives.fps;
534
535 let mut positives = Positives::zero();
536 let mut last_p = f64::NAN;
537 let mut last_counted_fp = 0.0f64;
538 let mut last_counted_tp = 0.0f64;
539 let mut area_under_curve = 0.0f64;
540
541 match labels_with_weights.next() {
543 None => (), Some((p, (label, w))) => {
545 positives.add(label, w);
546 last_p = p;
547 }
548 }
549
550 for (p, (label, w)) in labels_with_weights {
551 if last_p != p {
552 area_under_curve += area_under_line_segment(
553 last_counted_fp, positives.fps,
554 last_counted_tp, positives.tps,
555 );
556 last_counted_fp = positives.fps;
557 last_counted_tp = positives.tps;
558 last_p = p;
559 }
560 let mut next_pos = positives;
561 next_pos.add(label, w);
562 if next_pos.fps > false_positive_cutoff {
563 let dx = next_pos.fps - positives.fps;
564 let dy = next_pos.tps - positives.tps;
565 positives = Positives::new(
566 positives.tps + dy * false_positive_cutoff / dx,
567 false_positive_cutoff,
568 );
569 break;
570 } else {
571 positives = next_pos;
572 }
573 }
574
575 area_under_curve += area_under_line_segment(
576 last_counted_fp, positives.fps,
577 last_counted_tp, positives.tps,
578 );
579
580 let normalized_area_under_curve = area_under_curve / (total_positives.tps * total_positives.fps);
581 let min_area = 0.5 * max_fpr * max_fpr;
582 let max_area = max_fpr;
583 return 0.5 * (1.0 + (normalized_area_under_curve - min_area) / (max_area - min_area));
584 }
585}
586
587
588struct RocAucWithOptionalMaxFPR {
589 max_fpr: Option<f64>,
590}
591
592impl RocAucWithOptionalMaxFPR {
593 fn new(max_fpr: Option<f64>) -> Self {
594 return Self { max_fpr };
595 }
596}
597
598
599impl ScoreSortedDescending for RocAucWithOptionalMaxFPR {
600 fn _score(&self, labels_with_weights: impl Iterator<Item = (f64, (bool, f64))> + Clone) -> f64
601 {
602 return match self.max_fpr {
603 Some(mfpr) => RocAucWithMaxFPR::new(mfpr).score(labels_with_weights),
604 None => RocAuc.score(labels_with_weights),
605 }
606 }
607}
608
609
610pub fn average_precision<P, B, W>(
611 predictions: &(impl Data<P> + SortableData<P>),
612 labels: &impl Data<B>,
613 weights: Option<&impl Data<W>>,
614 order: Option<Order>,
615) -> f64
616where P: IntoF64 + TotalCmp + num::Float + TotalOrder, B: BinaryLabel, W: IntoF64
617{
618 return score_maybe_sorted_sample(AveragePrecision, predictions, labels, weights, order);
619}
620
621
622pub fn roc_auc<P, B, W>(
623 predictions: &(impl Data<P> + SortableData<P>),
624 labels: &impl Data<B>,
625 weights: Option<&impl Data<W>>,
626 order: Option<Order>,
627 max_fpr: Option<f64>,
628) -> f64
629where P: IntoF64 + TotalCmp + num::Float + TotalOrder, B: BinaryLabel, W: IntoF64
630{
631 return score_maybe_sorted_sample(RocAucWithOptionalMaxFPR::new(max_fpr), predictions, labels, weights, order);
632}
633
634
635fn area_under_line_segment(x0: f64, x1: f64, y0: f64, y1: f64) -> f64 {
636 let dx = x1 - x0;
637 let dy = y1 - y0;
638 return dx * y0 + dy * dx * 0.5;
639}
640
641
642#[inline]
647fn accum_row<F: num::Float + AddAssign>(row: impl Iterator<Item = F>, sum: &mut [F]) {
648 for (f, s) in row.zip(sum.iter_mut()) {
649 *s += f;
650 }
651}
652
653#[inline]
657fn score_row<F: num::Float + AddAssign>(
658 row: impl Iterator<Item = F>,
659 sum: impl Iterator<Item = F>,
660 loo_weight_factor: F,
661) -> (F, F, F) {
662 let mut prod_sum = F::zero();
663 let mut m_sqs = F::zero();
664 let mut l_sqs = F::zero();
665 for (f, s) in row.zip(sum) {
666 let m_f = f;
667 let l_f = (s - f) * loo_weight_factor;
668 prod_sum += m_f * l_f;
669 m_sqs += m_f * m_f;
670 l_sqs += l_f * l_f;
671 }
672 (prod_sum, m_sqs, l_sqs)
673}
674
675#[inline]
692fn loo_cossim_loops<F, Row>(
693 rows: impl Iterator<Item = Row> + Clone,
694 sum: &mut [F],
695 loo_weight_factor: F,
696 num_replicates: usize,
697) -> F
698where
699 F: num::Float + AddAssign,
700 Row: Iterator<Item = F>,
701{
702 for row in rows.clone() {
703 accum_row(row, sum);
704 }
705 let mut result = F::zero();
706 for row in rows {
707 let (prod_sum, m_sqs, l_sqs) = score_row(row, sum.iter().copied(), loo_weight_factor);
708 result += prod_sum / (m_sqs * l_sqs).sqrt();
709 }
710 result / F::from(num_replicates).unwrap()
711}
712
713pub fn loo_cossim<F: num::Float + AddAssign>(mat: &ArrayView2<'_, F>, replicate_sum: &mut ArrayViewMut1<'_, F>) -> F {
714 let num_replicates = mat.shape()[0];
715 let loo_weight_factor = F::from(1).unwrap() / F::from(num_replicates - 1).unwrap();
716 let ncols = mat.shape()[1];
717 let sum = replicate_sum.as_slice_mut().unwrap();
720
721 if let Some(mat_slice) = mat.as_slice() {
730 loo_cossim_loops(mat_slice.chunks(ncols).map(|s| s.iter().copied()), sum, loo_weight_factor, num_replicates)
731 } else {
732 loo_cossim_loops(mat.outer_iter().map(|r| r.into_iter().copied()), sum, loo_weight_factor, num_replicates)
733 }
734}
735
736
737pub fn loo_cossim_single<F: num::Float + AddAssign>(mat: &ArrayView2<'_, F>) -> F {
738 let mut replicate_sum = Array1::<F>::zeros(mat.shape()[1]);
739 return loo_cossim(mat, &mut replicate_sum.view_mut());
740}
741
742
743pub fn loo_cossim_many<F: num::Float + AddAssign>(mat: &ArrayView3<'_, F>) -> Array1<F> {
744 let mut cossims = Array1::<F>::zeros(mat.shape()[0]);
745 let mut replicate_sum = Array1::<F>::zeros(mat.shape()[2]);
746 for (m, c) in mat.outer_iter().zip(cossims.iter_mut()) {
747 replicate_sum.fill(F::zero());
748 *c = loo_cossim(&m, &mut replicate_sum.view_mut());
749 }
750 return cossims;
751}
752
753
754#[pyclass(eq, eq_int, from_py_object, name="Order")]
756#[derive(Clone, Copy, PartialEq)]
757pub enum PyOrder {
758 ASCENDING,
759 DESCENDING
760}
761
762fn py_order_as_order(order: PyOrder) -> Order {
763 return match order {
764 PyOrder::ASCENDING => Order::ASCENDING,
765 PyOrder::DESCENDING => Order::DESCENDING,
766 }
767}
768
769trait PyScoreGeneric<S: ScoreSortedDescending>: Ungil + Sync {
770
771 fn get_score(&self) -> S;
772
773 fn score_py<'py, P, B, W>(
774 &self,
775 py: Python<'py>,
776 labels: PyReadonlyArray1<'py, B>,
777 predictions: PyReadonlyArray1<'py, P>,
778 weights: Option<PyReadonlyArray1<'py, W>>,
779 order: Option<PyOrder>,
780 ) -> f64
781 where P: IntoF64 + TotalCmp + Element + num::Float + TotalOrder, B: BinaryLabel + Element, W: IntoF64 + Element
782 {
783 let labels = labels.as_array();
784 let predictions = predictions.as_array();
785 let order = order.map(py_order_as_order);
786 return match weights {
787 Some(weight) => {
788 let w = weight.as_array();
789 py.detach(move || {
790 score_maybe_sorted_sample(self.get_score(), &predictions, &labels, Some(&w), order)
791 })
792 },
793 None => py.detach(move || {
794 score_maybe_sorted_sample(self.get_score(), &predictions, &labels, None::<&Vec<W>>, order)
795 })
796 };
797 }
798
799 fn score_two_sorted_samples_py_generic<'py, B, F, B1, F1, F2, W1, W2>(
800 &self,
801 py: Python<'py>,
802 labels1: PyReadonlyArray1<'py, B1>,
803 predictions1: PyReadonlyArray1<'py, F1>,
804 weights1: Option<PyReadonlyArray1<'py, W1>>,
805 labels2: PyReadonlyArray1<'py, B1>,
806 predictions2: PyReadonlyArray1<'py, F2>,
807 weights2: Option<PyReadonlyArray1<'py, W2>>,
808 ) -> f64
809 where B: BinaryLabel + PartialOrd + Ungil, F: IntoF64 + Ungil,
810 B1: Element + Into<B> + Clone,
811 F1: Element + Into<F> + Clone, F2: Element + Into<F> + Clone,
812 W1: Element + Into<f64> + Clone, W2: Element + Into<f64> + Clone
813 {
814 let l1 = labels1.as_array().into_iter().cloned().map(|l| -> B { l.into() });
815 let l2 = labels2.as_array().into_iter().cloned().map(|l| -> B { l.into() });
816 let p1 = predictions1.as_array().into_iter().cloned().map(|f| -> F { f.into() });
817 let p2 = predictions2.as_array().into_iter().cloned().map(|f| -> F { f.into() });
818
819 return match (weights1, weights2) {
820 (None, None) => {
821 py.detach(move || {
822 score_two_sorted_samples(self.get_score(), p1, l1, repeat(1.0f64), p2, l2, repeat(1.0f64))
823 })
824 }
825 (Some(w1), None) => {
826 let w1i = w1.as_array().into_iter().cloned().map(|w| -> f64 { w.into() });
827 py.detach(move || {
828 score_two_sorted_samples(self.get_score(), p1, l1, w1i, p2, l2, repeat(1.0f64))
829 })
830 }
831 (None, Some(w2)) => {
832 let w2i = w2.as_array().into_iter().cloned().map(|w| -> f64 { w.into() });
833 py.detach(move || {
834 score_two_sorted_samples(self.get_score(), p1, l1, repeat(1.0f64), p2, l2, w2i)
835 })
836 }
837 (Some(w1), Some(w2)) => {
838 let w1i = w1.as_array().into_iter().cloned().map(|w| -> f64 { w.into() });
839 let w2i = w2.as_array().into_iter().cloned().map(|w| -> f64 { w.into() });
840 py.detach(move || {
841 score_two_sorted_samples(self.get_score(), p1, l1, w1i, p2, l2, w2i)
842 })
843 }
844 };
845 }
846}
847
848struct AveragePrecisionPyGeneric;
849
850impl PyScoreGeneric<AveragePrecision> for AveragePrecisionPyGeneric {
851 fn get_score(&self) -> AveragePrecision {
852 return AveragePrecision;
853 }
854}
855
856struct RocAucPyGeneric {
857 max_fpr: Option<f64>,
858}
859
860impl RocAucPyGeneric {
861 fn new(max_fpr: Option<f64>) -> Self {
862 return RocAucPyGeneric { max_fpr };
863 }
864}
865
866impl PyScoreGeneric<RocAucWithOptionalMaxFPR> for RocAucPyGeneric {
867 fn get_score(&self) -> RocAucWithOptionalMaxFPR {
868 return RocAucWithOptionalMaxFPR::new(self.max_fpr);
869 }
870}
871
872macro_rules! average_precision_py {
877 ($fname: ident, $pyname:literal, $label_type:ty, $prediction_type:ty) => {
878 #[pyfunction(name = $pyname)]
879 #[pyo3(signature = (labels, predictions, *, weights=None, order=None))]
880 pub fn $fname<'py>(
881 py: Python<'py>,
882 labels: PyReadonlyArray1<'py, $label_type>,
883 predictions: PyReadonlyArray1<'py, $prediction_type>,
884 weights: Option<PyReadonlyArray1<'py, $prediction_type>>,
885 order: Option<PyOrder>
886 ) -> f64
887 {
888 return AveragePrecisionPyGeneric.score_py(py, labels, predictions, weights, order);
889 }
890 };
891 ($fname: ident, $pyname:literal, $label_type:ty, $prediction_type:ty, $py_module:ident) => {
892 average_precision_py!($fname, $pyname, $label_type, $prediction_type);
893 $py_module.add_function(wrap_pyfunction!($fname, $py_module)?).unwrap();
894 };
895}
896
897
898macro_rules! roc_auc_py {
899 ($fname: ident, $pyname:literal, $label_type:ty, $prediction_type:ty) => {
900 #[pyfunction(name = $pyname)]
901 #[pyo3(signature = (labels, predictions, *, weights=None, order=None, max_fpr=None))]
902 pub fn $fname<'py>(
903 py: Python<'py>,
904 labels: PyReadonlyArray1<'py, $label_type>,
905 predictions: PyReadonlyArray1<'py, $prediction_type>,
906 weights: Option<PyReadonlyArray1<'py, $prediction_type>>,
907 order: Option<PyOrder>,
908 max_fpr: Option<f64>,
909 ) -> f64
910 {
911 return RocAucPyGeneric::new(max_fpr).score_py(py, labels, predictions, weights, order);
912 }
913 };
914 ($fname: ident, $pyname:literal, $label_type:ty, $prediction_type:ty, $py_module:ident) => {
915 roc_auc_py!($fname, $pyname, $label_type, $prediction_type);
916 $py_module.add_function(wrap_pyfunction!($fname, $py_module)?).unwrap();
917 };
918}
919
920
921macro_rules! average_precision_on_two_sorted_samples_py {
922 ($fname: ident, $pyname:literal, $label_type:ty, $prediction_type:ty) => {
923 #[pyfunction(name = $pyname)]
924 #[pyo3(signature = (labels1, predictions1, weights1, labels2, predictions2, weights2, *))]
925 pub fn $fname<'py>(
926 py: Python<'py>,
927 labels1: PyReadonlyArray1<'py, $label_type>,
928 predictions1: PyReadonlyArray1<'py, $prediction_type>,
929 weights1: Option<PyReadonlyArray1<'py, $prediction_type>>,
930 labels2: PyReadonlyArray1<'py, $label_type>,
931 predictions2: PyReadonlyArray1<'py, $prediction_type>,
932 weights2: Option<PyReadonlyArray1<'py, $prediction_type>>,
933 ) -> f64
934 {
935 return AveragePrecisionPyGeneric.score_two_sorted_samples_py_generic::<$label_type, $prediction_type, $label_type, $prediction_type, $prediction_type, $prediction_type, $prediction_type>(py, labels1, predictions1, weights1, labels2, predictions2, weights2);
936 }
937 };
938 ($fname: ident, $pyname:literal, $label_type:ty, $prediction_type:ty, $py_module:ident) => {
939 average_precision_on_two_sorted_samples_py!($fname, $pyname, $label_type, $prediction_type);
940 $py_module.add_function(wrap_pyfunction!($fname, $py_module)?).unwrap();
941 };
942}
943
944
945macro_rules! roc_auc_on_two_sorted_samples_py {
946 ($fname: ident, $pyname:literal, $label_type:ty, $prediction_type:ty) => {
947 #[pyfunction(name = $pyname)]
948 #[pyo3(signature = (labels1, predictions1, weights1, labels2, predictions2, weights2, *, max_fpr=None))]
949 pub fn $fname<'py>(
950 py: Python<'py>,
951 labels1: PyReadonlyArray1<'py, $label_type>,
952 predictions1: PyReadonlyArray1<'py, $prediction_type>,
953 weights1: Option<PyReadonlyArray1<'py, $prediction_type>>,
954 labels2: PyReadonlyArray1<'py, $label_type>,
955 predictions2: PyReadonlyArray1<'py, $prediction_type>,
956 weights2: Option<PyReadonlyArray1<'py, $prediction_type>>,
957 max_fpr: Option<f64>,
958 ) -> f64
959 {
960 return RocAucPyGeneric::new(max_fpr).score_two_sorted_samples_py_generic::<$label_type, $prediction_type, $label_type, $prediction_type, $prediction_type, $prediction_type, $prediction_type>(py, labels1, predictions1, weights1, labels2, predictions2, weights2);
961 }
962 };
963 ($fname: ident, $pyname:literal, $label_type:ty, $prediction_type:ty, $py_module:ident) => {
964 roc_auc_on_two_sorted_samples_py!($fname, $pyname, $label_type, $prediction_type);
965 $py_module.add_function(wrap_pyfunction!($fname, $py_module)?).unwrap();
966 };
967}
968
969
970#[pyfunction(name = "loo_cossim")]
971#[pyo3(signature = (data))]
972pub fn loo_cossim_py<'py>(
973 py: Python<'py>,
974 data: &Bound<'py, PyUntypedArray>
975) -> PyResult<f64> {
976 if data.ndim() != 2 {
977 return Err(PyTypeError::new_err(format!("Expected 2-dimensional array for data (samples x features) but found {} dimenisons.", data.ndim())));
978 }
979
980 let dt = data.dtype();
981 if dt.is_equiv_to(&dtype::<f32>(py)) {
982 let typed_data = data.cast::<PyArray2<f32>>().unwrap().readonly();
983 let array = typed_data.as_array();
984 let score = py.detach(move || {
985 loo_cossim_single(&array)
986 });
987 return Ok(score as f64);
988 }
989 if dt.is_equiv_to(&dtype::<f64>(py)) {
990 let typed_data = data.cast::<PyArray2<f64>>().unwrap().readonly();
991 let array = typed_data.as_array();
992 let score = py.detach(move || {
993 loo_cossim_single(&array)
994 });
995 return Ok(score);
996 }
997 return Err(PyTypeError::new_err(format!("Only float32 and float64 data supported, but found {}", dt)));
998}
999
1000pub fn loo_cossim_many_generic_py<'py, F: num::Float + AddAssign + Element>(
1001 py: Python<'py>,
1002 data: &Bound<'py, PyArrayDyn<F>>
1003) -> PyResult<Bound<'py, PyArray1<F>>> {
1004 if data.ndim() != 3 {
1005 return Err(PyTypeError::new_err(format!("Expected 3-dimensional array for data (outer(?) x samples x features) but found {} dimenisons.", data.ndim())));
1006 }
1007 let typed_data = data.cast::<PyArray3<F>>().unwrap().readonly();
1008 let array = typed_data.as_array();
1009 let score = py.detach(move || {
1010 loo_cossim_many(&array)
1011 });
1012 let score_py = PyArray::from_owned_array(py, score);
1014 return Ok(score_py);
1015}
1016
1017#[pyfunction(name = "loo_cossim_many_f64")]
1018#[pyo3(signature = (data))]
1019pub fn loo_cossim_many_py_f64<'py>(
1020 py: Python<'py>,
1021 data: &Bound<'py, PyUntypedArray>
1022) -> PyResult<Bound<'py, PyArray1<f64>>> {
1023 if data.ndim() != 3 {
1024 return Err(PyTypeError::new_err(format!("Expected 3-dimensional array for data (outer(?) x samples x features) but found {} dimenisons.", data.ndim())));
1025 }
1026
1027 let dt = data.dtype();
1028 if !dt.is_equiv_to(&dtype::<f64>(py)) {
1029 return Err(PyTypeError::new_err(format!("Only float64 data supported, but found {}", dt)));
1030 }
1031 let typed_data = data.cast::<PyArrayDyn<f64>>().unwrap();
1032 return loo_cossim_many_generic_py(py, typed_data);
1033}
1034
1035#[pyfunction(name = "loo_cossim_many_f32")]
1036#[pyo3(signature = (data))]
1037pub fn loo_cossim_many_py_f32<'py>(
1038 py: Python<'py>,
1039 data: &Bound<'py, PyUntypedArray>
1040) -> PyResult<Bound<'py, PyArray1<f32>>> {
1041 if data.ndim() != 3 {
1042 return Err(PyTypeError::new_err(format!("Expected 3-dimensional array for data (outer(?) x samples x features) but found {} dimenisons.", data.ndim())));
1043 }
1044
1045 let dt = data.dtype();
1046 if !dt.is_equiv_to(&dtype::<f32>(py)) {
1047 return Err(PyTypeError::new_err(format!("Only float32 data supported, but found {}", dt)));
1048 }
1049 let typed_data = data.cast::<PyArrayDyn<f32>>().unwrap();
1050 return loo_cossim_many_generic_py(py, typed_data);
1051}
1052
1053macro_rules! argsort_py {
1064 ($fname:ident, $pyname:literal, $type:ty) => {
1065 #[pyfunction(name = $pyname)]
1066 #[pyo3(signature = (data, *, stable=false, num_threads=None))]
1067 pub fn $fname<'py>(
1068 py: Python<'py>,
1069 data: PyReadonlyArray1<'py, $type>,
1070 stable: bool,
1071 num_threads: Option<usize>,
1072 ) -> PyResult<Bound<'py, PyArray1<i64>>> {
1073 let arr = data.as_array();
1074 let len = arr.len();
1075 let pool = build_pool(num_threads);
1076 let indices = py.detach(|| do_argsort(&arr, len, stable, pool.as_ref()));
1077 let out: Vec<i64> = indices.into_iter().map(|i| i as i64).collect();
1078 Ok(PyArray1::from_vec(py, out))
1079 }
1080 };
1081 ($fname:ident, $pyname:literal, $type:ty, $module:ident) => {
1082 argsort_py!($fname, $pyname, $type);
1083 $module.add_function(wrap_pyfunction!($fname, $module)?).unwrap();
1084 };
1085}
1086
1087macro_rules! sort_inplace_py {
1088 ($fname:ident, $pyname:literal, $type:ty) => {
1089 #[pyfunction(name = $pyname)]
1090 #[pyo3(signature = (data, *, stable=false, num_threads=None))]
1091 pub fn $fname<'py>(
1092 py: Python<'py>,
1093 data: &Bound<'py, PyArray1<$type>>,
1094 stable: bool,
1095 num_threads: Option<usize>,
1096 ) -> PyResult<()> {
1097 let mut arr = unsafe { data.as_array_mut() };
1098 let slice = arr.as_slice_mut()
1099 .ok_or_else(|| PyTypeError::new_err("sort_inplace requires a C-contiguous array"))?;
1100 let pool = build_pool(num_threads);
1101 py.detach(|| do_sort(slice, stable, pool.as_ref()));
1102 Ok(())
1103 }
1104 };
1105 ($fname:ident, $pyname:literal, $type:ty, $module:ident) => {
1106 sort_inplace_py!($fname, $pyname, $type);
1107 $module.add_function(wrap_pyfunction!($fname, $module)?).unwrap();
1108 };
1109}
1110
1111#[pymodule(name = "_scors")]
1112fn scors(m: &Bound<'_, PyModule>) -> PyResult<()> {
1113 average_precision_py!(average_precision_bool_f32, "average_precision_bool_f32", bool, f32, m);
1114 average_precision_py!(average_precision_i8_f32, "average_precision_i8_f32", i8, f32, m);
1115 average_precision_py!(average_precision_i16_f32, "average_precision_i16_f32", i16, f32, m);
1116 average_precision_py!(average_precision_i32_f32, "average_precision_i32_f32", i32, f32, m);
1117 average_precision_py!(average_precision_i64_f32, "average_precision_i64_f32", i64, f32, m);
1118 average_precision_py!(average_precision_u8_f32, "average_precision_u8_f32", u8, f32, m);
1119 average_precision_py!(average_precision_u16_f32, "average_precision_u16_f32", u16, f32, m);
1120 average_precision_py!(average_precision_u32_f32, "average_precision_u32_f32", u32, f32, m);
1121 average_precision_py!(average_precision_u64_f32, "average_precision_u64_f32", u64, f32, m);
1122 average_precision_py!(average_precision_bool_f64, "average_precision_bool_f64", bool, f64, m);
1123 average_precision_py!(average_precision_i8_f64, "average_precision_i8_f64", i8, f64, m);
1124 average_precision_py!(average_precision_i16_f64, "average_precision_i16_f64", i16, f64, m);
1125 average_precision_py!(average_precision_i32_f64, "average_precision_i32_f64", i32, f64, m);
1126 average_precision_py!(average_precision_i64_f64, "average_precision_i64_f64", i64, f64, m);
1127 average_precision_py!(average_precision_u8_f64, "average_precision_u8_f64", u8, f64, m);
1128 average_precision_py!(average_precision_u16_f64, "average_precision_u16_f64", u16, f64, m);
1129 average_precision_py!(average_precision_u32_f64, "average_precision_u32_f64", u32, f64, m);
1130 average_precision_py!(average_precision_u64_f64, "average_precision_u64_f64", u64, f64, m);
1131
1132 roc_auc_py!(roc_auc_bool_f32, "roc_auc_bool_f32", bool, f32, m);
1133 roc_auc_py!(roc_auc_i8_f32, "roc_auc_i8_f32", i8, f32, m);
1134 roc_auc_py!(roc_auc_i16_f32, "roc_auc_i16_f32", i16, f32, m);
1135 roc_auc_py!(roc_auc_i32_f32, "roc_auc_i32_f32", i32, f32, m);
1136 roc_auc_py!(roc_auc_i64_f32, "roc_auc_i64_f32", i64, f32, m);
1137 roc_auc_py!(roc_auc_u8_f32, "roc_auc_u8_f32", u8, f32, m);
1138 roc_auc_py!(roc_auc_u16_f32, "roc_auc_u16_f32", u16, f32, m);
1139 roc_auc_py!(roc_auc_u32_f32, "roc_auc_u32_f32", u32, f32, m);
1140 roc_auc_py!(roc_auc_u64_f32, "roc_auc_u64_f32", u64, f32, m);
1141 roc_auc_py!(roc_auc_bool_f64, "roc_auc_bool_f64", bool, f64, m);
1142 roc_auc_py!(roc_auc_i8_f64, "roc_auc_i8_f64", i8, f64, m);
1143 roc_auc_py!(roc_auc_i16_f64, "roc_auc_i16_f64", i16, f64, m);
1144 roc_auc_py!(roc_auc_i32_f64, "roc_auc_i32_f64", i32, f64, m);
1145 roc_auc_py!(roc_auc_i64_f64, "roc_auc_i64_f64", i64, f64, m);
1146 roc_auc_py!(roc_auc_u8_f64, "roc_auc_u8_f64", u8, f64, m);
1147 roc_auc_py!(roc_auc_u16_f64, "roc_auc_u16_f64", u16, f64, m);
1148 roc_auc_py!(roc_auc_u32_f64, "roc_auc_u32_f64", u32, f64, m);
1149 roc_auc_py!(roc_auc_u64_f64, "roc_auc_u64_f64", u64, f64, m);
1150
1151 average_precision_on_two_sorted_samples_py!(average_precision_on_two_sorted_samples_bool_f32, "average_precision_on_two_sorted_samples_bool_f32", bool, f32, m);
1152 average_precision_on_two_sorted_samples_py!(average_precision_on_two_sorted_samples_i8_f32, "average_precision_on_two_sorted_samples_i8_f32", i8, f32, m);
1153 average_precision_on_two_sorted_samples_py!(average_precision_on_two_sorted_samples_i16_f32, "average_precision_on_two_sorted_samples_i16_f32", i16, f32, m);
1154 average_precision_on_two_sorted_samples_py!(average_precision_on_two_sorted_samples_i32_f32, "average_precision_on_two_sorted_samples_i32_f32", i32, f32, m);
1155 average_precision_on_two_sorted_samples_py!(average_precision_on_two_sorted_samples_i64_f32, "average_precision_on_two_sorted_samples_i64_f32", i64, f32, m);
1156 average_precision_on_two_sorted_samples_py!(average_precision_on_two_sorted_samples_u8_f32, "average_precision_on_two_sorted_samples_u8_f32", u8, f32, m);
1157 average_precision_on_two_sorted_samples_py!(average_precision_on_two_sorted_samples_u16_f32, "average_precision_on_two_sorted_samples_u16_f32", u16, f32, m);
1158 average_precision_on_two_sorted_samples_py!(average_precision_on_two_sorted_samples_u32_f32, "average_precision_on_two_sorted_samples_u32_f32", u32, f32, m);
1159 average_precision_on_two_sorted_samples_py!(average_precision_on_two_sorted_samples_u64_f32, "average_precision_on_two_sorted_samples_u64_f32", u64, f32, m);
1160 average_precision_on_two_sorted_samples_py!(average_precision_on_two_sorted_samples_bool_f64, "average_precision_on_two_sorted_samples_bool_f64", bool, f64, m);
1161 average_precision_on_two_sorted_samples_py!(average_precision_on_two_sorted_samples_i8_f64, "average_precision_on_two_sorted_samples_i8_f64", i8, f64, m);
1162 average_precision_on_two_sorted_samples_py!(average_precision_on_two_sorted_samples_i16_f64, "average_precision_on_two_sorted_samples_i16_f64", i16, f64, m);
1163 average_precision_on_two_sorted_samples_py!(average_precision_on_two_sorted_samples_i32_f64, "average_precision_on_two_sorted_samples_i32_f64", i32, f64, m);
1164 average_precision_on_two_sorted_samples_py!(average_precision_on_two_sorted_samples_i64_f64, "average_precision_on_two_sorted_samples_i64_f64", i64, f64, m);
1165 average_precision_on_two_sorted_samples_py!(average_precision_on_two_sorted_samples_u8_f64, "average_precision_on_two_sorted_samples_u8_f64", u8, f64, m);
1166 average_precision_on_two_sorted_samples_py!(average_precision_on_two_sorted_samples_u16_f64, "average_precision_on_two_sorted_samples_u16_f64", u16, f64, m);
1167 average_precision_on_two_sorted_samples_py!(average_precision_on_two_sorted_samples_u32_f64, "average_precision_on_two_sorted_samples_u32_f64", u32, f64, m);
1168 average_precision_on_two_sorted_samples_py!(average_precision_on_two_sorted_samples_u64_f64, "average_precision_on_two_sorted_samples_u64_f64", u64, f64, m);
1169
1170 roc_auc_on_two_sorted_samples_py!(roc_auc_on_two_sorted_samples_bool_f32, "roc_auc_on_two_sorted_samples_bool_f32", bool, f32, m);
1171 roc_auc_on_two_sorted_samples_py!(roc_auc_on_two_sorted_samples_i8_f32, "roc_auc_on_two_sorted_samples_i8_f32", i8, f32, m);
1172 roc_auc_on_two_sorted_samples_py!(roc_auc_on_two_sorted_samples_i16_f32, "roc_auc_on_two_sorted_samples_i16_f32", i16, f32, m);
1173 roc_auc_on_two_sorted_samples_py!(roc_auc_on_two_sorted_samples_i32_f32, "roc_auc_on_two_sorted_samples_i32_f32", i32, f32, m);
1174 roc_auc_on_two_sorted_samples_py!(roc_auc_on_two_sorted_samples_i64_f32, "roc_auc_on_two_sorted_samples_i64_f32", i64, f32, m);
1175 roc_auc_on_two_sorted_samples_py!(roc_auc_on_two_sorted_samples_u8_f32, "roc_auc_on_two_sorted_samples_u8_f32", u8, f32, m);
1176 roc_auc_on_two_sorted_samples_py!(roc_auc_on_two_sorted_samples_u16_f32, "roc_auc_on_two_sorted_samples_u16_f32", u16, f32, m);
1177 roc_auc_on_two_sorted_samples_py!(roc_auc_on_two_sorted_samples_u32_f32, "roc_auc_on_two_sorted_samples_u32_f32", u32, f32, m);
1178 roc_auc_on_two_sorted_samples_py!(roc_auc_on_two_sorted_samples_u64_f32, "roc_auc_on_two_sorted_samples_u64_f32", u64, f32, m);
1179 roc_auc_on_two_sorted_samples_py!(roc_auc_on_two_sorted_samples_bool_f64, "roc_auc_on_two_sorted_samples_bool_f64", bool, f64, m);
1180 roc_auc_on_two_sorted_samples_py!(roc_auc_on_two_sorted_samples_i8_f64, "roc_auc_on_two_sorted_samples_i8_f64", i8, f64, m);
1181 roc_auc_on_two_sorted_samples_py!(roc_auc_on_two_sorted_samples_i16_f64, "roc_auc_on_two_sorted_samples_i16_f64", i16, f64, m);
1182 roc_auc_on_two_sorted_samples_py!(roc_auc_on_two_sorted_samples_i32_f64, "roc_auc_on_two_sorted_samples_i32_f64", i32, f64, m);
1183 roc_auc_on_two_sorted_samples_py!(roc_auc_on_two_sorted_samples_i64_f64, "roc_auc_on_two_sorted_samples_i64_f64", i64, f64, m);
1184 roc_auc_on_two_sorted_samples_py!(roc_auc_on_two_sorted_samples_u8_f64, "roc_auc_on_two_sorted_samples_u8_f64", u8, f64, m);
1185 roc_auc_on_two_sorted_samples_py!(roc_auc_on_two_sorted_samples_u16_f64, "roc_auc_on_two_sorted_samples_u16_f64", u16, f64, m);
1186 roc_auc_on_two_sorted_samples_py!(roc_auc_on_two_sorted_samples_u32_f64, "roc_auc_on_two_sorted_samples_u32_f64", u32, f64, m);
1187 roc_auc_on_two_sorted_samples_py!(roc_auc_on_two_sorted_samples_u64_f64, "roc_auc_on_two_sorted_samples_u64_f64", u64, f64, m);
1188
1189 m.add_function(wrap_pyfunction!(loo_cossim_py, m)?).unwrap();
1190 m.add_function(wrap_pyfunction!(loo_cossim_many_py_f64, m)?).unwrap();
1191 m.add_function(wrap_pyfunction!(loo_cossim_many_py_f32, m)?).unwrap();
1192 m.add_class::<PyOrder>().unwrap();
1193
1194 argsort_py!(argsort_f32, "argsort_f32", f32, m);
1195 argsort_py!(argsort_f64, "argsort_f64", f64, m);
1196 argsort_py!(argsort_i8, "argsort_i8", i8, m);
1197 argsort_py!(argsort_i16, "argsort_i16", i16, m);
1198 argsort_py!(argsort_i32, "argsort_i32", i32, m);
1199 argsort_py!(argsort_i64, "argsort_i64", i64, m);
1200 argsort_py!(argsort_u8, "argsort_u8", u8, m);
1201 argsort_py!(argsort_u16, "argsort_u16", u16, m);
1202 argsort_py!(argsort_u32, "argsort_u32", u32, m);
1203 argsort_py!(argsort_u64, "argsort_u64", u64, m);
1204 argsort_py!(argsort_bool, "argsort_bool", bool, m);
1205
1206 sort_inplace_py!(sort_inplace_f32, "sort_inplace_f32", f32, m);
1207 sort_inplace_py!(sort_inplace_f64, "sort_inplace_f64", f64, m);
1208 sort_inplace_py!(sort_inplace_i8, "sort_inplace_i8", i8, m);
1209 sort_inplace_py!(sort_inplace_i16, "sort_inplace_i16", i16, m);
1210 sort_inplace_py!(sort_inplace_i32, "sort_inplace_i32", i32, m);
1211 sort_inplace_py!(sort_inplace_i64, "sort_inplace_i64", i64, m);
1212 sort_inplace_py!(sort_inplace_u8, "sort_inplace_u8", u8, m);
1213 sort_inplace_py!(sort_inplace_u16, "sort_inplace_u16", u16, m);
1214 sort_inplace_py!(sort_inplace_u32, "sort_inplace_u32", u32, m);
1215 sort_inplace_py!(sort_inplace_u64, "sort_inplace_u64", u64, m);
1216 sort_inplace_py!(sort_inplace_bool, "sort_inplace_bool", bool, m);
1217
1218 return Ok(());
1219}
1220
1221
1222#[cfg(test)]
1223mod tests {
1224 use super::*;
1225
1226 #[test]
1227 fn test_average_precision_on_sorted() {
1228 let labels: [u8; 4] = [1, 0, 1, 0];
1229 let predictions: [f64; 4] = [0.8, 0.4, 0.35, 0.1];
1230 let weights: [f64; 4] = [1.0, 1.0, 1.0, 1.0];
1231 let actual: f64 = score_sorted_sample(AveragePrecision, &predictions, &labels, &weights, Order::DESCENDING);
1232 assert_eq!(actual, 0.8333333333333333);
1233 }
1234
1235 #[test]
1236 fn test_average_precision_on_sorted_double() {
1237 let labels: [u8; 8] = [1, 1, 0, 0, 1, 1, 0, 0];
1238 let predictions: [f64; 8] = [0.8, 0.8, 0.4, 0.4, 0.35, 0.35, 0.1, 0.1];
1239 let weights: [f64; 8] = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0];
1240 let actual: f64 = score_sorted_sample(AveragePrecision, &predictions, &labels, &weights, Order::DESCENDING);
1241 assert_eq!(actual, 0.8333333333333333);
1242 }
1243
1244 #[test]
1245 fn test_average_precision_unsorted() {
1246 let labels: [u8; 4] = [0, 0, 1, 1];
1247 let predictions: [f64; 4] = [0.1, 0.4, 0.35, 0.8];
1248 let weights: [f64; 4] = [1.0, 1.0, 1.0, 1.0];
1249 let actual: f64 = average_precision(&predictions, &labels, Some(&weights), None);
1250 assert_eq!(actual, 0.8333333333333333);
1251 }
1252
1253 #[test]
1254 fn test_average_precision_sorted() {
1255 let labels: [u8; 4] = [1, 0, 1, 0];
1256 let predictions: [f64; 4] = [0.8, 0.4, 0.35, 0.1];
1257 let weights: [f64; 4] = [1.0, 1.0, 1.0, 1.0];
1258 let actual: f64 = average_precision(&predictions, &labels, Some(&weights), Some(Order::DESCENDING));
1259 assert_eq!(actual, 0.8333333333333333);
1260 }
1261
1262 #[test]
1263 fn test_average_precision_sorted_pair() {
1264 let labels: [u8; 4] = [1, 0, 1, 0];
1265 let predictions: [f64; 4] = [0.8, 0.4, 0.35, 0.1];
1266 let weights: [f64; 4] = [1.0, 1.0, 1.0, 1.0];
1267 let actual: f64 = score_two_sorted_samples(
1268 AveragePrecision,
1269 predictions.iter().cloned(),
1270 labels.iter().cloned(),
1271 weights.iter().cloned(),
1272 predictions.iter().cloned(),
1273 labels.iter().cloned(),
1274 weights.iter().cloned()
1275 );
1276 assert_eq!(actual, 0.8333333333333333);
1277 }
1278
1279 #[test]
1280 fn test_roc_auc() {
1281 let labels: [u8; 4] = [1, 0, 1, 0];
1282 let predictions: [f64; 4] = [0.8, 0.4, 0.35, 0.1];
1283 let weights: [f64; 4] = [1.0, 1.0, 1.0, 1.0];
1284 let actual: f64 = roc_auc(&predictions, &labels, Some(&weights), Some(Order::DESCENDING), None);
1285 assert_eq!(actual, 0.75);
1286 }
1287
1288 #[test]
1289 fn test_roc_auc_double() {
1290 let labels: [u8; 8] = [1, 0, 1, 0, 1, 0, 1, 0];
1291 let predictions: [f64; 8] = [0.8, 0.4, 0.35, 0.1, 0.8, 0.4, 0.35, 0.1];
1292 let actual: f64 = roc_auc(&predictions, &labels, None::<&[f64; 8]>, None, None);
1293 assert_eq!(actual, 0.75);
1294 }
1295
1296 #[test]
1297 fn test_roc_sorted_pair() {
1298 let labels: [u8; 4] = [1, 0, 1, 0];
1299 let predictions: [f64; 4] = [0.8, 0.4, 0.35, 0.1];
1300 let weights: [f64; 4] = [1.0, 1.0, 1.0, 1.0];
1301 let actual: f64 = score_two_sorted_samples(
1302 RocAuc,
1303 predictions.iter().cloned(),
1304 labels.iter().cloned(),
1305 weights.iter().cloned(),
1306 predictions.iter().cloned(),
1307 labels.iter().cloned(),
1308 weights.iter().cloned()
1309 );
1310 assert_eq!(actual, 0.75);
1311 }
1312
1313 #[test]
1314 fn test_roc_auc_max_fpr() {
1315 let labels: [u8; 4] = [1, 0, 1, 0];
1316 let predictions: [f64; 4] = [0.8, 0.4, 0.35, 0.1];
1317 let weights: [f64; 4] = [1.0, 1.0, 1.0, 1.0];
1318 let actual: f64 = roc_auc(&predictions, &labels, Some(&weights), Some(Order::DESCENDING), Some(0.25));
1319 assert_eq!(actual, 0.7142857142857143);
1320 }
1321
1322 #[test]
1323 fn test_roc_auc_max_fpr_double() {
1324 let labels: [u8; 8] = [1, 0, 1, 0, 1, 0, 1, 0];
1325 let predictions: [f64; 8] = [0.8, 0.4, 0.35, 0.1, 0.8, 0.4, 0.35, 0.1];
1326 let actual: f64 = roc_auc(&predictions, &labels, None::<&[f64; 8]>, None, Some(0.25));
1327 assert_eq!(actual, 0.7142857142857143);
1328 }
1329
1330 #[test]
1331 fn test_roc_auc_max_fpr_sorted_pair() {
1332 let labels: [u8; 4] = [1, 0, 1, 0];
1333 let predictions: [f64; 4] = [0.8, 0.4, 0.35, 0.1];
1334 let weights: [f64; 4] = [1.0, 1.0, 1.0, 1.0];
1335 let actual: f64 = score_two_sorted_samples(
1336 RocAucWithMaxFPR::new(0.25f64),
1337 predictions.iter().cloned(),
1338 labels.iter().cloned(),
1339 weights.iter().cloned(),
1340 predictions.iter().cloned(),
1341 labels.iter().cloned(),
1342 weights.iter().cloned()
1343 );
1344 assert_eq!(actual, 0.7142857142857143);
1345 }
1346}