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.
Índice del expediente
- Tesis de negocio
- Datos y contrato de información
- Diseño metodológico
- Implementación reproducible
- Resultados: los tres ejemplos
- Arquitectura de solución
- Gobierno y operación
- 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:
| Campo | Tipo | Descripción |
|---|---|---|
categoria | Texto | Nombre de la causa o categoría — debe ser único por fila, no se agrupan duplicados automáticamente. |
valor | Numérico | La 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
--umbralcon 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.
3 de 10 categorías (30% del total de categorías) concentran 83.9% de devoluciones.
3 de 9 categorías (33.3% del total de categorías) concentran 82.7% de defectos.
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:
- devoluciones_retail.csv — dataset de entrada
- defectos_manufactura.csv — dataset de entrada
- ventas_skus.csv — dataset de entrada
- devoluciones_retail.json — resultado real del análisis
- defectos_manufactura.json — resultado real del análisis
- ventas_skus.json — resultado real del análisis
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:
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
--umbralde 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
assertactual 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:
¿Quieres ver los demás proyectos o leer la nota que ancla éste?