Fitting a linear spline with scipy.interpolate.splrep leads to many NaN coefficients

Viewed 23

I have some data that can be fit by eye with a linear spline with a single internal knot pretty easily. But scipy.interpolate.splrep is choking on the fit, no matter what smoothing parameter I provide. You can see a plot of my data below.

Here's some code that iterates tries different smoothing parameters until the scipy gives up and raises a RuntimeWarning:

import math
import itertools
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from scipy.interpolate import splrep, splev


def is_sorted(a):
    return np.all(a[:-1] <= a[1:])


def fit_spline(x, y):
    assert(is_sorted(x))
    
    best_n_nans = 2*len(x)
    for log_s in itertools.count(0.01, 0.0001):
        try:
            spl = splrep(x, y, s=math.exp(log_s), k=1)
        except RuntimeWarning:
            break
        
        n_nans = sum(math.isnan(t) for t in spl[1])
        if n_nans < best_n_nans:
            best_fit = spl
            best_n_nans = n_nans
            print(f"Found fit with {n_nans} nans")
            
        if len(spl[0]) <= 5:
            break
    return best_fit

matplotlib.use("Qt5Agg")
plot = plt.figure()
x = np.array([  0. ,   0. ,   0. ,   0. ,   0. ,   0.5,   1.2,   2.3,   2.6,
         3. ,   3.1,   3.1,   3.1,   5. ,   5.1,   5.2,   5.6,   6.3,
         6.5,   6.9,   9.6,   9.8,  10. ,  10.3,  10.7,  11.3,  11.3,
        12.4,  12.4,  12.4,  12.5,  12.5,  13.1,  14.2,  15.4,  15.7,
        16.1,  17.7,  18.5,  18.6,  20.5,  21.3,  22.7,  23. ,  24.3,
        26.3,  26.3,  26.6,  27. ,  27.7,  28.7,  31. ,  31. ,  31.1,
        31.1,  31.6,  32.7,  34.4,  34.5,  35.7,  35.7,  38.7,  39.8,
        43.9,  43.9,  46.9,  51. ,  52.6,  52.6,  53.3,  53.7,  54.5,
        57.7,  61.2,  62.5,  65. ,  66. ,  68.1,  70.8,  74.3,  74.6,
        76.3,  83.1,  85.7,  87.5,  90.8,  91. ,  92.2,  94.3,  94.6,
       101.1, 104.8, 109.6, 113.3, 114.8, 119. , 120.5, 121.1, 126.1,
       133.2, 136.2, 137.1, 138.5, 141.4, 144.6, 145.1, 145.3, 150.5,
       160.7, 167.4, 168.8, 170.5, 170.5, 173.3, 177. , 181.3, 185.1,
       193. , 200.3, 201.8, 204.3, 208.4, 209.9, 223.7, 230.9, 240.3,
       243.9, 250.6, 263.1])
y = np.array([ 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
        0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
        0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
        0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  7,  6,
        0,  0,  0,  9,  0,  0,  0,  0,  0,  0,  9,  1,  0,  3,  0,  9,  2,
       10,  0, 14, 19,  7, 10, 18, 22, 31, 12, 29, 16, 20,  2, 38, 26, 28,
       26, 25, 17, 35, 20, 29, 39, 51, 29, 27, 43, 38, 40, 47, 41, 39, 54,
       44, 53, 65, 50, 59, 51, 62, 69, 74, 66])

spl = fit_spline(x, y)
y2 = splev(x, spl)
plt.plot(x, y, 'o', x, y2)
plot.show()

The result has lots of NaN coefficients:

array([nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan,
       nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan,
       nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan,
       nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, 35., 38.,
       40., 47., 41., 39., 54., 44., 53., 65., 50., 59., 51., 62., 69.,
       74., 66.,  0.,  0.])

Here's a plot of the input data for this example with the partial fit I get:

Data with partial fit

What can I do to get a reasonable fit? I have tried supplying a weight vector with high weights for the first points but it doesn't do anything. I am not married to scipy if there's another library that does this better.

Ideally I would raise the smoothing parameter until I get a fit with only a few knots, but first I need to get any working fit at all.

BTW, if I supply the knot point the fit works without a problem:

 spl = splrep(x, y, w=w, s=math.exp(log_s), k=1, t=[75.0])

gives

Successful fit

However, I'd like to turn this into a repeatable process that doesn't require a human in the loop.

0 Answers
Related