Metadata-Version: 2.4
Name: inteligenciartificial
Version: 0.0.9
Summary: Librería de algoritmos de Inteligencia Artificial en Python.
Author-email: Pedro Shiguihara <p.shiguihara@gmail.com>
License: MIT
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: numpy>=1.23
Requires-Dist: pandas>=1.5
Requires-Dist: scikit-learn>=1.5
Provides-Extra: graficos
Requires-Dist: matplotlib>=3.6; extra == "graficos"

# Paquete Inteligencia Artificial

Paquete de algoritmos de Inteligencia Artificial: aprendizaje de estructura de Redes Bayesianas y reglas de asociación.

## Instalación
```bash
pip install inteligenciartificial
```

O desde fuente local:
```bash
pip install ./fia
```

## Uso

### Algoritmos de aprendizaje de estructura

#### K2

Algoritmo greedy que requiere un orden topológico de variables. Agrega padres iterativamente si mejoran la métrica de scoring.

```python
from inteligenciartificial.modelografosprobabilisticos.busqueda import K2
from inteligenciartificial.modelografosprobabilisticos.metricas import Entropia, BIC

orden = ["Churn", "tenure_cat", "OnlineSecurity", "TechSupport", "Contract"]
k2 = K2(orden, max_padres=4, df=df_train, metrica=Entropia())
# [K2] Grafos explorados: 35 | Tiempo: 0.1234 s | Throughput: 283.61 grafos/s

modelo = k2.fit(X_train, y_train, alpha=1.0)
```

#### Hill-Climbing

Algoritmo de búsqueda local que en cada paso evalúa operaciones de agregar, eliminar e invertir aristas, seleccionando la que mayor mejora produce.

A partir de la versión 0.0.6 acepta configurar el estado inicial:

- **Por defecto** parte de un grafo vacío.
- `grafo_inicial`: `Grafo` desde el cual iniciar la búsqueda.
- `grafos_iniciales`: lista de `Grafo` para ejecutar varias corridas con inicios manuales.
- `reinicios` + `seed`: número de reinicios desde DAGs aleatorios reproducibles.

```python
from inteligenciartificial.modelografosprobabilisticos.busqueda import HillClimbing
from inteligenciartificial.modelografosprobabilisticos.metricas import BIC
from inteligenciartificial.modelografosprobabilisticos.grafo import Grafo

variables = ["Churn", "tenure_cat", "OnlineSecurity", "TechSupport", "Contract"]

# Inicio por defecto (grafo vacío)
hc = HillClimbing(variables, max_padres=4, df=df_train, metrica=BIC(), max_iter=100)

# Inicio desde un grafo dado
hc = HillClimbing(variables, max_padres=4, df=df_train, metrica=BIC(), max_iter=100,
                  grafo_inicial=Grafo([("Contract", "Churn")]))

# Reinicios aleatorios reproducibles
hc = HillClimbing(variables, max_padres=4, df=df_train, metrica=BIC(), max_iter=100,
                  reinicios=5, seed=42)

# Reinicios manuales
hc = HillClimbing(variables, max_padres=4, df=df_train, metrica=BIC(), max_iter=100,
                  grafos_iniciales=[Grafo([]), Grafo([("Contract", "Churn")])])

modelo = hc.fit(X_train, y_train, alpha=1.0)
```

#### Simulated Annealing

Búsqueda estocástica que en cada iteración propone una operación vecina aleatoria (agregar / eliminar / invertir arista). Si la operación mejora el score se acepta; si lo empeora, se acepta con probabilidad `exp(Δ / T)`. La temperatura decae geométricamente con factor `enfriamiento`.

```python
from inteligenciartificial.modelografosprobabilisticos.busqueda import SimulatedAnnealing
from inteligenciartificial.modelografosprobabilisticos.metricas import BIC

sa = SimulatedAnnealing(
    variables, max_padres=4, df=df_train, metrica=BIC(),
    max_iter=2000, t_inicial=1.0, t_min=1e-3, enfriamiento=0.995, seed=42,
)
modelo = sa.fit(X_train, y_train, alpha=1.0)
```

#### Fuerza bruta

A partir de la versión 0.0.8. Enumera **todos** los DAG posibles (respetando `max_padres`) y garantiza el óptimo global, por lo que solo es viable con pocas variables: `n_variables ≤ 5`.

| Variables | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
| DAG posibles | 1 | 3 | 25 | 543 | 29 281 | 3 781 503 |

Qué conjuntos de variables explora, dado `variables` (con `objetivo`, este va en todos):

| Opción | Conjuntos explorados (ejemplo con `n_variables=3`) |
|---|---|
| por defecto | uno: las `n_variables` primeras |
| `recursivo=True` | los tamaños 1, 2 y 3 (las primeras variables de cada tamaño) |
| `combinatorio=True` | todas las combinaciones de 3 variables |
| ambos | todas las combinaciones de 1, 2 y 3 variables |

Cada DAG se puntúa con `metrica` (BIC o Entropía) y, si se indica `objetivo`, se evalúa como clasificador sobre `df_eval` (o sobre `df` si no se da, lo que es optimista) con AUC, accuracy, precision, recall y F1. Las redes con el mismo manto de Markov del objetivo predicen lo mismo y se evalúan una sola vez.

```python
from inteligenciartificial.modelografosprobabilisticos.busqueda import FuerzaBruta
from inteligenciartificial.modelografosprobabilisticos.metricas import BIC

variables = ["Churn", "Contract", "TechSupport", "tenure_cat", "OnlineSecurity", "InternetService"]

fb = FuerzaBruta(
    variables, max_padres=2, df=df_train, metrica=BIC(),
    objetivo="Churn", df_eval=df_test, n_variables=3,
    recursivo=True, combinatorio=True,           # tamaños 1..3, todas las combinaciones
    criterio="auc", umbral=0.3,                  # elige por AUC; F1, etc. con P(Yes) >= 0.3
    csv="resultados/fuerza_bruta.csv",           # una fila por DAG
    png="resultados", metricas_png=["auc", "f1"],  # un PNG por métrica
)
# [FuerzaBruta] Conjuntos de variables: 16 | Grafos explorados: 266 | Tiempo: … | Mejor auc: …

fb.resultados.sort_values("auc", ascending=False).head()   # variables, aristas, score y métricas
modelo = fb.fit(X_train, y_train, alpha=1.0)               # RedBayesiana con la mejor red
fb.graficar("recall", "resultados/recall.png")             # otro gráfico cuando se quiera
```

- **`criterio`**: `"score"` (por defecto con un solo conjunto de variables) o una de las métricas. Con `recursivo` o `combinatorio` se comparan redes con distintas variables, cuyas puntuaciones BIC no son comparables, así que hace falta `objetivo` y un criterio de clasificación (por defecto, `"auc"`). A igual valor gana la red más simple.
- **`umbral`** (solo objetivo binario): probabilidad mínima de `pos_label` (por defecto, la última clase: `"Yes"`, `1`) para predecirla. Sin él, accuracy, precision, recall y F1 usan la clase más probable; con clases desbalanceadas eso puede dar F1 = 0 en todas las redes. El AUC no depende del umbral.
- **Gráficos**: a la izquierda, todas las redes ordenadas por la métrica (color = número de variables); a la derecha, la métrica frente al score de estructura (un conjunto) o su rango en cada conjunto de variables (varios). Requieren matplotlib: `pip install "inteligenciartificial[graficos]"`.
- **`max_grafos`** (1 000 000 por defecto) evita búsquedas desmedidas; como referencia, 29 281 DAG con métricas tardan en torno a 1 s.

### Métricas de throughput

Todos los algoritmos (K2, Hill-Climbing, Simulated Annealing y Fuerza bruta) reportan automáticamente al finalizar el entrenamiento:

| Atributo | Descripción |
|---|---|
| `grafos_explorados` | Cantidad de configuraciones de grafo (conjuntos de padres) evaluadas |
| `tiempo_segundos` | Tiempo total de la búsqueda de estructura en segundos |
| `throughput` | Tasa de exploración: `grafos_explorados / tiempo_segundos` (grafos/s) |

```python
print(k2.grafos_explorados)   # e.g., 35
print(k2.tiempo_segundos)     # e.g., 0.1234
print(k2.throughput)           # e.g., 283.61
```

`ReglasAsociacion` (ver [Reglas de asociación](#reglas-de-asociación)) informa lo mismo con `itemsets_evaluados` (itemsets contados por Apriori), `tiempo_segundos` y `throughput` (itemsets/s).

### Métricas de scoring

| Métrica | Descripción |
|---|---|
| `Entropia` | Usa `-H(X\|Pa)` como score (mayor es mejor) |
| `BIC` | Bayesian Information Criterion con penalización `(k/2)*log(N)` |

### Modelos

- **RedBayesiana**: Red Bayesiana discreta con CPTs estimadas por conteos + suavizado Dirichlet(alpha).
- **NaiveBayes**: Caso especial donde todas las features dependen únicamente de la clase.

Ambos son clasificadores de scikit-learn (ver abajo).

### Integración con scikit-learn

A partir de la versión 0.0.7, `RedBayesiana` y `NaiveBayes` heredan de `ClassifierMixin` y `BaseEstimator`: se pueden clonar y, tras `fit`, exponen `classes_` y `predict_proba` (P(objetivo | X), una columna por clase). Por eso se acoplan directamente a `TunedThresholdClassifierCV` (scikit-learn ≥ 1.5), que elige por validación cruzada el umbral de decisión que maximiza una métrica en problemas binarios. En la `RedBayesiana` la estructura del grafo queda fija y en cada fold solo se re-estiman las CPTs.

```python
from sklearn.metrics import f1_score, make_scorer
from sklearn.model_selection import TunedThresholdClassifierCV

red = hc.fit(X_train, y_train, alpha=1.0)   # o RedBayesiana(grafo, alpha=1.0)

ajustado = TunedThresholdClassifierCV(
    red, scoring=make_scorer(f1_score, pos_label="Yes"), cv=5,
).fit(X_train, y_train)

print(ajustado.best_threshold_, ajustado.best_score_)
y_pred = ajustado.predict(X_test)
```

- La variable objetivo es el nombre de `y` (`pd.Series`). Si `y` no tiene nombre, indícala con `RedBayesiana(grafo, objetivo="Churn")`.
- Con etiquetas de texto, las métricas que usan `pos_label` (F1, precisión, recall) necesitan `make_scorer(..., pos_label=...)`; la métrica por defecto, `balanced_accuracy`, no.
- Con un conjunto de validación aparte: `TunedThresholdClassifierCV(red, cv="prefit", refit=False).fit(X_val, y_val)`.
- `NaiveBayes` se usa igual: `TunedThresholdClassifierCV(NaiveBayes(clase="Churn"), cv=5).fit(X_train, y_train)`.

Por la misma razón funcionan con `LearningCurveDisplay`, que re-entrena el modelo con porciones crecientes de los datos:

```python
import numpy as np
from sklearn.model_selection import LearningCurveDisplay

red = fb.fit(X_train, y_train)   # o hc.fit(...), RedBayesiana(grafo), NaiveBayes(clase="Churn")
LearningCurveDisplay.from_estimator(
    red, X_train, y_train, cv=5, scoring="roc_auc",
    train_sizes=np.linspace(0.1, 1.0, 5), shuffle=True, random_state=0,
)
```

- Para F1, precision o recall con etiquetas de texto: `scoring=make_scorer(f1_score, pos_label="Yes"), score_name="F1"`. Sin `score_name`, `LearningCurveDisplay` falla con un `make_scorer`.
- `shuffle=True` evita porciones pequeñas con una sola clase cuando los datos vienen ordenados.

### Reglas de asociación

A partir de la versión 0.0.9, el subpaquete `inteligenciartificial.reglasasociacion` busca reglas de asociación X → Y ("quien compra X también compra Y") y estima su soporte, confianza y lift, de forma frecuentista o bayesiana (previa de Dirichlet). Con `verbose=True` (o `--verbose` en la línea de órdenes) explica los pasos principales del cálculo con los números de los datos. El notebook `demo_v0_0_9_reglas_asociacion.ipynb` muestra cada funcionalidad sobre un dataset de ventas sintético.

#### Datos

El formato por defecto es el de `Dataset_Market_Basket.csv`: una línea por transacción (ticket) con sus ítems separados por comas, sin encabezado y con un número variable de ítems.

```text
shrimp,almonds,avocado,vegetables mix
burgers,meatballs,eggs
chutney
```

```python
from inteligenciartificial.reglasasociacion import Transacciones

tx = Transacciones.desde_csv("Dataset_Market_Basket.csv")   # el formato de cestas se detecta solo
# Transacciones(n_transacciones=7501, n_items=119, tamanio_medio=3.91)
```

También acepta el formato largo (`Transacciones.desde_largo(df, "ticket", "producto")`: una fila por ítem vendido), el binario (`Transacciones.desde_binario(df)`: una columna 0/1 por ítem) o una lista de cestas (`Transacciones([["pan", "leche"], ["pan"]])`). Solo importa si un ítem está o no en la transacción.

#### Soporte, confianza y lift de una regla

| Métrica | Fórmula frecuentista | Qué mide |
|---|---|---|
| soporte | n(X ∪ Y) / N | fracción de transacciones que contienen X e Y |
| confianza | n(X ∪ Y) / n(X) | estimación de P(Y \| X) |
| lift | confianza / soporte(Y) | cuántas veces más probable es Y si está X (1 es independencia) |

n(I) es el número de transacciones que contienen todos los ítems de I, y N el total.

```python
from inteligenciartificial.reglasasociacion import soporte, confianza, lift, evaluar_regla

soporte(tx, ["light cream", "chicken"])     # 0.0045
confianza(tx, "light cream", "chicken")     # 0.2906
lift(tx, "light cream", "chicken")          # 4.8440
evaluar_regla(tx, "light cream", "chicken", verbose=True)   # las tres en un DataFrame de una fila, con la explicación
```

#### Estimación bayesiana (previa de Dirichlet)

Con `bayesiano=True`, cada regla reparte las N transacciones en una tabla 2 × 2 (con o sin X, con o sin Y). Los conteos siguen una multinomial de probabilidades θ = (θ11, θ10, θ01, θ00) con previa Dirichlet(α11, α10, α01, α00). Como la Dirichlet es conjugada de la multinomial, la posterior es Dirichlet(n11 + α11, n10 + α10, n01 + α01, n00 + α00), y:

- soporte = θ11, con media exacta (n11 + α11) / (N + A), donde A = α11 + α10 + α01 + α00;
- confianza = θ11 / (θ11 + θ10) ~ Beta(n11 + α11, n10 + α10), con media exacta (n11 + α11) / (n(X) + α11 + α10);
- lift = θ11 / ((θ11 + θ10)(θ11 + θ01)) no tiene forma cerrada: su media se estima con `n_muestras` sorteos de la posterior (reproducibles con `seed`).

También se obtienen los intervalos de credibilidad del nivel `credibilidad` (columnas `soporte_inf`, `soporte_sup`, `confianza_inf`, `confianza_sup`, `lift_inf` y `lift_sup`) y `p_lift_mayor_1`, la probabilidad posterior de que el lift supere 1.

| Previa | Hiperparámetros | Con pocos datos |
|---|---|---|
| `previa="simetrica"` (por defecto) | Dirichlet(α, α, α, α), o cuatro valores en `alpha` | la confianza se acerca a 1/2 (α = 1 es Laplace; α = 0.5, Jeffreys) |
| `previa="independencia"` | Dirichlet(2αp, 2α(1 - p), 2αp, 2α(1 - p)), con p = n(Y) / N | la confianza se acerca a P(Y) y el lift a 1 (estimador m de Cestnik, con m = 2α) |

Con ítems raros, la previa simétrica puede inflar el lift, incluso el de dos ítems que nunca aparecen juntos; en ese caso conviene `previa="independencia"`.

```python
evaluar_regla(tx, "light cream", "chicken", bayesiano=True, alpha=1, seed=7)
# soporte 0.0047 [0.0032, 0.0063] · confianza 0.2941 [0.2160, 0.3784] · lift 4.90 [3.61, 6.27] · P(lift > 1) = 1
```

#### Reglas y top N

`ReglasAsociacion` busca los itemsets frecuentes con Apriori (soporte n(I) / N ≥ `soporte_min`), genera todas las reglas X → Y de cada itemset, estima sus métricas, filtra por `confianza_min` y `lift_min` (en la bayesiana, con las medias posteriores) y ordena por `ordenar_por`. `top(n, por=...)` devuelve el top N según lift, soporte o confianza.

```python
from inteligenciartificial.reglasasociacion import ReglasAsociacion

ra = ReglasAsociacion(soporte_min=0.003, confianza_min=0.2, lift_min=1).fit(tx)
# [ReglasAsociacion] Transacciones: 7501 | Itemsets frecuentes: 1443 | Reglas: 1355 | Tiempo: … | Throughput: …

ra.top(10, por="lift")                    # también por="soporte" o por="confianza"
print(ra.reporte(10, por="confianza"))    # la misma tabla, como texto
ra.reglas_                                # todas las reglas
ra.itemsets_                              # itemsets frecuentes
ra.guardar_csv("reglas.csv")

# Bayesiana, explicando cada paso
ReglasAsociacion(soporte_min=0.003, confianza_min=0.2, bayesiano=True, alpha=1, seed=7, verbose=True).fit(tx)
```

- Columnas de `reglas_`: `antecedente` y `consecuente` (tuplas de ítems), `regla` (texto), `n_xy`, `n_x`, `n_y` (conteos), `soporte`, `confianza`, `lift` y, en la bayesiana, los intervalos y `p_lift_mayor_1`.
- En la bayesiana, el top también se puede ordenar por otra columna: `top(10, por="lift_inf")` favorece las reglas con más evidencia.
- `max_longitud` limita los ítems por regla; `itemsets_frecuentes(tx, soporte_min=0.02, verbose=True)` devuelve solo los itemsets frecuentes.

#### Línea de órdenes

```bash
python -m inteligenciartificial.reglasasociacion Dataset_Market_Basket.csv --soporte-min 0.003 --confianza-min 0.2 --top 10 --ordenar-por lift
python -m inteligenciartificial.reglasasociacion Dataset_Market_Basket.csv --soporte-min 0.003 --confianza-min 0.2 --bayesiano --alpha 1 --seed 7 --verbose
python -m inteligenciartificial.reglasasociacion Dataset_Market_Basket.csv --antecedente pasta --consecuente escalope --bayesiano
python -m inteligenciartificial.reglasasociacion --help
```

Imprime la tabla del top N; con `--verbose`, precedida de la explicación de cada paso. `--salida reglas.csv` y `--salida-itemsets itemsets.csv` guardan los resultados, y `--formato largo|binario` (con `--col-transaccion` y `--col-item`) lee los otros formatos.

## Dependencias

- `numpy >= 1.23`
- `pandas >= 1.5`
- `scikit-learn >= 1.5`
- Opcional, para los gráficos PNG de `FuerzaBruta`, `LearningCurveDisplay` y los del notebook de reglas de asociación: `matplotlib >= 3.6` (`pip install "inteligenciartificial[graficos]"`)

Las reglas de asociación solo usan numpy y pandas.

## Licencia

MIT
