Reglas de asociación con python

Reglas de asociación con python

Joaquin Amat Rodrigo
Septiembre, 2023

Más sobre ciencia de datos: cienciadedatos.net

Introducción

Los algoritmos de reglas de asociación se utilizan para encontrar hechos o elementos que ocurren en común dentro de un determinado conjunto de datos. Una regla de asociación se define como una implicación del tipo "si $X$ entonces $Y$" ($X⇒Y$), donde $X$ e $Y$ son dos conjuntos disjuntos de ítems. Por ejemplo, la regla {$A$, $B$} ⇒ {$C$} significa que, cuando ocurren $A$ y $B$, también ocurre $C$. El lado izquierdo de la regla recibe el nombre de antecedente o lenft-hand-side (LHS) y el lado derecho el nombre de consecuente o right-hand-side (RHS).

A cada uno de los eventos o elementos que forman parte de una transacción se le conoce como ítem y a un conjunto de ellos ítemset. Una transacción es un conjunto de ítems relacionados de alguna forma, por ejemplo, los productos que forman parte de una compra en el supermercado. Puede estar formada por uno o varios ítems, en el caso de ser varios, cada posible subconjunto de ellos es a su vez un ítemset distinto. Por ejemplo, la transacción $T$ = {$A$, $B$, $C$} está formada por 3 ítems ($A$, $B$ y $C$) y sus posibles itemsets son: {$A$, $B$, $C$}, {$A$, $B$}, {$B$, $C$}, {$A$, $C$}, {$A$}, {$B$} y {$C$}.

  Nota

  • Ítem: cada uno de los elementos que componen una transacción.

  • Itemset: conjunto de ítems. Un k-itemset es un itemset con k items.

  • Transacción: conjunto de ítems vinculados a un evento concreto y con un identificador único.

La diferencia entre un itemset y una transacción radica en que un itemset es un conjunto de elementos (ítems) que pueden aparecer juntos, mientras que una transacción es una instancia específica que contiene un conjunto de ítems que representan una acción, evento u observación en el conjunto de datos. En el contexto de las reglas de asociación, el objetivo es descubrir relaciones entre los ítems en diferentes transacciones, es decir, encontrar patrones de co-ocurrencia y dependencia entre los ítems en los conjuntos de datos.

Existen varios algoritmos diseñados para identificar itemsets frecuentes y reglas de asociación. Tres de los más destacados son: Apriori, FP-Growth y Eclat. En este documento se describen las principales características de cada uno de ellos así como su utilización en Python con la librería mlxtend.

Almacenamiento de datos de transacciones

La información relacionada con las transacciones se almacena típicamente utilizando dos enfoques: el formato de lista o (basket) y el formato de matriz binaria.

Formato de matriz binaria

En este formato, cada fila representa una transacción y cada columna representa uno de los posibles ítems que podrían formar parte de ella. Las celdas de la matriz se rellenan con valores binarios (1 o 0) que indican si un artículo está presente (1) o ausente (0) en una transacción concreta.

id leche cerveza pan huevos servilletas
1 1 1 0 0 0
2 1 0 1 1 0
3 0 0 1 0 1
4 1 0 1 1 1

Este es el formato de entrada que utilizan la mayoría de algoritmos, sin embargo, no suele ser el más adecuado para almacenar las transacciones ya que requiere conocer de antemano todos los posibles ítems para crear las columnas y, dado que cada transacción suele contener solo una pequeña proporción de los posibles ítems, la mayoría de valores son cero (sparse).

Formato de lista (basket)

En este este formato, cada transacción se representa como una lista de los ítems que forman parte de ella y se le asigna un identificador único por transacción. Este formato es más eficiente desde el punto de vista de la memoria, ya que sólo almacena los elementos presentes en cada transacción.

id items
1 {leche, cerveza}
2 {leche, pan, huevos}
3 {pan, servilletas}
4 {leche, pan, huevos, servilletas}

Apriori

Propuesto por Agrawal and Srikant en 1994 es uno de los primeros algoritmos desarrollados para la identificación de itemsets frecuentes en bases de datos con transacciones, y posterior conversión a reglas de asociación. Si bien sucesivas modificaciones del algoritmo original han conseguido mejoras notables en su rendimiento, sigue siendo muy empleado. Este algoritmo tiene dos etapas:

  • Identificar todos los itemsets que ocurren con una frecuencia por encima de un determinado límite (itemsets frecuentes).

  • Convertir los itemsets frecuentes en reglas de asociación.

Con la finalidad de ilustrar el funcionamiento del algoritmo, se emplea un ejemplo sencillo. Supóngase la siguiente base de datos de un centro comercial en la que cada fila es una transacción. En este caso, el término transacción hace referencia a todos los productos comprados bajo un mismo ticket (misma cesta de la compra). Las letras A, B, C y D hacen referencia a 4 productos (ítems) distintos.

Transacción
{A, B, C, D}
{A, B, D}
{A, B}
{B, C, D}
{B, C}
{C, D}
{B, D}

Antes de entrar en los detalles del algoritmo, conviene definir una serie de conceptos:

  • Soporte: el soporte de un ítem o itemset $X$ es el número de transacciones que contienen $X$, dividido entre el total de transacciones.

  • Confianza: La confianza de una regla "Si $X$ entonces $Y$" se define acorde a la ecuación

$$\text{confianza(X=>Y)} = \frac{\text{soporte(unión(X,Y))}}{\text{soporte(X)}}$$

donde unión($XY$) es el itemset que contienen todos los ítems de $X$ y de $Y$. La confianza se interpreta como la probabilidad P($Y$|$X$), es decir, la probabilidad de que una transacción que contiene los ítems de $X$, también contenga los ítems de $Y$.

Volviendo al ejemplo del centro comercial, puede observarse que, el artículo A, aparece en 3 de las 7 transacciones, el artículo B en 6 y ambos artículos juntos en 3. El soporte del ítem {A} es por lo tanto del 43%, el del ítem {B} del 86% y del itemset {A, B} del 43%. De las 3 transacciones que incluyen A, las 3 incluyen B, por lo tanto, la regla "clientes que compran el artículo A también compran B", se cumple, acorde a los datos, un 100% de las veces. Esto significa que la confianza de la regla {A => B} es del 100%.

Encontrar itemsets frecuentes (itemsets con una frecuencia mayor o igual a un determinado soporte mínimo) no es un proceso trivial debido la explosión combinatoria de posibilidades, sin embargo, una vez identificados, es relativamente directo generar reglas de asociación que presenten una confianza mínima. El algoritmo Apriori hace una búsqueda exhaustiva por niveles de complejidad (de menor a mayor tamaño de itemsets). Para reducir el espacio de búsqueda aplica la norma de "si un itemset no es frecuente, ninguno de sus supersets (itemsets de mayor tamaño que contengan al primero) puede ser frecuente". Visto de otra forma, si un conjunto es infrecuente, entonces, todos los conjuntos donde este último se encuentre, también son infrecuentes. Por ejemplo, si el itemset {A, B} es infrecuente, entonces, {A, B, C} y {A, B, E} también son infrecuentes ya que todos ellos contienen {A, B}.

El funcionamiento del algoritmo es sencillo, se inicia identificando los ítems individuales que aparecen en el total de transacciones con una frecuencia por encima de un mínimo establecido por el usuario. A continuación, se sigue una estrategia bottom-up en la que se extienden los candidatos añadiendo un nuevo ítem y se eliminan aquellos que contienen un subconjunto infrecuente o que no alcanzan el soporte mínimo. Este proceso se repite hasta que el algoritmo no encuentra más ampliaciones exitosas de los itemsets previos o cuando se alcanza un tamaño máximo.

Se procede a identificar los itemsets frecuentes y, a partir de ellos, crear reglas de asociación.

Transacción
{A, B, C, D}
{A, B, D}
{A, B}
{B, C, D}
{B, C}
{C, D}
{B, D}

Para este ejemplo se considera que un ítem o itemset es frecuente si aparece en un mínimo de 3 transacciones, es decir, su soporte debe de ser igual o superior a 3/7 = 0.43. Se inicia el algoritmo identificando todos los ítems individuales (itemsets de un único ítem) y calculando su soporte.

Itemset (k=1) Ocurrencias Soporte
{A} 3 0.43
{B} 6 0.86
{C} 4 0.57
{D} 5 0.71

Todos los itemsets de tamaño $k$ = 1 tienen un soporte igual o superior al mínimo establecido, por lo que todos superan la primera fase de filtrado (poda).

A continuación, se generan todos los posibles itemsets de tamaño $k$ = 2 que se pueden crear con los itemsets que han superado el paso anterior y se calcula su soporte.

Itemset (k=2) Ocurrencias Soporte
{A, B} 3 0.43
{A, C} 1 0.14
{A, D} 2 0.29
{B, C} 3 0.43
{B, D} 4 0.57
{C, D} 3 0.43

Los itemsets {A, B}, {B, C}, {B, D} y {C, D} superan el límite de soporte, por lo que son frecuentes. Los itemsets {A, C} y {A, D} no superan el soporte mínimo por lo que se descartan. Además, cualquier futuro itemset que los contenga también será descartado ya que no puede ser frecuente por el hecho de que contiene un subconjunto infrecuente.

Itemset (k=2) Ocurrencias Soporte
{A, B} 3 0.43
{B, C} 3 0.43
{B, D} 4 0.57
{C, D} 3 0.43

Se repite el proceso, esta vez creando itemsets de tamaño k = 3.

Itemset (k=3)
{A, B, C}
{A, B, D}
{B, C, D}
{C, D, A}

Los itemsets {A, B, C}, {A, B, D} y {C, D, A} contienen subconjuntos infrecuentes, por lo que son descartados. Para los restantes se calcula su soporte.

Itemset (k=3) Ocurrencias Soporte
{B, C, D} 2 0.29

El itemset {B, C, D} no supera el soporte mínimo por lo que se considera infrecuente. Al no haber ningún nuevo itemset frecuente, se detiene el algoritmo.

Como resultado de la búsqueda, se han identificado los siguientes itemsets frecuentes:

Itemset frecuentes
{A, B}
{B, C}
{B, D}
{C, D}

El siguiente paso es crear las reglas de asociación a partir de cada uno de los itemsets frecuentes. De nuevo, se produce una explosión combinatoria de posibles reglas ya que, de cada itemset frecuente, se generan tantas reglas como posibles particiones binarias. El proceso es el siguiente:


  1. Por cada itemset frecuente $I$, obtener todos los posibles subsets de $I$.

    1.1 Para cada subset de $I$, crear la regla "$s => (I-s)$"

  2. Descartar todas las reglas que no superen un mínimo de confianza.


Supóngase que se desean únicamente reglas con una confianza igual o superior a 0.7, es decir, que la regla se cumpla un 70% de las veces. Tal y como se describió anteriormente, la confianza de una regla se calcula como el soporte del itemset formado por todos los ítems que participan en la regla, dividido por el soporte del itemset formado por los ítems del antecedente.

Reglas Confianza Confianza
{A} => {B} soporte{A, B} / soporte {A} 0.43 / 0.43 = 1
{B} => {A} soporte{A, B} / soporte {B} 0.43 / 0.86 = 0.5
{B} => {C} soporte{B, C} / soporte {B} 0.43 / 0.86 = 0.5
{C} => {B} soporte{B, C} / soporte {C} 0.43 / 0.57 = 0.75
{B} => {D} soporte{B, D} / soporte {B} 0.43 / 0.86 = 0.5
{D} => {B} soporte{B, D} / soporte {D} 0.43 / 0.71 = 0.6
{C} => {D} soporte{C, D} / soporte {C} 0.43 / 0.57 = 0.75
{D} => {C} soporte{C, D} / soporte {D} 0.43 / 0.71 = 0.6

De todas las posibles reglas, únicamente {A} => {B}, {C} => {D} y {C} => {B} superan el límite de confianza.

La principal desventaja del algoritmo Apriori es el número de veces que se tienen que escanear los datos en busca de los itemsets frecuentes, en concreto, el algoritmo escanea todas las transacciones un total de $k_{max +1}$, donde $k_{max}$ es el tamaño máximo de itemset permitido. Esto hace que el algoritmo Apriori no pueda aplicarse en situaciones con millones de registros. Sin embargo, se han desarrollado adaptaciones (FP-growth, Eclat, Hash-Based, partitioning... entre otros) que solucionan esta limitación.

FP-Growth

Los investigadores Han et al. propusieron en el 2000 un nuevo algoritmo llamado FP-Growth. Este permite extraer reglas de asociación a partir de itemsets frecuentes pero, a diferencia del algoritmo Apriori, estos se identifican sin necesidad de generar candidatos para cada tamaño.

En términos generales, el algoritmo emplea una estructura de árbol (Frequent Pattern Tree) donde almacena toda la información de las transacciones. Esta estructura permite comprimir la información de una base de datos de transacciones, haciendo posible que pueda ser cargada en memoria RAM. Una vez que la base de datos ha sido comprimida en una estructura FP-Tree, se divide en varias bases de datos condicionales, cada una asociada con un patrón frecuente. Finalmente, cada partición se analiza de forma separada y se concatenan los resultados obtenidos. En la mayoría de los casos, FP-Growth es más rápido que Apriori.

Eclat

En el 2000, Zaki propuso un nuevo algoritmo para encontrar patrones frecuentes (itemsets frecuentes) llamado Equivalence Class Transformation (Eclat). La principal diferencia entre este algoritmo y el Apriori es la forma en que se escanean y analizan los datos. El algoritmo Apriori emplea transacciones almacenadas de forma horizontal (lista o basket), es decir, todos los elementos que forman una misma transacción están en la misma línea. El algoritmo Eclat, sin embargo, analiza las transacciones en formato vertical, donde cada línea contiene un único ítem y el identificador de las transacciones en las que aparece ese ítem. Para ilustrar el funcionamiento del algoritmo Eclat, se muestra un ejemplo simplificado.

La siguiente tabla contiene la información de las transacciones en formato horizontal. El soporte mínimo para considerar un itemset frecuente es del 20%.

Transacción Items
T1 i4, i5
T2 i1, i3, i4, i5
T3 i1, i3, i5
T4 i3, i4
T5 i3, i4
T6 i4, i5
T7 i2, i3, i5
T8 i2, i5
T9 i3, i4, i5

En primer lugar, el algoritmo identifica los ítems que aparecen en el conjunto de todas las transacciones y los emplea para crear la primera columna de una nueva tabla. Después, se añade a cada ítem el identificador de las transacciones en las que aparece y se calcula su soporte. La siguiente tabla muestra el resultado de reestructurar los datos de formato horizontal a vertical.

Itemset (k = 1) Transacciones Soporte
{i1} T2, T3 0.22
{i2} T7, T8 0.22
{i3} T2, T3, T4, T5, T7, T9 0.66
{i4} T1, T2, T4, T5, T6, T9 0.66
{i5} T1, T2, T3, T6, T7, T8, T9 0.77

Calculando todas las posibles intersecciones de la columna Transacciones de la tabla Itemset (k = 1) se obtienen los itemsets de longitud $k+1$.

Itemset (k = 2) Transacciones Soporte
{i1, i3} T2, T3 0.22
{i1, i4} T2 0.11
{i1, i5} T2, T3 0.22
{i2, i3} T7 0.11
{i2, i5} T7, T8 0.22
{i3, i4} T2, T4, T5, T9 0.44
{i3, i5} T2, T3, T7, T9 0.44
{i4, i5} T1, T2, T6, T9 0.44

De nuevo, con las intersecciones de las transacciones de la tabla Itemset (k = 2) se obtienen los itemsets $k = 3$. Gracias al principio downward closure, no es necesario realizar la intersección de {i1, i5} e {i4, i5} ya que {i1, i4} no es frecuente y por lo tanto tampoco lo es {i1, i4, i5}.

Itemset (k = 3) Transacciones Soporte
{i1, i3, i5} T2, T3 0.22
{i3, i4, i5} T2, T9 0.22

El algoritmo finaliza cuando no hay más itemsets frecuentes.

Cabe destacar que, el algoritmo Eclat, permite la identificación de itemsets frecuentes, pero no genera reglas de asociación. A pesar de ello, una vez identificados los itemsets frecuentes, se puede aplicar la segunda parte del algoritmo Apriori para obtenerlas.

Métricas

En el proceso de identificar itemsets frecuentes y reglas de asociación, hay varias métricas importantes que se utilizan para evaluar la calidad y relevancia de los resultados obtenidos. Las métricas utilizadas con más frecuencia, algunas de las cuales ya descritas en apartados anteriores, son:

  • Soporte (Support): El soporte de un itemset se define como la proporción de transacciones en el conjunto de datos que contienen ese itemset. Un itemset se considera frecuente si su soporte es mayor que un umbral predefinido. Cuanto mayor el soporte de un itemsets, mayor suele ser su relevancia. Esta métrica se utiliza para itemsets, no para reglas de asociación. En general, debido a la propiedad de downward closure, todos los subconjuntos de un itemset frecuente son también frecuentes.

  • Confianza (Confidence): La confianza de una regla $A⇒C$ es la probabilidad de ver el consecuente $C$ en una transacción dada que contiene el antecedente $A$. Esta métrica no es simétrica ni dirigida; por ejemplo, la confianza para $A⇒C$ es diferente de la confianza para $C⇒A$. El valor de la confianza es 1 (máximo) para una regla $A⇒C$ si el consecuente y el antecedente siempre aparecen juntos en el conjunto de datos.

  • Lift: La métrica lift se utiliza para medir la frecuencia con la que el antecedente y el consecuente de una regla $A⇒C$ ocurran juntos, comparado a lo que cabría esperar si fueran estadísticamente independientes. Es decir, compara la frecuencia observada de un patrón con respecto a lo que se esperaría solo por azar. Se calcula diviendo la confianza de la regla ($Confianza(A⇒C)$) entre el soporte del consecuente ($Soporte(C)$).

    • Valores de lift menores que 1 indican una correlación negativa o sustitutiva entre A y C, los ítems se compran juntos menos frecuentemente de lo esperado.

    • Si A y C son independientes entre sí, la métrica lift es exactamente 1, los ítems se compran juntos tan frecuentemente como se esperaría por casualidad.

    • Valores mayores que 1 de lift indican que A y C están correlacionados positivamente, los ítems se compran juntos más frecuentemente de lo que se esperaría si fueran independientes.

Ejemplo cesta de la compra

Supóngase que se dispone del registro de todas las compras que se han realizado en un supermercado. El objetivo del análisis es identificar productos que tienden a comprarse de forma conjunta para así poder situarlos en posiciones cercanas dentro de la tienda y maximizar la probabilidad de que los clientes compren.

Librerías

In [1]:
# Librerías
# ==============================================================================
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from mlxtend.frequent_patterns import apriori
from mlxtend.preprocessing import TransactionEncoder
from mlxtend.frequent_patterns import association_rules

Datos

Para este ejemplo se emplea el set de datos Groceries del paquete de R arules. Este contiene un registro de todas las ventas realizadas por un supermercado durante 30 días. En total se dispone de 9835 transacciones formadas por combinaciones de 169 productos.

In [2]:
# Datos
# ==============================================================================
url = (
    "https://raw.githubusercontent.com/JoaquinAmatRodrigo/Estadistica-con-R/"
    "master/datos/datos_groceries.csv"
)
datos = pd.read_csv(url)
datos.head()
Out[2]:
id_compra item
0 1 citrus fruit
1 1 semi-finished bread
2 1 margarine
3 1 ready soups
4 2 tropical fruit

Exploración de transacciones e ítems

Uno de los primeros análisis que conviene realizar cuando se trabaja con transacciones es explorar su contenido y tamaño, es decir, el número de ítems que las forman y cuáles son.

En este conjunto de datos, cada línea contiene la información de un ítem y el identificador de la transacción (compra) a la que pertenece. Por ejemplo, la transacción con el identificador 14 está formada por 3 ítems.

In [3]:
# Ítems de la transacción 14
# ==============================================================================
datos.query("id_compra==14")
Out[3]:
id_compra item
45 14 frankfurter
46 14 rolls/buns
47 14 soda
In [4]:
# Total de transacciones e ítems (productos)
# ==============================================================================
print(f"Número total de transacciones: {datos['id_compra'].nunique()}")
print(f"Número total de ítems (productos): {datos['item'].nunique()}")
Número total de transacciones: 9835
Número total de ítems (productos): 169
In [5]:
# Ítems (productos) agrupados por transacción (compra)
# ==============================================================================
datos.groupby('id_compra')['item'].apply(list)
Out[5]:
id_compra
1       [citrus fruit, semi-finished bread, margarine,...
2                        [tropical fruit, yogurt, coffee]
3                                            [whole milk]
4         [pip fruit, yogurt, cream cheese, meat spreads]
5       [other vegetables, whole milk, condensed milk,...
                              ...                        
9831    [sausage, chicken, beef, hamburger meat, citru...
9832                                  [cooking chocolate]
9833    [chicken, citrus fruit, other vegetables, butt...
9834    [semi-finished bread, bottled water, soda, bot...
9835    [chicken, tropical fruit, other vegetables, vi...
Name: item, Length: 9835, dtype: object
In [6]:
# Distribución del número de ítems por transacción
# ==============================================================================
display(datos.groupby('id_compra')['item'].size().describe(percentiles=[.25, .5, .75, .9]))

fig, ax = plt.subplots(figsize=(7, 3))
datos.groupby('id_compra')['item'].size().plot.hist(ax=ax)
ax.set_title('Distribución del tamaño de las transacciones');
ax.set_xlabel('Número de ítems');
count    9835.000000
mean        4.409456
std         3.589385
min         1.000000
25%         2.000000
50%         3.000000
75%         6.000000
90%         9.000000
max        32.000000
Name: item, dtype: float64

La gran mayoría de clientes compra entre 2 y 6 productos, y el 90% de ellos compra como máximo 9.

El siguiente análisis básico consiste en identificar cuáles son los ítems más frecuentes (los que tienen mayor soporte) dentro del conjunto de todas las transacciones. Se calcula como la fracción de transacciones que contienen dicho ítem respecto al total de transacciones. Esto es distinto de la frecuencia de un ítem respecto al total de ítems, de ahí que la suma de todos los soportes no sea 1.

Para realizar este cálculo se genera una matriz binaria (valores 1/0 o True/False), con una fila por cada transacción, en este caso cada compra, y una columna por cada posible ítem, en este caso productos. La posición de la matriz ($i$, $j$) tiene el valor 1 si la transacción $i$ contiene el ítem $j$ y 0 de lo contrario. Existen múltiples formas de construir este tipo de matriz, una de ellas es utilizando la clase TransactionEncoder de la librería mlxtend.

In [7]:
# Codificar las transacciones en forma de matriz binaria
# ==============================================================================
# Crear una lista de listas que contenga los ítems de cada transacción
transacciones = datos.groupby('id_compra')['item'].apply(list).to_list()

# Entrenar el objeto TransactionEncoder y transformar los datos
encoder = TransactionEncoder()
transacciones_encoded = encoder.fit(transacciones).transform(transacciones)
transacciones_encoded = pd.DataFrame(transacciones_encoded, columns=encoder.columns_)
transacciones_encoded.head(3)
Out[7]:
Instant food products UHT-milk abrasive cleaner artif. sweetener baby cosmetics baby food bags baking powder bathroom cleaner beef ... turkey vinegar waffles whipped/sour cream whisky white bread white wine whole milk yogurt zwieback
0 False False False False False False False False False False ... False False False False False False False False False False
1 False False False False False False False False False False ... False False False False False False False False True False
2 False False False False False False False False False False ... False False False False False False False True False False

3 rows × 169 columns

In [8]:
# Porcentaje de transacciones en las que aparece cada producto (top 5)
# ==============================================================================
transacciones_encoded.mean(axis = 0).sort_values(ascending = False).head(5)
Out[8]:
whole milk          0.255516
other vegetables    0.193493
rolls/buns          0.183935
soda                0.174377
yogurt              0.139502
dtype: float64

Los 5 productos que aparecen con más frecuencia en las cestas de la compra son: whole milk, other vegetables, rolls/buns, soda y yogurt. Esto no significa necesariamente que sean los productos con mayor volumen de ventas ya que no se está teniendo en cuenta cuántas unidades que se venden, sino con qué frecuencia.

Es importante estudiar cómo se distribuye el soporte de los ítems individuales en un conjunto de transacciones antes de identificar itemsets frecuentes o crear reglas de asociación, ya que, dependiendo del caso, tendrá sentido emplear un límite de soporte u otro. Por lo general, cuando el número de posibles ítems es muy grande (varios miles) prácticamente todos los artículos son raros, por lo que los soportes son muy bajos.

In [9]:
# Distribución del sopoerte de los items
# ==============================================================================
fig, ax = plt.subplots(figsize=(7, 2.5))
transacciones_encoded.mean(axis = 0).plot.hist(ax = ax)
ax.set_xlim(0, )
ax.set_title('Distribución del soporte de los ítems');

Itemsets

Las funciones apriori y fpgrowth del paquete mlxtend permiten la identificación de itemsets aplicando cada una el algoritmo que les da nombre.

Ambas funciones requieren que los datos estén almacenados en un DataFrame de pandas, en formato one hot encoded, también conocido como dummy variables. Se trata de un formato de matriz binaria (valores 1/0 o True/False), con una fila por cada transacción, en este caso cada compra, y una columna por cada posible ítem, en este caso productos. La posición de la matriz ($i$, $j$) tiene el valor 1 si la transacción $i$ contiene el ítem $j$ y 0 de lo contrario. Existen múltiples formas de construir este tipo de matriz, una de ellas es utilizando la clase TransactionEncoder de este mismo paquete.

In [10]:
# Codificar las transacciones en forma de matriz binaria
# ==============================================================================
# Crear una lista de listas que contenga los artículos comprados en cada transacción
transacciones = datos.groupby('id_compra')['item'].apply(list).to_list()

# Entrenar el objeto TransactionEncoder y transformar los datos
encoder = TransactionEncoder()
transacciones_encoded = encoder.fit(transacciones).transform(transacciones)
transacciones_encoded = pd.DataFrame(transacciones_encoded, columns=encoder.columns_)
transacciones_encoded.head(3)
Out[10]:
Instant food products UHT-milk abrasive cleaner artif. sweetener baby cosmetics baby food bags baking powder bathroom cleaner beef ... turkey vinegar waffles whipped/sour cream whisky white bread white wine whole milk yogurt zwieback
0 False False False False False False False False False False ... False False False False False False False False False False
1 False False False False False False False False False False ... False False False False False False False False True False
2 False False False False False False False False False False ... False False False False False False False True False False

3 rows × 169 columns

Una vez que los datos se encuentran en el formato adecuado se ejecuta el algoritmo deseado para la identificación de itemsets frecuentes. Se procede a extraer aquellos itemsets, incluidos los formados por un único ítem, que hayan sido comprados al menos 30 veces. En un caso real, este valor sería excesivamente bajo si se tiene en cuenta la cantidad total de transacciones, sin embargo, se emplea 30 para que en los resultados aparezcan un número suficiente de itemsets y reglas de asociación que permitan mostrar las posibilidades de análisis que ofrece mlxtend.

In [11]:
# Identificación de itemsets frecuentes
# ==============================================================================
soporte = 30 / transacciones_encoded.shape[0]
print(f"Soporte mínimo: {soporte}")

itemsets = apriori(transacciones_encoded, min_support=soporte, use_colnames=True)
itemsets.sort_values(by='support', ascending=False)
Soporte mínimo: 0.003050330452465684
Out[11]:
support itemsets
133 0.255516 (whole milk)
83 0.193493 (other vegetables)
98 0.183935 (rolls/buns)
111 0.174377 (soda)
134 0.139502 (yogurt)
... ... ...
1439 0.003050 (butter, citrus fruit, yogurt)
1601 0.003050 (yogurt, coffee, tropical fruit)
664 0.003050 (margarine, dessert)
1884 0.003050 (whole milk, sausage, newspapers)
1366 0.003050 (other vegetables, bottled water, shopping bags)

2226 rows × 2 columns

Se han encontrado un total de 2226 itemsets frecuentes que superan el soporte mínimo de 0.00305.

In [12]:
# Top 10 itemsets con mayor soporte
# ==============================================================================
itemsets.sort_values(by='support', ascending=False).head(10)
Out[12]:
support itemsets
133 0.255516 (whole milk)
83 0.193493 (other vegetables)
98 0.183935 (rolls/buns)
111 0.174377 (soda)
134 0.139502 (yogurt)
9 0.110524 (bottled water)
99 0.108998 (root vegetables)
126 0.104931 (tropical fruit)
107 0.098526 (shopping bags)
104 0.093950 (sausage)

Filtrado de itemsets

Una vez que los itemsets frecuentes han sido identificados, suele ser interesante aplicar determinados filtrados, algunos de los más frecuentes son:

  • Itemsets de un tamaño mínimo.

  • Itemsets que contienen determinados ítems.

  • Itemsets que no contienen un determinado ítem.

In [13]:
# Top 10 itemsets con al menos 2 items, ordenados por soporte
# ==============================================================================
itemsets['n_items'] = itemsets['itemsets'].apply(lambda x: len(x))
itemsets.query('n_items >= 2').sort_values('support', ascending=False).head(10)
Out[13]:
support itemsets n_items
1076 0.074835 (whole milk, other vegetables) 2
1171 0.056634 (whole milk, rolls/buns) 2
1275 0.056024 (whole milk, yogurt) 2
1185 0.048907 (whole milk, root vegetables) 2
1055 0.047382 (other vegetables, root vegetables) 2
1077 0.043416 (other vegetables, yogurt) 2
1054 0.042603 (other vegetables, rolls/buns) 2
1263 0.042298 (whole milk, tropical fruit) 2
1240 0.040061 (whole milk, soda) 2
1161 0.038332 (soda, rolls/buns) 2
In [14]:
# Itemsets frecuentes que contienen el ítem newspapers
# ==============================================================================
mask = itemsets['itemsets'].map(lambda x: 'newspapers' in x)
itemsets.loc[mask].sort_values(by='support', ascending=False)
Out[14]:
support itemsets n_items
78 0.079817 (newspapers) 1
1017 0.027351 (whole milk, newspapers) 2
1006 0.019725 (newspapers, rolls/buns) 2
1002 0.019319 (other vegetables, newspapers) 2
1018 0.015353 (yogurt, newspapers) 2
... ... ... ...
1876 0.003050 (sausage, newspapers, rolls/buns) 3
1884 0.003050 (whole milk, sausage, newspapers) 3
957 0.003050 (meat, newspapers) 2
896 0.003050 (hygiene articles, newspapers) 2
2171 0.003050 (whole milk, other vegetables, root vegetables... 4

80 rows × 3 columns

In [15]:
# Itemsets frecuentes que contienen los items, al menos, newspapers y whole milk
# ==============================================================================
items = {'newspapers', 'whole milk'}
mask = itemsets['itemsets'].map(lambda x: x.issuperset(items))
itemsets.loc[mask].sort_values(by='support', ascending=False).reset_index()
Out[15]:
index support itemsets n_items
0 1017 0.027351 (whole milk, newspapers) 2
1 1872 0.008338 (whole milk, other vegetables, newspapers) 3
2 1879 0.007626 (whole milk, newspapers, rolls/buns) 3
3 1889 0.006609 (whole milk, yogurt, newspapers) 3
4 1882 0.005796 (whole milk, root vegetables, newspapers) 3
5 1887 0.005084 (whole milk, newspapers, tropical fruit) 3
6 1885 0.004779 (whole milk, soda, newspapers) 3
7 1360 0.004067 (whole milk, bottled water, newspapers) 3
8 1406 0.004067 (whole milk, brown bread, newspapers) 3
9 1874 0.003864 (whole milk, pastry, newspapers) 3
10 1553 0.003355 (whole milk, citrus fruit, newspapers) 3
11 1455 0.003152 (whole milk, butter, newspapers) 3
12 1520 0.003152 (chocolate, whole milk, newspapers) 3
13 1820 0.003152 (whole milk, margarine, newspapers) 3
14 1884 0.003050 (whole milk, sausage, newspapers) 3
15 2171 0.003050 (whole milk, other vegetables, root vegetables... 4

Puede observarse que muchos itemsets están a su vez contenidos en itemsets de orden superior, es decir, existen itemsets que son subsets de otros. Para identificar cuáles son, o cuales no lo son, se pueden emplear los métodos issubset() o issuperset().

In [16]:
# Identificación subsets
# ==============================================================================
itemset_a = {'other vegetables', 'whole milk', 'newspapers'}
itemset_b = {'other vegetables', 'whole milk', 'newspapers'}

print(itemset_a.issubset(itemset_b))
print(itemset_b.issuperset(itemset_a))
True
True

Reglas de asociación

Para crear reglas de asociación se sigue el mismo proceso que para obtener itemsets frecuentes mostrado en el apartado anterior. Una vez obtenidos los itemsets frecuentes, se pasan a la función association_rules (API Reference) encargada de generar las reglas de asociación y de seleccionar aquellas que alcanzan una valor mínimo de confianza o lift.

In [17]:
# Identificación de itemsets frecuentes
# ==============================================================================
soporte = 30 / transacciones_encoded.shape[0]
itemsets_frecuentes = apriori(transacciones_encoded, min_support=soporte, use_colnames=True)

# Crear reglas de asociación (confianza mínima del 70% para que una regla sea selecionada)
# ==============================================================================
confianza = 0.7 
reglas = association_rules(itemsets_frecuentes, metric="confidence", min_threshold=confianza)

print(f"Número de reglas generadas: {len(reglas)}")
print(f"Confianza mínima: {reglas['confidence'].min()}")
print(f"Confianza máxima: {reglas['confidence'].max()}")
reglas.sort_values(by='confidence').head(5)
Número de reglas generadas: 19
Confianza mínima: 0.7000000000000001
Confianza máxima: 0.8857142857142858
Out[17]:
antecedents consequents antecedent support consequent support support confidence lift leverage conviction zhangs_metric
14 (root vegetables, yogurt, tropical fruit) (whole milk) 0.008134 0.255516 0.005694 0.700000 2.739554 0.003616 2.481613 0.640185
12 (other vegetables, whipped/sour cream, domesti... (whole milk) 0.005084 0.255516 0.003559 0.700000 2.739554 0.002260 2.481613 0.638222
1 (butter, coffee) (whole milk) 0.004779 0.255516 0.003355 0.702128 2.747881 0.002134 2.499339 0.639138
4 (butter, pork) (whole milk) 0.005491 0.255516 0.003864 0.703704 2.754049 0.002461 2.512633 0.640415
17 (other vegetables, root vegetables, citrus fru... (whole milk) 0.004474 0.255516 0.003152 0.704545 2.757344 0.002009 2.519792 0.640196

Se han identificado un total de 19 reglas con una confianza superior al 70%.

Filtrado de reglas

Cuando se crean reglas de asociación, puede ser interesante seleccionar únicamente aquellas que contienen un determinado conjunto de ítems. El proceso de filtrado es similar al mostrado para itemsets frecuentes, pero esta vez, puede aplicarse al antecedente, al consecuente o ambos a la vez.

Supóngase que solo son de interés aquellas reglas que muestren productos que se vendan junto con other vegetables. Esto significa que el ítem other vegetables, debe aparecer en el lado derecho de la regla (consequents).

In [18]:
# Seleccionar reglas que tienen "other vegetables" en el consecuente
# ==============================================================================
mask = reglas['consequents'].map(lambda x: 'other vegetables' in x)
reglas.loc[mask]
Out[18]:
antecedents consequents antecedent support consequent support support confidence lift leverage conviction zhangs_metric
10 (root vegetables, citrus fruit, tropical fruit) (other vegetables) 0.005694 0.193493 0.004474 0.785714 4.060694 0.003372 3.763701 0.758053
13 (root vegetables, whipped/sour cream, tropical... (other vegetables) 0.004575 0.193493 0.003355 0.733333 3.789981 0.002470 3.024403 0.739530
16 (whole milk, root vegetables, citrus fruit, tr... (other vegetables) 0.003559 0.193493 0.003152 0.885714 4.577509 0.002463 7.056940 0.784332

Hay un total de 3 reglas que tienen el item other vegetables en el consecuente. Los ítems que suelen aparecer en la cesta de la compra junto con other vegetables son:

In [19]:
# Antecedentes de las reglas que tienen "other vegetables" en el consecuente
# ==============================================================================
antecedents = reglas.loc[mask, 'antecedents'].to_list()
set().union(*antecedents)
Out[19]:
{'citrus fruit',
 'root vegetables',
 'tropical fruit',
 'whipped/sour cream',
 'whole milk'}

Otro ejemplo de filtrado es encontrar aquellas reglas que contienen un conjunto de ítems teniendo en cuenta tanto el antecedente como el consecuente, por ejemplo, aquellas que contienen al menos los ítems: citrus fruit, root vegetables, tropical fruit, whole milk y other vegetables.

In [20]:
# Crear una columna con los ítems que forman parte del antecedente y el consecuente
# ======================================================================================
reglas['items'] = reglas[['antecedents', 'consequents']].apply(lambda x: set().union(*x), axis=1)
reglas.head(3)
Out[20]:
antecedents consequents antecedent support consequent support support confidence lift leverage conviction zhangs_metric items
0 (baking powder, yogurt) (whole milk) 0.004575 0.255516 0.003254 0.711111 2.783039 0.002085 2.577060 0.643626 {whole milk, baking powder, yogurt}
1 (butter, coffee) (whole milk) 0.004779 0.255516 0.003355 0.702128 2.747881 0.002134 2.499339 0.639138 {whole milk, butter, coffee}
2 (butter, curd) (whole milk) 0.006812 0.255516 0.004881 0.716418 2.803808 0.003140 2.625286 0.647755 {whole milk, butter, curd}
In [21]:
# Filtrar reglas
# ==============================================================================
seleccion_items = {'citrus fruit', 'root vegetables', 'tropical fruit',
                   'whole milk', 'other vegetables'}
mask = reglas['items'].map(lambda x: x.issubset(seleccion_items))
reglas.loc[mask]
Out[21]:
antecedents consequents antecedent support consequent support support confidence lift leverage conviction zhangs_metric items
10 (root vegetables, citrus fruit, tropical fruit) (other vegetables) 0.005694 0.193493 0.004474 0.785714 4.060694 0.003372 3.763701 0.758053 {other vegetables, root vegetables, citrus fru...
16 (whole milk, root vegetables, citrus fruit, tr... (other vegetables) 0.003559 0.193493 0.003152 0.885714 4.577509 0.002463 7.056940 0.784332 {whole milk, other vegetables, root vegetables...
17 (other vegetables, root vegetables, citrus fru... (whole milk) 0.004474 0.255516 0.003152 0.704545 2.757344 0.002009 2.519792 0.640196 {whole milk, other vegetables, root vegetables...

Reglas de asociación producto a producto

Un caso de uso típico en los sistema de recomendación es mostrar cuales son los productos más relevantes para un determinado producto. Por ejemplo, si un cliente está viendo un producto en una tienda online, se pueden mostrar los productos que se compran con mayor frecuencia junto con el producto que está viendo. Para ello, se identifican las reglas de asociación 1 a 1 que contienen un ítem en el antecedente y un ítem en el consecuente.

Dado que se están buscando itemsets de tamaño 2, se emplea la función apriori (API Reference) con el parámetro max_len = 2.

In [22]:
# Identificación de itemsets (2 items) frecuentes
# ==============================================================================
soporte = 30 / transacciones_encoded.shape[0]
itemsets_frecuentes = apriori(
                          transacciones_encoded, 
                          min_support  = soporte, 
                          use_colnames = True, 
                          max_len      = 2
                      )

Una estrategia adicional en el filtrado de reglas de asociación suele ser utilizar una combinación de lift y confianza. Con el lift se identifican relaciones significativas y luego se usa la confianza para priorizar reglas que tienen una alta probabilidad condicional.

Como punto de partida, se seleccionan las reglas que superen el lift promedio o mediano de todas las reglas posibles en el conjunto de datos. Esto identificará las reglas en las que el antecedente y el consecuente ocurren juntos más frecuentemente de lo que se esperaría si fueran independientes.

In [23]:
# Calcular lift base
# ==============================================================================
lift = 0
reglas = association_rules(itemsets_frecuentes, metric="lift", min_threshold=lift)

print(f"Número de reglas generadas: {len(reglas)}")
print(f"Lift medio: {reglas['lift'].mean()}")
print(f"Lift mediano: {reglas['lift'].median()}")
Número de reglas generadas: 2280
Lift medio: 1.644958572425791
Lift mediano: 1.5504543534185435

Se escoge un lift mínimo de 1,6.

In [24]:
# Crear reglas de asociación (lift mínimo de 1.6)
# ==============================================================================
lift = 1.6
reglas = association_rules(itemsets_frecuentes, metric="lift", min_threshold=lift)

print(f"Número de reglas generadas: {len(reglas)}")
print(f"Lift mínimo: {reglas['lift'].min()}")
print(f"Lift máximo: {reglas['lift'].max()}")
reglas.sort_values(by='lift').head(3)
Número de reglas generadas: 1044
Lift mínimo: 1.602248197776239
Lift máximo: 11.421437695970273
Out[24]:
antecedents consequents antecedent support consequent support support confidence lift leverage conviction zhangs_metric
362 (frozen vegetables) (coffee) 0.048094 0.058058 0.004474 0.093023 1.602248 0.001682 1.038551 0.394868
363 (coffee) (frozen vegetables) 0.058058 0.048094 0.004474 0.077058 1.602248 0.001682 1.031383 0.399045
928 (yogurt) (pot plants) 0.139502 0.017285 0.003864 0.027697 1.602341 0.001452 1.010708 0.436855

Una vez que se han identificado las reglas con un lift superior al mínimo establecido, se seleccionan aquellas que superan un determinado umbral de confianza.

In [25]:
# Filtrar por confianza mínima del 50%
# ==============================================================================
confianza = 0.5
reglas = reglas[reglas['confidence'] >= confianza]

print(f"Número de reglas generadas: {len(reglas)}")
print(f"Confianza mínima: {reglas['confidence'].min()}")
print(f"Confianza máxima: {reglas['confidence'].max()}")
reglas.sort_values(by='confidence').head(3)
Número de reglas generadas: 5
Confianza mínima: 0.5
Confianza máxima: 0.6428571428571428
Out[25]:
antecedents consequents antecedent support consequent support support confidence lift leverage conviction zhangs_metric
870 (specialty cheese) (other vegetables) 0.008541 0.193493 0.004270 0.500000 2.584078 0.002618 1.613015 0.618296
857 (rice) (other vegetables) 0.007626 0.193493 0.003965 0.520000 2.687441 0.002490 1.680224 0.632724
37 (baking powder) (whole milk) 0.017692 0.255516 0.009253 0.522989 2.046793 0.004732 1.560725 0.520642

De esta manera, se puede seleccionar cuáles son los ítems (consecuentes) más probables para un determinado producto (antecedente).

In [26]:
# Recomendaciones 1 a 1
# ==============================================================================
antecedente = 'rice'

mask = reglas['antecedents'].map(lambda x: antecedente in x)
reglas.loc[mask]
Out[26]:
antecedents consequents antecedent support consequent support support confidence lift leverage conviction zhangs_metric
857 (rice) (other vegetables) 0.007626 0.193493 0.003965 0.520000 2.687441 0.002490 1.680224 0.632724
945 (rice) (whole milk) 0.007626 0.255516 0.004677 0.613333 2.400371 0.002729 1.925390 0.587881

Información de sesión

In [27]:
import session_info
session_info.show(html=False)
-----
matplotlib          3.8.2
mlxtend             0.23.1
pandas              2.1.4
seaborn             0.13.2
session_info        1.0.0
-----
IPython             8.20.0
jupyter_client      8.6.0
jupyter_core        5.7.1
-----
Python 3.11.7 | packaged by Anaconda, Inc. | (main, Dec 15 2023, 18:05:47) [MSC v.1916 64 bit (AMD64)]
Windows-10-10.0.19045-SP0
-----
Session information updated at 2024-01-30 15:04

Bibliografía


Practical Data Science with R by Nina Zumel and John Mount, Foreword by Jim Porzak

Michael Hahsler, Kurt Hornik, and Thomas Reutterer (2006) Implications of probabilistic data modeling for mining association rules. In M. Spiliopoulou, R. Kruse, C. Borgelt, A. Nuernberger, and W. Gaul, editors, From Data and Information Analysis to Knowledge Engineering, Studies in Classification, Data Analysis, and Knowledge Organization, pages 598–605. Springer-Verlag.

Mining Frequent Patterns and Association Rules from Biological Data Ioannis Kavakiotis, George Tzanis, Ioannis Vlahavas

¿Cómo citar este documento?

Reglas de asociación con python by Joaquín Amat Rodrigo, available under a Attribution 4.0 International (CC BY 4.0) at https://www.cienciadedatos.net/documentos/py50-reglas-de-asociacion-python.html


¿Te ha gustado el artículo? Tu ayuda es importante

Mantener un sitio web tiene unos costes elevados, tu contribución me ayudará a seguir generando contenido divulgativo gratuito. ¡Muchísimas gracias! 😊


Creative Commons Licence
Este contenido, creado por Joaquín Amat Rodrigo, tiene licencia Attribution-NonCommercial-ShareAlike 4.0 International.