Integrantes del grupo: Edwin Rafael Santos Vidal, Eduardo Osvaldo Villablanca Obreque y Brayan Baddy Martínez Leguizamón.
Objetivo. Analizar el comportamiento de compra en línea de 500 clientes de una pequeña tienda de retail en EE. UU. y explicar su gasto anual (YearlyAmountSpent) a partir de su interacción con la tienda.
Carga (pregunta 2). El archivo se leyó con read.csv() y se guardó en el objeto ecommerce (500 filas y 8 columnas, sin valores faltantes: 0 NA). Para evitar problemas al llamar las columnas se eliminaron espacios, puntos y dos puntos de los nombres con gsub("[^A-Za-z0-9]", "", names(ecommerce)). La tabla de la derecha muestra la equivalencia.
Variables.
Email, Address y Avatar (color del avatar). Se excluyen del análisis, como pide el enunciado.Cómo leer este reporte. Cada pestaña de la barra superior responde una pregunta del enunciado (P3 a P8). Los gráficos son interactivos: al pasar el cursor se ven los valores, y se puede hacer zoom o aislar series desde la leyenda. Las tablas se desplazan horizontal y verticalmente, y pueden copiarse o descargarse en CSV. El código completo está disponible en el botón Source Code.
Localización: media, mediana, cuartiles, mínimo y máximo. Dispersión: desviación estándar, varianza, rango, rango intercuartílico (RIC) y coeficiente de variación (CV). Forma: coeficiente de asimetría de Fisher y curtosis en exceso (0 = distribución normal). La última columna cuenta los valores fuera de Q1 − 1,5·RIC y Q3 + 1,5·RIC.
Localización. En las cinco variables la media y la mediana prácticamente coinciden (por ejemplo, gasto anual: media 499,31 USD y mediana 498,89 USD). Eso ya adelanta distribuciones centradas y sin colas marcadas. El cliente típico pasa unos 33,1 minutos por sesión en tienda, 12,1 minutos en la app y 37,1 minutos en el sitio web, con 3,5 años de antigüedad. El gasto anual va de 256,67 a 765,52 USD, y el 50 % central de los clientes gasta entre 445,04 y 549,31 USD.
Dispersión. Las cuatro variables de comportamiento tienen una desviación estándar cercana a 1 (entre 0,993 y 1,010), así que un minuto o un año adicional equivalen aproximadamente a una desviación estándar. En términos relativos, la duración de la sesión (CV 3,0 %) y el tiempo en el sitio web (CV 2,7 %) son muy homogéneos entre clientes. La antigüedad es la más heterogénea (CV 28,3 %): conviven clientes recién llegados (0,27 años) con otros de casi 7 años. El gasto anual tiene una variabilidad moderada (DE 79,31 USD, CV 15,9 %).
Forma. Todas las asimetrías están entre -0,106 y 0,035, es decir, prácticamente simétricas. Las curtosis en exceso son cercanas a cero: TimeonWebsite es levemente platicúrtica y LengthofMembership y YearlyAmountSpent levemente leptocúrticas (colas algo más pesadas que la normal), lo que coincide con que tengan más valores atípicos (12 y 9). En conjunto, las variables se comportan de forma muy parecida a una distribución normal, lo que favorece el uso de la correlación de Pearson y de la regresión lineal en las preguntas 7 y 8.
Cada gráfico combina un boxplot (arriba) y un histograma (abajo), con la media marcada en amarillo. Las cinco variables muestran la forma de campana que anticipaba la tabla de la pregunta 3: un solo pico, centrado en la media y con colas parecidas a ambos lados. No aparecen grupos separados ni concentraciones anómalas de valores.
Los atípicos son pocos y están en ambos extremos, así que no desplazan la media. La antigüedad tiene la cola izquierda más visible (clientes con menos de un año de membresía), y el gasto anual tiene casos aislados bajo 300 USD y sobre 700 USD. Como son casos plausibles y no errores de registro, se conservan en el análisis.
Ninguna relación parece curva, por lo que un modelo lineal es razonable.
Construcción. AvgSessionGroup se creó con cut() sobre AvgSessionLength, con intervalos cerrados a la izquierda y abiertos a la derecha: [29, 32), [32, 33), [33, 34) y [34, 36,2). Todos los clientes quedan clasificados, ya que la duración observada va de 29,53 a 36,14 minutos. Los intervalos no tienen el mismo ancho: los extremos son más amplios (3 y 2,2 minutos) para reunir las colas de la distribución.
Como variable de agrupación. Es una variable cualitativa ordinal, por lo que se describe con frecuencias y no con media o desviación estándar. La categoría modal es [33,34) (179 clientes, 35,8 %) y la categoría mediana también es [33,34), porque ahí la frecuencia acumulada supera el 50 %. Los dos grupos centrales concentran el 69,2 % de los clientes, lo que refleja la forma de campana de la duración de sesión. El gráfico de barras es el adecuado para mostrar estas frecuencias.
Gasto anual por grupo. El gasto crece de forma sostenida con la duración de la sesión: la media pasa de 452,74 USD en el grupo más bajo a 545,39 USD en el más alto, una diferencia de 92,65 USD. Entre grupos consecutivos el aumento es de unos 25 a 36 USD por tramo. En cada grupo la media y la mediana son parecidas, así que el gasto es aproximadamente simétrico dentro de cada categoría. La desviación estándar es similar en los cuatro grupos (entre 71,11 y 82,07 USD), por lo que la variabilidad no depende del grupo. La mayor se da en [34, 36,2), donde hay algunos clientes con gasto muy alto. Aun así, la dispersión dentro de cada grupo (≈ 75 USD) es comparable a la diferencia entre grupos, señal de que la duración de sesión explica solo una parte del gasto.
Cada caja va de Q1 a Q3, la línea sólida es la mediana y la punteada, la media. Los puntos son los atípicos (fuera de 1,5·RIC). Todos los gráficos usan la misma escala vertical para poder compararlos.
[29, 32) · 69 clientes. Mediana 446,42 USD; caja (Q1–Q3) entre 416,36 y 494,64 USD (RIC = 78,28); bigotes de 319,93 a 591,78 USD; 4 atípicos (3 bajos y 1 altos). Es el grupo con menor gasto. La mediana está más cerca de Q1 que de Q3, lo que indica una leve asimetría positiva en el centro de la distribución: hay más clientes concentrados en gastos bajos y algunos que se alejan hacia arriba.
[32, 33) · 167 clientes. Mediana 486,84 USD; caja (Q1–Q3) entre 440,16 y 532,72 USD (RIC = 92,56); bigotes de 314,44 a 662,96 USD; 4 atípicos (2 bajos y 2 altos). La caja sube respecto del grupo anterior y es algo más ancha. La mediana queda casi al centro de la caja, así que la distribución es simétrica, con atípicos repartidos en ambos extremos.
[33, 34) · 179 clientes. Mediana 505,77 USD; caja (Q1–Q3) entre 460,59 y 558,29 USD (RIC = 97,70); bigotes de 357,59 a 689,79 USD; 2 atípicos (1 bajos y 1 altos). Es el grupo más numeroso y el de caja más ancha: el 50 % central de sus clientes cubre casi 100 USD. Tiene pocos atípicos y la media coincide con la mediana, por lo que es el grupo más cercano a una distribución simétrica.
[34, 36,2) · 85 clientes. Mediana 548,52 USD; caja (Q1–Q3) entre 506,13 y 584,11 USD (RIC = 77,97); bigotes de 402,17 a 700,92 USD; 7 atípicos (3 bajos y 4 altos). Es el grupo con mayor gasto: su mediana supera en unos 100 USD a la del primer grupo. La caja es compacta, pero concentra la mayor cantidad de atípicos, varios sobre 700 USD. Eso explica que tenga la desviación estándar más alta aunque su RIC sea de los menores.
En conjunto, las medianas suben de forma escalonada con la duración de sesión, pero las cajas de grupos contiguos se superponen bastante. La duración de sesión se asocia a un mayor gasto, aunque por sí sola no separa bien a los clientes.
Se usa el coeficiente de Pearson porque las variables son continuas, casi normales y con relaciones lineales (preguntas 3 y 4). La magnitud se clasifica con |r|: < 0,1 nula; 0,1–0,3 débil; 0,3–0,5 moderada; 0,5–0,7 moderada-fuerte; > 0,7 fuerte.
Entre las variables explicativas las correlaciones son muy bajas (|r| ≤ 0,082), así que no hay multicolinealidad. Cada variable aporta información propia y los coeficientes de la regresión de P8 serán estables.
Yv <- as.matrix(ecommerce$YearlyAmountSpent) # 500 x 1
X <- cbind(Intercepto = 1, as.matrix(ecommerce[, vars_x])) # 500 x 5
XtX <- t(X) %*% X
XtY <- t(X) %*% Yv
XtX_inv <- solve(XtX)
beta <- XtX_inv %*% XtY # (X'X)^-1 X'YGasto anual = -1.051,59 + 25,73·AvgSessionLength + 38,71·TimeonApp + 0,44·TimeonWebsite + 61,58·LengthofMembership
R² = 0,9843 · R² ajustado = 0,9842 · error estándar residual = 9,97 USD · n = 500
Los β se obtuvieron solo con operaciones matriciales (t(), %*%, solve()). Como control, se compararon con lm(): la diferencia máxima es 7.7e-10, es decir, cero a precisión numérica.
Cada coeficiente mide el cambio esperado en el gasto anual cuando su variable aumenta en una unidad y las demás se mantienen constantes.
Como las cuatro variables explicativas tienen desviación estándar ≈ 1 (P3), los coeficientes también se pueden leer como el efecto de una desviación estándar, y por eso son comparables entre sí: antigüedad > app > sesión > web. Además, como las variables explicativas casi no se correlacionan entre sí (P7), cada β es muy parecido a la pendiente simple de P4.
El modelo explica el 98,4 % de la variabilidad del gasto y su error típico (≈ 10 USD) es pequeño frente a la desviación estándar del gasto (79 USD). Para el negocio, la lectura es que el gasto depende sobre todo de retener clientes en el tiempo y de la experiencia en la app. El sitio web no está generando gasto, así que conviene revisarlo o dar prioridad a la inversión en la app y en programas de fidelización.
---
title: "Proyecto Final · Minería de datos con R"
author: "Edwin Santos · Eduardo Villablanca · Brayan Martínez"
date: "Septiembre 2026"
output:
flexdashboard::flex_dashboard:
orientation: columns
vertical_layout: fill
theme: cosmo
source_code: embed
navbar:
- { title: "Magíster en Analítica para los Negocios · Pontificia Universidad Católica de Chile", align: right }
---
```{r setup, include=FALSE}
# ============================================================
# Proyecto Final - Mineria de datos con R
# Magister en Analitica para los Negocios - Pontificia Universidad Catolica de Chile
# Trabajo grupal - Integrantes:
# Edwin Rafael Santos Vidal
# Eduardo Osvaldo Villablanca Obreque
# Brayan Baddy Martinez Leguizamon
# ============================================================
knitr::opts_chunk$set(echo = FALSE, message = FALSE, warning = FALSE)
library(flexdashboard)
library(plotly)
library(DT)
library(moments) # asimetria y curtosis
# Paleta institucional UC: azul UC, azul oscuro y amarillo
uc_azul <- "#0176DE"
uc_oscuro <- "#173F8A"
uc_navy <- "#03122E"
uc_amar <- "#FEC60D"
col_main <- uc_azul
col_acc <- "#F7A800" # amarillo UC algo mas oscuro para que se lea sobre blanco
col_grp <- c("#B3D7F7", "#4FA0EA", uc_azul, uc_oscuro) # grupos ordenados
# Tipografia y colores UC en todos los graficos plotly
plot_ly <- function(...) plotly::plot_ly(...) %>%
plotly::layout(font = list(family = "Roboto, Arial, sans-serif", color = uc_navy))
# Formato de numeros para el texto (coma decimal, punto de miles)
f <- function(x, d = 2) formatC(x, format = "f", digits = d, big.mark = ".", decimal.mark = ",")
# Tabla DT estandar: se ve completa, con desplazamiento horizontal/vertical
tabla <- function(df, digits = 3, scrollY = "100%", ...) {
# solo se redondean columnas con decimales (los conteos quedan enteros)
num <- names(df)[sapply(df, function(x) is.numeric(x) && any(x != round(x)))]
dt <- datatable(df, rownames = FALSE, extensions = "Buttons",
options = list(dom = "Bt", buttons = c("copy", "csv"),
paging = FALSE, scrollX = TRUE,
scrollY = scrollY, scrollCollapse = TRUE,
ordering = FALSE, ...))
if (length(num)) dt <- formatRound(dt, num, digits = digits,
mark = ".", dec.mark = ",")
dt
}
```
```{css}
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&family=Roboto+Slab:wght@600&display=swap');
body, .chart-title, .navbar, .dataTables_wrapper { font-family: 'Roboto', Arial, sans-serif; color: #03122E; }
body { background-color: #F4F6FA; }
/* Barra superior: azul UC con filete amarillo */
.navbar-inverse { background-color: #0176DE; border-color: #0176DE; border-bottom: 4px solid #FEC60D; }
.navbar-inverse .navbar-brand, .navbar-inverse .navbar-author { color: #FFFFFF; font-weight: 700; }
.navbar-inverse .navbar-brand { font-family: 'Roboto Slab', Georgia, serif; }
.navbar-inverse .navbar-nav > li > a { color: #FFFFFF; }
.navbar-inverse .navbar-nav > .active > a,
.navbar-inverse .navbar-nav > .active > a:hover,
.navbar-inverse .navbar-nav > .active > a:focus,
.navbar-inverse .navbar-nav > li > a:hover { background-color: #173F8A; color: #FEC60D; }
/* Tarjetas */
.chart-wrapper { border: 1px solid #D5DDEA; border-radius: 4px; box-shadow: 0 1px 2px rgba(3,18,46,.06); }
.chart-title { color: #173F8A; font-weight: 700; border-bottom: 3px solid #0176DE; }
.nav-tabs-custom > .nav-tabs > li.active { border-top-color: #FEC60D; }
.nav-tabs-custom > .nav-tabs > li.active > a { color: #173F8A; font-weight: 500; }
a, code { color: #0176DE; }
strong { color: #173F8A; }
table.dataTable thead th { background-color: #173F8A; color: #FFFFFF; }
.dt-button { background: #0176DE !important; color: #FFFFFF !important; border: none !important; }
.chart-stage { overflow-y: auto; }
.chart-stage p, .chart-stage li { font-size: 14px; line-height: 1.5; }
.chart-stage blockquote { font-size: 12.5px; border-left: 4px solid #FEC60D; background: #FFFBEA; margin: 8px 0; }
```
```{r datos}
# ---------- Pregunta 2: carga y renombrado ----------
# Se acepta el archivo como .csv (nombre del enunciado) o .txt (nombre de descarga)
archivo <- c("Ecommerce.csv", "Ecommerce.txt", "files/Ecommerce.txt")
archivo <- archivo[file.exists(archivo)][1]
ecommerce <- read.csv(archivo, check.names = FALSE, stringsAsFactors = FALSE,
encoding = "UTF-8")
nombres_originales <- names(ecommerce)
# Se eliminan espacios, puntos y cualquier caracter que no sea letra o numero
names(ecommerce) <- gsub("[^A-Za-z0-9]", "", names(ecommerce))
vars_cuant <- c("AvgSessionLength", "TimeonApp", "TimeonWebsite",
"LengthofMembership", "YearlyAmountSpent")
etiqueta <- c(AvgSessionLength = "Duración media de sesión (min)",
TimeonApp = "Tiempo en la app (min)",
TimeonWebsite = "Tiempo en el sitio web (min)",
LengthofMembership = "Antigüedad como miembro (años)",
YearlyAmountSpent = "Gasto anual (USD)")
Y <- ecommerce$YearlyAmountSpent
```
Inicio {data-icon="fa-house"}
=====================================
Column {data-width=380}
-------------------------------------
### Contexto y preparación de los datos
**Integrantes del grupo:** Edwin Rafael Santos Vidal, Eduardo Osvaldo Villablanca Obreque y Brayan Baddy Martínez Leguizamón.
**Objetivo.** Analizar el comportamiento de compra en línea de `r nrow(ecommerce)` clientes de una pequeña tienda de retail en EE. UU. y explicar su gasto anual (`YearlyAmountSpent`) a partir de su interacción con la tienda.
**Carga (pregunta 2).** El archivo se leyó con `read.csv()` y se guardó en el objeto `ecommerce` (`r nrow(ecommerce)` filas y `r ncol(ecommerce)` columnas, sin valores faltantes: `r sum(is.na(ecommerce))` NA). Para evitar problemas al llamar las columnas se eliminaron espacios, puntos y dos puntos de los nombres con `gsub("[^A-Za-z0-9]", "", names(ecommerce))`. La tabla de la derecha muestra la equivalencia.
**Variables.**
- Cualitativas identificadoras: `Email`, `Address` y `Avatar` (color del avatar). Se excluyen del análisis, como pide el enunciado.
- Cuantitativas continuas: duración media de la sesión en tienda, tiempo en la app, tiempo en el sitio web (en minutos), antigüedad de la membresía (en años) y gasto anual (en USD).
**Cómo leer este reporte.** Cada pestaña de la barra superior responde una pregunta del enunciado (P3 a P8). Los gráficos son interactivos: al pasar el cursor se ven los valores, y se puede hacer zoom o aislar series desde la leyenda. Las tablas se desplazan horizontal y verticalmente, y pueden copiarse o descargarse en CSV. El código completo está disponible en el botón **Source Code**.
Column {data-width=620 .tabset}
-------------------------------------
### Renombrado de variables
```{r}
tabla(data.frame(`Nombre original` = nombres_originales,
`Nombre en R` = names(ecommerce),
Tipo = c("Cualitativa", "Cualitativa", "Cualitativa",
rep("Cuantitativa", 5)),
check.names = FALSE))
```
### Base de datos completa (`r nrow(ecommerce)` filas)
```{r}
datatable(ecommerce, rownames = FALSE, extensions = "Buttons",
options = list(dom = "Bfrtip", buttons = c("copy", "csv"),
paging = FALSE, scrollX = TRUE, scrollY = "60vh")) %>%
formatRound(vars_cuant, digits = 2, mark = ".", dec.mark = ",")
```
P3 · Estadística descriptiva {data-icon="fa-table"}
=====================================
```{r p3}
# ---------- Pregunta 3: estadistica descriptiva ----------
desc <- do.call(rbind, lapply(vars_cuant, function(v) {
x <- ecommerce[[v]]
data.frame(Variable = v,
n = length(x),
# Localizacion
Media = mean(x), Mediana = median(x),
Q1 = unname(quantile(x, 0.25)), Q3 = unname(quantile(x, 0.75)),
Mínimo = min(x), Máximo = max(x),
# Dispersion
`Desv. estándar` = sd(x), Varianza = var(x),
Rango = diff(range(x)), RIC = IQR(x),
`CV (%)` = 100 * sd(x) / mean(x),
# Forma
Asimetría = skewness(x),
`Curtosis (exceso)` = kurtosis(x) - 3,
`Atípicos (1,5·RIC)` = length(boxplot.stats(x)$out),
check.names = FALSE)
}))
d <- setNames(lapply(vars_cuant, function(v) desc[desc$Variable == v, ]), vars_cuant)
```
Column {data-width=620}
-------------------------------------
### Medidas de localización, dispersión y forma de las variables cuantitativas
```{r}
tabla(desc, digits = 3)
```
> **Localización:** media, mediana, cuartiles, mínimo y máximo. **Dispersión:** desviación estándar, varianza, rango, rango intercuartílico (RIC) y coeficiente de variación (CV). **Forma:** coeficiente de asimetría de Fisher y curtosis en exceso (0 = distribución normal). La última columna cuenta los valores fuera de Q1 − 1,5·RIC y Q3 + 1,5·RIC.
Column {data-width=380}
-------------------------------------
### Interpretación
**Localización.** En las cinco variables la media y la mediana prácticamente coinciden (por ejemplo, gasto anual: media `r f(d$YearlyAmountSpent$Media)` USD y mediana `r f(d$YearlyAmountSpent$Mediana)` USD). Eso ya adelanta distribuciones centradas y sin colas marcadas. El cliente típico pasa unos `r f(d$AvgSessionLength$Media, 1)` minutos por sesión en tienda, `r f(d$TimeonApp$Media, 1)` minutos en la app y `r f(d$TimeonWebsite$Media, 1)` minutos en el sitio web, con `r f(d$LengthofMembership$Media, 1)` años de antigüedad. El gasto anual va de `r f(d$YearlyAmountSpent$Mínimo)` a `r f(d$YearlyAmountSpent$Máximo)` USD, y el 50 % central de los clientes gasta entre `r f(d$YearlyAmountSpent$Q1)` y `r f(d$YearlyAmountSpent$Q3)` USD.
**Dispersión.** Las cuatro variables de comportamiento tienen una desviación estándar cercana a 1 (entre `r f(min(desc$"Desv. estándar"[1:4]), 3)` y `r f(max(desc$"Desv. estándar"[1:4]), 3)`), así que un minuto o un año adicional equivalen aproximadamente a una desviación estándar. En términos relativos, la duración de la sesión (CV `r f(d$AvgSessionLength$"CV (%)", 1)` %) y el tiempo en el sitio web (CV `r f(d$TimeonWebsite$"CV (%)", 1)` %) son muy homogéneos entre clientes. La antigüedad es la más heterogénea (CV `r f(d$LengthofMembership$"CV (%)", 1)` %): conviven clientes recién llegados (`r f(d$LengthofMembership$Mínimo)` años) con otros de casi 7 años. El gasto anual tiene una variabilidad moderada (DE `r f(d$YearlyAmountSpent$"Desv. estándar")` USD, CV `r f(d$YearlyAmountSpent$"CV (%)", 1)` %).
**Forma.** Todas las asimetrías están entre `r f(min(desc$Asimetría), 3)` y `r f(max(desc$Asimetría), 3)`, es decir, prácticamente simétricas. Las curtosis en exceso son cercanas a cero: `TimeonWebsite` es levemente platicúrtica y `LengthofMembership` y `YearlyAmountSpent` levemente leptocúrticas (colas algo más pesadas que la normal), lo que coincide con que tengan más valores atípicos (`r d$LengthofMembership$"Atípicos (1,5·RIC)"` y `r d$YearlyAmountSpent$"Atípicos (1,5·RIC)"`). En conjunto, las variables se comportan de forma muy parecida a una distribución normal, lo que favorece el uso de la correlación de Pearson y de la regresión lineal en las preguntas 7 y 8.
P4 · Gráficos {data-icon="fa-chart-column"}
=====================================
```{r p4}
# ---------- Pregunta 4: graficos univariados y bivariados ----------
# Univariado: boxplot horizontal + histograma, compartiendo el eje X
graf_univ <- function(v) {
x <- ecommerce[[v]]
b <- plot_ly(x = x, type = "box", name = "", marker = list(color = col_main),
line = list(color = col_main), fillcolor = "rgba(1,118,222,0.25)",
hoverinfo = "x", boxpoints = "outliers")
h <- plot_ly(x = x, type = "histogram", nbinsx = 30,
marker = list(color = col_main, line = list(color = "white", width = 1)),
hovertemplate = "Intervalo: %{x}<br>Clientes: %{y}<extra></extra>") %>%
add_segments(x = mean(x), xend = mean(x), y = 0, yend = 1.05 * max(hist(x, breaks = 30, plot = FALSE)$counts), inherit = FALSE,
line = list(color = col_acc, dash = "dash"),
name = "Media", hoverinfo = "text",
text = paste("Media:", f(mean(x))))
subplot(b, h, nrows = 2, heights = c(0.2, 0.8), shareX = TRUE) %>%
layout(showlegend = FALSE, xaxis = list(title = etiqueta[[v]]),
yaxis2 = list(title = "Número de clientes"),
yaxis = list(showticklabels = FALSE))
}
# Bivariado: dispersion de YearlyAmountSpent vs X con recta MCO simple
graf_biv <- function(v) {
x <- ecommerce[[v]]
b1 <- cov(x, Y) / var(x); b0 <- mean(Y) - b1 * mean(x)
xs <- range(x)
plot_ly(ecommerce, x = x, y = Y, type = "scatter", mode = "markers",
marker = list(color = col_main, opacity = 0.55, size = 6),
text = ~Email, name = "Clientes",
hovertemplate = paste0("%{text}<br>", etiqueta[[v]], ": %{x:.2f}",
"<br>Gasto anual: %{y:.2f} USD<extra></extra>")) %>%
add_lines(x = xs, y = b0 + b1 * xs, inherit = FALSE, name = "Tendencia lineal",
line = list(color = col_acc, width = 3), hoverinfo = "skip") %>%
layout(xaxis = list(title = etiqueta[[v]]),
yaxis = list(title = "Gasto anual (USD)"),
legend = list(orientation = "h", y = 1.08),
annotations = list(x = 0.02, y = 0.97, xref = "paper", yref = "paper",
showarrow = FALSE, align = "left",
text = paste0("r = ", f(cor(x, Y), 3))))
}
r <- sapply(vars_cuant[1:4], function(v) cor(ecommerce[[v]], Y))
```
Column {data-width=350 .tabset}
-------------------------------------
### Duración de sesión
```{r}
graf_univ("AvgSessionLength")
```
### Tiempo en app
```{r}
graf_univ("TimeonApp")
```
### Tiempo en web
```{r}
graf_univ("TimeonWebsite")
```
### Antigüedad
```{r}
graf_univ("LengthofMembership")
```
### Gasto anual
```{r}
graf_univ("YearlyAmountSpent")
```
Column {data-width=350 .tabset}
-------------------------------------
### Gasto vs. duración de sesión
```{r}
graf_biv("AvgSessionLength")
```
### Gasto vs. tiempo en app
```{r}
graf_biv("TimeonApp")
```
### Gasto vs. tiempo en web
```{r}
graf_biv("TimeonWebsite")
```
### Gasto vs. antigüedad
```{r}
graf_biv("LengthofMembership")
```
Column {data-width=300}
-------------------------------------
### Interpretación de los gráficos univariados
Cada gráfico combina un boxplot (arriba) y un histograma (abajo), con la media marcada en amarillo. Las cinco variables muestran la forma de campana que anticipaba la tabla de la pregunta 3: un solo pico, centrado en la media y con colas parecidas a ambos lados. No aparecen grupos separados ni concentraciones anómalas de valores.
Los atípicos son pocos y están en ambos extremos, así que no desplazan la media. La antigüedad tiene la cola izquierda más visible (clientes con menos de un año de membresía), y el gasto anual tiene casos aislados bajo 300 USD y sobre 700 USD. Como son casos plausibles y no errores de registro, se conservan en el análisis.
### Interpretación de los gráficos bivariados
- **Antigüedad** (r = `r f(r["LengthofMembership"], 3)`): la relación más fuerte. La nube es estrecha y creciente; cuanto más tiempo lleva el cliente, más gasta.
- **Tiempo en la app** (r = `r f(r["TimeonApp"], 3)`): relación positiva y moderada. Los clientes que usan más la app tienden a gastar más, con más dispersión alrededor de la tendencia.
- **Duración de la sesión** (r = `r f(r["AvgSessionLength"], 3)`): relación positiva débil a moderada. La tendencia sube, pero la nube es ancha.
- **Tiempo en el sitio web** (r = `r f(r["TimeonWebsite"], 3)`): no hay relación. La recta es plana y la nube no muestra ningún patrón.
Ninguna relación parece curva, por lo que un modelo lineal es razonable.
P5 · AvgSessionGroup {data-icon="fa-layer-group"}
=====================================
```{r p5}
# ---------- Pregunta 5: variable de agrupacion ----------
cortes <- c(29, 32, 33, 34, 36.2)
ecommerce$AvgSessionGroup <- cut(ecommerce$AvgSessionLength, breaks = cortes,
right = FALSE, ordered_result = TRUE)
niv <- levels(ecommerce$AvgSessionGroup)
stopifnot(!anyNA(ecommerce$AvgSessionGroup)) # todos los clientes quedan clasificados
fa <- as.vector(table(ecommerce$AvgSessionGroup))
frec <- data.frame(Grupo = niv,
`Frec. absoluta` = fa,
`Frec. relativa (%)` = 100 * fa / sum(fa),
`Frec. abs. acumulada` = cumsum(fa),
`Frec. rel. acumulada (%)` = 100 * cumsum(fa) / sum(fa),
check.names = FALSE)
moda_grp <- niv[which.max(fa)]
mediana_grp <- niv[which(cumsum(fa) / sum(fa) >= 0.5)[1]]
est_grp <- do.call(rbind, lapply(niv, function(g) {
y <- Y[ecommerce$AvgSessionGroup == g]
data.frame(Grupo = g, n = length(y), Media = mean(y), Mediana = median(y),
`Desv. estándar` = sd(y), `CV (%)` = 100 * sd(y) / mean(y),
Mínimo = min(y), Máximo = max(y), check.names = FALSE)
}))
e5 <- lapply(niv, function(g) est_grp[est_grp$Grupo == g, ])
```
Column {data-width=560}
-------------------------------------
### Distribución de frecuencias de AvgSessionGroup
```{r}
tabla(frec, digits = 1)
```
### Estadísticas de YearlyAmountSpent por grupo
```{r}
tabla(est_grp, digits = 2)
```
Column {data-width=440 .tabset}
-------------------------------------
### Frecuencia por grupo
```{r}
plot_ly(frec, x = ~Grupo, y = ~`Frec. absoluta`, type = "bar",
marker = list(color = col_grp),
text = ~paste0(`Frec. absoluta`, " (", f(`Frec. relativa (%)`, 1), " %)"),
textposition = "outside",
hovertemplate = "Grupo %{x}<br>Clientes: %{y}<extra></extra>") %>%
layout(xaxis = list(title = "Duración media de sesión (min)",
categoryorder = "array", categoryarray = niv),
yaxis = list(title = "Número de clientes",
range = c(0, max(fa) * 1.15)))
```
### Gasto anual por grupo
```{r}
plot_ly(est_grp, x = ~Grupo, y = ~Media, type = "bar", name = "Media",
marker = list(color = col_grp),
error_y = list(type = "data", array = ~`Desv. estándar`, color = "#555"),
hovertemplate = "Grupo %{x}<br>Media: %{y:.2f} USD<extra></extra>") %>%
add_markers(y = ~Mediana, name = "Mediana",
marker = list(color = col_acc, size = 11, symbol = "diamond"),
hovertemplate = "Grupo %{x}<br>Mediana: %{y:.2f} USD<extra></extra>") %>%
layout(xaxis = list(title = "Duración media de sesión (min)",
categoryorder = "array", categoryarray = niv),
yaxis = list(title = "Gasto anual (USD)"),
legend = list(orientation = "h", y = 1.08))
```
### Interpretación
**Construcción.** `AvgSessionGroup` se creó con `cut()` sobre `AvgSessionLength`, con intervalos cerrados a la izquierda y abiertos a la derecha: [29, 32), [32, 33), [33, 34) y [34, 36,2). Todos los clientes quedan clasificados, ya que la duración observada va de `r f(min(ecommerce$AvgSessionLength))` a `r f(max(ecommerce$AvgSessionLength))` minutos. Los intervalos no tienen el mismo ancho: los extremos son más amplios (3 y 2,2 minutos) para reunir las colas de la distribución.
**Como variable de agrupación.** Es una variable cualitativa ordinal, por lo que se describe con frecuencias y no con media o desviación estándar. La categoría modal es `r moda_grp` (`r frec$"Frec. absoluta"[niv == moda_grp]` clientes, `r f(frec$"Frec. relativa (%)"[niv == moda_grp], 1)` %) y la categoría mediana también es `r mediana_grp`, porque ahí la frecuencia acumulada supera el 50 %. Los dos grupos centrales concentran el `r f(sum(frec$"Frec. relativa (%)"[2:3]), 1)` % de los clientes, lo que refleja la forma de campana de la duración de sesión. El gráfico de barras es el adecuado para mostrar estas frecuencias.
**Gasto anual por grupo.** El gasto crece de forma sostenida con la duración de la sesión: la media pasa de `r f(e5[[1]]$Media)` USD en el grupo más bajo a `r f(e5[[4]]$Media)` USD en el más alto, una diferencia de `r f(e5[[4]]$Media - e5[[1]]$Media)` USD. Entre grupos consecutivos el aumento es de unos 25 a 36 USD por tramo. En cada grupo la media y la mediana son parecidas, así que el gasto es aproximadamente simétrico dentro de cada categoría. La desviación estándar es similar en los cuatro grupos (entre `r f(min(est_grp$"Desv. estándar"))` y `r f(max(est_grp$"Desv. estándar"))` USD), por lo que la variabilidad no depende del grupo. La mayor se da en [34, 36,2), donde hay algunos clientes con gasto muy alto. Aun así, la dispersión dentro de cada grupo (≈ 75 USD) es comparable a la diferencia entre grupos, señal de que la duración de sesión explica solo una parte del gasto.
P6 · Boxplots {data-icon="fa-box"}
=====================================
```{r p6}
# ---------- Pregunta 6: boxplots de YearlyAmountSpent por grupo ----------
bx <- lapply(niv, function(g) {
y <- Y[ecommerce$AvgSessionGroup == g]
q <- quantile(y, c(0.25, 0.5, 0.75)); lim <- boxplot.stats(y)
list(y = y, q1 = q[[1]], me = q[[2]], q3 = q[[3]], ric = q[[3]] - q[[1]],
wmin = lim$stats[1], wmax = lim$stats[5], out = lim$out,
out_alto = sum(lim$out > q[[2]]), out_bajo = sum(lim$out < q[[2]]))
})
names(bx) <- niv
graf_box <- function(grupos) {
p <- plot_ly()
for (g in grupos) {
i <- match(g, niv)
p <- add_boxplot(p, y = bx[[g]]$y, name = g, boxmean = TRUE,
marker = list(color = col_grp[i]), line = list(color = uc_navy),
fillcolor = col_grp[i], boxpoints = "outliers")
}
layout(p, showlegend = FALSE,
xaxis = list(title = "AvgSessionGroup (min)"),
yaxis = list(title = "Gasto anual (USD)", range = c(240, 790)))
}
texto_box <- function(g) {
b <- bx[[g]]
paste0("Mediana ", f(b$me), " USD; caja (Q1–Q3) entre ", f(b$q1), " y ", f(b$q3),
" USD (RIC = ", f(b$ric), "); bigotes de ", f(b$wmin), " a ", f(b$wmax),
" USD; ", length(b$out), " atípicos (", b$out_bajo, " bajos y ",
b$out_alto, " altos).")
}
```
Column {data-width=600 .tabset}
-------------------------------------
### Comparación de los 4 grupos
```{r}
graf_box(niv)
```
### [29,32)
```{r}
graf_box(niv[1])
```
### [32,33)
```{r}
graf_box(niv[2])
```
### [33,34)
```{r}
graf_box(niv[3])
```
### [34,36.2)
```{r}
graf_box(niv[4])
```
Column {data-width=400}
-------------------------------------
### Interpretación de cada boxplot
Cada caja va de Q1 a Q3, la línea sólida es la mediana y la punteada, la media. Los puntos son los atípicos (fuera de 1,5·RIC). Todos los gráficos usan la misma escala vertical para poder compararlos.
**[29, 32) · `r length(bx[[1]]$y)` clientes.** `r texto_box(niv[1])` Es el grupo con menor gasto. La mediana está más cerca de Q1 que de Q3, lo que indica una leve asimetría positiva en el centro de la distribución: hay más clientes concentrados en gastos bajos y algunos que se alejan hacia arriba.
**[32, 33) · `r length(bx[[2]]$y)` clientes.** `r texto_box(niv[2])` La caja sube respecto del grupo anterior y es algo más ancha. La mediana queda casi al centro de la caja, así que la distribución es simétrica, con atípicos repartidos en ambos extremos.
**[33, 34) · `r length(bx[[3]]$y)` clientes.** `r texto_box(niv[3])` Es el grupo más numeroso y el de caja más ancha: el 50 % central de sus clientes cubre casi 100 USD. Tiene pocos atípicos y la media coincide con la mediana, por lo que es el grupo más cercano a una distribución simétrica.
**[34, 36,2) · `r length(bx[[4]]$y)` clientes.** `r texto_box(niv[4])` Es el grupo con mayor gasto: su mediana supera en unos 100 USD a la del primer grupo. La caja es compacta, pero concentra la mayor cantidad de atípicos, varios sobre 700 USD. Eso explica que tenga la desviación estándar más alta aunque su RIC sea de los menores.
**En conjunto,** las medianas suben de forma escalonada con la duración de sesión, pero las cajas de grupos contiguos se superponen bastante. La duración de sesión se asocia a un mayor gasto, aunque por sí sola no separa bien a los clientes.
P7 · Correlaciones {data-icon="fa-border-all"}
=====================================
```{r p7}
# ---------- Pregunta 7: matriz de correlaciones de Pearson ----------
R <- cor(ecommerce[, vars_cuant])
rY <- sort(R[1:4, "YearlyAmountSpent"], decreasing = TRUE)
fuerza <- function(x) cut(abs(x), c(0, 0.1, 0.3, 0.5, 0.7, 1),
labels = c("nula", "débil", "moderada",
"moderada-fuerte", "fuerte"),
include.lowest = TRUE)
```
Column {data-width=560 .tabset}
-------------------------------------
### Mapa de calor
```{r}
plot_ly(x = vars_cuant, y = vars_cuant, z = R, type = "heatmap",
colorscale = list(c(0, "#B2182B"), c(0.5, "#F7F7F7"), c(1, uc_oscuro)),
zmin = -1, zmax = 1,
hovertemplate = "%{y} vs %{x}<br>r = %{z:.3f}<extra></extra>") %>%
add_annotations(x = rep(vars_cuant, each = 5), y = rep(vars_cuant, 5),
text = f(as.vector(R), 3), showarrow = FALSE,
font = list(color = ifelse(abs(as.vector(R)) > 0.6, "white", "black"))) %>%
layout(xaxis = list(title = ""), yaxis = list(title = "", autorange = "reversed"))
```
### Matriz numérica
```{r}
tabla(data.frame(Variable = vars_cuant, R, check.names = FALSE), digits = 3)
```
Column {data-width=440}
-------------------------------------
### Correlación de cada variable con YearlyAmountSpent
```{r}
tabla(data.frame(Variable = names(rY), r = unname(rY), `r²` = unname(rY)^2,
Magnitud = as.character(fuerza(rY)), check.names = FALSE),
digits = 3)
```
### Interpretación
Se usa el coeficiente de Pearson porque las variables son continuas, casi normales y con relaciones lineales (preguntas 3 y 4). La magnitud se clasifica con |r|: < 0,1 nula; 0,1–0,3 débil; 0,3–0,5 moderada; 0,5–0,7 moderada-fuerte; > 0,7 fuerte.
- **LengthofMembership** (r = `r f(R["LengthofMembership","YearlyAmountSpent"], 3)`): correlación positiva **fuerte**. Por sí sola se asocia con el `r f(100*R["LengthofMembership","YearlyAmountSpent"]^2, 1)` % de la variabilidad del gasto (r²). Es la variable más relevante.
- **TimeonApp** (r = `r f(R["TimeonApp","YearlyAmountSpent"], 3)`): positiva y moderada, en el límite de moderada-fuerte (r² ≈ `r f(100*R["TimeonApp","YearlyAmountSpent"]^2, 0)` %).
- **AvgSessionLength** (r = `r f(R["AvgSessionLength","YearlyAmountSpent"], 3)`): positiva moderada (r² ≈ `r f(100*R["AvgSessionLength","YearlyAmountSpent"]^2, 0)` %), coherente con el aumento escalonado del gasto por grupo en P5 y P6.
- **TimeonWebsite** (r = `r f(R["TimeonWebsite","YearlyAmountSpent"], 3)`): correlación **nula**. El tiempo en el sitio web no se asocia con el gasto.
Entre las variables explicativas las correlaciones son muy bajas (|r| ≤ `r f(max(abs(R[1:4,1:4][upper.tri(R[1:4,1:4])])), 3)`), así que no hay multicolinealidad. Cada variable aporta información propia y los coeficientes de la regresión de P8 serán estables.
P8 · Regresión (MCO matricial) {data-icon="fa-calculator"}
=====================================
```{r p8}
# ---------- Pregunta 8: estimadores MCO con algebra matricial ----------
# Modelo: YearlyAmountSpent_i = b0 + b1*x1 + b2*x2 + b3*x3 + b4*x4 + e_i, i = 1..500
vars_x <- c("AvgSessionLength", "TimeonApp", "TimeonWebsite", "LengthofMembership")
Yv <- as.matrix(ecommerce$YearlyAmountSpent) # vector respuesta (500 x 1)
X <- cbind(Intercepto = 1, as.matrix(ecommerce[, vars_x])) # matriz de diseno (500 x 5)
XtX <- t(X) %*% X # X'X (5 x 5)
XtY <- t(X) %*% Yv # X'Y (5 x 1)
XtX_inv <- solve(XtX) # (X'X)^-1
beta <- XtX_inv %*% XtY # beta_hat = (X'X)^-1 X'Y
# Medidas de ajuste, tambien con matrices
n <- nrow(X); p <- ncol(X) - 1
Y_hat <- X %*% beta
e <- Yv - Y_hat
SCE <- as.numeric(t(e) %*% e)
SCT <- as.numeric(t(Yv - mean(Yv)) %*% (Yv - mean(Yv)))
R2 <- 1 - SCE / SCT
R2adj <- 1 - (1 - R2) * (n - 1) / (n - p - 1)
s2 <- SCE / (n - p - 1)
se <- sqrt(diag(s2 * XtX_inv))
t_val <- as.vector(beta) / se
p_val <- 2 * pt(abs(t_val), df = n - p - 1, lower.tail = FALSE)
coefs <- data.frame(Parámetro = paste0("β", 0:4),
Variable = colnames(X),
Estimación = as.vector(beta),
`Error est.` = se, t = t_val, `p-valor` = format.pval(p_val, digits = 3, eps = 1e-4),
check.names = FALSE)
b <- setNames(as.vector(beta), colnames(X))
# Control: el resultado coincide con lm() (solo verificacion, no se usa para estimar)
dif_lm <- max(abs(b - coef(lm(YearlyAmountSpent ~ ., data = ecommerce[, vars_cuant]))))
```
Column {data-width=560 .tabset}
-------------------------------------
### Estimadores β̂ = (X'X)⁻¹X'Y
```{r}
tabla(coefs, digits = 4)
```
### Coeficientes (gráfico)
```{r}
plot_ly(coefs[-1, ], x = ~Estimación, y = ~Variable, type = "bar", orientation = "h",
marker = list(color = c(col_grp[3], col_grp[4], "#BBBBBB", col_acc)),
error_x = list(type = "data", array = ~1.96 * `Error est.`),
hovertemplate = "%{y}<br>β = %{x:.3f} USD por unidad<extra></extra>") %>%
layout(xaxis = list(title = "Cambio en el gasto anual (USD) por unidad adicional"),
yaxis = list(title = "", categoryorder = "total ascending"))
```
### X (primeras 20 filas)
```{r}
tabla(data.frame(i = 1:20, Y = Yv[1:20], X[1:20, ], check.names = FALSE), digits = 4)
```
### X'X
```{r}
tabla(data.frame(` ` = colnames(X), XtX, check.names = FALSE), digits = 2)
```
### (X'X)⁻¹
```{r}
tabla(data.frame(` ` = colnames(X), XtX_inv, check.names = FALSE), digits = 6)
```
### X'Y
```{r}
tabla(data.frame(` ` = colnames(X), `X'Y` = as.vector(XtY), check.names = FALSE), digits = 2)
```
### Código del cálculo
```{r, echo=TRUE, eval=FALSE}
Yv <- as.matrix(ecommerce$YearlyAmountSpent) # 500 x 1
X <- cbind(Intercepto = 1, as.matrix(ecommerce[, vars_x])) # 500 x 5
XtX <- t(X) %*% X
XtY <- t(X) %*% Yv
XtX_inv <- solve(XtX)
beta <- XtX_inv %*% XtY # (X'X)^-1 X'Y
```
Column {data-width=440}
-------------------------------------
### Ecuación estimada y ajuste
**Gasto anual = `r f(b[1])` + `r f(b[2])`·AvgSessionLength + `r f(b[3])`·TimeonApp + `r f(b[4])`·TimeonWebsite + `r f(b[5])`·LengthofMembership**
R² = `r f(R2, 4)` · R² ajustado = `r f(R2adj, 4)` · error estándar residual = `r f(sqrt(s2))` USD · n = `r n`
Los β se obtuvieron solo con operaciones matriciales (`t()`, `%*%`, `solve()`). Como control, se compararon con `lm()`: la diferencia máxima es `r format(dif_lm, digits = 2)`, es decir, cero a precisión numérica.
### Interpretación
Cada coeficiente mide el cambio esperado en el gasto anual cuando su variable aumenta en una unidad y las demás se mantienen constantes.
- **β₄ LengthofMembership = `r f(b[5])`.** Cada año adicional de membresía se asocia con `r f(b[5])` USD más de gasto anual. Es el efecto más grande, en línea con la correlación fuerte (r = 0,81) de P7.
- **β₂ TimeonApp = `r f(b[3])`.** Cada minuto adicional en la app suma `r f(b[3])` USD al año. Es el segundo factor en importancia.
- **β₁ AvgSessionLength = `r f(b[2])`.** Cada minuto adicional de sesión suma `r f(b[2])` USD al año, lo que confirma el aumento escalonado por grupo observado en P5 y P6.
- **β₃ TimeonWebsite = `r f(b[4])`.** El efecto es prácticamente nulo y no es significativo (t = `r f(t_val[4])`, p = `r f(p_val[4], 3)`). Coincide con la correlación nula de P7 y la nube plana de P4.
- **β₀ = `r f(b[1])`.** Es el gasto teórico de un cliente con cero en todas las variables. Ese caso está muy fuera del rango observado (ningún cliente tiene sesiones de 0 minutos), así que β₀ solo ajusta el nivel de la recta y no tiene lectura práctica.
Como las cuatro variables explicativas tienen desviación estándar ≈ 1 (P3), los coeficientes también se pueden leer como el efecto de una desviación estándar, y por eso son comparables entre sí: **antigüedad > app > sesión > web**. Además, como las variables explicativas casi no se correlacionan entre sí (P7), cada β es muy parecido a la pendiente simple de P4.
El modelo explica el `r f(100*R2, 1)` % de la variabilidad del gasto y su error típico (≈ `r f(sqrt(s2), 0)` USD) es pequeño frente a la desviación estándar del gasto (`r f(sd(Y), 0)` USD). Para el negocio, la lectura es que el gasto depende sobre todo de retener clientes en el tiempo y de la experiencia en la app. El sitio web no está generando gasto, así que conviene revisarlo o dar prioridad a la inversión en la app y en programas de fidelización.