Prognosis de recurrencia de cáncer mamario

El dataset para hacer este práctico y una explicación de los datos se puede descargar desde: http://mldata.org/repository/data/viewslug/datasets-uci-breast-cancer/

El dataset está compuesto por 286 registros de pacientes que presentaron o no recurrencia de cáncer de mama despues de cinco años de una cirugía. Los atributos no son suficientes para discriminar con mucha precisión entre las dos clases, pero son un muy buen ejemplo para aprender a procesar datos.

Descargar desde la etiqueta “Summary” el archivo CSV “datasets-uci-breast-cancer.csv”.

Los nombres originales de las variables, una breve explicación y los tipos de los datos:

Los datos faltantes están indicados por “?” o por un código “nan”

Primeros pasos

Primero hay que cargar los datos en un dataframe de R. Los datos no tienen encabezado, así que se los agregamos nosotros. Verificamos que el dataframe tenga el tamaño y las variables esperadas.

df_crudo <- read.csv("datasets-uci-breast-cancer.csv", 
                     header=F, 
                     stringsAsFactors = F,
                     quote = "'")

names(df_crudo) <- c("edad", "menopausia", "tamano", 
                     "nodulos", "capsula", "grado", 
                     "mama", "cuadrante", "radiot", 
                     "recurrencia")
dim(df_crudo)
## [1] 286  10
str(df_crudo)
## 'data.frame':    286 obs. of  10 variables:
##  $ edad       : chr  "40-49" "50-59" "50-59" "40-49" ...
##  $ menopausia : chr  "premeno" "ge40" "ge40" "premeno" ...
##  $ tamano     : chr  "15-19" "15-19" "35-39" "35-39" ...
##  $ nodulos    : chr  "0-2" "0-2" "0-2" "0-2" ...
##  $ capsula    : chr  "yes" "no" "no" "yes" ...
##  $ grado      : int  3 1 2 3 2 2 3 2 2 2 ...
##  $ mama       : chr  "right" "right" "left" "right" ...
##  $ cuadrante  : chr  "left_up" "central" "left_low" "left_low" ...
##  $ radiot     : chr  "no" "no" "no" "yes" ...
##  $ recurrencia: chr  "recurrence-events" "no-recurrence-events" "recurrence-events" "no-recurrence-events" ...

Preparación de las variables

Casi todas las variables necesitan alguna preparación antes de poder analizarlas.

Edad

Las edades de las pacientes están indicadas por rangos en un vector de cadenas de caracteres. Para poder utilizarla hay que transformarla a una variable de tipo factor ordenado o una variable numérica entera, donde cada número indica un intervalo. Se podría codificar usando la edad media del intervalo, pero al momento de interpretar los análisis puede causar confusión. Esta variable no tiene datos faltantes.

Convertimos los números a enteros, para no trabajar con una variable ordered, es decir, un factor ordenado. La nueva variable se llama edad_c.

table(df_crudo$edad)
## 
## 20-29 30-39 40-49 50-59 60-69 70-79 
##     1    36    90    96    57     6
edad_c <- df_crudo$edad
edad_c <- sub("20-29", 1, edad_c)
edad_c <- sub("30-39", 2, edad_c)
edad_c <- sub("40-49", 3, edad_c)
edad_c <- sub("50-59", 4, edad_c)
edad_c <- sub("60-69", 5, edad_c)
edad_c <- sub("70-79", 6, edad_c)
head(edad_c)
## [1] "3" "4" "4" "3" "3" "4"
edad_c <- as.numeric(edad_c)

Llegado a este punto es importante compara la tabla con los datos crudos y los modificados:

table(df_crudo$edad)
## 
## 20-29 30-39 40-49 50-59 60-69 70-79 
##     1    36    90    96    57     6
table(edad_c)
## edad_c
##  1  2  3  4  5  6 
##  1 36 90 96 57  6

Menopausia

Los datos de menopausia registran la edad en que ocurrió la menopausia de la paciente (antes o después de los 40 años) o si se encuentra en condición premenopausica. Estos datos se pueden convertir a tipo factor no ordenados. A primera vista puede parecer que son datos ordenados, pero debido a la heterogeneidad de edad de las pacientes, no se puede establecer un orden claro. Por ejemplo, para una paciente premenopausica de 30 años, no podemos saber cuál será su estado a los 40.

table(df_crudo$menopausia)
## 
##    ge40    lt40 premeno 
##     129       7     150
menop_c <- as.factor(df_crudo$menopausia)

Tamaño del tumor

Los datos de tamaño del tumor están registrados en rangos de milimetros, como cadenas de caracteres, con un formato parecido a las edades. Para esta variable el ancho de cada clase es relativamente chico, y por lo tanto se puede usar el valor medio del intervalo.

table(df_crudo$tamano)
## 
##   0-4 10-14 15-19 20-24 25-29 30-34 35-39 40-44 45-49 50-54   5-9 
##     8    28    30    50    54    60    19    22     3     8     4

Nota: en R se puede usar la “ñ” en los nombres de variables, pero tuve algunos problemas al repartir el trabajo entre máquinas Windows y linux.

La operacion para modificar esta variables la vamos a necesitar para otras variables y por lo tanto, es más conveniente construir una función.

La función toma un vector de cadenas caracteres, del tipo “50-54”. Para transformar esto en un número hay que separar la cadena en dos usando como separador el guión. Para lograr esto conviene usar la función strsplit() que devuelve una lista (conviene detenerse un rato en esto, porque las listas no son muy intuitivas y en general presenta cuesta un poco de trabajar aprender a manejarlas). Cada elemento de la lista es un vector de dos números representados como caracteres. Hay que convertirlos a números y luego calcular la media.

conv_rangos <- function(vect){
  aux_1 <- strsplit(vect, "-")
  aux_2 <- lapply(aux_1, function(x) as.numeric(x))
  convertido <- sapply(aux_2, mean)
  return(convertido)  
}

Además del concepto de lista, para entender lo que hace esta función es importante entender qué hacen las funciones lapply() y sapply().

Ya podemos usar la función y revisar qué es lo que hicimos:

tam_c <- conv_rangos(df_crudo$tamano)
head(df_crudo$tamano)
## [1] "15-19" "15-19" "35-39" "35-39" "30-34" "25-29"
head(tam_c)
## [1] 17 17 37 37 32 27

Número de nódulos linfáticos comprometidos

El número de nódulos comprometidos se procesa de la misma forma que la variable anterior:

table(df_crudo$nodulos)
## 
##   0-2 12-14 15-17 24-26   3-5   6-8  9-11 
##   213     3     6     1    36    17    10
nod_c <- conv_rangos(df_crudo$nodulos)

Presencia de células cancerosas que atravesaron la cápsula de los nódulos

Esta variable no está registrada para todas las pacientes. Los datos faltantes se registraron como “nan”, hay que transformar estos datos en verdaderos datos faltantes de R asignándoles un valor NA.

table(df_crudo$capsula)
## 
## nan  no yes 
##   8 222  56
cap_c <- df_crudo$capsula
cap_c[cap_c == "nan"] <- NA

Las dos tablas que siguen sirven para ver la forma diferente en que table() puede mostrar los datos faltantes.

table(cap_c)
## cap_c
##  no yes 
## 222  56
table(cap_c, useNA = "always")
## cap_c
##   no  yes <NA> 
##  222   56    8

Ahora convertimos la variable a una de tipo lógico:

cap_c <- ifelse(cap_c=="yes", T, F)
table(cap_c, useNA = "always")
## cap_c
## FALSE  TRUE  <NA> 
##   222    56     8

Grado de malignidad

Estos datos no necesitan modificación.

table(df_crudo$grado)
## 
##   1   2   3 
##  71 130  85

Mama y cuadrante de mama afectado

La variable mama registra qué mama esta fectada, la izquierda o la derecha. Y para la variable cuadrante se divide la mama afectada en cuadrantes (“lef_low,”left_up“,”center“, right_up”, “right_down”) para indicar cuál es el que está afectado (esta variable contiene un dato faltante).

La primera variable no necesita modificación, pero cuadrante, si:

table(df_crudo$mama)
## 
##  left right 
##   152   134
table(df_crudo$cuadrante)
## 
##   central  left_low   left_up       nan right_low  right_up 
##        21       110        97         1        24        33
cuad_c <- df_crudo$cuadrante
cuad_c[cuad_c == "nan"] <- NA
table(cuad_c, useNA = "always")
## cuad_c
##   central  left_low   left_up right_low  right_up      <NA> 
##        21       110        97        24        33         1

Radioterapia

Esta variable registra si la paciente recibió radioterapia. Esta registrada como “yes/no”, y necesitamos una verdadera variable lógica. Esta variable no contiene valores faltantes.

table(df_crudo$radiot)
## 
##  no yes 
## 218  68
radiot_c <- df_crudo$radiot
radiot_c <- ifelse(radiot_c == "yes", T, F)
table(radiot_c)
## radiot_c
## FALSE  TRUE 
##   218    68

Recurrencia

Indica si a los cinco años de la cirugía hubo recurrencia del tumor. Los datos están registrados como “no-recurrence-events” o “recurrence-events”, y no hay datos faltantes.

table(df_crudo$recurrencia)
## 
## no-recurrence-events    recurrence-events 
##                  201                   85
recur_c <- df_crudo$recurrencia
recur_c <- ifelse(recur_c == "recurrence-events", T, F)

Siguiente paso, crear el dataframe para el análisis

El dataframe para los análisis lo vamos a llamar df1. Hay que prestar atención a las variables modificadas, que terminan con “_c" y a aquellas se pueden extraer directamente del dataframe original.

df1 <- data.frame(edad_c, menop_c, tam_c, 
                 nod_c, cap_c, df_crudo$grado,
                 df_crudo$mama, cuad_c, radiot_c,
                 recur_c)

names(df1) <- c("edad", "menopausia", "tamano", 
                     "nodulos", "capsula", "grado", 
                     "mama", "cuadrante", "radiot", 
                     "recurrencia")  

Es importante chequear que esté todo en orden:

str(df1)
## 'data.frame':    286 obs. of  10 variables:
##  $ edad       : num  3 4 4 3 3 4 4 3 3 3 ...
##  $ menopausia : Factor w/ 3 levels "ge40","lt40",..: 3 1 1 3 3 3 1 3 3 1 ...
##  $ tamano     : num  17 17 37 37 32 27 42 12 2 42 ...
##  $ nodulos    : num  1 1 1 1 4 4 1 1 1 16 ...
##  $ capsula    : logi  TRUE FALSE FALSE TRUE TRUE FALSE ...
##  $ grado      : int  3 1 2 3 2 2 3 2 2 2 ...
##  $ mama       : Factor w/ 2 levels "left","right": 2 2 1 2 1 2 1 1 2 2 ...
##  $ cuadrante  : Factor w/ 5 levels "central","left_low",..: 3 1 2 2 5 3 3 3 4 3 ...
##  $ radiot     : logi  FALSE FALSE FALSE TRUE FALSE TRUE ...
##  $ recurrencia: logi  TRUE FALSE TRUE FALSE TRUE FALSE ...
head(df1)
##   edad menopausia tamano nodulos capsula grado  mama cuadrante radiot
## 1    3    premeno     17       1    TRUE     3 right   left_up  FALSE
## 2    4       ge40     17       1   FALSE     1 right   central  FALSE
## 3    4       ge40     37       1   FALSE     2  left  left_low  FALSE
## 4    3    premeno     37       1    TRUE     3 right  left_low   TRUE
## 5    3    premeno     32       4    TRUE     2  left  right_up  FALSE
## 6    4    premeno     27       4   FALSE     2 right   left_up   TRUE
##   recurrencia
## 1        TRUE
## 2       FALSE
## 3        TRUE
## 4       FALSE
## 5        TRUE
## 6       FALSE

Análisis exploratorio

Lo primero es ver el rango de las variables, algunas medidas de tendencia central para las variables continuas, recuentos para las categóricas y presencia de valores faltantes.

summary(df1)
##       edad         menopausia      tamano         nodulos      
##  Min.   :1.000   ge40   :129   Min.   : 2.00   Min.   : 1.000  
##  1st Qu.:3.000   lt40   :  7   1st Qu.:22.00   1st Qu.: 1.000  
##  Median :4.000   premeno:150   Median :27.00   Median : 1.000  
##  Mean   :3.664                 Mean   :26.41   Mean   : 2.573  
##  3rd Qu.:4.000                 3rd Qu.:32.00   3rd Qu.: 4.000  
##  Max.   :6.000                 Max.   :52.00   Max.   :25.000  
##   capsula            grado          mama         cuadrante  
##  Mode :logical   Min.   :1.000   left :152   central  : 21  
##  FALSE:222       1st Qu.:2.000   right:134   left_low :110  
##  TRUE :56        Median :2.000               left_up  : 97  
##  NA's :8         Mean   :2.049               right_low: 24  
##                  3rd Qu.:3.000               right_up : 33  
##                  Max.   :3.000               NA's     :  1  
##    radiot        recurrencia    
##  Mode :logical   Mode :logical  
##  FALSE:218       FALSE:201      
##  TRUE :68        TRUE :85       
##  NA's :0         NA's :0        
##                                 
## 

Después nos va a interesar ver variables en particular, ya sea solas o en grupos más chicos. Por ejemplo, una que seguro nos va a interesar es la recurrencia, que es la clase a predecir:

table(df1$recurrencia)
## 
## FALSE  TRUE 
##   201    85

Hay algunas variables que de antemano podemos sospechar que están relacionadas con otras. Un ejemplo de esto es la relación entre recurrencia y radioterapia.

with(df1, table(recurrencia, radiot, dnn = c("recurrencia", "radioterapia")))
##            radioterapia
## recurrencia FALSE TRUE
##       FALSE   164   37
##       TRUE     54   31

Se pueden calcular a mano algunos odds. La proporción de pacientes que presentaron recurrencia habiendo recibido radioterapia es 31/(37+31). Esta misma proporción para las pacientes que no recibieron radioterapia es 54/(54+164), y luego se puede calcular el riesgo relativo de recurrencia asociado a radioterapia.

prop.rad <- 31/(37+31)
prop.norad <- 54/(54+164)
prop.rad
## [1] 0.4558824
prop.norad
## [1] 0.2477064
prop.rad/prop.norad
## [1] 1.840414

Este número sugiere que el riesgo de recurrencia es mayor en pacientes que recibieron radioterapia. Es importante entender que esta es una asociación, no una relación de causa - consecuencia. Probablemente las pacientes a las que se les aplicó radioterapia tenían una condición inicial más seria que las que no la recibieron, y el odd que se observa es un reflejo de la diferencia en condiciones basales.

Es importante determinar hacer alguna validación estadística de esta afirmación. Una opción es el test de independencia de Fisher.

with(df1, fisher.test( table(recurrencia, radiot)))
## 
##  Fisher's Exact Test for Count Data
## 
## data:  table(recurrencia, radiot)
## p-value = 0.001422
## alternative hypothesis: true odds ratio is not equal to 1
## 95 percent confidence interval:
##  1.380423 4.655846
## sample estimates:
## odds ratio 
##    2.53554

Esto indica que hay una asociación significativa, pero antes de declarar esto, tenemos que considerar que hay otras variables que pueden ser importante, y que además pueden presentar co-linealidad. Por lo tanto, para estar seguros de esta asociación, tenemos que controlar las otras variables. Esto se puede probar con regresión logística, que además sirve como método de clasificación.

Es recomendable conocer el dataset con el que uno está trabajando, incluso si uno conoce el dominio, porque puede haber variables con outliers, otras que se midieron de forma diferente a lo que estamos acostumbrados, etc.

Este dataset tiene pocas variables y de diferentes tipos, por lo que podemos enfocarnos en entender cada una de ellas.

Edad

Aqui es importante recordar que las edades en nuestro dataframe están codificadas y representan rangos:

cbind( sort(unique(df_crudo$edad)), sort(unique(df1$edad)))
##      [,1]    [,2]
## [1,] "20-29" "1" 
## [2,] "30-39" "2" 
## [3,] "40-49" "3" 
## [4,] "50-59" "4" 
## [5,] "60-69" "5" 
## [6,] "70-79" "6"

Podemos mirar las distribuciones de las edades:

table(df1$edad)
## 
##  1  2  3  4  5  6 
##  1 36 90 96 57  6

Vemos que la mayoría de las pacientes tienen edades entre los 40 y los 59 años. Podemos usar la misma variante de la función table() que vimos antes para explorar si hay relación entre la recurrencia y la edad:

table(df1$edad, df1$recurrencia)
##    
##     FALSE TRUE
##   1     1    0
##   2    21   15
##   3    63   27
##   4    71   25
##   5    40   17
##   6     5    1

Pero esta tabla no es muy clara porque las edades no están distribuidas de manera uniforme. Sería mejor una tabla con valores relativos. Pero vamos a hacerlo en dos pasos. Primero hacemos la tabla usando with() y la asignamos a una variable:

tbl_edad_rec <- with(df1, table(edad, recurrencia))
tbl_edad_rec
##     recurrencia
## edad FALSE TRUE
##    1     1    0
##    2    21   15
##    3    63   27
##    4    71   25
##    5    40   17
##    6     5    1

Nota: no es obligatorio usar with(), pero para obtene la misma tabla sin esta función es más largo:

table(df1$edad, df1$recurrencia, dnn=c("edad", "recurrencia"))
##     recurrencia
## edad FALSE TRUE
##    1     1    0
##    2    21   15
##    3    63   27
##    4    71   25
##    5    40   17
##    6     5    1

Ahora podemos calcular la tabla de valores relativos. Hay varias formas de hacer esto, nos podrían interesar las proporciones generales, es decir cada celda con respecto al total. A nosotros nos interesan ver las proporciones por fila, es decir, marginales en las filas.

prop_tbl_edad_rec <- prop.table(tbl_edad_rec,1)
prop_tbl_edad_rec
##     recurrencia
## edad     FALSE      TRUE
##    1 1.0000000 0.0000000
##    2 0.5833333 0.4166667
##    3 0.7000000 0.3000000
##    4 0.7395833 0.2604167
##    5 0.7017544 0.2982456
##    6 0.8333333 0.1666667

Antes de continuar es importante entender que hicimos al pedir que se calculan proporciones marginales por fila:

rowSums(prop_tbl_edad_rec)
## 1 2 3 4 5 6 
## 1 1 1 1 1 1

Para mejorar el aspecto y hacerla más clara podríamos dejar solo dos decimales:

round(prop_tbl_edad_rec, 2)
##     recurrencia
## edad FALSE TRUE
##    1  1.00 0.00
##    2  0.58 0.42
##    3  0.70 0.30
##    4  0.74 0.26
##    5  0.70 0.30
##    6  0.83 0.17

Utilizando la tabla original, con recuentos, podemos ver si la relación entre edad y recurrencia es significativa:

fisher.test(tbl_edad_rec)
## 
##  Fisher's Exact Test for Count Data
## 
## data:  tbl_edad_rec
## p-value = 0.5921
## alternative hypothesis: two.sided

Menopausia

Para esta variable podemos proceder de manera similar

with(df1, table(menopausia))
## menopausia
##    ge40    lt40 premeno 
##     129       7     150

La mayoría de las pacientes no había presentado menopausia al iniciarse el estudio, o la presentaron después de los 40 años.

tbl_menop_rec <- with(df1, table(menopausia, recurrencia))
tbl_menop_rec
##           recurrencia
## menopausia FALSE TRUE
##    ge40       94   35
##    lt40        5    2
##    premeno   102   48
fisher.test(tbl_menop_rec)
## 
##  Fisher's Exact Test for Count Data
## 
## data:  tbl_menop_rec
## p-value = 0.6705
## alternative hypothesis: two.sided
round( prop.table( tbl_menop_rec, 1 ),2)
##           recurrencia
## menopausia FALSE TRUE
##    ge40     0.73 0.27
##    lt40     0.71 0.29
##    premeno  0.68 0.32

En este caso tampoco parece haber una asociación entre menopausia y recurrencia.

Tamaño del tumor

El tamaño del tumor lo pudimos considerar como una variable contínua, aunque en realidad también son rangos, porque a diferencia de lo que pasa con la edad hay varias categorías, y cada una de ellas cubre un rango pequeño de medidas.

summary(df1$tamano)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##    2.00   22.00   27.00   26.41   32.00   52.00

Para comprender como se comporta esta variable nos puede ayudar un histograma:

hist(df1$tamano)

Para ver si hay alguna relación entre el tamaño del tumor y la recurrencia cinco años más tarde podemos hacer un boxplot:

with(df1, boxplot(tamano ~ recurrencia))

El boxplot parecería indicar que si bien en pacientes con o sin recurrencia se observan tumores de tamaño mediano o grandes, solo en las pacientes que no presentaron recurrencia hay tumores de tamaño pequeño. Tal vez nos puede ayudar mirar los histogramas superpuestos:

par(mfrow = c(2,1))
  with( df1[df1$recurrencia, ], hist(tamano, main = "recurrencia"))
  with( df1[!df1$recurrencia, ], hist(tamano, main = "sin recurrencia"))

par(mfrow = c(1,1))

También podríamos aplicar alguna técnica más formal, como un test de t.

t.test(tamano ~ recurrencia, data = df1)
## 
##  Welch Two Sample t-test
## 
## data:  tamano by recurrencia
## t = -3.279, df = 196.04, p-value = 0.001233
## alternative hypothesis: true difference in means is not equal to 0
## 95 percent confidence interval:
##  -6.447962 -1.604716
## sample estimates:
## mean in group FALSE  mean in group TRUE 
##            25.20896            29.23529

El tamaño del tumor es una variable que tenemos que considerar al construir algún modelo de predicción de la recurrencia.

Cantidad de nódulos comprometidos

summary(df1$nodulos)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   1.000   1.000   1.000   2.573   4.000  25.000
hist(df1$nodulos)

La mayoría de los casos presentan pocos nódulos linfáticos afectados. Podemos empezar con algo parecido a lo que hicimos antes:

par(mfrow = c(2,1))
  with( df1[df1$recurrencia, ], hist(nodulos, main = "recurrencia"))
  with( df1[!df1$recurrencia, ], hist(nodulos, main = "sin recurrencia"))

par(mfrow = c(1,1))

En este caso el gráfico no es tan útil. En primer lugar no coinciden los valores de los ejes x. Esto es porque en pacientes que presentaron recurrencia se habían observado mayor número de nódulos afectados. Por otro lado, como los casos con muchos nódulos afectados son pocos, no es fácil ver una tendencia.

Una tabla con los datos tal cual tampoco nos va a servir, pero podemos crear una variable nueva que indique si la cantidad de nódulos afectados es baja, intermedia o alta. Antes determinemos entre que valores varía esta variable y calculemos donde se ubican los quartiles:

range(df1$nodulos)
## [1]  1 25
quantile(df1$nodulos)
##   0%  25%  50%  75% 100% 
##    1    1    1    4   25

El 75% de los casos tuvieron cuatro o menos nódulos linfáticos afectados, por lo que puede ser mejor dividir la variable en dos, en lugar de las tres categorías inicialmente propuestas. Lo vamos a hacer en dos pasos

nod_cant <- cut(df1$nodulos, breaks = c(0,4,25))
head(nod_cant)
## [1] (0,4] (0,4] (0,4] (0,4] (0,4] (0,4]
## Levels: (0,4] (4,25]
head(data.frame(df1$nodulos, nod_cant))
##   df1.nodulos nod_cant
## 1           1    (0,4]
## 2           1    (0,4]
## 3           1    (0,4]
## 4           1    (0,4]
## 5           4    (0,4]
## 6           4    (0,4]
nod_cant <- cut(df1$nodulos, breaks = c(0,5,25), labels = c("bajo", "alto"))

Ahora podemos ver la distribución de esta variable sola y en relación a las recurrencias.

table(nod_cant)
## nod_cant
## bajo alto 
##  249   37
tbl_nod_cant_rec <- with(df1, table(recurrencia, nod_cant))
prop.table(tbl_nod_cant_rec, 1)
##            nod_cant
## recurrencia       bajo       alto
##       FALSE 0.92537313 0.07462687
##       TRUE  0.74117647 0.25882353
fisher.test(tbl_nod_cant_rec)
## 
##  Fisher's Exact Test for Count Data
## 
## data:  tbl_nod_cant_rec
## p-value = 6.626e-05
## alternative hypothesis: true odds ratio is not equal to 1
## 95 percent confidence interval:
##  1.995553 9.521653
## sample estimates:
## odds ratio 
##   4.304332

Estos resultados nos sugieren que el número de nódulos afectados está relacionado con la tasa de recurrencias. Por lo tanto es otra variable a considerar para el modelo de predicción de recurrencias.

Grado histológico

El grado de malignidad a partir de evidencia histológica se registra en una escala que toma los valores 1,2 y 3 para indicar niveles de malignidiad bajo, intermedio, alto. Si bien es una variable numérica, es una escala de tres valores y lo podemos analizar con herramientas que ya vimos.

table(df1$grado)
## 
##   1   2   3 
##  71 130  85
tbl_grado_rec <- with(df1, table(grado, recurrencia))
prop.table(tbl_grado_rec, 1)
##      recurrencia
## grado     FALSE      TRUE
##     1 0.8309859 0.1690141
##     2 0.7846154 0.2153846
##     3 0.4705882 0.5294118

Vemos que la tasa de recurrencia va aumentando a medida que aumenta el grado de malignidad, y que esa asociación es significativa:

fisher.test(tbl_grado_rec)
## 
##  Fisher's Exact Test for Count Data
## 
## data:  tbl_grado_rec
## p-value = 2.834e-07
## alternative hypothesis: two.sided

Células cancerosas fuera de las cápsulas de los nódulos linfáticos.

Esta es una variable lógica, la podemos analizar directamente en relación a la co-ocurrencia.

tbl_cpas_rec <- with(df1, table(recurrencia, capsula))
prop.table(tbl_cpas_rec, 1)
##            capsula
## recurrencia     FALSE      TRUE
##       FALSE 0.8724490 0.1275510
##       TRUE  0.6219512 0.3780488
fisher.test(tbl_cpas_rec)
## 
##  Fisher's Exact Test for Count Data
## 
## data:  tbl_cpas_rec
## p-value = 5.834e-06
## alternative hypothesis: true odds ratio is not equal to 1
## 95 percent confidence interval:
##  2.149635 8.036117
## sample estimates:
## odds ratio 
##   4.132377

En principio, esta es otra variable importante que influye sobre la recurrencia.

Ubicación del tumor

Aqui tenemos dos variables, mama y cuadrante, las dos son variables de tipo factor y para el análisis exploratorio podemos repetir lo que ya conocemos:

table(df1$mama)
## 
##  left right 
##   152   134
tbl_mama_rec <- with(df1, table(recurrencia, mama))
tbl_mama_rec
##            mama
## recurrencia left right
##       FALSE  103    98
##       TRUE    49    36
prop.table(tbl_mama_rec, 1)
##            mama
## recurrencia      left     right
##       FALSE 0.5124378 0.4875622
##       TRUE  0.5764706 0.4235294
prop.table(tbl_mama_rec, 2)
##            mama
## recurrencia      left     right
##       FALSE 0.6776316 0.7313433
##       TRUE  0.3223684 0.2686567
fisher.test(tbl_mama_rec)
## 
##  Fisher's Exact Test for Count Data
## 
## data:  tbl_mama_rec
## p-value = 0.3648
## alternative hypothesis: true odds ratio is not equal to 1
## 95 percent confidence interval:
##  0.4471809 1.3276194
## sample estimates:
## odds ratio 
##  0.7728802

No hay diferencias significativas entre recurrencia de tumores encontrados originalmente en mama izquierda o derecha. Ahora veamos los cuadrantes:

table(df1$cuadrante)
## 
##   central  left_low   left_up right_low  right_up 
##        21       110        97        24        33
tbl_cuad_rec <- with(df1, table(recurrencia, cuadrante))
round( prop.table(tbl_cuad_rec, 2), 2)
##            cuadrante
## recurrencia central left_low left_up right_low right_up
##       FALSE    0.81     0.68    0.73      0.75     0.61
##       TRUE     0.19     0.32    0.27      0.25     0.39

En primer lugar hay diferencias en cuanto a la ubicación del tumor en la mama. Son más frecuentes en el lateral izquierdo, arriba y abajo. Pero el punto importante para este análisis es determinar qué sucede con la recurrencia. La última tabla sugiere que hay algunas diferencias, porque las tasas de recurrencia varían entre 0.19 y 0.39, pero hay que determinar la significancia.

Primero calculamos la tasa general de recurrencia:

prop.table(table(df1$recurrencia))
## 
##     FALSE      TRUE 
## 0.7027972 0.2972028

Ahora veamos si las proporciones que observamops se alejan significativamente de 0.297.

recurrencia_cuadrante <- tbl_cuad_rec[2,]
casos_cuadrante <- colSums(tbl_cuad_rec)
p_esperada <- rep(0.297, 5)
prop.test(x = recurrencia_cuadrante, n = casos_cuadrante, p = p_esperada)
## 
##  5-sample test for given proportions without continuity correction
## 
## data:  recurrencia_cuadrante out of casos_cuadrante, null probabilities p_esperada
## X-squared = 3.5065, df = 5, p-value = 0.6224
## alternative hypothesis: two.sided
## null values:
## prop 1 prop 2 prop 3 prop 4 prop 5 
##  0.297  0.297  0.297  0.297  0.297 
## sample estimates:
##    prop 1    prop 2    prop 3    prop 4    prop 5 
## 0.1904762 0.3181818 0.2680412 0.2500000 0.3939394

A pesar de las diferencias entre proporciones de recurrencia por cuadrante, no podemos declarar que esas diferencias sean significativas.

Radioterapia

La aplicación de radioterapia está registrada como una variable lógica. Por lo tanto podemos probar la asociación con la recurrencia utilizando las herramientas que ya vimos antes:

with(df1, fisher.test( table(recurrencia, radiot)))
## 
##  Fisher's Exact Test for Count Data
## 
## data:  table(recurrencia, radiot)
## p-value = 0.001422
## alternative hypothesis: true odds ratio is not equal to 1
## 95 percent confidence interval:
##  1.380423 4.655846
## sample estimates:
## odds ratio 
##    2.53554

Hay una asociación significativa entre las dos variables. Prestar atención a esto:

with(df1, (prop.table( table(recurrencia, radiot), 2)))
##            radiot
## recurrencia     FALSE      TRUE
##       FALSE 0.7522936 0.5441176
##       TRUE  0.2477064 0.4558824

Ya habíamos discutido al inicio de esta sección la diferencia entre asociación y relaciones causa-efecto.

Resumen

Hasta el momento sabemos que hay variables que no parecen estar aociadas con la probabilidad de recurrencia:

Y otras variables que sí parecen ser importantes:

Algunas de las variables no importantes, pueden serlo cuando se las considera en conjunto con otras (interacciones), y algunas variables pueden presentar colinealidad entre ellas; es decir, que están asociadas entre ellas.

Guardar los datos

Llegados a a este punto nos puede interesar guardar ests datos en un archivo propio (diferente de .RData).

saveRDS(df1, "datos_limpios_1.RDS")

También podemos guardar una variante de este dataframe. Recordemos que antes habíamos analizado si la cantidad de nódulos afectados era alta o baja, en lugar del número real de nódulos afectados. Podemos crear un nuevo dataframe y también guardarlo.

df2 <- df1
df2$nodulos <- NULL
df2$nods_af <- nod_cant
saveRDS(df2, "datos_limpios_2.RDS")

Manipulación de datos con dplyr

Es muy útil contar con herramientas que permitan filtrar, seleccionar y ordenar los datos. Todo esto se puede hacer con herramientas presentes en R base, pero desde hace unos años existe un paquete llamado dplyr que simplifica mucho estas tareas. Si no lo tenemos instalado, primero hay que instalarlo (esto se hace solo la primera vez que se va a usar) y después, en cada sesión, cargarlo en nuestra biblioteca de paquetes.

# install.packages("dplyr")
library(dplyr)
## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union

Supongamos que necesitamos un listado de los datos de recurrencia y grado de malignidad de las pacientes en los grupos de edad 3 y 4, que recibieron radioterapia y que el listado esté ordenado por tamaño del tumor.

Primero lo vamos a hacer en varios pasos:

set1 <- filter(df1, edad %in% c(3,4) & radiot)
head(set1)
##   edad menopausia tamano nodulos capsula grado  mama cuadrante radiot
## 1    3    premeno     37       1    TRUE     3 right  left_low   TRUE
## 2    4    premeno     27       4   FALSE     2 right   left_up   TRUE
## 3    3       ge40     42      16    TRUE     2 right   left_up   TRUE
## 4    3    premeno     27       1   FALSE     2  left  left_low   TRUE
## 5    3    premeno     32       1   FALSE     1  left  left_low   TRUE
## 6    3    premeno     37      10    TRUE     2 right  right_up   TRUE
##   recurrencia
## 1       FALSE
## 2       FALSE
## 3       FALSE
## 4        TRUE
## 5        TRUE
## 6       FALSE
set1 <- arrange(set1, tamano)
head(set1)
##   edad menopausia tamano nodulos capsula grado  mama cuadrante radiot
## 1    3    premeno      7       1   FALSE     1  left  left_low   TRUE
## 2    3    premeno     12       1   FALSE     2  left  left_low   TRUE
## 3    4       ge40     17       1    TRUE     2  left   central   TRUE
## 4    3    premeno     17      13   FALSE     3 right right_low   TRUE
## 5    3    premeno     22       4    TRUE     2  left  left_low   TRUE
## 6    3       ge40     22       4   FALSE     3 right  left_low   TRUE
##   recurrencia
## 1       FALSE
## 2       FALSE
## 3       FALSE
## 4       FALSE
## 5        TRUE
## 6        TRUE
set2 <- select(set1, c(grado, recurrencia) )
head(set2)
##   grado recurrencia
## 1     1       FALSE
## 2     2       FALSE
## 3     2       FALSE
## 4     3       FALSE
## 5     2        TRUE
## 6     3        TRUE

Podríamos compactar todo esto en una sola línea:

set_c <- select( arrange( filter(df1, edad %in% c(3,4) & radiot), tamano),  c(grado, recurrencia))
head(set_c)
##   grado recurrencia
## 1     1       FALSE
## 2     2       FALSE
## 3     2       FALSE
## 4     3       FALSE
## 5     2        TRUE
## 6     3        TRUE

No es muy claro ¿No?. El paquete magrittr de R introdujo el operado pipe, “%>%” que sirve para pasar la salida de una función como entrada de otra. El paquete magrittr se instala al instalar dplyr. Veamos como usarlo

set_d <- df1 %>% filter(edad %in% c(3,4) & radiot) %>% arrange(tamano) %>% 
  select(grado, recurrencia)
head(set_d)
##   grado recurrencia
## 1     1       FALSE
## 2     2       FALSE
## 3     2       FALSE
## 4     3       FALSE
## 5     2        TRUE
## 6     3        TRUE

Otro ejemplo. Supongamos que necesitamos conocer el promedio, la media y el desvío estándar de los tamaños de los tumores:

df1 %>% summarize( media = mean(tamano), mediana = median(tamano), desvio = sd(tamano))
##      media mediana   desvio
## 1 26.40559      27 10.52965

Y otro ejemplo más. Supongamos que queremos repetir el cálculo anterior pero para cada uno de los grados de malignidad y guardar todo eso en un objeto de R.

df_grado_tam <- df1 %>% 
  group_by(grado) %>% 
  summarize( media = mean(tamano), mediana = median(tamano), desvio = sd(tamano))
  
df_grado_tam
## # A tibble: 3 × 4
##   grado    media mediana    desvio
##   <int>    <dbl>   <dbl>     <dbl>
## 1     1 23.33803      22 11.146997
## 2     2 26.03846      27 10.945534
## 3     3 29.52941      32  8.402931