Skip to content

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

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

github-actions[bot] wants to merge 4 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>
@github-actions github-actions Bot mentioned this pull request May 1, 2026
1 of 24 tasks
@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

This comment has been minimized.

@dsyme
dsyme marked this pull request as ready for review September 30, 2026 16:11
@annacwe annacwe self-assigned this Oct 2, 2026
@dsyme

dsyme commented Oct 6, 2026

Copy link
Copy Markdown
Member

/repo-assist Use the information above and the pull request to do additional testing and provide proof that this implementation is correct/incorrect

@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor Author

✗ Repo Assist encountered failed, see workflow run.

Warning

Firewall blocked 2 domains

The following domains were blocked by the firewall during workflow execution:

  • api.nuget.org
  • dc.services.visualstudio.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "api.nuget.org"
    - "dc.services.visualstudio.com"

See Network Configuration for more information.

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

@github-actions

This comment has been minimized.

@annacwe

annacwe commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator

I had already started working on this PR, so here are my thoughts before this is merged:

1: In kNearestImpute, euclidianNaNSquared is hardcoded as the distance metric. I don't know exactly why that is. It made me wonder whether both functions should be consistent in how they choose the distance metric. To me, it seems weird to have the weighted version be more liberal than the unweighted one in that regard, as the resulting interface reads like this:

- kNearestImpute is a standard KNN imputer implementation using a recommended distance
- kNearestWeightedImpute is the customizable version of kNearestImpute, that is also to be used if you want to apply a different distance metric, regardless if you actually want to use weights or not.

Personally, I don't think I would expect this behaviour as a user. The options I see are
a) make the distance metric here configurable,
b) hardcode the same metric for both functions or,
c) explicitly document kNearestImpute as a convenience function and kNearestWeightedImpute as the general function.

My personal preference would be a). In that case nothing would need to change in this PR and I would just open a seperate issue/pull request. I'd be interested in hearing other opinions on this.

2: The documentation says that distanceToWeight converts a raw distance value into a non-negative weight, but since the function is user-supplied, this is not actually enforced. I think the wording should be adjusted here to make clear that this is an expectation rather than a guarantee provided by the implementation.
I'll update the documentation accordingly before this is merged.

3: On that note, as that function is user-inputted, nothing keeps it from supplying NaN or Infinity weights. In that case those values would be propagated through the calculation and may result in NaN imputation, even if all neighbours themselves were valid.
I don't know if it would make sense to guard against that or whether this should be considered the user's responsibility. The RA also mentioned this in the most recent comment. Opinions on that would be appreciated, although currently I'm leaning towards not guarding against it and documenting it instead.

4: Again in the documentation it says that

For similarity measures (e.g. Pearson correlation) pass id directly, or its reciprocal if you stored it as a distance.

I don't think this is correct. Since neighbours are selected by sorting ascending on the returned value, using Pearson correlation directly would select the most negative correlation first rather than the most positive.
Unless I'm missing something, I intend to remove this example before this is merged.

5: Regarding the RA's suggestion about Double.Epsilon, I would like to defer to someone with more experience on numerical programming than me. The argument seems reasonable, though.

@bvenn

bvenn commented Oct 11, 2026

Copy link
Copy Markdown
Member

@annacwe

regarding comment 1.

  • absolutely the distance metric should be configurable also for the default imputer 👍
  • However as then correlation measures could be used as distance, it must be ensured, that the function is updated to not choose the k neighbours with the least correlation since correlation anticorrelates with distance! Currently I'm not entirely sure how to ensure the correct neighbours are selected based on the distance_metric. I could imagine creating a dedicated kNearestNeighbour_distance type that encodes the distance function together with an indicator whether the neighbours should maximized or minimize its value. But this seems quite complicated for this purpose, doesn't it?

regarding comment 2.

  • Absolutely agreed. It takes a moment to wrap the head around the weighting concept. It took me some minutes to recall what this was all about.
  • In k nearest neighbour imputation the next k neighbours are identified given a distance function. Afterwards the average at each coordinate position is calculated to define the coordinate of the unknown element.
  • The goal of a weighting is to give neighbours that are very similar in all other coordinates a higher weight, than neighbours that are within the k but further away.
  • I agree that a distance-to-weight function always must be linked to the given distance metric (see comment 1), but am unsure of how to encode it best.

regarding comment 3.

  • I also would go for not guarding this issue. If it breaks the user should stumble across this by seeing NaN or inf as result. Capturing that issue would lull the user into a false sense of security.

regarding comment 4.

  • I think here it directly is about the weighing, right? In that case the docs would be correct as the weighting (in its current form) should be low for points far away (high distance or low correlation), so low weighting for low correlation

regarding comment 5.

  • The epsilon just prevents from division by zero (e.g. at zero distance) which would render the weight infinity and the weighing pointless. I think for an inverse-distance weighting this would be a correct conversion.

Overall RA's approach seems reasonable but poses challenges for an easy use of that method. Especially the inverse connection of distance and weight is not easy to briefly explain in a documentation.

/repo-assist please review the information in this comment and above and propose actions to simplify the usage of the weighted knn imputation.

@annacwe

annacwe commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

@bvenn
regarding 4:
My bad, I think I might be confusing myself here. My understanding was that I could theoretically pass a Pearson correlation as the distanceMetric, in which case the sorting is still done before any weighing ever happened. So no matter what you pass in distanceToWeight, the rows with the lowest correlation would still be chosen over the one with high correlation because of the sorting step. And since that step is done before the actual weighing, distanceToWeight will not change that in any way. Is that correct?
If so, I still think that documentation text would be somewhat misleading. Using id or the reciprocal would not change that at the sorting step correlation is treated as distance. I am very unsure regarding this whole point.

Regarding the Repo Assist command:
""/repo-assist" has to be first thing in comment" (see more here).
I think that might be why it didn't trigger

@bvenn

bvenn commented Oct 12, 2026

Copy link
Copy Markdown
Member

Yes, thats also my understanding. The sorting is based on the defined distance metric. If it is pearson, the sorting will be the wrong way. The current weighting is exactly the other way round (id for pearson, reciprocal for distances).

@bvenn

bvenn commented Oct 12, 2026

Copy link
Copy Markdown
Member

/repo-assist please review the information in this comment and above and propose actions to simplify the usage of the weighted knn imputation.

@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor Author

✓ Repo Assist completed successfully, see workflow run.

Warning

Firewall blocked 2 domains

The following domains were blocked by the firewall during workflow execution:

  • api.nuget.org
  • dc.services.visualstudio.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "api.nuget.org"
    - "dc.services.visualstudio.com"

See Network Configuration for more information.

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

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor Author

🤖 This is an automated response from Repo Assist.

To simplify usage, I pushed a commit adding Imputation.kNearestWeightedImputeDefault k. It uses NaN-aware squared Euclidean distance with inverse-distance weights, so the common case is a one-liner:

Imputation.imputeBy (Imputation.kNearestWeightedImputeDefault 3) Ops.isNan data

kNearestWeightedImpute remains available for custom metrics and weight functions. I also added a docs section in docs/Imputation.fsx and a test that checks the default against the explicit configuration. Imputation tests pass (8/8).

Warning

Firewall blocked 2 domains

The following domains were blocked by the firewall during workflow execution:

  • api.nuget.org
  • dc.services.visualstudio.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "api.nuget.org"
    - "dc.services.visualstudio.com"

See Network Configuration for more information.

Generated by 🌈 Repo Assist, see workflow run. Learn more.
Comment /repo-assist to run again

Add this agentic workflow to your repo

To install this agentic workflow, run

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

This branch has not been deployed

No deployments
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

3 participants