DOSE_RESPONSE
Overview
The DOSE_RESPONSE function fits a variety of dose-response models to experimental data, commonly used in pharmacology, toxicology, and biochemistry to characterize how biological systems respond to varying concentrations of drugs, chemicals, or other stimuli. A dose-response relationship describes the magnitude of a response as a function of exposure to a stimulus, typically producing a sigmoidal curve when plotted on a logarithmic dose scale.
This implementation uses scipy.optimize.curve_fit from the SciPy library to perform non-linear least squares fitting. The function supports ten different models:
- Standard Dose Response Curve: Classic sigmoidal model with asymptotes, midpoint, and slope parameters
- Hill Cooperative Binding: Models cooperative binding based on the Hill equation
- Hill Binding With Baseline: Hill equation extended with start and end baseline values
- Four Parameter Logistic (4PL): Standard logistic model widely used for immunoassay analysis
- Five Parameter Logistic (5PL): Adds asymmetry parameter to handle non-symmetric curves
- Biphasic Dose Response: Models systems with two distinct response phases
- Seven Parameter Biphasic Sigmoid: Complex biphasic model with independent phase parameters
- Biphasic Hill Activation Inhibition: Models activation at low doses and inhibition at high doses
- Generalized Hyperbola: Flexible hyperbolic response model
- Single Site Binding Saturation: Simple Michaelis-Menten-like saturation kinetics
The core Hill equation, which underlies several of these models, is expressed as:
E = \frac{E_{max} \cdot [A]^n}{EC_{50}^n + [A]^n}
where E is the response, [A] is the drug concentration, EC_{50} is the concentration producing 50% maximal response, and n is the Hill coefficient describing cooperativity. The Hill coefficient indicates the steepness of the curve—values greater than 1 indicate positive cooperativity, while values less than 1 suggest negative cooperativity.
The function returns parameter names, fitted values, and standard errors derived from the covariance matrix. These metrics are essential for determining potency measures (EC50, IC50) and efficacy parameters commonly reported in drug development and toxicological assessment.
This example function is provided as-is without any representation of accuracy.
Excel Usage
=DOSE_RESPONSE(xdata, ydata, dose_response_model)
xdata(list[list], required): The xdata valueydata(list[list], required): The ydata valuedose_response_model(str, required): The dose_response_model value
Returns (list[list]): 2D list [param_names, fitted_values, std_errors], or error string.
Examples
Example 1: Demo case 1
Inputs:
| dose_response_model | xdata | ydata |
|---|---|---|
| standard_dose_response_curve | 0.1 | 2.75 |
| 1.3250000000000002 | 2.75 | |
| 2.5500000000000003 | 2.75 | |
| 3.7750000000000004 | 2.75 | |
| 5 | 2.75 |
Excel formula:
=DOSE_RESPONSE("standard_dose_response_curve", {0.1;1.3250000000000002;2.5500000000000003;3.7750000000000004;5}, {2.75;2.75;2.75;2.75;2.75})
Expected output:
"non-error"
Example 2: Demo case 2
Inputs:
| dose_response_model | xdata | ydata |
|---|---|---|
| hill_cooperative_binding | 0.1 | 0.01273095510527576 |
| 1.3250000000000002 | 0.01 | |
| 2.5500000000000003 | 0.10116243219286802 | |
| 3.7750000000000004 | 0.5533417003379951 | |
| 5 | 1.3344595337609784 |
Excel formula:
=DOSE_RESPONSE("hill_cooperative_binding", {0.1;1.3250000000000002;2.5500000000000003;3.7750000000000004;5}, {0.01273095510527576;0.01;0.10116243219286802;0.5533417003379951;1.3344595337609784})
Expected output:
"non-error"
Example 3: Demo case 3
Inputs:
| dose_response_model | xdata | ydata |
|---|---|---|
| hill_binding_with_baseline | 0.1 | 2.7606477079062306 |
| 1.3250000000000002 | 2.7497078533275006 | |
| 2.5500000000000003 | 2.834608579652217 | |
| 3.7750000000000004 | 3.2127948766463232 | |
| 5 | 3.8660934282364545 |
Excel formula:
=DOSE_RESPONSE("hill_binding_with_baseline", {0.1;1.3250000000000002;2.5500000000000003;3.7750000000000004;5}, {2.7606477079062306;2.7497078533275006;2.834608579652217;3.2127948766463232;3.8660934282364545})
Expected output:
"non-error"
Example 4: Demo case 4
Inputs:
| dose_response_model | xdata | ydata |
|---|---|---|
| four_parameter_logistic | 0.1 | 2.75 |
| 1.3250000000000002 | 2.75 | |
| 2.5500000000000003 | 2.75 | |
| 3.7750000000000004 | 2.75 | |
| 5 | 2.75 |
Excel formula:
=DOSE_RESPONSE("four_parameter_logistic", {0.1;1.3250000000000002;2.5500000000000003;3.7750000000000004;5}, {2.75;2.75;2.75;2.75;2.75})
Expected output:
"non-error"
Example 5: Demo case 5
Inputs:
| dose_response_model | xdata | ydata |
|---|---|---|
| five_parameter_logistic_asymmetric | 0.1 | 2.75 |
| 1.3250000000000002 | 2.75 | |
| 2.5500000000000003 | 2.75 | |
| 3.7750000000000004 | 2.75 | |
| 5 | 2.75 |
Excel formula:
=DOSE_RESPONSE("five_parameter_logistic_asymmetric", {0.1;1.3250000000000002;2.5500000000000003;3.7750000000000004;5}, {2.75;2.75;2.75;2.75;2.75})
Expected output:
"non-error"
Example 6: Demo case 6
Inputs:
| dose_response_model | xdata | ydata |
|---|---|---|
| biphasic_dose_response_dual_effect | 0.1 | 2.75 |
| 1.3250000000000002 | 2.75 | |
| 2.5500000000000003 | 2.75 | |
| 3.7750000000000004 | 2.75 | |
| 5 | 2.75 |
Excel formula:
=DOSE_RESPONSE("biphasic_dose_response_dual_effect", {0.1;1.3250000000000002;2.5500000000000003;3.7750000000000004;5}, {2.75;2.75;2.75;2.75;2.75})
Expected output:
"non-error"
Example 7: Demo case 7
Inputs:
| dose_response_model | xdata | ydata |
|---|---|---|
| seven_param_biphasic_sigmoid | 0.1 | 2.1495589080728332 |
| 0.8 | 1.9388050696676988 | |
| 1.5 | 1.3007784892461107 | |
| 2.2 | 0.6666853174024305 | |
| 2.9 | 0.4872847952010856 | |
| 3.6 | 0.6468199412679169 | |
| 4.3 | 1.5337915918649858 | |
| 5 | 2.877898193217767 | |
| 5.7 | 3.391432399977618 |
Excel formula:
=DOSE_RESPONSE("seven_param_biphasic_sigmoid", {0.1;0.8;1.5;2.2;2.9;3.6;4.3;5;5.7}, {2.1495589080728332;1.9388050696676988;1.3007784892461107;0.6666853174024305;0.4872847952010856;0.6468199412679169;1.5337915918649858;2.877898193217767;3.391432399977618})
Expected output:
"non-error"
Example 8: Demo case 8
Inputs:
| dose_response_model | xdata | ydata |
|---|---|---|
| biphasic_hill_activation_inhibition | 0.1 | 0.027563250575314988 |
| 1.3250000000000002 | 0.5895242843046955 | |
| 2.5500000000000003 | 2.229390151955633 | |
| 3.7750000000000004 | 2.9772003029928795 | |
| 5 | 2.1565911679591983 |
Excel formula:
=DOSE_RESPONSE("biphasic_hill_activation_inhibition", {0.1;1.3250000000000002;2.5500000000000003;3.7750000000000004;5}, {0.027563250575314988;0.5895242843046955;2.229390151955633;2.9772003029928795;2.1565911679591983})
Expected output:
"non-error"
Example 9: Demo case 9
Inputs:
| dose_response_model | xdata | ydata |
|---|---|---|
| generalized_hyperbola | 0.1 | 1.736625907103898 |
| 1.3250000000000002 | 1.9296383019658383 | |
| 2.5500000000000003 | 2.004903695261842 | |
| 3.7750000000000004 | 2.042460009056909 | |
| 5 | 2.083675785134604 |
Excel formula:
=DOSE_RESPONSE("generalized_hyperbola", {0.1;1.3250000000000002;2.5500000000000003;3.7750000000000004;5}, {1.736625907103898;1.9296383019658383;2.004903695261842;2.042460009056909;2.083675785134604})
Expected output:
"non-error"
Example 10: Demo case 10
Inputs:
| dose_response_model | xdata | ydata |
|---|---|---|
| single_site_binding_saturation | 0.1 | 0.06498008539702818 |
| 1.3250000000000002 | 0.5683446788689119 | |
| 2.5500000000000003 | 0.9377997088396869 | |
| 3.7750000000000004 | 1.211858535427366 | |
| 5 | 1.3626993822182172 |
Excel formula:
=DOSE_RESPONSE("single_site_binding_saturation", {0.1;1.3250000000000002;2.5500000000000003;3.7750000000000004;5}, {0.06498008539702818;0.5683446788689119;0.9377997088396869;1.211858535427366;1.3626993822182172})
Expected output:
"non-error"
Python Code
import numpy as np
from scipy.optimize import curve_fit as scipy_curve_fit
import math
def dose_response(xdata, ydata, dose_response_model):
"""
Fits dose_response 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
dose_response_model (str): The dose_response_model value Valid options: Standard Dose Response Curve, Hill Cooperative Binding, Hill Binding With Baseline, Four Parameter Logistic, Five Parameter Logistic Asymmetric, Biphasic Dose Response Dual Effect, Seven Param Biphasic Sigmoid, Biphasic Hill Activation Inhibition, Generalized Hyperbola, Single Site Binding Saturation.
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 = {
'standard_dose_response_curve': {
'params': ['A1', 'A2', 'LOGx0', 'p'],
'model': lambda x, A1, A2, LOGx0, p: A1 + (A2 - A1) / (1.0 + np.power(10.0, (LOGx0 - x) * p)),
'guess': lambda xa, ya: (float(np.min(ya)), float(np.max(ya)), float(np.median(xa)), 1.0),
},
'hill_cooperative_binding': {
'params': ['Vmax', 'k', 'n'],
'model': lambda x, Vmax, k, n: Vmax * np.power(x, n) / (np.power(k, n) + np.power(x, n)),
'guess': lambda xa, ya: (float(np.max(ya)), float(np.median(xa)), 1.0),
'bounds': (0.0, np.inf),
},
'hill_binding_with_baseline': {
'params': ['START', 'END', 'k', 'n'],
'model': lambda x, START, END, k, n: START + (END - START) * np.power(x, n) / (np.power(k, n) + np.power(x, n)),
'guess': lambda xa, ya: (float(np.min(ya)), float(np.max(ya)), float(np.median(xa)), 1.0),
'bounds': ([-np.inf, -np.inf, 0.0, 0.0], np.inf),
},
'four_parameter_logistic': {
'params': ['A1', 'A2', 'x0', 'p'],
'model': lambda x, A1, A2, x0, p: (A1 - A2) / (1.0 + np.power(x / x0, p)) + A2,
'guess': lambda xa, ya: (float(max(ya)), float(min(ya)), float(np.median(xa)), 1.0),
'bounds': ([-np.inf, -np.inf, -np.inf, 0.0], np.inf),
},
'five_parameter_logistic_asymmetric': {
'params': ['Amin', 'Amax', 'x0', 'h', 's'],
'model': lambda x, Amin, Amax, x0, h, s: Amin + (Amax - Amin) / np.power(1.0 + np.power(x / x0, -h), s),
'guess': lambda xa, ya: (float(np.min(ya)), float(np.max(ya)), float(np.median(xa)), 1.0, 1.0),
'bounds': ([-np.inf, -np.inf, -np.inf, -np.inf, 0.0], np.inf),
},
'biphasic_dose_response_dual_effect': {
'params': ['A1', 'A2', 'LOGx01', 'LOGx02', 'h1', 'h2', 'p'],
'model': lambda x, A1, A2, LOGx01, LOGx02, h1, h2, p: A1 + (A2 - A1) * (p / (1.0 + np.power(10.0, (LOGx01 - x) * h1)) + (1.0 - p) / (1.0 + np.power(10.0, (LOGx02 - x) * h2))),
'guess': lambda xa, ya: (float(np.min(ya)), float(np.max(ya)), float(np.median(xa)), float(np.median(xa)), 1.0, 1.0, 0.5),
'bounds': ([-np.inf, -np.inf, -np.inf, -np.inf, -np.inf, -np.inf, 0.0], [np.inf, np.inf, np.inf, np.inf, np.inf, np.inf, 1.0]),
},
'seven_param_biphasic_sigmoid': {
'params': ['Amin', 'Amax1', 'Amax2', 'x0_1', 'x0_2', 'h1', 'h2'],
'model': lambda x, Amin, Amax1, Amax2, x0_1, x0_2, h1, h2: Amin + (Amax1 - Amin) / (1.0 + np.power(10.0, (x - x0_1) * h1)) + (Amax2 - Amin) / (1.0 + np.power(10.0, (x0_2 - x) * h2)),
'guess': lambda xa, ya: (float(np.min(ya)), float(np.max(ya)), float(np.max(ya)), float(np.percentile(xa, 25)), float(np.percentile(xa, 75)), 1.0, 1.0),
},
'biphasic_hill_activation_inhibition': {
'params': ['Pm', 'Ka', 'Ki', 'Ha', 'Hi'],
'model': lambda x, Pm, Ka, Ki, Ha, Hi: Pm / ((1.0 + np.power(Ka / x, Ha)) * (1.0 + np.power(x / Ki, Hi))),
'guess': lambda xa, ya: (float(np.max(ya)), float(np.median(xa)), float(np.median(xa)), 1.0, 1.0),
'bounds': (0.0, np.inf),
},
'generalized_hyperbola': {
'params': ['a', 'b', 'c', 'd'],
'model': lambda x, a, b, c, d: a - b / np.power(1.0 + c * x, 1.0 / d),
'guess': lambda xa, ya: (float(np.max(ya)), 1.0, 0.1, 1.0),
},
'single_site_binding_saturation': {
'params': ['Bmax', 'K1'],
'model': lambda x, Bmax, K1: Bmax * x / (K1 + x),
'guess': lambda xa, ya: (float(np.max(ya)), float(np.median(xa))),
'bounds': (0.0, np.inf),
}
}
# Validate model parameter
if dose_response_model not in models:
return f"Invalid model: {str(dose_response_model)}. Valid models are: {', '.join(models.keys())}"
model_info = models[dose_response_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]