Paquetes con el algoritmo Apriori en Python

Publicado el 28 octubre 2022 por Daniel Rodríguez @analyticslane

El algoritmo Apriori es uno de los más empleados para la creación de reglas de asociación. A pesar de ello, no existe un paquete que se puede considerar el "estándar" en Python, como sucede con el caso de arules en R. En esta ocasión voy a analizar algunos paquetes que se pueden encontrar en PyPi en los que se implementa el algoritmo Apriori en Python para tener una comparativa de estos.

MLxtend

Hace un tiempo, al explicar las reglas de asociación, utilice la implementación de MLxtend para la creación de reglas de asociación. Para instalar este paquete solamente se es necesario ejecutar el siguiente comando en la terminal

pip install mlxtend

Una vez hecho esto, se puede usar el conjunto de datos Online Retail Data Set disponible en el repositorio de la Universidad de California Irvine. Este conjunto de datos se encuentra en un archivo Excel donde cada una de las filas son los productos de diferentes pedidos (identificable por la columna InvoiceNo). Los datos se pueden cargar y limpiar con el siguiente código.

import pandas as pd

df = pd.read_excel('http://archive.ics.uci.edu/ml/machine-learning-databases/00352/Online%20Retail.xlsx')

# Preparación de los datos
df['Description'] = df['Description'].str.strip()
df.dropna(axis=0, subset=['InvoiceNo'], inplace=True)
df['InvoiceNo'] = df['InvoiceNo'].astype('str')

# Se elimina la referencia envio al estar en todos los pedidos
df = df[df['Description'] != 'POSTAGE']

df.head()
  InvoiceNo StockCode                          Description  Quantity  \
0    536365    85123A   WHITE HANGING HEART T-LIGHT HOLDER         6   
1    536365     71053                  WHITE METAL LANTERN         6   
2    536365    84406B       CREAM CUPID HEARTS COAT HANGER         8   
3    536365    84029G  KNITTED UNION FLAG HOT WATER BOTTLE         6   
4    536365    84029E       RED WOOLLY HOTTIE WHITE HEART.         6   

          InvoiceDate  UnitPrice  CustomerID         Country  
0 2010-12-01 08:26:00       2.55     17850.0  United Kingdom  
1 2010-12-01 08:26:00       3.39     17850.0  United Kingdom  
2 2010-12-01 08:26:00       2.75     17850.0  United Kingdom  
3 2010-12-01 08:26:00       3.39     17850.0  United Kingdom  
4 2010-12-01 08:26:00       3.39     17850.0  United Kingdom

En los datos se ha eliminado los registros que contenga la referencia POSTAGE, ya que es el envío y se incluye en todos los pedidos.

Preparación de los datos para MLxtend

Para trabajar con la implementación de Apriori de MLxtend es necesario crear una tabla donde las filas representan los pedidos y las columnas las referencias. Indicando con el valor vedado cuando la referencia se incluye en el pedido y falso el cualquier otro caso. Una forma para crear esta tabla se puede ver en el siguiente ejemplo.

basket = (df[df['Country'] == 'Spain']
          .groupby(['InvoiceNo', 'Description'])['Quantity']
          .sum().apply(lambda x: x>0).unstack().reset_index().fillna(False)
          .set_index('InvoiceNo'))

Usando en este caso .groupby() para agrupar los datos, contar el número de operaciones con .sum() y asignar el valor verdadero cuando esta sea mayor de 0.

Obtención las reglas de asociación con MLxtend

Las reglas de asociación en MLxtend se obtienen en dos pasos. En primer lugar, es necesario obtener la lista de ítemsets candidatos con la función apriori(). Una vez hecho esto, las reglas de asociación se pueden obtener con la función association_rules(). En el siguiente código se muestran los pasos necesarios.

from mlxtend.frequent_patterns import apriori, association_rules

frequent_itemsets = apriori(basket, min_support=0.06, use_colnames=True)
rules = association_rules(frequent_itemsets, metric='confidence', min_threshold=0.8)

frequent_itemsets.head()
    support                               itemsets
0  0.145631               (6 RIBBONS RUSTIC CHARM)
1  0.067961           (ALARM CLOCK BAKELIKE GREEN)
2  0.116505        (ASSORTED COLOUR BIRD ORNAMENT)
3  0.097087  (CLASSIC METAL BIRDCAGE PLANT HOLDER)
4  0.087379                 (DOLLY GIRL LUNCH BOX)

La lista de los itemsets frecuentes se puede ver en una DataFrame donde la primera columna es soporte y la segunda el nombre del itemset.

Por otro lado, las reglas también son un DataFrame con toda la información necesaria: antecedente, consecuente, soporte del antecedente, soporte del consecuente, soporte de la regla, confían, lift (mejora de la confianza), leverage y convicción

rules.head()
                   antecedents                  consequents  \
0  (POPPY'S PLAYHOUSE BEDROOM)  (POPPY'S PLAYHOUSE KITCHEN)   
1  (POPPY'S PLAYHOUSE KITCHEN)  (POPPY'S PLAYHOUSE BEDROOM)   

   antecedent support  consequent support   support  confidence    lift  \
0            0.077670            0.067961  0.067961       0.875  12.875   
1            0.067961            0.077670  0.067961       1.000  12.875   

   leverage  conviction  
0  0.062683    7.456311  
1  0.062683         inf

Una tabla en la que se puede encontrar toda la información necesaria para decidir la importancia de todas las reglas de asociación encontradas.

El paquete Efficient-Apriori contiene una implementación en Python del algoritmo Apriori. Un paquete que se puede instalar ejecutando el siguiente comando en la terminal.

pip install efficient-apriori

A diferencia MLxtend las transacciones de deben indicar como una lista de lista. En esta, cada una de las listas del primer nivel representa un ticket y la segunda lista debe contener el identificador de cara una de las referencias del ticket. Lo que se puede conseguir mediante empleando una lista por comprensión en la que se seleccionan los elementos.

df_es = df[df['Country'] == 'Spain']

transactions = [df_es['Description'][df_es['InvoiceNo'] == x].tolist() for x in df_es['InvoiceNo'].unique()]

transactions[7:9]
[['RED 3 PIECE RETROSPOT CUTLERY SET',
  'PINK 3 PIECE POLKADOT CUTLERY SET',
  'BLUE 3 PIECE POLKADOT CUTLERY SET',
  'GREEN 3 PIECE POLKADOT CUTLERY SET'],
 ['6 RIBBONS RUSTIC CHARM',
  'RIBBON REEL STRIPES DESIGN',
  'RIBBON REEL LACE DESIGN',
  'CHOCOLATE BOX RIBBONS',
  'BABY BOOM RIBBONS',
  'LARGE WHITE/PINK ROSE ART FLOWER',
  'ASSORTED COLOUR BIRD ORNAMENT',
  'BLACK RECORD COVER FRAME',
  'WOODEN FRAME ANTIQUE WHITE',
  'FRENCH WC SIGN BLUE METAL',
  'RED SPOTTY BISCUIT TIN',
  'PACK OF 72 RETROSPOT CAKE CASES',
  '72 SWEETHEART FAIRY CAKE CASES',
  'SPACEBOY LUNCH BOX',
  'RED RETROSPOT CAKE STAND',
  'REGENCY CAKESTAND 3 TIER',
  'SET OF 72 RETROSPOT PAPER  DOILIES',
  'RED RETROSPOT MUG',
  'GIN AND TONIC MUG',
  'HOME SWEET HOME MUG',
  'BLOSSOM  IMAGES NOTEBOOK SET',
  'CURIOUS  IMAGES NOTEBOOK SET',
  'FANCY FONTS BIRTHDAY WRAP',
  'RED RETROSPOT WRAP',
  'BLUE POLKADOT WRAP',
  'WORLD WAR 2 GLIDERS ASSTD DESIGNS',
  'TRADITIONAL WOODEN SKIPPING ROPE',
  'SET OF 6 SOLDIER SKITTLES',
  'BOX OF VINTAGE ALPHABET BLOCKS',
  'CLASSIC METAL BIRDCAGE PLANT HOLDER']]

Lo que prepara los datos en el formato que necesita este paquete.

Obtención las reglas de asociación con Efficient-Apriori

Ahora, simplemente se tiene que llamar a la función apriori() para obtener la lista de itemsets y reglas.

from efficient_apriori import apriori

itemsets, rules = apriori(transactions, min_support=0.06, min_confidence=0.8)

itemsets
{1: {('LUNCH BAG PINK POLKADOT',): 10,
  ('LUNCH BAG RED RETROSPOT',): 8,
  ('PARTY BUNTING',): 8,
  ('WHITE HANGING HEART T-LIGHT HOLDER',): 11,
  ('ALARM CLOCK BAKELIKE GREEN',): 8,
  ('PLASTERS IN TIN SKULLS',): 11,
  ('PLASTERS IN TIN CIRCUS PARADE',): 7,
  ('REGENCY CAKESTAND 3 TIER',): 25,
  ('CLASSIC METAL BIRDCAGE PLANT HOLDER',): 11,
  ("POPPY'S PLAYHOUSE BEDROOM",): 8,
  ("POPPY'S PLAYHOUSE KITCHEN",): 7,
  ('SPACEBOY LUNCH BOX',): 12,
  ('STRAWBERRY CERAMIC TRINKET BOX',): 7,
  ('PAPER BUNTING RETROSPOT',): 7,
  ('SET OF 3 HEART COOKIE CUTTERS',): 7,
  ('SET OF 72 RETROSPOT PAPER  DOILIES',): 10,
  ('SET OF 36 PAISLEY FLOWER DOILIES',): 8,
  ('SET OF 36 TEATIME PAPER DOILIES',): 7,
  ('JAM MAKING SET WITH JARS',): 16,
  ('SET/10 PINK POLKADOT PARTY CANDLES',): 7,
  ('SET OF 6 GIRLS CELEBRATION CANDLES',): 7,
  ('PACK OF 60 PINK PAISLEY CAKE CASES',): 8,
  ('DOLLY GIRL LUNCH BOX',): 9,
  ('SET OF 3 CAKE TINS PANTRY DESIGN',): 7,
  ('SET/5 RED RETROSPOT LID GLASS BOWLS',): 7,
  ('6 RIBBONS RUSTIC CHARM',): 15,
  ('RED RETROSPOT CAKE STAND',): 10,
  ('PLASTERS IN TIN WOODLAND ANIMALS',): 9,
  ('RED RETROSPOT TAPE',): 7,
  ('ASSORTED COLOUR BIRD ORNAMENT',): 12,
  ('WOODEN FRAME ANTIQUE WHITE',): 7,
  ('PACK OF 72 RETROSPOT CAKE CASES',): 11,
  ('ROSES REGENCY TEACUP AND SAUCER',): 11,
  ('ROUND SNACK BOXES SET OF4 WOODLAND',): 12,
  ('DOORMAT SPOTTY HOME SWEET HOME',): 7,
  ('SET/20 RED RETROSPOT PAPER NAPKINS',): 9,
  ('POPCORN HOLDER',): 7,
  ('SPOTTY BUNTING',): 7,
  ('SET OF 3 REGENCY CAKE TINS',): 7},
 2: {('6 RIBBONS RUSTIC CHARM', 'ASSORTED COLOUR BIRD ORNAMENT'): 9,
  ("POPPY'S PLAYHOUSE BEDROOM", "POPPY'S PLAYHOUSE KITCHEN"): 7}}

En este caso los itemsets son un diccionario. En este diccionario la clave es el número de itemsets y el valor es un nuevo diccionario. Este segundo diccionario tiene como clave los itemsets y el valor es el número de veces que aparece. Así se puede comprobar que el resultado en ambos casos es similar ya que '6 RIBBONS RUSTIC CHARM' aparece 15 veces en 103 transacciones, esto es 0.14563 el mismo valor que se ha obtenido con MLxtend.

Por otro lado, las reglas es un listado de objetos reglas que contiene la información necesaria.

for rule in rules:
    print(rule)
{POPPY'S PLAYHOUSE KITCHEN} -> {POPPY'S PLAYHOUSE BEDROOM} (conf: 1.000, supp: 0.068, lift: 12.875, conv: 922330097.087)
{POPPY'S PLAYHOUSE BEDROOM} -> {POPPY'S PLAYHOUSE KITCHEN} (conf: 0.875, supp: 0.068, lift: 12.875, conv: 7.456)

Pudiéndose comprobar que los resultados son iguales a los vistos en MLxtend.

Comparación de MLxtend y Efficient-Apriori

En base a las pruebas realizadas los resultados en ambos paquetes son similares, lo que indica que las implementaciones son similares. Obteniendo los mismos resultados con el conjunto de datos de prueba. Así a la hora de decantarnos por uno u otro debemos fijarnos en cuál de ellos se adapta mejor a nuestro flujo de trabajo.

Quizás e mayor problema de MLxtend sean los datos de entrada. Crear un DataFrame con tantas columnas como referencias, aunque el contenido sea un campo booleano, requiere mucha memoria. Por eso en las pruebas realizadas se centró el análisis solamente a un país. Así, para grandes conjuntos la mejor solución de los dos es Efficient-Apriori.

Por otro lado, en cuanto a los resultados la forma en la que se presentan los resultados es más fácil de leer en MLxtend ya que estos se presentan en DataFrames. Obtener el soporte de un itemset es algo más complicado en Efficient-Apriori. De este modo, para conjuntos de datos pequeños, donde se desea analizar en detalle los resultados, la mejor opción sea MLxtend.

Conclusiones

En esta entrada se ha visto una comparación entre dos paquetes de PyPi donde se implementa el algoritmo Apriori en Python. Por un lado, MLxtend muestra los resultados de una forma más clara mientras que es menos eficiente en cuestión de memoria. Por otro lado, el paquete Efficient-Apriori requiere menos recursos, aunque obtener la información detallada de las reglas puede ser un poco más complicado. Aunque la eficiencia de este último lo convierte en la mejor elección para producción.