Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add numerical support #44

Open
wants to merge 20 commits into
base: 0.5.1dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ numpy>=1.23.0
ortools>=9.1.9552
pandas>=1.3.5
joblib>0.17
pydantic==1.10.7
sphinx
sphinx_gallery
sphinx_rtd_theme
Expand Down
5 changes: 5 additions & 0 deletions docs/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ Features:

* Remove support for python 3.7 and add support for python up to 3.11
* Update the project dependencies
* Support of non interger transactions. This allows for more accurate agent estimations.
* Replacement of parameter grid with automatic dictionary handeling for multiple scenarios and multiple parameters.
* Added enforce_trafficker_requirements to allow final rounding towards intergers. If using data for capacity estimations it is adviced to do rounding in your final step.
* Added pydantic based data model for ErlangC to ensure correctness of input values.
* ErlangC renamed to erlang for future expansions.

What's new in 0.5.0
-------------------
Expand Down
3 changes: 1 addition & 2 deletions pyworkforce/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
from .queuing import ErlangC, MultiErlangC
from .queuing import ErlangC
from .scheduling import MinRequiredResources, MinAbsDifference
from .rostering import MinHoursRoster
from ._version import __version__

__all__ = [
"ErlangC",
"MultiErlangC",
"MinRequiredResources",
"MinAbsDifference",
"MinHoursRoster",
Expand Down
4 changes: 2 additions & 2 deletions pyworkforce/queuing/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from pyworkforce.queuing.erlang import ErlangC, MultiErlangC
from pyworkforce.queuing.erlang import ErlangC

__all__ = ["ErlangC", "MultiErlangC"]
__all__ = ["ErlangC"]
407 changes: 148 additions & 259 deletions pyworkforce/queuing/erlang.py

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions pyworkforce/queuing/queueing_constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
cTransactions_name = 'transactions'
cShrinkage_name = 'shrinkage'
cAHT_name = 'aht'
cInterval_name = 'interval'
cASA_name = 'asa'

cErlangC_generic_variables = {
cTransactions_name:cTransactions_name,
cShrinkage_name:cShrinkage_name,
cAHT_name:cAHT_name,
cInterval_name:cInterval_name,
cASA_name:cASA_name,
}
155 changes: 66 additions & 89 deletions pyworkforce/queuing/tests/test_erlang.py
Original file line number Diff line number Diff line change
@@ -1,92 +1,69 @@
import pytest
from pyworkforce.queuing import ErlangC


def test_expected_erlangc_results():
erlang = ErlangC(transactions=100, asa=0.33, aht=3, interval=30, shrinkage=0.3)
results = erlang.required_positions(service_level=0.8, max_occupancy=0.85)
raw_positions = results['raw_positions']
positions = results['positions']
service_level = results['service_level']
occupancy = results['occupancy']
waiting_probability = results['waiting_probability']

assert raw_positions == 14
assert positions == 20
assert round(service_level, 3) == 0.888
assert round(occupancy, 3) == 0.714
assert round(waiting_probability, 3) == 0.174


def test_scale_positions_erlangc():
erlang = ErlangC(transactions=100, asa=0.33, aht=3, interval=30, shrinkage=0.3)
results = erlang.required_positions(service_level=0.8, max_occupancy=0.85)
positions = results['positions']
service_level = erlang.service_level(positions=positions, scale_positions=True)
occupancy = erlang.achieved_occupancy(positions=positions, scale_positions=True)
waiting_probability = erlang.waiting_probability(positions=positions, scale_positions=True)

assert positions == 20
assert round(service_level, 3) == 0.888
assert round(occupancy, 3) == 0.714
assert round(waiting_probability, 3) == 0.174


def test_over_occupancy_erlangc():
erlang = ErlangC(transactions=100, asa=0.33, aht=3, interval=30, shrinkage=0.3)
results = erlang.required_positions(service_level=0.8, max_occupancy=0.7)
raw_positions = results['raw_positions']
positions = results['positions']
service_level = erlang.service_level(positions=positions, scale_positions=True)
occupancy = erlang.achieved_occupancy(positions=positions, scale_positions=True)
waiting_probability = erlang.waiting_probability(positions=positions, scale_positions=True)

assert raw_positions == 15
assert positions == 22
assert round(service_level, 3) == 0.941
assert round(occupancy, 3) == 0.667
assert round(waiting_probability, 3) == 0.102


def test_wrong_transactions_erlangc():
with pytest.raises(Exception) as excinfo:
erlang = ErlangC(transactions=-20, asa=0.33, aht=3, interval=30, shrinkage=0.3)
assert str(excinfo.value) == "transactions can't be smaller or equals than 0"


def test_wrong_aht_erlangc():
with pytest.raises(Exception) as excinfo:
erlang = ErlangC(transactions=100, asa=0.33, aht=-5, interval=30, shrinkage=0.3)
assert str(excinfo.value) == "aht can't be smaller or equals than 0"


def test_wrong_asa_erlangc():
with pytest.raises(Exception) as excinfo:
erlang = ErlangC(transactions=100, asa=0, aht=5, interval=30, shrinkage=0.3)
assert str(excinfo.value) == "asa can't be smaller or equals than 0"


def test_wrong_interval_erlangc():
with pytest.raises(Exception) as excinfo:
erlang = ErlangC(transactions=100, asa=10, aht=5, interval=-30, shrinkage=0.3)
assert str(excinfo.value) == "interval can't be smaller or equals than 0"


def test_wrong_shrinkage_erlangc():
with pytest.raises(Exception) as excinfo:
erlang = ErlangC(transactions=100, asa=10, aht=5, interval=30, shrinkage=1)
assert str(excinfo.value) == "shrinkage must be between in the interval [0,1)"


def test_wrong_service_level_erlangc():
erlang = ErlangC(transactions=100, asa=0.33, aht=3, interval=30, shrinkage=0.3)
with pytest.raises(Exception) as excinfo:
results = erlang.required_positions(service_level=1.8, max_occupancy=0.85)
assert str(excinfo.value) == "service_level must be between 0 and 1"


def test_wrong_max_occupancy_erlangc():
erlang = ErlangC(transactions=100, asa=0.33, aht=3, interval=30, shrinkage=0.3)
with pytest.raises(Exception) as excinfo:
results = erlang.required_positions(service_level=0.8, max_occupancy=1.2)
assert str(excinfo.value) == "max_occupancy must be between 0 and 1"
class TestDefaultErlangCBehaviour:
"""
Test regular erlangC inputs for single and multi scenario dictionaries.
"""


single_scenario_erlangC_legacy = {"test_scenario 1": {"transactions": 100, "aht": 3.0, "asa": .33, "shrinkage": 0.3, "interval": 30, 'service_level_target':.8}}
single_scenario_erlangC = {"test_scenario 1": {"transactions": 100, "aht": 3.0, "asa": .33, "shrinkage": 0.3, "interval": 30, 'service_level_target':.8}}
multiple_scenario_erlangC = {"test_scenario 1": {"transactions": 100, "aht": 3.0, "asa": 20 / 60, "shrinkage": 0.3, "interval": 30, 'service_level_target':.8},
"test_scenario 2": {"transactions": [100,200], "aht": 3.0, "asa": 20 / 60, "shrinkage": 0.3, "interval": 30, 'service_level_target':.8}}

def test_erlangc_single_scenario_results_legacy(self):

erlang = ErlangC(erlang_scenarios=self.single_scenario_erlangC_legacy)
erlang.calculate_required_positions()
results = erlang.results_to_dataframe()
results = results.round(3)
assert (results['raw_positions'] == 14).all()
assert (results['positions'] == 19).all()
assert (results['achieved_service_level'] == 0.888).all()
assert (results['achieved_occupancy'] == 0.714).all()
assert (results['waiting_probability'] == 0.174).all()

def test_erlangc_single_scenario_results(self):

erlang = ErlangC(erlang_scenarios=self.single_scenario_erlangC)
erlang.calculate_required_positions(enforce_trafficking_requirements=False)
results = erlang.results_to_dataframe()
results = results.round(3)

assert (results['raw_positions'] == 13.1).all()
assert (results['positions'] == 18.714).all()
assert (results['achieved_service_level'] == 0.817).all()
assert (results['achieved_occupancy'] == 0.763).all()
assert (results['waiting_probability'] == 0.257).all()

def test_erlangc_multi_scenario_results(self):

erlang = ErlangC(erlang_scenarios=self.multiple_scenario_erlangC)
erlang.calculate_required_positions(enforce_trafficking_requirements=False)
results = erlang.results_to_dataframe()
results = results.round(3)

columns = ['scenario', 'subscenario', 'transactions', 'aht', 'asa', 'shrinkage',
'interval', 'service_level_target', 'achieved_service_level',
'raw_positions', 'positions', 'maximum_occupancy',
'waiting_probability', 'achieved_occupancy', 'intensity']

assert results.shape == (3,15)
assert (results.columns == columns).all()
assert (results['scenario'].isin(list(self.multiple_scenario_erlangC.keys()) )).all()

scenario_1 = results[results.scenario == "test_scenario 1"]
assert (scenario_1['raw_positions'] == 13.1).all()
assert (scenario_1['positions'] == 18.714).all()
assert (scenario_1['achieved_service_level'] == 0.818).all()
assert (scenario_1['achieved_occupancy'] == 0.763).all()
assert (scenario_1['waiting_probability'] == 0.257).all()

scenario_2 = results[results.scenario == "test_scenario 2"]
assert (scenario_2['raw_positions'] == [13.1,23.9]).all()
assert (scenario_2['positions'] == [18.714, 34.143]).all()
assert (scenario_2['achieved_service_level'] == [0.818, 0.801]).all()
assert (scenario_2['achieved_occupancy'] == [0.763, 0.837]).all()
assert (scenario_2['waiting_probability'] == [0.257, 0.307]).all()

95 changes: 0 additions & 95 deletions pyworkforce/queuing/tests/test_multi_erlang.py

This file was deleted.

1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
'ortools>=9.2.9972',
'pandas>=1.3.5',
'joblib>0.17'
'pydantic==1.10.7'
],
python_requires=">=3.8",
include_package_data=True,
Expand Down