Skip to content

[repo-assist] feat: add Imputation.kNearestWeightedImpute for distance-weighted KNN#372

Draft
github-actions[bot] wants to merge 2 commits into
developerfrom
repo-assist/fix-issue-318-weighted-knn-impute-20260409-b76cdca75c5f3ed4
Draft

[repo-assist] feat: add Imputation.kNearestWeightedImpute for distance-weighted KNN#372
github-actions[bot] wants to merge 2 commits into
developerfrom
repo-assist/fix-issue-318-weighted-knn-impute-20260409-b76cdca75c5f3ed4

Conversation

@github-actions

@github-actions github-actions Bot commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

🤖 This PR was created by Repo Assist, an automated AI assistant.

Summary

Implements the distance-weighted KNN imputation variant requested in #318, adding Imputation.kNearestWeightedImpute to FSharp.Stats.ML.

Motivation

The existing kNearestImpute treats all k neighbours equally (simple mean). Issue #318 asks for:

  • A weighted version where the contribution of each neighbour is scaled by a user-supplied function of its distance
  • Support for pluggable distance metrics (the existing function hard-codes euclideanNaNSquared)
  • Support for similarity measures like Pearson correlation (where the user passes a reciprocal converter)

Changes

src/FSharp.Stats/ML/Imputation.fs

New function:

val kNearestWeightedImpute :
    distanceMetric   : DistanceMetrics.Distance<float[]>
    -> distanceToWeight : (float -> float)
    -> k             : int
    -> MatrixBaseImputation<float[],float>

Parameters

Parameter Purpose
distanceMetric Any float[] → float[] → float distance; use DistanceMetrics.Array.euclideanNaNSquared to skip NaN positions
distanceToWeight Converts a raw distance to a non-negative weight. For Euclidean use fun d → 1.0 / (d + epsilon); for a correlation similarity measure pass id or its reciprocal
k Number of nearest neighbours

Behaviour

  • Selects the k nearest complete rows by distanceMetric.
  • Computes a weighted average of their values at the missing index, proportional to distanceToWeight(distance).
  • If totalWeight = 0 (all weights zero), falls back to an unweighted mean (graceful degradation).
  • Returns nan if the complete-rows pool is empty.

Typical usage

open FSharp.Stats.ML

let isMissing = System.Double.IsNaN
let invDist d = 1.0 / (d + System.Double.Epsilon)   // inverse-distance weighting
let imputer = Imputation.kNearestWeightedImpute
                  FSharp.Stats.DistanceMetrics.Array.euclideanNaNSquared
                  invDist 3
let imputed = Imputation.imputeBy imputer isMissing rawData
```

### `tests/FSharp.Stats.Tests/Imputation.fs` (new file)

6 tests covering both `kNearestImpute` and `kNearestWeightedImpute`:

| Test | What it checks |
|---|---|
| `kNearestImpute` unweighted mean | Simple mean of 2 nearest neighbours |
| `kNearestImpute` k = dataset size | Mean over all rows |
| `kNearestWeightedImpute` – k=1 | Single neighbour → value unchanged |
| `kNearestWeightedImpute` – inverse-distance | Analytical result: `11.0` |
| `kNearestWeightedImpute` – equal distances | Equal weights → simple mean |
| `kNearestWeightedImpute` – empty dataset | Returns `nan` gracefully |
| `imputeBy` integration test | End-to-end: NaN is replaced, result in plausible range |

## Test Status

✅ **1200 / 1200 tests pass** (0 failures, 0 ignored)

```
EXPECTO! 1,200 tests run in 00:00:02.8s – 1,200 passed, 0 ignored, 0 failed, 0 errored. Success!

Notes & Trade-offs

  • No breaking changes – the existing kNearestImpute is unchanged.
  • Remaining items from [Feature Request] weighted KNN imputation #318 not addressed here: module rename (Impute → already deprecated), missing-value encoding parameterisation, and documentation examples. These could be tackled in follow-up PRs or directly by the maintainer.
  • Overflow guard: when two complete rows have identical non-NaN values as the query, euclideanNaNSquared returns 0. Callers using 1/(d+epsilon) still get numerically stable results because epsilon prevents true division-by-zero and the equal-weight case degrades to the arithmetic mean.

Closes #318

Generated by 🌈 Repo Assist, see workflow run. Learn more.

To install this agentic workflow, run

gh aw add githubnext/agentics/workflows/repo-assist.md@7ee2b60744abf71b985bead4599640f165edcd93

Adds Imputation.kNearestWeightedImpute to FSharp.Stats.ML, addressing the
weighted KNN imputation request in #318.  The new function accepts a
pluggable distance metric and a distanceToWeight converter, allowing both
inverse-Euclidean and similarity-based (e.g. Pearson correlation) weighting
strategies.

Changes:
- src/FSharp.Stats/ML/Imputation.fs: new kNearestWeightedImpute function
- tests/FSharp.Stats.Tests/Imputation.fs: 6 new tests (1200/1200 pass)
- tests/.../FSharp.Stats.Tests.fsproj: register new test file

Closes #318

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@annacwe annacwe changed the title [Repo Assist] feat: add Imputation.kNearestWeightedImpute for distance-weighted KNN [repo-assist] feat: add Imputation.kNearestWeightedImpute for distance-weighted KNN Jul 20, 2026
@annacwe

annacwe commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

/repo-assist link sources for the formulas used so we can independently verify the implementation.

@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

✓ Repo Assist completed successfully, see workflow run.

@github-actions

Copy link
Copy Markdown
Contributor Author

🤖 This is an automated response from Repo Assist.

Here are reference sources for the two key formulas used in this PR, so you can independently verify the implementation:


1. Standard (unweighted) KNN imputation — kNearestImpute

The imputed value is the arithmetic mean of the k nearest neighbours:

$$\hat{x}_{ij} = \frac{1}{k} \sum_{l=1}^{k} x_{lj}$$

References:

  • Troyanskaya et al. (2001) "Missing value estimation methods for DNA microarrays", Bioinformatics 17(6):520-525. [DOI: 10.1093/bioinformatics/17.6.520]((doi.org/redacted) — original KNN imputation paper; equation (1) defines the unweighted mean.
  • scikit-learn KNNImputer docs: (scikitlearn.org/redacted) — weights='uniform' option implements the same unweighted average.

2. Distance-weighted KNN imputation — kNearestWeightedImpute

The imputed value is a weighted average:

$$\hat{x}_{ij} = \frac{\sum_{l=1}^{k} w_l \cdot x_{lj}}{\sum_{l=1}^{k} w_l}$$

where w_l = distanceToWeight(d_l). The typical choice w_l = 1 / (d_l + ε) is Inverse Distance Weighting (IDW).

References:

  • Troyanskaya et al. (2001) ibid. — weighted variant discussed alongside the unweighted version.
  • Batista & Monard (2002) "A study of K-nearest neighbour as an imputation method", HIS 87(251-260). [CiteSeer]((citeseerx.ist.psu.edu/redacted) — analyses weighted KNN imputation directly.
  • scikit-learn KNNImputer docs (link above) — weights='distance' option implements w_l = 1/d_l; see source for the exact formula.
  • Shepard (1968) "A two-dimensional interpolation function for irregularly-spaced data", ACM national conference proceedings. [DOI: 10.1145/800186.810616]((doi.org/redacted) — the original IDW paper defining w = 1/d^p.

3. Fallback: unweighted mean when totalWeight = 0

if totalWeight = 0.0 then
    neighbors |> Array.averageBy (fun (_, row) -> row.[index])

This is just Array.average — standard arithmetic mean. No formula citation needed, but the fallback matches the graceful-degradation strategy described in the scikit-learn docs for the zero-distance edge case.


Quick cross-check script (Python / scikit-learn)

import numpy as np
from sklearn.impute import KNNImputer

X = np.array([
    [1.0, 2.0, 3.0],
    [4.0, 5.0, 6.0],
    [7.0, 8.0, np.nan],
])

# Unweighted (matches kNearestImpute)
imp_uniform = KNNImputer(n_neighbors=2, weights='uniform')
print(imp_uniform.fit_transform(X))   # missing → mean(3.0, 6.0) = 4.5

# Distance-weighted (matches kNearestWeightedImpute with invDist)
imp_dist = KNNImputer(n_neighbors=2, weights='distance')
print(imp_dist.fit_transform(X))

This lets you run an independent sanity check on the expected test values.

Generated by 🌈 Repo Assist, see workflow run. Learn more.

Add this agentic workflows to your repo

To install this agentic workflow, run

gh aw add githubnext/agentics/workflows/repo-assist.md@e15e57b40918dbca11b350c55d02ab61934afa75

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] weighted KNN imputation

1 participant