Iniciando a execução dos notebooks
Olá!
Agora que concluímos a configuração do nosso ambiente, podemos seguir com as etapas de execução dos nossos notebooks.
Temos o nosso ambiente virtual configurado, com as dependências instaladas e tudo pronto.
Vamos executar o nosso primeiro Jupyter Notebook, GetData.ipynb. Nesse notebook, o objetivo será realizar a aquisição dos dados.
Apresentando o conjunto de dados amazon reviews 2023
Antes de prosseguir, vamos esclarecer quais são os dados que estamos analisando ao longo deste projeto. Trata-se do conjunto Amazon Reviews 2023. Podemos facilmente localizar esse material no Google, procurando pela plataforma Hugging Face. Ao buscar por Hugging Face e Amazon Reviews 2023, encontramos o conjunto de dados que precisamos analisar neste momento.
Na página do conjunto de dados, há um link para a página oficial das pessoas desenvolvedoras, com mais detalhes. Entre as informações, estão os anos contemplados: 2013, 2014, 2018 e 2023. A quantidade de avaliações é bastante grande; por exemplo, em 2023 há 571,54 milhões de avaliações. Também há a divisão por categorias, uma vez que, dentro da Amazon, existem diversos tipos de produtos, agrupados por categoria. Entre elas, encontramos All Beauty, Amazon Fashion, Appliances e Automotive, entre outras. É possível baixar as avaliações e os dados diretamente pela página, mas faremos isso utilizando linhas de código em Python.
Definindo o recorte dos dados
Voltando ao nosso ambiente, o objetivo será buscar os dados do conjunto de dados disponível no Hugging Face. Como recorte, usaremos a categoria All Beauty. Desse conjunto, vamos extrair as avaliações e armazená-las em um banco de dados local no formato .db, por exemplo, no arquivo reviews.db.
Para este exercício, vamos trabalhar com uma única categoria (All Beauty) e com o ano de 2021. Vamos obter todas as avaliações dessa categoria referentes ao ano de 2021. É possível adotar outras estratégias, como incluir mais categorias ou ampliar o recorte do conjunto de dados, mas isso exigiria mais memória para treinar o modelo. Por esse motivo, vamos fixar o ano de 2021 e a categoria All Beauty.
Configurando parâmetros no arquivo config.py
Antes de executar o Jupyter Notebook, vamos tratar da estrutura do projeto. Uma boa prática é centralizar, em um arquivo de configuração ou em variáveis de ambiente, as informações que podemos querer ajustar ao longo da execução do projeto. No arquivo config.py, reunimos uma série de configurações, como:
- o identificador do repositório (repo) do conjunto de dados, no caso, Amazon Reviews 2023;
- a categoria que vamos buscar (All Beauty);
- as colunas que vamos extrair das avaliações;
- as colunas que vamos extrair da parte de metadados (por exemplo,
META_COLS); - o ano-alvo;
- o mínimo de caracteres por avaliação;
- e o caminho onde vamos guardar o banco de dados (
DB_PATH).
Veja um exemplo de como centralizamos essas configurações em config.py:
import os
REPO_ID = 'McAuley-Lab/Amazon-Reviews-2023'
CATEGORIA = 'All_Beauty'
REVIEW_COLS = ["rating", "title", "text", "parent_asin", "user_id",
"timestamp", "helpful_vote", "verified_purchase"]
META_COLS = ["parent_asin", "title", "main_category",
"average_rating", "rating_number", "price", "store"]
Aquisição: uma categoria, um ano inteiro, sem amostragem.
ANO_ALVO = 2021
MIN_CARACTERES = 20
DB_PATH = 'data/reviews.db'
Alguns pontos podem gerar dúvidas, como, por exemplo, o significado das colunas. Para entendê-las melhor, podemos abrir a página do conjunto de dados no Hugging Face, acessar a aba Files and Versions (Arquivos e versões) e, em seguida, Raw (bruto). Ali, nas seções correspondentes a avaliações (por exemplo, Review Categories), são listadas as colunas disponíveis. Como estamos trabalhando com a categoria All Beauty, esse é o arquivo que vamos buscar. O arquivo é do tipo JSON Lines (JSON por linhas). Portanto, especificamos em config.py quais colunas vamos extrair desse formato para cada categoria. Essa configuração pode parecer intuitiva para quem já utiliza conjuntos de dados do Hugging Face, mas vamos avançar no desenvolvimento e, depois, voltamos a esses detalhes conforme necessário.
Preparando o notebook e as importações
Com a categoria e o ano definidos, criamos as variáveis correspondentes em config.py. Para executar o Jupyter Notebook, precisamos selecionar o kernel, isto é, o kernel associado ao nosso ambiente virtual. No nosso caso, usaremos o ambiente virtual .venv. Com isso selecionado, podemos executar as funções normalmente.
Do config.py, importamos informações como ano-alvo, categoria, DB_PATH, as colunas e o REPO_ID. No arquivo utils.py, criamos uma função para executar consultas SQL. Explicaremos esse ponto mais adiante; por ora, vamos até comentar o trecho associado, para que ocorra um erro mais à frente e possamos retomar a explicação no momento oportuno.
Primeiro, fazemos as importações gerais: sqlite3, pandas e utilitários do pacote huggingface_hub (vamos apelidar hf_hub_download como hf para facilitar). O snippet abaixo mostra essas importações, além de carregar as configurações do config.py:
import os
import sqlite3
import pandas as pd
from huggingface_hub import hf_hub_download as hf
from config import (ANO_ALVO, CATEGORIA, DB_PATH, META_COLS, MIN_CARACTERES,
REPO_ID, REVIEW_COLS)
from utils.sql import query_sql
Como mencionado, vamos comentar temporariamente a importação da função de utilitários para que possamos demonstrar o erro e, depois, corrigi-lo no momento oportuno:
from config import (ANO_ALVO, CATEGORIA, DB_PATH, META_COLS, MIN_CARACTERES,
REPO_ID, REVIEW_COLS)
Em seguida, limpamos todos os resultados anteriores e executamos a primeira célula. Será solicitado que instalemos o pacote do kernel para o Jupyter dentro do ambiente virtual (por exemplo, o pacote ipykernel). Concluída essa etapa, o notebook estará pronto para uso.
Neste ponto, importamos sqlite3, pandas e utilitários do pacote huggingface_hub (por exemplo, a função hf_hub_download), que será utilizada para realizar o download (transferência) do conjunto de dados. Podemos apelidar o módulo/função, conforme a conveniência do código, para facilitar as chamadas.
Para baixar os arquivos brutos, criaremos uma função auxiliar. Em linhas gerais, ela recebe:
- a pasta;
- o nome do arquivo;
- o
repo_id (identificador do repositório no Hugging Face); - o
repo_type (tipo de repositório); - o
filename (caminho do arquivo a ser baixado); - e a indicação de uso de cache (memória de cache), se desejado.
O repo_id corresponde ao identificador do nosso conjunto de dados (informação exibida no topo da página do repositório). O filename é o caminho do arquivo que será baixado. Por exemplo, podemos navegar até a raiz do repositório e, então, especificar o caminho dentro de raw (bruto): primeiro a pasta, como "review_categories" ou "meta_categories", e depois o nome do arquivo (relacionado à categoria de interesse). Assim, a função recebe a pasta e o nome, localiza o arquivo na pasta específica e o baixa para a máquina local.
Podemos implementar e usar essa função assim:
def baixar(pasta: str, nome: str) -> str:
return hf(repo_id&
filename&
arquivo_reviews = baixar("review_categories", CATEGORIA)
arquivo_meta = baixar("meta_categories", f"meta_{CATEGORIA}")
print("arquivos prontos")
Para baixar os arquivos de avaliações, chamaremos a função auxiliar informando "review_categories" e a categoria (which comporá o filename). Ao executar, o processo de download (transferência) é iniciado e os arquivos passam a ser armazenados dentro da pasta "data". Dentro de "data", o Hugging Face utiliza uma estrutura de cache (memória de cache), por exemplo, "hf_cache", para gerenciar os arquivos baixados localmente.
Após a conclusão, a ferramenta informa que o download (transferência) foi realizado. Se verificarmos o conteúdo do cache, veremos diretórios como "datasets" e "snapshots" (instantâneos), que registram os artefatos baixados.
Aqui, ReviewCategory e MetaCategory.
Em ReviewCategory, temos o arquivo em formato JSON Lines, exatamente aquele arquivo que estávamos verificando.
Temos uma série de colunas.
Agora, vamos filtrar as colunas de interesse. Foi por isso que definimos essas colunas em config.py.
Por exemplo, temos rating, title e text.
Essas são as informações necessárias dentro das revisões para que possamos treinar nosso modelo de análise de sentimento no módulo seguinte.
Neste momento, já conseguimos baixar as revisões do Hugging Face, utilizando o Hugging Face Hub e o processo de download.
Precisamos agora realizar algumas filtragens, porque o arquivo traz todos os dados e, portanto, vamos especificar as informações que queremos.
Convertendo JSONL em dataframe
A primeira função vai apenas receber o JSON Lines e retornar um DataFrame. Aqui não há nada muito robusto. Vamos, inclusive, comentar esse trecho de código.
Para isso, começamos implementando a função que lê o JSONL em pedaços e devolve um DataFrame com apenas as colunas selecionadas. Em seguida, carregamos as reviews inicialmente:
def jsonl_to_dataframe(arquivo: str, cols: list[str]) -> pd.DataFrame:
dfs &
with pd.read_json(arquivo, lines&
for chunk in reader:
dfs.append(chunk[cols])
return pd.concat(dfs, ignore_index&
reviews = jsonl_to_dataframe(arquivo_reviews, REVIEW_COLS)
print(f"reviews na categoria: len({reviews})")
Por ora, deixamos os passos de transformação comentados, mantendo apenas a leitura e a seleção das colunas:
reviews = jsonl_to_dataframe(arquivo_reviews, REVIEW_COLS)
print(f"reviews na categoria: len({reviews})")
reviews = reviews.rename(columns={"parent_asin": "product_id", "timestamp": "review_date"})
reviews["review_date"] = pd.to_datetime(reviews["review_date"], unit='ms')
reviews = reviews[(reviews["review_date"].dt.year == ANO_ALVO)
& (reviews["text"].str.len() >= MIN_CARACTERES)]
reviews = reviews.drop_duplicates(subset=["user_id", "text"])
reviews["review_date"] = reviews["review_date"].astype(str)
reviews["verified_purchase"] = reviews["verified_purchase"].astype(int)
reviews["helpful_vote"] = reviews["helpful_vote"].fillna(0).astype(int)
reviews = reviews.sort_values("review_date").reset_index(drop=True)
reviews.insert(0, 'review_id', range(1, len(reviews) + 1))
print(f"reviews no ano {ANO_ALVO} após filtros: {len(reviews)}")
reviews.head()
Se executarmos e verificarmos Reviews depois, basicamente, o que era JSON Lines agora está no formato DataFrame. É simples chegar a essa conclusão quando percebemos que a saída é de fato um DataFrame.
Passamos o arquivo, especificamos quais são as colunas e ele retorna como DataFrame. É exatamente isso que está sendo feito. Passamos REVIEW_COLS, filtramos dentro do JSON Lines somente as colunas de interesse e obtemos um DataFrame contendo apenas os dados necessários.
Aplicando transformações e filtros
Agora, vamos realizar outras alterações nesse DataFrame que já convertemos a partir do JSON Lines.
Primeiro, vamos fazer um renomeamento. A coluna parent_asin ficará como product_id, timestamp será convertida para review_date e review_date será convertido para o tipo DateTime.
Esse ajuste de nomes e a conversão de review_date para DateTime são necessários porque agora aplicaremos alguns filtros.
review_date deverá ser igual ao AnoAlvo, que é 2021. Assim, traremos apenas dados de 2021.
Também verificaremos se o tamanho dos textos dos nossos reviews é maior ou igual ao mínimo de caracteres definido anteriormente.
Vamos remover duplicatas: verificamos se o text atribuído a um user_id foi repetido; em caso afirmativo, mantemos somente o primeiro.
Em seguida, convertemos review_date novamente para string.
No caso da verificação de compra, o campo verified_purchase, que é booleano (True/False), será convertido para 0/1.
No campo de voto útil, helpful_vote, valores None serão convertidos para 0. Essa é uma decisão que pode impactar o resultado.
Aplicaremos um sort_values baseado em review_date.
Depois de executar, teremos o DataFrame final com as configurações dos nossos reviews, contendo review_id, rating, title, text, product_id e demais informações necessárias.
Podemos agora aplicar todas essas transformações:
reviews = jsonl_to_dataframe(arquivo_reviews, REVIEW_COLS)
print(f"reviews na categoria: len({reviews})")
reviews = reviews.rename(columns={"parent_asin": "product_id", "timestamp": "review_date"})
reviews["review_date"] = pd.to_datetime(reviews["review_date"], unit='ms')
Filtros
reviews = reviews[(reviews["review_date"].dt.year == ANO_ALVO)
& (reviews["text"].str.len() >= MIN_CARACTERES)]
reviews = reviews.drop_duplicates(subset=["user_id", "text"])
reviews["review_date"] = reviews["review_date"].astype(str)
reviews["verified_purchase"] = reviews["verified_purchase"].astype(int)
reviews["helpful_vote"] = reviews["helpful_vote"].fillna(0).astype(int)
reviews = reviews.sort_values("review_date").reset_index(drop=True)
reviews.insert(0, 'review_id', range(1, len(reviews) + 1))
print(f"reviews no ano {ANO_ALVO} após filtros: {len(reviews)}")
reviews.head()
Tratando os metadados do produto
Agora, vamos tratar dos metadados do produto.
Basicamente, usaremos a mesma função para processar os arquivos de metadados, META_COLS.
Faremos o renomeamento e alguns outros processamentos, incluindo a remoção de duplicatas com base em product_id.
Podemos imprimir para inspecionar. Ao executar, teremos product_id, title, main_category, a nota média (average_rating), rating_number e price.
products = jsonl_to_dataframe(arquivo_meta, META_COLS)
products = (products.rename(columns={"parent_asin": "product_id"})
.assign(price=pd.to_numeric(products["price"], errors='coerce'))
.dropna(subset=['price', 'store'])
.drop_duplicates(subset=["product_id"]))
print(f"produtos no metadado: len({products})")
products.head(3)
Com base nisso, conseguimos associar essas duas tabelas, pois ambas possuem product_id. Portanto, conseguimos realizar um JOIN entre as duas.
Processamos os produtos e também os nossos reviews.
Convertendo e enviando para sqlite
Agora, vamos converter e enviar tudo para um banco SQLite.
Como primeiro passo, criamos uma conexão com sqlite3.connect, passando dbpath.
Lembrando: dbpath é o que definimos em config.py.
Em seguida, executamos um script no qual passamos a query SQL.
Esse script, primeiro, remove as tabelas Review e Produto, caso existam. Em seguida, cria a tabela Produto, com as colunas ProductId (chave primária), Title, MainCategory, AverageRating, RatingNumber, Price e Store.
Também criamos a tabela Reviews, com ReviewId, ProductId, UserId, ASIN, Rating, Title, Text, ReviewDate, HelpfulVote e VerifiedPurchase.
Veja como isso fica no código (note que os nomes das tabelas/colunas no script estão em inglês, seguindo o padrão do nosso código):
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
DROP TABLE IF EXISTS produtos;
DROP TABLE IF EXISTS reviews;
cur.executescript('''
CREATE TABLE products (
product_id TEXT PRIMARY KEY,
title TEXT,
main_category TEXT,
average_rating REAL,
rating_number INTEGER,
price REAL,
store TEXT
);
CREATE TABLE reviews (
review_id INTEGER PRIMARY KEY,
product_id TEXT NOT NULL REFERENCES products(product_id),
user_id TEXT NOT NULL,
asin TEXT,
rating REAL NOT NULL,
title TEXT,
text TEXT,
review_date TEXT NOT NULL,
helpful_vote INTEGER,
verified_purchase INTEGER
);
CREATE INDEX idx_review_product ON reviews(product_id);
CREATE INDEX idx_review_date ON reviews(review_date);
''')
products.to_sql("products", conn, if_exists="append", index=False)
reviews.to_sql("reviews", conn, if_exists="append", index=False)
conn.commit()
print("banco gravado")
Depois de criar as tabelas, comentamos o trecho de código específico e executamos o comando para verificar o resultado. Dentro da pasta "Data", foi criado o arquivo Reviews.db. Nele, constam as estruturas para produtos e reviews.
Agora, pegamos os DataFrames criados anteriormente, Products e Reviews, usamos to_sql para converter cada DataFrame para SQL e enviamos para dentro do nosso Reviews.db.
CREATE INDEX idx_review_product ON reviews(product_id);
CREATE INDEX idx_review_date ON reviews(review_date);
''')
products.to_sql("products", conn, if_exists="append", index=False)
reviews.to_sql("reviews", conn, if_exists="append", index=False)
conn.commit()
print("banco gravado")
Ao executar, teremos a tabela de produtos e a tabela de reviews devidamente persistidas.
Com isso, concluímos a primeira demanda: buscar os dados no Hugging Face e armazená-los em Reviews.db.
Consultando o banco e definindo query_sql
Podemos fazer uma breve conferência. Se executarmos uma consulta agora, pode ocorrer um erro. A origem desse erro é a função QuerySql.
Adotamos aqui uma boa prática que utilizamos em nossos projetos: como estamos trabalhando com um banco SQLite e precisaremos executar QuerySql repetidamente, criamos dentro da pasta "utils" um módulo Python sql com a função QuerySql. Essa função recebe a conexão e a query e retorna o resultado no formato DataFrame. Precisamos importá-la.
Para começar a checagem, tentamos rodar consultas usando query_sql — lembrando que deixamos a importação comentada anteriormente, justamente para evidenciar o erro:
resumo = query_sql(conn, '''
SELECT COUNT(*) AS n_reviews,
ROUND(AVG(rating), 2) AS nota_media,
ROUND(100.0 * AVG(verified_purchase), 2) AS pct_verificados,
MIN(review_date) AS primeira,
MAX(DATE(review_date)) AS ultima
FROM reviews
''')
print(resumo.to_string(index=False))
per_mes = query_sql(conn, '''
SELECT strftime('%m', review_date) AS mes, COUNT(*) AS n, ROUND(AVG(rating), 2) AS nota
FROM reviews GROUP BY mes
''')
conn.close()
per_mes
Como esperado, sem a importação ativa, ao executar, veremos um erro indicando que query_sql não está definido.
Agora, vamos definir essa função no módulo utils/sql.py e então importá-la. A função simplesmente executa a query e retorna um DataFrame:
import pandas as pd
def query_sql(conn, query: str) -> pd.DataFrame:
"""Executa uma query SQL e devolve um DataFrame com o resultado."""
return pd.read_sql_query(query, conn)
Fazemos o seguinte: from utils.sql import query_sql. Passamos a conexão e a query para QuerySql. Ao executar, tudo funciona.
from utils.sql import query_sql
resumo = query_sql(conn, '''
SELECT COUNT(*) AS n_reviews,
ROUND(AVG(rating), 2) AS nota_media,
ROUND(100.0 * AVG(verified_purchase), 2) AS pct_verificados,
MIN(review_date) AS primeira,
MAX(DATE(review_date)) AS ultima
FROM reviews
''')
print(resumo.to_string(index=False))
per_mes = query_sql(conn, '''
SELECT strftime('%m', review_date) AS mes, COUNT(*) AS n, ROUND(AVG(rating), 2) AS nota
FROM reviews GROUP BY mes
''')
conn.close()
per_mes
Apresentamos algumas informações: estamos calculando a contagem do número de reviews (avaliações), a nota média, o percentual de verificados, além da primeira e da última avaliação.
Em seguida, obtemos o resumo como resultado.
O resultado é apresentado como um DataFrame (estrutura de dados tabular). Em seguida, convertemos para String (texto) e realizamos outra consulta.
Corrigindo o erro de conexão fechada
Identificamos um erro: o problema ocorre porque estamos fechando o banco de dados.
Vamos retornar ao início, onde criamos a conexão. Localizamos o trecho em que a conexão foi criada, vamos copiar esse caminho e inseri-lo aqui. Funcionou.
from utils.sql import query_sql
conn = sqlite3.connect(DB_PATH)
resumo = query_sql(conn, '''
Aqui trazemos essas métricas e realizamos outra verificação.
Agrupando por mês
Estamos efetuando um agrupamento por mês.
SELECT strftime('%m', review_date) AS mes, COUNT(*) AS n, ROUND(AVG(rating), 2) AS nota
FROM reviews GROUP BY mes
''')
conn.close()
per_mes
Esse é o resultado e, por ora, está funcionando.
Encerrando a aula
Concluímos nosso primeiro objetivo: extrair os dados do Hugging Face e inseri-los em um SQLite.
Na próxima aula, retornaremos para avaliar esses dados e realizar uma análise exploratória breve.
Pessoal, agradeço muito e te vejo na próxima aula.