En primer lugar cargamos los datos de la base de datos wisc_bs_data en formato csv con la función read.csv

wbcd <- read.csv("wisc_bc_data.csv", stringsAsFactors = FALSE, header=TRUE)
str(wbcd)
## 'data.frame':    569 obs. of  32 variables:
##  $ id               : int  87139402 8910251 905520 868871 9012568 906539 925291 87880 862989 89827 ...
##  $ diagnosis        : chr  "B" "B" "B" "B" ...
##  $ radius_mean      : num  12.3 10.6 11 11.3 15.2 ...
##  $ texture_mean     : num  12.4 18.9 16.8 13.4 13.2 ...
##  $ perimeter_mean   : num  78.8 69.3 70.9 73 97.7 ...
##  $ area_mean        : num  464 346 373 385 712 ...
##  $ smoothness_mean  : num  0.1028 0.0969 0.1077 0.1164 0.0796 ...
##  $ compactness_mean : num  0.0698 0.1147 0.078 0.1136 0.0693 ...
##  $ concavity_mean   : num  0.0399 0.0639 0.0305 0.0464 0.0339 ...
##  $ points_mean      : num  0.037 0.0264 0.0248 0.048 0.0266 ...
##  $ symmetry_mean    : num  0.196 0.192 0.171 0.177 0.172 ...
##  $ dimension_mean   : num  0.0595 0.0649 0.0634 0.0607 0.0554 ...
##  $ radius_se        : num  0.236 0.451 0.197 0.338 0.178 ...
##  $ texture_se       : num  0.666 1.197 1.387 1.343 0.412 ...
##  $ perimeter_se     : num  1.67 3.43 1.34 1.85 1.34 ...
##  $ area_se          : num  17.4 27.1 13.5 26.3 17.7 ...
##  $ smoothness_se    : num  0.00805 0.00747 0.00516 0.01127 0.00501 ...
##  $ compactness_se   : num  0.0118 0.03581 0.00936 0.03498 0.01485 ...
##  $ concavity_se     : num  0.0168 0.0335 0.0106 0.0219 0.0155 ...
##  $ points_se        : num  0.01241 0.01365 0.00748 0.01965 0.00915 ...
##  $ symmetry_se      : num  0.0192 0.035 0.0172 0.0158 0.0165 ...
##  $ dimension_se     : num  0.00225 0.00332 0.0022 0.00344 0.00177 ...
##  $ radius_worst     : num  13.5 11.9 12.4 11.9 16.2 ...
##  $ texture_worst    : num  15.6 22.9 26.4 15.8 15.7 ...
##  $ perimeter_worst  : num  87 78.3 79.9 76.5 104.5 ...
##  $ area_worst       : num  549 425 471 434 819 ...
##  $ smoothness_worst : num  0.139 0.121 0.137 0.137 0.113 ...
##  $ compactness_worst: num  0.127 0.252 0.148 0.182 0.174 ...
##  $ concavity_worst  : num  0.1242 0.1916 0.1067 0.0867 0.1362 ...
##  $ points_worst     : num  0.0939 0.0793 0.0743 0.0861 0.0818 ...
##  $ symmetry_worst   : num  0.283 0.294 0.3 0.21 0.249 ...
##  $ dimension_worst  : num  0.0677 0.0759 0.0788 0.0678 0.0677 ...

Los valores de ID no son informativos para este análisis, ya que son datos independientes y aleatorios, pueden confundir en el proceso de machine learning así que los eliminamos:

## exclimos los valores de id que no son informativos
wbcd<- wbcd[-1]

Para muchos procesos de machine learning se requiere que la caracteristica que tenemos como diana sea codificada como factor, asi que necesitamos recodificar la variable “diagnosis”, que es nuestro objetivo: averiguar el diagnóstico. Además, aprovechamos para simplificar el nombre de la variable recodificándola a Benign o Malignant.

wbcd$diagnosis<- factor(wbcd$diagnosis, levels = c("B", "M"),
        labels = c("Benign", "Malignant"))

Comprobamos la proporción de pacientes Benign y Malign con prop.table():

round(prop.table(table(wbcd$diagnosis)) * 100, digits = 1)
## 
##    Benign Malignant 
##      62.7      37.3

Si revisamos brevemente las dimensiones de los datos de la base de datos, obsevamos que cada variable tiene una escala diferente. Esto es un problema para el cálculo de los k-NN, ya que es altamente dependiente de la escala demedida de las variables a estudiar. De esta forma, las variables con mayor magnitud pesarán más en el cálculo. Esto podría causar problemas para nuestro clasificador, asi que vamos a aplicar la normalización a rescalar las características a un rango normal de valores.

Para normalizar los valores, necesitamos crear la función “r normalize()”.

normalize <- function(x) {
                        return ((x - min(x)) / (max(x) - min(x)))
}

Probamos en un par de vectores que la función hace su papel correctamente:

normalize(c(1, 2, 3, 4, 5))
## [1] 0.00 0.25 0.50 0.75 1.00
normalize(c(10, 20, 30, 40, 50))
## [1] 0.00 0.25 0.50 0.75 1.00

En efecto, ahora todas las variables tienen un mismo rango, de 0 a 1.

Utilizamos la función lapply() que coge una lista y aplica una función específica a cada elemento de la lista. Ya que una matriz de datos es una lista de vectores de misma longitud, podemos usar lapply() para aplicar normlize() a cada característica del dataset. El último paso es convertir la lista retornada por lapply() en un dataframe, usando la función as.data.frame():

wbcd_n <- as.data.frame(lapply(wbcd[2:31], normalize))

Confirmamos que la transformación se aplicó correctamente, miremos al resumen de estadísticas:

summary(wbcd_n$area_mean)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##  0.0000  0.1174  0.1729  0.2169  0.2711  1.0000

Preparación de los datos. Crear un training y probar los datasets.

Ante la asuencia de nuevos datasets que podamos probar después de entrenar el modelo, vamos a dividir nuestros datos en dos partes: un dataset de entrenamiento que será usado para construir el modelo de k-NN y un dataset que será usado para estimar le agudeza predictiva del modelo. Usaremos las primeras 469 registros para el training del dataset y el restante 100 para simular 100 nuevos pacientes.

Para ello usaremos un métodos de extracción, con el que difividiremos el wbcd_n data frame en wbcd_train y wbcd_test:

 wbcd_train <- wbcd_n[1:469, ]
wbcd_test <- wbcd_n[470:569, ]

Cuando construimos nuestros datasets de training y de prueba, excluimos la variable diana que es diagnosis. Para el entrenamiento del modelo necesitaremos guardar esta clase de etiquetas en vector de factores, dividido entre los datasets de training y test:

wbcd_train_labels <- wbcd[1:469, 1]
wbcd_test_labels <- wbcd[470:569, 1]

Esto lo que hace es coger todas las filas de los datasets y meterlas en un vector de la longitud de la columna 1 donde guarda todas las categorías de cada fila.

Entrenamiento del modelo sobre los datos.

Para el algoritmo de k-NN, la fase de entrenamiento en realidad no implica construccion del modelo, el proceso de entrenamiento de un aprendizaje suave como el k-NN implica el guardado del dato registrado en un formato estructurado.

Para clasificar los datos que vamos a testar, usaremos la implementacion de k-NN del paquete Class, el cual proporciona un conjunto de funciones de R para la clasificación. Si este paquete no esta ya instalado en nuestro sistema, podemos instalarlo:

install.packages("class",repos= "http://cran.us.r-project.org")
## 
## The downloaded binary packages are in
##  /var/folders/nn/fkq3ky797116dyb4r3tynkq00000gn/T//RtmpE0KEi4/downloaded_packages
library(class)

La función knn() del paquete class proporciona una implementación estándar y clásica del agoritmo k-NN. Para cada instancia de nuestro dataset de prueba, la función identificará los k-NN, usando las distancias euclídeas, donde k es un número especificado por el usuario. El dato testado es clasificado tomando un voto entre los k-NN, que implica asignar la clase de la mayoría de k-NN. Un empate se decide azarosamente. El entrenamiento usando la función knn() se lleva a cabo en una sola funcion de llamada, usando cuatro parámetros: knn(train,test,class,k)

Ya que nuestra matriz de entrenamiento contiene 469 registros, podríamos probar con k=21, un número impar parecido a la raiz cuadrada de 469. USar un numero impar elimina la posibilidad de tener un empate.

wbcd_test_pred <- knn(train = wbcd_train, test = wbcd_test,
                        cl = wbcd_train_labels, k = 21)

EVALUAR LA PERFORMANCE DEL MODELO

Evaluamos cómo de bien predijo la categoría de clase en el vector wbcd_test_pred con los valores conocidos del wbcd_test_labels. Para ello podemos usar el CrossTable(), en el paquete gmodels.

install.packages("gmodels", repos="http://cran.us.r-project.org")
## 
## The downloaded binary packages are in
##  /var/folders/nn/fkq3ky797116dyb4r3tynkq00000gn/T//RtmpE0KEi4/downloaded_packages
library(gmodels)
CrossTable(x=wbcd_test_labels,y=wbcd_test_pred,prop.chisq=FALSE)
## 
##  
##    Cell Contents
## |-------------------------|
## |                       N |
## |           N / Row Total |
## |           N / Col Total |
## |         N / Table Total |
## |-------------------------|
## 
##  
## Total Observations in Table:  100 
## 
##  
##                  | wbcd_test_pred 
## wbcd_test_labels |    Benign | Malignant | Row Total | 
## -----------------|-----------|-----------|-----------|
##           Benign |        61 |         0 |        61 | 
##                  |     1.000 |     0.000 |     0.610 | 
##                  |     0.968 |     0.000 |           | 
##                  |     0.610 |     0.000 |           | 
## -----------------|-----------|-----------|-----------|
##        Malignant |         2 |        37 |        39 | 
##                  |     0.051 |     0.949 |     0.390 | 
##                  |     0.032 |     1.000 |           | 
##                  |     0.020 |     0.370 |           | 
## -----------------|-----------|-----------|-----------|
##     Column Total |        63 |        37 |       100 | 
##                  |     0.630 |     0.370 |           | 
## -----------------|-----------|-----------|-----------|
## 
## 

MEJORANDO LA PERFORMANCE DEL MODELO. Para ello emplearemos un método alternativo para reescalar las características numéricas. En Segundo lugar intentaremos diferentes valores para K

1) Transformación - standarización del z score.

Para estandarizar un vector, podemos utilizar la función scale() en R, que por defecto reescala los valores usando la estandarización del z-score. La función scale() ofrece el beneficio adicional de que puede ser aplicado directamente a una base de datos o dataframe, de manera que así podemos evitar el uso de la función lappl(). Para crear la vesrsión de z-score de la base de datos wbc, usaremos el siguiente comando, que reescala todas las características, con la excepción de “diagnosis”, y guarda el resultado en un dataframe.

wbcd_z<-as.data.frame(scale(wbcd[-1]))
## confirmamos que la transformación se aplicó correctamente, usando el summary:
summary(wbcd_z$area_mean)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
## -1.4532 -0.6666 -0.2949  0.0000  0.3632  5.2459
##la media de una variable estandarizada en z score siempre tiene que ser 0, y el rango debería ser muy compacto. Un z-score mayor de 3 o menos de -3 indica un valor extremadamente raro. Con esto en mente, la transformación parece haber funcionado. Necesitamos ahora dividir los datos en training y datasets, y entonces clasificar los registros de test usando la función knn().
wbcd_train <- wbcd_z[1:469, ]
wbcd_test <- wbcd_z[470:569, ]
wbcd_train_labels <- wbcd[1:469, 1]
wbcd_test_labels <- wbcd[470:569, 1]

wbcd_test_pred <- knn(train = wbcd_train, test = wbcd_test,
                        cl = wbcd_train_labels, k = 21)
CrossTable(x = wbcd_test_labels, y = wbcd_test_pred,
                       prop.chisq = FALSE)
## 
##  
##    Cell Contents
## |-------------------------|
## |                       N |
## |           N / Row Total |
## |           N / Col Total |
## |         N / Table Total |
## |-------------------------|
## 
##  
## Total Observations in Table:  100 
## 
##  
##                  | wbcd_test_pred 
## wbcd_test_labels |    Benign | Malignant | Row Total | 
## -----------------|-----------|-----------|-----------|
##           Benign |        61 |         0 |        61 | 
##                  |     1.000 |     0.000 |     0.610 | 
##                  |     0.924 |     0.000 |           | 
##                  |     0.610 |     0.000 |           | 
## -----------------|-----------|-----------|-----------|
##        Malignant |         5 |        34 |        39 | 
##                  |     0.128 |     0.872 |     0.390 | 
##                  |     0.076 |     1.000 |           | 
##                  |     0.050 |     0.340 |           | 
## -----------------|-----------|-----------|-----------|
##     Column Total |        66 |        34 |       100 | 
##                  |     0.660 |     0.340 |           | 
## -----------------|-----------|-----------|-----------|
## 
##