Skip to content

feat: add comprehensive feature selection module with 432 implementations - #799

Merged
ooples merged 16 commits into
masterfrom
feat/preprocessing-migration
Feb 3, 2026
Merged

ooples merged 16 commits into
masterfrom
feat/preprocessing-migration

Conversation

@ooples

@ooples ooples commented Jan 31, 2026 •

Copy link
Copy Markdown
Owner

Summary

Adds a comprehensive feature selection module to AiDotNet with 432 implementations covering all critical, high-priority, and gap algorithms from the feature selection methods catalog.

Closes #324

Categories Implemented

Category Count Description
Filter Methods 70+ Univariate, multivariate, statistical, information-theoretic
Embedded Methods 35+ Regularization-based, tree-based importance
Wrapper Methods 25+ Search-based, metaheuristic optimization
Hybrid Methods 10+ Combined filter/wrapper approaches
Ensemble Methods 10+ Stability selection, voting, stacking
Metaheuristic 15+ Evolutionary and swarm intelligence
Clustering 20+ K-Means, Spectral, Hierarchical, DBSCAN variants
Statistical 25+ ANOVA, t-tests, chi-squared, Hotelling T², MANOVA
Information Theory 30+ Entropy, MI, MRMR, JMI, CMIM, differential entropy
Complexity/Nonlinear 15+ Hurst, Lyapunov, fractal dimension, distance correlation
Neural/Interpretability 10+ GradCAM, ScoreCAM, SHAP, LIME, attention flow
Privacy/Fairness 5+ Differential privacy, federated FS, fair feature selection
Bandit 2 UCB1, Thompson Sampling
Domain-Specific 20+ Text, image, genomics, time series
Incremental/Streaming 5+ Online and adaptive selection

Key Features

  • All implementations follow TransformerBase<T, Matrix<T>, Matrix<T>> pattern
  • Consistent API with Fit(), Transform(), FitTransform() methods
  • Proper documentation with beginner-friendly explanations
  • Support for both supervised and unsupervised feature selection
  • Handles multiple scoring functions and selection criteria
  • net471 and net10.0 compatibility

Filter Methods Include

  • Chi-squared, Gini impurity, entropy-based selection
  • ANOVA F-test, information gain, gain ratio, symmetric uncertainty
  • Variance threshold, correlation-based (Pearson, Spearman, Kendall)
  • Fisher score, ReliefF (and variants: SURF, MultiSURF)
  • MRMR, JMI, CMIM, DISR, FCBF, CIFE

Embedded Methods Include

  • Lasso, Ridge, Elastic Net feature selection
  • SCAD, MCP penalized regression
  • Tree-based importance (Random Forest, Gradient Boosting)
  • Permutation importance, drop-column importance
  • Group Lasso, Sparse Group Lasso, Fused Lasso

Wrapper Methods Include

  • Forward selection, backward elimination, bidirectional search
  • Recursive feature elimination (RFE)
  • Exhaustive search (for small feature sets)
  • Genetic algorithm, PSO, differential evolution
  • Firefly, harmony search, ant colony, grey wolf, whale optimization

Statistical Methods Include

  • One-way, Two-way ANOVA
  • MANOVA, ANCOVA
  • Hotelling's T² for multivariate comparisons
  • Kruskal-Wallis, Mann-Whitney U tests

Advanced Methods Include

  • Causal: PC algorithm, FCI, MMPC
  • Neural: GradCAM, ScoreCAM, Attention Flow
  • Privacy: Differential Privacy FS, Federated FS
  • Fairness: Fair Feature Selection
  • Bandit: UCB1, Thompson Sampling

Build Status

  • ✅ Builds successfully on net10.0 (0 errors, 0 warnings)
  • ✅ Builds successfully on net471 (0 errors, 0 warnings)

Test Plan

  • Verify all filter methods work with classification/regression targets
  • Test metaheuristic methods with various population sizes
  • Confirm embedded methods produce sparse feature selections
  • Validate wrapper methods with different evaluators
  • Check ensemble methods aggregate results correctly
  • Test streaming/incremental methods with batch updates
  • Verify privacy-preserving methods add appropriate noise
  • Test fairness-aware selection removes protected attribute proxies

🤖 Generated with Claude Code

ooples and others added 6 commits January 30, 2026 22:37
Migrates CorrelationFeatureSelector functionality to new preprocessing pattern:
- Extends TransformerBase<T, Matrix<T>, Matrix<T>>
- Implements Fit/Transform API
- Adds correlation matrix computation and storage
- Adds GetSupportMask() and GetCorrelationsWithSelected() utilities
- Full For Beginners documentation on all methods

Part of preprocessing migration for issue #324
Migrates LpNormNormalizer functionality to new preprocessing pattern:
- Scales features (columns) by dividing by Lp-norm
- Supports arbitrary p values (L1, L2, L-infinity)
- Supports inverse transform
- Full For Beginners documentation

Note: This is column-wise normalization, different from row-wise Normalizer

Part of preprocessing migration for issue #324
Reorganized src/Preprocessing/FeatureSelection/ into:
- Filter/Univariate: VarianceThreshold, SelectKBest, SelectPercentile, etc.
- Filter/Correlation: CorrelationSelector
- Wrapper: RFE, RFECV, SequentialFeatureSelector
- Embedded: SelectFromModel
- Helpers: StatisticalTestHelper

Updated namespaces and using statements accordingly.

Part of preprocessing migration for issue #324
- Update AiModelResult, AiModelResultOptions to use PreprocessingInfo
- Update DefaultModelEvaluator to use PreprocessingInfo
- Update GeneticAlgorithmRegression, SymbolicRegression to use PreprocessingPipeline
- Update QuantumNeuralNetwork to use PreprocessingPipeline
- Delete old Normalizers, FeatureSelectors, DataProcessor directories
- Delete old interfaces (INormalizer, IFeatureSelector, IDataPreprocessor)
- Delete old NormalizationInfo, NormalizerFactory
- Update test files to use new PreprocessingInfo pattern
- Delete obsolete test files that tested old APIs

This completes the migration to sklearn-style preprocessing pipeline.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…ions

Add extensive feature selection methods covering critical and high-priority
algorithms across multiple categories:

Filter Methods:
- Chi-squared, Gini impurity, entropy-based selection
- ANOVA F-test, information gain, gain ratio
- Variance threshold, correlation-based selection
- Symmetric uncertainty, Fisher score, ReliefF

Embedded Methods:
- Lasso, Ridge, Elastic Net regularization
- Tree-based importance (Random Forest, Gradient Boosting)
- Permutation importance, drop-column importance
- SCAD, MCP penalized regression

Wrapper Methods:
- Forward/backward selection, RFE
- Exhaustive search, genetic algorithms
- Particle swarm optimization

Hybrid Methods:
- MI + Correlation combined scoring
- Two-stage selection pipelines
- Ensemble combination methods

Metaheuristic Methods:
- Genetic algorithm, PSO, differential evolution
- Firefly algorithm, harmony search
- Artificial bee colony, ant colony optimization
- Simulated annealing, grey wolf optimizer

Incremental/Streaming Methods:
- Adaptive feature selector
- Online feature selection
- Streaming feature selector

Domain-Specific Methods:
- Genomic, text, and image feature selectors
- Bioinformatics methods (MRMR, SAM, Boruta)

All implementations follow TransformerBase<T, Matrix<T>, Matrix<T>>
pattern with proper documentation and beginner-friendly explanations.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jan 31, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
aidotnet-playground-api Ready Ready Preview, Comment Feb 3, 2026 6:38pm

@coderabbitai

coderabbitai Bot commented Jan 31, 2026 •

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • Documentation

    • Added a comprehensive Feature Selection Methods Catalog with categorized techniques, priorities, and a three‑phase roadmap.
  • New Features

    • Added a large collection of feature‑selection transformers across domains (bioinformatics, embedded, ensemble, causal, clustering, compression, complexity, classification, dimensionality, bandit, cross‑validation, etc.) with fit/transform APIs and selection metadata.
  • Refactor

    • Consolidated preprocessing into a unified pipeline; legacy per‑component normalization/feature‑selector hooks removed and model evaluation/options updated to use preprocessing info.

Walkthrough

Replaces legacy normalization/feature‑selection plumbing with a unified PreprocessingPipeline/PreprocessingInfo model; removes many old normalizer/feature‑selector interfaces and classes; adds ~70+ Transformer-based feature‑selection implementations and a planning catalog; updates AiModelBuilder, AiModelResult, evaluator, and related consumers to use the new pipeline.

Changes

Cohort / File(s) Summary
Documentation
docs/feature-selection-methods-catalog.md
Adds an 821-line exhaustive catalog and phased roadmap for feature‑selection methods (statuses, priorities, implementation order).
Builder / Model / Evaluator plumbing
src/AiModelBuilder.cs, src/Models/Results/AiModelResult.cs, src/Models/Results/AiModelResult.Uncertainty.cs, src/Models/Inputs/ModelEvaluationInput.cs, src/Evaluation/DefaultModelEvaluator.cs
Replace NormalizationInfo/INormalizer/DataPreprocessor wiring with PreprocessingInfo/PreprocessingPipeline; remove legacy ConfigureFeatureSelector/ConfigureNormalizer/ConfigureDataPreprocessor APIs; update prediction/evaluation/uncertainty flows to use pipeline transforms.
Removed interfaces & factories
src/Interfaces/IFeatureSelector.cs, src/Interfaces/INormalizer.cs, src/Interfaces/IDataPreprocessor.cs, src/Factories/NormalizerFactory.cs, src/Interfaces/IAiModelBuilder.cs
Delete legacy feature‑selector, normalizer, and data‑preprocessor interfaces and the NormalizerFactory; IAiModelBuilder loses legacy per-component configure methods (breaking API surface).
Removed data‑preprocessor & options
src/DataProcessor/DefaultDataPreprocessor.cs, src/Models/Options/DataProcessorOptions.cs
Remove default data preprocessor and DataProcessorOptions (three‑way split, shuffle/seed, normalize‑before‑selection logic).
Removed Normalization types
src/Models/NormalizationInfo.cs, src/Models/Options/AiModelResultOptions.cs, src/Normalizers/*, src/Normalizers/NormalizerBase.cs
Delete NormalizationInfo, NormalizerBase and many concrete normalizers (MinMax, ZScore, MeanVariance, Log, RobustScaling, Quantile, etc.); AiModelResult and uncertainty code now use PreprocessingInfo.
Removed legacy feature‑selector scaffolding & selectors
src/FeatureSelectors/*
Remove FeatureSelectorBase and legacy selectors (Correlation, VarianceThreshold, Univariate, RFE, Sequential, SelectFromModel, NoFeatureSelector, etc.).
New preprocessing API & transformers
src/Preprocessing/... (Bioinformatics, Embedded, Ensemble, Bayesian, Causal, Clustering, CostSensitive, Dimensionality, DomainSpecific, Classification, Complexity, Compression, Density, CrossValidation, Bandit, Boundary, etc.)
Add ~70+ TransformerBase<T,Matrix,Matrix> feature‑selection implementations (Fit/FitTransform/Transform/GetSupportMask/GetFeatureNamesOut) across many categories; large public API additions to review for numeric/generic correctness, parameter validation, and consistency.
Neural network update
src/NeuralNetworks/QuantumNeuralNetwork.cs
Replace INormalizer dependency with optional PreprocessingPipeline and apply pipeline.Transform during quantum state preparation; update instance creation/serialization.
Builder interface update
src/Interfaces/IAiModelBuilder.cs
Removed legacy per-component Configure* methods; public builder API reduced and now favors pipeline-centric ConfigurePreprocessing overloads.
Tests & benchmarks / global usings
AiDotNetBenchmarkTests/BenchmarkTests/NormalizersBenchmarks.cs, AiDotNetBenchmarkTests/GlobalUsings.cs
Remove NormalizersBenchmarks.cs; update global usings to new Preprocessing namespaces (Preprocessing.DataPreparation, Preprocessing.Scalers).
Other public model inputs/options
src/Models/Inputs/ModelEvaluationInput.cs, src/Models/Options/AiModelResultOptions.cs
Rename/replace NormInfo/NormalizationInfo usages with optional PreprocessingInfo throughout evaluation and result option types.
Large removals (normalizer-related files)
src/Normalizers/* (many files removed)
Extensive deletions of normalizer implementations and base abstractions—requires audit for callers expecting denormalization/coefficient workflows.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant AiModelBuilder
    participant AiModel as Model
    participant PreprocessingPipeline as "Preprocessing\nPipeline (PreprocessingInfo)"
    participant Evaluator

    User->>AiModelBuilder: Build(modelConfig, preprocessingPipeline?)
    AiModelBuilder->>AiModel: Create AiModelResult (attach PreprocessingInfo if provided)
    User->>AiModel: Predict(rawInput)
    alt PreprocessingInfo configured and fitted
        AiModel->>PreprocessingPipeline: TransformFeatures(rawInput)
        PreprocessingPipeline-->>AiModel: transformedInput
        AiModel->>AiModel: Predict(transformedInput)
        AiModel->>PreprocessingPipeline: InverseTransformPredictions(predictions)
        PreprocessingPipeline-->>AiModel: finalPredictions
    else
        AiModel->>AiModel: Predict(rawInput)
    end
    User->>Evaluator: EvaluateModel(input, PreprocessingInfo?)
    Evaluator->>AiModel: Request predictions/statistics
    AiModel-->>Evaluator: Predictions + PreprocessingInfo (if any)
    Evaluator-->>User: ModelStats (uses PreprocessingInfo when provided)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

"I’m a rabbit that hops through code and logs,
I swapped old scalers for pipeline frogs.
Seventy methods sprout from the ground,
I nibble features till the best are found.
🐇🌿 — a tiny hop for preprocessing, hooray!"

🚥 Pre-merge checks | ✅ 2 | ❌ 3
❌ Failed checks (3 warnings)
Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning Does not satisfy linked-issue ACs: required interfaces/types (IFeatureSelector) were removed, implementations moved to Preprocessing rather than src/FeatureSelectors, INumericOperations generics are not followed, and required unit tests and PredictionModelBuilder integration are missing. Restore IFeatureSelector and required src/FeatureSelectors classes (Univariate, Sequential, SelectFromModel) per ACs; implement numeric generics via INumericOperations; add the mandated unit tests and PredictionModelBuilder Configure hooks; split API-breaking removals into separate PRs.
Out of Scope Changes check ⚠️ Warning PR contains out-of-scope, breaking changes: core normalizer/preprocessing APIs and types (INormalizer, NormalizationInfo, NormalizerBase, NormalizerFactory, IDataPreprocessor, DataProcessorOptions) and legacy selector APIs were deleted, unrelated to adding selectors. Re-scope this change: revert or remove API-breaking deletions from this PR and keep feature-selection additions isolated; if migrations are required, open separate migration PRs with compatibility shims, upgrade notes, and tests.
Docstring Coverage ⚠️ Warning Docstring coverage is 3.93% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed Title concisely and accurately summarizes the primary change: adding a comprehensive feature selection module.
Description check ✅ Passed Description aligns with the changeset, detailing the added feature-selection modules, categories, and test plan.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/preprocessing-migration

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot added the feature Feature work item label Jan 31, 2026
…l, etc.)

Add 18 new feature selection methods across 12 new categories:

Bayesian Methods:
- ARDSelector: Automatic Relevance Determination for sparse selection
- BayesianFeatureSelector: Posterior probability-based selection
- SpikeAndSlabSelector: Spike-and-slab prior for variable selection

Causal Methods:
- MarkovBlanketSelector: Identifies Markov blanket of target
- PCAlgorithmSelector: PC algorithm for causal discovery

Clustering Methods:
- ClusteringBasedFS: K-means feature clustering for redundancy removal
- HierarchicalFS: Agglomerative clustering for feature grouping

Constraint-based:
- ConstraintBasedSelector: Respects mandatory/forbidden features and groups

Fuzzy Methods:
- FuzzyRoughSetSelector: Fuzzy rough set theory for uncertain data

Graph-based:
- GraphBasedFS: Uses PageRank/centrality on feature correlation graphs

Kernel Methods:
- KernelPCASelector: Kernel PCA-based non-linear importance
- KernelFisherSelector: Kernel Fisher discriminant analysis

Multi-objective:
- NSGAIISelector: NSGA-II for accuracy vs feature count trade-off

Neural Network-based:
- AutoencoderSelector: Bottleneck autoencoder importance
- AttentionBasedSelector: Learned attention weights for selection

Robust Methods:
- RobustFeatureSelector: Outlier-resistant rank-based selection

Rough Set Methods:
- RoughSetSelector: Reduct-based selection using indiscernibility

Transfer Learning:
- TransferLearningSelector: Combines source and target domain importance

All methods follow TransformerBase<T, Matrix<T>, Matrix<T>> pattern
with beginner-friendly documentation.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/NeuralNetworks/QuantumNeuralNetwork.cs (1)

403-414: ⚠️ Potential issue | 🟠 Major

Ensure preprocessed values are non‑negative before Sqrt.

PrepareQuantumState now uses the preprocessing pipeline, which can output negative values (e.g., standardization). That will yield NaNs when computing amplitudes.

🛡️ Proposed fix
         for (int i = 0; i < flatInput.Length; i++)
         {
-            var amplitude = NumOps.Sqrt(flatInput[i]);
+            var value = flatInput[i];
+            if (NumOps.LessThan(value, NumOps.Zero))
+                throw new ArgumentException("Preprocessed input must be non-negative for quantum state preparation.", nameof(input));
+            var amplitude = NumOps.Sqrt(value);
             quantumState[i] = new Complex<T>(amplitude, NumOps.Zero);
         }
src/AiModelBuilder.cs (1)

1306-1312: ⚠️ Potential issue | 🟠 Major

Streaming training advertises preprocessing it never applies.

In BuildStreamingSupervisedAsync there is no Fit/Transform on _preprocessingPipeline, yet Line 1310 attaches PreprocessingInfo. If a pipeline is configured, the model trains on raw inputs while inference will transform inputs (and the pipeline may be unfitted), causing incorrect results or runtime errors. Either apply preprocessing during streaming (with partial-fit support) or keep PreprocessingInfo null / throw when a pipeline is configured.

🛠️ Suggested fix (avoid advertising preprocessing before it's applied)
-            PreprocessingInfo = _preprocessingPipeline is not null ? new PreprocessingInfo<T, TInput, TOutput>(_preprocessingPipeline) : null,
+            PreprocessingInfo = null,
🤖 Fix all issues with AI agents
In `@src/Preprocessing/FeatureSelection/Bioinformatics/SAM.cs`:
- Around line 86-97: The current split uses a fixed threshold (threshold=0.5)
and assigns indices to class0/class1 by comparing NumOps.ToDouble(target[i]),
which fails for binary labels that aren't in [0,1]; instead, determine the two
unique label values from target (map the first unique value to class0 and the
second to class1), iterate target and compare equality against those two values
to populate class0 and class1, and throw a clear exception if there are not
exactly two distinct labels; update any downstream assumptions that used the
threshold-based split accordingly.
- Around line 127-145: The code computes s0 from sortedSE (using _s0Percentile)
and then divides by (standardErrors[j] + s0) in the d-statistic loop; if all
standardErrors are zero s0 becomes zero and causes divide-by-zero/NaN. Fix by
flooring s0 to a small positive epsilon (e.g., s0 = Math.Max(s0, 1e-8) or set s0
to a tiny constant when sortedSE yields 0) before using it in the denominator so
that (standardErrors[j] + s0) is always positive; update the logic around s0
(computed from sortedSE/_s0Percentile) and ensure _dStatistics calculation uses
the floored s0.

In `@src/Preprocessing/FeatureSelection/Bioinformatics/SignificanceAnalysis.cs`:
- Around line 135-150: The SAM score computation can divide by zero if s0 is
zero/NaN; after computing s0 in SignificanceAnalysis.cs (the line setting s0
from pooledStds.OrderBy(...).Skip(...).First()), add a guard: if
double.IsNaN(s0) or Math.Abs(s0) == 0, replace s0 with a small positive epsilon
(e.g. 1e-8 or derived from pooledStds magnitude); also guard pooledStds[j] when
computing _samScores[j] by treating NaN/zero pooledStds entries as at least
epsilon so the denominator pooledStds[j] + s0 is never zero or NaN. Ensure you
reference and update the s0 variable and the denominator calculation that
assigns _samScores[j].
- Around line 79-88: The current split by threshold (NumOps.ToDouble(target[i])
< 0.5) in SignificanceAnalysis collapses non {0,1} labels; change logic to
compute the distinct labels from the target array (e.g., collect unique values
from target), ensure there are exactly two distinct labels (throw/return error
if not), then build class0Idx and class1Idx by comparing each target entry for
equality to the two distinct label values (use the actual label values, not
0.5). Update any variable names (target, class0Idx, class1Idx) accordingly and
ensure downstream code expects those two explicit label values.
- Around line 43-58: Add validation in the SignificanceAnalysis constructor to
guard against invalid nPermutations and fdrThreshold: ensure nPermutations is >
0 (throw ArgumentOutOfRangeException/ArgumentException if <= 0) and ensure
fdrThreshold is within a valid probability range (e.g., 0 < fdrThreshold <= 1 or
0 <= fdrThreshold <= 1 per project convention—throw ArgumentOutOfRangeException
if outside). Update the constructor that sets _nPermutations and _fdrThreshold
to perform these checks before assigning fields so downstream FDR/q-value
calculations cannot divide by zero or use nonsensical thresholds.

In `@src/Preprocessing/FeatureSelection/Bioinformatics/VolcanoPlotSelector.cs`:
- Around line 174-195: The IncompleteBeta implementation is missing
normalization by the Beta(a,b) term, so modify IncompleteBeta (used by
TwoTailedTTest) to return the regularized incomplete beta: compute the
normalization factor Beta(a,b)=exp(lgamma(a)+lgamma(b)-lgamma(a+b)) (or use an
existing LogGamma/regularized beta utility) and divide the current integrand
term bt*BetaCF(...) by Beta(a,b) (or equivalently multiply by
exp(lgamma(a+b)-lgamma(a)-lgamma(b))); ensure both branches that return bt *
BetaCF(...) apply this normalization so TwoTailedTTest yields correct p-values.

In `@src/Preprocessing/FeatureSelection/CostSensitive/CostSensitiveFS.cs`:
- Around line 24-26: The Fit overload that accepts featureCosts creates a
temporary CostSensitiveFS and only copies scores/indices, leaving
this._nInputFeatures at 0 and this._featureCosts null; update the overload in
class CostSensitiveFS so it persists the effective input feature count and the
resolved featureCosts into the current instance (_nInputFeatures and
_featureCosts) before delegating to the primary Fit logic (or call the main Fit
from this after setting those fields), ensuring GetSupportMask(), FeatureCosts
and GetTotalCost() operate on the correct state when costs are supplied or
defaulted.

In `@src/Preprocessing/FeatureSelection/Dimensionality/TruncatedSVDSelection.cs`:
- Around line 78-90: The loop computing singular values currently calls
ComputeEigenvalue twice and passes possibly-negative values to Math.Sqrt,
causing NaNs; modify the loop in TruncatedSVDSelection.cs to call
ComputeEigenvalue once (store it in a local variable, e.g., eigenvalue), clamp
that eigenvalue to be >= 0 before taking Math.Sqrt to set singularValues[k], and
then reuse the same clamped eigenvalue when deflating deflatedXtX; ensure you
still populate loadings[k, j] from the eigenvector returned by PowerIteration
and only perform the deflation using the stored eigenvalue.

In `@src/Preprocessing/FeatureSelection/DomainSpecific/GenomicFeatureSelector.cs`:
- Around line 72-112: The Fit method currently can divide by zero when data.Rows
or data.Columns is zero and can index past the end when _variancePercentile ==
1; add guards at the start of Fit to validate n = data.Rows and p = data.Columns
(throw ArgumentException with a clear message if n == 0 or p == 0), and ensure
later variance calculation assumes n > 0; when computing varianceThreshold,
compute an index safely (e.g., int idx = Math.Min((int)(p *
_variancePercentile), p - 1)) and use that index into sortedVariances so
_variancePercentile == 1 maps to the last element; reference the Fit method,
local variables n and p, field _variances, and variable
varianceThreshold/_variancePercentile when making these changes.

In `@src/Preprocessing/FeatureSelection/DomainSpecific/ImageFeatureSelector.cs`:
- Around line 43-61: The constructor ImageFeatureSelector currently doesn't
validate numChannels which can be zero or negative and cause divide/modulo by
zero in GetSpatialPosition; add a check in the ImageFeatureSelector constructor
to ensure numChannels is >= 1 (throw ArgumentException or
ArgumentOutOfRangeException with a clear message and parameter name) and ensure
the private field _numChannels is only set after validation so
GetSpatialPosition (and any other spatial indexing) cannot encounter
division/modulo by zero.
- Around line 70-81: The Fit method can pass an empty dataset to
ComputeBaseScores which divides by n and causes a divide-by-zero; update Fit (in
ImageFeatureSelector.cs) to explicitly check for data.Rows == 0 (and/or
target.Length == 0) before computing scores and throw a clear ArgumentException
(or InvalidOperationException) indicating the dataset/target is empty; ensure
this check occurs before setting _nInputFeatures and before calling
ComputeBaseScores so ComputeBaseScores is never invoked with n == 0.

In `@src/Preprocessing/FeatureSelection/DomainSpecific/TextFeatureSelector.cs`:
- Around line 70-90: In Fit (the public void Fit(Matrix<T> data, Vector<T>
target) method) add an early guard to reject empty datasets before computing
_documentFrequencies: check if data.Rows == 0 (and/or target.Length == 0) and
throw a clear ArgumentException (e.g., "Data must contain at least one row.") so
the subsequent loop that sets _documentFrequencies[j] = (double)docCount / n
cannot divide by zero; place this check alongside the existing target/data
length validation so the document frequency computation is only reached when n >
0.

In `@src/Preprocessing/FeatureSelection/Embedded/ElasticNetFS.cs`:
- Around line 118-119: ElasticNetFS computes l1Penalty and l2Penalty without
scaling by the sample count, causing inconsistent behavior versus
ElasticNetSelector/ElasticNetFeatureSelection; modify the l1Penalty and
l2Penalty calculations in ElasticNetFS to multiply by the number of samples (the
same n used in the other classes) so they read _alpha * _l1Ratio * n and _alpha
* (1 - _l1Ratio) * n (use the local sample-count variable in scope, e.g., n or
sampleCount) to align the penalty scaling with ElasticNetSelector and
ElasticNetFeatureSelection.

In `@src/Preprocessing/FeatureSelection/Embedded/GroupLassoSelector.cs`:
- Around line 75-77: Validate user-provided _groups in the GroupLassoSelector
code path where groups and nGroups are computed: if _groups is null keep the
current default, otherwise check that _groups is non-empty, every group contains
at least one index, every index value x satisfies 0 <= x < p, and no index
appears in more than one group (detect duplicates by flattening groups and
checking counts); when a validation fails throw an ArgumentException with a
clear message identifying the problem so callers know whether it’s out-of-range,
empty group list, empty group, or overlapping indices.

In `@src/Preprocessing/FeatureSelection/Embedded/MCPSelector.cs`:
- Around line 169-184: The MCPThreshold method incorrectly applies one formula
across all regions; update MCPThreshold(z, lambda, gamma) to explicitly handle
three cases: if Math.Abs(z) <= lambda return 0; else if Math.Abs(z) <= gamma *
lambda return Math.Sign(z) * (Math.Abs(z) - lambda) * gamma / (gamma - 1); else
return z. Ensure you reference MCPThreshold and use the existing parameters z,
lambda, gamma and Math.Sign/Math.Abs to implement the corrected piecewise
proximal operator.

In `@src/Preprocessing/FeatureSelection/Embedded/SCADSelector.cs`:
- Around line 170-190: In SCADThreshold(double z, double lambda, double a) the
soft-threshold region boundary is incorrect; change the conditional that checks
the soft-threshold region from using lambda to using 2 * lambda (i.e., replace
the current absZ <= lambda check with absZ < 2 * lambda) so the regions become:
soft threshold for |z| < 2λ, transition for 2λ ≤ |z| ≤ aλ, and no shrinkage for
|z| > aλ; keep the rest of the logic (Math.Sign(z) * Math.Max(0, absZ - lambda),
the transition formula using (a - 1) * z - Math.Sign(z) * a * lambda over (a -
2), and return z) unchanged.
🟡 Minor comments (31)
src/Preprocessing/FeatureSelection/Embedded/RidgeSelector.cs-156-157 (1)

156-157: ⚠️ Potential issue | 🟡 Minor

Silent handling of near-singular matrices may produce incorrect coefficients.

When the pivot element is near zero (line 156), the code continues without proper handling. Similarly, during back-substitution (line 176), skipping the division leaves x[i] with an incorrect value.

While alpha > 0 ensures the matrix is positive definite, alpha = 0 is permitted by the constructor and could lead to singularity with collinear features. Consider either:

  1. Throwing an exception when singularity is detected, or
  2. Requiring alpha > 0 in the constructor.
🛡️ Option 1: Throw on singularity
             if (Math.Abs(augmented[col, col]) < 1e-10)
-                continue;
+                throw new InvalidOperationException(
+                    $"Matrix is singular or near-singular at column {col}. Consider increasing alpha.");
🛡️ Option 2: Require alpha > 0
-        if (alpha < 0)
-            throw new ArgumentException("Alpha must be non-negative.", nameof(alpha));
+        if (alpha <= 0)
+            throw new ArgumentException("Alpha must be positive.", nameof(alpha));

Also applies to: 176-177

src/Preprocessing/FeatureSelection/Embedded/RidgeSelector.cs-54-61 (1)

54-61: ⚠️ Potential issue | 🟡 Minor

Add validation for empty data.

If data has zero rows, division by n on lines 73, 80, and 87 will produce NaN or infinity. Consider adding early validation.

🛡️ Proposed fix
     public void Fit(Matrix<T> data, Vector<T> target)
     {
         if (data.Rows != target.Length)
             throw new ArgumentException("Target length must match rows in data.");
+        if (data.Rows == 0)
+            throw new ArgumentException("Data must have at least one row.", nameof(data));
+        if (data.Columns == 0)
+            throw new ArgumentException("Data must have at least one column.", nameof(data));
 
         _nInputFeatures = data.Columns;
src/Preprocessing/FeatureSelection/Embedded/RidgeSelector.cs-222-233 (1)

222-233: ⚠️ Potential issue | 🟡 Minor

Inconsistent behavior when not fitted, and silent filtering of invalid indices.

  1. Line 224: Returns an empty array when not fitted, but GetSupportMask() throws InvalidOperationException. Consider being consistent.

  2. Line 230: The .Where(i => i < inputFeatureNames.Length) silently drops indices if inputFeatureNames is shorter than expected. This could hide bugs where the caller passes an incorrect names array, resulting in fewer output names than selected features.

♻️ Proposed fix for consistency and explicit validation
     public override string[] GetFeatureNamesOut(string[]? inputFeatureNames = null)
     {
-        if (_selectedIndices is null) return Array.Empty<string>();
+        if (_selectedIndices is null)
+            throw new InvalidOperationException("RidgeSelector has not been fitted.");
 
         if (inputFeatureNames is null)
             return _selectedIndices.Select(i => $"Feature{i}").ToArray();
 
+        if (inputFeatureNames.Length < _nInputFeatures)
+            throw new ArgumentException(
+                $"Expected at least {_nInputFeatures} feature names, but got {inputFeatureNames.Length}.",
+                nameof(inputFeatureNames));
+
         return _selectedIndices
-            .Where(i => i < inputFeatureNames.Length)
             .Select(i => inputFeatureNames[i])
             .ToArray();
     }
src/Preprocessing/FeatureSelection/Embedded/GradientBoostingImportance.cs-270-281 (1)

270-281: ⚠️ Potential issue | 🟡 Minor

Inconsistent behavior when not fitted.

GetSupportMask() throws InvalidOperationException when not fitted, but GetFeatureNamesOut() silently returns an empty array (line 272). Consider throwing for consistency, or document this intentional difference.

Also, the .Where(i => i < inputFeatureNames.Length) filter silently drops indices if the input array is too short, which could produce fewer output names than selected features.

🛠️ Proposed fix for consistency
     public override string[] GetFeatureNamesOut(string[]? inputFeatureNames = null)
     {
-        if (_selectedIndices is null) return [];
+        if (_selectedIndices is null)
+            throw new InvalidOperationException("GradientBoostingImportance has not been fitted.");

         if (inputFeatureNames is null)
             return _selectedIndices.Select(i => $"Feature{i}").ToArray();

+        if (inputFeatureNames.Length < _nInputFeatures)
+            throw new ArgumentException(
+                $"Expected at least {_nInputFeatures} feature names, but got {inputFeatureNames.Length}.",
+                nameof(inputFeatureNames));
+
         return _selectedIndices
-            .Where(i => i < inputFeatureNames.Length)
             .Select(i => inputFeatureNames[i])
             .ToArray();
     }
src/Preprocessing/FeatureSelection/Embedded/GradientBoostingImportance.cs-41-58 (1)

41-58: ⚠️ Potential issue | 🟡 Minor

Add validation for nEstimators, maxDepth, and learningRate.

The constructor validates nFeaturesToSelect but not the other parameters. If nEstimators <= 0, the boosting loop never runs and importances remain zero. If maxDepth <= 0, all nodes become leaves immediately with no splits (no importance accumulated). If learningRate <= 0, the model won't converge properly.

🛡️ Proposed fix to add parameter validation
     public GradientBoostingImportance(
         int nFeaturesToSelect = 10,
         int nEstimators = 100,
         int maxDepth = 3,
         double learningRate = 0.1,
         int? randomState = null,
         int[]? columnIndices = null)
         : base(columnIndices)
     {
         if (nFeaturesToSelect < 1)
             throw new ArgumentException("Number of features must be at least 1.", nameof(nFeaturesToSelect));
+        if (nEstimators < 1)
+            throw new ArgumentException("Number of estimators must be at least 1.", nameof(nEstimators));
+        if (maxDepth < 1)
+            throw new ArgumentException("Max depth must be at least 1.", nameof(maxDepth));
+        if (learningRate <= 0)
+            throw new ArgumentException("Learning rate must be positive.", nameof(learningRate));

         _nFeaturesToSelect = nFeaturesToSelect;
src/Preprocessing/FeatureSelection/Embedded/SCAD.cs-41-62 (1)

41-62: ⚠️ Potential issue | 🟡 Minor

Add validation for maxIterations and tolerance parameters.

The constructor validates nFeaturesToSelect, lambda, and a, but maxIterations and tolerance are not validated. A non-positive maxIterations would skip optimization entirely, and a non-positive tolerance could cause unexpected convergence behavior.

🛡️ Proposed fix to add validation
         if (a <= 2)
             throw new ArgumentException("Parameter 'a' must be greater than 2.", nameof(a));
+        if (maxIterations < 1)
+            throw new ArgumentException("Max iterations must be at least 1.", nameof(maxIterations));
+        if (tolerance <= 0)
+            throw new ArgumentException("Tolerance must be positive.", nameof(tolerance));
 
         _nFeaturesToSelect = nFeaturesToSelect;
src/Preprocessing/FeatureSelection/Embedded/SCAD.cs-261-284 (1)

261-284: ⚠️ Potential issue | 🟡 Minor

Inconsistent fitted-state handling and silent index filtering.

Two issues:

  1. GetSupportMask() throws if not fitted, but GetFeatureNamesOut() returns an empty array. Consider consistent behavior across both methods.

  2. Line 281 silently filters out indices when inputFeatureNames is shorter than expected. This could return fewer names than selected features, which may confuse users expecting a 1:1 mapping.

🔧 Proposed fix for consistency
     public override string[] GetFeatureNamesOut(string[]? inputFeatureNames = null)
     {
-        if (_selectedIndices is null) return [];
+        if (_selectedIndices is null)
+            throw new InvalidOperationException("SCAD has not been fitted.");
 
         if (inputFeatureNames is null)
             return _selectedIndices.Select(i => $"Feature{i}").ToArray();
 
+        if (inputFeatureNames.Length < _nInputFeatures)
+            throw new ArgumentException(
+                $"Expected at least {_nInputFeatures} feature names but got {inputFeatureNames.Length}.",
+                nameof(inputFeatureNames));
+
         return _selectedIndices
-            .Where(i => i < inputFeatureNames.Length)
             .Select(i => inputFeatureNames[i])
             .ToArray();
     }
src/Preprocessing/FeatureSelection/Dimensionality/PCABasedSelection.cs-78-84 (1)

78-84: ⚠️ Potential issue | 🟡 Minor

Division by zero when data has only one sample.

When n == 1, the denominator (n - 1) becomes zero, causing a division-by-zero exception. While single-sample datasets are uncommon for PCA, the method should fail gracefully.

🛡️ Proposed fix to add validation
 protected override void FitCore(Matrix<T> data)
 {
     _nInputFeatures = data.Columns;
     int n = data.Rows;
     int p = data.Columns;
+
+    if (n < 2)
+        throw new ArgumentException("PCA-based selection requires at least 2 samples.", nameof(data));
 
     // Center the data
src/Preprocessing/FeatureSelection/Dimensionality/PCABasedSelection.cs-207-218 (1)

207-218: ⚠️ Potential issue | 🟡 Minor

Inconsistent behavior when not fitted, and silent index filtering.

Two concerns:

  1. Line 209: Returns an empty array when not fitted, whereas GetSupportMask() and TransformCore() throw InvalidOperationException. Consider throwing for consistency.

  2. Lines 214-217: Silently filters indices where i >= inputFeatureNames.Length. If the caller provides a mismatched array, this silently returns fewer names than expected, potentially hiding bugs.

🛡️ Proposed fix for consistent error handling
 public override string[] GetFeatureNamesOut(string[]? inputFeatureNames = null)
 {
-    if (_selectedIndices is null) return [];
+    if (_selectedIndices is null)
+        throw new InvalidOperationException("PCABasedSelection has not been fitted.");
 
     if (inputFeatureNames is null)
         return _selectedIndices.Select(i => $"Feature{i}").ToArray();
 
+    if (inputFeatureNames.Length < _nInputFeatures)
+        throw new ArgumentException(
+            $"Expected at least {_nInputFeatures} feature names, but got {inputFeatureNames.Length}.",
+            nameof(inputFeatureNames));
+
     return _selectedIndices
-        .Where(i => i < inputFeatureNames.Length)
         .Select(i => inputFeatureNames[i])
         .ToArray();
 }
src/Preprocessing/FeatureSelection/Bioinformatics/SAM.cs-53-64 (1)

53-64: ⚠️ Potential issue | 🟡 Minor

Validate s0Percentile bounds to avoid out-of-range index.

If s0Percentile is outside [0, 1], s0Index can go negative or exceed p-1, causing runtime failures at Line 129.

🔧 Proposed fix
         if (nPermutations < 10)
             throw new ArgumentException("Number of permutations must be at least 10.", nameof(nPermutations));
         if (fdrThreshold <= 0 || fdrThreshold >= 1)
             throw new ArgumentException("FDR threshold must be between 0 and 1.", nameof(fdrThreshold));
+        if (s0Percentile < 0 || s0Percentile > 1)
+            throw new ArgumentException("s0Percentile must be between 0 and 1.", nameof(s0Percentile));
src/Preprocessing/FeatureSelection/Embedded/RandomForestImportance.cs-41-60 (1)

41-60: ⚠️ Potential issue | 🟡 Minor

Add validation for maxDepth and minSamplesLeaf parameters.

The constructor validates nFeaturesToSelect and nTrees but not maxDepth or minSamplesLeaf. Setting maxDepth <= 0 would prevent any splits (since depth >= _maxDepth is checked at line 139), and minSamplesLeaf < 1 could cause unexpected behavior.

🛡️ Proposed fix to add validation
         if (nTrees < 1)
             throw new ArgumentException("Number of trees must be at least 1.", nameof(nTrees));
+        if (maxDepth < 1)
+            throw new ArgumentException("Max depth must be at least 1.", nameof(maxDepth));
+        if (minSamplesLeaf < 1)
+            throw new ArgumentException("Min samples per leaf must be at least 1.", nameof(minSamplesLeaf));
 
         _nFeaturesToSelect = nFeaturesToSelect;
src/Preprocessing/FeatureSelection/Embedded/RandomForestImportance.cs-109-109 (1)

109-109: ⚠️ Potential issue | 🟡 Minor

Unused rand parameter passed to BuildTree.

The rand parameter is passed to BuildTree but never used within the method. Either remove the parameter or use it (e.g., for random tie-breaking or per-node feature re-sampling, which is standard Random Forest behavior).

♻️ Option 1: Remove unused parameter
-            var treeImportances = BuildTree(X, y, sampleIndices, featureSubset, 0, rand);
+            var treeImportances = BuildTree(X, y, sampleIndices, featureSubset, 0);

And update the method signature at line 134:

-    private double[] BuildTree(double[,] X, double[] y, int[] indices, int[] features, int depth, Random rand)
+    private double[] BuildTree(double[,] X, double[] y, int[] indices, int[] features, int depth)
src/Preprocessing/FeatureSelection/Embedded/RandomForestImportance.cs-244-255 (1)

244-255: ⚠️ Potential issue | 🟡 Minor

Silent filtering may mask dimension mismatches.

Line 252 filters out selected indices that exceed inputFeatureNames.Length. This silently produces a shorter result if the input names don't match the original feature count, potentially masking bugs where the model was fit on different data.

Consider throwing or logging a warning when inputFeatureNames.Length != _nInputFeatures.

🛡️ Proposed fix to validate input feature names length
     public override string[] GetFeatureNamesOut(string[]? inputFeatureNames = null)
     {
         if (_selectedIndices is null) return Array.Empty<string>();
 
         if (inputFeatureNames is null)
             return _selectedIndices.Select(i => $"Feature{i}").ToArray();
 
+        if (inputFeatureNames.Length != _nInputFeatures)
+            throw new ArgumentException(
+                $"Expected {_nInputFeatures} feature names but received {inputFeatureNames.Length}.",
+                nameof(inputFeatureNames));
+
-        return _selectedIndices
-            .Where(i => i < inputFeatureNames.Length)
-            .Select(i => inputFeatureNames[i])
-            .ToArray();
+        return _selectedIndices.Select(i => inputFeatureNames[i]).ToArray();
     }
src/Preprocessing/FeatureSelection/Embedded/MCP.cs-41-62 (1)

41-62: ⚠️ Potential issue | 🟡 Minor

Validate maxIterations and tolerance to avoid silent no-ops.

If maxIterations <= 0 the optimizer never runs; if tolerance <= 0 convergence checks become meaningless. Guard these like the other parameters.

🧩 Suggested validation
         if (gamma <= 1)
             throw new ArgumentException("Gamma must be greater than 1.", nameof(gamma));
+        if (maxIterations < 1)
+            throw new ArgumentException("Max iterations must be at least 1.", nameof(maxIterations));
+        if (tolerance <= 0)
+            throw new ArgumentException("Tolerance must be positive.", nameof(tolerance));

         _nFeaturesToSelect = nFeaturesToSelect;
src/Preprocessing/FeatureSelection/Dimensionality/TruncatedSVDSelection.cs-194-204 (1)

194-204: ⚠️ Potential issue | 🟡 Minor

Avoid silently dropping feature names when input list is shorter.
Currently, shorter inputFeatureNames produces fewer names than selected features. Prefer explicit validation to prevent silent mismatch.

✅ Suggested fix
     public override string[] GetFeatureNamesOut(string[]? inputFeatureNames = null)
     {
         if (_selectedIndices is null) return [];

         if (inputFeatureNames is null)
             return _selectedIndices.Select(i => $"Feature{i}").ToArray();
+        if (inputFeatureNames.Length < _nInputFeatures)
+            throw new ArgumentException($"Expected at least {_nInputFeatures} feature names.", nameof(inputFeatureNames));

         return _selectedIndices
-            .Where(i => i < inputFeatureNames.Length)
             .Select(i => inputFeatureNames[i])
             .ToArray();
     }
src/Preprocessing/FeatureSelection/Dimensionality/TruncatedSVDSelection.cs-161-172 (1)

161-172: ⚠️ Potential issue | 🟡 Minor

Validate input feature count before indexing.
If TransformCore receives data with a different column count than fit, this will throw an index error. A clear guard improves reliability.

✅ Suggested fix
     protected override Matrix<T> TransformCore(Matrix<T> data)
     {
         if (_selectedIndices is null)
             throw new InvalidOperationException("TruncatedSVDSelection has not been fitted.");
+        if (data.Columns != _nInputFeatures)
+            throw new ArgumentException($"Expected {_nInputFeatures} features, got {data.Columns}.", nameof(data));

         int numRows = data.Rows;
         int numCols = _selectedIndices.Length;
         var result = new T[numRows, numCols];
src/Preprocessing/FeatureSelection/Embedded/PermutationImportance.cs-246-256 (1)

246-256: ⚠️ Potential issue | 🟡 Minor

Fail fast when not fitted in GetFeatureNamesOut

GetSupportMask() throws when not fitted, but GetFeatureNamesOut() silently returns an empty array. This can hide misuse and produce confusing downstream behavior. Consider throwing for consistency.

🛡️ Suggested change
-        if (_selectedIndices is null) return [];
+        if (_selectedIndices is null)
+            throw new InvalidOperationException("PermutationImportance has not been fitted.");
src/Preprocessing/FeatureSelection/Bioinformatics/FoldChangeSelector.cs-25-52 (1)

25-52: ⚠️ Potential issue | 🟡 Minor

_logTransform has no effect on selection.

The parameter is stored but never used; selection always ranks by abs(log2 fold change), so callers can’t disable log-based scoring. Either remove the parameter or use it to drive ranking.

💡 Suggested fix (use _logTransform to drive scoring)
-        var candidateFeatures = new List<(int Index, double AbsFC)>();
+        var candidateFeatures = new List<(int Index, double Score)>();
         for (int j = 0; j < p; j++)
         {
             if (_foldChanges[j] >= _minFoldChange)
-                candidateFeatures.Add((j, Math.Abs(_log2FoldChanges[j])));
+            {
+                double score = _logTransform ? Math.Abs(_log2FoldChanges[j]) : _foldChanges[j];
+                candidateFeatures.Add((j, score));
+            }
         }
 
         if (candidateFeatures.Count >= _nFeaturesToSelect)
         {
             _selectedIndices = candidateFeatures
-                .OrderByDescending(x => x.AbsFC)
+                .OrderByDescending(x => x.Score)
                 .Take(_nFeaturesToSelect)
                 .Select(x => x.Index)
                 .OrderBy(x => x)
                 .ToArray();
         }
         else
         {
             // Fall back to top by fold change
             _selectedIndices = _foldChanges
-                .Select((fc, idx) => (FC: fc, Index: idx))
-                .OrderByDescending(x => x.FC)
+                .Select((fc, idx) => (Score: _logTransform ? Math.Abs(_log2FoldChanges[idx]) : fc, Index: idx))
+                .OrderByDescending(x => x.Score)
                 .Take(_nFeaturesToSelect)
                 .Select(x => x.Index)
                 .OrderBy(x => x)
                 .ToArray();
         }

Also applies to: 119-144

src/Preprocessing/FeatureSelection/Embedded/TreeBasedImportance.cs-106-116 (1)

106-116: ⚠️ Potential issue | 🟡 Minor

Variance calculation may produce negative values.

The formula sumSq / _nTrees - _importances[j] * _importances[j] at line 115 can produce small negative values due to floating-point errors when variance is near zero. This would cause Math.Sqrt to return NaN.

Proposed fix
-            _importanceStds[j] = Math.Sqrt(sumSq / _nTrees - _importances[j] * _importances[j]);
+            double variance = sumSq / _nTrees - _importances[j] * _importances[j];
+            _importanceStds[j] = Math.Sqrt(Math.Max(0, variance));
src/Preprocessing/FeatureSelection/Embedded/DropColumnImportance.cs-196-200 (1)

196-200: ⚠️ Potential issue | 🟡 Minor

Potential correlation formula issue in ComputeR2Score.

The correlation calculation at line 199 appears to have an extra nTrain factor under the square root. For Pearson correlation, sxy and sxx are already sums over all samples, so the denominator should be Math.Sqrt(sxx * yVar * nTrain * nTrain) or equivalently the formula should use sample covariance/variance instead of sums.

Current: sxy / Math.Sqrt(sxx * yVar * nTrain)
Expected for correlation: sxy / Math.Sqrt(sxx * syy) where syy = yVar * nTrain

This may cause the correlation values to be scaled incorrectly, though feature ranking could still work since all features are equally affected.

Proposed fix
-            correlations[j] = (sxx > 1e-10 && yVar > 1e-10) ? sxy / Math.Sqrt(sxx * yVar * nTrain) : 0;
+            double syy = yVar * nTrain;
+            correlations[j] = (sxx > 1e-10 && syy > 1e-10) ? sxy / Math.Sqrt(sxx * syy) : 0;
src/Preprocessing/FeatureSelection/Embedded/ElasticNetSelector.cs-50-51 (1)

50-51: ⚠️ Potential issue | 🟡 Minor

Inconsistent validation: alpha >= 0 here vs alpha > 0 in ElasticNetFS.

ElasticNetSelector allows alpha = 0 (no regularization), while ElasticNetFS requires alpha > 0. This API inconsistency may confuse users.

Consider aligning the validation across all three Elastic Net implementations.

src/Preprocessing/FeatureSelection/Embedded/BorutaSelector.cs-98-101 (1)

98-101: ⚠️ Potential issue | 🟡 Minor

Incorrect initialization of maxShadow biases against negative importances.

Initializing maxShadow = 0 means that if all shadow importances are negative (possible with some importance functions), no real feature will register a "hit" even if it outperforms all shadows.

Proposed fix
             // Find max shadow importance
-            double maxShadow = 0;
+            double maxShadow = double.NegativeInfinity;
             for (int j = p; j < p * 2; j++)
                 maxShadow = Math.Max(maxShadow, importances[j]);
src/Preprocessing/FeatureSelection/Embedded/BorutaSelector.cs-43-55 (1)

43-55: ⚠️ Potential issue | 🟡 Minor

Missing constructor validation and unused parameter.

  1. No validation for nIterations and nTrees - values <= 0 would cause issues
  2. _percentile is stored but never used in the implementation
Suggested validation
     public BorutaSelector(
         int nIterations = 100,
         int nTrees = 50,
         double percentile = 100.0,
         int? randomState = null,
         int[]? columnIndices = null)
         : base(columnIndices)
     {
+        if (nIterations < 1)
+            throw new ArgumentException("Number of iterations must be at least 1.", nameof(nIterations));
+        if (nTrees < 1)
+            throw new ArgumentException("Number of trees must be at least 1.", nameof(nTrees));
+        
         _nIterations = nIterations;
         _nTrees = nTrees;
-        _percentile = percentile;
+        _percentile = percentile;  // TODO: implement percentile-based selection or remove parameter
         _randomState = randomState;
     }
src/Preprocessing/FeatureSelection/Bioinformatics/Boruta.cs-232-247 (1)

232-247: ⚠️ Potential issue | 🟡 Minor

NormalQuantile approximation incorrect for p < 0.5.

The Abramowitz-Stegun rational approximation used here is designed for the upper tail. When p < 0.5, Math.Log(1 - p) produces a negative value and Math.Sqrt(-2 * Math.Log(1 - p)) will produce NaN since the argument becomes negative.

For BinomialThreshold, this is called with 1 - alpha (typically 0.95), so it works. However, the method is fragile if reused elsewhere.

Suggested fix for robustness
     private double NormalQuantile(double p)
     {
-        // Approximate inverse normal CDF
         if (p <= 0) return double.NegativeInfinity;
         if (p >= 1) return double.PositiveInfinity;
+        
+        // Use symmetry for lower tail
+        if (p < 0.5)
+            return -NormalQuantile(1 - p);

         double t = Math.Sqrt(-2 * Math.Log(1 - p));
src/Preprocessing/FeatureSelection/Embedded/TreeBasedFS.cs-127-130 (1)

127-130: ⚠️ Potential issue | 🟡 Minor

Edge case: No thresholds evaluated when feature has exactly 2 distinct values.

When values.Count == 2, Skip(1).Take(values.Count - 2) results in Take(0), meaning no thresholds are evaluated for that feature. This prevents binary features from ever contributing to splits.

Consider using Skip(1) alone or Take(values.Count - 1) to include at least one threshold:

Proposed fix
-            foreach (double threshold in values.Skip(1).Take(values.Count - 2))
+            foreach (double threshold in values.Skip(1))
src/Preprocessing/FeatureSelection/CostSensitive/BudgetConstrainedFS.cs-23-24 (1)

23-24: ⚠️ Potential issue | 🟡 Minor

Persist and validate feature costs used for budgeting.

When Fit(..., featureCosts) is used, _featureCosts never updates, so FeatureCosts can remain null even though costs were provided. Also, negative/NaN/∞ costs can invalidate the budget constraint (e.g., negative costs decrease _usedBudget). Persist the effective costs and validate they’re finite and non‑negative.

🛠️ Suggested fix
-    private readonly double[]? _featureCosts;
+    private double[]? _featureCosts;
@@
         var costs = _featureCosts ?? CreateUnitCosts(p);
         if (costs.Length != p)
             throw new ArgumentException("Feature costs length must match number of features.");
+        for (int j = 0; j < p; j++)
+        {
+            if (double.IsNaN(costs[j]) || double.IsInfinity(costs[j]) || costs[j] < 0)
+                throw new ArgumentException("Feature costs must be finite and non-negative.");
+        }
+        _featureCosts = costs;
@@
     public void Fit(Matrix<T> data, Vector<T> target, double[] featureCosts)
     {
         if (featureCosts.Length != data.Columns)
             throw new ArgumentException("Feature costs must match number of columns.");
-
-        _nInputFeatures = data.Columns;
-        int n = data.Rows;
-        int p = data.Columns;
-
-        // Compute relevance scores
-        _relevanceScores = ComputeRelevanceScores(data, target, n, p);
-
-        // Compute value per cost
-        _valuePerCost = new double[p];
-        for (int j = 0; j < p; j++)
-        {
-            _valuePerCost[j] = featureCosts[j] > 1e-10
-                ? _relevanceScores[j] / featureCosts[j]
-                : _relevanceScores[j] * 1000;
-        }
-
-        // Greedy selection within budget
-        var selected = new List<int>();
-        _usedBudget = 0;
-
-        var sortedFeatures = Enumerable.Range(0, p)
-            .OrderByDescending(j => _valuePerCost[j])
-            .ToList();
-
-        foreach (int j in sortedFeatures)
-        {
-            if (_usedBudget + featureCosts[j] <= _budget)
-            {
-                selected.Add(j);
-                _usedBudget += featureCosts[j];
-            }
-        }
-
-        _selectedIndices = selected.OrderBy(x => x).ToArray();
-
-        IsFitted = true;
+        _featureCosts = featureCosts;
+        Fit(data, target);
     }

Also applies to: 33-33, 59-104, 106-147

docs/feature-selection-methods-catalog.md-103-111 (1)

103-111: ⚠️ Potential issue | 🟡 Minor

Escape the pipe in H(Y|X) to keep the table columns aligned.

Line 109 uses H(Y|X), which creates an extra column in Markdown tables (MD056). Escaping the pipe (or wrapping in code) fixes rendering.

🔧 Proposed fix
- | Conditional entropy | H(Y|X) | High |
+ | Conditional entropy | H(Y\|X) | High |
src/Preprocessing/FeatureSelection/DomainSpecific/TextFeatureSelector.cs-51-56 (1)

51-56: ⚠️ Potential issue | 🟡 Minor

Add an ordering check for min/max document frequency.

minDocumentFrequency > maxDocumentFrequency is an invalid configuration and silently yields empty candidates.

🔧 Proposed fix
         if (maxDocumentFrequency < 0 || maxDocumentFrequency > 1)
             throw new ArgumentException("Max document frequency must be between 0 and 1.", nameof(maxDocumentFrequency));
+        if (minDocumentFrequency > maxDocumentFrequency)
+            throw new ArgumentException("Min document frequency must be <= max document frequency.", nameof(minDocumentFrequency));
src/Evaluation/DefaultModelEvaluator.cs-559-560 (1)

559-560: ⚠️ Potential issue | 🟡 Minor

Doc wording still says “normalized.”
The parameter now represents preprocessing (not just normalization). Consider updating the XML doc for clarity.

📄 Suggested doc tweak
-    /// <param name="preprocessingInfo">Information about how the data was normalized.</param>
+    /// <param name="preprocessingInfo">Information about how the data was preprocessed.</param>
src/Preprocessing/FeatureSelection/Bioinformatics/MRMR.cs-68-171 (1)

68-171: ⚠️ Potential issue | 🟡 Minor

Guard against empty datasets.

With data.Rows == 0, relevance stays zero and selection becomes arbitrary. Prefer failing fast.

🔧 Suggested fix
 public void Fit(Matrix<T> data, Vector<T> target)
 {
     if (data.Rows != target.Length)
         throw new ArgumentException("Target length must match rows in data.");
+    if (data.Rows == 0)
+        throw new ArgumentException("Data must contain at least one row.", nameof(data));
src/Preprocessing/FeatureSelection/Bioinformatics/MRMR.cs-250-264 (1)

250-264: ⚠️ Potential issue | 🟡 Minor

Validate input shape before indexing selected columns.

If Transform is called on data with a different column count than fit-time, this can index the wrong columns or throw.

🔧 Suggested fix
 protected override Matrix<T> TransformCore(Matrix<T> data)
 {
     if (_selectedIndices is null)
         throw new InvalidOperationException("MRMR has not been fitted.");
+    if (data.Columns != _nInputFeatures)
+        throw new ArgumentException("Input feature count does not match the fitted data.", nameof(data));
 
     int numRows = data.Rows;

Comment thread src/Preprocessing/FeatureSelection/Bioinformatics/SAM.cs
Comment thread src/Preprocessing/FeatureSelection/Bioinformatics/SAM.cs
Comment thread src/Preprocessing/FeatureSelection/Bioinformatics/SignificanceAnalysis.cs Outdated
Comment thread src/Preprocessing/FeatureSelection/Embedded/ElasticNetFS.cs Outdated
Comment thread src/Preprocessing/FeatureSelection/Embedded/GroupLassoSelector.cs
Comment thread src/Preprocessing/FeatureSelection/Embedded/MCPSelector.cs
Comment thread src/Preprocessing/FeatureSelection/Embedded/SCADSelector.cs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

🤖 Fix all issues with AI agents
In `@src/Preprocessing/FeatureSelection/Bayesian/ARDSelector.cs`:
- Around line 150-151: In ARDSelector where beta is computed from gammaSum and
residualSum (beta = (n - gammaSum) / (residualSum + 1e-10)), guard against
gammaSum >= n by clamping beta to a small positive floor: compute the raw value
then replace beta with Math.Max(rawBeta, minBeta) (e.g., minBeta = 1e-10 or
configurable) so beta never becomes zero or negative and subsequent iterations
remain valid; reference variables: gammaSum, beta, n, residualSum and
class/method ARDSelector where this update occurs.

In `@src/Preprocessing/FeatureSelection/Bayesian/BayesianFeatureSelector.cs`:
- Around line 121-126: The likelihoodRatio computation can overflow: instead of
calling Math.Exp(tStatistic * tStatistic / 2) directly, compute the exponent
(e.g., var exp = tStatistic * tStatistic / 2), clamp it to a safe maximum (like
Math.Min(exp, 50) as done in SpikeAndSlabSelector), then call
Math.Exp(clampedExp) to produce likelihoodRatio; recalc posteriorOdds using
_priorProbability and handle any remaining infinite odds by capping to a large
finite value before computing _posteriorProbabilities[j] to avoid NaN.

In `@src/Preprocessing/FeatureSelection/Bayesian/SpikeAndSlabSelector.cs`:
- Around line 39-60: The constructor SpikeAndSlabSelector currently lacks
validation for priorInclusionProbability and nIterations; add checks in the
constructor to ensure priorInclusionProbability is strictly between 0 and 1
(throw ArgumentException with nameof(priorInclusionProbability) if not) to avoid
division-by-zero or degenerate odds, and ensure nIterations is at least 1 (throw
ArgumentException with nameof(nIterations) if not); reference the fields
_priorInclusionProbability and _nIterations when adding these validations so
invalid values are rejected early.

In `@src/Preprocessing/FeatureSelection/Causal/MarkovBlanketSelector.cs`:
- Around line 37-49: The MarkovBlanketSelector constructor does not validate
nBins allowing nBins <= 0 which will cause invalid discretization/negative bin
indices; update the MarkovBlanketSelector constructor to check that nBins is
greater than 0 and throw an ArgumentException (or ArgumentOutOfRangeException)
with a clear message and parameter name when the check fails, ensuring the field
_nBins is only set after validation.
- Around line 108-115: When the Markov blanket is empty the fallback selects up
to 5 features ignoring the MaxFeatures limit; update the fallback in the block
that sets _selectedIndices so it respects MaxFeatures by replacing the
Take(Math.Min(5, p)) with a nested Math.Min that also includes MaxFeatures
(e.g., Take(Math.Min(MaxFeatures, Math.Min(5, p)))), keeping the same
OrderByDescending on _relevanceScores and final OrderBy(x => x) to produce the
sorted index array.
- Around line 197-249: In ComputeConditionalMI, totalMI is being accumulated
over conditioning features/groups but never normalized, so MI scales with the
size of conditionSet; update the logic to average rather than sum by dividing
totalMI by the number of conditioning features considered (use the existing
count or conditionSet.Count) before returning (or adjust weighting to intended
normalization), ensuring you still fallback to ComputeMutualInformation when
count == 0; reference totalMI, count, conditionSet and ComputeConditionalMI when
making the change.

In `@src/Preprocessing/FeatureSelection/Causal/PCAlgorithmSelector.cs`:
- Around line 235-271: ComputeResiduals currently performs sequential univariate
regressions (order-dependent) instead of a single multivariate OLS; replace this
by constructing the design matrix Z of all conditioning predictors for the
target (including a column of ones for the intercept), solve the normal
equations β = (Z'Z)^{-1} Z' y (implement a stable solver such as Cholesky or
Gaussian elimination in a helper like SolveOLS), then compute residuals as y - Z
* β; update references to ComputeResiduals to call SolveOLS and remove the
iterative per-predictor subtraction so residuals reflect simultaneous regression
of all predictors.
- Around line 323-334: GetFeatureNamesOut currently returns an empty array when
the selector isn't fitted (_selectedIndices is null) and silently drops
out-of-range indices from inputFeatureNames; make it consistent with
GetSupportMask by throwing InvalidOperationException when _selectedIndices is
null, and when inputFeatureNames is provided validate that all indices in
_selectedIndices are < inputFeatureNames.Length, throwing an ArgumentException
(or similar) if any index is out-of-range, otherwise map indices to names;
reference GetFeatureNamesOut, GetSupportMask, and _selectedIndices when locating
the change.
- Around line 175-181: In TestConditionalIndependence, prevent numerical
instability in the Fisher z-transformation by (1) computing df = n -
condSet.Count - 3 and if df <= 0 return false (treat as dependent) to avoid sqrt
of non-positive and invalid inference, and (2) clamp the partialCorr returned by
ComputePartialCorrelation into a safe open interval (e.g. -1 + eps .. 1 - eps
with eps = 1e-10) or add eps to both (1+partialCorr) and (1-partialCorr) before
taking Math.Log to avoid Log(0)/division by zero; then compute z = Math.Sqrt(df)
* 0.5 * Math.Log((1+pc)/(1-pc)) and pValue via NormalCDF, keeping the existing
comparison to _alpha. Ensure these changes are made inside
TestConditionalIndependence and that ComputePartialCorrelation and NormalCDF
names are used unchanged.

In `@src/Preprocessing/FeatureSelection/Clustering/ClusteringBasedFS.cs`:
- Around line 79-86: The code divides by (n - 1) when computing variance in the
FitCore loop (assigning _featureScores[j]) and similarly in
ComputeFeatureCorrelations, which causes division by zero for n == 1; add a
guard so when n <= 1 you use a safe fallback (e.g., set variance/correlation to
0 or use population variance by dividing by n when n == 1) and only divide by (n
- 1) for n > 1, updating both the variance computation in FitCore (where
_featureScores[j] is set) and the analogous computation inside
ComputeFeatureCorrelations.
- Around line 38-49: The constructor ClusteringBasedFS currently validates
nClusters but not maxIterations; add validation in the ClusteringBasedFS(...)
constructor to ensure maxIterations > 0 and throw an ArgumentException (use
nameof(maxIterations)) when it's not, then only assign _maxIterations after
validation so the k-means loop won't be skipped by non-positive values.
- Around line 257-268: GetFeatureNamesOut currently silently returns an empty
array when not fitted and drops out-of-range indices when inputFeatureNames
length mismatches; align it with GetSupportMask by throwing when
_selectedIndices is null (e.g., InvalidOperationException) and validate
inputFeatureNames by checking if any index in _selectedIndices >=
inputFeatureNames.Length, then throw an ArgumentException (or log and throw)
describing the mismatch and listing the offending indices; keep the rest of the
mapping logic (Select(i => inputFeatureNames[i]) and default generated names)
intact.

In `@src/Preprocessing/FeatureSelection/Clustering/HierarchicalFS.cs`:
- Around line 49-105: In FitCore (and ensure the same guard is present in
ComputeFeatureDistances) add an early validation that n (data.Rows) is at least
2 and throw or return a clear error if not to avoid division by zero when
computing variance/correlation; also validate that _nFeaturesToSelect is between
1 and p (data.Columns) and if it's larger than p clamp it to p or throw an
ArgumentException so clustering/AgglomerativeClustering receives a valid k;
ensure you do not perform the variance loop or divide by (n - 1) when n < 2 and
set _selectedIndices/_featureScores to sensible defaults before
returning/throwing so downstream code sees a consistent state.
- Around line 221-234: TransformCore currently assumes input has enough columns
and will throw IndexOutOfRangeException if not; add an explicit shape check at
the start of TransformCore (before computing numRows/numCols) that verifies
data.Columns is greater than the maximum index in _selectedIndices (e.g.,
data.Columns > _selectedIndices.Max()) and throw a clear ArgumentException or
InvalidOperationException with a message like "Input matrix has too few columns
for fitted selected indices" when the check fails; reference TransformCore and
_selectedIndices to locate where to add the check and ensure it runs before the
element-wise copy and before constructing the result Matrix<T>.

In `@src/Preprocessing/FeatureSelection/Constraint/ConstraintBasedSelector.cs`:
- Around line 129-138: Validate indices before adding a feature group: when
iterating the discovered group (variable group from _featureGroups), filter out
any indices that are negative or >= the number of columns in the data (the same
bound checks used by TransformCore) and exclude them from groupToAdd and
forbidden checks; only count and add indices that pass this validation and still
respect _nFeaturesToSelect and existing selected/forbidden sets (update
groupToAdd generation to use the validated index set).
- Around line 189-200: GetFeatureNamesOut currently returns an empty array when
_selectedIndices is null and silently filters out-of-range indices, which is
inconsistent with GetSupportMask/TransformCore; update GetFeatureNamesOut to
throw InvalidOperationException if _selectedIndices is null (like
GetSupportMask/TransformCore), and when inputFeatureNames is provided validate
that every index in _selectedIndices is within inputFeatureNames.Length and
throw an ArgumentException (or InvalidOperationException) if any index is out of
range instead of filtering; retain the existing fallback of generating
"Feature{i}" names only when inputFeatureNames is null.
- Around line 38-53: In the ConstraintBasedSelector constructor add validation
to detect contradictory or impossible constraints: check for any overlap between
mandatoryFeatures and forbiddenFeatures (e.g., any element present in both) and
throw an ArgumentException naming the conflict; also verify
mandatoryFeatures.Length <= nFeaturesToSelect and throw an ArgumentException if
it exceeds _nFeaturesToSelect; perform null checks before accessing arrays and
assign to _mandatoryFeatures/_forbiddenFeatures/_nFeaturesToSelect only after
validations so the object cannot be created in an invalid state.
🧹 Nitpick comments (11)
src/Preprocessing/FeatureSelection/Bayesian/SpikeAndSlabSelector.cs (1)

184-195: Silent filtering may produce fewer output names than selected features.

When inputFeatureNames.Length < _nInputFeatures, indices beyond the array bounds are silently filtered out (line 192), causing the returned array to have fewer elements than _selectedIndices.Length. Consider throwing an exception or returning placeholder names for out-of-bounds indices to maintain consistency.

♻️ Alternative: validate or pad missing names
     public override string[] GetFeatureNamesOut(string[]? inputFeatureNames = null)
     {
         if (_selectedIndices is null) return [];
 
         if (inputFeatureNames is null)
             return _selectedIndices.Select(i => $"Feature{i}").ToArray();
 
         return _selectedIndices
-            .Where(i => i < inputFeatureNames.Length)
-            .Select(i => inputFeatureNames[i])
+            .Select(i => i < inputFeatureNames.Length ? inputFeatureNames[i] : $"Feature{i}")
             .ToArray();
     }
src/Preprocessing/FeatureSelection/Bayesian/ARDSelector.cs (2)

41-56: Missing validation for alphaThreshold, maxIterations, and tolerance.

These parameters should be validated: alphaThreshold should be positive, maxIterations should be at least 1, and tolerance should be non-negative.

🛡️ Proposed validation additions
     public ARDSelector(
         int nFeaturesToSelect = 10,
         double alphaThreshold = 1e8,
         int maxIterations = 300,
         double tolerance = 1e-4,
         int[]? columnIndices = null)
         : base(columnIndices)
     {
         if (nFeaturesToSelect < 1)
             throw new ArgumentException("Number of features must be at least 1.", nameof(nFeaturesToSelect));
+        if (alphaThreshold <= 0)
+            throw new ArgumentException("Alpha threshold must be positive.", nameof(alphaThreshold));
+        if (maxIterations < 1)
+            throw new ArgumentException("Max iterations must be at least 1.", nameof(maxIterations));
+        if (tolerance < 0)
+            throw new ArgumentException("Tolerance must be non-negative.", nameof(tolerance));
 
         _nFeaturesToSelect = nFeaturesToSelect;

225-236: Same silent filtering issue as other selectors.

The Where(i => i < inputFeatureNames.Length) filter silently drops indices, potentially returning fewer names than selected features. Consider using the same fix pattern suggested for SpikeAndSlabSelector for consistency across all Bayesian selectors.

src/Preprocessing/FeatureSelection/Bayesian/BayesianFeatureSelector.cs (3)

27-27: _threshold field is stored but never used.

The threshold constructor parameter is assigned to _threshold (line 53) but never referenced in any method. Either remove the unused parameter/field or implement the intended threshold-based selection logic.

Also applies to: 42-42, 53-53


78-79: Unused variables positiveCount and negativeCount.

These variables are computed but never used in the algorithm. Remove them to reduce confusion.

♻️ Remove dead code
         for (int i = 0; i < n; i++)
             targetArray[i] = NumOps.ToDouble(target[i]);
 
-        double positiveCount = targetArray.Count(y => y > 0.5);
-        double negativeCount = n - positiveCount;
-
         for (int j = 0; j < p; j++)

178-189: Same silent filtering pattern as other selectors.

For consistency, apply the same fix across all three Bayesian selectors.

src/Preprocessing/FeatureSelection/Clustering/ClusteringBasedFS.cs (2)

33-35: Consider defensive copies for exposed arrays.

Properties ClusterAssignments, FeatureScores, and SelectedIndices return internal arrays directly. External code could inadvertently mutate internal state. Consider returning copies or using ReadOnlySpan<T>/IReadOnlyList<T> if immutability guarantees are desired.

♻️ Example using array copies
-    public int[]? ClusterAssignments => _clusterAssignments;
-    public double[]? FeatureScores => _featureScores;
-    public int[]? SelectedIndices => _selectedIndices;
+    public int[]? ClusterAssignments => _clusterAssignments?.ToArray();
+    public double[]? FeatureScores => _featureScores?.ToArray();
+    public int[]? SelectedIndices => _selectedIndices?.ToArray();

156-158: Consider using standard Random instead of cryptographic RNG for k-means initialization.

RandomHelper.CreateSecureRandom() provides cryptographically secure randomness, which is unnecessary for k-means centroid initialization and may have performance overhead. A standard Random instance would suffice for this non-security-sensitive use case.

src/Preprocessing/FeatureSelection/Constraint/ConstraintBasedSelector.cs (1)

34-35: Consider defensive copies for exposed arrays.

FeatureScores and SelectedIndices return internal arrays directly, allowing callers to mutate the selector's state. Consider returning clones or ReadOnlySpan<T>/IReadOnlyList<T>.

🛡️ Suggested defensive copy pattern
-    public double[]? FeatureScores => _featureScores;
-    public int[]? SelectedIndices => _selectedIndices;
+    public double[]? FeatureScores => _featureScores?.ToArray();
+    public int[]? SelectedIndices => _selectedIndices?.ToArray();
src/Preprocessing/FeatureSelection/Causal/PCAlgorithmSelector.cs (2)

40-55: Consider validating maxConditioningSetSize parameter.

The constructor validates maxFeatures and alpha, but maxConditioningSetSize is not validated. A negative value would cause the PC algorithm loop (line 91) to skip entirely, potentially leading to unexpected behavior where no edges are pruned.

Proposed validation
         if (alpha <= 0 || alpha >= 1)
             throw new ArgumentException("Alpha must be between 0 and 1.", nameof(alpha));
+        if (maxConditioningSetSize < 0)
+            throw new ArgumentException("Max conditioning set size cannot be negative.", nameof(maxConditioningSetSize));
 
         _maxFeatures = maxFeatures;

158-173: Minor allocation overhead in combination generation.

The recursive items.Skip(i + 1).ToList() creates a new list for each recursive call. For typical PC algorithm usage with small conditioning sets (default max 3), this is acceptable. If larger conditioning sets are anticipated, consider using index-based iteration to avoid allocations.

Comment thread src/Preprocessing/FeatureSelection/Bayesian/ARDSelector.cs Outdated
Comment thread src/Preprocessing/FeatureSelection/Bayesian/SpikeAndSlabSelector.cs
Comment thread src/Preprocessing/FeatureSelection/Causal/MarkovBlanketSelector.cs
Comment thread src/Preprocessing/FeatureSelection/Causal/MarkovBlanketSelector.cs
Comment thread src/Preprocessing/FeatureSelection/Clustering/HierarchicalFS.cs
Comment thread src/Preprocessing/FeatureSelection/Clustering/HierarchicalFS.cs
Add extensive feature selection methods across multiple categories:

**New categories added:**
- Boundary: MarginSelector, DecisionBoundarySelector
- Complexity: LempelZivSelector, HurstExponentSelector, FractalDimensionSelector, LyapunovSelector
- Distribution: GaussianitySelector, UniformitySelector, ModalitySelector
- Entropy: DifferentialEntropySelector, PermutationEntropySelector, ApproximateEntropySelector, SampleEntropySelector
- Gain: InformationGainRatioSelector, GiniImpuritySelector
- Moment: BimodalitySelector, SkewnessSelector, KurtosisSelector, CoefOfVariationSelector
- Nonlinear: MutualInformationSelector, MaximalInformationCoefficientSelector, DistanceCorrelationSelector, HilbertSchmidtSelector
- Quantile: QuantileRatioSelector, QuantileDispersionSelector
- Range: IQRSelector, MADSelector, PercentileRangeSelector
- Sparsity: L1RegularizationSelector, ElasticNetSelector, GroupLassoSelector

**Key improvements:**
- All selectors follow consistent TransformerBase pattern
- Proper 'new' keyword for FitTransform on unsupervised selectors
- Fixed net471 compatibility (FirstOrDefault usage)
- Comprehensive documentation with "For Beginners" explanations

Total: 419 feature selection methods covering statistical, information-theoretic,
complexity, entropy, distribution, and regularization-based approaches.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Statistical methods:
- TwoWayANOVASelector - Two-factor ANOVA for factorial designs
- MANOVASelector - Multivariate ANOVA with Wilks' Lambda
- ANCOVASelector - ANOVA with covariate adjustment
- HotellingT2Selector - Multivariate t-test generalization

Causal methods:
- MMPCSelector - Max-Min Parents and Children for Markov blanket

Neural methods:
- GradCAMSelector - Gradient-weighted class activation mapping
- ScoreCAMSelector - Gradient-free activation mapping
- AttentionFlowSelector - Attention flow propagation analysis

Privacy methods:
- DifferentialPrivacySelector - Privacy-preserving with Laplace noise
- FederatedFSSelector - Federated learning simulation

Fairness methods:
- FairFeatureSelector - Balance predictive power with fairness

Multi-Armed Bandit methods:
- UCB1Selector - Upper Confidence Bound exploration/exploitation
- ThompsonSamplingSelector - Bayesian bandit with Beta posteriors

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@src/Preprocessing/FeatureSelection/Bandit/ThompsonSamplingSelector.cs`:
- Around line 214-219: The SampleStandardNormal(Random rand) method can produce
Math.Log(0) when rand.NextDouble() returns 0; update the function to guard u1
against zero by replacing the raw u1 with a clipped value (e.g., double eps =
1e-12 or double.Epsilon) or loop until u1 > 0 before computing Math.Log(u1),
keeping the rest of the Box–Muller implementation unchanged; reference the
SampleStandardNormal method and the local variables u1/u2/random to locate where
to apply the clamp or retry.
- Around line 227-241: TransformCore currently assumes input has columns for
every _selectedIndices entry and will throw IndexOutOfRangeException if not; at
the start of TransformCore (before using data[i, _selectedIndices[j]]), validate
that every index in _selectedIndices is within data.Columns (e.g. check
data.Columns > _selectedIndices.Max() or loop check for any idx >= data.Columns)
and throw a clear ArgumentException/InvalidOperationException (mentioning the
mismatch and required column count) if the check fails; keep the existing
behavior of throwing when _selectedIndices is null.

In `@src/Preprocessing/FeatureSelection/Bandit/UCB1Selector.cs`:
- Around line 137-139: The UCB1 selection loop currently starts at iter = p so
no UCB iterations run when _nIterations <= p; update the loop in UCB1Selector
(the "UCB1 selection loop") to run _nIterations iterations after the initial p
pulls—e.g. change the loop bound so it iterates from p to p + _nIterations (use
for (int iter = p; iter < p + _nIterations; iter++)) or alternatively validate
in Fit/constructor that _nIterations > p and throw/adjust accordingly; reference
variables p and _nIterations when making the change.
- Around line 225-236: GetFeatureNamesOut currently returns an empty array when
_selectedIndices is null and silently drops out-of-range indices from
inputFeatureNames, causing inconsistent behavior with
GetSupportMask()/TransformCore and potential mismatches; change
GetFeatureNamesOut (and use of _selectedIndices) to throw the same
NotFitted/InvalidOperation exception as GetSupportMask()/TransformCore when
_selectedIndices is null, and instead of filtering out-of-range indices
silently, validate that every index in _selectedIndices is <
inputFeatureNames.Length and throw a clear ArgumentException (or similar) if any
index is out of range so callers get a deterministic error rather than a
silently truncated list.

In `@src/Preprocessing/FeatureSelection/Causal/MMPCSelector.cs`:
- Around line 92-129: The code in MMPCSelector.cs incorrectly treats _alpha as a
raw association threshold (used in the bestMinAssoc < _alpha check) instead of a
significance level for conditional independence tests; either rename _alpha to
make intent explicit (e.g., minAssociationThreshold) and update
constructor/field docs and all references (including the forward-selection loop
and any external callers) or replace the simple magnitude comparison with a
proper statistical test that computes a p-value from ComputePartialAssociation
(or a new TestConditionalIndependence method) and compare p-value < _alpha;
update variable names, XML docs, and method signatures (e.g.,
ComputePartialAssociation -> returns test statistic or p-value) consistently to
reflect the chosen approach.
- Around line 270-281: GetFeatureNamesOut currently returns an empty array when
_selectedIndices is null and silently drops indices if inputFeatureNames is too
short; make it consistent with GetSupportMask by throwing an
InvalidOperationException when _selectedIndices is null, and instead of
filtering out out-of-range indices, validate that every index in
_selectedIndices is < inputFeatureNames.Length and throw a descriptive
ArgumentException (or IndexOutOfRangeException) if not; locate and update the
method GetFeatureNamesOut and reference the field _selectedIndices when adding
these checks so callers are notified of not-fitted state and mismatched
inputFeatureNames rather than getting a silently truncated result.
🧹 Nitpick comments (3)
src/Preprocessing/FeatureSelection/Bandit/ThompsonSamplingSelector.cs (1)

260-271: Inconsistent behavior when not fitted.

GetSupportMask() throws InvalidOperationException when not fitted, but GetFeatureNamesOut() returns an empty array. Consider making the behavior consistent for better API predictability.

♻️ Proposed fix for consistent behavior
 public override string[] GetFeatureNamesOut(string[]? inputFeatureNames = null)
 {
-    if (_selectedIndices is null) return [];
+    if (_selectedIndices is null)
+        throw new InvalidOperationException("ThompsonSamplingSelector has not been fitted.");

     if (inputFeatureNames is null)
         return _selectedIndices.Select(i => $"Feature{i}").ToArray();

     return _selectedIndices
-        .Where(i => i < inputFeatureNames.Length)
         .Select(i => inputFeatureNames[i])
         .ToArray();
 }
src/Preprocessing/FeatureSelection/Bandit/UCB1Selector.cs (1)

38-41: Properties expose mutable internal arrays, allowing external mutation.

These properties return direct references to internal arrays. External code could modify them, corrupting the selector's state (e.g., selector.SelectedIndices[0] = -1).

♻️ Return defensive copies or read-only wrappers
-    public double[]? UCBScores => _ucbScores;
-    public double[]? EstimatedRewards => _estimatedRewards;
-    public int[]? PullCounts => _pullCounts;
-    public int[]? SelectedIndices => _selectedIndices;
+    public IReadOnlyList<double>? UCBScores => _ucbScores;
+    public IReadOnlyList<double>? EstimatedRewards => _estimatedRewards;
+    public IReadOnlyList<int>? PullCounts => _pullCounts;
+    public IReadOnlyList<int>? SelectedIndices => _selectedIndices;
src/Preprocessing/FeatureSelection/Causal/MMPCSelector.cs (1)

131-163: Backward phase uses pairwise conditioning instead of full conditioning set.

The backward phase tests conditional independence by iterating through each other candidate individually (Lines 144-155) rather than conditioning on all other candidates simultaneously. Standard MMPC would compute: "Is feature j independent of Y given all other candidates?"

Current approach may retain features that would be pruned under full multivariate conditioning. If this is intentional for computational efficiency, consider documenting this simplification in the class remarks.

Comment thread src/Preprocessing/FeatureSelection/Bandit/UCB1Selector.cs Outdated
Comment thread src/Preprocessing/FeatureSelection/Bandit/UCB1Selector.cs
Comment thread src/Preprocessing/FeatureSelection/Causal/MMPCSelector.cs
Comment thread src/Preprocessing/FeatureSelection/Causal/MMPCSelector.cs
@ooples ooples changed the title feat: add comprehensive feature selection module with 229 implementations feat: add comprehensive feature selection module with 432 implementations Jan 31, 2026
- Fix binary class split to use actual labels instead of fixed 0.5 threshold (SAM, SignificanceAnalysis)
- Add guards against division by zero and NaN (s0 fudge factor, eigenvalues, variance calculations)
- Add proper Beta function normalization for p-value calculations (VolcanoPlotSelector)
- Fix penalty scaling consistency (ElasticNetFS)
- Add validation for constructor parameters (nBins, maxIterations, priorInclusionProbability, etc.)
- Add bounds validation for feature group indices and constraint conflicts
- Fix MCP and SCAD proximal operators with correct threshold boundaries
- Replace sequential univariate regression with proper multivariate OLS (PCAlgorithmSelector)
- Fix Fisher z-transformation numerical instability
- Normalize conditional MI calculation (MarkovBlanketSelector)
- Fix UCB1 selection loop to run additional iterations after initial pulls
- Add column/shape validation in Transform methods
- Make GetFeatureNamesOut consistent with GetSupportMask (throw when not fitted)
- Fix Box-Muller transform to avoid log(0) (ThompsonSamplingSelector)
- Rename alpha to minAssociationThreshold for semantic clarity (MMPCSelector)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🤖 Fix all issues with AI agents
In `@src/Preprocessing/FeatureSelection/Bayesian/ARDSelector.cs`:
- Around line 41-56: The ARDSelector constructor currently only validates
nFeaturesToSelect; add argument validation for alphaThreshold, maxIterations,
and tolerance in the ARDSelector(...) constructor: require alphaThreshold > 0
(throw ArgumentOutOfRangeException(nameof(alphaThreshold))), require
maxIterations >= 1 (throw ArgumentOutOfRangeException(nameof(maxIterations))),
and require tolerance > 0 (and optionally < 1, throw
ArgumentOutOfRangeException(nameof(tolerance))). Assign to the existing fields
(_alphaThreshold, _maxIterations, _tolerance) only after these checks to prevent
invalid configuration.

In `@src/Preprocessing/FeatureSelection/Bayesian/BayesianFeatureSelector.cs`:
- Around line 39-54: The constructor stores a threshold but it is never
used—update the class so the supplied threshold is validated (ensure 0 <
threshold < 1 and throw ArgumentException otherwise) and applied during
selection: in the feature-selection method (locate methods like Select,
SelectFeatures, ScoreAndSelect or where _nFeaturesToSelect and _threshold are
used around lines ~130-135) first filter candidate features by their
posterior/probability score >= _threshold, then from those passing the threshold
select up to _nFeaturesToSelect (fall back to selecting top-N only from
threshold-passing features); ensure the selection logic respects both the
threshold and the max-count (_nFeaturesToSelect).

In `@src/Preprocessing/FeatureSelection/Bayesian/SpikeAndSlabSelector.cs`:
- Around line 98-128: The Gibbs loop in SpikeAndSlabSelector.cs can produce
divide-by-zero/NaN when n<2 or xjNorm≈0; modify the sampling in the for-loop
over j to (1) ensure residualVariance uses a safeDenominator = Math.Max(1, n -
1) or early-return when n<2, (2) clamp xjNorm with a small epsilon (e.g. xjNorm
= Math.Max(xjNorm, 1e-12)) before computing betaHat and any divisions, (3)
protect logBFSlab/logBFSpike computations by adding epsilons inside denominators
and Math.Log arguments, (4) after computing logBF and posteriorOdds, check for
double.IsFinite and cap exponent inputs (e.g. Math.Min/Max) and if values are
not finite set posterior inclusion gamma[j] to 0 (or prior) to avoid
contaminating downstream probabilities. Ensure you reference and update
variables residualVariance, xjNorm, betaHat, logBFSlab, logBFSpike,
posteriorOdds and gamma[j].

In `@src/Preprocessing/FeatureSelection/Bioinformatics/SAM.cs`:
- Around line 44-65: The constructor for SAM should validate s0Percentile to
ensure it's within [0, 1] to prevent downstream s0Index calculations from going
out of range; add a check in the SAM(...) constructor that throws an
ArgumentException (or ArgumentOutOfRangeException) if s0Percentile is < 0 or >
1, and keep assigning _s0Percentile only after this validation so any code that
computes s0Index or uses _s0Percentile (referenced by _s0Percentile and the
SAM(...) constructor) will not produce an IndexOutOfRangeException.

In `@src/Preprocessing/FeatureSelection/Bioinformatics/VolcanoPlotSelector.cs`:
- Around line 41-54: The constructor VolcanoPlotSelector currently doesn't
validate minFoldChange or pValueThreshold; add input validation in the
VolcanoPlotSelector(...) constructor to ensure minFoldChange > 0 and 0 <
pValueThreshold <= 1 (throw ArgumentException/ArgumentOutOfRangeException with
clear parameter name if violated) before assigning to _minFoldChange and
_pValueThreshold so downstream log-threshold and filtering calculations cannot
produce NaN/∞ or meaningless results.
- Around line 71-85: The current split into class0/class1 uses a fixed 0.5
threshold on NumOps.ToDouble(target[i]) which fails for label sets like {1,2} or
{-1,1}; update the logic in VolcanoPlotSelector (the loop that populates class0
and class1 using n and target) to identify the two distinct label values present
(e.g., find the two unique values in target), ensure there are exactly two
classes, then assign indices to class0/class1 by equality comparison against
those two labels (rather than using 0.5); keep the existing check that each
class has at least 2 samples and throw the same ArgumentException if not.

In `@src/Preprocessing/FeatureSelection/Clustering/ClusteringBasedFS.cs`:
- Around line 226-240: ClusteringBasedFS.TransformCore lacks input-shape
validation and will IndexOutOfRange when passed a matrix with fewer columns than
the fitted features; modify TransformCore (in class ClusteringBasedFS, method
TransformCore) to verify that data.Columns is greater than the highest selected
index (e.g. compute maxIndex = _selectedIndices.Max()) or that data.Columns >=
_selectedIndices.Length/expected fitted columns and if the check fails throw a
clear ArgumentException (or InvalidOperationException consistent with
HierarchicalFS) with a message like "Input data has fewer columns than data used
to fit ClusteringBasedFS."; perform this check before the row/column copy loop.

In `@src/Preprocessing/FeatureSelection/Clustering/HierarchicalFS.cs`:
- Around line 260-271: GetFeatureNamesOut currently returns an empty array when
_selectedIndices is null and silently skips out-of-range indices, which is
inconsistent with ClusteringBasedFS.GetFeatureNamesOut and GetSupportMask;
change GetFeatureNamesOut to mirror ClusteringBasedFS behavior by throwing
InvalidOperationException when _selectedIndices is null (not fitted) and
validate that inputFeatureNames (when provided) has length >=
max(_selectedIndices)+1, throwing ArgumentException/InvalidOperationException
for out-of-range inputs rather than filtering them; update references in this
class (GetFeatureNamesOut, _selectedIndices) so behavior matches GetSupportMask
and ClusteringBasedFS.GetFeatureNamesOut.

In `@src/Preprocessing/FeatureSelection/Constraint/ConstraintBasedSelector.cs`:
- Around line 83-115: The current scoring converts inputs to double arrays and
uses System.Math, violating the INumericOperations<T> requirement; change
ConstraintBasedSelector so you do not cast all data to double early: keep values
as the generic numeric type (use the existing NumOps/INumericOperations<T>
helpers to accumulate sums, compute means, compute sxx/syy/sxy with
NumOps.Add/Multiply/Subtract/Divide/etc.), compute mean and variance/covariance
using NumOps, and only convert the final correlation value to double when
assigning to _featureScores[j]; update the code paths that create X and y
(replace double[,] X and double[] y) and the loops that compute xMean, yMean,
sxy, sxx, syy to use NumOps methods instead of Math so numeric behavior remains
type‑safe for non‑double T.
- Around line 117-132: Validate that all indices in _mandatoryFeatures and
_forbiddenFeatures are within [0, p-1] after p is computed instead of silently
ignoring them: check each element of _mandatoryFeatures and _forbiddenFeatures
(used when building selected and forbidden) and if any index is <0 or >=p throw
an ArgumentOutOfRangeException (or a clear custom exception) mentioning the
offending index and the collection name; remove the silent bounds checks in the
foreach that builds selected and rely on the prior validation so candidates and
the OrderByDescending on _featureScores operate on guaranteed-valid indices.
🧹 Nitpick comments (8)
src/Preprocessing/FeatureSelection/Causal/MarkovBlanketSelector.cs (2)

37-52: Previous issues addressed; constructor validation looks good.

The constructor now properly validates both maxFeatures and nBins. However, threshold is not validated and could accept negative values, which would make the conditional MI check at line 92 (conditionalMI > _threshold) behave unexpectedly (always pass for non-negative MI).

Consider adding validation:

🛡️ Optional fix
     if (nBins < 1)
         throw new ArgumentException("nBins must be at least 1.", nameof(nBins));
+    if (threshold < 0)
+        throw new ArgumentException("Threshold must be non-negative.", nameof(threshold));

298-309: Inconsistent not-fitted handling and silent index filtering.

GetSupportMask() throws InvalidOperationException when not fitted, but GetFeatureNamesOut() returns an empty array (line 300). Additionally, the .Where(i => i < inputFeatureNames.Length) filter silently drops out-of-range indices.

For consistency with other selectors in this PR (MMPCSelector, PCAlgorithmSelector) that were updated to throw, consider aligning this method:

🔧 Proposed fix
 public override string[] GetFeatureNamesOut(string[]? inputFeatureNames = null)
 {
-    if (_selectedIndices is null) return [];
+    if (_selectedIndices is null)
+        throw new InvalidOperationException("MarkovBlanketSelector has not been fitted.");

     if (inputFeatureNames is null)
         return _selectedIndices.Select(i => $"Feature{i}").ToArray();

+    if (inputFeatureNames.Length < _nInputFeatures)
+        throw new ArgumentException(
+            $"Expected at least {_nInputFeatures} feature names, got {inputFeatureNames.Length}.",
+            nameof(inputFeatureNames));
+
-    return _selectedIndices
-        .Where(i => i < inputFeatureNames.Length)
-        .Select(i => inputFeatureNames[i])
-        .ToArray();
+    return _selectedIndices.Select(i => inputFeatureNames[i]).ToArray();
 }
src/Preprocessing/FeatureSelection/Causal/PCAlgorithmSelector.cs (1)

40-55: Consider validating maxConditioningSetSize.

The constructor validates maxFeatures and alpha but not maxConditioningSetSize. A negative value would cause the PC algorithm loop (line 91) to execute only once with condSize = 0, which may not be the intended behavior.

🛡️ Optional fix
     if (alpha <= 0 || alpha >= 1)
         throw new ArgumentException("Alpha must be between 0 and 1.", nameof(alpha));
+    if (maxConditioningSetSize < 0)
+        throw new ArgumentException("Max conditioning set size must be non-negative.", nameof(maxConditioningSetSize));
src/Preprocessing/FeatureSelection/CostSensitive/CostSensitiveFS.cs (2)

216-222: Inconsistent error handling: consider throwing when not fitted.

GetSupportMask() throws InvalidOperationException when the selector is not fitted, but GetTotalCost() silently returns 0. This inconsistency could mask usage errors where a caller expects a meaningful cost value.

♻️ Suggested fix for consistency
 public double GetTotalCost()
 {
-    if (_selectedIndices is null || _featureCosts is null)
-        return 0;
+    if (_selectedIndices is null)
+        throw new InvalidOperationException("CostSensitiveFS has not been fitted.");
+    if (_featureCosts is null)
+        return 0; // Uniform costs were used internally

     return _selectedIndices.Sum(j => _featureCosts[j]);
 }

224-235: Silent truncation may hide input mismatches.

Two concerns:

  1. Inconsistent unfitted behavior: Returns [] when not fitted (line 226), whereas GetSupportMask() throws. Consider throwing for consistency.

  2. Silent index filtering (lines 231-234): If inputFeatureNames has fewer elements than the maximum selected index, those indices are silently dropped. This could cause the returned array to have fewer elements than _selectedIndices.Length, creating a mismatch between feature names and transformed columns.

♻️ Suggested fix
 public override string[] GetFeatureNamesOut(string[]? inputFeatureNames = null)
 {
-    if (_selectedIndices is null) return [];
+    if (_selectedIndices is null)
+        throw new InvalidOperationException("CostSensitiveFS has not been fitted.");

     if (inputFeatureNames is null)
         return _selectedIndices.Select(i => $"Feature{i}").ToArray();

+    if (inputFeatureNames.Length < _nInputFeatures)
+        throw new ArgumentException(
+            $"Expected at least {_nInputFeatures} feature names, but got {inputFeatureNames.Length}.",
+            nameof(inputFeatureNames));
+
     return _selectedIndices
-        .Where(i => i < inputFeatureNames.Length)
         .Select(i => inputFeatureNames[i])
         .ToArray();
 }
src/Preprocessing/FeatureSelection/Clustering/HierarchicalFS.cs (1)

36-47: Consider validating the linkage parameter.

The linkage parameter accepts any string and silently falls back to "average" linkage in the switch default case (lines 202-203). Consider validating the input and throwing for unsupported values to avoid silent behavior changes.

♻️ Proposed fix
     public HierarchicalFS(
         int nFeaturesToSelect = 10,
         string linkage = "average",
         int[]? columnIndices = null)
         : base(columnIndices)
     {
         if (nFeaturesToSelect < 1)
             throw new ArgumentException("Number of features must be at least 1.", nameof(nFeaturesToSelect));
 
+        var validLinkages = new[] { "single", "complete", "average" };
+        if (!validLinkages.Contains(linkage.ToLowerInvariant()))
+            throw new ArgumentException($"Linkage must be one of: {string.Join(", ", validLinkages)}.", nameof(linkage));
+
         _nFeaturesToSelect = nFeaturesToSelect;
         _linkage = linkage.ToLowerInvariant();
     }
src/Preprocessing/FeatureSelection/Bandit/ThompsonSamplingSelector.cs (2)

181-186: Latent issue: potential zero result when shape < 1.

The shape < 1 branch can return 0 if rand.NextDouble() returns 0, since Math.Pow(0, 1.0/shape) = 0. While this branch is never executed in the current implementation (alpha and beta are always ≥ 1), it could cause division-by-zero in SampleBeta if the method is reused with smaller shape values in the future.

🛡️ Proposed guard against zero
 if (shape < 1)
 {
     // Use Gamma(shape+1) / U^(1/shape)
-    double u = rand.NextDouble();
+    double u;
+    do
+    {
+        u = rand.NextDouble();
+    } while (u <= double.Epsilon);
     return SampleGamma(shape + 1, rand) * Math.Pow(u, 1.0 / shape);
 }

270-281: Inconsistent error handling for unfitted state and mismatched input.

Two concerns:

  1. Unfitted state: Line 272 returns an empty array when not fitted, but GetSupportMask() and TransformCore() throw InvalidOperationException. This inconsistency can confuse users.

  2. Silent filtering: Lines 277-280 silently drop indices when inputFeatureNames is shorter than _nInputFeatures. This differs from TransformCore which throws on dimension mismatch, potentially masking caller errors.

♻️ Proposed fix for consistent behavior
 public override string[] GetFeatureNamesOut(string[]? inputFeatureNames = null)
 {
-    if (_selectedIndices is null) return [];
+    if (_selectedIndices is null)
+        throw new InvalidOperationException("ThompsonSamplingSelector has not been fitted.");
 
     if (inputFeatureNames is null)
         return _selectedIndices.Select(i => $"Feature{i}").ToArray();
 
-    return _selectedIndices
-        .Where(i => i < inputFeatureNames.Length)
-        .Select(i => inputFeatureNames[i])
-        .ToArray();
+    if (inputFeatureNames.Length < _nInputFeatures)
+        throw new ArgumentException(
+            $"Input feature names has {inputFeatureNames.Length} elements but selector was fitted on {_nInputFeatures} features.",
+            nameof(inputFeatureNames));
+
+    return _selectedIndices.Select(i => inputFeatureNames[i]).ToArray();
 }

Comment thread src/Preprocessing/FeatureSelection/Bayesian/ARDSelector.cs
Comment thread src/Preprocessing/FeatureSelection/Bayesian/SpikeAndSlabSelector.cs
Comment thread src/Preprocessing/FeatureSelection/Bioinformatics/SAM.cs
Comment thread src/Preprocessing/FeatureSelection/Clustering/ClusteringBasedFS.cs
Comment thread src/Preprocessing/FeatureSelection/Clustering/HierarchicalFS.cs
- ARDSelector: validate alphaThreshold, maxIterations, tolerance
- BayesianFeatureSelector: apply threshold in selection, validate threshold range
- SpikeAndSlabSelector: guard against division-by-zero and NaNs in Bayes factor
- SAM: validate s0Percentile is in [0, 1]
- VolcanoPlotSelector: validate minFoldChange/pValueThreshold, use actual labels
- ClusteringBasedFS: validate input shape in TransformCore
- HierarchicalFS: consistent error handling in GetFeatureNamesOut
- ConstraintBasedSelector: validate mandatory/forbidden feature indices

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Fix all issues with AI agents
In `@src/Preprocessing/FeatureSelection/Bayesian/ARDSelector.cs`:
- Around line 70-77: In Fit(Matrix<T> data, Vector<T> target) add validation to
ensure data.Rows > 0 and data.Columns > 0 and that target.Length equals
data.Rows (retain existing check), then set _nInputFeatures = data.Columns and
enforce nFeaturesToSelect <= data.Columns by throwing an ArgumentException when
nFeaturesToSelect is greater; this prevents operating on empty inputs or
requesting more features than available and keeps the existing variable usage
(Fit, _nInputFeatures, nFeaturesToSelect, data.Rows, data.Columns,
target.Length).
- Around line 198-212: TransformCore currently indexes data using
_selectedIndices without checking that the input has enough columns, causing
IndexOutOfRangeException for mismatched input; update TransformCore (and related
callers if necessary) to validate the input's column count before indexing by
ensuring every index in _selectedIndices is < data.Columns (or that data.Columns
matches the expected training column count if you store it), and throw a clear
InvalidOperationException describing the mismatch if validation fails so callers
know why transformation cannot proceed.
- Around line 231-241: GetFeatureNamesOut currently returns an empty array when
the selector isn’t fitted and silently drops out-of-range names; change it to
throw instead: if _selectedIndices is null throw an InvalidOperationException
(mentioning that the selector must be fitted) and if inputFeatureNames is
provided but does not contain entries for all indices in _selectedIndices (e.g.,
any i >= inputFeatureNames.Length) throw an ArgumentException (or
ArgumentOutOfRangeException) describing the mismatch; keep the existing behavior
of mapping indices to names when valid (using _selectedIndices and
inputFeatureNames) but do not silently return [] or drop names.

In `@src/Preprocessing/FeatureSelection/Bayesian/SpikeAndSlabSelector.cs`:
- Around line 168-180: TransformCore currently assumes incoming data has the
same number of columns as whatever was used when fitting, which can cause
out-of-range indexing into _selectedIndices; before copying values, validate the
input shape by ensuring data.Columns (or equivalent property) is large enough
for all indices in _selectedIndices (e.g., check that _selectedIndices.Max() <
data.Columns) and throw a clear InvalidOperationException like "Input has X
columns but selector expects at least Y" if the check fails; add this guard at
the start of TransformCore in SpikeAndSlabSelector (before the
numRows/numCols/result allocation) so you fail fast with a friendly message
rather than causing an index exception.

In `@src/Preprocessing/FeatureSelection/Bioinformatics/VolcanoPlotSelector.cs`:
- Around line 127-131: The current log2 fold change calculation can yield NaN
when mean0 or mean1 are non‑positive; update the computation in
VolcanoPlotSelector so you first guard/normalize the means: if mean0 or mean1
are <= 0, compute a signed fold change by taking the ratio of their absolute
values plus eps and multiplying the log2 by Math.Sign(mean1 - mean0); otherwise
compute the usual log2((mean1+eps)/(mean0+eps)). Assign the result to
_log2FoldChanges[j] and ensure eps is applied to the absolute values to avoid
division by zero.

In `@src/Preprocessing/FeatureSelection/Clustering/ClusteringBasedFS.cs`:
- Around line 53-58: FitCore currently assumes data.Rows > 0 (n) and later does
mean /= n, causing divide-by-zero; add an early validation at the start of the
FitCore(Matrix<T> data) method to check n (or data.Rows) and throw a clear
exception (e.g., ArgumentException or InvalidOperationException) if n == 0, with
a message like "input data must contain at least one row"; ensure this check
occurs before using n or computing mean so _nInputFeatures and subsequent code
only run on non-empty input.

In `@src/Preprocessing/FeatureSelection/Clustering/HierarchicalFS.cs`:
- Around line 36-46: Validate the constructor parameter `linkage` in
HierarchicalFS instead of letting unknown values fall through: in the
HierarchicalFS constructor check for null/whitespace and normalize the string,
then verify it is one of the supported linkage values that
AgglomerativeClustering accepts (e.g., "average", "complete", "single", "ward" —
match actual supported set in AgglomerativeClustering). If the value is invalid,
throw an ArgumentException describing allowed values and the parameter name;
otherwise assign the normalized value to _linkage. Ensure this validation uses
the same identifiers (_linkage, HierarchicalFS, AgglomerativeClustering) so
callers get immediate feedback on invalid configuration.
🧹 Nitpick comments (6)
src/Preprocessing/FeatureSelection/Bioinformatics/SAM.cs (3)

98-105: Floating-point equality comparison may fail for certain numeric types.

Using == to compare doubles (Lines 101, 103) can be unreliable due to floating-point precision issues, especially when T is float and values undergo conversion. Consider using a tolerance-based comparison.

🔧 Proposed fix
         for (int i = 0; i < n; i++)
         {
             double y = NumOps.ToDouble(target[i]);
-            if (y == label0)
+            if (Math.Abs(y - label0) < 1e-12)
                 class0.Add(i);
-            else if (y == label1)
+            else if (Math.Abs(y - label1) < 1e-12)
                 class1.Add(i);
         }

190-221: Q-value assignment lacks monotonicity correction.

The _qValues array is assigned directly from _fdr (Line 220), but proper q-values (Storey's method) require a cumulative minimum correction to ensure monotonicity when sorted by p-value/d-statistic. Currently QValues is just a copy of local FDR estimates.

This works for selection purposes but may confuse users expecting standard q-values.


301-304: Silent filtering may return fewer feature names than selected features.

The Where clause at Line 302 silently skips indices exceeding inputFeatureNames.Length. If a user provides an incomplete inputFeatureNames array, the returned array will have fewer entries than SelectedIndices, potentially causing downstream index mismatches.

Consider either throwing when inputFeatureNames.Length < _nInputFeatures or documenting this behavior explicitly.

src/Preprocessing/FeatureSelection/Bioinformatics/VolcanoPlotSelector.cs (1)

87-94: Floating-point equality comparison on class labels.

Same concern as SAM.cs: using == to compare doubles (Lines 90, 92) may be unreliable. Consider using tolerance-based comparison for consistency.

🔧 Proposed fix
         for (int i = 0; i < n; i++)
         {
             double y = NumOps.ToDouble(target[i]);
-            if (y == label0)
+            if (Math.Abs(y - label0) < 1e-12)
                 class0.Add(i);
             else
                 class1.Add(i);
         }
src/Preprocessing/FeatureSelection/Bayesian/BayesianFeatureSelector.cs (2)

80-81: Remove unused variables positiveCount and negativeCount.

These variables are computed but never used in the method.

🧹 Proposed fix to remove dead code
-        double positiveCount = targetArray.Count(y => y > 0.5);
-        double negativeCount = n - positiveCount;
-
         for (int j = 0; j < p; j++)

182-193: Silent truncation may hide input/selection mismatches.

Lines 189-192 silently filter out selected indices that exceed inputFeatureNames.Length. If _selectedIndices contains index 5 but inputFeatureNames has only 4 elements, that feature name is silently dropped rather than raising an error or using a fallback name.

Consider throwing an exception or using a fallback pattern consistent with line 187.

♻️ Proposed fix to handle mismatches explicitly
         return _selectedIndices
-            .Where(i => i < inputFeatureNames.Length)
-            .Select(i => inputFeatureNames[i])
+            .Select(i => i < inputFeatureNames.Length ? inputFeatureNames[i] : $"Feature{i}")
             .ToArray();

Comment thread src/Preprocessing/FeatureSelection/Bayesian/ARDSelector.cs
Comment thread src/Preprocessing/FeatureSelection/Bayesian/ARDSelector.cs
Comment thread src/Preprocessing/FeatureSelection/Bayesian/ARDSelector.cs Outdated
Comment thread src/Preprocessing/FeatureSelection/Bayesian/SpikeAndSlabSelector.cs
Comment thread src/Preprocessing/FeatureSelection/Bioinformatics/VolcanoPlotSelector.cs Outdated
Comment thread src/Preprocessing/FeatureSelection/Clustering/ClusteringBasedFS.cs
Comment thread src/Preprocessing/FeatureSelection/Clustering/HierarchicalFS.cs Outdated
- ARDSelector: add input validation and enforce nFeaturesToSelect <= columns
- ARDSelector: validate input column count in TransformCore
- ARDSelector: throw instead of returning empty array in GetFeatureNamesOut
- SpikeAndSlabSelector: add input-shape validation in TransformCore
- VolcanoPlotSelector: handle negative/zero means in log2 fold change
- ClusteringBasedFS: add empty input validation
- HierarchicalFS: validate linkage values instead of silently defaulting

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 2, 2026 00:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Introduces a large feature-selection expansion and migrates the library from legacy “normalizer/feature selector” primitives to a pipeline-based preprocessing approach across model building, evaluation, prediction, and uncertainty estimation.

Changes:

  • Added new feature selection transformers (cost-sensitive, constraint-based, compression, complexity, clustering, classification, bioinformatics, Bayesian, bandit, association).
  • Replaced NormalizationInfo/INormalizer usage with PreprocessingPipeline/PreprocessingInfo in core model result/prediction paths and evaluation.
  • Removed legacy normalizer/feature-selector infrastructure (interfaces, factories, base classes, implementations, and options).

Reviewed changes

Copilot reviewed 76 out of 485 changed files in this pull request and generated 17 comments.

Show a summary per file
File Description
src/Preprocessing/FeatureSelection/CostSensitive/BudgetConstrainedFS.cs Adds budget-constrained supervised feature selection.
src/Preprocessing/FeatureSelection/Constraint/ConstraintBasedSelector.cs Adds constraint-aware supervised selector (mandatory/forbidden/groups).
src/Preprocessing/FeatureSelection/Compression/NormalizedCompressionSelector.cs Adds MI-based “compression” relevance selector (named as NCD).
src/Preprocessing/FeatureSelection/Compression/CompressionRatioSelector.cs Adds entropy-based compressibility selector.
src/Preprocessing/FeatureSelection/Complexity/LyapunovSelector.cs Adds Lyapunov exponent-based complexity selector.
src/Preprocessing/FeatureSelection/Complexity/LempelZivSelector.cs Adds Lempel–Ziv complexity selector.
src/Preprocessing/FeatureSelection/Complexity/HurstExponentSelector.cs Adds Hurst exponent selector.
src/Preprocessing/FeatureSelection/Complexity/FractalDimensionSelector.cs Adds Higuchi fractal dimension selector.
src/Preprocessing/FeatureSelection/Clustering/SilhouetteBasedSelector.cs Adds silhouette-score-based selector (uses provided labels).
src/Preprocessing/FeatureSelection/Clustering/ClusterSeparabilitySelector.cs Adds k-means separability selector.
src/Preprocessing/FeatureSelection/Classification/NaiveBayesSelector.cs Adds Naive Bayes KL-based selector.
src/Preprocessing/FeatureSelection/Classification/LDAProjectionSelector.cs Adds LDA variance-ratio selector.
src/Preprocessing/FeatureSelection/Classification/GiniImpuritySelector.cs Adds split-based Gini reduction selector.
src/Preprocessing/FeatureSelection/Boundary/MarginSelector.cs Adds margin-based class separation selector.
src/Preprocessing/FeatureSelection/Boundary/DecisionBoundarySelector.cs Adds threshold boundary clarity selector.
src/Preprocessing/FeatureSelection/Bioinformatics/FoldChangeSelector.cs Adds fold-change selector for 2-class differential analysis.
src/Preprocessing/FeatureSelection/Bayesian/SpikeAndSlabSelector.cs Adds spike-and-slab inclusion probability selector.
src/Preprocessing/FeatureSelection/Bayesian/HorseshoeSelector.cs Adds horseshoe-prior selector.
src/Preprocessing/FeatureSelection/Bayesian/BayesianNetworkSelector.cs Adds MI/redundancy “network” selector.
src/Preprocessing/FeatureSelection/Bayesian/BayesianModelAveraging.cs Adds BMA-based inclusion selector.
src/Preprocessing/FeatureSelection/Bayesian/BayesianFeatureSelector.cs Adds Bayes-factor-like posterior probability selector.
src/Preprocessing/FeatureSelection/Bayesian/ARDSelector.cs Adds ARD selector with threshold fallback.
src/Preprocessing/FeatureSelection/Bandit/UCB1Selector.cs Adds UCB1-inspired exploration/exploitation selector.
src/Preprocessing/FeatureSelection/Association/FrequentPatternSelector.cs Adds frequent-pattern scoring selector.
src/Preprocessing/FeatureSelection/Association/AprioriBasedSelector.cs Adds binning + association-score selector.
src/NeuralNetworks/QuantumNeuralNetwork.cs Switches from normalizer to preprocessing pipeline for state prep.
src/Models/Results/AiModelResult.cs Replaces normalization-based predict flow with preprocessing pipeline.
src/Models/Results/AiModelResult.Uncertainty.cs Updates UQ predict flow to use preprocessing; changes variance denorm.
src/Models/Options/AiModelResultOptions.cs Removes legacy NormalizationInfo option in favor of preprocessing.
src/Models/Inputs/ModelEvaluationInput.cs Updates evaluation input to carry PreprocessingInfo instead of norm info.
src/Evaluation/DefaultModelEvaluator.cs Passes preprocessing info to model-stats computation.
src/AiModelBuilder.cs Removes legacy normalizer/feature selector/preprocessor hooks; uses pipelines.
src/Normalizers/ZScoreNormalizer.cs Removes legacy Z-score normalizer implementation.
src/Normalizers/NormalizerBase.cs Removes legacy normalizer base class.
src/Models/Options/DataProcessorOptions.cs Removes legacy data processor options.
src/Models/NormalizationInfo.cs Removes legacy normalization info container.
src/Interfaces/INormalizer.cs Removes legacy normalizer interface.
src/Interfaces/IFeatureSelector.cs Removes legacy feature selector interface.
src/Interfaces/IDataPreprocessor.cs Removes legacy data preprocessor interface.
src/FeatureSelectors/VarianceThresholdFeatureSelector.cs Removes legacy feature selector implementation.
src/FeatureSelectors/RecursiveFeatureElimination.cs Removes legacy RFE implementation.
src/FeatureSelectors/NoFeatureSelector.cs Removes legacy “no-op” feature selector implementation.
src/FeatureSelectors/FeatureSelectorBase.cs Removes legacy feature selector base class.
src/FeatureSelectors/CorrelationFeatureSelector.cs Removes legacy correlation feature selector.
src/Factories/NormalizerFactory.cs Removes legacy normalizer factory.
src/Interfaces/IAiModelBuilder.cs Removes legacy builder hooks for feature selector/normalizer/data preprocessor.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Preprocessing/FeatureSelection/Bandit/UCB1Selector.cs
Comment thread src/Models/Results/AiModelResult.Uncertainty.cs
Comment thread src/Models/Results/AiModelResult.cs
Comment thread src/Models/Results/AiModelResult.cs
@coderabbitai coderabbitai Bot added the roadmap Roadmap-tracked item label Feb 2, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/Preprocessing/FeatureSelection/Clustering/ClusteringBasedFS.cs`:
- Around line 53-62: FitCore currently validates row counts but misses the
zero-columns edge case; add a check at the start of FitCore that throws an
ArgumentException (or ArgumentOutOfRangeException) when data.Columns == 0 to
fail fast instead of producing empty _selectedIndices. Modify FitCore to
validate data.Columns, include a clear message and parameter name, and ensure
_nInputFeatures is only set after this check so the method never proceeds with
zero columns.
🧹 Nitpick comments (2)
src/Preprocessing/FeatureSelection/Bayesian/SpikeAndSlabSelector.cs (1)

204-215: Consider consistent error handling when not fitted.

GetSupportMask() throws InvalidOperationException when the selector isn't fitted, but GetFeatureNamesOut() silently returns an empty array. Additionally, line 212 silently filters indices that exceed inputFeatureNames.Length, which could mask mismatches between selected indices and provided names.

♻️ Proposed fix for consistency
     public override string[] GetFeatureNamesOut(string[]? inputFeatureNames = null)
     {
-        if (_selectedIndices is null) return [];
+        if (_selectedIndices is null)
+            throw new InvalidOperationException("SpikeAndSlabSelector has not been fitted.");

         if (inputFeatureNames is null)
             return _selectedIndices.Select(i => $"Feature{i}").ToArray();

+        if (inputFeatureNames.Length < _nInputFeatures)
+            throw new ArgumentException(
+                $"Expected at least {_nInputFeatures} feature names but got {inputFeatureNames.Length}.",
+                nameof(inputFeatureNames));
+
-        return _selectedIndices
-            .Where(i => i < inputFeatureNames.Length)
-            .Select(i => inputFeatureNames[i])
-            .ToArray();
+        return _selectedIndices.Select(i => inputFeatureNames[i]).ToArray();
     }
src/Preprocessing/FeatureSelection/Clustering/ClusteringBasedFS.cs (1)

33-35: Consider returning defensive copies to protect internal state.

These properties return the internal arrays directly, allowing callers to mutate them (e.g., selector.SelectedIndices![0] = -1). This could corrupt the fitted state.

♻️ Proposed fix using defensive copies
-    public int[]? ClusterAssignments => _clusterAssignments;
-    public double[]? FeatureScores => _featureScores;
-    public int[]? SelectedIndices => _selectedIndices;
+    public int[]? ClusterAssignments => _clusterAssignments?.ToArray();
+    public double[]? FeatureScores => _featureScores?.ToArray();
+    public int[]? SelectedIndices => _selectedIndices?.ToArray();

Alternatively, use IReadOnlyList<int>? as the return type if allocation overhead is a concern.

Comment thread src/Preprocessing/FeatureSelection/Clustering/ClusteringBasedFS.cs
franklinic and others added 2 commits February 2, 2026 14:18
- Update GlobalUsings.cs to use AiDotNet.Preprocessing.* namespaces
- Delete NormalizersBenchmarks.cs (uses deleted Normalizer classes)
- Delete Enhanced*Example.cs files (use deleted preprocessing API)
- Update Program.cs to remove deleted example references

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 2, 2026 20:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 77 out of 491 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@src/AiModelBuilder.cs`:
- Around line 1131-1135: The code is attaching
PreprocessingInfo<T,TInput,TOutput> using _preprocessingPipeline even in
inference-only builds where the pipeline may be unfitted; update the
AiModelBuilder code that builds the AiModelResultOptions<T,TInput,TOutput> so
that before creating new PreprocessingInfo(...) you verify the pipeline is
actually fitted (or contains fitted parameters); if the pipeline is not fitted
then either set PreprocessingInfo to null or throw a clear exception (fail-fast)
depending on the intended behavior for inference-only paths. Reference the
symbols _preprocessingPipeline, PreprocessingInfo<T,TInput,TOutput>, and
AiModelResultOptions<T,TInput,TOutput> when making the change.
- Around line 1309-1314: BuildStreamingSupervisedAsync currently returns
AiModelResultOptions with PreprocessingInfo set when _preprocessingPipeline
exists, but streaming training never applies that pipeline to batches, causing
train/infer mismatch; fix by either (A) applying the _preprocessingPipeline
inside BuildStreamingSupervisedAsync’s streaming loop (ensure each batch is
passed through the pipeline before tokenization/optimization) and keep
PreprocessingInfo, or (B) forbid preprocessing for streaming by setting
PreprocessingInfo = null (and/or throwing from BuildStreamingSupervisedAsync
when _preprocessingPipeline != null) so callers don’t get exposed to an
unapplied transform; update references to _preprocessingPipeline,
PreprocessingInfo, and AiModelResultOptions<T,TInput,TOutput> accordingly.

In `@src/Models/Results/AiModelResult.cs`:
- Around line 1379-1381: The code currently calls
PreprocessingInfo.InverseTransformPredictions whenever PreprocessingInfo is
non-null, which can error or distort outputs if only the feature pipeline is
fitted; change the guard to ensure the target pipeline is fitted before
inverse-transforming by checking PreprocessingInfo.IsTargetFitted (or equivalent
property) and only call InverseTransformPredictions when IsTargetFitted is true,
otherwise return normalizedPredictions unchanged; apply this same guard to the
other identical call sites (the other uses of InverseTransformPredictions noted
in the review) so all places use PreprocessingInfo != null &&
PreprocessingInfo.IsTargetFitted before invoking InverseTransformPredictions.

Comment thread src/AiModelBuilder.cs
Comment thread src/AiModelBuilder.cs
Comment thread src/Models/Results/AiModelResult.cs Outdated
- Support preprocessing in streaming training: fit on first batch, transform each input
- Support inference-only builds with pre-fitted pipelines
- Guard inverse-transform on IsTargetFitted to avoid errors when target not preprocessed

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@ooples
ooples merged commit 2cc6086 into master Feb 3, 2026
38 checks passed
@ooples
ooples deleted the feat/preprocessing-migration branch February 3, 2026 23:23

This branch was successfully deployed

1 active deployment
Preview — e3c5c302 Deployed Feb 3, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Feature work item roadmap Roadmap-tracked item

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Gap Analysis] Implement Advanced Feature Selection Methods

3 participants