pyworkforce 0.5.1__tar.gz → 0.5.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 (34) hide show
  1. {pyworkforce-0.5.1 → pyworkforce-0.5.2}/PKG-INFO +66 -35
  2. {pyworkforce-0.5.1 → pyworkforce-0.5.2}/README.md +67 -51
  3. pyworkforce-0.5.2/pyworkforce/_version.py +1 -0
  4. {pyworkforce-0.5.1 → pyworkforce-0.5.2}/pyworkforce/queuing/erlang.py +81 -77
  5. {pyworkforce-0.5.1 → pyworkforce-0.5.2}/pyworkforce/queuing/tests/test_erlang.py +35 -5
  6. {pyworkforce-0.5.1 → pyworkforce-0.5.2}/pyworkforce/queuing/tests/test_multi_erlang.py +14 -6
  7. {pyworkforce-0.5.1 → pyworkforce-0.5.2}/pyworkforce/rostering/binary_programming.py +26 -25
  8. {pyworkforce-0.5.1 → pyworkforce-0.5.2}/pyworkforce/scheduling/base.py +15 -13
  9. {pyworkforce-0.5.1 → pyworkforce-0.5.2}/pyworkforce/scheduling/shifts_selection.py +30 -28
  10. {pyworkforce-0.5.1 → pyworkforce-0.5.2}/pyworkforce/utils/grid.py +1 -1
  11. {pyworkforce-0.5.1 → pyworkforce-0.5.2}/pyworkforce/utils/tests/test_parameter_grid.py +13 -6
  12. {pyworkforce-0.5.1 → pyworkforce-0.5.2}/pyworkforce.egg-info/PKG-INFO +66 -35
  13. pyworkforce-0.5.2/pyworkforce.egg-info/requires.txt +4 -0
  14. {pyworkforce-0.5.1 → pyworkforce-0.5.2}/setup.py +11 -12
  15. pyworkforce-0.5.1/pyworkforce/_version.py +0 -1
  16. pyworkforce-0.5.1/pyworkforce.egg-info/requires.txt +0 -4
  17. {pyworkforce-0.5.1 → pyworkforce-0.5.2}/LICENSE +0 -0
  18. {pyworkforce-0.5.1 → pyworkforce-0.5.2}/pyworkforce/__init__.py +0 -0
  19. {pyworkforce-0.5.1 → pyworkforce-0.5.2}/pyworkforce/queuing/__init__.py +0 -0
  20. {pyworkforce-0.5.1 → pyworkforce-0.5.2}/pyworkforce/queuing/tests/__init__.py +0 -0
  21. {pyworkforce-0.5.1 → pyworkforce-0.5.2}/pyworkforce/rostering/__init__.py +0 -0
  22. {pyworkforce-0.5.1 → pyworkforce-0.5.2}/pyworkforce/rostering/tests/__init__.py +0 -0
  23. {pyworkforce-0.5.1 → pyworkforce-0.5.2}/pyworkforce/rostering/tests/test_rostering.py +0 -0
  24. {pyworkforce-0.5.1 → pyworkforce-0.5.2}/pyworkforce/scheduling/__init__.py +0 -0
  25. {pyworkforce-0.5.1 → pyworkforce-0.5.2}/pyworkforce/scheduling/tests/__init__.py +0 -0
  26. {pyworkforce-0.5.1 → pyworkforce-0.5.2}/pyworkforce/scheduling/tests/test_shifts.py +0 -0
  27. {pyworkforce-0.5.1 → pyworkforce-0.5.2}/pyworkforce/scheduling/tests/test_utils.py +0 -0
  28. {pyworkforce-0.5.1 → pyworkforce-0.5.2}/pyworkforce/scheduling/utils.py +0 -0
  29. {pyworkforce-0.5.1 → pyworkforce-0.5.2}/pyworkforce/utils/__init__.py +0 -0
  30. {pyworkforce-0.5.1 → pyworkforce-0.5.2}/pyworkforce/utils/tests/__init__.py +0 -0
  31. {pyworkforce-0.5.1 → pyworkforce-0.5.2}/pyworkforce.egg-info/SOURCES.txt +0 -0
  32. {pyworkforce-0.5.1 → pyworkforce-0.5.2}/pyworkforce.egg-info/dependency_links.txt +0 -0
  33. {pyworkforce-0.5.1 → pyworkforce-0.5.2}/pyworkforce.egg-info/top_level.txt +0 -0
  34. {pyworkforce-0.5.1 → pyworkforce-0.5.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: pyworkforce
3
- Version: 0.5.1
3
+ Version: 0.5.2
4
4
  Summary: Common tools for workforce management, schedule and optimization problems
5
5
  Home-page: https://github.com/rodrigo-arenas/pyworkforce
6
6
  Author: Rodrigo Arenas
@@ -11,66 +11,96 @@ Project-URL: Source Code, https://github.com/rodrigo-arenas/pyworkforce
11
11
  Project-URL: Bug Tracker, https://github.com/rodrigo-arenas/pyworkforce/issues
12
12
  Classifier: License :: OSI Approved :: MIT License
13
13
  Classifier: Programming Language :: Python :: 3
14
- Classifier: Programming Language :: Python :: 3.8
15
- Classifier: Programming Language :: Python :: 3.9
16
- Classifier: Programming Language :: Python :: 3.10
17
- Classifier: Programming Language :: Python :: 3.11
18
- Requires-Python: >=3.8
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Requires-Python: >=3.12,<3.15
19
18
  Description-Content-Type: text/markdown
20
19
  License-File: LICENSE
20
+ Requires-Dist: numpy>=1.26.0
21
+ Requires-Dist: ortools>=9.12.4544
22
+ Requires-Dist: pandas>=2.2.0
23
+ Requires-Dist: joblib>=1.4.0
24
+ Dynamic: author
25
+ Dynamic: author-email
26
+ Dynamic: classifier
27
+ Dynamic: description
28
+ Dynamic: description-content-type
29
+ Dynamic: home-page
30
+ Dynamic: license
31
+ Dynamic: license-file
32
+ Dynamic: project-url
33
+ Dynamic: requires-dist
34
+ Dynamic: requires-python
35
+ Dynamic: summary
21
36
 
22
37
 
23
38
  [![Build Status](https://www.travis-ci.com/rodrigo-arenas/pyworkforce.svg?branch=main)](https://www.travis-ci.com/rodrigo-arenas/pyworkforce)
24
39
  [![Codecov](https://codecov.io/gh/rodrigo-arenas/pyworkforce/branch/main/graphs/badge.svg?branch=main&service=github)](https://codecov.io/github/rodrigo-arenas/pyworkforce?branch=main)
25
40
  [![PyPI Version](https://badge.fury.io/py/pyworkforce.svg)](https://badge.fury.io/py/pyworkforce)
26
- [![Python Version](https://img.shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10%20%7C%203.11-blue)](https://www.python.org/downloads/)
41
+ [![Python Version](https://img.shields.io/badge/python-3.12%20%7C%203.13%20%7C%203.14-blue)](https://www.python.org/downloads/)
27
42
 
28
43
 
29
44
  # pyworkforce
30
- Standard tools for workforce management, queuing, scheduling, rostering and optimization problems.
45
+ Tools for workforce management problems such as queue staffing, shift scheduling,
46
+ rostering, and operations research optimization.
31
47
 
32
- Make sure to check the documentation, which is available [here](https://pyworkforce.readthedocs.io/en/stable/)
48
+ The full documentation is available at
49
+ [pyworkforce.readthedocs.io](https://pyworkforce.readthedocs.io/en/stable/).
33
50
 
34
- # Usage:
35
- Install pyworkforce
51
+ ## Installation
36
52
 
37
- It's advised to install pyworkforce using a virtual env, inside the env use:
53
+ We recommend installing pyworkforce in a virtual environment:
38
54
 
39
- ```
55
+ ```bash
40
56
  pip install pyworkforce
41
57
  ```
42
58
 
43
- If you are having troubles with or-tools installation, check the [or-tools guide](https://github.com/google/or-tools#installation)
59
+ pyworkforce supports Python 3.12, 3.13, and 3.14.
60
+
61
+ If you are using Anaconda and run into installation issues, update the environment first:
62
+
63
+ ```bash
64
+ conda update --all
65
+ ```
66
+
67
+ If the issue is related to OR-Tools, check the
68
+ [OR-Tools installation guide](https://github.com/google/or-tools#installation).
44
69
 
45
- For complete list and details of examples go to the
46
- [examples folder](https://github.com/rodrigo-arenas/pyworkforce/tree/develop/examples)
70
+ For runnable examples, see the
71
+ [examples folder](https://github.com/rodrigo-arenas/pyworkforce/tree/develop/examples).
47
72
 
48
- ## Features:
49
- pyworkforce currently includes:
73
+ ## What pyworkforce Does
74
+
75
+ pyworkforce is organized around three planning steps:
50
76
 
51
77
  ### Queuing
52
- It solves the following system resource requirements:
78
+
79
+ Use `pyworkforce.queuing` when you need to estimate how many resources are required
80
+ to handle incoming work, for example calls arriving at a call center. The current
81
+ implementation uses Erlang C assumptions: constant arrival rate, infinite queue,
82
+ and no customer dropout.
53
83
 
54
84
  ![queue_system](https://raw.githubusercontent.com/rodrigo-arenas/pyworkforce/main/docs/images/erlangc_queue_system.png)
55
85
 
56
- - **queuing.ErlangC:** Find the number of resources required to attend incoming traffic to a constant rate,
57
- infinite queue length, and no dropout.
86
+ - **queuing.ErlangC:** Calculate staffing requirements and performance metrics for one queue scenario.
87
+ - **queuing.MultiErlangC:** Run multiple Erlang C scenarios from a parameter grid.
58
88
 
59
89
  ### Scheduling
60
90
 
61
- It finds the number of resources to schedule in a shift based on the number of required positions per time interval
62
- (found, for example, using [queuing.ErlangC](./pyworkforce/queuing/erlang.py)), maximum capacity restrictions and static shifts coverage.<br>
63
- - **scheduling.MinAbsDifference:** This module finds the "optimal" assignation by minimizing the total absolute
64
- differences between required resources per interval against the scheduled resources found by the solver.
65
- - **scheduling.MinRequiredResources**: This module finds the "optimal" assignation by minimizing the total
66
- weighted amount of scheduled resources (optionally weighted by shift cost), it ensures that in all intervals, there are
67
- never fewer resources shifted than the ones required per period.
91
+ Use `pyworkforce.scheduling` when you already know the required resources by time
92
+ interval and need to choose how many people to place on each predefined shift.
93
+
94
+ - **scheduling.MinAbsDifference:** Minimizes the total absolute difference between required and scheduled resources.
95
+ - **scheduling.MinRequiredResources:** Minimizes the total weighted number of scheduled resources while ensuring every interval is covered.
68
96
 
69
97
  ### Rostering
70
98
 
71
- It assigns a list of resources to a list of required positions per day and shifts; it takes into account
72
- different restrictions as shift bans, consecutive shifts, resting days, and others.
73
- It also introduces soft restrictions like shift preferences.
99
+ Use `pyworkforce.rostering` when you have named resources and need to assign them
100
+ to days and shifts while respecting rules such as banned shifts, rest days,
101
+ minimum working hours, and preferences.
102
+
103
+ - **rostering.MinHoursRoster:** Builds a resource-level roster that covers shift requirements with the minimum scheduled hours.
74
104
 
75
105
  ### Queue systems:
76
106
 
@@ -95,7 +125,8 @@ Output:
95
125
  'waiting_probability': 0.1741319335950498}
96
126
  ```
97
127
 
98
- If you want to run different scenarios at the same time, you can use the MultiErlangC, for example, trying different service levels:
128
+ If you want to run several scenarios at the same time, use `MultiErlangC`.
129
+ For example, this tries different service-level targets:
99
130
 
100
131
  ```python
101
132
  from pyworkforce.queuing import MultiErlangC
@@ -143,13 +174,13 @@ A brief introduction can be found in this [medium post](https://towardsdatascien
143
174
  ```python
144
175
  from pyworkforce.scheduling import MinAbsDifference, MinRequiredResources
145
176
 
146
- # Rows are the days, each entry of a row, is number of positions required at an hour of the day (24).
177
+ # Rows are days. Each value is the number of required positions for one hour of the day.
147
178
  required_resources = [
148
179
  [9, 11, 17, 9, 7, 12, 5, 11, 8, 9, 18, 17, 8, 12, 16, 8, 7, 12, 11, 10, 13, 19, 16, 7],
149
180
  [13, 13, 12, 15, 18, 20, 13, 16, 17, 8, 13, 11, 6, 19, 11, 20, 19, 17, 10, 13, 14, 23, 16, 8]
150
181
  ]
151
182
 
152
- # Each entry of a shift,an hour of the day (24), 1 if the shift covers that hour, 0 otherwise
183
+ # Each shift has 24 entries, one per hour. Use 1 if the shift covers that hour, otherwise 0.
153
184
  shifts_coverage = {"Morning": [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
154
185
  "Afternoon": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0],
155
186
  "Night": [1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1],
@@ -2,54 +2,69 @@
2
2
  [![Build Status](https://www.travis-ci.com/rodrigo-arenas/pyworkforce.svg?branch=main)](https://www.travis-ci.com/rodrigo-arenas/pyworkforce)
3
3
  [![Codecov](https://codecov.io/gh/rodrigo-arenas/pyworkforce/branch/main/graphs/badge.svg?branch=main&service=github)](https://codecov.io/github/rodrigo-arenas/pyworkforce?branch=main)
4
4
  [![PyPI Version](https://badge.fury.io/py/pyworkforce.svg)](https://badge.fury.io/py/pyworkforce)
5
- [![Python Version](https://img.shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10%20%7C%203.11-blue)](https://www.python.org/downloads/)
6
-
7
-
8
- # pyworkforce
9
- Standard tools for workforce management, queuing, scheduling, rostering and optimization problems.
10
-
11
- Make sure to check the documentation, which is available [here](https://pyworkforce.readthedocs.io/en/stable/)
12
-
13
- # Usage:
14
- Install pyworkforce
15
-
16
- It's advised to install pyworkforce using a virtual env, inside the env use:
17
-
18
- ```
19
- pip install pyworkforce
20
- ```
21
-
22
- If you are having troubles with or-tools installation, check the [or-tools guide](https://github.com/google/or-tools#installation)
23
-
24
- For complete list and details of examples go to the
25
- [examples folder](https://github.com/rodrigo-arenas/pyworkforce/tree/develop/examples)
26
-
27
- ## Features:
28
- pyworkforce currently includes:
29
-
30
- ### Queuing
31
- It solves the following system resource requirements:
32
-
33
- ![queue_system](https://raw.githubusercontent.com/rodrigo-arenas/pyworkforce/main/docs/images/erlangc_queue_system.png)
34
-
35
- - **queuing.ErlangC:** Find the number of resources required to attend incoming traffic to a constant rate,
36
- infinite queue length, and no dropout.
37
-
38
- ### Scheduling
39
-
40
- It finds the number of resources to schedule in a shift based on the number of required positions per time interval
41
- (found, for example, using [queuing.ErlangC](./pyworkforce/queuing/erlang.py)), maximum capacity restrictions and static shifts coverage.<br>
42
- - **scheduling.MinAbsDifference:** This module finds the "optimal" assignation by minimizing the total absolute
43
- differences between required resources per interval against the scheduled resources found by the solver.
44
- - **scheduling.MinRequiredResources**: This module finds the "optimal" assignation by minimizing the total
45
- weighted amount of scheduled resources (optionally weighted by shift cost), it ensures that in all intervals, there are
46
- never fewer resources shifted than the ones required per period.
47
-
48
- ### Rostering
49
-
50
- It assigns a list of resources to a list of required positions per day and shifts; it takes into account
51
- different restrictions as shift bans, consecutive shifts, resting days, and others.
52
- It also introduces soft restrictions like shift preferences.
5
+ [![Python Version](https://img.shields.io/badge/python-3.12%20%7C%203.13%20%7C%203.14-blue)](https://www.python.org/downloads/)
6
+
7
+
8
+ # pyworkforce
9
+ Tools for workforce management problems such as queue staffing, shift scheduling,
10
+ rostering, and operations research optimization.
11
+
12
+ The full documentation is available at
13
+ [pyworkforce.readthedocs.io](https://pyworkforce.readthedocs.io/en/stable/).
14
+
15
+ ## Installation
16
+
17
+ We recommend installing pyworkforce in a virtual environment:
18
+
19
+ ```bash
20
+ pip install pyworkforce
21
+ ```
22
+
23
+ pyworkforce supports Python 3.12, 3.13, and 3.14.
24
+
25
+ If you are using Anaconda and run into installation issues, update the environment first:
26
+
27
+ ```bash
28
+ conda update --all
29
+ ```
30
+
31
+ If the issue is related to OR-Tools, check the
32
+ [OR-Tools installation guide](https://github.com/google/or-tools#installation).
33
+
34
+ For runnable examples, see the
35
+ [examples folder](https://github.com/rodrigo-arenas/pyworkforce/tree/develop/examples).
36
+
37
+ ## What pyworkforce Does
38
+
39
+ pyworkforce is organized around three planning steps:
40
+
41
+ ### Queuing
42
+
43
+ Use `pyworkforce.queuing` when you need to estimate how many resources are required
44
+ to handle incoming work, for example calls arriving at a call center. The current
45
+ implementation uses Erlang C assumptions: constant arrival rate, infinite queue,
46
+ and no customer dropout.
47
+
48
+ ![queue_system](https://raw.githubusercontent.com/rodrigo-arenas/pyworkforce/main/docs/images/erlangc_queue_system.png)
49
+
50
+ - **queuing.ErlangC:** Calculate staffing requirements and performance metrics for one queue scenario.
51
+ - **queuing.MultiErlangC:** Run multiple Erlang C scenarios from a parameter grid.
52
+
53
+ ### Scheduling
54
+
55
+ Use `pyworkforce.scheduling` when you already know the required resources by time
56
+ interval and need to choose how many people to place on each predefined shift.
57
+
58
+ - **scheduling.MinAbsDifference:** Minimizes the total absolute difference between required and scheduled resources.
59
+ - **scheduling.MinRequiredResources:** Minimizes the total weighted number of scheduled resources while ensuring every interval is covered.
60
+
61
+ ### Rostering
62
+
63
+ Use `pyworkforce.rostering` when you have named resources and need to assign them
64
+ to days and shifts while respecting rules such as banned shifts, rest days,
65
+ minimum working hours, and preferences.
66
+
67
+ - **rostering.MinHoursRoster:** Builds a resource-level roster that covers shift requirements with the minimum scheduled hours.
53
68
 
54
69
  ### Queue systems:
55
70
 
@@ -74,7 +89,8 @@ Output:
74
89
  'waiting_probability': 0.1741319335950498}
75
90
  ```
76
91
 
77
- If you want to run different scenarios at the same time, you can use the MultiErlangC, for example, trying different service levels:
92
+ If you want to run several scenarios at the same time, use `MultiErlangC`.
93
+ For example, this tries different service-level targets:
78
94
 
79
95
  ```python
80
96
  from pyworkforce.queuing import MultiErlangC
@@ -122,13 +138,13 @@ A brief introduction can be found in this [medium post](https://towardsdatascien
122
138
  ```python
123
139
  from pyworkforce.scheduling import MinAbsDifference, MinRequiredResources
124
140
 
125
- # Rows are the days, each entry of a row, is number of positions required at an hour of the day (24).
141
+ # Rows are days. Each value is the number of required positions for one hour of the day.
126
142
  required_resources = [
127
143
  [9, 11, 17, 9, 7, 12, 5, 11, 8, 9, 18, 17, 8, 12, 16, 8, 7, 12, 11, 10, 13, 19, 16, 7],
128
144
  [13, 13, 12, 15, 18, 20, 13, 16, 17, 8, 13, 11, 6, 19, 11, 20, 19, 17, 10, 13, 14, 23, 16, 8]
129
145
  ]
130
146
 
131
- # Each entry of a shift,an hour of the day (24), 1 if the shift covers that hour, 0 otherwise
147
+ # Each shift has 24 entries, one per hour. Use 1 if the shift covers that hour, otherwise 0.
132
148
  shifts_coverage = {"Morning": [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
133
149
  "Afternoon": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0],
134
150
  "Night": [1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1],
@@ -0,0 +1 @@
1
+ __version__ = "0.5.2"
@@ -5,22 +5,22 @@ from joblib import Parallel, delayed
5
5
 
6
6
  class ErlangC:
7
7
  """
8
- Computes the number of positions required to attend a number of transactions in a
9
- queue system based on erlangc.rst. Implementation inspired on:
8
+ Computes the number of positions required to handle transactions in an
9
+ Erlang C queue system. Implementation inspired by:
10
10
  https://lucidmanager.org/data-science/call-centre-workforce-planning-erlang-c-in-r/
11
11
 
12
12
  Parameters
13
13
  ----------
14
14
  transactions: float,
15
- The number of total transactions that comes in an interval.
15
+ Total number of transactions arriving in the interval.
16
16
  aht: float,
17
17
  Average handling time of a transaction (minutes).
18
18
  asa: float,
19
19
  The required average speed of answer (minutes).
20
20
  interval: int,
21
- Interval length (minutes) where the transactions come in
21
+ Interval length, in minutes.
22
22
  shrinkage: float,
23
- Percentage of time that an operator unit is not available.
23
+ Fraction of time that an operator unit is not available.
24
24
  """
25
25
 
26
26
  def __init__(self, transactions: float, aht: float, asa: float,
@@ -46,51 +46,61 @@ class ErlangC:
46
46
  self.aht = aht
47
47
  self.interval = interval
48
48
  self.asa = asa
49
- self.intensity = (self.n_transactions / self.interval) * self.aht
50
- self.shrinkage = shrinkage
51
-
52
- def waiting_probability(self, positions: int, scale_positions: bool = False):
49
+ self.intensity = (self.n_transactions / self.interval) * self.aht
50
+ self.shrinkage = shrinkage
51
+
52
+ def _productive_positions(self, positions: int, scale_positions: bool = False):
53
+ if scale_positions:
54
+ productive_positions = floor((1 - self.shrinkage) * positions)
55
+ else:
56
+ productive_positions = positions
57
+
58
+ if productive_positions <= 0:
59
+ raise ValueError("productive positions must be greater than 0")
60
+
61
+ if productive_positions <= self.intensity:
62
+ raise ValueError("positions must be greater than traffic intensity")
63
+
64
+ return productive_positions
65
+
66
+ def waiting_probability(self, positions: int, scale_positions: bool = False):
53
67
  """
54
- Returns the probability of waiting in the queue
68
+ Returns the probability that a transaction waits in queue.
55
69
 
56
70
  Parameters
57
71
  ----------
58
- positions: int,
59
- The number of positions to attend the transactions.
60
- scale_positions: bool, default=False
61
- Set it to True if the positions were calculated using shrinkage.
72
+ positions: int,
73
+ The number of positions available to handle transactions.
74
+ Productive positions must be greater than traffic intensity.
75
+ scale_positions: bool, default=False
76
+ Set to True when ``positions`` includes shrinkage.
62
77
 
63
78
  """
64
79
 
65
- if scale_positions:
66
- productive_positions = floor((1 - self.shrinkage) * positions)
67
- else:
68
- productive_positions = positions
69
-
70
- erlang_b_inverse = 1
71
- for position in range(1, productive_positions + 1):
72
- erlang_b_inverse = 1 + (erlang_b_inverse * position / self.intensity)
80
+ productive_positions = self._productive_positions(positions, scale_positions)
81
+
82
+ erlang_b_inverse = 1
83
+ for position in range(1, productive_positions + 1):
84
+ erlang_b_inverse = 1 + (erlang_b_inverse * position / self.intensity)
73
85
 
74
86
  erlang_b = 1 / erlang_b_inverse
75
87
  return productive_positions * erlang_b / (productive_positions - self.intensity * (1 - erlang_b))
76
88
 
77
89
  def service_level(self, positions: int, scale_positions: bool = False):
78
90
  """
79
- Returns the expected service level given a number of positions
91
+ Returns the expected service level for a number of positions.
80
92
 
81
93
  Parameters
82
94
  ----------
83
95
 
84
- positions: int,
85
- The number of positions attending.
86
- scale_positions: bool, default = False
87
- Set it to True if the positions were calculated using shrinkage.
96
+ positions: int,
97
+ The number of positions available to handle transactions.
98
+ Productive positions must be greater than traffic intensity.
99
+ scale_positions: bool, default = False
100
+ Set to True when ``positions`` includes shrinkage.
88
101
 
89
102
  """
90
- if scale_positions:
91
- productive_positions = floor((1 - self.shrinkage) * positions)
92
- else:
93
- productive_positions = positions
103
+ productive_positions = self._productive_positions(positions, scale_positions)
94
104
 
95
105
  probability_wait = self.waiting_probability(productive_positions, scale_positions=False)
96
106
  exponential = exp(-(productive_positions - self.intensity) * (self.asa / self.aht))
@@ -98,57 +108,58 @@ class ErlangC:
98
108
 
99
109
  def achieved_occupancy(self, positions: int, scale_positions: bool = False):
100
110
  """
101
- Returns the expected occupancy of positions
111
+ Returns the expected occupancy of positions.
102
112
 
103
113
  Parameters
104
114
  ----------
105
115
 
106
- positions: int,
107
- The number of raw positions
108
- scale_positions: bool, default=False
109
- Set it to True if the positions were calculated using shrinkage.
116
+ positions: int,
117
+ The number of raw positions.
118
+ Productive positions must be greater than traffic intensity.
119
+ scale_positions: bool, default=False
120
+ Set to True when ``positions`` includes shrinkage.
110
121
 
111
122
  """
112
- if scale_positions:
113
- productive_positions = floor((1 - self.shrinkage) * positions)
114
- else:
115
- productive_positions = positions
123
+ productive_positions = self._productive_positions(positions, scale_positions)
116
124
 
117
125
  return self.intensity / productive_positions
118
126
 
119
127
  def required_positions(self, service_level: float, max_occupancy: float = 1.0):
120
128
  """
121
- Computes the requirements using erlangc.rst
129
+ Computes the required positions for a target service level.
122
130
 
123
131
  Parameters
124
132
  ----------
125
133
 
126
134
  service_level: float,
127
- Target service level
128
- max_occupancy: float,
129
- The maximum fraction of time that a transaction can occupy a position
135
+ Target service level.
136
+ max_occupancy: float,
137
+ The maximum fraction of time that a transaction can occupy a position.
138
+ Must be greater than 0 and less than or equal to 1.
130
139
 
131
140
  Returns
132
141
  -------
133
142
 
134
143
  raw_positions: int,
135
- The required positions assuming shrinkage = 0
144
+ Required positions before applying shrinkage.
136
145
  positions: int,
137
- The number of positions needed to ensure the required service level
146
+ Positions needed after applying shrinkage.
138
147
  service_level: float,
139
- The fraction of transactions that are expected to be assigned to a position,
140
- before the asa time
148
+ Fraction of transactions expected to reach a position before the target ASA.
141
149
  occupancy: float,
142
- The expected occupancy of positions
150
+ Expected occupancy of positions.
143
151
  waiting_probability: float,
144
- The probability of a transaction waiting in the queue
152
+ Probability that a transaction waits in queue.
145
153
  """
146
154
 
147
155
  if service_level < 0 or service_level > 1:
148
156
  raise ValueError("service_level must be between 0 and 1")
149
157
 
150
- if max_occupancy < 0 or max_occupancy > 1:
151
- raise ValueError("max_occupancy must be between 0 and 1")
158
+ if max_occupancy < 0 or max_occupancy > 1:
159
+ raise ValueError("max_occupancy must be between 0 and 1")
160
+
161
+ if max_occupancy == 0:
162
+ raise ValueError("max_occupancy must be greater than 0")
152
163
 
153
164
  positions = round(self.intensity + 1)
154
165
  achieved_service_level = self.service_level(positions, scale_positions=False)
@@ -177,47 +188,40 @@ class ErlangC:
177
188
 
178
189
  class MultiErlangC:
179
190
  """
180
- This class uses the erlangc.rst class using joblib's Parallel,
181
- allowing to run multiple scenarios at once.
182
- It finds solutions iterating over all possible combinations provided by the users,
183
- inspired how Sklearn's Grid Search works
191
+ Runs Erlang C calculations over multiple parameter combinations.
192
+
193
+ This class uses joblib's ``Parallel`` to evaluate every combination from
194
+ ``param_grid`` and the method-specific argument grid. Its interface is
195
+ inspired by scikit-learn's grid search utilities.
184
196
 
185
197
  Parameters
186
198
  ----------
187
199
 
188
200
  param_grid: dict,
189
- Dictionary with the erlangc.rst.__init__ parameters, each key of the dictionary must be the
190
- expected parameter and the value must be a list with the different options to iterate
201
+ Dictionary with :class:`ErlangC` initialization parameters. Each key must be an
202
+ expected parameter, and each value must be a list of options to iterate over.
191
203
  example: {"transactions": [100, 200], "aht": [3], "interval": [30], "asa": [20 / 60], "shrinkage": [0.3]}
192
204
  n_jobs: int, default=2
193
- The maximum number of concurrently running jobs.
205
+ Maximum number of concurrently running jobs.
194
206
  If -1 all CPUs are used. If 1 is given, no parallel computing code is used at all, which is useful for debugging.
195
207
  For n_jobs below -1, (n_cpus + 1 + n_jobs) are used. Thus for n_jobs = -2, all CPUs but one are used.
196
208
  None is a marker for ‘unset’ that will be interpreted as n_jobs=1 (sequential execution)
197
209
  unless the call is performed under a parallel_backend() context manager that sets another value for n_jobs.
198
210
  pre_dispatch: {"all", int, or expression}, default='2 * n_jobs'
199
- The number of batches (of tasks) to be pre-dispatched. Default is 2*n_jobs’.
211
+ Number of task batches to pre-dispatch. Default is ``2*n_jobs``.
200
212
  See joblib's documentation for more details: https://joblib.readthedocs.io/en/latest/generated/joblib.Parallel.html
201
213
 
202
214
  Attributes
203
215
  ----------
204
216
 
205
217
  waiting_probability_params: list[tuple],
206
- Each tuple of the list represents the used parameters in param_grid for ErlangC and
207
- arguments_grid for waiting_probability method,corresponding to the same order returned
208
- by the MultiErlangC.waiting_probability method.
218
+ Parameters used for each ``waiting_probability`` result, in result order.
209
219
  service_level_params: list[tuple],
210
- Each tuple of the list represents the used parameters in param_grid for ErlangC and
211
- arguments_grid for service_level method,corresponding to the same order returned
212
- by the MultiErlangC.service_level method.
220
+ Parameters used for each ``service_level`` result, in result order.
213
221
  achieved_occupancy_params: list[tuple],
214
- Each tuple of the list represents the used parameters in param_grid for ErlangC and
215
- arguments_grid for achieved_occupancy method,corresponding to the same order returned
216
- by the MultiErlangC.achieved_occupancy method.
222
+ Parameters used for each ``achieved_occupancy`` result, in result order.
217
223
  required_positions_params: list[tuple],
218
- Each tuple of the list represents the used parameters in param_grid for ErlangC and
219
- arguments_grid for required_positions method,corresponding to the same order returned
220
- by the MultiErlangC.required_positions method.
224
+ Parameters used for each ``required_positions`` result, in result order.
221
225
  """
222
226
 
223
227
  def __init__(self, param_grid: dict, n_jobs: int = 2, pre_dispatch: str = '2 * n_jobs'):
@@ -347,8 +351,8 @@ class MultiErlangC:
347
351
  if len(solutions) < 1: # noqa
348
352
  raise ValueError("Could not find any solution, make sure the param_grid is defined correctly")
349
353
 
350
- if len(solutions) != combinations:
351
- raise ValueError('Inconsistent results. Expected {} '
352
- 'solutions, got {}'
353
- .format(len(self.param_list),
354
- len(solutions))) # noqa
354
+ if len(solutions) != combinations:
355
+ raise ValueError('Inconsistent results. Expected {} '
356
+ 'solutions, got {}'
357
+ .format(combinations,
358
+ len(solutions))) # noqa
@@ -85,8 +85,38 @@ def test_wrong_service_level_erlangc():
85
85
  assert str(excinfo.value) == "service_level must be between 0 and 1"
86
86
 
87
87
 
88
- def test_wrong_max_occupancy_erlangc():
89
- erlang = ErlangC(transactions=100, asa=0.33, aht=3, interval=30, shrinkage=0.3)
90
- with pytest.raises(Exception) as excinfo:
91
- results = erlang.required_positions(service_level=0.8, max_occupancy=1.2)
92
- assert str(excinfo.value) == "max_occupancy must be between 0 and 1"
88
+ def test_wrong_max_occupancy_erlangc():
89
+ erlang = ErlangC(transactions=100, asa=0.33, aht=3, interval=30, shrinkage=0.3)
90
+ with pytest.raises(Exception) as excinfo:
91
+ results = erlang.required_positions(service_level=0.8, max_occupancy=1.2)
92
+ assert str(excinfo.value) == "max_occupancy must be between 0 and 1"
93
+
94
+
95
+ def test_zero_max_occupancy_erlangc():
96
+ erlang = ErlangC(transactions=100, asa=0.33, aht=3, interval=30, shrinkage=0.3)
97
+ with pytest.raises(Exception) as excinfo:
98
+ erlang.required_positions(service_level=0.8, max_occupancy=0)
99
+ assert str(excinfo.value) == "max_occupancy must be greater than 0"
100
+
101
+
102
+ def test_waiting_probability_requires_productive_positions():
103
+ erlang = ErlangC(transactions=1, asa=0.33, aht=1, interval=30, shrinkage=0.9)
104
+ with pytest.raises(Exception) as excinfo:
105
+ erlang.waiting_probability(positions=1, scale_positions=True)
106
+ assert str(excinfo.value) == "productive positions must be greater than 0"
107
+
108
+
109
+ def test_erlang_methods_require_stable_system():
110
+ erlang = ErlangC(transactions=100, asa=0.33, aht=3, interval=30, shrinkage=0.0)
111
+
112
+ with pytest.raises(Exception) as excinfo:
113
+ erlang.waiting_probability(positions=10)
114
+ assert str(excinfo.value) == "positions must be greater than traffic intensity"
115
+
116
+ with pytest.raises(Exception) as excinfo:
117
+ erlang.service_level(positions=10)
118
+ assert str(excinfo.value) == "positions must be greater than traffic intensity"
119
+
120
+ with pytest.raises(Exception) as excinfo:
121
+ erlang.achieved_occupancy(positions=10)
122
+ assert str(excinfo.value) == "positions must be greater than traffic intensity"
@@ -86,10 +86,18 @@ def test_expected_multierlangc_wrong_arguments():
86
86
  "ErlangC.required_positions() missing 1 required positional argument: 'service_level'"]
87
87
 
88
88
 
89
- def test_multierlangc_wrong_grid():
90
- param_grid = {"transactions": 100, "asa": [0.33], "aht": [3], "interval": [30], "shrinkage": [0.3]}
91
-
92
- with pytest.raises(Exception) as excinfo:
93
- results = MultiErlangC(param_grid=param_grid)
94
- assert str(excinfo.value) == "Parameter grid value is not iterable (key='transactions', value=100)"
89
+ def test_multierlangc_wrong_grid():
90
+ param_grid = {"transactions": 100, "asa": [0.33], "aht": [3], "interval": [30], "shrinkage": [0.3]}
91
+
92
+ with pytest.raises(Exception) as excinfo:
93
+ results = MultiErlangC(param_grid=param_grid)
94
+ assert str(excinfo.value) == "Parameter grid value is not iterable (key='transactions', value=100)"
95
+
96
+
97
+ def test_multierlangc_check_solutions_expected_combinations():
98
+ erlang = MultiErlangC(param_grid={"transactions": [100], "asa": [0.33], "aht": [3], "interval": [30]})
99
+
100
+ with pytest.raises(Exception) as excinfo:
101
+ erlang._check_solutions([{}], combinations=2)
102
+ assert str(excinfo.value) == "Inconsistent results. Expected 2 solutions, got 1"
95
103
 
@@ -5,48 +5,49 @@ from ortools.sat.python import cp_model
5
5
  class MinHoursRoster:
6
6
  """
7
7
 
8
- It assigns a list of resources to a list of required positions per day and shifts; it takes into account
9
- different restrictions as shift bans, consecutive shifts, resting days, and others.
10
- It also introduces soft restrictions like shift preferences.
11
- The "optimal" criteria is defined as the minimum total scheduled hours,
12
- optionally weighted by resources shifts preferences
8
+ Assigns named resources to required positions by day and shift.
9
+
10
+ The solver supports restrictions such as banned shifts, non-sequential
11
+ shifts, rest days, and minimum working hours. It also supports soft shift
12
+ preferences. The objective is to minimize total scheduled hours, optionally
13
+ weighted by resource shift preferences.
13
14
 
14
15
  Parameters
15
16
  ----------
16
17
 
17
18
  num_days: int,
18
- Number of days needed to schedule
19
+ Number of days to schedule.
19
20
  resources: list[str],
20
- Resources available to shift
21
+ Resources available to schedule.
21
22
  shifts: list,
22
- Array of shifts names
23
+ List of shift names.
23
24
  shifts_hours: list,
24
- Array of size [shifts] with the total hours within the shift
25
+ Array of size ``[shifts]`` with the duration of each shift.
25
26
  min_working_hours: int,
26
- Minimum working hours per resource in the horizon
27
+ Minimum working hours per resource in the planning horizon.
27
28
  banned_shifts: list[dict]
28
- Each element {"resource": resource_index, "shift": shift_name, "day": day_number} indicating
29
- that the resource can't be assigned to that shift that particular day
29
+ Each element has the form ``{"resource": resource_id, "shift": shift_name, "day": day_number}``
30
+ and marks a shift that the resource cannot work on that day.
30
31
  example: banned_shifts": [{"resource":"e.johnston@randatmail.com", "shift": "Night", "day": 0}],
31
32
  max_resting: int,
32
- Maximum number of resting days per resource in the total interval
33
+ Maximum number of resting days per resource in the planning horizon.
33
34
  required_resources: dict[list]
34
- Each key of the dict must be one of the shifts, the value must be a list of length [days]
35
- specifying the number of resources to shift in each day for that shift
35
+ Each key must be a shift name, and each value must be a list of length
36
+ ``num_days`` with the required resources for that shift each day.
36
37
  non_sequential_shifts: List[dict]
37
- Each element must have the form {"origin": first_shift, "destination": second_shift}
38
- to make sure that destination shift can't be after origin shift.
38
+ Each element must have the form ``{"origin": first_shift, "destination": second_shift}``
39
+ to prevent ``destination`` from being assigned the day after ``origin``.
39
40
  example: [{"origin":"Night", "destination":"Morning"}]
40
41
  resources_preferences: list[dict]
41
- Each element must have the form {"resource": resource_idx, "shifts":shift_name}
42
- indicating the resources that have preference for shift
42
+ Each element must have the form ``{"resource": resource_id, "shift": shift_name}``
43
+ and indicates that the resource prefers that shift.
43
44
  resources_prioritization: list[dict], default=None
44
- Each element must have the form {"resource": resource_idx, "weight": weight_percentage}
45
- this represent the relative importance for resources_preferences assignment
45
+ Each element must have the form ``{"resource": resource_id, "weight": weight}``
46
+ and represents the relative importance of that resource's preferences.
46
47
  max_search_time: float, default = 240
47
- Maximum time in seconds to search for a solution
48
+ Maximum time, in seconds, to search for a solution.
48
49
  num_search_workers: int, default = 2
49
- Number of workers to search for a solution
50
+ Number of workers used to search for a solution.
50
51
  """
51
52
 
52
53
  def __init__(self, num_days: int,
@@ -108,7 +109,7 @@ class MinHoursRoster:
108
109
 
109
110
  # Constrains
110
111
 
111
- # The number of shifted resource must be ge that required resource, for each day and shift
112
+ # The number of scheduled resources must be greater than or equal to the requirement for each day and shift
112
113
  for d in range(self._num_days):
113
114
  for s in range(self.num_shifts):
114
115
  sch_model.Add(sum(shifted_resource[n][d][s] for n in range(self.num_resource))
@@ -144,7 +145,7 @@ class MinHoursRoster:
144
145
  shifted_resource[n][d + 1][j]
145
146
  for j in range(self.num_shifts)) <= 1)
146
147
 
147
- # resource can't be assigned to banned shifts
148
+ # Resources cannot be assigned to banned shifts
148
149
  if self.banned_shifts is not None:
149
150
  for ban in self.banned_shifts:
150
151
  resource_idx = self.resources.index(ban['resource'])
@@ -13,31 +13,33 @@ class BaseShiftScheduler:
13
13
  num_search_workers=2):
14
14
 
15
15
  """
16
- Base class to solve the following schedule problem:
17
-
18
- Its required to find the optimal number of resources (agents, operators, doctors, etc) to allocate
19
- in a shift, based on a pre-defined requirement of number of resources per period of the day (periods of hours,
20
- half-hour, etc)
16
+ Base class for shift scheduling problems.
17
+
18
+ Scheduling finds the number of resources (agents, operators, doctors, etc.)
19
+ to allocate to each shift, based on predefined resource requirements by
20
+ period of the day.
21
21
 
22
22
  Parameters
23
23
  ----------
24
24
 
25
25
  num_days: int,
26
- Number of days needed to schedule
26
+ Number of days to schedule.
27
27
  periods: int,
28
- Number of working periods in a day
28
+ Number of working periods in a day.
29
29
  shifts_coverage: dict,
30
- dict with structure {"shift_name": "shift_array"} where "shift_array" is an array of size [periods] (p), 1 if shift covers period p, 0 otherwise
30
+ Dictionary of the form ``{"shift_name": shift_array}``, where each
31
+ ``shift_array`` has length ``periods`` and uses 1 when the shift
32
+ covers a period, otherwise 0.
31
33
  required_resources: list,
32
- Array of size [days, periods]
34
+ Array of size ``[days, periods]``.
33
35
  max_period_concurrency: int,
34
- Maximum resources that are allowed to shift in any period and day
36
+ Maximum resources allowed in any period and day.
35
37
  max_shift_concurrency: int,
36
- Number of maximum allowed resources in the same shift
38
+ Maximum resources allowed in the same shift.
37
39
  max_search_time: float, default = 240
38
- Maximum time in seconds to search for a solution
40
+ Maximum time, in seconds, to search for a solution.
39
41
  num_search_workers: int, default = 2
40
- Number of workers to search for a solution
42
+ Number of workers used to search for a solution.
41
43
  """
42
44
 
43
45
  is_valid_num_days = check_positive_integer("num_days", num_days)
@@ -15,29 +15,30 @@ class MinAbsDifference(BaseShiftScheduler):
15
15
  num_search_workers=2,
16
16
  *args, **kwargs):
17
17
  """
18
- The "optimal" criteria is defined as the number of resources per shift
19
- that minimize the total absolute difference between the required resources
20
- per period and the actual scheduling found by the solver
18
+ Minimizes the total absolute difference between required resources per
19
+ period and resources scheduled by the solver.
21
20
 
22
21
  Parameters
23
22
  ----------
24
23
 
25
24
  num_days: int,
26
- Number of days needed to schedule
25
+ Number of days to schedule.
27
26
  periods: int,
28
- Number of working periods in a day
27
+ Number of working periods in a day.
29
28
  shifts_coverage: dict,
30
- dict with structure {"shift_name": "shift_array"} where "shift_array" is an array of size [periods] (p), 1 if shift covers period p, 0 otherwise
29
+ Dictionary of the form ``{"shift_name": shift_array}``, where each
30
+ ``shift_array`` has length ``periods`` and uses 1 when the shift
31
+ covers a period, otherwise 0.
31
32
  required_resources: list,
32
- Array of size [days, periods]
33
+ Array of size ``[days, periods]``.
33
34
  max_period_concurrency: int,
34
- Maximum resources that are allowed to shift in any period and day
35
+ Maximum resources allowed in any period and day.
35
36
  max_shift_concurrency: int,
36
- Number of maximum allowed resources in the same shift
37
+ Maximum resources allowed in the same shift.
37
38
  max_search_time: float, default = 240
38
- Maximum time in seconds to search for a solution
39
+ Maximum time, in seconds, to search for a solution.
39
40
  num_search_workers: int, default = 2
40
- Number of workers to search for a solution
41
+ Number of workers used to search for a solution.
41
42
  """
42
43
 
43
44
  super().__init__(num_days,
@@ -56,8 +57,8 @@ class MinAbsDifference(BaseShiftScheduler):
56
57
  Returns
57
58
  -------
58
59
  solution: dict,
59
- Dictionary with the status on the optimization, the resources to schedule per day and the
60
- final value of the cost function
60
+ Dictionary with optimization status, scheduled resources by day and
61
+ shift, and final objective value.
61
62
  """
62
63
  sch_model = cp_model.CpModel()
63
64
 
@@ -90,7 +91,7 @@ class MinAbsDifference(BaseShiftScheduler):
90
91
  - sum(resources[d][s] * self.shifts_coverage_matrix[s][p]
91
92
  for s in range(self.num_shifts))))
92
93
 
93
- # Total programmed resources, must be less or equals to max_period_concurrency, for each day and period
94
+ # Total programmed resources must be less than or equal to max_period_concurrency for each day and period
94
95
  for d in range(self.num_days):
95
96
  for p in range(self.num_periods):
96
97
  sch_model.Add(
@@ -139,32 +140,33 @@ class MinRequiredResources(BaseShiftScheduler):
139
140
  num_search_workers: int = 2,
140
141
  *args, **kwargs):
141
142
  """
142
- The "optimal" criteria is defined as the minimum weighted amount
143
- of resources (by optional shift cost), that ensures that there are never
144
- fewer resources shifted than the ones required per period
143
+ Minimizes the weighted number of scheduled resources while ensuring that
144
+ every period has at least the required number of resources.
145
145
 
146
146
  Parameters
147
147
  ----------
148
148
 
149
149
  num_days: int,
150
- Number of days needed to schedule
150
+ Number of days to schedule.
151
151
  periods: int,
152
- Number of working periods in a day
152
+ Number of working periods in a day.
153
153
  shifts_coverage: dict,
154
- dict with structure {"shift_name": "shift_array"} where "shift_array" is an array of size [periods] (p), 1 if shift covers period p, 0 otherwise
154
+ Dictionary of the form ``{"shift_name": shift_array}``, where each
155
+ ``shift_array`` has length ``periods`` and uses 1 when the shift
156
+ covers a period, otherwise 0.
155
157
  required_resources: list,
156
- Array of size [days, periods]
158
+ Array of size ``[days, periods]``.
157
159
  max_period_concurrency: int,
158
- Maximum resources that are allowed to shift in any period and day
160
+ Maximum resources allowed in any period and day.
159
161
  max_shift_concurrency: int,
160
- Number of maximum allowed resources in the same shift
162
+ Maximum resources allowed in the same shift.
161
163
  cost_dict: dict, default = None
162
- dictionary of form {shift: cost_value}, where shift must be the same options listed in the
163
- shifts_coverage matrix, and they must be all integers
164
+ Dictionary of the form ``{shift: cost_value}``. It must contain the
165
+ same shifts as ``shifts_coverage``.
164
166
  max_search_time: float, default = 240
165
- Maximum time in seconds to search for a solution
167
+ Maximum time, in seconds, to search for a solution.
166
168
  num_search_workers: int, default = 2
167
- Number of workers to search for a solution
169
+ Number of workers used to search for a solution.
168
170
  """
169
171
 
170
172
  super().__init__(num_days,
@@ -214,7 +216,7 @@ class MinRequiredResources(BaseShiftScheduler):
214
216
  sch_model.Add(sum(resources[d][s] * self.shifts_coverage_matrix[s][p]
215
217
  for s in range(self.num_shifts)) >= self.required_resources[d][p])
216
218
 
217
- # Total programmed resources, must be less or equals to max_period_concurrency, for each day and period
219
+ # Total programmed resources must be less than or equal to max_period_concurrency for each day and period
218
220
  for d in range(self.num_days):
219
221
  for p in range(self.num_periods):
220
222
  sch_model.Add(
@@ -98,7 +98,7 @@ class ParameterGrid:
98
98
  # Reverse so most frequent cycling parameter comes first
99
99
  keys, values_lists = zip(*sorted(sub_grid.items())[::-1])
100
100
  sizes = [len(v_list) for v_list in values_lists]
101
- total = np.product(sizes)
101
+ total = np.prod(sizes)
102
102
 
103
103
  if ind >= total:
104
104
  # Try the next grid
@@ -8,7 +8,7 @@ def assert_grid_iter_equals_getitem(grid):
8
8
  assert list(grid) == [grid[i] for i in range(len(grid))]
9
9
 
10
10
 
11
- def test_parameter_grid():
11
+ def test_parameter_grid():
12
12
  """
13
13
  Test taken from scikit-learn
14
14
  """
@@ -39,13 +39,20 @@ def test_parameter_grid():
39
39
  assert len(empty) == 1
40
40
  assert list(empty) == [{}]
41
41
  assert_grid_iter_equals_getitem(empty)
42
- with pytest.raises(IndexError):
43
- empty[1]
44
-
45
- has_empty = ParameterGrid([{'C': [1, 10]}, {}, {'C': [.5]}])
42
+ with pytest.raises(IndexError):
43
+ empty[1]
44
+
45
+ has_empty = ParameterGrid([{'C': [1, 10]}, {}, {'C': [.5]}])
46
46
  assert len(has_empty) == 4
47
47
  assert list(has_empty) == [{'C': 1}, {'C': 10}, {}, {'C': .5}]
48
- assert_grid_iter_equals_getitem(has_empty)
48
+ assert_grid_iter_equals_getitem(has_empty)
49
+
50
+
51
+ def test_parameter_grid_getitem_uses_numpy_2_compatible_product():
52
+ grid = ParameterGrid({"a": [1, 2], "b": [3, 4]})
53
+
54
+ assert grid[0] == {"a": 1, "b": 3}
55
+ assert grid[3] == {"a": 2, "b": 4}
49
56
 
50
57
 
51
58
  def test_non_iterable_parameter_grid():
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: pyworkforce
3
- Version: 0.5.1
3
+ Version: 0.5.2
4
4
  Summary: Common tools for workforce management, schedule and optimization problems
5
5
  Home-page: https://github.com/rodrigo-arenas/pyworkforce
6
6
  Author: Rodrigo Arenas
@@ -11,66 +11,96 @@ Project-URL: Source Code, https://github.com/rodrigo-arenas/pyworkforce
11
11
  Project-URL: Bug Tracker, https://github.com/rodrigo-arenas/pyworkforce/issues
12
12
  Classifier: License :: OSI Approved :: MIT License
13
13
  Classifier: Programming Language :: Python :: 3
14
- Classifier: Programming Language :: Python :: 3.8
15
- Classifier: Programming Language :: Python :: 3.9
16
- Classifier: Programming Language :: Python :: 3.10
17
- Classifier: Programming Language :: Python :: 3.11
18
- Requires-Python: >=3.8
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Requires-Python: >=3.12,<3.15
19
18
  Description-Content-Type: text/markdown
20
19
  License-File: LICENSE
20
+ Requires-Dist: numpy>=1.26.0
21
+ Requires-Dist: ortools>=9.12.4544
22
+ Requires-Dist: pandas>=2.2.0
23
+ Requires-Dist: joblib>=1.4.0
24
+ Dynamic: author
25
+ Dynamic: author-email
26
+ Dynamic: classifier
27
+ Dynamic: description
28
+ Dynamic: description-content-type
29
+ Dynamic: home-page
30
+ Dynamic: license
31
+ Dynamic: license-file
32
+ Dynamic: project-url
33
+ Dynamic: requires-dist
34
+ Dynamic: requires-python
35
+ Dynamic: summary
21
36
 
22
37
 
23
38
  [![Build Status](https://www.travis-ci.com/rodrigo-arenas/pyworkforce.svg?branch=main)](https://www.travis-ci.com/rodrigo-arenas/pyworkforce)
24
39
  [![Codecov](https://codecov.io/gh/rodrigo-arenas/pyworkforce/branch/main/graphs/badge.svg?branch=main&service=github)](https://codecov.io/github/rodrigo-arenas/pyworkforce?branch=main)
25
40
  [![PyPI Version](https://badge.fury.io/py/pyworkforce.svg)](https://badge.fury.io/py/pyworkforce)
26
- [![Python Version](https://img.shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10%20%7C%203.11-blue)](https://www.python.org/downloads/)
41
+ [![Python Version](https://img.shields.io/badge/python-3.12%20%7C%203.13%20%7C%203.14-blue)](https://www.python.org/downloads/)
27
42
 
28
43
 
29
44
  # pyworkforce
30
- Standard tools for workforce management, queuing, scheduling, rostering and optimization problems.
45
+ Tools for workforce management problems such as queue staffing, shift scheduling,
46
+ rostering, and operations research optimization.
31
47
 
32
- Make sure to check the documentation, which is available [here](https://pyworkforce.readthedocs.io/en/stable/)
48
+ The full documentation is available at
49
+ [pyworkforce.readthedocs.io](https://pyworkforce.readthedocs.io/en/stable/).
33
50
 
34
- # Usage:
35
- Install pyworkforce
51
+ ## Installation
36
52
 
37
- It's advised to install pyworkforce using a virtual env, inside the env use:
53
+ We recommend installing pyworkforce in a virtual environment:
38
54
 
39
- ```
55
+ ```bash
40
56
  pip install pyworkforce
41
57
  ```
42
58
 
43
- If you are having troubles with or-tools installation, check the [or-tools guide](https://github.com/google/or-tools#installation)
59
+ pyworkforce supports Python 3.12, 3.13, and 3.14.
60
+
61
+ If you are using Anaconda and run into installation issues, update the environment first:
62
+
63
+ ```bash
64
+ conda update --all
65
+ ```
66
+
67
+ If the issue is related to OR-Tools, check the
68
+ [OR-Tools installation guide](https://github.com/google/or-tools#installation).
44
69
 
45
- For complete list and details of examples go to the
46
- [examples folder](https://github.com/rodrigo-arenas/pyworkforce/tree/develop/examples)
70
+ For runnable examples, see the
71
+ [examples folder](https://github.com/rodrigo-arenas/pyworkforce/tree/develop/examples).
47
72
 
48
- ## Features:
49
- pyworkforce currently includes:
73
+ ## What pyworkforce Does
74
+
75
+ pyworkforce is organized around three planning steps:
50
76
 
51
77
  ### Queuing
52
- It solves the following system resource requirements:
78
+
79
+ Use `pyworkforce.queuing` when you need to estimate how many resources are required
80
+ to handle incoming work, for example calls arriving at a call center. The current
81
+ implementation uses Erlang C assumptions: constant arrival rate, infinite queue,
82
+ and no customer dropout.
53
83
 
54
84
  ![queue_system](https://raw.githubusercontent.com/rodrigo-arenas/pyworkforce/main/docs/images/erlangc_queue_system.png)
55
85
 
56
- - **queuing.ErlangC:** Find the number of resources required to attend incoming traffic to a constant rate,
57
- infinite queue length, and no dropout.
86
+ - **queuing.ErlangC:** Calculate staffing requirements and performance metrics for one queue scenario.
87
+ - **queuing.MultiErlangC:** Run multiple Erlang C scenarios from a parameter grid.
58
88
 
59
89
  ### Scheduling
60
90
 
61
- It finds the number of resources to schedule in a shift based on the number of required positions per time interval
62
- (found, for example, using [queuing.ErlangC](./pyworkforce/queuing/erlang.py)), maximum capacity restrictions and static shifts coverage.<br>
63
- - **scheduling.MinAbsDifference:** This module finds the "optimal" assignation by minimizing the total absolute
64
- differences between required resources per interval against the scheduled resources found by the solver.
65
- - **scheduling.MinRequiredResources**: This module finds the "optimal" assignation by minimizing the total
66
- weighted amount of scheduled resources (optionally weighted by shift cost), it ensures that in all intervals, there are
67
- never fewer resources shifted than the ones required per period.
91
+ Use `pyworkforce.scheduling` when you already know the required resources by time
92
+ interval and need to choose how many people to place on each predefined shift.
93
+
94
+ - **scheduling.MinAbsDifference:** Minimizes the total absolute difference between required and scheduled resources.
95
+ - **scheduling.MinRequiredResources:** Minimizes the total weighted number of scheduled resources while ensuring every interval is covered.
68
96
 
69
97
  ### Rostering
70
98
 
71
- It assigns a list of resources to a list of required positions per day and shifts; it takes into account
72
- different restrictions as shift bans, consecutive shifts, resting days, and others.
73
- It also introduces soft restrictions like shift preferences.
99
+ Use `pyworkforce.rostering` when you have named resources and need to assign them
100
+ to days and shifts while respecting rules such as banned shifts, rest days,
101
+ minimum working hours, and preferences.
102
+
103
+ - **rostering.MinHoursRoster:** Builds a resource-level roster that covers shift requirements with the minimum scheduled hours.
74
104
 
75
105
  ### Queue systems:
76
106
 
@@ -95,7 +125,8 @@ Output:
95
125
  'waiting_probability': 0.1741319335950498}
96
126
  ```
97
127
 
98
- If you want to run different scenarios at the same time, you can use the MultiErlangC, for example, trying different service levels:
128
+ If you want to run several scenarios at the same time, use `MultiErlangC`.
129
+ For example, this tries different service-level targets:
99
130
 
100
131
  ```python
101
132
  from pyworkforce.queuing import MultiErlangC
@@ -143,13 +174,13 @@ A brief introduction can be found in this [medium post](https://towardsdatascien
143
174
  ```python
144
175
  from pyworkforce.scheduling import MinAbsDifference, MinRequiredResources
145
176
 
146
- # Rows are the days, each entry of a row, is number of positions required at an hour of the day (24).
177
+ # Rows are days. Each value is the number of required positions for one hour of the day.
147
178
  required_resources = [
148
179
  [9, 11, 17, 9, 7, 12, 5, 11, 8, 9, 18, 17, 8, 12, 16, 8, 7, 12, 11, 10, 13, 19, 16, 7],
149
180
  [13, 13, 12, 15, 18, 20, 13, 16, 17, 8, 13, 11, 6, 19, 11, 20, 19, 17, 10, 13, 14, 23, 16, 8]
150
181
  ]
151
182
 
152
- # Each entry of a shift,an hour of the day (24), 1 if the shift covers that hour, 0 otherwise
183
+ # Each shift has 24 entries, one per hour. Use 1 if the shift covers that hour, otherwise 0.
153
184
  shifts_coverage = {"Morning": [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
154
185
  "Afternoon": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0],
155
186
  "Night": [1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1],
@@ -0,0 +1,4 @@
1
+ numpy>=1.26.0
2
+ ortools>=9.12.4544
3
+ pandas>=2.2.0
4
+ joblib>=1.4.0
@@ -25,12 +25,11 @@ setup(
25
25
  author_email="rodrigo.arenas456@gmail.com",
26
26
  license="MIT",
27
27
  classifiers=[
28
- 'License :: OSI Approved :: MIT License',
29
- "Programming Language :: Python :: 3",
30
- "Programming Language :: Python :: 3.8",
31
- "Programming Language :: Python :: 3.9",
32
- "Programming Language :: Python :: 3.10",
33
- "Programming Language :: Python :: 3.11",
28
+ 'License :: OSI Approved :: MIT License',
29
+ "Programming Language :: Python :: 3",
30
+ "Programming Language :: Python :: 3.12",
31
+ "Programming Language :: Python :: 3.13",
32
+ "Programming Language :: Python :: 3.14",
34
33
  ],
35
34
  project_urls={
36
35
  "Documentation": "https://pyworkforce.readthedocs.io/en/stable/",
@@ -39,11 +38,11 @@ setup(
39
38
  },
40
39
  packages=find_packages(include=['pyworkforce', 'pyworkforce.*']),
41
40
  install_requires=[
42
- 'numpy>=1.23.0',
43
- 'ortools>=9.2.9972',
44
- 'pandas>=1.3.5',
45
- 'joblib>0.17'
46
- ],
47
- python_requires=">=3.8",
41
+ 'numpy>=1.26.0',
42
+ 'ortools>=9.12.4544',
43
+ 'pandas>=2.2.0',
44
+ 'joblib>=1.4.0'
45
+ ],
46
+ python_requires=">=3.12,<3.15",
48
47
  include_package_data=True,
49
48
  )
@@ -1 +0,0 @@
1
- __version__ = "0.5.1"
@@ -1,4 +0,0 @@
1
- numpy>=1.23.0
2
- ortools>=9.2.9972
3
- pandas>=1.3.5
4
- joblib>0.17
File without changes
File without changes