Este texto se basa (parcialmente) en los siguientes materiales:
> library(patchwork)
> library(tidyverse)
> library(tidymodels)
tidyverse
Supongamos la siguiente situación: tenemos un dataset con varias variables referidas a la condición de vida de la población. Queremos lograr dos objetivos:
Ambos objetivos forman parte de un tipo de problema típico del aprendizaje automático llamado reducción de dimensionalidad. Vamos a ver una de las técnicas para atacar este tipo de problema que es particularmente útil si se cumplen al menos tres condiciones:
La técnica se llama Análisis de Componentes Principales o PCA (por sus siglas en inglés). Cuando nos enfrentamos a un gran conjunto de variables correlacionadas, PCA nos va a permitir “resumir” esas variables en un conjunto menor que permitan explicar la mayor parte de variablidad (información) en esas variables originales. Se trata de un enfoque no supervisado dado que solo va a utilizar un conjunto de variables independientes (\(X_1, X_2, ..., X_p\)) sin ninguna variable de respuesta asociada. Además de producir variables derivadas para su uso en el aprendizaje supervisado, PCA también sirve como una herramienta para la visualización de datos (visualización de las observaciones o visualización de las variables). También se puede utilizar como un herramienta para la imputación de datos, es decir, para completar los valores faltantes en un matriz.
Supongamos que deseamos visualizar \(n\) observaciones con una serie de \(p\) variables medidas en \(X_1, X_2,...,X_p\), como parte de un análisis exploratorio de datos. Podríamos intentar examinando diagramas de dispersión bidimensionales de los datos, cada uno de los cuales contiene las mediciones de \(n\) observaciones en dos de los características. Sin embargo, hay \(\binom{p}{2} = \frac{p(p-1)}{2}\) gráficos posibles. Por ejemplo, si hubiera 10 variables (un número nada elevado) tendríamos 45 gráficos para analizar. Si \(p\) es grande, entonces ciertamente no será posible mirarlos todos; además, lo más probable es que ninguno de ellos ser informativo ya que cada uno contiene solo una pequeña fracción del total información presente en el conjunto de datos. Claramente, se requiere un método mejor. En particular, nos gustaría encontrar una representación de los datos que tenga una menor dimensionalidad y que capture la mayor parte de la información como sea posible. Por ejemplo, si podemos obtener una representación de esas \(p\) variables en dos dimensiones que capturen la mayor parte de la información, entonces podríamos generar un solo gráfico.
PCA proporciona una herramienta para hacer precisamente esto. El supuesto por detrás es que nuestras \(n\) observaciones “viven” en un espacio de \(p\) dimensiones. Pero la cuestion es que no todas esas dimensiones son igual de relevantes. Entonces, PCA intenta encontrar un conjunto de dimensiones relevantes que sea menor a \(p\). En este caso, la idea de “relevancia” o “información” se define como la variabilidad de información que contienen. Cada una de las dimensiones encontradas por PCA es una combinación lineal de las características \(p\). ¿Cómo se estiman esas nuevas dimensiones?
El primer componente principal (PC1
) se define como la
combinación lineal normalizada de las \(p\) variables independientes
\[ Z_{1} = \phi_{1,1} \times X_{1} + \phi_{2,1} \times X_{2} + ... + \phi_{p1} \times X_{p} \]
que tiene la mayor varianza. Al decir “normalizada” queremos decir que \(\sum^p_{j=1}\phi_{j1}^2=1\). Todos los elementos \(\phi_{11},...,\phi_{p1}\) son los llamados loadings o “cargas” del primer componente. En conjunto conforman el vector de pesos o cargas del primer componente \(\phi_{1} = (\phi_{11}, ..., \phi_{1p})^T\).
Dado un dataset de \(n \times p\), llamado \(\textbf{X}\), ¿cómo calculamos el primer componente principal? Como solo estamos interesados en la varianza, suponemos que cada una de las variables en \(\textbf{X}\) se han centrado para tener una media cero (es decir, las medias de la columna de \(\textbf{X}\) son cero). Luego, buscamos la combinación lineal de los valores de características de muestra de la forma
\[ z_{i1} = \phi_{11} \times x_{1} + \phi_{21} \times x_{2} + ... + \phi_{p1} \times x_{p} \]
que tenga la mayor varianza (con la restricción \(\sum^p_{j=1}\phi_{j1}^2=1\)). En otras palabras, el vector de cargas factoriales del primer componente resuelve el siguiente problema de optimización:
\[ \max_{\phi_{11}, ..., \phi_{p1}} = \left\{\frac{1}{n}\sum^n_{i=1} \bigg( \sum^p_{j=1} \phi_{j1}x_{ij} \bigg)^2 \right\} \textrm{sujeto a} \sum^p_{j=1}\phi^2_{j1} =1 \] De la ecuación previa, podemos reescrbir la anterior como \(\frac{1}{n}\sum^n_{i=1}z_{i1}^2\). Dado que \(\frac{1}{n}\sum^n_{i=1}x_{ij}=0\) (el promedio es cero porque se asume que las varibales están normalizadas), el promedio de \(z_{11},...,z_{n1}\) también va a ser cero. Así, el problema de maximización anterior se convierte en un problema en el que lo se maximiza es la varianza muestral de los \(n\) valores de \(z_{i2}\). Este problema puede resolverse mediante una técnica llamada eigendecomposition, ampliamente utilizada en álgebra de matrices. No obstante, el desarrollo de sus detalles queda por fuera de los objetivos de este curso.
Hay una buena interpretación geométrica para el primer componente principal. El vector de pesos \(\phi_{1}\) con elementos \(\phi_{11}, \phi_{21}, ..., \phi_{p1}\) define una dirección en espacio de características a lo largo del cual los datos varían más. Si proyectamos los \(n\) datos \(x_{1},...,x_{n}\) en esta dirección, los valores proyectados son las puntuaciones del componente principal \(z_{11},...,z_{n1}\) en sí mismas.
El segundo componente principal \(Z_{2}\) es una combinación lineal de las variables \(X_1,...,X_p\) ´que explica la mayor cantidad de varianza de todas las combinaciones lineales posibles que están descorrelacionadas (son ortogonales, es decir perpendiculares) a \(Z_{1}\).
\[ z_{i1} = \phi_{12} \times x_{1} + \phi_{22} \times x_{2} + ... + \phi_{p2} \times x_{p} \]
Una vez que hemos calculado los componentes principales, podemos graficarlos entre sí para producir vistas de baja dimensión de los datos. Por ejemplo, podemos graficar el vector de puntaje \(Z_1\) contra \(Z_2\), \(Z_1\) contra \(Z_3\) y así sucesivamente. Geométricamente, esto equivale a proyectar los datos originales en el subespacio dividido por \(\phi_1\), \(\phi_2\) y \(\phi_3\) y trazado de los puntos proyectados.
Vamos a ejemplificar este desarrollo teórico. El dataset
HDI_HDR2020_040722.csv
contiene las componentes del Human
Development Index (HDI) elaborado por UNDP para el período 1992-2019. El
HDI es un promedio a nivel país de cuatro variables fundamentales:
En el dataset se encuentran bajo los siguientes nombres:
le_2019
: expectativa de vida al nacer de la
poblacióneys_2019
: años “esperados” de escolaridad de la
poblaciónmys_2019
: años promedios de escolaridadgnipc_2019
: producto bruto interno per cápitahdi_2019
: HDI para el paísEn ambos datasets van a encontrar los códigos de país
(iso3c
) y las etiquetas de país (country
) y
algunas otras menos útiles.
Carguemos el dataset:
> idh <- read_csv("./data/HDI_HDR2020_040722.csv")
Primero, vamos a quedarnos soamente co las variables correspondientes al año 2019 de cada indicador:
> idh <- idh %>%
+ select(iso3c:region, ends_with("_2019")) %>%
+ drop_na()
>
> head(idh)
## # A tibble: 6 × 10
## iso3c country hdicode region hdi_rank_2019 hdi_2019 le_2019 eys_2019 mys_2019
## <chr> <chr> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 AFG Afghani… Low SA 169 0.511 64.8 10.2 3.93
## 2 AGO Angola Medium SSA 148 0.581 61.2 11.8 5.17
## 3 ALB Albania High ECA 69 0.795 78.6 14.7 10.1
## 4 ARE United … Very H… AS 31 0.89 78.0 14.3 12.1
## 5 ARG Argenti… Very H… LAC 46 0.845 76.7 17.7 10.9
## 6 ARM Armenia High ECA 81 0.776 75.1 13.1 11.3
## # ℹ 1 more variable: gnipc_2019 <dbl>
De esta forma, nos queda un dataset de \(n=147\) y \(p=4\) para estimar nuestros componentes
principales. Vamos a utilizar PCA para calcular un “índice alternativo”
al IDH. Para eso, vamos a utilizar un paquete dentro del metapquete
tidymodels
, llamado recipes
(receta). Una
recipe
es un objeto que define una serie de pasos para el
procesamiento de datos. Una recipe
define los pasos a
través de las funciones step_*()
sin ejecutarlas
inmediatamente; es sólo una especificación de lo que debe hacerse:
> recipe_pca <- idh %>%
+ select(le_2019:gnipc_2019) %>%
+ recipe(~.) %>%
+ step_normalize(all_numeric()) %>%
+ step_pca(all_numeric(), num_comp = 2)
>
> recipe_pca
Veamos qué pasó acá, paso por paso.
Llamamos a recipes()
con una fórmula que define los
roles de los “ingredientes” o variables (por ejemplo, predictor,
resultado). Solo usa los datos idh
en para determinar los
tipos de datos para las columnas. En este caso (como estamos en
aprendizaje no supervisado, todas son predictores).
step_normalize()
va normalizar todas las variables
especificadas (en este caso, las cuatro componentes del IDH) para que
tengan media igual a cero y desvío estándar igual a uno. Vamos a volver
sobre este punto al final.
step_pca()
va a efectuar el análisis de componentes
principales sobre esas mismas cuatro variables
Para especificar las variables nosotros las nombramos. Pero podríamos haber usado otras instrucciones. Por ejemplo ,
all_numeric()
, all_integer()
,
all_double()
, all_nominal()
,
all_ordered()
, all_unordered()
,
all_factor()
, all_string()
,
all_date()
y all_datetime()
pueden usarse para
seleccionar variables según su tipo.
all_predictors
o all_outcomes
para
seleccionar según su rol en el objeto recipe()
También podrían usar algunos viejos conocidos:
starts_with()
, ends_with()
de
dplyr
Más detalles acá.
¿Cuál es la ventaja de usar una receta sobre una fórmula o predictores sin procesar? Hay algunos, incluyendo:
all_nominal_predictors()
se puede usar para capturar muchas
variables para tipos específicos de procesamiento, mientras que una
fórmula requeriría que cada una se enumere explícitamente.Bueno, vamos a ejectuar el PCA de una vez:
> pca_estimates <- prep(recipe_pca, training = idh)
> pca_data <- bake(pca_estimates, idh)
Aquí hemos utilizado dos métodos importantes dentro del paquete
recipes
:
prep()
: estima el modelo (en este caso, la
recipe()
a los datos de entrenamientobake()
: aplica la recipe()
a un
datasetEn el esquema anterior puede verse que prep
es un
análogo a los métodos de fiteo y bake
, a los métodos
predict
.
Ahora bien, esto quiere decir que en pca_estimates
tenemos toda la información que se genera como resultado de estimar el
modelo PCA. En pca_data
en cambio, vamos a tener las
“predicciones” del modelo PCA, es decir, los componentesen cuestión. De
hecho, si observan, pca_estimates
es una lista y
pca_data
una tibble.
Vamos a proceder, entonces, a evaluar los resultados de nuestro modelo. Una primera forma de evaluarlo, tratar de interpretar las llamadas cargas o pesos factoriales (los habíamos llamado \(\phi_{p,k}\) más arriba). Como vimos, el primer componente (\(Z_{1}\)) y el segundo (\(Z_{2}\)) son combinaciones lineales de las features (\(X_{p}\))
\[ z_{1} = \phi_{1,1} \times mys\_2019 + \phi_{2,1} \times ie\_2019 + \phi_{3,1} \times gnipc\_2019 + \phi_{4,1} \times eys\_2019 \\ z_{2} = \phi_{1,2} \times mys\_2019 + \phi_{2,2} \times ie\_2019 + \phi_{3,2} \times gnipc\_2019 + \phi_{4,2} \times eys\_2019 \] De esta forma, cada peso factorial nos dice qué tan asociadas están las variables a cada uno de los factores:
> tidy(pca_estimates, 2, type = "coef") %>%
+ filter(component %in% c("PC1", "PC2")) %>%
+ ggplot(aes(value, terms, fill = terms)) + geom_col(show.legend = FALSE) + geom_text(aes(label = round(value,
+ 2))) + labs(title = "Cargas factoriales (comp. 1 y 2)", x = "Valor", y = "Variable") +
+ facet_wrap(~component, nrow = 1) + theme_minimal()
Puede verse que el primer componente (PC1
), entonces,
tiene cargas bastante bien repartidas. Es un poco más baja en el
producto per cápita1. Pero en todos los casos, PC1
muestra signo positivo con las cuatro variables. Es decir que cuando
PC1
aumenta, también aumentan las cuatro variables
componentes y, por ende, tendrán un mayor “desarrollo humano”.
Ahora podemos hacer una pregunta que es casi natural: ¿cuánta de la información en un conjunto de datos dado se pierde al proyectar las observaciones en los primeros componentes principales? Es decir, ¿cuánto de la varianza en los datos no es contenidos en los primeros componentes principales? De manera más general, estamos interesado en conocer la proporción de la varianza explicada por cada componente principal. La varianza total presente en un conjunto de datos (suponiendo que las variables se han centrado para tener media cero) se define como
\[ \sum^p_{j=1} Var(X_{j}) = \sum^p_{j=1}\frac{1}{n}\sum^n_{i=1}X^2_{ij} \]
Y la varianza explicada por el m-ésimo componente
\[ \frac{1}{n}\sum^n_{i=1}Z^2_{im} = \frac{1}{n}\sum^n_{i=1}\bigg(\sum^p_{j=1}\phi_{jm}X_{ij}\bigg)^2 \] Por lo tanto, la PVE del componente principal m-ésimo está dada por:
\[ \frac{\frac{1}{n}\sum^n_{i=1}Z^2_{im}}{\sum^p_{j=1} Var(X_{j}) } = \frac{\frac{1}{n}\sum^n_{i=1}\bigg(\sum^p_{j=1}\phi_{jm}X_{ij}\bigg)^2}{\sum^p_{j=1}\frac{1}{n}\sum^n_{i=1}X^2_{ij}} \] De esta forma, podemos obtener la PVE de cada uno de los componentes identificados a partir de la ecuación anterior y sumarlas todas.
Mediante algunas manipulaciones algebráicas (ver el libro citado al principio) podemos llegar a la siguiente expresión de la PVE del m-ésimo componente principal:
\[ 1 - \frac{\sum^p_{j=1}\sum^n_{i=1} (x_{ij} - \sum_{m=1}^M z_{im}\phi_{jm})^2}{\sum^p_{j=1}\sum^n_{i=1}x_{ij}^2} = 1 - \frac{RSS}{TSS} \]
donde \(TSS\) representa la suma total de elementos cuadrados de \(X\)), y \(RSS\) representa la suma residual de cuadrados de la aproximación M-dimensional dada por los componentes principales. Recordando la definición de \(R^2\), esto significa que podemos interpretar la PVE como el \(R^2\) de la aproximación para \(X\)) dada por las primeras M componentes principales.
¿Qué ocurre con nuestro IDH recargado?
> tidy(pca_estimates, 2, type = "variance") %>%
+ filter(terms == "percent variance") %>%
+ mutate(component = paste0("PC", component)) %>%
+ ggplot(aes(x = component, y = value, group = terms)) + geom_col() + ylim(0, 100) +
+ labs(title = "% varianza", x = "Componente", y = "Valor") + theme_minimal() +
+
+ tidy(pca_estimates, 2, type = "variance") %>%
+ filter(terms == "cumulative percent variance") %>%
+ mutate(component = paste0("PC", component)) %>%
+ ggplot(aes(x = component, y = value, group = terms)) + geom_col() + ylim(0, 100) +
+ labs(title = "% varianza acum.", x = "Componente", y = "Valor") + theme_minimal()
Vemos cómo el
PC1
explica casi el 71% de la varianza total
contenida en las cuatro variables originales. El PC2
más
del 14% y, entre ambos, concentran más del 85% de la variabilidad total
contenida en el dataset. Esto significa que elPC1
arroja un
muy buen resumen de la información total, resumen que será incluso mejor
si agregamos el PC2
.
Pero recordemos que estamos buscando obtener un solo índice. Por
ello, por ahora, solo nos ocuparemos del PC1
.
> idh <- idh %>%
+ bind_cols(pca_data)
Los resultados de PCA también dependen del hecho de si las variables están escaladas individualmente o no. Si realizamos PCA en las variables sin escalar, las variables con mayor varianza tendrán una carga muy grande. Como no es deseable que los componentes principales obtenidos dependan de la escala de las variables, escalamos cada variable para que tenga la desviación estándar 1 antes de realizar el PCA. Si las variables individuales se miden en la misma unidad, no es necesario escalar.
Por ejemplo, ¿qué ocurriría si no hubiésemos normalizado nuestras
variables? Repitamos el proceso anterior, pero omitamos el
step_normalize()
del objeto recipe
.
> recipe_pca2 <- idh %>%
+ select(le_2019:gnipc_2019) %>%
+ recipe(~.) %>%
+ step_pca(all_numeric(), num_comp = 2)
>
> pca_estimates2 <- prep(recipe_pca2, training = idh)
> pca_data2 <- bake(pca_estimates2, idh)
Veamos, ahora, las cargas factoriales:
> tidy(pca_estimates2, 1, type = "coef") %>%
+ ggplot(aes(value, terms, fill = terms)) + geom_col(show.legend = FALSE) + geom_text(aes(label = round(value,
+ 2))) + xlim(-1.1, 1.1) + labs(title = "Cargas factoriales (comp. 1 y 2)", x = "Valor",
+ y = "Variable") + facet_wrap(~component, nrow = 2) + theme_minimal()
Vemos como los pesos de cada componente cambian notablemente. El
PC1
aparece altamente asociado a gnipc_2019
.
Esto se debe a la diferencia de escala y, por ende, a la mayor varianza
de esta variable.
> tidy(pca_estimates2, 1, type = "variance") %>%
+ filter(terms == "percent variance") %>%
+ mutate(component = paste0("PC", component)) %>%
+ ggplot(aes(x = component, y = value, group = terms)) + geom_col() + ylim(0, 100) +
+ labs(title = "% varianza", x = "Componente", y = "Valor") + theme_minimal() +
+
+ tidy(pca_estimates2, 1, type = "variance") %>%
+ filter(terms == "cumulative percent variance") %>%
+ mutate(component = paste0("PC", component)) %>%
+ ggplot(aes(x = component, y = value, group = terms)) + geom_col() + ylim(0, 100) +
+ labs(title = "% varianza acum.", x = "Componente", y = "Valor") + theme_minimal()
Comparen (de la forma que crean más conveniente) los resultados de
nuestro índice orignal (con las variables escaladas) y el IDH original
producido por la ONU (hdi_2019
). ¿Qué puede decir al
respecto’
> ###
Los vectores de puntuación de componentes principales se pueden utilizar como características en varias técnicas de aprendizaje supervisado. Por ejemplo, podríamos utilizar los componentes principales como predictores en una regresión o un árbol de decisión. Esto conducirá a resultados menos ruidosos, ya que a menudo la señal en el conjunto de datos se concentra en los primeros componentes principales.
También puede usarse PCA (u otras formas de reducción de dimensionalidad) para generar sets de variables que nos permitan generar clústers de forma más eficiente. Veremos algunos ejemplos en las próximas clases.
De hecho, el PC2
aparece represetantdo casi
exclusivamente al producto.↩︎