///|
/// Deterministic feature selection diagnostics for numeric feature matrices.
pub struct FeatureScore {
index : Int
relevance : Double
redundancy : Double
stability : Double
selected : Bool
}
///|
pub struct FeatureSelection {
scores : Array[FeatureScore]
selected_indices : Array[Int]
threshold : Double
objective : Double
}
///|
pub fn feature_score(
index : Int,
relevance : Double,
redundancy : Double,
stability : Double,
) -> FeatureScore {
{ index, relevance, redundancy, stability, selected: false }
}
///|
pub fn feature_relevance(
feature : Array[Double],
target : Array[Double],
) -> Double {
if feature.length() != target.length() || feature.length() < 2 {
0.0
} else {
abs_double(pearson_correlation(feature, target))
}
}
///|
pub fn feature_redundancy(
feature : Array[Double],
others : Array[Array[Double]],
) -> Double {
if others.length() == 0 {
return 0.0
}
let mut maximum = 0.0
for other in others {
if other.length() == feature.length() {
let value = abs_double(pearson_correlation(feature, other))
if value > maximum {
maximum = value
}
}
}
maximum
}
///|
pub fn feature_stability(feature : Array[Double], folds : Int) -> Double {
let count = if folds < 2 { 2 } else { folds }
let chunks = sampling_kfold(feature, count, 0)
if chunks.length() == 0 {
0.0
} else {
let means = []
for chunk in chunks {
means.push(mean(chunk.holdout))
}
1.0 / (1.0 + sample_stddev(means))
}
}
///|
pub fn feature_selection_scores(
features : Array[Array[Double]],
target : Array[Double],
folds : Int,
) -> Array[FeatureScore] {
let result = []
for index = 0; index < features.length(); index = index + 1 {
let others = []
for other_index = 0
other_index < features.length()
other_index = other_index + 1 {
if other_index != index {
others.push(features[other_index])
}
}
result.push(
feature_score(
index,
feature_relevance(features[index], target),
feature_redundancy(features[index], others),
feature_stability(features[index], folds),
),
)
}
result
}
///|
pub fn feature_selection_rank(
scores : Array[FeatureScore],
) -> Array[FeatureScore] {
let result = scores.copy()
result.sort_by((left, right) => {
let left_score = left.relevance * left.stability - left.redundancy
let right_score = right.relevance * right.stability - right.redundancy
if left_score > right_score {
-1
} else if left_score < right_score {
1
} else {
0
}
})
result
}
///|
pub fn feature_selection_run(
features : Array[Array[Double]],
target : Array[Double],
threshold : Double,
folds : Int,
) -> FeatureSelection {
let scores = feature_selection_rank(
feature_selection_scores(features, target, folds),
)
let selected = []
let safe_threshold = if threshold < 0.0 { 0.0 } else { threshold }
let mut objective = 0.0
for index = 0; index < scores.length(); index = index + 1 {
let score = scores[index]
let value = score.relevance * score.stability - score.redundancy
let keep = value >= safe_threshold
if keep {
selected.push(score.index)
objective += value
}
scores[index] = { ..score, selected: keep }
}
{ scores, selected_indices: selected, threshold: safe_threshold, objective }
}
///|
pub fn feature_selection_project(
features : Array[Array[Double]],
selection : FeatureSelection,
) -> Array[Array[Double]] {
let result = []
let rows = if features.length() == 0 { 0 } else { features[0].length() }
for row_index = 0; row_index < rows; row_index = row_index + 1 {
let row = []
for index in selection.selected_indices {
if index < features.length() && row_index < features[index].length() {
row.push(features[index][row_index])
}
}
result.push(row)
}
result
}
///|
pub fn feature_selection_indices(selection : FeatureSelection) -> Array[Int] {
selection.selected_indices.copy()
}
///|
pub fn feature_selection_scores_vector(
selection : FeatureSelection,
) -> Array[Double] {
let result = []
for score in selection.scores {
result.push(score.relevance * score.stability - score.redundancy)
}
result
}
///|
pub fn feature_selection_relevance(
selection : FeatureSelection,
) -> Array[Double] {
let result = []
for score in selection.scores {
result.push(score.relevance)
}
result
}
///|
pub fn feature_selection_redundancy(
selection : FeatureSelection,
) -> Array[Double] {
let result = []
for score in selection.scores {
result.push(score.redundancy)
}
result
}
///|
pub fn feature_selection_is_selected(
selection : FeatureSelection,
index : Int,
) -> Bool {
selection.selected_indices.contains(index)
}
///|
pub fn feature_selection_count(selection : FeatureSelection) -> Int {
selection.selected_indices.length()
}
///|
pub fn feature_selection_quality(selection : FeatureSelection) -> Double {
if selection.scores.length() == 0 {
1.0
} else {
selection.objective / selection.scores.length().to_double()
}
}
///|
pub fn feature_selection_lines(selection : FeatureSelection) -> Array[String] {
let lines = [
"selected=" + selection.selected_indices.length().to_string(),
"threshold=" + selection.threshold.to_string(),
"objective=" + selection.objective.to_string(),
]
for score in selection.scores {
lines.push(
score.index.to_string() +
"|" +
score.relevance.to_string() +
"|" +
score.redundancy.to_string() +
"|" +
score.stability.to_string() +
"|" +
score.selected.to_string(),
)
}
lines
}
///|
pub fn feature_selection_string(selection : FeatureSelection) -> String {
feature_selection_lines(selection).join("\n")
}
///|
pub fn feature_selection_compare(
left : FeatureSelection,
right : FeatureSelection,
) -> Array[Double] {
[
feature_selection_count(left).to_double(),
feature_selection_count(right).to_double(),
feature_selection_quality(left),
feature_selection_quality(right),
right.objective - left.objective,
]
}
///|
pub fn feature_selection_stable(
left : FeatureSelection,
right : FeatureSelection,
) -> Bool {
left.selected_indices == right.selected_indices
}
///|
pub fn feature_selection_batch(
features : Array[Array[Array[Double]]],
target : Array[Double],
threshold : Double,
folds : Int,
) -> Array[Double] {
let result = []
for matrix in features {
result.push(
feature_selection_quality(
feature_selection_run(matrix, target, threshold, folds),
),
)
}
result
}
///|
pub fn feature_selection_summary(selection : FeatureSelection) -> Array[Double] {
[
selection.selected_indices.length().to_double(),
selection.scores.length().to_double(),
selection.threshold,
selection.objective,
feature_selection_quality(selection),
]
}