Análisis Exploratorio de Datos de Cadena Farmacéutica

Análisis de Calidad y Exploración Inicial con Álgebra Lineal

Autor/a

Proyecto Académico

Fecha de publicación

3 de enero de 2026

1 Resumen Ejecutivo

Este documento presenta un análisis exploratorio inicial de los datos de ventas, productos y stock de una cadena farmacéutica para los años 2018-2020. El objetivo es evaluar la calidad de los datos y proporcionar insights iniciales sobre la estructura y características de la información disponible.

2 Carga de Datos

Ver código
# Definir rutas de archivos
archivos <- list(
  ventas_2018 = "datos/daily_sales_2018.csv",
  ventas_2019 = "datos/daily_sales_2019.csv",
  ventas_2020 = "datos/daily_sales_2020.csv",
  productos = "datos/products.csv",
  stock = "datos/stock_pdv.csv"
)

# Cargar datos
ventas_2018 <- fread(archivos$ventas_2018)
ventas_2019 <- fread(archivos$ventas_2019)
ventas_2020 <- fread(archivos$ventas_2020)
productos <- fread(archivos$productos)
stock <- fread(archivos$stock)

# Consolidar ventas
ventas_completas <- rbindlist(list(ventas_2018, ventas_2019, ventas_2020))

# Crear columna de fecha
ventas_completas[, fecha := as.Date(paste(year, month, day, sep = "-"))]

cat("Datos cargados exitosamente:\n")
Datos cargados exitosamente:
Ver código
cat(sprintf("- Ventas totales: %s registros\n", 
            format(nrow(ventas_completas), big.mark = ",")))
- Ventas totales: 144,553,878 registros
Ver código
cat(sprintf("- Productos: %s registros\n", 
            format(nrow(productos), big.mark = ",")))
- Productos: 16,344 registros
Ver código
cat(sprintf("- Stock: %s registros\n", 
            format(nrow(stock), big.mark = ",")))
- Stock: 2,331,644 registros

3 Análisis de Calidad de Datos

3.1 Fórmula de Completitud

La completitud de una columna se define como:

\[ \text{Completitud}(X) = \frac{N_{\text{no-nulos}}}{N_{\text{total}}} \times 100\% \]

Donde: - \(N_{\text{no-nulos}}\) = cantidad de valores no nulos en la columna \(X\) - \(N_{\text{total}}\) = cantidad total de registros

3.2 Fórmula de Diversidad

La diversidad mide la cardinalidad relativa:

\[ \text{Diversidad}(X) = \frac{N_{\text{únicos}}}{N_{\text{no-nulos}}} \]

Donde: - \(N_{\text{únicos}}\) = cantidad de valores únicos (distintos) - Valores cercanos a 1 indican alta cardinalidad - Valores cercanos a 0 indican baja cardinalidad

Ver código
# Función para analizar calidad de datos
analisis_calidad <- function(df, nombre_dataset, excluir_cols = c()) {
  
  resultados <- data.frame(
    columna = character(),
    tipo = character(),
    total_registros = integer(),
    nulos = integer(),
    completitud_pct = numeric(),
    valores_unicos = integer(),
    diversidad_pct = numeric(),
    stringsAsFactors = FALSE
  )
  
  for (col in names(df)) {
    # Saltar columnas excluidas
    if (col %in% excluir_cols) next
    
    total <- nrow(df)
    nulos <- sum(is.na(df[[col]]))
    no_nulos <- total - nulos
    
    if (no_nulos > 0) {
      unicos <- length(unique(df[[col]][!is.na(df[[col]])]))
      diversidad <- unicos / no_nulos
    } else {
      unicos <- 0
      diversidad <- 0
    }
    
    resultados <- rbind(resultados, data.frame(
      columna = col,
      tipo = class(df[[col]])[1],
      total_registros = total,
      nulos = nulos,
      completitud_pct = round((no_nulos / total) * 100, 2),
      valores_unicos = unicos,
      diversidad_pct = round(diversidad * 100, 2)
    ))
  }
  
  return(resultados)
}

3.3 Análisis de Ventas

Ver código
calidad_ventas <- analisis_calidad(
  ventas_completas, 
  "Ventas Diarias",
  excluir_cols = c("idPharmacy", "idProduct")
)

kable(calidad_ventas,
      caption = "Métricas de Calidad - Datos de Ventas",
      format.args = list(big.mark = ","),
      align = c('l', 'c', 'r', 'r', 'r', 'r', 'r')) %>%
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed", "responsive"),
    full_width = FALSE,
    position = "center",
    font_size = 14
  ) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#2E86AB") %>%
  scroll_box(width = "100%", height = "400px")
Métricas de Calidad - Datos de Ventas
columna tipo total_registros nulos completitud_pct valores_unicos diversidad_pct
year integer 144,553,878 0 100 3 0.00
month integer 144,553,878 0 100 12 0.00
day integer 144,553,878 0 100 31 0.00
unitSales numeric 144,553,878 0 100 53,718 0.04
fecha Date 144,553,878 0 100 994 0.00

3.4 Análisis de Productos

Ver código
calidad_productos <- analisis_calidad(
  productos,
  "Productos",
  excluir_cols = c("idProduct")
)

kable(calidad_productos,
      caption = "Métricas de Calidad - Datos de Productos",
      format.args = list(big.mark = ","),
      align = c('l', 'c', 'r', 'r', 'r', 'r', 'r')) %>%
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed", "responsive"),
    full_width = FALSE,
    position = "center",
    font_size = 14
  ) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#2E86AB")
Métricas de Calidad - Datos de Productos
columna tipo total_registros nulos completitud_pct valores_unicos diversidad_pct
isRestricted integer 16,344 0 100 2 0.01
productCost numeric 16,344 0 100 15,460 94.59
sellingPrice numeric 16,344 0 100 10,779 65.95

3.5 Análisis de Stock

Ver código
calidad_stock <- analisis_calidad(
  stock,
  "Stock por Punto de Venta",
  excluir_cols = c("idPharmacy", "idProduct")
)

kable(calidad_stock,
      caption = "Métricas de Calidad - Datos de Stock",
      format.args = list(big.mark = ","),
      align = c('l', 'c', 'r', 'r', 'r', 'r', 'r')) %>%
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed", "responsive"),
    full_width = FALSE,
    position = "center",
    font_size = 14
  ) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#2E86AB") %>%
  scroll_box(width = "100%", height = "400px")
Métricas de Calidad - Datos de Stock
columna tipo total_registros nulos completitud_pct valores_unicos diversidad_pct
stock numeric 2,331,644 0 100 836 0.04

4 Estadísticas Descriptivas Avanzadas

4.1 Fórmulas Estadísticas

4.1.1 Asimetría (Skewness)

\[ \text{Asimetría}(X) = \frac{\frac{1}{n} \sum_{i=1}^n (x_i - \bar{x})^3}{\left(\frac{1}{n} \sum_{i=1}^n (x_i - \bar{x})^2\right)^{3/2}} \]

Interpretación: - \(> 0\): Distribución asimétrica hacia la derecha (cola larga a la derecha) - \(= 0\): Distribución simétrica - \(< 0\): Distribución asimétrica hacia la izquierda (cola larga a la izquierda)

4.1.2 Curtosis (Kurtosis)

\[ \text{Curtosis}(X) = \frac{\frac{1}{n} \sum_{i=1}^n (x_i - \bar{x})^4}{\left(\frac{1}{n} \sum_{i=1}^n (x_i - \bar{x})^2\right)^2} - 3 \]

Interpretación: - \(> 0\): Distribución leptocúrtica (más puntiaguda que la normal) - \(= 0\): Distribución mesocúrtica (similar a la normal) - \(< 0\): Distribución platicúrtica (más achatada que la normal)

4.1.3 Coeficiente de Variación

\[ CV = \frac{\sigma}{\mu} \times 100\% \]

Donde \(\sigma\) es la desviación estándar y \(\mu\) es la media.

Ver código
# Función para estadísticas avanzadas
estadisticas_avanzadas <- function(df, excluir_cols = c()) {
  
  # Seleccionar columnas numéricas
  cols_numericas <- names(df)[sapply(df, is.numeric)]
  cols_numericas <- setdiff(cols_numericas, excluir_cols)
  
  resultados <- data.frame()
  
  for (col in cols_numericas) {
    datos <- df[[col]][!is.na(df[[col]])]
    
    if (length(datos) >= 2) {
      media <- mean(datos)
      mediana <- median(datos)
      desv_std <- sd(datos)
      cv <- ifelse(media != 0, (desv_std / media) * 100, 0)
      
      asimetria <- ifelse(length(datos) > 2, skewness(datos), NA)
      curtosis <- ifelse(length(datos) > 3, kurtosis(datos) - 3, NA)
      
      q1 <- quantile(datos, 0.25)
      q3 <- quantile(datos, 0.75)
      iqr <- q3 - q1
      
      resultados <- rbind(resultados, data.frame(
        columna = col,
        n_valores = length(datos),
        media = round(media, 4),
        mediana = round(mediana, 4),
        desv_std = round(desv_std, 4),
        cv_pct = round(cv, 2),
        asimetria = round(asimetria, 4),
        curtosis = round(curtosis, 4),
        minimo = round(min(datos), 4),
        Q1 = round(q1, 4),
        Q3 = round(q3, 4),
        maximo = round(max(datos), 4),
        IQR = round(iqr, 4)
      ))
    }
  }
  
  return(resultados)
}

4.2 Estadísticas de Ventas

Ver código
stats_ventas <- estadisticas_avanzadas(
  ventas_completas,
  excluir_cols = c("idPharmacy", "idProduct", "year", "month", "day")
)

kable(stats_ventas,
      caption = "Estadísticas Descriptivas Avanzadas - Ventas",
      format.args = list(big.mark = ","),
      digits = 4) %>%
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed", "responsive"),
    full_width = FALSE,
    position = "center",
    font_size = 13
  ) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#A23B72") %>%
  scroll_box(width = "100%", height = "300px")
Estadísticas Descriptivas Avanzadas - Ventas
columna n_valores media mediana desv_std cv_pct asimetria curtosis minimo Q1 Q3 maximo IQR
25% unitSales 144,553,878 1.53 1.03 17.4777 1,142.34 457.8607 453,905.6 -18,961.95 0.3125 1.09 33,034.63 0.7775

4.3 Estadísticas de Productos

Ver código
stats_productos <- estadisticas_avanzadas(
  productos,
  excluir_cols = c("idProduct", "isRestricted")
)

kable(stats_productos,
      caption = "Estadísticas Descriptivas Avanzadas - Productos",
      format.args = list(big.mark = ","),
      digits = 4) %>%
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed", "responsive"),
    full_width = FALSE,
    position = "center",
    font_size = 13
  ) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#A23B72")
Estadísticas Descriptivas Avanzadas - Productos
columna n_valores media mediana desv_std cv_pct asimetria curtosis minimo Q1 Q3 maximo IQR
25% productCost 16,344 9.8328 4.1604 63.4563 645.35 54.6687 3,581.48 0.0027 1.9669 9.6167 4,995.936 7.6498
25%1 sellingPrice 16,344 16.7946 7.5007 104.6050 622.85 52.2806 3,152.10 0.0089 3.5490 17.0130 7,008.714 13.4640

4.4 Estadísticas de Stock

Ver código
stats_stock <- estadisticas_avanzadas(
  stock,
  excluir_cols = c("idPharmacy", "idProduct")
)

kable(stats_stock,
      caption = "Estadísticas Descriptivas Avanzadas - Stock",
      format.args = list(big.mark = ","),
      digits = 4) %>%
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed", "responsive"),
    full_width = FALSE,
    position = "center",
    font_size = 13
  ) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#A23B72")
Estadísticas Descriptivas Avanzadas - Stock
columna n_valores media mediana desv_std cv_pct asimetria curtosis minimo Q1 Q3 maximo IQR
25% stock 2,331,644 3.2016 2 15.5466 485.59 298.8226 169,781.7 0.1 1 3.7 12,000 2.7

5 Detección de Valores Atípicos (Outliers)

5.1 Método del Rango Intercuartílico (IQR)

Los límites para detectar outliers se calculan como:

\[ Q_1 = \text{Percentil}(25\%), \quad Q_3 = \text{Percentil}(75\%) \]

\[ IQR = Q_3 - Q_1 \]

\[ \text{Límite Inferior} = Q_1 - 1.5 \times IQR \]

\[ \text{Límite Superior} = Q_3 + 1.5 \times IQR \]

Los valores fuera de \([\text{Límite Inferior}, \text{Límite Superior}]\) se consideran outliers.

Ver código
# Función para detectar outliers
detectar_outliers_iqr <- function(df, excluir_cols = c()) {
  
  cols_numericas <- names(df)[sapply(df, is.numeric)]
  cols_numericas <- setdiff(cols_numericas, excluir_cols)
  
  resultados <- data.frame()
  
  for (col in cols_numericas) {
    datos <- df[[col]][!is.na(df[[col]])]
    
    if (length(datos) >= 4) {
      q1 <- quantile(datos, 0.25)
      q3 <- quantile(datos, 0.75)
      iqr <- q3 - q1
      
      limite_inf <- q1 - 1.5 * iqr
      limite_sup <- q3 + 1.5 * iqr
      
      outliers <- datos[datos < limite_inf | datos > limite_sup]
      
      resultados <- rbind(resultados, data.frame(
        columna = col,
        Q1 = round(q1, 4),
        Q3 = round(q3, 4),
        IQR = round(iqr, 4),
        limite_inferior = round(limite_inf, 4),
        limite_superior = round(limite_sup, 4),
        n_outliers = length(outliers),
        pct_outliers = round(length(outliers) / length(datos) * 100, 2),
        total_valores = length(datos)
      ))
    }
  }
  
  return(resultados)
}

5.2 Outliers en Ventas

Ver código
outliers_ventas <- detectar_outliers_iqr(
  ventas_completas,
  excluir_cols = c("idPharmacy", "idProduct", "year", "month", "day")
)

kable(outliers_ventas,
      caption = "Detección de Outliers - Ventas (Método IQR)",
      format.args = list(big.mark = ","),
      digits = 4) %>%
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed", "responsive"),
    full_width = FALSE,
    position = "center",
    font_size = 13
  ) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#F18F01") %>%
  column_spec(7, bold = TRUE, color = "red")
Detección de Outliers - Ventas (Método IQR)
columna Q1 Q3 IQR limite_inferior limite_superior n_outliers pct_outliers total_valores
25% unitSales 0.3125 1.09 0.7775 -0.8538 2.2563 10,496,819 7.26 144,553,878

5.3 Outliers en Productos

Ver código
outliers_productos <- detectar_outliers_iqr(
  productos,
  excluir_cols = c("idProduct", "isRestricted")
)

kable(outliers_productos,
      caption = "Detección de Outliers - Productos (Método IQR)",
      format.args = list(big.mark = ","),
      digits = 4) %>%
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed", "responsive"),
    full_width = FALSE,
    position = "center",
    font_size = 13
  ) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#F18F01") %>%
  column_spec(7, bold = TRUE, color = "red")
Detección de Outliers - Productos (Método IQR)
columna Q1 Q3 IQR limite_inferior limite_superior n_outliers pct_outliers total_valores
25% productCost 1.9669 9.6167 7.6498 -9.5078 21.0913 1,405 8.60 16,344
25%1 sellingPrice 3.5490 17.0130 13.4640 -16.6470 37.2090 1,271 7.78 16,344

6 Análisis de Correlación

6.1 Coeficiente de Correlación de Pearson

\[ r_{XY} = \frac{\sum_{i=1}^n (x_i - \bar{x})(y_i - \bar{y})}{\sqrt{\sum_{i=1}^n (x_i - \bar{x})^2 \sum_{i=1}^n (y_i - \bar{y})^2}} \]

Donde: - \(r_{XY} \in [-1, 1]\) - \(r = 1\): correlación positiva perfecta - \(r = -1\): correlación negativa perfecta - \(r = 0\): sin correlación lineal

6.1.1 Interpretación de la Magnitud

  • \(|r| > 0.9\): Correlación muy fuerte
  • \(0.7 < |r| \leq 0.9\): Correlación fuerte
  • \(0.5 < |r| \leq 0.7\): Correlación moderada
  • \(0.3 < |r| \leq 0.5\): Correlación débil
  • \(|r| \leq 0.3\): Correlación muy débil o inexistente

6.2 Matriz de Correlación - Productos

Ver código
# Seleccionar variables numéricas de productos
productos_num <- productos %>%
  select(where(is.numeric), -idProduct) %>%
  na.omit()

if (ncol(productos_num) >= 2) {
  cor_productos <- cor(productos_num)
  
  corrplot(cor_productos, 
           method = "color",
           type = "upper",
           addCoef.col = "black",
           tl.col = "black",
           tl.srt = 45,
           number.cex = 0.8,
           title = "Matriz de Correlación - Productos",
           mar = c(0, 0, 2, 0))
  
  # Tabla de correlaciones significativas
  cor_long <- as.data.frame(as.table(cor_productos))
  colnames(cor_long) <- c("Variable1", "Variable2", "Correlacion")
  
  cor_sig <- cor_long %>%
    filter(Variable1 != Variable2) %>%
    filter(abs(Correlacion) > 0.5) %>%
    arrange(desc(abs(Correlacion))) %>%
    distinct(Correlacion, .keep_all = TRUE) %>%
    mutate(Correlacion = round(Correlacion, 4))
  
  if (nrow(cor_sig) > 0) {
    kable(cor_sig,
          caption = "Correlaciones Significativas en Productos (|r| > 0.5)",
          digits = 4) %>%
      kable_styling(
        bootstrap_options = c("striped", "hover", "condensed", "responsive"),
        full_width = FALSE,
        position = "center",
        font_size = 14
      ) %>%
      row_spec(0, bold = TRUE, color = "white", background = "#06A77D")
  }
}

Correlaciones Significativas en Productos (|r| > 0.5)
Variable1 Variable2 Correlacion
sellingPrice productCost 0.9746

7 Análisis Temporal de Ventas

7.1 Distribución de Ventas por Año

Ver código
ventas_por_anio <- ventas_completas %>%
  group_by(year) %>%
  summarise(
    total_ventas = sum(unitSales, na.rm = TRUE),
    promedio_diario = mean(unitSales, na.rm = TRUE),
    n_registros = n()
  )

ggplot(ventas_por_anio, aes(x = factor(year), y = total_ventas)) +
  geom_col(fill = "#2E86AB", alpha = 0.8) +
  geom_text(aes(label = format(total_ventas, big.mark = ",")), 
            vjust = -0.5, size = 4) +
  labs(title = "Ventas Totales por Año",
       x = "Año",
       y = "Unidades Vendidas") +
  scale_y_continuous(labels = comma) +
  theme_minimal() +
  theme(plot.title = element_text(hjust = 0.5, face = "bold", size = 14))

7.2 Distribución de Ventas por Mes

Ver código
ventas_por_mes <- ventas_completas %>%
  group_by(year, month) %>%
  summarise(total_ventas = sum(unitSales, na.rm = TRUE), .groups = "drop")

ggplot(ventas_por_mes, aes(x = month, y = total_ventas, color = factor(year))) +
  geom_line(linewidth = 1.2) +
  geom_point(size = 2.5) +
  labs(title = "Evolución Mensual de Ventas",
       x = "Mes",
       y = "Unidades Vendidas",
       color = "Año") +
  scale_x_continuous(breaks = 1:12) +
  scale_y_continuous(labels = comma) +
  theme_minimal() +
  theme(plot.title = element_text(hjust = 0.5, face = "bold", size = 14),
        legend.position = "bottom")

8 Análisis de Descomposición de Valores Singulares (SVD)

8.1 Fundamento Matemático del SVD

La descomposición en valores singulares (SVD) de una matriz \(\mathbf{X}\) de dimensiones \(m \times n\) se expresa como:

\[ \mathbf{X} = \mathbf{U} \boldsymbol{\Sigma} \mathbf{V}^T \]

Donde: - \(\mathbf{U}\) es una matriz \(m \times m\) ortogonal (vectores singulares izquierdos) - \(\boldsymbol{\Sigma}\) es una matriz diagonal \(m \times n\) con los valores singulares \(\sigma_1 \geq \sigma_2 \geq \cdots \geq \sigma_r > 0\) - \(\mathbf{V}^T\) es la transpuesta de una matriz \(n \times n\) ortogonal (vectores singulares derechos)

8.2 Varianza Explicada

La varianza explicada por los primeros \(k\) componentes es:

\[ \text{Varianza Explicada}_k = \frac{\sum_{i=1}^k \sigma_i^2}{\sum_{i=1}^r \sigma_i^2} \times 100\% \]

Ver código
# Función para análisis SVD
analisis_svd <- function(df, excluir_cols = c(), n_componentes = 5) {
  
  # Seleccionar columnas numéricas
  cols_numericas <- names(df)[sapply(df, is.numeric)]
  cols_numericas <- setdiff(cols_numericas, excluir_cols)
  
  if (length(cols_numericas) < 2) {
    return(list(error = "Se requieren al menos 2 columnas numéricas"))
  }
  
  # Preparar matriz de datos
  datos_matriz <- df %>%
    select(all_of(cols_numericas)) %>%
    na.omit() %>%
    as.matrix()
  
  if (nrow(datos_matriz) < 2) {
    return(list(error = "Datos insuficientes después de remover NAs"))
  }
  
  # Normalizar datos (z-score)
  datos_norm <- scale(datos_matriz)
  
  # Realizar SVD
  svd_resultado <- svd(datos_norm)
  
  # Calcular varianza explicada
  valores_singulares <- svd_resultado$d
  varianza_total <- sum(valores_singulares^2)
  varianza_explicada <- cumsum(valores_singulares^2) / varianza_total * 100
  
  # Crear resumen
  n_comp <- min(n_componentes, length(valores_singulares))
  
  resumen <- data.frame(
    Componente = 1:n_comp,
    Valor_Singular = round(valores_singulares[1:n_comp], 4),
    Varianza_Explicada_Pct = round(varianza_explicada[1:n_comp], 2)
  )
  
  return(list(
    resumen = resumen,
    valores_singulares = valores_singulares,
    varianza_explicada = varianza_explicada,
    V = svd_resultado$v,
    columnas = cols_numericas
  ))
}

8.3 SVD de Datos de Productos

Ver código
svd_productos <- analisis_svd(
  productos,
  excluir_cols = c("idProduct", "isRestricted"),
  n_componentes = 3
)

if (!is.null(svd_productos$resumen)) {
  kable(svd_productos$resumen,
        caption = "Análisis SVD - Productos (Primeros Componentes)",
        digits = 4) %>%
    kable_styling(
      bootstrap_options = c("striped", "hover", "condensed", "responsive"),
      full_width = FALSE,
      position = "center",
      font_size = 14
    ) %>%
    row_spec(0, bold = TRUE, color = "white", background = "#6A4C93")
  
  # Gráfico de varianza explicada
  if (length(svd_productos$varianza_explicada) > 1) {
    df_var <- data.frame(
      Componente = 1:length(svd_productos$varianza_explicada),
      Varianza = svd_productos$varianza_explicada
    )
    
    ggplot(df_var, aes(x = Componente, y = Varianza)) +
      geom_line(color = "#2E86AB", linewidth = 1.2) +
      geom_point(color = "#A23B72", size = 3) +
      geom_hline(yintercept = c(70, 80, 90, 95), 
                 linetype = "dashed", color = "gray50", alpha = 0.5) +
      labs(title = "Varianza Explicada Acumulada - SVD",
           x = "Número de Componentes",
           y = "Varianza Explicada (%)") +
      scale_y_continuous(limits = c(0, 100), breaks = seq(0, 100, 10)) +
      theme_minimal() +
      theme(plot.title = element_text(hjust = 0.5, face = "bold", size = 14))
  }
}

9 Conclusiones y Recomendaciones

9.1 Hallazgos Principales

Ver código
cat("\n### Calidad de Datos\n\n")

9.1.1 Calidad de Datos

Ver código
# Análisis de completitud
completitud_promedio <- mean(c(
  mean(calidad_ventas$completitud_pct),
  mean(calidad_productos$completitud_pct),
  mean(calidad_stock$completitud_pct)
))

cat(sprintf("- **Completitud promedio general**: %.2f%%\n", completitud_promedio))
  • Completitud promedio general: 100.00%
Ver código
# Identificar columnas críticas
columnas_criticas <- rbind(
  calidad_ventas %>% filter(completitud_pct < 90) %>% mutate(dataset = "Ventas"),
  calidad_productos %>% filter(completitud_pct < 90) %>% mutate(dataset = "Productos"),
  calidad_stock %>% filter(completitud_pct < 90) %>% mutate(dataset = "Stock")
)

if (nrow(columnas_criticas) > 0) {
  cat("\n- **Columnas con completitud crítica (< 90%)**:\n")
  for (i in 1:nrow(columnas_criticas)) {
    cat(sprintf("  - %s.%s: %.2f%%\n", 
                columnas_criticas$dataset[i],
                columnas_criticas$columna[i],
                columnas_criticas$completitud_pct[i]))
  }
}

cat("\n### Distribución de Datos\n\n")

9.1.2 Distribución de Datos

Ver código
# Análisis de outliers
total_outliers <- sum(outliers_ventas$n_outliers, outliers_productos$n_outliers)
cat(sprintf("- **Total de valores atípicos detectados**: %s\n", 
            format(total_outliers, big.mark = ",")))
  • Total de valores atípicos detectados: 10,499,495
Ver código
cat("\n### Volumen de Información\n\n")

9.1.3 Volumen de Información

Ver código
cat(sprintf("- **Período de análisis**: 2018-2020\n"))
  • Período de análisis: 2018-2020
Ver código
cat(sprintf("- **Registros de ventas**: %s\n", 
            format(nrow(ventas_completas), big.mark = ",")))
  • Registros de ventas: 144,553,878
Ver código
cat(sprintf("- **Productos únicos**: %s\n", 
            format(nrow(productos), big.mark = ",")))
  • Productos únicos: 16,344
Ver código
cat(sprintf("- **Puntos de venta**: %s\n", 
            format(length(unique(ventas_completas$idPharmacy)), big.mark = ",")))
  • Puntos de venta: 806

9.2 Recomendaciones

  1. Calidad de Datos:
    • Investigar y corregir valores faltantes en columnas críticas
    • Implementar validaciones en el proceso de captura de datos
  2. Valores Atípicos:
    • Revisar registros con valores extremos para detectar posibles errores de entrada
    • Considerar transformaciones de datos para análisis posteriores
  3. Próximos Pasos:
    • Realizar análisis de series temporales para identificar tendencias y estacionalidad
    • Implementar modelos predictivos de demanda por producto y punto de venta
    • Analizar relación entre precios, costos y volumen de ventas

Fecha de generación: 2026-01-03

Herramientas utilizadas: R 4.5.1, tidyverse, data.table, knitr