Un motor de recomendación es un sistema que predice la preferencia o calificación que un usuario daría a un artículo. El sistema selecciona artículos relevantes para un usuario basándose en su comportamiento y el conocimiento que tiene sobre ellos. Hoy en día, los sistemas de recomendación se utilizan en muchos servicios en línea. Ayudan a los usuarios al seleccionar cosas en las que podrían estar interesados entre la gran cantidad de datos disponibles que les resultan irrelevantes. Ofrecer buenas recomendaciones mejora la participación del usuario. Los sitios de comercio electrónico también se benefician al ofrecer recomendaciones de productos relevantes al aumentar su ingreso promedio por usuario.
Vas a crear un motor de recomendación simple pero potente que sugiera productos que generalmente se compran juntos. Vamos a sugerir productos basados en ventas históricas, identificando así los productos que normalmente se compran conjuntamente. También vamos a sugerir productos complementarios en dos escenarios diferentes:
- Página de detalle del producto: Mostraremos una lista de productos que generalmente se compran con el producto dado. Esto se mostrará como "Los usuarios que compraron esto también compraron X, Y, y Z". Necesitas una estructura de datos que te permita almacenar el número de veces que cada producto se ha comprado junto con el producto que se muestra.
- Página de detalle del carrito: Basándote en los productos que los usuarios agregan al carrito, vas a sugerir productos que generalmente se compran junto con estos. En este caso, la puntuación que calculas para obtener productos relacionados tiene que ser añadido.
Vas a utilizar Redis para almacenar los productos que generalmente se compran juntos.
Redis es una base de datos avanzada de clave/valor que te permite guardar diferentes tipos de datos. También cuenta con operaciones de E/S extremadamente rápidas. Redis almacena todo en memoria, pero los datos pueden guardarse al volcar el conjunto de datos al disco de vez en cuando, o al agregar cada comando a un registro. Redis es muy versátil en comparación con otras bases de datos clave/valor: proporciona un conjunto de comandos poderosos y admite diversas estructuras de datos, como cadenas, hashes, listas, conjuntos, conjuntos ordenados e incluso mapas de bits o Hyper-LogLogs.
Aunque SQL es más adecuado para el almacenamiento de datos persistentes, Redis ofrece numerosas ventajas al tratar con datos que cambian rápidamente, almacenamiento volátil o cuando se necesita un caché rápido. Veamos cómo Redis puede ser utilizado para agregar nuevas funcionalidades al proyecto.
Puedes encontrar más información sobre Redis en su página de inicio en https://redis.io/.
Redis proporciona una imagen de Docker que facilita mucho desplegar un servidor Redis con una configuración estándar.
Instalando Redis con Docker.
Usar Redis con Python
Necesitarás los enlaces de Python para Redis. Instala redis-py a través de pip usando el siguiente comando:
$ pip install redis
Puedes encontrar la documentación de redis-py en https://redis-py.readthedocs.io/.
El paquete redis-py interactúa con Redis, proporcionando una interfaz de Python que sigue la sintaxis de los comandos de Redis. Abre el shell de Python con el siguiente comando:
$ python manage.py shell
Ejecuta el siguiente código:
>>> import redis
>>> r = redis.Redis(host='localhost', port=6379, db=0)
El código anterior crea una conexión con la base de datos de Redis. En Redis, las bases de datos se identifican por un índice entero en lugar de un nombre de base de datos. Por defecto, un cliente está conectado a la base de datos 0. El número de bases de datos de Redis disponibles se establece en 16, pero puedes cambiar esto en el archivo de configuración redis.conf.
A continuación, establece una clave usando el shell de Python:
>>> r.set('foo', 'bar')
True
El comando devuelve True, lo que indica que la clave se ha creado correctamente. Ahora puedes recuperar la clave usando el comando get():
>>> r.get('foo')
b'bar'
Como podrás observar en el código anterior, los métodos de Redis siguen la sintaxis de los comandos de Redis.
Integremos Redis en nuestro proyecto. Edita el archivo settings.py del proyecto y añade la siguiente configuración:
PracticaDjango/PracticaDjango/settings.py
# Configuración para Redis
REDIS_HOST = 'localhost'
REDIS_PORT = 6379
REDIS_DB = 1
Estas son la configuración requerida para establecer una conexión con el servidor Redis. Crea un nuevo archivo dentro del directorio de aplicaciones de la tienda y llámalo recomendar.py. Agrega el siguiente código:
PracticaDjango/Tienda/recomendar.py
import redis from django.conf import settings from .models import Producto # Conectamos a redis r = redis.Redis(host=settings.REDIS_HOST, port=settings.REDIS_PORT, db=settings.REDIS_DB) class Recomendar(): def obtener_key_producto(self, id): return f'producto:{id}:comprado_junto' def productos_comprados(self, productos): productos_ids = [p.id for p in productos] for producto_id in productos_ids: for junto_id in productos_ids: # obtenemos el otro producto comprado junto con este if producto_id != junto_id: # incrementamos el contador de junto comprado r.zincrby(self.obtener_key_producto(producto_id), 1, junto_id)
Esta es la clase Recomendar, que te permitirá almacenar compras de productos y recuperar sugerencias de productos para un producto o productos dados.
El método obtener_key_producto() recibe un ID de un objeto Producto y construye la clave Redis para el conjunto ordenado donde se almacenan los productos relacionados, que se ve así: producto:[id]:comprado_junto.
El método productos_comprados() recibe una lista de objetos Producto que se han comprado juntos (es decir, pertenecen al mismo pedido).
En este método, realizas las siguientes tareas:
1. Obtienes los IDs de los productos para los objetos Producto dados.
2. Iteras sobre los IDs de los productos. Para cada ID, iteras nuevamente sobre los IDs de los productos y omites el mismo producto para que obtengas los productos que se compran junto con cada producto.
3. Obtienes la clave del producto Redis para cada producto comprado usando el método obtener_key_producto(). Para un producto con un ID de 33, este método devuelve la clave producto:33:comprado_junto. Esta es la clave para el conjunto ordenado que contiene los IDs de productos que se compraron junto con este.
4. Incrementas la puntuación de cada ID de producto contenido en el conjunto ordenado en 1. La puntuación representa el número de veces que otro producto se ha comprado junto con el producto dado.
Ahora tienes un método para almacenar y puntuar los productos que se compraron juntos. A continuación, necesitas un método para recuperar los productos que se compraron juntos para una lista de productos dados. Agrega el siguiente método sugerir_productos() a la clase Recomendar:
PracticaDjango/Tienda/recomendar.py
class Recomendar: #... def sugerir_productos(self, productos, max_resultados=6): productos_ids = [p.id for p in productos] if len(productos) == 1: # solamente hay un producto sugerencias = r.zrange( self.obtener_key_producto(productos_ids[0]), 0, -1, desc=True)[:max_resultados] else: # generamos una clave temporal flat_ids = ''.join([str(id) for id in productos_ids]) tmp_key = f'tmp_{flat_ids}' # multiples productos, combinamos las puntuaciones de los productos # guardamos el resultado ordenado en una clave temporal keys = [self.obtener_key_producto(id) for id in productos_ids] r.zunionstore(tmp_key, keys) # eliminamos los identificadores de los productos recomendados r.zrem(tmp_key, *productos_ids) # obtener los ID de los productos por su puntuación, ordenada de forma descendente sugerencias = r.zrange(tmp_key, 0, -1, desc=True)[:max_resultados] # removemos la clave temporal r.delete(tmp_key) productos_sugeridos_ids = [int(id) for id in sugerencias] # establecemos los productos sugeridos y los ordenamos por orden de aparición. productos_sugeridos = list( Producto.objects.filter(id__in=productos_sugeridos_ids)) productos_sugeridos.sort( key=lambda x: productos_sugeridos_ids.index(x.id)) return productos_sugeridos
El método sugerir_productos() recibe los siguientes parámetros:
- productos: Esta es una lista de objetos Producto para los que obtener recomendaciones. Puede contener uno o más productos.
- max_resultados: Este es un entero que representa el número máximo de recomendaciones a devolver.
En este método, realizas las siguientes acciones:
1. Obtienes los IDs de los productos para los objetos Producto dados.
2. Si solo se proporciona un producto, recuperas el ID de los productos que se compraron junto con el producto dado, ordenados por el número total de veces que se compraron juntos. Para hacerlo, utilizas el comando ZRANGE de Redis. Limitas el número de resultados al número especificado en el atributo max_resultados (6 por defecto).
3. Si se proporcionan más de un producto, generas una clave temporal de Redis construida con los IDs de los productos.
4. Combina y suma todos las puntuaciones para los elementos contenidos en el conjunto ordenado de cada uno de los productos dados. Esto se hace utilizando el comando ZUNIONSTORE de Redis. El comando ZUNIONSTORE realiza una unión de los conjuntos ordenados con las claves dadas y almacena la suma agregada de las puntuaciones de los elementos en una nueva clave de Redis. Puedes leer más sobre este comando en https://redis.io/commands/zunionstore/. Guardas las puntuaciones agregadas en la clave temporal.
5. Dado que estás agregando puntuaciones, es posible que obtengas los mismos productos para los que estás obteniendo recomendaciones. Los eliminas del conjunto ordenado generado utilizando el comando ZREM.
6. Recuperas los IDs de los productos de la clave temporal, ordenados por sus puntuaciones utilizando el comando ZRANGE. Limitas el número de resultados al número especificado en el atributo max_resultados. Luego, eliminas la clave temporal.
7. Finalmente, obtienes los objetos Producto con los IDs dados y ordenas los productos en el mismo orden que ellos.
Para propósitos prácticos, también añadiremos un método para borrar las recomendaciones. Agrega el siguiente método a la clase Recomendar:
PracticaDjango/Tienda/recomendar.py
class Recomendar:
#...
def limpiar_compras(self):
for id in Producto.objects.values_list('id', flat=True):
r.delete(self.obtener_key_producto(id))
Una vez que tenemos el código vamos a probar nuestro motor de recomendaciones. Asegurate de tener varios objetos Producto en la base de datos. En mi caso ya tengo grabados varios juegos para PS4 que son los que voy a utilizar para la prueba. Empecemos inicializando el contenedor de Redis en Docker usando el siguiente comando en una consola del terminal:
>>> sudo docker run -it --rm --name redis -p 6379:6379 redis
Abre otra consola del terminal y ejecuta el shell de Python:
>>> python manage.py shell
Asegúrate de que al menos tienes cuatro productos diferentes almacenados en tu base de datos. Vamos a empezar por recuperarlos usando sus nombres, de la siguiente forma:
>>> from Tienda.models import Producto
>>> nba2k23 = Producto.objects.get(nombre="NBA2K23")
>>> ghost = Producto.objects.get(nombre="GHOST OF TSUSHIMA")
>>> god = Producto.objects.get(nombre="GOD OF WAR")
>>> read = Producto.objects.get(nombre="READ DEAD REDEMPTION 2")
Luego, añadiremos algunas compras de prueba para que puedan ser procesados por el motor de recomendaciones:
>>> from Tienda.recomendar import Recomendar >>> r = Recomendar() >>> r.productos_comprados([nba2k23, ghost]) >>> r.productos_comprados([nba2k23, god]) >>> r.productos_comprados([ghost, nba2k23, read]) >>> r.productos_comprados([god, read]) >>> r.productos_comprados([nba2k23, read]) >>> r.productos_comprados([ghost, god])
Con lo cual hemos guardado las siguientes puntuaciones para los productos:
nba2k23: ghost(2), read(2), god(1)
ghost: nba2k23(2), read(1), god(1)
god: nba2k23(1), read(1), ghost(1)
read: nba2k23(2), ghost(1), god(1)
Esta es una representación de los productos que se han comprado junto con cada uno de los productos, incluyendo cuantas veces se han comprado juntos.
Recuperemos las recomendaciones para un solo producto:
>>> r.sugerir_productos([nba2k23]) [<Producto: GHOST OF TSUSHIMA>, <Producto: READ DEAD REDEMPTION 2>, <Producto: GOD OF WAR>] >>> r.sugerir_productos([ghost]) [<Producto: NBA2K23>, <Producto: GOD OF WAR>, <Producto: READ DEAD REDEMPTION 2>] >>> r.sugerir_productos([god]) [<Producto: GHOST OF TSUSHIMA>, <Producto: READ DEAD REDEMPTION 2>, <Producto: NBA2K23>] >>> r.sugerir_productos([read]) [<Producto: NBA2K23>, <Producto: GOD OF WAR>, <Producto: GHOST OF TSUSHIMA>] >>>
Como puedes observar el orden de recomendación de los productos está basado en su puntuación. Vamos a obtener recomendaciones para múltiples productos usando sus puntuaciones:
>>> r.sugerir_productos([nba2k23, ghost]) [<Producto: READ DEAD REDEMPTION 2>, <Producto: GOD OF WAR>] >>> r.sugerir_productos([god, ghost]) [<Producto: NBA2K23>, <Producto: READ DEAD REDEMPTION 2>] >>> r.sugerir_productos([read, nba2k23]) [<Producto: GHOST OF TSUSHIMA>, <Producto: GOD OF WAR>]
Como puedes ver el orden de las recomendaciones coincide con las puntuaciones. Por ejemplo, los juegos sugeridos para nba2k23 y ghost son read(2+1) y god(1+1).
Si en vez de introducir una simulación de productos comprados conjuntamente como hemos visto hasta ahora quieres basarte en los pedidos u ordenes que estén en la base de datos del proyecto puedes utilizar el siguiente código en su lugar. Empecemos inicializando el contenedor de Redis en Docker usando el siguiente comando en una consola del terminal:
>>> sudo docker run -it --rm --name redis -p 6379:6379 redis
Abre otra consola del terminal y ejecuta el shell de Python:
>>> python manage.py shell
Introduce el siguiente código:
>>> from Orders.models import OrderItem # Obtener todos los objetos OrderItem >>> order_items = OrderItem.objects.all() # Crear una lista para almacenar los productos comprados, donde cada elemento representa una orden >>> productos_comprados = [] # Inicializar una lista vacía para cada orden >>> for _ in range(len(order_items)): productos_comprados.append([]) # Iterar sobre cada OrderItem y agrupar los productos por orden >>> for order_item in order_items: orden_id = order_item.order_id producto_comprado = order_item.product # Agregar el producto a la lista correspondiente a la orden productos_comprados[orden_id - 1].append(producto_comprado) # Ahora 'productos_comprados' contiene una lista de listas donde cada lista interna # representa los productos comprados en una orden específica
Para pasar la lista de los productos comprados conjuntamente al motor de recomendación en Redis, puedes iterar sobre la lista productos_comprados y pasar cada lista de productos de cada orden al método productos_comprados de tu instancia Recomendar. Aquí te muestro cómo hacerlo:
python manage.py shell
>>> from Tienda.models import Producto
>>> from Tienda.recomendar import Recomendar
# Crear una instancia del motor de recomendación
>>> recomendador = Recomendar()
# Iterar sobre cada lista de productos comprados por orden
>>> for productos_en_orden in productos_comprados:
# Crear una lista de instancias de Producto a partir de los nombres de los productos
productos_instancias = [Producto.objects.get(nombre=producto.nombre) for producto in productos_en_orden]
# Pasar la lista de instancias de Producto al método productos_comprados del motor de recomendación
recomendador.productos_comprados(productos_instancias)
y ya solo nos queda ver si funciona el recomendador para ello instanciaremos un juego y probaremos si funciona la recomendación. Lo haremos de la siguiente forma:
>>> nba2k23 = Producto.objects.get(nombre="NBA2K23") >>> recomendador.sugerir_productos([nba2k23]) [<Producto: READ DEAD REDEMPTION 2>, <Producto: GOD OF WAR>, <Producto: GHOST OF TSUSHIMA>]
Una vez que hemos verificado el funcionamiento correcto del motor de recomendaciones bien utilizando datos que le hemos proporcionado o usando los datos reales de los pedidos de la aplicación vamos a aplicarlo al modelo. Para ello cuando hagamos una compra y vayamos al carro la aplicación no s mostrará los productos recomendados junto con los que el usuario haya añadido al carro. Edita el archivo views.py de la aplicación Carro, importa la clase Recomendar y edita el método mostrar_carro y añade el código resaltado:
PracticaBlog/PracticaDjango/Carro/views.py
#... # Para aplicar el recomendador de productos from Tienda.recomendar import Recomendar # vista para mostrar los productos del carro def mostrar_carro(request): productos = Producto.objects.all() carro = Carro(request) formulario_cupon = CuponFormulario() # para aplicar el recomendador de productos r = Recomendar() productos_carro = [item['producto'] for item in carro] if(productos_carro): productos_recomendados = r.sugerir_productos(productos_carro, max_resultados=4) else: productos_recomendados = [] contexto = { "productos": productos, "carro": carro, "formulario_cupon": formulario_cupon, "productos_recomendados": productos_recomendados, } return render(request, "Tienda/carro_detalle.html", contexto)
PracticaDjango/Tienda/templates/Tienda/carro_detalle.html
<div>
{% if productos_recomendados %}
<h3 style="color:aliceblue;">Comprados juntos habitualmente</h3>
<div class="row">
{% for p in productos_recomendados %}
<div class="col-md-3">
<div class="row">
<div class="col-md-12">
<a href="{% url 'carro:agregar' p.id %}">
<img src="{% thumbnail p.imagen 100x0 %}" class="img-fluid">
</a>
</div>
</div>
<div class="row">
<div class="col-md-12">
<a href="{% url 'carro:agregar' p.id %}" style="color: black;">
{{ p.nombre }}
</a>
</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
</div>
Si ahora realizas alguna compra y entras en el carro, deberías ver una imagen muy similar a esta:
Si haces clic en alguno de ellos será añadido al carrito y el motor de recomendaciones volverá a funcionar y te recomendará nuevos productos.
No hay comentarios:
Publicar un comentario