Skip to main content

sklears_datasets/validation/
types.rs

1//! Core data structures for dataset validation framework
2//!
3//! This module defines the data structures used throughout the validation
4//! system for statistical analysis, quality metrics, and validation reporting.
5
6use std::collections::HashMap;
7use std::fmt;
8
9/// Comprehensive statistical summary for a dataset feature
10#[derive(Debug, Clone)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12pub struct FeatureStatistics {
13    pub name: String,
14    pub count: usize,
15    pub mean: f64,
16    pub median: f64,
17    pub std_dev: f64,
18    pub variance: f64,
19    pub min: f64,
20    pub max: f64,
21    pub range: f64,
22    pub skewness: f64,
23    pub kurtosis: f64,
24    pub q1: f64,
25    pub q3: f64,
26    pub iqr: f64,
27    pub outlier_count: usize,
28    pub outlier_ratio: f64,
29    pub missing_count: usize,
30    pub missing_ratio: f64,
31    pub unique_count: usize,
32    pub mode: Option<f64>,
33    pub percentiles: HashMap<u8, f64>,
34}
35
36/// Summary statistics for target/label data
37#[derive(Debug, Clone)]
38#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
39pub struct TargetStatistics {
40    pub name: String,
41    pub data_type: String, // "continuous", "categorical", "binary"
42    pub count: usize,
43    pub unique_count: usize,
44    pub missing_count: usize,
45    pub missing_ratio: f64,
46    pub class_distribution: HashMap<String, usize>,
47    pub class_balance_ratio: f64,
48    pub entropy: f64,
49    pub continuous_stats: Option<FeatureStatistics>,
50}
51
52/// Complete dataset statistical summary
53#[derive(Debug, Clone)]
54#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
55pub struct StatisticalSummary {
56    pub dataset_name: String,
57    pub n_samples: usize,
58    pub n_features: usize,
59    pub feature_statistics: Vec<FeatureStatistics>,
60    pub target_statistics: Option<TargetStatistics>,
61    pub correlation_matrix: Vec<Vec<f64>>,
62    pub feature_correlations: HashMap<String, f64>,
63    pub data_quality_score: f64,
64    pub missing_data_pattern: String,
65    pub outlier_summary: HashMap<String, usize>,
66    pub distribution_types: HashMap<String, String>,
67    pub generation_timestamp: String,
68    pub metadata: HashMap<String, String>,
69}
70
71impl StatisticalSummary {
72    /// Create a new statistical summary
73    pub fn new(dataset_name: String, n_samples: usize, n_features: usize) -> Self {
74        Self {
75            dataset_name,
76            n_samples,
77            n_features,
78            feature_statistics: Vec::new(),
79            target_statistics: None,
80            correlation_matrix: Vec::new(),
81            feature_correlations: HashMap::new(),
82            data_quality_score: 0.0,
83            missing_data_pattern: "None".to_string(),
84            outlier_summary: HashMap::new(),
85            distribution_types: HashMap::new(),
86            generation_timestamp: chrono::Utc::now().to_rfc3339(),
87            metadata: HashMap::new(),
88        }
89    }
90
91    /// Add feature statistics
92    pub fn add_feature_stats(&mut self, stats: FeatureStatistics) {
93        self.feature_statistics.push(stats);
94    }
95
96    /// Set target statistics
97    pub fn set_target_stats(&mut self, stats: TargetStatistics) {
98        self.target_statistics = Some(stats);
99    }
100
101    /// Calculate data quality score (0-100)
102    pub fn calculate_quality_score(&mut self) {
103        let mut score = 100.0;
104
105        // Penalize for missing data
106        let total_missing_ratio: f64 = self
107            .feature_statistics
108            .iter()
109            .map(|fs| fs.missing_ratio)
110            .sum::<f64>()
111            / self.feature_statistics.len() as f64;
112        score -= total_missing_ratio * 50.0;
113
114        // Penalize for excessive outliers
115        let total_outlier_ratio: f64 = self
116            .feature_statistics
117            .iter()
118            .map(|fs| fs.outlier_ratio)
119            .sum::<f64>()
120            / self.feature_statistics.len() as f64;
121        if total_outlier_ratio > 0.1 {
122            score -= (total_outlier_ratio - 0.1) * 100.0;
123        }
124
125        // Penalize for extreme class imbalance
126        if let Some(ref target_stats) = self.target_statistics {
127            if target_stats.class_balance_ratio < 0.1 {
128                score -= (0.1 - target_stats.class_balance_ratio) * 200.0;
129            }
130        }
131
132        self.data_quality_score = score.max(0.0);
133    }
134
135    /// Export summary as JSON string
136    #[cfg(feature = "serde")]
137    pub fn to_json(&self) -> Result<String, serde_json::Error> {
138        serde_json::to_string_pretty(self)
139    }
140
141    /// Export summary as CSV string
142    pub fn to_csv(&self) -> String {
143        let mut csv = String::new();
144        csv.push_str("metric,value\n");
145        csv.push_str(&format!("dataset_name,{}\n", self.dataset_name));
146        csv.push_str(&format!("n_samples,{}\n", self.n_samples));
147        csv.push_str(&format!("n_features,{}\n", self.n_features));
148        csv.push_str(&format!(
149            "data_quality_score,{:.2}\n",
150            self.data_quality_score
151        ));
152        csv.push_str(&format!(
153            "missing_data_pattern,{}\n",
154            self.missing_data_pattern
155        ));
156        csv.push_str(&format!(
157            "generation_timestamp,{}\n",
158            self.generation_timestamp
159        ));
160
161        for (i, feature) in self.feature_statistics.iter().enumerate() {
162            csv.push_str(&format!("feature_{}_mean,{:.4}\n", i, feature.mean));
163            csv.push_str(&format!("feature_{}_std,{:.4}\n", i, feature.std_dev));
164            csv.push_str(&format!("feature_{}_min,{:.4}\n", i, feature.min));
165            csv.push_str(&format!("feature_{}_max,{:.4}\n", i, feature.max));
166            csv.push_str(&format!(
167                "feature_{}_outlier_ratio,{:.4}\n",
168                i, feature.outlier_ratio
169            ));
170        }
171
172        csv
173    }
174}
175
176impl fmt::Display for StatisticalSummary {
177    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178        writeln!(f, "=== Statistical Summary: {} ===", self.dataset_name)?;
179        writeln!(
180            f,
181            "Dataset Shape: {} samples × {} features",
182            self.n_samples, self.n_features
183        )?;
184        writeln!(f, "Data Quality Score: {:.2}/100", self.data_quality_score)?;
185        writeln!(f, "Missing Data Pattern: {}", self.missing_data_pattern)?;
186        writeln!(f, "Generation Time: {}", self.generation_timestamp)?;
187        writeln!(f)?;
188
189        writeln!(f, "Feature Statistics:")?;
190        for (i, feature) in self.feature_statistics.iter().enumerate() {
191            writeln!(f, "  Feature {}: {}", i, feature.name)?;
192            writeln!(
193                f,
194                "    Mean: {:.4}, Std: {:.4}",
195                feature.mean, feature.std_dev
196            )?;
197            writeln!(
198                f,
199                "    Min: {:.4}, Max: {:.4}, Range: {:.4}",
200                feature.min, feature.max, feature.range
201            )?;
202            writeln!(
203                f,
204                "    Skewness: {:.4}, Kurtosis: {:.4}",
205                feature.skewness, feature.kurtosis
206            )?;
207            writeln!(
208                f,
209                "    Outliers: {} ({:.2}%)",
210                feature.outlier_count,
211                feature.outlier_ratio * 100.0
212            )?;
213            writeln!(
214                f,
215                "    Missing: {} ({:.2}%)",
216                feature.missing_count,
217                feature.missing_ratio * 100.0
218            )?;
219        }
220
221        if let Some(ref target) = self.target_statistics {
222            writeln!(f)?;
223            writeln!(f, "Target Statistics:")?;
224            writeln!(f, "  Type: {}", target.data_type)?;
225            writeln!(f, "  Unique Values: {}", target.unique_count)?;
226            writeln!(
227                f,
228                "  Class Balance Ratio: {:.4}",
229                target.class_balance_ratio
230            )?;
231            writeln!(f, "  Entropy: {:.4}", target.entropy)?;
232            if !target.class_distribution.is_empty() {
233                writeln!(f, "  Class Distribution:")?;
234                for (class, count) in &target.class_distribution {
235                    writeln!(f, "    {}: {}", class, count)?;
236                }
237            }
238        }
239
240        Ok(())
241    }
242}
243
244/// Configuration for statistical summary generation
245#[derive(Debug, Clone)]
246#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
247pub struct SummaryConfig {
248    pub include_percentiles: bool,
249    pub percentile_values: Vec<u8>,
250    pub include_correlation_matrix: bool,
251    pub outlier_threshold: f64,
252    pub missing_threshold: f64,
253    pub include_distribution_analysis: bool,
254    pub feature_names: Option<Vec<String>>,
255    pub target_name: Option<String>,
256}
257
258impl Default for SummaryConfig {
259    fn default() -> Self {
260        Self {
261            include_percentiles: true,
262            percentile_values: vec![5, 10, 25, 50, 75, 90, 95],
263            include_correlation_matrix: true,
264            outlier_threshold: 1.5,
265            missing_threshold: 0.05,
266            include_distribution_analysis: true,
267            feature_names: None,
268            target_name: None,
269        }
270    }
271}
272
273/// Validation result for a single statistical property
274#[derive(Debug, Clone)]
275pub struct ValidationResult {
276    pub property: String,
277    pub passed: bool,
278    pub expected: f64,
279    pub actual: f64,
280    pub tolerance: f64,
281    pub message: String,
282}
283
284/// Comprehensive validation report for a dataset
285#[derive(Debug, Clone)]
286pub struct ValidationReport {
287    pub total_tests: usize,
288    pub passed_tests: usize,
289    pub failed_tests: usize,
290    pub results: Vec<ValidationResult>,
291    pub overall_pass: bool,
292}
293
294impl Default for ValidationReport {
295    fn default() -> Self {
296        Self::new()
297    }
298}
299
300impl ValidationReport {
301    /// Create a new validation report
302    pub fn new() -> Self {
303        Self {
304            total_tests: 0,
305            passed_tests: 0,
306            failed_tests: 0,
307            results: Vec::new(),
308            overall_pass: true,
309        }
310    }
311
312    /// Add a validation result to the report
313    pub fn add_result(&mut self, result: ValidationResult) {
314        self.total_tests += 1;
315        if result.passed {
316            self.passed_tests += 1;
317        } else {
318            self.failed_tests += 1;
319            self.overall_pass = false;
320        }
321        self.results.push(result);
322    }
323
324    /// Get the success rate as a percentage
325    pub fn success_rate(&self) -> f64 {
326        if self.total_tests == 0 {
327            0.0
328        } else {
329            (self.passed_tests as f64 / self.total_tests as f64) * 100.0
330        }
331    }
332}
333
334impl fmt::Display for ValidationReport {
335    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
336        writeln!(f, "=== Dataset Validation Report ===")?;
337        writeln!(f, "Total tests: {}", self.total_tests)?;
338        writeln!(f, "Passed: {}", self.passed_tests)?;
339        writeln!(f, "Failed: {}", self.failed_tests)?;
340        writeln!(f, "Success rate: {:.2}%", self.success_rate())?;
341        writeln!(
342            f,
343            "Overall: {}",
344            if self.overall_pass { "PASS" } else { "FAIL" }
345        )?;
346        writeln!(f)?;
347
348        for result in &self.results {
349            let status = if result.passed { "PASS" } else { "FAIL" };
350            writeln!(f, "[{}] {}: {}", status, result.property, result.message)?;
351        }
352
353        Ok(())
354    }
355}
356
357/// Dataset validation configuration
358#[derive(Debug, Clone)]
359pub struct ValidationConfig {
360    pub tolerance: f64,
361    pub min_samples: usize,
362    pub check_normality: bool,
363    pub check_correlation: bool,
364    pub check_distribution: bool,
365    pub check_outliers: bool,
366}
367
368impl Default for ValidationConfig {
369    fn default() -> Self {
370        Self {
371            tolerance: 0.1,
372            min_samples: 10,
373            check_normality: true,
374            check_correlation: true,
375            check_distribution: true,
376            check_outliers: true,
377        }
378    }
379}
380
381/// Dataset quality metrics for comprehensive quality assessment
382#[derive(Debug, Clone)]
383#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
384pub struct DatasetQualityMetrics {
385    pub overall_quality_score: f64,
386    pub completeness_score: f64,
387    pub consistency_score: f64,
388    pub validity_score: f64,
389    pub accuracy_score: f64,
390    pub uniqueness_score: f64,
391    pub timeliness_score: f64,
392    pub missing_data_ratio: f64,
393    pub outlier_ratio: f64,
394    pub duplicate_ratio: f64,
395    pub data_type_violations: usize,
396    pub range_violations: usize,
397    pub pattern_violations: usize,
398    pub fingerprint: String,
399    pub quality_issues: Vec<String>,
400    pub recommendations: Vec<String>,
401}
402
403impl DatasetQualityMetrics {
404    /// Calculate overall quality score from individual metrics
405    pub fn calculate_overall_score(&mut self) {
406        let weights = [0.25, 0.20, 0.15, 0.15, 0.10, 0.15]; // Completeness, Consistency, Validity, Accuracy, Uniqueness, Timeliness
407        let scores = [
408            self.completeness_score,
409            self.consistency_score,
410            self.validity_score,
411            self.accuracy_score,
412            self.uniqueness_score,
413            self.timeliness_score,
414        ];
415
416        self.overall_quality_score = scores
417            .iter()
418            .zip(weights.iter())
419            .map(|(score, weight)| score * weight)
420            .sum::<f64>()
421            .clamp(0.0, 100.0);
422    }
423
424    /// Add a quality issue with recommendation
425    pub fn add_issue(&mut self, issue: String, recommendation: String) {
426        self.quality_issues.push(issue);
427        self.recommendations.push(recommendation);
428    }
429
430    /// Generate quality report summary
431    pub fn generate_report(&self) -> String {
432        let mut report = String::new();
433        report.push_str("=== Dataset Quality Report ===\n");
434        report.push_str(&format!(
435            "Overall Quality Score: {:.2}/100\n",
436            self.overall_quality_score
437        ));
438        report.push_str(&format!(
439            "Completeness: {:.2}/100\n",
440            self.completeness_score
441        ));
442        report.push_str(&format!("Consistency: {:.2}/100\n", self.consistency_score));
443        report.push_str(&format!("Validity: {:.2}/100\n", self.validity_score));
444        report.push_str(&format!("Accuracy: {:.2}/100\n", self.accuracy_score));
445        report.push_str(&format!("Uniqueness: {:.2}/100\n", self.uniqueness_score));
446        report.push_str(&format!("Timeliness: {:.2}/100\n", self.timeliness_score));
447        report.push_str("\nData Issues:\n");
448        report.push_str(&format!(
449            "- Missing Data: {:.2}%\n",
450            self.missing_data_ratio * 100.0
451        ));
452        report.push_str(&format!("- Outliers: {:.2}%\n", self.outlier_ratio * 100.0));
453        report.push_str(&format!(
454            "- Duplicates: {:.2}%\n",
455            self.duplicate_ratio * 100.0
456        ));
457        report.push_str(&format!(
458            "- Type Violations: {}\n",
459            self.data_type_violations
460        ));
461        report.push_str(&format!("- Range Violations: {}\n", self.range_violations));
462        report.push_str(&format!(
463            "- Pattern Violations: {}\n",
464            self.pattern_violations
465        ));
466        report.push_str(&format!("\nFingerprint: {}\n", self.fingerprint));
467
468        if !self.quality_issues.is_empty() {
469            report.push_str("\nQuality Issues:\n");
470            for issue in &self.quality_issues {
471                report.push_str(&format!("- {}\n", issue));
472            }
473        }
474
475        if !self.recommendations.is_empty() {
476            report.push_str("\nRecommendations:\n");
477            for rec in &self.recommendations {
478                report.push_str(&format!("- {}\n", rec));
479            }
480        }
481
482        report
483    }
484}
485
486/// Data drift detection result
487#[derive(Debug, Clone)]
488#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
489pub struct DataDriftReport {
490    pub drift_detected: bool,
491    pub drift_score: f64,
492    pub drift_threshold: f64,
493    pub affected_features: Vec<String>,
494    pub drift_statistics: HashMap<String, f64>,
495    pub drift_type: String, // "covariate", "concept", "prior_probability"
496    pub detection_method: String,
497    pub confidence_level: f64,
498    pub timestamp: String,
499}
500
501impl Default for DataDriftReport {
502    fn default() -> Self {
503        Self::new()
504    }
505}
506
507impl DataDriftReport {
508    /// Create a new drift report
509    pub fn new() -> Self {
510        Self {
511            drift_detected: false,
512            drift_score: 0.0,
513            drift_threshold: 0.05,
514            affected_features: Vec::new(),
515            drift_statistics: HashMap::new(),
516            drift_type: "none".to_string(),
517            detection_method: "kolmogorov_smirnov".to_string(),
518            confidence_level: 0.95,
519            timestamp: chrono::Utc::now().to_rfc3339(),
520        }
521    }
522
523    /// Add drift detection for a feature
524    pub fn add_feature_drift(&mut self, feature_name: String, drift_statistic: f64) {
525        self.drift_statistics
526            .insert(feature_name.clone(), drift_statistic);
527        if drift_statistic > self.drift_threshold {
528            self.affected_features.push(feature_name);
529            self.drift_detected = true;
530        }
531    }
532
533    /// Calculate overall drift score
534    pub fn calculate_overall_drift(&mut self) {
535        if self.drift_statistics.is_empty() {
536            return;
537        }
538
539        self.drift_score =
540            self.drift_statistics.values().sum::<f64>() / self.drift_statistics.len() as f64;
541
542        if self.drift_score > self.drift_threshold {
543            self.drift_detected = true;
544        }
545    }
546}
547
548/// Anomaly detection result in generated data
549#[derive(Debug, Clone)]
550#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
551pub struct AnomalyDetectionResult {
552    pub anomalies_detected: bool,
553    pub anomaly_count: usize,
554    pub anomaly_ratio: f64,
555    pub anomaly_threshold: f64,
556    pub anomaly_indices: Vec<usize>,
557    pub anomaly_scores: Vec<f64>,
558    pub detection_method: String,
559    pub feature_anomalies: HashMap<String, Vec<usize>>,
560}
561
562impl Default for AnomalyDetectionResult {
563    fn default() -> Self {
564        Self::new()
565    }
566}
567
568impl AnomalyDetectionResult {
569    /// Create a new anomaly detection result
570    pub fn new() -> Self {
571        Self {
572            anomalies_detected: false,
573            anomaly_count: 0,
574            anomaly_ratio: 0.0,
575            anomaly_threshold: 0.05,
576            anomaly_indices: Vec::new(),
577            anomaly_scores: Vec::new(),
578            detection_method: "isolation_forest".to_string(),
579            feature_anomalies: HashMap::new(),
580        }
581    }
582
583    /// Add an anomaly
584    pub fn add_anomaly(&mut self, index: usize, score: f64) {
585        self.anomaly_indices.push(index);
586        self.anomaly_scores.push(score);
587        self.anomalies_detected = true;
588    }
589
590    /// Calculate anomaly statistics
591    pub fn calculate_statistics(&mut self, total_samples: usize) {
592        self.anomaly_count = self.anomaly_indices.len();
593        self.anomaly_ratio = self.anomaly_count as f64 / total_samples as f64;
594
595        if self.anomaly_ratio > self.anomaly_threshold {
596            self.anomalies_detected = true;
597        }
598    }
599}