A previous release had provenance metadata, but this release does not. View details.
walopy
Teoría de colas, análisis de operaciones, modelos de inventario, árboles de KPI y más para Python.
🇬🇧 English version of this document
pip install walopy
Tabla de contenidos
- Modelos de colas clásicos
- Modelos avanzados y de capacidad finita
- Colas con prioridad
- Simulación Monte Carlo G/G/1
- Ajuste de parámetros desde datos reales
- Redes de Jackson
- Inventarios
- Análisis de operaciones
- Análisis de cuellos de botella
- Balance de línea y tiempo takt
- Análisis de punto de equilibrio
- Curvas de intercambio
- Programación de producción (scheduling)
- Confiabilidad
- Proyectos: CPM y PERT
- Árboles de KPI
- Solvers y optimización
- Análisis de sensibilidad
- Escenarios en lote (batch_model)
- Comparar múltiples resultados
- Gráficas
- CLI
- Referencia de módulos
- Idioma de los textos
- Requisitos
1. Modelos de colas clásicos
Todos los modelos devuelven un QueueResult con los indicadores estándar de teoría de colas.
QueueResult — atributos principales
| Atributo | Descripción |
|---|---|
model |
Nombre del modelo (ej. "M/M/1") |
lam |
Tasa de llegada λ |
mu |
Tasa de servicio μ por servidor |
servers |
Número de servidores c |
rho |
Utilización por servidor ρ = λ/(c·μ) |
L |
Número promedio de unidades en el sistema |
Lq |
Número promedio de unidades en la cola |
W |
Tiempo promedio en el sistema |
Wq |
Tiempo promedio de espera en cola |
params |
Parámetros adicionales específicos del modelo |
Métodos disponibles en todos los resultados:
.summary()/print(r)— tabla de indicadores.to_frame()— exporta apd.DataFramede una fila.plot()— gráfica interactiva de sensibilidad
mm1(lam, mu) — M/M/1
Un servidor, llegadas Poisson, servicio exponencial. El modelo más simple de cola.
Fórmulas clave:
- ρ = λ/μ (debe ser < 1 para estabilidad)
- Lq = ρ² / (1 − ρ)
- L = ρ / (1 − ρ)
- Wq = Lq / λ, W = L / λ
import walopy as wl
r = wl.mm1(lam=3.0, mu=5.0)
print(r)
# Modelo : M/M/1
# λ : 3.0 (tasa de llegada)
# μ : 5.0 (tasa de servicio por servidor)
# ρ : 0.6 (utilización por servidor)
# L : 1.5 (unidades promedio en el sistema)
# Lq : 0.9 (unidades promedio en cola)
# W : 0.5 (tiempo promedio en el sistema)
# Wq : 0.3 (tiempo promedio de espera en cola)
df = r.to_frame() # exportar a DataFrame
r.plot() # gráfica Wq y Lq vs ρ
mmc(lam, mu, c) — M/M/c
Multi-servidor con llegadas Poisson y servicio exponencial. Usa la fórmula de Erlang-C para calcular la probabilidad de espera C(c, a).
Fórmulas:
- ρ = λ/(c·μ) (utilización por servidor, debe ser < 1)
- a = λ/μ (carga ofrecida)
- C(c,a) = P(esperar) — fórmula de Erlang-C
- Lq = C(c,a) · ρ / (1 − ρ)
r = wl.mmc(lam=8.0, mu=5.0, c=2)
print(r.params["C(c,a) Erlang-C"]) # probabilidad de espera
r_1 = wl.mmc(lam=4.0, mu=5.0, c=1) # equivalente a mm1
r_3 = wl.mmc(lam=4.0, mu=5.0, c=3)
print(r_3.Wq < r_1.Wq) # True: más servidores → menor espera
md1(lam, mu) — M/D/1
Llegadas Poisson, servicio determinístico (tiempo de servicio constante = 1/μ). Produce exactamente la mitad de cola que M/M/1 bajo la misma carga.
Fórmulas:
- Lq = ρ² / (2·(1 − ρ)) ← mitad que M/M/1
- L = ρ + Lq
r_mm1 = wl.mm1(lam=3.0, mu=5.0)
r_md1 = wl.md1(lam=3.0, mu=5.0)
print(r_md1.Lq / r_mm1.Lq) # ≈ 0.5 — M/D/1 tiene la mitad de cola
kingman(lam, mu, ca2, cs2) — G/G/1 (aproximación VUT)
Modelo general G/G/1 mediante la aproximación de Kingman (también llamada VUT o fórmula de Pollaczek). Acepta cualquier distribución de llegadas y servicio a través de sus coeficientes de variación al cuadrado.
Fórmula:
Wq ≈ (ρ / (1 − ρ)) · ((ca² + cs²) / 2) · (1/μ)
| Parámetro | Descripción |
|---|---|
ca2 |
CV² de los tiempos entre llegadas (1.0 para Poisson) |
cs2 |
CV² de los tiempos de servicio (1.0 exponencial, 0.0 determinístico) |
# Proceso de llegada más regular que Poisson (ca2 < 1)
r = wl.kingman(lam=3.0, mu=5.0, ca2=0.5, cs2=1.0)
# Servicio muy variable (ca2 >> 1)
r = wl.kingman(lam=3.0, mu=5.0, ca2=1.0, cs2=3.0)
# Recuperar parámetros ajustados desde datos reales y usarlos directamente
import numpy as np
rng = np.random.default_rng(42)
fit = wl.fit_from_data(inter_arrivals=rng.exponential(0.2, 500), service_times=rng.exponential(0.1, 500))
r = wl.kingman(**fit.to_model_kwargs())
Equivalencias:
ca2=1, cs2=1→ M/M/1
ca2=1, cs2=0→ M/D/1 (aproximado)
mg1(lam, mu, cs2) — M/G/1 (Pollaczek-Khinchine exacto)
Llegadas Poisson, servicio de distribución arbitraria parametrizado por su media (1/μ) y su variabilidad (cs²). La fórmula P-K es exacta, no una aproximación.
Fórmula P-K:
Wq = λ · E[S²] / (2 · (1 − ρ)), donde E[S²] = (1 + cs²) / μ²
# Servicio con distribución triangular: calcular cs² primero
cs2 = wl.cv2_triangular(a=2, m=5, b=10) # a=mín, m=moda, b=máx
r = wl.mg1(lam=0.15, mu=1/5, cs2=cs2)
# Equivalencia con M/M/1 (cs²=1) y M/D/1 (cs²=0)
r_mm1 = wl.mg1(lam=3.0, mu=5.0, cs2=1.0) # idéntico a mm1(3, 5)
r_md1 = wl.mg1(lam=3.0, mu=5.0, cs2=0.0) # idéntico a md1(3, 5)
Helpers cv2_* — CV² por distribución
El CV² (varianza / media²) es el único parámetro de distribución que M/G/1 necesita:
| Función | Distribución | CV² |
|---|---|---|
cv2_normal(mean, std) |
Normal | (σ/μ)² |
cv2_triangular(a, m, b) |
Triangular | fórmula exacta |
cv2_uniform(a, b) |
Uniforme | (b−a)² / (12·μ²) |
cv2_erlang(k) |
Erlang-k | 1/k |
cv2_gamma(shape) |
Gamma | 1/shape |
cv2_lognormal(mean, std) |
Lognormal | (σ/μ)² |
cv2_weibull(shape) |
Weibull | Γ(1+2/k)/Γ(1+1/k)²−1 |
# Servicio con distribución Weibull de forma 2 (rayleigh)
cs2 = wl.cv2_weibull(shape=2.0) # ≈ 0.273
r = wl.mg1(lam=1.0, mu=2.0, cs2=cs2)
print(r.Wq) # tiempo medio de espera en cola
# Servicio Erlang-3 (menos variable que exponencial)
r = wl.mg1(lam=3.0, mu=5.0, cs2=wl.cv2_erlang(3))
littles_law(*, L, lam, W) — Ley de Little
Resuelve L = λ · W para la variable que falte. Exactamente uno de los tres parámetros debe ser None.
L = wl.littles_law(lam=5.0, W=0.4) # L = 2.0
W = wl.littles_law(L=2.0, lam=5.0) # W = 0.4
lam = wl.littles_law(L=3.0, W=0.6) # λ = 5.0
2. Modelos avanzados y de capacidad finita
mm1k(lam, mu, K) — M/M/1/K
Cola con capacidad finita K (servidor + sala de espera). Los clientes que llegan cuando el sistema está lleno se pierden. El sistema es siempre estable incluso si λ > μ.
Parámetros adicionales en result.params:
PK (blocking prob)— probabilidad de rechazo (sistema lleno)λ_eff (effective rate)— tasa de llegada efectiva = λ · (1 − P_K)
r = wl.mm1k(lam=5.0, mu=3.0, K=10)
print(r.params["PK (prob. de bloqueo)"]) # fracción de clientes rechazados
print(r.params["λ_eff (tasa efectiva)"]) # tasa real de entrada al sistema
mmck(lam, mu, c, K) — M/M/c/K
Multi-servidor con capacidad finita. Generaliza tanto M/M/c (K→∞) como M/M/1/K (c=1). Las probabilidades de estado se calculan en espacio logarítmico para evitar desbordamientos con K grande.
r = wl.mmck(lam=8.0, mu=3.0, c=2, K=20)
print(r.params["PK (prob. de bloqueo)"])
# Verificación: con K muy grande se acerca a M/M/c
r_inf = wl.mmc(lam=3.0, mu=5.0, c=2)
r_fin = wl.mmck(lam=3.0, mu=5.0, c=2, K=500)
print(abs(r_fin.Lq - r_inf.Lq) < 0.01) # True
Restricción: K debe ser ≥ c (la capacidad total no puede ser menor que el número de servidores).
erlang_b(lam, mu, c) — Erlang B
Probabilidad de bloqueo para un sistema de pérdidas M/M/c/c (sin sala de espera: si todos los servidores están ocupados el cliente se va). Usado en dimensionamiento de líneas telefónicas y redes de comunicación.
pb = wl.erlang_b(lam=5.0, mu=1.0, c=8)
print(f"Blocking probability: {pb:.2%}") # ej. 1.3%
# Cuántos servidores para bloqueo < 2%?
for c in range(1, 20):
if wl.erlang_b(5.0, 1.0, c) < 0.02:
print(f"Need c = {c} servers")
break
queue_length_pmf(lam, mu, n_max) — PMF de longitud de cola
Distribución de probabilidad del número de clientes en el sistema para M/M/1.
Devuelve un pd.DataFrame con columnas n, P(N=n), P(N<=n).
df = wl.queue_length_pmf(lam=3.0, mu=5.0, n_max=20)
print(df.head())
# n P(N=n) P(N<=n)
# 0 0.400 0.400
# 1 0.240 0.640
# 2 0.144 0.784
# ...
# ¿Probabilidad de tener más de 5 clientes en sistema?
prob_gt5 = 1 - df.loc[df["n"] == 5, "P(N<=n)"].iloc[0]
sojourn_cdf(lam, mu, t_max, n_points) — CDF del tiempo de permanencia
Distribución acumulada del tiempo total en el sistema (sojourn time) para M/M/1.
Devuelve un pd.DataFrame con columnas t, F(t), f(t).
df = wl.sojourn_cdf(lam=3.0, mu=5.0)
# ¿Cuánto tiempo cubre el 95% de los clientes?
t95 = df.loc[df["F(t)"] >= 0.95, "t"].iloc[0]
print(f"95% of customers leave before t = {t95:.3f}")
3. Colas con prioridad
mm1_priority(lam_list, mu, *, class_names) — M/M/1 HOL no-preemptiva
Cola de un servidor con múltiples clases de prioridad. La clase 0 tiene la prioridad más alta. La disciplina es Head-Of-Line (HOL) no-preemptiva: un cliente de alta prioridad que llega no interrumpe al que está siendo atendido, pero sí pasa delante de todos los que esperan.
Fórmula de Kleinrock:
Wq_k = R / ((1 − σ_{k−1}) · (1 − σ_k))
donde R = ρ_total / μ es el tiempo residual de servicio y σ_k = Σ_{i=0}^{k} ρ_i.
# 3 clases: alta (λ=2), media (λ=1.5), baja (λ=0.5), μ=5 compartido
pri = wl.mm1_priority([2.0, 1.5, 0.5], mu=5.0)
print(pri)
for cls in pri.classes:
print(f"Clase {cls['class_id']}: Wq = {cls['Wq']:.4f}")
# Con nombres de clase personalizados
pri = wl.mm1_priority(
[2.0, 1.0],
mu=5.0,
class_names=["VIP", "Estándar"],
)
print(pri.classes[0]["Wq"]) # VIP espera mucho menos
print(pri.classes[1]["Wq"]) # Estándar espera más que en cola FIFO
df = pri.to_frame() # DataFrame con una fila por clase
PriorityQueueResult — atributos:
| Atributo | Descripción |
|---|---|
classes |
Lista de dicts con class_id, lam, rho, Wq, W, Lq, L por clase |
rho_total |
Utilización total del servidor |
mu |
Tasa de servicio compartida |
Restricción: la suma de todas las tasas de llegada debe ser < μ (sistema estable).
4. Simulación Monte Carlo G/G/1
monte_carlo_gg1(lam, mu, ca2, cs2, n_customers, seed) — Simulación event-driven
Simula una cola G/G/1 con distribuciones gamma calibradas para reproducir cualquier ca² y cs². Útil para validar aproximaciones analíticas y obtener percentiles que los modelos cerrados no dan.
sim = wl.monte_carlo_gg1(
lam=3.0, mu=5.0,
ca2=1.0, cs2=0.5, # ca2=1 → Poisson, cs2=0.5 → Erlang-2
n_customers=50_000,
seed=42,
)
print(sim.Wq_mean) # promedio
print(sim.Wq_p50) # mediana
print(sim.Wq_p90) # percentil 90
print(sim.Wq_p95) # percentil 95
print(sim.Wq_p99) # percentil 99
sim.plot() # histograma + CDF empírica con percentiles anotados
SimulationResult — atributos:
| Atributo | Descripción |
|---|---|
Wq_mean |
Tiempo promedio de espera simulado |
Wq_p50/p90/p95/p99 |
Percentiles de la distribución de espera |
L_mean, Lq_mean |
Longitudes de sistema y cola promedio |
model |
Descripción del modelo simulado |
5. Ajuste de parámetros desde datos reales
fit_from_data(inter_arrivals, service_times, *, arrival_timestamps) — Estimación de parámetros
Estima λ, μ, ca², cs² directamente desde datos observados. Útil para calibrar modelos cuando los parámetros no se conocen teóricamente.
Formas de uso:
import numpy as np
import walopy as wl
rng = np.random.default_rng(42)
# 1. Desde tiempos entre llegadas y tiempos de servicio
ia = rng.exponential(scale=0.2, size=1000) # media = 0.2 → λ ≈ 5
svc = rng.exponential(scale=0.1, size=1000) # media = 0.1 → μ ≈ 10
fit = wl.fit_from_data(ia, svc)
print(fit)
# λ ≈ 5.0 ca² ≈ 1.0 (exponencial → CV² = 1)
# μ ≈ 10.0 cs² ≈ 1.0
# 2. Solo tiempos entre llegadas (sin datos de servicio)
fit_ia = wl.fit_from_data(ia)
print(fit_ia.lam, fit_ia.ca2)
print(fit_ia.mu) # None
# 3. Desde timestamps crudos (no se tienen los intervalos directamente)
ts = np.cumsum(rng.exponential(0.2, 1000)) # timestamps acumulados
fit = wl.fit_from_data(arrival_timestamps=ts)
# 4. Conectar directamente con un modelo (necesita λ, μ, ca² y cs²: ajuste con ambas muestras)
fit_completo = wl.fit_from_data(ia, svc)
r = wl.kingman(**fit_completo.to_model_kwargs()) # usa lam, mu, ca2, cs2
FitResult — atributos:
| Atributo | Descripción |
|---|---|
lam |
Tasa de llegada estimada (None si no hay datos de llegada) |
mu |
Tasa de servicio estimada (None si no hay datos de servicio) |
ca2 |
CV² de tiempos entre llegadas |
cs2 |
CV² de tiempos de servicio |
n_arrivals |
Número de observaciones de llegada |
n_services |
Número de observaciones de servicio |
mean_ia |
Media de tiempos entre llegadas |
mean_svc |
Media de tiempos de servicio |
Métodos: .to_model_kwargs() → dict con lam, mu, ca2, cs2 listos para kingman(). .to_frame() → DataFrame.
6. Redes de Jackson
jackson_network(station_names, mu, gamma, routing, *, servers) — Red abierta de colas
Analiza una red de colas de Jackson abierta. Por el Teorema de Jackson, cada estación se comporta como una cola M/M/c independiente con una tasa de llegada efectiva que satisface las ecuaciones de tráfico:
(I − P^T) · λ = γ
donde γ_i es la tasa de llegada externa a la estación i y P_ij es la probabilidad de routing de i a j.
# Red de 3 estaciones en serie con estación final de 2 servidores
net = wl.jackson_network(
station_names=["Recepción", "Control de calidad", "Empaque"],
mu=[10.0, 8.0, 12.0], # tasa de servicio por servidor
gamma=[5.0, 0.0, 0.0], # solo llegan clientes externos a Recepción
routing=[
[0.0, 1.0, 0.0], # Recepción → 100% a QC
[0.0, 0.0, 1.0], # QC → 100% a Empaque
[0.0, 0.0, 0.0], # Empaque → sale del sistema
],
servers=[1, 1, 2], # servidores por estación
)
print(net)
print(net.to_frame()) # DataFrame con métricas por estación
# Acceder a métricas por estación
for st in net.stations:
print(f"{st.name}: λ={st.lam_total:.2f}, Wq={st.Wq:.4f}, ρ={st.rho:.2f}")
# Indicadores del sistema completo
print(f"L_sistema = {net.L_system:.4f}") # clientes totales en toda la red
print(f"W_sistema = {net.W_system:.4f}") # tiempo total en la red
# Red con bifurcación
net2 = wl.jackson_network(
station_names=["Entrada", "Ruta A", "Ruta B"],
mu=[20.0, 8.0, 10.0],
gamma=[10.0, 0.0, 0.0],
routing=[
[0.0, 0.6, 0.4], # 60% va a Ruta A, 40% a Ruta B
[0.0, 0.0, 0.0],
[0.0, 0.0, 0.0],
],
)
JacksonResult — atributos:
| Atributo | Descripción |
|---|---|
stations |
Lista de StationMetrics por estación |
L_system |
Número total promedio de clientes en toda la red |
W_system |
Tiempo total promedio en la red (Little: L = λ_ext · W) |
StationMetrics — atributos:
| Atributo | Descripción |
|---|---|
name |
Nombre de la estación |
lam_total |
Tasa de llegada total (externa + interna) |
lam_external |
Tasa de llegada externa γ_i |
mu |
Tasa de servicio por servidor |
servers |
Número de servidores |
rho |
Utilización ρ = λ / (c·μ) |
L, Lq, W, Wq |
Indicadores estándar de cola |
Restricciones: todas las estaciones deben ser estables (ρ < 1). La suma de probabilidades de routing por fila debe ser ≤ 1.
7. Inventarios
eoq(demand_rate, ordering_cost, holding_cost) — Cantidad Económica de Pedido
Fórmula de Wilson/Harris: minimiza la suma de costo de ordenar y costo de almacenar.
Fórmula:
Q* = √(2 · D · K / h)
En el óptimo: costo de mantener = costo de ordenar (propiedad de cruce).
r = wl.eoq(
demand_rate=1000, # D: unidades por período
ordering_cost=50, # K: costo fijo por orden
holding_cost=2, # h: costo por unidad por período
)
print(r)
# EOQ (cantidad de pedido): 223.6 unidades
# Frecuencia de pedidos : 4.472 pedidos/periodo
# Tiempo de ciclo : 0.2236 periodos
# Costo total : 447.2
# Costo de mantener : 223.6
# Costo de ordenar : 223.6 ← iguales en el óptimo
r.plot() # gráfica de curvas de costo
df = r.to_frame()
EOQResult — atributos:
| Atributo | Descripción |
|---|---|
eoq |
Cantidad óptima Q* |
total_cost |
Costo total mínimo por período |
holding_cost_total |
Componente de almacenamiento en Q* |
ordering_cost_total |
Componente de pedido en Q* |
order_frequency |
Número de pedidos por período D/Q* |
cycle_time |
Tiempo entre pedidos 1/(D/Q*) |
reorder_point(demand_rate, lead_time, *, demand_std, lead_time_std, service_level) — Punto de reorden
Calcula el nivel de inventario al que se debe lanzar una orden de reposición, incluyendo stock de seguridad para cubrir la variabilidad de la demanda y del tiempo de entrega.
Fórmulas:
σ_DLT = √(L̄ · σ_D² + D̄² · σ_L²)
ROP = D̄ · L̄ + z · σ_DLT
r = wl.reorder_point(
demand_rate=50, # demanda media por período
lead_time=2, # tiempo de entrega medio (en mismas unidades)
demand_std=10, # desviación estándar de la demanda
lead_time_std=0.5, # desviación estándar del lead time
service_level=0.95, # nivel de servicio deseado
)
print(r)
# Punto de reorden : 132.4 unidades
# Stock de seguridad : 32.4 unidades
# Nivel de servicio : 95.00%
# z : 1.645
# Demanda media en LT : 100.0
# Std demand LT : 19.72
# Sin variabilidad → solo demanda esperada durante LT
r_det = wl.reorder_point(demand_rate=50, lead_time=2,
demand_std=0, lead_time_std=0)
print(r_det.safety_stock) # 0.0 — no se necesita buffer
# Comparar niveles de servicio
r_90 = wl.reorder_point(50, 2, demand_std=10, service_level=0.90)
r_99 = wl.reorder_point(50, 2, demand_std=10, service_level=0.99)
print(r_99.safety_stock > r_90.safety_stock) # True
ReorderResult — atributos:
| Atributo | Descripción |
|---|---|
reorder_point |
Nivel ROP en unidades |
safety_stock |
Stock de seguridad = z · σ_DLT |
service_level |
Nivel de servicio de ciclo |
z_score |
Factor de seguridad z |
mean_demand_lt |
Demanda esperada durante el lead time |
std_demand_lt |
Desviación estándar de la demanda durante LT |
newsvendor(demand_mean, demand_std, price, cost, *, salvage) — Modelo del vendedor de periódicos
Optimiza la cantidad a ordenar para un producto perecedero o de temporada bajo demanda incierta. Equilibra el costo de quedarse corto (Cu) con el costo de sobrar (Co).
Fórmulas:
Cu = precio − costo (costo de substock: venta perdida)
Co = costo − salvage (costo de sobrestock: unidad no vendida)
CR = Cu / (Cu + Co) (ratio crítico)
Q* = F⁻¹(CR) (cuantil CR de la demanda)
r = wl.newsvendor(
demand_mean=100,
demand_std=20,
price=10, # precio de venta
cost=6, # costo de compra
salvage=2, # valor residual de unidades no vendidas
)
print(r)
# Cantidad óptima : 100.0 unidades
# Razón crítica : 0.5
# Utilidad esperada : 320.0
# Ventas esperadas : 100.0
# Sobrante esperado : 0.0
# Faltante esperado : 0.0
# Costo de subestimar Cu: 4.0
# Costo de sobrestimar Co: 4.0
# Producto con alto margen → pedir por encima de la media
r_alto = wl.newsvendor(demand_mean=100, demand_std=20,
price=50, cost=6, salvage=0)
print(r_alto.optimal_qty > 100) # True
# Producto con bajo margen → pedir por debajo de la media
r_bajo = wl.newsvendor(demand_mean=100, demand_std=20,
price=7, cost=6, salvage=2)
print(r_bajo.optimal_qty < 100) # True
NewsvendorResult — atributos:
| Atributo | Descripción |
|---|---|
optimal_qty |
Q* óptimo |
critical_ratio |
CR = Cu/(Cu+Co) |
expected_profit |
Beneficio esperado en Q* |
expected_sales |
Ventas esperadas E[min(D, Q*)] |
expected_leftover |
Sobrante esperado E[max(Q*−D, 0)] |
expected_stockout |
Rotura esperada E[max(D−Q*, 0)] |
underage_cost |
Cu — costo de substock |
overage_cost |
Co — costo de sobrestock |
ebq(demand_rate, setup_cost, holding_cost, production_rate) — Lote Económico de Producción (EBQ/EPQ)
Extiende el EOQ al caso en que la producción y el consumo ocurren simultáneamente (tasa de producción finita P > D).
Fórmula:
Q* = √(2 · D · S / (h · (1 − D/P)))
Inventario máximo = Q* · (1 − D/P)
Inventario medio = Inventario máximo / 2
r = wl.ebq(
demand_rate=1000, # D: unidades/período
setup_cost=50, # S: costo fijo de setup por lote
holding_cost=2, # h: costo/unidad/período
production_rate=4000, # P: tasa de producción (debe ser > D)
)
print(r)
# EBQ (tamaño de lote): 258.2 unidades
# Inventario máximo : 193.6
# Inventario promedio : 96.82
# Frecuencia de lotes : 3.873 lotes/periodo
# Tiempo de ciclo : 0.2582 periodos
# Tiempo de producción: 0.06455 periodos
# Costo total : 193.6
# Costo de mantener : 96.82
# Costo de preparación: 96.82
EBQResult — atributos:
| Atributo | Descripción |
|---|---|
ebq |
Lote óptimo Q* |
max_inventory |
Nivel máximo de inventario Q*(1−D/P) |
avg_inventory |
Inventario promedio = max/2 |
production_time |
Tiempo de producción por ciclo = Q*/P |
cycle_time |
Duración del ciclo = Q*/D |
total_cost |
Costo total mínimo |
eoq_multi(demand_rates, ordering_costs, holding_costs, *, names) — EOQ multi-ítem independiente
Resuelve el EOQ para cada ítem por separado sin restricciones compartidas.
r = wl.eoq_multi(
demand_rates = [1000, 500, 800],
ordering_costs = [50, 30, 40 ],
holding_costs = [2, 1, 1.5],
names = ["A", "B", "C"],
)
print(r.total_cost) # suma de costos óptimos individuales
df = r.to_frame() # DataFrame con EOQ, costos y frecuencias por ítem
ebq_multi(demand_rates, setup_costs, holding_costs, production_rates, *, names) — EBQ multi-ítem independiente
r = wl.ebq_multi(
demand_rates = [1000, 500],
setup_costs = [50, 30 ],
holding_costs = [2, 1 ],
production_rates= [4000, 2000],
)
df = r.to_frame() # EBQ, inventario máximo/medio, costo por ítem
eoq_multi_constrained(demand_rates, ordering_costs, holding_costs, ...) — EOQ multi-ítem con restricciones
Minimiza el costo total de inventario sujeto a restricciones lineales sobre las cantidades de pedido mediante relajación Lagrangiana (búsqueda binaria sobre el multiplicador).
Restricciones soportadas:
| Parámetro | Restricción |
|---|---|
budget, budget_unit_costs |
Σ(c_i · Q_i / 2) ≤ budget — valor promedio en inventario |
space, space_per_unit |
Σ(s_i · Q_i / 2) ≤ space — espacio promedio ocupado |
constraints |
Lista de {"name", "weights", "bound"} — Σ(a_i · Q_i) ≤ B |
# Restricción de presupuesto
r = wl.eoq_multi_constrained(
[1000, 500, 800], [50, 30, 40], [2, 1, 1.5],
budget=5000,
budget_unit_costs=[10, 8, 12], # costo unitario de compra
)
print(r.binding_constraints) # ["budget"] si la restricción está activa
print(r.lagrange_multipliers) # {"budget": λ}
df = r.to_frame() # Q*, holding cost, ordering cost por ítem
# Restricción de espacio
r2 = wl.eoq_multi_constrained(
[1000, 500, 800], [50, 30, 40], [2, 1, 1.5],
space=200,
space_per_unit=[0.5, 0.3, 0.4],
)
# Restricción genérica: suma total de unidades pedidas ≤ 400
r3 = wl.eoq_multi_constrained(
[1000, 500, 800], [50, 30, 40], [2, 1, 1.5],
constraints=[{"name": "total_qty", "weights": [1, 1, 1], "bound": 400}],
)
ConstrainedMultiEOQResult — atributos:
| Atributo | Descripción |
|---|---|
items |
Lista de dicts por ítem: nombre, Q*, costos |
total_cost |
Costo total bajo las restricciones |
unconstrained_total_cost |
Costo sin restricciones (cota inferior) |
lagrange_multipliers |
Diccionario {nombre_restricción: λ} |
binding_constraints |
Nombres de restricciones activas (λ > 0) |
lot_for_lot(demands, setup_cost, holding_cost) — Lote por Lote
Heurística de dimensionado dinámico: ordena exactamente la demanda de cada período. Minimiza el costo de almacenamiento (cero inventario en tránsito) a cambio de un setup por período.
demands = [100, 80, 0, 120, 60] # demandas por período
r = wl.lot_for_lot(demands, setup_cost=200, holding_cost=1)
print(r.total_holding_cost) # 0.0 — sin inventario residual
print(r.n_orders) # 4 — un pedido por período con demanda > 0
print(r)
# Método : Lote por lote
# Número de pedidos : 4
# Costo total : 800.0
silver_meal(demands, setup_cost, holding_cost) — Silver-Meal
Heurística de dimensionado dinámico que agrupa períodos mientras el costo promedio por período decrece, reduciendo el número de setups.
r = wl.silver_meal(demands, setup_cost=200, holding_cost=1)
print(r.total_cost <= wl.lot_for_lot(demands, 200, 1).total_cost) # True
df = r.to_frame() # pedidos: período, cantidad, períodos cubiertos
LotSizingResult — atributos (Lot-for-Lot y Silver-Meal):
| Atributo | Descripción |
|---|---|
orders |
Lista de pedidos: período, cantidad, períodos cubiertos |
total_cost |
Costo total (setup + almacenamiento) |
total_setup_cost |
Costo total de setups |
total_holding_cost |
Costo total de almacenamiento |
n_orders |
Número de pedidos emitidos |
method |
Nombre de la heurística usada |
eoq_quantity_discount(demand_rate, ordering_cost, holding_cost_rate, price_breaks) — EOQ con descuentos por cantidad
Evalúa todos los tramos de precio (all-units) y selecciona la cantidad que minimiza el costo total anual (compra + pedido + almacenamiento), ajustando el EOQ al tramo válido cuando cae fuera de rango.
r = wl.eoq_quantity_discount(
demand_rate=1000,
ordering_cost=50,
holding_cost_rate=0.20, # 20% del precio unitario por año
price_breaks=[
(0, 10.00), # precio base
(500, 9.50), # ≥ 500 unidades → $9.50
(1000, 9.00), # ≥ 1000 unidades → $9.00
],
)
print(r.optimal_qty) # cantidad óptima seleccionada
print(r.unit_price) # precio en ese tramo
print(r.total_cost) # costo anual mínimo
df = r.to_frame() # todos los candidatos evaluados
QuantityDiscountResult — atributos:
| Atributo | Descripción |
|---|---|
optimal_qty |
Cantidad óptima ajustada al tramo |
unit_price |
Precio unitario en el tramo seleccionado |
total_cost |
Costo total anual (compra + pedido + almacenamiento) |
purchase_cost |
Costo de compra anual D · precio |
ordering_cost_total |
Costo de pedido anual (D/Q) · K |
holding_cost_total |
Costo de almacenamiento anual (Q/2) · h · precio |
candidates |
Lista de todos los tramos evaluados |
ABC, XYZ y ABC-XYZ — clasificación de artículos
abc_analysis clasifica por valor anual (Pareto): un artículo es A mientras el acumulado previo a él sea
menor que a_threshold (80 %), B mientras sea menor que b_threshold (95 %) y C en el resto. Los artículos
sin demanda quedan en C. xyz_analysis clasifica por variabilidad (coeficiente de variación): X si
CV ≤ x_threshold (0.5), Y hasta y_threshold (1.0) y Z por encima. abc_xyz combina ambas y devuelve la
matriz 3×3.
items = [
{"name": "A", "demand": 500, "unit_value": 10.0, "demand_std": 120},
{"name": "B", "demand": 300, "unit_value": 10.0, "demand_std": 60},
{"name": "C", "demand": 100, "unit_value": 10.0, "demand_std": 150},
{"name": "D", "demand": 700, "unit_value": 1.0, "demand_std": 300},
{"name": "E", "demand": 300, "unit_value": 1.0, "demand_std": 10},
]
abc = wl.abc_analysis(items)
print(abc) # resumen por clase: A = 2 artículos (80 % del valor)
print(abc.to_frame()[["artículo", "valor_anual", "pct_acumulado", "clase"]])
# XYZ: cada artículo aporta 'cv' directamente o 'demand_std' + 'demand_mean' (o 'demand_rate')
xyz = wl.xyz_analysis([
{"name": i["name"], "demand_std": i["demand_std"], "demand_mean": i["demand"]} for i in items
])
print(xyz.to_frame()) # columnas: artículo, cv, clase
# ABC-XYZ: matriz de conteos (filas ABC, columnas XYZ)
combo = wl.abc_xyz([{**i, "cv": i["demand_std"] / i["demand"]} for i in items])
print(combo.matrix_frame())
print(combo.items[0]["combined_class"]) # 'AX'
| Parámetro | Descripción |
|---|---|
items |
Lista de diccionarios. ABC: demand (≥ 0) y unit_value (> 0). XYZ: cv o demand_std + demand_mean/demand_rate |
a_threshold, b_threshold |
Límites acumulados de las clases A y B (0 < a < b < 1) |
x_threshold, y_threshold |
Límites de CV de las clases X e Y |
Errores: lista vacía, elementos que no son diccionarios, claves faltantes y valores no finitos lanzan ValueError/TypeError
con el elemento y la clave afectados; si todas las demandas son 0 no hay nada que clasificar y se lanza ValueError.
MRP de un nivel — mrp
Plan de requerimientos de materiales para un artículo: necesidades netas, órdenes planificadas (recepción y
liberación) y existencias proyectadas. Admite lote por lote ("LFL") o lote fijo, tiempo de entrega,
stock de seguridad y recepciones programadas.
r = wl.mrp(
gross_requirements=[0, 50, 0, 30, 60, 20],
initial_on_hand=40,
scheduled_receipts=[0, 0, 25, 0, 0, 0],
lead_time=1,
lot_size=40, # "LFL" para lote por lote
safety_stock=10,
item_name="Eje",
)
print(r) # tabla por periodo: GR, SR, OH, NR, PR, PO
print(r.to_frame()) # el mismo plan como DataFrame
print(r.past_due_releases) # 0.0
Si el lead_time es mayor que el periodo en que se necesita la orden, la liberación cae antes del periodo 1:
mrp emite un UserWarning y acumula esa cantidad en past_due_releases (no aparece en planned_releases).
8. Análisis de operaciones
oee(availability, performance, quality) — OEE
Calcula la Eficiencia Global de los Equipos (OEE = Disponibilidad × Rendimiento × Calidad).
r = wl.oee(
availability=0.90, # tiempo disponible / tiempo planificado
performance=0.80, # producción real / producción teórica
quality=0.95, # unidades buenas / unidades totales
)
print(r)
# OEE = 68.4%
# Clase mundial: ≥ 85%
r.plot() # gráfica horizontal con referencia world-class (85%)
utilization_efficiency(actual_output, capacity) — Utilización
r = wl.utilization_efficiency(actual_output=750, capacity=1000)
print(r.utilization) # 0.75 — 75% de utilización
print(1 - r.utilization) # 0.25 — 25% de capacidad ociosa
unit_cost(fixed_cost, variable_cost_per_unit, units_produced) — Costo unitario
r = wl.unit_cost(fixed_cost=10_000, variable_cost_per_unit=10, units_produced=500)
print(r.unit_cost) # 30.0 $/unidad
print(r.fixed_cost_per_unit) # 20.0
print(r.variable_cost_per_unit) # 10.0
9. Análisis de cuellos de botella
bottleneck_analysis(station_names, capacities, demand_rate) — Teoría de restricciones
Identifica el cuello de botella de una línea de producción comparando la capacidad de cada estación con la demanda.
r = wl.bottleneck_analysis(
station_names=["Corte", "Soldadura", "Pintura"],
capacities=[120.0, 80.0, 100.0], # unidades/hora por estación
demand_rate=70.0, # demanda requerida
)
print(r)
# Cuello de botella: Soldadura (capacidad 80 < demanda 70 con menor margen)
# Utilization: Corte=58.3%, Soldadura=87.5%, Pintura=70.0%
r.plot() # barras de utilización con cuello de botella en rojo
10. Balance de línea y tiempo takt
takt_time(available_time, demand) — Tiempo takt
Ritmo de producción requerido para satisfacer la demanda del cliente.
takt = wl.takt_time(available_time=480, demand=60)
print(takt) # 8.0 min/unidad
line_balance(station_names, cycle_times, takt) — Balance de línea
Analiza si cada estación puede cumplir con el tiempo takt y calcula la eficiencia de balance.
lb = wl.line_balance(
station_names=["A", "B", "C"],
cycle_times=[5.0, 9.0, 4.0],
takt=10.0,
)
print(lb.bottleneck) # "B" — estación más lenta
print(lb.balance_efficiency) # eficiencia de balance de la línea
print(lb.theoretical_min_stations) # número mínimo teórico de estaciones
lb.plot() # barras de cycle time vs takt
11. Análisis de punto de equilibrio
break_even(fixed_cost, price_per_unit, variable_cost_per_unit, *, actual_units) — Punto de equilibrio
be = wl.break_even(
fixed_cost=10_000,
price_per_unit=25,
variable_cost_per_unit=15,
actual_units=1_500,
)
print(be.bep_units) # 1000.0 — punto de equilibrio en unidades
print(be.bep_revenue) # 25000.0 — ingresos en el punto de equilibrio
print(be.margin_of_safety_pct) # 0.333 — margen de seguridad (fracción) sobre ventas actuales
print(be.margin_of_safety_units) # 500.0 — unidades por encima del punto de equilibrio
be.plot() # líneas de ingresos/costos con BEP y zona de beneficio
break_even_multi(fixed_cost, prices, variable_costs, sales_mix) — Multi-producto
Calcula el punto de equilibrio para una mezcla de varios productos usando la contribución marginal ponderada (WACM).
Fórmula: WACM = Σ(CMᵢ × mixᵢ) / Σmixᵢ, luego BEP_total = FC / WACM
r = wl.break_even_multi(
fixed_cost=120_000,
prices=[50, 80, 120],
variable_costs=[30, 50, 70],
sales_mix=[3, 2, 1], # proporción de ventas
)
print(r.weighted_avg_cm) # WACM ≈ 36.67
print(r.bep_units_total) # unidades totales en el punto de equilibrio
print(r.bep_revenue_total) # ingresos totales en el punto de equilibrio
for item in r.items:
print(item["Producto"], item["PE unidades"])
r.to_frame() # DataFrame con una fila por producto
| Atributo | Descripción |
|---|---|
weighted_avg_cm |
Contribución marginal ponderada |
bep_units_total |
Unidades totales en el BEP |
bep_revenue_total |
Ingresos totales en el BEP |
items |
Lista de dicts por producto con BEP units, BEP revenue |
break_even_sales(fixed_cost, variable_cost_ratio, *, actual_revenue) — Punto de equilibrio en ingresos
Calcula el BEP en términos de ingresos cuando los costos variables se expresan como porcentaje de las ventas.
Fórmula: BEP = FC / (1 − RCV)
r = wl.break_even_sales(
fixed_cost=50_000,
variable_cost_ratio=0.60, # 60% de los ingresos son costos variables
actual_revenue=200_000,
)
print(r.bep_revenue) # 125 000 — ingresos en el BEP
print(r.contribution_margin_ratio) # 0.40 — margen de contribución
print(r.margin_of_safety_units) # 75 000 — margen de seguridad en ingresos
print(r.margin_of_safety_pct) # 0.375 — porcentaje del margen de seguridad
12. Curvas de intercambio
Herramientas para tomar decisiones de política agregada en familias de artículos.
Tipo 1 — ciclo: exchange_curve
Traza la hipérbola entre número de pedidos por año (N) e inversión promedio en inventario de ciclo (I):
N(k) = N* / k I(k) = k · I* → N · I = N* · I* (constante)
Dado un target de N o de I, resuelve el multiplicador k óptimo y recalcula Q para cada artículo.
import walopy as wl
items = [
{"name": "A", "demand": 1000, "ordering_cost": 50, "holding_cost": 2, "unit_value": 10},
{"name": "B", "demand": 500, "ordering_cost": 30, "holding_cost": 1, "unit_value": 5},
{"name": "C", "demand": 2000, "ordering_cost": 100, "holding_cost": 4, "unit_value": 8},
]
# Sin target → punto EOQ (k = 1)
r = wl.exchange_curve(items)
print(r)
Objetivo : eoq
Multiplicador k : 1.0000
N pedidos/año (EOQ) : 87.27
N pedidos/año (óptimo) : 87.27
Inversión (EOQ) : 5430
Inversión (óptimo) : 5430
# Reducir a 20 pedidos/año (k > 1 → Q más grandes → más inversión)
r = wl.exchange_curve(items, target_orders=20)
# Limitar inversión a 3 000 (k < 1 → Q más pequeños → más pedidos)
r = wl.exchange_curve(items, target_investment=3000)
# Tabla por artículo
df = r.to_frame()
# columnas: artículo | Q_eoq | Q_óptima | n_pedidos | inversión
# Hipérbola para graficar
curve = r.curve_to_frame()
# columnas: N | I
Parámetros de cada artículo
| Clave | Requerido | Descripción |
|---|---|---|
demand |
✓ | Demanda anual Di |
ordering_cost |
✓ | Costo de pedido Ki |
holding_cost |
✓ | Costo de mantenimiento hi |
unit_value |
— | Valor unitario vi (default 1) |
name |
— | Etiqueta (default I1, I2, …) |
Tipo 2 — seguridad: safety_stock_curve
Traza la curva entre inversión en stock de seguridad e nivel de servicio por ciclo usando una política z común para toda la familia:
SS_i(z) = z · σᵢ_DLT · vᵢ σᵢ_DLT = √(Lᵢ·σ²_Di + D²ᵢ·σ²_Li)
SS_total(z) = z · Σ σᵢ_DLT · vᵢ SL(z) = Φ(z)
items = [
{"name": "A", "demand_rate": 100, "demand_std": 10, "lead_time": 2, "unit_value": 10},
{"name": "B", "demand_rate": 50, "demand_std": 5, "lead_time": 1, "unit_value": 5},
{"name": "C", "demand_rate": 200, "demand_std": 20, "lead_time": 3, "unit_value": 8},
]
# Target: nivel de servicio 95%
r = wl.safety_stock_curve(items, target_service_level=0.95)
print(r)
Objetivo : nivel_de_servicio=95.0000%
z : 1.6449
Nivel de servicio : 95.0000%
Inversión en SS : 1 847
# Target: presupuesto máximo de SS
r = wl.safety_stock_curve(items, target_ss_investment=1000)
# Tabla por artículo (incluye punto de reorden cuando se da demand_rate)
df = r.to_frame()
# columnas: artículo | sigma_dlt | stock_de_seguridad | inversión | punto_de_reorden
# Curva completa (z de −2 a 4)
curve = r.curve_to_frame()
# columnas: z | service_level | ss_investment
Parámetros de cada artículo
| Clave | Requerido | Descripción |
|---|---|---|
demand_std |
✓ | Desviación estándar de la demanda |
lead_time |
✓ | Lead time medio |
demand_rate |
— | Demanda media; activa columna reorder_point |
lead_time_std |
— | Desviación estándar del lead time (default 0) |
unit_value |
— | Valor unitario (default 1) |
name |
— | Etiqueta (default I1, I2, …) |
Relación entre ambas curvas
| Tipo 1 (ciclo) | Tipo 2 (seguridad) | |
|---|---|---|
| Función | exchange_curve |
safety_stock_curve |
| Decide | Tamaño de lote Q | Punto de reorden r |
| Tradeoff | N pedidos/año ↔ Inversión ciclo | Nivel de servicio ↔ SS |
| Resultado | ExchangeCurveResult |
SafetyStockCurveResult |
| Parámetro | Multiplicador k | Factor z |
Usadas juntas cubren la política completa (Q, r) para toda la familia.
13. Programación de producción (scheduling)
Máquina única — schedule_single
Asigna n trabajos a una sola máquina según una regla de prioridad.
import walopy as wl
p = [3, 1, 4, 1, 5] # tiempos de proceso
d = [8, 3, 10, 4, 12] # fechas de entrega
w = [2, 5, 1, 4, 1] # pesos de importancia
r = wl.schedule_single(p, rule="SPT", due_dates=d, weights=w,
names=["A","B","C","D","E"])
print(r)
Regla : SPT
Secuencia : B → D → A → C → E
Makespan (Cmax) : 14
Finalización total ΣCj : 37
Finalización pond. ΣwCj : 89
Retraso máximo : 2
Tardanza total ΣTj : 2
Trabajos con retraso : 1
Reglas disponibles
rule |
Criterio minimizado | Datos requeridos |
|---|---|---|
'SPT' |
ΣCj (tiempo total de completación) | — |
'EDD' |
Lmax (máxima tardanza) | due_dates |
'WSPT' |
ΣwjCj (completación ponderada) | weights |
'CR' |
Ratio crítico dj/pj | due_dates |
'FIFO' |
Orden de llegada (línea base) | — |
Atributos de ScheduleResult
| Atributo | Descripción |
|---|---|
rule |
Regla utilizada |
sequence |
Lista de nombres en orden de procesamiento |
jobs |
Lista de JobSchedule (por trabajo) |
makespan |
Cmax = suma de tiempos de proceso |
total_completion_time |
ΣCj |
total_weighted_completion_time |
ΣwjCj |
max_lateness |
max(Lj) |
total_tardiness |
ΣTj |
n_tardy |
Número de trabajos tarde |
df = r.to_frame() # columnas: Trabajo, p, d, w, Inicio, C, L, T, Con retraso
Flow-shop de 2 máquinas — johnson_flowshop
El algoritmo de Johnson encuentra la secuencia óptima que minimiza el makespan cuando todos los trabajos pasan primero por M1 y luego por M2.
m1 = [3, 8, 5, 7, 2] # tiempos en máquina 1
m2 = [5, 2, 8, 4, 6] # tiempos en máquina 2
r = wl.johnson_flowshop(m1, m2)
print(r)
Secuencia : J1 → J5 → J4 → J3 → J2
Makespan : 34
df = r.to_frame()
# columnas: Job | M1 start | M1 end | M2 start | M2 end
Flow-shop de m máquinas — neh_flowshop
Heurística de Nawaz-Enscore-Ham para el flow-shop de permutación con cualquier número de máquinas: ordena los trabajos por tiempo total decreciente e inserta cada uno en la mejor posición (aceleración de Taillard: O(n²·m)). Es una heurística: en casos pequeños el makespan queda típicamente a pocos puntos porcentuales del óptimo.
# filas = trabajos, columnas = máquinas
tiempos = [[5, 9, 8], [9, 3, 10], [9, 4, 5], [4, 8, 8]]
r = wl.neh_flowshop(tiempos, names=["P1", "P2", "P3", "P4"])
print(r) # secuencia y makespan
print(r.to_frame()) # Gantt: columnas M1_inicio, M1_fin, M2_inicio, ...
print(r.sequence, r.makespan)
14. Confiabilidad — reliability
Modelos de confiabilidad con distribución exponencial (tasa de falla constante).
Componente individual — mtbf_analysis
r = wl.mtbf_analysis(failure_rate=0.01, mttr=5.0, t=100)
print(r)
Topología : component
Componentes : 1
Tasas de falla λ: ['0.01']
MTBF : 100
R(t=100) : 0.367879
Disponibilidad : 95.2381%
Sistema en serie — series_system
El sistema falla si cualquier componente falla. λ_sys = Σλi.
r = wl.series_system([0.01, 0.02, 0.03], t=10)
print(r.mtbf) # 1/0.06 ≈ 16.67
print(r.R_t) # e^{-0.06·10} ≈ 0.549
Sistema en paralelo — parallel_system
El sistema opera si al menos uno de los componentes opera.
R_sys(t) = 1 − ∏(1 − e^{−λi·t}). MTBF calculado numéricamente.
r = wl.parallel_system([0.01, 0.02], t=50)
print(r.R_t) # mayor que el sistema en serie
print(r.mtbf) # mayor que cualquier componente individual
Sistema k-de-n — koon_system
Opera si al menos k de n componentes idénticos están en servicio.
MTBF exacto = (1/λ) · Σ_{j=k}^{n} (1/j).
# 2-de-3: requiere al menos 2 de 3 componentes idénticos
r = wl.koon_system(n=3, k=2, failure_rate=0.01, t=50)
print(r.mtbf) # (1/0.01) * (1/2 + 1/3) ≈ 83.33
print(r.R_t)
Casos especiales
k=1→ equivale aparallel_systemcon n componentes idénticos.k=n→ equivale aseries_systemcon n componentes idénticos.
Atributos de ReliabilityResult
| Atributo | Descripción |
|---|---|
topology |
'component', 'series', 'parallel', 'k-of-n' |
n_components |
Número de componentes |
failure_rates |
Lista de λi |
mtbf |
MTBF del sistema |
R_t |
Confiabilidad R(t) si se proporcionó t |
availability |
Disponibilidad estacionaria A si se proporcionó mttr |
mttr |
Tiempo medio de reparación (si se proporcionó) |
df = r.to_frame() # columnas: Topología, Componentes, MTBF[, R(t), Disponibilidad]
r.R(t=200) # evalúa R(t) en cualquier tiempo posterior
Distribución de Weibull — weibull_analysis
Ajusta una Weibull de dos parámetros (forma β, escala η) a tiempos de falla completos por máxima
verosimilitud (method="MLE", bisección sobre la ecuación de verosimilitud) o por regresión de rangos
(method="RRY", rangos medianos de Benard). β < 1 indica mortalidad infantil, β ≈ 1 fallas aleatorias y
β > 1 desgaste.
tiempos = [12.5, 18.3, 24.1, 31.7, 38.2, 45.9, 52.4, 61.0, 70.8, 85.3]
w = wl.weibull_analysis(tiempos)
print(w) # β ≈ 2.10, η ≈ 49.86, MTTF ≈ 44.16
print(w.R(30), w.F(30)) # confiabilidad y probabilidad de falla a t = 30
print(w.h(30)) # tasa de falla instantánea
print(w.b10, w.b50, w.b_life(5)) # vidas B10, B50 y B5
print(w.to_frame())
w_rry = wl.weibull_analysis(tiempos, method="RRY")
Se necesitan al menos 2 tiempos finitos y positivos; si todos son iguales la forma no es estimable
(ValueError) y, si casi no hay dispersión, se avisa (UserWarning) de que β alcanzó la cota superior de 100.
15. Proyectos: CPM y PERT — project
Programación de proyectos sobre una red de actividades. Cada actividad es un diccionario con name,
predecessors (lista de nombres, puede omitirse) y su duración. Los nombres deben ser únicos; los
ciclos y los predecesores desconocidos lanzan ValueError.
CPM — duraciones determinísticas
Paso hacia adelante (ES, EF) y hacia atrás (LS, LF); holgura total HT = LS − ES, holgura libre HL y ruta crítica (HT = 0).
acts = [
{"name": "A", "duration": 3, "predecessors": []},
{"name": "B", "duration": 4, "predecessors": ["A"]},
{"name": "C", "duration": 2, "predecessors": ["A"]},
{"name": "D", "duration": 1, "predecessors": ["B", "C"]},
]
r = wl.cpm(acts)
print(r) # duración 8, ruta crítica A → B → D
print(r.to_frame()) # ES, EF, LS, LF, HT, HL y si es crítica
print(r.critical_path) # ['A', 'B', 'D']
PERT — tres estimaciones por actividad
Duración esperada tₑ = (a + 4m + b)/6 y varianza ((b − a)/6)²; la varianza del proyecto es la suma de las
de la ruta crítica. probability(T) da P(duración ≤ T) con aproximación normal.
acts = [
{"name": "A", "optimistic": 1, "most_likely": 3, "pessimistic": 5, "predecessors": []},
{"name": "B", "optimistic": 2, "most_likely": 4, "pessimistic": 9, "predecessors": ["A"]},
]
p = wl.pert(acts)
print(p) # duración esperada 7.5, σ ≈ 1.34
print(p.probability(9.0)) # ≈ 0.868: probabilidad de terminar en 9 unidades o menos
probability solo está disponible en resultados PERT con varianza distinta de cero.
16. Árboles de KPI
Los árboles de KPI crean jerarquías interactivas visualizadas como treemap o sunburst con Plotly.
oee_kpi_tree(availability, performance, quality)
tree = wl.oee_kpi_tree(0.90, 0.80, 0.95)
tree.plot() # treemap (por defecto)
tree.plot(kind="sunburst") # diagrama radial
throughput_kpi_tree(actual_throughput, capacity, defect_rate)
tree = wl.throughput_kpi_tree(
actual_throughput=750,
capacity=1000,
defect_rate=0.05,
)
tree.plot()
roi_kpi_tree(revenue, fixed_cost, variable_cost_per_unit, units_sold, investment)
tree = wl.roi_kpi_tree(
revenue=50_000,
fixed_cost=10_000,
variable_cost_per_unit=8,
units_sold=2_000,
investment=20_000,
)
print(tree.value) # ROI = 0.2 (20%)
tree.plot()
KPINode — árbol personalizado
rev = wl.KPINode("Revenue", value=200_000, unit="€", children=[
wl.KPINode("Product A", value=120_000, unit="€"),
wl.KPINode("Product B", value=80_000, unit="€"),
])
cost = wl.KPINode("Operating costs", value=100_000, unit="€")
root = wl.KPINode("EBITDA", value=100_000, unit="€", formula="Revenue − Operating costs",
children=[rev, cost])
from walopy.plotting import plot_kpi_tree
fig = plot_kpi_tree(root, kind="treemap")
fig.show()
17. Solvers y optimización
solve_lam(target_metric, target_value, *, mu, model) — Máxima tasa de llegada
Encuentra la mayor λ tal que una métrica no supere el objetivo.
r = wl.solve_lam("Wq", 0.5, mu=5.0, model="mm1")
print(r.value) # λ máxima ≈ 3.33
print(r.achieved_value) # Wq ≈ 0.5
print(r.model_result) # QueueResult completo en la solución
solve_mu(target_metric, target_value, *, lam, model) — Mínima tasa de servicio
r = wl.solve_mu("Wq", 0.3, lam=3.0, model="mm1")
print(r.value) # μ mínima necesaria ≈ 5.0
solve_servers(target_metric, target_value, *, lam, mu) — Mínimo número de servidores
r = wl.solve_servers("Wq", 0.1, lam=8.0, mu=5.0)
print(r.value) # c mínimo = 3
print(r.model_result) # QueueResult para M/M/3
optimize_servers(lam, mu, cost_per_server, cost_per_wait) — Optimización de costo total
Minimiza cost_per_server × c + cost_per_wait × λ × Wq evaluando todos los c viables.
r = wl.optimize_servers(
lam=6.0,
mu=5.0,
cost_per_server=10.0, # costo fijo por servidor por unidad de tiempo
cost_per_wait=5.0, # costo por unidad de tiempo de espera de cada cliente
)
print(r.optimal_servers) # c óptimo
print(r.min_cost) # costo mínimo
r.plot() # gráfica de costo vs número de servidores
18. Análisis de sensibilidad
sensitivity(model_fn, param, values, **fixed_kwargs) — Barrido paramétrico
Evalúa un modelo sobre un rango de valores de un parámetro.
import numpy as np
df = wl.sensitivity(
wl.mm1,
param="lam",
values=np.linspace(0.5, 4.5, 20),
mu=5.0, # parámetros fijos
)
print(df.columns.tolist()) # ['lam', 'rho', 'L', 'Lq', 'W', 'Wq', ...]
# Barrido sobre μ
df2 = wl.sensitivity(wl.mm1, "mu", np.linspace(4.0, 10.0, 15), lam=3.0)
# Barrido sobre número de servidores en M/M/c
df3 = wl.sensitivity(wl.mmc, "c", range(1, 6), lam=8.0, mu=5.0)
# Visualizar
from walopy.plotting import plot_sensitivity
fig = plot_sensitivity(df, "lam", metrics=["Wq", "Lq"])
fig.show()
19. Escenarios en lote (batch_model)
batch_model(model_fn, df, **fixed_kwargs) — Aplicar modelo a un DataFrame
Aplica cualquier función de walopy a cada fila de un DataFrame de escenarios.
import pandas as pd
# Varios escenarios con distintas tasas de llegada
escenarios = pd.DataFrame({
"lam": [1.0, 2.0, 3.0, 4.0]
})
resultado = wl.batch_model(wl.mm1, escenarios, mu=5.0)
print(resultado[["lam", "rho", "Wq", "L"]])
# Escenarios mixtos (lambda y mu variables)
escenarios2 = pd.DataFrame({
"lam": [2.0, 3.0, 4.0],
"mu": [5.0, 6.0, 7.0],
})
resultado2 = wl.batch_model(wl.mm1, escenarios2)
# Con número de servidores variable (columna int)
escenarios3 = pd.DataFrame({
"c": [1, 2, 3, 4],
})
resultado3 = wl.batch_model(wl.mmc, escenarios3, lam=8.0, mu=5.0)
# Las filas con error quedan marcadas en "_error" (la columna solo existe si alguna fila falla;
# aquí c=1 es inestable porque λ=8 > μ=5)
print(resultado3[resultado3["_error"].notna()]) # filas fallidas
# Para que la primera excepción se propague en lugar de recogerse:
# wl.batch_model(wl.mmc, escenarios3, errors="raise", lam=8.0, mu=5.0)
20. Comparar múltiples resultados
compare(*results, labels) — DataFrame comparativo
Construye un DataFrame con una fila por resultado para facilitar la comparación.
df = wl.compare(
wl.mm1(3.0, 5.0),
wl.mmc(3.0, 5.0, 2),
wl.md1(3.0, 5.0),
labels=["M/M/1", "M/M/2", "M/D/1"],
)
print(df[["label", "ρ (utilización)", "Wq (tiempo de espera)", "L (en el sistema)"]])
# label ρ (utilización) Wq (tiempo de espera) L (en el sistema)
# 0 M/M/1 0.6 0.3000 1.50
# 1 M/M/2 0.3 0.0302 1.02
# 2 M/D/1 0.6 0.1500 1.05
# Sin etiquetas → scenario_1, scenario_2, ...
df2 = wl.compare(wl.mm1(1, 5), wl.mm1(2, 5), wl.mm1(3, 5))
21. Gráficas
Todos los resultados exponen .plot() que devuelve una figura de Matplotlib o Plotly. También se pueden llamar directamente desde walopy.plotting.
Función .plot() |
Tipo | Descripción |
|---|---|---|
QueueResult.plot() |
Matplotlib | Curvas Wq y Lq vs ρ con punto operativo |
OEEResult.plot() |
Matplotlib | Barras horizontales OEE con referencia 85% |
BottleneckResult.plot() |
Matplotlib | Utilización por estación, cuello de botella en rojo |
EOQResult.plot() |
Matplotlib | Curvas de costo de mantener, ordenar y total |
LineBalanceResult.plot() |
Plotly | Tiempo de ciclo vs takt por estación |
BreakEvenResult.plot() |
Plotly | Líneas ingreso/costo con BEP y zona de beneficio |
SimulationResult.plot() |
Plotly | Histograma de Wq + CDF empírica con percentiles |
OptimizeResult.plot() |
Plotly | Costo por servidor, costo de espera y total vs c |
KPINode.plot(kind=) |
Plotly | Treemap o sunburst interactivo |
# Guardar como HTML interactivo (Plotly)
fig = r.plot()
fig.write_html("resultado.html")
# Guardar como imagen (Matplotlib)
fig = wl.oee(0.9, 0.8, 0.95).plot()
fig.savefig("oee.png", dpi=150)
Para la curva de sensibilidad:
from walopy.plotting import plot_sensitivity
df = wl.sensitivity(wl.mm1, "lam", np.linspace(0.5, 4.5, 20), mu=5.0)
fig = plot_sensitivity(df, "lam", metrics=["Wq", "Lq", "L"])
fig.show()
22. CLI
walopy incluye una interfaz de línea de comandos para uso rápido sin escribir código.
# M/M/1
python -m walopy mm1 --lam 3 --mu 5
# M/M/c
python -m walopy mmc --lam 8 --mu 5 --c 2
# M/D/1
python -m walopy md1 --lam 3 --mu 5
# G/G/1 (Kingman)
python -m walopy gg1 --lam 3 --mu 5 --ca2 1.2 --cs2 0.8
# Ley de Little (resolver W)
python -m walopy littles --L 2.5 --lam 5.0
# EOQ
python -m walopy eoq --demand 1000 --ordering 50 --holding 2
# Versión
python -m walopy --version
Ejemplo de salida:
Modelo : M/M/1
λ : 3.0 (tasa de llegada)
μ : 5.0 (tasa de servicio por servidor)
ρ : 0.6 (utilización por servidor)
L : 1.5 (unidades promedio en el sistema)
Lq : 0.9 (unidades promedio en cola)
W : 0.5 (tiempo promedio en el sistema)
Wq : 0.3 (tiempo promedio de espera en cola)
P0 : 0.4
23. Referencia de módulos
| Módulo | Funciones y clases principales |
|---|---|
queuing |
mm1, mmc, md1, kingman, mg1, littles_law, QueueResult, cv2_normal, cv2_triangular, cv2_uniform, cv2_erlang, cv2_gamma, cv2_lognormal, cv2_weibull |
advanced |
mm1k, mmck, erlang_b, mm1_priority, monte_carlo_gg1, takt_time, line_balance, break_even, break_even_multi, break_even_sales, queue_length_pmf, sojourn_cdf, PriorityQueueResult, SimulationResult, LineBalanceResult, BreakEvenResult, BreakEvenMultiResult |
fitting |
fit_from_data, FitResult |
inventory |
eoq, ebq, eoq_multi, ebq_multi, eoq_multi_constrained, lot_for_lot, silver_meal, eoq_quantity_discount, wagner_whitin, rq_policy, rs_policy, exchange_curve, safety_stock_curve, abc_analysis, xyz_analysis, abc_xyz, mrp, reorder_point, newsvendor, EOQResult, EBQResult, MultiItemResult, ConstrainedMultiEOQResult, LotSizingResult, QuantityDiscountResult, ReorderResult, NewsvendorResult, RQPolicyResult, RSPolicyResult, ExchangeCurveResult, SafetyStockCurveResult, ABCResult, XYZResult, ABCXYZResult, MRPResult |
scheduling |
schedule_single, johnson_flowshop, neh_flowshop, ScheduleResult, JobSchedule, FlowShopResult, NEHResult |
project |
cpm, pert, ProjectResult, ActivityResult |
reliability |
mtbf_analysis, series_system, parallel_system, koon_system, weibull_analysis, ReliabilityResult, WeibullResult |
network |
jackson_network, JacksonResult, StationMetrics |
operations |
oee, utilization_efficiency, unit_cost, OEEResult, UtilizationResult, UnitCostResult |
bottleneck |
bottleneck_analysis, BottleneckResult, StationResult |
kpi |
KPINode, oee_kpi_tree, throughput_kpi_tree, roi_kpi_tree |
solver |
solve_lam, solve_mu, solve_servers, optimize_servers, sensitivity, batch_model, compare, SolverResult, OptimizeResult |
plotting |
plot_queue_sensitivity, plot_queue_metrics, plot_oee, plot_bottleneck, plot_eoq, plot_kpi_tree, plot_sensitivity, plot_optimize_servers, plot_simulation, plot_line_balance, plot_break_even, plot_queue_distribution |
24. Idioma de los textos
Los mensajes de error, avisos, etiquetas de summary(), cabeceras de to_frame(), títulos de gráficas y la ayuda del CLI se muestran en el
idioma activo: español ("es", por defecto) o inglés ("en"). Si algún texto no estuviera traducido, se muestra en español: nunca falla por eso.
El idioma se elige, de mayor a menor prioridad, con:
with walopy.language("en"):— solo dentro del bloque (seguro con hilos yasyncio).walopy.set_language("en")— para todo el proceso.- La variable de entorno
WALOPY_LANG=en(también para el CLI). Un valor desconocido se ignora y se usa el español.
import walopy as wl
wl.get_language() # 'es' (por defecto)
with wl.language("en"): # cambia el idioma solo dentro del bloque
print(wl.mm1(2, 3).summary().splitlines()[1]) # λ : 2 (arrival rate)
print(list(wl.mm1(2, 3).to_frame().columns)[:3])
wl.set_language("es") # fija el idioma para todo el proceso
try:
wl.set_language("fr") # un idioma no admitido lanza ValueError
except ValueError as e:
print(e)
Qué no cambia con el idioma: los nombres de funciones y argumentos, las claves de result.params (por ejemplo "P0 (prob. de sistema vacío)";
summary() sí muestra su etiqueta traducida) y los nombres de las claves de datos ("Q*", "name", las clases "A"/"B"/"C"). Los números
tampoco cambian. Sí cambian, porque se crean en el idioma activo en ese momento: los nombres de las columnas de los DataFrame
("Artículo" → "Item"), las claves de texto de las tablas internas (result.items[0]["Producto"]), el texto de result.model o result.method
cuando es una frase ("Lote por lote" → "Lot-for-lot") y los nombres por defecto (Artículo-1 → Item-1). Si cambias de idioma entre crear
un resultado y mostrarlo o graficarlo, walopy sigue encontrando sus columnas; tu propio código debe usar el nombre que la tabla tenga.
Desde la línea de comandos: WALOPY_LANG=en python -m walopy mm1 --lam 2 --mu 3.
25. Requisitos
Python ≥ 3.9
numpy ≥ 1.22
pandas ≥ 1.4
matplotlib ≥ 3.5
plotly ≥ 5.0
No se requiere scipy. Los cálculos estadísticos usan statistics.NormalDist de la biblioteca estándar de Python.
Licencia
MIT — ver LICENSE.
Contribuciones
Pull requests bienvenidas. Asegúrate de que pytest tests/ pase antes de abrir un PR.
Metadata
Release files for walopy 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Loss of provenance
These files do not have provenance metadata, despite files in a prior release including this data.
We recommend further investigation before installing these files. View details.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| walopy-0.4.0.tar.gz | 214.0 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| walopy-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 345.7 kB
Release files / walopy-0.4.0.tar.gz
Loss of provenance
This file belongs to a release without provenance metadata, despite a prior release including this data.
We recommend further investigation before installing this file. View details.
| Download URL | walopy-0.4.0.tar.gz |
|---|---|
| Size | 214.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bc718c2e18dda1fabf8235e52316d519f1fc7ecfe3c28f6908f5e0bd1bc9771c
|
|
BLAKE2b-256 checksum How to use checksums |
39ea56af2d741e0f3619d47d1a4edb3e2f9251a55a7316fbea8cd659e6563646
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / walopy-0.4.0-py3-none-any.whl
Loss of provenance
This file belongs to a release without provenance metadata, despite a prior release including this data.
We recommend further investigation before installing this file. View details.
| Download URL | walopy-0.4.0-py3-none-any.whl |
|---|---|
| Size | 131.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
677d0b01db398e6e7e705dc3e0ed650d5187c8ac4acb096b74f46f0488758e16
|
|
BLAKE2b-256 checksum How to use checksums |
b0f3238dc5522675ca188181aa462167be5c1d2b0d9109bef8c753745b812164
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Security for walopy 0.4.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Loss of provenance
A previous release of these files had provenance metadata, but this release does not.
While this could reflect a routine change by the project maintainers, it may also indicate that the release files have been compromised.
Release comparison
- Removed 0.2.6 2 files With provenance
- Added 0.4.0 2 files Without provenance
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.