DuckDB x Pandas

Git do Projeto: DuckDb

A ideia do projeto é gerar dados de acordo com o desejo do usuário, para que possamos simular uma situação real de utilização de dados, para tratamento comparando o desempenho do Pandas e DuckDB

Regras de Negócio

Para esse projeto o Gestor solicitou que fosse extraido o resultado do total de vendas, como também total de vendas por produtos e por clientes. Para gerar arquivos nos formatos csv e parquet, destas informações para o time de DataViz poder criar incríveis e infaliveis dashboards.

Estrutura

Basicamente o projeto contém 2 arquivos python, responsáveis pela geração dos dados, e outros arquivos de ETL com python, usando pandas e duckdb;

gerador.py

O arquivo gerador.py é responsável pela função de geração de dados, como também de criação de pastas para armazenar estes dados.

Primeiramente a etapa de importações:

import csv
import os
from random import choice

from faker import Faker
from tqdm import tqdm

Criando Estruturas a serem usadas:

# Criando Dicionário de Produtos
lista_produtos = {
    "Computador": 2000,
    "Celular Xiaomi": 1000,
    "Celular Samsung": 1500,
    "Celular Iphone": 5000,
    "Monitor": 700,
    "Teclado": 50,
    "Mouse": 20,
    "HeadSet": 100,
    "HeadSet JBL": 250,
    "Carregador Iphone": 105,
    "Carregador Samsung": 80,
    "Carregador Xiaomi": 90,
    "Carregador Motorola": 100,
    "Carregador Nokia": 110,
    "PenDrive 4 GB": 20,
    "PenDrive 8 GB": 30,
    "PenDrive 16 GB": 40,
}

# Criando Lista de Clientes, para ser possível o controle de quantidade de clientes
cliente = []

Iniciando Função de geração:

# Definindo fução para gerar dados
def gerando_dados(
    total_linhas: int, total_clientes: int, nome_arquivo: str, tamanho_bloco: int = 100
):
    # Invocando a biblioteca faker
    falso = Faker()

    # Criando Arquivo
    with open(nome_arquivo, "w", newline="") as file:
        escrever = csv.writer(file)
        escrever.writerow(
            ["Cliente", "Data_Compra", "Produto", "ValorProduto", "Quantidade"]
        )

        # Definindo Lista aleatória de clientes
        for _ in range(total_clientes):
            cliente.append(falso.name())

        # Criando Lista para armazenar os dados
        data = []
        # Interando Barra de Progresso considerando o total de linhas, para gerar dados
        for i in tqdm(range(total_linhas), desc="Progresso", unit="linha"):
            produto = choice(list(lista_produtos.keys()))
            data.append(
                [
                    choice(cliente), # Escolhendo Cliente
                    falso.date(), # Gerando Data Aleatória
                    produto, # Escolhendo Produto previsto na lista
                    lista_produtos[produto], # Escolhendo Valor
                    falso.random_number(1, 10), # Definindo Quantidade
                ]
            )

            # Progressão dos dados na Barra
            if i % tamanho_bloco == tamanho_bloco - 1:
                escrever.writerows(data)
                data = []

        if data:
            escrever.writerows(data)

    print("Dados gerados com sucesso!")

Agora por fim neste arquivo temos a função responsável pela verificação e criação das pastas onde será armazenados os dados:

def criar_pastas(diretorio_atual):
    """
    Cria a estrutura de pastas 'data/csv/' e 'data/parquet/' na pasta atual.

    Args:
      diretorio_atual: Caminho da pasta atual.
    """

    pastas = ["data/csv/", "data/parquet/"]

    for pasta in pastas:
        diretorio_completo = os.path.join(diretorio_atual, pasta)

        # Verifica se o diretório existe
        if not os.path.exists(diretorio_completo):
            # Cria o diretório
            os.makedirs(diretorio_completo)
            print(f"Diretório '{diretorio_completo}' criado com sucesso!")

gerar.py

Agora temos também o arquivo gerar, responsável apenas para chamar as funções do arquivo anterior definindo os parametros:

from gerador import gerando_dados, criar_pastas
import os

gerando_dados(100000000, 20000, "dados.csv")

# Obtém o diretório atual
diretorio_atual = os.getcwd()

# Cria as pastas
criar_pastas(diretorio_atual)

teste01-duck-csv.py (Processo de ETL usando Python + DuckDB, para arquivo CSV)

Script para realizar ETL dos dados e exportar em CSV, utilizando a biblioteca DuckDB

import duckdb
import time

print("Iniciando CSV ...")
# Registrando o tempo de início
inicio = time.time()

# Conectando ao banco de dados DuckDB
con = duckdb.connect()

# Carregando dados
con.execute("CREATE TABLE dados AS SELECT * FROM read_csv('dados.csv');")

# Criando nova coluna de Valor Total
con.execute("ALTER TABLE dados ADD COLUMN ValorTotal DECIMAL(10,2)")
con.execute("UPDATE dados SET ValorTotal = ValorProduto * Quantidade")

# Salvando dados-duck.csv
con.execute("COPY (SELECT * FROM dados) TO 'data/csv/dados-duck.csv'")

# Salvando total_vendas_produtos-duck.csv
con.execute(
    "COPY (SELECT Produto, SUM(ValorTotal) AS TotalVendas FROM dados GROUP BY Produto ORDER BY TotalVendas DESC) TO 'data/csv/total_vendas_produtos-duck.csv'"
)

# Salvando total_vendas_cliente-duck.csv
con.execute(
    "COPY (SELECT Cliente, SUM(ValorTotal) AS TotalVendas FROM dados GROUP BY Cliente ORDER BY TotalVendas DESC) TO 'data/csv/total_vendas_cliente-duck.csv'"
)

# Fechando a conexão com o banco de dados
con.close()

# Registrando o tempo de término
fim = time.time()

# Calculando o tempo de execução
tempo_execucao = fim - inicio

print("Finalizado com Sucesso!!!")
print(f"Tempo de execução: {tempo_execucao:.2f} segundos")

teste02-pd-csv.py (Processo de ETL usando Python + Pandas, para arquivo CSV)

Script para realizar ETL dos dados e exportar em CSV, utilizando a biblioteca Pandas

import pandas as pd
import time
import os


print("Iniciando CSV.....!!!")
# Registrando o tempo de início
inicio = time.time()

# Carregando dados
df = pd.read_csv("./dados.csv")

# Criando nova coluna de Valor Total
df["ValorTotal"] = df["ValorProduto"] * df["Quantidade"]

# Verificando o total de vendas por produtos
total_vendas_produtos = df.groupby("Produto")["ValorTotal"].sum().reset_index()
total_vendas_produtos = total_vendas_produtos.rename(
    columns={"ValorTotal": "TotalVendas"}
)
total_vendas_produtos = total_vendas_produtos.sort_values(
    by="TotalVendas", ascending=False
)


# Verificando o total de vendas por clientes
total_vendas_cliente = df.groupby("Cliente")["ValorTotal"].sum().reset_index()
total_vendas_cliente = total_vendas_cliente.rename(
    columns={"ValorTotal": "TotalVendas"}
)
total_vendas_cliente = total_vendas_cliente.sort_values(
    by="TotalVendas", ascending=False
)

# Salvando DF em CSV
df.to_csv("data/csv/dados-pandas.csv", index=False)

# Salvando DF de Total De Vendas por Produto
total_vendas_produtos.to_csv("data/csv/total_vendas_produtos-pandas.csv", index=False)

# Salvando DF de Total De Vendas por Cliente
total_vendas_cliente.to_csv("data/csv/total_vendas_cliente-pandas.csv", index=False)

# Registrando o tempo de término
fim = time.time()

# Calculando o tempo de execução
tempo_execucao = fim - inicio
print("Finalizado com Sucesso!!!")
print(f"Tempo de execução: {tempo_execucao:.2f} segundos")

Resultado

Ao utilizar o pandas para o tratamento de dados o mesmo desempenho todas o pipeline de dados em aproximadamente 1.008 segundos (Cerca de 16 minutos), exportando os dados tanto em csv, como também em parquet. Conforme imagem abaixo:

Image

Ao utilizar o DuckDB, para realizar o mesmo pipeline de dados ele levou 584,83 segundos (Menos de 10 minutos). Conforme imagem abaixo:

Imagem

-- O DuckDB teve uma perfomace 72% melhor do que o Pandas

Configuração do Computador Utilizado

  • Processador: AMD Ryzen 3 5300U with Radeon Graphics 2.6 Ghz
  • RAM: 8 GB DDR-4
  • SSD Sata: 256 GB
  • SO: Windows 11 23H2

Stack Utilizada no Projeto

  • Python
  • Pandas
  • Faker
  • DuckDB
  • Pyarrow
  • Fastparquet
  • TQDM
  • Taskipy
  • Mkdocs