Python Code Style Review — cultiva-lead-scorer

lead_scorer.py  ·  Módulo de puntuación de leads MLOps  ·  Revisado por Skill estilo-codigo-python
● 5 errores críticos ● 4 avisos ● 3 mejoras
34
Calidad
5
Errores criticos
4
Avisos PEP 8
3
Mejoras de estilo
0
Tests cubren types
🔍
Problemas Detectados (12 total)
Linea Categoria Severidad Descripcion Correccion
L6 Imports ● alta Imports separados por coma: import os, sys Un import por línea; eliminar si no se usan (F401)
L7 Typing ● alta from typing import * — wildcard import prohibido Importar sólo lo necesario: from collections.abc import Callable
L9 Naming ● alta API_url — mezcla camelCase y UPPER; constante debe ser SCREAMING_SNAKE_CASE Renombrar a API_BASE_URL
L10 Naming ● alta maxRetries — camelCase para constante de módulo Renombrar a MAX_RETRIES
L12 Naming ● alta class lead_model — clase no sigue PascalCase Renombrar a class LeadScoringModel
L13 Types ● media Constructor sin type annotations en parámetros ni retorno Añadir mdl_path: Path, thresh: float = 0.5None
L20 Imports ● media import pickle dentro de función — import tardío Mover al bloque de imports al inicio del módulo
L24 Builtins ● media if self.mdl == None — comparar con None usando == (E711) Usar if self.mdl is None
L25 Exceptions ● media raise Exception("...") — excepción genérica Crear class ModelNotLoadedError(RuntimeError) o usar RuntimeError
L26-27 Types ● baja Método score sin type hints en parámetros/retorno Anotar con pd.DataFrame, bool, np.ndarray
L27 Style ● baja if ret_proba == True — comparación redundante con booleano (E712) Simplificar a if ret_proba:
Todos Docstrings ● baja Ninguna función ni clase tiene docstring Añadir Google-style docstrings a todos los métodos públicos
Antes → Despues: lead_scorer.py
✗ ANTES — lead_scorer.py (original) 34/100
# lead_scorer.py
import pandas as pd
import numpy as np
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
import os, sys   # ❌ import múltiple en una línea
from typing import *   # ❌ wildcard import

API_url = "https://api.cultivaia.com/leads"   # ❌ naming
maxRetries = 3   # ❌ camelCase en constante

class lead_model:   # ❌ snake_case en clase
    def __init__(self, mdl_path, thresh=0.5):
        # ❌ sin type annotations
        self.mdl_path = mdl_path
        self.thresh = thresh
        self.mdl = None

    def load(self):
        import pickle   # ❌ import tardío
        with open(self.mdl_path, 'rb') as f:
            self.mdl = pickle.load(f)

    def score(self, X, ret_proba=False):
        if self.mdl == None:   # ❌ usar is None
            raise Exception("Model not loaded")  # ❌ genérico
        p = self.mdl.predict_proba(X)[:, 1]
        if ret_proba == True:   # ❌ comparación redundante
            return p
        return (p >= self.thresh).astype(int)

def process_leads(leads_list, output_p, model, skip_err=False):
    # ❌ sin docstring, sin type hints
    res = []
    for l in leads_list:
        try:
            score = model.score(pd.DataFrame([l]))
            res.append({'id': l['id'], 'score': int(score[0])})
        except Exception as e:
            if skip_err == False:   # ❌
                raise e
    pd.DataFrame(res).to_csv(output_p, index=False)
    return res
✓ DESPUES — lead_scorer.py (corregido) 97/100
"""Módulo de puntuación de leads para CULTIVA IA.

Integra modelos de ML para clasificar leads de marketing
en tiempo real con soporte de procesamiento por lotes.
"""
# Stdlib
import pickle   # ✓ import al inicio
from pathlib import Path
from collections.abc import Callable

# Terceros
import numpy as np
import pandas as pd

API_BASE_URL = "https://api.cultivaia.com/leads"   # ✓
MAX_RETRIES = 3   # ✓ SCREAMING_SNAKE_CASE


class ModelNotLoadedError(RuntimeError):   # ✓ excepción específica
    """Se lanza al intentar usar el modelo antes de cargarlo."""


class LeadScoringModel:   # ✓ PascalCase
    """Wrapper de modelo ML para puntuación de leads en producción.

    Attributes:
        model_path: Ruta al archivo pickle del modelo entrenado.
        threshold: Umbral de decisión para clasificación binaria.
    """

    def __init__(self, model_path: Path, threshold: float = 0.5) -> None:
        """Inicializa el modelo de puntuación.

        Args:
            model_path: Ruta al .pkl del modelo entrenado.
            threshold: Probabilidad mínima para score=1. Default 0.5.
        """
        self.model_path = model_path
        self.threshold = threshold
        self._model: object | None = None

    def load(self) -> None:
        """Carga el modelo desde disco."""
        with open(self.model_path, "rb") as f:
            self._model = pickle.load(f)

    def score(self, features: pd.DataFrame, return_proba: bool = False) -> np.ndarray:
        """Puntúa un DataFrame de features.

        Args:
            features: DataFrame con las features del lead.
            return_proba: Si True, retorna probabilidades crudas.

        Returns:
            Array de probabilidades (float) o etiquetas binarias (int).

        Raises:
            ModelNotLoadedError: Si no se ha llamado a load() antes.
        """
        if self._model is None:   # ✓ is None
            raise ModelNotLoadedError("Llama a load() antes de score()")  # ✓
        probas = self._model.predict_proba(features)[:, 1]
        if return_proba:   # ✓ sin == True
            return probas
        return (probas >= self.threshold).astype(int)


def process_leads(   # ✓ con type hints y docstring
    leads: list[dict],
    output_path: Path,
    model: LeadScoringModel,
    on_error: Callable[[Exception], None] | None = None,
) -> list[dict]:
    """Puntúa un lote de leads y los guarda en CSV.

    Args:
        leads: Lista de dicts con datos de cada lead (requiere 'id').
        output_path: Ruta de destino del CSV de resultados.
        model: Instancia de LeadScoringModel ya cargada.
        on_error: Callback opcional para errores por lead. Si None,
            los errores se propagan.

    Returns:
        Lista de dicts {id, score} con los resultados.

    Raises:
        ModelNotLoadedError: Si el modelo no ha sido cargado.
    """
    results: list[dict] = []
    for lead in leads:
        try:
            score = model.score(pd.DataFrame([lead]))
            results.append({"id": lead["id"], "score": int(score[0])})
        except Exception as exc:
            if on_error is not None:   # ✓ callback vs flag booleano
                on_error(exc)
            else:
                raise
    pd.DataFrame(results).to_csv(output_path, index=False)
    return results
⚙️
Configuracion Recomendada — pyproject.toml
cultiva-lead-scorer / pyproject.toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "cultiva-lead-scorer"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
    "pandas>=2.2",
    "numpy>=1.26",
    "scikit-learn>=1.4",
]

[project.optional-dependencies]
dev = ["ruff", "mypy", "pytest", "pandas-stubs"]

[tool.ruff]
line-length = 120
target-version = "py312"

[tool.ruff.lint]
select = [
    "E",   # pycodestyle errors
    "W",   # pycodestyle warnings
    "F",   # pyflakes
    "I",   # isort
    "B",   # flake8-bugbear
    "C4", # comprehensions
    "UP", # pyupgrade
    "SIM",# simplify
]
ignore = ["E501"]  # line length handled by formatter

[tool.ruff.format]
quote-style = "double"
indent-style = "space"

[tool.mypy]
python_version = "3.12"
strict = true
warn_return_any = true
warn_unused_ignores = true
disallow_untyped_defs = true
disallow_incomplete_defs = true

[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false
Checklist de Buenas Practicas — Estado del Proyecto