leaguescheduler 0.1.0__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.
- leaguescheduler-0.1.0/LICENSE +7 -0
- leaguescheduler-0.1.0/PKG-INFO +210 -0
- leaguescheduler-0.1.0/README.md +178 -0
- leaguescheduler-0.1.0/leaguescheduler/__init__.py +11 -0
- leaguescheduler-0.1.0/leaguescheduler/constants.py +8 -0
- leaguescheduler-0.1.0/leaguescheduler/input_parser.py +109 -0
- leaguescheduler-0.1.0/leaguescheduler/league_scheduler.py +803 -0
- leaguescheduler-0.1.0/leaguescheduler/main.py +117 -0
- leaguescheduler-0.1.0/leaguescheduler/params.py +46 -0
- leaguescheduler-0.1.0/leaguescheduler/transportation_problem_solver.py +78 -0
- leaguescheduler-0.1.0/leaguescheduler/utils.py +83 -0
- leaguescheduler-0.1.0/leaguescheduler.egg-info/PKG-INFO +210 -0
- leaguescheduler-0.1.0/leaguescheduler.egg-info/SOURCES.txt +18 -0
- leaguescheduler-0.1.0/leaguescheduler.egg-info/dependency_links.txt +1 -0
- leaguescheduler-0.1.0/leaguescheduler.egg-info/entry_points.txt +2 -0
- leaguescheduler-0.1.0/leaguescheduler.egg-info/requires.txt +12 -0
- leaguescheduler-0.1.0/leaguescheduler.egg-info/top_level.txt +1 -0
- leaguescheduler-0.1.0/pyproject.toml +99 -0
- leaguescheduler-0.1.0/setup.cfg +4 -0
- leaguescheduler-0.1.0/tests/test_oracle.py +38 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2026 Samuel Borms
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: leaguescheduler
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Generate optimal schedules for your time-relaxed double round-robin (2RR) sports leagues
|
|
5
|
+
Author-email: Samuel Borms <sam@desirdata.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/sborms/leaguescheduler
|
|
8
|
+
Project-URL: Repository, https://github.com/sborms/leaguescheduler
|
|
9
|
+
Project-URL: Issues, https://github.com/sborms/leaguescheduler/issues
|
|
10
|
+
Keywords: scheduling,sports,league,round-robin,tabu-search
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering
|
|
17
|
+
Requires-Python: <3.14,>=3.13
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: matplotlib>=3.9.0
|
|
21
|
+
Requires-Dist: numpy>=2.3.4
|
|
22
|
+
Requires-Dist: openpyxl>=3.1.3
|
|
23
|
+
Requires-Dist: pandas>=2.3.0
|
|
24
|
+
Requires-Dist: typer>=0.15.2
|
|
25
|
+
Requires-Dist: XlsxWriter>=3.2.0
|
|
26
|
+
Requires-Dist: fasttps>=0.1.0
|
|
27
|
+
Requires-Dist: tool>=0.8.0
|
|
28
|
+
Requires-Dist: ty>=0.0.19
|
|
29
|
+
Provides-Extra: app
|
|
30
|
+
Requires-Dist: streamlit>=1.54.0; extra == "app"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# ⚽📅 2RR League Scheduler
|
|
34
|
+
|
|
35
|
+
[](https://pypi.org/project/leaguescheduler/)
|
|
36
|
+
[](https://pypi.org/project/leaguescheduler/)
|
|
37
|
+
[](https://github.com/sborms/leaguescheduler/actions/workflows/ci.yaml)
|
|
38
|
+
[](https://opensource.org/licenses/MIT)
|
|
39
|
+
[](https://leaguescheduler.streamlit.app)
|
|
40
|
+
|
|
41
|
+
If you are looking to schedule a sports league with at least the following constraints...
|
|
42
|
+
|
|
43
|
+
- Everyone plays 1 home game and 1 away game against each other (double round-robin)
|
|
44
|
+
- Home games are played on reserved dates
|
|
45
|
+
- Away games are not played on unavailable dates
|
|
46
|
+
- No team plays 2 games on the same day
|
|
47
|
+
- The calendar is spread out such that teams have enough rest days between games
|
|
48
|
+
|
|
49
|
+
... then this will help you!
|
|
50
|
+
|
|
51
|
+
This software implements **constrained time-relaxed double round-robin (2RR) sports league scheduling** using the tabu search based heuristic algorithm described in the paper [**Scheduling a non-professional indoor football league**](https://pure.tue.nl/ws/portalfiles/portal/121797609/Bulck2019_Article_SchedulingANon_professionalInd.pdf) by Van Bulck, Goosens and Spieksma (2019). The meta-algorithm heavily relies on the Hungarian algorithm to recurrently solve the transportation problem. Some additional tricks were added, especially to minimize excessive rest days (internally fixed at 28) between consecutive games of teams.
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
Install from PyPI:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install leaguescheduler
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Or with [uv](https://docs.astral.sh/uv):
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
uv add leaguescheduler
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
For development, clone the repository and run:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
uv venv
|
|
71
|
+
uv sync
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Usage
|
|
75
|
+
|
|
76
|
+
### Input
|
|
77
|
+
|
|
78
|
+
The Excel file `example/input.xlsx` contains an example of the input data. The input should always, in the exact format as in the example, include the reserved dates (together with location and time) and unavailable dates of all teams from a single league.
|
|
79
|
+
|
|
80
|
+
One sheet corresponds to one league.
|
|
81
|
+
|
|
82
|
+
For instance, a league could consist of 10 teams, each with about 12 to 20 reserved dates and a number of unavailable dates.
|
|
83
|
+
|
|
84
|
+
### Output
|
|
85
|
+
|
|
86
|
+
The generated output for a single league is a solutions matrix `X` that can easily be converted into a clear `DataFrame` calendar, and stored as an Excel file.
|
|
87
|
+
|
|
88
|
+
The calendar includes the date, time, location, home team and away team for each game. The unplanned games are put at the bottom.
|
|
89
|
+
|
|
90
|
+
### Scheduling
|
|
91
|
+
|
|
92
|
+
#### CLI
|
|
93
|
+
|
|
94
|
+
You can use the scheduler from the command line as follows:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
2rr \
|
|
98
|
+
--input_file "example/input.xlsx" \
|
|
99
|
+
--output_folder "example/output" \
|
|
100
|
+
--seed 321 \
|
|
101
|
+
--n_iterations 500
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Alternatively, you can execute `make example` which runs the above example.
|
|
105
|
+
|
|
106
|
+
See `2rr --help` (and the research paper mentioned at the top) for more information about all the available arguments.
|
|
107
|
+
|
|
108
|
+
<details>
|
|
109
|
+
<summary><b>CLI reference</b></summary>
|
|
110
|
+
|
|
111
|
+
| Option | Type | Default | Description |
|
|
112
|
+
|--------|------|---------|-------------|
|
|
113
|
+
| `--input-file` | text | *required* | Input Excel file with for every team their (un)availability data |
|
|
114
|
+
| `--output-folder` | text | *required* | Folder where the outputs (logs, overview, schedules) will be stored |
|
|
115
|
+
| `--seed` | integer | `None` | Optional seed for `np.random.seed()` |
|
|
116
|
+
| `--unavailable` | text | `NIET` | Cell value to indicate that a team is unavailable |
|
|
117
|
+
| `--clip-bot` | integer | `1` | Value for clipping rest days plot on low end |
|
|
118
|
+
| `--clip-upp` | integer | `41` | Value for clipping rest days plot on high end |
|
|
119
|
+
| `--net / --no-net` | flag | `--no-net` | Report the adjusted number of rest days |
|
|
120
|
+
| `--tabu-length` | integer | `4` | Number of iterations during which a team cannot be selected |
|
|
121
|
+
| `--perturbation-length` | integer | `50` | Check perturbation need every this many iterations |
|
|
122
|
+
| `--n-iterations` | integer | `10000` | Number of tabu phase iterations |
|
|
123
|
+
| `--m` | integer | `7` | Minimum number of time slots between 2 games with same pair of teams |
|
|
124
|
+
| `--p` | integer | `1000` | Cost from dummy supply node q to non-dummy demand node |
|
|
125
|
+
| `--r-max` | integer | `4` | Minimum required time slots for 2 games of same team |
|
|
126
|
+
| `--alpha` | float | `0.5` | Probability of picking perturbation operator 1 |
|
|
127
|
+
| `--beta` | float | `0.01` | Probability of removing a game in operator 1 |
|
|
128
|
+
| `--cost-excessive-rest-days` | float | `500` | Cost for excessive rest days |
|
|
129
|
+
|
|
130
|
+
</details>
|
|
131
|
+
|
|
132
|
+
#### Classes
|
|
133
|
+
|
|
134
|
+
To more freely play around, you can import the core classes in your own Python script or notebook.
|
|
135
|
+
|
|
136
|
+
Here's a minimal example with default parameters:
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
from leaguescheduler import InputParser, LeagueScheduler
|
|
140
|
+
|
|
141
|
+
input = InputParser(input_file)
|
|
142
|
+
input.from_excel(sheet_name=input.sheet_names[0])
|
|
143
|
+
input.parse()
|
|
144
|
+
|
|
145
|
+
scheduler = LeagueScheduler(input=input)
|
|
146
|
+
scheduler.construction_phase()
|
|
147
|
+
scheduler.tabu_phase()
|
|
148
|
+
|
|
149
|
+
df = scheduler.create_calendar()
|
|
150
|
+
scheduler.store_calendar(df, file="out/calendar.xlsx")
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Type `help(LeagueScheduler)` to show the full documentation.
|
|
154
|
+
|
|
155
|
+
<details>
|
|
156
|
+
<summary><b>Python API reference</b></summary>
|
|
157
|
+
|
|
158
|
+
**`InputParser(filename, unavailable="NIET")`** — Reads and parses input Excel file.
|
|
159
|
+
|
|
160
|
+
**`SchedulerParams(...)`** — Configuration dataclass for the scheduler.
|
|
161
|
+
|
|
162
|
+
| Parameter | Type | Default | Description |
|
|
163
|
+
|-----------|------|---------|-------------|
|
|
164
|
+
| `tabu_length` | int | `4` | Iterations during which a team cannot be selected |
|
|
165
|
+
| `perturbation_length` | int | `50` | Check perturbation need every this many iterations |
|
|
166
|
+
| `n_iterations` | int | `10000` | Number of tabu phase iterations |
|
|
167
|
+
| `m` | int | `7` | Min time slots between 2 games with same pair |
|
|
168
|
+
| `p` | int | `1000` | Cost from dummy supply node to non-dummy demand node |
|
|
169
|
+
| `r_max` | int | `4` | Min required time slots for 2 games of same team |
|
|
170
|
+
| `penalties` | dict | `{}` | Custom penalty mapping for rest days |
|
|
171
|
+
| `alpha` | float | `0.5` | Probability of picking perturbation operator 1 |
|
|
172
|
+
| `beta` | float | `0.01` | Probability of removing a game in operator 1 |
|
|
173
|
+
| `cost_excessive_rest_days` | float | `500` | Cost for excessive rest days |
|
|
174
|
+
|
|
175
|
+
**`LeagueScheduler(input, params=SchedulerParams(), logger=None)`** — Main scheduler class.
|
|
176
|
+
|
|
177
|
+
</details>
|
|
178
|
+
|
|
179
|
+
#### Web application
|
|
180
|
+
|
|
181
|
+
The league scheduler is also made available through a hosted [Streamlit application](https://leaguescheduler.streamlit.app).
|
|
182
|
+
|
|
183
|
+
It has a more limited set of parameters (namely `m`, `r_max`, `n_iterations`, and `penalties`) but can be used out of the box yet without logging.
|
|
184
|
+
|
|
185
|
+
Additionally, the output file includes for every league and by team the distribution of the **number of *adjusted* rest days between games** (meaning that unavailable dates by that team are not considered in the count of the rest days), as well as the **unused home time slots per team**. This facilitates post-analysis of the quality of the generated calendar.
|
|
186
|
+
|
|
187
|
+
If the app sleeps due to inactivity 😴, just wake it back up. You can run the app locally with `make web`.
|
|
188
|
+
|
|
189
|
+
#### Timings
|
|
190
|
+
|
|
191
|
+
How long does the scheduler take? This table sheds some baseline light for a league of **13 teams**:
|
|
192
|
+
|
|
193
|
+
| Iterations | Time |
|
|
194
|
+
|------------------|----------- |
|
|
195
|
+
| 10 | <1s |
|
|
196
|
+
| 100 | <1s |
|
|
197
|
+
| 1k | <1s |
|
|
198
|
+
| 10k | ~3s |
|
|
199
|
+
| 100k | ~25s |
|
|
200
|
+
| 1M | ~245s |
|
|
201
|
+
|
|
202
|
+
*Run on a few years old Windows 10 Pro machine with Intel i7–7700HQ CPU and 32GB RAM.*
|
|
203
|
+
|
|
204
|
+
A few 100(0)s iterations are typically sufficient to arrive at a good schedule.
|
|
205
|
+
|
|
206
|
+
Quite fast. Thanks to Ra-Ra-Rust! 🦀
|
|
207
|
+
|
|
208
|
+
## Feedback?
|
|
209
|
+
|
|
210
|
+
Let me know if you have any feedback or suggestions for improvement!
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# ⚽📅 2RR League Scheduler
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/leaguescheduler/)
|
|
4
|
+
[](https://pypi.org/project/leaguescheduler/)
|
|
5
|
+
[](https://github.com/sborms/leaguescheduler/actions/workflows/ci.yaml)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://leaguescheduler.streamlit.app)
|
|
8
|
+
|
|
9
|
+
If you are looking to schedule a sports league with at least the following constraints...
|
|
10
|
+
|
|
11
|
+
- Everyone plays 1 home game and 1 away game against each other (double round-robin)
|
|
12
|
+
- Home games are played on reserved dates
|
|
13
|
+
- Away games are not played on unavailable dates
|
|
14
|
+
- No team plays 2 games on the same day
|
|
15
|
+
- The calendar is spread out such that teams have enough rest days between games
|
|
16
|
+
|
|
17
|
+
... then this will help you!
|
|
18
|
+
|
|
19
|
+
This software implements **constrained time-relaxed double round-robin (2RR) sports league scheduling** using the tabu search based heuristic algorithm described in the paper [**Scheduling a non-professional indoor football league**](https://pure.tue.nl/ws/portalfiles/portal/121797609/Bulck2019_Article_SchedulingANon_professionalInd.pdf) by Van Bulck, Goosens and Spieksma (2019). The meta-algorithm heavily relies on the Hungarian algorithm to recurrently solve the transportation problem. Some additional tricks were added, especially to minimize excessive rest days (internally fixed at 28) between consecutive games of teams.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
Install from PyPI:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install leaguescheduler
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or with [uv](https://docs.astral.sh/uv):
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
uv add leaguescheduler
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
For development, clone the repository and run:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
uv venv
|
|
39
|
+
uv sync
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
### Input
|
|
45
|
+
|
|
46
|
+
The Excel file `example/input.xlsx` contains an example of the input data. The input should always, in the exact format as in the example, include the reserved dates (together with location and time) and unavailable dates of all teams from a single league.
|
|
47
|
+
|
|
48
|
+
One sheet corresponds to one league.
|
|
49
|
+
|
|
50
|
+
For instance, a league could consist of 10 teams, each with about 12 to 20 reserved dates and a number of unavailable dates.
|
|
51
|
+
|
|
52
|
+
### Output
|
|
53
|
+
|
|
54
|
+
The generated output for a single league is a solutions matrix `X` that can easily be converted into a clear `DataFrame` calendar, and stored as an Excel file.
|
|
55
|
+
|
|
56
|
+
The calendar includes the date, time, location, home team and away team for each game. The unplanned games are put at the bottom.
|
|
57
|
+
|
|
58
|
+
### Scheduling
|
|
59
|
+
|
|
60
|
+
#### CLI
|
|
61
|
+
|
|
62
|
+
You can use the scheduler from the command line as follows:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
2rr \
|
|
66
|
+
--input_file "example/input.xlsx" \
|
|
67
|
+
--output_folder "example/output" \
|
|
68
|
+
--seed 321 \
|
|
69
|
+
--n_iterations 500
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Alternatively, you can execute `make example` which runs the above example.
|
|
73
|
+
|
|
74
|
+
See `2rr --help` (and the research paper mentioned at the top) for more information about all the available arguments.
|
|
75
|
+
|
|
76
|
+
<details>
|
|
77
|
+
<summary><b>CLI reference</b></summary>
|
|
78
|
+
|
|
79
|
+
| Option | Type | Default | Description |
|
|
80
|
+
|--------|------|---------|-------------|
|
|
81
|
+
| `--input-file` | text | *required* | Input Excel file with for every team their (un)availability data |
|
|
82
|
+
| `--output-folder` | text | *required* | Folder where the outputs (logs, overview, schedules) will be stored |
|
|
83
|
+
| `--seed` | integer | `None` | Optional seed for `np.random.seed()` |
|
|
84
|
+
| `--unavailable` | text | `NIET` | Cell value to indicate that a team is unavailable |
|
|
85
|
+
| `--clip-bot` | integer | `1` | Value for clipping rest days plot on low end |
|
|
86
|
+
| `--clip-upp` | integer | `41` | Value for clipping rest days plot on high end |
|
|
87
|
+
| `--net / --no-net` | flag | `--no-net` | Report the adjusted number of rest days |
|
|
88
|
+
| `--tabu-length` | integer | `4` | Number of iterations during which a team cannot be selected |
|
|
89
|
+
| `--perturbation-length` | integer | `50` | Check perturbation need every this many iterations |
|
|
90
|
+
| `--n-iterations` | integer | `10000` | Number of tabu phase iterations |
|
|
91
|
+
| `--m` | integer | `7` | Minimum number of time slots between 2 games with same pair of teams |
|
|
92
|
+
| `--p` | integer | `1000` | Cost from dummy supply node q to non-dummy demand node |
|
|
93
|
+
| `--r-max` | integer | `4` | Minimum required time slots for 2 games of same team |
|
|
94
|
+
| `--alpha` | float | `0.5` | Probability of picking perturbation operator 1 |
|
|
95
|
+
| `--beta` | float | `0.01` | Probability of removing a game in operator 1 |
|
|
96
|
+
| `--cost-excessive-rest-days` | float | `500` | Cost for excessive rest days |
|
|
97
|
+
|
|
98
|
+
</details>
|
|
99
|
+
|
|
100
|
+
#### Classes
|
|
101
|
+
|
|
102
|
+
To more freely play around, you can import the core classes in your own Python script or notebook.
|
|
103
|
+
|
|
104
|
+
Here's a minimal example with default parameters:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from leaguescheduler import InputParser, LeagueScheduler
|
|
108
|
+
|
|
109
|
+
input = InputParser(input_file)
|
|
110
|
+
input.from_excel(sheet_name=input.sheet_names[0])
|
|
111
|
+
input.parse()
|
|
112
|
+
|
|
113
|
+
scheduler = LeagueScheduler(input=input)
|
|
114
|
+
scheduler.construction_phase()
|
|
115
|
+
scheduler.tabu_phase()
|
|
116
|
+
|
|
117
|
+
df = scheduler.create_calendar()
|
|
118
|
+
scheduler.store_calendar(df, file="out/calendar.xlsx")
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Type `help(LeagueScheduler)` to show the full documentation.
|
|
122
|
+
|
|
123
|
+
<details>
|
|
124
|
+
<summary><b>Python API reference</b></summary>
|
|
125
|
+
|
|
126
|
+
**`InputParser(filename, unavailable="NIET")`** — Reads and parses input Excel file.
|
|
127
|
+
|
|
128
|
+
**`SchedulerParams(...)`** — Configuration dataclass for the scheduler.
|
|
129
|
+
|
|
130
|
+
| Parameter | Type | Default | Description |
|
|
131
|
+
|-----------|------|---------|-------------|
|
|
132
|
+
| `tabu_length` | int | `4` | Iterations during which a team cannot be selected |
|
|
133
|
+
| `perturbation_length` | int | `50` | Check perturbation need every this many iterations |
|
|
134
|
+
| `n_iterations` | int | `10000` | Number of tabu phase iterations |
|
|
135
|
+
| `m` | int | `7` | Min time slots between 2 games with same pair |
|
|
136
|
+
| `p` | int | `1000` | Cost from dummy supply node to non-dummy demand node |
|
|
137
|
+
| `r_max` | int | `4` | Min required time slots for 2 games of same team |
|
|
138
|
+
| `penalties` | dict | `{}` | Custom penalty mapping for rest days |
|
|
139
|
+
| `alpha` | float | `0.5` | Probability of picking perturbation operator 1 |
|
|
140
|
+
| `beta` | float | `0.01` | Probability of removing a game in operator 1 |
|
|
141
|
+
| `cost_excessive_rest_days` | float | `500` | Cost for excessive rest days |
|
|
142
|
+
|
|
143
|
+
**`LeagueScheduler(input, params=SchedulerParams(), logger=None)`** — Main scheduler class.
|
|
144
|
+
|
|
145
|
+
</details>
|
|
146
|
+
|
|
147
|
+
#### Web application
|
|
148
|
+
|
|
149
|
+
The league scheduler is also made available through a hosted [Streamlit application](https://leaguescheduler.streamlit.app).
|
|
150
|
+
|
|
151
|
+
It has a more limited set of parameters (namely `m`, `r_max`, `n_iterations`, and `penalties`) but can be used out of the box yet without logging.
|
|
152
|
+
|
|
153
|
+
Additionally, the output file includes for every league and by team the distribution of the **number of *adjusted* rest days between games** (meaning that unavailable dates by that team are not considered in the count of the rest days), as well as the **unused home time slots per team**. This facilitates post-analysis of the quality of the generated calendar.
|
|
154
|
+
|
|
155
|
+
If the app sleeps due to inactivity 😴, just wake it back up. You can run the app locally with `make web`.
|
|
156
|
+
|
|
157
|
+
#### Timings
|
|
158
|
+
|
|
159
|
+
How long does the scheduler take? This table sheds some baseline light for a league of **13 teams**:
|
|
160
|
+
|
|
161
|
+
| Iterations | Time |
|
|
162
|
+
|------------------|----------- |
|
|
163
|
+
| 10 | <1s |
|
|
164
|
+
| 100 | <1s |
|
|
165
|
+
| 1k | <1s |
|
|
166
|
+
| 10k | ~3s |
|
|
167
|
+
| 100k | ~25s |
|
|
168
|
+
| 1M | ~245s |
|
|
169
|
+
|
|
170
|
+
*Run on a few years old Windows 10 Pro machine with Intel i7–7700HQ CPU and 32GB RAM.*
|
|
171
|
+
|
|
172
|
+
A few 100(0)s iterations are typically sufficient to arrive at a good schedule.
|
|
173
|
+
|
|
174
|
+
Quite fast. Thanks to Ra-Ra-Rust! 🦀
|
|
175
|
+
|
|
176
|
+
## Feedback?
|
|
177
|
+
|
|
178
|
+
Let me know if you have any feedback or suggestions for improvement!
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from leaguescheduler.input_parser import InputParser
|
|
2
|
+
from leaguescheduler.league_scheduler import LeagueScheduler
|
|
3
|
+
from leaguescheduler.params import SchedulerParams
|
|
4
|
+
from leaguescheduler.transportation_problem_solver import TransportationProblemSolver
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"InputParser",
|
|
8
|
+
"SchedulerParams",
|
|
9
|
+
"LeagueScheduler",
|
|
10
|
+
"TransportationProblemSolver",
|
|
11
|
+
]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# NOTE: Should be large enough to avoid overlap with any possible time slot (difference)
|
|
2
|
+
LARGE_NBR = 9999
|
|
3
|
+
|
|
4
|
+
# NOTE: The order of columns should stay date - time - location - home - away!
|
|
5
|
+
OUTPUT_COLS = ["Date", "Hour", "Location", "Home", "Away"]
|
|
6
|
+
|
|
7
|
+
# NOTE: Fixed (so not a parameter)
|
|
8
|
+
MAX_ALLOWED_REST_DAYS = 28
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
|
|
4
|
+
from .utils import fill_value
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class InputParser:
|
|
8
|
+
"""Reads input from Excel file for given league and parses relevant data."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, filename: str, unavailable: str = "NIET") -> None:
|
|
11
|
+
"""
|
|
12
|
+
Initializes a new instance of the InputParser class.
|
|
13
|
+
|
|
14
|
+
Every sheet should be structured as follows:
|
|
15
|
+
- Row 1 has the team names T (starting from column 2).
|
|
16
|
+
- Row 2 has the locations for home games (starting from column 2).
|
|
17
|
+
- Column 1 has the league dates S (starting from row 4) [row 3 is for comments].
|
|
18
|
+
- Cells with value 'unavailable' indicate where a column team is not available (A).
|
|
19
|
+
- Cells with another value are considered the home slot hours, e.g., "20h30" (H).
|
|
20
|
+
- Cells with no value are considered baseline available slots.
|
|
21
|
+
|
|
22
|
+
Optionally, a single sheet can be named 'penalties' and contain the penalties
|
|
23
|
+
for the algorithm. The penalties are read as a dictionary with the number of
|
|
24
|
+
days as keys (column 1) and the penalties as values (column 2). In this case,
|
|
25
|
+
the number of days is defined as the exact number of rest days. This class
|
|
26
|
+
internally adds 1 to each key so it conforms to the rest days + 1 convention
|
|
27
|
+
used in SchedulerParams.penalties.
|
|
28
|
+
|
|
29
|
+
:param filename: Path location where input Excel file is stored.
|
|
30
|
+
:param unavailable: Cell value to indicate that a team is unavailable.
|
|
31
|
+
"""
|
|
32
|
+
self.unavailable = unavailable
|
|
33
|
+
|
|
34
|
+
self.file = pd.ExcelFile(filename)
|
|
35
|
+
self.sheet_names = [
|
|
36
|
+
sheet_name
|
|
37
|
+
for sheet_name in self.file.sheet_names
|
|
38
|
+
if sheet_name != "penalties"
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
self.penalties = self.get_penalties() # same regardless of league sheet
|
|
42
|
+
|
|
43
|
+
self.data = None
|
|
44
|
+
self.parsed = False
|
|
45
|
+
|
|
46
|
+
def get_penalties(self) -> dict:
|
|
47
|
+
"""Reads penalties (if available) from input Excel file and returns them as a dictionary."""
|
|
48
|
+
penalties = {} # fallback
|
|
49
|
+
|
|
50
|
+
if "penalties" in self.file.sheet_names:
|
|
51
|
+
penalties = (
|
|
52
|
+
pd.read_excel(self.file, sheet_name="penalties", index_col=0)
|
|
53
|
+
.iloc[:, 0]
|
|
54
|
+
.to_dict()
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# format needs {n_days: penalty} where n_days = rest days + 1 as an int
|
|
58
|
+
# 0 --> e.g., a game on Monday and Tuesday (0 rest days but delta t is 1)
|
|
59
|
+
penalties = {int(k) + 1: v for k, v in penalties.items()}
|
|
60
|
+
|
|
61
|
+
return penalties
|
|
62
|
+
|
|
63
|
+
def from_excel(self, sheet_name: str = None) -> None:
|
|
64
|
+
"""Reads data from input Excel file and sheet, then assigns it to self.data."""
|
|
65
|
+
if sheet_name is None or sheet_name in self.sheet_names:
|
|
66
|
+
data = pd.read_excel(self.file, sheet_name=sheet_name)
|
|
67
|
+
data = data.drop(1, axis=0) # drop row at index 1 (row 3 in Excel file)
|
|
68
|
+
self.data = data.reset_index(drop=True)
|
|
69
|
+
else:
|
|
70
|
+
raise ValueError(f"Sheet name {sheet_name} not found in file.")
|
|
71
|
+
|
|
72
|
+
def parse(self) -> None:
|
|
73
|
+
"""Extracts (aka parses) relevant data from input file."""
|
|
74
|
+
data = self.data
|
|
75
|
+
|
|
76
|
+
team_names = data.columns[1:]
|
|
77
|
+
|
|
78
|
+
# names of locations for home games
|
|
79
|
+
self.locations = {team_name: data[team_name][0] for team_name in team_names}
|
|
80
|
+
|
|
81
|
+
# get team indices and names (T)
|
|
82
|
+
teams = dict(enumerate(team_names))
|
|
83
|
+
|
|
84
|
+
# get all slots (S)
|
|
85
|
+
dates = pd.to_datetime(data.iloc[1:, 0]) # not necessarily continuous
|
|
86
|
+
slots = dict(enumerate(dates))
|
|
87
|
+
|
|
88
|
+
# process core (i.e. without dates and locations) for remaining sets extraction
|
|
89
|
+
self.core = data.iloc[1:, 1:].reset_index(drop=True)
|
|
90
|
+
|
|
91
|
+
mat = self.core.copy()
|
|
92
|
+
mat = mat.map(fill_value, unavailable=self.unavailable)
|
|
93
|
+
mat = mat.to_numpy()
|
|
94
|
+
|
|
95
|
+
# get all available home slots by team index (H)
|
|
96
|
+
sets_home = {key: np.where(mat[:, key] == 1)[0] for key in teams}
|
|
97
|
+
|
|
98
|
+
# get all non-available away slots by team index (A)
|
|
99
|
+
sets_forbidden = {key: np.where(mat[:, key] == -1)[0] for key in teams}
|
|
100
|
+
|
|
101
|
+
# assemble sets
|
|
102
|
+
self.sets = {
|
|
103
|
+
"teams": teams,
|
|
104
|
+
"slots": slots,
|
|
105
|
+
"home": sets_home,
|
|
106
|
+
"forbidden": sets_forbidden,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
self.parsed = True
|