Generador de descuentos

Objetivos

  • Incentivar nuevas compras del cliente en el establecimiento

  • Fomentar el consumo de otros productos

  • Fomentar el consumo de productos con más margen de beneficio

Entradas y Salidas

  • Entrada: Lista de artículos que ha comprado el consumidor
  • Salida: Lista de cupones descuento que imprimir junto al recibo de compra

In [1]:
import re

from pyknow import *

Hechos

Definiremos a continuación los hechos que manejará el sistema.


In [2]:
class Producto(Fact):
    """
    Producto que ha comprado un cliente.

    >>> Producto(nombre="pepsi", tipo="refresco de cola", cantidad=1)

    """
    pass

class Cupon(Fact):
    """
    Cupón a generar para la próxima compra del cliente.

    >>> Cupon(tipo="2x1", producto="pepsi")
    
    """
    pass

In [3]:
class Promo(Fact):
    """
    Promoción vigente en el comercio.

    >>> Promo(tipo="2x1", **depende_de_la_promo)

    """
    pass

class Beneficio(Fact):
    """
    Define los beneficios que obtiene el comercio por cada producto.

    >>> Beneficio(nombre="pepsi", tipo="refresco de cola", ganancias=0.2)

    """
    pass

Objetivo 1

Incentivar nuevas compras del cliente en el establecimiento

Para esto no hay nada mejor que las típicas promociones 2x1, 3x2, etc.

Implementación


In [4]:
class OfertasNxM(KnowledgeEngine):
    @DefFacts()
    def carga_promociones_nxm(self):
        """
        Hechos iniciales.
        
        Genera las promociones vigentes
        """
        yield Promo(tipo="2x1", producto="Dodot")
        yield Promo(tipo="2x1", producto="Leche Pascual")
        yield Promo(tipo="3x2", producto="Pilas AAA")
    
    @Rule(Promo(tipo=MATCH.t & P(lambda t: re.match(r"\d+x\d+", t)),
                producto=MATCH.p),
          Producto(nombre=MATCH.p))
    def oferta_nxm(self, t, p):
        """
        Sabemos que el cliente volverá para aprovechar
        la promoción, ya que hoy ha comprado el producto.
        """
        self.declare(Cupon(tipo=t, producto=p))

Pruebas

Utilizaremos la función watch para ver qué está haciendo el motor durante la ejecución.


In [5]:
watch('RULES', 'FACTS')

In [6]:
nxm = OfertasNxM()

In [7]:
nxm.reset()


INFO:pyknow.watchers.FACTS: ==> <f-0>: InitialFact()
INFO:pyknow.watchers.FACTS: ==> <f-1>: Promo(producto='Dodot', tipo='2x1')
INFO:pyknow.watchers.FACTS: ==> <f-2>: Promo(producto='Leche Pascual', tipo='2x1')
INFO:pyknow.watchers.FACTS: ==> <f-3>: Promo(producto='Pilas AAA', tipo='3x2')

In [8]:
nxm.declare(Producto(nombre="Dodot"))


INFO:pyknow.watchers.FACTS: ==> <f-4>: Producto(nombre='Dodot')
Out[8]:
Producto(nombre='Dodot')

In [9]:
nxm.declare(Producto(nombre="Agua Mineral"))


INFO:pyknow.watchers.FACTS: ==> <f-5>: Producto(nombre='Agua Mineral')
Out[9]:
Producto(nombre='Agua Mineral')

In [10]:
nxm.declare(Producto(nombre="Pilas AAA"))


INFO:pyknow.watchers.FACTS: ==> <f-6>: Producto(nombre='Pilas AAA')
Out[10]:
Producto(nombre='Pilas AAA')

In [11]:
nxm.run()


INFO:pyknow.watchers.RULES:FIRE 1 oferta_nxm: <f-6>, <f-3>
INFO:pyknow.watchers.FACTS: ==> <f-7>: Cupon(producto='Pilas AAA', tipo='3x2')
INFO:pyknow.watchers.RULES:FIRE 2 oferta_nxm: <f-1>, <f-4>
INFO:pyknow.watchers.FACTS: ==> <f-8>: Cupon(producto='Dodot', tipo='2x1')

In [13]:
nxm.facts


Out[13]:
FactList([(0, InitialFact()),
          (1, Promo(producto='Dodot', tipo='2x1')),
          (2, Promo(producto='Leche Pascual', tipo='2x1')),
          (3, Promo(producto='Pilas AAA', tipo='3x2')),
          (4, Producto(nombre='Dodot')),
          (5, Producto(nombre='Agua Mineral')),
          (6, Producto(nombre='Pilas AAA')),
          (7, Cupon(producto='Pilas AAA', tipo='3x2')),
          (8, Cupon(producto='Dodot', tipo='2x1'))])

Objetivo 2

Fomentar el consumo de otros productos

Para lograr este objetivo generaremos cupones con packs descuento. Ejemplo:

  • Si compras una fregona y una mopa a la vez, tienes un 25% de descuento en ambos productos

Implementación


In [15]:
class OfertasPACK(KnowledgeEngine):
    @DefFacts()
    def carga_promociones_pack(self):
        """Genera las promociones vigentes"""
        yield Promo(tipo="PACK", producto1="Fregona ACME", producto2="Mopa ACME", descuento="25%")
        yield Promo(tipo="PACK", producto1="Pasta Gallo", producto2="Tomate Frito", descuento="10%")

    @Rule(Promo(tipo="PACK", producto1=MATCH.p1, producto2=MATCH.p2, descuento=MATCH.d),
          OR(
              AND(
                  NOT(Producto(nombre=MATCH.p1)),
                  Producto(nombre=MATCH.p2)
              ),
              AND(
                  Producto(nombre=MATCH.p1),
                  NOT(Producto(nombre=MATCH.p2))
              )
          )
    )
    def pack(self, p1, p2, d):
        """
        El cliente querrá comprar un producto adicional en su próxima visita.
        """
        self.declare(Cupon(tipo="PACK", producto1=p1, producto2=p2, descuento=d))

Pruebas


In [16]:
pack = OfertasPACK()

In [17]:
pack.reset()


INFO:pyknow.watchers.FACTS: ==> <f-0>: InitialFact()
INFO:pyknow.watchers.FACTS: ==> <f-1>: Promo(producto2='Mopa ACME', producto1='Fregona ACME', descuento='25%', tipo='PACK')
INFO:pyknow.watchers.FACTS: ==> <f-2>: Promo(producto2='Tomate Frito', producto1='Pasta Gallo', descuento='10%', tipo='PACK')

In [18]:
pack.declare(Producto(nombre="Tomate Frito"))


INFO:pyknow.watchers.FACTS: ==> <f-3>: Producto(nombre='Tomate Frito')
Out[18]:
Producto(nombre='Tomate Frito')

In [19]:
pack.declare(Producto(nombre="Fregona ACME"))


INFO:pyknow.watchers.FACTS: ==> <f-4>: Producto(nombre='Fregona ACME')
Out[19]:
Producto(nombre='Fregona ACME')

In [20]:
pack.run()


INFO:pyknow.watchers.RULES:FIRE 1 pack: <f-1>, <f-4>
INFO:pyknow.watchers.FACTS: ==> <f-5>: Cupon(producto2='Mopa ACME', producto1='Fregona ACME', descuento='25%', tipo='PACK')
INFO:pyknow.watchers.RULES:FIRE 2 pack: <f-2>, <f-3>
INFO:pyknow.watchers.FACTS: ==> <f-6>: Cupon(producto2='Tomate Frito', producto1='Pasta Gallo', descuento='10%', tipo='PACK')

Si compramos ambos productos de un pack no se nos debe generar la promoción, ya que en este caso el comercio perdería beneficio.


In [21]:
pack.reset()


INFO:pyknow.watchers.FACTS: ==> <f-0>: InitialFact()
INFO:pyknow.watchers.FACTS: ==> <f-1>: Promo(producto2='Mopa ACME', producto1='Fregona ACME', descuento='25%', tipo='PACK')
INFO:pyknow.watchers.FACTS: ==> <f-2>: Promo(producto2='Tomate Frito', producto1='Pasta Gallo', descuento='10%', tipo='PACK')

In [22]:
pack.declare(Producto(nombre="Fregona ACME"))


INFO:pyknow.watchers.FACTS: ==> <f-3>: Producto(nombre='Fregona ACME')
Out[22]:
Producto(nombre='Fregona ACME')

In [23]:
pack.declare(Producto(nombre="Mopa ACME"))


INFO:pyknow.watchers.FACTS: ==> <f-4>: Producto(nombre='Mopa ACME')
Out[23]:
Producto(nombre='Mopa ACME')

In [24]:
pack.run()

Objetivo 3

Fomentar el consumo de productos con más margen de beneficio

El truco para cumplir este objetivo es conocer qué beneficio se obtiene por cada producto, y si existe un producto del mismo tipo con un beneficio mayor, generar un cupón de descuento para ese producto que nos permita seguir ganando más.

Implementación


In [26]:
class OfertasDescuento(KnowledgeEngine):
    @DefFacts()
    def carga_beneficios(self):
        """
        Define las beneficios por producto.
        """
        yield Beneficio(nombre="Mahou", tipo="Cerveza", ganancias=0.5)
        yield Beneficio(nombre="Cerveza Hacendado", tipo="Cerveza", ganancias=0.9)

        yield Beneficio(nombre="Pilas AAA Duracell", tipo="Pilas AAA", ganancias=1.5)
        yield Beneficio(nombre="Pilas AAA Hacendado", tipo="Pilas AAA", ganancias=2)
        
    @Rule(Producto(nombre=MATCH.p1),
          Beneficio(nombre=MATCH.p1, tipo=MATCH.t, ganancias=MATCH.g1),
          Beneficio(nombre=MATCH.p2, tipo=MATCH.t, ganancias=MATCH.g2),
          TEST(lambda g1, g2: g2 > g1)
    )
    def descuento_producto_con_mayor_beneficio(self, p2, g1, g2, **_):
        """
        """
        diferencia_ganancia = g2 - g1
        self.declare(Cupon(tipo="DESCUENTO",
                           producto=p2,
                           cantidad=diferencia_ganancia / 2))

Pruebas


In [27]:
descuento = OfertasDescuento()

In [28]:
descuento.reset()


INFO:pyknow.watchers.FACTS: ==> <f-0>: InitialFact()
INFO:pyknow.watchers.FACTS: ==> <f-1>: Beneficio(nombre='Mahou', ganancias=0.5, tipo='Cerveza')
INFO:pyknow.watchers.FACTS: ==> <f-2>: Beneficio(nombre='Cerveza Hacendado', ganancias=0.9, tipo='Cerveza')
INFO:pyknow.watchers.FACTS: ==> <f-3>: Beneficio(nombre='Pilas AAA Duracell', ganancias=1.5, tipo='Pilas AAA')
INFO:pyknow.watchers.FACTS: ==> <f-4>: Beneficio(nombre='Pilas AAA Hacendado', ganancias=2, tipo='Pilas AAA')

In [29]:
descuento.declare(Producto(nombre="Mahou"))


INFO:pyknow.watchers.FACTS: ==> <f-5>: Producto(nombre='Mahou')
Out[29]:
Producto(nombre='Mahou')

In [30]:
descuento.run()


INFO:pyknow.watchers.RULES:FIRE 1 descuento_producto_con_mayor_beneficio: <f-2>, <f-5>, <f-1>
INFO:pyknow.watchers.FACTS: ==> <f-6>: Cupon(producto='Cerveza Hacendado', cantidad=0.2, tipo='DESCUENTO')

El sistema no debe generar cupón si se ha comprado el producto con mayor beneficio


In [ ]:
descuento.reset()

In [ ]:
descuento.declare(Producto(nombre="Pilas AAA Hacendado"))

In [ ]:
descuento.run()

Juntándolo todo

Gracias a Python podemos utilizar herencia múltiple para unir nuestros distintos motores en uno y darle un mejor interfaz de usuario.


In [31]:
class GeneradorCupones(OfertasNxM, OfertasPACK, OfertasDescuento):
    def generar_cupones(self, *nombre_productos):
        # Reiniciamos el motor
        self.reset()

        # Declaramos los productos que ha comprado el cliente
        for nombre in nombre_productos:
            self.declare(Producto(nombre=nombre))

        # Ejecutamos el motor
        self.run()
        
        # Extraemos las promociones generadas
        for fact in self.facts.values():
            if isinstance(fact, Cupon):
                yield fact

In [32]:
ke = GeneradorCupones()

In [33]:
[cupon for cupon in ke.generar_cupones("Pilas AAA", "Mahou", "Tomate Frito")]


INFO:pyknow.watchers.FACTS: ==> <f-0>: InitialFact()
INFO:pyknow.watchers.FACTS: ==> <f-1>: Beneficio(nombre='Mahou', ganancias=0.5, tipo='Cerveza')
INFO:pyknow.watchers.FACTS: ==> <f-2>: Beneficio(nombre='Cerveza Hacendado', ganancias=0.9, tipo='Cerveza')
INFO:pyknow.watchers.FACTS: ==> <f-3>: Beneficio(nombre='Pilas AAA Duracell', ganancias=1.5, tipo='Pilas AAA')
INFO:pyknow.watchers.FACTS: ==> <f-4>: Beneficio(nombre='Pilas AAA Hacendado', ganancias=2, tipo='Pilas AAA')
INFO:pyknow.watchers.FACTS: ==> <f-5>: Promo(producto='Dodot', tipo='2x1')
INFO:pyknow.watchers.FACTS: ==> <f-6>: Promo(producto='Leche Pascual', tipo='2x1')
INFO:pyknow.watchers.FACTS: ==> <f-7>: Promo(producto='Pilas AAA', tipo='3x2')
INFO:pyknow.watchers.FACTS: ==> <f-8>: Promo(producto2='Mopa ACME', producto1='Fregona ACME', descuento='25%', tipo='PACK')
INFO:pyknow.watchers.FACTS: ==> <f-9>: Promo(producto2='Tomate Frito', producto1='Pasta Gallo', descuento='10%', tipo='PACK')
INFO:pyknow.watchers.FACTS: ==> <f-10>: Producto(nombre='Pilas AAA')
INFO:pyknow.watchers.FACTS: ==> <f-11>: Producto(nombre='Mahou')
INFO:pyknow.watchers.FACTS: ==> <f-12>: Producto(nombre='Tomate Frito')
INFO:pyknow.watchers.RULES:FIRE 1 pack: <f-9>, <f-12>
INFO:pyknow.watchers.FACTS: ==> <f-13>: Cupon(producto2='Tomate Frito', producto1='Pasta Gallo', descuento='10%', tipo='PACK')
INFO:pyknow.watchers.RULES:FIRE 2 descuento_producto_con_mayor_beneficio: <f-11>, <f-2>, <f-1>
INFO:pyknow.watchers.FACTS: ==> <f-14>: Cupon(producto='Cerveza Hacendado', cantidad=0.2, tipo='DESCUENTO')
INFO:pyknow.watchers.RULES:FIRE 3 oferta_nxm: <f-7>, <f-10>
INFO:pyknow.watchers.FACTS: ==> <f-15>: Cupon(producto='Pilas AAA', tipo='3x2')
Out[33]:
[Cupon(producto2='Tomate Frito', producto1='Pasta Gallo', descuento='10%', tipo='PACK'),
 Cupon(producto='Cerveza Hacendado', cantidad=0.2, tipo='DESCUENTO'),
 Cupon(producto='Pilas AAA', tipo='3x2')]

In [ ]: