Implementation
This page describes the internal flow of lin_fit and the order in which validation, coefficient estimation, optional iterative weight updates, diagnostics, and plotting are applied. Unlike Overview, the goal here is not to repeat the parameter list, but to show how the function actually builds the weighted linear fit.
The following snippets are taken from the current src/mespy/fit_utils.py implementation. The base formulas adapt the theoretical formulation already present in the legacy fit_utils.tex documentation; the stopping criterion, returned diagnostics, and plot band are instead explained from the current implementation. Private helpers are mentioned only to clarify the flow; complete details are documented in _as_float_vector, _fit_coefficients, _validate_axis_limits, _validate_decimals, _validate_figsize, and _style_context. When the symbols \(\bar{x}_w\), \(\mathrm{Var}_w\), and \(\mathrm{Cov}_w\) are needed, the practical references are the public functions weighted_mean, variance, and covariance.
Execution sequence
The implementation follows this sequence:
Converts
x,y, andsigma_yinto one-dimensional, finitefloat64vectors.Checks that
x,y, andsigma_yhave the same length and that there are at least 3 points.Validates
decimalsas a readable non-negative integer.Validates
tolas a positive scalar andmax_iteras a positive integer.If
sigma_xis provided, it validates it as a strictly positive vector with the same shape asx.Builds the initial weights \(w_i = 1 / \sigma_{y_i}^2\).
Estimates an initial line with
_fit_coefficients(...).If
sigma_xis provided, it iteratively updates the weights using the effective variance \(\sigma_{\mathrm{eff},i}^2 = \sigma_{y_i}^2 + m^2 \sigma_{x_i}^2\) until the relative change in the slope falls belowtol.Computes parameter variances, covariance, correlation, residuals,
chi2,reduced_chi2, and other diagnostics.If
show_plot=True, it enters_style_context(style)and builds a two-panel figure with data, fitted line, and residuals.Returns a
LinearFitResultwith all numeric results and the optional figure.
Input validation and numerical setup
x_values = _as_float_vector("x", x)
y_values = _as_float_vector("y", y)
sigma_y_values = _validate_positive_vector("sigma_y", sigma_y)
if x_values.shape != y_values.shape or x_values.shape != sigma_y_values.shape:
raise ValueError("x, y e sigma_y devono avere la stessa lunghezza")
n = x_values.size
if n < 3:
raise ValueError("Servono almeno 3 punti per effettuare un fit lineare")
decimals = _validate_decimals(decimals)
tol_value = _validate_positive_scalar("tol", tol)
max_iter_value = _validate_max_iter(max_iter)
use_sigma_x = sigma_x is not None
sigma_x_values: FloatVector | None = None
if use_sigma_x:
sigma_x_values = _validate_positive_vector(
"sigma_x",
sigma_x,
expected_shape=x_values.shape,
)
sigma_y2 = sigma_y_values**2
sigma_x2 = sigma_x_values**2 if sigma_x_values is not None else None
weights = 1.0 / sigma_y2
This first block defines the numeric contract of the fit.
x,y, andsigma_yare not used directly: they first pass through validators that enforce numeric, one-dimensional, finite arrays.sigma_yand, if provided,sigma_xmust be strictly positive. This is essential to build weights that are physically and numerically meaningful.The function rejects datasets with fewer than 3 points, because the linear fit returns two parameters and then uses
dof = n - 2in its diagnostics.decimalsis validated even whenshow_plot=False, because it is part of the function’s general contract and of legend formatting when the plot is active.I pesi iniziali del caso base sono
Questa e la formulazione standard del fit lineare pesato quando le incertezze sono solo sulle ordinate.
Le quantita pesate che compaiono piu avanti si appoggiano alle funzioni statistiche del package:
Nel codice queste medie non vengono riscritte a mano: vengono delegate a
weighted_mean. Lo stesso vale per varianza e covarianza pesata, che vengono delegate avarianceecovariance.
Coefficient estimation and iterative branch
slope, intercept, var_x = _fit_coefficients(x_values, y_values, weights)
iterations = 0
converged = not use_sigma_x
if use_sigma_x:
previous_slope = slope
for _ in range(max_iter_value):
sigma_eff2 = sigma_y2 + slope**2 * sigma_x2
weights = 1.0 / sigma_eff2
iterations += 1
next_slope, next_intercept, next_var_x = _fit_coefficients(
x_values,
y_values,
weights,
)
rel_change = abs(next_slope - previous_slope) / max(
abs(next_slope),
1e-300,
)
if rel_change < tol_value:
slope = next_slope
intercept = next_intercept
var_x = next_var_x
converged = True
break
previous_slope = next_slope
slope = next_slope
intercept = next_intercept
var_x = next_var_x
else:
raise RuntimeError(
f"Il fit lineare non converge entro max_iter={max_iter_value}"
)
Qui avviene la stima vera e propria della retta.
Nel caso base, _fit_coefficients(...) usa varianza e covarianza pesata di x e y per stimare
La quantita var_x restituita dall’helper coincide con \(\mathrm{Var}_w(x)\) e viene riusata anche nel calcolo delle incertezze sui parametri.
Se
xnon ha abbastanza variabilita,_fit_coefficients(...)fallisce. In pratica il codice richiede che la varianza pesata dixsia finita e non troppo vicina a zero.Se
sigma_x is None, il fit si ferma qui:iterations = 0econverged = True.
Quando invece sono presenti incertezze anche su x, la funzione passa a un algoritmo iterativo.
La varianza efficace di ogni punto diventa
Questa formula ha una motivazione teorica precisa: nel modello lineare \(y = mx + c\), un’incertezza orizzontale \(\sigma_{x_i}\) puo essere propagata lungo l’asse verticale con la regola lineare
In altre parole,
lin_fitusa l’idea della varianza efficace: tratta l’errore suxcome un contributo aggiuntivo all’errore suy, ottenendo una singola varianza verticale per punto.Questo porta naturalmente alla quantita che il fit cerca di rendere il piu piccola possibile:
Qui si vede perche serve iterare: nel denominatore compare la pendenza
m. Quindi i pesi dipendono proprio dalla quantita che stiamo cercando di stimare.Per questo il metodo non puo chiudersi in un solo passaggio come nel caso con sole incertezze su
y. La funzione procede invece per passi successivi:
In pratica il procedimento e questo: si parte da una prima stima di
m, si calcolano i pesi con quella stima, si ricalcola la retta con i nuovi pesi e si ripete finche il risultato cambia pochissimo.La funzione non usa un metodo geometrico completo che minimizza direttamente tutte le distanze nel piano. Fa una scelta piu semplice: trasforma l’effetto di
sigma_xin un contributo aggiuntivo all’incertezza verticale suy. E proprio questa idea che porta alla formula della varianza efficace usata nel codice.I nuovi pesi sono quindi
A ogni iterazione il codice ricalcola la retta con questi nuovi pesi e misura la variazione relativa della pendenza tramite
rel_change = abs(next_slope - previous_slope) / max(abs(next_slope), 1e-300)
Il criterio di arresto guarda la pendenza, non l’intercetta, perche nella formula dei pesi compare solo
m. Semsmette quasi di cambiare, allora smettono quasi di cambiare anche \(\sigma_{\mathrm{eff},i}^2\) e quindi i pesi.L’intercetta non entra nella varianza efficace. Quando i pesi sono ormai quasi stabili, anche
cevar_xsi assestano di conseguenza.La scelta di usare una variazione relativa, invece di una assoluta, serve a non legare il test alle unita di misura. Per esempio, una stessa differenza numerica su
mpuo sembrare grande o piccola a seconda di come sono misuratixey.Il denominatore usa
max(abs(next_slope), 1e-300)per evitare una divisione per zero quando la pendenza stimata e molto piccola o nulla.Il valore
1e-300non ha un significato fisico o statistico: e solo una protezione numerica molto piccola per evitare problemi quandonext_slopee vicino a zero.La convergenza e dichiarata quando
rel_change < tol_value.Anche
tolva letto come parametro numerico, non come una costante teorica universale. Nel repository non compare una derivazione speciale del valore di default: la scelta pratica e usare una soglia relativa molto piccola (1e-10) per fermarsi quando la pendenza, e quindi anche i pesi, stanno ormai cambiando in modo trascurabile.In modo molto concreto,
tolrisponde a questa domanda: “quanto deve essere piccolo l’ultimo cambiamento della pendenza per poter dire che il procedimento si e assestato?” Non misura quanto il fit sia “buono” dal punto di vista fisico e non va letta come una soglia suchi2.Ridurre
tolsignifica chiedere un assestamento ancora piu stretto e quindi, spesso, piu iterazioni. Aumentaretolsignifica fermarsi prima, accettando una stabilita finale un po meno rigorosa.iterationsconta quanti aggiornamenti dei pesi sono stati effettuati davvero.convergedindica se il criterio di arresto e stato soddisfatto prima di esauriremax_iter.max_itercompleta il criterio di stop come limite di sicurezza: non decide da solo la convergenza, ma impedisce che un caso difficile o molto lento lasci il ciclo aperto troppo a lungo.Se il ciclo termina senza convergere, la funzione alza
RuntimeErrorinvece di restituire un risultato parziale.
Fit diagnostics
sum_w = float(np.sum(weights))
var_m = 1.0 / (var_x * sum_w)
var_c = weighted_mean(x_values**2, weights) / (var_x * sum_w)
cov_mc = -weighted_mean(x_values, weights) / (var_x * sum_w)
sigma_m = float(np.sqrt(var_m))
sigma_c = float(np.sqrt(var_c))
rho_mc = float(cov_mc / (sigma_m * sigma_c))
residuals = y_values - slope * x_values - intercept
dof = n - 2
residual_std = float(np.sqrt(np.sum(residuals**2) / dof))
sigma_fit2 = sigma_y2 if sigma_x2 is None else sigma_y2 + slope**2 * sigma_x2
chi2 = float(np.sum((residuals**2) / sigma_fit2))
reduced_chi2 = float(chi2 / dof)
Dopo aver fissato i pesi finali, lin_fit costruisce le principali grandezze diagnostiche del modello.
Se chiamiamo
allora il codice usa le formule
Da qui seguono direttamente
e il coefficiente di correlazione tra pendenza e intercetta
I residui sono definiti come
e i gradi di liberta sono
La dispersione sintetica dei residui e calcolata come
Per il chi quadrato, invece, il codice usa una varianza di fit che dipende dal ramo seguito:
e quindi
A few practical observations help interpret these numbers correctly.
residual_stdnon pesa i residui con le incertezze sperimentali: misura solo la dispersione quadratica dei residui attorno alla retta.chi2ereduced_chi2invece confrontano i residui con le incertezze del modello, quindi hanno una lettura statistica diversa.Nel caso con
sigma_x, sia i pesi finali siachi2usano la stessa forma di varianza efficace.
Optional plot and return object
if save_path is not None and not show_plot:
raise ValueError("save_path può essere usato solo se show_plot=True")
fig = None
if show_plot:
with _style_context(style):
if xlim is not None:
xlim = _validate_axis_limits(...)
if ylim is not None:
ylim = _validate_axis_limits(...)
subplots_kwargs = {
"gridspec_kw": {"height_ratios": [3, 1]},
"sharex": True,
"constrained_layout": True,
}
if figsize is not None:
subplots_kwargs["figsize"] = _validate_figsize(figsize)
if dpi is not None:
subplots_kwargs["dpi"] = dpi
fig, (ax_fit, ax_res) = plt.subplots(2, 1, **subplots_kwargs)
errorbar_kwargs = {
"yerr": sigma_y_values,
"xerr": sigma_x_values if use_sigma_x else None,
"fmt": "o",
"markersize": 4,
"elinewidth": 1,
"capsize": 3,
"alpha": data_alpha,
}
if point_color is not None:
errorbar_kwargs["color"] = point_color
errorbar_kwargs["ecolor"] = point_color
ax_fit.errorbar(x_values, y_values, **errorbar_kwargs)
fmt = f".{decimals}f"
x_fit = np.linspace(x_values.min(), x_values.max(), 200)
y_fit = slope * x_fit + intercept
fit_label = (
f"Fit: m={slope:{fmt}}, c={intercept:{fmt}}"
if show_fit_params
else "Fit"
)
ax_fit.plot(x_fit, y_fit, color=fit_color, linewidth=1.5, label=fit_label)
if show_band:
x_bar = weighted_mean(x_values, weights)
sigma_y_fit = np.sqrt(
1.0 / sum_w + (x_fit - x_bar) ** 2 / (var_x * sum_w)
)
ax_fit.fill_between(
x_fit,
y_fit - sigma_y_fit,
y_fit + sigma_y_fit,
color=band_color,
alpha=band_alpha,
label=r"$\pm 1 \sigma$ retta",
)
ax_res.errorbar(x_values, residuals, **errorbar_kwargs)
ax_res.axhline(0, color=fit_color, linewidth=1, linestyle="--")
if not show_grid:
ax_fit.grid(False)
ax_res.grid(False)
elif grid_alpha is not None:
ax_fit.grid(True, axis="y", alpha=grid_alpha)
ax_res.grid(True, axis="y", alpha=grid_alpha)
if save_path is not None:
savefig_kwargs = {"bbox_inches": "tight"}
if dpi is not None:
savefig_kwargs["dpi"] = dpi
fig.savefig(save_path, **savefig_kwargs)
return LinearFitResult(
slope=float(slope),
intercept=float(intercept),
slope_std=sigma_m,
intercept_std=sigma_c,
covariance=float(cov_mc),
correlation=rho_mc,
residuals=residuals,
residual_std=residual_std,
chi2=chi2,
reduced_chi2=reduced_chi2,
dof=dof,
iterations=iterations,
converged=converged,
figure=fig,
)
L’ultima parte gestisce plotting e packaging finale del risultato.
save_pathpuo essere usato solo seshow_plot=True. La funzione controlla questa incompatibilita prima di entrare nel ramo Matplotlib.Se
show_plot=False, tutta la parte grafica viene saltata efigurenel risultato finale valeNone.When plotting is active,
lin_fituses the same_style_contextashistogram:style=Nonekeeps the currentrcParams,style="mespy"loads the package style, and any other string is passed to Matplotlib.Quando il grafico e attivo,
lin_fitcrea sempre due pannelli verticali: in alto il fit, in basso i residui.figsizeanddpiare passed toplt.subplots(...)only when they have been specified. Without overrides, the active style decides.point_color, when provided, is applied to both markers and error bars;fit_coloris used for the fitted line and for the horizontal residual line;band_colorcontrols the band around the line.xlimviene applicato sia aax_fitsia aax_res, mentreylimviene applicato solo al pannello superiore.show_grid=Falseturns the grid off explicitly on both panels;show_grid=Truewithgrid_alpha is Noneleaves the grid to the active style; an explicitgrid_alphaapplies a grid on theyaxis of both panels.decimalseshow_fit_paramsinfluiscono solo sulla stringa della legenda della retta, non sul calcolo numerico del fit.Saving always uses
bbox_inches="tight";dpiis passed tosavefig(...)only when it is explicitly provided.
La retta visualizzata e
Se show_band=True, il codice costruisce anche una banda attorno alla retta usando
Il grafico mostra quindi
Questa e la formula implementata oggi nel codice: non viene moltiplicata per reduced_chi2 ne per altri fattori di scala aggiuntivi.
Il valore restituito non e una coppia (fig, ax) ma un LinearFitResult, cioe un contenitore immutabile che raccoglie parametri del fit, incertezze, diagnostiche, residui e figura opzionale.
Important interactions between parameters
Alcune combinazioni di parametri definiscono il comportamento pratico piu importante della funzione.
sigma_xattiva il ramo iterativo e rende rilevantitolemax_iter.tole una soglia di convergenza numerica sulla pendenza relativa, non una soglia statistica sulla qualita del fit.Se
sigma_xnon e presente, il fit usa solo i pesi1 / sigma_y**2, non itera e marca subitoconverged=True.style=Noneuses the currentrcParams,style="mespy"uses the package style, and any other string is passed straight through Matplotlib.point_color,title_fontsize,title_pad,legend_fontsize,legend_loc, andgrid_alphaoverride the style only when they are notNone.show_plot=Falsedisables the entire Matplotlib branch: in that casefigure=None, andxlimandylimare not even validated, butdecimals,tol, andmax_iterare still checked.save_pathnon e un salvataggio indipendente dal plotting: e ammesso solo insieme ashow_plot=True.show_fit_params=Truecambia la label della retta da"Fit"a una stringa conmecformattati condecimals.show_bandcontrolla solo la banda attorno alla retta, non il calcolo del fit.show_legend=Falselascia comunque disegnati punti, retta e banda, ma senza legenda.xlimagisce su entrambi i pannelli condivisi, mentreylimlimita solo il pannello superiore.
Commented example
import numpy as np
from mespy import lin_fit
x = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
y = np.array([2.2, 4.1, 5.8, 8.2, 9.9])
sigma_y = np.array([0.20, 0.20, 0.25, 0.25, 0.30])
sigma_x = np.array([0.05, 0.05, 0.05, 0.08, 0.08])
result = lin_fit(
x,
y,
sigma_y,
sigma_x=sigma_x,
xlabel="tempo [s]",
ylabel="spazio [m]",
title="Fit lineare pesato",
show_fit_params=True,
)
print(result.slope)
print(result.intercept)
print(result.reduced_chi2)
print(result.iterations, result.converged)
In questo esempio la presenza di sigma_x forza il ramo iterativo con varianza efficace. Il risultato finale resta un LinearFitResult: i campi slope, intercept, reduced_chi2, iterations e converged permettono di leggere subito sia l’esito numerico del fit sia il comportamento dell’algoritmo.