Machine Learning

Como Aumentar as Vendas com Algumas Linhas de Código e um Banco de Dados

Não sou nenhum Vanderbilt, mas esse trem que vou mostrar faz fumaça!


Estou começando um hobby nesse negócio de câmeras fotográficas, e comprei na internet uma Canon T7i [me falaram que era boa], com uma lente adicional e um flash.

Aí o site vem e me oferece um tal de Difusor Rebatedor Universal, porque quem comprou aquele flash, também comprou esse difusor. What the heck isso faz? Não tenho a mínima… mas era tão barato que [já que tô aqui…] comprei, pois fiquei com peso na consciência de não comprar algo – por míseros reais – que poderá me quebrar um galho depois.

Aproveitei e comprei também um cartão de memória, uma bolsa e um tripé – tudo na base do “quem comprou isso, também comprou aquilo”. 

No final, eles conseguiram me tirar alguns reais além do que eu havia planejado. Se bobear esse difusor estava encalhado no estoque, e eles conseguiram desovar [comigo!]. O lado positivo é que tudo que comprei valeu a pena, e acabei qualificando a compra com cinco estrelas.

Todo mundo saiu feliz.


O fato é que eu gastei mais dinheiro, e eles me venderam produtos que, é importante registrar, EU NUNCA TERIA COMPRADO SE NÃO FOSSEM AS RECOMENDAÇÕES DO SITE.

O site usou um modelo de machine learning, e um dos mais simples! Você também consegue implementá-lo com poucas linhas de código. Há variações e outras abordagens, claro, mas vou lhe mostrar uma delas, bem popular, porém bastante eficiente.

Figura 1 – Exemplo de recomendação de produtos em tempo real

Esse modelo não é somente para recomendação de produtos em tempo real no mundo online. Pode ser usado também para previnir incêndios em florestas, descobrir uma pré-disposição a diabetes, detectar reações adversas a drogas, verificar a efetividade de um treinamento de corrida de rua, analisar estudantes admitidos em universidades, só para citar alguns.

Mas a maioria das aplicações deste modelo tem a ver com vendas mesmo. O caso de uso mais comum no mundo offline é o Market Basket, e pode gerar as seguintes ações para aumentar as vendas: colocar dois produtos na mesma prateleira, então quem comprar um item poderá comprar o outro; descontos promocionais poderiam ser aplicados somente a um dos itens; anúncios e propagandas para um produto podem direcionar a compra de outro produto; dois produtos poderiam ser combinamos em uma única oferta. Essas foram só para citar algumas.

Enfim, o que você puder imaginar nesse sentido de promover uma venda que outrora não ocorreria.

Neste artigo vou utilizar dados reais de um ponto de venda no comércio [uma padaria]. Vou carregar esses dados em um banco de dados, e executar o modelo de machine learning dentro deste mesmo banco de dados. Depois vou construir uma aplicação simples em Python que faz recomendações em tempo real, usando as regras geradas por esse modelo. O modelo de machine learning que usaremos é do tipo Associação de Regras, e o algoritmo utilizado chama-se Apriori. Você poderá reproduzí-lo integralmente com os códigos que vou deixar aqui neste artigo.

Mas há um problema [você realmente achou que ia ser fácil?!]. Esse modelo, apesar de simples, é compute intensive. Isto é, quanto mais dados MELHOR, porém, quanto mais dados MAIS LENTO: eu tenho uma solução para esse problema, e vou comentar sobre isso mais pra frente.

Os Dados de uma Padaria

A padaria não é igual àquela do seu bairro! É uma bakery très chic, crème de la crème, da Europa. Por isso os nomes dos itens vendidos não estão em português.

Fiz o download dos dados de transações desta bakery no site Kaggle: este link lhe coloca direto na página.

O Kaggle, se você não conhece, disponibiliza dados reais de empresas para competições de machine learning e data science. É muito útil pra aprender e treinar.

O arquivo chama-se BreadBasket_DMS.csv. Ele é bem simples e correto, o que nos poupa de fazer data wrangling: são 4 colunas – date, time, transaction e item.

Transaction é um código sequêncial que agrupa os itens vendidos. Por exemplo, se a primeira venda teve apenas um item, este recebe o código 1. Se a segunda venda teve três itens, as próximas três linhas recebem o código 2. Isto é, cada transaction representa a venda de um conjunto de itens, a cestinha de compras de uma pessoa:

Figura 2 – Dataset da Bakery obtido no Kaggle

Em nosso caso, o arquivo já está preparado para o processamento do modelo. Quando aplicar ao seu caso, aos seus dados, você deverá organizá-los de forma semelhante: agrupe itens relacionados, e atribua um código para o modelo saber que alguma vez esses itens ocorreram juntos.

O próximo passo é carregar os dados no banco de dados. Eu vou usar o Oracle XE. Podemos criar uma tabela externa, e através dela carregar os dados para uma tabela dentro do banco:


-- Tabela que armazenaremos as transações
CREATE TABLE transactions
(DT_DIA VARCHAR2(10), DT_HORA VARCHAR2(8), TRANS_ID NUMBER(38), ITEM VARCHAR2(60));

-- Local em disco onde está o arquivo BreadBasket_DMS.csv
CREATE DIRECTORY my_directory AS '/tmp';

-- Tabela externa para carregar as transações
CREATE TABLE transactions_ext
( DT_DIA VARCHAR2(10), DT_HORA VARCHAR2(8), TRANS_ID NUMBER(38), ITEM VARCHAR2(60) )
ORGANIZATION EXTERNAL ( 
TYPE ORACLE_LOADER DEFAULT DIRECTORY my_directory
ACCESS PARAMETERS 
(RECORDS DELIMITED BY '\r\n' 
SKIP 1  FIELDS TERMINATED BY ','
OPTIONALLY ENCLOSED BY '"' AND '"' LRTRIM  )
LOCATION ('BreadBasket_DMS.csv')
);

-- Carga da tabela externa para a tabela de transações
INSERT INTO transactions 
SELECT * FROM transactions_ext ;

COMMIT;

OK, então criei uma tabela chamada transactions, e depois criei um diretório chamado my_directory, que é a forma de falar para o Oracle em qual caminho está nosso arquivo que baixei do Kaggle.

Criei também uma tabela externa chamada transactions_ext, que serve apenas para configurar [delimitação, nome do arquivo, etc] a carga do arquivo para a tabela transactions. No final, o comando insert select coloca os dados da tabela externa, ou seja, do arquivo, na tabela transactions.

Agora vou preparar o algoritmo Apriori. Há dois requisitos:

[1] Criar uma view chamada trans_view com a coluna que agrupa as transações [trans_id], e a coluna com o nome dos itens [item]. Daqui pra frente, vamos utilizar a view ao invés da tabela transactions.

[2] Criar uma tabela de parâmetros para calibrar o algoritmo. Perceba que o nome da tabela de parâmetros pode ser qualquer um, mas o nome das colunas e os datatypes devem ser iguais aos que coloquei no script abaixo.

-- Criamos uma view com o grupo de transações e os itens
CREATE VIEW trans_view AS SELECT trans_id, item FROM transactions;

-- Criamos uma tabela para armazenar os parâmetros do algoritmo
CREATE TABLE parametros
(setting_name VARCHAR2(30), setting_value VARCHAR2(4000));

-- Inserimos os parâmetros na tabela
begin 
 insert into parametros values (DBMS_DATA_MINING.ASSO_MAX_RULE_LENGTH, 3);
 insert into parametros values (DBMS_DATA_MINING.ASSO_MIN_CONFIDENCE, 0.01);
 insert into parametros values (DBMS_DATA_MINING.ASSO_MIN_SUPPORT, 0.01);
 insert into parametros values (DBMS_DATA_MINING.ODMS_ITEM_ID_COLUMN_NAME, 'ITEM');
 commit;
end;
/

É importante entender que o algoritmo Apriori gera regras. Por exemplo, vamos supor que se alguém comprar pão e café, muito provavelmente também comprará manteiga. A parte antecedente, a regra, é “pão e café”, e a parte consequente, aquela que o algoritmo calcula, é a manteiga.

O algoritmo Apriori gera uma lista de regras, cada uma com sua relevância, e também as respectivas consequências, isto é, a associação correspondente [o que ele recomenda]. Para isso existe também uma série de parâmetros que precisam ser calibrados para o algoritmo não gerar muitas regras inúteis [o modelo não considera as regras que não estão de acordo com esses parâmetros]. Alguns desses parâmetros nós inserimos na tabela de parâmetros:

O parâmetro MAX_RULE_LENGTH determina qual é o tamanho da maior regra [até quantos itens ela poderá ter], por exemplo, a compra de “um pão” é uma regra com 1 item, a compra de “um pão e um café” tem 2 itens, e a compra de “um pão, um café e um bolo” tem 3 itens. MIN_CONFIDENCE é o porcentual mínimo de confiança da regra, isto é, são as chances de alguém que comprou pão também comprar café. MIN_SUPPORT determina a popularidade mínima dos itens da regra no conjunto total de dados. Por exemplo, se a regra “pão e café” participou de 10 transações num total de 100, o suporte desta regra é 10%. ITEM_ID_COLUMN_NAME é o nome da coluna que corresponde aos itens.

Eu utilizei 3 para max rule length, e 1% para min confidence e support. Isso significa que qualquer regra que não estiver de acordo com esses parâmetros, não será considerada no conjunto de regras que o modelo vai gerar.

1% é um valor baixo – normalmente você poderia utilizar de 10% a 30% para o mínimo de confiança e suporte. Mas como não temos muitos dados, decidi colocar um valor baixo. Assim eu gero mais regras, e decido qual é a restrição ideal mais pra frente quando embutir as regras na aplicação, como veremos mais adiante.

Obs: com muitos dados é importante não gerar muitas regras, às vezes inúteis [baixa confiança e pouco suporte podem gerar associações irrelevantes]. Esse algoritmo é caro em termos de processamento, como comentei antes.

Para uma explicação mais aprofundada sobre o algoritmo Apriori e seus parâmetros [que não é o objetivo deste artigo], você pode ver neste link.

Dados carregados, parâmetros calibrados. Agora vou criar o modelo. Como já falei, o modelo de machine learning utiliza o algoritmo Apriori para gerar as regras, com base nos dados, e nos parâmetros.


BEGIN 
DBMS_DATA_MINING.CREATE_MODEL(
model_name            => 'MODELO_PADARIA',
mining_function       =>  DBMS_DATA_MINING.ASSOCIATION,
data_table_name       => 'TRANS_VIEW',
case_id_column_name   => 'TRANS_ID',
target_column_name    =>  NULL,
settings_table_name   => 'PARAMETROS');
END;
/

A criação do modelo é feita através de uma API no banco de dados, chamada DBMS_DATA_MINING. Primeiro temos que dar um nome ao modelo [para identificá-lo e utilizá-lo na aplicação] com o atributo MODEL_NAME. Temos que selecionar o algoritmo no atributo MINING_FUNCTION, que em nosso caso é ASSOCIATION [este modelo utiliza o algoritmo Apriori]. No atributo DATA_TABLE_NAME temos que informar o nome da view que criamos anteriormente. No atributo CASE_ID_COLUMN_NAME temos que informar qual é a coluna da view que agrupa as transações [a cesta de compras]. O atributo TARGET_COLUMN_NAME só é usado para modelos supervisionados, e como ASSOCIATION é não-supervisionado podemos deixar nulo. SETTINGS_TABLE_NAME é o nome da tabela de parâmetros que definimos anteriormente.

Bom, agora que o modelo está criado, vamos ver o resultado [as regras criadas]. Uma observação importante é que podemos gravar as regras em uma tabela, ou podemos consultar direto de uma view de resultado, chamada DM$VR + Nome do Modelo, que em nosso caso é DM$VRMODELO_PADARIA, criada automaticamente quando criamos o modelo.

Você pode recriar o modelo com uma peridiocidade, por exemplo, uma vez por dia, ou uma vez por semana, ou mês. Depende muito do caso uso, se os dados tem muita variação ou não.

Vamos ver os resultados. Não se assuste com essa query abaixo. Eu explico.


SELECT list_of_antecedents, consequent, lift, confidence, support 
FROM (
   SELECT ROW_NUMBER() OVER (PARTITION BY rule_id ORDER BY ant_items.piece) as row_number,
          LISTAGG(ant_items.item, ' AND ') WITHIN GROUP (ORDER BY ant_items.piece) OVER (PARTITION BY RULE_ID) as list_of_antecedents,
          consequent,
          support, confidence, lift
   FROM (SELECT rule_id,
                antecedent, 
                consequent_name as consequent, 
                round(to_number(rule_support)*100,4) as support, 
                round(to_number(rule_confidence)*100,4) as confidence, 
                round(to_number(rule_lift),4) as lift
         FROM DM$VRMODELO_PADARIA) rules,
                   XMLTABLE ('/itemset/item'  PASSING rules.ANTECEDENT
                   COLUMNS item varchar2(30)  PATH 'item_name',
                           piece for ordinality ) ant_items
         WHERE lift > 1 AND confidence > 10 AND support > 1
   ORDER BY confidence DESC, support DESC
) WHERE row_number = 1

As linhas mais importantes são as 13 e 17. Na linha 13, temos a view que guarda os resultados da criação do modelo, isto é, onde estão as nossas regras e vários indicadores para cada uma delas [support, confidence, lift, e mais alguns outros]. Na linha 17, eu coloco as restrições, por exemplo, somente as regras com lift maior que 1, confidence maior que 10%, e support maior que 1%.

Normalmente alguns itens muito frequentes [support é maior] participam de regras com itens menos frequentes [support é menor]. Por exemplo: pão aparece em 70 de 100 transações, enquanto que manteiga aparece em 5 de 100. Certamente haverá uma regra pão e manteiga, e outra manteiga e pão. Quando há regras assim com o suporte dos itens tão discrepantes, isso pode acabar inflando o indicador de confiança [no meu exemplo, quando alguém comprar manteiga]. Desta forma, podemos usar um indicador adicional, o Lift, que nos mostra o quão provável é a compra da manteiga se alguém comprar o pão. Se for maior que 1, é provável que compre. Se for 1, é indiferente. E se for menor que 1, é improvável que compre. Por isso na query filtrei lift maior que 1.

A query realmente é o que está entre as linhas 7 e 17. Todo o entorno eu fiz para melhorar o output, e colocá-lo depois na aplicação simples que fiz em Python. Quando uma regra tem mais de um item, a linha se repete na view. Por isso usei LISTAGG e concatenei com um AND para ficar algo como “Bread AND Coffee”. E a função analítica ROW_NUMBER() utilizei para retornar só uma linha, a primeira, pois “Bread AND Coffee” iria retornar duas vezes. Se você executar esse código SQL aos poucos, primeiro o que está entre as linhas 7 e 17, e depois o entorno, vai entender melhor o que estou dizendo.

Esse é o resultado da query:

Figura 3 – Resultado da query no SQL Developer

Em uma aplicação, quando alguém seleciona por exemplo, Toast, você recomenda Coffee. Há 70% de confiança de que essa regra seja verdadeira.

Nota: todos esses códigos até aqui poderiam ter sido feitos no SQL Developer, com a feature Data Miner: é como um wizard que lhe permite implementar os algoritmos de machine learning com drag & drop.

Não Pare! É uma Vergonha Você Chegar até Aqui e Não Colocar esse Código na sua Aplicação

Vamos colocar esse código em uma aplicação simples para simular como seria.

Veja. Esse é só um blog. Não pense que vou usar algum design pattern com altos requintes de polimorfismo, um framework robusto, uma infraestrutura de kubernetes, microserviço, e tal… vai no Python script mode mesmo. Você vai entender!


import sys
import cx_Oracle

# itemsList é uma lista de itens que o usuário informa
# se for mais que um, um AND é incluído para cada par de itens
itemsList = ''
for args in sys.argv[1:]:
    if len(itemsList) == 0:
        itemsList = args
    else:
        itemsList = itemsList + ' AND ' + args

# Conecta no banco de dados Oracle
conn = cx_Oracle.connect("demouser", "demopass", "localhost/XE")
cur = conn.cursor()

# É a vez daquela query que retorna os resultados
# Mas desta vez estou filtrando com itemsList

sql = """SELECT rownum, consequent FROM (
       SELECT ROW_NUMBER() OVER (PARTITION BY rule_id ORDER BY ant_items.piece) as row_number,
          LISTAGG(ant_items.item, ' AND ')
          WITHIN GROUP (ORDER BY ant_items.piece)
             OVER (PARTITION BY RULE_ID) as list_of_antecedents,
          consequent, support, confidence, lift
   FROM (SELECT rule_id,
                antecedent, 
                consequent_name as consequent, 
                round(to_number(rule_support)*100,4) as support, 
                round(to_number(rule_confidence)*100,4) as confidence, 
                round(to_number(rule_lift),4) as lift
         FROM DM$VRANALISE_ASSOCIACAO) rules,
                   XMLTABLE ('/itemset/item'  PASSING rules.ANTECEDENT
                   COLUMNS item varchar2(30)  PATH 'item_name',
                           piece for ordinality ) ant_items
         WHERE lift > 1 AND confidence > 10 AND support > 1
   ORDER BY confidence DESC, support DESC
) WHERE row_number = 1 and list_of_antecedents = :antecedentList"""

# Executa a query e mostra os itens que devemos recomendar
cur.execute(sql, antecedentList=itemsList)
res = cur.fetchall()
print(res)

A aplicação app.py recebe um argumento que é a lista de antecedentes. Poderia ser por exemplo um clique do usuário, ou os produtos que ele coloca num carrinho de compras. A aplicação retornar uma lista de consequentes por ordem de confiança.

Veja alguns exemplos de execução deste código:

Quando o usuário entra com Coffee, a aplicação recomenda Cake. Quando o usuário entra com Bread e Coffee, a aplicação recomenda primeiro Pastry, e depois Cake. Quando o usuário entra com Tea, a aplicação recomenda Cake e depois Sandwich. E quando entra com Cake, a aplicação recomenda primeiro Coffee, depois Tea, e depois Hot Chocolate.

Essa é a vantagem de ter os algoritmos de machine learning dentro do banco de dados. Você aproveita os recursos do servidor do banco, que entrega bastante performance, e também a linguagem SQL, que é muito fácil de usar.

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Foto do Google

Você está comentando utilizando sua conta Google. Sair /  Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s