---
title: "Análisis Exploratorio de Datos de Cadena Farmacéutica"
subtitle: "Análisis de Calidad y Exploración Inicial con Álgebra Lineal"
author: "Proyecto Académico"
date: "`r Sys.Date()`"
lang: es
format:
html:
toc: true
toc-depth: 3
toc-location: left
toc-title: "Contenido"
number-sections: true
code-fold: true
code-summary: "Ver código"
code-tools: true
smooth-scroll: true
theme:
light: flatly
dark: darkly
css: styles.css
df-print: paged
fig-width: 10
fig-height: 6
fig-align: center
embed-resources: false
execute:
warning: false
message: false
echo: true
---
```{r setup, include=FALSE}
knitr:: opts_chunk$ set (
echo = TRUE ,
warning = FALSE ,
message = FALSE ,
fig.width = 10 ,
fig.height = 6 ,
fig.align = 'center'
)
# Cargar librerías necesarias
if (! require ("pacman" )) install.packages ("pacman" )
pacman:: p_load (
tidyverse, # Manipulación de datos
data.table, # Lectura eficiente
skimr, # Resúmenes estadísticos
corrplot, # Gráficos de correlación
knitr, # Tablas
kableExtra, # Tablas avanzadas
moments, # Asimetría y curtosis
psych, # Estadísticas descriptivas
scales # Formateo de números
)
```
# 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.
# Carga de Datos
```{r carga-datos}
# 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 " )
cat (sprintf ("- Ventas totales: %s registros \n " ,
format (nrow (ventas_completas), big.mark = "," )))
cat (sprintf ("- Productos: %s registros \n " ,
format (nrow (productos), big.mark = "," )))
cat (sprintf ("- Stock: %s registros \n " ,
format (nrow (stock), big.mark = "," )))
```
# Análisis de Calidad de Datos
## 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
## 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
```{r calidad-datos-funcion}
# 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)
}
```
## Análisis de Ventas
```{r calidad-ventas}
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" )
```
## Análisis de Productos
```{r calidad-productos}
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" )
```
## Análisis de Stock
```{r calidad-stock}
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" )
```
# Estadísticas Descriptivas Avanzadas
## Fórmulas Estadísticas
### 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)
### 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)
### Coeficiente de Variación
$$
CV = \frac{\sigma}{\mu} \times 100\%
$$
Donde $\sigma$ es la desviación estándar y $\mu$ es la media.
```{r estadisticas-avanzadas}
# 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)
}
```
## Estadísticas de Ventas
```{r stats-ventas}
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 de Productos
```{r stats-productos}
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 de Stock
```{r stats-stock}
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" )
```
# Detección de Valores Atípicos (Outliers)
## 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.
```{r deteccion-outliers}
# 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)
}
```
## Outliers en Ventas
```{r outliers-ventas}
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" )
```
## Outliers en Productos
```{r outliers-productos}
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" )
```
# Análisis de Correlación
## 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
### 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
## Matriz de Correlación - Productos
```{r correlacion-productos, fig.height=6}
# 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" )
}
}
```
# Análisis Temporal de Ventas
## Distribución de Ventas por Año
```{r ventas-anuales, fig.height=5}
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 ))
```
## Distribución de Ventas por Mes
```{r ventas-mensuales, fig.height=6}
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" )
```
# Análisis de Descomposición de Valores Singulares (SVD)
## 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)
## 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\%
$$
```{r svd-analysis}
# 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
))
}
```
## SVD de Datos de Productos
```{r svd-productos}
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 ))
}
}
```
# Conclusiones y Recomendaciones
## Hallazgos Principales
```{r conclusiones, results='asis'}
cat (" \n ### Calidad de Datos \n\n " )
# 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))
# 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 " )
# 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 = "," )))
cat (" \n ### Volumen de Información \n\n " )
cat (sprintf ("- **Período de análisis**: 2018-2020 \n " ))
cat (sprintf ("- **Registros de ventas**: %s \n " ,
format (nrow (ventas_completas), big.mark = "," )))
cat (sprintf ("- **Productos únicos**: %s \n " ,
format (nrow (productos), big.mark = "," )))
cat (sprintf ("- **Puntos de venta**: %s \n " ,
format (length (unique (ventas_completas$ idPharmacy)), big.mark = "," )))
```
## 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**: `r Sys.Date()`
**Herramientas utilizadas**: R `r getRversion()` , tidyverse, data.table, knitr