Modules
Adapters for loading and saving data.
Initially we have CSV files locally, and Google Docs Spreadsheets.
GSheetAdapter
¶
Source code in src/sortition_algorithms/adapters.py
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 |
|
messages()
¶
Return accumulated messages and reset
Source code in src/sortition_algorithms/adapters.py
152 153 154 155 156 |
|
find_any_committee(features, people, number_people_wanted, settings)
¶
Find any single feasible committee that satisfies the quotas.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
features
|
FeatureCollection
|
FeatureCollection with min/max quotas |
required |
people
|
People
|
People object with pool members |
required |
number_people_wanted
|
int
|
desired size of the panel |
required |
settings
|
Settings
|
Settings object containing configuration |
required |
Returns:
Type | Description |
---|---|
tuple[list[frozenset[str]], list[str]]
|
tuple of (list containing one committee as frozenset of person_ids, empty list of messages) |
Raises:
Type | Description |
---|---|
InfeasibleQuotasError
|
If quotas are infeasible |
SelectionError
|
If solver fails for other reasons |
Source code in src/sortition_algorithms/committee_generation.py
355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 |
|
find_distribution_leximin(features, people, number_people_wanted, settings)
¶
Find a distribution over feasible committees that maximizes the minimum probability of an agent being selected (just like maximin), but breaks ties to maximize the second-lowest probability, breaks further ties to maximize the third-lowest probability and so forth.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
features
|
FeatureCollection
|
FeatureCollection with min/max quotas |
required |
people
|
People
|
People object with pool members |
required |
number_people_wanted
|
int
|
desired size of the panel |
required |
settings
|
Settings
|
Settings object containing configuration |
required |
Returns:
Type | Description |
---|---|
list[frozenset[str]]
|
tuple of (committees, probabilities, output_lines) |
list[float]
|
|
list[str]
|
|
tuple[list[frozenset[str]], list[float], list[str]]
|
|
Raises:
Type | Description |
---|---|
RuntimeError
|
If Gurobi is not available |
Source code in src/sortition_algorithms/committee_generation.py
1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 |
|
find_distribution_maximin(features, people, number_people_wanted, settings)
¶
Find a distribution over feasible committees that maximizes the minimum probability of an agent being selected.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
features
|
FeatureCollection
|
FeatureCollection with min/max quotas |
required |
people
|
People
|
People object with pool members |
required |
number_people_wanted
|
int
|
desired size of the panel |
required |
settings
|
Settings
|
Settings object containing configuration |
required |
Returns:
Type | Description |
---|---|
list[frozenset[str]]
|
tuple of (committees, probabilities, output_lines) |
list[float]
|
|
list[str]
|
|
tuple[list[frozenset[str]], list[float], list[str]]
|
|
Source code in src/sortition_algorithms/committee_generation.py
816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 |
|
find_distribution_nash(features, people, number_people_wanted, settings)
¶
Find a distribution over feasible committees that maximizes the Nash welfare, i.e., the product of selection probabilities over all persons.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
features
|
FeatureCollection
|
FeatureCollection with min/max quotas |
required |
people
|
People
|
People object with pool members |
required |
number_people_wanted
|
int
|
desired size of the panel |
required |
settings
|
Settings
|
Settings object containing configuration |
required |
Returns:
Type | Description |
---|---|
list[frozenset[str]]
|
tuple of (committees, probabilities, output_lines) |
list[float]
|
|
list[str]
|
|
tuple[list[frozenset[str]], list[float], list[str]]
|
|
The algorithm maximizes the product of selection probabilities Πᵢ pᵢ by equivalently maximizing log(Πᵢ pᵢ) = Σᵢ log(pᵢ). If some person i is not included in any feasible committee, their pᵢ is 0, and this sum is -∞. We maximize Σᵢ log(pᵢ) where i is restricted to range over persons that can possibly be included.
Source code in src/sortition_algorithms/committee_generation.py
1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 |
|
standardize_distribution(committees, probabilities)
¶
Remove committees with zero probability and renormalize.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
committees
|
list[frozenset[str]]
|
list of committees |
required |
probabilities
|
list[float]
|
corresponding probabilities |
required |
Returns:
Type | Description |
---|---|
tuple[list[frozenset[str]], list[float]]
|
tuple of (filtered_committees, normalized_probabilities) |
Source code in src/sortition_algorithms/committee_generation.py
1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 |
|
find_random_sample(features, people, number_people_wanted, settings, selection_algorithm='maximin', test_selection=False, number_selections=1)
¶
Main algorithm to find one or multiple random committees.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
features
|
FeatureCollection
|
FeatureCollection with min/max quotas |
required |
people
|
People
|
People object with pool members |
required |
number_people_wanted
|
int
|
desired size of the panel |
required |
settings
|
Settings
|
Settings object containing configuration |
required |
selection_algorithm
|
str
|
one of "legacy", "maximin", "leximin", or "nash" |
'maximin'
|
test_selection
|
bool
|
if set, do not do a random selection, but just return some valid panel. Useful for quickly testing whether quotas are satisfiable, but should always be false for actual selection! |
False
|
number_selections
|
int
|
how many panels to return. Most of the time, this should be set to 1, which means that a single panel is chosen. When specifying a value n ≥ 2, the function will return a list of length n, containing multiple panels (some panels might be repeated in the list). In this case the eventual panel should be drawn uniformly at random from the returned list. |
1
|
Returns:
Type | Description |
---|---|
list[frozenset[str]]
|
tuple of (committee_lottery, output_lines) |
list[str]
|
|
tuple[list[frozenset[str]], list[str]]
|
|
Raises:
Type | Description |
---|---|
InfeasibleQuotasError
|
if the quotas cannot be satisfied, which includes a suggestion for how to modify them |
SelectionError
|
in multiple other failure cases |
ValueError
|
for invalid parameters |
RuntimeError
|
if required solver is not available |
Source code in src/sortition_algorithms/core.py
239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 |
|
lottery_rounding(committees, probabilities, number_selections)
¶
Convert probability distribution over committees to a discrete lottery.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
committees
|
list[frozenset[str]]
|
list of committees |
required |
probabilities
|
list[float]
|
corresponding probabilities (must sum to 1) |
required |
number_selections
|
int
|
number of committees to return |
required |
Returns:
Type | Description |
---|---|
list[frozenset[str]]
|
list of committees (may contain duplicates) of length number_selections |
Source code in src/sortition_algorithms/core.py
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 |
|
pipage_rounding(marginals)
¶
Pipage rounding algorithm for converting fractional solutions to integer solutions.
Takes a list of (object, probability) pairs and randomly rounds them to a set of objects such that the expected number of times each object appears equals its probability.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
marginals
|
list[tuple[int, float]]
|
list of (object, probability) pairs where probabilities sum to an integer |
required |
Returns:
Type | Description |
---|---|
list[int]
|
list of objects that were selected |
Source code in src/sortition_algorithms/core.py
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
|
run_stratification(features, people, number_people_wanted, settings, test_selection=False, number_selections=1)
¶
Run stratified random selection with retry logic.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
features
|
FeatureCollection
|
FeatureCollection with min/max quotas for each feature value |
required |
people
|
People
|
People object containing the pool of candidates |
required |
number_people_wanted
|
int
|
Desired size of the panel |
required |
settings
|
Settings
|
Settings object containing configuration |
required |
test_selection
|
bool
|
If True, don't randomize (for testing only) |
False
|
number_selections
|
int
|
Number of panels to return |
1
|
Returns:
Type | Description |
---|---|
bool
|
Tuple of (success, selected_committees, output_lines) |
list[frozenset[str]]
|
|
list[str]
|
|
tuple[bool, list[frozenset[str]], list[str]]
|
|
Raises:
Type | Description |
---|---|
Exception
|
If number_people_wanted is outside valid range for any feature |
ValueError
|
For invalid parameters |
RuntimeError
|
If required solver is not available |
InfeasibleQuotasError
|
If quotas cannot be satisfied |
Source code in src/sortition_algorithms/core.py
489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 |
|
selected_remaining_tables(full_people, people_selected, features, settings)
¶
write some text
people_selected is a single frozenset[str] - it must be unwrapped before being passed to this function.
Source code in src/sortition_algorithms/core.py
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
|
FeatureCollection
¶
A full set of features for a stratification.
The keys here are the names of the features. They could be: gender, age_bracket, education_level etc
The values are FeatureValues objects - the breakdown of the values for a feature.
Source code in src/sortition_algorithms/features.py
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 |
|
check_desired(desired_number)
¶
Check if the desired number of people is within the min/max of every feature.
Source code in src/sortition_algorithms/features.py
217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 |
|
check_min_max()
¶
If the min is bigger than the max we're in trouble i.e. there's an input error
Source code in src/sortition_algorithms/features.py
205 206 207 208 209 210 211 212 213 214 215 |
|
maximum_selection()
¶
The maximum selection for this set of features is the smallest maximum selection of any individual feature.
Source code in src/sortition_algorithms/features.py
196 197 198 199 200 201 202 203 |
|
minimum_selection()
¶
The minimum selection for this set of features is the largest minimum selection of any individual feature.
Source code in src/sortition_algorithms/features.py
187 188 189 190 191 192 193 194 |
|
set_default_max_flex()
¶
Note this only sets it if left at the default value
Source code in src/sortition_algorithms/features.py
168 169 170 171 172 |
|
FeatureValues
¶
A full set of values for a single feature.
If the feature is gender, the values could be: male, female, non_binary_other
The values are FeatureValueCounts objects - the min, max and current counts of the selected people in that feature value.
Source code in src/sortition_algorithms/features.py
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
|
maximum_selection()
¶
For this feature, we have to select at most the sum of the maximum of each value
Source code in src/sortition_algorithms/features.py
118 119 120 121 122 |
|
minimum_selection()
¶
For this feature, we have to select at least the sum of the minimum of each value
Source code in src/sortition_algorithms/features.py
112 113 114 115 116 |
|
set_default_max_flex(max_flex)
¶
Note this only sets it if left at the default value
Source code in src/sortition_algorithms/features.py
91 92 93 94 |
|
read_in_features(features_head, features_body)
¶
Read in stratified selection features and values
Note we do want features_head to ensure we don't have multiple columns with the same name
Source code in src/sortition_algorithms/features.py
327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 |
|
Selection algorithms for stratified sampling.
find_random_sample_legacy(people, features, number_people_wanted, check_same_address=False, check_same_address_columns=None)
¶
Legacy stratified random selection algorithm.
Implements the original algorithm that uses greedy selection based on priority ratios. Always selects from the most urgently needed category first (highest ratio of (min-selected)/remaining), then randomly picks within that category.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
people
|
People
|
People collection |
required |
features
|
FeatureCollection
|
Feature definitions with min/max targets |
required |
number_people_wanted
|
int
|
Number of people to select |
required |
check_same_address
|
bool
|
Whether to remove household members when selecting someone |
False
|
check_same_address_columns
|
list[str] | None
|
Address columns for household identification |
None
|
Returns:
Type | Description |
---|---|
list[frozenset[str]]
|
Tuple of (selected_committees, output_messages) where: |
list[str]
|
|
tuple[list[frozenset[str]], list[str]]
|
|
Raises:
Type | Description |
---|---|
SelectionError
|
If selection becomes impossible (not enough people, etc.) |
Source code in src/sortition_algorithms/find_sample.py
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
|
MaxRatioResult
¶
Result from finding the category with maximum selection ratio.
Source code in src/sortition_algorithms/people_features.py
16 17 18 19 20 21 22 |
|
PeopleFeatures
¶
This class manipulates people and features together, making a deepcopy on init.
Source code in src/sortition_algorithms/people_features.py
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 |
|
delete_all_with_feature_value(feature_name, feature_value)
¶
When a feature/value is "full" we delete everyone else in it. "Full" means that the number selected equals the "max" amount - that is detected elsewhere and then this method is called. Returns count of those deleted, and count of those left
Source code in src/sortition_algorithms/people_features.py
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
|
find_max_ratio_category()
¶
Find the feature/value combination with the highest selection ratio.
The ratio is calculated as: (min - selected) / remaining This represents how urgently we need people from this category. Higher ratio = more urgent need (fewer people available relative to what we still need).
Returns:
Type | Description |
---|---|
MaxRatioResult
|
MaxRatioResult containing the feature name, value, and a random person index |
Raises:
Type | Description |
---|---|
SelectionError
|
If insufficient people remain to meet minimum requirements |
Source code in src/sortition_algorithms/people_features.py
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 |
|
handle_category_full_deletions(selected_person_data)
¶
Check if any categories are now full after a selection and delete remaining people.
When a person is selected, some categories may reach their maximum quota. This method identifies such categories and removes all remaining people from them.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
selected_person_data
|
dict[str, str]
|
Dictionary of the selected person's feature values |
required |
Returns:
Type | Description |
---|---|
list[str]
|
List of output messages about categories that became full and people deleted |
Raises:
Type | Description |
---|---|
SelectionError
|
If deletions would violate minimum constraints |
Source code in src/sortition_algorithms/people_features.py
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 |
|
prune_for_feature_max_0()
¶
Check if any feature_value.max is set to zero. if so delete everyone with that feature value NOT DONE: could then check if anyone is left.
Source code in src/sortition_algorithms/people_features.py
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
|
select_person(person_key)
¶
Selecting a person means:
- remove the person from our copy of People
- update the selected
and remaining
counts of the FeatureCollection
- if check_same_address is True, also remove household members (without adding to selected)
Returns:
Type | Description |
---|---|
list[str]
|
List of additional people removed due to same address (empty if check_same_address is False) |
Source code in src/sortition_algorithms/people_features.py
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
|
WeightedSample
¶
Source code in src/sortition_algorithms/people_features.py
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 |
|
__init__(features)
¶
This produces a set of lists of feature values for each feature. Each value
is in the list fv_counts.max
times - so a random choice with represent the max.
So if we had feature "ethnicity", value "white" w max 4, "asian" w max 3 and "black" with max 2 we'd get:
["white", "white", "white", "white", "asian", "asian", "asian", "black", "black"]
Then making random choices from that list produces a weighted sample.
Source code in src/sortition_algorithms/people_features.py
247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 |
|
simple_add_selected(person_keys, people, features)
¶
Just add the person to the selected counts for the feature values for that person. Don't do the more complex handling of the full PeopleFeatures.add_selected()
Source code in src/sortition_algorithms/people_features.py
235 236 237 238 239 240 241 242 243 |
|
People
¶
Source code in src/sortition_algorithms/people.py
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
|
find_person_by_position_in_category(feature_name, feature_value, position)
¶
Find the nth person (1-indexed) in a specific feature category.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
feature_name
|
str
|
Name of the feature (e.g., "gender") |
required |
feature_value
|
str
|
Value of the feature (e.g., "male") |
required |
position
|
int
|
1-indexed position within the category |
required |
Returns:
Type | Description |
---|---|
str
|
Person key of the person at the specified position |
Raises:
Type | Description |
---|---|
SelectionError
|
If no person is found at the specified position |
Source code in src/sortition_algorithms/people.py
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
|
households(address_columns)
¶
Generates a dict with: - keys: a tuple containing the address strings - values: a list of person_key for each person at that address
Source code in src/sortition_algorithms/people.py
65 66 67 68 69 70 71 72 73 74 75 |
|
matching_address(person_key, address_columns)
¶
Returns a list of person keys for all people who have an address matching the address of the person passed in.
Source code in src/sortition_algorithms/people.py
77 78 79 80 81 82 83 84 85 86 87 88 |
|
RandomProvider
¶
Bases: ABC
This is something of a hack. Mostly we want to use the secrets
module.
But for repeatable testing we might want to set the random.seed sometimes.
So we have a global _random_provider
which can be switched between an
instance of this class that uses the secrets
module and an instance that
uses random
with a seed. The switch is done by the set_random_provider()
function.
Then every time we want some randomness, we call random_provider()
to get
the current version of the global.
Source code in src/sortition_algorithms/utils.py
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
|
StrippedDict
¶
Wraps a dict, and whenever we get a value from it, we convert to str and strip() whitespace
Source code in src/sortition_algorithms/utils.py
22 23 24 25 26 27 28 29 30 31 32 |
|
print_ret(message)
¶
Print and return a message for output collection.
Source code in src/sortition_algorithms/utils.py
11 12 13 14 15 |
|