Issue: #73 Version: 1.1.1 Status: Implemented Target: aprender 0.10.0 + entrenar 0.2.0 PMAT Work Item: APR-0073 Author: Aprender Team Date: 2026-01-21
This specification defines the integrated model evaluation and drift detection system for the aprender ecosystem.
Adhering to the Toyota Way, it transforms model evaluation from a manual, ad-hoc process into an automated,
continuous control loop (Jidoka).
Crucially, this framework is not isolated. It integrates directly with:
- Renacer: To trace evaluation overhead and ensure "Science" (metrics) doesn't kill "Systems" (latency).
- TruenoDB: To store historical performance data for longitudinal drift analysis.
- Entrenar: To trigger automated retraining loops when drift is detected (Andon Cord).
- Brick Architecture: To provide standardized, zero-JS visualizations for evaluation reports.
Currently, aprender evaluation is manual and fragmented:
- No Standardization: Users write custom loops for accuracy/F1.
- No History: "It worked yesterday" is anecdotal, not data.
- No Automation: Retraining is a manual decision, leading to stale models.
Add aprender::eval module with:
- Standardized Metrics: Classification, Regression, Clustering (Parity with sklearn).
- Drift Detection: Statistical tests (KS, Chi-Square, PSI) to detect Data Drift and Concept Drift.
- Renacer Integration: Every evaluation is a Span; every metric is a Tag.
- Entrenar Hooks: "If
drift > threshold, triggerentrenar::retrain."
To ensure ecosystem consistency, the following "Gurus" (Workflows) MUST use aprender::eval:
- Fine-Tuning Guru (LoRA/QLoRA): Must use
ModelEvaluatorto compare Base vs. Fine-Tuned performance (e.g., "Win Rate" or specific task metrics). - Quantization Guru (PTQ/QAT): Must use
ModelEvaluatorto quantify accuracy degradation (e.g., FP16 vs INT4). Relative accuracy drop > 1% triggers a failure. - Distillation Guru: Must use
ModelEvaluatorto verify Student vs. Teacher parity.
┌─────────────────────────────────────────────────────────────────────────┐
│ aprender::eval Module │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌──────────────────┐ ┌───────────────────────┐ │
│ │ metrics/ │ │ evaluator/ │ │ drift/ │ │
│ │ │ │ │ │ │ │
│ │ - accuracy │ │ - ModelEvaluator │ │ - DriftDetector │ │
│ │ - f1_score │ │ - CrossValidate │ │ - KSTest / PSI │ │
│ │ - confusion_mat │ │ - Leaderboard │ │ - AndonCallback │ │
│ └────────┬────────┘ └────────┬─────────┘ └──────────┬────────────┘ │
│ │ │ │ │
│ └────────────────────┼───────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────┐ │
│ │ renacer::Trace │ (System Observability) │
│ │ - span: "eval" │ │
│ │ - tag: "acc=0.95" │ │
│ └───────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────┐ │
│ │ entrenar::Trainer │ (Correction Loop) │
│ │ - trigger_retrain() │ │
│ └───────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
Standardized metrics ensuring consistent calculation across the ecosystem.
/// Averaging strategy for multi-class metrics
#[derive(Clone, Copy, Debug)]
pub enum Average {
/// Calculate metrics for each label, return unweighted mean (Macro)
Macro,
/// Calculate metrics globally by counting total TP, FP, FN (Micro)
Micro,
/// Weighted mean by support (number of true instances per label)
Weighted,
/// Return array of metrics per class (no averaging)
None,
}
/// Compute classification accuracy
/// accuracy = (TP + TN) / (TP + TN + FP + FN)
pub fn accuracy(y_pred: &[usize], y_true: &[usize]) -> f32;
/// Compute precision score
/// precision = TP / (TP + FP)
pub fn precision(y_pred: &[usize], y_true: &[usize], average: Average) -> f32;
/// Compute recall score
/// recall = TP / (TP + FN)
pub fn recall(y_pred: &[usize], y_true: &[usize], average: Average) -> f32;
/// Compute F1 score (harmonic mean of precision and recall)
/// F1 = 2 * (precision * recall) / (precision + recall)
pub fn f1_score(y_pred: &[usize], y_true: &[usize], average: Average) -> f32;
/// Compute confusion matrix
/// Returns Matrix<usize> where element [i,j] is count of samples
/// with true label i and predicted label j
pub fn confusion_matrix(y_pred: &[usize], y_true: &[usize]) -> Matrix<usize>;
/// Generate text classification report (sklearn-style)
/// *Mieruka* (Visual Control) for CLI usage.
pub fn classification_report(y_pred: &[usize], y_true: &[usize]) -> String;The ModelEvaluator manages the lifecycle of testing and comparison. It is responsible for reporting metrics to
Renacer.
/// Configuration for model evaluation
#[derive(Clone, Debug)]
pub struct EvalConfig {
/// Metrics to compute
pub metrics: Vec<Metric>,
/// Number of cross-validation folds (0 = no CV)
pub cv_folds: usize,
/// Random seed for reproducibility
pub seed: u64,
/// Parallel evaluation (requires rayon feature)
pub parallel: bool,
/// Enable Renacer tracing (defaults to true if feature enabled)
pub trace_enabled: bool,
}
/// Available evaluation metrics
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Metric {
// Classification
Accuracy,
Precision(Average),
Recall(Average),
F1(Average),
// Regression
R2,
MSE,
MAE,
RMSE,
// Clustering
Silhouette,
Inertia,
}
/// Model evaluation results
#[derive(Clone, Debug)]
pub struct EvalResult {
pub model_name: String,
pub scores: HashMap<Metric, f32>,
pub cv_scores: Option<Vec<f32>>,
pub cv_mean: Option<f32>,
pub cv_std: Option<f32>,
pub inference_time_ms: f64,
/// Link to Renacer trace ID
pub trace_id: Option<String>,
}
/// Leaderboard for comparing multiple models
#[derive(Clone, Debug)]
pub struct Leaderboard {
pub results: Vec<EvalResult>,
pub primary_metric: Metric,
}
impl Leaderboard {
/// Print formatted leaderboard to stdout (Mieruka)
pub fn print(&self);
/// Export as markdown table for documentation
pub fn to_markdown(&self) -> String;
/// Get best model by primary metric
pub fn best(&self) -> &EvalResult;
}
/// Main evaluator struct
pub struct ModelEvaluator {
config: EvalConfig,
}
impl ModelEvaluator {
pub fn new(config: EvalConfig) -> Self;
/// Evaluate single model
pub fn evaluate<M: Estimator>(
&self,
model: &M,
x: &Matrix<f32>,
y: &[usize],
) -> Result<EvalResult>;
/// Compare multiple models, return leaderboard
pub fn compare<M: Estimator>(
&self,
models: &[(&str, &M)],
x: &Matrix<f32>,
y: &[usize],
) -> Result<Leaderboard>;
}Implements Jidoka (Automation with a Human Touch). Detects when the process is out of control and signals for help (Retraining).
/// Statistical test for drift detection
#[derive(Clone, Copy, Debug)]
pub enum DriftTest {
/// Kolmogorov-Smirnov test (continuous features)
KS { threshold: f64 },
/// Chi-square test (categorical features)
ChiSquare { threshold: f64 },
/// Population Stability Index (Standard industry metric)
PSI { threshold: f64 },
/// Wasserstein Distance (Earth Mover's Distance)
Wasserstein { threshold: f64 },
}
/// Drift detection result
#[derive(Clone, Debug)]
pub struct DriftResult {
pub feature: String,
pub test: DriftTest,
pub statistic: f64,
pub p_value: f64,
pub drifted: bool,
pub severity: Severity,
}
#[derive(Clone, Copy, Debug)]
pub enum Severity {
None,
Warning, // Log warning, continue
Critical, // Stop inference or trigger retrain
}
/// Callback type for drift events
pub type DriftCallback = Box<dyn Fn(&[DriftResult]) -> Result<()> + Send + Sync>;
/// Drift detector with retraining hooks
pub struct DriftDetector {
tests: Vec<DriftTest>,
baseline: Option<Matrix<f32>>,
callbacks: Vec<DriftCallback>,
}
impl DriftDetector {
pub fn new(tests: Vec<DriftTest>) -> Self;
/// Set baseline distribution (e.g., training data)
pub fn set_baseline(&mut self, data: &Matrix<f32>);
/// Check new data for drift
pub fn check(&self, current: &Matrix<f32>) -> Vec<DriftResult>;
/// Register callback for drift events (Andon Cord)
pub fn on_drift<F>(&mut self, callback: F)
where
F: Fn(&[DriftResult]) -> Result<()> + Send + Sync + 'static;
/// Check and trigger callbacks if drift detected
pub fn check_and_trigger(&self, current: &Matrix<f32>) -> Result<Vec<DriftResult>>;
}The bridge to the training loop.
#[cfg(feature = "entrenar")]
pub mod retrain {
use entrenar::Trainer;
/// Retraining trigger policy
#[derive(Clone, Debug)]
pub enum RetrainPolicy {
/// Retrain if > N% of features drift
FeatureCount { count: usize },
/// Retrain if any critical feature drifts
CriticalFeature { names: Vec<String> },
/// Retrain if prediction confidence drops below threshold
ConfidenceDrop { threshold: f32 },
}
/// Auto-retrainer with drift detection
pub struct AutoRetrainer {
drift_detector: DriftDetector,
policy: RetrainPolicy,
config: RetrainConfig,
}
impl AutoRetrainer {
/// Ingest batch, check drift, trigger retrain if policy met
pub fn process_batch(&mut self, batch: &Batch) -> Result<Action>;
}
pub enum Action {
None,
WarningLogged,
RetrainTriggered(String), // Job ID
}
}aprender/src/
├── eval/
│ ├── mod.rs # Module exports
│ ├── evaluator.rs # ModelEvaluator, Leaderboard
│ ├── drift.rs # DriftDetector, statistical tests
│ └── retrain.rs # AutoRetrainer (feature-gated)
├── metrics/
│ ├── mod.rs # Re-exports
│ ├── regression.rs # Existing: r_squared, mse, mae, rmse
│ ├── clustering.rs # Existing: silhouette_score, inertia
│ └── classification.rs # NEW: accuracy, f1, precision, recall
[features]
default = []
eval = ["dep:statrs"]
# Enable eval module
entrenar = ["eval", "dep:entrenar"]
# Enable retraining integration
renacer = ["dep:renacer"] # Enable tracing integration| Crate | Version | Purpose | Feature |
|---|---|---|---|
| aprender | 0.9.x | Base library | - |
| entrenar | 0.2.x | Retraining | entrenar |
| statrs | 0.16 | Statistical tests | eval |
| renacer | 0.8.x | Tracing/Observability | renacer |
| rayon | 1.8 | Parallel evaluation | parallel |
| ID | Task | Hours | Priority |
|---|---|---|---|
| APR-073-1 | Classification metrics (Accuracy, F1, Matrix) | 8 | P0 |
| APR-073-2 | ModelEvaluator + Leaderboard + Renacer Trace | 16 | P0 |
| APR-073-3 | Cross-validation integration | 8 | P1 |
| APR-073-4 | Drift detection (KS, Chi-sq, PSI) | 16 | P1 |
| APR-073-5 | Entrenar integration (Andon loop) | 16 | P2 |
| APR-073-6 | Property tests (Proptest) + Documentation | 12 | P0 |
Total: 76 hours
- Test Coverage: ≥95% via
make coverage(Strict).- Performance Constraint: Coverage suite must run in < 5 minutes to ensure rapid feedback loops (Toyota Way).
- Mutation Score: ≥85% (cargo-mutants).
- Property Tests: 1000+ iterations per metric.
- No Panics: All public APIs must return
Result. - WASM Compatible: Core logic must compile to WASM (excluding
entrenarfeature). - Compliance: Must pass
pmat comply(full strict mode).
use aprender::prelude::*;
use aprender::eval::{ModelEvaluator, EvalConfig, Metric, DriftDetector, DriftTest};
fn main() -> Result<()> {
// 1. Configure Evaluator
let evaluator = ModelEvaluator::new(EvalConfig {
metrics: vec![
Metric::Accuracy,
Metric::F1(Average::Weighted),
],
cv_folds: 5,
seed: 42,
parallel: true,
trace_enabled: true,
});
// 2. Compare Models (Leaderboard)
let leaderboard = evaluator.compare(&[
("RandomForest", &rf_model),
("GradientBoosting", &gb_model),
], &x_test, &y_test)?;
leaderboard.print();
// Output:
// ┌─────────────────┬──────────┬─────────┐
// │ Model │ Accuracy │ F1 │
// ├─────────────────┼──────────┼─────────┤
// │ GradientBoosting│ 0.9423 │ 0.9401 │
// │ RandomForest │ 0.9318 │ 0.9295 │
// └─────────────────┴──────────┴─────────┘
// 3. Setup Drift Detection (Jidoka)
let mut detector = DriftDetector::new(vec![
DriftTest::PSI { threshold: 0.1 }, // Population Stability Index
DriftTest::KS { threshold: 0.05 },
]);
detector.set_baseline(&x_train);
// 4. Register Andon Cord (Retraining)
detector.on_drift(|results| {
println!("⚠️ DRIFT DETECTED: Triggering Retraining Protocol...");
// In real app: entrenar::trigger_job(...)
Ok(())
});
Ok(())
}To ensure this specification is implemented correctly, the following 10-Point PMAT Checklist must be verified before release (v1.0.0).
- Unit Tests: All metrics (
accuracy,f1, etc.) matchsklearnreference values to within1e-6precision. - Property Tests:
proptestpasses 100,000 iterations (up from 10k) for all metrics, verifying invariants (e.g.,accuracy <= 1.0,F1 symmetry). - Mutation Testing:
cargo mutantsscore > 90% (up from 85%) formetrics/anddrift/modules, ensuring no "pseudo-tested" code. - Integration:
entrenarcallback successfully triggers a dummy job within 10ms when drift is simulated (Real-time Andon). - Drift Statistical Power: KS and Chi-Square tests demonstrate p-value calibration
(uniform under null hypothesis) via
examples/calibration_check.rs. - Renacer Tracing:
ModelEvaluatorproduces a visible Span injaegerwith correct tags, adding < 2% overhead to inference. - Performance Complexity: Metric calculation complexity verified as O(N); overhead must be < 5ms for N=1M samples on Reference Hardware.
- WASM:
aprender-evalcompiles towasm32-unknown-unknownand runs in browser with zero allocations in the hot loop. - Documentation: All examples in
mdbookuse{{#include}}and compile; "Documentation as Code" principle enforced. - Compliance:
pmat complypasses with 0 violations (Grade A, clean logs, nounwrap(), nopanic!).
- Drift Detection: Gama, J., et al. (2014). "A Survey on Concept Drift Adaptation." ACM Computing Surveys. DOI:10.1145/2523813
- Continuous Learning (Jidoka): Lesort, T., et al. (2020). "Continual Learning for Robotics: Definition, Framework, Learning Strategies, Opportunities and Challenges." Information Fusion. DOI:10.1016/j.inffus.2019.12.004
- LoRA Evaluation: Hu, E. J., et al. (2021). "LoRA: Low-Rank Adaptation of Large Language Models." ICLR 2022. arXiv:2106.09685
- Quantization (QLoRA): Dettmers, T., et al. (2023). "QLoRA: Efficient Finetuning of Quantized LLMs." NeurIPS 2023. arXiv:2305.14314
- MLOps Architecture: Kreuzberger, D., et al. (2023). "Machine Learning Operations (MLOps): Overview, Definition, and Architecture." IEEE Access.
- Toyota Way: Jidoka (Automation), Mieruka (Visual Control).
- Vision Sync:
entrenar/docs/specifications/paiml-sai-vision-sync.md - Sklearn: Classification Report
- PSI: Population Stability Index Guide
- Brick Architecture:
docs/specifications/brick-architecture.md(Zero-JS Visualization Standard)
The Model Evaluation Framework (APR-073) has been fully implemented and verified against this specification.
- Classification Metrics: Verified with 8
sklearnparity tests (1e-6 precision). - Model Evaluator: Leaderboard and performance tracking verified.
- Cross-Validation: KFold integration complete.
- Drift Detection: KS, Chi-Square, and PSI verified via
examples/calibration_check.rsandexamples/drift_simulation.rs. - Entrenar Integration:
AutoRetrainerand Andon loop verified with <10ms callback latency. - WASM Compatibility: Verified core logic compilation for
wasm32-unknown-unknown. - Testing: 63 module tests + 17 property tests (100k iterations each) passing.
- H0 Falsified? NO. All protocols passed as of Jan 21, 2026.
To ensure "Documentation is Code" (The Toyota Way), all examples in the mdbook MUST be sourced directly from compiled,
tested Rust files using the {{#include ...}} directive.
DO NOT write code blocks manually in markdown.
\```rust
let acc = accuracy(&y_pred, &y_true); // Might not compile!
\```\```rust
{{#include ../../examples/eval_metrics.rs:10:20}}
\```Run mdbook test to verify that all included snippets compile and pass tests.