El siguiente documento muestra un ejercicio basado en la Extracción, Transformación y Carga (ETL), el cual tiene como propósito principal obtener información financiera de mercado, realizar procesos de limpieza y transformación, y garantizar la calidad e integridad de los datos antes de proceder al análisis estadístico.
Para ello, se emplean diversas librerías especializadas del ecosistema de R, entre ellas herramientas orientadas al análisis financiero y a la manipulación eficiente de datos. Particularmente, se utiliza el paquete yfR para la descarga de información histórica desde Yahoo Finance, junto con librerías como quantmod, tidyverse, tidyquant, zoo y PerformanceAnalytics, que permiten el tratamiento de series temporales financieras y el cálculo de métricas de rendimiento.
En esta fase inicial es necesario instalar las librerías que permitirán realizar la descarga, transformación y análisis de los datos financieros. Los paquetes quantmod, tidyverse, tidyquant, zoo y PerformanceAnalytics se instalan desde el repositorio oficial de CRAN mediante la función install.packages(), ya que forman parte del ecosistema estándar de R para análisis de datos y finanzas.
Por su parte, el paquete yfR, utilizado para descargar información histórica desde Yahoo Finance, puede instalarse desde CRAN; sin embargo, en este ejercicio se opta por instalarlo directamente desde GitHub mediante:
library(yfR) # Descarga datos desde Yahoo Finance
library(quantmod) # Modelado cuantitativo financiero
library(tidyverse) # Manipulación y visualización de datos
library(tidyquant) # Integración tidy + finanzas
library(zoo) # Series temporales
library(PerformanceAnalytics)# Métricas de rendimiento
library(dplyr) # Manipulación de datos
library(tidyr) # Transformación de estructurasComo primer paso del proceso ETL se definen los activos financieros objeto de estudio. En este caso, se seleccionan acciones que componen el índice COLCAP, el cual agrupa a las principales empresas listadas en la Bolsa de Valores de Colombia. La identificación de cada activo se realiza mediante su respectivo ticker, información que debe consultarse directamente en Yahoo Finance.
tickers <- c("CEMARGOS.CL" , "CORFICOLCF.CL", "PFCIBEST.CL",
"BOGOTA.CL", "ECOPETROL.CL", "GEB.CL", "GRUPOARGOS.CL",
"MINEROS.CL", "GRUPOSURA.CL", "NUTRESA.CL","GRUPOAVAL.CL","PFAVAL.CL",
"PFCEMARGOS.CL", "PFDAVVNDA.CL", "PFGRUPOARG.CL", "PFGRUPSURA.CL"
,"PROMIGAS.CL", "TERPEL.CL", "GRUBOLIVAR.CL")Posteriormente, se define el horizonte temporal del análisis estableciendo una fecha de inicio y una fecha de finalización bajo el formato día-mes-año (DD-MM-AAAA). Inicialmente, ambas fechas se declaran como cadenas de texto y posteriormente se convierten al tipo de dato Date mediante la función as.Date() especificando el formato correspondiente (“%d-%m-%Y”). Esta transformación es esencial para garantizar una correcta interpretación cronológica, permitir filtrados por rango temporal y facilitar el manejo adecuado de las series de tiempo dentro del entorno de análisis.
fecha_inicio <- "01-01-2019"
fecha_fin <- "02-01-2025"
fecha_inicio <- as.Date(fecha_inicio, format = "%d-%m-%Y")
fecha_fin <- as.Date(fecha_fin, format = "%d-%m-%Y")Una vez definidos los tickers, se procede a la descarga de la información histórica utilizando la función yf_get() del paquete yfR. Para este análisis se establece una frecuencia diaria (daily), adecuada para estudiar la dinámica de corto y mediano plazo de los precios. Asimismo, se emplean retornos logarítmicos (type_return = “log”), preferidos en finanzas por su propiedad de aditividad temporal y su conveniencia en modelaciones econométricas. Finalmente, se activa el parámetro do_complete_data = TRUE con el fin de garantizar la integridad de las series temporales, evitando inconsistencias o saltos injustificados en los datos descargados.
datos_financieros <- yf_get(
tickers = tickers,
first_date = fecha_inicio,
last_date = fecha_fin,
freq_data = "daily",
type_return = "log",
do_complete_data = TRUE
)Visualizamos nuestra base de datos incial
## # A tibble: 27,994 × 11
## ticker ref_date price_open price_high price_low price_close volume
## * <chr> <date> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 BOGOTA.CL 2019-01-01 51887. 51887. 51887. 51887. 0
## 2 BOGOTA.CL 2019-01-02 50269. 53932. 50232. 50269. 1478
## 3 BOGOTA.CL 2019-01-03 53003. 53077. 50269. 53003. 2358
## 4 BOGOTA.CL 2019-01-04 52110. 54844. 52073. 52110. 7626
## 5 BOGOTA.CL 2019-01-07 52110. 52110. 52110. 52110. 0
## 6 BOGOTA.CL 2019-01-08 52538. 52538. 52166. 52538. 12387
## 7 BOGOTA.CL 2019-01-09 53932. 53932. 53003. 53932. 3540
## 8 BOGOTA.CL 2019-01-10 53932. 54825. 53932. 53932. 2222
## 9 BOGOTA.CL 2019-01-11 53932. 54769. 53932. 53932. 9576
## 10 BOGOTA.CL 2019-01-14 53932. 53932. 53932. 53932. 6291
## # ℹ 27,984 more rows
## # ℹ 4 more variables: price_adjusted <dbl>, ret_adjusted_prices <dbl>,
## # ret_closing_prices <dbl>, cumret_adjusted_prices <dbl>
En este momento es pertienente hacer un filtro de verificación en la obtención de los datos.Se establecio un número de activos financieros anteriormente, entonces podemos revisar si todos los datos de los activos financieros fueron cargados correctamente.
El resultado del siguiente fragmento es un vector vacío si todos los activos financieros fueron correctamente descargados. En caso contrario, el resultado mostrará cuáles no se cargaron, permitiendo tomar acciones correctivas.
## [1] 19
## [1] 18
## [1] "BOGOTA.CL" "CEMARGOS.CL" "CORFICOLCF.CL" "ECOPETROL.CL"
## [5] "GEB.CL" "GRUBOLIVAR.CL" "GRUPOARGOS.CL" "GRUPOAVAL.CL"
## [9] "GRUPOSURA.CL" "MINEROS.CL" "NUTRESA.CL" "PFAVAL.CL"
## [13] "PFCIBEST.CL" "PFDAVVNDA.CL" "PFGRUPOARG.CL" "PFGRUPSURA.CL"
## [17] "PROMIGAS.CL" "TERPEL.CL"
## [1] "PFCEMARGOS.CL"
Por ejemplo, la acción PFCEMARGOS.CL fue eliminada automáticamente por la función yf_get dado que el paquete exige que al menos el 75% de los datos del período solicitado sean válidos, pero como solo había 4%, el activo fue descartado automáticamente. Las razones pueden ser:
En esta etapa se filtran únicamente las variables necesarias para el análisis, optimizando la estructura de la base de datos y asegurando que se trabaje exclusivamente con información pertinente para la evaluación financiera. Es fundamental ser cuidadoso con los nombres de las columnas, ya que deben coincidir exactamente con los reconocidos por la función de descarga.
Las variables seleccionadas son:
datos_finales <- datos_financieros %>%
select(
ticker,
ref_date,
price_open,
price_high,
price_close,
volume,
price_adjusted,
ret_adjusted_prices,
ret_closing_prices,
cumret_adjusted_prices
)
datos_finales## # A tibble: 27,994 × 10
## ticker ref_date price_open price_high price_close volume price_adjusted
## <chr> <date> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 BOGOTA.CL 2019-01-01 51887. 51887. 51887. 0 33350.
## 2 BOGOTA.CL 2019-01-02 50269. 53932. 50269. 1478 32467.
## 3 BOGOTA.CL 2019-01-03 53003. 53077. 53003. 2358 34233.
## 4 BOGOTA.CL 2019-01-04 52110. 54844. 52110. 7626 33656.
## 5 BOGOTA.CL 2019-01-07 52110. 52110. 52110. 0 33656.
## 6 BOGOTA.CL 2019-01-08 52538. 52538. 52538. 12387 33933.
## 7 BOGOTA.CL 2019-01-09 53932. 53932. 53932. 3540 34833.
## 8 BOGOTA.CL 2019-01-10 53932. 54825. 53932. 2222 34833.
## 9 BOGOTA.CL 2019-01-11 53932. 54769. 53932. 9576 34833.
## 10 BOGOTA.CL 2019-01-14 53932. 53932. 53932. 6291 34833.
## # ℹ 27,984 more rows
## # ℹ 3 more variables: ret_adjusted_prices <dbl>, ret_closing_prices <dbl>,
## # cumret_adjusted_prices <dbl>
Como segundo paso del proceso ETL se transforman los datos para garantizar la consistencia estructural, facilitar la indexación de la base de datos y asegurar la compatibilidad técnica con los modelos de análisis financiero.
Siguiendo las buenas prácticas de bases de datos argumentadas por las formas normales, se genera una Llave Primaria Sintética combinando el Ticker y la Fecha. Esto garantiza que cada registro sea único e identificable.
datos_finales <- datos_finales %>%
mutate(
# Llave: Ticker + Fecha formateada (Ej: ECOPETROL_20240101)
llave_id = paste0(ticker, "_", format(ref_date, "%Y%m%d"))
) %>%
select(llave_id, everything())
# Validamos que sea una llave primaria (Valores únicos = Número de registros)
n_distinct(datos_finales$llave_id) == nrow(datos_finales)## [1] TRUE
## # A tibble: 27,994 × 11
## llave_id ticker ref_date price_open price_high price_close volume
## <chr> <chr> <date> <dbl> <dbl> <dbl> <dbl>
## 1 BOGOTA.CL_20190101 BOGOT… 2019-01-01 51887. 51887. 51887. 0
## 2 BOGOTA.CL_20190102 BOGOT… 2019-01-02 50269. 53932. 50269. 1478
## 3 BOGOTA.CL_20190103 BOGOT… 2019-01-03 53003. 53077. 53003. 2358
## 4 BOGOTA.CL_20190104 BOGOT… 2019-01-04 52110. 54844. 52110. 7626
## 5 BOGOTA.CL_20190107 BOGOT… 2019-01-07 52110. 52110. 52110. 0
## 6 BOGOTA.CL_20190108 BOGOT… 2019-01-08 52538. 52538. 52538. 12387
## 7 BOGOTA.CL_20190109 BOGOT… 2019-01-09 53932. 53932. 53932. 3540
## 8 BOGOTA.CL_20190110 BOGOT… 2019-01-10 53932. 54825. 53932. 2222
## 9 BOGOTA.CL_20190111 BOGOT… 2019-01-11 53932. 54769. 53932. 9576
## 10 BOGOTA.CL_20190114 BOGOT… 2019-01-14 53932. 53932. 53932. 6291
## # ℹ 27,984 more rows
## # ℹ 4 more variables: price_adjusted <dbl>, ret_adjusted_prices <dbl>,
## # ret_closing_prices <dbl>, cumret_adjusted_prices <dbl>
Una forma de verificar la correcta creación de la llave es comparar si el número de registros únicos de la llave es igual al número de registros del dataset. En el siguiente fragmento, si el valor es TRUE significa que la llave sintética efectivamente toma el papel de ID.
# Validamos que sea una llave primaria (Valores únicos = Número de registros)
n_distinct(datos_finales$llave_id) == nrow(datos_finales)## [1] TRUE
A continuación, se realiza un proceso de limpieza y estandarización de tipos de datos. Se asegura que la variable de fecha sea de tipo Date, mientras que todas las variables de precios, volumen y retornos se convierten explícitamente a formato numérico. Este paso es crítico, ya que un tipo de dato incorrecto puede generar errores en cálculos estadísticos, visualizaciones o modelos econométricos.
datos_finales <- datos_finales %>%
mutate(
# 1. Asegurar que la fecha sea objeto tipo Date
ref_date = as.Date(ref_date),
# 2. Convertir columnas de precios y retornos a numérico
# (Usamos as.numeric para forzar por si vienen como caracteres)
price_open = as.numeric(price_open),
price_high = as.numeric(price_high),
price_close = as.numeric(price_close),
price_adjusted = as.numeric(price_adjusted),
volume = as.numeric(volume),
# 3. Asegurar que los retornos sean numéricos (muy importante para EDA)
ret_adjusted_prices = as.numeric(ret_adjusted_prices),
ret_closing_prices = as.numeric(ret_closing_prices),
cumret_adjusted_prices = as.numeric(cumret_adjusted_prices))
datos_finales## # A tibble: 27,994 × 11
## llave_id ticker ref_date price_open price_high price_close volume
## <chr> <chr> <date> <dbl> <dbl> <dbl> <dbl>
## 1 BOGOTA.CL_20190101 BOGOT… 2019-01-01 51887. 51887. 51887. 0
## 2 BOGOTA.CL_20190102 BOGOT… 2019-01-02 50269. 53932. 50269. 1478
## 3 BOGOTA.CL_20190103 BOGOT… 2019-01-03 53003. 53077. 53003. 2358
## 4 BOGOTA.CL_20190104 BOGOT… 2019-01-04 52110. 54844. 52110. 7626
## 5 BOGOTA.CL_20190107 BOGOT… 2019-01-07 52110. 52110. 52110. 0
## 6 BOGOTA.CL_20190108 BOGOT… 2019-01-08 52538. 52538. 52538. 12387
## 7 BOGOTA.CL_20190109 BOGOT… 2019-01-09 53932. 53932. 53932. 3540
## 8 BOGOTA.CL_20190110 BOGOT… 2019-01-10 53932. 54825. 53932. 2222
## 9 BOGOTA.CL_20190111 BOGOT… 2019-01-11 53932. 54769. 53932. 9576
## 10 BOGOTA.CL_20190114 BOGOT… 2019-01-14 53932. 53932. 53932. 6291
## # ℹ 27,984 more rows
## # ℹ 4 more variables: price_adjusted <dbl>, ret_adjusted_prices <dbl>,
## # ret_closing_prices <dbl>, cumret_adjusted_prices <dbl>
Posteriormente, se efectúa un análisis de valores faltantes (NA). Los valores NA deben ser gestionados cuidadosamente, ya que pueden generar inconsistencias en cálculos posteriores. En el caso específico de los retornos financieros, es esperable que el primer registro de cada activo contenga un valor NA, dado que el cálculo del retorno requiere un valor del período inmediatamente anterior. Por lo tanto, el número de valores faltantes iniciales debería coincidir con la cantidad de activos analizados, lo cual constituye una validación lógica del proceso.
resumen_nas <- datos_finales %>%
summarise(across(everything(), ~ sum(is.na(.)))) %>%
pivot_longer(cols = everything(), names_to = "Variable", values_to = "Cantidad_NAs") %>%
mutate(Porcentaje = (Cantidad_NAs / nrow(datos_finales)) * 100)
print(resumen_nas)## # A tibble: 11 × 3
## Variable Cantidad_NAs Porcentaje
## <chr> <int> <dbl>
## 1 llave_id 0 0
## 2 ticker 0 0
## 3 ref_date 0 0
## 4 price_open 0 0
## 5 price_high 0 0
## 6 price_close 0 0
## 7 volume 0 0
## 8 price_adjusted 0 0
## 9 ret_adjusted_prices 18 0.0643
## 10 ret_closing_prices 18 0.0643
## 11 cumret_adjusted_prices 0 0
Para garantizar que los modelos de volatilidad y rendimientos acumulados funcionen correctamente, se procede a la eliminación de registros NA. Se prioriza la limpieza sobre las variables ret_adjusted_prices y price_close, ya que son los pilares de la fase de modelado.
La fase final del proceso ETL consiste en consolidar la información transformada en una estructura de datos definitiva. Esta “Base de Datos de Trabajo” representa el insumo refinado que garantiza la integridad de los resultados en las fases posteriores de Análisis Exploratorio (EDA) y Optimización de Portafolios.
## Rows: 27,976
## Columns: 11
## $ llave_id <chr> "BOGOTA.CL_20190102", "BOGOTA.CL_20190103", "BO…
## $ ticker <chr> "BOGOTA.CL", "BOGOTA.CL", "BOGOTA.CL", "BOGOTA.…
## $ ref_date <date> 2019-01-02, 2019-01-03, 2019-01-04, 2019-01-07…
## $ price_open <dbl> 50268.70, 53002.51, 52109.83, 52109.83, 52537.5…
## $ price_high <dbl> 53932.38, 53076.90, 54843.65, 52109.83, 52537.5…
## $ price_close <dbl> 50268.70, 53002.51, 52109.83, 52109.83, 52537.5…
## $ volume <dbl> 1478, 2358, 7626, 0, 12387, 3540, 2222, 9576, 6…
## $ price_adjusted <dbl> 32467.20, 34232.91, 33656.34, 33656.34, 33932.6…
## $ ret_adjusted_prices <dbl> -0.0268288530, 0.0529570276, -0.0169860248, 0.0…
## $ ret_closing_prices <dbl> -0.0316792789, 0.0529567059, -0.0169855841, 0.0…
## $ cumret_adjusted_prices <dbl> 0.9731711, 1.0261282, 1.0091421, 1.0091421, 1.0…
La correcta ejecución de las fases de Extracción, Transformación y Carga permite concluir que los datos se encuentran en un estado óptimo para el análisis avanzado. Es importante resaltar que, aunque los precios nominales están denominados en Pesos Colombianos (COP), la estandarización hacia retornos logarítmicos elimina las disparidades de escala entre activos, permitiendo comparaciones directas de volatilidad y rentabilidad.
Con la creación de la base de datos BD, se ha mitigado el riesgo de errores por valores nulos o tipos de datos inconsistentes, asegurando que cualquier hallazgo estadístico posterior sea reflejo fiel de la dinámica del mercado financiero colombiano y no un artefacto de la calidad de la información.