use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Decision {
Keep,
Review,
Drop,
}
impl std::fmt::Display for Decision {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Decision::Keep => write!(f, "keep"),
Decision::Review => write!(f, "review"),
Decision::Drop => write!(f, "drop"),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct StabilityComponents {
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub score_consistency: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub budget_robustness: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub seed_robustness: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub model_agreement: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub evaluator_agreement: Option<f64>,
}
impl StabilityComponents {
pub fn weakest(&self) -> Option<(&'static str, f64)> {
let candidates = [
("label", self.label),
("score_consistency", self.score_consistency),
("budget_robustness", self.budget_robustness),
("seed_robustness", self.seed_robustness),
("model_agreement", self.model_agreement),
("evaluator_agreement", self.evaluator_agreement),
];
candidates
.into_iter()
.filter_map(|(name, val)| val.map(|v| (name, v)))
.min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StabilityReport {
pub sample_id: String,
pub n_observations: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub majority_label: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub label_agreement: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub label_agreement_lcb: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub label_margin: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub label_entropy: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub score_mean: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub score_std: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub score_range: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub score_mad: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub score_iqr: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub budget_sensitivity: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub budget_slope: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub seed_sensitivity: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub model_agreement: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub evaluator_agreement: Option<f64>,
pub confidence: f64,
pub adjusted_stability_score: f64,
pub disagreement_score: f64,
pub stability_score: f64,
pub decision: Decision,
pub components: StabilityComponents,
}