walopy 0.2.1__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- walopy-0.2.2/PKG-INFO +1071 -0
- walopy-0.2.2/README.md +1034 -0
- {walopy-0.2.1 → walopy-0.2.2}/pyproject.toml +1 -1
- walopy-0.2.2/src/walopy.egg-info/PKG-INFO +1071 -0
- walopy-0.2.1/PKG-INFO +0 -214
- walopy-0.2.1/README.md +0 -177
- walopy-0.2.1/src/walopy.egg-info/PKG-INFO +0 -214
- {walopy-0.2.1 → walopy-0.2.2}/LICENSE +0 -0
- {walopy-0.2.1 → walopy-0.2.2}/setup.cfg +0 -0
- {walopy-0.2.1 → walopy-0.2.2}/src/walopy/__init__.py +0 -0
- {walopy-0.2.1 → walopy-0.2.2}/src/walopy/__main__.py +0 -0
- {walopy-0.2.1 → walopy-0.2.2}/src/walopy/_utils.py +0 -0
- {walopy-0.2.1 → walopy-0.2.2}/src/walopy/advanced.py +0 -0
- {walopy-0.2.1 → walopy-0.2.2}/src/walopy/bottleneck.py +0 -0
- {walopy-0.2.1 → walopy-0.2.2}/src/walopy/fitting.py +0 -0
- {walopy-0.2.1 → walopy-0.2.2}/src/walopy/inventory.py +0 -0
- {walopy-0.2.1 → walopy-0.2.2}/src/walopy/kpi.py +0 -0
- {walopy-0.2.1 → walopy-0.2.2}/src/walopy/network.py +0 -0
- {walopy-0.2.1 → walopy-0.2.2}/src/walopy/operations.py +0 -0
- {walopy-0.2.1 → walopy-0.2.2}/src/walopy/plotting.py +0 -0
- {walopy-0.2.1 → walopy-0.2.2}/src/walopy/queuing.py +0 -0
- {walopy-0.2.1 → walopy-0.2.2}/src/walopy/solver.py +0 -0
- {walopy-0.2.1 → walopy-0.2.2}/src/walopy.egg-info/SOURCES.txt +0 -0
- {walopy-0.2.1 → walopy-0.2.2}/src/walopy.egg-info/dependency_links.txt +0 -0
- {walopy-0.2.1 → walopy-0.2.2}/src/walopy.egg-info/requires.txt +0 -0
- {walopy-0.2.1 → walopy-0.2.2}/src/walopy.egg-info/top_level.txt +0 -0
- {walopy-0.2.1 → walopy-0.2.2}/tests/test_advanced.py +0 -0
- {walopy-0.2.1 → walopy-0.2.2}/tests/test_bottleneck.py +0 -0
- {walopy-0.2.1 → walopy-0.2.2}/tests/test_fitting.py +0 -0
- {walopy-0.2.1 → walopy-0.2.2}/tests/test_inventory.py +0 -0
- {walopy-0.2.1 → walopy-0.2.2}/tests/test_kpi.py +0 -0
- {walopy-0.2.1 → walopy-0.2.2}/tests/test_network.py +0 -0
- {walopy-0.2.1 → walopy-0.2.2}/tests/test_new_models.py +0 -0
- {walopy-0.2.1 → walopy-0.2.2}/tests/test_operations.py +0 -0
- {walopy-0.2.1 → walopy-0.2.2}/tests/test_queuing.py +0 -0
- {walopy-0.2.1 → walopy-0.2.2}/tests/test_solver.py +0 -0
walopy-0.2.2/PKG-INFO
ADDED
|
@@ -0,0 +1,1071 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: walopy
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: Queuing theory, operations analysis, OEE, bottleneck analysis and KPI trees for Python
|
|
5
|
+
Author-email: LeoSanta15 <angelsanta1@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/LeoSanta15/walopy
|
|
8
|
+
Project-URL: Issues, https://github.com/LeoSanta15/walopy/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/LeoSanta15/walopy/blob/main/CHANGELOG.md
|
|
10
|
+
Keywords: queuing theory,operations research,little's law,kingman,bottleneck,OEE,efficiency,utilization,unit cost,KPI
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Intended Audience :: Science/Research
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: numpy>=1.22
|
|
24
|
+
Requires-Dist: pandas>=1.4
|
|
25
|
+
Requires-Dist: matplotlib>=3.5
|
|
26
|
+
Requires-Dist: plotly>=5.0
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-cov>=4; extra == "dev"
|
|
30
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
31
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
32
|
+
Provides-Extra: docs
|
|
33
|
+
Requires-Dist: sphinx>=7; extra == "docs"
|
|
34
|
+
Requires-Dist: sphinx-rtd-theme>=2; extra == "docs"
|
|
35
|
+
Requires-Dist: myst-parser>=2; extra == "docs"
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# walopy
|
|
39
|
+
|
|
40
|
+
**Teoría de colas, análisis de operaciones, modelos de inventario, árboles de KPI y más para Python.**
|
|
41
|
+
|
|
42
|
+
[](https://pypi.org/project/walopy/)
|
|
43
|
+
[](https://pypi.org/project/walopy/)
|
|
44
|
+
[](LICENSE)
|
|
45
|
+
[](https://github.com/LeoSanta15/walopy/actions/workflows/ci.yml)
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install walopy
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Tabla de contenidos
|
|
54
|
+
|
|
55
|
+
1. [Modelos de colas clásicos](#1-modelos-de-colas-clásicos)
|
|
56
|
+
2. [Modelos avanzados y de capacidad finita](#2-modelos-avanzados-y-de-capacidad-finita)
|
|
57
|
+
3. [Colas con prioridad](#3-colas-con-prioridad)
|
|
58
|
+
4. [Simulación Monte Carlo G/G/1](#4-simulación-monte-carlo-gg1)
|
|
59
|
+
5. [Ajuste de parámetros desde datos reales](#5-ajuste-de-parámetros-desde-datos-reales)
|
|
60
|
+
6. [Redes de Jackson](#6-redes-de-jackson)
|
|
61
|
+
7. [Inventarios](#7-inventarios)
|
|
62
|
+
8. [Análisis de operaciones](#8-análisis-de-operaciones)
|
|
63
|
+
9. [Análisis de cuellos de botella](#9-análisis-de-cuellos-de-botella)
|
|
64
|
+
10. [Balance de línea y tiempo takt](#10-balance-de-línea-y-tiempo-takt)
|
|
65
|
+
11. [Análisis de punto de equilibrio](#11-análisis-de-punto-de-equilibrio)
|
|
66
|
+
12. [Árboles de KPI](#12-árboles-de-kpi)
|
|
67
|
+
13. [Solvers y optimización](#13-solvers-y-optimización)
|
|
68
|
+
14. [Análisis de sensibilidad](#14-análisis-de-sensibilidad)
|
|
69
|
+
15. [Escenarios en lote (batch_model)](#15-escenarios-en-lote-batch_model)
|
|
70
|
+
16. [Comparar múltiples resultados](#16-comparar-múltiples-resultados)
|
|
71
|
+
17. [Gráficas](#17-gráficas)
|
|
72
|
+
18. [CLI](#18-cli)
|
|
73
|
+
19. [Referencia de módulos](#19-referencia-de-módulos)
|
|
74
|
+
20. [Requisitos](#20-requisitos)
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 1. Modelos de colas clásicos
|
|
79
|
+
|
|
80
|
+
Todos los modelos devuelven un `QueueResult` con los indicadores estándar de teoría de colas.
|
|
81
|
+
|
|
82
|
+
### `QueueResult` — atributos principales
|
|
83
|
+
|
|
84
|
+
| Atributo | Descripción |
|
|
85
|
+
|---|---|
|
|
86
|
+
| `model` | Nombre del modelo (ej. `"M/M/1"`) |
|
|
87
|
+
| `lam` | Tasa de llegada λ |
|
|
88
|
+
| `mu` | Tasa de servicio μ por servidor |
|
|
89
|
+
| `servers` | Número de servidores *c* |
|
|
90
|
+
| `rho` | Utilización por servidor ρ = λ/(c·μ) |
|
|
91
|
+
| `L` | Número promedio de unidades en el sistema |
|
|
92
|
+
| `Lq` | Número promedio de unidades en la cola |
|
|
93
|
+
| `W` | Tiempo promedio en el sistema |
|
|
94
|
+
| `Wq` | Tiempo promedio de espera en cola |
|
|
95
|
+
| `params` | Parámetros adicionales específicos del modelo |
|
|
96
|
+
|
|
97
|
+
Métodos disponibles en todos los resultados:
|
|
98
|
+
- `.summary()` / `print(r)` — tabla de indicadores
|
|
99
|
+
- `.to_frame()` — exporta a `pd.DataFrame` de una fila
|
|
100
|
+
- `.plot()` — gráfica interactiva de sensibilidad
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
### `mm1(lam, mu)` — M/M/1
|
|
105
|
+
|
|
106
|
+
Un servidor, llegadas Poisson, servicio exponencial. El modelo más simple de cola.
|
|
107
|
+
|
|
108
|
+
**Fórmulas clave:**
|
|
109
|
+
- ρ = λ/μ (debe ser < 1 para estabilidad)
|
|
110
|
+
- Lq = ρ² / (1 − ρ)
|
|
111
|
+
- L = ρ / (1 − ρ)
|
|
112
|
+
- Wq = Lq / λ, W = L / λ
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
import walopy as wl
|
|
116
|
+
|
|
117
|
+
r = wl.mm1(lam=3.0, mu=5.0)
|
|
118
|
+
print(r)
|
|
119
|
+
# Model : M/M/1
|
|
120
|
+
# λ : 3.0 (arrival rate)
|
|
121
|
+
# μ : 5.0 (service rate per server)
|
|
122
|
+
# ρ : 0.6 (utilization)
|
|
123
|
+
# L : 1.5 (avg units in system)
|
|
124
|
+
# Lq : 0.9 (avg units in queue)
|
|
125
|
+
# W : 0.5 (avg time in system)
|
|
126
|
+
# Wq : 0.3 (avg wait time in queue)
|
|
127
|
+
|
|
128
|
+
df = r.to_frame() # exportar a DataFrame
|
|
129
|
+
r.plot() # gráfica Wq y Lq vs ρ
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
### `mmc(lam, mu, c)` — M/M/c
|
|
135
|
+
|
|
136
|
+
Multi-servidor con llegadas Poisson y servicio exponencial. Usa la fórmula de Erlang-C para calcular la probabilidad de espera C(c, a).
|
|
137
|
+
|
|
138
|
+
**Fórmulas:**
|
|
139
|
+
- ρ = λ/(c·μ) (utilización por servidor, debe ser < 1)
|
|
140
|
+
- a = λ/μ (carga ofrecida)
|
|
141
|
+
- C(c,a) = P(esperar) — fórmula de Erlang-C
|
|
142
|
+
- Lq = C(c,a) · ρ / (1 − ρ)
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
r = wl.mmc(lam=8.0, mu=5.0, c=2)
|
|
146
|
+
print(r.params["C(c,a) Erlang-C"]) # probabilidad de espera
|
|
147
|
+
|
|
148
|
+
r_1 = wl.mmc(lam=4.0, mu=5.0, c=1) # equivalente a mm1
|
|
149
|
+
r_3 = wl.mmc(lam=4.0, mu=5.0, c=3)
|
|
150
|
+
print(r_3.Wq < r_1.Wq) # True: más servidores → menor espera
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
### `md1(lam, mu)` — M/D/1
|
|
156
|
+
|
|
157
|
+
Llegadas Poisson, servicio **determinístico** (tiempo de servicio constante = 1/μ). Produce exactamente la mitad de cola que M/M/1 bajo la misma carga.
|
|
158
|
+
|
|
159
|
+
**Fórmulas:**
|
|
160
|
+
- Lq = ρ² / (2·(1 − ρ)) ← mitad que M/M/1
|
|
161
|
+
- L = ρ + Lq
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
r_mm1 = wl.mm1(lam=3.0, mu=5.0)
|
|
165
|
+
r_md1 = wl.md1(lam=3.0, mu=5.0)
|
|
166
|
+
print(r_md1.Lq / r_mm1.Lq) # ≈ 0.5 — M/D/1 tiene la mitad de cola
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
### `kingman(lam, mu, ca2, cs2)` — G/G/1 (aproximación VUT)
|
|
172
|
+
|
|
173
|
+
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.
|
|
174
|
+
|
|
175
|
+
**Fórmula:**
|
|
176
|
+
```
|
|
177
|
+
Wq ≈ (ρ / (1 − ρ)) · ((ca² + cs²) / 2) · (1/μ)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
| Parámetro | Descripción |
|
|
181
|
+
|---|---|
|
|
182
|
+
| `ca2` | CV² de los tiempos entre llegadas (1.0 para Poisson) |
|
|
183
|
+
| `cs2` | CV² de los tiempos de servicio (1.0 exponencial, 0.0 determinístico) |
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
# Proceso de llegada más regular que Poisson (ca2 < 1)
|
|
187
|
+
r = wl.kingman(lam=3.0, mu=5.0, ca2=0.5, cs2=1.0)
|
|
188
|
+
|
|
189
|
+
# Servicio muy variable (ca2 >> 1)
|
|
190
|
+
r = wl.kingman(lam=3.0, mu=5.0, ca2=1.0, cs2=3.0)
|
|
191
|
+
|
|
192
|
+
# Recuperar parámetros ajustados desde datos reales y usarlos directamente
|
|
193
|
+
fit = wl.fit_from_data(inter_arrivals=..., service_times=...)
|
|
194
|
+
r = wl.kingman(**fit.to_model_kwargs())
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
> **Equivalencias:**
|
|
198
|
+
> `ca2=1, cs2=1` → M/M/1
|
|
199
|
+
> `ca2=1, cs2=0` → M/D/1 (aproximado)
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
### `littles_law(*, L, lam, W)` — Ley de Little
|
|
204
|
+
|
|
205
|
+
Resuelve L = λ · W para la variable que falte. Exactamente uno de los tres parámetros debe ser `None`.
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
L = wl.littles_law(lam=5.0, W=0.4) # L = 2.0
|
|
209
|
+
W = wl.littles_law(L=2.0, lam=5.0) # W = 0.4
|
|
210
|
+
lam = wl.littles_law(L=3.0, W=0.6) # λ = 5.0
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## 2. Modelos avanzados y de capacidad finita
|
|
216
|
+
|
|
217
|
+
### `mm1k(lam, mu, K)` — M/M/1/K
|
|
218
|
+
|
|
219
|
+
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 λ > μ.
|
|
220
|
+
|
|
221
|
+
**Parámetros adicionales en `result.params`:**
|
|
222
|
+
- `PK (blocking prob)` — probabilidad de rechazo (sistema lleno)
|
|
223
|
+
- `lam_eff` — tasa de llegada efectiva = λ · (1 − P_K)
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
r = wl.mm1k(lam=5.0, mu=3.0, K=10)
|
|
227
|
+
print(r.params["PK (blocking prob)"]) # fracción de clientes rechazados
|
|
228
|
+
print(r.params["lam_eff"]) # tasa real de entrada al sistema
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
### `mmck(lam, mu, c, K)` — M/M/c/K
|
|
234
|
+
|
|
235
|
+
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.
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
r = wl.mmck(lam=8.0, mu=3.0, c=2, K=20)
|
|
239
|
+
print(r.params["PK (blocking prob)"])
|
|
240
|
+
|
|
241
|
+
# Verificación: con K muy grande se acerca a M/M/c
|
|
242
|
+
r_inf = wl.mmc(lam=3.0, mu=5.0, c=2)
|
|
243
|
+
r_fin = wl.mmck(lam=3.0, mu=5.0, c=2, K=500)
|
|
244
|
+
print(abs(r_fin.Lq - r_inf.Lq) < 0.01) # True
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**Restricción:** K debe ser ≥ c (la capacidad total no puede ser menor que el número de servidores).
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
### `erlang_b(lam, mu, c)` — Erlang B
|
|
252
|
+
|
|
253
|
+
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.
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
pb = wl.erlang_b(lam=5.0, mu=1.0, c=8)
|
|
257
|
+
print(f"Blocking probability: {pb:.2%}") # ej. 1.3%
|
|
258
|
+
|
|
259
|
+
# Cuántos servidores para bloqueo < 2%?
|
|
260
|
+
for c in range(1, 20):
|
|
261
|
+
if wl.erlang_b(5.0, 1.0, c) < 0.02:
|
|
262
|
+
print(f"Need c = {c} servers")
|
|
263
|
+
break
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
### `queue_length_pmf(lam, mu, n_max)` — PMF de longitud de cola
|
|
269
|
+
|
|
270
|
+
Distribución de probabilidad del número de clientes en el sistema para M/M/1.
|
|
271
|
+
Devuelve un `pd.DataFrame` con columnas `n`, `P(N=n)`, `P(N<=n)`.
|
|
272
|
+
|
|
273
|
+
```python
|
|
274
|
+
df = wl.queue_length_pmf(lam=3.0, mu=5.0, n_max=20)
|
|
275
|
+
print(df.head())
|
|
276
|
+
# n P(N=n) P(N<=n)
|
|
277
|
+
# 0 0.400 0.400
|
|
278
|
+
# 1 0.240 0.640
|
|
279
|
+
# 2 0.144 0.784
|
|
280
|
+
# ...
|
|
281
|
+
|
|
282
|
+
# ¿Probabilidad de tener más de 5 clientes en sistema?
|
|
283
|
+
prob_gt5 = 1 - df.loc[df["n"] == 5, "P(N<=n)"].iloc[0]
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
### `sojourn_cdf(lam, mu, t_max, n_points)` — CDF del tiempo de permanencia
|
|
289
|
+
|
|
290
|
+
Distribución acumulada del tiempo total en el sistema (sojourn time) para M/M/1.
|
|
291
|
+
Devuelve un `pd.DataFrame` con columnas `t`, `F(t)`, `f(t)`.
|
|
292
|
+
|
|
293
|
+
```python
|
|
294
|
+
df = wl.sojourn_cdf(lam=3.0, mu=5.0)
|
|
295
|
+
# ¿Cuánto tiempo cubre el 95% de los clientes?
|
|
296
|
+
t95 = df.loc[df["F(t)"] >= 0.95, "t"].iloc[0]
|
|
297
|
+
print(f"95% of customers leave before t = {t95:.3f}")
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## 3. Colas con prioridad
|
|
303
|
+
|
|
304
|
+
### `mm1_priority(lam_list, mu, *, class_names)` — M/M/1 HOL no-preemptiva
|
|
305
|
+
|
|
306
|
+
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.
|
|
307
|
+
|
|
308
|
+
**Fórmula de Kleinrock:**
|
|
309
|
+
```
|
|
310
|
+
Wq_k = R / ((1 − σ_{k−1}) · (1 − σ_k))
|
|
311
|
+
```
|
|
312
|
+
donde R = ρ_total / μ es el tiempo residual de servicio y σ_k = Σ_{i=0}^{k} ρ_i.
|
|
313
|
+
|
|
314
|
+
```python
|
|
315
|
+
# 3 clases: alta (λ=2), media (λ=1.5), baja (λ=0.5), μ=5 compartido
|
|
316
|
+
pri = wl.mm1_priority([2.0, 1.5, 0.5], mu=5.0)
|
|
317
|
+
print(pri)
|
|
318
|
+
|
|
319
|
+
for cls in pri.classes:
|
|
320
|
+
print(f"Clase {cls['class_id']}: Wq = {cls['Wq']:.4f}")
|
|
321
|
+
|
|
322
|
+
# Con nombres de clase personalizados
|
|
323
|
+
pri = wl.mm1_priority(
|
|
324
|
+
[2.0, 1.0],
|
|
325
|
+
mu=5.0,
|
|
326
|
+
class_names=["VIP", "Estándar"],
|
|
327
|
+
)
|
|
328
|
+
print(pri.classes[0]["Wq"]) # VIP espera mucho menos
|
|
329
|
+
print(pri.classes[1]["Wq"]) # Estándar espera más que en cola FIFO
|
|
330
|
+
|
|
331
|
+
df = pri.to_frame() # DataFrame con una fila por clase
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
**`PriorityQueueResult` — atributos:**
|
|
335
|
+
|
|
336
|
+
| Atributo | Descripción |
|
|
337
|
+
|---|---|
|
|
338
|
+
| `classes` | Lista de dicts con `class_id`, `lam`, `rho`, `Wq`, `W`, `Lq`, `L` por clase |
|
|
339
|
+
| `rho_total` | Utilización total del servidor |
|
|
340
|
+
| `mu` | Tasa de servicio compartida |
|
|
341
|
+
|
|
342
|
+
**Restricción:** la suma de todas las tasas de llegada debe ser < μ (sistema estable).
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## 4. Simulación Monte Carlo G/G/1
|
|
347
|
+
|
|
348
|
+
### `monte_carlo_gg1(lam, mu, ca2, cs2, n_customers, seed)` — Simulación event-driven
|
|
349
|
+
|
|
350
|
+
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.
|
|
351
|
+
|
|
352
|
+
```python
|
|
353
|
+
sim = wl.monte_carlo_gg1(
|
|
354
|
+
lam=3.0, mu=5.0,
|
|
355
|
+
ca2=1.0, cs2=0.5, # ca2=1 → Poisson, cs2=0.5 → Erlang-2
|
|
356
|
+
n_customers=50_000,
|
|
357
|
+
seed=42,
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
print(sim.Wq_mean) # promedio
|
|
361
|
+
print(sim.Wq_p50) # mediana
|
|
362
|
+
print(sim.Wq_p90) # percentil 90
|
|
363
|
+
print(sim.Wq_p95) # percentil 95
|
|
364
|
+
print(sim.Wq_p99) # percentil 99
|
|
365
|
+
|
|
366
|
+
sim.plot() # histograma + CDF empírica con percentiles anotados
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
**`SimulationResult` — atributos:**
|
|
370
|
+
|
|
371
|
+
| Atributo | Descripción |
|
|
372
|
+
|---|---|
|
|
373
|
+
| `Wq_mean` | Tiempo promedio de espera simulado |
|
|
374
|
+
| `Wq_p50/p90/p95/p99` | Percentiles de la distribución de espera |
|
|
375
|
+
| `L_mean`, `Lq_mean` | Longitudes de sistema y cola promedio |
|
|
376
|
+
| `model` | Descripción del modelo simulado |
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
## 5. Ajuste de parámetros desde datos reales
|
|
381
|
+
|
|
382
|
+
### `fit_from_data(inter_arrivals, service_times, *, arrival_timestamps)` — Estimación de parámetros
|
|
383
|
+
|
|
384
|
+
Estima λ, μ, ca², cs² directamente desde datos observados. Útil para calibrar modelos cuando los parámetros no se conocen teóricamente.
|
|
385
|
+
|
|
386
|
+
**Formas de uso:**
|
|
387
|
+
|
|
388
|
+
```python
|
|
389
|
+
import numpy as np
|
|
390
|
+
import walopy as wl
|
|
391
|
+
|
|
392
|
+
rng = np.random.default_rng(42)
|
|
393
|
+
|
|
394
|
+
# 1. Desde tiempos entre llegadas y tiempos de servicio
|
|
395
|
+
ia = rng.exponential(scale=0.2, size=1000) # media = 0.2 → λ ≈ 5
|
|
396
|
+
svc = rng.exponential(scale=0.1, size=1000) # media = 0.1 → μ ≈ 10
|
|
397
|
+
|
|
398
|
+
fit = wl.fit_from_data(ia, svc)
|
|
399
|
+
print(fit)
|
|
400
|
+
# λ ≈ 5.0 ca² ≈ 1.0 (exponencial → CV² = 1)
|
|
401
|
+
# μ ≈ 10.0 cs² ≈ 1.0
|
|
402
|
+
|
|
403
|
+
# 2. Solo tiempos entre llegadas (sin datos de servicio)
|
|
404
|
+
fit_ia = wl.fit_from_data(ia)
|
|
405
|
+
print(fit_ia.lam, fit_ia.ca2)
|
|
406
|
+
print(fit_ia.mu) # None
|
|
407
|
+
|
|
408
|
+
# 3. Desde timestamps crudos (no se tienen los intervalos directamente)
|
|
409
|
+
ts = np.cumsum(rng.exponential(0.2, 1000)) # timestamps acumulados
|
|
410
|
+
fit = wl.fit_from_data(arrival_timestamps=ts)
|
|
411
|
+
|
|
412
|
+
# 4. Conectar directamente con un modelo
|
|
413
|
+
r = wl.kingman(**fit.to_model_kwargs()) # usa lam, mu, ca2, cs2
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
**`FitResult` — atributos:**
|
|
417
|
+
|
|
418
|
+
| Atributo | Descripción |
|
|
419
|
+
|---|---|
|
|
420
|
+
| `lam` | Tasa de llegada estimada (None si no hay datos de llegada) |
|
|
421
|
+
| `mu` | Tasa de servicio estimada (None si no hay datos de servicio) |
|
|
422
|
+
| `ca2` | CV² de tiempos entre llegadas |
|
|
423
|
+
| `cs2` | CV² de tiempos de servicio |
|
|
424
|
+
| `n_arrivals` | Número de observaciones de llegada |
|
|
425
|
+
| `n_services` | Número de observaciones de servicio |
|
|
426
|
+
| `mean_ia` | Media de tiempos entre llegadas |
|
|
427
|
+
| `mean_svc` | Media de tiempos de servicio |
|
|
428
|
+
|
|
429
|
+
Métodos: `.to_model_kwargs()` → dict con `lam`, `mu`, `ca2`, `cs2` listos para `kingman()`. `.to_frame()` → DataFrame.
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
## 6. Redes de Jackson
|
|
434
|
+
|
|
435
|
+
### `jackson_network(station_names, mu, gamma, routing, *, servers)` — Red abierta de colas
|
|
436
|
+
|
|
437
|
+
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:
|
|
438
|
+
|
|
439
|
+
```
|
|
440
|
+
(I − P^T) · λ = γ
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
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.
|
|
444
|
+
|
|
445
|
+
```python
|
|
446
|
+
# Red de 3 estaciones en serie con estación final de 2 servidores
|
|
447
|
+
net = wl.jackson_network(
|
|
448
|
+
station_names=["Recepción", "Control de calidad", "Empaque"],
|
|
449
|
+
mu=[10.0, 8.0, 12.0], # tasa de servicio por servidor
|
|
450
|
+
gamma=[5.0, 0.0, 0.0], # solo llegan clientes externos a Recepción
|
|
451
|
+
routing=[
|
|
452
|
+
[0.0, 1.0, 0.0], # Recepción → 100% a QC
|
|
453
|
+
[0.0, 0.0, 1.0], # QC → 100% a Empaque
|
|
454
|
+
[0.0, 0.0, 0.0], # Empaque → sale del sistema
|
|
455
|
+
],
|
|
456
|
+
servers=[1, 1, 2], # servidores por estación
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
print(net)
|
|
460
|
+
print(net.to_frame()) # DataFrame con métricas por estación
|
|
461
|
+
|
|
462
|
+
# Acceder a métricas por estación
|
|
463
|
+
for st in net.stations:
|
|
464
|
+
print(f"{st.name}: λ={st.lam_total:.2f}, Wq={st.Wq:.4f}, ρ={st.rho:.2f}")
|
|
465
|
+
|
|
466
|
+
# Indicadores del sistema completo
|
|
467
|
+
print(f"L_sistema = {net.L_system:.4f}") # clientes totales en toda la red
|
|
468
|
+
print(f"W_sistema = {net.W_system:.4f}") # tiempo total en la red
|
|
469
|
+
|
|
470
|
+
# Red con bifurcación
|
|
471
|
+
net2 = wl.jackson_network(
|
|
472
|
+
station_names=["Entrada", "Ruta A", "Ruta B"],
|
|
473
|
+
mu=[20.0, 8.0, 10.0],
|
|
474
|
+
gamma=[10.0, 0.0, 0.0],
|
|
475
|
+
routing=[
|
|
476
|
+
[0.0, 0.6, 0.4], # 60% va a Ruta A, 40% a Ruta B
|
|
477
|
+
[0.0, 0.0, 0.0],
|
|
478
|
+
[0.0, 0.0, 0.0],
|
|
479
|
+
],
|
|
480
|
+
)
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
**`JacksonResult` — atributos:**
|
|
484
|
+
|
|
485
|
+
| Atributo | Descripción |
|
|
486
|
+
|---|---|
|
|
487
|
+
| `stations` | Lista de `StationMetrics` por estación |
|
|
488
|
+
| `L_system` | Número total promedio de clientes en toda la red |
|
|
489
|
+
| `W_system` | Tiempo total promedio en la red (Little: L = λ_ext · W) |
|
|
490
|
+
|
|
491
|
+
**`StationMetrics` — atributos:**
|
|
492
|
+
|
|
493
|
+
| Atributo | Descripción |
|
|
494
|
+
|---|---|
|
|
495
|
+
| `name` | Nombre de la estación |
|
|
496
|
+
| `lam_total` | Tasa de llegada total (externa + interna) |
|
|
497
|
+
| `lam_external` | Tasa de llegada externa γ_i |
|
|
498
|
+
| `mu` | Tasa de servicio por servidor |
|
|
499
|
+
| `servers` | Número de servidores |
|
|
500
|
+
| `rho` | Utilización ρ = λ / (c·μ) |
|
|
501
|
+
| `L`, `Lq`, `W`, `Wq` | Indicadores estándar de cola |
|
|
502
|
+
|
|
503
|
+
**Restricciones:** todas las estaciones deben ser estables (ρ < 1). La suma de probabilidades de routing por fila debe ser ≤ 1.
|
|
504
|
+
|
|
505
|
+
---
|
|
506
|
+
|
|
507
|
+
## 7. Inventarios
|
|
508
|
+
|
|
509
|
+
### `eoq(demand_rate, ordering_cost, holding_cost)` — Cantidad Económica de Pedido
|
|
510
|
+
|
|
511
|
+
Fórmula de Wilson/Harris: minimiza la suma de costo de ordenar y costo de almacenar.
|
|
512
|
+
|
|
513
|
+
**Fórmula:**
|
|
514
|
+
```
|
|
515
|
+
Q* = √(2 · D · K / h)
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
En el óptimo: costo de mantener = costo de ordenar (propiedad de cruce).
|
|
519
|
+
|
|
520
|
+
```python
|
|
521
|
+
r = wl.eoq(
|
|
522
|
+
demand_rate=1000, # D: unidades por período
|
|
523
|
+
ordering_cost=50, # K: costo fijo por orden
|
|
524
|
+
holding_cost=2, # h: costo por unidad por período
|
|
525
|
+
)
|
|
526
|
+
print(r)
|
|
527
|
+
# EOQ (order quantity): 223.6 units
|
|
528
|
+
# Order frequency : 4.472 orders/period
|
|
529
|
+
# Cycle time : 0.2236 periods
|
|
530
|
+
# Total cost : 447.2
|
|
531
|
+
# Holding cost : 223.6
|
|
532
|
+
# Ordering cost : 223.6 ← iguales en el óptimo
|
|
533
|
+
|
|
534
|
+
r.plot() # gráfica de curvas de costo
|
|
535
|
+
df = r.to_frame()
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
**`EOQResult` — atributos:**
|
|
539
|
+
|
|
540
|
+
| Atributo | Descripción |
|
|
541
|
+
|---|---|
|
|
542
|
+
| `eoq` | Cantidad óptima Q* |
|
|
543
|
+
| `total_cost` | Costo total mínimo por período |
|
|
544
|
+
| `holding_cost_total` | Componente de almacenamiento en Q* |
|
|
545
|
+
| `ordering_cost_total` | Componente de pedido en Q* |
|
|
546
|
+
| `order_frequency` | Número de pedidos por período D/Q* |
|
|
547
|
+
| `cycle_time` | Tiempo entre pedidos 1/(D/Q*) |
|
|
548
|
+
|
|
549
|
+
---
|
|
550
|
+
|
|
551
|
+
### `reorder_point(demand_rate, lead_time, *, demand_std, lead_time_std, service_level)` — Punto de reorden
|
|
552
|
+
|
|
553
|
+
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.
|
|
554
|
+
|
|
555
|
+
**Fórmulas:**
|
|
556
|
+
```
|
|
557
|
+
σ_DLT = √(L̄ · σ_D² + D̄² · σ_L²)
|
|
558
|
+
ROP = D̄ · L̄ + z · σ_DLT
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
```python
|
|
562
|
+
r = wl.reorder_point(
|
|
563
|
+
demand_rate=50, # demanda media por período
|
|
564
|
+
lead_time=2, # tiempo de entrega medio (en mismas unidades)
|
|
565
|
+
demand_std=10, # desviación estándar de la demanda
|
|
566
|
+
lead_time_std=0.5, # desviación estándar del lead time
|
|
567
|
+
service_level=0.95, # nivel de servicio deseado
|
|
568
|
+
)
|
|
569
|
+
print(r)
|
|
570
|
+
# Reorder point : 132.4 units
|
|
571
|
+
# Safety stock : 32.4 units
|
|
572
|
+
# Service level : 95.00%
|
|
573
|
+
# z-score : 1.645
|
|
574
|
+
# Mean demand LT : 100.0
|
|
575
|
+
# Std demand LT : 19.72
|
|
576
|
+
|
|
577
|
+
# Sin variabilidad → solo demanda esperada durante LT
|
|
578
|
+
r_det = wl.reorder_point(demand_rate=50, lead_time=2,
|
|
579
|
+
demand_std=0, lead_time_std=0)
|
|
580
|
+
print(r_det.safety_stock) # 0.0 — no se necesita buffer
|
|
581
|
+
|
|
582
|
+
# Comparar niveles de servicio
|
|
583
|
+
r_90 = wl.reorder_point(50, 2, demand_std=10, service_level=0.90)
|
|
584
|
+
r_99 = wl.reorder_point(50, 2, demand_std=10, service_level=0.99)
|
|
585
|
+
print(r_99.safety_stock > r_90.safety_stock) # True
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
**`ReorderResult` — atributos:**
|
|
589
|
+
|
|
590
|
+
| Atributo | Descripción |
|
|
591
|
+
|---|---|
|
|
592
|
+
| `reorder_point` | Nivel ROP en unidades |
|
|
593
|
+
| `safety_stock` | Stock de seguridad = z · σ_DLT |
|
|
594
|
+
| `service_level` | Nivel de servicio de ciclo |
|
|
595
|
+
| `z_score` | Factor de seguridad z |
|
|
596
|
+
| `mean_demand_lt` | Demanda esperada durante el lead time |
|
|
597
|
+
| `std_demand_lt` | Desviación estándar de la demanda durante LT |
|
|
598
|
+
|
|
599
|
+
---
|
|
600
|
+
|
|
601
|
+
### `newsvendor(demand_mean, demand_std, price, cost, *, salvage)` — Modelo del vendedor de periódicos
|
|
602
|
+
|
|
603
|
+
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).
|
|
604
|
+
|
|
605
|
+
**Fórmulas:**
|
|
606
|
+
```
|
|
607
|
+
Cu = precio − costo (costo de substock: venta perdida)
|
|
608
|
+
Co = costo − salvage (costo de sobrestock: unidad no vendida)
|
|
609
|
+
CR = Cu / (Cu + Co) (ratio crítico)
|
|
610
|
+
Q* = F⁻¹(CR) (cuantil CR de la demanda)
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
```python
|
|
614
|
+
r = wl.newsvendor(
|
|
615
|
+
demand_mean=100,
|
|
616
|
+
demand_std=20,
|
|
617
|
+
price=10, # precio de venta
|
|
618
|
+
cost=6, # costo de compra
|
|
619
|
+
salvage=2, # valor residual de unidades no vendidas
|
|
620
|
+
)
|
|
621
|
+
print(r)
|
|
622
|
+
# Optimal quantity : 100.0 units
|
|
623
|
+
# Critical ratio : 0.5
|
|
624
|
+
# Expected profit : 320.0
|
|
625
|
+
# Expected sales : 100.0
|
|
626
|
+
# Expected leftover : 0.0
|
|
627
|
+
# Expected stockout : 0.0
|
|
628
|
+
# Underage cost Cu : 4.0
|
|
629
|
+
# Overage cost Co : 4.0
|
|
630
|
+
|
|
631
|
+
# Producto con alto margen → pedir por encima de la media
|
|
632
|
+
r_alto = wl.newsvendor(demand_mean=100, demand_std=20,
|
|
633
|
+
price=50, cost=6, salvage=0)
|
|
634
|
+
print(r_alto.optimal_qty > 100) # True
|
|
635
|
+
|
|
636
|
+
# Producto con bajo margen → pedir por debajo de la media
|
|
637
|
+
r_bajo = wl.newsvendor(demand_mean=100, demand_std=20,
|
|
638
|
+
price=7, cost=6, salvage=2)
|
|
639
|
+
print(r_bajo.optimal_qty < 100) # True
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
**`NewsvendorResult` — atributos:**
|
|
643
|
+
|
|
644
|
+
| Atributo | Descripción |
|
|
645
|
+
|---|---|
|
|
646
|
+
| `optimal_qty` | Q* óptimo |
|
|
647
|
+
| `critical_ratio` | CR = Cu/(Cu+Co) |
|
|
648
|
+
| `expected_profit` | Beneficio esperado en Q* |
|
|
649
|
+
| `expected_sales` | Ventas esperadas E[min(D, Q*)] |
|
|
650
|
+
| `expected_leftover` | Sobrante esperado E[max(Q*−D, 0)] |
|
|
651
|
+
| `expected_stockout` | Rotura esperada E[max(D−Q*, 0)] |
|
|
652
|
+
| `underage_cost` | Cu — costo de substock |
|
|
653
|
+
| `overage_cost` | Co — costo de sobrestock |
|
|
654
|
+
|
|
655
|
+
---
|
|
656
|
+
|
|
657
|
+
## 8. Análisis de operaciones
|
|
658
|
+
|
|
659
|
+
### `oee(availability, performance, quality)` — OEE
|
|
660
|
+
|
|
661
|
+
Calcula la **Eficiencia Global de los Equipos** (OEE = Availability × Performance × Quality).
|
|
662
|
+
|
|
663
|
+
```python
|
|
664
|
+
r = wl.oee(
|
|
665
|
+
availability=0.90, # tiempo disponible / tiempo planificado
|
|
666
|
+
performance=0.80, # producción real / producción teórica
|
|
667
|
+
quality=0.95, # unidades buenas / unidades totales
|
|
668
|
+
)
|
|
669
|
+
print(r)
|
|
670
|
+
# OEE = 68.4%
|
|
671
|
+
# Clase mundial: ≥ 85%
|
|
672
|
+
|
|
673
|
+
r.plot() # gráfica horizontal con referencia world-class (85%)
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
### `utilization_efficiency(actual_output, max_output)` — Utilización
|
|
677
|
+
|
|
678
|
+
```python
|
|
679
|
+
r = wl.utilization_efficiency(actual_output=750, max_output=1000)
|
|
680
|
+
print(r.utilization) # 0.75 — 75% de utilización
|
|
681
|
+
print(r.idle_fraction) # 0.25 — 25% capacidad ociosa
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
### `unit_cost(fixed_cost, variable_cost, units)` — Costo unitario
|
|
685
|
+
|
|
686
|
+
```python
|
|
687
|
+
r = wl.unit_cost(fixed_cost=10_000, variable_cost=5_000, units=500)
|
|
688
|
+
print(r.unit_cost) # 30.0 $/unidad
|
|
689
|
+
print(r.fixed_cost_per_unit) # 20.0
|
|
690
|
+
print(r.var_cost_per_unit) # 10.0
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
---
|
|
694
|
+
|
|
695
|
+
## 9. Análisis de cuellos de botella
|
|
696
|
+
|
|
697
|
+
### `bottleneck_analysis(station_names, capacities, demand_rate)` — Teoría de restricciones
|
|
698
|
+
|
|
699
|
+
Identifica el cuello de botella de una línea de producción comparando la capacidad de cada estación con la demanda.
|
|
700
|
+
|
|
701
|
+
```python
|
|
702
|
+
r = wl.bottleneck_analysis(
|
|
703
|
+
station_names=["Corte", "Soldadura", "Pintura"],
|
|
704
|
+
capacities=[120.0, 80.0, 100.0], # unidades/hora por estación
|
|
705
|
+
demand_rate=70.0, # demanda requerida
|
|
706
|
+
)
|
|
707
|
+
print(r)
|
|
708
|
+
# Bottleneck: Soldadura (capacidad 80 < demanda 70 con menor margen)
|
|
709
|
+
# Utilization: Corte=58.3%, Soldadura=87.5%, Pintura=70.0%
|
|
710
|
+
|
|
711
|
+
r.plot() # barras de utilización con cuello de botella en rojo
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
---
|
|
715
|
+
|
|
716
|
+
## 10. Balance de línea y tiempo takt
|
|
717
|
+
|
|
718
|
+
### `takt_time(available_time, demand)` — Tiempo takt
|
|
719
|
+
|
|
720
|
+
Ritmo de producción requerido para satisfacer la demanda del cliente.
|
|
721
|
+
|
|
722
|
+
```python
|
|
723
|
+
takt = wl.takt_time(available_time=480, demand=60)
|
|
724
|
+
print(takt) # 8.0 min/unidad
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
### `line_balance(names, cycle_times, takt)` — Balance de línea
|
|
728
|
+
|
|
729
|
+
Analiza si cada estación puede cumplir con el tiempo takt y calcula la eficiencia de balance.
|
|
730
|
+
|
|
731
|
+
```python
|
|
732
|
+
lb = wl.line_balance(
|
|
733
|
+
names=["A", "B", "C"],
|
|
734
|
+
cycle_times=[5.0, 9.0, 4.0],
|
|
735
|
+
takt=10.0,
|
|
736
|
+
)
|
|
737
|
+
print(lb.bottleneck) # "B" — estación más lenta
|
|
738
|
+
print(lb.balance_efficiency) # 0.60 — 60% de eficiencia
|
|
739
|
+
print(lb.total_idle_time) # tiempo ocioso total
|
|
740
|
+
|
|
741
|
+
lb.plot() # barras de cycle time vs takt
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
---
|
|
745
|
+
|
|
746
|
+
## 11. Análisis de punto de equilibrio
|
|
747
|
+
|
|
748
|
+
### `break_even(fixed_cost, price_per_unit, variable_cost_per_unit, *, actual_units)` — Punto de equilibrio
|
|
749
|
+
|
|
750
|
+
```python
|
|
751
|
+
be = wl.break_even(
|
|
752
|
+
fixed_cost=10_000,
|
|
753
|
+
price_per_unit=25,
|
|
754
|
+
variable_cost_per_unit=15,
|
|
755
|
+
actual_units=1_500,
|
|
756
|
+
)
|
|
757
|
+
print(be.bep_units) # 1000.0 — punto de equilibrio en unidades
|
|
758
|
+
print(be.bep_revenue) # 25000.0 — ingresos en el punto de equilibrio
|
|
759
|
+
print(be.margin_of_safety_pct) # 33.3% — margen de seguridad sobre ventas actuales
|
|
760
|
+
print(be.profit) # 5000.0 — beneficio con actual_units
|
|
761
|
+
|
|
762
|
+
be.plot() # líneas de ingresos/costos con BEP y zona de beneficio
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
---
|
|
766
|
+
|
|
767
|
+
## 12. Árboles de KPI
|
|
768
|
+
|
|
769
|
+
Los árboles de KPI crean jerarquías interactivas visualizadas como **treemap** o **sunburst** con Plotly.
|
|
770
|
+
|
|
771
|
+
### `oee_kpi_tree(availability, performance, quality)`
|
|
772
|
+
|
|
773
|
+
```python
|
|
774
|
+
tree = wl.oee_kpi_tree(0.90, 0.80, 0.95)
|
|
775
|
+
tree.plot() # treemap (por defecto)
|
|
776
|
+
tree.plot(kind="sunburst") # diagrama radial
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
### `throughput_kpi_tree(actual_throughput, capacity, defect_rate)`
|
|
780
|
+
|
|
781
|
+
```python
|
|
782
|
+
tree = wl.throughput_kpi_tree(
|
|
783
|
+
actual_throughput=750,
|
|
784
|
+
capacity=1000,
|
|
785
|
+
defect_rate=0.05,
|
|
786
|
+
)
|
|
787
|
+
tree.plot()
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
### `roi_kpi_tree(revenue, fixed_cost, variable_cost_per_unit, units_sold, investment)`
|
|
791
|
+
|
|
792
|
+
```python
|
|
793
|
+
tree = wl.roi_kpi_tree(
|
|
794
|
+
revenue=50_000,
|
|
795
|
+
fixed_cost=10_000,
|
|
796
|
+
variable_cost_per_unit=8,
|
|
797
|
+
units_sold=2_000,
|
|
798
|
+
investment=20_000,
|
|
799
|
+
)
|
|
800
|
+
print(tree.value) # ROI = 0.2 (20%)
|
|
801
|
+
tree.plot()
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
### `KPINode` — árbol personalizado
|
|
805
|
+
|
|
806
|
+
```python
|
|
807
|
+
root = wl.KPINode("EBITDA", value=100_000, unit="€")
|
|
808
|
+
rev = root.add_child("Revenue", value=200_000, unit="€")
|
|
809
|
+
cost = root.add_child("Operating costs", value=100_000, unit="€")
|
|
810
|
+
rev.add_child("Product A", value=120_000, unit="€")
|
|
811
|
+
rev.add_child("Product B", value=80_000, unit="€")
|
|
812
|
+
|
|
813
|
+
from walopy.plotting import plot_kpi_tree
|
|
814
|
+
fig = plot_kpi_tree(root, kind="treemap")
|
|
815
|
+
fig.show()
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
---
|
|
819
|
+
|
|
820
|
+
## 13. Solvers y optimización
|
|
821
|
+
|
|
822
|
+
### `solve_lam(metric, target, *, mu, model)` — Máxima tasa de llegada
|
|
823
|
+
|
|
824
|
+
Encuentra la mayor λ tal que una métrica no supere el objetivo.
|
|
825
|
+
|
|
826
|
+
```python
|
|
827
|
+
r = wl.solve_lam("Wq", target=0.5, mu=5.0, model="mm1")
|
|
828
|
+
print(r.value) # λ máxima ≈ 3.33
|
|
829
|
+
print(r.achieved_value) # Wq ≈ 0.5
|
|
830
|
+
print(r.model_result) # QueueResult completo en la solución
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
### `solve_mu(metric, target, *, lam, model)` — Mínima tasa de servicio
|
|
834
|
+
|
|
835
|
+
```python
|
|
836
|
+
r = wl.solve_mu("Wq", target=0.3, lam=3.0, model="mm1")
|
|
837
|
+
print(r.value) # μ mínima necesaria ≈ 5.0
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
### `solve_servers(metric, target, *, lam, mu)` — Mínimo número de servidores
|
|
841
|
+
|
|
842
|
+
```python
|
|
843
|
+
r = wl.solve_servers("Wq", target=0.1, lam=8.0, mu=5.0)
|
|
844
|
+
print(r.value) # c mínimo = 3
|
|
845
|
+
print(r.model_result) # QueueResult para M/M/3
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
### `optimize_servers(lam, mu, cost_per_server, cost_per_wait)` — Optimización de costo total
|
|
849
|
+
|
|
850
|
+
Minimiza cost_per_server × c + cost_per_wait × λ × Wq evaluando todos los c viables.
|
|
851
|
+
|
|
852
|
+
```python
|
|
853
|
+
r = wl.optimize_servers(
|
|
854
|
+
lam=6.0,
|
|
855
|
+
mu=5.0,
|
|
856
|
+
cost_per_server=10.0, # costo fijo por servidor por unidad de tiempo
|
|
857
|
+
cost_per_wait=5.0, # costo por unidad de tiempo de espera de cada cliente
|
|
858
|
+
)
|
|
859
|
+
print(r.optimal_servers) # c óptimo
|
|
860
|
+
print(r.optimal_cost) # costo mínimo
|
|
861
|
+
|
|
862
|
+
r.plot() # gráfica de costo vs número de servidores
|
|
863
|
+
```
|
|
864
|
+
|
|
865
|
+
---
|
|
866
|
+
|
|
867
|
+
## 14. Análisis de sensibilidad
|
|
868
|
+
|
|
869
|
+
### `sensitivity(model_fn, param, values, **fixed_kwargs)` — Barrido paramétrico
|
|
870
|
+
|
|
871
|
+
Evalúa un modelo sobre un rango de valores de un parámetro.
|
|
872
|
+
|
|
873
|
+
```python
|
|
874
|
+
import numpy as np
|
|
875
|
+
|
|
876
|
+
df = wl.sensitivity(
|
|
877
|
+
wl.mm1,
|
|
878
|
+
param="lam",
|
|
879
|
+
values=np.linspace(0.5, 4.5, 20),
|
|
880
|
+
mu=5.0, # parámetros fijos
|
|
881
|
+
)
|
|
882
|
+
print(df.columns.tolist()) # ['lam', 'rho', 'L', 'Lq', 'W', 'Wq', ...]
|
|
883
|
+
|
|
884
|
+
# Barrido sobre μ
|
|
885
|
+
df2 = wl.sensitivity(wl.mm1, "mu", np.linspace(4.0, 10.0, 15), lam=3.0)
|
|
886
|
+
|
|
887
|
+
# Barrido sobre número de servidores en M/M/c
|
|
888
|
+
df3 = wl.sensitivity(wl.mmc, "c", range(1, 6), lam=8.0, mu=5.0)
|
|
889
|
+
|
|
890
|
+
# Visualizar
|
|
891
|
+
from walopy.plotting import plot_sensitivity
|
|
892
|
+
fig = plot_sensitivity(df, "lam", metrics=["Wq", "Lq"])
|
|
893
|
+
fig.show()
|
|
894
|
+
```
|
|
895
|
+
|
|
896
|
+
---
|
|
897
|
+
|
|
898
|
+
## 15. Escenarios en lote (batch_model)
|
|
899
|
+
|
|
900
|
+
### `batch_model(model_fn, df, **fixed_kwargs)` — Aplicar modelo a un DataFrame
|
|
901
|
+
|
|
902
|
+
Aplica cualquier función de walopy a cada fila de un DataFrame de escenarios.
|
|
903
|
+
|
|
904
|
+
```python
|
|
905
|
+
import pandas as pd
|
|
906
|
+
|
|
907
|
+
# Varios escenarios con distintas tasas de llegada
|
|
908
|
+
escenarios = pd.DataFrame({
|
|
909
|
+
"lam": [1.0, 2.0, 3.0, 4.0]
|
|
910
|
+
})
|
|
911
|
+
resultado = wl.batch_model(wl.mm1, escenarios, mu=5.0)
|
|
912
|
+
print(resultado[["lam", "rho", "Wq", "L"]])
|
|
913
|
+
|
|
914
|
+
# Escenarios mixtos (lambda y mu variables)
|
|
915
|
+
escenarios2 = pd.DataFrame({
|
|
916
|
+
"lam": [2.0, 3.0, 4.0],
|
|
917
|
+
"mu": [5.0, 6.0, 7.0],
|
|
918
|
+
})
|
|
919
|
+
resultado2 = wl.batch_model(wl.mm1, escenarios2)
|
|
920
|
+
|
|
921
|
+
# Con número de servidores variable (columna int)
|
|
922
|
+
escenarios3 = pd.DataFrame({
|
|
923
|
+
"c": [1, 2, 3, 4],
|
|
924
|
+
})
|
|
925
|
+
resultado3 = wl.batch_model(wl.mmc, escenarios3, lam=8.0, mu=5.0)
|
|
926
|
+
|
|
927
|
+
# Las filas con error quedan marcadas en "_error"
|
|
928
|
+
print(resultado[resultado["_error"].notna()]) # filas fallidas
|
|
929
|
+
```
|
|
930
|
+
|
|
931
|
+
---
|
|
932
|
+
|
|
933
|
+
## 16. Comparar múltiples resultados
|
|
934
|
+
|
|
935
|
+
### `compare(*results, labels)` — DataFrame comparativo
|
|
936
|
+
|
|
937
|
+
Construye un DataFrame con una fila por resultado para facilitar la comparación.
|
|
938
|
+
|
|
939
|
+
```python
|
|
940
|
+
df = wl.compare(
|
|
941
|
+
wl.mm1(3.0, 5.0),
|
|
942
|
+
wl.mmc(3.0, 5.0, 2),
|
|
943
|
+
wl.md1(3.0, 5.0),
|
|
944
|
+
labels=["M/M/1", "M/M/2", "M/D/1"],
|
|
945
|
+
)
|
|
946
|
+
print(df[["label", "ρ (utilization)", "Wq (wait time)", "L (system)"]])
|
|
947
|
+
# label ρ (utilization) Wq (wait time) L (system)
|
|
948
|
+
# 0 M/M/1 0.6 0.3000 1.50
|
|
949
|
+
# 1 M/M/2 0.3 0.0302 1.02
|
|
950
|
+
# 2 M/D/1 0.6 0.1500 1.05
|
|
951
|
+
|
|
952
|
+
# Sin etiquetas → scenario_1, scenario_2, ...
|
|
953
|
+
df2 = wl.compare(wl.mm1(1, 5), wl.mm1(2, 5), wl.mm1(3, 5))
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
---
|
|
957
|
+
|
|
958
|
+
## 17. Gráficas
|
|
959
|
+
|
|
960
|
+
Todos los resultados exponen `.plot()` que devuelve una figura de Matplotlib o Plotly. También se pueden llamar directamente desde `walopy.plotting`.
|
|
961
|
+
|
|
962
|
+
| Función `.plot()` | Tipo | Descripción |
|
|
963
|
+
|---|---|---|
|
|
964
|
+
| `QueueResult.plot()` | Matplotlib | Curvas Wq y Lq vs ρ con punto operativo |
|
|
965
|
+
| `OEEResult.plot()` | Matplotlib | Barras horizontales OEE con referencia 85% |
|
|
966
|
+
| `BottleneckResult.plot()` | Matplotlib | Utilización por estación, cuello de botella en rojo |
|
|
967
|
+
| `EOQResult.plot()` | Matplotlib | Curvas de costo de mantener, ordenar y total |
|
|
968
|
+
| `LineBalanceResult.plot()` | Plotly | Cycle time vs takt por estación |
|
|
969
|
+
| `BreakEvenResult.plot()` | Plotly | Líneas ingreso/costo con BEP y zona de beneficio |
|
|
970
|
+
| `SimulationResult.plot()` | Plotly | Histograma de Wq + CDF empírica con percentiles |
|
|
971
|
+
| `OptimizeResult.plot()` | Plotly | Costo por servidor, costo de espera y total vs c |
|
|
972
|
+
| `KPINode.plot(kind=)` | Plotly | Treemap o sunburst interactivo |
|
|
973
|
+
|
|
974
|
+
```python
|
|
975
|
+
# Guardar como HTML interactivo (Plotly)
|
|
976
|
+
fig = r.plot()
|
|
977
|
+
fig.write_html("resultado.html")
|
|
978
|
+
|
|
979
|
+
# Guardar como imagen (Matplotlib)
|
|
980
|
+
fig = wl.oee(0.9, 0.8, 0.95).plot()
|
|
981
|
+
fig.savefig("oee.png", dpi=150)
|
|
982
|
+
```
|
|
983
|
+
|
|
984
|
+
Para la curva de sensibilidad:
|
|
985
|
+
```python
|
|
986
|
+
from walopy.plotting import plot_sensitivity
|
|
987
|
+
df = wl.sensitivity(wl.mm1, "lam", np.linspace(0.5, 4.5, 20), mu=5.0)
|
|
988
|
+
fig = plot_sensitivity(df, "lam", metrics=["Wq", "Lq", "L"])
|
|
989
|
+
fig.show()
|
|
990
|
+
```
|
|
991
|
+
|
|
992
|
+
---
|
|
993
|
+
|
|
994
|
+
## 18. CLI
|
|
995
|
+
|
|
996
|
+
walopy incluye una interfaz de línea de comandos para uso rápido sin escribir código.
|
|
997
|
+
|
|
998
|
+
```bash
|
|
999
|
+
# M/M/1
|
|
1000
|
+
python -m walopy mm1 --lam 3 --mu 5
|
|
1001
|
+
|
|
1002
|
+
# M/M/c
|
|
1003
|
+
python -m walopy mmc --lam 8 --mu 5 --c 2
|
|
1004
|
+
|
|
1005
|
+
# M/D/1
|
|
1006
|
+
python -m walopy md1 --lam 3 --mu 5
|
|
1007
|
+
|
|
1008
|
+
# G/G/1 (Kingman)
|
|
1009
|
+
python -m walopy gg1 --lam 3 --mu 5 --ca2 1.2 --cs2 0.8
|
|
1010
|
+
|
|
1011
|
+
# Ley de Little (resolver W)
|
|
1012
|
+
python -m walopy littles --L 2.5 --lam 5.0
|
|
1013
|
+
|
|
1014
|
+
# EOQ
|
|
1015
|
+
python -m walopy eoq --demand 1000 --ordering 50 --holding 2
|
|
1016
|
+
|
|
1017
|
+
# Versión
|
|
1018
|
+
python -m walopy --version
|
|
1019
|
+
```
|
|
1020
|
+
|
|
1021
|
+
Ejemplo de salida:
|
|
1022
|
+
```
|
|
1023
|
+
Model : M/M/1
|
|
1024
|
+
λ : 3.0 (arrival rate)
|
|
1025
|
+
μ : 5.0 (service rate per server)
|
|
1026
|
+
ρ : 0.6 (utilization)
|
|
1027
|
+
L : 1.5 (avg units in system)
|
|
1028
|
+
Lq : 0.9 (avg units in queue)
|
|
1029
|
+
W : 0.5 (avg time in system)
|
|
1030
|
+
Wq : 0.3 (avg wait time in queue)
|
|
1031
|
+
P0 : 0.4
|
|
1032
|
+
```
|
|
1033
|
+
|
|
1034
|
+
---
|
|
1035
|
+
|
|
1036
|
+
## 19. Referencia de módulos
|
|
1037
|
+
|
|
1038
|
+
| Módulo | Funciones y clases principales |
|
|
1039
|
+
|---|---|
|
|
1040
|
+
| `queuing` | `mm1`, `mmc`, `md1`, `kingman`, `littles_law`, `QueueResult` |
|
|
1041
|
+
| `advanced` | `mm1k`, `mmck`, `erlang_b`, `mm1_priority`, `monte_carlo_gg1`, `takt_time`, `line_balance`, `break_even`, `queue_length_pmf`, `sojourn_cdf`, `PriorityQueueResult`, `SimulationResult`, `LineBalanceResult`, `BreakEvenResult` |
|
|
1042
|
+
| `fitting` | `fit_from_data`, `FitResult` |
|
|
1043
|
+
| `inventory` | `eoq`, `reorder_point`, `newsvendor`, `EOQResult`, `ReorderResult`, `NewsvendorResult` |
|
|
1044
|
+
| `network` | `jackson_network`, `JacksonResult`, `StationMetrics` |
|
|
1045
|
+
| `operations` | `oee`, `utilization_efficiency`, `unit_cost`, `OEEResult`, `UtilizationResult`, `UnitCostResult` |
|
|
1046
|
+
| `bottleneck` | `bottleneck_analysis`, `BottleneckResult`, `StationResult` |
|
|
1047
|
+
| `kpi` | `KPINode`, `oee_kpi_tree`, `throughput_kpi_tree`, `roi_kpi_tree` |
|
|
1048
|
+
| `solver` | `solve_lam`, `solve_mu`, `solve_servers`, `optimize_servers`, `sensitivity`, `batch_model`, `compare`, `SolverResult`, `OptimizeResult` |
|
|
1049
|
+
| `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` |
|
|
1050
|
+
|
|
1051
|
+
---
|
|
1052
|
+
|
|
1053
|
+
## 20. Requisitos
|
|
1054
|
+
|
|
1055
|
+
```
|
|
1056
|
+
Python ≥ 3.9
|
|
1057
|
+
numpy ≥ 1.22
|
|
1058
|
+
pandas ≥ 1.4
|
|
1059
|
+
matplotlib ≥ 3.5
|
|
1060
|
+
plotly ≥ 5.0
|
|
1061
|
+
```
|
|
1062
|
+
|
|
1063
|
+
No se requiere `scipy`. Los cálculos estadísticos usan `statistics.NormalDist` de la biblioteca estándar de Python.
|
|
1064
|
+
|
|
1065
|
+
## Licencia
|
|
1066
|
+
|
|
1067
|
+
MIT — ver [LICENSE](LICENSE).
|
|
1068
|
+
|
|
1069
|
+
## Contribuciones
|
|
1070
|
+
|
|
1071
|
+
Pull requests bienvenidas. Asegúrate de que `pytest tests/` pase antes de abrir un PR.
|