Programación en R

Ejemplos de tratamiento de datos con R

  

En este documento vamos a ver algunos ejemplos de cómo poner en práctica lo visto en documentos anteriores: secuenciar diferentes etapas de pre-procesamiento de datos y definir algunas estrategias de análisis de esos datos, como pasos previos a la ingesta de esos datos por parte de un modelo de machine learning, o como estrategia para analizar y comprender mejor los datos con los que estamos trabajando.

1. Ejemplo de pre-procesado de datos

El pre-procesado de datos en una etapa crucial en todo proceso de data science o machine learning, ya que ayuda a mejorar la calidad y consistencia de los datos con que se trabaja. Se extraen las características relevantes, se limpian y formatean los datos y se preparan para etapas posteriores. Veremos aquí un ejemplo de algunos pasos habituales en este pre-procesamiento, partiendo de este CSV que contiene datos sobre clientes de una tienda: nacionalidad, edad, salario anual y si compró o no un determinado producto.

1.1. Librerías necesarias

Comenzaremos importando los paquetes necesarios:

# Para análisis de datos
library(dplyr)
# Para generación de conjuntos de entrenamiento y test
library(caTools)

1.2. Carga inicial de los datos

Lo primero que haremos será cargar los datos en un data frame. Podemos usar la instrucción View para mostrar los datos que hemos cargado (en IDEs como RStudio, que dispone de visualizador).

datos <- read.csv("ejemplo_dataset.csv")
View(datos)

1.3. Filtrado de columnas relevantes

Ahora elegiremos las columnas que nos interesan: todas menos el Id

datos <- datos %>%
    select(-Id)

1.4. Gestión de valores nulos (missing values) y duplicados

En una colección de datos en R, los valores inexistentes se identifican con el término NA (Not Available). Podemos detectar si hay valores faltantes en una colección con la instrucción is.na, que devuelve un booleano por cada posición de la colección, o la instrucción anyNA, que devuelve un resultado global para todo el conjunto

v <- c(1, 4, NA, 8)
is.na(v)            # FALSE FALSE TRUE FALSE
anyNA(v)            # TRUE

Para los conjuntos de datos que tienen valores nulos podemos querer hacer alguna operación para reemplazarlos. Por ejemplo, sustituirlos por la media de valores del conjunto. Podemos hacerlo con algo así:

v2 <- ifelse(is.na(v), 
             mean(v, na.rm=TRUE),
             v)

Lo que hace la instrucción anterior es crear un vector donde aplica la función ifelse a cada elemento. Si es nulo, se sustituye por la media de los valores de v. Si no es nulo, se deja el propio valor.

En el caso de querer utilizar otro valor como reemplazo (por ejemplo, la moda o la mediana), tendríamos que cambiar la función a utilizar en la expresión anterior. En el caso de la mediana podemos reemplazar la función mean anterior por median, y en el caso de la moda tenemos que calcularla manualmente, aunque basta con hacer algo como esto:

mode <- function(v) {
   uniqv <- unique(v)
   uniqv[which.max(tabulate(match(v, uniqv)))]
}

En algunas ocasiones nos puede interesar eliminar valores duplicados de un conjunto de datos. La instrucción unique nos permite hacerlo:

v <- c("Uno", "Dos", "Uno", "Tres", "Dos")
v2 <- unique(v)  # "Uno" "Dos" "Tres"

Aplicado al ejemplo que estamos tratando, si observamos los datos de entrada, falta alguno en las columnas de edad y salario. Reemplazaremos los valores faltantes por la media de la columna correspondiente:

datos$Age <- ifelse(is.na(datos$Age),
                    mean(datos$Age, na.rm = TRUE),
                    datos$Age)
datos$Salary <- ifelse(is.na(datos$Salary),
                    mean(datos$Salary, na.rm = TRUE),
                    datos$Salary)

1.5. Codificación de columnas categóricas

A continuación vamos a codificar los datos categóricos para poder realizar operaciones matemáticas con ellos (por ejemplo, a la hora de establecer una regresión lineal u operaciones similares). La columna Purchased tiene valores de Yes o No, que codificaremos como 1 y 0 respectivamente:

datos$Purchased <- factor(datos$Purchased,
                    levels=c("No", "Yes"),
                    labels=c(0, 1))
# El 0 queda como nulo, hay que recodificarlo de nuevo
datos$Purchased[is.na(datos$Purchased)] <- 0

En cuanto a la columna del país, podríamos también codificarla con valores numéricos consecutivos. Por ejemplo, 0 para España, 1 para Francia, etc. Sin embargo, este tipo de codificación puede otorgar un orden arbitrario a los datos, y que el modelo que construyamos después “aprenda” que España es mejor o peor que Francia porque su código es menor o mayor. Para evitar esta situación realizamos una codificación sin orden establecido, llamada one hot encoding, donde añadimos una columna por cada posible país, y se pone a 1 la columna del país correspondiente.

Podemos utilizar para esto librerías adicionales, como mltools o caret. Sin embargo es más difícil controlar cómo se van a llamar las columnas generadas, y cómo se van a añadir al data frame existente. No es difícil hacer este proceso a mano:

datos %>%
    mutate(France = ifelse(Country=="France", 1, 0)) %>%
    mutate(Germany = ifelse(Country=="Germany", 1, 0)) %>%
    mutate(Spain = ifelse(Country=="Spain", 1, 0)) %>%
    # Eliminamos la columna original
    select(-Country)

# Opcionalmente podemos reorganizar las columnas
datos <- datos[, c("France", "Germany", "Spain", "Age", "Salary", "Purchased")]

El resultado será algo así:

France Germany Spain Age Salary Purchased
1 0 0 44.00000 72000.00 1
0 0 1 27.00000 48000.00 0

1.6. Escalado de los datos

La operación de escalado de datos es muy habitual cuando trabajamos con valores heterogéneos, de forma que igualamos la magnitud de los datos que utilizamos. Por ejemplo, imaginemos que estamos trabajando con edades de personas y salarios anuales. Son valores muy dispares, porque las edades oscilan normalmente entre 0 y 100 años, y los salarios alcanzan varias decenas o centenas de miles de euros, o dólares. Si hacemos operaciones matemáticas con los datos, como sumas o multiplicaciones, claramente el salario va a ser mucho más determinante que la edad en el resultado final. Para evitar esta desigualdad se escalan o normalizan los datos a un rango común, como puede ser de 0 a 1 o de -1 a 1.

Para escalar datos en R usamos la función nativa scale, a la que le indicamos los datos a escalar. Por ejemplo, si queremos escalar las columnas 2 y 3 de un data frame haremos algo así:

datos[, 2:3] <- scale(datos[, 2:3])

Vamos ahora escalar los valores numéricos (edad y salario) de nuestro ejemplo:

datos[, c("Age", "Salary")] <- scale(datos[, c("Age", "Salary")])

1.7. División en conjuntos de entrenamiento y test

Una tarea muy habitual en muchos procesos de machine learning es dividir el conjunto de datos de que se dispone en una parte para entrenar el modelo y otra para validarlo una vez entrenado. Para hacer esto en R podemos emplear el paquete caTools que hemos incorporado al principio del ejemplo. Deberemos instalarlo previamente con install.packages("caTools"), si no lo tenemos instalado.

Podemos, opcionalmente, establecer una semilla aleatoria fija, de modo que siempre se genere el mismo subconjunto de entrenamiento y test. Esto se consigue con la instrucción set.seed nativa de R:

set.seed(1)

Finalmente, llamamos al método sample.split de caTools. Le tenemos que pasar como parámetros:

Como resultado, sample.split nos devuelve un vector de booleanos, donde a TRUE estarán las casillas reservadas para entrenamiento y a FALSE las destinadas para test. Con este vector podemos establecer los conjuntos de datos para cada cosa, usando la función subset. Aquí vemos un ejemplo de todos estos pasos:

datos <- # Cargar data frame
muestra <- sample.split(datos$Columna, SplitRatio=0.8)
entrenamiento <- subset(datos, muestra==TRUE)
test <- subset(datos, muestra==FALSE)

Aplicado a nuestro ejemplo, dividiremos ahora los datos en una parte para entrenamiento y otra para test, usando caTools:

set.seed(1)
split <- sample.split(datos$Purchased, SplitRatio=0.8)
train <- subset(datos, split==TRUE)
test <- subset(datos, split==FALSE)

Aquí tienes el ejemplo completo del código fuente que hemos utilizado en este apartado.

2. Ejemplo de análisis exploratorio de datos

El análisis exploratorio de datos (en inglés EDA, Exploratory Data Analysis) es un proceso de análisis de los datos de un problema para extraer características relevantes, comprender mejor los datos e incluso intuir o inferir otros nuevos. Es un proceso creativo, a menudo continuación del pre-procesado anterior. Podemos estudiar variaciones o tendencias en los datos, mostrar gráficos representativos de ciertas características, etc.

Comenzaremos incluyendo los paquetes que vamos a necesitar para nuestro ejemplo:

library(ggplot2)  # Para gráficos
library(tibble)   # Para dinamizar el tratamiento de data frames
library(dplyr)    # Para funciones varias de tratamiento de datos

2.1. Cargando los datos

Para hacer las pruebas vamos a utilizar un dataset ya incorporado con ggplot2 como muestra, llamado diamonds. Podemos consultar su estructura aquí y, como vemos, es una tabla con características de unos 50.000 diamantes diferentes:

Algunos de estos datos se pueden pre-procesar si se considera necesario para nuestro propósito. Por ejemplo, codificar numéricamente las columnas categóricas, o normalizar lo datos numéricos. Sin embargo, para otras operaciones, como ciertos gráficos que podemos obtener, no interesa esta codificación o escalado previo, porque perdemos los valores originales que queremos estudiar.

Podemos obtener un vistazo preliminar de los valores del dataset con la función summary, que nos mostrará un resumen por columna con su valor máximo, mínimo, media, conteo de valores por categoría en columnas categóricas, etc.

datos <- diamonds
summary(datos)

2.2. Algunos gráficos representativos

Vamos a representar algunos gráficos representativos de nuestro dataset usando ggplot2.

ggplot(data=datos, mapping=aes(x=cut)) +
  geom_bar()
ggplot(data=datos, mapping=aes(x=cut, fill=clarity)) +
  # position="dodge" hace que las cajas se pinten adyacentes
  # de izquierda a derecha, no apiladas
  geom_bar(position="dodge") +
  xlab("Tipo de corte") +
  ylab("Cantidad por tipo de corte") +
  ggtitle("Tipos de corte contrastados por claridad")
ggplot(data=datos, mapping=aes(x=cut)) +
   geom_bar() +
   facet_wrap(~ clarity)
ggplot(data=datos, mapping=aes(x=carat)) +
   geom_histogram(bins=10)
datos %>%
  filter(between(carat, 1, 2)) %>%
  ggplot(mapping=aes(x=cut)) + geom_bar()
ggplot(data=datos, mapping=aes(x=cut, y=price)) +
   geom_boxplot()
ggplot(data=datos, mapping=aes(x=cut, y=color)) +
   geom_count()

2.3. Otras estadísticas relevantes

Además de mostrar representaciones gráficas también podemos obtener datos numéricos o textuales del dataset:

datos %>% count(cut)
table(datos$cut, datos$clarity)
round(prop.table(table(datos$cut, datos$clarity)) * 100, 2)
datos %>%
   count(cut_width(carat, 0.5))

Aquí tienes el ejemplo completo del código fuente que hemos utilizado en este apartado.

Ejercicio 1:

Utiliza este CSV sobre parques eólicos en Castilla y León. Se piden dos operaciones:

AYUDA: vídeo con solución del ejercicio