“Super Store” es una gran cadena de almacenes, que ante un entorno altamente competitivo necesita comprender mejor el desempeño de sus sucursales. Para lo cual se le ha encargado realizar un análisis para identificar a que productos, regiones, categorías y segmentos debería enforcarse o renunciar.
Para ello debe desarrollar:
Importación y resumen de datos
# Importar el archivo CSV
df <- read.csv("Superstore Dataset.csv", header = TRUE, sep = ",")
#head(df)
kable(head(df, n = 3))
| Row.ID | Order.ID | Order.Date | Ship.Date | Ship.Mode | Customer.ID | Customer.Name | Segment | Country | City | State | Postal.Code | Region | Product.ID | Category | Sub.Category | Product.Name | Sales | Quantity | Discount | Profit |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | CA-2016-152156 | 11/8/2016 | 11/11/2016 | Second Class | CG-12520 | Claire Gute | Consumer | United States | Henderson | Kentucky | 42420 | South | FUR-BO-10001798 | Furniture | Bookcases | Bush Somerset Collection Bookcase | 261.96 | 2 | 0 | 41.9136 |
| 2 | CA-2016-152156 | 11/8/2016 | 11/11/2016 | Second Class | CG-12520 | Claire Gute | Consumer | United States | Henderson | Kentucky | 42420 | South | FUR-CH-10000454 | Furniture | Chairs | Hon Deluxe Fabric Upholstered Stacking Chairs, Rounded Back | 731.94 | 3 | 0 | 219.5820 |
| 3 | CA-2016-138688 | 6/12/2016 | 6/16/2016 | Second Class | DV-13045 | Darrin Van Huff | Corporate | United States | Los Angeles | California | 90036 | West | OFF-LA-10000240 | Office Supplies | Labels | Self-Adhesive Address Labels for Typewriters by Universal | 14.62 | 2 | 0 | 6.8714 |
#summary(df)
# Verificar la estructura de los datos importados
str(df)
## 'data.frame': 9994 obs. of 21 variables:
## $ Row.ID : int 1 2 3 4 5 6 7 8 9 10 ...
## $ Order.ID : chr "CA-2016-152156" "CA-2016-152156" "CA-2016-138688" "US-2015-108966" ...
## $ Order.Date : chr "11/8/2016" "11/8/2016" "6/12/2016" "10/11/2015" ...
## $ Ship.Date : chr "11/11/2016" "11/11/2016" "6/16/2016" "10/18/2015" ...
## $ Ship.Mode : chr "Second Class" "Second Class" "Second Class" "Standard Class" ...
## $ Customer.ID : chr "CG-12520" "CG-12520" "DV-13045" "SO-20335" ...
## $ Customer.Name: chr "Claire Gute" "Claire Gute" "Darrin Van Huff" "Sean O'Donnell" ...
## $ Segment : chr "Consumer" "Consumer" "Corporate" "Consumer" ...
## $ Country : chr "United States" "United States" "United States" "United States" ...
## $ City : chr "Henderson" "Henderson" "Los Angeles" "Fort Lauderdale" ...
## $ State : chr "Kentucky" "Kentucky" "California" "Florida" ...
## $ Postal.Code : int 42420 42420 90036 33311 33311 90032 90032 90032 90032 90032 ...
## $ Region : chr "South" "South" "West" "South" ...
## $ Product.ID : chr "FUR-BO-10001798" "FUR-CH-10000454" "OFF-LA-10000240" "FUR-TA-10000577" ...
## $ Category : chr "Furniture" "Furniture" "Office Supplies" "Furniture" ...
## $ Sub.Category : chr "Bookcases" "Chairs" "Labels" "Tables" ...
## $ Product.Name : chr "Bush Somerset Collection Bookcase" "Hon Deluxe Fabric Upholstered Stacking Chairs, Rounded Back" "Self-Adhesive Address Labels for Typewriters by Universal" "Bretford CR4500 Series Slim Rectangular Table" ...
## $ Sales : num 262 731.9 14.6 957.6 22.4 ...
## $ Quantity : int 2 3 2 5 2 7 4 6 3 5 ...
## $ Discount : num 0 0 0 0.45 0.2 0 0 0.2 0.2 0 ...
## $ Profit : num 41.91 219.58 6.87 -383.03 2.52 ...
Se observa que los atributos de fecha no contienen dicho formato y se procede a cambiar
# Convertir las columnas Order.Date y Ship.Date al formato de fecha MM/DD/YYYY
df$Order.Date <- as.Date(df$Order.Date, format = "%m/%d/%Y")
df$Ship.Date <- as.Date(df$Ship.Date, format = "%m/%d/%Y")
Verificar si existe faltante de datos
md.pattern(df)
## Row.ID Order.ID Order.Date Ship.Date Ship.Mode Customer.ID Customer.Name
## 9988 1 1 1 1 1 1 1
## 6 1 1 1 1 1 1 1
## 0 0 0 0 0 0 0
## Segment Country City State Postal.Code Region Product.ID Category
## 9988 1 1 1 1 1 1 1 1
## 6 1 1 1 1 1 1 1 1
## 0 0 0 0 0 0 0 0
## Sub.Category Product.Name Sales Quantity Discount Profit
## 9988 1 1 1 1 1 1 0
## 6 1 1 0 0 0 0 4
## 0 0 6 6 6 6 24
Se observa que 6 registros contienen los atributos de Sales, Quantity, Discount y Profit como nulos (sin valores), debido a que no representan una gran cantidad se decide eliminar estos registros.
# Eliminar registros con Sales vacío
df <- subset(df, !is.na(Sales))
print(dim(df))
## [1] 9988 21
Se extrae el mes y Año del Order Date y se agrupa la venta y el ingreso por año y mes
# Extraer el año y el mes en nuevas columnas
df$YYYY_Order <- year(df$Order.Date)
df$MM_Order <- month(df$Order.Date)
# Agrupar por YYYY_Order y MM_Order y calcular la suma de Sales y Profit
df_agrupado <- df %>%
group_by(YYYY_Order, MM_Order) %>%
summarise(Sales_Total = sum(Sales),
Profit_Total = sum(Profit))
## `summarise()` has grouped output by 'YYYY_Order'. You can override using the
## `.groups` argument.
# Verificar los resultados
kable(head(df_agrupado, n = 10))
| YYYY_Order | MM_Order | Sales_Total | Profit_Total |
|---|---|---|---|
| 2014 | 1 | 14236.895 | 2450.1907 |
| 2014 | 2 | 4519.892 | 862.3084 |
| 2014 | 3 | 55691.009 | 498.7299 |
| 2014 | 4 | 28178.561 | 3466.9382 |
| 2014 | 5 | 23648.287 | 2738.7096 |
| 2014 | 6 | 34595.128 | 4976.5244 |
| 2014 | 7 | 33946.393 | -841.4826 |
| 2014 | 8 | 27909.468 | 5318.1050 |
| 2014 | 9 | 81777.351 | 8328.0994 |
| 2014 | 10 | 31453.393 | 3448.2573 |
Se estima modelo de RLM para la Venta (RLM SALES):
Para estimar el modelo de RLM usamos como variable dependiente la venta y como variables independientes el año y el mes en el que se genera la orden.
modelo <- lm(Sales_Total ~ YYYY_Order + MM_Order, data = df_agrupado)
residuos <- resid(modelo)
summary(modelo)
##
## Call:
## lm(formula = Sales_Total ~ YYYY_Order + MM_Order, data = df_agrupado)
##
## Residuals:
## Min 1Q Median 3Q Max
## -31058 -8823 -1817 8408 37256
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -1.484e+07 4.044e+06 -3.669 0.000642 ***
## YYYY_Order 7.370e+03 2.007e+03 3.673 0.000635 ***
## MM_Order 5.237e+03 6.499e+02 8.058 2.84e-10 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 15540 on 45 degrees of freedom
## Multiple R-squared: 0.6354, Adjusted R-squared: 0.6192
## F-statistic: 39.21 on 2 and 45 DF, p-value: 1.383e-10
# Escribir el dataframe en un archivo Excel
#write.xlsx(df, "archivo.xlsx", row.names = FALSE)
Del modelo de RLM de Venta podemos sacar las siguientes conclusiones:
Las variables independientes son significativas para el modelo ya que presentan un valor P menor que 0.05, esta prueba nos indica que los coeficientes de dichas variables son estadísticamente diferente de cero.
Es importante destacar que el modelo exhibe un coeficiente de determinación ajustado (R² ajustado) de 0.6192, lo cual es un indicador positivo. Este valor sugiere que el modelo tiene un buen rendimiento al explicar una gran parte de la variabilidad en la variable de respuesta (Y) y el modelo no se comporta como una herramienta aleatoria.
Evaluación de supuestos del Modelo de RLM SALES
Homocedasticidad
#Homocedasticidad. Gráfico de residuos estandarizados vs. valores ajustados
plot(modelo, which = 3)
Se puede observar la aleatoriedad de los residuales lo cual es bueno para el modelo ya que no se esta violando el principio de Homocedasticidad, en otras palabras la varianza de los errores no es constante a lo largo de las observaciones.
Errores aleatorios con distribución normal
#Normalidad. Gráfico de cuantiles de los residuos vs. cuantiles teóricos normales
qqnorm(residuos)
shapiro.test(residuos)
##
## Shapiro-Wilk normality test
##
## data: residuos
## W = 0.97633, p-value = 0.4365
Como regla general, si el valor p es mayor que un nivel de significancia dado (por ejemplo, 0.05), no rechazaríamos la hipótesis nula. En este caso el valor p (0.4365) es mayor que 0.05, no hay suficiente evidencia para rechazar la hipótesis nula y podríamos asumir que los residuos siguen una distribución normal.
Los errores tienen media cero
# Prueba estadística de la media de los residuos
print(mean(residuos))
## [1] 1.39902e-13
Al calcular la media de los residuales encontramos que el valor es muy cercano a cero, En este caso, la diferencia entre la media de los residuos y cero es tan pequeña que se puede considerar prácticamente cero en términos prácticos.
Los errores son mutuamente independientes
# Gráfico de residuos vs. orden de observación
plot(residuos, type = "o", ylab = "Residuos", xlab = "Orden de Observación")
abline(h = 0, col = "red", lty = 2)
# Calcula el estadístico Durbin-Watson
dwtest(modelo)
##
## Durbin-Watson test
##
## data: modelo
## DW = 2.3935, p-value = 0.8711
## alternative hypothesis: true autocorrelation is greater than 0
Al examinar tanto el gráfico de los residuos como los resultados de la prueba Durbin-Watson, se llega a la conclusión de que los errores en el modelo son mutuamente independientes. En particular, el valor del estadístico Durbin-Watson (DW) es 2.3935, lo cual está cercano a 2. Un valor de DW cercano a 2 generalmente sugiere que los residuos son independientes entre sí. Además, dado que el p-valor asociado con la prueba es mayor que 0.05, no se encuentra evidencia significativa de autocorrelación en los residuos.
El modelo de RLM SALES ha superado la evaluación de supuestos y se considera óptimo para el pronóstico. A continuación, se presenta el pronostico de la venta para el siguiente año (2018).
# Crear un dataframe para almacenar los resultados de los pronósticos
pronosticos <- data.frame(YYYY_Order = rep(2018, 12), MM_Order = 1:12)
# Hacer pronósticos para cada mes del 2018
pronosticos$Sales_Pronosticadas <- predict(modelo, newdata = pronosticos)
# Mostrar los pronósticos
print(pronosticos)
## YYYY_Order MM_Order Sales_Pronosticadas
## 1 2018 1 37439.15
## 2 2018 2 42676.06
## 3 2018 3 47912.97
## 4 2018 4 53149.89
## 5 2018 5 58386.80
## 6 2018 6 63623.71
## 7 2018 7 68860.62
## 8 2018 8 74097.54
## 9 2018 9 79334.45
## 10 2018 10 84571.36
## 11 2018 11 89808.27
## 12 2018 12 95045.19
# Escribir los pronósticos en un archivo Excel
write.xlsx(pronosticos, "pronosticos.xlsx", row.names = FALSE)
## Warning: Please use 'rowNames' instead of 'row.names'