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.
Files changed (36) hide show
  1. walopy-0.2.2/PKG-INFO +1071 -0
  2. walopy-0.2.2/README.md +1034 -0
  3. {walopy-0.2.1 → walopy-0.2.2}/pyproject.toml +1 -1
  4. walopy-0.2.2/src/walopy.egg-info/PKG-INFO +1071 -0
  5. walopy-0.2.1/PKG-INFO +0 -214
  6. walopy-0.2.1/README.md +0 -177
  7. walopy-0.2.1/src/walopy.egg-info/PKG-INFO +0 -214
  8. {walopy-0.2.1 → walopy-0.2.2}/LICENSE +0 -0
  9. {walopy-0.2.1 → walopy-0.2.2}/setup.cfg +0 -0
  10. {walopy-0.2.1 → walopy-0.2.2}/src/walopy/__init__.py +0 -0
  11. {walopy-0.2.1 → walopy-0.2.2}/src/walopy/__main__.py +0 -0
  12. {walopy-0.2.1 → walopy-0.2.2}/src/walopy/_utils.py +0 -0
  13. {walopy-0.2.1 → walopy-0.2.2}/src/walopy/advanced.py +0 -0
  14. {walopy-0.2.1 → walopy-0.2.2}/src/walopy/bottleneck.py +0 -0
  15. {walopy-0.2.1 → walopy-0.2.2}/src/walopy/fitting.py +0 -0
  16. {walopy-0.2.1 → walopy-0.2.2}/src/walopy/inventory.py +0 -0
  17. {walopy-0.2.1 → walopy-0.2.2}/src/walopy/kpi.py +0 -0
  18. {walopy-0.2.1 → walopy-0.2.2}/src/walopy/network.py +0 -0
  19. {walopy-0.2.1 → walopy-0.2.2}/src/walopy/operations.py +0 -0
  20. {walopy-0.2.1 → walopy-0.2.2}/src/walopy/plotting.py +0 -0
  21. {walopy-0.2.1 → walopy-0.2.2}/src/walopy/queuing.py +0 -0
  22. {walopy-0.2.1 → walopy-0.2.2}/src/walopy/solver.py +0 -0
  23. {walopy-0.2.1 → walopy-0.2.2}/src/walopy.egg-info/SOURCES.txt +0 -0
  24. {walopy-0.2.1 → walopy-0.2.2}/src/walopy.egg-info/dependency_links.txt +0 -0
  25. {walopy-0.2.1 → walopy-0.2.2}/src/walopy.egg-info/requires.txt +0 -0
  26. {walopy-0.2.1 → walopy-0.2.2}/src/walopy.egg-info/top_level.txt +0 -0
  27. {walopy-0.2.1 → walopy-0.2.2}/tests/test_advanced.py +0 -0
  28. {walopy-0.2.1 → walopy-0.2.2}/tests/test_bottleneck.py +0 -0
  29. {walopy-0.2.1 → walopy-0.2.2}/tests/test_fitting.py +0 -0
  30. {walopy-0.2.1 → walopy-0.2.2}/tests/test_inventory.py +0 -0
  31. {walopy-0.2.1 → walopy-0.2.2}/tests/test_kpi.py +0 -0
  32. {walopy-0.2.1 → walopy-0.2.2}/tests/test_network.py +0 -0
  33. {walopy-0.2.1 → walopy-0.2.2}/tests/test_new_models.py +0 -0
  34. {walopy-0.2.1 → walopy-0.2.2}/tests/test_operations.py +0 -0
  35. {walopy-0.2.1 → walopy-0.2.2}/tests/test_queuing.py +0 -0
  36. {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
+ [![PyPI version](https://img.shields.io/pypi/v/walopy.svg)](https://pypi.org/project/walopy/)
43
+ [![Python](https://img.shields.io/pypi/pyversions/walopy.svg)](https://pypi.org/project/walopy/)
44
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
45
+ [![CI](https://github.com/LeoSanta15/walopy/actions/workflows/ci.yml/badge.svg)](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.