Repository: scikit-learn/scikit-learn
Pull request: MNT: Make error message clearer for n_neighbors
<!--
Thanks for contributing a pull request! Please ensure you have taken a look at
the contribution guidelines: https://github.com/scikit-learn/scikit-learn/blob/main/CONTRIBUTING.md
-->

#### Reference Issues/PRs
#17207
<!--
Example: Fixes #1234. See also #3456.
Please use keywords (e.g., Fixes) to create link to the issues or pull requests
you resolved, so that they will automatically be closed when your pull request
is merged. See https://github.com/blog/1506-closing-issues-via-pull-requests
-->


#### What does this implement/fix? Explain your changes.
Fixes https://github.com/scikit-learn/scikit-learn/issues/17207 by making error message clearer


<!--
Please be aware that we are a loose team of volunteers so patience is
necessary; assistance handling other issues is very welcome. We value
all user contributions, no matter how minor they are. If we are slow to
review, either the pull request needs some benchmarking, tinkering,
convincing, etc. or more likely the reviewers are simply busy. In either
case, we ask for your understanding during the review process.
For more information, see our FAQ on this topic:
http://scikit-learn.org/dev/faq.html#why-is-my-pull-request-not-getting-any-attention.

Thanks for contributing!
-->
Tagging @cmarmo and @joshuacwnewton for review based on the earlier PR

Problem statement:
LocalOutlierFactor error when n_samples = 1 and n_neighbors = 1
Calling fit_predict() on LocalOutlierFactor with n_samples = n_neighbors = 1 results in the following ValueError : 
Expected n_neighbors <= n_samples, but n_samples = 1, n_neighbors = 2

This error message is a bit confusing as n_neighbors was set to 1. It seems it is set to 2 by LocalOutlierFactor without warning the user. 

The following code can be used to reproduce this error message : 
```python
X = [[0, 1, 2, 3]]
clf = LocalOutlierFactor(contamination = 'auto', n_neighbors = 1) 
clf.fit_predict(X)
```

Hints from issue discussion:
This is a tricky one. Basically `n_neighbors` semantic here mean that you want to use the sample itself and its nearest neighbour which corresponds to `n_neighbors=2` if you are using `NearestNeighbors.kneighbors`. So in practice, the algorithm does exactly what you expect. The relevant lines of code are:

https://github.com/scikit-learn/scikit-learn/blob/7117a6313791db6f8b737bac28c1f47277a68cfb/sklearn/neighbors/_base.py#L610-L620

@jnothman @amueller @GaelVaroquaux I don't know if there is a fix to clarify the error message? 

I'd be fine to have a separate "Expected n_neighbors < n_samples" (instead
of "Expected n_neighbors <= n_samples") error message where X is None, and
report the original n_neighbors. Why not?

Commit message: Add review suggestions to avoid repetition (@thomasjpfan)

Co-authored-by: Thomas J. Fan <thomasjpfan@gmail.com>

Patch to review:
diff --git a/sklearn/neighbors/_base.py b/sklearn/neighbors/_base.py
index 10e99a34e6497..3943ddb973bac 100644
--- a/sklearn/neighbors/_base.py
+++ b/sklearn/neighbors/_base.py
@@ -721,6 +721,7 @@ class from an array representing our data set and ask who's
                [2]]...)
         """
         check_is_fitted(self)
+        original_n_neighbors = n_neighbors
 
         if n_neighbors is None:
             n_neighbors = self.n_neighbors
@@ -746,9 +747,15 @@ class from an array representing our data set and ask who's
 
         n_samples_fit = self.n_samples_fit_
         if n_neighbors > n_samples_fit:
+            if query_is_train:
+                n_neighbors -= 1
+                inequality_str = "n_neighbors < n_samples"
+            else:
+                inequality_str = "n_neighbors <= n_samples"
+                
             raise ValueError(
-                "Expected n_neighbors <= n_samples, "
-                " but n_samples = %d, n_neighbors = %d" % (n_samples_fit, n_neighbors)
+                f"Expected {inequality_str}, but "
+                f"n_neighbors = {n_neighbors}, n_samples = {n_samples}"
             )
 
         n_jobs = effective_n_jobs(self.n_jobs)


Code review comments:
diff_hunk: @@ -746,9 +747,15 @@ class from an array representing our data set and ask who's
 
         n_samples_fit = self.n_samples_fit_
         if n_neighbors > n_samples_fit:
+            if query_is_train:
+                n_neighbors -= 1
+                inequality_str = "n_neighbors < n_samples"
+            else:
+                inequality_str = "n_neighbors <= n_samples"
+                
             raise ValueError(
-                "Expected n_neighbors <= n_samples, "
-                " but n_samples = %d, n_neighbors = %d" % (n_samples_fit, n_neighbors)
+                f"Expected {inequality_str}, but "
+                f"n_neighbors = {n_neighbors}, n_samples = {n_samples}"
path: sklearn/neighbors/_base.py
text: @user1:
```suggestion
                f"n_neighbors = {n_neighbors}, n_samples_fit = {n_samples_fit}"
```

diff_hunk: @@ -746,9 +747,15 @@ class from an array representing our data set and ask who's
 
         n_samples_fit = self.n_samples_fit_
         if n_neighbors > n_samples_fit:
+            if query_is_train:
+                n_neighbors -= 1
+                inequality_str = "n_neighbors < n_samples"
+            else:
+                inequality_str = "n_neighbors <= n_samples"
path: sklearn/neighbors/_base.py
text: @user1:
This feels more inline with what the code is doing:

```suggestion
                inequality_str = "n_neighbors < n_samples_fit"
            else:
                inequality_str = "n_neighbors <= n_samples_fit"
```

diff_hunk: @@ -721,6 +721,7 @@ class from an array representing our data set and ask who's
                [2]]...)
         """
         check_is_fitted(self)
+        original_n_neighbors = n_neighbors
path: sklearn/neighbors/_base.py
text: @user1:
This is not required anymore.

```suggestion
```

diff_hunk: @@ -746,9 +747,15 @@ class from an array representing our data set and ask who's
 
         n_samples_fit = self.n_samples_fit_
         if n_neighbors > n_samples_fit:
+            if query_is_train:
+                n_neighbors -= 1
+                inequality_str = "n_neighbors < n_samples"
+            else:
+                inequality_str = "n_neighbors <= n_samples"
+                
path: sklearn/neighbors/_base.py
text: @user1:
Linting error:

```suggestion

```