Índice

  1. Introducción

  2. Exploracion de los datos

Introducción

Este estudio se centra en analizar las ventas de café en un período específico, utilizando datos detallados de transacciones para identificar patrones y tendencias.

El dataset utilizado en este estudio, denominado Coffee Sales Overview, proviene de Kaggle. Este conjunto de datos contiene registros detallados de ventas de café de una máquina expendedora. La máquina expendedora es obra del autor del dataset, quien está comprometido a proporcionar un conjunto de datos abierto a la comunidad. Está destinado al análisis de patrones de compra, tendencias de ventas y preferencias de los clientes relacionadas con los productos de café.

El período de recopilación de datos abarca desde marzo de 2024 hasta la fecha actual, capturando datos de transacciones diarias. Además, se continúa añadiendo nueva información, lo que permite un análisis continuo y actualizado de las ventas de café. Este estudio se enfocará principalmente en predicciones y análisis de regresión, utilizando modelos como ARIMA para prever tendencias futuras y analizar patrones históricos de ventas. Asimismo, se realizarán pruebas de hipótesis utilizando métodos como las Variables Instrumentales (IV) y la Estimación de Mínimos Cuadrados en Dos Etapas (2SLS) para abordar problemas de endogeneidad y obtener estimaciones consistentes1.

Además, se incorporarán conceptos de estabilidad y predicción, así como pruebas de hipótesis e inferencia estadística. La estabilidad en los modelos de predicción ees crucial para asegurar que las estimaciones sean robustas y fiables a lo largo del tiempo. Las pruebas de hipótesis permitirán validar los resultados obtenidos y asegurar que las conclusiones sean estadísticamente significativas

Exploracion de los datos

Contenido

El archivo coffee-sales.csv contiene datos de ventas de café, incluyendo información sobre la fecha, hora, tipo de pago, monto y tipo de café vendido. Aquí tienes un resumen de los datos:

  • Fecha y hora: Las ventas están registradas con precisión de fecha y hora.
  • Tipo de pago: Se registran pagos con tarjeta y en efectivo.
  • Monto: El monto de cada venta varía según el tipo de café.
  • Tipo de café: Incluye variedades como Latte, Hot Chocolate, Americano, Americano with Milk, Cocoa, Cortado, Espresso, y Cappuccino.

Importar datos

Se importa el dataset coffee-sales.csv

library(corrplot) # Llamar a librerias
library(ggplot2)
library(dplyr)
library(tsibble)
library(ggplot2)
library(lubridate) 
library(readxl)

# Especifica la ruta del archivo 
file_path <- "C:/Users/enano/OneDrive/Escritorio/Analisis_Longitudinal/coffee-sales.csv"


# Importa los datos
df <- read.csv(file_path)

El dataset contiene 1917 registros y 6 columnas.

Visualizacion de las varibles

nulls

# Contar valores nulos en todo el dataframe
total_na <- sum(is.na(df))

# Contar valores nulos por columna
na_por_columna <- colSums(is.na(df))

# Mostrar resultados
total_na
## [1] 0
na_por_columna
##        date    datetime   cash_type        card       money coffee_name 
##           0           0           0           0           0           0

No hay presencia de null en los datos frame.

summary(df)
##      date             datetime          cash_type             card          
##  Length:1917        Length:1917        Length:1917        Length:1917       
##  Class :character   Class :character   Class :character   Class :character  
##  Mode  :character   Mode  :character   Mode  :character   Mode  :character  
##                                                                             
##                                                                             
##                                                                             
##      money       coffee_name       
##  Min.   :18.12   Length:1917       
##  1st Qu.:27.92   Class :character  
##  Median :32.82   Mode  :character  
##  Mean   :31.56                     
##  3rd Qu.:35.76                     
##  Max.   :40.00

Histograma

# Histograma acumulado de coffee_name
ggplot(df, aes(x = coffee_name, fill = coffee_name)) +
  geom_bar(stat = "count") +
  labs(title = "Histograma Acumulado de Coffee Name", x = "Coffee Name", y = "Frecuencia") +
  theme_minimal()

# Histograma acumulado de cash_type
ggplot(df, aes(x = cash_type, fill = cash_type)) +
  geom_bar(stat = "count") +
  labs(title = "Histograma Acumulado de Cash Type", x = "Cash Type", y = "Frecuencia") +
  theme_minimal()

# Crear un histograma y un gráfico de densidad
ggplot(df, aes(x = money)) +
  geom_histogram(aes(y = ..density..), bins = 30, fill = "blue", alpha = 0.5) +
  geom_density(color = "red", size = 1) +
  labs(title = "Distribución de Money", x = "Money", y = "Densidad") +
  theme_minimal()
## Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
## ℹ Please use `linewidth` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: The dot-dot notation (`..density..`) was deprecated in ggplot2 3.4.0.
## ℹ Please use `after_stat(density)` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.

Test de Shapiro-Wilk

# Realizar el test de Shapiro-Wilk
shapiro.test(df$money)
## 
##  Shapiro-Wilk normality test
## 
## data:  df$money
## W = 0.93565, p-value < 2.2e-16

El test de Shapiro-Wilk se utilizó para evaluar la normalidad de la variable money en el conjunto de datos. Los resultados del test arrojaron un estadístico W de 0.93565 y un valor p menor a 2.2e-16.

El estadístico W, que varía entre 0 y 1, se utiliza para medir la similitud de la distribución de los datos con una distribución normal. Un valor de W cercano a 1 indica una alta similitud con la normalidad. Sin embargo, en este caso, aunque el valor de W es relativamente alto, el valor p asociado es extremadamente pequeño.

El valor p obtenido es significativamente menor que el umbral comúnmente aceptado de 0.05. Este resultado sugiere que hay suficiente evidencia para rechazar la hipótesis nula de que los datos provienen de una distribución normal. En otras palabras, la distribución de la variable money no sigue una distribución normal, lo que implica que se deben considerar otras distribuciones o transformaciones para un análisis adecuado de los datos.

transformación logaritmica de money

# Aplicar la transformación logarítmica
df$log_money <- log(df$money)

# Verificar la distribución después de la transformación
ggplot(df, aes(x = log_money)) +
  geom_histogram(aes(y = ..density..), bins = 30, fill = "blue", alpha = 0.5) +
  geom_density(color = "red", size = 1) +
  labs(title = "Distribución de Log(Money)", x = "Log(Money)", y = "Densidad") +
  theme_minimal()

# Realizar el test de Shapiro-Wilk nuevamente
shapiro.test(df$log_money)
## 
##  Shapiro-Wilk normality test
## 
## data:  df$log_money
## W = 0.91263, p-value < 2.2e-16

El resultado del test de Shapiro-Wilk aplicado a la variable original ‘money’ muestra un valor W de 0.93565 con un p-value inferior a 2.2e-16. Este resultado indica que la distribución de ‘money’ se aleja significativamente de la normalidad.

Al aplicar el mismo test a la variable transformada ‘log_money’, se obtiene un valor W de 0.91263 y un p-value igualmente inferior a 2.2e-16. Aunque la transformación logarítmica presenta un valor de W ligeramente menor, lo que sugiere una distribución aún más distante de la normalidad, la transformación logarítmica generalmente mejora la normalidad en distribuciones sesgadas y estabiliza la varianza.

En resumen, ambos conjuntos de datos presentan desviaciones significativas de la normalidad, pero la transformación logarítmica podría ser útil para ciertos modelos estadísticos que requieren variables más normales. Sin embargo, en ambos casos, el p-value muy bajo indica que ninguna de las distribuciones se ajusta bien a una distribución normal.

Estacionalidad

Una serie es estacionaria cuando sus propiedades no dependen del momento del tiempo en que la serie es observada. De esta forma, series con tendencia o estacionalidad no son estacionarias (las tendencias y la estacionalidad van a afectar el valor de la serie en distintos momentos del tiempo). Por otro lado, el ruido blanco, es estacionario - no importa el momento en que se lo observe, siempre va a tener un aspecto muy similar.

df <- df %>%  # Convertir la columna datetime a formato Date si aún no lo está
  mutate(datetime = as.POSIXct(datetime, format = "%Y-%m-%d %H:%M:%S"),
         date = as.Date(datetime))


df_monthly <- df %>%   # Crear una serie temporal agregada a nivel mensual
  group_by(month = yearmonth(date)) %>%
  summarise(money = sum(money, na.rm = TRUE))

# Convertir a serie temporal (ts) con frecuencia mensual
money_ts <- ts(df_monthly$money, start = c(year(min(df_monthly$month)), 
                                           month(min(df_monthly$month))), 
               frequency = 12)


Dmoney <- diff(money_ts) # Calcular diferencias de la serie temporal

# Combinar series original y diferenciada
money_ts_combined <- ts.union(original = money_ts, diff = Dmoney)

# Graficar las series
par(mfrow = c(2, 1)) # Dividir la pantalla para 2 gráficos
plot(money_ts_combined[, "original"], main = "Serie Original (money)", 
     ylab = "Money", xlab = "Tiempo", col = "blue")
plot(money_ts_combined[, "diff"], main = "Serie Diferenciada (Dmoney)", 
     ylab = "Cambio en Money", xlab = "Tiempo", col = "orange")

# Reiniciar los gráficos
par(mfrow = c(1, 1))

Test Dickey-Fuller

El test de Dickey-Fuller (o Prueba de Dickey-Fuller Aumentada, ADF) se utiliza para verificar si una serie temporal es estacionaria o presenta una raíz unitaria, lo que indica que la serie tiene una tendencia temporal (no es estacionaria)

library(tseries)
## Registered S3 method overwritten by 'quantmod':
##   method            from
##   as.zoo.data.frame zoo
# Prueba de Dickey-Fuller Aumentada
adf_test <- adf.test(money_ts)
print(adf_test)
## 
##  Augmented Dickey-Fuller Test
## 
## data:  money_ts
## Dickey-Fuller = -3.8207, Lag order = 1, p-value = 0.03424
## alternative hypothesis: stationary

El resultado de la prueba de Dickey-Fuller aumentada para la serie temporal money_ts muestra un estadístico de prueba de -3.8207 con un orden de rezago igual a 1 y un valor p de 0.03424.

Dado que el valor p es inferior a 0.05, se rechaza la hipótesis nula de que la serie tiene una raíz unitaria, lo que indica que la serie es estacionaria bajo la hipótesis alternativa. Esto sugiere que la serie no presenta una tendencia no estacionaria y que sus características estadísticas son constantes a lo largo del tiempo.

Outliers

En el siguiente análisis, se van a tratar los outliers en la variable de ventas diarias (money). Se identificará y manejará cualquier valor atípico que se desvíe significativamente de la distribución general de las ventas, ya sea porque sean errores de medición o porque representen eventos excepcionales.

Histograma acumulado de ventas diarias

El histograma acumulado de ventas diarias tiene como objetivo visualizar la distribución de las ventas diarias sumadas a lo largo del tiempo. Al mostrar la frecuencia de las diferentes cantidades de ventas diarias, permite identificar patrones, como si las ventas tienden a concentrarse en ciertos rangos o si existen días con ventas excepcionalmente altas o bajas, y detectar posibles anomalías (outliers)

# Cargar paquetes necesarios
library(dplyr)
library(ggplot2)

# Asegurarse de que la columna 'datetime' está en formato fecha
df <- df %>%
  mutate(date = as.Date(datetime))  # Convertir 'datetime' a solo fechas

# Acumular ventas diarias (equivalente a resample('D').sum())
df_daily <- df %>%
  group_by(date) %>%
  summarise(daily_sales = sum(money, na.rm = TRUE))  # Sumar 'money' por día

# Crear el histograma (equivalente a sns.histplot())
ggplot(df_daily, aes(x = daily_sales)) +
  geom_histogram(binwidth = 10, fill = "blue", color = "black", alpha = 0.7) +
  labs(title = "Distribución de Ventas Diarias Acumuladas",
       x = "Ventas Diarias (Money)",
       y = "Frecuencia") +
  theme_minimal()

### boxplot

# Crear un boxplot con las ventas diarias acumuladas
ggplot(df_daily, aes(y = daily_sales)) +
  geom_boxplot(fill = "skyblue", color = "black", outlier.color = "red", outlier.size = 4) +
  labs(title = "Boxplot de Ventas Diarias Acumuladas",
       y = "Ventas Diarias (Money)",
       x = "") +
  theme_minimal()

Rango Intercuartil para detectar outliers

# Calcular los cuartiles y el IQR
Q1 <- quantile(df_daily$daily_sales, 0.25, na.rm = TRUE)
Q3 <- quantile(df_daily$daily_sales, 0.75, na.rm = TRUE)
IQR_value <- IQR(df_daily$daily_sales, na.rm = TRUE)

# Definir los límites para detectar outliers
lower_limit <- Q1 - 1.5 * IQR_value
upper_limit <- Q3 + 1.5 * IQR_value

# Filtrar los outliers
outliers <- df_daily %>%
  filter(daily_sales < lower_limit | daily_sales > upper_limit)

# Mostrar los outliers
print(outliers)
## # A tibble: 2 × 2
##   date       daily_sales
##   <date>           <dbl>
## 1 2024-09-17        713.
## 2 2024-10-11        770.

El rango intercuartílico (IQR) se utiliza para identificar y eliminar valores atípicos en un conjunto de datos. Los valores atípicos son aquellos que se encuentran fuera de los límites definidos por:

  • Límite inferior: Q1 - 1.5 * IQR

    • Q1 (Primer Cuartil): Este es el valor que marca el 25% de los datos por debajo de él.
    • IQR (Rango Intercuartílico): Es la diferencia entre el tercer cuartil (Q3) y el primer cuartil (Q1). Representa la dispersión de la mitad central de los datos.
    • 1.5 * IQR: Este es un multiplicador que se utiliza para definir un rango razonable alrededor de los cuartiles. Multiplicar el IQR por * 1.5 ayuda a identificar valores que están significativamente alejados de la mayoría de los datos.
    • Q1 - 1.5 * IQR (Límite Inferior): Este cálculo define el límite inferior para detectar valores atípicos. Cualquier valor por debajo de este límite se considera un outlier. La fórmula completa es: [ = Q1 - 1.5 IQR ]
  • Límite superior: Q3 + 1.5 * IQR

    • Q3 (Tercer Cuartil): Este es el valor que marca el 75% de los datos por debajo de él.
    • Q3 + 1.5 * IQR (Límite Superior): Este cálculo define el límite superior para detectar valores atípicos. Cualquier valor por encima de este límite se considera un outlier. La fórmula completa es: [ = Q3 + 1.5 IQR ]

Estos límites se establecen porque los valores que caen fuera de este rango son considerados extremos y pueden distorsionar el análisis estadístico y los modelos predictivos. Al eliminar estos valores atípicos, se mejora la precisión de la regresión, ya que se basa en datos que representan mejor la tendencia general.

En este caso, los valores de ventas diarias que excedieron los límites establecidos se consideran para eliminación con el fin de mejorar la calidad del análisis y la precisión de la regresión. Sin embargo, en este caso, se opta por reemplazarlos con la media. Eliminar estos valores resultaría en la pérdida de registros, lo que afectaría negativamente el desempeño de la predicción. Por lo tanto, el reemplazo por la media permite mantener la integridad del conjunto de datos y mejorar la precisión del modelo predictivo.

Reemplazar los outliers por la media

Se va a proceder a identificar los outliers en la columna money del dataframe. Primero, se calculará la media de las ventas y se determinarán los límites para detectar los outliers. A continuación, se agregará una columna para indicar si un valor ha sido modificado, marcando como “modificados” aquellos valores fuerad de los límites establecidos. Finalmente, se filtrarán y mostrarán solo las filas que contienen los valores marcados como modificados.

# Calcular la media de 'money' para reemplazar los outliers
mean_sales <- mean(df$money, na.rm = TRUE)

# Reemplazar los outliers por la media y agregar una columna para indicar si se modificó
df_clean <- df %>%
  mutate(
    money = ifelse(money < lower_limit | money > upper_limit, mean_sales, money),  # Reemplazar outliers
    modified = ifelse(money == mean_sales, "Modified", "Unmodified")  # Etiquetar si fue modificado
  )

# Ver los primeros registros del dataframe limpio
head(df_clean)
##         date            datetime cash_type                card money
## 1 2024-03-01 2024-03-01 10:15:50      card ANON-0000-0000-0001  38.7
## 2 2024-03-01 2024-03-01 12:19:22      card ANON-0000-0000-0002  38.7
## 3 2024-03-01 2024-03-01 12:20:18      card ANON-0000-0000-0002  38.7
## 4 2024-03-01 2024-03-01 13:46:33      card ANON-0000-0000-0003  28.9
## 5 2024-03-01 2024-03-01 13:48:14      card ANON-0000-0000-0004  38.7
## 6 2024-03-01 2024-03-01 15:39:47      card ANON-0000-0000-0005  33.8
##           coffee_name log_money   modified
## 1               Latte  3.655840 Unmodified
## 2       Hot Chocolate  3.655840 Unmodified
## 3       Hot Chocolate  3.655840 Unmodified
## 4           Americano  3.363842 Unmodified
## 5               Latte  3.655840 Unmodified
## 6 Americano with Milk  3.520461 Unmodified
# Filtrar y mostrar solo las filas modificadas
modified_rows <- df_clean %>% filter(modified == "Modified")
head(modified_rows)
## [1] date        datetime    cash_type   card        money       coffee_name
## [7] log_money   modified   
## <0 rows> (o 0- extensión row.names)

Dummies

Las variables dummy se crean cuando es necesario incluir información categórica en un modelo que solo admite variables numéricas, como la regresión lineal o logística. Estas variables representan los niveles de una variable categórica mediante valores binarios (0 o 1), indicando la presencia o ausencia de un nivel específico.

# Asegúrate de que las columnas categóricas estén como factores
df_clean$cash_type <- as.factor(df_clean$cash_type)
df_clean$coffee_name <- as.factor(df_clean$coffee_name)

# Generar variables dummy excluyendo una columna de referencia automáticamente
dummies <- model.matrix(~ cash_type + coffee_name, data = df_clean)

# Convertir las dummies a un dataframe
df_clean_dummies <- as.data.frame(dummies)

# Verificar las columnas generadas
str(df_clean_dummies)
## 'data.frame':    1917 obs. of  9 variables:
##  $ (Intercept)                   : num  1 1 1 1 1 1 1 1 1 1 ...
##  $ cash_typecash                 : num  0 0 0 0 0 0 0 0 0 0 ...
##  $ coffee_nameAmericano with Milk: num  0 0 0 0 0 1 0 1 0 1 ...
##  $ coffee_nameCappuccino         : num  0 0 0 0 0 0 0 0 0 0 ...
##  $ coffee_nameCocoa              : num  0 0 0 0 0 0 0 0 1 0 ...
##  $ coffee_nameCortado            : num  0 0 0 0 0 0 0 0 0 0 ...
##  $ coffee_nameEspresso           : num  0 0 0 0 0 0 0 0 0 0 ...
##  $ coffee_nameHot Chocolate      : num  0 1 1 0 0 0 1 0 0 0 ...
##  $ coffee_nameLatte              : num  1 0 0 0 1 0 0 0 0 0 ...

La eliminación del nivel de referencia rompe la dependencia lineal entre las columnas. En lugar de usar todas las dummies, el modelo usa las dummies restantes para medir las diferencias con respecto al nivel de referencia, asegurando un modelo estable y sin redundancias, evitando la multicolinealidad.

  • Exclusión automática de una columna:

    • En model.matrix, si no se especifica -1 en la fórmula, el paquete automáticamente crea un modelo con una columna de referencia para evitar multicolinealidad.

    • El nivel de referencia no aparecerá como una variable explícita en los datos. En su lugar, los coeficientes de las dummies restantes se interpretarán como la diferencia respecto a este nivel de referencia.