Skip to main content

dataprof_core/
config.rs

1use serde::{Deserialize, Serialize};
2
3/// Relative weights used to aggregate assessed quality dimensions.
4///
5/// Weights do not need to sum to one: the score renormalizes them over the
6/// dimensions that were actually assessed. Values should be non-negative and
7/// at least one weight must be positive.
8#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
9#[serde(default)]
10pub struct QualityScoreWeights {
11    pub completeness: f64,
12    pub consistency: f64,
13    pub uniqueness: f64,
14    pub accuracy: f64,
15    pub timeliness: f64,
16    pub validity: f64,
17    pub precision: f64,
18}
19
20impl Default for QualityScoreWeights {
21    fn default() -> Self {
22        Self {
23            completeness: 0.25,
24            consistency: 0.20,
25            uniqueness: 0.15,
26            accuracy: 0.15,
27            timeliness: 0.10,
28            validity: 0.10,
29            precision: 0.05,
30        }
31    }
32}
33
34impl QualityScoreWeights {
35    pub fn is_default(&self) -> bool {
36        self == &Self::default()
37    }
38
39    pub fn is_valid(&self) -> bool {
40        let weights = [
41            self.completeness,
42            self.consistency,
43            self.uniqueness,
44            self.accuracy,
45            self.timeliness,
46            self.validity,
47            self.precision,
48        ];
49        weights
50            .iter()
51            .all(|weight| weight.is_finite() && *weight >= 0.0)
52            && weights.iter().any(|weight| *weight > 0.0)
53    }
54}
55
56/// Quality thresholds and score settings informed by ISO 8000/25012 concepts.
57/// These values are dataprof configuration, not ISO-mandated parameters.
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct IsoQualityConfig {
60    /// Maximum acceptable null percentage for a column (default: 50%).
61    pub max_null_percentage: f64,
62    /// High cardinality warning threshold (uniqueness ratio, default: 95%).
63    pub high_cardinality_threshold: f64,
64    /// IQR multiplier for outlier detection (default: 1.5, ISO standard).
65    pub outlier_iqr_multiplier: f64,
66    /// Minimum samples required for outlier detection (default: 4).
67    pub outlier_min_samples: usize,
68    /// Maximum age for fresh data (default: 5 years of 365.2425 days each).
69    /// Fractional years are retained; the cutoff is rounded to a whole date.
70    pub max_data_age_years: f64,
71    /// Dataprof-defined weights for the overall quality score.
72    #[serde(default)]
73    pub score_weights: QualityScoreWeights,
74}
75
76impl Default for IsoQualityConfig {
77    fn default() -> Self {
78        Self {
79            max_null_percentage: 50.0,
80            high_cardinality_threshold: 95.0,
81            outlier_iqr_multiplier: 1.5,
82            outlier_min_samples: 4,
83            max_data_age_years: 5.0,
84            score_weights: QualityScoreWeights::default(),
85        }
86    }
87}
88
89impl IsoQualityConfig {
90    /// Create strict thresholds for high-compliance industries (finance, healthcare).
91    pub fn strict() -> Self {
92        Self {
93            max_null_percentage: 30.0,
94            high_cardinality_threshold: 98.0,
95            outlier_iqr_multiplier: 1.5,
96            outlier_min_samples: 10,
97            max_data_age_years: 2.0,
98            score_weights: QualityScoreWeights::default(),
99        }
100    }
101
102    /// Create lenient thresholds for exploratory/marketing data.
103    pub fn lenient() -> Self {
104        Self {
105            max_null_percentage: 70.0,
106            high_cardinality_threshold: 90.0,
107            outlier_iqr_multiplier: 2.0,
108            outlier_min_samples: 4,
109            max_data_age_years: 10.0,
110            score_weights: QualityScoreWeights::default(),
111        }
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn legacy_quality_config_defaults_score_weights() {
121        let mut value = serde_json::to_value(IsoQualityConfig::default()).unwrap();
122        value
123            .as_object_mut()
124            .expect("quality config object")
125            .remove("score_weights");
126
127        let restored: IsoQualityConfig = serde_json::from_value(value).unwrap();
128        assert_eq!(restored.score_weights, QualityScoreWeights::default());
129    }
130
131    #[test]
132    fn five_dimension_weights_gain_new_dimension_defaults() {
133        let weights: QualityScoreWeights = serde_json::from_str(
134            r#"{
135                "completeness": 0.30,
136                "consistency": 0.25,
137                "uniqueness": 0.20,
138                "accuracy": 0.15,
139                "timeliness": 0.10
140            }"#,
141        )
142        .unwrap();
143
144        assert_eq!(weights.validity, 0.10);
145        assert_eq!(weights.precision, 0.05);
146    }
147
148    #[test]
149    fn all_zero_score_weights_are_invalid() {
150        let weights = QualityScoreWeights {
151            completeness: 0.0,
152            consistency: 0.0,
153            uniqueness: 0.0,
154            accuracy: 0.0,
155            timeliness: 0.0,
156            validity: 0.0,
157            precision: 0.0,
158        };
159
160        assert!(!weights.is_valid());
161    }
162}