Práctica 3

Autor/a

Objetivos de la práctica

Al finalizar esta sesión, el alumnado será capaz de:

  • Reconocer el papel que juegan los paréntesis, los corchetes y las llaves en el lenguaje R.
  • Crear condiciones lógicas.
  • Seleccionar (filtrar) casos mediante condiciones lógicas.
  • Categorizar y recodificar variables.
  • Manejar valores faltantes (recodificación y argumentos na.omit).
  • Realizar ejercicios de recapitulación de las tres prácticas .

  • Una vez abierto RStudio, crea un nuevo script para desarrollar esta práctica y ponle como título de cabecera # Práctica 3 ----
  • Establece el directorio de trabajo que desees para guardar todo lo que vayamos haciendo (recuerda que lo puedes hacer usando el atajo ‘Ctrl’ + ‘Mayús’ + ‘H’ y la conveniencia de que uses tu pen-drive).
  • Guarda el script que estamos iniciando


R A través de este enlace, puedes acceder a una página con los cuadros resumen de funciones de R.


Importa el archivo de datos colesterol01.rds, es el mismo que ya exploramos en la práctica 2.

## Lectura del archivo .rds desde una URL ----
url_datos <- "https://www.ugr.es/~pfemia/BSRLab/dat/colesterol_01.rds"
datos <- readRDS(url(url_datos))
  • Comprueba si la lectura se ha efectuado de forma correcta (por ejemplo con head(datos)).
  • Por cierto, ¿de qué edad son los sujetos que conforman la base de datos? ¿Qué proporción de mujeres hay?

1 Consolidando aspectos del lenguaje R

En R los paréntesis, corchetes y llaves cumplen funciones distintas. Entenderlos bien es fundamental para leer, manipular y analizar datos correctamente.

  • Paréntesis (): Agrupan expresiones y definen argumentos de funciones.
  • Corchetes []: Seleccionan elementos de vectores o data frames.
  • Llaves {}: Agrupan varias líneas de código para ejecutarlas como un bloque.

1.1 Uso del paréntesis ( )

Recordemos con un ejemplo algo que ya vimos en la práctica 1 sobre la necesidad de agrupar expresiones en las operaciones aritméticas: El índice de masa corporal de Quetelet es el cociente entre el peso (Kg) dividido por la talla (m) al cuadrado

\[ IMC=\frac{peso}{talla^{2}}. \]

Si la talla está expresada en cm, y no en metros, el cálculo debe hacerse como

\[ IMC=\frac{peso}{(talla/100)^{2}} \]

Omitir el paréntesis en el denominador supone que el cuadrado solo afecta al valor 100, no al cociente talla/100. Recordémoslo con números, supongamos que un sujeto tiene un peso de 80 kg y mide 170 cm.

# valores de peso y talla:
peso <- 80
talla <- 170

# El cálculo del imc debe hacerse como
peso / ((talla / 100)^2)

# compara el resultado con este cálculo en el que se omite el paréntesis
peso / talla / 100^2

En una expresión aritmética, R sigue un orden de prioridad: primero evalúa los paréntesis, comenzando por el más interno, luego las potencias, después la multiplicación y división, y finalmente la suma y resta (a igualdad de prioridad, la evaluación es de izquierda a derecha). Este orden garantiza que las operaciones se interpreten correctamente.

En la práctica, es habitual que, para evitar malentendidos y hacer el código más legible, añadamos paréntesis aunque no sean estrictamente necesarios. En general, los paréntesis más vale que sobren a que falten; por ejemplo, sabiendo el orden de prioridad, el paréntesis que envuelve a todo el denominador en la expresión anterior del IMC, no resulta necesario, pero tampoco estorba.

Si has pensado que bastaba con escribir inicialmente el valor de la talla en metros, como 1.7, plantéate si esto es operativo para calcular el índice de masa corporal en todos los sujetos de una base de datos.

Ejercicio propuesto:

  • Observa la cabecera de datos (por ejemplo, con la función head()) e identifica en qué unidades está expresada la variable talla.
  • Agrega a datos la variable imc con el cálculo correcto del índice de masa corporal.
  • Comprueba el resultado: ¿cuánto vale la media de esta variable? (debe ser 22.95)

También hemos tenido ya ocasión de apreciar que los paréntesis son imprescindibles para indicar los argumentos de una función. Cada vez que ejecutamos una función, debemos escribir su nombre seguido —sin espacios en blanco intermedios— de un par de paréntesis. Si la función no requiere argumentos, los paréntesis pueden ir vacíos, pero deben aparecer igualmente.

A continuación tienes algunos ejemplos de funciones básicas sin argumentos. Observa qué ocurre si intentas llamarlas sin escribir los paréntesis:

date() # fecha y hora actuales del sistema
ls() # listado con los objetos definidos en el entorno
getwd() # indica la carpeta de trabajo

1.2 Uso del corchete [ ]

El corchete tiene un propósito muy diferente al del paréntesis. En la práctica 1 ya vimos que es la forma de seleccionar elementos de un vector y que también es válido para los data frame (ver práctica 1). Manejar esta notación con soltura es muy importante en R, así que vamos a repasarla brevemente.

Intenta resolverlo tú, mira la solución solamente para comprobar que has sabido hacerlo.

Obtén los siguientes valores, o conjuntos de valores, del data frame datos:

  1. El vector con el nivel de colesterol que tienen todos los casos.
  2. ¿Qué nivel de colesterol tiene el sujeto que ocupa la fila 73? ¿Es un hombre, o una mujer?
  3. Recupera el valor de la variable que ocupa la séptima columna de datos para el sujeto de la fila 73.
  4. Recupera un vector con los nombres de las variables que aparecen en datos.
  5. Usando el número de la columna, recupera el nombre de la variable que ocupa la 7ª columna (se trata de hacerlo con código, no de mostrar los datos y ponerse a contar columnas a ojo).
  6. Recupera el registro del sujeto de la fila 73, es decir, el valor que tiene en todas y cada una de las variables.
# 1
datos$colesterol
# 2
datos$colesterol[73]
datos$sexo[73]
# 3.
datos[73, 7]
# 4
colnames(datos)
# 5
colnames(datos)[7]
# 6
datos[73, ]

Observaciones:

  • En el ejercicio 5, observa la diferencia entre el cometido que tiene el paréntesis y el que tiene el corchete.
  • Cuando el corchete va a la derecha del nombre de un vector, solo hay que indicar una coordenada (solo hay una dimensión). Pero si aparece a la derecha del nombre de un data frame, hay que indicar dos (separadas por una coma): la fila y la columna.
    Ya sabemos que si se quiere aludir a todas las filas, se deja el hueco de la fila en blanco; si se quiere aludir a todas las columnas, se deja el hueco de la columna en blanco, pero la coma de separación siempre hay que escribirla.

Razona la respuesta a estas dos cuestiones:

  • ¿Qué hace cada una de estas dos sentencias?:
    datos
    datos[,]
  • ¿Hacen lo mismo las dos sentencias siguientes?:
    datos[3,]$peso
    datos$peso[3]

1.3 Uso de las llaves

Aunque ya habíamos trabajado con paréntesis y corchetes, las llaves aparecen ahora como un elemento nuevo. Su función es encerrar un conjunto de líneas de código que deben ejecutarse de manera conjunta. Dentro de las llaves, las instrucciones se ejecutan en el orden en que están escritas, de arriba abajo; sin embargo, desde fuera, todo el bloque se comporta como si fuera una única sentencia, eso sí, más elaborada.

Cuando se ejecuta un bloque de código encerrado entre llaves, el valor que se obtiene —ya sea asignándolo a una variable o mostrándolo directamente en la consola— corresponde siempre al resultado de la última sentencia del bloque. Veamos un ejemplo:

Un paciente pesa 86 kg y mide 160 cm. Se desea calcular su índice de masa corporal (IMC) y guardar en una variable llamada obesidad el valor TRUE o FALSE según el resultado. La variable debe tomar el valor TRUE únicamente si el IMC es superior a 30.

Este ejemplo sirve como ilustración de la función de las llaves.

# asignamos los valores de peso y talla
peso <- 86
talla <- 160

# Definimos la variable obesidad como sigue:
obesidad <- {
  imc <- peso / ((talla / 100)^2) # <-- cálculo del IMC
  imc >= 30 # <-- la última línea es el valor devuelto por este bloque
}

# leemos el valor que ha tomado obesidad
obesidad
[1] TRUE

Observa que si has posicionado el cursor delante de la definición de obesidad, el bloque se ha ejecutado por completo, no has tenido que ir línea a línea.

En las primeras etapas de aprendizaje de R puede parecer que las llaves no son especialmente importantes. Sin embargo, este símbolo de agrupación se vuelve esencial cuando empezamos a trabajar con estructuras de control y funciones, algo que abordaremos más adelante. Por ahora, conviene quedarse con la idea de que las llaves sirven para organizar cálculos que requieren varios pasos internos, pero que, en conjunto, producen un único resultado final.

Si observas el código anterior, verás que RStudio muestra en el margen izquierdo (entre el número de fila y la definición de la variable obesidad) un triángulo con el vértice hacia abajo. Si pinchas en él con el ratón, verás que permite colapsar el contenido de las llaves. Esto es muy útil para tener un código claro cuando se hacen scripts largos y elaborados.

2 Selección de casos

En la práctica 1 vimos que para acceder a elementos de un vector o de un data frame no solo podemos usar sus números de fila, sino también condiciones que deben cumplir esas filas. Ahora vamos a profundizar en cómo construir esas condiciones lógicas, que son la clave para filtrar y seleccionar casos de forma precisa.

2.1 Creación de condiciones lógicas de filtrado

Una condición lógica simple es una expresión que compara dos valores y devuelve únicamente uno de dos posibles resultados: TRUE o FALSE, dependiendo de si se cumple o no la condición. La estructura general de una condición lógica simple es la siguiente:

variable’   ‘operaror relacional’    ‘valor o variable


Por ejemplo: datos$edad < 16.

Si necesitas revisar cuáles son los operadores relacionales, aquí tienes el resumen:

Suponemos aquí que x e y representan dos valores o dos vectores numéricos

operador Propósito Tipo Ejemplo
== igual lógico x == y
!= distinto lógico x !- y
< menor lógico x < y
<= menor o igual lógico x <= y
> mayor lógico x > y
>= mayor o igual lógico x >= y
Cuidado con no poner un espacio intercalado en los operadores que se componen de dos signos. Deben ir juntos.

Si en una condición lógica simple solo intervienen valores individuales (no vectores), el resultado será un único TRUE o FALSE. En cambio, cuando la condición se aplica a un vector, la comparación se evalúa elemento por elemento, y el resultado es otro vector lógico, formado por los TRUE y FALSE obtenidos en cada posición. Veamos algunos ejemplos:

datos$edad[3] < 16 # devuelve un único valor lógico

datos$edad < 16 # devuelve un vector de valores lógicos

Una condición lógica se puede asignar a una variable y utilizarla como un filtro. Por ejemplo:

filtro <- datos$edad < 16 # Creamos un vector lógico a partir de la condición

# Ahora podemos usar el vector 'filtro' para seleccionar las filas de
# 'datos' que cumplen la condición indicada
datos[filtro, ]

¿Qué está ocurriendo?

  • Al evaluar datos$edad < 16, R compara cada edad con 16 y genera un vector lógico, formado por TRUE y FALSE dependiendo de si cada caso cumple o no la condición.
  • Guardamos ese vector en un objeto llamado filtro, simplemente para poder reutilizarlo.
  • Cuando escribimos datos[filtro, ], estamos diciendo: “Muéstrame todas las filas de datos (i=1,2,...) para las que filtro[i] sea TRUE; si es FALSE, esa fila se descarta.”

Este mecanismo es la base del filtrado por condiciones en R: una idea simple, pero muy potente.

None Caso clínico: Selección de casos con el nivel de colesterol superior a un umbral

Se desea identificar qué casos del data frame datos presentan un nivel de colesterol total superior al umbral clínico de 200 mg/dL.

Además, interesa comunicar la proporción de sujetos cuyo nivel de colesterol excede dicho umbral.

No olvides tomar alguna nota en tus apuntes con el razonamiento para resolver este tipo de cuestiones

Consejo: En adelante, evita usar valores fijos en los cálculos. Es mejor definir un objeto con el valor que sea y, en adelante, usar ese objeto, no el valor. Así, si cambiamos de valor no hay que cambiar todas las fórmulas en donde interviene, bastará con cambiar solo aquella en donde se define.

# 1. Creamos el objeto con el valor del umbral (si cambiamos de
# valor, solo habrá que actualizar esta línea)
umbral <- 200

# Ahora elaboramos el filtro
filtro <- datos$colesterol > umbral

# 2. Aplicamos el filtro a datos, y
# guardamos el resultado en un nuevo objeto
col_alto <- datos[filtro, ]

# Comprueba que el objeto 'col_alto'
# es un data.frame
class(col_alto)

# 3. La proporción de casos pedida será el cociente
# entre el número de filas de `col_alto` y el de `datos`
prop_col_alto <- nrow(col_alto) / nrow(datos)

# veamos su valor
prop_col_alto

# si lo multiplicas por 100, lo puedes indicar como %

2.2 Creación de condiciones lógicas más complejas

La unidad lógica básica, que no se puede descomponer sin perder su sentido, es la sentencia lógica simple, formada por la estructura variableoperador relacionalvalor. También puede escribirse al revés, es decir, valoroperador relacionalvariable, pero lo esencial es que el operador relacional queda siempre intercalado entre los dos elementos que queremos comparar.

Para construir sentencias lógicas más complejas, lo que hacemos es combinar varias sentencias simples sin alterar su estructura interna. Esto se consigue mediante los operadores lógicos: Y (&), O (|) y NO (!) cuyos símbolos aparecen entre paréntesis. Veamos un ejemplo:

None Caso clínico: Selección de casos con más de un criterio

Se desea poner en marcha un programa de promoción de la vida saludable en la población juvenil y se quiere prestar especial atención a aquellos casos que presentan un nivel de colesterol superior a 200 mg/dL y un pliegue abdominal mayor de 25 mm. Crea un filtro para detectar los casos que cumplen estas condiciones en nuestra base de datos (dats) e indica qué proporción representan respecto al total.

Ahora en la condición para el filtrado intervienen dos variables, el nivel de colesterol y el pliegue abdominal. La condición debe implicar a un operador lógico Y, ya que hay que detectar aquellos casos que tengan el colesterol Y el pliegue abdominal superior a cada umbral.

# 1. Creamos los umbrales
umbral_col <- 200
umbral_pabd <- 25

# 2. Creamos la condición de filtrado
filtro <- (datos$colesterol > umbral_col) & (datos$pabdominal > umbral_pabd)
# los paréntesis no son estrictamente necesarios,
# pero proporcionan claridad a la expresión.
# Ya sabes: más vale que sobren a que falten.

# 3. Creamos un data.frame con la selección
datos_preven <- datos[filtro, ]

# 4. Vamos a explorar estos casos:
nrow(datos_preven)
# como son solo 6, podemos ver el
# data.frame completo
datos_preven

# 5. Veamos qué proporción representan
nrow(datos_preven) / nrow(datos)
None Selección de casos pertenecientes a un intervalo

Detecta qué casos (en datos) tienen un pliegue abdominal mayor o igual a 12 mm pero menor (estricto) a 25 mm.

Ahora la condición de filtrado afecta sola a una variable, pero eso da igual, se construye como antes

# 1. Creamos los umbrales
umbral_pabd_inf <- 12
umbral_pabd_sup <- 25

# 2. Creamos el filtro
filtro <- (datos$pabdominal >= umbral_pabd_inf) &
  (datos$pabdominal < umbral_pabd_sup)

# 3. Generamos el data.frame con los casos seleccionados
datos_pabd <- datos[filtro, ]

CUIDADO: si has probado a hacer
filtro <- umbral_pabd_inf <= datos$pabdominal < umbral_pabd_sup
habrás visto que no funciona. Observa que esta línea rompe la estructura de una sentencia lógica simple tal y como se indicó al principio.

None Selección de casos con antígeno “B”

Se necesita prestar atención específica a los sujetos con grupo sanguíneo B o AB, ya que ambos comparten el antígeno B en la superficie de los glóbulos rojos. Este antígeno no solo determina la compatibilidad transfusional, sino que también se ha asociado en distintos estudios a diferencias en la respuesta inmunitaria, en la susceptibilidad a determinadas infecciones y en ciertos perfiles metabólicos. Aunque estas asociaciones no implican causalidad directa, sí justifican que, en intervenciones preventivas o de cribado, pueda ser relevante agrupar a los individuos con presencia del antígeno B para análisis comparativos o para identificar patrones diferenciales de salud.

Obten un data frame en donde aparezcan solo los sujetos que presente el antígeno B. Determina qué proporción de casos representan respecto al total.

filtro <- (datos$gs == "B") | (datos$gs == "AB")
datos_B <- datos[filtro, ]

# veamos la cabecera:
head(datos_B)

# obtenemos la proporción pedida
nrow(datos_B) / nrow(datos)

2.3 Selección de columnas

Hasta ahora hemos aplicado filtros sobre las filas del data frame para seleccionar determinados casos y siempre hemos recuperado todas las columnas. Pero ¿qué ocurre si solo queremos quedarnos con algunas columnas de interés?

Existen varias maneras de hacerlo. Una opción es crear un vector con los números de las columnas que queremos conservar —o, si los indicamos en negativo, las que queremos excluir—. Otra alternativa, normalmente más intuitiva, consiste en escribir directamente los nombres de las columnas que deseamos seleccionar (siempre entre comillas). Vamos a practicarlo.

Responde a las cuestiones que

  1. Variable sexo y grupo sanguíneo (gs) de los sujetos con grupo sanguíneo “A”. Indica cuántos casos hay y qué proporción representan respecto al número de casos totales.
  2. Variables colesterol, HDL y LDL de los sujetos con grupos sanguíneos “B” o “AB”
  3. Genera un data.frame que contenga todas las variables de datos salvo el sexo

Para ganar simplicidad, no vamos a hacer variables de filtro. Ponemos directamente las condiciones dentro de los corchetes.

# 1
df_1 <- datos[datos$gs == "A", c("gs", "sexo")]
head(df_1) # vemos la cabecera
nrow(df_1) # nº de casos
nrow(df_1) / nrow(datos) # proporción

# 2
# incluimos el grupo sanguíneo en la selección para
# comprobar que, efectivamente, es correcta
df_2 <- datos[
  (datos$gs == "B") | (datos$gs == "AB"),
  c("gs", "colesterol", "HDL", "LDL")
]
head(df_2)

# 3
# veamos en qué columna aparece la variable sexo
head(datos)
# generamos un data.frame sin la segunda columna
df_3 <- datos[, -2]
head(df_3)

Si de un data frame se extrae una sola columna, el objeto generado es un vector, no un data frame.

2.4 Función subset()

La función subset() permite seleccionar filas y/o columnas de un data frame de forma muy legible, ya que utiliza directamente los nombres de las variables sin necesidad de repetir el nombre del data frame con ‘$’ ni de escribir los nombres entre comillas.

Los argumentos de subset() son:

  • x: permite indicar cuál es el data frame original.
  • subset: es opcional y se utiliza para indicar un filtrado sobre las filas.
  • select: es opcional y permite seleccionar las columnas deseadas.

Veamos algunos ejemplos:

# como no se indica select, se incluyen todas las columnas:
df_4 <- subset(datos, subset = (colesterol > 200) | (LDL > 90))
head(df_4)

# como no se indica subset, se incluyen todas las filas:
df_5 <- subset(datos, select = c(colesterol, LDL, HDL))
head(df_5)

# seleccion del grupo sanguíneo y de la edad de los casos con antígeno B:
df_6 <- subset(datos, subset = (gs == "AB") | (gs == "B"), select = c(gs, edad))
head(df_6)

3 Categorización y recodificación de variables

En la práctica es habitual que necesitemos categorizar variables cuantitativas, ya sea para elaborar tablas de frecuencias que sirvan de síntesis, o para transformar sus valores en grupos que faciliten la interpretación clínica. También es frecuente tener que recodificar las categorías de una variable cualitativa cuando queremos simplificarla o reagrupar niveles. En este apartado aprenderás a abordar ambas tareas: categorizar variables numéricas y recodificar variables cualitativas.

Para ello utilizaremos herramientas básicas de R: la función ifelse(), útil en los dos casos; el operador %in%, especialmente práctico para recodificar factores; y la función cut(), diseñada para crear categorías a partir de variables continuas.

3.1 La función ifelse()

La función ifelse() evalúa una condición lógica y devuelve un valor si se cumple y otro diferente en caso contrario. Los argumentos de esta función son:

  • test: Es la condición lógica a evaluar.
  • yes: Resultado que devuelve la función si se cumple la condición dada en test.
  • no: Resultado que devuelve la función si no se cumple la condición test.

Veamos un par de ejemplos:

# Queremos hacer una consulta sobre la edad del primer sujeto de `datos`
condicion <- datos$edad[1] > 17
ifelse(test = condicion, yes = "Si", no = "No")
[1] "No"
# observa que si mantenemos el orden de los argumentos
# no hace falta nombrarlos. La siguiente sentencia
# equivale a la anterior
ifelse(condicion, "Si", "No")
[1] "No"

Si en la expresión lógica participa un vector, se evalúa la condición para cada uno de sus elementos y el resultado de la función es también un vector.

condicion <- datos$edad > 17
grupo_edad <- ifelse(test = condicion, yes = "menor", no = "mayor")
head(grupo_edad)
[1] "mayor" "menor" "mayor" "menor" "mayor" "menor"

Obtener una variable binaria llamada ag_B que tome el valor 1 si el grupo sanguíneo presenta el antígeno B (estos son los grupos sanguíneos B y AB), y 0 en caso contrario.

# Elaboramos la condición
condicion <- datos$gs == "B" | datos$gs == "AB"

# La usamos como argumento de ifelse
datos$ag_B <- ifelse(test = condicion, yes = 1, no = 0)

# vamos a comprobar el resultado
# pedimos la tabla de frecuencias del grupo sanguíneo
table(datos$gs)
# pedimos la tabla de frecuencias del ag_B
table(datos$ag_B)

# observa que las frecuencias de gs=="B" y gs=="AB"
# suman 32, lo mismo que la de ag_B==1

3.2 El operador %in%

El operador %in% permite comprobar si los elementos de un vector pertenecen a un conjunto determinado. En la práctica, compara cada valor del vector con una lista de posibles valores y devuelve TRUE para aquellos que forman parte de ese conjunto.

Vamos a ver su funcionamiento con un ejemplo muy simple: creamos un vector con la lista de las capitales de provincia andaluzas y después consultamos si una ciudad que sea de interés pertenece a esa lista:

# Definimos un vector con la lista de capitales andaluzas
Andalucia <- c(
  "Almería",
  "Granada",
  "Jaén",
  "Córdoba",
  "Málaga",
  "Sevilla",
  "Cádiz",
  "Huelva"
)

# Usamos %in% para comprobar si un elemento, por ejemplo "Madrid" está
# en el listado anterior
"Madrid" %in% Andalucia
[1] FALSE

Vemos otro ejemplo. Ahora la consulta es también un vector:

# Definimos un vector con las ciudades de procedencia de cinco pacientes
ciudades <- c("Granada", "Salamanca", "Cáceres", "Teruel", "Huelva")

# Realizamos la consulta sobre ciudades andaluzas
# observa que el resultado generado es un vector lógico
ciudades %in% Andalucia
[1]  TRUE FALSE FALSE FALSE  TRUE

Vamos a repetir el ejemplo de la sección anterior: se trata de obtener una variable binaria llamada ag_B que tome el valor 1 si el grupo sanguíneo presenta el antígeno B y 0 en caso contrario, pero esta vez utilizando el operador %in%.

condicion <- datos$gs %in% c("B", "AB")
resultado <- ifelse(condicion, 1, 0)
# comprobamos con la tabla de frecuencias
# que la asignación coincide a la que hemos
# hecho antes
table(resultado)

Recodificación de una variable cualitativa. La variable nivel_AF de datos es un factor con niveles baja, moderada y alta que indica la intensidad de la práctica de la actividad física. Se pide dicotomizar esta variable en forma de una nueva variable con dos niveles: si, cuando el nivel de actividad física sea moderado o alto, y no en caso contrario.

# Creamos la condición
condicion <- datos$nivel_AF %in% c("moderada", "alta")
# Observa que esto es un vector; échale un vistazo:
head(condicion)

# Generamos un nuevo vector con la evaluación condicional
practicaAF <- ifelse(condicion, "Si", "No")
# vemos también los primeros casos:
head(practicaAF)

# Comprobamos la coherencia de la recodificación
# observa la coherencia de las frecuencias:
table(datos$nivel_AF)
table(practicaAF)

3.3 La función cut()

La función cut() permite transformar una variable numérica en un conjunto de intervalos definidos por el propio usuario. Es decir, convierte una escala continua en grupos discretos basados en puntos de corte. Esta función automatiza una operación que, de otro modo, tendríamos que hacer con múltiples ifelse() anidados (algo que es mejor evitar).

Con cut() podemos:

  • crear categorías por rangos de una variable;
  • definir intervalos de tamaño regular (por ejemplo, grupos de peso de 5 en 5 kg);
  • aplicar puntos de corte clínicos establecidos en guías;
  • generar grupos basados en la distribución de los datos (cuartiles, terciles, etc.).

La función cut() contempla los siguientes argumentos:

  • x: Es la variable numérica a categorizar
  • breaks: Indicación de los puntos de corte de los intervalos. Puede ser
    • un vector numérico con los puntos de corte. Por ejemplo c(0,10,20,30); o bien
    • un número entero que indique cuántos grupos se desean (R los crea automáticamente).
  • labels: Nombres para cada intervalo creado. Por ejemplo c("bajo","medio","alto").
  • right: Indica qué lado del intervalo es cerrado. El valor por defecto es TRUE, lo que supone que el límite inferior no entra en el intervalo y el límite superior sí. Esta situación se invierte si se cambia a right=FALSE.
  • dig.lab: Controla cuántos dígitos usa cut() para mostrar los límites cuando crea etiquetas automáticamente. Lo podemos dejar en su valor por defecto.

Veamos un ejemplo: se trata de obtener una variable que sea el resultado de agrupar los niveles de colesterol en bajo, si es menor a 160 mg/dL, normal si está entre 160 y 200 mg/dL y alto si está por encima de 200 mg/dL.

col_cat <- cut(
  datos$colesterol,
  breaks = c(-Inf, 160, 200, Inf),
  labels = c("bajo", "normal", "alto")
)

# veamos el resultado:
table(col_cat)
col_cat
  bajo normal   alto 
   102     33     15 
# observa qué el resultado de cut() es un objeto de tipo factor
class(col_cat)
[1] "factor"

Observa en el ejemplo que en breaks hay que indicar los límites donde comienza el primer intervalo y donde termina el último. Esto se resuelve de forma sencilla usando la palabra reservada Inf, que alude a infinito; de este modo no necesitamos calcular previamente el mínimo y el máximo de la variable a categorizar.

Considerando el data frame datos, categoriza la variable peso en tres grupos, de manera que cada uno tenga, aproximadamente, 1/3 de los casos.

Lo que se pide es formar tres grupos utilizando como puntos de corte los dos terciles (cuantiles 33.3 y 66.6, respectivamente).

Comenzamos determinando estos puntos de corte y después se los damos como argumento a la función cut()

# calculamos los dos terciles del peso
tercil_1 <- quantile(datos$peso, 0.333)
tercil_2 <- quantile(datos$peso, 0.666)

# aplicamos cut con estos puntos de corte
grupos_peso <- cut(
  datos$peso,
  breaks = c(-Inf, tercil_1, tercil_2, Inf),
  labels = c("grupo T1", "grupo T2", "grupo_T3")
)
# veamos las frecuencias absolutas del peso categorizado
table(grupos_peso)

# veamos las frecuencias relativas
prop.table(table(grupos_peso))

4 Información faltante

4.1 Recordatorio: cómo detectar la presencia de información faltante

En la práctica 2 vimos cómo detectar la presencia de valores faltantes, codificados como NA en un data frame. Recordemos cómo hacerlo:

# ¿Hay algún valor faltante en el data.frame?
anyNA(datos)

# Generación de un listado de variables indicando cuántos
# valores faltantes tiene cada una
colSums(is.na(datos))

Ahora que conocemos los filtros lógicos, podemos entender con claridad qué hace colSums(is.na(datos)). Primero actúa is.na(datos): esta función examina cada celda del data frame y devuelve TRUE cuando encuentra un valor faltante, NA, y FALSE en caso contrario. Ese conjunto de valores lógicos pasa después a colSums(), que se encarga de sumar los TRUE de cada columna. El resultado final es un vector que indica cuántos NA contiene cada variable.

Recuerda que la función summary(), aplicada al data frame datos, también indica qué variables tienen casos faltantes.

summary(datos)

Si has ejecutado cualquiera de las dos opciones anteriores para detectar casos faltantes, habrás visto que sí, que efectivamente hay 5 valores faltantes en el indicador de hemoglobina glucosilada HbA1c.

4.2 Identificación de los casos con información faltante

Normalmente, cuando falta información, conviene identificar en qué casos ocurre. Esto es muy sencillo de hacer si usamos la función is.na() como condición. Explica qué hace este código:

datos[is.na(datos$HbA1c), ]

Observa que hemos utilizado la evaluación lógica generada por is.na() como filtro para las filas de datos (el hueco en blanco de las columnas indica que queremos recuperar la información de todas las variables). En consecuencia, el resultado de la línea de código anterior es un listado de todas las variables, pero solo en aquellos casos en los que falte el valor de HbA1c. El propósito es que, con esta información, intentemos saber qué ha pasado en la toma e implementación de los datos. Por qué no aparece el valor de HbA1c.

Vamos a suponer que, al revisar nuestras notas, comprobamos que en el caso correspondiente a la fila 54 —una mujer de 15 años con el identificador ID 69, como se observa en la salida anterior— la falta de información en la hemoglobina glucosilada se debe a un error de introducción de datos. En nuestras anotaciones consta que el valor correcto que debería registrarse es 6.41. Vamos a ver cómo corregirlo:

Corrige el valor de HbA1c en el sujeto que ocupa la fila 54. Aparece NA y debe aparecer 6.41.

datos$HbA1c[54] <- 6.41
# simple, ¿no?

# comprobemos qué ha pasado. Vamos a ver el listado
# de todas las variables de este caso
datos[54, ]

Si del resto de casos faltantes no podemos recuperar la información, no queda más remedio que trabajar sin ella. El problema que se presenta es que la mayoría de las funciones descriptivas que hemos visto en la práctica 2 devuelven como resultado NA cuando al intentar hacer el cálculo se tropiezan con un valor codificado como NA. Por ejemplo, vamos a intentar calcular la media del nivel de hemoblogina glucosilada:

mean(datos$HbA1c)
[1] NA

Lo mismo ocurre con el resto de las funciones descriptivas que hemos visto, var(), sd(), quantile(), etc. Afortunadamente, a todas se les puede indicar el argumento na.rm=TRUE, que le indica a la función que elimine los casos con NA antes de hacer el cálculo. Veamos entonces cómo se calcula la media en presencia de valores faltantes:

mean(datos$HbA1c, na.rm = TRUE)
[1] 6.412945

Lo mismo podemos hacer con el resto de medidas descriptivas.

Calcula el valor de la desviación típica y de los percentiles 5 y 95 del nivel de hemoglobina glucosilada (HbA1c).

Intenta resolverlo tú antes de mirar la solución
sd(datos$HbA1c, na.rm = T) # desviación típica
quantile(datos$HbA1c, 0.05, na.rm = T) # percentil 5
quantile(datos$HbA1c, 0.95, na.rm = T) # percentil 95


5 Ejercicios de recapitulación

Vamos a recapitular lo que hemos visto en estas tres primeras prácticas. Para ello, es interesante que limpiemos el entorno y volvamos a cargar el data.frame, para no acumular posibles errores.

Limpiamos el entorno: para borrar todo el contenido generado hoy puedes usar la brocha que aparece en el marco superior derecho (“Environment”) de RStudio.

Al final de todas las tareas propuestas, puedes ver, de forma muy resumida, las soluciones, pero

Míra las soluciones solamente si necesitas comprobar que lo has hecho bien. Si tienes dudas, intenta resolverlas con los ejercicios que llevamos hechos, y con tus apuntes, antes de mirar la solución. De lo contrario, esto no sirve para aprender.

Comenzamos.

¡Toma notas en tus apuntes!

1. Crea un nuevo script

  • Ponle por título:
# Ejercicios de recapitulación ----
  • Guarda (“Ctrl”+“S”) este nuevo script con el nombre “Ejercicios_P_1-3” (comprueba que la carpeta de trabajo es la que definiste al principio de la práctica. Como no la hemos cambiado, debe de ser la misma).
  • Si lo deseas, puedes guardar y cerrar el script “Practica03” que estabas usando hasta ahora.

2. Importa la base de datos llamada “colesterol_01.rds” que puedes descargar a través del siguiente enlace

“https://www.ugr.es/~pfemia/BSRLab/dat/colesterol_01.rds”

Asígnala a un objeto llamado datos.

Recuerda que esto ya lo hemos hecho. Los pasos son:

  1. Asigna la dirección de internet a una variable (en el código que sigue hemos llamado url_datos a dicha variable).
  2. Como el archivo tiene formato “.rds”, tienes que usar la función readRDS().
# 1. Definimos la variable con la dirección de internet indicada
url_datos <- "https://www.ugr.es/~pfemia/BSRLab/dat/colesterol_01.rds"

# 2. Leemos esa dirección con la función readRDS(). El resultado
# lo asignamos al objeto "datos"
datos <- readRDS(url(url_datos))

3. Comprueba que has leído bien el archivo de datos. Por ejemplo, muestra (por la consola) la cabecera del archivo.

recuerda la función head().
Si los datos no se hubieran importado bien, tienes que ver qué ha fallado y repetir el paso anterior.

4. Aplica la función summary() a datos e indica lo siguiente:

  1. ¿Cuál es la edad media de los sujetos que componen la base de datos? ¿Crees que hay algún valor muy extremo en la edad? (comprueba los dos extremos de la distribución).
  2. ¿Cuántos casos hay con un nivel bajo de actividad física (variable nivel_AF)?
  3. Observa los tres cuartiles de la variable METs. A la vista de ellos, ¿puedes decir si la distribución es simétrica?

5. Define una nueva columna en datos que contenga el índice de masa corporal (llámale imc).

Recuerda que el imc se define como el cociente entre el peso (en kg) dividido por la talla (en metros) al cuadrado (¡comprueba si son correctas las unidades!).

6. Los valores normativos del imc son:

  • Peso bajo, si imc<18.5.
  • Peso saludable (normal), si 18.5 <imc \(\le\) 25.
  • Sobrepeso, si 25 <imc \(\le\) 30.
  • Obesidad, si imc>30.
  1. Obtén las categorías de peso según estos puntos de corte en el imc. Llama a esta nueva variable cat_peso y a las categorías “bajo”, “normal”, “sobrepeso” y “obesidad”.

    Utiliza la función cut() para conseguirlo.

  2. Obtén la tabla de frecuencias absolutas y la de frecuencias relativas de las categorías del peso (cat_peso).

    Estos son dos pasos, obtén primero las frecuencias absolutas y después las realtivas.
    Si no recuerdas el nombre de las funciones para hacer tablas de frecuencias, puedes consultar el apartado de la práctica 2 o bien el glosario de funciones de R

  3. Obtén un diagrama de frecuencias que sea apropiado para este tipo de variable (cat_peso).

7. Ejemplo clínico:

Un índice aterogénico muy utilizado es el Atherogenic Index of Plasma (AIP). Se calcula como sigue:

\[ \text{AIP}=\log_{10} \left( \frac{\text{TG}}{\text{HDL}} \right) \]

pero los triglicéridos (TG) y el HDL deben estar expresados en mmol/L para poder aplicar la fórmula correctamente. En nuestra base de datos, las unidades son mg/dL en ambos casos, por lo que hay que aplicar el siguiente factor de conversión:

\[ \text{TG (mmol/L)} = \frac{\text{triglicéridos (mg/dL)}}{88.57}, \qquad \text{HDL (mmol/L)}=\frac{\text{HDL (mg/dL)}}{38.67} \]

En adolescentes y adultos jóvenes, un valor de AIP < 0.11 indica un riesgo bajo, mientras que valores mayores indican un perfil lipídico desfavorable, con una relación TG/HDL que tiende hacia patrones aterogénicos.

  1. Incorpora el AIP como columna adicional al data frame datos (llámale aip) haciendo los cálculos descritos anteriormente
    Si no lo recuerdas, consulta en la práctica 1 o en el glosario de funciones de R cómo obtener el logaritmo decimal con R.
  2. Obten un diagrama de frecuencias que sea apropiado para el AIP.
  3. Determina si hay, y en caso afirmativo, cuántos son, los casos con AIP \(\ge\) 0.11.
  4. Si son menos de 10, identifica el código identificativo (id) el sexo y la edad de los sujetos que tienen el AIP por encima de 0.11 (se trata de ver el listado por la ventana de la consola).
  5. Compara la distribución del AIP entre hombres y mujeres:
  1. Considera la media y la desviación típica de cada grupo.
  2. Haz un diagrama de cajas del AIP por sexos. ¿Detectas valores extremos en alguna de las distribuciones? ¿Crees que son simétricas las distribuciones? En caso de que sean simétricas ¿se podría hacer una interpretación de la media, de forma aproximada, como si fuera la mediana?
  1. ¿Se puede calcular el coeficiente de variación del AIP en el grupo de hombres y en el de mujeres?

8. Ejemplo clínico:

Se desea desarrollar un programa de promoción de la actividad física y hábitos saludables en aquellos casos que tengan una actividad física semanal por debajo de 600 MET-min/semana (MET son los equivalentes metabólicos, esta información la tenemos en la variable datos$METs).

Se pide:

  1. Crea una variable indicadora (binaria), que se llame bajo_met, que tome el valor 1 si el nivel de METs es inferior a 600 y 0 en caso contrario

    Utiliza ifelse().

  2. Genera, a partir de esta variable un factor que se llame fbajo_met, tal que al valor bajo_met==1 le asigne si y al valor bajo_met==0 le asigne no.
  3. Compara las frecuencias de ambas variables para comprobar si concuerdan adecuadamente.

Usa estas soluciones solo para comprobar que lo has hecho bien.

4.

  1. 16.8 años. No.
  2. 27
  3. Claramente asimétrica. Cola derecha muy larga.

5.

datos$imc <- datos$peso / ((datos$talla / 100)^2)

6.

# a.
puntos <- c(-Inf, 18.5, 25, 30, Inf)
grupos <- c("bajo", "normal", "sobrepeso", "obesidad")

datos$cat_peso <- cut(datos$imc, breaks = puntos, labels = grupos)

# b. frecuencias absolutas
table(datos$cat_peso)

# frecuencias relativas (redondeadas a tres decimales)
round(prop.table(table(datos$cat_peso)), 3)

# c. diagrama de barras
barplot(table(datos$cat_peso))

7.

# a.
datos$tgmml <- datos$trigliceridos / 88.57
datos$HDLmml <- datos$HDL / 38.67

datos$aip <- log10(datos$tgmml / datos$HDLmml)

# b.
hist(datos$aip) # histograma
plot(density(datos$aip)) # curva de densidad empírica
# los dos diagramas juntos
{
  hist(datos$aip, freq = FALSE)
  lines(density(datos$aip))
}
# c.
filtro <- datos$aip >= 0.11
riesgo <- datos[filtro, ]
nrow(riesgo)
nrow(riesgo) / nrow(datos)

# d.
riesgo[, c("id", "sexo", "edad")]

# e.1.
# Condiciones de filtrado
hombres <- datos$sexo == "hombre"
mujeres <- datos$sexo == "mujer"
# medias
mean(datos[hombres, ]$aip)
mean(datos[mujeres, ]$aip)
# desviaciones típicas
sd(datos[hombres, ]$aip)
sd(datos[mujeres, ]$aip)

# e.2.
boxplot(datos$aip ~ datos$sexo)
# AIP de mujeres con dos valores extremos en la cola derecha

# Sí, las dos distribuciones son razonamblemente simétricas. En este caso
# la media y la mediana se aproximan mucho y se puede decir que a cada lado
# de la media hay, aproximadamente, un 50% de los casos.

# f. No, el CV no se puede calcular si la media es menor a 1

8.

# a
condicion <- datos$METs < 600
bajo_met <- ifelse(condicion, 1, 0)
# b
fbajo_met <- factor(bajo_met, levels = c(1, 0), labels = c("si", "no"))
# c
table(bajo_met)
table(fbajo_met)
::: {#refs} :::