Module adadjust.functions
Expand source code
from typing import Callable, Union, Optional, Collection
from scipy.optimize import leastsq
import logging
import numpy as np
import tablewriter
import pandas as pd
import matplotlib.pyplot as plt
from colorstylecycler import Cycler
logger = logging.getLogger(__name__)
class Function:
def __init__(self, method: Callable, equation: str):
"""
Mathematical function to fit on some data.
For now, only 1-D functions are supported.
Examples:
>>> from adadjust import Function
>>> # noinspection PyShadowingNames
>>> import numpy as np
>>> # noinspection PyShadowingNames
>>> import matplotlib.pyplot as plt
>>> plt.rcParams.update({"text.usetex": True}) # Needs texlive installed
>>>
>>> nsamples = 1000
>>> a = 0.3
>>> b = -10
>>> xstart = 0
>>> xend = 1
>>> noise = 0.01
>>> x = np.linspace(xstart, xend, nsamples)
>>> y = a * x ** 2 + b + np.random.normal(0, noise, nsamples)
>>>
>>>
>>> def linfunc(xx, p):
>>> return xx * p[0] + p[1]
>>>
>>>
>>> def square(xx, p):
>>> return xx ** 2 * p[0] + p[1]
>>>
>>>
>>> func = Function(linfunc, "$a \\times p[0] + p[1]$")
>>> func2 = Function(square, "$a^2 \\times p[0] + p[1]$")
>>>
>>> params = func.fit(x, y, np.array([0, 0]))[0]
>>> rr = func.compute_rsquared(x, y, params)
>>>
>>> params2 = func2.fit(x, y, np.array([0, 0]))[0]
>>> rr2 = func2.compute_rsquared(x, y, params2)
>>>
>>> table = Function.make_table(
>>> [func, func2], [params, params2], [rr, rr2], caption="Linear and Square fit", path_output="table.pdf"
>>> )
>>> table.compile()
>>> Function.plot(x, [func, func2], [params, params2], y=y, rsquared=[rr, rr2])
>>> plt.gcf().savefig("plot.pdf")
Parameters
----------
method: Callable
The function to fit. It must take as first argument the x on which to compute the function, then
the adjustable parameters in one tuple, then and any number of additionnal arguments.
equation: str
The function's equation in LaTeX, for rendering. Adjustable parameters must be specified through the
synthax 'p[i]'. For example, a linear function's equation would be '$p[0] \\times x + p[1]$'.
"""
self.method = method
self.equation = equation
def __call__(self, *args):
return self.method(*args)
def make_result_equation(self, params: Union[list, tuple, np.ndarray], r: Optional[float] = None) -> str:
"""From a given set of adjustable parameter values, and an optionnal r² value, replaces the 'p[i]' in the
function's equation by their corresponding values in 'params'. If r² is specified, will be appended to the
equation in a new line.
Examples:
>>> from adadjust import Function
>>> def func(x, p):
>>> return p[0] * x + p[1]
>>> f = Function(func, "$p[0] \\times x + p[1]$")
>>> res = f.make_result_equation((1, -2), 0.8)
"\\setlength{\\parindent}{0cm} $1 \\times x - 2$\\\\$r^2=0.8$"
Parameters
----------
params: Union[list, tuple, np.ndarray]
r: Optional[float]
Returns
-------
str
The equation with values instead of 'p[i]'
"""
s = self.equation
for iparam in range(len(params)):
param = params[iparam]
if param < 0:
s = s.replace(f"+ p[{iparam}]", format_x(param))
s = s.replace(f"+p[{iparam}]", format_x(param))
s = s.replace(f"- p[{iparam}]", format_x(float(str(param).replace("-", ""))))
s = s.replace(f"-p[{iparam}]", format_x(float(str(param).replace("-", ""))))
s = s.replace(f"p[{iparam}]", f"({format_x(param)})")
else:
s = s.replace(f"p[{iparam}]", f"{format_x(param)}")
if r is not None:
s = f"{s}\\\\$r^2={r}$"
s = "".join(["\\setlength{\\parindent}{0cm} ", s])
return s
def fit(
self,
x: np.ndarray,
y: np.ndarray,
init: np.ndarray,
yerrup: Optional[np.ndarray] = None,
yerrdown: Optional[np.ndarray] = None,
yerr: Optional[np.ndarray] = None,
args: tuple = (),
**kwargs,
):
"""Adjust the function on 'x' and 'y' by using least square method.
Parameters
----------
x: np.ndarray
The coordinates on which to fit
y: np.ndarray
The results on which to fit
init: np.ndarray
The initial values of the function's parameters
yerrup: Optional[np.ndarray]
The upper error of 'y', used to weight the data points
yerrdown: Optional[np.ndarray]
The lower error of 'y', used to weight the data points
yerr: Optional[np.ndarray]
The error of 'y', used to weight the data points. Replaces yerrup and yerrdown.
args: tuple
Any additionnal arguments to give to self.method
**kwargs
Any additionnal keyword arguments to pass to scipy.optimize.leastsq
Returns
-------
Same as scipy.optimize.leastsq
"""
if yerr is not None and (yerrup is not None or yerrdown is not None):
raise ValueError("If yerr is specified, can not specify yerrup or yerrdown too")
if (yerrup is not None and yerrdown is None) or (yerrdown is not None and yerrup is None):
raise ValueError("If one of yerrup or yerrdown is specified, the other must be too")
if yerr is not None:
yerrup = yerr
yerrdown = -yerr
def my_error(*args_):
yfit = self(x, *args_)
weight = np.ones_like(yfit)
if yerrdown is None:
return (yfit - y) ** 2
weight[yfit > y] = yerrup[yfit > y]
weight[yfit <= y] = yerrdown[yfit <= y]
return (yfit - y) ** 2 / weight ** 2
if len(x) < len(init):
logger.warning("Can not fit a function with less observations than parameters")
return None
if len(x) == len(init):
logger.warning("Fitting a function with the same number of observations than parameters")
results = leastsq(my_error, x0=init, args=args, **kwargs)
return results
def predict(self, x: np.ndarray, params: np.ndarray, *args):
"""Same as calling self(x, params, *args)"""
return self.method(x, params, *args)
def compute_rsquared(self, x: np.ndarray, y: np.ndarray, params: np.ndarray, *args) -> float:
"""Comptue the r² of a fit result.
r² indicates how much better the fitted parameters are compared to simply predicting the means of 'y'. It can be
negative if the fit is worse than predicting the mean. If r²=0, the parameters predict the mean of 'y'. If
r²=1, the fit is perfect (all predicted points perfectly match observations).
Note that if the mean of 'y' also is a perfect fit (i.e, an ohrizontal line), the value of r is not defined for
a division by 0 would occur.
Parameters
----------
x: np.ndarray
The coordinates on which the fit was done
y: np.ndarray
The results on which the fit was done
params: np.ndarray
The fitted values of the function's parameters
*args
additionnal arguments to pass to self.method
Returns
-------
float
r² value
"""
rss = np.sum((y - self(x, params, *args)) ** 2)
tss = np.sum((y - np.mean(y)) ** 2)
rr = 1 - (rss / tss)
return rr
@staticmethod
def make_table(
functions: Collection["Function"],
params: Collection[np.ndarray],
rsquared: Optional[Collection[float]] = None,
**table_kwargs,
) -> tablewriter.TableWriter:
"""Create a TableWriter object representing the fit results of several Function objects.
Parameters
----------
functions: Collection[Function]
Several Functions fitted on the same data, passed in a collection of any kind.
params: Collection[np.ndarray]
The fitted parameters of the Functions.
rsquared: Optional[Collection[float]]
The r²s of the Functions.
table_kwargs
Any additionnal keyword arguments to give to TableWriter
Returns
-------
tablewriter.TableWriter
"""
nparams = max([len(par) for par in params])
# noinspection PyUnresolvedReferences
data = [
[format_x(params[if_][ip], True) if ip < nparams else np.nan for ip in range(len(params[if_]))]
for if_ in range(len(functions))
]
table_g = pd.DataFrame(
columns=[f"param {i}" for i in range(nparams)], index=[f.equation for f in functions], data=data
)
if rsquared is not None:
s = pd.DataFrame(index=table_g.index, data=rsquared, columns=["$r^2$"])
table_g = pd.concat([table_g, s], axis=1)
return tablewriter.TableWriter(data=table_g, **table_kwargs)
@staticmethod
def plot(
x: np.ndarray,
functions: Collection["Function"],
params: Collection[np.ndarray],
y: Optional[np.ndarray] = None,
ax: Optional[plt.Axes] = None,
yerr: Optional[np.ndarray] = None,
xerr: Optional[np.ndarray] = None,
xshow: Optional[np.ndarray] = None,
rsquared: Collection[float] = None,
argss: Collection[tuple] = None,
**plot_kwargs,
) -> plt.Axes:
"""
Plots the result of the fits of several Function.
Parameters
----------
x: np.ndarray
The 'x' on which the fit was done
functions: Collection[Function]
A collection of Functions that were fitted on 'x'
params: Collection[np.ndarray]
The fitter parameters of the Functions
y: Optional[np.ndarray]
The measured 'y' values used in the fit.
ax: Optional[plt.Axes]
The plt.Axes on which to plot. Will use plt.gca() if None.
yerr: Optional[np.ndarray]
The error on y. See plt.errorbar.
xerr: Optional[np.ndarray]
The error on x. See plt.errorbar.
xshow: Optional[np.ndarray]
The values of 'x' on which the fitted function should be plotted. If None, uses 'x'
rsquared: Collection[float]
The r² of the fitted Functions
argss: Collection[tuple]
The additionnal arguments the the fitted Functions
**plot_kwargs
Any keyword arguments to pass to plot methods.
* 'lw' will be used for plotting the Functions (default value = 4)
* 'fmt' or 'marker' will be used for plotting 'y' vs 'x' (default value = "o")
* 's' will be used for plotting 'y' vs 'x' (default value = 10)
* 'label' will define the label of 'y' vs 'x' (default value = "data")
Any other arguments must be valid for plt.scatter, plt.errorbar and plt.plot.
Returns
-------
plt.Axes
"""
if argss is None:
argss = [[] for _ in functions]
if ax is None:
ax = plt.gca()
if rsquared is None:
rsquared = [None for _ in functions]
nitems = len(functions)
fmt = plot_kwargs.get("fmt", "o")
ms = plot_kwargs.get("s", 10)
lw = plot_kwargs.get("lw", 4)
ylabel = plot_kwargs.get("label", "data")
if "fmt" in plot_kwargs:
del plot_kwargs["fmt"]
if "s" in plot_kwargs:
del plot_kwargs["s"]
if "lw" in plot_kwargs:
del plot_kwargs["lw"]
if "label" in plot_kwargs:
del plot_kwargs["label"]
cycler = Cycler(ncurves=nitems, color_start="darkred", color_end="darkblue")
plt.rc("axes", prop_cycle=cycler.cycler)
if y is not None:
if yerr is None and xerr is None:
ax.scatter(x=x, y=y, c="black", marker=fmt, label=ylabel, s=ms, **plot_kwargs)
else:
ax.errorbar(x=x, y=y, yerr=yerr, xerr=xerr, c="black", fmt=fmt, ms=ms, label=ylabel, **plot_kwargs)
if xshow is not None:
x = xshow
for function, param, r, args in zip(functions, params, rsquared, argss):
ax.plot(
x,
function(x, param, *args),
label=function.make_result_equation(param, r),
lw=lw,
**plot_kwargs,
)
ax.legend()
return ax
def format_x(s: Union[float, int], with_dollar: bool = False) -> str:
"""For a given number, will put it in scientific notation if its absolute value is lower than 0.01 and greater than,
1000 using LaTex synthax. If 'with_dollar' is True and if s is in LaTeX synthax, will return $2.0\\cdot 10^{3}$
instead of 2.0\\cdot 10^{3}.
Parameters
----------
s: Union[float, int]
with_dollar: bool
Default to False
Returns
-------
str
"""
if 1000 > abs(s) > 0.01:
xstr = str(round(s, 2))
else:
xstr = "{:.4E}".format(s)
if "E-" in xstr:
lead, tail = xstr.split("E-")
middle = "-"
else:
lead, tail = xstr.split("E")
middle = ""
lead = round(float(lead), 2)
tail = round(float(tail), 2)
if with_dollar:
xstr = ("$\\cdot 10^{" + middle).join([str(lead), str(tail)]) + "}$"
else:
xstr = ("\\cdot 10^{" + middle).join([str(lead), str(tail)]) + "}"
return xstr
Functions
def format_x(s: Union[float, int], with_dollar: bool = False) ‑> str
-
For a given number, will put it in scientific notation if its absolute value is lower than 0.01 and greater than, 1000 using LaTex synthax. If 'with_dollar' is True and if s is in LaTeX synthax, will return $2.0\cdot 10^{3}$ instead of 2.0\cdot 10^{3}.
Parameters
s
:Union[float, int]
with_dollar
:bool
- Default to False
Returns
str
Expand source code
def format_x(s: Union[float, int], with_dollar: bool = False) -> str: """For a given number, will put it in scientific notation if its absolute value is lower than 0.01 and greater than, 1000 using LaTex synthax. If 'with_dollar' is True and if s is in LaTeX synthax, will return $2.0\\cdot 10^{3}$ instead of 2.0\\cdot 10^{3}. Parameters ---------- s: Union[float, int] with_dollar: bool Default to False Returns ------- str """ if 1000 > abs(s) > 0.01: xstr = str(round(s, 2)) else: xstr = "{:.4E}".format(s) if "E-" in xstr: lead, tail = xstr.split("E-") middle = "-" else: lead, tail = xstr.split("E") middle = "" lead = round(float(lead), 2) tail = round(float(tail), 2) if with_dollar: xstr = ("$\\cdot 10^{" + middle).join([str(lead), str(tail)]) + "}$" else: xstr = ("\\cdot 10^{" + middle).join([str(lead), str(tail)]) + "}" return xstr
Classes
class Function (method: Callable, equation: str)
-
Mathematical function to fit on some data. For now, only 1-D functions are supported.
Examples:
>>> from adadjust import Function >>> # noinspection PyShadowingNames >>> import numpy as np >>> # noinspection PyShadowingNames >>> import matplotlib.pyplot as plt >>> plt.rcParams.update({"text.usetex": True}) # Needs texlive installed >>> >>> nsamples = 1000 >>> a = 0.3 >>> b = -10 >>> xstart = 0 >>> xend = 1 >>> noise = 0.01 >>> x = np.linspace(xstart, xend, nsamples) >>> y = a * x ** 2 + b + np.random.normal(0, noise, nsamples) >>> >>> >>> def linfunc(xx, p): >>> return xx * p[0] + p[1] >>> >>> >>> def square(xx, p): >>> return xx ** 2 * p[0] + p[1] >>> >>> >>> func = Function(linfunc, "$a \times p[0] + p[1]$") >>> func2 = Function(square, "$a^2 \times p[0] + p[1]$") >>> >>> params = func.fit(x, y, np.array([0, 0]))[0] >>> rr = func.compute_rsquared(x, y, params) >>> >>> params2 = func2.fit(x, y, np.array([0, 0]))[0] >>> rr2 = func2.compute_rsquared(x, y, params2) >>> >>> table = Function.make_table( >>> [func, func2], [params, params2], [rr, rr2], caption="Linear and Square fit", path_output="table.pdf" >>> ) >>> table.compile() >>> Function.plot(x, [func, func2], [params, params2], y=y, rsquared=[rr, rr2]) >>> plt.gcf().savefig("plot.pdf")
Parameters
method
:Callable
- The function to fit. It must take as first argument the x on which to compute the function, then the adjustable parameters in one tuple, then and any number of additionnal arguments.
equation
:str
- The function's equation in LaTeX, for rendering. Adjustable parameters must be specified through the synthax 'p[i]'. For example, a linear function's equation would be '$p[0] \times x + p[1]$'.
Expand source code
class Function: def __init__(self, method: Callable, equation: str): """ Mathematical function to fit on some data. For now, only 1-D functions are supported. Examples: >>> from adadjust import Function >>> # noinspection PyShadowingNames >>> import numpy as np >>> # noinspection PyShadowingNames >>> import matplotlib.pyplot as plt >>> plt.rcParams.update({"text.usetex": True}) # Needs texlive installed >>> >>> nsamples = 1000 >>> a = 0.3 >>> b = -10 >>> xstart = 0 >>> xend = 1 >>> noise = 0.01 >>> x = np.linspace(xstart, xend, nsamples) >>> y = a * x ** 2 + b + np.random.normal(0, noise, nsamples) >>> >>> >>> def linfunc(xx, p): >>> return xx * p[0] + p[1] >>> >>> >>> def square(xx, p): >>> return xx ** 2 * p[0] + p[1] >>> >>> >>> func = Function(linfunc, "$a \\times p[0] + p[1]$") >>> func2 = Function(square, "$a^2 \\times p[0] + p[1]$") >>> >>> params = func.fit(x, y, np.array([0, 0]))[0] >>> rr = func.compute_rsquared(x, y, params) >>> >>> params2 = func2.fit(x, y, np.array([0, 0]))[0] >>> rr2 = func2.compute_rsquared(x, y, params2) >>> >>> table = Function.make_table( >>> [func, func2], [params, params2], [rr, rr2], caption="Linear and Square fit", path_output="table.pdf" >>> ) >>> table.compile() >>> Function.plot(x, [func, func2], [params, params2], y=y, rsquared=[rr, rr2]) >>> plt.gcf().savefig("plot.pdf") Parameters ---------- method: Callable The function to fit. It must take as first argument the x on which to compute the function, then the adjustable parameters in one tuple, then and any number of additionnal arguments. equation: str The function's equation in LaTeX, for rendering. Adjustable parameters must be specified through the synthax 'p[i]'. For example, a linear function's equation would be '$p[0] \\times x + p[1]$'. """ self.method = method self.equation = equation def __call__(self, *args): return self.method(*args) def make_result_equation(self, params: Union[list, tuple, np.ndarray], r: Optional[float] = None) -> str: """From a given set of adjustable parameter values, and an optionnal r² value, replaces the 'p[i]' in the function's equation by their corresponding values in 'params'. If r² is specified, will be appended to the equation in a new line. Examples: >>> from adadjust import Function >>> def func(x, p): >>> return p[0] * x + p[1] >>> f = Function(func, "$p[0] \\times x + p[1]$") >>> res = f.make_result_equation((1, -2), 0.8) "\\setlength{\\parindent}{0cm} $1 \\times x - 2$\\\\$r^2=0.8$" Parameters ---------- params: Union[list, tuple, np.ndarray] r: Optional[float] Returns ------- str The equation with values instead of 'p[i]' """ s = self.equation for iparam in range(len(params)): param = params[iparam] if param < 0: s = s.replace(f"+ p[{iparam}]", format_x(param)) s = s.replace(f"+p[{iparam}]", format_x(param)) s = s.replace(f"- p[{iparam}]", format_x(float(str(param).replace("-", "")))) s = s.replace(f"-p[{iparam}]", format_x(float(str(param).replace("-", "")))) s = s.replace(f"p[{iparam}]", f"({format_x(param)})") else: s = s.replace(f"p[{iparam}]", f"{format_x(param)}") if r is not None: s = f"{s}\\\\$r^2={r}$" s = "".join(["\\setlength{\\parindent}{0cm} ", s]) return s def fit( self, x: np.ndarray, y: np.ndarray, init: np.ndarray, yerrup: Optional[np.ndarray] = None, yerrdown: Optional[np.ndarray] = None, yerr: Optional[np.ndarray] = None, args: tuple = (), **kwargs, ): """Adjust the function on 'x' and 'y' by using least square method. Parameters ---------- x: np.ndarray The coordinates on which to fit y: np.ndarray The results on which to fit init: np.ndarray The initial values of the function's parameters yerrup: Optional[np.ndarray] The upper error of 'y', used to weight the data points yerrdown: Optional[np.ndarray] The lower error of 'y', used to weight the data points yerr: Optional[np.ndarray] The error of 'y', used to weight the data points. Replaces yerrup and yerrdown. args: tuple Any additionnal arguments to give to self.method **kwargs Any additionnal keyword arguments to pass to scipy.optimize.leastsq Returns ------- Same as scipy.optimize.leastsq """ if yerr is not None and (yerrup is not None or yerrdown is not None): raise ValueError("If yerr is specified, can not specify yerrup or yerrdown too") if (yerrup is not None and yerrdown is None) or (yerrdown is not None and yerrup is None): raise ValueError("If one of yerrup or yerrdown is specified, the other must be too") if yerr is not None: yerrup = yerr yerrdown = -yerr def my_error(*args_): yfit = self(x, *args_) weight = np.ones_like(yfit) if yerrdown is None: return (yfit - y) ** 2 weight[yfit > y] = yerrup[yfit > y] weight[yfit <= y] = yerrdown[yfit <= y] return (yfit - y) ** 2 / weight ** 2 if len(x) < len(init): logger.warning("Can not fit a function with less observations than parameters") return None if len(x) == len(init): logger.warning("Fitting a function with the same number of observations than parameters") results = leastsq(my_error, x0=init, args=args, **kwargs) return results def predict(self, x: np.ndarray, params: np.ndarray, *args): """Same as calling self(x, params, *args)""" return self.method(x, params, *args) def compute_rsquared(self, x: np.ndarray, y: np.ndarray, params: np.ndarray, *args) -> float: """Comptue the r² of a fit result. r² indicates how much better the fitted parameters are compared to simply predicting the means of 'y'. It can be negative if the fit is worse than predicting the mean. If r²=0, the parameters predict the mean of 'y'. If r²=1, the fit is perfect (all predicted points perfectly match observations). Note that if the mean of 'y' also is a perfect fit (i.e, an ohrizontal line), the value of r is not defined for a division by 0 would occur. Parameters ---------- x: np.ndarray The coordinates on which the fit was done y: np.ndarray The results on which the fit was done params: np.ndarray The fitted values of the function's parameters *args additionnal arguments to pass to self.method Returns ------- float r² value """ rss = np.sum((y - self(x, params, *args)) ** 2) tss = np.sum((y - np.mean(y)) ** 2) rr = 1 - (rss / tss) return rr @staticmethod def make_table( functions: Collection["Function"], params: Collection[np.ndarray], rsquared: Optional[Collection[float]] = None, **table_kwargs, ) -> tablewriter.TableWriter: """Create a TableWriter object representing the fit results of several Function objects. Parameters ---------- functions: Collection[Function] Several Functions fitted on the same data, passed in a collection of any kind. params: Collection[np.ndarray] The fitted parameters of the Functions. rsquared: Optional[Collection[float]] The r²s of the Functions. table_kwargs Any additionnal keyword arguments to give to TableWriter Returns ------- tablewriter.TableWriter """ nparams = max([len(par) for par in params]) # noinspection PyUnresolvedReferences data = [ [format_x(params[if_][ip], True) if ip < nparams else np.nan for ip in range(len(params[if_]))] for if_ in range(len(functions)) ] table_g = pd.DataFrame( columns=[f"param {i}" for i in range(nparams)], index=[f.equation for f in functions], data=data ) if rsquared is not None: s = pd.DataFrame(index=table_g.index, data=rsquared, columns=["$r^2$"]) table_g = pd.concat([table_g, s], axis=1) return tablewriter.TableWriter(data=table_g, **table_kwargs) @staticmethod def plot( x: np.ndarray, functions: Collection["Function"], params: Collection[np.ndarray], y: Optional[np.ndarray] = None, ax: Optional[plt.Axes] = None, yerr: Optional[np.ndarray] = None, xerr: Optional[np.ndarray] = None, xshow: Optional[np.ndarray] = None, rsquared: Collection[float] = None, argss: Collection[tuple] = None, **plot_kwargs, ) -> plt.Axes: """ Plots the result of the fits of several Function. Parameters ---------- x: np.ndarray The 'x' on which the fit was done functions: Collection[Function] A collection of Functions that were fitted on 'x' params: Collection[np.ndarray] The fitter parameters of the Functions y: Optional[np.ndarray] The measured 'y' values used in the fit. ax: Optional[plt.Axes] The plt.Axes on which to plot. Will use plt.gca() if None. yerr: Optional[np.ndarray] The error on y. See plt.errorbar. xerr: Optional[np.ndarray] The error on x. See plt.errorbar. xshow: Optional[np.ndarray] The values of 'x' on which the fitted function should be plotted. If None, uses 'x' rsquared: Collection[float] The r² of the fitted Functions argss: Collection[tuple] The additionnal arguments the the fitted Functions **plot_kwargs Any keyword arguments to pass to plot methods. * 'lw' will be used for plotting the Functions (default value = 4) * 'fmt' or 'marker' will be used for plotting 'y' vs 'x' (default value = "o") * 's' will be used for plotting 'y' vs 'x' (default value = 10) * 'label' will define the label of 'y' vs 'x' (default value = "data") Any other arguments must be valid for plt.scatter, plt.errorbar and plt.plot. Returns ------- plt.Axes """ if argss is None: argss = [[] for _ in functions] if ax is None: ax = plt.gca() if rsquared is None: rsquared = [None for _ in functions] nitems = len(functions) fmt = plot_kwargs.get("fmt", "o") ms = plot_kwargs.get("s", 10) lw = plot_kwargs.get("lw", 4) ylabel = plot_kwargs.get("label", "data") if "fmt" in plot_kwargs: del plot_kwargs["fmt"] if "s" in plot_kwargs: del plot_kwargs["s"] if "lw" in plot_kwargs: del plot_kwargs["lw"] if "label" in plot_kwargs: del plot_kwargs["label"] cycler = Cycler(ncurves=nitems, color_start="darkred", color_end="darkblue") plt.rc("axes", prop_cycle=cycler.cycler) if y is not None: if yerr is None and xerr is None: ax.scatter(x=x, y=y, c="black", marker=fmt, label=ylabel, s=ms, **plot_kwargs) else: ax.errorbar(x=x, y=y, yerr=yerr, xerr=xerr, c="black", fmt=fmt, ms=ms, label=ylabel, **plot_kwargs) if xshow is not None: x = xshow for function, param, r, args in zip(functions, params, rsquared, argss): ax.plot( x, function(x, param, *args), label=function.make_result_equation(param, r), lw=lw, **plot_kwargs, ) ax.legend() return ax
Static methods
def make_table(functions: Collection[ForwardRef('Function')], params: Collection[numpy.ndarray], rsquared: Optional[Collection[float]] = None, **table_kwargs) ‑> tablewriter.tablewriter.TableWriter
-
Create a TableWriter object representing the fit results of several Function objects.
Parameters
functions
:Collection[Function]
- Several Functions fitted on the same data, passed in a collection of any kind.
params
:Collection[np.ndarray]
- The fitted parameters of the Functions.
rsquared
:Optional[Collection[float]]
- The r²s of the Functions.
table_kwargs
- Any additionnal keyword arguments to give to TableWriter
Returns
tablewriter.TableWriter
Expand source code
@staticmethod def make_table( functions: Collection["Function"], params: Collection[np.ndarray], rsquared: Optional[Collection[float]] = None, **table_kwargs, ) -> tablewriter.TableWriter: """Create a TableWriter object representing the fit results of several Function objects. Parameters ---------- functions: Collection[Function] Several Functions fitted on the same data, passed in a collection of any kind. params: Collection[np.ndarray] The fitted parameters of the Functions. rsquared: Optional[Collection[float]] The r²s of the Functions. table_kwargs Any additionnal keyword arguments to give to TableWriter Returns ------- tablewriter.TableWriter """ nparams = max([len(par) for par in params]) # noinspection PyUnresolvedReferences data = [ [format_x(params[if_][ip], True) if ip < nparams else np.nan for ip in range(len(params[if_]))] for if_ in range(len(functions)) ] table_g = pd.DataFrame( columns=[f"param {i}" for i in range(nparams)], index=[f.equation for f in functions], data=data ) if rsquared is not None: s = pd.DataFrame(index=table_g.index, data=rsquared, columns=["$r^2$"]) table_g = pd.concat([table_g, s], axis=1) return tablewriter.TableWriter(data=table_g, **table_kwargs)
def plot(x: numpy.ndarray, functions: Collection[ForwardRef('Function')], params: Collection[numpy.ndarray], y: Optional[numpy.ndarray] = None, ax: Optional[matplotlib.axes._axes.Axes] = None, yerr: Optional[numpy.ndarray] = None, xerr: Optional[numpy.ndarray] = None, xshow: Optional[numpy.ndarray] = None, rsquared: Collection[float] = None, argss: Collection[tuple] = None, **plot_kwargs) ‑> matplotlib.axes._axes.Axes
-
Plots the result of the fits of several Function.
Parameters
x
:np.ndarray
- The 'x' on which the fit was done
functions
:Collection[Function]
- A collection of Functions that were fitted on 'x'
params
:Collection[np.ndarray]
- The fitter parameters of the Functions
y
:Optional[np.ndarray]
- The measured 'y' values used in the fit.
ax
:Optional[plt.Axes]
- The plt.Axes on which to plot. Will use plt.gca() if None.
yerr
:Optional[np.ndarray]
- The error on y. See plt.errorbar.
xerr
:Optional[np.ndarray]
- The error on x. See plt.errorbar.
xshow
:Optional[np.ndarray]
- The values of 'x' on which the fitted function should be plotted. If None, uses 'x'
rsquared
:Collection[float]
- The r² of the fitted Functions
argss
:Collection[tuple]
- The additionnal arguments the the fitted Functions
**plot_kwargs
- Any keyword arguments to pass to plot methods. * 'lw' will be used for plotting the Functions (default value = 4) * 'fmt' or 'marker' will be used for plotting 'y' vs 'x' (default value = "o") * 's' will be used for plotting 'y' vs 'x' (default value = 10) * 'label' will define the label of 'y' vs 'x' (default value = "data") Any other arguments must be valid for plt.scatter, plt.errorbar and plt.plot.
Returns
plt.Axes
Expand source code
@staticmethod def plot( x: np.ndarray, functions: Collection["Function"], params: Collection[np.ndarray], y: Optional[np.ndarray] = None, ax: Optional[plt.Axes] = None, yerr: Optional[np.ndarray] = None, xerr: Optional[np.ndarray] = None, xshow: Optional[np.ndarray] = None, rsquared: Collection[float] = None, argss: Collection[tuple] = None, **plot_kwargs, ) -> plt.Axes: """ Plots the result of the fits of several Function. Parameters ---------- x: np.ndarray The 'x' on which the fit was done functions: Collection[Function] A collection of Functions that were fitted on 'x' params: Collection[np.ndarray] The fitter parameters of the Functions y: Optional[np.ndarray] The measured 'y' values used in the fit. ax: Optional[plt.Axes] The plt.Axes on which to plot. Will use plt.gca() if None. yerr: Optional[np.ndarray] The error on y. See plt.errorbar. xerr: Optional[np.ndarray] The error on x. See plt.errorbar. xshow: Optional[np.ndarray] The values of 'x' on which the fitted function should be plotted. If None, uses 'x' rsquared: Collection[float] The r² of the fitted Functions argss: Collection[tuple] The additionnal arguments the the fitted Functions **plot_kwargs Any keyword arguments to pass to plot methods. * 'lw' will be used for plotting the Functions (default value = 4) * 'fmt' or 'marker' will be used for plotting 'y' vs 'x' (default value = "o") * 's' will be used for plotting 'y' vs 'x' (default value = 10) * 'label' will define the label of 'y' vs 'x' (default value = "data") Any other arguments must be valid for plt.scatter, plt.errorbar and plt.plot. Returns ------- plt.Axes """ if argss is None: argss = [[] for _ in functions] if ax is None: ax = plt.gca() if rsquared is None: rsquared = [None for _ in functions] nitems = len(functions) fmt = plot_kwargs.get("fmt", "o") ms = plot_kwargs.get("s", 10) lw = plot_kwargs.get("lw", 4) ylabel = plot_kwargs.get("label", "data") if "fmt" in plot_kwargs: del plot_kwargs["fmt"] if "s" in plot_kwargs: del plot_kwargs["s"] if "lw" in plot_kwargs: del plot_kwargs["lw"] if "label" in plot_kwargs: del plot_kwargs["label"] cycler = Cycler(ncurves=nitems, color_start="darkred", color_end="darkblue") plt.rc("axes", prop_cycle=cycler.cycler) if y is not None: if yerr is None and xerr is None: ax.scatter(x=x, y=y, c="black", marker=fmt, label=ylabel, s=ms, **plot_kwargs) else: ax.errorbar(x=x, y=y, yerr=yerr, xerr=xerr, c="black", fmt=fmt, ms=ms, label=ylabel, **plot_kwargs) if xshow is not None: x = xshow for function, param, r, args in zip(functions, params, rsquared, argss): ax.plot( x, function(x, param, *args), label=function.make_result_equation(param, r), lw=lw, **plot_kwargs, ) ax.legend() return ax
Methods
def compute_rsquared(self, x: numpy.ndarray, y: numpy.ndarray, params: numpy.ndarray, *args) ‑> float
-
Comptue the r² of a fit result.
r² indicates how much better the fitted parameters are compared to simply predicting the means of 'y'. It can be negative if the fit is worse than predicting the mean. If r²=0, the parameters predict the mean of 'y'. If r²=1, the fit is perfect (all predicted points perfectly match observations).
Note that if the mean of 'y' also is a perfect fit (i.e, an ohrizontal line), the value of r is not defined for a division by 0 would occur.
Parameters
x
:np.ndarray
- The coordinates on which the fit was done
y
:np.ndarray
- The results on which the fit was done
params
:np.ndarray
- The fitted values of the function's parameters
*args
- additionnal arguments to pass to self.method
Returns
float
- r² value
Expand source code
def compute_rsquared(self, x: np.ndarray, y: np.ndarray, params: np.ndarray, *args) -> float: """Comptue the r² of a fit result. r² indicates how much better the fitted parameters are compared to simply predicting the means of 'y'. It can be negative if the fit is worse than predicting the mean. If r²=0, the parameters predict the mean of 'y'. If r²=1, the fit is perfect (all predicted points perfectly match observations). Note that if the mean of 'y' also is a perfect fit (i.e, an ohrizontal line), the value of r is not defined for a division by 0 would occur. Parameters ---------- x: np.ndarray The coordinates on which the fit was done y: np.ndarray The results on which the fit was done params: np.ndarray The fitted values of the function's parameters *args additionnal arguments to pass to self.method Returns ------- float r² value """ rss = np.sum((y - self(x, params, *args)) ** 2) tss = np.sum((y - np.mean(y)) ** 2) rr = 1 - (rss / tss) return rr
def fit(self, x: numpy.ndarray, y: numpy.ndarray, init: numpy.ndarray, yerrup: Optional[numpy.ndarray] = None, yerrdown: Optional[numpy.ndarray] = None, yerr: Optional[numpy.ndarray] = None, args: tuple = (), **kwargs)
-
Adjust the function on 'x' and 'y' by using least square method.
Parameters
x
:np.ndarray
- The coordinates on which to fit
y
:np.ndarray
- The results on which to fit
init
:np.ndarray
- The initial values of the function's parameters
yerrup
:Optional[np.ndarray]
- The upper error of 'y', used to weight the data points
yerrdown
:Optional[np.ndarray]
- The lower error of 'y', used to weight the data points
yerr
:Optional[np.ndarray]
- The error of 'y', used to weight the data points. Replaces yerrup and yerrdown.
args
:tuple
- Any additionnal arguments to give to self.method
**kwargs
- Any additionnal keyword arguments to pass to scipy.optimize.leastsq
Returns
Same as scipy.optimize.leastsq
Expand source code
def fit( self, x: np.ndarray, y: np.ndarray, init: np.ndarray, yerrup: Optional[np.ndarray] = None, yerrdown: Optional[np.ndarray] = None, yerr: Optional[np.ndarray] = None, args: tuple = (), **kwargs, ): """Adjust the function on 'x' and 'y' by using least square method. Parameters ---------- x: np.ndarray The coordinates on which to fit y: np.ndarray The results on which to fit init: np.ndarray The initial values of the function's parameters yerrup: Optional[np.ndarray] The upper error of 'y', used to weight the data points yerrdown: Optional[np.ndarray] The lower error of 'y', used to weight the data points yerr: Optional[np.ndarray] The error of 'y', used to weight the data points. Replaces yerrup and yerrdown. args: tuple Any additionnal arguments to give to self.method **kwargs Any additionnal keyword arguments to pass to scipy.optimize.leastsq Returns ------- Same as scipy.optimize.leastsq """ if yerr is not None and (yerrup is not None or yerrdown is not None): raise ValueError("If yerr is specified, can not specify yerrup or yerrdown too") if (yerrup is not None and yerrdown is None) or (yerrdown is not None and yerrup is None): raise ValueError("If one of yerrup or yerrdown is specified, the other must be too") if yerr is not None: yerrup = yerr yerrdown = -yerr def my_error(*args_): yfit = self(x, *args_) weight = np.ones_like(yfit) if yerrdown is None: return (yfit - y) ** 2 weight[yfit > y] = yerrup[yfit > y] weight[yfit <= y] = yerrdown[yfit <= y] return (yfit - y) ** 2 / weight ** 2 if len(x) < len(init): logger.warning("Can not fit a function with less observations than parameters") return None if len(x) == len(init): logger.warning("Fitting a function with the same number of observations than parameters") results = leastsq(my_error, x0=init, args=args, **kwargs) return results
def make_result_equation(self, params: Union[list, tuple, numpy.ndarray], r: Optional[float] = None) ‑> str
-
From a given set of adjustable parameter values, and an optionnal r² value, replaces the 'p[i]' in the function's equation by their corresponding values in 'params'. If r² is specified, will be appended to the equation in a new line.
Examples:
>>> from adadjust import Function >>> def func(x, p): >>> return p[0] * x + p[1] >>> f = Function(func, "$p[0] \times x + p[1]$") >>> res = f.make_result_equation((1, -2), 0.8) "\setlength{\parindent}{0cm} $1 \times x - 2$\\$r^2=0.8$"
Parameters
params
:Union[list, tuple, np.ndarray]
r
:Optional[float]
Returns
str
- The equation with values instead of 'p[i]'
Expand source code
def make_result_equation(self, params: Union[list, tuple, np.ndarray], r: Optional[float] = None) -> str: """From a given set of adjustable parameter values, and an optionnal r² value, replaces the 'p[i]' in the function's equation by their corresponding values in 'params'. If r² is specified, will be appended to the equation in a new line. Examples: >>> from adadjust import Function >>> def func(x, p): >>> return p[0] * x + p[1] >>> f = Function(func, "$p[0] \\times x + p[1]$") >>> res = f.make_result_equation((1, -2), 0.8) "\\setlength{\\parindent}{0cm} $1 \\times x - 2$\\\\$r^2=0.8$" Parameters ---------- params: Union[list, tuple, np.ndarray] r: Optional[float] Returns ------- str The equation with values instead of 'p[i]' """ s = self.equation for iparam in range(len(params)): param = params[iparam] if param < 0: s = s.replace(f"+ p[{iparam}]", format_x(param)) s = s.replace(f"+p[{iparam}]", format_x(param)) s = s.replace(f"- p[{iparam}]", format_x(float(str(param).replace("-", "")))) s = s.replace(f"-p[{iparam}]", format_x(float(str(param).replace("-", "")))) s = s.replace(f"p[{iparam}]", f"({format_x(param)})") else: s = s.replace(f"p[{iparam}]", f"{format_x(param)}") if r is not None: s = f"{s}\\\\$r^2={r}$" s = "".join(["\\setlength{\\parindent}{0cm} ", s]) return s
def predict(self, x: numpy.ndarray, params: numpy.ndarray, *args)
-
Same as calling self(x, params, *args)
Expand source code
def predict(self, x: np.ndarray, params: np.ndarray, *args): """Same as calling self(x, params, *args)""" return self.method(x, params, *args)