EXP_DECAY
Overview
The EXP_DECAY function fits exponential decay models to observed data using non-linear least squares regression. Exponential decay describes processes where a quantity decreases at a rate proportional to its current value, commonly observed in radioactive decay, chemical reactions, pharmacokinetics, and signal attenuation.
This implementation provides eleven different exponential decay model variants, ranging from simple single-parameter models to complex multi-phase decay with offsets and delays. The function uses scipy.optimize.curve_fit from the SciPy library to perform the optimization, which internally applies the Levenberg-Marquardt algorithm for unconstrained problems or trust region reflective methods when parameter bounds are specified.
The basic exponential decay model follows the form:
y(x) = A \cdot e^{-\lambda x}
where A is the initial amplitude and \lambda is the decay constant. More complex variants include offset terms (y_0), multiple decay phases with independent time constants (t_1, t_2, t_3), and delayed onset parameters (x_0) to model phenomena like drug absorption-elimination kinetics or multi-component fluorescence decay.
The function returns fitted parameter values along with their standard errors, which are computed from the covariance matrix of the fit. The standard error for each parameter is calculated as the square root of the corresponding diagonal element: \sigma_i = \sqrt{\text{pcov}_{ii}}. Large standard errors or ill-conditioned covariance matrices may indicate overfitting or insufficient data for the chosen model complexity.
For more information on exponential decay in physical systems, see the Wikipedia article on exponential decay. The SciPy source code is available on GitHub.
This example function is provided as-is without any representation of accuracy.
Excel Usage
=EXP_DECAY(xdata, ydata, exp_decay_model)
xdata(list[list], required): The xdata valueydata(list[list], required): The ydata valueexp_decay_model(str, required): The exp_decay_model value
Returns (list[list]): 2D list [param_names, fitted_values, std_errors], or error string.
Examples
Example 1: Demo case 1
Inputs:
| exp_decay_model | xdata | ydata |
|---|---|---|
| simple_exp_decay | 0.01 | 0.982529587940258 |
| 2.0075 | 0.01 | |
| 4.005 | 0.012605950927158065 | |
| 6.0024999999999995 | 0.029604034326789003 | |
| 8 | 0.01 |
Excel formula:
=EXP_DECAY("simple_exp_decay", {0.01;2.0075;4.005;6.0024999999999995;8}, {0.982529587940258;0.01;0.012605950927158065;0.029604034326789003;0.01})
Expected output:
"non-error"
Example 2: Demo case 2
Inputs:
| exp_decay_model | xdata | ydata |
|---|---|---|
| base_power_decay | 0.1 | 1.0072745882786605 |
| 1.3250000000000002 | 1.0661188412899587 | |
| 2.5500000000000003 | 1.1355938144237552 | |
| 3.7750000000000004 | 1.209544427004773 | |
| 5 | 1.2751578987045502 |
Excel formula:
=EXP_DECAY("base_power_decay", {0.1;1.3250000000000002;2.5500000000000003;3.7750000000000004;5}, {1.0072745882786605;1.0661188412899587;1.1355938144237552;1.209544427004773;1.2751578987045502})
Expected output:
"non-error"
Example 3: Demo case 3
Inputs:
| exp_decay_model | xdata | ydata |
|---|---|---|
| shifted_exp_growth | 0.01 | 1.8988905273423975 |
| 2.0075 | 0.01 | |
| 4.005 | 5.899693224624839 | |
| 6.0024999999999995 | 31.479307137051748 | |
| 8 | 189.70156134696128 |
Excel formula:
=EXP_DECAY("shifted_exp_growth", {0.01;2.0075;4.005;6.0024999999999995;8}, {1.8988905273423975;0.01;5.899693224624839;31.479307137051748;189.70156134696128})
Expected output:
"non-error"
Example 4: Demo case 4
Inputs:
| exp_decay_model | xdata | ydata |
|---|---|---|
| base_log_scaled_decay | 0.1 | 0.567970362843519 |
| 1.3250000000000002 | 0.2694032123341639 | |
| 2.5500000000000003 | 0.13646302107980088 | |
| 3.7750000000000004 | 0.07738260035919173 | |
| 5 | 0.02781278646057439 |
Excel formula:
=EXP_DECAY("base_log_scaled_decay", {0.1;1.3250000000000002;2.5500000000000003;3.7750000000000004;5}, {0.567970362843519;0.2694032123341639;0.13646302107980088;0.07738260035919173;0.02781278646057439})
Expected output:
"non-error"
Example 5: Demo case 5
Inputs:
| exp_decay_model | xdata | ydata |
|---|---|---|
| single_phase_decay_with_offset | 0.01 | 2.7639133322218 |
| 2.0075 | 1.8425614042259757 | |
| 4.005 | 1.269484248440239 | |
| 6.0024999999999995 | 0.8971107943728295 | |
| 8 | 0.5549576278757673 |
Excel formula:
=EXP_DECAY("single_phase_decay_with_offset", {0.01;2.0075;4.005;6.0024999999999995;8}, {2.7639133322218;1.8425614042259757;1.269484248440239;0.8971107943728295;0.5549576278757673})
Expected output:
"non-error"
Example 6: Demo case 6
Inputs:
| exp_decay_model | xdata | ydata |
|---|---|---|
| double_phase_decay_with_offset | 0.01 | 5.5278266644436 |
| 2.0075 | 3.6851228084519514 | |
| 4.005 | 2.538968496880478 | |
| 6.0024999999999995 | 1.794221588745659 | |
| 8 | 1.1099152557515346 |
Excel formula:
=EXP_DECAY("double_phase_decay_with_offset", {0.01;2.0075;4.005;6.0024999999999995;8}, {5.5278266644436;3.6851228084519514;2.538968496880478;1.794221588745659;1.1099152557515346})
Expected output:
"non-error"
Example 7: Demo case 7
Inputs:
| exp_decay_model | xdata | ydata |
|---|---|---|
| triple_phase_decay_with_offset | 0.01 | 8.2917399966654 |
| 2.0075 | 5.5276842126779275 | |
| 4.005 | 3.808452745320717 | |
| 6.0024999999999995 | 2.6913323831184885 | |
| 8 | 1.664872883627302 |
Excel formula:
=EXP_DECAY("triple_phase_decay_with_offset", {0.01;2.0075;4.005;6.0024999999999995;8}, {8.2917399966654;5.5276842126779275;3.808452745320717;2.6913323831184885;1.664872883627302})
Expected output:
"non-error"
Example 8: Demo case 8
Inputs:
| exp_decay_model | xdata | ydata |
|---|---|---|
| delayed_single_phase_decay | 0.01 | 2.7639133322218 |
| 2.0075 | 1.8425614042259757 | |
| 4.005 | 1.269484248440239 | |
| 6.0024999999999995 | 0.8971107943728295 | |
| 8 | 0.5549576278757673 |
Excel formula:
=EXP_DECAY("delayed_single_phase_decay", {0.01;2.0075;4.005;6.0024999999999995;8}, {2.7639133322218;1.8425614042259757;1.269484248440239;0.8971107943728295;0.5549576278757673})
Expected output:
"non-error"
Example 9: Demo case 9
Inputs:
| exp_decay_model | xdata | ydata |
|---|---|---|
| delayed_double_phase_decay | 0.01 | 5.5278266644436 |
| 2.0075 | 3.6851228084519514 | |
| 4.005 | 2.538968496880478 | |
| 6.0024999999999995 | 1.794221588745659 | |
| 8 | 1.1099152557515346 |
Excel formula:
=EXP_DECAY("delayed_double_phase_decay", {0.01;2.0075;4.005;6.0024999999999995;8}, {5.5278266644436;3.6851228084519514;2.538968496880478;1.794221588745659;1.1099152557515346})
Expected output:
"non-error"
Example 10: Demo case 10
Inputs:
| exp_decay_model | xdata | ydata |
|---|---|---|
| delayed_triple_phase_decay | 0.5 | 6 |
| 1.5 | 3.8487584 | |
| 2.5 | 2.75886454 | |
| 3.5 | 2.16391676 | |
| 4.5 | 1.80995328 | |
| 5.5 | 1.58067112 | |
| 6.5 | 1.4210089 | |
| 7.5 | 1.30359124 | |
| 8.5 | 1.2138856 | |
| 9.5 | 1.14355541 |
Excel formula:
=EXP_DECAY("delayed_triple_phase_decay", {0.5;1.5;2.5;3.5;4.5;5.5;6.5;7.5;8.5;9.5}, {6;3.8487584;2.75886454;2.16391676;1.80995328;1.58067112;1.4210089;1.30359124;1.2138856;1.14355541})
Expected output:
"non-error"
Example 11: Demo case 11
Inputs:
| exp_decay_model | xdata | ydata |
|---|---|---|
| self_scaling_exp_decay | 0.01 | 2.70195636683571 |
| 2.0075 | 0.01 | |
| 4.005 | 0.03466636504968468 | |
| 6.0024999999999995 | 0.08141109439866974 | |
| 8 | 0.01 |
Excel formula:
=EXP_DECAY("self_scaling_exp_decay", {0.01;2.0075;4.005;6.0024999999999995;8}, {2.70195636683571;0.01;0.03466636504968468;0.08141109439866974;0.01})
Expected output:
"non-error"
Python Code
import numpy as np
from scipy.optimize import curve_fit as scipy_curve_fit
import math
def exp_decay(xdata, ydata, exp_decay_model):
"""
Fits exp_decay models to data using scipy.optimize.curve_fit. See https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.curve_fit.html for details.
See: https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.curve_fit.html
This example function is provided as-is without any representation of accuracy.
Args:
xdata (list[list]): The xdata value
ydata (list[list]): The ydata value
exp_decay_model (str): The exp_decay_model value Valid options: Simple Exp Decay, Base Power Decay, Shifted Exp Growth, Base Log Scaled Decay, Single Phase Decay With Offset, Double Phase Decay With Offset, Triple Phase Decay With Offset, Delayed Single Phase Decay, Delayed Double Phase Decay, Delayed Triple Phase Decay, Self Scaling Exp Decay.
Returns:
list[list]: 2D list [param_names, fitted_values, std_errors], or error string.
"""
def _validate_data(xdata, ydata):
"""Validate and convert both xdata and ydata to numpy arrays."""
for name, arg in [("xdata", xdata), ("ydata", ydata)]:
if not isinstance(arg, list) or len(arg) < 2:
raise ValueError(f"{name}: must be a 2D list with at least two rows")
vals = []
for i, row in enumerate(arg):
if not isinstance(row, list) or len(row) == 0:
raise ValueError(f"{name} row {i}: must be a non-empty list")
try:
vals.append(float(row[0]))
except Exception:
raise ValueError(f"{name} row {i}: non-numeric value")
if name == "xdata":
x_arr = np.asarray(vals, dtype=np.float64)
else:
y_arr = np.asarray(vals, dtype=np.float64)
if x_arr.shape[0] != y_arr.shape[0]:
raise ValueError("xdata and ydata must have the same number of rows")
return x_arr, y_arr
# Model definitions dictionary
models = {
'simple_exp_decay': {
'params': ['A'],
'model': lambda x, A: np.exp(-A * x),
'guess': lambda xa, ya: (1.0,),
'bounds': (0.0, np.inf),
},
'base_power_decay': {
'params': ['B'],
'model': lambda x, B: np.power(B, x),
'guess': lambda xa, ya: (0.9,),
'bounds': (0.0, np.inf),
},
'shifted_exp_growth': {
'params': ['A'],
'model': lambda x, A: np.exp(x - A),
'guess': lambda xa, ya: (float(np.log(np.max(ya)) if np.all(ya > 0) and np.max(ya) > 0 else 0.0),),
},
'base_log_scaled_decay': {
'params': ['B'],
'model': lambda x, B: -np.log(B) * np.power(B, x),
'guess': lambda xa, ya: (0.5,),
'bounds': (0.0, 1.0),
},
'single_phase_decay_with_offset': {
'params': ['y0', 'A', 't'],
'model': lambda x, y0, A, t: y0 + A * np.exp(-x / t),
'guess': lambda xa, ya: (float(np.min(ya)), float(np.ptp(ya) if np.ptp(ya) else 1.0), 1.0),
'bounds': ([-np.inf, -np.inf, 0.0], np.inf),
},
'double_phase_decay_with_offset': {
'params': ['y0', 'A1', 't1', 'A2', 't2'],
'model': lambda x, y0, A1, t1, A2, t2: y0 + A1 * np.exp(-x / t1) + A2 * np.exp(-x / t2),
'guess': lambda xa, ya: (float(np.min(ya)), float(np.ptp(ya) / 2 if np.ptp(ya) else 0.5), 1.0, float(np.ptp(ya) / 2 if np.ptp(ya) else 0.5), 5.0),
'bounds': ([-np.inf, -np.inf, 0.0, -np.inf, 0.0], np.inf),
},
'triple_phase_decay_with_offset': {
'params': ['y0', 'A1', 't1', 'A2', 't2', 'A3', 't3'],
'model': lambda x, y0, A1, t1, A2, t2, A3, t3: y0 + A1 * np.exp(-x/t1) + A2 * np.exp(-x/t2) + A3 * np.exp(-x/t3),
'guess': lambda xa, ya: (0.0, float(max(ya))/3, 1.0, float(max(ya))/3, 2.0, float(max(ya))/3, 10.0),
'bounds': ([-np.inf, -np.inf, 0.0, -np.inf, 0.0, -np.inf, 0.0], np.inf),
},
'delayed_single_phase_decay': {
'params': ['y0', 'x0', 'A1', 't1'],
'model': lambda x, y0, x0, A1, t1: y0 + A1 * np.exp(-(x - x0) / t1),
'guess': lambda xa, ya: (float(np.min(ya)), float(np.min(xa)), float(np.ptp(ya) if np.ptp(ya) else 1.0), 1.0),
'bounds': ([-np.inf, -np.inf, -np.inf, 0.0], np.inf),
},
'delayed_double_phase_decay': {
'params': ['y0', 'x0', 'A1', 't1', 'A2', 't2'],
'model': lambda x, y0, x0, A1, t1, A2, t2: y0 + A1 * np.exp(-(x - x0) / t1) + A2 * np.exp(-(x - x0) / t2),
'guess': lambda xa, ya: (float(np.min(ya)), float(np.min(xa)), float(np.ptp(ya) if np.ptp(ya) else 1.0), 1.0, float(np.ptp(ya) if np.ptp(ya) else 0.5), 5.0),
'bounds': ([-np.inf, -np.inf, -np.inf, 0.0, -np.inf, 0.0], np.inf),
},
'delayed_triple_phase_decay': {
'params': ['y0', 'x0', 'A1', 't1', 'A2', 't2', 'A3', 't3'],
'model': lambda x, y0, x0, A1, t1, A2, t2, A3, t3: y0 + A1 * np.exp(-(x - x0) / t1) + A2 * np.exp(-(x - x0) / t2) + A3 * np.exp(-(x - x0) / t3),
'guess': lambda xa, ya: (float(np.min(ya)), float(np.min(xa)), float(np.ptp(ya) if np.ptp(ya) else 1.0), 0.5, float(np.ptp(ya) if np.ptp(ya) else 0.5), 2.0, float(np.ptp(ya) if np.ptp(ya) else 0.25), 5.0),
'bounds': ([-np.inf, -np.inf, -np.inf, 0.0, -np.inf, 0.0, -np.inf, 0.0], np.inf),
},
'self_scaling_exp_decay': {
'params': ['A'],
'model': lambda x, A: A * np.exp(-A * x),
'guess': lambda xa, ya: (1.0,),
'bounds': (0.0, np.inf),
}
}
# Validate model parameter
if exp_decay_model not in models:
return f"Invalid model: {str(exp_decay_model)}. Valid models are: {', '.join(models.keys())}"
model_info = models[exp_decay_model]
# Validate and convert input data
try:
x_arr, y_arr = _validate_data(xdata, ydata)
except ValueError as e:
return f"Invalid input: {e}"
# Perform curve fitting
try:
p0 = model_info['guess'](x_arr, y_arr)
bounds = model_info.get('bounds', (-np.inf, np.inf))
if bounds == (-np.inf, np.inf):
popt, pcov = scipy_curve_fit(model_info['model'], x_arr, y_arr, p0=p0, maxfev=10000)
else:
popt, pcov = scipy_curve_fit(model_info['model'], x_arr, y_arr, p0=p0, bounds=bounds, maxfev=10000)
fitted_vals = [float(v) for v in popt]
for v in fitted_vals:
if math.isnan(v) or math.isinf(v):
return "Fitting produced invalid numeric values (NaN or inf)."
except ValueError as e:
return f"Initial guess error: {e}"
except Exception as e:
return f"curve_fit error: {e}"
# Calculate standard errors
std_errors = None
try:
if pcov is not None and np.isfinite(pcov).all():
std_errors = [float(v) for v in np.sqrt(np.diag(pcov))]
except Exception:
pass
return [model_info['params'], fitted_vals, std_errors] if std_errors else [model_info['params'], fitted_vals]