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.
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.
Comenzaremos importando los paquetes necesarios:
# Para análisis de datos
library(dplyr)
# Para generación de conjuntos de entrenamiento y test
library(caTools)
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)
Ahora elegiremos las columnas que nos interesan: todas menos el Id
datos <- datos %>%
select(-Id)
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)
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 |
… | … | … | … | … | … |
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")])
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.
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
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)
Vamos a representar algunos gráficos representativos de nuestro dataset usando ggplot2.
geom_bar
, que muestre el conteo de valores de cada tipo de corte (columna cut):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()
Además de mostrar representaciones gráficas también podemos obtener datos numéricos o textuales del dataset:
datos %>% count(cut)
table
podemos indicar dos nombres de columna, y se crea una tabla donde las filas son los valores de una columna y las columnas los valores de otra, contrastando así cuántos elementos con un valor X en una columna tienen un valor Y en la otra.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:
- Reemplazar los valores nulos de la columna de potencia total (potencia) por la media de esa columna
- Construir un gráfico de barras con el conteo de parques eólicos por provincia