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()

🌐
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
from scipy import optimize def piecewise_linear(x, x0, x1, b, k1, k2, k3): condlist = [x < x0, (x >= x0) & (x < x1), x >= x1] funclist = [lambda x: k1*x + b, lambda x: k1*x + b + k2*(x-x0), lambda x: k1*x + b + k2*(x-x0) + k3*(x - x1)] return np.piecewise(x, condlist, funclist) p , e = optimize.curve_fit(piecewise_linear, x, y) xd = np.linspace(-30, 30, 1000) plt.plot(x, y, "o") plt.plot(xd, piecewise_linear(xd, *p)) ... python, AI, LLM, AGI, AIG, GPT, data mining, sklearn, pytorch, career growth, linux, deep learning, leetcode, dynamic programming, flask, highcharts, sql, webCrawl, random walk, multiprocessing, data visualization, numpy, tensorflow, quant, statsmodels, pandas, docker, matplotlib, data minging, remote access, mysql, base, tweepy, bokeh, sentiment analysis, map, apply, apply_async, git, PyQt, cx_freeze, tkinter, pelican, spyre, shiny, R, re
🌐
NumPy
numpy.org › doc › stable › reference › generated › numpy.interp.html
numpy.interp — NumPy v2.4 Manual
One-dimensional linear interpolation for monotonically increasing sample points. Returns the one-dimensional piecewise linear interpolant to a function with given discrete data points (xp, fp), evaluated at x.
🌐
Mabouali
mabouali.com › 2024 › 02 › 04 › piecewise-linear-functions-part-i
Piecewise-Linear Functions: Part I – Moe’s Homepage
February 4, 2024 - values for which the piecewise function needs to be evaluated, a list of conditions that defines how many segments you have, and · A list of functions for each segment. If the first condition is true, the first function from the list is returned; If the second condition is true, the second function is returned. ... import numpy as np def my_piecewise_function_2(x): return np.piecewise( x, # The value at which the piecewise function should be evaluated [ # A list of conditions x < -0.5, (-0.5 <= x) & (x &lt; 0), (0 <= x) & (x &lt; 0.5), (0.5 <= x) ], [ # One function corresponding to ech of the condition above.
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.

🌐
w3resource
w3resource.com › numpy › functional-programming › piecewise.php
NumPy Functional programming: piecewise() function - w3resource
August 19, 2022 - >>> import numpy as np >>> x = np.linspace(-3.5, 3.5, 5) >>> y = -6 >>> np.piecewise(y, [y < 0, y >= 0], [lambda x: -x, lambda x: x])
🌐
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.])
Find elsewhere
🌐
JAX Documentation
docs.jax.dev › en › latest › _autosummary › jax.numpy.piecewise.html
jax.numpy.piecewise — JAX documentation
Here’s an example of a function which is zero for negative values, and linear for positive values: >>> x = jnp.array([-4, -3, -2, -1, 0, 1, 2, 3, 4]) >>> condlist = [x < 0, x >= 0] >>> funclist = [lambda x: 0 * x, lambda x: x] >>> jnp.piecewise(x, condlist, funclist) Array([0, 0, 0, 0, 0, ...
🌐
Sketchy thoughts
sfalsharif.wordpress.com › 2009 › 12 › 18 › piecwise-function-definitions-in-numpy
Piecewise function definitions in NumPy | Sketchy thoughts
March 28, 2010 - The basic format of the piecewise statement, ignoring optional arguments, is numpy.piecewise(x, condlist, funclist) , where x is a NumPy ndarray, condlist is the list of boolean arrays upon which the piecwise function defenition depends, and funclist is a list of functions corresponding to each of the boolean conditons.
🌐
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.])
🌐
NumPy
numpy.org › doc › stable › search.html
Search - NumPy v2.4 Manual
Created using Sphinx 7.2.6 · Built with the PyData Sphinx Theme 0.16.1
Top answer
1 of 1
12

In general, numpy arrays are very good at doing sensible things when you just write the code as if they were just numbers. Chaining comparisons is one of the rare exceptions. The error you're seeing is essentially this (obfuscated a bit by piecewise internals and ipython error formatting):

>>> a = np.array([1, 2, 3])
>>> 1.5 < a
array([False,  True,  True], dtype=bool)
>>> 
>>> 1.5 < a < 2.5
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()
>>> 
>>> (1.5 < a) & (a < 2.5)
array([False,  True, False], dtype=bool)
>>> 

You can alternatively use np.logical_and, but bitwise & (not and) works just fine here.

As far as plotting is concerned, numpy itself doesn't do any. Here's an example with matplotlib:

>>> import numpy as np
>>> def piecew(x):
...   conds = [x < 0, (x > 0) & (x < 1), (x > 1) & (x < 2), x > 2]
...   funcs = [lambda x: x+1, lambda x: 1, 
...            lambda x: -x + 2., lambda x: (x-2)**2]
...   return np.piecewise(x, conds, funcs)
>>>
>>> import matplotlib.pyplot as plt
>>> xx = np.linspace(-0.5, 3.1, 100)
>>> plt.plot(xx, piecew(xx))
>>> plt.show() # or plt.savefig('foo.eps')

Notice that piecewise is a capricious beast. In particular, it needs its x argument to be an array, and won't even try converting it if it isn't (in numpy parlance: x needs to be an ndarray, not an array_like):

>>> piecew(2.1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in piecew
  File "/home/br/.local/lib/python2.7/site-packages/numpy/lib/function_base.py", line 690, in piecewise
    "function list and condition list must be the same")
ValueError: function list and condition list must be the same
>>> 
>>> piecew(np.asarray([2.1]))
array([ 0.01])
🌐
Python Pool
pythonpool.com › home › blog › all about numpy piecewise function
All about Numpy Piecewise Function - Python Pool
June 8, 2021 - Now, we shall use the linspace() function present in numpy to return evenly spaced numbers between a given interval. Here, we shall return 5 numbers between the intervals [ -10, 11 ]. ... We will now pass the x value as the first argument to ...
🌐
ProgramCreek
programcreek.com › python › example › 102171 › numpy.piecewise
Python Examples of numpy.piecewise
You may also want to check out all available functions/classes of the module numpy , or try the search function . ... def quintic_spline(dx, dtype=np.float64): def inner(x): return 1 + x ** 3 / 12 * (-95 + 138 * x - 55 * x ** 2) def middle(x): return (x - 1) * (x - 2) / 24 * (-138 + 348 * x - 249 * x ** 2 + 55 * x ** 3) def outer(x): return (x - 2) * (x - 3) ** 2 / 24 * (-54 + 50 * x - 11 * x ** 2) window = np.arange(-3, 4) x = np.abs(dx - window) result = np.piecewise( x, [x <= 1, (x > 1) & (x <= 2), (x > 2) & (x <= 3)], [lambda x: inner(x), lambda x: middle(x), lambda x: outer(x)], ) return result, window
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

🌐
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%