Skip to main content

sklears_python/metrics/
classification.rs

1//! Python bindings for classification metrics
2
3use super::common::*;
4use numpy::{PyArray2, PyReadonlyArray1};
5use scirs2_core::ndarray::Array1;
6use sklears_metrics::basic_metrics::{
7    accuracy_score as skl_accuracy, confusion_matrix as skl_confusion_matrix, f1_score as skl_f1,
8    precision_score as skl_precision, recall_score as skl_recall,
9};
10use std::collections::HashMap;
11
12/// Calculate accuracy score for classification
13#[pyfunction]
14#[pyo3(signature = (y_true, y_pred, normalize=true, sample_weight=None))]
15pub fn accuracy_score(
16    y_true: PyReadonlyArray1<i32>,
17    y_pred: PyReadonlyArray1<i32>,
18    normalize: bool,
19    sample_weight: Option<PyReadonlyArray1<f64>>,
20) -> PyResult<f64> {
21    let _ = sample_weight;
22    let yt = Array1::from_vec(y_true.as_array().to_vec());
23    let yp = Array1::from_vec(y_pred.as_array().to_vec());
24
25    validate_int_arrays_same_length(yt.as_slice().unwrap(), yp.as_slice().unwrap())?;
26
27    match skl_accuracy(&yt, &yp) {
28        Ok(acc) => Ok(if normalize {
29            acc
30        } else {
31            acc * yt.len() as f64
32        }),
33        Err(e) => Err(PyValueError::new_err(format!("accuracy_score: {}", e))),
34    }
35}
36
37/// Calculate precision score for binary classification
38#[pyfunction]
39#[pyo3(signature = (y_true, y_pred, labels=None, pos_label=1, average="binary", sample_weight=None, zero_division="warn"))]
40pub fn precision_score(
41    y_true: PyReadonlyArray1<i32>,
42    y_pred: PyReadonlyArray1<i32>,
43    labels: Option<PyReadonlyArray1<i32>>,
44    pos_label: i32,
45    average: &str,
46    sample_weight: Option<PyReadonlyArray1<f64>>,
47    zero_division: &str,
48) -> PyResult<f64> {
49    let _ = (labels, average, sample_weight, zero_division);
50    let yt = Array1::from_vec(y_true.as_array().to_vec());
51    let yp = Array1::from_vec(y_pred.as_array().to_vec());
52
53    validate_int_arrays_same_length(yt.as_slice().unwrap(), yp.as_slice().unwrap())?;
54
55    match skl_precision(&yt, &yp, Some(pos_label)) {
56        Ok(v) => Ok(v),
57        Err(e) => Err(PyValueError::new_err(format!("precision_score: {}", e))),
58    }
59}
60
61/// Calculate recall score for binary classification
62#[pyfunction]
63#[pyo3(signature = (y_true, y_pred, labels=None, pos_label=1, average="binary", sample_weight=None, zero_division="warn"))]
64pub fn recall_score(
65    y_true: PyReadonlyArray1<i32>,
66    y_pred: PyReadonlyArray1<i32>,
67    labels: Option<PyReadonlyArray1<i32>>,
68    pos_label: i32,
69    average: &str,
70    sample_weight: Option<PyReadonlyArray1<f64>>,
71    zero_division: &str,
72) -> PyResult<f64> {
73    let _ = (labels, average, sample_weight, zero_division);
74    let yt = Array1::from_vec(y_true.as_array().to_vec());
75    let yp = Array1::from_vec(y_pred.as_array().to_vec());
76
77    validate_int_arrays_same_length(yt.as_slice().unwrap(), yp.as_slice().unwrap())?;
78
79    match skl_recall(&yt, &yp, Some(pos_label)) {
80        Ok(v) => Ok(v),
81        Err(e) => Err(PyValueError::new_err(format!("recall_score: {}", e))),
82    }
83}
84
85/// Calculate F1 score for binary classification
86#[pyfunction]
87#[pyo3(signature = (y_true, y_pred, labels=None, pos_label=1, average="binary", sample_weight=None, zero_division="warn"))]
88pub fn f1_score(
89    y_true: PyReadonlyArray1<i32>,
90    y_pred: PyReadonlyArray1<i32>,
91    labels: Option<PyReadonlyArray1<i32>>,
92    pos_label: i32,
93    average: &str,
94    sample_weight: Option<PyReadonlyArray1<f64>>,
95    zero_division: &str,
96) -> PyResult<f64> {
97    let _ = (labels, average, sample_weight, zero_division);
98    let yt = Array1::from_vec(y_true.as_array().to_vec());
99    let yp = Array1::from_vec(y_pred.as_array().to_vec());
100
101    validate_int_arrays_same_length(yt.as_slice().unwrap(), yp.as_slice().unwrap())?;
102
103    match skl_f1(&yt, &yp, Some(pos_label)) {
104        Ok(v) => Ok(v),
105        Err(e) => Err(PyValueError::new_err(format!("f1_score: {}", e))),
106    }
107}
108
109/// Calculate confusion matrix
110#[pyfunction]
111#[pyo3(signature = (y_true, y_pred, labels=None, sample_weight=None, normalize=None))]
112pub fn confusion_matrix(
113    py: Python,
114    y_true: PyReadonlyArray1<i32>,
115    y_pred: PyReadonlyArray1<i32>,
116    labels: Option<PyReadonlyArray1<i32>>,
117    sample_weight: Option<PyReadonlyArray1<f64>>,
118    normalize: Option<&str>,
119) -> PyResult<Py<PyArray2<i64>>> {
120    let _ = (labels, sample_weight, normalize);
121    let yt = Array1::from_vec(y_true.as_array().to_vec());
122    let yp = Array1::from_vec(y_pred.as_array().to_vec());
123
124    validate_int_arrays_same_length(yt.as_slice().unwrap(), yp.as_slice().unwrap())?;
125
126    match skl_confusion_matrix(&yt, &yp) {
127        Ok(cm) => {
128            let cm_i64 = cm.mapv(|v| v as i64);
129            Ok(PyArray2::from_array(py, &cm_i64).unbind())
130        }
131        Err(e) => Err(PyValueError::new_err(format!("confusion_matrix: {}", e))),
132    }
133}
134
135/// Calculate classification report (returns a nested dict when output_dict=True).
136/// Note: `digits` and `zero_division` are accepted for API compatibility but ignored.
137#[pyfunction]
138#[pyo3(signature = (y_true, y_pred, labels=None, target_names=None, sample_weight=None, output_dict=true))]
139pub fn classification_report(
140    y_true: PyReadonlyArray1<i32>,
141    y_pred: PyReadonlyArray1<i32>,
142    labels: Option<PyReadonlyArray1<i32>>,
143    target_names: Option<Vec<String>>,
144    sample_weight: Option<PyReadonlyArray1<f64>>,
145    output_dict: bool,
146) -> PyResult<HashMap<String, HashMap<String, f64>>> {
147    let _ = (labels, target_names, sample_weight);
148    if !output_dict {
149        return Err(PyValueError::new_err(
150            "String output not supported; use output_dict=True.",
151        ));
152    }
153
154    let yt = Array1::from_vec(y_true.as_array().to_vec());
155    let yp = Array1::from_vec(y_pred.as_array().to_vec());
156
157    validate_int_arrays_same_length(yt.as_slice().unwrap(), yp.as_slice().unwrap())?;
158
159    let pos_label = *yt.iter().max().unwrap_or(&1);
160    let support = yt.len() as f64;
161
162    let precision = skl_precision(&yt, &yp, Some(pos_label)).unwrap_or(0.0);
163    let recall = skl_recall(&yt, &yp, Some(pos_label)).unwrap_or(0.0);
164    let f1 = skl_f1(&yt, &yp, Some(pos_label)).unwrap_or(0.0);
165
166    let mut class_entry = HashMap::new();
167    class_entry.insert("precision".to_string(), precision);
168    class_entry.insert("recall".to_string(), recall);
169    class_entry.insert("f1-score".to_string(), f1);
170    class_entry.insert("support".to_string(), support);
171
172    let mut report = HashMap::new();
173    report.insert(pos_label.to_string(), class_entry);
174    Ok(report)
175}