Skip to main content

datasynth_eval/ml/
mod.rs

1//! ML-readiness evaluation module.
2//!
3//! Validates that generated data is suitable for machine learning tasks
4//! including feature distributions, label quality, and graph structure.
5//!
6//! Also provides baseline task definitions for benchmarking synthetic data.
7
8mod anomaly_scoring;
9mod baselines;
10mod cross_modal;
11mod detectability;
12mod domain_gap;
13mod embedding_readiness;
14mod feature_quality;
15mod features;
16mod gnn_readiness;
17mod graph;
18mod labels;
19mod scheme_detectability;
20mod splits;
21mod temporal_fidelity;
22
23pub use anomaly_scoring::{
24    AnomalyScoringAnalysis, AnomalyScoringAnalyzer, AnomalyScoringThresholds, ScoredRecord,
25};
26pub use baselines::{
27    get_accounting_baseline_tasks, BaselineAlgorithm, BaselineConfig, BaselineEvaluation,
28    BaselineResult, BaselineSummary, BaselineTask, ClassificationMetrics, ExpectedMetrics,
29    MLTaskType, PerformanceGrade, RankingMetrics, RegressionMetrics,
30};
31pub use cross_modal::{
32    CrossModalAnalysis, CrossModalAnalyzer, CrossModalThresholds, EntityModalData,
33};
34pub use detectability::{DetectabilityAnalyzer, DetectabilityReport};
35pub use domain_gap::{
36    DistributionSample, DomainGapAnalysis, DomainGapAnalyzer, DomainGapDetail, DomainGapThresholds,
37};
38pub use embedding_readiness::{
39    EmbeddingInput, EmbeddingReadinessAnalysis, EmbeddingReadinessAnalyzer,
40    EmbeddingReadinessThresholds,
41};
42pub use feature_quality::{
43    FeatureQualityAnalysis, FeatureQualityAnalyzer, FeatureQualityThresholds, FeatureVector,
44};
45pub use features::{FeatureAnalysis, FeatureAnalyzer, FeatureStats};
46pub use gnn_readiness::GraphData as GnnGraphData;
47pub use gnn_readiness::{GnnReadinessAnalysis, GnnReadinessAnalyzer, GnnReadinessThresholds};
48pub use graph::{GraphAnalysis, GraphAnalyzer, GraphMetrics};
49pub use labels::{LabelAnalysis, LabelAnalyzer, LabelDistribution};
50pub use scheme_detectability::{
51    SchemeDetectabilityAnalysis, SchemeDetectabilityAnalyzer, SchemeDetectabilityThresholds,
52    SchemeRecord,
53};
54pub use splits::{SplitAnalysis, SplitAnalyzer, SplitMetrics};
55pub use temporal_fidelity::{
56    TemporalFidelityAnalysis, TemporalFidelityAnalyzer, TemporalFidelityThresholds, TemporalRecord,
57};
58
59use serde::{Deserialize, Serialize};
60
61/// Combined ML-readiness evaluation results.
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct MLReadinessEvaluation {
64    /// Feature distribution analysis.
65    pub features: Option<FeatureAnalysis>,
66    /// Label quality analysis.
67    pub labels: Option<LabelAnalysis>,
68    /// Train/test split analysis.
69    pub splits: Option<SplitAnalysis>,
70    /// Graph structure analysis.
71    pub graph: Option<GraphAnalysis>,
72    /// Anomaly scoring analysis.
73    #[serde(default, skip_serializing_if = "Option::is_none")]
74    pub anomaly_scoring: Option<AnomalyScoringAnalysis>,
75    /// Feature quality analysis.
76    #[serde(default, skip_serializing_if = "Option::is_none")]
77    pub feature_quality: Option<FeatureQualityAnalysis>,
78    /// GNN readiness analysis.
79    #[serde(default, skip_serializing_if = "Option::is_none")]
80    pub gnn_readiness: Option<GnnReadinessAnalysis>,
81    /// Domain gap analysis.
82    #[serde(default, skip_serializing_if = "Option::is_none")]
83    pub domain_gap: Option<DomainGapAnalysis>,
84    /// Temporal fidelity analysis.
85    #[serde(default, skip_serializing_if = "Option::is_none")]
86    pub temporal_fidelity: Option<TemporalFidelityAnalysis>,
87    /// Scheme detectability analysis.
88    #[serde(default, skip_serializing_if = "Option::is_none")]
89    pub scheme_detectability: Option<SchemeDetectabilityAnalysis>,
90    /// Cross-modal consistency analysis.
91    #[serde(default, skip_serializing_if = "Option::is_none")]
92    pub cross_modal: Option<CrossModalAnalysis>,
93    /// Embedding readiness analysis.
94    #[serde(default, skip_serializing_if = "Option::is_none")]
95    pub embedding_readiness: Option<EmbeddingReadinessAnalysis>,
96    /// Overall ML-readiness score (0.0-1.0).
97    pub overall_score: f64,
98    /// Whether data meets ML-readiness criteria.
99    pub passes: bool,
100    /// ML-readiness issues found.
101    pub issues: Vec<String>,
102    /// ML-readiness failures (alias for issues).
103    pub failures: Vec<String>,
104}
105
106impl MLReadinessEvaluation {
107    /// Create a new empty evaluation.
108    pub fn new() -> Self {
109        Self {
110            features: None,
111            labels: None,
112            splits: None,
113            graph: None,
114            anomaly_scoring: None,
115            feature_quality: None,
116            gnn_readiness: None,
117            domain_gap: None,
118            temporal_fidelity: None,
119            scheme_detectability: None,
120            cross_modal: None,
121            embedding_readiness: None,
122            overall_score: 1.0,
123            passes: true,
124            issues: Vec::new(),
125            failures: Vec::new(),
126        }
127    }
128
129    /// Check all results against thresholds.
130    pub fn check_thresholds(&mut self, thresholds: &crate::config::EvaluationThresholds) {
131        self.issues.clear();
132        self.failures.clear();
133        let mut scores = Vec::new();
134
135        if let Some(ref labels) = self.labels {
136            // Check anomaly rate is within expected range
137            if labels.anomaly_rate < thresholds.anomaly_rate_min {
138                self.issues.push(format!(
139                    "Anomaly rate {} < {} (min threshold)",
140                    labels.anomaly_rate, thresholds.anomaly_rate_min
141                ));
142            }
143            if labels.anomaly_rate > thresholds.anomaly_rate_max {
144                self.issues.push(format!(
145                    "Anomaly rate {} > {} (max threshold)",
146                    labels.anomaly_rate, thresholds.anomaly_rate_max
147                ));
148            }
149
150            // Check label coverage
151            if labels.label_coverage < thresholds.label_coverage_min {
152                self.issues.push(format!(
153                    "Label coverage {} < {} (threshold)",
154                    labels.label_coverage, thresholds.label_coverage_min
155                ));
156            }
157
158            scores.push(labels.quality_score);
159        }
160
161        if let Some(ref splits) = self.splits {
162            if !splits.is_valid {
163                self.issues
164                    .push("Train/test split validation failed".to_string());
165            }
166            scores.push(if splits.is_valid { 1.0 } else { 0.0 });
167        }
168
169        if let Some(ref graph) = self.graph {
170            if graph.connectivity_score < thresholds.graph_connectivity_min {
171                self.issues.push(format!(
172                    "Graph connectivity {} < {} (threshold)",
173                    graph.connectivity_score, thresholds.graph_connectivity_min
174                ));
175            }
176            scores.push(graph.connectivity_score);
177        }
178
179        if let Some(ref features) = self.features {
180            scores.push(features.quality_score);
181        }
182
183        // New ML enrichment evaluators
184        if let Some(ref as_eval) = self.anomaly_scoring {
185            if !as_eval.passes {
186                self.issues.extend(as_eval.issues.clone());
187            }
188            scores.push(as_eval.anomaly_separability);
189        }
190        if let Some(ref fq_eval) = self.feature_quality {
191            if !fq_eval.passes {
192                self.issues.extend(fq_eval.issues.clone());
193            }
194            scores.push(fq_eval.feature_quality_score);
195        }
196        if let Some(ref gnn_eval) = self.gnn_readiness {
197            if !gnn_eval.passes {
198                self.issues.extend(gnn_eval.issues.clone());
199            }
200            scores.push(gnn_eval.gnn_readiness_score);
201        }
202        if let Some(ref dg_eval) = self.domain_gap {
203            if !dg_eval.passes {
204                self.issues.extend(dg_eval.issues.clone());
205            }
206            // Domain gap is inverted: lower = better, so score = 1 - gap
207            scores.push(1.0 - dg_eval.domain_gap_score);
208        }
209        if let Some(ref tf_eval) = self.temporal_fidelity {
210            if !tf_eval.passes {
211                self.issues.extend(tf_eval.issues.clone());
212            }
213            scores.push(tf_eval.temporal_fidelity_score);
214        }
215        if let Some(ref sd_eval) = self.scheme_detectability {
216            if !sd_eval.passes {
217                self.issues.extend(sd_eval.issues.clone());
218            }
219            scores.push(sd_eval.detectability_score);
220        }
221        if let Some(ref cm_eval) = self.cross_modal {
222            if !cm_eval.passes {
223                self.issues.extend(cm_eval.issues.clone());
224            }
225            scores.push(cm_eval.consistency_score);
226        }
227        if let Some(ref er_eval) = self.embedding_readiness {
228            if !er_eval.passes {
229                self.issues.extend(er_eval.issues.clone());
230            }
231            scores.push(er_eval.embedding_readiness_score);
232        }
233
234        self.overall_score = if scores.is_empty() {
235            1.0
236        } else {
237            scores.iter().sum::<f64>() / scores.len() as f64
238        };
239
240        // Sync failures with issues
241        self.failures = self.issues.clone();
242        self.passes = self.issues.is_empty();
243    }
244}
245
246impl Default for MLReadinessEvaluation {
247    fn default() -> Self {
248        Self::new()
249    }
250}