You can use numpy.piecewise() to create the piecewise function and then use curve_fit(), Here is the code

from scipy import optimize
import matplotlib.pyplot as plt
import numpy as np
%matplotlib inline

x = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ,11, 12, 13, 14, 15], dtype=float)
y = np.array([5, 7, 9, 11, 13, 15, 28.92, 42.81, 56.7, 70.59, 84.47, 98.36, 112.25, 126.14, 140.03])

def piecewise_linear(x, x0, y0, k1, k2):
    return np.piecewise(x, [x < x0], [lambda x:k1*x + y0-k1*x0, lambda x:k2*x + y0-k2*x0])

p , e = optimize.curve_fit(piecewise_linear, x, y)
xd = np.linspace(0, 15, 100)
plt.plot(x, y, "o")
plt.plot(xd, piecewise_linear(xd, *p))

the output:

For an N parts fitting, please reference segments_fit.ipynb

Answer from HYRY on Stack Overflow
🌐
NumPy
numpy.org › doc › stable › reference › generated › numpy.piecewise.html
numpy.piecewise — NumPy v2.4 Manual
>>> x = np.linspace(-2.5, 2.5, 6) >>> np.piecewise(x, [x < 0, x >= 0], [-1, 1]) array([-1., -1., -1., 1., 1., 1.])
Top answer
1 of 12
88

You can use numpy.piecewise() to create the piecewise function and then use curve_fit(), Here is the code

from scipy import optimize
import matplotlib.pyplot as plt
import numpy as np
%matplotlib inline

x = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ,11, 12, 13, 14, 15], dtype=float)
y = np.array([5, 7, 9, 11, 13, 15, 28.92, 42.81, 56.7, 70.59, 84.47, 98.36, 112.25, 126.14, 140.03])

def piecewise_linear(x, x0, y0, k1, k2):
    return np.piecewise(x, [x < x0], [lambda x:k1*x + y0-k1*x0, lambda x:k2*x + y0-k2*x0])

p , e = optimize.curve_fit(piecewise_linear, x, y)
xd = np.linspace(0, 15, 100)
plt.plot(x, y, "o")
plt.plot(xd, piecewise_linear(xd, *p))

the output:

For an N parts fitting, please reference segments_fit.ipynb

2 of 12
39

You can use pwlf to perform continuous piecewise linear regression in Python. This library can be installed using pip.

There are two approaches in pwlf to perform your fit:

  1. You can fit for a specified number of line segments.
  2. You can specify the x locations where the continuous piecewise lines should terminate.

Let's go with approach 1 since it's easier, and will recognize the 'gradient change point' that you are interested in.

I notice two distinct regions when looking at the data. Thus it makes sense to find the best possible continuous piecewise line using two line segments. This is approach 1.

import numpy as np
import matplotlib.pyplot as plt
import pwlf

x = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15])
y = np.array([5, 7, 9, 11, 13, 15, 28.92, 42.81, 56.7, 70.59,
              84.47, 98.36, 112.25, 126.14, 140.03])

my_pwlf = pwlf.PiecewiseLinFit(x, y)
breaks = my_pwlf.fit(2)
print(breaks)

[ 1. 5.99819559 15. ]

The first line segment runs from [1., 5.99819559], while the second line segment runs from [5.99819559, 15.]. Thus the gradient change point you asked for would be 5.99819559.

We can plot these results using the predict function.

x_hat = np.linspace(x.min(), x.max(), 100)
y_hat = my_pwlf.predict(x_hat)

plt.figure()
plt.plot(x, y, 'o')
plt.plot(x_hat, y_hat, '-')
plt.show()

Top answer
1 of 5
11

numpy.piecewise can do this.

piecewise(x, condlist, funclist, *args, **kw)

Evaluate a piecewise-defined function.

Given a set of conditions and corresponding functions, evaluate each function on the input data wherever its condition is true.

An example is given on SO here. For completeness, here is an example:

from scipy import optimize
import matplotlib.pyplot as plt
import numpy as np

x = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ,11, 12, 13, 14, 15], dtype=float)
y = np.array([5, 7, 9, 11, 13, 15, 28.92, 42.81, 56.7, 70.59, 84.47, 98.36, 112.25, 126.14, 140.03])

def piecewise_linear(x, x0, y0, k1, k2):
    return np.piecewise(x, [x < x0, x >= x0], [lambda x:k1*x + y0-k1*x0, lambda x:k2*x + y0-k2*x0])

p , e = optimize.curve_fit(piecewise_linear, x, y)
xd = np.linspace(0, 15, 100)
plt.plot(x, y, "o")
plt.plot(xd, piecewise_linear(xd, *p))
2 of 5
10

The method proposed by Vito M. R. Muggeo[1] is relatively simple and efficient. It works for a specified number of segments, and for a continuous function. The positions of the breakpoints are iteratively estimated by performing, for each iteration, a segmented linear regression allowing jumps at the breakpoints. From the values of the jumps, the next breakpoint positions are deduced, until there are no more discontinuity (jumps).

"the process is iterated until possible convergence, which is not, in general, guaranteed"

In particular, the convergence or the result may depends on the first estimation of the breakpoints.

This is the method used in the R Segmented package.

Here is an implementation in python:

import numpy as np
from numpy.linalg import lstsq

ramp = lambda u: np.maximum( u, 0 )
step = lambda u: ( u > 0 ).astype(float)

def SegmentedLinearReg( X, Y, breakpoints ):
    nIterationMax = 10

    breakpoints = np.sort( np.array(breakpoints) )

    dt = np.min( np.diff(X) )
    ones = np.ones_like(X)

    for i in range( nIterationMax ):
        # Linear regression:  solve A*p = Y
        Rk = [ramp( X - xk ) for xk in breakpoints ]
        Sk = [step( X - xk ) for xk in breakpoints ]
        A = np.array([ ones, X ] + Rk + Sk )
        p =  lstsq(A.transpose(), Y, rcond=None)[0] 

        # Parameters identification:
        a, b = p[0:2]
        ck = p[ 2:2+len(breakpoints) ]
        dk = p[ 2+len(breakpoints): ]

        # Estimation of the next break-points:
        newBreakpoints = breakpoints - dk/ck 

        # Stop condition
        if np.max(np.abs(newBreakpoints - breakpoints)) < dt/5:
            break

        breakpoints = newBreakpoints
    else:
        print( 'maximum iteration reached' )

    # Compute the final segmented fit:
    Xsolution = np.insert( np.append( breakpoints, max(X) ), 0, min(X) )
    ones =  np.ones_like(Xsolution) 
    Rk = [ c*ramp( Xsolution - x0 ) for x0, c in zip(breakpoints, ck) ]

    Ysolution = a*ones + b*Xsolution + np.sum( Rk, axis=0 )

    return Xsolution, Ysolution

Example:

import matplotlib.pyplot as plt

X = np.linspace( 0, 10, 27 )
Y = 0.2*X  - 0.3* ramp(X-2) + 0.3*ramp(X-6) + 0.05*np.random.randn(len(X))
plt.plot( X, Y, 'ok' );

initialBreakpoints = [1, 7]
plt.plot( *SegmentedLinearReg( X, Y, initialBreakpoints ), '-r' );
plt.xlabel('X'); plt.ylabel('Y');

[1]: Muggeo, V. M. (2003). Estimating regression models with unknown breakpoints. Statistics in medicine, 22(19), 3055-3071.

🌐
Readthedocs
symfit.readthedocs.io › en › stable › examples › ex_piecewise.html
Example: Piecewise continuous function — symfit 0.5.6 documentation
from symfit import parameters, variables, Fit, Piecewise, exp, Eq, Model import numpy as np import matplotlib.pyplot as plt x, y = variables('x, y') a, b, x0 = parameters('a, b, x0') # Make a piecewise model y1 = x**2 - a * x y2 = a * x + b model = Model({y: Piecewise((y1, x <= x0), (y2, x > x0))}) # As a constraint, we demand equality between the two models at the point x0 # to do this, we substitute x -> x0 and demand equality using `Eq` constraints = [ Eq(y1.subs({x: x0}), y2.subs({x: x0})) ] # Generate example data xdata = np.linspace(-4, 4., 50) ydata = model(x=xdata, a=0.0, b=1.0, x0=1.0
🌐
GitHub
github.com › cjekel › piecewise_linear_fit_py
GitHub - cjekel/piecewise_linear_fit_py: fit piecewise linear data for a specified number of line segments
A library for fitting continuous piecewise linear functions to data.
Starred by 344 users
Forked by 64 users
Languages   Python 98.0% | Python 98.0%
🌐
NumPy
numpy.org › doc › 2.1 › reference › generated › numpy.piecewise.html
numpy.piecewise — NumPy v2.1 Manual
>>> x = np.linspace(-2.5, 2.5, 6) >>> np.piecewise(x, [x < 0, x >= 0], [-1, 1]) array([-1., -1., -1., 1., 1., 1.])
🌐
Songhuiming
songhuiming.github.io › pages › 2015 › 09 › 22 › piecewise-linear-function-and-the-explanation
piecewise linear function and the explanation — pydata: Huiming's learning notes
# piecewise linear data prepare x1 = np.where(x > -15, x + 15, 0) x2 = np.where(x > 10, x - 10, 0) dtest = pd.DataFrame([y, x, x1, x2]).T dtest.columns = ['y', 'x', 'x1', 'x2'] # piecewise linear regression f2 = smf.ols(formula = 'y ~ x + x1 + x2', data = dtest).fit() dtest['f2_pred'] = f2.predict() # print f2.summary() dtest.sort('x', inplace = True) fig = plt.figure(figsize = (16, 12)) ax = fig.add_subplot(111) ax.plot(x, y, linestyle = '', color = 'k', linewidth = 0.25, markeredgecolor='none', marker = '.', label = r'scatter plot') ax.plot(dtest.x, dtest.f2_pred, color = 'b', linestyle = '-
🌐
GitHub
github.com › chasmani › piecewise-regression
GitHub - chasmani/piecewise-regression: piecewise-regression (aka segmented regression) in python. For fitting straight line models to data with one or more breakpoints where the gradient changes. · GitHub
piecewise-regression (aka segmented regression) in python. For fitting straight line models to data with one or more breakpoints where the gradient changes. - chasmani/piecewise-regression
Starred by 125 users
Forked by 25 users
Languages   Python 96.7% | TeX 3.3%
Top answer
1 of 3
2

You could directly copy the segments_fit implementation

from scipy import optimize

def segments_fit(X, Y, count):
    xmin = X.min()
    xmax = X.max()

    seg = np.full(count - 1, (xmax - xmin) / count)

    px_init = np.r_[np.r_[xmin, seg].cumsum(), xmax]
    py_init = np.array([Y[np.abs(X - x) < (xmax - xmin) * 0.01].mean() for x in px_init])

    def func(p):
        seg = p[:count - 1]
        py = p[count - 1:]
        px = np.r_[np.r_[xmin, seg].cumsum(), xmax]
        return px, py

    def err(p):
        px, py = func(p)
        Y2 = np.interp(X, px, py)
        return np.mean((Y - Y2)**2)

    r = optimize.minimize(err, x0=np.r_[seg, py_init], method='Nelder-Mead')
    return func(r.x)

Then you apply it as follows

import numpy as np;

# mimic your data
x = np.linspace(0, 50)
y = 50 - np.clip(x, 10, 40)

# apply the segment fit
fx, fy = segments_fit(x, y, 3)

This will give you (fx,fy) the corners your piecewise fit, let's plot it

import matplotlib.pyplot as plt

# show the results
plt.figure(figsize=(8, 3))
plt.plot(fx, fy, 'o-')
plt.plot(x, y, '.')
plt.legend(['fitted line', 'given points'])

EDIT: Introducing constant segments

As mentioned in the comments the above example doesn't guarantee that the output will be constant in the end segments.

Based on this implementation the easier way I can think is to restrict func(p) to do that, a simple way to ensure a segment is constant, is to set y[i+1]==y[i]. Thus I added xanchor and yanchor. If you give an array with repeated numbers you can bind multiple points to the same value.

from scipy import optimize

def segments_fit(X, Y, count, xanchors=slice(None), yanchors=slice(None)):
    xmin = X.min()
    xmax = X.max()
    seg = np.full(count - 1, (xmax - xmin) / count)

    px_init = np.r_[np.r_[xmin, seg].cumsum(), xmax]
    py_init = np.array([Y[np.abs(X - x) < (xmax - xmin) * 0.01].mean() for x in px_init])

    def func(p):
        seg = p[:count - 1]
        py = p[count - 1:]
        px = np.r_[np.r_[xmin, seg].cumsum(), xmax]
        py = py[yanchors]
        px = px[xanchors]
        return px, py

    def err(p):
        px, py = func(p)
        Y2 = np.interp(X, px, py)
        return np.mean((Y - Y2)**2)

    r = optimize.minimize(err, x0=np.r_[seg, py_init], method='Nelder-Mead')
    return func(r.x)

I modified a little the data generation to make it more clear the effect of the change

import matplotlib.pyplot as plt
import numpy as np;

# mimic your data
x = np.linspace(0, 50)
y = 50 - np.clip(x, 10, 40) + np.random.randn(len(x)) + 0.25 * x
# apply the segment fit
fx, fy = segments_fit(x, y, 3)
plt.plot(fx, fy, 'o-')
plt.plot(x, y, '.k')
# apply the segment fit with some consecutive points having the 
# same anchor
fx, fy = segments_fit(x, y, 3, yanchors=[1,1,2,2])
plt.plot(fx, fy, 'o--r')
plt.legend(['fitted line', 'given points', 'with const segments'])

2 of 3
1

You can get a one line solution (not counting the import) using univariate splines of degree one. Like this

from scipy.interpolate import UnivariateSpline

f = UnivariateSpline(x,y,k=1,s=0)

Here k=1 means we interpolate using polynomials of degree one aka lines. s is the smoothing parameter. It decides how much you want to compromise on the fit to avoid using too many segments. Setting it to zero means no compromises i.e. the line HAS to go threw all points. See the documentation.

Then

plt.plot(x, y, "o", label='original data')
plt.plot(x, f(x), label='linear interpolation')
plt.legend()
plt.savefig("out.png", dpi=300)

gives

Find elsewhere
🌐
University of Texas at Austin
het.as.utexas.edu › HET › Software › Numpy › reference › generated › numpy.piecewise.html
numpy.piecewise — NumPy v1.9 Manual
>>> x = np.linspace(-2.5, 2.5, 6) >>> np.piecewise(x, [x < 0, x >= 0], [-1, 1]) array([-1., -1., -1., 1., 1., 1.])
🌐
Readthedocs
piecewise-regression.readthedocs.io
piecewise-regression — piecewise-regression 1 documentation
For demonstration purposes, substitute with your own data to fit. ... import piecewise_regression import numpy as np alpha_1 = -4 alpha_2 = -2 constant = 100 breakpoint_1 = 7 n_points = 200 np.random.seed(0) xx = np.linspace(0, 20, n_points) yy = constant + alpha_1*xx + (alpha_2-alpha_1) * np.maximum(xx - breakpoint_1, 0) + np.random.normal(size=n_points)
🌐
Jekel
jekel.me › 2017 › Fit-a-piecewise-linear-function-to-data
Charles Jekel - jekel.me - Fitting a piecewise linear function to data
April 1, 2017 - The result is the optimal continuous piecewise linear function for the specified number of line segments. Feel free to download the library. You can install the library with pip. ... A few examples of the fits are provided bellow. So let’s get started by importing the libraries and using some sample data. # import our libraires import numpy as np import pwlf # your data y = np.array([ 0.00000000e+00, 9.69801700e-03, 2.94350340e-02, 4.39052750e-02, 5.45343950e-02, 6.74104940e-02, 8.34831790e-02, 1.02580042e-01, 1.22767939e-01, 1.42172312e-01, 0.00000000e+00, 8.58600000e-06, 8.31543400e-03, 2.
🌐
SciPy
docs.scipy.org › doc › scipy › tutorial › interpolate › splines_and_polynomials.html
Piecewise polynomials and splines — SciPy v1.17.0 Manual
All piecewise polynomials can be constructed with N-dimensional y values. If y.ndim > 1, it is understood as a stack of 1D y values, which are arranged along the interpolation axis (with the default value of 0). The latter is specified via the axis argument, and the invariant is that len(x) ...
🌐
Mabouali
mabouali.com › 2024 › 02 › 04 › piecewise-linear-functions-part-i
Piecewise-Linear Functions: Part I – Moe’s Homepage
February 4, 2024 - Piecewise-Linear Function and Linear Function are different concepts; Now as of the question of how do we implement them in Python? Well, it depends. For example, the first function could be implemented as: import numpy as np def my_piecewise_function(x): return np.abs(x)