Repository: scipy/scipy

Issue:
griddata linear / LinearNDInterpolator unexpected behavior
I was trying to speed up my code recently, by reducing the data I feed into griddata with method linear or in other words LinearNDInterpolator. I was greatly surprised to find out that this altered my interpolated values significantly.

Reading the docs of LinearNDInterpolator my understanding of this LinearNDInterpolator or linear method of griddata respectivly is the following:
The surface is modelled with triangular planes between the points. Interpolation to a certain new coordinate should be only dependent on the three next nearest coordinates and values.
Therefore, in principle I should be able to only keep the three nearest data points for each coordinate of interest. Especially it shouldn't matter if I remove data that is not anywhere near the area of concern.

I wrote a "minimal example" that illustrates the problem well. 
1. I define an outer grid with 10x10 positions and random values between 0 and 1.
2. From that I copy a smaller inner grid with the exact same values with size 5x5.
3. I define 50 random positions within this inner grid.
4. I perform liner interpolation to the coordinates defined in 3. based on maps defined in 1. and 2.
5. I measure the difference between the two interpolations.

For many points the two interpolations are identical. However, there is always a difference for some positions with significant value like .2 or .5.

```
import numpy as np
import pandas as pd
from scipy.interpolate import griddata

low, high = -10, 10
low_in, high_in = -2, 2

points_huge = pd.DataFrame(
    [[i, j, np.random.rand()] for i in range(low, high) for j in range(low, high)],
    columns=['x', 'y', 'value']
)

points_in = points_huge[
    (points_huge.x >= low_in) &
    (points_huge.x <= high_in) &
    (points_huge.y >= low_in) &
    (points_huge.y <= high_in)
].copy()

eval_rand = pd.DataFrame(
    [[
        low_in + (high_in - low_in) * np.random.rand() * 0.9,
        low_in + (high_in - low_in) * np.random.rand() * 0.9
    ] for i in range(50)],
    columns=['x', 'y']
)

eval_rand['value_huge'] = griddata(
    points_huge[['x', 'y']],
    points_huge.value,
    eval_rand[['x', 'y']],
    method='linear'
)

eval_rand['value_in'] = griddata(
    points_in[['x', 'y']],
    points_in.value,
    eval_rand[['x', 'y']],
    method='linear'
)

print(max(abs(eval_rand.value_huge-eval_rand.value_in)))
```
I saw that the inner interpolation is done by QHull and found on the page of QHull that triangulation of non-convex surfaces is not supported. Does that mean my random surface can't be interpolated correctly?
![Capture](https://user-images.githubusercontent.com/26696299/200874003-ae8b95bc-77d8-465e-bec3-676a64dc304f.PNG)



Discussion:
 It is not entirely surprising that triangulation changes when you remove some points. Consider a 2D example, where you connect points on a plane with straight lines. Let's compare two interpolants like this, one with just two points, 1 and 2, and the other with three points, 1, 2, and 3.

Then the values of the interpolant at a fixed value of `x` is very different, as sketched with `o` and `*` symbols below.

```
1 -o----------2
  \         /
   *      /
     \   /
       3
```

This is I believe what is happening in your example. Also, as far as I can see, the non-zero difference occurs close to the edges of the `_in` area, this may or may not be relevant.

Conclusion: surprising, yes. Not  a bug though.
While in general one can expect of course interpolation to be changed if points are removed, I can't believe this, in case of linear interpolation and removing only points that are "screened" by other data points from the new coordinates.

I found in the meanwhile another function in scipy.interpolate that suits my needs and works like expected.
To proof it, I wrote another small demonstrator. I can shrink the inner grid down to one unit cell like 4 data points surrounding the coordinates to which I interpolate. I went up with a grid of 600x600 data points. This surrounding never affects the interpolation within the inner cell. The difference between the two interpolants is always zero.
That is what I would expect from linear interpolation.

It might be that this issue stems from triangulation. However, even if that behavior is expected by developers, I find naming of this function as griddata and description and especially the picture in docs of LinearNDInterpolator highly misleading. One should at least warn about this possible effects and maybe reference to linear method of RegularGridInterpolator, although this is not suitable for every usecase.

```
import numpy as np
import pandas as pd
from scipy.interpolate import RegularGridInterpolator
import time

low, high = -10, 10
ly, hy = -10, 15
low_in, high_in = -1, 1

points_huge = pd.DataFrame(
    [[i, j, np.random.rand()] for i in range(low, high) for j in range(ly, hy)],
    columns=['x', 'y', 'value']
)

points_in = points_huge[
    (points_huge.x >= low_in) &
    (points_huge.x < high_in) &
    (points_huge.y >= low_in) &
    (points_huge.y < high_in)
].copy()

eval_rand = pd.DataFrame(
    [[
        low_in + (high_in - low_in - 1) * np.random.rand() * 0.9,
        low_in + (high_in - low_in - 1) * np.random.rand() * 0.9
    ] for i in range(50)],
    columns=['x', 'y']
)

#%%
# Reshaping
points_huge.sort_values(by=['x', 'y'], inplace=True, ignore_index=True)
x_huge = [i for i in range(low, high)]
y_huge = [i for i in range(ly, hy)]
value_mat_huge = np.reshape(points_huge.value.values, (len(x_huge), len(y_huge)))

points_in.sort_values(by=['x', 'y'], inplace=True, ignore_index=True)
x_in = y_in = [i for i in range(low_in, high_in)]
value_mat_in = np.reshape(points_in.value.values, (len(x_in), len(y_in)))

#%%
t0 = time.perf_counter_ns()
interp_huge = RegularGridInterpolator(
    (x_huge, y_huge),
    value_mat_huge,
    bounds_error=False,
    fill_value=None,
    method='linear'
)

eval_rand['value_huge'] = interp_huge(eval_rand[['x', 'y']])
t1 = time.perf_counter_ns()
print((t1-t0)/10**9)
#%%
interp_in = RegularGridInterpolator(
    (x_in, y_in),
    value_mat_in,
    bounds_error=False,
    fill_value=None,
    method='linear'
)

eval_rand['value_in'] = interp_in(eval_rand[['x', 'y']])

print(max(abs(eval_rand.value_huge-eval_rand.value_in)))
```
Indeed, the issue you found is due to triangulation. LinearNDInterpolator operates with what QHull did triangulating the data. You can somewhat influence what QHull does, and if you manage to get a desired triangulation, you can pass it to LinearNDInterpolator.

RegularGridInterpolator does not use triangulation, so it is somewhat more predictable. However as you mentioned, it assumes that the data is on a regular (rectilinear) grid. For this sort of data, LinearNDInterpolator is certainly a wrong tool and should not be used. 

While `griddata` is indeed an unfortunate name for a scattered interpolator, the name is frozen by backwards compatibility and cannot be changed anymore. 

Documentation improvements are however most welcome! Either in docstrings (i.e. the API reference) or a tutorial (which was recently expanded, see https://scipy.github.io/devdocs/tutorial/interpolate.html). If you have an idea of how improve either of these, please send a pull request, we will gladly review it.
Hmm, since I am really no expert neither for the mathematical details nor the numerical details of interpolation I frankly don't feel confident in writing documentation for a highly used mathematical library.
To clarify the "good first issue" label: I think a helpful thing to do would be to add a `.. note::`  admonition to the Notes section of the `griddata` docstring, https://scipy.github.io/devdocs/reference/generated/scipy.interpolate.griddata.html#scipy.interpolate.griddata. It currently says _For data on a regular grid use [interpn] instead_ --- let's make this sentence stand out more, so that it is better visible in the docs. Or maybe even a `.. warning::` so that it's red.
The same note should then be added to the Notes sections of the griddata implementation classes, `NearestND`, `LinearND` and `CloughTocher2D` interpolators.

However it's full well possible that I'm missing what is best for a first-time user, so if someone has a better idea, please go for it.
Pull requests welcome!
Hello @ev-br. I’m interested in working on this issue. I’d like to clarify some things before I begin.

First, I wanted to ask about the test status for my fork. I followed this [documentation ](https://scipy.github.io/devdocs/dev/contributor/development_workflow.html#development-workflow)to set up my local fork. After activating my conda environment with `conda activate name-of-your-virtual-environment` running the test command `python dev.py test -v`, I end up with some failed tests, despite not changing anything on my fork.

My results were: 38223 passed, 2258 skipped, 12352 deselected, 139 xfailed, 9 xpassed.

As well, I wanted to confirm the required changes to be made to the documentation. 
”For data on a regular grid use [interpn] instead” -> “.. note:: For data on a regular grid use [interpn] instead” by changing the scipy/interpolate/_ndgriddata.py file for griddata and NearestND (Note would need to be added to NearestND, as this statement doesn’t exist for NearestND). The comment above would be added for LinearND and CloughTocher2D in scipy/interpolate/interpnd.pyx.

Please let me know what my next steps should be about the failed tests, and if my proposed documentation changes are correct. 

Looking forward to hearing from you and making my first contribution. Thanks
> 38223 passed, 2258 skipped, 12352 deselected, 139 xfailed, 9 xpassed.

If that's the test output, it means all tests passed. (xfailed= "expected failures"). xpasses can be looked at in a separate PR.

Your suggested wording sounds about right indeed.
Thanks for the confirmation @ev-br. I have made these changes in the documentation code in the 4 locations we spoke about earlier. Although the documentation build passes (I followed [these](https://scipy.github.io/devdocs/dev/contributor/rendering_documentation.html#rendering-documentation-locally-with-sphinx) instructions), the left side is showing nothing for the menu options: User Guide, API reference, Development, and Release Notes. The image below illustrates one example of this.
￼
<img width="1552" alt="scipy documentation  error" src="https://user-images.githubusercontent.com/74992167/213556308-7a14c337-9d25-41bd-98b1-4aff9c59a4c9.png">

Can you suggest any next steps for me to resolve this problem? Despite the tests passing, the local rendering is wrong.

Thanks again for your help.
Please just send the PR, we'll take it from there
> Can you suggest any next steps for me to resolve this problem? Despite the tests passing, the local rendering is wrong.

We can help you fix any remaining issues in the PR review - if you need help submitting make sure to check our [Contributing guide](https://scipy.github.io/devdocs/dev/contributor/contributor_toc.html#editing-scipy) and feel free to ping me for specific questions. Cheers!



Resolving patch:
diff --git a/scipy/interpolate/_ndgriddata.py b/scipy/interpolate/_ndgriddata.py
--- a/scipy/interpolate/_ndgriddata.py
+++ b/scipy/interpolate/_ndgriddata.py
@@ -50,6 +50,8 @@ class NearestNDInterpolator(NDInterpolatorBase):
     -----
     Uses ``scipy.spatial.cKDTree``
 
+    .. note:: For data on a regular grid use `interpn` instead.
+
     Examples
     --------
     We can interpolate values on a 2D plane:
@@ -173,7 +175,7 @@ def griddata(points, values, xi, method='linear', fill_value=np.nan,
 
     .. versionadded:: 0.9
 
-    For data on a regular grid use `interpn` instead.
+    .. note:: For data on a regular grid use `interpn` instead.
 
     Examples
     --------


Test patch: