3. Ajuste de curvas por mínimos cuadrados#
3.1. Introducción#
La gran mayoría de las fórmulas en la ciencia no pueden ser determinadas de forma teórica y debemos recurrir a relaciones empíricas en base a experimentos.
Por ejemplo, en mecánica de fluidos la fuerza de arrastre, \(F_D\), es proporcional al cuadrado de la velocidad del flujo alrededor el cuerpo, \(V\):
donde \(\rho\) es la densidad del fluido, \(A\) es el área proyectada en la dirección del flujo, y \(C_D\) es el coeficiente de arrastre.
Aunque esta relación es válida para cualquier cuerpo, el valor de \(C_D\), cambia dependiendo del objeto.
En la gran mayoría de los casos, este valor no se puede determinar de forma analítica, y debemos recurrir a ensayos en un tunel de viento para determinar la relación entre estas dos variables.
El valor de \(C_D\) estará dado por la curva que mejor se ajuste a estos valores experimentales.
3.1.1. Regresión lineal unidimensional#
Consideremos el caso más simple, donde buscamos ajustar nuestros datos \(x_i, y_i\) \((i = 0,1,\cdots,n)\) al modelo:
donde \(a_0\) y \(a_1\) son coeficientes representando el intercepto y la pendiente, respectivamente.
¿Cómo determinamos los coeficientes?
Si intentamos determinar \(a_0\) y \(a_1\) mediante un sistema de ecuaciones:
Notamos que tenemos un sistema de 2 incognitas y \(n\) ecuaciones linealmente independientes. Es decir, no existe una solución única a este problema.
Lo que debemos hacer, entonces, es encontrar el valor de \(a_0\) y \(a_1\) que minimiza el error.
Particularmente, buscamos minimizar la expresión
donde \(i = 1,...,m\) son los datos de la muestra considerando un total de \(m\) datos.
Este criterio se denomina ajuste por mínimos cuadrados.
3.1.2. Ajuste por mínimos cuadrados paso a paso#
Primero, para buscar el mínimo de \(S_r\) aplicamos la derivada respecto a sus variables, es decir, \(a_0\) y \(a_1\)
El mínimo está dado cuando ambas derivadas son \(0\).
El resultado podemos expresarlo como un sistema de ecuaciones lineales:
La solución de este sistema nos entregará los valores de \(a_0\) y \(a_1\)
Consideremos los datos del problema del tunel de viento
\(V\) (m/s) |
10 |
20 |
30 |
40 |
50 |
60 |
---|---|---|---|---|---|---|
\(F\) (N) |
25 |
70 |
380 |
550 |
610 |
1220 |
import numpy as np
# creamos un arreglo con los datos tabulados
xi = np.array([ 10, 20, 30, 40, 50, 60, 70, 80]) # velocidad (m/s)
yi = np.array([ 25, 70, 380, 550, 610, 1220, 830, 1450]) # fuerza de arrastre (N)
# construimos un sistema Ax = b
m = len(xi)
A = np.array([[ m , np.sum(xi) ],
[np.sum(xi), np.sum(xi**2)]])
b = np.array([[np.sum(yi)],
[np.sum(yi*xi)]])
# resolvemos el sistema
a = np.linalg.solve(A,b)
print('Modelo lineal: \n y = %.3f + %.3f*x' % (a[0], a[1]))
Modelo lineal:
y = -234.286 + 19.470*x
import matplotlib.pyplot as plt
# formato del lienzo
plt.rcParams.update({'font.size': 10}) # tamaño de fuente
plt.figure(figsize = (5,4)) # tamaño del lienzo
# ploteamos nuestro resultado
y = lambda x: a[0] + a[1]*x # creamos una función con el modelo de ajuste
x = np.linspace(0,85,100) # creamos un arreglo para ploteo
plt.plot(xi, yi, 'bo',label = 'xi, yi') # gráfico con los datos tabulados
plt.plot(x, y(x), '-r', label = 'y = %.2f + %.2fx' % (a[0], a[1])) # gráfico del modelo
plt.xlabel('Velocidad del viento, $V$ (m/s)') # etiqueta eje x
plt.ylabel('Fuerza de arrastre, $F$ (N)') # etiqueta eje y
plt.legend() # incluir leyenda
plt.show() # mostrar gráfico
3.1.3. Cuantificación del error#
Para cuantificar la calidad de nuestro modelo utilizaremos el coeficiente de determinación, \(r^2\):
donde
\(S_r = \sum\left[y_i - y(x_i)\right]^2\) es el error del modelo de regresión (\(y(x) = a_0 + a_1x\), en este caso)
\(S_t = \sum\left(y_i - \bar{y}\right)^2\) es la desviación de los datos respecto a la media \(\bar{y}\).
El coeficiente de determinación nos permite cuantificar la calidad de nuestro modelo para representar una muestra, en comparación con la media \(\bar{y}\).
En python este valor está dado por la función r2_score
de la librería scikit-learn
from sklearn.metrics import r2_score
print('coef. de determinación')
print('r2 = %.4f' % r2_score(yi,y(xi)))
coef. de determinación
r2 = 0.8805
El resultado indica que el modelo lineal explica un 88.05% de los datos
Nota.
r2_score(yi,y(xi)))
requiere dos arreglos de iguales dimensiones.
3.1.4. Linealización de funciones no lineales#
Existen algúnos modelos no lineales comúnes en ingeniería que pueden ser linealizados para luego realizar ajuste por mínimos cuadrados.
Algúnos ejemplos son:
modelo exponencial
modelo de potencia
modelo de tasa de crecimiento de saturación
Nota Para el modelo de potencia, podemos usar “\(\log\)” o “\(\ln\)” para linealizar, tomando la precaución de usar la función inversa correcta para recuperar el modelo original, es decir: \begin{align*} y &= 10^{\log \alpha + \beta\log x}\quad\mathrm{o,}\ y &= e^{\ln \alpha + \beta\ln x} \end{align*}
Los coeficientes del modelo linealizado serán diferentes dependiendo de si se usa “\(\log\)” o “\(\ln\)”. Sin embargo, el modelo original debe ser el mismo, independientemente de la función utilizada para la linealización.
Analicemos el ejemplo anterior, ahora ajustando los datos al modelo de potencia \(y = \alpha x^{\beta}\)
Primero, aplicaremos un modelo de ajuste de la forma: \(\ln(y) = a_0 + a_1\ln(x)\).
import numpy as np
from numpy import log # en python log(x) = ln(x)
xi = np.array([ 10, 20, 30, 40, 50, 60, 70, 80])
yi = np.array([ 25, 70, 380, 550, 610, 1220, 830, 1450])
# linealizamos las variables
log_xi = log(xi)
log_yi = log(yi)
# construimos un sistema Ax = b
m = len(xi) # numero de datos
A = np.array([[ m , np.sum(log_xi) ],
[np.sum(log_xi), np.sum(log_xi**2)]])
b = np.array([[np.sum(log_yi)],
[np.sum(log_yi*log_xi)]])
# resolvemos el sistema
a = np.linalg.solve(A,b)
print('Modelo linealizado: \n ln(y) = %.3f + %.3f*ln(x)'
% (a[0], a[1]))
Modelo linealizado:
ln(y) = -1.294 + 1.984*ln(x)
Para retornar al modelo original, aplicamos
donde deducimos que \(\alpha = e^{a_0}\) y \(\beta = a_1\).
alpha = np.exp(a[0])
beta = a[1]
print('Modelo no-lineal')
print('y = %.3f*x^%.3f' % (alpha, beta))
Modelo no-lineal
y = 0.274*x^1.984
Graficamos el resultado con el modelo no-lineal \(y = \alpha x^{\beta}\)
# formato del lienzo
plt.rcParams.update({'font.size': 10}) # tamaño de fuente
plt.figure(figsize = (5,4)) # tamaño del lienzo
# ploteamos nuestro resultado)
y = lambda x: alpha*x**beta # función con el modelo no-lineal
x = np.linspace(1,85,100) # creamos un arreglo para ploteo
plt.plot(xi, yi, 'bo',label = 'xi, yi') # gráfico con los datos tabulados
plt.plot(x, y(x), '-r', label = r'$y = %.3fx^{%.3f}$' % (alpha, beta)) # gráfico del modelo
plt.xlabel('Velocidad del viento, $V$ (m/s)') # etiqueta eje x
plt.ylabel('Fuerza de arrastre, $F$ (N)') # etiqueta eje y
plt.legend() # incluir leyenda
plt.show() # mostrar gráfico
Analizamos la calidad del modelo
# analizamos la calidad del modelo
from sklearn.metrics import r2_score
print('coef. de determinación')
print('r2 = %.4f' % r2_score(yi,y(xi)))
coef. de determinación
r2 = 0.8088
El resultado indica que el modelo lineal explica un 80.88% de los datos
El coeficiente de determinación para el modelo no-lineal (\(r^2=80.88\%\)) es menor que para el modelo lineal (\(r^2=88.05\%\)). Sin embargo, el modelo no-lineal se ajusta mejor al modelo físico (por ejemplo, \(F = 0\) para \(v = 0\)).
Por lo tanto, el modelo más adecuado para los datos es el de potencia:
3.2. Otros modelos lineales#
El procedimiento de ajuste de curva por mínimos cuadrados se puede extender para modelos de ajuste más complejos, tales como:
Regresión polinomial
Regresión lineal multivariable
3.2.1. Regresión polinomial#
En su forma general, el modelo polinomial corresponde a una función univariable de la forma
Por ejemplo, consideremos el modelo \(y = a_0 + a_1 x + a_2 x^2\). Según el método de regresión por mínimos cuadrados, la mejor curva está dada por el mínimo de:
Aplicando \(\frac{\partial S_r}{\partial a_0} =0\), \(\frac{\partial S_r}{\partial a_1} =0\) y \(\frac{\partial S_r}{\partial a_2} =0\), llegamos al sistema de ecuaciones:
Cuya solución nos entrega el valor de los coeficientes \(a_0\), \(a_1\) y \(a_2\)
3.2.2. Regresión lineal multidimensional#
Para problemas con más de una variable independiente se deben untilizar modelos multidimencionales. Un modelo común corresponde al modelo linear de la forma:
Por ejemplo, en dos dimensiones tenemos un modelo de la forma \(y = a_0 + a_1x_1+a_2x_2\). La mejor curva está dada por el mínimo de:
Aplicando \(\frac{\partial S_r}{\partial a_0} =0\), \(\frac{\partial S_r}{\partial a_1} =0\) y \(\frac{\partial S_r}{\partial a_2} =0\), llegamos al sistema de ecuaciones:
Cuya solución nos entrega el valor de los coeficientes \(a_0\), \(a_1\) y \(a_2\)
Gráficamente, el método de regresión por mínimos cuadrados corresponde a determinar el plano que minimice el error cuadrático.
3.3. Modelos de ajuste en python#
Es importante mencionar que en materia de modelos de ajuste la librería más adecuada es scikit-learn
. La librería, sin embargo, puede ser demasiado compleja para modelos lineales simples.
En este capítulo, mostraremos los métodos más simples para regreción lineal polinomial y no-lineal, utilizando numpy
y scipy
.
3.3.1. Modelo polinomial unidimencional (numpy.polyfit
)#
Esta función está especificamente diseñada para modelos polinomiales de una dimensión, es decir, \(y = a_0 + a_1x +a_2x^2+...a_nx^n\)
Por ejemplo, para generar un modelo en base a un polinomio de orden 2,
a = numpy.polyfit(xi,yi,2) # coeficientes a0, a1, a2, ...
donde \(a_0=\) a[2]
, \(a_1=\) a[1]
, \(a_2=\) a[0]
Tal como indica la documentación oficial, los coeficientes de polyfit
están ordenados de mayor potencia a menor potencia.
Para evitar confusiones con el orden de los coeficientes, se recomienda utilizar numpy.polyval(a,x)
, que genera una función en base al modelo determinado, donde x
es un valor arbitrario y a
son los coeficientes determinados por polyfit
.
y = numpy.polyval(a,x) # función en base al modelo y(x) = a0 + a1*x + ... am*x^m
Analicemos el uso de polyfit
y polyval
con el ejemplo del tunel de viento
import numpy as np
# construimos el modelo
xi = np.array([ 10, 20, 30, 40, 50, 60, 70, 80])
yi = np.array([ 25, 70, 380, 550, 610, 1220, 830, 1450])
# Aplicamos modelo polinomial con polyfit
a = np.polyfit(xi,yi,2)
print('Modelo polinomial:')
print('y = %.3f + %.3f*x + %.3f*x^2' % (a[2], a[1], a[0]))
# evaluamos polinomio de ajuste con polyval
x0 = 34 # valor de prueba
print('y(%.3f) = %.3f'% (x0,np.polyval(a,x0)))
Modelo polinomial:
y = -178.482 + 16.122*x + 0.037*x^2
y(34.000) = 412.673
Graficamos la solución con polyval
:
# formato del lienzo
plt.rcParams.update({'font.size': 10}) # tamaño de fuente
plt.figure(figsize = (5,4)) # tamaño del lienzo
# ploteamos nuestro resultado)
x = np.linspace(1,85,100) # creamos un arreglo para ploteo
plt.plot(xi, yi, 'bo',label = 'xi, yi') # gráfico con los datos tabulados
plt.plot(x, np.polyval(a,x), '-r', label = 'polyfit') # gráfico del modelo
plt.xlabel('Velocidad del viento, $V$ (m/s)') # etiqueta eje x
plt.ylabel('Fuerza de arrastre, $F$ (N)') # etiqueta eje y
plt.legend() # incluir leyenda
plt.show() # mostrar gráfico
# analizamos la calidad del modelo
from sklearn.metrics import r2_score
print('coef. de determinación')
print('r2 = %.4f' % r2_score(yi,np.polyval(a,xi)))
coef. de determinación
r2 = 0.8818
Notar que el gráfico del modelo muestra una tendencia aproximadamente lineal.
Esto es interesante, porque al incluir término \(a_2x^2\) en nuestro modelo lineal, \(a_0 + a_1x\), esperabamos un mejor ajuste con el modelo físico \(F_D = \frac{1}{2}\rho A C_D V^2\).
Sin embargo, la tendencia del método es minimizar el error (en otras palabras, mejorar \(r^2\)). Esto significa que el método va a forzar una curva lineal, aunque esta no corresponda a la física del problema.
3.3.2. scipy.optimize.curve_fit
(regresión no-lineal)#
Esta función, parte del módulo optimize
de la librería scipy
, utiliza un método iterativo para minimizar el error cuadrático. La función curve_fit
puede ser utilizada para cualquier tipo de modelo, linear o no-linear, unidimensional o multidimensional.
Nota Para más información de
curve_fit
, revisar documentación en línea
Para usar curve_fit
primero debemos crear el modelo de ajuste en formato de función, por ejemplo para \(y = \alpha x^\beta\):
def model(x,a,b):
y = a*x**b
return y
El primer argumento de la función
x
, siempre debe ser la variable independiente. Los parámetros de ajuste,a,b
van indicados en los argumentos siguientes.
Luego, llamamos a curve_fit
usando el modelo de ajuste y la serie de datos \(x_i, y_i\), como argumentos:
coef = curve_fit(model, xdata = xi, ydata = yi)[0]
a = coef[0] # parámetro alfa
b = coef[1] # parámetro beta
El índice
[0]
al final decurve_fit
indica que necesitamos solo el primer output, el cual llamamoscoef
. Esta variable viene en formato de arreglo numpy (ndarray), donde el orden de los coeficientes corresponde al orden dea
yb
enmodel
.
Por ejemplo, ajustemos los datos al modelo \(y=\alpha x^\beta\)
import numpy as np
from scipy.optimize import curve_fit
xi = np.array([ 10, 20, 30, 40, 50, 60, 70, 80])
yi = np.array([ 25, 70, 380, 550, 610, 1220, 830, 1450])
# creamos nuestro modelo de ajuste en formato de función
def model(x,a,b):
y = a*x**b
return y
# usamos curve_firt para determinar el valor de los parámetros a y b
coef = curve_fit(model, xdata = xi, ydata = yi)[0] # extraemo|s el primer output del modelo
a = coef[0] # parámetro alfa
b = coef[1] # parámetro beta
print('Modelo no-lineal')
print('y = %.3f*x^%.3f' % (a, b))
Modelo no-lineal
y = 2.538*x^1.436
# formato del lienzo
plt.rcParams.update({'font.size': 10}) # tamaño de fuente
plt.figure(figsize = (5,4)) # tamaño del lienzo
# ploteamos nuestro resultad)
x = np.linspace(1,85,100) # creamos un arreglo para ploteo
plt.plot(xi, yi, 'bo',label = 'xi, yi') # gráfico con los datos tabulados
plt.plot(x, model(x,a,b), '-r', label = r'$y = %.3fx^{%.3f}$' % (a, b)) # gráfico del modelo
plt.xlabel('Velocidad del viento, $V$ (m/s)') # etiqueta eje x
plt.ylabel('Fuerza de arrastre, $F$ (N)') # etiqueta eje y
plt.legend() # incluir leyenda
plt.show() # mostrar gráfico
Notar que los coeficientes de este modelo son diferentes a los que determinamos mediante regresión lineal en la función linealizada.
Esto es debido a que la regresión no-lineal busca minimizar el error
de forma iterativa. Así, curve_fit
permite buscar soluciones con valores \(r^2\) más cercanos a \(1\) que no son accesibles para el modelo lineal
En efecto, si analizamos el coeficiente de determinación del modelo generado por curve_fit
:
from sklearn.metrics import r2_score
print('coef. de determinación')
print('r2 = %.4f' % r2_score(yi,model(xi,a,b)))
coef. de determinación
r2 = 0.8769
En el caso de un modelo lineal, ambos métodos generan el mismo modelo
import numpy as np
from scipy.optimize import curve_fit
xi = np.array([ 10, 20, 30, 40, 50, 60, 70, 80])
yi = np.array([ 25, 70, 380, 550, 610, 1220, 830, 1450])
def model(x,a,b):
y = a+ b*x
return y
coef = curve_fit(model, xdata = xi, ydata = yi)[0]
print('Modelo lineal: y = %.3f + %.3f*x' % (coef[0], coef[1]))
Modelo lineal: y = -234.286 + 19.470*x
3.4. Regresión lineal vs no-lineal#
Respecto a la regresión no-lineal:
Ventajas
Permite trabajar con modelos más generales.
Para modelos no-lineales, el método produce curvas con mejores coeficientes de determinación en comparación con modelos lineales en base a linealización.
Desventajas
Como todo método iterativo, el metodo puede sufrir problemas de inestabilidad condicionados al modelo propuesto, \(f_\mathrm{nl}(x)\). Esto puede derivar en problemas de convergencia, soluciones locales, o sensibilidad a los valorse iniciales.
En general, se recomienda utilizar
scipy.optimize.curve_fit
, u otros métodos de ajuste no-lineal, exclusivamente para modelos no-lineales.
3.5. Referencias#
Kong Q., Siauw T., Bayen A. M. Chapter 16: Least Square Regression in Python Programming and Numerical Methods – A Guide for Engineers and Scientists, 1st Ed., Academic Press, 2021
Chapra S., Canale R. Capítulo 17: Regresión por mínimos cuadrados en Métodos Numéricos para Ingenieros, 6ta Ed., McGraw Hill, 2011