Taller 1 ADFG

Author

Camilo Gómez - Juan Holguín - Laura Venegas

Objetivo

Este documento detalla el proceso de para la creación de un dataset financiero unificado. El objetivo es consolidar múltiples fuentes de datos de acciones, tratar valores faltantes y estandarizar las divisas.

Contexto

Como equipo, optamos por la ruta ‘A’, consistente en el seguimiento de los precios diarios de activos financieros. Específicamente, seleccionamos empresas del sector retail enfocadas en el cuidado del hogar y personal: Colgate, Kenvue, Unilever, P&G, Reckitt, Essity y Kimberly Clark. La fuente de información utilizada fue la plataforma Refinitiv, extrayendo el valor de las acciones para el periodo comprendido entre el 1 de enero de 2021 y el 31 de diciembre de 2025. Cabe señalar que, en el caso de Kenvue, los registros solo están disponibles a partir de 2023; el tratamiento dado a estos datos faltantes se explicará más adelante.

Diccionario

-Nombres: Colgate, Kenvue, Unilever, P&G, Reckitt, Essity y Kimberly Clark.

-Descripción: Empresas del sector retail (Enfocadas en Cuidado del Hogar y Personal)

-Unidad: Dólar estadounidense (USD), con conversiones para activos en otras divisas

-Frecuencia: Diario (comprendido entre 1 de enero 2021 - 31 de diciembre 2026)

-Llave: Fecha

Obtención de Datos

Los datos fueron obtenidos de la herramienta Refinitiv. Decidimos ir por la ruta A y descargar datos de las acciones de empresas manufactureras.Hicimos una revisión individual a los archivos para entender sus formatos, las fechas que tenian datos disponibles,etc.

Entorno

Para garantizar la reproducibilidad y el manejo de datos, cargamos el ecosistema tidyverse y herramientas específicas para la limpieza de nombres y manejo de fechas.

library(readxl)
library(dplyr)
library(lubridate) #Usamos lubridate para asegurar que la fecha este de forma crónologica y no como texto
library(stringr)
library(janitor) #Usamos Janitor para normalizar los nombres de las columnas

Carga de Datos

En esta etapa, automatizamos la lectura de múltiples archivos Excel guardados en una carpeta dentro del PC. Usamos la función read_one para extraer el nombre de la empresa del nombre del archivo y estandarizamos las columnas a date y ticker.

data_dir <- "C:/Users/sebas/Downloads/Taller 1 Prueba ADFG"
files <- list.files(data_dir, pattern = "\\.xlsx$", full.names = TRUE)

read_one <- function(path) {
  ticker <- tools::file_path_sans_ext(basename(path)) |>
    str_remove("^Acciones\\s*") |>
    str_replace_all("\\s+", "_") |>
    tolower()

  df <- read_excel(path) |>
    clean_names() |>
    select(1:2)

  names(df) <- c("date", ticker)
  df$date <- as.Date(df$date)

  df
}

dfs <- setNames(lapply(files, read_one),
                nm = tools::file_path_sans_ext(basename(files)))

# Ver qué cargó
lapply(dfs, nrow) #Aqui confirmamos que se han cargado los archivos de nuestra carpeta y revisamos que el número de filas sea el correcto.
$`Acciones Colgate`
[1] 1255

$`Acciones Essity`
[1] 1257

$`Acciones Kenvue`
[1] 668

$`Acciones Kimberly Clark`
[1] 1255

$`Acciones P&G`
[1] 1255

$`Acciones Reckit`
[1] 1285

$`Acciones Unilever`
[1] 1285

Antes de unificar la información, debemos garantizar la integridad de los datos. Con esto nos referimos a que no existan registros duplicados para una misma fecha en los archivos individuales

library(dplyr)
library(purrr)

#| warning: false
#| echo: true


#La función fix_dupes identifica la columna de interés(En este caso la fecha)las agrupa y en caso de existir duplicidad, selecciona el último registro válido (no nulo) disponible

fix_dupes <- function(df) {
  value_col <- setdiff(names(df), "date")[1]  # la columna de la empresa

  df %>%
    arrange(date) %>%
    group_by(date) %>%
    summarise(
      "{value_col}" := dplyr::last(na.omit(.data[[value_col]])),
      .groups = "drop"
    )
}

dfs_clean <- lapply(dfs, fix_dupes)

Después de confirmar que los archivos individuales esten “Limpios”, procedemos a unirlos por medio de Full_join. Adicional nos preguntamos cuantos faltantes identificamos por empresa.

df_prices <- reduce(dfs_clean, full_join, by = "date") %>%
  arrange(date)

head(df_prices, 10)
# Revisemos faltantes por empresa|
sapply(df_prices, function(x) sum(is.na(x)))
# A tibble: 10 × 8
   date       colgate essity kenvue kimberly_clark `p&g` reckit unilever
   <date>       <dbl>  <dbl>  <dbl>          <dbl> <dbl>  <dbl>    <dbl>
 1 2021-01-04    84.3   268.     NA           133.  138.  6794.    4779.
 2 2021-01-05    84.5   265.     NA           134.  139.  6718.    4752.
 3 2021-01-06    82.2    NA      NA           133.  140.  6746.    4785.
 4 2021-01-07    81.8   264.     NA           132.  139.  6678.    4764.
 5 2021-01-08    82.4   264.     NA           132.  139.  6670.    4767.
 6 2021-01-11    82.1   260.     NA           131.  138.  6566.    4730.
 7 2021-01-12    82.3   262.     NA           130.  137.  6367.    4629.
 8 2021-01-13    82.5   266.     NA           132.  137.  6317.    4662.
 9 2021-01-14    81.9   270.     NA           132.  136.  6335.    4664.
10 2021-01-15    81.9   265.     NA           131.  135.  6283.    4707.
          date        colgate         essity         kenvue kimberly_clark 
             0             37             35            624             37 
           p&g         reckit       unilever 
            37             31             31 

Faltantes

Después de detectar los datos faltantes y para facilitar su análisis, extrajimos estos registros en un Dataframe independiente, lo que nos ayudó a definir la mejor estrategia de limpieza para cada faltante.

na_rows <- df_prices %>%
  dplyr::filter(dplyr::if_any(-date, is.na)) %>%
  dplyr::arrange(date)

na_rows <- as.data.frame(na_rows)

Para el archivo Kenvue, encontramos que desde el 4 de Enero de 2021 hasta el 3 de Mayo de 2023, no se tienen datos. Para efectos de este análisis, se optó por completar estos faltantes mediante la generación de valores aleatorios, basados en el promedio y la desviación estándar de los datos disponibles.

library(dplyr)
library(lubridate)

set.seed(123)  

start <- dmy("04/01/2021")
end   <- dmy("03/05/2023")

# Calculamos el promedio y la desviación estandar
kenvue_obs <- df_prices$kenvue[!is.na(df_prices$kenvue)]
mu    <- mean(kenvue_obs)
sigma <- sd(kenvue_obs)

# Identificamos las filas del rango que definimos (Solo las NA)
cond <- df_prices$date >= start & df_prices$date <= end & is.na(df_prices$kenvue)
idx <- which(cond)

length(idx)
[1] 604
# Generamos valores aleatorios
df_prices$kenvue[idx] <- rnorm(length(idx), mean = mu, sd = sigma)

# Evitamos valores negativos 
df_prices$kenvue[idx] <- pmax(df_prices$kenvue[idx], 0)

# Revisamos que en el rango determinado no existen más NA
sum(is.na(df_prices$kenvue) & df_prices$date >= start & df_prices$date <= end)
[1] 0

Para los demás datos faltantes, notamos que se pueden deber a que las acciones cotizan en distintas bolsas y no todas cierran los mismos días festivos. Para solucionar esto, aplicamos la técnica LOCF (Last Observation Carried Forward), que básicamente consiste en llenar el espacio vacío usando el precio de cierre del día anterior.

library(dplyr)
library(tidyr)

df_prices <- df_prices %>% arrange(date)

# Revisamos los NA actuales
na_before <- sapply(df_prices %>% select(-date), \(x) sum(is.na(x)))

# Cambiamos el valor de los NA con el resultado del día anterior más cercano
df_prices_filled <- df_prices %>%
  tidyr::fill(-date, .direction = "down")

# Revisamos nuevamente los NA para confirmar que se hayan cambiado
na_after <- sapply(df_prices_filled %>% select(-date), \(x) sum(is.na(x)))
na_after
       colgate         essity         kenvue kimberly_clark            p&g 
             0              0              0              0              0 
        reckit       unilever 
             0              0 

Conversión de Divisas

Ahora procederemos a la conversión de divisas para Unilever y Reckitt. Como sus precios están expresados en GBX (peniques), los pasaremos a USD tomando la tasa del 7 de marzo de 2026, que sitúa el cambio en 75.08 GBX por cada dólar.

# Convertimos los valores de Reckitt y Unilever que vienen en GBX a USD
rate_gbx_per_usd <- 75.08  # 1 USD = 75.08 GBX  =>  USD = GBX / 75.08

# Ajustamos nombres de columnas
cols_gbx <- intersect(names(df_prices_filled), c("reckitt", "reckit", "unilever"))

if (length(cols_gbx) == 0) {
  cols_gbx <- names(df_prices_filled)[grepl("reckitt|reckit|unilever", names(df_prices_filled), ignore.case = TRUE)]
}

cols_gbx  
[1] "reckit"   "unilever"
# Aplicamos la conversión
df_prices_filled <- df_prices_filled %>%
  mutate(across(all_of(cols_gbx), ~ .x / rate_gbx_per_usd))

# Imprimimos el DF con los nuevos valores
df_final <- as.data.frame(df_prices_filled)

head(df_final[, c("date", cols_gbx)], 10)
         date   reckit unilever
1  2021-01-04 90.49111 63.64764
2  2021-01-05 89.47735 63.29095
3  2021-01-06 89.85084 63.73324
4  2021-01-07 88.94379 63.44789
5  2021-01-08 88.83708 63.49069
6  2021-01-11 87.44984 63.00559
7  2021-01-12 84.80873 61.65018
8  2021-01-13 84.14179 62.09247
9  2021-01-14 84.38189 62.12101
10 2021-01-15 83.68827 62.69171

De igual manera, realizamos la conversión de divisa para la empresa Essity, cuyos precios están expresados en Coronas Suecas (SEK). Para llevarlos a USD, utilizaremos el tipo de cambio del 7 de marzo de 2026, donde 9.18 SEK equivalen a 1 USD.

# Convertimos los valores de Essity que estan en Coronas Suecas (Sek) en USD
rate_sek_per_usd <- 9.18  # 9.18 SEK = 1 USD  =>  USD = SEK / 9.18

# Detectamps columna Essity 
col_essity <- intersect(names(df_prices_filled), c("essity"))

if (length(col_essity) == 0) {
  col_essity <- names(df_prices_filled)[grepl("essity", names(df_prices_filled), ignore.case = TRUE)]
}

col_essity 
[1] "essity"
# Aplicamos la conversión
df_prices_filled <- df_prices_filled %>%
  mutate(across(all_of(col_essity), ~ .x / rate_sek_per_usd))

# Imprimimos el DF con los nuevos valores
df_final <- as.data.frame(df_prices_filled)

head(df_final[, c("date", col_essity)], 10)
         date   essity
1  2021-01-04 29.15033
2  2021-01-05 28.87800
3  2021-01-06 28.87800
4  2021-01-07 28.81264
5  2021-01-08 28.79085
6  2021-01-11 28.33333
7  2021-01-12 28.52941
8  2021-01-13 29.00871
9  2021-01-14 29.35730
10 2021-01-15 28.89978

DATASET FINAL

Como resultado final, obtenemos un Dataset consolidado, libre de valores faltantes y con todos los precios estandarizados en una misma divisa (USD).

# --- DATASET FINAL (sin NA y en USD) ---

# Hacemos una última revisión de faltantes
na_by_company_final <- colSums(is.na(df_prices_filled %>% dplyr::select(-date)))
na_total_final <- sum(na_by_company_final)

na_by_company_final
       colgate         essity         kenvue kimberly_clark            p&g 
             0              0              0              0              0 
        reckit       unilever 
             0              0 
na_total_final
[1] 0
stopifnot(na_total_final == 0)  # falla si todavía queda algún NA

# Dataset Final
df_final_usd <- as.data.frame(df_prices_filled)

dim(df_final_usd)
[1] 1292    8
# Podemos ver el DF completo en el ambiente de R para confirmar que todo quedo bien.

View(df_final_usd)

# Descargamos el archivo en formato CSV para usarlo como base en el Taller #2.
write.csv(df_final_usd,
          "C:/Users/sebas/Downloads/Taller 2/dataset_final_usd.csv",
          row.names = FALSE)