3.3 Introducción a la minería de datos con R

La minería de datos tiene como objetivo tratar de descubrir patrones ocultos en conjuntos de datos. En las siguientes secciones veremos cómo se pueden realizar con R algunas tareas que se suelen llevar a cabo como parte del proceso de minería de datos.

3.3.1 Análisis de correlación

A través del análisis de correlación podemos determinar si existe (alta correlación) o no (baja correlación) una relación lineal entre variables y en qué sentido se produce (correlación positiva o negativa).

Para determinar si existe correlación entre dos variables utilizaremos la función cor.test de R, que calcula la correlación y un p-valor para estimar si esta es o no significativa (con un nivel de confianza del 95 % se considera que si p<0.05 la correlación es significativa, puesto que se rechazaría la hipótesis nula de que la correlación es cero).

Por ejemplo, supongamos que disponemos del siguiente data frame que muestra en dos columnas la relación entre la temperatura medida en una determinada ciudad y las ventas (en euros) en un puesto de refrescos.

tabla 
#    temp ventas 
# 1  22.3  40.61 
# 2  34.1  61.28 
# 3  25.8  48.01 
# 4   9.6  18.42 
# 5  17.8  31.38 
# 6  21.2  37.82 
# 7  19.9  35.79 
# 8  32.7  59.52 
# 9  27.6  50.12 
# 10 24.4  44.95 

cor.test(tabla$temp, tabla$ventas) 
 
#   Pearson's product-moment correlation 
#  
# data:  tabla$temp and tabla$ventas 
# t = 51.7933, df = 8, p-value = 2.14e-11 
# alternative hypothesis: true correlation is not equal to 0 
# 95 percent confidence interval: 
#  0.9934703 0.9996617 
# sample estimates: 
#       cor  
# 0.9985122 

Se puede ver que existe una alta correlación entre las variables (cor=0.99851) y además es positiva (cuando crece la temperatura esperaríamos que creciesen también las ventas). También podemos observar que el p-valor es muy pequeño, con lo que la correlación sería en este caso significativa.

3.3.2 Regresión

El objetivo del análisis de regresión es el de encontrar una función que permita predecir los valores de una variable dependiente en base a los de una o varias variables independientes. Existen distintos tipos de análisis de regresión, dependiendo del tipo de función que queramos emplear, aunque en este tema nos centraremos en el caso en el que la función es una recta (regresión lineal), dado que es el más habitualmente empleado.

Para calcular la recta de regresión a partir de unos datos utilizaremos la función lm de R. El principal argumento de esta función es una fórmula con la estructura:

Donde VD representa la variable dependiente y VI las distintas variables independientes. Veamos un ejemplo: se trata de determinar si el número de veces que un grillo canta en una determinada unidad de tiempo (por ejemplo, minutos) guarda relación con la temperatura ambiente. Tras realizar una serie de medidas se ha llegado a los siguientes datos:

temp <- c(88.6, 71.6, 93.3, 84.3, 80.6, 75.2, 69.7, 82.0,
          69.4, 83.3, 79.6, 82.6, 80.6, 83.5, 76.3)
sonido <- c(20.0, 16.0, 19.8, 18.4, 17.1, 15.5, 14.7, 17.1,
            15.4, 16.2, 15.0, 17.2, 16.0, 17.0, 14.4)
tabla <- data.frame(temp, sonido)

tabla 
# temp sonido 
# 1  88.6   20.0 
# 2  71.6   16.0 
# 3  93.3   19.8 
# 4  84.3   18.4 
# 5  80.6   17.1 
# 6  75.2   15.5 
# 7  69.7   14.7 
# 8  82.0   17.1 
# 9  69.4   15.4 
# 10 83.3   16.2 
# 11 79.6   15.0 
# 12 82.6   17.2 
# 13 80.6   16.0 
# 14 83.5   17.0 
# 15 76.3   14.4 

Donde temp representa la temperatura en grados Fahrenheit y sonido el número de cantos por minuto. Podemos intentar ajustar un modelo lineal entre ambas variables haciendo:

fit<-lm(sonido~temp, data=tabla) 
fit 
# Call: 
# lm(formula = sonido ~ temp, data=tabla) 
#  
# Coefficients: 
# (Intercept)         temp   
#     -0.3091       0.2119   

Donde, como se puede ver, se sugiere que la ecuación de la recta estimada es:

sonido = 0.2119*temp – 0.3091

Aunque hemos determinado la ecuación, no sabemos si el ajuste realizado es bueno o no. Para ello, podemos utilizar la función summary que nos ofrece más información sobre el ajuste:

summary(fit)
# Call: 
# lm(formula = sonido ~ temp, data = tabla) 
# 
# Residuals: 
#      Min       1Q   Median       3Q      Max  
# -1.56009 -0.57930  0.03129  0.59020  1.53259  
#  
# Coefficients: 
#             Estimate Std. Error t value Pr(>|t|)     
# (Intercept) -0.30914    3.10858  -0.099 0.922299     
# temp         0.21193    0.03871   5.475 0.000107 *** 
# --- 
# Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1 
#  
# Residual standard error: 0.9715 on 13 degrees of freedom 
# Multiple R-squared:  0.6975,  Adjusted R-squared:  0.6742  
# F-statistic: 29.97 on 1 and 13 DF,  p-value: 0.0001067 

Para interpretar estos resultados debemos fijarnos en varios aspectos:

  • En Residuals se nos ofrece información sobre los residuos del ajuste. Estos residuos deberían seguir una distribución normal de media cero, así que la mediana de los residuos debería estar cerca de cero (aquí 0.03129) y el valor absoluto del mínimo y el máximo debería ser similar (ambos cerca de 1.5).

  • Se nos indica también si los coeficientes estimados en el análisis son o no significativos por medio de un p-valor (última columna de la tabla Coefficients). Así se puede ver que el coeficiente estimado para tabla$temp (0.21193) sí lo es (p-valor muy pequeño), mientras que la estimación del punto de interceptación (-0.30914) tiene un p-valor alto (0.922299) con lo que no se puede considerar significativo.

  • Los valores de R-squared sirven como medida de la bondad del ajuste. Para que el ajuste sea bueno, estos valores deberían estar próximos a 1.

Por último, puede ser interesante en ocasiones hacernos una idea de la bondad del ajuste mediante la representación gráfica de los datos y la recta de regresión.

Mediante la función abline podemos dibujar la recta de regresión, mientras que utilizaremos la función plot para representar los datos de la tabla:

temp <- c(88.6, 71.6, 93.3, 84.3, 80.6, 75.2, 69.7, 82.0,
          69.4, 83.3, 79.6, 82.6, 80.6, 83.5, 76.3)
sonido <- c(20.0, 16.0, 19.8, 18.4, 17.1, 15.5, 14.7, 17.1,
            15.4, 16.2, 15.0, 17.2, 16.0, 17.0, 14.4)
tabla <- data.frame(temp, sonido)
fit<-lm(sonido~temp, data=tabla) 
plot(tabla$temp, tabla$sonido) 
abline(fit) 
Representación gráfica del ajuste lineal de unos datos.

Figura 3.4: Representación gráfica del ajuste lineal de unos datos.

3.3.3 Clasificación

La tarea de clasificación tiene como objetivo organizar una serie de datos en un conjunto de categorías predefinidas. En la práctica existen multitud de alternativas en lo que a algoritmos de clasificación se refiere, muchas de las cuales están implementadas en R a través de distintos paquetes.

En esta sección haremos uso del paquete e1071, desarrollado por el Departamento de Estadística de la Universidad Tecnológica de Viena y que incluye la implementación de algunos mecanismos de clasificación de datos, tales como el algoritmo de Bayes ingenuo (Naive Bayes) o las máquinas de vectores soporte (Support Vector Machines, SVM).

Para ello, instalaremos el paquete en nuestro sistema ejecutando en la consola de R:

install.packages("e1071", dependencies=T) 
library(e1071)

Para mostrar el uso de estas técnicas de clasificación necesitamos un conjunto de datos con el que hacer pruebas. En nuestro caso utilizaremos el denominado Iris Flower Dataset, un conjunto de datos sencillo que se suele emplear como ejemplo en tareas de clasificación. Tan frecuente es su uso que, de hecho, R incluye una copia de este conjunto de datos que podemos cargar ejecutando en el terminal el comando:

data(iris) 

Lo que creará una variable de tipo data frame y nombre iris en el entorno. Podemos ver las primeras entradas de la tabla ejecutando el comando:

head(iris) 
#   Sepal.Length Sepal.Width Petal.Length Petal.Width Species 
# 1          5.1         3.5          1.4         0.2  setosa 
# 2          4.9         3.0          1.4         0.2  setosa 
# 3          4.7         3.2          1.3         0.2  setosa 
# 4          4.6         3.1          1.5         0.2  setosa 
# 5          5.0         3.6          1.4         0.2  setosa 
# 6          5.4         3.9          1.7         0.4  setosa 

Los datos de Iris se obtuvieron midiendo cuatro características (longitud y ancho del sépalo, longitud y ancho del pétalo, todos ellos expresados en centímetros) de una serie de especies vegetales (Iris setosa, Iris virgínica, Iris versicolor). Para cada una de las especies la tabla ofrece 50 muestras (filas en el data frame).

Las técnicas de clasificación son técnicas supervisadas, en las que es necesario entrenar un clasificador para, posteriormente, utilizarlo a la hora de predecir la clase a la que pertenece una o varias muestras nuevas (no incluidas en el conjunto de entrenamiento).

En nuestro caso, dividiremos el conjunto Iris en dos partes: una la usaremos para entrenar el clasificador (120 muestras, 40 de cada clase) y la otra para evaluarlo (30 muestras, 10 de cada clase):

train<-iris[c(1:40,51:90,101:140),] 
eval<-iris[c(41:50,91:100,141:150),] 
dim(train) 
# [1] 120   5 

dim(eval) 
# [1] 30  5 

A continuación, entrenaremos el clasificador utilizando el conjunto de entrenamiento:

nbClass <- naiveBayes(
  Species ~ Sepal.Length+Sepal.Width
  +Petal.Length+Petal.Width, data = train) 

Donde estamos utilizando la función naiveBayes para entrenar un clasificador bayesiano ingenuo. El primer argumento representa una fórmula que indica que queremos predecir la clase indicada por Species utilizando como variables independientes el resto de columnas del data frame (esto se podría haber abreviado por un simple punto, con lo que podríamos escribir directamente Species ~ .). El segundo argumento indica de dónde sacamos los datos.

Una vez entrenado el clasificador podemos utilizarlo para predecir la clase de cada una de las muestras del conjunto de evaluación mediante la función predict:

predicted<-predict(nbClass, eval[,-5]) 

Podemos obtener la matriz de confusión del clasificador utilizando la función table:

matrizconf<-table(predicted, eval$Species) 
matrizconf             
# predicted    setosa versicolor virginica 
#   setosa          10          0             0 
#   versicolor      0          10             0 
#   virginica       0           0            10 

Y podemos obtener su tasa de aciertos haciendo:

sum(diag(matrizconf))/sum(matrizconf) 
# [1] 1 

Con lo que vemos que en este caso el clasificador ha acertado en todas las predicciones. Un proceso similar se seguiría para utilizar el clasificador SVM, aunque en este caso la función para entrenar el clasificador sería la función svm.

En el ejemplo anterior del clasificador bayesiano ingenuo, hemos dividido los datos en entrenamiento y evaluación a mano, utilizando el operador de selección [] con los índices de los elementos que queremos añadir en cada grupo. Sin embargo, podríamos haber hecho la división de manera aleatoria utilizando las funciones:

  • sample: permite extraer una muestra aleatoria de tamaño dado de un vector de elementos.

  • split: permite dividir un conjunto de elementos en grupos, definidos mediante una variable categórica (factor).

  • unsplit: realiza el proceso contrario a split, uniendo una serie de grupos en un único conjunto de elementos.

Por ejemplo:

datasets<-split(iris, iris$Species) 
etiquetados<-lapply(
  datasets, function(x) {s<-sample(1:nrow(x), 10);
  x[s,"t"]<-"eval"; x[-s,"t"]<-"train"; return(x)}) 
dataset<-unsplit(etiquetados, iris$Species) 
train<-dataset[dataset$t=="train",-6] 
eval<-dataset[dataset$t=="eval",-6] 

El primer comando utiliza la función split para dividir el data frame iris en base al valor de la columna Species. El data frame original se divide así en tres partes, una por cada especie (Setosa, Virgínica, Versicolor), obteniendo como resultado una lista con tres data frames. A cada uno de los elementos de esa lista se le aplica (con lapply) una función que selecciona aleatoriamente 10 entradas (con sample).

Las entradas seleccionadas se etiquetan añadiendo una nueva columna ‘t’ con valor ‘eval’ mientras que el resto de entradas se marcan con el valor ‘train’ en esa columna ‘t.’ El siguiente comando usa unsplit para juntar los tres data frames ya etiquetados en uno solo. A continuación, los dos siguientes comandos utilizan el valor de la columna ‘t’ para seleccionar las filas que constituirán el conjunto de entrenamiento y el de evaluación, eliminando al mismo tiempo esa misma columna ‘t’ (la última, en la posición 6).

3.3.4 Agrupamiento (clustering)

La tarea de agrupamiento tiene como objetivo organizar una serie de datos en grupos de forma que los más similares (de acuerdo a una métrica determinada) acaben en el mismo grupo. Al igual que ocurría con los algoritmos de clasificación, también existen múltiples algoritmos que podemos utilizar para agrupar unos datos. En esta sección veremos cómo utilizar uno de los más populares, el algoritmo k-means.

En R el algoritmo k-means se implementa por medio de la función kmeans. Los dos parámetros principales de esta función son:

  • x: Son los datos que queremos procesar (debe ser una matriz numérica)

  • centers: En el que indicaremos el número de grupos en los que queremos repartir los datos.

Tomemos por ejemplo los datos de Iris, y asumamos que no conocemos la clase a la que pertenece cada muestra (lo que podemos hacer eliminando la última columna):

datos<-iris[,-5] 

Podemos llevar a cabo un análisis de agrupamiento de estos datos utilizando la siguiente secuencia de comandos de R:

m<-as.matrix(datos) 
norm_m<-scale(m) 
fit<-kmeans(norm_m, 3) 

En donde se puede ver que primero convertimos el data frame en una matriz con la llamada a la función as.matrix, a continuación escalamos la matriz (función scale), de forma que se trate de manera homogénea a las distintas variables (haciendo que tengan misma media y desviación típica) y, por último, realizamos el agrupamiento, indicando que queremos tres grupos. Podemos a continuación acceder a los resultados del agrupamiento a través de la variable fit:

  • fit$centers: Nos proporciona los centros de los agrupamientos.

  • fit$cluster: Indica el grupo en el que se ha asignado cada muestra.

Un parámetro de calidad del agrupamiento generado mediante el algoritmo de K-means se puede calcular mediante la expresión:

fit$betweenss/fit$totss 
# [1] 0.7669658 

Que se podría interpretar como que aproximadamente un 77 % de la varianza de los datos queda explicada al hacer el agrupamiento. En general, cuanto mayor es este valor, mejor el agrupamiento, pero hay que tener en cuenta que el valor aumenta a medida que aumentamos el número de grupos, con lo que interesa encontrar un compromiso entre el parámetro de calidad y el número de grupos (y de hecho, esto se utiliza como un mecanismo para intentar estimar el número de grupos, a través del denominado «método del codo» ─Elbow Method─).

Además del parámetro de calidad, en nuestro caso podemos comparar las asignaciones a grupos con las clases iniciales que venían asignadas en Iris:

table(fit$cluster, iris[,5])    
#   setosa versicolor virginica 
# 1      0         11        36 
# 2      0         39        14 
# 3     50          0         0 

De donde podríamos deducir que el grupo 1 corresponde a la especie «virginica», el 2 a «versicolor» y el 3 a «setosa» y que el mecanismo se equivocaría en la asignación de 11+14=25 casos sobre un total de 150.