48  Itemsets Cerrados y Maximales: Representaciones Compactas

Autor/a

Diego Villalba

Apriori y FP-Growth resuelven el problema de encontrar todos los itemsets frecuentes. Este capítulo trata un problema distinto que aparece justo después: hay demasiados. Si un cliente típico compra juntos 20 productos de despensa, el itemset con esos 20 productos es frecuente y, por la propiedad antimonótona, también lo son sus \(2^{20} - 1 \approx 10^6\) subconjuntos. La lista completa es correcta, pero casi toda es redundante: describe una y otra vez el mismo hecho. Las representaciones compactas guardan solo una parte de los itemsets frecuentes, elegida de modo que el resto se pueda deducir (Pasquier et al. 1999; Han et al. 2012).

48.1 Intuición: once patrones que dicen casi lo mismo

Retomemos los cinco tickets del capítulo de FP-Growth, con conteo mínimo 3:

Ticket Productos
\(T_1\) Pan, Leche
\(T_2\) Pan, Leche, Pañales, Cerveza
\(T_3\) Leche, Pañales, Cerveza
\(T_4\) Pan, Leche, Pañales, Cerveza
\(T_5\) Pan, Leche, Pañales
Tabla 48.1: Base de transacciones del capítulo de FP-Growth.

Con esos datos hay 11 itemsets frecuentes. Mira dos de ellos: \(\{\text{Pan}\}\) aparece en 4 tickets y \(\{\text{Leche}, \text{Pan}\}\) también en 4. No es coincidencia: todo ticket que tiene pan tiene leche. Saber que \(\{\text{Leche}, \text{Pan}\}\) tiene conteo 4 ya nos dice el conteo de \(\{\text{Pan}\}\); reportar ambos es repetir información.

Hay dos maneras naturales de quitar esa redundancia:

  • Quedarse con los itemsets que no se pueden agrandar sin perder conteo. \(\{\text{Pan}\}\) se puede agrandar a \(\{\text{Leche}, \text{Pan}\}\) sin perder nada, así que sobra. Estos itemsets se llaman cerrados y, como veremos, con ellos se recupera el conteo exacto de cualquier itemset frecuente.
  • Quedarse con los itemsets que no se pueden agrandar sin dejar de ser frecuentes. Son la «frontera» de la retícula y se llaman maximales. Con ellos sabemos cuáles itemsets son frecuentes, pero no cuánto.

48.2 Construcción numérica paso a paso

Para cada itemset frecuente preguntamos dos cosas: ¿tiene algún superconjunto con el mismo conteo? (si sí, no es cerrado) y ¿tiene algún superconjunto frecuente? (si sí, no es maximal). Basta revisar los superconjuntos con un ítem más1.

1 Si algún superconjunto \(Y \supsetneq X\) tiene el mismo conteo que \(X\), entonces también lo tiene cualquier itemset intermedio \(X \subsetneq Z \subseteq Y\), porque por antimonotonía \(\sigma(X) \ge \sigma(Z) \ge \sigma(Y) = \sigma(X)\). En particular, lo tiene algún superconjunto con un solo ítem más. Lo mismo vale para «ser frecuente».

Itemset frecuente Conteo Superconjunto con el mismo conteo ¿Cerrado? ¿Maximal?
\(\{\text{Cerveza}\}\) 3 \(\{\text{Cerveza}, \text{Leche}\}\) (3) no no
\(\{\text{Leche}\}\) 5 ninguno (los pares llegan a 4) sí no
\(\{\text{Pan}\}\) 4 \(\{\text{Leche}, \text{Pan}\}\) (4) no no
\(\{\text{Pañales}\}\) 4 \(\{\text{Leche}, \text{Pañales}\}\) (4) no no
\(\{\text{Cerveza}, \text{Leche}\}\) 3 \(\{\text{Cerveza}, \text{Leche}, \text{Pañales}\}\) (3) no no
\(\{\text{Cerveza}, \text{Pañales}\}\) 3 \(\{\text{Cerveza}, \text{Leche}, \text{Pañales}\}\) (3) no no
\(\{\text{Leche}, \text{Pan}\}\) 4 ninguno: \(\{\text{Leche}, \text{Pan}, \text{Pañales}\}\) baja a 3 sí no
\(\{\text{Leche}, \text{Pañales}\}\) 4 ninguno: los tríos bajan a 3 sí no
\(\{\text{Pan}, \text{Pañales}\}\) 3 \(\{\text{Leche}, \text{Pan}, \text{Pañales}\}\) (3) no no
\(\{\text{Cerveza}, \text{Leche}, \text{Pañales}\}\) 3 ninguno: el cuarteto tiene conteo 2 sí sí
\(\{\text{Leche}, \text{Pan}, \text{Pañales}\}\) 3 ninguno: el cuarteto tiene conteo 2 sí sí
Tabla 48.2: Clasificación de los 11 itemsets frecuentes con conteo mínimo 3.

El resultado es una cadena de tres conjuntos: 11 frecuentes, 5 cerrados y 2 maximales. Los cerrados son \(\{\text{Leche}\}{:}5\), \(\{\text{Leche}, \text{Pan}\}{:}4\), \(\{\text{Leche}, \text{Pañales}\}{:}4\), \(\{\text{Leche}, \text{Pan}, \text{Pañales}\}{:}3\) y \(\{\text{Cerveza}, \text{Leche}, \text{Pañales}\}{:}3\); los maximales, los dos tríos.

Un atajo para encontrar el cerrado de un itemset. Junta los tickets que contienen al itemset y quédate con los productos que están en todos ellos. Para \(\{\text{Cerveza}\}\): aparece en \(T_2\), \(T_3\) y \(T_4\), y los productos comunes a esos tres tickets son Cerveza, Leche y Pañales. Por lo tanto, el cerrado que «representa» a \(\{\text{Cerveza}\}\) es \(\{\text{Cerveza}, \text{Leche}, \text{Pañales}\}\), con el mismo conteo 3. Esta operación se llama clausura.

Recuperar un conteo desde los cerrados. ¿Cuál es el conteo de \(\{\text{Pan}, \text{Pañales}\}\) si solo guardamos los 5 cerrados? Buscamos los cerrados que lo contienen: solo \(\{\text{Leche}, \text{Pan}, \text{Pañales}\}{:}3\). El conteo es 3. Para \(\{\text{Pan}\}\) hay dos cerrados que lo contienen, \(\{\text{Leche}, \text{Pan}\}{:}4\) y \(\{\text{Leche}, \text{Pan}, \text{Pañales}\}{:}3\); el conteo es el mayor, 4.

Lo que se pierde con los maximales. Con los dos tríos maximales sabemos que \(\{\text{Pan}, \text{Pañales}\}\) es frecuente (está contenido en \(\{\text{Leche}, \text{Pan}, \text{Pañales}\}\)) y que \(\{\text{Cerveza}, \text{Pan}\}\) no lo es (ningún maximal lo contiene). Pero del conteo de \(\{\text{Pan}\}\) solo podemos decir que es al menos 3.

48.3 Formalización

Usamos la notación de los capítulos anteriores: \(\mathcal{D}\) es la base de transacciones, \(\sigma(X)\) el conteo de soporte de \(X\) y \(\mathcal{F}\) el conjunto de itemsets frecuentes con un umbral dado.

Definición (itemset cerrado). Un itemset \(X\) es cerrado si ningún superconjunto propio tiene el mismo conteo: \[X \text{ es cerrado} \iff \nexists\, Y \supsetneq X \text{ con } \sigma(Y) = \sigma(X).\] El conjunto de itemsets frecuentes cerrados es \(\mathcal{C} = \{X \in \mathcal{F} : X \text{ es cerrado}\}\).

Definición (itemset maximal). Un itemset frecuente \(X\) es maximal si ningún superconjunto propio es frecuente: \[X \text{ es maximal} \iff X \in \mathcal{F} \text{ y } \nexists\, Y \supsetneq X \text{ con } Y \in \mathcal{F}.\] Lo denotamos \(\mathcal{M}\).

El operador de clausura. Sea \(t(X) = \{T \in \mathcal{D} : X \subseteq T\}\) el conjunto de tickets que contienen a \(X\). La clausura de \(X\) es la intersección de esos tickets: \[\gamma(X) = \bigcap_{T \in t(X)} T. \tag{48.1}\]

\(\gamma(X)\) son los ítems que acompañan a \(X\) siempre. Tiene las tres propiedades de un operador de clausura2: contiene a \(X\), respeta la inclusión y aplicarlo dos veces no cambia nada. Además, \(X\) y \(\gamma(X)\) aparecen exactamente en los mismos tickets, así que \(\sigma(\gamma(X)) = \sigma(X)\). De ahí sale una caracterización muy útil: \[X \text{ es cerrado} \iff \gamma(X) = X. \tag{48.2}\]

2 Un operador de clausura sobre los subconjuntos de \(\mathcal{I}\) es una función \(\gamma\) que cumple tres propiedades: es extensiva, \(X \subseteq \gamma(X)\); es monótona, \(X \subseteq Y \Rightarrow \gamma(X) \subseteq \gamma(Y)\); y es idempotente, \(\gamma(\gamma(X)) = \gamma(X)\). Los conjuntos fijos (\(\gamma(X) = X\)) se llaman cerrados. El nombre viene de la topología y del álgebra, donde «cerrar» un conjunto es agregarle todo lo que se deduce de él.

Tres resultados que justifican el uso de estas representaciones:

  1. Cadena de inclusiones. \(\mathcal{M} \subseteq \mathcal{C} \subseteq \mathcal{F}\). Si \(X\) es maximal y \(Y \supsetneq X\), entonces \(Y\) no es frecuente y \(\sigma(Y) < \sigma(X)\); por lo tanto \(X\) es cerrado.
  2. Los cerrados no pierden información (Pasquier et al. 1999). Para todo \(X \in \mathcal{F}\), \[\sigma(X) = \max\{\sigma(C) : C \in \mathcal{C},\; X \subseteq C\}. \tag{48.3}\] El máximo se alcanza en \(C = \gamma(X)\), que es cerrado y tiene el mismo conteo que \(X\); cualquier otro cerrado que contenga a \(X\) tiene conteo menor o igual por antimonotonía. Si ningún cerrado frecuente contiene a \(X\), entonces \(X\) no es frecuente.
  3. Los maximales describen la frontera, no los conteos. \(X\) es frecuente si y solo si está contenido en algún maximal, pero de su conteo solo se sabe que es al menos el umbral.

Por eso se habla de representaciones condensadas3: los cerrados son una compresión sin pérdida de \(\mathcal{F}\) con sus conteos; los maximales son una compresión con pérdida, mucho más pequeña.

3 Una representación es condensada si a partir de ella se puede reconstruir toda la información de interés. Es la misma distinción que en compresión de archivos: ZIP es sin pérdida (se recupera el original exacto), JPEG es con pérdida (se recupera una aproximación). Los cerrados son el «ZIP» de los itemsets frecuentes; los maximales, el «JPEG».

Relación con las reglas. Una regla con confianza 1 dice que el antecedente arrastra siempre al consecuente; eso es justo la clausura. \(\text{Pan} \Rightarrow \text{Leche}\) tiene confianza \(4/4 = 1\) porque \(\gamma(\{\text{Pan}\}) = \{\text{Leche}, \text{Pan}\}\). En general, toda regla exacta tiene la forma \(X \Rightarrow \gamma(X) \setminus X\), y las reglas generadas desde los cerrados evitan buena parte de la redundancia que produce Apriori (Pasquier et al. 1999).

Algoritmos. Se pueden obtener \(\mathcal{C}\) o \(\mathcal{M}\) filtrando la salida de Apriori o FP-Growth, que es lo que haremos en Python. Cuando \(\mathcal{F}\) es enorme conviene minar directamente la representación compacta:

Algoritmo Qué mina Idea principal
A-Close Cerrados Apriori por niveles, generando solo clausuras (Pasquier et al. 1999)
CHARM Cerrados Búsqueda en profundidad con listas de tickets (tidsets) (Zaki y Hsiao 2002)
CLOSET+ Cerrados Crecimiento de patrones sobre un FP-tree (Wang et al. 2003)
MaxMiner Maximales Mira hacia adelante: si la rama completa es frecuente, la poda entera (Bayardo 1998)
FPMax Maximales FP-Growth que descarta subconjuntos de maximales ya encontrados (Grahne y Zhu 2003)
Tabla 48.3: Algoritmos clásicos para representaciones compactas.

48.4 Laboratorio: frecuentes, cerrados y maximales en la retícula

El laboratorio dibuja los 15 itemsets de la Tabla 48.1 y los clasifica según el conteo mínimo. Haz clic en un nodo para ver su clausura (la flecha morada lleva al cerrado que lo representa) y cómo se recupera su conteo desde los cerrados. Al final puedes editar los tickets.

Laboratorio · Representaciones compactas

¿Cuántos itemsets hay que guardar para no perder nada?

Mueve el conteo mínimo, haz clic en un nodo y, al final, edita los tickets.

Retícula de itemsets (C = Cerveza, L = Leche, P = Pan, Ñ = Pañales)

maximal (y cerrado) cerrado, no maximal frecuente, no cerrado infrecuente clausura γ(X)
Frecuentes |F|
Cerrados |C|
Maximales |M|
Se guarda (cerrados)

Tickets

Recuperar el conteo

Editar los tickets

Guía del laboratorio.

  1. Predice antes de hacer clic: ¿cuál es la clausura de \(\{\text{Ñ}\}\) (Pañales)? Haz clic en el nodo y comprueba en el panel la intersección de tickets.
  2. Comprueba los números de la Tabla 48.2: con conteo mínimo 3 deben aparecer 11 frecuentes, 5 cerrados y 2 maximales. Activa «Mostrar todas las clausuras»: ¿a qué cerrado apunta cada uno de los 6 itemsets azules?
  3. Selecciona \(\{P, Ñ\}\) y recupera su conteo desde los cerrados. ¿Qué puedes decir de él si solo conservas los maximales?
  4. Baja el conteo mínimo a 2. ¿Por qué solo queda un maximal? ¿Por qué el número de cerrados casi no cambia mientras que el de frecuentes sube a 15?
  5. Rómpelo: quita la leche de \(T_1\). ¿Sigue siendo \(\{L\}\) cerrado? ¿Qué pasa con \(\{P\}\)? Explica el cambio con la clausura.

48.5 Implementación en Python

48.5.1 Desde cero: clausura y clasificación

Las definiciones se traducen casi literalmente. La clasificación revisa solo los superconjuntos con un ítem más, como en la construcción manual.

Mostrar código
from itertools import combinations
import pandas as pd

tickets = [
    {"Pan", "Leche"},
    {"Pan", "Leche", "Pañales", "Cerveza"},
    {"Leche", "Pañales", "Cerveza"},
    {"Pan", "Leche", "Pañales", "Cerveza"},
    {"Pan", "Leche", "Pañales"},
]
items = sorted(set().union(*tickets))
min_conteo = 3

def conteo(X):
    return sum(X <= t for t in tickets)

def clausura(X):
    """Intersección de los tickets que contienen a X (Ec. de la clausura)."""
    cubren = [t for t in tickets if X <= t]
    return frozenset.intersection(*map(frozenset, cubren)) if cubren else None

todos = {frozenset(c): conteo(set(c))
         for k in range(1, len(items) + 1) for c in combinations(items, k)}
frecuentes = {X: c for X, c in todos.items() if c >= min_conteo}

filas = []
for X, c in frecuentes.items():
    superiores = [X | {i} for i in items if i not in X]
    filas.append({
        "itemset": "{" + ", ".join(sorted(X)) + "}",
        "conteo": c,
        "clausura": "{" + ", ".join(sorted(clausura(X))) + "}",
        "cerrado": all(todos[Y] < c for Y in superiores),
        "maximal": all(todos[Y] < min_conteo for Y in superiores),
    })
tabla = pd.DataFrame(filas).sort_values(["cerrado", "conteo"], ascending=False)
print(f"Frecuentes: {len(tabla)} | cerrados: {tabla['cerrado'].sum()} | maximales: {tabla['maximal'].sum()}")
tabla
Frecuentes: 11 | cerrados: 5 | maximales: 2
itemset conteo clausura cerrado maximal
1 {Leche} 5 {Leche} True False
6 {Leche, Pan} 4 {Leche, Pan} True False
7 {Leche, Pañales} 4 {Leche, Pañales} True False
9 {Cerveza, Leche, Pañales} 3 {Cerveza, Leche, Pañales} True True
10 {Leche, Pan, Pañales} 3 {Leche, Pan, Pañales} True True
2 {Pan} 4 {Leche, Pan} False False
3 {Pañales} 4 {Leche, Pañales} False False
0 {Cerveza} 3 {Cerveza, Leche, Pañales} False False
4 {Cerveza, Leche} 3 {Cerveza, Leche, Pañales} False False
5 {Cerveza, Pañales} 3 {Cerveza, Leche, Pañales} False False
8 {Pan, Pañales} 3 {Leche, Pan, Pañales} False False

La columna clausura confirma la caracterización de la Ecuación 48.2: un itemset es cerrado exactamente cuando coincide con su clausura.

48.5.2 Desde la salida de mlxtend

En la práctica, los itemsets frecuentes salen de fpgrowth o apriori (Raschka 2018). El resultado es un DataFrame con una columna itemsets de frozenset, así que basta un diccionario itemset → conteo para clasificar. Trabajamos con conteos enteros (soporte × \(n\)) para que la comparación «mismo conteo» sea exacta y no dependa del redondeo de los flotantes.

Mostrar código
import numpy as np
from mlxtend.preprocessing import TransactionEncoder
from mlxtend.frequent_patterns import fpgrowth, fpmax

def clasificar_itemsets(frecuentes_df, n):
    """Agrega columnas conteo, cerrado y maximal a la salida de fpgrowth/apriori."""
    conteos = {X: int(round(s * n)) for X, s in zip(frecuentes_df["itemsets"], frecuentes_df["support"])}
    universo = set().union(*conteos)
    cerrado, maximal = [], []
    for X, c in conteos.items():
        # superconjuntos frecuentes con un ítem más (los no frecuentes no están en el diccionario)
        sup = [conteos[X | {i}] for i in universo - X if X | {i} in conteos]
        cerrado.append(all(cs < c for cs in sup))
        maximal.append(len(sup) == 0)
    return frecuentes_df.assign(conteo=list(conteos.values()), cerrado=cerrado, maximal=maximal)

te = TransactionEncoder()
df = pd.DataFrame(te.fit_transform([list(t) for t in tickets]), columns=te.columns_)
n = len(df)
fi = clasificar_itemsets(fpgrowth(df, min_support=min_conteo / n, use_colnames=True), n)

cerrados = fi[fi["cerrado"]]
maximales = fi[fi["maximal"]]
print(f"fpgrowth: {len(fi)} frecuentes -> {len(cerrados)} cerrados -> {len(maximales)} maximales")

# Comprobación cruzada con FPMax, que mina los maximales directamente
mx = fpmax(df, min_support=min_conteo / n, use_colnames=True)
print("¿FPMax coincide?", set(mx["itemsets"]) == set(maximales["itemsets"]))
cerrados[["itemsets", "conteo", "maximal"]].sort_values("conteo", ascending=False)
fpgrowth: 11 frecuentes -> 5 cerrados -> 2 maximales
¿FPMax coincide? True
itemsets conteo maximal
0 (Leche) 5 False
4 (Pan, Leche) 4 False
7 (Pañales, Leche) 4 False
6 (Pan, Pañales, Leche) 3 True
10 (Cerveza, Pañales, Leche) 3 True

Un detalle sobre «el mismo conteo»: si un superconjunto tiene el mismo conteo que \(X\), es frecuente y, por lo tanto, está en la salida de fpgrowth. Por eso basta mirar el diccionario de frecuentes para decidir si \(X\) es cerrado.

48.5.3 Reconstruir los soportes desde los cerrados

Ahora olvidamos la lista completa y conservamos solo los 5 cerrados. La Ecuación 48.3 reconstruye el conteo de cualquier itemset; lo comprobamos sobre los 11 frecuentes y sobre un itemset infrecuente.

Mostrar código
guardados = dict(zip(cerrados["itemsets"], cerrados["conteo"]))

def conteo_desde_cerrados(X, guardados):
    candidatos = [c for C, c in guardados.items() if X <= C]
    return max(candidatos) if candidatos else None   # None = no es frecuente

reconstruidos = fi.assign(reconstruido=[conteo_desde_cerrados(X, guardados) for X in fi["itemsets"]])
print("¿Todos los conteos coinciden?", (reconstruidos["reconstruido"] == reconstruidos["conteo"]).all())
print("Conteo de {Cerveza, Pan} desde los cerrados:",
      conteo_desde_cerrados(frozenset({"Cerveza", "Pan"}), guardados), "(infrecuente)")
reconstruidos[["itemsets", "conteo", "reconstruido"]].sort_values("conteo", ascending=False)
¿Todos los conteos coinciden? True
Conteo de {Cerveza, Pan} desde los cerrados: None (infrecuente)
itemsets conteo reconstruido
0 (Leche) 5 5
1 (Pan) 4 4
2 (Pañales) 4 4
4 (Pan, Leche) 4 4
7 (Pañales, Leche) 4 4
3 (Cerveza) 3 3
5 (Pan, Pañales) 3 3
6 (Pan, Pañales, Leche) 3 3
8 (Cerveza, Pañales) 3 3
9 (Cerveza, Leche) 3 3
10 (Cerveza, Pañales, Leche) 3 3

48.5.4 Ejemplo práctico: datos dispersos frente a datos densos

¿Cuánto se ahorra en la práctica? Depende de cuánto se repitan las combinaciones. Comparamos dos bases:

  • Dispersa: las 50 canastas de 3 productos de los capítulos de Apriori y FP-Growth.
  • Enriquecida con la jerarquía: 300 canastas simuladas en las que, además de cada producto, se agregan su categoría y su departamento, tal como vienen en la dimensión producto del almacén de datos (café molido → café → bebidas). Es una práctica común para encontrar patrones a varios niveles de detalle, y vuelve los datos mucho más densos.
Mostrar código
import plotly.graph_objects as go
from plotly.subplots import make_subplots

canastas = [
    ["whole milk", "yogurt", "bread"], ["whole milk", "butter", "eggs"], ["whole milk", "yogurt", "vegetables"],
    ["bread", "butter", "eggs"], ["whole milk", "soda", "chips"], ["yogurt", "fruit", "bread"],
    ["whole milk", "coffee", "bread"], ["eggs", "butter", "cheese"], ["whole milk", "vegetables", "fruit"],
    ["soda", "chips", "candy"], ["whole milk", "bread", "eggs"], ["yogurt", "fruit", "granola"],
    ["whole milk", "coffee", "sugar"], ["bread", "eggs", "cheese"], ["whole milk", "yogurt", "coffee"],
    ["butter", "eggs", "bread"], ["chips", "soda", "beer"], ["whole milk", "vegetables", "butter"],
    ["yogurt", "granola", "fruit"], ["whole milk", "bread", "sugar"], ["eggs", "cheese", "vegetables"],
    ["whole milk", "soda", "bread"], ["coffee", "sugar", "bread"], ["yogurt", "whole milk", "fruit"],
    ["butter", "whole milk", "eggs"], ["bread", "chips", "soda"], ["whole milk", "granola", "yogurt"],
    ["vegetables", "fruit", "bread"], ["whole milk", "butter", "cheese"], ["eggs", "bread", "coffee"],
    ["whole milk", "yogurt", "butter"], ["chips", "candy", "soda"], ["whole milk", "coffee", "eggs"],
    ["bread", "vegetables", "fruit"], ["yogurt", "cheese", "eggs"], ["whole milk", "bread", "coffee"],
    ["soda", "beer", "chips"], ["whole milk", "vegetables", "eggs"], ["yogurt", "granola", "whole milk"],
    ["bread", "butter", "sugar"], ["whole milk", "fruit", "cheese"], ["eggs", "vegetables", "butter"],
    ["whole milk", "chips", "soda"], ["coffee", "bread", "butter"], ["yogurt", "whole milk", "bread"],
    ["eggs", "cheese", "bread"], ["whole milk", "sugar", "butter"], ["vegetables", "fruit", "yogurt"],
    ["whole milk", "bread", "butter"], ["granola", "yogurt", "fruit"],
]

jerarquia = {  # producto: (categoría, departamento)
    "café molido": ("café", "bebidas"), "café soluble": ("café", "bebidas"),
    "leche entera": ("lácteos", "frescos"), "leche deslactosada": ("lácteos", "frescos"),
    "yogur": ("lácteos", "frescos"), "pan de caja": ("panadería", "abarrotes"),
    "bolillo": ("panadería", "abarrotes"), "refresco cola": ("refrescos", "bebidas"),
    "agua mineral": ("aguas", "bebidas"), "papas": ("botanas", "abarrotes"),
    "cacahuates": ("botanas", "abarrotes"), "manzana": ("fruta", "frescos"),
    "plátano": ("fruta", "frescos"),
}
prob_compra = [.25, .15, .30, .15, .20, .25, .20, .25, .15, .20, .10, .20, .20]
rng = np.random.default_rng(7)
densas = []
for _ in range(300):
    compra = [p for p, q in zip(jerarquia, prob_compra) if rng.random() < q] or [str(rng.choice(list(jerarquia)))]
    niveles = set(compra) | {jerarquia[p][0] for p in compra} | {jerarquia[p][1] for p in compra}
    densas.append(sorted(niveles))
print("Ejemplo de canasta enriquecida:", densas[0])

def conteos_por_umbral(transacciones, umbrales):
    te = TransactionEncoder()
    X = pd.DataFrame(te.fit_transform(transacciones), columns=te.columns_)
    filas = []
    for s in umbrales:
        f = clasificar_itemsets(fpgrowth(X, min_support=s, use_colnames=True), len(X))
        filas.append({"soporte mínimo": s, "frecuentes": len(f),
                      "cerrados": int(f["cerrado"].sum()), "maximales": int(f["maximal"].sum())})
    return pd.DataFrame(filas)

umbrales = [0.06, 0.08, 0.10, 0.15, 0.20, 0.30]
resultados = {"Canastas dispersas (50)": conteos_por_umbral(canastas, umbrales),
              "Canastas con jerarquía (300)": conteos_por_umbral(densas, umbrales)}

colores = {"frecuentes": "#2563eb", "cerrados": "#16a34a", "maximales": "#ea580c"}
fig = make_subplots(rows=1, cols=2, subplot_titles=list(resultados), shared_yaxes=True)
for col, (nombre, r) in enumerate(resultados.items(), start=1):
    for serie, color in colores.items():
        fig.add_scatter(x=r["soporte mínimo"], y=r[serie], name=serie, mode="lines+markers",
                        line=dict(color=color, width=2), showlegend=(col == 1), row=1, col=col)
fig.update_yaxes(type="log", title_text="Número de itemsets", row=1, col=1)
fig.update_xaxes(title_text="Soporte mínimo")
fig.update_layout(height=380, legend=dict(orientation="h", y=-0.25))
fig.show()
Ejemplo de canasta enriquecida: ['abarrotes', 'bolillo', 'panadería']
Figura 48.1: Número de itemsets frecuentes, cerrados y maximales según el soporte mínimo (escala logarítmica). En las canastas dispersas, frecuentes y cerrados casi coinciden; en las canastas con jerarquía, al bajar el umbral los frecuentes crecen mucho más rápido que los cerrados, y estos más que los maximales.
Mostrar código
pd.DataFrame({nombre: r.set_index("soporte mínimo").loc[0.10] for nombre, r in resultados.items()}).T
frecuentes cerrados maximales
Canastas dispersas (50) 26 24 15
Canastas con jerarquía (300) 313 85 37
Tabla 48.4: Itemsets frecuentes, cerrados y maximales con soporte mínimo 0.10 en las dos bases.

En las canastas dispersas casi todo itemset frecuente ya es cerrado: con tres productos por canasta, rara vez un producto «arrastra» siempre a otro. En las canastas con jerarquía ocurre lo contrario: todo ticket con café molido tiene también «café» y «bebidas», así que \(\gamma(\{\text{café molido}\}) \supseteq \{\text{café molido}, \text{café}, \text{bebidas}\}\) y cada combinación de productos aparece repetida con todas las combinaciones de sus ancestros, con el mismo conteo. Los frecuentes se multiplican; los cerrados, no. La lección práctica: en datos densos o con dependencias exactas, reporta cerrados (si necesitas los conteos) o maximales (si solo necesitas saber qué combinaciones son frecuentes).

Observa también que la compresión de los cerrados depende de que haya conteos exactamente iguales. Si dos productos casi siempre van juntos, pero no siempre, sus itemsets siguen siendo cerrados; para resumir esas dependencias aproximadas existen variantes como los itemsets δ-libres o los cerrados aproximados, que quedan fuera de este capítulo.

48.6 Cuándo usar cada representación

Representación Tamaño ¿Conteos exactos? Uso típico
Frecuentes \(\mathcal{F}\) El más grande Sí, directos Datos dispersos; generar todas las reglas
Cerrados \(\mathcal{C}\) Intermedio Sí, con la Ecuación 48.3 Datos densos; reglas no redundantes; análisis formal de conceptos4
Maximales \(\mathcal{M}\) El más pequeño No, solo «≥ umbral» Describir la frontera; patrones largos; resumen para el negocio

4 El análisis formal de conceptos (formal concept analysis) estudia exactamente esta estructura: pares (conjunto de ítems cerrado, conjunto de tickets que lo contienen), llamados conceptos, ordenados en una retícula (Ganter y Wille 1999). Los itemsets cerrados son las «intenciones» de esos conceptos.

Tabla 48.5: Comparación de las tres representaciones.

Las mismas ideas se extienden más allá de los itemsets. En el capítulo siguiente, de patrones secuenciales, los patrones son secuencias ordenadas de eventos, y allí también se habla de secuencias cerradas y maximales con exactamente las mismas definiciones.

Referencias