Metadata-Version: 2.4
Name: inteligenciartificial
Version: 0.0.8
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 para aprendizaje de estructura de Redes Bayesianas.

## 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
```

### 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.

## Dependencias

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

## Licencia

MIT
