← ProyectosProyecto independiente · reproducible

Analizador de Pareto

Una herramienta genérica — no un ejemplo de un solo uso — que convierte cualquier CSV de causas o categorías en un diagrama de Pareto completo, con código Python reproducible.

La nota El principio de Pareto: la regla del 80/20 mostró un script hecho a la medida de un solo ejemplo (devoluciones en retail). Este proyecto generaliza ese script en una herramienta reutilizable: el mismo código, sin modificar una sola línea, produce el análisis completo para tres datasets distintos — declarados como ilustrativos, no datos de ninguna empresa real — y para cualquier CSV nuevo que alguien le pase.

En una mirada
3Datasets de ejemplo corridos con el mismo script, sin cambiar códigodevoluciones, defectos, ventas por SKU
30-33%% de categorías que concentran ≥80% del total, en los tres ejemplosnunca exactamente 20% — el matiz central de la nota
3/10Causas que concentran 83.9% de las devolucionesejemplo 1 de 3, ver sección 05
80%Umbral configurable del corte de "pocos vitales"parámetro del CLI, no un valor fijo en el código

Índice del expediente

  1. Tesis de negocio
  2. Datos y contrato de información
  3. Diseño metodológico
  4. Implementación reproducible
  5. Resultados: los tres ejemplos
  6. Arquitectura de solución
  7. Gobierno y operación
  8. Límites y siguientes pasos

01 · Tesis de negocio

Cada vez que alguien necesita priorizar un conjunto de causas —por qué se devuelve un producto, por qué falla una pieza, qué productos realmente mueven las ventas—, la tentación es escribir un script específico para ese caso, usarlo una vez, y descartarlo. El resultado es que el mismo análisis se reescribe de cero cada vez que aparece un conjunto de causas nuevo, en vez de construir una sola vez una herramienta que sirva para cualquiera de ellos.

Pregunta de negocio: ¿puede un único script, sin modificaciones, producir el análisis de Pareto correcto para cualquier dataset de causas que alguien le entregue — sin importar si mide devoluciones, defectos o ingresos?

Cómo se mide el éxito: que el mismo comando, cambiando solo el archivo de entrada y el título, produzca resultados correctos y una gráfica legible para datasets de naturaleza completamente distinta — la prueba está en la sección 05, donde se corre tres veces sin tocar el código.

02 · Datos y contrato de información

La herramienta no asume nada sobre qué mide "valor" — puede ser conteo de devoluciones, número de defectos, o ingresos en pesos. Lo único que exige es el contrato mínimo de dos columnas:

CampoTipoDescripción
categoriaTextoNombre de la causa o categoría — debe ser único por fila, no se agrupan duplicados automáticamente.
valorNuméricoLa magnitud de esa categoría, en la unidad que se esté midiendo (conteo, costo, ingresos) — el álgebra del método es la misma sin importar la unidad.

Los tres datasets de ejemplo — ilustrativos, declarados como tales

  • Devoluciones en retail (10 categorías, 690 devoluciones): el mismo ejemplo de la nota que ancla este proyecto.
  • Defectos de manufactura (9 categorías, 456 defectos): el contexto clásico de control de calidad donde Joseph Juran aplicó por primera vez la idea de "pocos vitales".
  • Ventas por SKU (20 productos, $1,581,000 MXN de ingresos simulados): el caso clásico de inventario — qué productos realmente mueven la caja registradora.

Ninguno de los tres es de una empresa real — son datasets sintéticos construidos para este proyecto, con el único propósito de demostrar que la herramienta generaliza.

03 · Diseño metodológico

El método central —ordenar de mayor a menor, calcular el % de cada categoría sobre el total, y acumular ese porcentaje en ese mismo orden— es el mismo descrito en la nota del blog. La diferencia de diseño de este proyecto está en dos decisiones deliberadas:

  • Separar el cálculo de la graficación: la función analizar() no sabe nada de matplotlib ni de colores — recibe un DataFrame y un umbral, y devuelve números. Eso permite probar la lógica del método sin necesidad de generar una imagen, y reutilizarla en cualquier otro contexto (una API, un notebook, un job programado).
  • El umbral de "pocos vitales" es un parámetro, no una constante: el 80% es el valor por defecto porque es la heurística más citada, pero el CLI acepta --umbral con cualquier valor — la nota del blog es explícita en que el 80/20 es una observación empírica, no una ley exacta, y la herramienta debe reflejar ese matiz en su diseño, no solo en el texto.

04 · Implementación reproducible

El script completo, tal como está en el repositorio. Se ejecuta igual para cualquiera de los tres ejemplos, cambiando solo los tres argumentos de entrada:

python3 analizador_pareto.py datasets/devoluciones_retail.csv \
    salidas/devoluciones_retail "Causas de devolución — tienda retail" \
    --umbral 80 --etiqueta-valor "devoluciones"

python3 analizador_pareto.py datasets/defectos_manufactura.csv \
    salidas/defectos_manufactura "Causas de defecto — línea de manufactura" \
    --umbral 80 --etiqueta-valor "defectos"

python3 analizador_pareto.py datasets/ventas_skus.csv \
    salidas/ventas_skus "Ventas por SKU — tienda retail (ingresos MXN)" \
    --umbral 80 --etiqueta-valor "ingresos MXN"
#!/usr/bin/env python3
"""
Analizador de Pareto genérico y reutilizable.

Recibe CUALQUIER dataset de causas/categorías en CSV (dos columnas:
"categoria" y "valor" — frecuencia, costo, unidades, lo que se esté
midiendo) y produce, sin tocar código:

  1. La clasificación ordenada de mayor a menor.
  2. El porcentaje de cada categoría sobre el total.
  3. El porcentaje acumulado.
  4. El corte de "pocos vitales": cuántas categorías (y qué % del total
     de categorías) concentran al menos `--umbral` % del total (80% por
     defecto, pero configurable — el 80/20 es una heurística común, no
     una ley fija).
  5. El diagrama de Pareto (barras + línea acumulada) como PNG.
  6. Un JSON con todos los resultados, para consumir desde cualquier
     otro sistema (incluida esta misma página web).
"""
import argparse
import json
import os

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd


def analizar(df: pd.DataFrame, umbral: float):
    """Core del método: ordenar, calcular %, acumular, encontrar el corte.
    No depende de qué represente `valor` — el álgebra es la misma sea
    cual sea la unidad."""
    df = df.sort_values("valor", ascending=False).reset_index(drop=True)
    total = df["valor"].sum()
    df["pct"] = df["valor"] / total * 100
    df["acumulado"] = df["pct"].cumsum()

    # Corte de "pocos vitales": primera categoría donde el acumulado
    # alcanza o supera el umbral.
    corte_idx = int(np.argmax(df["acumulado"].values >= umbral))
    n_corte = corte_idx + 1

    return df, {
        "total": float(total),
        "n_categorias": int(len(df)),
        "umbral_pct": float(umbral),
        "n_categorias_corte": n_corte,
        "pct_categorias_corte": round(n_corte / len(df) * 100, 1),
        "pct_valor_cubierto_en_corte": round(float(df["acumulado"].iloc[corte_idx]), 1),
    }


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("entrada_csv")
    ap.add_argument("prefijo_salida")
    ap.add_argument("titulo")
    ap.add_argument("--umbral", type=float, default=80.0)
    ap.add_argument("--etiqueta-valor", default="valor")
    args = ap.parse_args()

    df_crudo = pd.read_csv(args.entrada_csv)
    assert set(["categoria", "valor"]).issubset(df_crudo.columns)

    df, resumen = analizar(df_crudo, args.umbral)
    os.makedirs(os.path.dirname(args.prefijo_salida) or ".", exist_ok=True)
    df.to_csv(f"{args.prefijo_salida}.csv", index=False)

    salida_json = {
        "titulo": args.titulo,
        "etiqueta_valor": args.etiqueta_valor,
        **resumen,
        "categorias": df["categoria"].tolist(),
        "valores": [float(v) for v in df["valor"].tolist()],
        "pct": [round(float(v), 2) for v in df["pct"].tolist()],
        "acumulado": [round(float(v), 2) for v in df["acumulado"].tolist()],
    }
    with open(f"{args.prefijo_salida}.json", "w") as f:
        json.dump(salida_json, f, indent=2, ensure_ascii=False)

    graficar(df, resumen, args.titulo, args.etiqueta_valor, f"{args.prefijo_salida}.png")
    print(json.dumps(salida_json, indent=2, ensure_ascii=False))


if __name__ == "__main__":
    main()

(La función graficar() completa —que genera el PNG con matplotlib, igual estilo que la gráfica de la nota del blog— está en el archivo completo del repositorio; se omite aquí por espacio, el enlace está en la sección "Código fuente completo" al final.)

05 · Resultados: los tres ejemplos

El mismo script, sin cambiar una sola línea de código, corrido tres veces sobre tres datasets de naturaleza distinta. Cada resultado es la salida real de esa corrida —el JSON y el PNG están disponibles para descarga al final de esta sección.

Causas de devolución — tienda retailPocos vitales (3 de 10 = 83.9%)090179269359devoluciones80%Producto dañadoTalla o modelo equivocadoNo cumplió expectativasCambio de opiniónError de precio o cobroEmpaque dañadoProducto incompletoDemora en la entregaColor o variante distintaOtras causas

3 de 10 categorías (30% del total de categorías) concentran 83.9% de devoluciones.

Causas de defecto — línea de manufacturaPocos vitales (3 de 9 = 82.7%)053106159212defectos80%Soldadura defectuosaMaterial fuera de especi…Error de ensamble manualDesajuste de maquinariaFalla de sensor de controlContaminación en líneaEmpaque final dañadoEtiquetado incorrectoOtras causas

3 de 9 categorías (33.3% del total de categorías) concentran 82.7% de defectos.

Ventas por SKU — tienda retail (ingresos MXN)Pocos vitales (6 de 20 = 84.2%)0120,175240,350360,525480,700ingresos MXN80%SKU-104 Refresco cola 60…SKU-087 Agua embotellada…SKU-212 Botana papas salSKU-019 Cerveza clara lataSKU-305 Pan de caja blancoSKU-150 Leche entera 1LSKU-044 Detergente líquidoSKU-233 Café soluble 200gSKU-078 Cereal de maízSKU-191 Papel higiénico …SKU-260 Jabón de barraSKU-012 Shampoo 400mlSKU-134 Galletas dulcesSKU-299 Salsa picanteSKU-067 Atún en lataSKU-221 Chocolate en barraSKU-310 ServilletasSKU-055 Especias surtidasSKU-178 Hilo dentalSKU-340 Otros SKUs de ba…

6 de 20 categorías (30% del total de categorías) concentran 84.2% de ingresos MXN.

Los tres ejemplos coinciden en algo que la nota del blog señala como el matiz más importante del principio: el corte de "pocos vitales" ronda el 30-33% de las categorías, no el 20% que sugiere el nombre popular de la regla. La proporción 80/20 es una aproximación conveniente, no un resultado garantizado — y esta herramienta lo hace visible en cada corrida, en vez de asumirlo.

Evidencia descargable

Los datasets de entrada y los archivos de salida reales (CSV, JSON y PNG) de los tres ejemplos:

06 · Arquitectura de solución

Así se integraría esta herramienta en un flujo real de operación continua, en vez de correrse una sola vez a mano:

registros crudosCSV categoria,valortabla ordenada + % acumuladopocas causas, mucho impactose vuelve a correr periódicamente — el corte de "pocos vitales" puede cambiar con datos nuevosFuente de datosERP, ticket de calidad, POSExportación periódicaCSV: categoria, valorAnalizador de Paretoclasifica, ordena, acumula, cortaGráfica + JSONdiagrama y corte de pocos vitalesDecisión de prioridaddueño del proceso revisa y actúa

Una fuente de datos existente (el ERP, el sistema de tickets de calidad, el punto de venta) exporta periódicamente un CSV con el contrato mínimo de dos columnas. El analizador corre sobre ese archivo —sin intervención manual— y publica la gráfica y el JSON de resultados, que el dueño del proceso revisa para decidir dónde enfocar el esfuerzo. La decisión que se tome (corregir una causa, descontinuar un SKU) cambia el proceso de origen, lo que altera la siguiente exportación — el análisis se vuelve a correr periódicamente, nunca una sola vez.

07 · Gobierno y operación

Para que esta herramienta fuera mantenible como parte de un proceso real, no solo como un script de demostración, necesitaría:

  • Versionado del umbral: si alguien cambia --umbral de 80% a 90%, esa decisión debe quedar registrada junto con el resultado — un corte distinto no es un error, pero debe ser explícito y trazable.
  • Validación del contrato de datos: el assert actual confirma que existan las columnas correctas; en producción necesitaría además validar tipos, valores negativos y categorías duplicadas antes de analizar.
  • Separación de quien corre el análisis y quien decide: la herramienta solo prioriza — la decisión de qué hacer con esa priorización (y el riesgo de ignorar la cola larga) le corresponde al dueño del proceso, no al script.
  • Historial de corridas: guardar cada JSON de salida con fecha, para poder comparar cómo cambia el corte de "pocos vitales" en el tiempo, no solo ver la foto de una corrida aislada.

08 · Límites y siguientes pasos

La herramienta asume que las categorías son mutuamente excluyentes y que cada observación pertenece a una sola — si la clasificación de origen es ambigua (una devolución con dos causas válidas), el análisis hereda ese problema sin poder corregirlo. Tampoco distingue entre categorías de magnitud similar: si las primeras cuatro categorías tienen un peso casi idéntico, el "corte" que marca la herramienta es técnicamente correcto pero menos útil como guía de priorización — ahí el criterio humano importa más que el número exacto del corte. Extensiones razonables, no implementadas aquí: comparar el mismo análisis entre dos periodos para detectar si las causas dominantes están cambiando, y una versión que reciba el CSV por una interfaz web en vez de la línea de comandos.

Código fuente completo

El script completo (incluida la función graficar()), los tres datasets y los archivos de resultados que alimentan esta página están en el repositorio del sitio:

Ver código en GitHub →

¿Quieres ver los demás proyectos o leer la nota que ancla éste?