El sistema experto de 06-02 decide devoluciones con reglas nítidas porque la política es nítida: 14 días son 14 días. El diagnóstico de incidencias del caso 9 es otra historia. Un paquete que llega abollado sugiere daño en transporte, pero también puede ser un error de picking con mal embalaje; un retraso apunta al almacén de Getafe, pero la mensajería también se retrasa; y incidencias.csv tiene la mitad de las causas en blanco. Aquí las reglas SI-ENTONCES se rompen: cada síntoma es compatible con varias causas, con distinta fuerza. Esta lección enseña a razonar con grados de creencia. Veremos por qué las reglas nítidas no bastan; los dos enfoques históricos que lo intentaron dentro de los sistemas expertos (los factores de certeza de MYCIN y la lógica difusa) y por qué la probabilidad acabó imponiéndose; un repaso de probabilidad desde cero (conjunta, condicional, independencia, regla del producto, marginalización) hasta el teorema de Bayes, con el diagnóstico de NovaMarket calculado a mano y en código; el Naive Bayes de 04-04 hecho a mano; las redes bayesianas, con una red de diagnóstico de incidencias y la inferencia por enumeración en Python puro; y cómo convertir esas probabilidades en decisiones con la utilidad esperada de 02-01. Es importante porque casi toda la IA moderna, del filtro de spam al LLM, es probabilidad aplicada; y porque en NovaMarket la pregunta de Diego no es "¿cuál es la causa?" sino "¿qué hago con esta incidencia sabiendo lo que sé?".

Contenido

  1. Por qué las reglas nítidas no bastan
  2. Enfoques históricos: factores de certeza y lógica difusa
  3. Probabilidad desde cero: conjunta, condicional, independencia, producto y marginalización
  4. El teorema de Bayes, a mano y en código
  5. Naive Bayes a mano: la causa de una incidencia a partir de varios síntomas
  6. Redes bayesianas: nodos, arcos y tablas de probabilidad condicional
  7. Código: red de diagnóstico de incidencias e inferencia por enumeración
  8. Inferencia aproximada y aprendizaje de las CPT
  9. Decisiones bajo incertidumbre: utilidad esperada
  10. Errores Comunes y Consejos
  11. Ejercicios
  12. Conclusión

  1. Por qué las reglas nítidas no bastan

Intentemos escribir el diagnóstico de incidencias como reglas de 06-02. Los tipos de incidencia que registra Diego son: paquete dañado, producto no llega, producto equivocado, producto defectuoso y retraso; las causas: error de picking en almacén, daño en transporte, dirección incorrecta, stock desactualizado (sobre todo en Getafe, cuyo inventario se sincroniza con retraso) y fallo del proveedor. Un primer intento:

  • SI paquete_dañado ENTONCES causa = daño_transporte
  • SI retraso ENTONCES causa = stock_desactualizado

Y enseguida los contraejemplos: paquetes abollados por un embalaje mal elegido en picking; retrasos por direcciones incorrectas que obligan a un segundo intento; productos equivocados que vienen así del proveedor. Cada regla es "casi siempre" cierta, y "casi siempre" no cabe en la lógica de 06-01. Tres razones de fondo:

  • Pereza: enumerar todas las condiciones y excepciones para que una regla sea exacta es inviable (habría que incluir el operario, el embalaje, la ruta, la mensajería...).
  • Ignorancia teórica: no existe una teoría completa que diga qué causa cada síntoma.
  • Ignorancia práctica: aunque existiera, no tenemos todos los datos del caso (no vemos el paquete hasta que el cliente lo fotografía).

Lo que sí sabemos es que un paquete abollado hace más probable el daño en transporte, y cuánto más. Necesitamos un lenguaje para grados de creencia que se combinen correctamente cuando llegan varias evidencias y que se puedan estimar de incidencias.csv. Ese lenguaje es la probabilidad; antes de llegar a él, veamos los dos atajos que se probaron.

  1. Enfoques históricos: factores de certeza y lógica difusa

2.1 Factores de certeza (MYCIN)

MYCIN (01-01, 06-02) asociaba a cada regla un factor de certeza CF entre −1 (certeza de que es falso) y +1 (certeza de que es cierto), y combinaba los de varias reglas que apoyaban la misma conclusión con una fórmula sencilla:

  • Ambos positivos: CF = CF₁ + CF₂·(1 − CF₁)
  • Ambos negativos: CF = CF₁ + CF₂·(1 + CF₁)
  • Signos distintos: CF = (CF₁ + CF₂) / (1 − min(|CF₁|, |CF₂|))
def combinar_cf(cf1, cf2):
    if cf1 >= 0 and cf2 >= 0:
        return cf1 + cf2 * (1 - cf1)
    if cf1 <= 0 and cf2 <= 0:
        return cf1 + cf2 * (1 + cf1)
    return (cf1 + cf2) / (1 - min(abs(cf1), abs(cf2)))

print("CF abollado(0.6) + retraso(0.3):", round(combinar_cf(0.6, 0.3), 3))
print("... y evidencia en contra (-0.4):", round(combinar_cf(combinar_cf(0.6, 0.3), -0.4), 3))

Salida:

CF abollado(0.6) + retraso(0.3): 0.72
... y evidencia en contra (-0.4): 0.533

Dos evidencias a favor se refuerzan (0,6 y 0,3 dan 0,72, nunca más de 1); una en contra resta. Era intuitivo, barato y funcionó en MYCIN. Se abandonó porque no tiene una semántica clara: los números no son probabilidades ni nada definido, la fórmula trata todas las evidencias como independientes (dos síntomas que en realidad van siempre juntos cuentan doble), no distingue "no sé" de "evidencias contradictorias", y Heckerman demostró en 1986 que los CF solo son coherentes con la probabilidad bajo supuestos muy restrictivos. Cuando las redes bayesianas hicieron la probabilidad tratable, los CF quedaron como pieza de museo.

2.2 Lógica difusa

La lógica difusa (Zadeh, 1965) ataca otro problema: la vaguedad de las palabras. "Retraso grave" no es verdadero o falso a partir de una hora exacta; un retraso de 36 horas es "bastante leve" y "un poco grave". Cada término tiene una función de pertenencia entre 0 y 1:

def pertenencia_leve(horas):
    if horas <= 0: return 1.0
    if horas >= 48: return 0.0
    return 1 - horas / 48

def pertenencia_grave(horas):
    if horas <= 24: return 0.0
    if horas >= 96: return 1.0
    return (horas - 24) / 72

for h in (6, 24, 36, 60, 96):
    print(f"retraso {h:3d} h: leve={pertenencia_leve(h):.2f} grave={pertenencia_grave(h):.2f}")

Salida:

retraso   6 h: leve=0.88 grave=0.00
retraso  24 h: leve=0.50 grave=0.00
retraso  36 h: leve=0.25 grave=0.17
retraso  60 h: leve=0.00 grave=0.50
retraso  96 h: leve=0.00 grave=1.00

Sobre estas pertenencias se escriben reglas difusas ("SI retraso es grave Y cliente es valioso ENTONCES compensación es alta") que se combinan con mínimos y máximos y se desdifuminan en un número. Es útil en control (lavadoras, frenos ABS, climatización) y en interfaces donde la vaguedad lingüística importa. Pero atención: la pertenencia 0,5 de "grave" a las 60 horas no es una probabilidad; el retraso es exactamente de 60 horas, sin incertidumbre; lo difuso es la palabra. La incertidumbre del diagnóstico ("¿fue transporte o picking?") es otra cosa: no sabemos qué pasó, y para eso el instrumento correcto es la probabilidad.

  1. Probabilidad desde cero: conjunta, condicional, independencia, producto y marginalización

Trabajaremos con variables aleatorias discretas: Causa (cinco valores) y síntomas binarios como Abollado. Una distribución asigna a cada valor un número entre 0 y 1 que suman 1. Los números que siguen son ficticios pero verosímiles, redondeados a partir de las ~200 incidencias etiquetadas de NovaMarket.

Probabilidad a priori (o marginal): lo que creemos antes de ver el caso. P(Causa = daño_transporte) = 0,25 significa que, de las incidencias, una de cada cuatro se debe a transporte.

Causa error_picking dano_transporte direccion_incorrecta stock_desactualizado fallo_proveedor
P(Causa) 0,30 0,25 0,15 0,20 0,10

Probabilidad conjunta P(Causa, Abollado): la de que ocurran las dos cosas a la vez. La forma más intuitiva de verla es una tabla de frecuencias sobre 1.000 incidencias:

Causa (de 1.000) Abollado No abollado Total
error_picking 30 270 300
dano_transporte 200 50 250
direccion_incorrecta 3 147 150
stock_desactualizado 4 196 200
fallo_proveedor 15 85 100
Total 252 748 1.000

Cada celda dividida por 1.000 es la conjunta: P(dano_transporte, Abollado) = 0,20.

Marginalización: sumar la conjunta sobre la variable que no interesa. P(Abollado) = 30 + 200 + 3 + 4 + 15 = 252 de 1.000 = 0,252. Es "el total de la columna". En fórmula: P(A) = Σ_c P(A, c).

Probabilidad condicional P(A | B): la de A sabiendo que B ocurrió: restringimos la tabla a la fila o columna de B. P(Abollado | dano_transporte) = 200/250 = 0,80: de los daños en transporte, el 80 % llegan abollados. Y P(dano_transporte | Abollado) = 200/252 = 0,794: de los abollados, el 79 % son daños de transporte. No son la misma cosa: la primera es una verosimilitud (qué produce la causa), la segunda es lo que queremos diagnosticar. Confundirlas es el error más común de todo este campo.

Regla del producto: P(A, B) = P(A | B) · P(B). Con nuestros números: 0,80 · 0,25 = 0,20. Es la definición de la condicional reordenada, y encadenada da la regla de la cadena P(A, B, C) = P(A | B, C) · P(B | C) · P(C), que usaremos en las redes bayesianas.

Independencia: A y B son independientes si P(A | B) = P(A), es decir, saber B no cambia lo que creemos de A; entonces P(A, B) = P(A)·P(B). Abollado y Causa no son independientes (0,80 frente a 0,252). Y hay una noción más fina que será la clave de la sección 6: independencia condicional: dos síntomas pueden depender entre sí (los paquetes abollados vienen más a menudo con producto equivocado que los no abollados, porque ambos apuntan a errores de manipulación) y sin embargo ser independientes una vez conocida la causa: si ya sé que fue error de picking, saber que está abollado no me dice nada nuevo sobre si el producto es el equivocado. P(A | B, Causa) = P(A | Causa).

  1. El teorema de Bayes, a mano y en código

De la regla del producto escrita en los dos sentidos, P(C, A) = P(A | C)·P(C) = P(C | A)·P(A), despejamos:

P(C | A) = P(A | C) · P(C) / P(A)

  • P(C): prior, lo que creíamos de la causa antes de ver el síntoma.
  • P(A | C): verosimilitud, cuánto produce esa causa ese síntoma.
  • P(A): evidencia, la probabilidad total del síntoma, que se calcula marginalizando: P(A) = Σ_c P(A | c)·P(c). Sirve para que los posteriores sumen 1.
  • P(C | A): posterior, lo que creemos después.

Es la fórmula que en 04-04 sustentaba Naive Bayes y que ahora ya podemos justificar. Calculamos a mano P(Causa | Abollado) para las cinco causas:

Causa c Prior P(c) Verosimilitud P(Abollado | c) Producto Posterior = producto / 0,252
error_picking 0,30 0,10 0,030 0,119
dano_transporte 0,25 0,80 0,200 0,794
direccion_incorrecta 0,15 0,02 0,003 0,012
stock_desactualizado 0,20 0,02 0,004 0,016
fallo_proveedor 0,10 0,15 0,015 0,060
Suma 1,00 0,252 = P(Abollado) 1,000

Y en código, con fractions para que no haya redondeos:

from fractions import Fraction as F

priors = {"error_picking": F(30,100), "dano_transporte": F(25,100), "direccion_incorrecta": F(15,100),
          "stock_desactualizado": F(20,100), "fallo_proveedor": F(10,100)}
veros = {"error_picking": F(10,100), "dano_transporte": F(80,100), "direccion_incorrecta": F(2,100),
         "stock_desactualizado": F(2,100), "fallo_proveedor": F(15,100)}   # P(abollado | causa)

p_abollado = sum(priors[c] * veros[c] for c in priors)                    # marginalización
print("P(abollado) =", p_abollado, float(p_abollado))
for c in priors:
    post = priors[c] * veros[c] / p_abollado                              # teorema de Bayes
    print(f"  P({c} | abollado) = {priors[c]*veros[c]} / {p_abollado} = {float(post):.3f}")

Salida:

P(abollado) = 63/250 0.252
  P(error_picking | abollado) = 3/100 / 63/250 = 0.119
  P(dano_transporte | abollado) = 1/5 / 63/250 = 0.794
  P(direccion_incorrecta | abollado) = 3/1000 / 63/250 = 0.012
  P(stock_desactualizado | abollado) = 1/250 / 63/250 = 0.016
  P(fallo_proveedor | abollado) = 3/200 / 63/250 = 0.060

Observa el papel del prior: el fallo de proveedor produce paquetes abollados el 15 % de las veces, más que el error de picking (10 %), y sin embargo el picking tiene más posterior (0,119 frente a 0,060) porque es tres veces más frecuente. Ignorar el prior (fijarse solo en "qué causa explica mejor el síntoma") es la falacia de la tasa base.

Enlace con 04-05. Si tratamos "abollado" como una prueba diagnóstica de "daño en transporte", la sensibilidad de la prueba es P(Abollado | transporte) = 0,80 y su especificidad es P(No abollado | no transporte) = 1 − 52/750 = 0,931 (de las 750 incidencias que no son de transporte, 52 están abolladas). Bayes en ese lenguaje: posterior = sens·prev / (sens·prev + (1 − espec)·(1 − prev)) = 0,80·0,25 / (0,80·0,25 + 0,069·0,75) = 0,794. El valor predictivo positivo de 04-05 es un posterior bayesiano, y depende de la prevalencia (el prior) tanto como de la calidad de la prueba: la misma prueba aplicada en Getafe, donde el transporte pesa menos, daría un posterior menor.

  1. Naive Bayes a mano: la causa de una incidencia a partir de varios síntomas

Con un síntoma basta una tabla. Con tres (paquete dañado, producto equivocado, retraso) la conjunta P(Causa, S₁, S₂, S₃) tendría 5·2·2·2 = 40 celdas que habría que estimar; con veinte síntomas, millones. Naive Bayes (04-04) sale del paso con la suposición de independencia condicional de la sección 3: dados la causa, los síntomas son independientes, luego P(S₁, S₂, S₃ | c) = P(S₁ | c)·P(S₂ | c)·P(S₃ | c) y solo hacen falta tres tablas pequeñas:

P(síntoma = sí | causa) error_picking dano_transporte direccion_incorrecta stock_desactualizado fallo_proveedor
paquete_danado 0,10 0,80 0,02 0,02 0,15
producto_equivocado 0,70 0,02 0,01 0,10 0,30
retraso 0,10 0,30 0,60 0,90 0,70

Incidencia 7731: paquete dañado , retraso , producto equivocado no. Para cada causa multiplicamos prior por las tres verosimilitudes (para "no" usamos 1 − p) y normalizamos:

Causa Prior ·P(dañado) ·P(retraso) ·P(no equivocado) Numerador Posterior
error_picking 0,30 0,10 0,10 0,30 0,00090 0,012
dano_transporte 0,25 0,80 0,30 0,98 0,05880 0,816
direccion_incorrecta 0,15 0,02 0,60 0,99 0,00178 0,025
stock_desactualizado 0,20 0,02 0,90 0,90 0,00324 0,045
fallo_proveedor 0,10 0,15 0,70 0,70 0,00735 0,102
Suma 0,07207 1,000
CAUSAS = ["error_picking", "dano_transporte", "direccion_incorrecta", "stock_desactualizado", "fallo_proveedor"]
prior = {"error_picking": 0.30, "dano_transporte": 0.25, "direccion_incorrecta": 0.15,
         "stock_desactualizado": 0.20, "fallo_proveedor": 0.10}
p_sintoma = {   # P(síntoma = True | causa)
    "paquete_danado":      {"error_picking": 0.10, "dano_transporte": 0.80, "direccion_incorrecta": 0.02, "stock_desactualizado": 0.02, "fallo_proveedor": 0.15},
    "producto_equivocado": {"error_picking": 0.70, "dano_transporte": 0.02, "direccion_incorrecta": 0.01, "stock_desactualizado": 0.10, "fallo_proveedor": 0.30},
    "retraso":             {"error_picking": 0.10, "dano_transporte": 0.30, "direccion_incorrecta": 0.60, "stock_desactualizado": 0.90, "fallo_proveedor": 0.70},
}

def naive_bayes(evidencia):
    """evidencia: dict síntoma -> True/False. Devuelve P(causa | evidencia)."""
    puntuaciones = {}
    for c in CAUSAS:
        p = prior[c]
        for s, valor in evidencia.items():
            p *= p_sintoma[s][c] if valor else 1 - p_sintoma[s][c]
        puntuaciones[c] = p                    # numerador: P(causa) · Π P(síntoma | causa)
    total = sum(puntuaciones.values())         # P(evidencia): normalizador
    return {c: p / total for c, p in puntuaciones.items()}, puntuaciones, total

ev = {"paquete_danado": True, "retraso": True, "producto_equivocado": False}
post, num, total = naive_bayes(ev)
print(f"P(evidencia) = {total:.5f}")
for c in CAUSAS:
    print(f"  {c:22s} numerador {num[c]:.5f}  posterior {post[c]:.3f}")

Salida:

P(evidencia) = 0.07207
  error_picking          numerador 0.00090  posterior 0.012
  dano_transporte        numerador 0.05880  posterior 0.816
  direccion_incorrecta   numerador 0.00178  posterior 0.025
  stock_desactualizado   numerador 0.00324  posterior 0.045
  fallo_proveedor        numerador 0.00735  posterior 0.102

Es exactamente lo que GaussianNB o MultinomialNB de scikit-learn hacen por dentro (con las verosimilitudes estimadas de los datos, sección 8). El "retraso sí" solo, sin paquete dañado, daría stock_desactualizado 0,51 y dirección incorrecta 0,28: cada síntoma nuevo mueve la creencia, y el producto de verosimilitudes es la forma correcta de combinar evidencias que a los factores de certeza les faltaba.

  1. Redes bayesianas: nodos, arcos y tablas de probabilidad condicional

Naive Bayes es una red bayesiana muy particular: una causa con flechas a todos los síntomas. Una red bayesiana general es un grafo dirigido sin ciclos en el que:

  • cada nodo es una variable aleatoria;
  • cada arco X → Y expresa una influencia directa (a menudo causal) de X sobre Y;
  • cada nodo lleva una tabla de probabilidad condicional (CPT) que da P(nodo | padres): para un nodo sin padres, su prior; para uno con padres, una fila por cada combinación de valores de los padres.

La red codifica independencias condicionales: cada nodo es independiente de sus no descendientes dada la información de sus padres. Gracias a ello, la conjunta completa se factoriza con la regla de la cadena en el producto de las CPT: P(X₁, ..., Xₙ) = Π P(Xᵢ | padres(Xᵢ)). En lugar de una tabla gigante, tablas pequeñas y locales que un experto (Diego) puede rellenar o que se estiman de los datos.

Nuestra red de diagnóstico de incidencias añade a Naive Bayes un nodo padre, el almacén, porque la distribución de causas no es la misma en Zaragoza que en Getafe (allí el stock desactualizado pesa mucho más), y un cuarto síntoma, "no llega":

flowchart TD
    A[almacen<br/>zaragoza 0,60 / getafe 0,40] --> C[causa<br/>5 valores, CPT por almacén]
    C --> D[paquete_danado]
    C --> E[producto_equivocado]
    C --> N[no_llega]
    C --> R[retraso]

CPT del nodo causa (una fila por almacén; cada fila suma 1):

P(causa | almacen) error_picking dano_transporte direccion_incorrecta stock_desactualizado fallo_proveedor
zaragoza 0,35 0,30 0,15 0,08 0,12
getafe 0,25 0,20 0,12 0,33 0,10

CPT de los cuatro síntomas (P(síntoma = sí | causa); las tres primeras filas son las de la sección 5):

P(sí | causa) error_picking dano_transporte direccion_incorrecta stock_desactualizado fallo_proveedor
paquete_danado 0,10 0,80 0,02 0,02 0,15
producto_equivocado 0,70 0,02 0,01 0,10 0,30
no_llega 0,05 0,10 0,85 0,40 0,30
retraso 0,10 0,30 0,60 0,90 0,70

Cuenta parámetros: 1 + 8 + 4·5 = 29 números frente a los 2·5·2⁴ − 1 = 159 de la conjunta completa. Y lee las independencias: los síntomas son condicionalmente independientes entre sí dada la causa (por eso Naive Bayes es un caso particular), y el almacén solo influye en los síntomas a través de la causa: P(retraso | causa, almacen) = P(retraso | causa).

  1. Código: red de diagnóstico de incidencias e inferencia por enumeración

Inferencia es calcular P(consulta | evidencia) para cualquier nodo y cualquier evidencia. El algoritmo más simple es la enumeración: la conjunta se obtiene multiplicando las CPT, y para obtener P(consulta, evidencia) se suma la conjunta sobre todas las combinaciones de las variables ocultas (ni consultadas ni observadas); dividir por la suma total normaliza. Es Bayes y marginalización, sin más.

from itertools import product

CAUSAS = ["error_picking", "dano_transporte", "direccion_incorrecta", "stock_desactualizado", "fallo_proveedor"]

def cpt_binaria(p_true_por_causa):
    """Construye la CPT de un síntoma binario a partir de P(síntoma=True | causa)."""
    return {(c,): {True: p, False: round(1 - p, 4)} for c, p in p_true_por_causa.items()}

# Cada nodo: (lista de padres, dominio, CPT). La CPT es un dict que, para cada
# combinación de valores de los padres (tupla), da la distribución sobre el nodo.
RED = {
    "almacen": ([], ["zaragoza", "getafe"], {(): {"zaragoza": 0.60, "getafe": 0.40}}),
    "causa": (["almacen"], CAUSAS, {
        ("zaragoza",): {"error_picking": 0.35, "dano_transporte": 0.30, "direccion_incorrecta": 0.15, "stock_desactualizado": 0.08, "fallo_proveedor": 0.12},
        ("getafe",):   {"error_picking": 0.25, "dano_transporte": 0.20, "direccion_incorrecta": 0.12, "stock_desactualizado": 0.33, "fallo_proveedor": 0.10},
    }),
    "paquete_danado":      (["causa"], [True, False], cpt_binaria({"error_picking": 0.10, "dano_transporte": 0.80, "direccion_incorrecta": 0.02, "stock_desactualizado": 0.02, "fallo_proveedor": 0.15})),
    "producto_equivocado": (["causa"], [True, False], cpt_binaria({"error_picking": 0.70, "dano_transporte": 0.02, "direccion_incorrecta": 0.01, "stock_desactualizado": 0.10, "fallo_proveedor": 0.30})),
    "no_llega":            (["causa"], [True, False], cpt_binaria({"error_picking": 0.05, "dano_transporte": 0.10, "direccion_incorrecta": 0.85, "stock_desactualizado": 0.40, "fallo_proveedor": 0.30})),
    "retraso":             (["causa"], [True, False], cpt_binaria({"error_picking": 0.10, "dano_transporte": 0.30, "direccion_incorrecta": 0.60, "stock_desactualizado": 0.90, "fallo_proveedor": 0.70})),
}
ORDEN = ["almacen", "causa", "paquete_danado", "producto_equivocado", "no_llega", "retraso"]  # padres antes que hijos

def prob_local(nodo, valor, asignacion):
    """P(nodo = valor | padres) leyendo la CPT con los valores de los padres en 'asignacion'."""
    padres, _, cpt = RED[nodo]
    clave = tuple(asignacion[p] for p in padres)
    return cpt[clave][valor]

def prob_conjunta(asignacion):
    """Regla de la cadena: producto de las probabilidades locales de todos los nodos."""
    p = 1.0
    for nodo in ORDEN:
        p *= prob_local(nodo, asignacion[nodo], asignacion)
    return p

def inferir(consulta, evidencia):
    """P(consulta | evidencia) por enumeración: suma la conjunta sobre las variables ocultas."""
    ocultas = [n for n in ORDEN if n != consulta and n not in evidencia]
    resultado = {}
    for valor in RED[consulta][1]:
        total = 0.0
        for combinacion in product(*[RED[n][1] for n in ocultas]):   # todas las combinaciones de ocultas
            asignacion = dict(evidencia, **{consulta: valor}, **dict(zip(ocultas, combinacion)))
            total += prob_conjunta(asignacion)
        resultado[valor] = total                  # = P(consulta=valor, evidencia)
    z = sum(resultado.values())                   # = P(evidencia)
    return {v: p / z for v, p in resultado.items()}

def mostrar(titulo, dist):
    print(titulo)
    for v, p in sorted(dist.items(), key=lambda kv: -kv[1]):
        print(f"  {str(v):22s} {p:.3f}")

mostrar("P(causa) sin evidencia:", inferir("causa", {}))
mostrar("P(causa | retraso):", inferir("causa", {"retraso": True}))
mostrar("P(causa | retraso, almacen=getafe):", inferir("causa", {"retraso": True, "almacen": "getafe"}))
mostrar("P(causa | paquete_danado, retraso, no equivocado, sí llega):",
        inferir("causa", {"paquete_danado": True, "retraso": True, "producto_equivocado": False, "no_llega": False}))
mostrar("P(almacen | producto_equivocado):", inferir("almacen", {"producto_equivocado": True}))
mostrar("P(no_llega | almacen=getafe, retraso):", inferir("no_llega", {"almacen": "getafe", "retraso": True}))

Salida:

P(causa) sin evidencia:
  error_picking          0.310
  dano_transporte        0.260
  stock_desactualizado   0.180
  direccion_incorrecta   0.138
  fallo_proveedor        0.112
P(causa | retraso):
  stock_desactualizado   0.375
  direccion_incorrecta   0.192
  fallo_proveedor        0.181
  dano_transporte        0.180
  error_picking          0.072
P(causa | retraso, almacen=getafe):
  stock_desactualizado   0.567
  direccion_incorrecta   0.137
  fallo_proveedor        0.134
  dano_transporte        0.115
  error_picking          0.048
P(causa | paquete_danado, retraso, no equivocado, sí llega):
  dano_transporte        0.864
  fallo_proveedor        0.090
  stock_desactualizado   0.027
  error_picking          0.014
  direccion_incorrecta   0.004
P(almacen | producto_equivocado):
  zaragoza               0.646
  getafe                 0.354
P(no_llega | almacen=getafe, retraso):
  False                  0.603
  True                   0.397

Explicación:

  • RED es la red entera: para cada nodo, sus padres, sus valores posibles y su CPT como diccionario de diccionarios; cpt_binaria evita escribir dos veces cada síntoma. ORDEN lista los nodos con los padres antes que los hijos, para poder aplicar la regla de la cadena.
  • prob_local lee la CPT: busca la fila correspondiente a los valores actuales de los padres y devuelve la probabilidad del valor del nodo. prob_conjunta multiplica las seis probabilidades locales: P(almacen)·P(causa | almacen)·P(dañado | causa)·... Es la factorización de la sección 6.
  • inferir hace la enumeración: para cada valor de la consulta, suma la conjunta sobre todas las combinaciones de las variables ocultas (product), lo que da P(consulta = valor, evidencia); al final divide por la suma (P(evidencia)) para obtener el posterior. Con 160 combinaciones totales es instantáneo.
  • Lee las respuestas como un diagnosticador: sin evidencia, la causa más probable es el picking (0,31); un retraso solo desplaza la creencia al stock desactualizado (0,375) y la dirección incorrecta; saber que el pedido salió de Getafe dispara el stock a 0,567 (el almacén influye en el síntoma a través de la causa); la evidencia completa de la incidencia 7731 (dañado, con retraso, producto correcto, ha llegado) da transporte al 0,864, un poco más que el 0,816 de Naive Bayes porque "sí llega" descarta dirección incorrecta y stock. La red también razona hacia arriba: un producto equivocado hace más probable Zaragoza (0,646 frente al prior 0,60), porque allí pesa más el picking; y entre síntomas: sabiendo el almacén y el retraso, predice si el paquete llegará (0,397 de que no). Todo con las mismas 29 cifras.

Este último punto es lo que un sistema de reglas no puede hacer: usar el mismo conocimiento para diagnosticar (síntomas → causa), predecir (causa → síntomas) y explicar (dado un síntoma, ¿qué otro esperar?). Y las variables observadas se explican con la traza de la fórmula: "transporte 0,864 porque el prior de transporte en Zaragoza/Getafe es 0,26 y el paquete dañado es 8 veces más probable con transporte que con picking".

  1. Inferencia aproximada y aprendizaje de las CPT

Dos notas para no crear falsas expectativas:

  • Inferencia aproximada. La enumeración suma sobre todas las combinaciones de variables ocultas: exponencial. Con seis nodos son 160; con cincuenta síntomas y causas intermedias, inabordable. Los algoritmos exactos más listos (eliminación de variables, árboles de unión) explotan la estructura del grafo, y cuando ni eso alcanza se usa muestreo: generar miles de "incidencias virtuales" siguiendo las CPT (muestreo directo, con rechazo, ponderación por verosimilitud, MCMC) y contar en qué fracción la causa es cada cosa. Es la misma idea Monte Carlo del módulo 3 y de la búsqueda de AlphaGo.
  • Aprender las CPT de incidencias.csv. Las 29 cifras de la red las hemos puesto nosotros; en la práctica se estiman contando: P(paquete_danado = sí | causa = transporte) ≈ (nº de incidencias de transporte con paquete dañado + 1) / (nº de incidencias de transporte + 2), con el "+1/+2" del suavizado de Laplace para que un síntoma nunca visto con una causa no reciba probabilidad exactamente cero (que anularía cualquier producto). Es lo que hace MultinomialNB.fit. Y aquí reaparece el problema de 02-03: solo ~200 filas tienen causa; con 5 causas y 4 síntomas, algunas celdas se estiman con menos de diez casos, y las 200 se rellenaron los días tranquilos (sesgo de muestreo). Diego hizo bien en hacer obligatoria la causa: la calidad de la red la fijan los datos con que se estimen sus tablas; la estructura (qué flecha va a dónde) suele ponerla el experto, aunque también existen algoritmos para aprenderla.

  1. Decisiones bajo incertidumbre: utilidad esperada

Saber que P(transporte | evidencia) = 0,864 no es todavía una acción. En 02-01 dijimos que el agente racional elige la acción de mayor utilidad esperada: para cada acción, suma sobre los estados posibles de la probabilidad del estado por la utilidad del resultado. Diego tiene, ante una incidencia de 60 €, dos acciones: reembolsar directamente (rápido, el cliente contento, pero se pierde la posibilidad de recuperar el importe del transportista o el producto) o abrir investigación (cuesta tiempo de una persona y retrasa la solución, pero según la causa se recupera parte del coste). Ponemos utilidades en euros (negativas: coste para NovaMarket), a partir de la experiencia de operaciones:

Utilidad (€) error_picking dano_transporte direccion_incorrecta stock_desactualizado fallo_proveedor
reembolsar_directo −60 −60 −60 −60 −60
investigar −45 (se recupera el producto) −25 (reclama al transportista) −30 (reenvío a cargo del cliente) −95 (nada que recuperar y cliente más enfadado) −30 (cargo al proveedor)
UTILIDAD = {
    "reembolsar_directo": {c: -60 for c in CAUSAS},
    "investigar": {"error_picking": -45, "dano_transporte": -25, "direccion_incorrecta": -30,
                   "stock_desactualizado": -95, "fallo_proveedor": -30},
}
def utilidad_esperada(accion, posterior):
    return sum(posterior[c] * UTILIDAD[accion][c] for c in CAUSAS)

def decidir(evidencia):
    post = inferir("causa", evidencia)
    eus = {a: utilidad_esperada(a, post) for a in UTILIDAD}
    mejor = max(eus, key=eus.get)
    print(f"evidencia {evidencia}")
    for a, u in eus.items():
        print(f"  UE({a}) = {u:7.2f} €")
    print(f"  -> {mejor}")

decidir({})
decidir({"retraso": True})
decidir({"retraso": True, "almacen": "getafe"})
decidir({"paquete_danado": True})

Salida:

evidencia {}
  UE(reembolsar_directo) =  -60.00 €
  UE(investigar) =  -45.05 €
  -> investigar
evidencia {'retraso': True}
  UE(reembolsar_directo) =  -60.00 €
  UE(investigar) =  -54.54 €
  -> investigar
evidencia {'retraso': True, 'almacen': 'getafe'}
  UE(reembolsar_directo) =  -60.00 €
  UE(investigar) =  -66.98 €
  -> reembolsar_directo
evidencia {'paquete_danado': True}
  UE(reembolsar_directo) =  -60.00 €
  UE(investigar) =  -28.70 €
  -> investigar

Sin saber nada, investigar compensa (−45 frente a −60). Con un retraso, todavía, pero por poco. Con retraso y Getafe, el stock desactualizado es tan probable (0,567) que investigar sale más caro que reembolsar: la decisión cambia con la evidencia. Con un paquete dañado, investigar es claramente mejor porque casi seguro se reclama al transportista. Esto es un agente basado en utilidad de 02-01 con la red como modelo del mundo, y es también el esquema de la revisión humana de 02-04: se puede añadir una tercera acción, "pasar a una persona", con su coste, y dejar que la utilidad esperada diga cuándo merece la pena. La red diagnostica; la utilidad decide; y las dos partes son legibles y ajustables por Diego.

Errores Comunes y Consejos

  • Confundir P(síntoma | causa) con P(causa | síntoma). La primera la da el experto o los datos ("el 80 % de los daños de transporte llegan abollados"); la segunda es la que buscas y necesita el prior. Bayes es el puente; no lo cruces mentalmente sin él.
  • Ignorar la tasa base. La causa que "mejor explica" el síntoma no es la más probable si es rara. Mira siempre la columna del prior.
  • Tratar pertenencias difusas o factores de certeza como probabilidades. No lo son; no las combines con Bayes ni sumes sobre ellas.
  • Ceros en las CPT. Un 0 estimado de pocos datos aniquila el producto y hace imposible una causa para siempre. Suaviza (Laplace) y reserva el 0 para lo lógicamente imposible.
  • Suponer independencia condicional sin pensarla. Naive Bayes funciona sorprendentemente bien, pero si dos síntomas son casi el mismo (retraso y "no llega en fecha"), contarán doble. Una red con la estructura adecuada lo corrige.
  • Olvidar que las probabilidades son de un contexto. Las CPT estimadas con incidencias de Zaragoza no valen para Getafe: por eso el almacén es un nodo.
  • Parar en la probabilidad. El objetivo de Diego es decidir; sin utilidades no hay decisión, y una utilidad mal puesta (olvidar el coste de un cliente enfadado) decide mal aunque la red sea perfecta.

Ejercicios

Ejercicio 1. Con la tabla de 1.000 incidencias de la sección 3, calcula a mano: (a) P(no abollado); (b) P(error_picking | no abollado); (c) P(dano_transporte | no abollado). Comprueba que (b) es mayor que el prior de picking y (c) menor que el de transporte, y explica por qué en una frase.

Ejercicio 2. Con inferir, calcula P(causa | no_llega = True) para almacen = "zaragoza" y para almacen = "getafe". ¿Cuál es la causa más probable en cada almacén? ¿Qué acción de la sección 9 elegirías en cada caso (calcula las utilidades esperadas con decidir)? Explica el papel del nodo almacen.

Ejercicio 3. Un producto llega equivocado y además el paquete está dañado. Calcula P(causa | producto_equivocado) y P(causa | producto_equivocado, paquete_danado). ¿Sube o baja error_picking al añadir el daño? ¿Y fallo_proveedor? Explica el resultado en términos de las verosimilitudes de la CPT de paquete_danado.

Soluciones

Solución 1. (a) 748/1.000 = 0,748. (b) 270/748 = 0,361 (prior 0,30). (c) 50/748 = 0,067 (prior 0,25). Un paquete que no está abollado es evidencia en contra del transporte (que casi siempre abolla) y, por eliminación, a favor de las demás causas: la ausencia de un síntoma también informa, y tanto más cuanto más sensible sea el síntoma a la causa.

Solución 2. inferir("causa", {"no_llega": True, "almacen": "zaragoza"}) da direccion_incorrecta 0,525, fallo_proveedor 0,148, stock 0,132, transporte 0,123, picking 0,072; con Getafe: stock_desactualizado 0,445, direccion_incorrecta 0,344, proveedor 0,101, transporte 0,067, picking 0,042. En Zaragoza, la causa más probable de un "no llega" es la dirección incorrecta; en Getafe, el stock desactualizado. decidir({"no_llega": True, "almacen": "zaragoza"}) da investigar −39,02 € frente a −60: investigar con claridad (la dirección se corrige y el reenvío se cobra); con Getafe, investigar −59,23 € frente a −60: empate práctico, en el que bastaría un poco más de peso del stock (o un coste de investigación algo mayor) para que ganara reembolsar. El nodo almacen cambia el prior de las causas, y con él todo el diagnóstico y la decisión, sin tocar las CPT de los síntomas.

Solución 3. P(causa | equivocado): picking 0,789, proveedor 0,122, stock 0,065, transporte 0,019, dirección 0,005. P(causa | equivocado, dañado): picking 0,694, proveedor 0,161, transporte 0,133, stock 0,012, dirección 0,001. El picking baja (0,789 → 0,694) y el proveedor y el transporte suben: el daño es poco probable con picking (0,10) y algo más con proveedor (0,15) y mucho más con transporte (0,80), así que la nueva evidencia reparte creencia hacia ellos; pero el picking sigue siendo la causa más probable porque el producto equivocado es 35 veces más verosímil con picking que con transporte. Bayes hace la cuenta exacta que la intuición hace a ojo.

Conclusión

En esta lección hemos aprendido a razonar cuando las reglas no bastan. Hemos visto por qué el diagnóstico de incidencias no cabe en SI-ENTONCES nítidos (pereza, ignorancia teórica y práctica), y hemos situado los dos atajos históricos: los factores de certeza de MYCIN, intuitivos pero sin semántica, y la lógica difusa, que modela la vaguedad de las palabras ("retraso grave" con pertenencia 0,5) pero no la incertidumbre sobre los hechos. Hemos construido la probabilidad desde la tabla de 1.000 incidencias: conjunta, marginalización, condicional, regla del producto e independencia (y la independencia condicional), hasta el teorema de Bayes, que ha diagnosticado daño en transporte con 0,794 para un paquete abollado, y que hemos leído también en el lenguaje de sensibilidad y especificidad de 04-05. Hemos hecho Naive Bayes a mano con tres síntomas (0,816 para transporte en la incidencia 7731) y hemos generalizado a una red bayesiana de seis nodos (almacén → causa → cuatro síntomas, 29 parámetros en lugar de 159) con inferencia por enumeración en Python puro, capaz de diagnosticar, predecir y razonar hacia arriba con el mismo conocimiento; hemos anotado cómo se aproxima la inferencia cuando la red crece y cómo se aprenden las CPT de incidencias.csv con suavizado. Y hemos cerrado el círculo con 02-01: la utilidad esperada convierte el posterior en la decisión de investigar o reembolsar, y la decisión cambia cuando llega la evidencia de Getafe.

Con esto NovaMarket dispone de las tres herramientas simbólicas del módulo: la lógica para representar (06-01), el sistema experto para decidir con reglas (06-02) y la probabilidad para decidir con incertidumbre (06-03). Falta ver dónde se usan de verdad, hoy, fuera de nuestro laboratorio: en la medicina que MYCIN inauguró, en la banca y el cumplimiento normativo, en la industria, en el derecho y en el comercio electrónico; en qué se han convertido los sistemas expertos (motores de reglas de negocio, tablas de decisión, grafos de conocimiento) y, sobre todo, cómo se combinan con el ML y los LLM de los módulos 4 y 5 en el enfoque neurosimbólico que anunciamos al cerrar 05-05: un modelo que estima una probabilidad y una regla que decide con ella. Es el tema de la última lección del módulo, 06-04.

Fundamentos de Inteligencia Artificial (IA)

Módulo 1: Introducción a la Inteligencia Artificial

Módulo 2: Principios Básicos de la IA

Módulo 3: Algoritmos en IA

Módulo 4: Aprendizaje Automático (Machine Learning)

Módulo 5: Redes Neuronales y Deep Learning

Módulo 6: Lógica y Sistemas Expertos

Módulo 7: Herramientas y Lenguajes de Programación en IA

Módulo 8: Proyectos y Casos de Estudio

Módulo 9: Ejercicios y Prácticas

Módulo 10: Recursos Adicionales

© Copyright 2026. Todos los derechos reservados