Fluxo Técnico Modular para Preparação de Dados de Vendas

Luciano Magalhães   |    Novermbro, 2024   | Engenharia de Dados

Banner do Projeto BTC

1. Contexto

Antes de iniciar qualquer análise, é importante garantir que os dados estejam organizados e disponíveis em um formato que permita leitura e validação. Este projeto foi estruturado como um pipeline em Python para tratar dados de vendas, com foco em etapas como ingestão, verificação e exportação. A proposta é facilitar o uso dos dados em relatórios e visualizações.


2. Objetivo

Apresentar um fluxo de trabalho em Python para organizar e validar dados de vendas. O projeto é dividido em módulos que tratam cada etapa separadamente. Os resultados são gerados em formatos como CSV, JSON e TXT, e podem ser utilizados por diferentes perfis de usuários.


3. Metodologia

O projeto foi dividido em módulos independentes, cada um com uma função específica. A execução é feita por um arquivo principal que chama os demais componentes. A seguir estão os módulos utilizados:

  • main.py: executa o pipeline completo;
  • config.py: define os caminhos e parâmetros do projeto;
  • utils.py: contém funções auxiliares como leitura de arquivos e configuração de logs;
  • validators.py: aplica regras de estrutura e negócio aos dados;
  • diagnostics.py: gera informações técnicas sobre os dados e o processo.

A sequência de execução segue as etapas de leitura, validação, diagnóstico e exportação.


4. Preparação do Projeto

Nesta etapa inicial o objetivo é garantir que o ambiente de execução do pipeline esteja corretamente configurado e que os artefatos básicos (entradas, diretórios de saída e logging) sejam criados de forma reprodutível e rastreável. A preparação aborda:

  • Configuração do logging com rotação e saída no console;
  • Criação dos diretórios necessários (logs e output);
  • Leitura segura do arquivo de entrada (dados_vendas.csv) com validações de existência e encoding;
  • Aplicação das validações de estrutura e regras de negócio (módulo validators.py);
  • Geração de diagnóstico técnico (módulo diagnostics.py) e exportação dos metadados.
In [1]:
# ============================================================
# 4. Preparação do Projeto
# ============================================================
# Compatibilidade de anotações de tipos
from __future__ import annotations

# Bibliotecas padrão
import logging                  # Sistema de logging
from pathlib import Path         # Manipulação de caminhos
from typing import Dict, Any     # Tipagem de funções
import os                        # Operações de diretórios
import uuid                      # Identificadores únicos (usados em modais HTML)
import html                      # Escapar conteúdo de arquivos para exibição segura

# Bibliotecas externas
import pandas as pd              # Manipulação de DataFrames
from IPython.display import display, HTML  # Exibição em notebooks

# Imports do projeto
from config import (             # Configurações globais
    INPUT_CSV,
    LOGS_DIR,
    OUTPUT_DIR,
    META_JSON,
    RELATORIO_TXT
)

from utils import (              # Funções auxiliares
    setup_logging,               # Configuração de logging
    ensure_dirs,                 # Criação de diretórios
    read_csv_safely,             # Leitura segura de CSV
    save_json,                   # Salvamento em JSON
    save_dataframe,              # Salvamento de DataFrame em CSV
    moldura_texto                # Impressão de moldura de seção
)

from validators import run_validations   # Validações de estrutura e regras de negócio

from diagnostics import (                # Diagnóstico técnico
    diagnostico_dados,
    gerar_relatorio_texto
)


def preparar_ambiente(
    input_csv: str = INPUT_CSV,
    logs_dir: str = LOGS_DIR,
    output_dir: str = OUTPUT_DIR,
) -> Dict[str, Any]:
    """
    Etapa 4 - Preparação do Projeto.

    - Configura logging (arquivo + console, com rotação)
    - Cria diretórios necessários (logs, output, metadados, resumo)
    - Lê o dataset CSV de vendas de forma segura
    - Aplica validações de estrutura e regras de negócio
    - Gera diagnóstico técnico e exporta metadados e relatório
    """

     # Configuração do logger (arquivo pipeline.log + console) com nível INFO
    logger = setup_logging(
        logs_dir=logs_dir,
        filename="pipeline.log",
        level=logging.INFO
    )
    
    # Garantia de diretórios base e derivados usados na execução
    ensure_dirs(
        logs_dir,
        output_dir,
        Path(output_dir) / "metadados",
        Path(output_dir) / "resumo"
    )
    # Registro do início da etapa
    logger.info(">>> Iniciando etapa 4: Preparação do Projeto")

    # 2) Leitura segura do dataset CSV (existência e encoding)
    df = read_csv_safely(input_csv, logger=logger)

    # 3) Validação e padronização das colunas e tipos
    df = run_validations(df, logger=logger)

    # 4) Diagnóstico técnico e metadados (shape, dtypes, nulos, conversões)
    meta = diagnostico_dados(df, logger=logger)

    # 5) Salvamento dos metadados (JSON) e relatório textual consolidado
    try:
        # Salva metadados em JSON no diretório de saída/metadados
        save_json(meta, str(Path(output_dir) / "metadados" / Path(META_JSON).name))

        # Gera relatório textual consolidado no diretório de logs
        gerar_relatorio_texto(
            meta=meta,
            caminho=str(Path(logs_dir) / Path(RELATORIO_TXT).name),
            logger=logger
        )

        # Registro de sucesso
        logger.info("Metadados e relatório gerados com sucesso.")
    except Exception as exc:
        # Aviso em caso de falha ao salvar
        logger.warning(f"Falha ao salvar metadados/relatório: {exc}")

    # Registro de conclusão
    logger.info(">>> Etapa 4 concluída com sucesso.")

    # Retorno para etapas seguintes
    return {"df": df, "meta": meta, "logger": logger}


# Execução local para validação
if __name__ == "__main__":
    # Executa a preparação do ambiente e captura o resultado
    resultado = preparar_ambiente()

    # Extrai DataFrame e metadados do retorno para inspeção
    df = resultado["df"]
    meta = resultado["meta"]

    # Cabeçalho da seção de preparação no console
    moldura = "-" * 25
    print(f"\n{moldura}>>> 4. Preparação do Projeto <<< {moldura}\n")

    # Exibe dimensões do dataset (linhas e colunas) usando os metadados
    print(f"Linhas: {meta['shape']['linhas']} | Colunas: {meta['shape']['colunas']}")

    # Exibe status da conversão da coluna 'mes' para datetime, se aplicável
    print(f"Conversão 'mes' para datetime: {meta.get('mes_convertido_para_datetime', False)}")
2024-11-16 15:25:34,175 | INFO | pipeline_dados | >>> Iniciando etapa 4: Preparação do Projeto
2024-11-16 15:25:34,183 | INFO | pipeline_dados | Arquivo CSV carregado com sucesso: dados_vendas.csv (linhas=500, colunas=7)
2024-11-16 15:25:34,185 | INFO | pipeline_dados | Estrutura validada: todas as colunas obrigatórias presentes.
2024-11-16 15:25:34,191 | INFO | pipeline_dados | Coluna 'mes' convertida para datetime64[ns].
2024-11-16 15:25:34,193 | INFO | pipeline_dados | Coluna 'vendedor_id' convertida para category.
2024-11-16 15:25:34,197 | INFO | pipeline_dados | Coluna 'produto_id' convertida para category.
2024-11-16 15:25:34,199 | INFO | pipeline_dados | Coluna 'quantidade' convertida para int64.
2024-11-16 15:25:34,201 | INFO | pipeline_dados | Coluna 'preco_unitario' convertida para float64.
2024-11-16 15:25:34,203 | INFO | pipeline_dados | Coluna 'desconto' convertida para float64.
2024-11-16 15:25:34,205 | INFO | pipeline_dados | Coluna 'valor_total' convertida para float64.
2024-11-16 15:25:34,207 | INFO | pipeline_dados | Dtypes após padronização: {'mes': 'datetime64[ns]', 'vendedor_id': 'category', 'produto_id': 'category', 'quantidade': 'int64', 'preco_unitario': 'float64', 'desconto': 'float64', 'valor_total': 'float64'}
2024-11-16 15:25:34,210 | INFO | pipeline_dados | Regras de negócio validadas sem inconsistências.
2024-11-16 15:25:34,212 | INFO | pipeline_dados | Executando diagnóstico dos dados...
2024-11-16 15:25:34,217 | INFO | pipeline_dados | Coluna 'mes' convertida para datetime com sucesso.
2024-11-16 15:25:34,219 | INFO | pipeline_dados | Gerando relatório textual consolidado...
2024-11-16 15:25:34,222 | INFO | pipeline_dados | Relatório textual salvo em logs\relatorio_pipeline.txt
2024-11-16 15:25:34,224 | INFO | pipeline_dados | Metadados e relatório gerados com sucesso.
2024-11-16 15:25:34,225 | INFO | pipeline_dados | >>> Etapa 4 concluída com sucesso.
------------------------->>> 4. Preparação do Projeto <<< -------------------------

Linhas: 500 | Colunas: 7
Conversão 'mes' para datetime: True

5. Metadados do Dataset e Visualização de Arquivos

Esta etapa tem como objetivo gerar e registrar os metadados técnicos do dataset de vendas processado. Esses metadados permitem documentar as principais características da base, como dimensões, tipos de dados e informações de integridade. A saída é exportada em formato CSV e JSON para o diretório de saída configurado no pipeline.

Disponibilizamos também um recurso opcional para leitura e exibição dos arquivos principais do projeto, permitindo visualizar o conteúdo de cada módulo (main.py, config.py, validators.py, diagnostics.py, utils.py).

  • Exportação de metadados técnicos do dataset em CSV (output/metadados/metadados.csv);
  • Salvamento de informações de diagnóstico em JSON (output/pipeline_metadados.json);
  • Geração do relatório textual consolidado (logs/relatorio_pipeline.txt);
  • Visualização opcional dos arquivos de código-fonte do pipeline diretamente no notebook.
In [2]:
# ============================================================
# 5. Metadados do Dataset e Visualização de Arquivos
# ============================================================

def gerar_metadados(df: pd.DataFrame, logger=None) -> pd.DataFrame:
    """
    Etapa 5 - Metadados do Dataset e Visualização de Arquivos.

    - Extrai metadados técnicos do dataset de vendas processado
    - Cria DataFrame com colunas, descrições e tipos de dados
    - Exporta os metadados em CSV no diretório de saída
    - Exibe os metadados no notebook

    Parâmetros:
        df (pd.DataFrame): DataFrame de vendas validado.
        logger (logging.Logger, opcional): Logger configurado do pipeline.

    Retorno:
        pd.DataFrame: DataFrame contendo metadados e descrições de variáveis.
    """
    # Exibe título da etapa
    moldura_texto("5. Metadados do Dataset")

    # Diretório para exportação
    meta_dir = os.path.join(OUTPUT_DIR, "metadados")
    os.makedirs(meta_dir, exist_ok=True)
    meta_csv_path = os.path.join(meta_dir, "metadados.csv")

    # Extração dos metadados técnicos
    meta = diagnostico_dados(df, logger)

    # Dicionário com descrições das variáveis
    descricao_variaveis = {
        "mes": "Data da venda (mês/ano)",
        "vendedor_id": "Identificação do vendedor",
        "produto_id": "Identificação do produto",
        "quantidade": "Quantidade vendida",
        "preco_unitario": "Preço unitário do produto",
        "desconto": "Percentual de desconto aplicado",
        "valor_total": "Valor total da venda (após desconto)",
    }

    # Monta DataFrame com colunas, descrições e tipos
    df_meta = pd.DataFrame({
        "Coluna": list(meta["dtypes"].keys()),
        "Descrição": [descricao_variaveis.get(c, "") for c in meta["dtypes"].keys()],
        "Tipo de Dados": list(meta["dtypes"].values()),
    })

    # Exporta CSV e exibe resumo
    save_dataframe(df_meta, meta_csv_path, index=False)
    if logger:
        logger.info(f"Metadados exportados para {meta_csv_path}")

    display(df_meta)
    return df_meta


def visualizar_arquivos_projeto():
    """
    Exibe, no notebook, o conteúdo dos principais arquivos do pipeline.
    Utiliza janelas modais simples em HTML, sem acionar downloads.
    """
    # Exibe título da etapa
    moldura_texto("Visualização dos Arquivos do Projeto")

    # Função auxiliar para ler e escapar conteúdo de arquivos
    def read_text(path: str) -> str:
        with open(path, "r", encoding="utf-8") as f:
            return html.escape(f.read())

    # Arquivos principais do pipeline
    arquivos = {
        "main.py": "main.py",
        "config.py": "config.py",
        "validators.py": "validators.py",
        "diagnostics.py": "diagnostics.py",
        "utils.py": "utils.py",
    }

    blocks = []
    for nome, caminho in arquivos.items():
        if not os.path.exists(caminho):
            continue

        # Cria ID único para cada modal
        modal_id = str(uuid.uuid4())
        conteudo = read_text(caminho)

        # Estrutura HTML do botão + modal
        block = f"""
        <button onclick="document.getElementById('{modal_id}').style.display='block'"
                style="background-color:#006666;color:white;padding:8px 16px;border:none;
                       border-radius:8px;cursor:pointer;margin:5px;">
            📄 {nome}
        </button>

        <div id="{modal_id}" style="display:none; position:fixed; top:8%; left:10%; width:65%; height:65%;
             background-color:white; border:2px solid #333; box-shadow:0 8px 24px rgba(0,0,0,0.18);
             padding:20px; overflow:auto; z-index:1000;">
          <div style="display:flex; align-items:center; justify-content:space-between;">
            <h3 style="margin:0;">{nome}</h3>
            <button onclick="document.getElementById('{modal_id}').style.display='none'"
                    style="background:#333;color:#fff;border:none;border-radius:6px;padding:6px 12px;cursor:pointer;">
              Fechar
            </button>
          </div>
          <pre style="white-space:pre-wrap; font-family:monospace; font-size:14px;">{conteudo}</pre>
        </div>
        """
        blocks.append(block)

    # Exibe os modais no notebook
    display(HTML("".join(blocks)))


# Execução dentro do notebook
if __name__ == "__main__":
    import logging
    from utils import setup_logging, read_csv_safely
    from validators import run_validations
    from config import INPUT_CSV, LOGS_DIR

    # Configuração do logger
    logger = setup_logging(logs_dir=LOGS_DIR, filename="pipeline.log", level=logging.INFO)

    # Leitura e validação do dataset
    df = read_csv_safely(INPUT_CSV, logger)
    df = run_validations(df, logger)

    # Geração dos metadados e exibição dos arquivos
    gerar_metadados(df, logger)
    visualizar_arquivos_projeto()
2024-11-16 15:25:34,291 | INFO | pipeline_dados | Arquivo CSV carregado com sucesso: dados_vendas.csv (linhas=500, colunas=7)
2024-11-16 15:25:34,292 | INFO | pipeline_dados | Estrutura validada: todas as colunas obrigatórias presentes.
2024-11-16 15:25:34,300 | INFO | pipeline_dados | Coluna 'mes' convertida para datetime64[ns].
2024-11-16 15:25:34,308 | INFO | pipeline_dados | Coluna 'vendedor_id' convertida para category.
2024-11-16 15:25:34,314 | INFO | pipeline_dados | Coluna 'produto_id' convertida para category.
2024-11-16 15:25:34,316 | INFO | pipeline_dados | Coluna 'quantidade' convertida para int64.
2024-11-16 15:25:34,316 | INFO | pipeline_dados | Coluna 'preco_unitario' convertida para float64.
2024-11-16 15:25:34,316 | INFO | pipeline_dados | Coluna 'desconto' convertida para float64.
2024-11-16 15:25:34,316 | INFO | pipeline_dados | Coluna 'valor_total' convertida para float64.
2024-11-16 15:25:34,328 | INFO | pipeline_dados | Dtypes após padronização: {'mes': 'datetime64[ns]', 'vendedor_id': 'category', 'produto_id': 'category', 'quantidade': 'int64', 'preco_unitario': 'float64', 'desconto': 'float64', 'valor_total': 'float64'}
2024-11-16 15:25:34,334 | INFO | pipeline_dados | Regras de negócio validadas sem inconsistências.
2024-11-16 15:25:34,338 | INFO | pipeline_dados | Executando diagnóstico dos dados...
2024-11-16 15:25:34,344 | INFO | pipeline_dados | Coluna 'mes' convertida para datetime com sucesso.
2024-11-16 15:25:34,353 | INFO | pipeline_dados | Metadados exportados para output\metadados\metadados.csv
-------------------->>> 5. Metadados do Dataset <<<--------------------

Coluna Descrição Tipo de Dados
0 mes Data da venda (mês/ano) datetime64[ns]
1 vendedor_id Identificação do vendedor category
2 produto_id Identificação do produto category
3 quantidade Quantidade vendida int64
4 preco_unitario Preço unitário do produto float64
5 desconto Percentual de desconto aplicado float64
6 valor_total Valor total da venda (após desconto) float64
-------------------->>> Visualização dos Arquivos do Projeto <<<--------------------

main.py

"""
Arquivo: main.py

Ponto de entrada do pipeline de engenharia de dados.

Fluxo:
1) Configura logging e diretórios.
2) Lê dados de vendas (com encoding UTF-8).
3) Valida estrutura, padroniza tipos e checa regras de negócio (validators.run_validations).
4) Executa diagnóstico técnico (metadados e integridade).
5) Salva metadados e relatório textual consolidado.

Execução:
    python main.py

Autor: Luciano Magalhães
Novembro 2024
"""

from __future__ import annotations
import logging

from config import (
    INPUT_CSV,
    LOGS_DIR,
    OUTPUT_DIR,
    META_JSON,
    RELATORIO_TXT,
)
from diagnostics import (
    diagnostico_dados,
    gerar_relatorio_texto,
)
from utils import (
    ensure_dirs,
    read_csv_safely,
    save_json,
    setup_logging,
)
from validators import run_validations


def main() -> None:
    """
    Executa o pipeline completo de engenharia de dados para a base de vendas.
    """
    # 1) Logging e diretórios
    logger = setup_logging(
        logs_dir=LOGS_DIR,
        filename="pipeline.log",
        level=logging.INFO,
    )
    ensure_dirs(LOGS_DIR, OUTPUT_DIR)

    # 2) Carregar dados (com encoding UTF-8 garantido em utils.read_csv_safely)
    df = read_csv_safely(INPUT_CSV, logger=logger)

    # 3) Validar e padronizar dataset
    df = run_validations(df, logger=logger)

    # 4) Diagnóstico técnico
    meta = diagnostico_dados(df, logger=logger)
    save_json(meta, META_JSON)

    # 5) Relatório textual consolidado
    gerar_relatorio_texto(
        meta=meta,
        caminho=RELATORIO_TXT,
        logger=logger,
    )

    logger.info("Pipeline concluído com sucesso.")


if __name__ == "__main__":
    main()

config.py

"""
Arquivo config.py

Guardamos caminhos e nomes padrão para entradas e saídas
do pipeline de engenharia de dados.

Autor: Luciano Magalhães
Novembro 2024
"""

INPUT_CSV = "dados_vendas.csv"
LOGS_DIR = "logs"
OUTPUT_DIR = "output"

# Saídas principais do pipeline
RELATORIO_TXT = "logs/relatorio_pipeline.txt"
META_JSON = "output/pipeline_metadados.json"

validators.py

"""
Arquivo: validators.py

Validações e padronizações de estrutura e regras de negócio
para o pipeline de engenharia de dados.

Este módulo executa:
- Validação de colunas obrigatórias;
- Padronização de tipos (datetime, numéricos, categóricos);
- Checagens de consistência semântica (desconto em [0,1], não-negativos);
- Verificação aproximada de valor_total.

Autor: Luciano Magalhães
Novembro 2024
"""
from __future__ import annotations

import logging
from typing import Dict, List

import numpy as np
import pandas as pd


# Colunas obrigatórias (padrão do dataset atual)
REQUIRED_COLS: List[str] = [
    "mes",
    "vendedor_id",
    "produto_id",
    "quantidade",
    "preco_unitario",
    "desconto",
    "valor_total",
]

# Tipos esperados por coluna (padrão do dataset atual)
EXPECTED_TYPES: Dict[str, str] = {
    "mes": "datetime64[ns]",
    "vendedor_id": "category",
    "produto_id": "category",
    "quantidade": "int64",
    "preco_unitario": "float64",
    "desconto": "float64",
    "valor_total": "float64",
}


def validate_structure(
    df: pd.DataFrame,
    required_cols: List[str],
    logger: logging.Logger,
) -> None:
    """
    Valida se colunas obrigatórias existem no DataFrame.
    """
    missing = [c for c in required_cols if c not in df.columns]
    if missing:
        logger.error(f"Colunas ausentes: {missing}")
        raise ValueError(f"Colunas obrigatórias faltando: {missing}")
    logger.info("Estrutura validada: todas as colunas obrigatórias presentes.")


def standardize_types(df: pd.DataFrame, logger: logging.Logger) -> pd.DataFrame:
    """
    Padroniza tipos de colunas conforme EXPECTED_TYPES.
    """
    for col, tipo in EXPECTED_TYPES.items():
        if col not in df.columns:
            logger.warning(f"Coluna esperada '{col}' não encontrada. (ignorada na padronização)")
            continue

        try:
            if "datetime" in tipo:
                df[col] = pd.to_datetime(df[col], errors="coerce")
            elif tipo == "category":
                df[col] = df[col].astype("category")
            else:
                df[col] = df[col].astype(tipo, errors="ignore")
            logger.info(f"Coluna '{col}' convertida para {tipo}.")
        except Exception as e:
            logger.warning(f"Falha ao converter '{col}' para {tipo}: {e}")
  
    logger.info(f"Dtypes após padronização: {df.dtypes.astype(str).to_dict()}")
    return df


def validate_business_rules(df: pd.DataFrame, logger: logging.Logger) -> None:
    """
    Valida regras de negócio básicas:
    - quantidade >= 0
    - preco_unitario >= 0
    - desconto entre 0 e 1
    - valor_total consistente (aproximação)
    """
    issues = []

    if "quantidade" in df.columns and (df["quantidade"] < 0).any():
        issues.append("Há quantidade negativa.")

    if "preco_unitario" in df.columns and (df["preco_unitario"] < 0).any():
        issues.append("Há preço unitário negativo.")

    if "desconto" in df.columns:
        desconto = df["desconto"]
        if (desconto < 0).any() or (desconto > 1).any():
            issues.append("Desconto fora do intervalo [0,1].")

    # Checagem aproximada de valor_total
    needed = {"quantidade", "preco_unitario", "desconto", "valor_total"}
    if needed.issubset(df.columns):
        calc = df["quantidade"].astype(float) * df["preco_unitario"].astype(float) * (1 - df["desconto"].astype(float))
        mask_incons = ~np.isclose(df["valor_total"].astype(float), calc, rtol=1e-03, atol=1e-02)
        if mask_incons.any():
            qtd = int(mask_incons.sum())
            issues.append(f"Valor total inconsistente em {qtd} registros (vs. quantidade, preço e desconto).")
    else:
        faltantes = list(needed - set(df.columns))
        logger.warning(f"Regra de negócio não checada: faltam colunas {faltantes}.")

    if issues:
        for msg in issues:
            logger.warning(f"Regra de negócio: {msg}")
    else:
        logger.info("Regras de negócio validadas sem inconsistências.")


def run_validations(df: pd.DataFrame, logger: logging.Logger) -> pd.DataFrame:
    """
    Pipeline de validação e padronização:
    1) Valida estrutura mínima (REQUIRED_COLS);
    2) Padroniza tipos conforme EXPECTED_TYPES;
    3) Valida regras de negócio.

    Retorno:
        pd.DataFrame: DataFrame pronto para uso no pipeline.
    """
    validate_structure(df, REQUIRED_COLS, logger)
    df = standardize_types(df, logger)
    validate_business_rules(df, logger)
    return df

diagnostics.py

"""
Arquivo diagnostics.py

Módulo de diagnóstico e metadados do pipeline de dados de vendas.
Responsável por:
- Dimensões do dataset
- Tipos de dados
- Valores nulos
- Conversão de colunas críticas (ex.: 'mes' para datetime)

Saídas:
- Metadados (JSON)
- Relatório textual (logs/relatorio_dados.txt)

Autor: Luciano Magalhães
Novembro 2024
"""

from __future__ import annotations
from typing import Dict
import logging
import pandas as pd

# ============================================================
# Diagnóstico básico do dataset
# ============================================================

def diagnostico_dados(df: pd.DataFrame, logger: logging.Logger) -> Dict:
    """
    Retorna metadados básicos do dataset:
    - Dimensões
    - Valores nulos
    - Tipos de dados
    - Conversão da coluna 'mes' para datetime
    """
    logger.info("Executando diagnóstico dos dados...")

    metadados = {
        "shape": {"linhas": df.shape[0], "colunas": df.shape[1]},
        "nulos": df.isnull().sum().to_dict(),
        "dtypes": df.dtypes.astype(str).to_dict(),
        "mes_convertido_para_datetime": False,
    }

    # Conversão de 'mes' para datetime, se existir
    if "mes" in df.columns:
        try:
            df["mes"] = pd.to_datetime(df["mes"], errors="coerce")
            metadados["mes_convertido_para_datetime"] = True
            logger.info("Coluna 'mes' convertida para datetime com sucesso.")
        except Exception as e:
            logger.warning(f"Falha ao converter 'mes' para datetime: {e}")

    return metadados


# ============================================================
# Relatório textual consolidado
# ============================================================

def gerar_relatorio_texto(meta: Dict, caminho: str, logger: logging.Logger) -> None:
    """
    Gera relatório textual consolidado com:
    - Metadados
    - Valores nulos
    - Tipos de dados
    """
    logger.info("Gerando relatório textual consolidado...")

    linhas = []
    linhas.append("=== Relatório de Diagnóstico ===\n")

    linhas.append("Dimensões do dataset:")
    linhas.append(str(meta["shape"]))
    linhas.append("\nValores nulos por variável:")
    linhas.append(str(meta["nulos"]))
    linhas.append("\nTipos de dados:")
    linhas.append(str(meta["dtypes"]))
    linhas.append("\nConversão de 'mes' para datetime:")
    linhas.append(str(meta.get("mes_convertido_para_datetime", False)))
    linhas.append("\n")

    with open(caminho, "w", encoding="utf-8") as f:
        f.write("\n".join(linhas))

    logger.info(f"Relatório textual salvo em {caminho}")

utils.py

"""
Arquivo: utils.py

Utilidades de logging, IO e moldura de texto.

Este módulo inclui funções auxiliares para:
- Configuração de logs com rotação;
- Salvar DataFrames e JSON em diretórios conhecidos;
- Impressão com moldura no console;
- Leitura segura de CSV com validação e logging.

Autor: Luciano Magalhães
Novembro 2024
"""

from __future__ import annotations

import json
import logging
from logging.handlers import RotatingFileHandler
from pathlib import Path
from typing import Any, Dict, Optional

import pandas as pd


# ============================================================
# Logging
# ============================================================

def setup_logging(logs_dir: str, filename: str, level: int = logging.INFO) -> logging.Logger:
    """
    Configura logger com rotação de arquivo, evitando duplicações.
    """
    Path(logs_dir).mkdir(parents=True, exist_ok=True)
    logger = logging.getLogger("pipeline_dados")  # nome atualizado
    logger.setLevel(level)
    logger.propagate = False

    # Limpa handlers antigos para evitar duplicação
    if logger.hasHandlers():
        logger.handlers.clear()

    log_path = Path(logs_dir) / filename
    handler = RotatingFileHandler(
        log_path, maxBytes=2_000_000, backupCount=3, encoding="utf-8"
    )
    formatter = logging.Formatter(
        "%(asctime)s | %(levelname)s | %(name)s | %(message)s"
    )
    handler.setFormatter(formatter)
    logger.addHandler(handler)

    console = logging.StreamHandler()
    console.setFormatter(formatter)
    logger.addHandler(console)

    return logger

# ============================================================
# Utilidades de texto e diretórios
# ============================================================

def moldura_texto(texto: str) -> None:
    """
    Imprime texto com moldura simples no console.
    """
    moldura = "-" * 20
    print(f"\n{moldura}>>> {texto} <<<{moldura}\n")


def ensure_dirs(*dirs: str) -> None:
    """
    Garante que diretórios de saída existem.
    """
    for d in dirs:
        Path(d).mkdir(parents=True, exist_ok=True)


# ============================================================
# IO: DataFrames, JSON e CSV
# ============================================================

def save_dataframe(df: pd.DataFrame, path: str, index: bool = False) -> None:
    """
    Salva DataFrame em CSV com encoding UTF-8.
    """
    Path(path).parent.mkdir(parents=True, exist_ok=True)
    df.to_csv(path, index=index, encoding="utf-8")


def save_json(data: Dict[str, Any], path: str) -> None:
    """
    Salva dicionário em JSON com encoding UTF-8.
    """
    Path(path).parent.mkdir(parents=True, exist_ok=True)
    with open(path, "w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False, indent=2)


def read_csv_safely(path: str, logger: Optional[logging.Logger] = None) -> pd.DataFrame:
    """
    Lê CSV com validação de existência, encoding UTF-8 e logging.
    """
    if not Path(path).exists():
        msg = f"Arquivo não encontrado: {path}"
        if logger:
            logger.error(msg)
        raise FileNotFoundError(msg)

    try:
        df = pd.read_csv(path, encoding="utf-8")
        if logger:
            logger.info(
                f"Arquivo CSV carregado com sucesso: {path} (linhas={df.shape[0]}, colunas={df.shape[1]})"
            )
        return df

    except pd.errors.EmptyDataError:
        msg = f"O arquivo {path} está vazio."
        if logger:
            logger.error(msg)
        raise ValueError(msg)

    except pd.errors.ParserError as e:
        msg = f"Erro ao ler o CSV {path}: {e}"
        if logger:
            logger.error(msg)
        raise ValueError(msg)

    except Exception as e:
        msg = f"Erro inesperado ao carregar {path}: {e}"
        if logger:
            logger.error(msg)
        raise RuntimeError(msg)

6. Qualidade e integridade dos dados

Nesta etapa, o pipeline avalia a qualidade e integridade do dataset de vendas. São verificadas dimensões, tipos de dados, valores nulos e consistência de colunas críticas, como mes. Esse processo garante que os dados estejam prontos para análises posteriores, reforçando a confiabilidade do fluxo.

In [3]:
# Item 6 - Qualidade e integridade dos dados

# Configuração do logger
logger = logging.getLogger("diagnostics")
logger.setLevel(logging.INFO)

# Validar e padronizar dataset
df = run_validations(df, logger)

# Exibir moldura
moldura_texto("6. Qualidade e integridade dos dados")

# Executar análise de qualidade e integridade usando função do módulo
metadados = diagnostico_dados(df, logger)

# Exibir dimensões
print("\nDimensões do dataset")
display(pd.DataFrame([metadados["shape"]]))

# Exibir valores nulos
print("\nValores nulos por variável")
display(pd.DataFrame(list(metadados["nulos"].items()), columns=["Variável", "Nulos"]))

# Exibir tipos de dados
print("\nTipos de dados por variável")
display(pd.DataFrame(list(metadados["dtypes"].items()), columns=["Variável", "Tipo de Dados"]))

# Exibir status da conversão de 'mes'
print("\nConversão da coluna 'mes' para datetime")
print(metadados["mes_convertido_para_datetime"])

print("\n")

# Exibir primeiras linhas do dataset validado
df.head()
-------------------->>> 6. Qualidade e integridade dos dados <<<--------------------


Dimensões do dataset
linhas colunas
0 500 7
Valores nulos por variável
Variável Nulos
0 mes 0
1 vendedor_id 0
2 produto_id 0
3 quantidade 0
4 preco_unitario 0
5 desconto 0
6 valor_total 0
Tipos de dados por variável
Variável Tipo de Dados
0 mes datetime64[ns]
1 vendedor_id category
2 produto_id category
3 quantidade int64
4 preco_unitario float64
5 desconto float64
6 valor_total float64
Conversão da coluna 'mes' para datetime
True


Out[3]:
mes vendedor_id produto_id quantidade preco_unitario desconto valor_total
0 2023-07-31 14 7 68 884.87 0.06 56560.89
1 2023-04-30 5 8 12 447.60 0.26 3974.69
2 2023-11-30 12 42 59 337.58 0.20 15933.78
3 2023-08-31 16 15 37 454.29 0.09 15295.94
4 2023-05-31 16 47 61 672.70 0.22 32007.07

© Copyright 2025 | Luciano Magalhães