VBAF.ML.Regression.ps1

#Requires -Version 5.1
<#
.SYNOPSIS
    Classic Regression Algorithms for Machine Learning
.DESCRIPTION
    Implements classic regression algorithms from scratch.
    Designed as a TEACHING resource - every step explained.
    Algorithms included:
      - Linear Regression (OLS - Ordinary Least Squares)
      - Ridge Regression (L2 regularization)
      - Lasso Regression (L1 regularization)
      - Logistic Regression (binary & multiclass)
    Utilities included:
      - Feature scaling (StandardScaler, MinMaxScaler)
      - Cross-validation (k-fold)
      - Metrics (MSE, RMSE, MAE, R2, Accuracy)
    Standalone - no external VBAF dependencies required.
.NOTES
    Part of VBAF - Phase 4 Machine Learning Module
    PS 5.1 compatible
    Teaching project - math shown explicitly, no black boxes!
#>

$basePath = $PSScriptRoot

# ============================================================
# TEACHING NOTE: What is Regression?
# Regression finds the relationship between:
# - Input features X (what we know)
# - Output target y (what we want to predict)
# We find weights W such that: y_hat = X * W
# Then minimize the error between y_hat and y.
# ============================================================

# ============================================================
# FEATURE SCALING UTILITIES
# ============================================================
# TEACHING NOTE: Why scale features?
# If one feature is 0-1 and another is 0-1000000,
# the large feature dominates learning.
# Scaling puts all features on equal footing.
# ============================================================

class StandardScaler {
    # Transforms features to mean=0, std=1
    # Formula: z = (x - mean) / std
    [double[]] $Mean
    [double[]] $Std
    [bool]     $IsFitted = $false

    [void] Fit([double[][]]$X) {
        $nFeatures  = $X[0].Length
        $this.Mean  = @(0.0) * $nFeatures
        $this.Std   = @(1.0) * $nFeatures
        $nSamples   = $X.Length

        # Calculate mean for each feature
        for ($f = 0; $f -lt $nFeatures; $f++) {
            $sum = 0.0
            foreach ($row in $X) { $sum += $row[$f] }
            $this.Mean[$f] = $sum / $nSamples
        }

        # Calculate std for each feature
        for ($f = 0; $f -lt $nFeatures; $f++) {
            $sumSq = 0.0
            foreach ($row in $X) {
                $diff   = $row[$f] - $this.Mean[$f]
                $sumSq += $diff * $diff
            }
            $variance       = $sumSq / $nSamples
            $this.Std[$f]   = [Math]::Max([Math]::Sqrt($variance), 1e-8)
        }
        $this.IsFitted = $true
    }

    [double[][]] Transform([double[][]]$X) {
        $result = @()
        foreach ($row in $X) {
            $scaled = @(0.0) * $row.Length
            for ($f = 0; $f -lt $row.Length; $f++) {
                $scaled[$f] = ($row[$f] - $this.Mean[$f]) / $this.Std[$f]
            }
            $result += ,$scaled
        }
        return $result
    }

    [double[][]] FitTransform([double[][]]$X) {
        $this.Fit($X)
        return $this.Transform($X)
    }

    [double[]] InverseTransform([double[]]$y) {
        $result = @(0.0) * $y.Length
        for ($i = 0; $i -lt $y.Length; $i++) {
            $result[$i] = $y[$i] * $this.Std[0] + $this.Mean[0]
        }
        return $result
    }
}

class MinMaxScaler {
    # Transforms features to range [0, 1]
    # Formula: z = (x - min) / (max - min)
    [double[]] $Min
    [double[]] $Max
    [bool]     $IsFitted = $false

    [void] Fit([double[][]]$X) {
        $nFeatures = $X[0].Length
        $this.Min  = @(0.0) * $nFeatures
        $this.Max  = @(1.0) * $nFeatures

        for ($f = 0; $f -lt $nFeatures; $f++) {
            $minVal = [double]::MaxValue
            $maxVal = [double]::MinValue
            foreach ($row in $X) {
                if ($row[$f] -lt $minVal) { $minVal = $row[$f] }
                if ($row[$f] -gt $maxVal) { $maxVal = $row[$f] }
            }
            $this.Min[$f] = $minVal
            $this.Max[$f] = $maxVal
        }
        $this.IsFitted = $true
    }

    [double[][]] Transform([double[][]]$X) {
        $result = @()
        foreach ($row in $X) {
            $scaled = @(0.0) * $row.Length
            for ($f = 0; $f -lt $row.Length; $f++) {
                $range      = [Math]::Max($this.Max[$f] - $this.Min[$f], 1e-8)
                $scaled[$f] = ($row[$f] - $this.Min[$f]) / $range
            }
            $result += ,$scaled
        }
        return $result
    }

    [double[][]] FitTransform([double[][]]$X) {
        $this.Fit($X)
        return $this.Transform($X)
    }
}

# ============================================================
# METRICS
# ============================================================
# TEACHING NOTE: How do we measure how good our model is?
# MSE = Mean Squared Error - penalizes large errors heavily
# RMSE = Root Mean Squared Error - same units as target
# MAE = Mean Absolute Error - robust to outliers
# R2 = R-squared - 1.0 = perfect, 0.0 = baseline
# ============================================================

function Get-RegressionMetrics {
    param([double[]]$yTrue, [double[]]$yPred)

    $n      = $yTrue.Length
    $mse    = 0.0
    $mae    = 0.0
    $yMean  = ($yTrue | Measure-Object -Average).Average

    $ssTot  = 0.0
    $ssRes  = 0.0

    for ($i = 0; $i -lt $n; $i++) {
        $err    = $yTrue[$i] - $yPred[$i]
        $mse   += $err * $err
        $mae   += [Math]::Abs($err)
        $ssRes += $err * $err
        $ssTot += ($yTrue[$i] - $yMean) * ($yTrue[$i] - $yMean)
    }

    $mse  = $mse / $n
    $rmse = [Math]::Sqrt($mse)
    $mae  = $mae / $n
    $r2   = if ($ssTot -gt 1e-10) { 1.0 - ($ssRes / $ssTot) } else { 0.0 }

    return @{
        MSE  = [Math]::Round($mse,  6)
        RMSE = [Math]::Round($rmse, 6)
        MAE  = [Math]::Round($mae,  6)
        R2   = [Math]::Round($r2,   6)
    }
}

function Get-ClassificationMetrics {
    param([int[]]$yTrue, [int[]]$yPred)

    $n       = $yTrue.Length
    $correct = 0
    for ($i = 0; $i -lt $n; $i++) {
        if ($yTrue[$i] -eq $yPred[$i]) { $correct++ }
    }
    $accuracy = $correct / $n

    return @{
        Accuracy = [Math]::Round($accuracy, 6)
        Correct  = $correct
        Total    = $n
    }
}

# ============================================================
# LINEAR REGRESSION (OLS)
# ============================================================
# TEACHING NOTE: Linear Regression finds weights W such that:
# y_hat = X*W + bias
# We minimize: Loss = (1/n) * sum((y - y_hat)^2) <- MSE
# Using gradient descent:
# W = W - learningRate * dLoss/dW
# dLoss/dW = (2/n) * X^T * (y_hat - y)
# ============================================================

class LinearRegression {
    [double[]] $Weights
    [double]   $Bias
    [double]   $LearningRate
    [int]      $MaxIter
    [bool]     $IsFitted = $false
    [System.Collections.Generic.List[double]] $LossHistory

    LinearRegression() {
        $this.LearningRate = 0.0001
        $this.MaxIter      = 1000
        $this.LossHistory  = [System.Collections.Generic.List[double]]::new()
    }

    LinearRegression([double]$learningRate, [int]$maxIter) {
        $this.LearningRate = $learningRate
        $this.MaxIter      = $maxIter
        $this.LossHistory  = [System.Collections.Generic.List[double]]::new()
    }

    # Predict: y_hat = X*W + bias
    [double[]] Predict([double[][]]$X) {
        $yHat = @(0.0) * $X.Length
        for ($i = 0; $i -lt $X.Length; $i++) {
            $dot = $this.Bias
            for ($j = 0; $j -lt $X[$i].Length; $j++) {
                $dot += $X[$i][$j] * $this.Weights[$j]
            }
            $yHat[$i] = $dot
        }
        return $yHat
    }

    # Train using gradient descent
    [void] Fit([double[][]]$X, [double[]]$y) {
        $n          = $X.Length
        $nFeatures  = $X[0].Length
        $this.Weights = @(0.0) * $nFeatures
        $this.Bias    = 0.0

        for ($iter = 0; $iter -lt $this.MaxIter; $iter++) {
            $yHat = $this.Predict($X)

            # Compute gradients
            $dW   = @(0.0) * $nFeatures
            $dB   = 0.0
            $loss = 0.0

            for ($i = 0; $i -lt $n; $i++) {
                $err   = $yHat[$i] - $y[$i]
                $loss += $err * $err
                $dB   += $err
                for ($j = 0; $j -lt $nFeatures; $j++) {
                    $dW[$j] += $err * $X[$i][$j]
                }
            }

            $loss = $loss / $n
            $this.LossHistory.Add($loss)

            # Update weights
            $this.Bias -= $this.LearningRate * (2.0 / $n) * $dB
            for ($j = 0; $j -lt $nFeatures; $j++) {
                $this.Weights[$j] -= $this.LearningRate * (2.0 / $n) * $dW[$j]
            }
        }
        $this.IsFitted = $true
    }

    [void] PrintSummary() {
        Write-Host ""
        Write-Host "╔══════════════════════════════════════╗" -ForegroundColor Cyan
        Write-Host "║ Linear Regression Summary ║" -ForegroundColor Cyan
        Write-Host "╠══════════════════════════════════════╣" -ForegroundColor Cyan
        Write-Host ("║ Bias : {0,-24}║" -f [Math]::Round($this.Bias, 6))   -ForegroundColor White
        for ($j = 0; $j -lt $this.Weights.Length; $j++) {
            Write-Host ("║ Weight[{0}] : {1,-24}║" -f $j, [Math]::Round($this.Weights[$j], 6)) -ForegroundColor White
        }
        $finalLoss = if ($this.LossHistory.Count -gt 0) { $this.LossHistory[-1] } else { 0.0 }
        Write-Host ("║ Final Loss: {0,-24}║" -f [Math]::Round($finalLoss, 6))  -ForegroundColor Magenta
        Write-Host "╚══════════════════════════════════════╝" -ForegroundColor Cyan
        Write-Host ""
    }
}

# ============================================================
# RIDGE REGRESSION (L2 Regularization)
# ============================================================
# TEACHING NOTE: Ridge adds a penalty for large weights:
# Loss = MSE + lambda * sum(W^2) <- L2 penalty
# This SHRINKS weights toward zero but never to exactly zero.
# Use Ridge when: many features, all somewhat useful.
# Lambda controls strength: larger = more shrinkage.
# ============================================================

class RidgeRegression : LinearRegression {
    [double] $Lambda   # Regularization strength

    RidgeRegression([double]$lambda) : base(0.01, 1000) {
        $this.Lambda = $lambda
    }

    RidgeRegression([double]$lambda, [double]$lr, [int]$maxIter) : base($lr, $maxIter) {
        $this.Lambda = $lambda
    }

    [void] Fit([double[][]]$X, [double[]]$y) {
        $n         = $X.Length
        $nFeatures = $X[0].Length
        $this.Weights = @(0.0) * $nFeatures
        $this.Bias    = 0.0

        for ($iter = 0; $iter -lt $this.MaxIter; $iter++) {
            $yHat = $this.Predict($X)

            $dW   = @(0.0) * $nFeatures
            $dB   = 0.0
            $loss = 0.0

            for ($i = 0; $i -lt $n; $i++) {
                $err   = $yHat[$i] - $y[$i]
                $loss += $err * $err
                $dB   += $err
                for ($j = 0; $j -lt $nFeatures; $j++) {
                    $dW[$j] += $err * $X[$i][$j]
                }
            }

            # L2 regularization term added to loss and gradient
            $l2Loss = 0.0
            for ($j = 0; $j -lt $nFeatures; $j++) {
                $l2Loss += $this.Weights[$j] * $this.Weights[$j]
            }
            $loss = ($loss / $n) + $this.Lambda * $l2Loss
            $this.LossHistory.Add($loss)

            $this.Bias -= $this.LearningRate * (2.0 / $n) * $dB
            for ($j = 0; $j -lt $nFeatures; $j++) {
                # Ridge gradient: add 2*lambda*W to weight gradient
                $ridgeGrad = (2.0 / $n) * $dW[$j] + 2.0 * $this.Lambda * $this.Weights[$j]
                $this.Weights[$j] -= $this.LearningRate * $ridgeGrad
            }
        }
        $this.IsFitted = $true
    }

    [void] PrintSummary() {
        Write-Host ""
        Write-Host "╔══════════════════════════════════════╗" -ForegroundColor Cyan
        Write-Host "║ Ridge Regression Summary ║" -ForegroundColor Cyan
        Write-Host "╠══════════════════════════════════════╣" -ForegroundColor Cyan
        Write-Host ("║ Lambda : {0,-24}║" -f $this.Lambda)                   -ForegroundColor Yellow
        Write-Host ("║ Bias : {0,-24}║" -f [Math]::Round($this.Bias, 6))   -ForegroundColor White
        for ($j = 0; $j -lt $this.Weights.Length; $j++) {
            Write-Host ("║ Weight[{0}] : {1,-24}║" -f $j, [Math]::Round($this.Weights[$j], 6)) -ForegroundColor White
        }
        $finalLoss = if ($this.LossHistory.Count -gt 0) { $this.LossHistory[-1] } else { 0.0 }
        Write-Host ("║ Final Loss: {0,-24}║" -f [Math]::Round($finalLoss, 6))  -ForegroundColor Magenta
        Write-Host "╚══════════════════════════════════════╝" -ForegroundColor Cyan
        Write-Host ""
    }
}

# ============================================================
# LASSO REGRESSION (L1 Regularization)
# ============================================================
# TEACHING NOTE: Lasso adds a different penalty:
# Loss = MSE + lambda * sum(|W|) <- L1 penalty
# This can shrink weights to EXACTLY zero = feature selection!
# Use Lasso when: many features, only some are useful.
# Lasso automatically picks the most important features.
# ============================================================

class LassoRegression : LinearRegression {
    [double] $Lambda

    LassoRegression([double]$lambda) : base(0.01, 1000) {
        $this.Lambda = $lambda
    }

    LassoRegression([double]$lambda, [double]$lr, [int]$maxIter) : base($lr, $maxIter) {
        $this.Lambda = $lambda
    }

    [void] Fit([double[][]]$X, [double[]]$y) {
        $n         = $X.Length
        $nFeatures = $X[0].Length
        $this.Weights = @(0.0) * $nFeatures
        $this.Bias    = 0.0

        for ($iter = 0; $iter -lt $this.MaxIter; $iter++) {
            $yHat = $this.Predict($X)

            $dW   = @(0.0) * $nFeatures
            $dB   = 0.0
            $loss = 0.0

            for ($i = 0; $i -lt $n; $i++) {
                $err   = $yHat[$i] - $y[$i]
                $loss += $err * $err
                $dB   += $err
                for ($j = 0; $j -lt $nFeatures; $j++) {
                    $dW[$j] += $err * $X[$i][$j]
                }
            }

            # L1 regularization
            $l1Loss = 0.0
            for ($j = 0; $j -lt $nFeatures; $j++) {
                $l1Loss += [Math]::Abs($this.Weights[$j])
            }
            $loss = ($loss / $n) + $this.Lambda * $l1Loss
            $this.LossHistory.Add($loss)

            $this.Bias -= $this.LearningRate * (2.0 / $n) * $dB
            for ($j = 0; $j -lt $nFeatures; $j++) {
                # Lasso gradient: sign(W)*lambda (subgradient)
                $sign     = if ($this.Weights[$j] -gt 0) { 1.0 } elseif ($this.Weights[$j] -lt 0) { -1.0 } else { 0.0 }
                $lassoGrad = (2.0 / $n) * $dW[$j] + $this.Lambda * $sign
                $this.Weights[$j] -= $this.LearningRate * $lassoGrad
            }
        }
        $this.IsFitted = $true
    }

    [void] PrintSummary() {
        Write-Host ""
        Write-Host "╔══════════════════════════════════════╗" -ForegroundColor Cyan
        Write-Host "║ Lasso Regression Summary ║" -ForegroundColor Cyan
        Write-Host "╠══════════════════════════════════════╣" -ForegroundColor Cyan
        Write-Host ("║ Lambda : {0,-24}║" -f $this.Lambda)                   -ForegroundColor Yellow
        Write-Host ("║ Bias : {0,-24}║" -f [Math]::Round($this.Bias, 6))   -ForegroundColor White
        for ($j = 0; $j -lt $this.Weights.Length; $j++) {
            $w    = [Math]::Round($this.Weights[$j], 6)
            $color = if ([Math]::Abs($w) -lt 0.001) { "DarkGray" } else { "White" }
            Write-Host ("║ Weight[{0}] : {1,-24}║" -f $j, $w) -ForegroundColor $color
        }
        $finalLoss = if ($this.LossHistory.Count -gt 0) { $this.LossHistory[-1] } else { 0.0 }
        Write-Host ("║ Final Loss: {0,-24}║" -f [Math]::Round($finalLoss, 6))  -ForegroundColor Magenta
        Write-Host " (DarkGray weights ≈ 0 = Lasso eliminated them!)" -ForegroundColor DarkGray
        Write-Host "╚══════════════════════════════════════╝" -ForegroundColor Cyan
        Write-Host ""
    }
}

# ============================================================
# LOGISTIC REGRESSION
# ============================================================
# TEACHING NOTE: Logistic Regression is for CLASSIFICATION.
# Instead of predicting a number, it predicts a probability.
# Uses the sigmoid function: sigma(z) = 1 / (1 + e^-z)
# Output is always between 0 and 1.
# If probability > 0.5 -> class 1, else class 0
# Loss function: Binary Cross-Entropy
# Loss = -(y*log(p) + (1-y)*log(1-p))
# ============================================================

class LogisticRegression {
    [double[]] $Weights
    [double]   $Bias
    [double]   $LearningRate
    [int]      $MaxIter
    [double]   $Lambda        # Regularization (0 = none)
    [bool]     $IsFitted = $false
    [System.Collections.Generic.List[double]] $LossHistory

    LogisticRegression() {
        $this.LearningRate = 0.1
        $this.MaxIter      = 1000
        $this.Lambda       = 0.0
        $this.LossHistory  = [System.Collections.Generic.List[double]]::new()
    }

    LogisticRegression([double]$learningRate, [int]$maxIter, [double]$lambda) {
        $this.LearningRate = $learningRate
        $this.MaxIter      = $maxIter
        $this.Lambda       = $lambda
        $this.LossHistory  = [System.Collections.Generic.List[double]]::new()
    }

    # Sigmoid: maps any number to [0,1]
    hidden [double] Sigmoid([double]$z) {
        return 1.0 / (1.0 + [Math]::Exp(-[Math]::Max(-500, [Math]::Min(500, $z))))
    }

    # Predict probabilities
    [double[]] PredictProba([double[][]]$X) {
        $proba = @(0.0) * $X.Length
        for ($i = 0; $i -lt $X.Length; $i++) {
            $z = $this.Bias
            for ($j = 0; $j -lt $X[$i].Length; $j++) {
                $z += $X[$i][$j] * $this.Weights[$j]
            }
            $proba[$i] = $this.Sigmoid($z)
        }
        return $proba
    }

    # Predict class labels (0 or 1)
    [int[]] Predict([double[][]]$X) {
        $proba  = $this.PredictProba($X)
        $labels = @(0) * $X.Length
        for ($i = 0; $i -lt $proba.Length; $i++) {
            $labels[$i] = if ($proba[$i] -ge 0.5) { 1 } else { 0 }
        }
        return $labels
    }

    [void] Fit([double[][]]$X, [int[]]$y) {
        $n         = $X.Length
        $nFeatures = $X[0].Length
        $this.Weights = @(0.0) * $nFeatures
        $this.Bias    = 0.0

        for ($iter = 0; $iter -lt $this.MaxIter; $iter++) {
            $proba = $this.PredictProba($X)

            $dW   = @(0.0) * $nFeatures
            $dB   = 0.0
            $loss = 0.0

            for ($i = 0; $i -lt $n; $i++) {
                $err   = $proba[$i] - $y[$i]
                $dB   += $err
                # Binary cross-entropy loss
                $p     = [Math]::Max(1e-10, [Math]::Min(1 - 1e-10, $proba[$i]))
                $loss -= $y[$i] * [Math]::Log($p) + (1 - $y[$i]) * [Math]::Log(1 - $p)
                for ($j = 0; $j -lt $nFeatures; $j++) {
                    $dW[$j] += $err * $X[$i][$j]
                }
            }

            $loss = $loss / $n
            $this.LossHistory.Add($loss)

            $this.Bias -= $this.LearningRate * $dB / $n
            for ($j = 0; $j -lt $nFeatures; $j++) {
                $regTerm = $this.Lambda * $this.Weights[$j]
                $this.Weights[$j] -= $this.LearningRate * ($dW[$j] / $n + $regTerm)
            }
        }
        $this.IsFitted = $true
    }

    [void] PrintSummary() {
        Write-Host ""
        Write-Host "╔══════════════════════════════════════╗" -ForegroundColor Cyan
        Write-Host "║ Logistic Regression Summary ║" -ForegroundColor Cyan
        Write-Host "╠══════════════════════════════════════╣" -ForegroundColor Cyan
        Write-Host ("║ Lambda : {0,-24}║" -f $this.Lambda)                   -ForegroundColor Yellow
        Write-Host ("║ Bias : {0,-24}║" -f [Math]::Round($this.Bias, 6))   -ForegroundColor White
        for ($j = 0; $j -lt $this.Weights.Length; $j++) {
            Write-Host ("║ Weight[{0}] : {1,-24}║" -f $j, [Math]::Round($this.Weights[$j], 6)) -ForegroundColor White
        }
        $finalLoss = if ($this.LossHistory.Count -gt 0) { $this.LossHistory[-1] } else { 0.0 }
        Write-Host ("║ Final Loss: {0,-24}║" -f [Math]::Round($finalLoss, 6))  -ForegroundColor Magenta
        Write-Host "╚══════════════════════════════════════╝" -ForegroundColor Cyan
        Write-Host ""
    }
}

# ============================================================
# CROSS VALIDATION
# ============================================================
# TEACHING NOTE: Why cross-validate?
# If we test on the same data we trained on, we cheat!
# k-fold splits data into k parts:
# - Train on k-1 parts, test on 1 part
# - Repeat k times, each part gets a turn as test set
# - Average the scores = honest estimate of performance
# ============================================================

function Invoke-KFoldCV {
    param(
        [object]     $Model,
        [double[][]] $X,
        [object[]]   $y,
        [int]        $K        = 5,
        [string]     $Task     = "regression",  # or "classification"
        [switch]     $Verbose
    )

    $n       = $X.Length
    $foldSize = [Math]::Floor($n / $K)
    $scores  = [System.Collections.Generic.List[double]]::new()

    Write-Host ""
    Write-Host "🔄 $K-Fold Cross Validation" -ForegroundColor Yellow
    Write-Host " Samples : $n" -ForegroundColor Cyan
    Write-Host " Fold size: $foldSize" -ForegroundColor Cyan
    Write-Host ""

    for ($fold = 0; $fold -lt $K; $fold++) {
        $testStart = $fold * $foldSize
        $testEnd   = [Math]::Min($testStart + $foldSize, $n) - 1

        # Split into train/test
        $XTrain = @(); $yTrain = @()
        $XTest  = @(); $yTest  = @()

        for ($i = 0; $i -lt $n; $i++) {
            if ($i -ge $testStart -and $i -le $testEnd) {
                $XTest  += ,$X[$i]
                $yTest  += $y[$i]
            } else {
                $XTrain += ,$X[$i]
                $yTrain += $y[$i]
            }
        }

        # Create fresh model instance same type
        $foldModel = $Model.GetType()::new()

        # Train
        if ($Task -eq "classification") {
            $foldModel.Fit($XTrain, [int[]]$yTrain)
            $yPred  = $foldModel.Predict($XTest)
            $metrics = Get-ClassificationMetrics -yTrue ([int[]]$yTest) -yPred ([int[]]$yPred)
            $score  = $metrics.Accuracy
            $label  = "Accuracy"
        } else {
            $foldModel.Fit($XTrain, [double[]]$yTrain)
            $yPred  = $foldModel.Predict($XTest)
            $metrics = Get-RegressionMetrics -yTrue ([double[]]$yTest) -yPred ([double[]]$yPred)
            $score  = $metrics.R2
            $label  = "R2"
        }

        $scores.Add($score)
        if ($Verbose) {
            Write-Host (" Fold {0}: {1} = {2:F4}" -f ($fold+1), $label, $score) -ForegroundColor White
        }
    }

    $avgScore = ($scores | Measure-Object -Average).Average
    $minScore = ($scores | Measure-Object -Minimum).Minimum
    $maxScore = ($scores | Measure-Object -Maximum).Maximum

    Write-Host ""
    Write-Host "╔══════════════════════════════════════╗" -ForegroundColor Yellow
    Write-Host "║ Cross Validation Results ║" -ForegroundColor Yellow
    Write-Host "╠══════════════════════════════════════╣" -ForegroundColor Yellow
    Write-Host ("║ Folds : {0,-24}║" -f $K)                              -ForegroundColor White
    Write-Host ("║ Avg Score : {0,-24}║" -f [Math]::Round($avgScore, 4))     -ForegroundColor Green
    Write-Host ("║ Min Score : {0,-24}║" -f [Math]::Round($minScore, 4))     -ForegroundColor White
    Write-Host ("║ Max Score : {0,-24}║" -f [Math]::Round($maxScore, 4))     -ForegroundColor White
    Write-Host "╚══════════════════════════════════════╝" -ForegroundColor Yellow
    Write-Host ""

    return @{ AvgScore = $avgScore; MinScore = $minScore; MaxScore = $maxScore; Scores = $scores }
}

# ============================================================
# BUILT-IN DEMO DATASETS
# ============================================================
function Get-VBAFDataset {
    param([string]$Name = "HousePrice")

    switch ($Name) {
        "HousePrice" {
            # Simple house price dataset
            # Features: [size_sqm, bedrooms, age_years]
            # Target: price in 1000s
            Write-Host "📊 Dataset: HousePrice (20 samples)" -ForegroundColor Cyan
            Write-Host " Features: [size_sqm, bedrooms, age_years]" -ForegroundColor Cyan
            Write-Host " Target : price (1000s)" -ForegroundColor Cyan
            $X = @(
                @(50.0,  1.0, 20.0), @(75.0,  2.0, 15.0), @(100.0, 3.0, 10.0),
                @(120.0, 3.0,  5.0), @(150.0, 4.0,  2.0), @(60.0,  2.0, 25.0),
                @(80.0,  2.0, 18.0), @(90.0,  3.0, 12.0), @(110.0, 3.0,  8.0),
                @(130.0, 4.0,  3.0), @(55.0,  1.0, 22.0), @(70.0,  2.0, 16.0),
                @(95.0,  3.0, 11.0), @(115.0, 3.0,  6.0), @(140.0, 4.0,  1.0),
                @(65.0,  2.0, 19.0), @(85.0,  2.0, 14.0), @(105.0, 3.0,  9.0),
                @(125.0, 4.0,  4.0), @(160.0, 5.0,  1.0)
            )
            $y = @(
                150.0, 220.0, 310.0, 370.0, 450.0, 175.0, 240.0, 270.0, 340.0, 400.0,
                160.0, 210.0, 290.0, 355.0, 430.0, 195.0, 255.0, 320.0, 385.0, 500.0
            )
            return @{ X = $X; y = $y; Task = "regression" }
        }
        "Iris2Class" {
            # Simplified 2-class Iris (Setosa vs Versicolor)
            # Features: [sepal_length, petal_length]
            # Target: 0=Setosa, 1=Versicolor
            Write-Host "📊 Dataset: Iris2Class (20 samples)" -ForegroundColor Cyan
            Write-Host " Features: [sepal_length, petal_length]" -ForegroundColor Cyan
            Write-Host " Target : 0=Setosa, 1=Versicolor" -ForegroundColor Cyan
            $X = @(
                @(5.1, 1.4), @(4.9, 1.4), @(4.7, 1.3), @(4.6, 1.5), @(5.0, 1.4),
                @(5.4, 1.7), @(4.6, 1.4), @(5.0, 1.5), @(4.4, 1.4), @(4.9, 1.5),
                @(7.0, 4.7), @(6.4, 4.5), @(6.9, 4.9), @(5.5, 4.0), @(6.5, 4.6),
                @(5.7, 4.5), @(6.3, 4.7), @(4.9, 3.3), @(6.6, 4.6), @(5.2, 3.9)
            )
            $y = @(0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1)
            return @{ X = $X; y = $y; Task = "classification" }
        }
        default {
            Write-Host "❌ Unknown dataset: $Name" -ForegroundColor Red
            Write-Host " Available: HousePrice, Iris2Class" -ForegroundColor Yellow
            return $null
        }
    }
}

# ============================================================
# TEST
# 1. Run VBAF.LoadAll.ps1
# 2. $data = Get-VBAFDataset -Name "HousePrice"
# 3. $scaler = [StandardScaler]::new()
# $Xs = $scaler.FitTransform($data.X)
# 4. $model = [LinearRegression]::new()
# $model.Fit($Xs, $data.y)
# $model.PrintSummary()
# 5. $yPred = $model.Predict($Xs)
# Get-RegressionMetrics -yTrue $data.y -yPred $yPred
# 6. Compare Ridge vs Lasso:
# $ridge = [RidgeRegression]::new(0.1)
# $ridge.Fit($Xs, $data.y)
# $ridge.PrintSummary()
# $lasso = [LassoRegression]::new(0.1)
# $lasso.Fit($Xs, $data.y)
# $lasso.PrintSummary()
# 7. Classification:
# $data2 = Get-VBAFDataset -Name "Iris2Class"
# $logit = [LogisticRegression]::new()
# $logit.Fit($data2.X, [int[]]$data2.y)
# $logit.PrintSummary()
# Get-ClassificationMetrics -yTrue $data2.y -yPred $logit.Predict($data2.X)
# ============================================================
Write-Host "📦 VBAF.ML.Regression.ps1 loaded" -ForegroundColor Green
Write-Host " Classes : StandardScaler, MinMaxScaler"        -ForegroundColor Cyan
Write-Host " LinearRegression, RidgeRegression"    -ForegroundColor Cyan
Write-Host " LassoRegression, LogisticRegression"  -ForegroundColor Cyan
Write-Host " Functions : Get-RegressionMetrics"               -ForegroundColor Cyan
Write-Host " Get-ClassificationMetrics"            -ForegroundColor Cyan
Write-Host " Invoke-KFoldCV"                       -ForegroundColor Cyan
Write-Host " Get-VBAFDataset"                      -ForegroundColor Cyan
Write-Host ""
Write-Host " Quick start:" -ForegroundColor Yellow
Write-Host ' $data = Get-VBAFDataset -Name "HousePrice"'                  -ForegroundColor White
Write-Host ' $scaler = [StandardScaler]::new()'                             -ForegroundColor White
Write-Host ' $Xs = $scaler.FitTransform($data.X)'                       -ForegroundColor White
Write-Host ' $model = [LinearRegression]::new()'                           -ForegroundColor White
Write-Host ' $model.Fit($Xs, $data.y)'                                      -ForegroundColor White
Write-Host ' $model.PrintSummary()'                                         -ForegroundColor White
Write-Host ""