Appendix A — Herramientas

Se han aglutinado en este anexo diversas herramientas transversales a varios Labs.

A.1 Paquetes

El material original del Lab3 Regresión lineal empieza comentando las nociones básicas sobre cargar paquetes (o instalarlos cuando es necesario), pero en todos los Labs es necesario cargar paquetes. Aquí, simplemente recordamos que

  • Al iniciar una sesión de R se cargan unos pocos paquetes, los básicos, por lo que el resto de paquetes es necesario cargarlos en cada sesión.

  • la función para cargar un paquete en una sesión es library().

library(ISLR2)
Warning: package 'ISLR2' was built under R version 4.4.1
  • Basta con instalar un paquete 1 vez. Aunque existe una función para instalar paquetes, como es una tarea puntual mi recomendación es hacerlo mediante menús. En RStudio, Pestaña Packages > Install

A.2 Escritura de funciones

Aunque R viene con muchas funciones útiles, y hay muchas más funciones disponibles a través de los paquetes de R, en ocasiones nos interesará realizar una operación para la que no se dispone de ninguna función. Pues bien, es posible crear/escribir funciones nuevas.

En el material original se crean varias funciones:

  • El Lab3 Regresión Lineal dedica el último apartado (pequeño) a la creación/escritura de funciones. Se crea LoadLibraries() que lee los paquetes ISLR2 y MASS.
  • En el Lab5 Remuestreo: se crea boot.fn()
  • En el Lab6 Selección de variables: se crea predict.regsubsets()
  • En el Lab9 SVM: se crea rocplot()

Aquí se ha decidido explicar de forma práctica y sencilla cómo escribir una función. Aprovechamos el ejemplo de creación de la función LoadLibraries().

El nombre que le vamos a dar a la función es LoadLibraries. Para decirle a R que es una función utilizamos function() cuya sintaxis es:

function( arglist ) expr

Es decir hay que decir que argumentos espera la función: arglist, y en expr la expresión o expresiones que queremos que ejecute la función. Si son varias expresiones se deben escribir entre llaves, { al principio y } al final de la función.

LoadLibraries <- function() {
 library(ISLR2)
 library(MASS)
 print("Los paquetes ISLR2 y MASS han sido cargados.")
}

En este ejemplo sencillo, no hay arglist pues la función que se va a definir no necesita argumentos. Y se utilizan llaves dado que hay 3 expresiones que se van a ejecutar: cargar los dos paquetes e imprimir en la consola/pantalla que se han cargado.

Ahora, si llamamos a la función, se cargan los paquetes y se imprime la sentencia indicada.

LoadLibraries()

Adjuntando el paquete: 'MASS'
The following object is masked from 'package:ISLR2':

    Boston
[1] "Los paquetes ISLR2 y MASS han sido cargados."

Otro ejemplo es la función boot.fn definida en el Lab5 Remuestreo:

boot.fn <- function(data, index) ...

Como se ve se define con 2 argumentos: data e index, que después se utilizan en la expresión (se omite por ser específica del tema, no tener aquí el contexto suficiente). Ahora bien, cabe mencionar que al ser una sola expresión la que se va a ejecutar se define sin llaves (véanse más detalles en el Lab5 Remuestreo).

A.3 Bucle usando for()

En el material original se definen varios bucles usando esta función for():

  • en el Lab5 Remuestreo
  • en el Lab6 Selección de variables donde además uno de los bucles es doble, es decir, se anida un bucle dentro de otro.

Esta función for() sirve para repetir un procedimiento de forma iterativa. Su sintaxis es:

for(var in seq) expr

donde var es la variable que va cambiando de valor según lo indicado en seq. Para cada valor que tome var ejecuta la expresión indica en expr que puede constar de una sola línea o varias (para lo que es necesario usar llaves de apertura y cierre).

Ejemplo sencillo: imprimir por pantalla una serie de números. Una función para imprimir por pantalla es print(), basta utilizar i como variable índice y definir la serie de números, por ejemplo del 1 al 6, o los números 3, 7, 8 y 12.

for (i in 1:6) print(i)
[1] 1
[1] 2
[1] 3
[1] 4
[1] 5
[1] 6
for (i in c(3, 7, 8, 12)) print(i)
[1] 3
[1] 7
[1] 8
[1] 12

Ejemplo algo más complejo: guardar en un par de vectores una serie de números. Previamente es necesario inicializar los vectores (si no R no sabe a qué se refieren los objetos vector1 y vector2)

vector1 <- vector2 <- rep(0,5)
for (i in 1:5) {
  vector1[i] = i-1
  vector2[i] = vector1[i]^2
}
data.frame(vector1, vector2)
  vector1 vector2
1       0       0
2       1       1
3       2       4
4       3       9
5       4      16

Nótese que se han inicializado los dos vectores a la vez. El vector1 se crea utilizando la variable índice i, restándole 1 a cada elemento, es decir que el elemento i-ésimo de dicho vector, vector1[i], es i-1. El vector2 se crea utilizando el vector1, elevando cada elemento i al cuadrado.

Nota: Estos mismos vectores se pueden definir de otras maneras alternativas. Una de ellas sería:

vector1 <- vector2 <- rep(0,5)
for (i in 0:4) {
  vector1[i+1] = i
}
vector2 <- vector1^2
data.frame(vector1, vector2)
  vector1 vector2
1       0       0
2       1       1
3       2       4
4       3       9
5       4      16

Nótese la necesidad de poner i+1 en el índice para obtener el mismo resultado que antes.

A.4 Datos de entrenamiento y test

Para crear, a partir de un conjunto de datos, dos subconjuntos (completamente separados), uno para utilizar como datos de entrenamiento (train) y otro para utilizarlos como datos de test/prueba/validación (test), tenemos varios opciones:

  • Crear un vector de tipo booleano
    (aparece en el Lab4 Clasificación)

  • Utilizar la función sample()
    (aparece en Lab5 Remuestreo, Lab6 Selección de variables, Lab8 Árboles y Lab9 SVM)

  • Utilizar una variable índice, bien usando la función sample() anterior, bien usando los valores que se deseen, por ejemplo los números enteros entre \(1\) y \(n\).
    (aparece en el Lab4 Clasificación)

De las tres opciones, en las que se use sample() es la que se puede considerar aleatoria. Ahora bien, cualquier enfoque funciona igualmente bien y es válido.

Nota técnica: Para ajustar los modelos se usará sólo el subconjunto de observaciones train:

  • utilizando el argumento subset: subset = train (aparece en el Lab6 Selección de variables),
  • utilizando expresiones del tipo: Hitters[train, ], (aparece en el Lab6 Selección de variables)

A.4.1 con vector booleano

Se implementa esta estrategia utilizando inteligentemente alguna de las variables del conjunto de datos.

Por ejemplo, en el Lab4 Clasificación, se dispone de los datos Smarket que contienen la variable Year que toma valores de 2001 a 2005 (en total 1250 valores). Se pueden tomar los datos de los años 2001 a 2004 como de entrenamiento y los de 2005 como de test. ¿Cómo se puede hacer esta división con R?… Usando esta estrategia de vector booleano: creando primero un vector correspondiente a las observaciones de 2001 a 2004, y usándolo después para crear el subconjunto de datos de observaciones de 2005.

train <- (Smarket$Year < 2005)
Smarket.2005 <- Smarket[!train, ]
  • El objeto train es un vector booleano, ya que sus elementos son TRUE y FALSE. Es un vector de 1250 elementos, correspondientes a todas las observaciones en nuestro conjunto de datos.

    • Los elementos del vector que corresponden a observaciones que ocurrieron antes de 2005 se establecen en TRUE,
    • mientras que los que corresponden a observaciones en 2005 se establecen en FALSE.
length(train)
[1] 1250
head(train) # aprovechando que están ordenadas por Year
[1] TRUE TRUE TRUE TRUE TRUE TRUE
tail(train) # aprovechando que están ordenadas por Year
[1] FALSE FALSE FALSE FALSE FALSE FALSE
  • Los vectores booleanos se pueden utilizar para obtener un subconjunto de filas o columnas de una matriz. Así,
dim(Smarket[train, ])
[1] 998   9
dim(Smarket.2005)
[1] 252   9
  • la sentencia Smarket[train, ] selecciona una submatriz del conjunto de datos del mercado de valores, correspondiente solo a las fechas anteriores a 2005, ya que esas son aquellas para las que los elementos de train son TRUE. El resultado de dim() indica que hay 998 observaciones del año 2001 al 2004.

  • Smarket[!train, ] produce una submatriz de los datos del mercado de valores que contiene solo las observaciones para las cuales train es FALSE—es decir, las observaciones con fechas en 2005. Esto es así por el símbolo ! (que significa distinto) y se utiliza aquí para invertir todos los elementos del vector booleano. Es decir, !train es un vector similar a train, excepto que los elementos que son TRUE en train se cambian a FALSE en !train, y los elementos que son FALSE en train se cambia a TRUE en !train. El resultado de dim() indica que hay 252 de tales observaciones.

A.4.2 con sample()

La función sample() permite obtener muestras aleatorias del conjunto x que se le especifique, y tantos valores como se le indiquen en size.

sample(x, size,...)

Al tener un componente aleatorio para garantizar la reproducibilidad de resultados se debe utilizar la función set.seed() que establece la semilla de aleatorización (véase más adelante en Funciones interesantes). Es decir, estableciendo la misma semilla aleatoria cualquier usuario obtendrá la misma división de conjuntos de entrenamiento y test.

Se ilustra aquí su uso:

numeros <- 0:9 + 0.5
set.seed(1)
(train1 <- sample(numeros, 4))
[1] 8.5 3.5 6.5 0.5
(train2 <- sample(numeros, 6))
[1] 1.5 6.5 2.5 5.5 9.5 7.5
set.seed(2)
(train2 <- sample(numeros, 6))
[1] 4.5 5.5 8.5 0.5 9.5 6.5

Con sample() se selecciona aleatoriamente del conjunto de observaciones las que se indiquen en size (segundo argumento): 4 en el primer caso, 6 en el segundo y tercero, por defecto sin reemplazamiento. Aquí se ha omitido el nombre del argumento (en tal caso debe ponerse en el orden indicado en la función), vea ?sample para más detalles. Por ejemplo, para saber cómo se puede indicar que la selección aleatoria sea con reemplazamiento, como hace el método bootstrap que también aparece en el Lab5 Remuestreo.

En dicho Lab5 Remuestreo se utiliza sample() de una forma ligeramente distinta a la aquí utilizada (los detalles se explican en dicha sección).

A.4.3 Con índices

En ocasiones la selección es no aleatoria, se elige por el orden/índice en el que están los datos.

Por ejemplo, en el Lab4 Clasificación se utilizan los datos Caravan donde se escogen para el conjunto de test las primeras 1000 observaciones, y para el conjunto de entrenamiento, las observaciones restantes. En dicho Lab, primero se escalan/estandarizan las variables (véase la función scale() en el apartado Funciones interesantes de este mismo anexo). Aquí se ilustra el uso de la selección mediante índices sobre el conjunto de datos original:

dim(Caravan)
[1] 5822   86
test <- 1:1000
test.X <- Caravan[test, ]
train.X <- Caravan[-test, ]
dim(test.X)
[1] 1000   86
dim(train.X)
[1] 4822   86
head(Caravan[,1])
[1] 33 37 37  9 40 23
head(test.X[,1])
[1] 33 37 37  9 40 23
Caravan[1001:1006 , 1]
[1] 40 26 10 38 39  9
head(train.X[,1])
[1] 40 26 10 38 39  9

El vector test se crea con valores desde 1 hasta 1000. Al escribir Caravan[test, ] se obtiene la submatriz de los datos que contienen las observaciones cuyos índices varían entre 1 y 1000, mientras que al escribir Caravan[-test, ] produce la submatriz que contiene las observaciones restantes, las que sus índices no están en ese rango. Para completitud, utilizando la función head() se muestran, para la primera variable, los primeros valores de ambos conjuntos y se comparan con los valores originales de Caravan.

A.5 NA valores faltantes

Aparece sólo en el Lab6 Selección de variables, pero es un tema que puede afectar a otros Labs.

Para algunos métodos estadísticos que el vector o el data.frame contenga NAs produce resultados no deseados. De una manera muy ilustrativa se explica la problemática de los valores faltantes en el libro R for Data Science, en este apartado: https://es.r4ds.hadley.nz/05-transform.html#valores-faltantes… Se dice que los NA ¡son “contagiosos”!

¿Cómo manejar/trabajar con datos/valores faltantes?

La función is.na() se puede utilizar para identificar las observaciones que faltan. Devuelve un vector de la misma longitud que el vector de entrada, con un TRUE para los elementos que faltan y un FALSE para los elementos que no faltan.

Aprovechamos el conjunto de datos Hitters que se utiliza en el Lab6 Selección de variables. Sabemos que en la variable Salary hay datos faltantes:

library(ISLR2)
head(Hitters$Salary)
[1]    NA 475.0 480.0 500.0  91.5 750.0
length(Hitters$Salary)
[1] 322
sum(is.na(Hitters$Salary))
[1] 59

La función head() muestra los primeros valores del vector, cuya length() es 322. Con la función sum() se obtienen los elementos/datos que faltan en Salary, concretamente falta el salario de 59 jugadores.

Como se ha mencionado, trabajar con variables/vectores con NAs puede conducir a resultados no deseados. En algunos casos, la solución es incluir un argumento:

mean(Hitters$Salary)
[1] NA
mean(Hitters$Salary, na.rm = TRUE)
[1] 535.9259

En otros casos hay que acudir a la función na.omit(), que elimina todas las filas de un data.frame que tengan valores faltantes en cualquier variable.

dim(Hitters)
[1] 322  20
Hitters <- na.omit(Hitters)
mean(Hitters$Salary) # sólo se han quitado los 59 sin Salary
[1] 535.9259
dim(Hitters)
[1] 263  20
sum(is.na(Hitters))
[1] 0

A.6 Funciones interesantes utilizadas

Dado que el interés de cada función depende de factores subjetivos, aquí se presentan por orden alfabético (no subjetivo).

A.6.1 anova()

Aparece en: Lab3 Regresión Lineal y Lab7 Modelización No lineal.

La función anova() realiza un contraste de hipótesis comparando los modelos que se le indiquen:

anova(modelo1, modelo2, ...)

Los modelos deben estar anidados: los predictores de modelo1 deben ser un subconjunto de los predictores de modelo2; los de modelo2 deben serlo de modelo3, etc.

La comparación la hace por pares de modelos. La hipótesis nula es que los dos modelos se ajustan igualmente bien a los datos, y la hipótesis alternativa es que el modelo más grande se ajusta significativamente mejor a los datos.

En lugar de proporcionar aquí un ejemplo, remitimos al Lab7 Modelización No lineal para ver un ejemplo con su contexto.

A.6.2 contrast()

Aparece en: Lab3 Regresión Lineal y Lab4 Clasificación.

La función contrasts() devuelve la codificación que R usa para las variables ficticias, dummys, asociadas a variables cualitativas.

contrasts(iris$Species)
           versicolor virginica
setosa              0         0
versicolor          1         0
virginica           0         1

Así, R ha creado una variable ficticia Speciesversicolor que toma el valor 1 si la especie es versicolor y 0 en caso contrario. También ha creado una variable ficticia Speciesvirginica que equivale a 1 si la especie es virginica y 0 en caso contrario. La especie setosa corresponde a 0 para cada una de las dos variables ficticias anteriores.

Use ?contrasts para aprender sobre otros contrastes y cómo configurarlos.

A.6.3 I()

Aparece en: Lab3 Regresión Lineal, Lab5 Remuestreo y Lab7 Modelización No lineal.

La función I() permite envolver la expresión que se escriba dentro. Típicamente es necesaria para incluir en una fórmula una potencia usando ^. Envolver con I() permite el uso estándar en R, que es elevar X a la potencia 2, pues se crea un vector/variable con los valores correspondientes a la potencia considera. Con ello aparecerá el coeficiente asociado a dicha “potencia” en el modelo que se esté ajustando (con lm(), glm(), …).

Sin embargo, en el Lab7 Modelización No lineal se hace un uso distinto al anterior: se crea una variable de respuesta binaria sobre la marcha: I(wage > 250).

A.6.4 poly()

Aparece en: Lab3 Regresión Lineal y Lab7 Modelización No lineal.

La función poly() permite crear de forma abreviada la fórmula de un polinomio, evitando escribir una fórmula larga con potencias de la variable explicativa o predictora. Típicamente se usa para incluirla en un modelo: y ~ .... Por ejemplo, para definir el polinomio de quinto orden de una variable llamada X1

poly(X1, 5)

Por defecto, la función poly() ortogonaliza los predictores: esto significa que las características que genera esta función no son simplemente una secuencia de potencias del argumento. Concretamente, devuelve una matriz cuyas columnas son una base de polinomios ortogonales (cada columna es una combinación lineal de las variables X1, X1^2, X1^3…).

Para obtener los polinomios “sin procesar”, la base polinómica, se debe usar en poly() el argumento Raw = TRUE. Es equivalente a “envolver” las potencias X1^2, etc. a través de la función I() (vista anteriormente) pues el símbolo ^ tiene un significado especial en las fórmulas (de ahí la necesidad de envolver las potencias). Es decir,

  • poly(X1, 4) es equivalente a X1 + I(X1^2) + I(X1^3) + I(X1^4)

Otra forma equivalente (y algo más compacta) es:

  • cbind(X1, X1^2, X1^3, X1^4)

construye una matriz a partir de una colección de vectores. Cualquier llamada de función como cbind() dentro de una fórmula también sirve como ‘envoltorio’.

Se muestra así la flexibilidad del lenguaje de fórmulas en R.

Nota: La elección de la base afecta a las estimaciones de los coeficientes del modelo que se quiera ajustar, pero no afecta a los valores ajustados obtenidos.

A.6.5 scale()

Aparece en: Lab4 Clasificación e indirectamente en Lab9 SVM.

En ocasiones es bueno/necesario escalar/estandarizar/tipificar las variables, en el sentido de llevarlas a la misma escala, que aquí significa que las variables tengan media 0 y desviación típica 1. Así se consigue que las variables sean comparables.

En el Lab4 Clasificación en el método KNN es bueno/necesario escalar las variables dado que se busca el vecino más próximo de una observación/dato, por lo que la escala afecta a la cercanía. Las variables con una escala grande tendrán un efecto mucho mayor en la distancia entre las observaciones, que las variables con una escala pequeña. Por ejemplo, si en un conjunto de datos se tienen dos variables, “salario anual” y “edad” (medidas en dólares y años, respectivamente). En lo que respecta a “distancia”, una diferencia de 1000 en salario es enorme en comparación con una diferencia de 50 en edad. Esto es contrario a nuestra intuición de que una diferencia salarial de 1000 dólares anuales es bastante pequeña en comparación con una diferencia de edad de 50 años. Además, si midiéramos el “salario” en yenes japoneses, o si midiéramos la “edad” en minutos, los resultados de distancia serían bastante diferentes de los que obtenemos midiendo en dólares y años.

La función scale() estandariza los datos. Lo hacemos sobre las dos primeras variables del conjunto de datos Caravan, del paquete ISLR2:

head(Caravan[, 1:2])
  MOSTYPE MAANTHUI
1      33        1
2      37        1
3      37        1
4       9        1
5      40        1
6      23        1
summary(Caravan[, 1:2])
    MOSTYPE         MAANTHUI     
 Min.   : 1.00   Min.   : 1.000  
 1st Qu.:10.00   1st Qu.: 1.000  
 Median :30.00   Median : 1.000  
 Mean   :24.25   Mean   : 1.111  
 3rd Qu.:35.00   3rd Qu.: 1.000  
 Max.   :41.00   Max.   :10.000  
c(sd(Caravan[, 1]), sd(Caravan[, 2]))
[1] 12.8467057  0.4058421
standardized.X <- scale(Caravan[,1:2])
head(standardized.X)
      MOSTYPE   MAANTHUI
1  0.68084775 -0.2725565
2  0.99221162 -0.2725565
3  0.99221162 -0.2725565
4 -1.18733547 -0.2725565
5  1.22573452 -0.2725565
6 -0.09756193 -0.2725565
summary(standardized.X)
    MOSTYPE           MAANTHUI      
 Min.   :-1.8101   Min.   :-0.2726  
 1st Qu.:-1.1095   1st Qu.:-0.2726  
 Median : 0.4473   Median :-0.2726  
 Mean   : 0.0000   Mean   : 0.0000  
 3rd Qu.: 0.8365   3rd Qu.:-0.2726  
 Max.   : 1.3036   Max.   :21.9036  
c(sd(standardized.X[, 1]), sd(standardized.X[, 2]))
[1] 1 1

A.6.6 set.seed()

Aparece en casi todos los Labs

La función set.seed() permite establecer una semilla de aleatorización. Siempre que se utilice la misma semilla, los resultados aleatorios posteriores arrojarán los mismos resultados en cualquier máquina que se ejecuten: reproducibilidad

rnorm(4) # genera 4 números aleatorios utilizando la Normal(0,1)
[1]  0.1256203 -0.7098623 -0.9122442  1.0517744
# En cada ordenador se obtendrán unos valores distintos
set.seed(1)
rnorm(4) # en todos los ordenadores se obtendrán los mismos valores
[1] -0.6264538  0.1836433 -0.8356286  1.5952808