# Guía para el Caso de Estudio: Aprendizaje No Supervisado + Dashboard
### Maestría en Sistemas de Información — PUCE

**Dataset:** `sri_recaudacion_2026.csv` — Recaudación tributaria del Servicio de Rentas Internas (SRI) del Ecuador, enero a mayo de 2026.

---

## 1. Objetivos de aprendizaje

Al finalizar el caso, el estudiante será capaz de:

1. **Entrenar un modelo de aprendizaje no supervisado** (clustering) sobre datos reales de recaudación tributaria, justificando el preprocesamiento, la elección del algoritmo y la evaluación de los resultados.
2. **Construir un dashboard interactivo** que comunique tanto el análisis exploratorio como los hallazgos del modelo a una audiencia no técnica (p. ej., un director de planificación del SRI).

> El énfasis no está solo en el código, sino en la **narrativa analítica**: ¿qué patrones se descubrieron y qué decisión podrían informar?

---

## 2. Conocimiento del dataset

El archivo está delimitado por **`|`** (pipe) y los decimales usan **coma** (`139,5400`). Tiene ~304.000 filas y 11 columnas.

| Columna | Tipo | Descripción | Notas |
|---|---|---|---|
| `ANIO` | entero | Año fiscal | Constante = 2026 (descartable como variable) |
| `MES` | texto | Mes (`01 Enero` … `05 Mayo`) | Variable temporal; conviene extraer el número |
| `GRUPO_IMPUESTO` | categórica | Macro-grupo (IVA, Renta, ICE, Vehículos, Multas…) | 15 categorías |
| `SUBGRUPO_IMPUESTO` | categórica | Detalle del grupo | Alta cardinalidad |
| `IMPUESTO` | categórica | Concepto específico | Alta cardinalidad |
| `GRAN_CONTRIBUYENTE` | categórica | Marca de gran contribuyente (`NO`, `G`, `Q`, `S`) | Casi todo `NO` (desbalance) |
| `CODIGO_OPERA_FAMILIA` | categórica | Código interno de familia de operación | A–I, 9… |
| `TIPO_CONTRIBUYENTE` | categórica | `PERSONAS NATURALES`, `SOCIEDADES`, `NO TIENE` | |
| `PROVINCIA` | categórica | 25 provincias | |
| `CANTON` | categórica | 221 cantones | |
| `VALOR_RECAUDADO` | numérica | Monto recaudado (USD) | **Variable clave**; decimal con coma |

**Granularidad:** cada fila es la recaudación de un concepto de impuesto, en un mes, para una combinación provincia/cantón/tipo de contribuyente. Es un dataset **transaccional/agregado**, no de individuos.

### Decisión metodológica importante
El aprendizaje no supervisado necesita **un vector de características por cada objeto a agrupar**. Las filas individuales no son los "objetos" interesantes. Lo valioso es **construir un perfil** por una unidad de análisis. Se recomienda elegir una de estas opciones:

- **Opción A (recomendada): perfil por CANTÓN.** Agrupar los 221 cantones según su *composición tributaria* (qué % recauda en IVA, Renta, ICE, etc.) y su volumen. Pregunta de negocio: *"¿Qué tipos de economías cantonales existen en el Ecuador según su estructura de recaudación?"*
- **Opción B: perfil por PROVINCIA** (25 objetos — pocos para clustering robusto, pero más interpretable).
- **Opción C: segmentación de conceptos de impuesto** por su comportamiento mensual.

> El resto de la guía desarrolla la **Opción A**. Es trasladable a las demás.

---

## 3. Parte 1 — Entrenar el modelo no supervisado

### 3.1 Entorno

```bash
python -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate
pip install pandas numpy scikit-learn matplotlib seaborn plotly streamlit
```

### 3.2 Carga y limpieza

```python
import pandas as pd
import numpy as np

df = pd.read_csv(
    "sri_recaudacion_2026.csv",
    sep="|",
    decimal=",",          # decimales con coma
    encoding="utf-8",
)

# Normalizar tipos
df["VALOR_RECAUDADO"] = pd.to_numeric(df["VALOR_RECAUDADO"], errors="coerce")
df["MES_NUM"] = df["MES"].str.extract(r"(\d+)").astype(int)

# Saneamiento básico
df = df.dropna(subset=["VALOR_RECAUDADO", "CANTON", "PROVINCIA"])
df = df[df["VALOR_RECAUDADO"] >= 0]          # descartar valores negativos/atípicos si no aplican

print(df.shape)
print(df["GRUPO_IMPUESTO"].value_counts())
```

**Checklist de calidad de datos (documentar en el informe):**
- Nulos por columna (`df.isna().sum()`).
- Valores negativos en `VALOR_RECAUDADO` (¿notas de crédito/devoluciones?).
- Outliers extremos (Quito y Guayaquil dominarán los montos).
- Categorías raras (p. ej. `IMP.ING EXTRAORD` con 1 sola fila).

### 3.3 Ingeniería de características (construir el perfil por cantón)

La idea es pasar de la tabla larga a una tabla **una fila por cantón**, donde las columnas describen su estructura de recaudación.

```python
# Total recaudado por cantón
total_canton = df.groupby(["PROVINCIA", "CANTON"])["VALOR_RECAUDADO"].sum()

# Composición por GRUPO_IMPUESTO (tabla pivote)
pivot = (df.pivot_table(index=["PROVINCIA", "CANTON"],
                        columns="GRUPO_IMPUESTO",
                        values="VALOR_RECAUDADO",
                        aggfunc="sum",
                        fill_value=0))

# Convertir montos absolutos a PROPORCIONES (estructura, no tamaño)
prop = pivot.div(pivot.sum(axis=1), axis=0)
prop.columns = [f"pct_{c}" for c in prop.columns]

# Características adicionales de comportamiento
extra = df.groupby(["PROVINCIA", "CANTON"]).agg(
    total_recaudado=("VALOR_RECAUDADO", "sum"),
    n_transacciones=("VALOR_RECAUDADO", "size"),
    ticket_promedio=("VALOR_RECAUDADO", "mean"),
    pct_sociedades=("TIPO_CONTRIBUYENTE",
                    lambda s: (s == "SOCIEDADES").mean()),
    pct_gran_contrib=("GRAN_CONTRIBUYENTE",
                      lambda s: (s != "NO").mean()),
)
extra["log_total"] = np.log1p(extra["total_recaudado"])   # comprimir la escala

features = prop.join(extra).fillna(0)
print(features.shape)        # ~221 filas (cantones) × N columnas
```

> **Por qué proporciones:** sin normalizar, el clustering solo separaría "cantones grandes vs. pequeños". Usar el `%` de cada grupo de impuesto captura la **estructura económica** (un cantón agrícola vs. uno comercial vs. uno minero/petrolero).

### 3.4 Escalado

Los algoritmos basados en distancia (K-Means) exigen variables en escala comparable.

```python
from sklearn.preprocessing import StandardScaler

X = StandardScaler().fit_transform(features)
```

### 3.5 Elegir el número de clusters (k)

Combinar el **método del codo** y el **coeficiente de silueta**.

```python
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score

inertias, sils = [], []
ks = range(2, 11)
for k in ks:
    km = KMeans(n_clusters=k, random_state=42, n_init=10).fit(X)
    inertias.append(km.inertia_)
    sils.append(silhouette_score(X, km.labels_))

# Graficar inertias (codo) y sils (máximo) para decidir k
```

**Criterio:** elegir el `k` donde el codo se aplana y la silueta es alta (típicamente entre 3 y 6 para este dataset). Justificar la elección en el informe.

### 3.6 Entrenar el modelo final

```python
K = 4   # ejemplo: el valor justificado por el codo/silueta
model = KMeans(n_clusters=K, random_state=42, n_init=10)
features["cluster"] = model.fit_predict(X)
```

**Algoritmos alternativos a comparar (al menos uno, para enriquecer el análisis):**
- **DBSCAN** — detecta outliers (cantones atípicos como Quito/Guayaquil) sin forzar `k`.
- **Clustering jerárquico (Ward)** — genera un dendrograma; bueno para narrativa visual.
- **Gaussian Mixture (GMM)** — clusters con forma elíptica y probabilidad de pertenencia.

### 3.7 Reducción de dimensionalidad para visualizar

```python
from sklearn.decomposition import PCA

coords = PCA(n_components=2, random_state=42).fit_transform(X)
features["pca1"], features["pca2"] = coords[:, 0], coords[:, 1]
```

PCA permite graficar los clusters en 2D y explicar qué variables pesan en cada componente (`pca.components_`).

### 3.8 Interpretación de clusters (lo más importante)

```python
perfil = features.groupby("cluster").mean(numeric_only=True)
tamanos = features["cluster"].value_counts()
```

Para cada cluster, redactar una **etiqueta de negocio**, por ejemplo:
- *Cluster 0 — "Economías comerciales/urbanas":* alto `%IVA`, muchas sociedades.
- *Cluster 1 — "Cantones petroleros/mineros":* peso de impuestos específicos, ticket alto.
- *Cluster 2 — "Cantones rurales pequeños":* bajo volumen, alta proporción de personas naturales.
- *Cluster 3 — "Outliers metropolitanos":* Quito/Guayaquil, montos extremos.

> Una clusterización sin interpretación de negocio **no aprueba**. El valor está en nombrar y explicar los grupos.

### 3.9 Validación y entregables del modelo

- Reportar **silhouette score** del modelo final y comparar entre algoritmos.
- Mostrar estabilidad (¿cambian mucho los clusters con otro `random_state`?).
- Guardar los artefactos para el dashboard:

```python
import joblib
features.reset_index().to_parquet("cantones_clusterizados.parquet")
joblib.dump(model, "modelo_kmeans.pkl")
```

---

## 4. Parte 2 — Construir el dashboard

Se recomienda **Streamlit** (Python puro, rápido de aprender, despliegue gratuito en Streamlit Community Cloud). Alternativas válidas: **Power BI / Looker Studio** (sin código, conecta al CSV/parquet) o **Dash/Plotly**.

### 4.1 Estructura mínima sugerida (3 secciones)

1. **Visión general (EDA):** KPIs (total recaudado, # transacciones, top provincias), evolución mensual, recaudación por grupo de impuesto.
2. **Exploración geográfica:** mapa o ranking por provincia/cantón; filtros por mes, grupo de impuesto y tipo de contribuyente.
3. **Resultados del modelo:** scatter PCA coloreado por cluster, tabla de perfil de cada cluster, y la "etiqueta de negocio" con su interpretación.

### 4.2 Esqueleto de código (`app.py`)

```python
import streamlit as st
import pandas as pd
import plotly.express as px

st.set_page_config(page_title="SRI 2026 — Recaudación", layout="wide")

@st.cache_data
def load_data():
    df = pd.read_csv("sri_recaudacion_2026.csv", sep="|", decimal=",")
    df["VALOR_RECAUDADO"] = pd.to_numeric(df["VALOR_RECAUDADO"], errors="coerce")
    df["MES_NUM"] = df["MES"].str.extract(r"(\d+)").astype(int)
    clusters = pd.read_parquet("cantones_clusterizados.parquet")
    return df, clusters

df, clusters = load_data()

st.title("Recaudación Tributaria SRI — Ecuador 2026")

# ---------- Filtros ----------
st.sidebar.header("Filtros")
meses = st.sidebar.multiselect("Mes", sorted(df["MES"].unique()),
                               default=sorted(df["MES"].unique()))
grupos = st.sidebar.multiselect("Grupo de impuesto",
                                sorted(df["GRUPO_IMPUESTO"].unique()))
f = df[df["MES"].isin(meses)]
if grupos:
    f = f[f["GRUPO_IMPUESTO"].isin(grupos)]

# ---------- Tab 1: KPIs ----------
tab1, tab2, tab3 = st.tabs(["Visión general", "Geografía", "Clusters"])

with tab1:
    c1, c2, c3 = st.columns(3)
    c1.metric("Total recaudado (USD)", f"${f['VALOR_RECAUDADO'].sum():,.0f}")
    c2.metric("Transacciones", f"{len(f):,}")
    c3.metric("Provincias", f["PROVINCIA"].nunique())

    mensual = f.groupby("MES_NUM")["VALOR_RECAUDADO"].sum().reset_index()
    st.plotly_chart(px.line(mensual, x="MES_NUM", y="VALOR_RECAUDADO",
                            markers=True, title="Recaudación mensual"),
                    use_container_width=True)

    por_grupo = (f.groupby("GRUPO_IMPUESTO")["VALOR_RECAUDADO"].sum()
                   .sort_values(ascending=False).reset_index())
    st.plotly_chart(px.bar(por_grupo, x="VALOR_RECAUDADO", y="GRUPO_IMPUESTO",
                           orientation="h", title="Recaudación por grupo"),
                    use_container_width=True)

# ---------- Tab 2: Geografía ----------
with tab2:
    prov = (f.groupby("PROVINCIA")["VALOR_RECAUDADO"].sum()
              .sort_values(ascending=False).reset_index())
    st.plotly_chart(px.bar(prov, x="PROVINCIA", y="VALOR_RECAUDADO",
                           title="Recaudación por provincia"),
                    use_container_width=True)
    st.dataframe(prov, use_container_width=True)

# ---------- Tab 3: Modelo no supervisado ----------
with tab3:
    st.subheader("Segmentación de cantones (K-Means)")
    fig = px.scatter(clusters, x="pca1", y="pca2", color="cluster",
                     hover_data=["PROVINCIA", "CANTON", "total_recaudado"],
                     title="Cantones proyectados con PCA")
    st.plotly_chart(fig, use_container_width=True)

    st.markdown("**Perfil promedio por cluster**")
    st.dataframe(clusters.groupby("cluster").mean(numeric_only=True),
                 use_container_width=True)
```

### 4.3 Ejecutar y desplegar

```bash
streamlit run app.py            # local: http://localhost:8501
```

Despliegue gratuito: subir el repo a GitHub y conectarlo en **share.streamlit.io**.

### 4.4 Buenas prácticas del dashboard
- Usar `@st.cache_data` para no recargar el CSV en cada interacción.
- Formatear montos (`$1,234,567`), no números crudos.
- Cada gráfico debe responder a una pregunta; evitar "gráficos decorativos".
- Incluir una sección de **conclusiones e interpretación de los clusters** redactada en lenguaje de negocio.

---

## 5. Rúbrica de evaluación sugerida

| Criterio | Peso | Qué se evalúa |
|---|---|---|
| Comprensión y limpieza de datos | 15% | Manejo de separador/decimales, nulos, outliers, documentación de calidad |
| Ingeniería de características | 15% | Justificación de la unidad de análisis y del uso de proporciones/escalado |
| Modelo no supervisado | 25% | Elección de `k`, comparación de algoritmos, métricas (silueta), reproducibilidad |
| Interpretación de clusters | 20% | Etiquetas de negocio claras y sustentadas en los datos |
| Dashboard | 20% | Funcionalidad, interactividad (filtros), claridad visual, despliegue |
| Comunicación / informe | 5% | Narrativa, conclusiones, recomendaciones |

---

## 6. Entregables

1. **Notebook / script** reproducible del pipeline (carga → features → modelo → evaluación).
2. **Dashboard** funcional (URL desplegada o instrucciones de ejecución).
3. **Informe corto (4–6 págs.)**: problema, metodología, resultados, interpretación de clusters y limitaciones.
4. **Presentación (10 min)** orientada a un tomador de decisiones del SRI.

---

## 7. Preguntas guía para profundizar (opcional / nota extra)

- ¿Cómo cambian los clusters si se usa el **monto absoluto** en lugar de proporciones? ¿Qué descubre cada enfoque?
- ¿Qué cantones quedan como **outliers** con DBSCAN y por qué?
- ¿La estructura tributaria varía **mes a mes** (estacionalidad de marzo/abril por declaración de Renta)?
- ¿Qué política pública podría informar la segmentación encontrada?

---

### Notas técnicas sobre el archivo
- **Separador:** `|` (no coma). **Decimal:** coma. Cargar siempre con `sep="|", decimal=","`.
- **Cobertura temporal:** enero–mayo 2026 (`ANIO` es constante; úsalo solo para documentar, no como variable del modelo).
- **Desbalance:** la mayoría de registros son `GRAN_CONTRIBUYENTE = NO` y `PERSONAS NATURALES`; tenerlo en cuenta al interpretar proporciones.
