Universidad Nacional Autonoma de México
Facultad de Ciencias
Seminario de Estadística I
Autor: Zárate Almaguer Francisco Javier
ANCOVA es un tipo de modelo lineal general (GLM) que incluye al menos una variable independiente continua y una categórica (tratamientos). ANCOVA es útil cuando el efecto de los tratamientos es importante mientras hay una variable continua adicional en el estudio. ANCOVA es propuesto por el estadístico británico Ronald A. Fisher durante la década de 1930.
La variable independiente continua adicional en ANCOVA se denomina covariable (también conocida como variable de control, concomitante o de confusión).
ANCOVA estima las diferencias entre grupos en una variable independiente categórica (interés primario) ajustando estadísticamente el efecto de la covariable (eliminando la varianza asociada a la covariable). ANCOVA aumenta el poder estadístico y reduce el término de error al eliminar la varianza asociada con la covariable.
ANCOVA proporciona medias ajustadas (que están controladas estadísticamente por covariables) para cada grupo en una variable independiente categórica. Los medios ajustados eliminan el sesgo de la covariable del modelo.
ANCOVA utiliza el enfoque de regresión múltiple (como la regresión simple en ANOVA) para estudiar los efectos ajustados de la variable independiente sobre la variable dependiente.
ANCOVA sigue suposiciones similares a las de ANOVA para la independencia de las observaciones, la normalidad y la homogeneidad de las varianzas. Además, ANCOVA debe cumplir con los siguientes supuestos,
Suposición de linealidad : en cada nivel de variable independiente categórica, la covariable debe estar linealmente relacionada con la variable dependiente. Si la relación no es lineal, el ajuste realizado a la covariable estará sesgado.
Homogeneidad de las pendientes de regresión dentro del grupo (paralelismo o no interacción) : No debe haber interacción entre la variable independiente categórica y la covariable, es decir, las líneas de regresión entre la covariable y la variable dependiente para cada grupo de la variable independiente deben ser paralelas (misma pendiente ).
La variable dependiente y la covariable deben medirse en una escala continua
La covariable debe medirse sin error o con el menor error posible.
Algunos investigadores están interesados en comparar la efectividad de tres tratamientos para tratar la depresión. Por simplicidad, se denotan los tres tratamientos como tratamiento A, B y C. Los investigadores recolectaron información al respecto a través de una muestra aleatoria de tamaño n = 36 de individuos con depresión.
# Comenzamos por cargar las librerias que ocuparemos
import numpy as np
import pandas as pd
import statsmodels.api as sm
from statsmodels.formula.api import ols
import matplotlib.pyplot as plt
después, cargamos los datos en un dataframe
df = pd.read_table("Datos.txt")
df.head()
y | age | x2 | x3 | TRT | |
---|---|---|---|---|---|
0 | 56 | 21 | 1 | 0 | A |
1 | 41 | 23 | 0 | 1 | B |
2 | 40 | 30 | 0 | 1 | B |
3 | 28 | 19 | 0 | 0 | C |
4 | 55 | 28 | 1 | 0 | A |
Donde las variables x2
y x3
corresponde a variables indicadoras referentes a si el individuo recibió el tratamiento A o B respectivamente. Notemos que nos falta una variable indicadora que haga referencia al tratamiento C, por lo tanto procedemos a generarla
# Si el individuo ha recibido el tratamiento C, entonces asignaremos el valor
# de 1, caso contrario asignaremos el valor de 0
def trat_C(tratamiento):
if tratamiento == "C":
return 1
else:
return 0
# Agregamos una columna nueva y aplicamos la funcion anterior
# a cada una de las filas de la columna (serie) TRT
df["xC"] = df["TRT"].apply(lambda x: trat_C(x))
df.head()
y | age | x2 | x3 | TRT | xC | |
---|---|---|---|---|---|---|
0 | 56 | 21 | 1 | 0 | A | 0 |
1 | 41 | 23 | 0 | 1 | B | 0 |
2 | 40 | 30 | 0 | 1 | B | 0 |
3 | 28 | 19 | 0 | 0 | C | 1 |
4 | 55 | 28 | 1 | 0 | A | 0 |
Ahora procedamos a renombrar las variables, para un mayor entendimiento.
df = df.rename(columns={'age':'x1',
"TRT" : "tratamiento",
"x2" : "xA",
"x3" : "xB"})
df.head()
y | x1 | xA | xB | tratamiento | xC | |
---|---|---|---|---|---|---|
0 | 56 | 21 | 1 | 0 | A | 0 |
1 | 41 | 23 | 0 | 1 | B | 0 |
2 | 40 | 30 | 0 | 1 | B | 0 |
3 | 28 | 19 | 0 | 0 | C | 1 |
4 | 55 | 28 | 1 | 0 | A | 0 |
Procedamos a ver que sucede con los datos.
# Algunas caracteristicas sobre la columna tratamiento
df["tratamiento"].describe()
count 36 unique 3 top A freq 12 Name: tratamiento, dtype: object
# Veamos dichas caracteristicas para cada una de las columnas
for i in df.columns:
print(df[i].describe(), end = "\n", sep = "|")
count 36.000000 mean 55.166667 std 12.415428 min 25.000000 25% 46.750000 50% 58.000000 75% 63.250000 max 73.000000 Name: y, dtype: float64 count 36.000000 mean 44.111111 std 14.628305 min 19.000000 25% 32.250000 50% 44.000000 75% 56.500000 max 67.000000 Name: x1, dtype: float64 count 36.000000 mean 0.333333 std 0.478091 min 0.000000 25% 0.000000 50% 0.000000 75% 1.000000 max 1.000000 Name: xA, dtype: float64 count 36.000000 mean 0.333333 std 0.478091 min 0.000000 25% 0.000000 50% 0.000000 75% 1.000000 max 1.000000 Name: xB, dtype: float64 count 36 unique 3 top A freq 12 Name: tratamiento, dtype: object count 36.000000 mean 0.333333 std 0.478091 min 0.000000 25% 0.000000 50% 0.000000 75% 1.000000 max 1.000000 Name: xC, dtype: float64
# Datos agrupados por efectividad
# Consideraremos solo las columnas del tratamiento
# y la efectividad
grupos = df[["tratamiento", "y"]]
# Agrupamos por tratamiento y observamos algunas caracteristicas
# por tratamiento y efectividad
grupos.groupby('tratamiento').describe()
y | ||||||||
---|---|---|---|---|---|---|---|---|
count | mean | std | min | 25% | 50% | 75% | max | |
tratamiento | ||||||||
A | 12.0 | 62.333333 | 6.386539 | 52.0 | 57.50 | 62.5 | 66.00 | 73.0 |
B | 12.0 | 51.916667 | 8.360713 | 40.0 | 45.75 | 51.5 | 58.50 | 64.0 |
C | 12.0 | 51.250000 | 17.189452 | 25.0 | 35.50 | 54.5 | 64.25 | 71.0 |
# Varianza de la efectividad por tratamiento
grupos.groupby('tratamiento').var()
y | |
---|---|
tratamiento | |
A | 40.787879 |
B | 69.901515 |
C | 295.477273 |
Muestralmente observamos lo siguiente:
El número de observaciones es el mismo $(n = 12)$ para cada Tratamiento.
En promedio la efectividad del tratamineto $A$ es de 62.33, la del tratamiento $B$ es de 51.92 y por último la del tratamiento $C$ es de 51.25.
Notamos mayor variabilidad de la efectividad en los datos del tratamiento $C$ y menor variabilidad en los datos del tratamiento $A$.
Procedemos a realizar un análisis gráfico de nuestros datos:
import seaborn as sns
import matplotlib.pyplot as plt
# Configuramos un estilo de graficacion
plt.style.use('seaborn')
# Configuramos el tamagnio de salida de nuestros graficos
plt.rcParams['figure.figsize'] = [14, 6]
# Mostraremos dos graficos
fig, axs = plt.subplots(ncols=2)
sns.scatterplot(data=df, x="y", y="x1", hue=df.tratamiento.tolist(), ax=axs[0])
sns.boxplot(data=df, x="tratamiento", y="y", hue=df.tratamiento.tolist(), ax=axs[1])
plt.show()
La gráfica anterior sugiere que la $edad$ influye en la Efectividad de cada tratamiento (se nota una pendiente $\neq$ 0 en cada uno de los grupos).
Visualmente observamos lo siguiente:
Para edades menores a 40 años, la efectividad del tratamiento A parece ser la mejor, la efectividad del tratamiento B parece ser la segunda mejor y la peor efectividad parece ser la del tratamiento C.
Para edades mayores a 40 años, no parece que la eficacia sea diferente dependiento del tratamiento.
El modelo de RLM con interacciones esta parametrizado de la siguiente manera
$y_i = \beta_0 + \underbrace{\beta_1 x_{i,1}}_{Edad} + \underbrace{\beta_2 x_{i,B} + \beta_3 x_{i,C}}_{Tratamiento} + \underbrace{\beta_4 x_{i,B}x_{i,1} + \beta_5 x_{i.1}x_{i,C}}_{Tratamiento} + \epsilon_i$
donde $\epsilon_i \sim \text{N}\left ( 0, \sigma^2 \right ), \forall i\in \left \{ 1 , \dots , n \right \}. $
Observación: En la parametrización del modelo se consideró al tratamiento A como el nivel de referencia, entonces
# Para realizar el ajuste del modelo utilizamos la funcion ols() como sigue
# --- modelo --- -- Ajuste --
model = ols('y ~ x1 + xB + xC + x1:xB + x1:xC', data=df).fit()
# Veamos la informacion de la tabla anova obtenida de nuestro
# ajuste del modelo anterior
sm.stats.anova_lm(model, typ=2)
sum_sq | df | F | PR(>F) | |
---|---|---|---|---|
x1 | 3302.258622 | 1.0 | 214.363829 | 3.331105e-15 |
xB | 602.599365 | 1.0 | 39.117320 | 6.862837e-07 |
xC | 620.533401 | 1.0 | 40.281495 | 5.311571e-07 |
x1:xB | 42.282024 | 1.0 | 2.744708 | 1.080006e-01 |
x1:xC | 641.064054 | 1.0 | 41.614229 | 3.982473e-07 |
Residual | 462.147738 | 30.0 | NaN | NaN |
Obtenemos los siguientes parámetros
model.params
Intercept 47.515591 x1 0.330507 xB -18.597385 xC -41.304210 x1:xB 0.193177 x1:xC 0.702884 dtype: float64
# Podemos ver mas informacion sobre nuestro ajuste del modelo
print(model.summary())
OLS Regression Results ============================================================================== Dep. Variable: y R-squared: 0.914 Model: OLS Adj. R-squared: 0.900 Method: Least Squares F-statistic: 64.04 Date: Tue, 20 Sep 2022 Prob (F-statistic): 4.26e-15 Time: 21:22:29 Log-Likelihood: -97.024 No. Observations: 36 AIC: 206.0 Df Residuals: 30 BIC: 215.5 Df Model: 5 Covariance Type: nonrobust ============================================================================== coef std err t P>|t| [0.025 0.975] ------------------------------------------------------------------------------ Intercept 47.5156 3.825 12.422 0.000 39.703 55.328 x1 0.3305 0.081 4.056 0.000 0.164 0.497 xB -18.5974 5.416 -3.434 0.002 -29.658 -7.537 xC -41.3042 5.085 -8.124 0.000 -51.688 -30.920 x1:xB 0.1932 0.117 1.657 0.108 -0.045 0.431 x1:xC 0.7029 0.109 6.451 0.000 0.480 0.925 ============================================================================== Omnibus: 2.593 Durbin-Watson: 1.633 Prob(Omnibus): 0.273 Jarque-Bera (JB): 1.475 Skew: -0.183 Prob(JB): 0.478 Kurtosis: 2.079 Cond. No. 577. ============================================================================== Notes: [1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
donde el p-value de la prueba $F$ nos dice que el modelo tiene sentido. Al parecer la interacción entre la edad y eñ tratamiento B no está aportando suficiente información al modelo, lo cual nos indica que tal vez podríamos quitar dicha interacción y reducir el modelo.
Por lo tanto, tenemos la siguiente parametrización,
$$\begin{align*} \mathbb{E}\left [ y_i | \text{ Tratamiento, } x_{i,1} = x_1^* \right ] &= \begin{cases} 47.51 + 0.33 x_1^* & \text{ si el individuo } i \text{ recibio el tratamiento } A, \\ (47.51 - 18.59) + (0.33+0.19) x_1^* & \text{ si el individuo } i \text{ recibio el tratamiento } B, \\ (47.51 - 41.30) + (0.33+0.70) x_1^* & \text{ si el individuo } i \text{ recibio el tratamiento } C \end{cases}\\ &= \begin{cases} 47.51 + 0.33 x_1^* & \text{ si el individuo } i \text{ recibio el tratamiento } A, \\ 28.91 + 0.53 x_1^* & \text{ si el individuo } i \text{ recibio el tratamiento } B, \\ 6.21 + 1.03 x_1^* & \text{ si el individuo } i \text{ recibio el tratamiento } C \end{cases} \end{align*}$$Generemos la gráfica de cada función observada antes
# Rango de graficacion
x = range(10, 80)
# Promedio de la eficacia del tratamiento considerando
# el tratamiento A
def tA(x):
return 47.51 + 0.33 * x
# Promedio de la eficacia del tratamiento considerando
# el tratamiento B
def tB(x):
return 28.91 + 0.53 * x
# Promedio de la eficacia del tratamiento considerando
# el tratamiento C
def tC(x):
return 6.21 + 1.03 * x
# Consideramos tres dataframes para separar a los individuos
# de cada tratamiento
df1 = df[df["tratamiento"] == "A"]
df2 = df[df["tratamiento"] == "B"]
df3 = df[df["tratamiento"] == "C"]
# Graficamos el ajuste de las rectas para cada uno de los tratamientos
plt.plot(x, [tA(i) for i in x])
plt.scatter(df1.x1, df1.y)
plt.plot(x, [tB(i) for i in x])
plt.scatter(df2.x1, df2.y)
plt.plot(x, [tC(i) for i in x])
plt.scatter(df3.x1, df3.y)
plt.legend(['Modelo Tratamiento A', 'Tratamiento A',
"Modelo Tratamiento B", 'Tratamiento B',
"Modelo Tratamiento C", 'Tratamiento C'])
plt.xlabel("Edad")
plt.ylabel("Efectividad")
Text(0, 0.5, 'Efectividad')
Para poder reducir el modelo, es necesario verificar:
$H_0 : \beta_1 = \beta_2 = \cdots = \beta_5 = 0 \text{ v.s. } H_1 : \beta_i \neq 0, \text{ para alguna } i = 1, \dots , 5. $
Matricialmente (prueba lineal general):
$$H_0: \begin{pmatrix} 0 & 1 & 0 & 0 & 0 & 0\\ 0 & 0 & 1 & 0 & 0 & 0\\ 0 & 0 & 0 & 1 & 0 & 0\\ 0 & 0 & 0 & 0 & 1 & 0\\ 0 & 0 & 0 & 0 & 0 & 1 \end{pmatrix}\begin{pmatrix} \beta_0 \\ \beta_1 \\ \beta_2 \\ \beta_3 \\ \beta_4\\ \beta_5 \end{pmatrix} = \begin{pmatrix} 0\\ 0\\ 0\\ 0\\ 0 \end{pmatrix} \text{ v.s. } H_1:\beta \neq 0 \text{ para alguna } i = \left \{ 1, 2, 3, 4, 5 \right \}$$En pyhton utilizamos el atributo f_pvalue.
f = model.f_pvalue
f
4.264070157904808e-15
Por lo tanto, rechazamos la hipotesis nula y aceptamos la alternativa. y concluimos que: Con una confianza del 95% la variable Edad (x1) o los distintos factores de la variable Tratamiento (xA, xB, xC) nos ayudan a modelar el valor promedio de la efectividad del tratamiento, esto es, el modelo tiene sentido.
Forma alternativa de manera matricial
A = np.identity(len(model.params))
A = A[1:, :]
A
array([[0., 1., 0., 0., 0., 0.], [0., 0., 1., 0., 0., 0.], [0., 0., 0., 1., 0., 0.], [0., 0., 0., 0., 1., 0.], [0., 0., 0., 0., 0., 1.]])
model.f_test(A)
<class 'statsmodels.stats.contrast.ContrastResults'> <F test: F=64.04253685100939, p=4.264070157905692e-15, df_denom=30, df_num=5>
$H_0 : \beta_4 = 0 \text{ y } \beta_5 = 0 \text{ v.s.} H_1 : \beta_4 \neq 0 \text{ o } \beta_5 \neq 0 $.
Matricialmente (prueba lineal general):
$$H_0: \begin{pmatrix} 0 & 0 & 0 & 0 & 1 & 0\\ 0 & 0 & 0 & 0 & 0 & 1 \end{pmatrix}\begin{pmatrix} \beta_0 \\ \beta_1 \\ \beta_2 \\ \beta_3 \\ \beta_4\\ \beta_5 \end{pmatrix} = \begin{pmatrix} 0\\ 0 \end{pmatrix} \text{ v.s. } H_1:\beta_4 \neq 0 \text{ o } \beta_5 \neq 0$$Para esté apartado lo pasaremos a su lenguaje matricial
A = [[0,0,0,0,1,0], [0,0,0,0,0,1]]
model.f_test(A)
<class 'statsmodels.stats.contrast.ContrastResults'> <F test: F=22.83123711361978, p=9.410457463646548e-07, df_denom=30, df_num=2>
Por lo tanto rechazamos la hipotesis nula y concluimos que con una confianza del 95%
$\beta_4 \neq 0 \text{ o } \beta_5 \neq 0$
El efecto de la variable Edad (x1) es distinto para al menos un factor de la variable Tratamiento, es decir, para algun tratamiento.
Al menos hay una pendiente que es distinta.
Notar que aquí se incluyen las tres hipótesis individuales asociadas a:
$$\begin{cases} H_{0,1}: & \text{ Las pendientes del tratamiento } A \text{ y } B \text{ son iguales} \\ H_{0,2}: & \text{ Las pendientes del tratamiento } A \text{ y } C \text{ son iguales} \\ H_{0,3}: & \text{ Las pendientes del tratamiento } B \text{ y } C \text{ son iguales} \end{cases}$$En términos de los parametros:
$$\begin{cases} H_{0,1}: & \beta_1 = \beta_1 + \beta_4 \\ H_{0,1}: & \beta_1 = \beta_1 + \beta_5 \\ H_{0,1}: & \beta_1 + \beta_4 = \beta_1 + \beta_5 \end{cases}$$Es decir,
$$\begin{cases} H_{0,1}: & \beta_4 = 0 \\ H_{0,1}: & \beta_5 = 0 \\ H_{0,1}: & \beta_4 - \beta_5 = 0 \end{cases}$$Matricialmente
$$H_0: \begin{pmatrix} 0 & 0 & 0 & 0 & 1 & 0\\ 0 & 0 & 0 & 0 & 0 & 1\\ 0 & 0 & 0 & 0 & 1 & -1 \end{pmatrix}\begin{pmatrix} \beta_0\\ \beta_1\\ \beta_2\\ \beta_3\\ \beta_4\\ \beta_5 \end{pmatrix}= \begin{pmatrix} 0\\ 0\\ 0 \end{pmatrix} \text{ v.s. } H_1: \beta_4 \neq 0 \text{ o } \beta_5 \neq 0 \text{ o } \beta_4 -\beta_5 \neq 0$$En R utilizamos la librería multcomp
. Trabajaremos con dicha librería de R pero utilizando Python, para lo cual ocuparemos la librería de Python rpy2
import rpy2
print(rpy2.__version__)
3.5.4
Generamos el ajuste del modelo en R
# Mandamos a llamar la estructura de vector en R
# para que podemos utilizarla
from rpy2.robjects import FloatVector
# Importamos una funcion para poder utilizar librerias de R
from rpy2.robjects.packages import importr
# Objetos de R
from rpy2 import robjects
# Consideramos (importamos) las librerias de R que ocuparemos
stats = importr('stats')
base = importr('base')
multcomp = importr('multcomp')
Unable to determine R home: [WinError 2] El sistema no puede encontrar el archivo especificado
# Convertimos cada una de nuestras variables a un objeto vector de R.
y = FloatVector(df.y)
x1 = FloatVector(df.x1)
xB = FloatVector(df.xB)
xC = FloatVector(df.xC)
las variables que hemos definido antes son variables dentro de Python. Requerimos que dichas variables sean variables dentro de R, para lo cual escribimos
# Variables de Python asignadas a variables de R (R objetos)
# dentro de un ambiente global.
# (robjects.globalenv["y"] <- y)
robjects.globalenv["y"] = y
robjects.globalenv["x1"] = x1
robjects.globalenv["xB"] = xB
robjects.globalenv["xC"] = xC
# Realizamos el ajuste del modelo utilizando la libreria stats de R
# como si fuera una libreria de Python
Ajuste = stats.lm(formula = "y ~ x1 + xB + xC + x1:xB + x1:xC")
Si bien el modelo anterior ya lo habíamos generado, éste fue generado en un ambiente de Python, lo que hacemos ahora es generar el ajuste del modelo en un ambiente de R. Continuando
# Summary del ajuste
print(base.summary(Ajuste))
Call: (function (formula, data, subset, weights, na.action, method = "qr", model = TRUE, x = FALSE, y = FALSE, qr = TRUE, singular.ok = TRUE, contrasts = NULL, offset, ...) { ret.x <- x ret.y <- y cl <- match.call() mf <- match.call(expand.dots = FALSE) m <- match(c("formula", "data", "subset", "weights", "na.action", "offset"), names(mf), 0L) mf <- mf[c(1L, m)] mf$drop.unused.levels <- TRUE mf[[1L]] <- quote(stats::model.frame) mf <- eval(mf, parent.frame()) if (method == "model.frame") return(mf) else if (method != "qr") warning(gettextf("method = '%s' is not supported. Using 'qr'", method), domain = NA) mt <- attr(mf, "terms") y <- model.response(mf, "numeric") w <- as.vector(model.weights(mf)) if (!is.null(w) && !is.numeric(w)) stop("'weights' must be a numeric vector") offset <- model.offset(mf) mlm <- is.matrix(y) ny <- if (mlm) nrow(y) else length(y) if (!is.null(offset)) { if (!mlm) offset <- as.vector(offset) if (NROW(offset) != ny) stop(gettextf("number of offsets is %d, should equal %d (number of observations)", NROW(offset), ny), domain = NA) } if (is.empty.model(mt)) { x <- NULL z <- list(coefficients = if (mlm) matrix(NA_real_, 0, ncol(y)) else numeric(), residuals = y, fitted.values = 0 * y, weights = w, rank = 0L, df.residual = if (!is.null(w)) sum(w != 0) else ny) if (!is.null(offset)) { z$fitted.values <- offset z$residuals <- y - offset } } else { x <- model.matrix(mt, mf, contrasts) z <- if (is.null(w)) lm.fit(x, y, offset = offset, singular.ok = singular.ok, ...) else lm.wfit(x, y, w, offset = offset, singular.ok = singular.ok, ...) } class(z) <- c(if (mlm) "mlm", "lm") z$na.action <- attr(mf, "na.action") z$offset <- offset z$contrasts <- attr(x, "contrasts") z$xlevels <- .getXlevels(mt, mf) z$call <- cl z$terms <- mt if (model) z$model <- mf if (ret.x) z$x <- x if (ret.y) z$y <- y if (!qr) z$qr <- NULL z })(formula = "y ~ x1 + xB + xC + x1:xB + x1:xC") Residuals: Min 1Q Median 3Q Max -6.4366 -2.7637 0.1887 2.9075 6.5634 Coefficients: Estimate Std. Error t value Pr(>|t|) (Intercept) 47.51559 3.82523 12.422 2.34e-13 *** x1 0.33051 0.08149 4.056 0.000328 *** xB -18.59739 5.41573 -3.434 0.001759 ** xC -41.30421 5.08453 -8.124 4.56e-09 *** x1:xB 0.19318 0.11660 1.657 0.108001 x1:xC 0.70288 0.10896 6.451 3.98e-07 *** --- Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 Residual standard error: 3.925 on 30 degrees of freedom Multiple R-squared: 0.9143, Adjusted R-squared: 0.9001 F-statistic: 64.04 on 5 and 30 DF, p-value: 4.264e-15
Procedemos a crear una matriz de R para poder realizar la prueba simultánea
# Objeto que "vive" en Python pero es de R
K = robjects.FloatVector([0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,1,-1])
# Objeto de R
K = robjects.r['matrix'](K, ncol=6, nrow=3)
print(K)
# Objeto que "vive" en Python pero es de R
m = robjects.FloatVector([0,0,0])
# Objeto de R
m = robjects.r['matrix'](m, ncol=1)
print(m)
[,1] [,2] [,3] [,4] [,5] [,6] [1,] 0 0 0 0 1 0 [2,] 0 0 0 0 0 1 [3,] 0 0 0 0 1 -1 [,1] [1,] 0 [2,] 0 [3,] 0
Alternativa de lo anterior:
robjects.globalenv["K"] = K
robjects.globalenv["m"] = m
Continuando
# Realizamos la prueba lineal general para pruebas simultáneas
print(base.summary(multcomp.glht(Ajuste, linfct=K, rhs=m)))
# base: R base
Simultaneous Tests for General Linear Hypotheses Fit: (function (formula, data, subset, weights, na.action, method = "qr", model = TRUE, x = FALSE, y = FALSE, qr = TRUE, singular.ok = TRUE, contrasts = NULL, offset, ...) { ret.x <- x ret.y <- y cl <- match.call() mf <- match.call(expand.dots = FALSE) m <- match(c("formula", "data", "subset", "weights", "na.action", "offset"), names(mf), 0L) mf <- mf[c(1L, m)] mf$drop.unused.levels <- TRUE mf[[1L]] <- quote(stats::model.frame) mf <- eval(mf, parent.frame()) if (method == "model.frame") return(mf) else if (method != "qr") warning(gettextf("method = '%s' is not supported. Using 'qr'", method), domain = NA) mt <- attr(mf, "terms") y <- model.response(mf, "numeric") w <- as.vector(model.weights(mf)) if (!is.null(w) && !is.numeric(w)) stop("'weights' must be a numeric vector") offset <- model.offset(mf) mlm <- is.matrix(y) ny <- if (mlm) nrow(y) else length(y) if (!is.null(offset)) { if (!mlm) offset <- as.vector(offset) if (NROW(offset) != ny) stop(gettextf("number of offsets is %d, should equal %d (number of observations)", NROW(offset), ny), domain = NA) } if (is.empty.model(mt)) { x <- NULL z <- list(coefficients = if (mlm) matrix(NA_real_, 0, ncol(y)) else numeric(), residuals = y, fitted.values = 0 * y, weights = w, rank = 0L, df.residual = if (!is.null(w)) sum(w != 0) else ny) if (!is.null(offset)) { z$fitted.values <- offset z$residuals <- y - offset } } else { x <- model.matrix(mt, mf, contrasts) z <- if (is.null(w)) lm.fit(x, y, offset = offset, singular.ok = singular.ok, ...) else lm.wfit(x, y, w, offset = offset, singular.ok = singular.ok, ...) } class(z) <- c(if (mlm) "mlm", "lm") z$na.action <- attr(mf, "na.action") z$offset <- offset z$contrasts <- attr(x, "contrasts") z$xlevels <- .getXlevels(mt, mf) z$call <- cl z$terms <- mt if (model) z$model <- mf if (ret.x) z$x <- x if (ret.y) z$y <- y if (!qr) z$qr <- NULL z })(formula = "y ~ x1 + xB + xC + x1:xB + x1:xC") Linear Hypotheses: Estimate Std. Error t value Pr(>|t|) 1 == 0 0.1932 0.1166 1.657 0.238 2 == 0 0.7029 0.1090 6.451 <0.001 *** 3 == 0 -0.5097 0.1104 -4.617 <0.001 *** --- Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 (Adjusted p values reported -- single-step method)
Fijando $\alpha = 0,05$, obtenemos los siguientes resultados de forma simultanea:
pvalue = 0,238 > 0,05, asociado a la hipótesis $H_{01} : \beta_4 = 0$. No se rechaza H01 por lo tanto no hay evidencia estadística suficiente para considerar que $\beta_4 \neq 0$, de este modo, es plausible identificar que $\beta_4 = 0$.
pvalue = 0,001 < 0,05, asociado a la hipótesis $H_{02} : \beta_5 = 0$. Se rechaza H02 por lo tanto hay evidencia estadística suficiente para considerar que $\beta_5 \neq 0$
pvalue = 0,001 < 0,05, asociado a la hipótesis $H_{03} : \beta_4 − \beta_5 = 0$. Se rechaza $H_{02}$ por lo tanto hay evidencia estadística suficiente para considerar que $\beta_4 \neq \beta_5$. En otras palabras, de forma simultánea las pendientes asociadas a los tratamientos A y B podrían ser las mismas y por otro lado es posible asumir que la pendiente asociada al tratamiento C es distinta tanto a la pendiente del tratamiento A como a la pendiente del tratamiento B.
Por lo tanto,
$\hat{\mathbb{E}}\left [ y_i; Tratamiento, Edad \right ] = \hat{\beta}_0 +\hat{\beta}_1x_{i,1} + \hat{\beta}_2x_{i,B} + \hat{\beta}_3x_{i,C} + \hat{\beta}_5x_ix_{i,C}$
Consideramos entonces el ajuste del modelo reducido, regresamos a programar solo en "Python".
# Ajuste del modelo reducido
model_r = ols('y ~ x1 + xB + xC + x1:xC', data=df).fit()
sm.stats.anova_lm(model, typ=2)
sum_sq | df | F | PR(>F) | |
---|---|---|---|---|
x1 | 3302.258622 | 1.0 | 214.363829 | 3.331105e-15 |
xB | 602.599365 | 1.0 | 39.117320 | 6.862837e-07 |
xC | 620.533401 | 1.0 | 40.281495 | 5.311571e-07 |
x1:xB | 42.282024 | 1.0 | 2.744708 | 1.080006e-01 |
x1:xC | 641.064054 | 1.0 | 41.614229 | 3.982473e-07 |
Residual | 462.147738 | 30.0 | NaN | NaN |
# Parametros obtenidos del ajuste del modelo
model_r.params
Intercept 43.285243 x1 0.424864 xB -10.027208 xC -37.073862 x1:xC 0.608526 dtype: float64
print(model_r.summary())
OLS Regression Results ============================================================================== Dep. Variable: y R-squared: 0.907 Model: OLS Adj. R-squared: 0.894 Method: Least Squares F-statistic: 75.14 Date: Tue, 27 Sep 2022 Prob (F-statistic): 1.68e-15 Time: 10:00:30 Log-Likelihood: -98.600 No. Observations: 36 AIC: 207.2 Df Residuals: 31 BIC: 215.1 Df Model: 4 Covariance Type: nonrobust ============================================================================== coef std err t P>|t| [0.025 0.975] ------------------------------------------------------------------------------ Intercept 43.2852 2.927 14.787 0.000 37.315 49.255 x1 0.4249 0.060 7.093 0.000 0.303 0.547 xB -10.0272 1.648 -6.085 0.000 -13.388 -6.667 xC -37.0739 4.519 -8.204 0.000 -46.290 -27.858 x1:xC 0.6085 0.095 6.374 0.000 0.414 0.803 ============================================================================== Omnibus: 2.617 Durbin-Watson: 1.804 Prob(Omnibus): 0.270 Jarque-Bera (JB): 1.661 Skew: -0.284 Prob(JB): 0.436 Kurtosis: 2.113 Cond. No. 369. ============================================================================== Notes: [1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
plt.rcParams['figure.figsize'] = [14, 10]
# Graficos: variable independiente vs variables dependientes
fig = sm.graphics.plot_partregress_grid(model_r)
fig.tight_layout(pad=1.0)
eval_env: 1 eval_env: 1 eval_env: 1 eval_env: 1 eval_env: 1
plt.rcParams['figure.figsize'] = [12, 6]
sns.histplot(model_r.resid, bins = 10)
plt.show()
Lo cual no nos brinda alguna información relevante sobre la distribución de los residuales. Mejoramos el gráfico anterior, pero antes calculamos la media y la desviación estándar de los residuales
from scipy import stats as st
mu, std = st.norm.fit(model_r.resid)
mu, std
(6.17777434146976e-14, 3.743252307963197)
Luego:
fig, ax = plt.subplots()
# Grafico de los residuales:
# histograma junto con la densidad
sns.histplot(x=model_r.resid, ax=ax, stat="density", linewidth=0, kde=True)
ax.set(title="Distribución de los residuales", xlabel="Residuales")
# Grafico correspondiente a una densidad proveniente de una distribucion
# normal (mu, std^2)
xmin, xmax = plt.xlim() # valores maximos para el intervalo de graficacion del eje X
x = np.linspace(xmin, xmax, 100) # generamos algunos valores para la x
p = st.norm.pdf(x, mu, std) # Funcion de densidad obtenida de una normal (mu, std^2)
sns.lineplot(x=x, y=p, color="red", ax=ax) # Graficamos la densidad anterior
plt.show()
Lo cual, al parecer, nos da indicios de que la distribución de los residuales no se distribuye normal. Completamos mediante el gráfico de Q-Qplot
sm.qqplot(model_r.resid, line='s');
Al parecer los datos sí se están distribuyendo normal.
Pero recordemos que los gráficos solo nos da un idea detras del comportamiento, si queremos mayor certeza deberemos de aplicar pruebas de hipótesis.
Pero las graficas no garantizan nada
Los test Shapiro-Wilk test y D'Agostino's K-squared test son dos de los test de hipótesis más empleados para analizar la normalidad. En ambos, se considera como hipótesis nula que los datos proceden de una distribución normal.
El p-value de estos test indica la probabilidad de obtener unos datos como los observados si realmente procediesen de una población con una distribución normal con la misma media y desviación que estos. Por lo tanto, si el p-value es menor que un determinado valor (típicamente 0.05), entonces se considera que hay evidencias suficientes para rechazar la normalidad.
# Shapiro-Wilk test
# ==============================================================================
shapiro_test = st.shapiro(model_r.resid)
shapiro_test
ShapiroResult(statistic=0.9656770825386047, pvalue=0.319449245929718)
# D'Agostino's K-squared test
# ==============================================================================
k2, p_value = st.normaltest(model_r.resid)
print(f"Estadístico = {k2}, p-value = {p_value}")
Estadístico = 2.6169126708165154, p-value = 0.27023688969450765
Por lo tanto, los residuales tienen distribución normal.
La suposición de homoscedasticidad es una suposición vital. Si esta suposición es violada, entonces los errores estándar serán sesgados. Los errores estándar se utilizan para realizar pruebas de significación y calcular los intervalos de confianza.
Esto puede ser probado usando un residual vs. valores ajustados, mirando un gráfico de dispersión (si una forma de como está presente entonces heteroscedasticidad está presente), o mediante el uso de una prueba estadística como Bruesch-Pagan, prueba de Cook-Weisberg, o prueba general blanca. En este ejemplo, se utilizará la prueba Bruesch-Pagan.
sm.graphics.plot_fit(model_r, 1, vlines=False);
import statsmodels.stats.diagnostic as sms
name = ['Lagrange multiplier statistic', 'p-value',
'f-value', 'f p-value']
test = sms.het_breuschpagan(model.resid, model.model.exog)
result = zip(name, test)
print(list(result))
[('Lagrange multiplier statistic', 2.9958191453619496), ('p-value', 0.7006304411123819), ('f-value', 0.5446253900782909), ('f p-value', 0.740988751661612)]
entonces sí se está cumpliendo el supuesto de la homocedasticidad.
¿La efectividad media de los tres tratamientos es la misma para todas las edades?
A = [[0,0,1, 0,0],
[0,0,0, 1,0],
[0,0,0, 0,1],
[0,0,1,-1,0]]
A
model_r.f_test(A)
C:\Users\fjza9\anaconda3\lib\site-packages\statsmodels\base\model.py:1871: ValueWarning: covariance of constraints does not have full rank. The number of constraints is 4, but rank is 3 warnings.warn('covariance of constraints does not have full '
<class 'statsmodels.stats.contrast.ContrastResults'> <F test: F=30.034106752500577, p=2.663200219438571e-09, df_denom=31, df_num=3>
¿El efecto de la edad en la efectividad media es la misma para los tres tratamientos?
A = [[0,0,0,0,1]]
A
model_r.f_test(A)
<class 'statsmodels.stats.contrast.ContrastResults'> <F test: F=40.63101538056917, p=4.2498150170287766e-07, df_denom=31, df_num=1>
Considere el archivo Ejercicio_ANCOVA.
Todo lo anterior deberá ser realizado en PYTHON.