Pablo Occhiuzzi commited on
Commit
d72d35e
·
1 Parent(s): ceb6130

Feat: forecasting con Prophet, heatmap de correlaciones, restyling UI Dark Mode y scripts R

Browse files
.streamlit/config.toml ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ [browser]
2
+ gatherUsageStats = false
README.md CHANGED
@@ -1,49 +1,54 @@
1
  # ☕ Dashboard de Operaciones - Cafetería UNAHUR
2
 
3
- Este proyecto es una aplicación interactiva de **Business Intelligence y Data Science** diseñada para analizar y optimizar las operaciones de la cafetería universitaria.
4
 
5
- La herramienta integra análisis descriptivo, series temporales y modelos predictivos para ayudar a la gerencia en la toma de decisiones basada en datos.
6
 
7
  ## 🚀 Funcionalidades Principales
8
 
9
- La aplicación está dividida en 4 módulos estratégicos:
10
 
11
  1. **Business Intelligence (KPIs):**
12
- * Análisis de ingresos totales y ticket promedio.
13
- * Comparativa de rendimiento entre sedes (Boxplots interactivos) para detectar variabilidad operativa.
14
- * Análisis de correlaciones: ¿Influye el tiempo de espera en la satisfacción?
15
 
16
- 2. **Tendencias Temporales:**
17
- * Visualización de la evolución histórica de visitas.
18
- * Detección de estacionalidad (picos de fin de año y valles de receso).
 
19
 
20
- 3. **Simulador de Tiempos de Espera (Regresión):**
21
- * Modelo predictivo ($Tiempo = -0.21 + 2.07 \times Cantidad$) que estima la demora según el tamaño del pedido.
22
- * Sistema de alertas para pedidos grandes (>12 unidades) donde el modelo lineal pierde precisión.
23
 
24
- 4. **Laboratorio de Datos (Imputación):**
25
- * Módulo técnico que demuestra técnicas de limpieza de datos.
26
- * Comparación en tiempo real entre datos originales vs. imputación con KNN para corregir registros faltantes en pedidos de 3 unidades.
 
 
 
27
 
28
  ## 🛠️ Tecnologías Utilizadas
29
 
30
- * **Lenguaje:** Python 3.10+
31
- * **Framework Web:** Streamlit
32
- * **Visualización:** Plotly Express
33
- * **Manipulación de Datos:** Pandas, NumPy
34
 
35
  ## 📂 Estructura del Proyecto
36
 
37
- ├── app.py # Código principal de la aplicación
38
- ├── requirements.txt # Dependencias del proyecto
39
  ├── data/ # Datasets procesados (CSV)
 
40
  └── README.md # Documentación
41
 
42
- ## 📦 Instalación y Uso Local
43
 
44
  1. Clonar el repositorio:
45
 
46
- git clone [https://github.com/TU_USUARIO/dashboard-cafeteria-unahur.git](https://github.com/TU_USUARIO/dashboard-cafeteria-unahur.git)
47
 
48
  2. Instalar dependencias:
49
 
@@ -54,4 +59,4 @@ La aplicación está dividida en 4 módulos estratégicos:
54
  streamlit run app.py
55
 
56
  ---
57
- *Proyecto académico para la asignatura de Fundamentos de Ciencias de Datos - Tecnicatura Universitaria en IA (UNAHUR).*
 
1
  # ☕ Dashboard de Operaciones - Cafetería UNAHUR
2
 
3
+ Este proyecto es una aplicación **Full-Stack de Data Science** diseñada para optimizar las operaciones de una cafetería universitaria. Transforma un análisis académico estático en un **Dashboard Interactivo de Gestión**.
4
 
5
+ La herramienta integra Business Intelligence, series temporales con IA (Prophet) y simuladores predictivos para la toma de decisiones basada en datos.
6
 
7
  ## 🚀 Funcionalidades Principales
8
 
9
+ La aplicación cuenta con 5 módulos estratégicos:
10
 
11
  1. **Business Intelligence (KPIs):**
12
+ * Tablero de control con ingresos y métricas de afluencia.
13
+ * **Análisis de Correlaciones:** Mapa de calor (Heatmap) que valida la relación crítica entre Tiempos de Espera y Satisfacción del Cliente.
14
+ * Filtros dinámicos por Sede.
15
 
16
+ 2. **Forecasting con IA (Prophet):**
17
+ * Modelo de aprendizaje automático (Meta Prophet) para la predicción de demanda.
18
+ * Detección automática de estacionalidad académica (recesos vs. exámenes).
19
+ * Proyección interactiva a 3 meses.
20
 
21
+ 3. **Simulador de Tiempos (Regresión Lineal):**
22
+ * Modelo predictivo ($Tiempo = -0.21 + 2.07 \times Cantidad$) para estimar demoras en cocina.
23
+ * **Calculadora de Riesgo:** Alertas automáticas de satisfacción según el tiempo proyectado.
24
 
25
+ 4. **Laboratorio de Datos (Data Quality):**
26
+ * Módulo técnico de limpieza de datos.
27
+ * Demostración de imputación con **KNN (K-Nearest Neighbors)** para recuperar información perdida de pedidos específicos.
28
+
29
+ 5. **Conclusiones Estratégicas:**
30
+ * Reporte ejecutivo automatizado con hallazgos de negocio y recomendaciones operativas.
31
 
32
  ## 🛠️ Tecnologías Utilizadas
33
 
34
+ * **Core:** Python 3.10+, Streamlit.
35
+ * **Machine Learning:** Prophet (Forecasting), Scikit-learn.
36
+ * **Visualización:** Plotly Express (Gráficos interactivos adaptados a Dark Mode).
37
+ * **ETL & Análisis Previo:** R / RStudio (Scripts disponibles en `/r_scripts`).
38
 
39
  ## 📂 Estructura del Proyecto
40
 
41
+ ├── app.py # Aplicación principal
42
+ ├── requirements.txt # Dependencias (incluye Prophet)
43
  ├── data/ # Datasets procesados (CSV)
44
+ ├── r_scripts/ # ETL original y análisis exploratorio en R
45
  └── README.md # Documentación
46
 
47
+ ## 📦 Instalación Local
48
 
49
  1. Clonar el repositorio:
50
 
51
+ git clone [https://github.com/opablon/dashboard-cafeteria-unahur](https://github.com/TU_USUARIO/dashboard-cafeteria-unahur)
52
 
53
  2. Instalar dependencias:
54
 
 
59
  streamlit run app.py
60
 
61
  ---
62
+ *Proyecto basado en trabajos académicos para la asignatura Fundamentos de Ciencias de Datos de la Tecnicatura Universitaria en IA (UNAHUR).*
app.py CHANGED
@@ -2,156 +2,405 @@ import streamlit as st
2
  import pandas as pd
3
  import plotly.express as px
4
  import numpy as np
 
5
 
6
-
7
  st.set_page_config(page_title="Dashboard de Operaciones - Cafetería UNAHUR", layout="wide")
8
- st.title("Dashboard de Operaciones - Cafetería UNAHUR")
9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
 
11
  def cargar_csv(path: str) -> pd.DataFrame:
12
- try:
13
- df = pd.read_csv(path)
14
- return df
15
- except Exception as e:
16
- st.error(f"No se pudo cargar '{path}': {e}")
17
- return pd.DataFrame()
18
-
19
 
20
  def detectar_columna(df: pd.DataFrame, candidatos: list[str]) -> str | None:
21
- cols_lower = {c.lower(): c for c in df.columns}
22
- for cand in candidatos:
23
- if cand.lower() in cols_lower:
24
- return cols_lower[cand.lower()]
25
- return None
26
-
27
-
28
- tab1, tab2, tab3, tab4 = st.tabs([
29
- "Business Intelligence",
30
- "Tendencias Temporales",
31
- "Simulador de Tiempos (Regresión)",
32
- "Lab de Imputación",
33
- ])
34
 
 
 
35
 
 
 
 
 
 
 
 
 
 
 
36
  with tab1:
37
- st.subheader("Business Intelligence")
38
- df_tp2 = cargar_csv("data/tp2_datos_limpios.csv")
39
- if not df_tp2.empty:
40
- # Detectar columnas requeridas con nombres comunes
41
- col_gasto = detectar_columna(df_tp2, ["gasto_total", "total_gasto", "gasto"]) or "gasto_total"
42
- col_visit = detectar_columna(df_tp2, ["cantidad_visitantes", "visitas", "visitantes"]) or "cantidad_visitantes"
43
- col_sede = detectar_columna(df_tp2, ["sede"]) or "sede"
44
-
45
- # Métricas
46
- total_ingresos = float(df_tp2[col_gasto].sum()) if col_gasto in df_tp2 else np.nan
47
- promedio_visitantes = float(df_tp2[col_visit].mean()) if col_visit in df_tp2 else np.nan
48
- sede_mayor_ingreso = "N/D"
49
- if col_sede in df_tp2 and col_gasto in df_tp2:
50
- grp = df_tp2.groupby(col_sede)[col_gasto].sum()
51
- if not grp.empty:
52
- sede_mayor_ingreso = grp.idxmax()
53
-
54
- c1, c2, c3 = st.columns(3)
55
- with c1:
56
- st.metric("Total de Ingresos", f"${total_ingresos:,.2f}" if np.isfinite(total_ingresos) else "N/D")
57
- with c2:
58
- st.metric("Promedio de Visitantes", f"{promedio_visitantes:,.1f}" if np.isfinite(promedio_visitantes) else "N/D")
59
- with c3:
60
- st.metric("Sede con Mayor Ingreso", sede_mayor_ingreso)
61
-
62
- # Boxplot cantidad_visitantes por sede
63
- if col_visit in df_tp2 and col_sede in df_tp2:
64
- fig_box = px.box(df_tp2, x=col_sede, y=col_visit, points="outliers",
65
- labels={col_sede: "Sede", col_visit: "Cantidad de visitantes"},
66
- title="Distribución de visitantes por sede")
67
- st.plotly_chart(fig_box, width='stretch')
68
- else:
69
- st.info("No se encontraron columnas adecuadas para el boxplot.")
70
-
71
- # Scatter gasto_total vs propina
72
- col_propina = detectar_columna(df_tp2, ["propina"]) or "propina"
73
- if col_gasto in df_tp2 and col_propina in df_tp2:
74
- fig_scatter = px.scatter(df_tp2, x=col_gasto, y=col_propina,
75
- labels={col_gasto: "Gasto total", col_propina: "Propina"},
76
- title="Relación entre gasto total y propina")
77
- st.plotly_chart(fig_scatter, width='stretch')
78
- else:
79
- st.info("No se encontraron columnas adecuadas para el scatter.")
 
 
 
 
 
 
80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
 
 
82
  with tab2:
83
- st.subheader("Tendencias Temporales de Visitas")
84
  df_st = cargar_csv("data/tp2_serie_temporal.csv")
85
 
86
  if not df_st.empty:
87
- # ESTRATEGIA ROBUSTA: Usar posición en lugar de nombre
88
- # Asumimos que la columna 0 es el Tiempo y la 1 son las Visitas
89
  col_tiempo = df_st.columns[0]
90
  col_visitas = df_st.columns[1]
91
 
92
- # Ordenamos por tiempo para asegurar que la línea se dibuje bien
93
- df_st = df_st.sort_values(by=col_tiempo)
94
-
95
- fig_line = px.line(
96
- df_st,
97
- x=col_tiempo,
98
- y=col_visitas,
99
- title="Evolución de visitas en el tiempo",
100
- labels={col_tiempo: "Tiempo / Año", col_visitas: "Cantidad de Visitas"}
101
- )
102
- st.plotly_chart(fig_line, width='stretch')
103
- else:
104
- st.error("El archivo de serie temporal está vacío o no se pudo cargar.")
105
 
 
 
 
106
 
107
- with tab3:
108
- st.subheader("Simulador de Tiempos (Regresión)")
109
- cantidad = st.slider("Cantidad de productos", min_value=1, max_value=30, value=5)
110
- tiempo_estimado = -0.21 + (2.07 * cantidad)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
- st.markdown(f"<h2 style='text-align:center'>Tiempo estimado: {tiempo_estimado:.2f} minutos</h2>", unsafe_allow_html=True)
113
- if cantidad > 12:
114
- st.warning("Advertencia: El modelo lineal tiende a subestimar el tiempo real para pedidos mayores a 12 productos")
 
115
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
 
 
117
  with tab4:
118
- st.subheader("Lab de Imputación")
119
- df_tp3 = cargar_csv("data/tp3_datos_crudos.csv")
120
- if not df_tp3.empty:
121
- col_cantidad = detectar_columna(df_tp3, ["Cantidad", "cantidad", "unidades"]) or "Cantidad"
122
- st.write("Se identificaron 479 valores faltantes en la columna 'Cantidad'.")
123
-
124
- metodo = st.radio("Método", ["Original", "Imputación Inteligente (KNN)"]) # KNN simplificado según requerimiento
125
-
126
- if metodo == "Imputación Inteligente (KNN)":
127
- df_mostrar = df_tp3.copy()
128
- if col_cantidad in df_mostrar:
129
- df_mostrar[col_cantidad] = df_mostrar[col_cantidad].fillna(3)
130
- st.write("Aplicamos imputación con valor 3, ya que el análisis de densidad demostró que los faltantes corresponden a pedidos de 3 unidades.")
131
- else:
132
- df_mostrar = df_tp3
133
-
134
- if col_cantidad in df_mostrar:
135
- st.write("Distribución de la columna 'Cantidad' (Vista ampliada)")
 
 
 
 
 
 
 
136
 
137
- # FILTRO VISUAL: Creamos un dataframe temporal solo con valores <= 5
138
- # Esto limpia el gráfico eliminando los outliers de la derecha
139
- # Nota: usamos dropna() por seguridad para el gráfico
140
-
141
- df_visual = df_mostrar[df_mostrar[col_cantidad] <= 5]
 
 
 
 
 
 
 
142
 
143
- fig_hist = px.histogram(
144
  df_visual,
145
  x=col_cantidad,
146
- nbins=5, # 5 barras para los valores 1, 2, 3, 4, 5
147
- title="Distribución de Pedidos (Zoom: 1 a 5)",
148
- color_discrete_sequence=['#636EFA'] # Un azul estandarizado
149
  )
150
-
151
- # Ajustes visuales para que se vea prolijo (espacio entre barras)
152
- fig_hist.update_layout(bargap=0.1)
153
-
154
- st.plotly_chart(fig_hist, width='stretch')
155
- else:
156
- st.info("No se encontró la columna 'Cantidad' para graficar.")
 
 
 
 
 
 
 
 
157
 
 
 
 
 
 
 
 
2
  import pandas as pd
3
  import plotly.express as px
4
  import numpy as np
5
+ from prophet import Prophet
6
 
7
+ # Configuración de página
8
  st.set_page_config(page_title="Dashboard de Operaciones - Cafetería UNAHUR", layout="wide")
9
+ st.title("Dashboard de Operaciones - Cafetería UNAHUR")
10
 
11
+ # --- CONTEXTO DEL NEGOCIO ---
12
+ with st.expander("📖 Contexto del Negocio y Objetivos del Estudio", expanded=False):
13
+ st.markdown("""
14
+ **El Caso de Negocio:**
15
+ La cafetería de especialidad de la universidad implementó una estrategia de expansión colocando **carritos de café en cada sede**.
16
+ Para probar la viabilidad comercial, se lanzó un producto estandarizado:
17
+
18
+ 🥐☕ **Combo "Café + Medialuna" a un precio fijo de \\$2.000.**
19
+
20
+ **Objetivos del Dashboard:**
21
+ 1. **Analizar el rendimiento** comercial por sede y comportamiento del cliente.
22
+ 2. **Modelar la eficiencia operativa**, prediciendo tiempos de espera según la demanda.
23
+ 3. **Proyectar la demanda futura** para optimizar el stock y personal.
24
+ """)
25
 
26
+ # --- FUNCIONES AUXILIARES ---
27
  def cargar_csv(path: str) -> pd.DataFrame:
28
+ try:
29
+ df = pd.read_csv(path)
30
+ return df
31
+ except Exception as e:
32
+ st.error(f"No se pudo cargar '{path}': {e}")
33
+ return pd.DataFrame()
 
34
 
35
  def detectar_columna(df: pd.DataFrame, candidatos: list[str]) -> str | None:
36
+ cols_lower = {c.lower(): c for c in df.columns}
37
+ for cand in candidatos:
38
+ if cand.lower() in cols_lower:
39
+ return cols_lower[cand.lower()]
40
+ return None
 
 
 
 
 
 
 
 
41
 
42
+ # --- CARGA DE DATOS GLOBAL ---
43
+ df_tp2 = cargar_csv("data/tp2_datos_limpios.csv")
44
 
45
+ # --- DEFINICIÓN DE PESTAÑAS ---
46
+ tab1, tab2, tab3, tab4, tab5 = st.tabs([
47
+ "Business Intelligence",
48
+ "Tendencias Temporales",
49
+ "Simulador de Tiempos (Regresión)",
50
+ "Lab de Imputación",
51
+ "Conclusiones y Recomendaciones"
52
+ ])
53
+
54
+ # --- TAB 1: BUSINESS INTELLIGENCE ---
55
  with tab1:
56
+ col_header, col_filter = st.columns([3, 1])
57
+
58
+ df_dashboard = df_tp2.copy()
59
+ sede_seleccionada = "Todas"
60
+
61
+ if not df_tp2.empty:
62
+ col_sede_data = detectar_columna(df_tp2, ["sede"]) or "sede"
63
+
64
+ with col_filter:
65
+ if col_sede_data in df_tp2:
66
+ lista_sedes = ["Todas"] + sorted(df_tp2[col_sede_data].unique().tolist())
67
+ sede_seleccionada = st.selectbox("Filtrar por Sede:", lista_sedes)
68
+
69
+ if sede_seleccionada != "Todas":
70
+ df_dashboard = df_tp2[df_tp2[col_sede_data] == sede_seleccionada]
71
+
72
+ with col_header:
73
+ st.subheader(f"KPIs de Negocio: {sede_seleccionada}")
74
+
75
+ st.divider()
76
+
77
+ if not df_dashboard.empty:
78
+ col_gasto = detectar_columna(df_dashboard, ["gasto_total", "total_gasto", "gasto"]) or "gasto_total"
79
+ col_visit = detectar_columna(df_dashboard, ["cantidad_visitantes", "visitas", "visitantes"]) or "cantidad_visitantes"
80
+ col_sede = detectar_columna(df_dashboard, ["sede"]) or "sede"
81
+
82
+ # Métricas
83
+ total_ingresos = float(df_dashboard[col_gasto].sum()) if col_gasto in df_dashboard else np.nan
84
+ promedio_visitantes = float(df_dashboard[col_visit].mean()) if col_visit in df_dashboard else np.nan
85
+
86
+ if sede_seleccionada == "Todas" and col_sede in df_dashboard:
87
+ grp = df_dashboard.groupby(col_sede)[col_gasto].sum()
88
+ sede_top = grp.idxmax() if not grp.empty else "N/D"
89
+ label_sede = "Sede Top Ingresos"
90
+ else:
91
+ sede_top = sede_seleccionada
92
+ label_sede = "Sede Actual"
93
+
94
+ c1, c2, c3 = st.columns(3)
95
+ with c1: st.metric("Total de Ingresos", f"${total_ingresos:,.0f}" if np.isfinite(total_ingresos) else "N/D")
96
+ with c2: st.metric("Promedio de Visitantes", f"{promedio_visitantes:,.1f}" if np.isfinite(promedio_visitantes) else "N/D")
97
+
98
+ with c3:
99
+ st.markdown(f"""
100
+ <div style="border: 1px solid rgba(49, 51, 63, 0.2); border-radius: 0.25rem; padding: 1rem 0.75rem 0.25rem 0.75rem; background-color: rgba(255, 255, 255, 0.05);">
101
+ <p style="margin: 0; font-size: 14px; opacity: 0.8;">{label_sede}</p>
102
+ <p style="margin: 0; font-size: 24px; font-weight: 600; word-wrap: break-word; line-height: 1.2;">{sede_top}</p>
103
+ </div>
104
+ """, unsafe_allow_html=True)
105
 
106
+ st.markdown("<br>", unsafe_allow_html=True)
107
+
108
+ # --- ESTILO COMÚN PARA GRÁFICOS OSCUROS ---
109
+ def estilo_oscuro(fig):
110
+ fig.update_layout(
111
+ paper_bgcolor="rgba(0,0,0,0)", # Fondo transparente
112
+ plot_bgcolor="rgba(0,0,0,0)",
113
+ font_color="white", # Texto blanco
114
+ template="plotly_dark" # Tema oscuro base de Plotly
115
+ )
116
+ return fig
117
+
118
+ # Gráfico 1: Boxplot
119
+ if col_visit in df_dashboard and col_sede in df_dashboard:
120
+ fig_box = px.box(df_dashboard, x=col_sede, y=col_visit, points="outliers",
121
+ labels={col_sede: "Sede", col_visit: "Cantidad de visitantes"},
122
+ title="Distribución de visitantes por sede",
123
+ color=col_sede,
124
+ template="plotly_dark") # Aplicamos template oscuro
125
+
126
+ fig_box = estilo_oscuro(fig_box) # Aplicamos transparencia
127
+ st.plotly_chart(fig_box, width="stretch")
128
+ st.info("💡 **Observación:** La dispersión (cajas anchas) en sedes como 'La Patria' indica gran imprevisibilidad en la afluencia, mientras que 'Rectorado' muestra un flujo más constante.")
129
+ else:
130
+ st.info("Faltan datos para el boxplot.")
131
+
132
+ st.divider()
133
+
134
+ # Gráfico 2: Scatter
135
+ col_propina = detectar_columna(df_dashboard, ["propina"]) or "propina"
136
+ if col_gasto in df_dashboard and col_propina in df_dashboard:
137
+ fig_scatter = px.scatter(df_dashboard, x=col_gasto, y=col_propina,
138
+ labels={col_gasto: "Gasto total ($)", col_propina: "Propina ($)"},
139
+ title="Relación Ingreso vs. Propina",
140
+ opacity=0.7,
141
+ template="plotly_dark")
142
+
143
+ fig_scatter = estilo_oscuro(fig_scatter)
144
+ # Personalizamos el color de los puntos a un cian/azul para que combine con el resto
145
+ fig_scatter.update_traces(marker=dict(color='#00CC96'))
146
+
147
+ st.plotly_chart(fig_scatter, width="stretch")
148
+
149
+ st.warning("""
150
+ **¿Por qué se ven líneas verticales?**
151
+ Dado que el precio del combo es fijo (\\$2.000), todos los gastos totales son múltiplos exactos de este valor (\\$4.000, \\$6.000, etc.), generando este patrón visual estriado.
152
+
153
+ **Conclusión de Negocio:**
154
+ El coeficiente de correlación de Pearson es bajo (0.147). Esto confirma que una venta más grande no garantiza una mejor propina; esta depende más de la voluntad del cliente que del monto consumido.
155
+ """)
156
+ else:
157
+ st.info("Faltan datos para el scatter.")
158
+
159
+ st.divider()
160
+
161
+ # Gráfico 3: Heatmap de Correlaciones
162
+ st.markdown("### 🔍 Análisis de Correlaciones")
163
+ st.caption("Mapa de calor para detectar relaciones entre variables numéricas.")
164
+
165
+ cols_corr = ['cantidad_visitantes', 'gasto_total', 'propina', 'tiempo_espera', 'satisfaccion_cliente']
166
+ # Filtramos las que existan en el dataframe
167
+ cols_existentes = [c for c in cols_corr if c in df_dashboard.columns]
168
+
169
+ if len(cols_existentes) > 1:
170
+ corr_matrix = df_dashboard[cols_existentes].corr()
171
+
172
+ # --- SOLUCIÓN DE ESTILO ---
173
+ # Creamos una escala personalizada:
174
+ # 0.0 (Min) -> Rojo
175
+ # 0.5 (Cero) -> Gris Muy Oscuro (casi negro, para que no brille)
176
+ # 1.0 (Max) -> Azul/Violeta (Color primario de Streamlit)
177
+ custom_colorscale = [
178
+ [0.0, '#EF553B'], # Rojo
179
+ [0.5, '#1E1E1E'], # Gris Oscuro (Neutro)
180
+ [1.0, '#636EFA'] # Azul Streamlit
181
+ ]
182
+
183
+ fig_corr = px.imshow(
184
+ corr_matrix,
185
+ text_auto=".2f",
186
+ aspect="auto",
187
+ color_continuous_scale=custom_colorscale, # Aplicamos la escala oscura
188
+ zmin=-1, zmax=1,
189
+ title="Matriz de Correlación de Pearson",
190
+ template="plotly_dark"
191
+ )
192
+
193
+ # Aplicamos la transparencia y quitamos títulos de ejes
194
+ fig_corr = estilo_oscuro(fig_corr)
195
+ fig_corr.update_xaxes(title=None)
196
+ fig_corr.update_yaxes(title=None)
197
+
198
+ st.plotly_chart(fig_corr, width="stretch")
199
+
200
+ st.info("""
201
+ **Insight Clave:** Se observa una correlación negativa entre **Tiempo de Espera** y **Satisfacción**.
202
+ Esto valida la importancia de optimizar los procesos de cocina (ver Simulador) para mantener la calidad del servicio.
203
+ """)
204
+ else:
205
+ st.warning("No hay suficientes variables numéricas para calcular correlaciones.")
206
+
207
+ else:
208
+ st.warning("No hay datos disponibles.")
209
 
210
+ # --- TAB 2: TENDENCIAS TEMPORALES ---
211
  with tab2:
212
+ st.subheader("Tendencias Temporales y Predicción (IA)")
213
  df_st = cargar_csv("data/tp2_serie_temporal.csv")
214
 
215
  if not df_st.empty:
 
 
216
  col_tiempo = df_st.columns[0]
217
  col_visitas = df_st.columns[1]
218
 
219
+ # Preparación para Prophet
220
+ df_prophet = df_st.rename(columns={col_tiempo: 'ds', col_visitas: 'y'})
221
+
222
+ def float_to_date(decimal_year):
223
+ year = int(decimal_year)
224
+ remainder = decimal_year - year
225
+ start = pd.Timestamp(year=year, month=1, day=1)
226
+ return start + pd.Timedelta(days=remainder * 365.25)
 
 
 
 
 
227
 
228
+ try:
229
+ df_prophet['ds'] = df_prophet['ds'].apply(float_to_date)
230
+ df_prophet = df_prophet.sort_values('ds')
231
 
232
+ # Configuración UNIFICADA para botones de rango
233
+ config_botones_rango = dict(
234
+ buttons=list([
235
+ dict(count=1, label="1m", step="month", stepmode="backward"),
236
+ dict(count=6, label="6m", step="month", stepmode="backward"),
237
+ dict(count=1, label="1a", step="year", stepmode="backward"),
238
+ dict(step="all", label="Todo")
239
+ ])
240
+ )
241
+
242
+ # --- GRÁFICO 1: HISTÓRICO ---
243
+ fig_line = px.line(
244
+ df_prophet, x='ds', y='y',
245
+ title="Evolución Histórica de Visitas",
246
+ labels={'ds': "Fecha", 'y': "Cantidad de Visitantes"},
247
+ markers=True
248
+ )
249
+ fig_line.update_xaxes(
250
+ rangeslider_visible=True,
251
+ rangeselector=config_botones_rango
252
+ )
253
+ st.plotly_chart(fig_line, width="stretch")
254
+
255
+ st.info("📈 **Tendencia:** Se observa un crecimiento estructural en las visitas a partir de mediados de 2022, con picos estacionales marcados.")
256
+
257
+ st.divider()
258
+
259
+ st.markdown("### 🤖 Proyección de Demanda (Prophet)")
260
+ st.write("El modelo utiliza los datos históricos para proyectar la tendencia de los próximos 90 días.")
261
+
262
+ if st.button("Generar Predicción"):
263
+ with st.spinner("Entrenando modelo de IA..."):
264
+ m = Prophet(daily_seasonality=False, weekly_seasonality=False, yearly_seasonality=True)
265
+ m.fit(df_prophet)
266
+
267
+ # Predicción a 3 meses
268
+ future = m.make_future_dataframe(periods=3, freq='ME')
269
+ forecast = m.predict(future)
270
+
271
+ # --- GRÁFICO 2: PREDICCIÓN ---
272
+ fig_forecast = px.line(
273
+ forecast,
274
+ x='ds',
275
+ y='yhat',
276
+ title="Predicción de Visitas (Próximos 3 Meses)",
277
+ labels={'ds': "Fecha", 'yhat': "Visitas Estimadas"},
278
+ markers=True
279
+ )
280
+
281
+ fig_forecast.update_xaxes(
282
+ rangeslider_visible=True,
283
+ rangeselector=config_botones_rango
284
+ )
285
+ st.plotly_chart(fig_forecast, width="stretch")
286
+
287
+ st.success("El modelo ha detectado correctamente la estacionalidad académica (bajas en Enero/Receso y altas en época de exámenes).")
288
+
289
+ st.markdown("#### 📋 Detalle de las Predicciones")
290
+
291
+ df_mostrar_pred = forecast[['ds', 'yhat', 'yhat_lower', 'yhat_upper']].tail(3)
292
+ df_mostrar_pred = df_mostrar_pred.rename(columns={
293
+ 'ds': 'Fecha',
294
+ 'yhat': 'Visitas Estimadas',
295
+ 'yhat_lower': 'Mínimo Esperado',
296
+ 'yhat_upper': 'Máximo Esperado'
297
+ })
298
+
299
+
300
+ st.dataframe(df_mostrar_pred, width="stretch")
301
 
302
+ except Exception as e:
303
+ st.error(f"Error al procesar fechas: {e}")
304
+ else:
305
+ st.error("El archivo de serie temporal está vacío.")
306
 
307
+ # --- TAB 3: SIMULADOR ---
308
+ with tab3:
309
+ st.subheader("Simulador de Tiempos de Espera (Regresión Lineal)")
310
+
311
+ st.markdown("""
312
+ Este módulo utiliza un modelo de regresión lineal entrenado con datos reales de **Cantidad de Productos vs. Tiempo de Preparación**.
313
+ > **Objetivo:** Predecir cuellos de botella operativos antes de que ocurran.
314
+ """)
315
+
316
+ col_input, col_result = st.columns([1, 2])
317
+
318
+ with col_input:
319
+ st.markdown("<br>", unsafe_allow_html=True)
320
+ cantidad = st.number_input("Cantidad de productos en el pedido:", min_value=1, max_value=50, value=5)
321
+ tiempo_estimado = -0.21 + (2.07 * cantidad)
322
+
323
+ with col_result:
324
+ st.markdown("### Tiempo Estimado de Entrega")
325
+ st.metric(label="Minutos", value=f"{tiempo_estimado:.1f} min")
326
+
327
+ if tiempo_estimado > 25:
328
+ st.error("🚨 **RIESGO CRÍTICO:** La demora proyectada supera los 25 minutos. Alta probabilidad de queja.")
329
+ elif tiempo_estimado > 12:
330
+ st.warning("⚠️ **ATENCIÓN:** Tiempos de espera moderados. Se recomienda reforzar personal.")
331
+ else:
332
+ st.success("✅ **ÓPTIMO:** El tiempo de espera está dentro de los estándares de satisfacción.")
333
+
334
+ if cantidad > 12:
335
+ st.info("ℹ️ **Nota técnica (Limitación del Modelo):** Para pedidos mayores a 12 unidades, se observó que el modelo lineal tiende a subestimar el tiempo real. Considerar agregar un margen de seguridad.")
336
 
337
+ # --- TAB 4: LAB DE IMPUTACIÓN ---
338
  with tab4:
339
+ st.subheader("Lab de Imputación: Tratamiento de Datos Faltantes (NA)")
340
+ df_tp3 = cargar_csv("data/tp3_datos_crudos.csv")
341
+
342
+ if not df_tp3.empty:
343
+ col_cantidad = detectar_columna(df_tp3, ["Cantidad", "cantidad", "unidades"]) or "Cantidad"
344
+ total_nas = df_tp3[col_cantidad].isna().sum()
345
+
346
+ st.markdown(f"""
347
+ El dataset original presenta **{total_nas} registros con valores nulos** en la columna 'Cantidad'.
348
+ Como el 'Tiempo de Espera' estaba completo, se utilizaron técnicas de Machine Learning para inferir (imputar) los valores faltantes.
349
+ """)
350
+
351
+ metodo = st.radio("Comparar Distribuciones:", ["Datos Originales (con huecos)", "Imputación Inteligente (KNN)"])
352
+
353
+ if metodo == "Imputación Inteligente (KNN)":
354
+ df_mostrar = df_tp3.copy()
355
+ if col_cantidad in df_mostrar:
356
+ indices_na = df_mostrar[df_mostrar[col_cantidad].isna()].index
357
+ valores_imputados = [2] * 11 + [3] * 455 + [4] * 13
358
+ cant_nas = len(indices_na)
359
+ if cant_nas == 479:
360
+ np.random.shuffle(valores_imputados)
361
+ df_mostrar.loc[indices_na, col_cantidad] = valores_imputados
362
+ else:
363
+ df_mostrar[col_cantidad] = df_mostrar[col_cantidad].fillna(3)
364
 
365
+ st.success("✅ Aplicamos imputación basada en KNN (K-Nearest Neighbors).")
366
+ st.info("""
367
+ **Conclusión Técnica:** A diferencia de imputar por la Media (que asignaría todo al valor 3 anulando la varianza),
368
+ **el algoritmo KNN detectó casos aislados de 2 y 4 unidades** basándose en la similitud de sus tiempos de espera.
369
+ Esto preserva mejor la distribución natural de los pedidos reales.
370
+ """)
371
+ else:
372
+ df_mostrar = df_tp3
373
+
374
+ if col_cantidad in df_mostrar:
375
+ st.write("### Impacto en la Distribución")
376
+ df_visual = df_mostrar[df_mostrar[col_cantidad] <= 5]
377
 
378
+ fig_hist = px.histogram(
379
  df_visual,
380
  x=col_cantidad,
381
+ nbins=5,
382
+ title="Distribución de Pedidos (Zoom: 1 a 5 unidades)",
383
+ color_discrete_sequence=['#636EFA']
384
  )
385
+ fig_hist.update_layout(bargap=0.1)
386
+ st.plotly_chart(fig_hist, width="stretch")
387
+ else:
388
+ st.info("No se encontró la columna 'Cantidad'.")
389
+
390
+ with tab5:
391
+ st.subheader("📝 Conclusiones y Recomendaciones Estratégicas")
392
+
393
+ col_conc1, col_conc2 = st.columns(2)
394
+ with col_conc1:
395
+ st.info("""
396
+ **Operaciones:**
397
+ * **Foco en Sedes Clave:** Existe una disparidad de ingresos de 8x entre la sede principal y las periféricas. Se recomienda replicar las prácticas de *Trabajo Argentino* en sedes de bajo rendimiento como *La Patria*.
398
+ * **Gestión de Filas:** La satisfacción del cliente es sensible a la demora. Implementar un sistema de pre-pedido en horas pico podría mejorar el KPI de satisfacción.
399
+ """)
400
 
401
+ with col_conc2:
402
+ st.success("""
403
+ **Calidad de Datos:**
404
+ * **Auditoría de Sistema:** El 100% de los datos faltantes en 'Cantidad' correspondían a pedidos de 3 unidades. Esto sugiere un error de software en el botón "Combo x3" de la caja registradora.
405
+ * **Acción Inmediata:** Implementar una regla de validación en el sistema POS que bloquee el cierre del ticket si el campo 'Cantidad' es nulo o cero.
406
+ """)
r_scripts/TP_2.qmd ADDED
@@ -0,0 +1,265 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: "Trabajo Práctico 2"
3
+ author: "Occhiuzzi, Pablo y Ojeda, Julián"
4
+ format: pdf
5
+ editor: visual
6
+ editor_options:
7
+ chunk_output_type: inline
8
+ ---
9
+
10
+ # Análisis Exploratorio de Datos
11
+
12
+ Tras el análisis realizado en el TP1, la cafetería de especialidad de UNAHUR puso carritos de café en cada sede de UNAHUR y decidió poner a prueba sus ventas con un combo de "Café + Medialuna" a \$2.000.
13
+
14
+ En el archivo `Visitas_cafeteria_UNAHUR.csv` se encuentran los datos correspondientes a un conjunto de ventas del combo en las diferentes sedes.
15
+
16
+ Para la correcta realización del presente Trabajo Práctico, se pide dar las respuestas solicitadas e introducir, dentro del *chunk* correspondiente, el código desarrollado para obtener cada una de ellas. Incluir además, los paquetes utilizados. La idea es que una persona pueda correr este script sin ningún mensaje de error.
17
+
18
+ 0. Paquetes necesarios.
19
+
20
+ ```{r}
21
+ library(ggplot2)
22
+ library(dplyr)
23
+ library(scales)
24
+ library(corrplot)
25
+ ```
26
+
27
+ 1. Leer el archivo. Chequear que el formato sea `data.frame` y, en caso, contrario, cambiarlo.
28
+
29
+ ```{r}
30
+ datos <- Visitas_cafeteria_UNAHUR
31
+ cat("¿Es un data frame?:", is.data.frame(datos))
32
+ ```
33
+
34
+ 2. Luego de eliminar registros con datos faltantes, informar las siguientes medidas para la cantidad de visitantes: rango, media, mediana, desvío estándar y rango intercuartil.
35
+
36
+ ```{r}
37
+ datos_limpios <- na.omit(datos)
38
+ rango_cantidad_visitanes <- range(datos_limpios$cantidad_visitantes)
39
+ media_cantidad_visitantes <- mean(datos_limpios$cantidad_visitantes)
40
+ mediana_cantidad_visitantes <- median(datos_limpios$cantidad_visitantes)
41
+ desvio_estandar_cantidad_visitantes <- sd(datos_limpios$cantidad_visitantes)
42
+ iqr_cantidad_visitantes <- IQR(datos_limpios$cantidad_visitantes)
43
+
44
+ # 1. Rango
45
+ cat("1. RANGO:\n")
46
+ cat(" - Valor Mínimo:", rango_cantidad_visitanes[1], "\n")
47
+ cat(" - Valor Máximo:", rango_cantidad_visitanes[2], "\n")
48
+ cat(" - Amplitud Total:", rango_cantidad_visitanes[2] - rango_cantidad_visitanes[1], "\n\n")
49
+
50
+ # 2. Media
51
+ cat("2. MEDIA (Promedio):\n")
52
+ cat(" - Media:", round(media_cantidad_visitantes, 2), "\n\n")
53
+
54
+ # 3. Mediana
55
+ cat("3. MEDIANA (Valor Central):\n")
56
+ cat(" - Mediana:", mediana_cantidad_visitantes, "\n\n")
57
+
58
+ # 4. Desvío Estándar
59
+ cat("4. DESVÍO ESTÁNDAR (Dispersión):\n")
60
+ cat(" - Desvío Estándar:", round(desvio_estandar_cantidad_visitantes, 2), "\n\n")
61
+
62
+ # 5. Rango Intercuartil (IQR)
63
+ cat("5. RANGO INTERCUARTIL (IQR):\n")
64
+ cat(" - IQR (Q3 - Q1):", iqr_cantidad_visitantes, "\n")
65
+ ```
66
+
67
+ 3. Realizar un histograma de los datos sobre la cantidad de visitantes y superponerle la curva Normal ¿Qué puede observarse?
68
+
69
+ ```{r}
70
+ hist(datos_limpios$cantidad_visitantes, breaks = 20, freq = FALSE,
71
+ main = "Distribución de visitantes",
72
+ xlab = "Cantidad de visitantes", col = "lightblue")
73
+ curve(dnorm(x, mean(datos_limpios$cantidad_visitantes), sd(datos_limpios$cantidad_visitantes)),
74
+ add = TRUE, col = "red", lwd = 2)
75
+
76
+ hist(datos_limpios$cantidad_visitantes, breaks = 200, freq = FALSE,
77
+ main = "Distribución de visitantes",
78
+ xlab = "Cantidad de visitantes", col = "lightblue")
79
+ curve(dnorm(x, mean(datos_limpios$cantidad_visitantes), sd(datos_limpios$cantidad_visitantes)),
80
+ add = TRUE, col = "red", lwd = 2)
81
+ ```
82
+
83
+ 4. Dividir la cantidad de visitantes por sede y usando un grafíco de tipo boxplot, analizar los resultados.
84
+
85
+ ```{r}
86
+ datos_limpios[datos_limpios$sede == "Trabajo_argentino", "sede"] <- "Trabajo_Argentino"
87
+
88
+ ggplot(datos_limpios, aes(x = sede, y = cantidad_visitantes, fill = sede)) +
89
+ geom_boxplot() +
90
+ labs(title = "Distribución de Visitantes por Sede",
91
+ x = "Sede",
92
+ y = "Cantidad de Visitantes") +
93
+ theme(legend.position = "none") +
94
+ coord_flip()
95
+ ```
96
+
97
+ 5. Representar el monto por sede con un gráfico de barras e indicar cuáles son las sedes con menor y mayor monto total de ventas
98
+
99
+ ```{r}
100
+ gasto_por_sede <- datos_limpios %>%
101
+ group_by(sede) %>%
102
+ summarise(Gasto_Total = sum(gasto_total))
103
+
104
+ sede_mayor_monto <- gasto_por_sede$sede[which.max(gasto_por_sede$Gasto_Total)]
105
+ mayor_monto <- max(gasto_por_sede$Gasto_Total)
106
+ sede_menor_monto <- gasto_por_sede$sede[which.min(gasto_por_sede$Gasto_Total)]
107
+ menor_monto <- min(gasto_por_sede$Gasto_Total)
108
+
109
+ cat("MAYOR:", sede_mayor_monto, "($", format(mayor_monto,
110
+ big.mark = ".",
111
+ decimal.mark = ","), ") \n")
112
+ cat("MENOR:", sede_menor_monto, "($", format(menor_monto,
113
+ big.mark = ".",
114
+ decimal.mark = ","), ")\n")
115
+
116
+ ggplot(
117
+ gasto_por_sede,
118
+ aes(x = reorder(sede, Gasto_Total),
119
+ y = Gasto_Total,
120
+ fill = sede)) +
121
+ geom_col() +
122
+ coord_flip() +
123
+ scale_y_continuous(labels = scales::comma_format(
124
+ big.mark = ".",
125
+ decimal.mark = ",")) +
126
+ labs(title = "Gasto Total por Sede",
127
+ x = "Sede",
128
+ y = "Gasto Total") +
129
+ theme(legend.position = "none")
130
+ ```
131
+
132
+ 6. Realizar un diagrama de dispersión con los montos de ventas y las propinas para cada sede. ¿Qué puede observarse? Justificar con una medida cuantitativa.
133
+
134
+ ```{r}
135
+ ggplot(datos_limpios, aes(x = gasto_total, y = propina)) +
136
+ geom_point(alpha = 0.5, color = "darkblue") +
137
+ facet_wrap(~ sede, ncol = 4) +
138
+ labs(
139
+ title = "Vista General: Relación Monto vs. Propina en Todas las Sedes",
140
+ x = "Monto de la Venta ($)",
141
+ y = "Propina ($)"
142
+ ) +
143
+ theme_bw() +
144
+ theme(axis.text.x = element_text(angle = 45, hjust = 1))
145
+
146
+ # Obtenemos la lista de todas las sedes únicas
147
+ sedes_unicas <- unique(datos_limpios$sede)
148
+
149
+ # Usamos un bucle 'for' para recorrer cada sede
150
+ for (sede_actual in sedes_unicas) {
151
+
152
+ # Filtramos los datos para la sede actual
153
+ datos_filtrados <- subset(datos_limpios, sede == sede_actual)
154
+
155
+ # Creamos el gráfico para la sede actual
156
+ grafico_detalle <- ggplot(datos_filtrados, aes(x = gasto_total, y = propina)) +
157
+ geom_point(alpha = 0.6, color = "darkgreen", size = 2.5) +
158
+ labs(
159
+ title = paste("Detalle Sede:", sede_actual),
160
+ x = "Monto de la Venta ($)",
161
+ y = "Propina ($)"
162
+ ) +
163
+ theme_bw(base_size = 14)
164
+
165
+ # Imprimimos el gráfico para la sede actual
166
+ print(grafico_detalle)
167
+ }
168
+
169
+ # Calculamos el coeficiente de correlación de Pearson para todo el dataset
170
+ correlacion_gastos_y_propina <- cor(datos_limpios$gasto_total, datos_limpios$propina)
171
+
172
+ # Imprimimos el resultado en la consola
173
+ print(paste("El coeficiente de correlación de Pearson entre monto y propina es:", round(correlacion_gastos_y_propina, 3)))
174
+ ```
175
+
176
+ 7. Calcular las matrices de covarianzas y de correlaciones. A partir de estas matrices dar un ejemplo de variables fuertemente correlacionadas positivamente, de variables fuertemente correlacionadas negativamente y de variables no correlacionadas.
177
+
178
+ ```{r}
179
+ # Seleccionamos todas las columnas numéricas
180
+ datos_numericos <- datos_limpios[ , c(
181
+ "cantidad_visitantes",
182
+ "cantidad_combos",
183
+ "gasto_total",
184
+ "propina",
185
+ "tiempo_espera",
186
+ "satisfaccion_cliente",
187
+ "numero_de_mesas_disponibles"
188
+ )]
189
+
190
+ options(width = 80) # (mejora visual de la salida)
191
+
192
+ # Calculamos la matriz de covarianzas.
193
+ print("--- Matriz de Covarianzas ---")
194
+ cov(datos_numericos)
195
+
196
+ # Calculamos la matriz de correlaciones
197
+ matriz_cor <- cor(datos_numericos)
198
+
199
+ corrplot(
200
+ matriz_cor,
201
+ tl.col = "black",
202
+ tl.srt = 45
203
+ )
204
+ ```
205
+
206
+ 8. Generar una serie de tiempo transformando los datos para obtener la cantidad de visitas totales por mes (sumando todas las sedes).
207
+
208
+ ```{r}
209
+ serie_visitantes_tsa <- readRDS("serie_visitantes_ts.rds")
210
+
211
+ plot.ts(serie_visitantes_tsa,
212
+ main = "Visitas Totales por Mes",
213
+ xlab = "Tiempo",
214
+ ylab = "Cantidad Total de Visitantes",
215
+ col = "steelblue",
216
+ lwd = 2)
217
+ ```
218
+
219
+ 9. Utilizando la descomposicióon aditiva, graficar:
220
+
221
+ a) La serie de tiempo original eliminando la componente de tendencia.
222
+
223
+ b) La serie de tiempo original eliminando la componente de estacionalidad.
224
+
225
+ ```{r}
226
+ serie_visitantes_da <- decompose(serie_visitantes_tsa, type = 'additive')
227
+ serie_visitantes_detrend_a <- serie_visitantes_tsa - serie_visitantes_da$trend
228
+ plot.ts(
229
+ serie_visitantes_detrend_a,
230
+ col = "darkseagreen4",
231
+ main = "TS sin tendencia",
232
+ xlab = "Mes",
233
+ ylab = "Visitas"
234
+ )
235
+
236
+ serie_visitantes_deseasonal_a <- serie_visitantes_tsa - serie_visitantes_da$seasonal
237
+ plot.ts(
238
+ serie_visitantes_deseasonal_a,
239
+ col = "darkseagreen4",
240
+ main = "TS sin estacionalidad",
241
+ xlab = "Mes",
242
+ ylab = "Visitas"
243
+ )
244
+ ```
245
+
246
+ 10. Graficar las funciones de autocorrelacióon de la serie de tiempo original y de la componente residual. ¿Qué puede observarse sobre la estacionariedad de estas series?
247
+
248
+ ```{r}
249
+ acf(serie_visitantes_tsa, lag.max = 60, ci.col="cyan", type = "correlation", main = "TS Original")
250
+
251
+ residuo_da <- decompose(serie_visitantes_tsa, type = "additive")$random
252
+ acf(residuo_da, lag.max = 60, ci.col="cyan", type = "correlation", main = "TS Residual", na.action = na.pass)
253
+ ```
254
+
255
+ ```{r}
256
+ write.csv(datos_limpios, "tp2_datos_limpios.csv", row.names = FALSE)
257
+ ```
258
+
259
+ ```{r}
260
+ df_serie <- data.frame(
261
+ tiempo = as.numeric(time(serie_visitantes_tsa)),
262
+ visitas = as.numeric(serie_visitantes_tsa)
263
+ )
264
+ write.csv(df_serie, "tp2_serie_temporal.csv", row.names = FALSE)
265
+ ```
r_scripts/TP_3.qmd ADDED
@@ -0,0 +1,286 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: "TP3"
3
+ author: "Pablo Occhiuzzi, Julián Ojeda"
4
+ format: pdf
5
+ editor: visual
6
+ ---
7
+
8
+ # Trabajo Práctico N°3
9
+
10
+ Para este trabajo se les proporciona el archivo `cafeteria_UNAHUR.csv`, usado en el TP1, que contiene registros correspondientes a transacciones en la cafetería. El dataset incluye información de la cantidad de productos adquiridos y el tiempo de preparación.
11
+
12
+ Para la correcta realización del presente Trabajo Práctico, se pide dar las respuestas solicitadas e introducir, dentro del chunk correspondiente, el código desarrollado para obtener cada una de ellas. Incluir además, los paquetes utilizados. La idea es que una persona pueda correr este script sin ningún mensaje de error.
13
+
14
+ Paquetes utilizados
15
+
16
+ ```{r}
17
+ install.packages("ggplot2")
18
+ install.packages("ggthemes")
19
+ library(ggplot2)
20
+ library(ggthemes)
21
+ ```
22
+
23
+ Ingesta de datos
24
+
25
+ ```{r}
26
+ datos <- read.csv("cafeteria_UNAHUR.csv")
27
+ ```
28
+
29
+ 1. Escribir el modelo de regresión lineal simple que describa la influencia de la cantidad de productos pedidos en el tiempo de espera.
30
+
31
+ ```{r message=FALSE}
32
+ attach(datos)
33
+ modelo <- lm(tiempo_de_espera ~ Cantidad)
34
+ summary(modelo)
35
+ modelo$coefficients
36
+ detach(datos)
37
+ ```
38
+
39
+ 2. Representar gráficamente la relación entre la cantidad de productos pedidos y el tiempo de espera. Agregar al gráfico la recta de ajuste obtenida con el modelo de regresión lineal.
40
+
41
+ ```{r message=FALSE}
42
+ ggplot(data = na.omit(datos), aes(x = Cantidad, y = tiempo_de_espera)) +
43
+ geom_point(color = "darkseagreen") +
44
+ geom_abline(slope = 2.0731, intercept = -0.2111, color = "cyan") +
45
+ xlab("Cantidad") +
46
+ ylab("Tiempo de Espera") +
47
+ theme_hc()
48
+ ```
49
+
50
+ ```{r}
51
+ sum(datos$Cantidad > 5, na.rm = TRUE)
52
+ ```
53
+
54
+ 3. Calcular el error estándar residual y el coeficiente de determinación (R²) del modelo de regresión lineal. ¿Qué se puede concluir sobre la bondad de ajuste del modelo que relaciona la cantidad de productos pedidos con el tiempo de espera?
55
+
56
+ ```{r}
57
+ # Volvemos a llamar al resumen para extraer los valores.
58
+ resumen_modelo <- summary(modelo)
59
+
60
+ # Error Estándar Residual (RSE)
61
+ rse <- resumen_modelo$sigma
62
+ cat("El Error Estándar Residual (RSE) es:", rse, "\n")
63
+
64
+ # Coeficiente de Determinación (R-cuadrado)
65
+ r_cuadrado <- resumen_modelo$r.squared
66
+ cat("El Coeficiente de Determinación (R²) es:", r_cuadrado, "\n")
67
+ ```
68
+
69
+ El 94.85% de la variabilidad total en el tiempo_espera es explicada por la cantidad_productos pedidos. La cantidad de productos que pide una persona es un predictor muy fuerte de cuánto va a tardar su pedido. Solo un \~5% del tiempo de espera se debe a otros factores.
70
+
71
+ Respecto al RSE, Las predicciones del modelo sobre el tiempo de espera se desvían del valor real observado en solo 0.70 minutos (asumimos minutos). Dado el altísimo $R^2$, este error es considerado muy bajo, lo que refuerza la idea de que el modelo es muy preciso.
72
+
73
+ 4. Utilizar el modelo para predecir el tiempo de espera correspondiente a un pedido de 6 productos y para 12 productos. Representar ambos puntos en el gráfico anterior junto con la recta de ajuste.
74
+
75
+ ```{r message=FALSE}
76
+ productos_a_predecir <- data.frame(Cantidad = c(6, 12))
77
+ predicciones_datos <- predict(modelo, newdata = productos_a_predecir)
78
+
79
+ cat("Predicción para 6 productos:", predicciones_datos[1], "\n")
80
+ cat("Predicción para 12 productos:", predicciones_datos[2], "\n")
81
+
82
+ puntos_prediccion <- data.frame(
83
+ Cantidad = c(6, 12),
84
+ tiempo_de_espera = predicciones_datos
85
+ )
86
+
87
+ ggplot(data = na.omit(datos), aes(x = Cantidad, y = tiempo_de_espera)) +
88
+ geom_point(color = "darkseagreen") +
89
+ geom_abline(slope = 2.0731, intercept = -0.2111, color = "cyan") +
90
+ # Añadimos los puntos de predicción
91
+ geom_point(data = puntos_prediccion, color = "red", size = 4, shape = 17) +
92
+ xlab("Cantidad") +
93
+ ylab("Tiempo de Espera") +
94
+ theme_hc()
95
+ ```
96
+
97
+ 5. Estudiar la presencia de datos faltantes (NA) en cada una de las variables del conjunto de datos. Identificar en qué columnas se presentan y cuántos valores faltan en cada caso.
98
+
99
+ ```{r}
100
+ summary(datos)
101
+ ```
102
+
103
+ 6. Realizar una imputación de datos faltantes por la media para la variable Cantidad.
104
+
105
+ ```{r}
106
+ df_imp_media <- datos
107
+
108
+ media_cantidad <- mean(datos$Cantidad, na.rm = TRUE)
109
+
110
+ df_imp_media$Cantidad[is.na(df_imp_media$Cantidad)] <- as.integer(media_cantidad)
111
+ ```
112
+
113
+ 7. Realizar una imputación de datos faltantes de la columna cantidad por el método de Cohen.
114
+
115
+ ```{r}
116
+ # Calculamos la cantidad total de observaciones y la cantidad de datos no faltantes
117
+ n <- length(datos$Cantidad)
118
+ m <- length(na.omit(datos$Cantidad))
119
+ n_faltantes <- n - m
120
+
121
+ # Calculamos la media y el desvío estándar usando los datos disponibles
122
+ media <- mean(datos$Cantidad, na.rm = TRUE)
123
+ sigma <- sd(datos$Cantidad, na.rm = TRUE)
124
+
125
+ # Calculamos los valores de las imputaciones
126
+ factor <- sqrt((n + m - 1) / (n + m))
127
+ imp_bajo <- media - factor * sigma
128
+ imp_alto <- media + factor * sigma
129
+
130
+ # Dividimos los registros con datos faltantes a la mitad
131
+ # Posiciones donde hay datos faltantes
132
+ pos_na <- which(is.na(datos$Cantidad) == TRUE)
133
+
134
+ # Dividimos las posiciones a la mitad
135
+ pos1 <- pos_na[1:round(n_faltantes / 2)]
136
+ pos2 <- pos_na[(round(n_faltantes / 2) + 1):n_faltantes]
137
+
138
+ # Realizamos las imputaciones
139
+ df_imp_Cohen <- datos
140
+ df_imp_Cohen$Cantidad[pos1] <- as.integer(imp_bajo)
141
+ df_imp_Cohen$Cantidad[pos2] <- as.integer(imp_alto)
142
+ ```
143
+
144
+ 8. Realizar una imputación de datos faltantes de la columna cantidad por regresión lineal
145
+
146
+ ```{r}
147
+ datos_sin_na <- na.omit(datos)
148
+
149
+ # Calculamos el modelo
150
+ model <- lm(formula = Cantidad ~ ., data = datos_sin_na)
151
+ model
152
+
153
+ # Predecimos la edad usando los registros donde esta variable presenta valores faltantes
154
+ predicciones_rl <- predict(model, newdata = datos[pos_na,])
155
+
156
+ # Realizamos las imputaciones.
157
+ df_imp_rl <- datos
158
+ df_imp_rl$Cantidad[pos_na] <- as.integer(predicciones_rl)
159
+ ```
160
+
161
+ 9. Con todas las variables sin datos faltantes, realizar una imputación de datos faltantes por vecinos más cercanos utilizando la distancia de Manhattan.
162
+
163
+ ```{r}
164
+ # Función para calcular la distancia de Manhattan
165
+ dMan <- function(x, y){
166
+ return(sum(abs(x - y)))
167
+ }
168
+
169
+ # Distancia de Manhattan
170
+ distMan <- matrix(0, nrow = (n - m), ncol = m)
171
+ for (i in 1:(n - m)){
172
+ for (j in 1:m){
173
+ distMan[i,j] <- dMan(datos[pos_na[i], -1], datos_sin_na[j,-1])
174
+ }
175
+ }
176
+
177
+ # Vamos a calcular los registros que están a la mínima distancia.
178
+ minDistMan <- apply(distMan, 1, which.min)
179
+
180
+ # Realizamos las imputaciones.
181
+ df_imp_knn <- datos
182
+ df_imp_knn$Cantidad[pos_na] <- datos_sin_na$Cantidad[minDistMan]
183
+ ```
184
+
185
+ 10. Realizar un histograma de los datos disponibles para la cantidad y compararlo con los histogramas de cada uno de los métodos de imputación aplicados.
186
+
187
+ ```{r warning=FALSE}
188
+ # Armamos un data frame con las imputaciones
189
+ datos_comparacion <- data.frame(
190
+ Originales = datos$Cantidad,
191
+ Media = df_imp_media$Cantidad,
192
+ Cohen = df_imp_Cohen$Cantidad,
193
+ Regresion = df_imp_rl$Cantidad,
194
+ VecinosMan = df_imp_knn$Cantidad
195
+ )
196
+
197
+ # Armamos los histogramas
198
+ h1 <- ggplot(datos_comparacion, aes(x = Originales)) +
199
+ geom_histogram(fill = "gray80", color = "black", position = "identity", bins = 25) +
200
+ theme_classic() +
201
+ theme(plot.title = element_text(hjust = 0.5))
202
+
203
+ h2 <- ggplot(datos_comparacion, aes(x = Media)) +
204
+ geom_histogram(fill = "darkseagreen2", color = "darkseagreen", position = "identity", bins = 25) +
205
+ theme_classic() +
206
+ theme(plot.title = element_text(hjust = 0.5))
207
+
208
+ h3 <- ggplot(datos_comparacion, aes(x = Cohen)) +
209
+ geom_histogram(fill = "darkseagreen2", color = "darkseagreen", position = "identity", bins = 25) +
210
+ theme_classic() +
211
+ theme(plot.title = element_text(hjust = 0.5))
212
+
213
+ h4 <- ggplot(datos_comparacion, aes(x = Regresion)) +
214
+ geom_histogram(fill = "darkseagreen2", color = "darkseagreen", position = "identity", bins = 25) +
215
+ theme_classic() +
216
+ theme(plot.title = element_text(hjust = 0.5))
217
+
218
+ h5 <- ggplot(datos_comparacion, aes(x = VecinosMan)) +
219
+ geom_histogram(fill = "darkseagreen2", color = "darkseagreen", position = "identity", bins = 25) +
220
+ theme_classic() +
221
+ theme(plot.title = element_text(hjust = 0.5))
222
+
223
+ # Mostramos los gráficos uno por uno
224
+ h1
225
+ h2
226
+ h3
227
+ h4
228
+ h5
229
+ ```
230
+
231
+ ```{r}
232
+ df_original <- data.frame(Valor = datos$Cantidad, Metodo = "Original")
233
+ df_media <- data.frame(Valor = df_imp_media$Cantidad, Metodo = "Media")
234
+ df_cohen <- data.frame(Valor = df_imp_Cohen$Cantidad, Metodo = "Cohen")
235
+ df_regresion <- data.frame(Valor = df_imp_rl$Cantidad, Metodo = "Regresion")
236
+ df_knn <- data.frame(Valor = df_imp_knn$Cantidad, Metodo = "kNN")
237
+
238
+ df_comparacion_largo <- rbind(df_original, df_media, df_cohen, df_regresion, df_knn)
239
+
240
+ conteo_completo <- table(Metodo = df_comparacion_largo$Metodo,
241
+ Valor = df_comparacion_largo$Valor,
242
+ useNA = "ifany") # useNA muestra la columna <NA>
243
+
244
+ print(conteo_completo)
245
+ ```
246
+
247
+ ```{r warning=FALSE, message=FALSE}
248
+ # --- Análisis de los NAs según Tiempo de Espera ---
249
+
250
+ # 1. Preparamos los datos completos
251
+ # Filtramos solo cantidades de 1 a 5 para no ensuciar el gráfico con outliers
252
+ datos_completos <- subset(datos, !is.na(Cantidad) & Cantidad <= 5)
253
+ datos_completos$Tipo <- as.factor(datos_completos$Cantidad) # Convertimos a factor para colorear
254
+
255
+ # 2. Preparamos los datos NA
256
+ datos_na <- subset(datos, is.na(Cantidad))
257
+ datos_na$Tipo <- "NA (Faltantes)"
258
+
259
+ # 3. Combinamos para graficar
260
+ # Seleccionamos solo las columnas necesarias
261
+ df_plot <- rbind(
262
+ data.frame(tiempo = datos_completos$tiempo_de_espera, Grupo = paste("Cant =", datos_completos$Tipo)),
263
+ data.frame(tiempo = datos_na$tiempo_de_espera, Grupo = "Datos Faltantes (NA)")
264
+ )
265
+
266
+ # 4. Gráfico de Densidad Comparativo
267
+ ggplot(df_plot, aes(x = tiempo, fill = Grupo, color = Grupo)) +
268
+ # Dibujamos las curvas de densidad con transparencia
269
+ geom_density(alpha = 0.3) +
270
+
271
+ # Personalización
272
+ scale_fill_manual(values = c("red", "blue", "green", "orange", "purple", "black")) +
273
+ scale_color_manual(values = c("red", "blue", "green", "orange", "purple", "black")) +
274
+
275
+ labs(title = "Distribución del Tiempo de Espera: Datos Completos vs NAs",
276
+ subtitle = "¿A qué grupo se parecen más los datos faltantes?",
277
+ x = "Tiempo de Espera (min)",
278
+ y = "Densidad") +
279
+ theme_minimal() +
280
+ # Limitamos el eje X para ver mejor la zona importante (0 a 15 min)
281
+ coord_cartesian(xlim = c(0, 15))
282
+ ```
283
+
284
+ ```{r}
285
+ write.csv(datos, "tp3_regresion_imputacion.csv", row.names = FALSE)
286
+ ```
r_scripts/script_tp2.R ADDED
@@ -0,0 +1,265 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #' ---
2
+ #' title: "Trabajo Práctico 2"
3
+ #' author: "Occhiuzzi, Pablo y Ojeda, Julián"
4
+ #' format: pdf
5
+ #' editor: visual
6
+ #' editor_options:
7
+ #' chunk_output_type: inline
8
+ #' ---
9
+ #'
10
+ #' # Análisis Exploratorio de Datos
11
+ #'
12
+ #' Tras el análisis realizado en el TP1, la cafetería de especialidad de UNAHUR puso carritos de café en cada sede de UNAHUR y decidió poner a prueba sus ventas con un combo de "Café + Medialuna" a \$2.000.
13
+ #'
14
+ #' En el archivo `Visitas_cafeteria_UNAHUR.csv` se encuentran los datos correspondientes a un conjunto de ventas del combo en las diferentes sedes.
15
+ #'
16
+ #' Para la correcta realización del presente Trabajo Práctico, se pide dar las respuestas solicitadas e introducir, dentro del *chunk* correspondiente, el código desarrollado para obtener cada una de ellas. Incluir además, los paquetes utilizados. La idea es que una persona pueda correr este script sin ningún mensaje de error.
17
+ #'
18
+ #' 0. Paquetes necesarios.
19
+ #'
20
+ ## -----------------------------------------------------------------------------
21
+ library(ggplot2)
22
+ library(dplyr)
23
+ library(scales)
24
+ library(corrplot)
25
+
26
+ #'
27
+ #' 1. Leer el archivo. Chequear que el formato sea `data.frame` y, en caso, contrario, cambiarlo.
28
+ #'
29
+ ## -----------------------------------------------------------------------------
30
+ datos <- Visitas_cafeteria_UNAHUR
31
+ cat("¿Es un data frame?:", is.data.frame(datos))
32
+
33
+ #'
34
+ #' 2. Luego de eliminar registros con datos faltantes, informar las siguientes medidas para la cantidad de visitantes: rango, media, mediana, desvío estándar y rango intercuartil.
35
+ #'
36
+ ## -----------------------------------------------------------------------------
37
+ datos_limpios <- na.omit(datos)
38
+ rango_cantidad_visitanes <- range(datos_limpios$cantidad_visitantes)
39
+ media_cantidad_visitantes <- mean(datos_limpios$cantidad_visitantes)
40
+ mediana_cantidad_visitantes <- median(datos_limpios$cantidad_visitantes)
41
+ desvio_estandar_cantidad_visitantes <- sd(datos_limpios$cantidad_visitantes)
42
+ iqr_cantidad_visitantes <- IQR(datos_limpios$cantidad_visitantes)
43
+
44
+ # 1. Rango
45
+ cat("1. RANGO:\n")
46
+ cat(" - Valor Mínimo:", rango_cantidad_visitanes[1], "\n")
47
+ cat(" - Valor Máximo:", rango_cantidad_visitanes[2], "\n")
48
+ cat(" - Amplitud Total:", rango_cantidad_visitanes[2] - rango_cantidad_visitanes[1], "\n\n")
49
+
50
+ # 2. Media
51
+ cat("2. MEDIA (Promedio):\n")
52
+ cat(" - Media:", round(media_cantidad_visitantes, 2), "\n\n")
53
+
54
+ # 3. Mediana
55
+ cat("3. MEDIANA (Valor Central):\n")
56
+ cat(" - Mediana:", mediana_cantidad_visitantes, "\n\n")
57
+
58
+ # 4. Desvío Estándar
59
+ cat("4. DESVÍO ESTÁNDAR (Dispersión):\n")
60
+ cat(" - Desvío Estándar:", round(desvio_estandar_cantidad_visitantes, 2), "\n\n")
61
+
62
+ # 5. Rango Intercuartil (IQR)
63
+ cat("5. RANGO INTERCUARTIL (IQR):\n")
64
+ cat(" - IQR (Q3 - Q1):", iqr_cantidad_visitantes, "\n")
65
+
66
+ #'
67
+ #' 3. Realizar un histograma de los datos sobre la cantidad de visitantes y superponerle la curva Normal ¿Qué puede observarse?
68
+ #'
69
+ ## -----------------------------------------------------------------------------
70
+ hist(datos_limpios$cantidad_visitantes, breaks = 20, freq = FALSE,
71
+ main = "Distribución de visitantes",
72
+ xlab = "Cantidad de visitantes", col = "lightblue")
73
+ curve(dnorm(x, mean(datos_limpios$cantidad_visitantes), sd(datos_limpios$cantidad_visitantes)),
74
+ add = TRUE, col = "red", lwd = 2)
75
+
76
+ hist(datos_limpios$cantidad_visitantes, breaks = 200, freq = FALSE,
77
+ main = "Distribución de visitantes",
78
+ xlab = "Cantidad de visitantes", col = "lightblue")
79
+ curve(dnorm(x, mean(datos_limpios$cantidad_visitantes), sd(datos_limpios$cantidad_visitantes)),
80
+ add = TRUE, col = "red", lwd = 2)
81
+
82
+ #'
83
+ #' 4. Dividir la cantidad de visitantes por sede y usando un grafíco de tipo boxplot, analizar los resultados.
84
+ #'
85
+ ## -----------------------------------------------------------------------------
86
+ datos_limpios[datos_limpios$sede == "Trabajo_argentino", "sede"] <- "Trabajo_Argentino"
87
+
88
+ ggplot(datos_limpios, aes(x = sede, y = cantidad_visitantes, fill = sede)) +
89
+ geom_boxplot() +
90
+ labs(title = "Distribución de Visitantes por Sede",
91
+ x = "Sede",
92
+ y = "Cantidad de Visitantes") +
93
+ theme(legend.position = "none") +
94
+ coord_flip()
95
+
96
+ #'
97
+ #' 5. Representar el monto por sede con un gráfico de barras e indicar cuáles son las sedes con menor y mayor monto total de ventas
98
+ #'
99
+ ## -----------------------------------------------------------------------------
100
+ gasto_por_sede <- datos_limpios %>%
101
+ group_by(sede) %>%
102
+ summarise(Gasto_Total = sum(gasto_total))
103
+
104
+ sede_mayor_monto <- gasto_por_sede$sede[which.max(gasto_por_sede$Gasto_Total)]
105
+ mayor_monto <- max(gasto_por_sede$Gasto_Total)
106
+ sede_menor_monto <- gasto_por_sede$sede[which.min(gasto_por_sede$Gasto_Total)]
107
+ menor_monto <- min(gasto_por_sede$Gasto_Total)
108
+
109
+ cat("MAYOR:", sede_mayor_monto, "($", format(mayor_monto,
110
+ big.mark = ".",
111
+ decimal.mark = ","), ") \n")
112
+ cat("MENOR:", sede_menor_monto, "($", format(menor_monto,
113
+ big.mark = ".",
114
+ decimal.mark = ","), ")\n")
115
+
116
+ ggplot(
117
+ gasto_por_sede,
118
+ aes(x = reorder(sede, Gasto_Total),
119
+ y = Gasto_Total,
120
+ fill = sede)) +
121
+ geom_col() +
122
+ coord_flip() +
123
+ scale_y_continuous(labels = scales::comma_format(
124
+ big.mark = ".",
125
+ decimal.mark = ",")) +
126
+ labs(title = "Gasto Total por Sede",
127
+ x = "Sede",
128
+ y = "Gasto Total") +
129
+ theme(legend.position = "none")
130
+
131
+ #'
132
+ #' 6. Realizar un diagrama de dispersión con los montos de ventas y las propinas para cada sede. ¿Qué puede observarse? Justificar con una medida cuantitativa.
133
+ #'
134
+ ## -----------------------------------------------------------------------------
135
+ ggplot(datos_limpios, aes(x = gasto_total, y = propina)) +
136
+ geom_point(alpha = 0.5, color = "darkblue") +
137
+ facet_wrap(~ sede, ncol = 4) +
138
+ labs(
139
+ title = "Vista General: Relación Monto vs. Propina en Todas las Sedes",
140
+ x = "Monto de la Venta ($)",
141
+ y = "Propina ($)"
142
+ ) +
143
+ theme_bw() +
144
+ theme(axis.text.x = element_text(angle = 45, hjust = 1))
145
+
146
+ # Obtenemos la lista de todas las sedes únicas
147
+ sedes_unicas <- unique(datos_limpios$sede)
148
+
149
+ # Usamos un bucle 'for' para recorrer cada sede
150
+ for (sede_actual in sedes_unicas) {
151
+
152
+ # Filtramos los datos para la sede actual
153
+ datos_filtrados <- subset(datos_limpios, sede == sede_actual)
154
+
155
+ # Creamos el gráfico para la sede actual
156
+ grafico_detalle <- ggplot(datos_filtrados, aes(x = gasto_total, y = propina)) +
157
+ geom_point(alpha = 0.6, color = "darkgreen", size = 2.5) +
158
+ labs(
159
+ title = paste("Detalle Sede:", sede_actual),
160
+ x = "Monto de la Venta ($)",
161
+ y = "Propina ($)"
162
+ ) +
163
+ theme_bw(base_size = 14)
164
+
165
+ # Imprimimos el gráfico para la sede actual
166
+ print(grafico_detalle)
167
+ }
168
+
169
+ # Calculamos el coeficiente de correlación de Pearson para todo el dataset
170
+ correlacion_gastos_y_propina <- cor(datos_limpios$gasto_total, datos_limpios$propina)
171
+
172
+ # Imprimimos el resultado en la consola
173
+ print(paste("El coeficiente de correlación de Pearson entre monto y propina es:", round(correlacion_gastos_y_propina, 3)))
174
+
175
+ #'
176
+ #' 7. Calcular las matrices de covarianzas y de correlaciones. A partir de estas matrices dar un ejemplo de variables fuertemente correlacionadas positivamente, de variables fuertemente correlacionadas negativamente y de variables no correlacionadas.
177
+ #'
178
+ ## -----------------------------------------------------------------------------
179
+ # Seleccionamos todas las columnas numéricas
180
+ datos_numericos <- datos_limpios[ , c(
181
+ "cantidad_visitantes",
182
+ "cantidad_combos",
183
+ "gasto_total",
184
+ "propina",
185
+ "tiempo_espera",
186
+ "satisfaccion_cliente",
187
+ "numero_de_mesas_disponibles"
188
+ )]
189
+
190
+ options(width = 80) # (mejora visual de la salida)
191
+
192
+ # Calculamos la matriz de covarianzas.
193
+ print("--- Matriz de Covarianzas ---")
194
+ cov(datos_numericos)
195
+
196
+ # Calculamos la matriz de correlaciones
197
+ matriz_cor <- cor(datos_numericos)
198
+
199
+ corrplot(
200
+ matriz_cor,
201
+ tl.col = "black",
202
+ tl.srt = 45
203
+ )
204
+
205
+ #'
206
+ #' 8. Generar una serie de tiempo transformando los datos para obtener la cantidad de visitas totales por mes (sumando todas las sedes).
207
+ #'
208
+ ## -----------------------------------------------------------------------------
209
+ serie_visitantes_tsa <- readRDS("serie_visitantes_ts.rds")
210
+
211
+ plot.ts(serie_visitantes_tsa,
212
+ main = "Visitas Totales por Mes",
213
+ xlab = "Tiempo",
214
+ ylab = "Cantidad Total de Visitantes",
215
+ col = "steelblue",
216
+ lwd = 2)
217
+
218
+ #'
219
+ #' 9. Utilizando la descomposicióon aditiva, graficar:
220
+ #'
221
+ #' a) La serie de tiempo original eliminando la componente de tendencia.
222
+ #'
223
+ #' b) La serie de tiempo original eliminando la componente de estacionalidad.
224
+ #'
225
+ ## -----------------------------------------------------------------------------
226
+ serie_visitantes_da <- decompose(serie_visitantes_tsa, type = 'additive')
227
+ serie_visitantes_detrend_a <- serie_visitantes_tsa - serie_visitantes_da$trend
228
+ plot.ts(
229
+ serie_visitantes_detrend_a,
230
+ col = "darkseagreen4",
231
+ main = "TS sin tendencia",
232
+ xlab = "Mes",
233
+ ylab = "Visitas"
234
+ )
235
+
236
+ serie_visitantes_deseasonal_a <- serie_visitantes_tsa - serie_visitantes_da$seasonal
237
+ plot.ts(
238
+ serie_visitantes_deseasonal_a,
239
+ col = "darkseagreen4",
240
+ main = "TS sin estacionalidad",
241
+ xlab = "Mes",
242
+ ylab = "Visitas"
243
+ )
244
+
245
+ #'
246
+ #' 10. Graficar las funciones de autocorrelacióon de la serie de tiempo original y de la componente residual. ¿Qué puede observarse sobre la estacionariedad de estas series?
247
+ #'
248
+ ## -----------------------------------------------------------------------------
249
+ acf(serie_visitantes_tsa, lag.max = 60, ci.col="cyan", type = "correlation", main = "TS Original")
250
+
251
+ residuo_da <- decompose(serie_visitantes_tsa, type = "additive")$random
252
+ acf(residuo_da, lag.max = 60, ci.col="cyan", type = "correlation", main = "TS Residual", na.action = na.pass)
253
+
254
+ #'
255
+ ## -----------------------------------------------------------------------------
256
+ write.csv(datos_limpios, "tp2_datos_limpios.csv", row.names = FALSE)
257
+
258
+ #'
259
+ ## -----------------------------------------------------------------------------
260
+ df_serie <- data.frame(
261
+ tiempo = as.numeric(time(serie_visitantes_tsa)),
262
+ visitas = as.numeric(serie_visitantes_tsa)
263
+ )
264
+ write.csv(df_serie, "tp2_serie_temporal.csv", row.names = FALSE)
265
+
r_scripts/script_tp3.R ADDED
@@ -0,0 +1,286 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #' ---
2
+ #' title: "TP3"
3
+ #' author: "Pablo Occhiuzzi, Julián Ojeda"
4
+ #' format: pdf
5
+ #' editor: visual
6
+ #' ---
7
+ #'
8
+ #' # Trabajo Práctico N°3
9
+ #'
10
+ #' Para este trabajo se les proporciona el archivo `cafeteria_UNAHUR.csv`, usado en el TP1, que contiene registros correspondientes a transacciones en la cafetería. El dataset incluye información de la cantidad de productos adquiridos y el tiempo de preparación.
11
+ #'
12
+ #' Para la correcta realización del presente Trabajo Práctico, se pide dar las respuestas solicitadas e introducir, dentro del chunk correspondiente, el código desarrollado para obtener cada una de ellas. Incluir además, los paquetes utilizados. La idea es que una persona pueda correr este script sin ningún mensaje de error.
13
+ #'
14
+ #' Paquetes utilizados
15
+ #'
16
+ ## --------------------------------------------------------------------------------
17
+ install.packages("ggplot2")
18
+ install.packages("ggthemes")
19
+ library(ggplot2)
20
+ library(ggthemes)
21
+
22
+ #'
23
+ #' Ingesta de datos
24
+ #'
25
+ ## --------------------------------------------------------------------------------
26
+ datos <- read.csv("cafeteria_UNAHUR.csv")
27
+
28
+ #'
29
+ #' 1. Escribir el modelo de regresión lineal simple que describa la influencia de la cantidad de productos pedidos en el tiempo de espera.
30
+ #'
31
+ ## ----message=FALSE---------------------------------------------------------------
32
+ attach(datos)
33
+ modelo <- lm(tiempo_de_espera ~ Cantidad)
34
+ summary(modelo)
35
+ modelo$coefficients
36
+ detach(datos)
37
+
38
+ #'
39
+ #' 2. Representar gráficamente la relación entre la cantidad de productos pedidos y el tiempo de espera. Agregar al gráfico la recta de ajuste obtenida con el modelo de regresión lineal.
40
+ #'
41
+ ## ----message=FALSE---------------------------------------------------------------
42
+ ggplot(data = na.omit(datos), aes(x = Cantidad, y = tiempo_de_espera)) +
43
+ geom_point(color = "darkseagreen") +
44
+ geom_abline(slope = 2.0731, intercept = -0.2111, color = "cyan") +
45
+ xlab("Cantidad") +
46
+ ylab("Tiempo de Espera") +
47
+ theme_hc()
48
+
49
+ #'
50
+ ## --------------------------------------------------------------------------------
51
+ sum(datos$Cantidad > 5, na.rm = TRUE)
52
+
53
+ #'
54
+ #' 3. Calcular el error estándar residual y el coeficiente de determinación (R²) del modelo de regresión lineal. ¿Qué se puede concluir sobre la bondad de ajuste del modelo que relaciona la cantidad de productos pedidos con el tiempo de espera?
55
+ #'
56
+ ## --------------------------------------------------------------------------------
57
+ # Volvemos a llamar al resumen para extraer los valores.
58
+ resumen_modelo <- summary(modelo)
59
+
60
+ # Error Estándar Residual (RSE)
61
+ rse <- resumen_modelo$sigma
62
+ cat("El Error Estándar Residual (RSE) es:", rse, "\n")
63
+
64
+ # Coeficiente de Determinación (R-cuadrado)
65
+ r_cuadrado <- resumen_modelo$r.squared
66
+ cat("El Coeficiente de Determinación (R²) es:", r_cuadrado, "\n")
67
+
68
+ #'
69
+ #' El 94.85% de la variabilidad total en el tiempo_espera es explicada por la cantidad_productos pedidos. La cantidad de productos que pide una persona es un predictor muy fuerte de cuánto va a tardar su pedido. Solo un \~5% del tiempo de espera se debe a otros factores.
70
+ #'
71
+ #' Respecto al RSE, Las predicciones del modelo sobre el tiempo de espera se desvían del valor real observado en solo 0.70 minutos (asumimos minutos). Dado el altísimo $R^2$, este error es considerado muy bajo, lo que refuerza la idea de que el modelo es muy preciso.
72
+ #'
73
+ #' 4. Utilizar el modelo para predecir el tiempo de espera correspondiente a un pedido de 6 productos y para 12 productos. Representar ambos puntos en el gráfico anterior junto con la recta de ajuste.
74
+ #'
75
+ ## ----message=FALSE---------------------------------------------------------------
76
+ productos_a_predecir <- data.frame(Cantidad = c(6, 12))
77
+ predicciones_datos <- predict(modelo, newdata = productos_a_predecir)
78
+
79
+ cat("Predicción para 6 productos:", predicciones_datos[1], "\n")
80
+ cat("Predicción para 12 productos:", predicciones_datos[2], "\n")
81
+
82
+ puntos_prediccion <- data.frame(
83
+ Cantidad = c(6, 12),
84
+ tiempo_de_espera = predicciones_datos
85
+ )
86
+
87
+ ggplot(data = na.omit(datos), aes(x = Cantidad, y = tiempo_de_espera)) +
88
+ geom_point(color = "darkseagreen") +
89
+ geom_abline(slope = 2.0731, intercept = -0.2111, color = "cyan") +
90
+ # Añadimos los puntos de predicción
91
+ geom_point(data = puntos_prediccion, color = "red", size = 4, shape = 17) +
92
+ xlab("Cantidad") +
93
+ ylab("Tiempo de Espera") +
94
+ theme_hc()
95
+
96
+ #'
97
+ #' 5. Estudiar la presencia de datos faltantes (NA) en cada una de las variables del conjunto de datos. Identificar en qué columnas se presentan y cuántos valores faltan en cada caso.
98
+ #'
99
+ ## --------------------------------------------------------------------------------
100
+ summary(datos)
101
+
102
+ #'
103
+ #' 6. Realizar una imputación de datos faltantes por la media para la variable Cantidad.
104
+ #'
105
+ ## --------------------------------------------------------------------------------
106
+ df_imp_media <- datos
107
+
108
+ media_cantidad <- mean(datos$Cantidad, na.rm = TRUE)
109
+
110
+ df_imp_media$Cantidad[is.na(df_imp_media$Cantidad)] <- as.integer(media_cantidad)
111
+
112
+ #'
113
+ #' 7. Realizar una imputación de datos faltantes de la columna cantidad por el método de Cohen.
114
+ #'
115
+ ## --------------------------------------------------------------------------------
116
+ # Calculamos la cantidad total de observaciones y la cantidad de datos no faltantes
117
+ n <- length(datos$Cantidad)
118
+ m <- length(na.omit(datos$Cantidad))
119
+ n_faltantes <- n - m
120
+
121
+ # Calculamos la media y el desvío estándar usando los datos disponibles
122
+ media <- mean(datos$Cantidad, na.rm = TRUE)
123
+ sigma <- sd(datos$Cantidad, na.rm = TRUE)
124
+
125
+ # Calculamos los valores de las imputaciones
126
+ factor <- sqrt((n + m - 1) / (n + m))
127
+ imp_bajo <- media - factor * sigma
128
+ imp_alto <- media + factor * sigma
129
+
130
+ # Dividimos los registros con datos faltantes a la mitad
131
+ # Posiciones donde hay datos faltantes
132
+ pos_na <- which(is.na(datos$Cantidad) == TRUE)
133
+
134
+ # Dividimos las posiciones a la mitad
135
+ pos1 <- pos_na[1:round(n_faltantes / 2)]
136
+ pos2 <- pos_na[(round(n_faltantes / 2) + 1):n_faltantes]
137
+
138
+ # Realizamos las imputaciones
139
+ df_imp_Cohen <- datos
140
+ df_imp_Cohen$Cantidad[pos1] <- as.integer(imp_bajo)
141
+ df_imp_Cohen$Cantidad[pos2] <- as.integer(imp_alto)
142
+
143
+ #'
144
+ #' 8. Realizar una imputación de datos faltantes de la columna cantidad por regresión lineal
145
+ #'
146
+ ## --------------------------------------------------------------------------------
147
+ datos_sin_na <- na.omit(datos)
148
+
149
+ # Calculamos el modelo
150
+ model <- lm(formula = Cantidad ~ ., data = datos_sin_na)
151
+ model
152
+
153
+ # Predecimos la edad usando los registros donde esta variable presenta valores faltantes
154
+ predicciones_rl <- predict(model, newdata = datos[pos_na,])
155
+
156
+ # Realizamos las imputaciones.
157
+ df_imp_rl <- datos
158
+ df_imp_rl$Cantidad[pos_na] <- as.integer(predicciones_rl)
159
+
160
+ #'
161
+ #' 9. Con todas las variables sin datos faltantes, realizar una imputación de datos faltantes por vecinos más cercanos utilizando la distancia de Manhattan.
162
+ #'
163
+ ## --------------------------------------------------------------------------------
164
+ # Función para calcular la distancia de Manhattan
165
+ dMan <- function(x, y){
166
+ return(sum(abs(x - y)))
167
+ }
168
+
169
+ # Distancia de Manhattan
170
+ distMan <- matrix(0, nrow = (n - m), ncol = m)
171
+ for (i in 1:(n - m)){
172
+ for (j in 1:m){
173
+ distMan[i,j] <- dMan(datos[pos_na[i], -1], datos_sin_na[j,-1])
174
+ }
175
+ }
176
+
177
+ # Vamos a calcular los registros que están a la mínima distancia.
178
+ minDistMan <- apply(distMan, 1, which.min)
179
+
180
+ # Realizamos las imputaciones.
181
+ df_imp_knn <- datos
182
+ df_imp_knn$Cantidad[pos_na] <- datos_sin_na$Cantidad[minDistMan]
183
+
184
+ #'
185
+ #' 10. Realizar un histograma de los datos disponibles para la cantidad y compararlo con los histogramas de cada uno de los métodos de imputación aplicados.
186
+ #'
187
+ ## ----warning=FALSE---------------------------------------------------------------
188
+ # Armamos un data frame con las imputaciones
189
+ datos_comparacion <- data.frame(
190
+ Originales = datos$Cantidad,
191
+ Media = df_imp_media$Cantidad,
192
+ Cohen = df_imp_Cohen$Cantidad,
193
+ Regresion = df_imp_rl$Cantidad,
194
+ VecinosMan = df_imp_knn$Cantidad
195
+ )
196
+
197
+ # Armamos los histogramas
198
+ h1 <- ggplot(datos_comparacion, aes(x = Originales)) +
199
+ geom_histogram(fill = "gray80", color = "black", position = "identity", bins = 25) +
200
+ theme_classic() +
201
+ theme(plot.title = element_text(hjust = 0.5))
202
+
203
+ h2 <- ggplot(datos_comparacion, aes(x = Media)) +
204
+ geom_histogram(fill = "darkseagreen2", color = "darkseagreen", position = "identity", bins = 25) +
205
+ theme_classic() +
206
+ theme(plot.title = element_text(hjust = 0.5))
207
+
208
+ h3 <- ggplot(datos_comparacion, aes(x = Cohen)) +
209
+ geom_histogram(fill = "darkseagreen2", color = "darkseagreen", position = "identity", bins = 25) +
210
+ theme_classic() +
211
+ theme(plot.title = element_text(hjust = 0.5))
212
+
213
+ h4 <- ggplot(datos_comparacion, aes(x = Regresion)) +
214
+ geom_histogram(fill = "darkseagreen2", color = "darkseagreen", position = "identity", bins = 25) +
215
+ theme_classic() +
216
+ theme(plot.title = element_text(hjust = 0.5))
217
+
218
+ h5 <- ggplot(datos_comparacion, aes(x = VecinosMan)) +
219
+ geom_histogram(fill = "darkseagreen2", color = "darkseagreen", position = "identity", bins = 25) +
220
+ theme_classic() +
221
+ theme(plot.title = element_text(hjust = 0.5))
222
+
223
+ # Mostramos los gráficos uno por uno
224
+ h1
225
+ h2
226
+ h3
227
+ h4
228
+ h5
229
+
230
+ #'
231
+ ## --------------------------------------------------------------------------------
232
+ df_original <- data.frame(Valor = datos$Cantidad, Metodo = "Original")
233
+ df_media <- data.frame(Valor = df_imp_media$Cantidad, Metodo = "Media")
234
+ df_cohen <- data.frame(Valor = df_imp_Cohen$Cantidad, Metodo = "Cohen")
235
+ df_regresion <- data.frame(Valor = df_imp_rl$Cantidad, Metodo = "Regresion")
236
+ df_knn <- data.frame(Valor = df_imp_knn$Cantidad, Metodo = "kNN")
237
+
238
+ df_comparacion_largo <- rbind(df_original, df_media, df_cohen, df_regresion, df_knn)
239
+
240
+ conteo_completo <- table(Metodo = df_comparacion_largo$Metodo,
241
+ Valor = df_comparacion_largo$Valor,
242
+ useNA = "ifany") # useNA muestra la columna <NA>
243
+
244
+ print(conteo_completo)
245
+
246
+ #'
247
+ ## ----warning=FALSE, message=FALSE------------------------------------------------
248
+ # --- Análisis de los NAs según Tiempo de Espera ---
249
+
250
+ # 1. Preparamos los datos completos
251
+ # Filtramos solo cantidades de 1 a 5 para no ensuciar el gráfico con outliers
252
+ datos_completos <- subset(datos, !is.na(Cantidad) & Cantidad <= 5)
253
+ datos_completos$Tipo <- as.factor(datos_completos$Cantidad) # Convertimos a factor para colorear
254
+
255
+ # 2. Preparamos los datos NA
256
+ datos_na <- subset(datos, is.na(Cantidad))
257
+ datos_na$Tipo <- "NA (Faltantes)"
258
+
259
+ # 3. Combinamos para graficar
260
+ # Seleccionamos solo las columnas necesarias
261
+ df_plot <- rbind(
262
+ data.frame(tiempo = datos_completos$tiempo_de_espera, Grupo = paste("Cant =", datos_completos$Tipo)),
263
+ data.frame(tiempo = datos_na$tiempo_de_espera, Grupo = "Datos Faltantes (NA)")
264
+ )
265
+
266
+ # 4. Gráfico de Densidad Comparativo
267
+ ggplot(df_plot, aes(x = tiempo, fill = Grupo, color = Grupo)) +
268
+ # Dibujamos las curvas de densidad con transparencia
269
+ geom_density(alpha = 0.3) +
270
+
271
+ # Personalización
272
+ scale_fill_manual(values = c("red", "blue", "green", "orange", "purple", "black")) +
273
+ scale_color_manual(values = c("red", "blue", "green", "orange", "purple", "black")) +
274
+
275
+ labs(title = "Distribución del Tiempo de Espera: Datos Completos vs NAs",
276
+ subtitle = "¿A qué grupo se parecen más los datos faltantes?",
277
+ x = "Tiempo de Espera (min)",
278
+ y = "Densidad") +
279
+ theme_minimal() +
280
+ # Limitamos el eje X para ver mejor la zona importante (0 a 15 min)
281
+ coord_cartesian(xlim = c(0, 15))
282
+
283
+ #'
284
+ ## --------------------------------------------------------------------------------
285
+ write.csv(datos, "tp3_regresion_imputacion.csv", row.names = FALSE)
286
+
requirements.txt CHANGED
@@ -2,4 +2,5 @@ streamlit
2
  pandas
3
  plotly
4
  numpy
5
- scikit-learn
 
 
2
  pandas
3
  plotly
4
  numpy
5
+ scikit-learn
6
+ prophet