Compare commits

..

2 commits

Author SHA1 Message Date
f478c69c65
updates 2026-04-12 03:29:12 -07:00
cbedbbeb48 sync 2025-12-20 08:08:47 -08:00
12 changed files with 1875 additions and 611 deletions

View file

@ -3,7 +3,7 @@ bibliography: REFERENCES.bib
...
# Carousel
*A simple Stable Matching Solver.*
*A simple Stable Matching solver.*
`carousel` is a solver for the
[Envy-free](https://en.wikipedia.org/wiki/Envy-free_matching)
@ -14,30 +14,29 @@ bibliography: REFERENCES.bib
### Gale-Shapley Deferred Acceptance
The most basic version(s) of the stable matching problem was outlined and solved in [@gale&shapley1962] and won Shapley the
[2012 Nobel Prize in Economics](https://www.nobelprize.org/prizes/economic-sciences/2012/popular-information/).
- Gives a solution to the basic problem.
The basic problem being solved is as follows:
### GS + Rotation Enumeration of Solutions
#### Stable Marriage Problem
The "Stable Marriage problem" TODO
- Can implement post-selection measures/constraints/criteria.
TODO
### Integer Programming (Huang++)
#### College Admissions Problem
Solving the stable marriage problem also provides a solution to the "College Admissions Problem",
with just a little more work.
### Polytope Solution Sampling (Large Problems)
TODO
### Brute-Force Combinatoric Search
A benchmark/testbed implementation. Beware.
### More?
Contribute!
## Data - Input/Output
All [input table formats supported by `polars`](https://docs.pola.rs/user-guide/io/) are supported by `carousel` (`csv`, `excel`, `json` to name a few),
which accepts a few inter-related tabular schemes for the input/output data.
### Input
Input describes the preferences/rankings of the "applicants" of "reviewers" to which they will be matched (possibly many-to-one, as in the "College Admission Problem"),
as well as the preferences/rankings for the reviwers of applicants.
Input should be in one of three forms:
Input describes the preferences/rankings of the "applicants" of "reviewers" to which they will be matched (possibly many-to-one, as in the "College Admission Problem"), as well as the preferences/rankings for the reviwers of applicants.
Input should be in one of two forms:
#### Preferences
Preferences enumerate by-name some preferences in descending order,
@ -61,10 +60,13 @@ e.g. Alice, Bob and Charlie rank the fruit apples, bananas and cherries as:
| banana | 3 | 1 | 2 |
| cherry | 2 | 3 | 1 |
### Intermediate
#### Ranking Matrix
In order to perform a matching, `carousel` either needs a pair of preferences
(e.g. a set of doctor's preferences for residencies, and a set of residencies' preferences for doctors),
a pair of corresponding rankings, *or* a matrix encoding both rankings at once:
a pair of corresponding rankings. A ranking matrix is a concise way to express
a pair of rankings - used to display rankings:
| names | Alice | Bob | Charlie |
|---------|------------|---------|-----------|
@ -72,6 +74,25 @@ a pair of corresponding rankings, *or* a matrix encoding both rankings at once:
| CaseMed | (3, 2) | (1, 1) | (2, 3) |
| Emory | (2, 1) | (3, 3) | (1, 2) |
#### Priorities
Internally, preferences/rankings are often serialized to rows as a priority listing of the form:
| App. | Rank | Rev |
|-------|---|--------|
| Alice | 1 | apple |
| Alice | 2 | cherry |
| Alice | 3 | banana |
| Bob | 1 | banana |
| Bob | 2 | apple |
| Bob | 3 | cherry |
| Charlie | 1 | cherry |
| Charlie | 2 | banana |
| Charlie | 3 | apple |
(in no specific row order). This is not a supported input format in the frontends, but may be relevant
when interacting with the package programmatically - priorities can be followed as edges in a graph.
### Output
#### Matching
@ -112,6 +133,9 @@ using the [pages.uoregon.edu CGI feature](https://service.uoregon.edu/TDClient/2
Submit your applicant and reviewer preferences in tabular form,
or as excel uploads and the server will return a table of matches for you to choose from.
This resource has *very* limited compute, so excessive usage might result in limitations or restricted access.
Try not to ruin a good thing! A moderate number of problems on the scale of those in the Examples section below should be sustainable.
### Command Line Binary
TODO
@ -141,13 +165,13 @@ TODO
It's often desirable to enforce additional criteria on solutions
that are not well-posed within the core optimization problem.
Since the solver itself is stochastic to some extent, these are often most easily implemented
by a post-selection.
by a post-selection on a sampling of solutions.
## Examples
Here are some usage examples:
### Departmental TA Assignments
### University of Oregon Physics Department Graduate TA Assignments
TODO
### Caltech Housing Rotation

View file

@ -1,5 +1,3 @@
@article{gale&shapley1962,
ISSN = {0002989, 19300972},
URL = {https://www.jstor.org/stable/2312726},
@ -14,3 +12,64 @@
year = {1962},
}
@article{doi:10.1137/0215048,
author = {Irving, Robert W. and Leather, Paul},
title = {The Complexity of Counting Stable Marriages},
journal = {SIAM Journal on Computing},
volume = {15},
number = {3},
pages = {655-667},
year = {1986},
doi = {10.1137/0215048},
URL = {https://doi.org/10.1137/0215048},
eprint = {https://doi.org/10.1137/0215048}
}
@article{doi:10.1137/0216010,
author = {Gusfield, Dan},
title = {Three Fast Algorithms for Four Problems in Stable Marriage},
journal = {SIAM Journal on Computing},
volume = {16},
number = {1},
pages = {111-128},
year = {1987},
doi = {10.1137/0216010},
URL = { https://doi.org/10.1137/0216010},
eprint = { https://doi.org/10.1137/0216010 }
}
@article{https://doi.org/10.3982/TE4830,
author = {Huang, Chao},
title = {Stable matching: An integer programming approach},
journal = {Theoretical Economics},
volume = {18},
number = {1},
pages = {37-63},
keywords = {Two-sided matching, stability, integer programming, many-to-one matching, complementarity, total unimodularity, demand type, C61, C78, D47, D63},
doi = {https://doi.org/10.3982/TE4830},
url = {https://onlinelibrary.wiley.com/doi/abs/10.3982/TE4830},
eprint = {https://onlinelibrary.wiley.com/doi/pdf/10.3982/TE4830}
}
@article{DELORME2019426,
title = {Mathematical models for stable matching problems with ties and incomplete lists},
journal = {European Journal of Operational Research},
volume = {277},
number = {2},
pages = {426-441},
year = {2019},
issn = {0377-2217},
doi = {https://doi.org/10.1016/j.ejor.2019.03.017},
url = {https://www.sciencedirect.com/science/article/pii/S0377221719302565},
author = {Maxence Delorme and Sergio García and Jacek Gondzio and Jörg Kalcsics and David Manlove and William Pettersson},
}
@misc{gutin2024findingstablematchingsassignment,
title={Finding all stable matchings with assignment constraints},
author={Gregory Gutin and Philip R. Neary and Anders Yeo},
year={2024},
eprint={2204.03989},
archivePrefix={arXiv},
primaryClass={econ.TH},
url={https://arxiv.org/abs/2204.03989},
}

View file

@ -1,14 +1,11 @@
list:
just --list
run:
uv run carousel
#run:
# uv run carousel
python *arguments:
uv run python -c "import code; from rich import pretty; pretty.install(); code.interact()" {{arguments}}
check:
uv run pyright src
uv run python -c {{arguments}}
test:
uv run pytest -vvv --tb=short --log-cli-level=INFO
@ -16,7 +13,16 @@ test:
format:
uv run ruff format src test
compile:
check:
uv run pyright src
sync:
uv sync --upgrade
lock:
uv pip compile pyproject.toml -o requirements.txt --group dev
build:
uv run pyinstaller src/cli.py
uv run pyinstaller src/gui.py
@ -29,5 +35,3 @@ wipe:
just clean
rm -rf .venv
lock:
uv pip compile pyproject.toml -o requirements.txt --group dev

View file

@ -1,42 +1,137 @@
# This file was autogenerated by uv via the following command:
# uv pip compile pyproject.toml -o requirements.txt --group dev
altair==5.5.0
# via marimo
altgraph==0.17.4
# via pyinstaller
annotated-types==0.7.0
# via pydantic
anyio==4.10.0
# via
# httpx
# openai
# starlette
attrs==25.3.0
# via hypothesis
# via
# hypothesis
# jsonschema
# referencing
certifi==2025.8.3
# via
# httpcore
# httpx
click==8.1.8
# via carousel (pyproject.toml)
# via
# carousel (pyproject.toml)
# marimo
# uvicorn
distro==1.9.0
# via openai
docutils==0.22
# via marimo
duckdb==1.3.2
# via marimo
faker==37.6.0
# via carousel (pyproject.toml:dev)
fastjsonschema==2.21.2
# via nbformat
h11==0.16.0
# via
# httpcore
# uvicorn
httpcore==1.0.9
# via httpx
httpx==0.28.1
# via openai
hypothesis==6.131.5
# via carousel (pyproject.toml:dev)
idna==3.10
# via
# anyio
# httpx
iniconfig==2.1.0
# via pytest
itsdangerous==2.2.0
# via marimo
jedi==0.19.2
# via marimo
jinja2==3.1.6
# via altair
jiter==0.10.0
# via openai
jsonschema==4.25.1
# via
# altair
# nbformat
jsonschema-specifications==2025.4.1
# via jsonschema
jupyter-core==5.8.1
# via nbformat
loro==1.6.0
# via marimo
marimo==0.15.2
# via carousel (pyproject.toml:dev)
markdown==3.9
# via
# marimo
# pymdown-extensions
markdown-it-py==3.0.0
# via rich
markupsafe==3.0.2
# via jinja2
mdurl==0.1.2
# via markdown-it-py
narwhals==2.3.0
# via
# altair
# marimo
nbformat==5.10.4
# via marimo
nodeenv==1.9.1
# via pyright
numpy==2.2.4
# via carousel (pyproject.toml)
openai==1.106.1
# via marimo
packaging==24.2
# via
# altair
# marimo
# pyinstaller
# pyinstaller-hooks-contrib
# pytest
parso==0.8.5
# via jedi
platformdirs==4.4.0
# via jupyter-core
pluggy==1.5.0
# via pytest
polars==1.27.1
# via carousel (pyproject.toml)
# via
# carousel (pyproject.toml)
# marimo
psutil==7.0.0
# via marimo
py-cpuinfo==9.0.0
# via pytest-benchmark
pyarrow==21.0.0
# via polars
pyclean==3.1.0
# via carousel (pyproject.toml:dev)
pydantic==2.11.7
# via openai
pydantic-core==2.33.2
# via pydantic
pygments==2.19.1
# via rich
# via
# marimo
# rich
pyinstaller==6.13.0
# via carousel (pyproject.toml:dev)
pyinstaller-hooks-contrib==2025.3
# via pyinstaller
pymdown-extensions==10.16.1
# via marimo
pyright==1.1.399
# via carousel (pyproject.toml:dev)
pyside6==6.9.0
@ -53,10 +148,24 @@ pytest==8.3.5
# pytest-benchmark
pytest-benchmark==5.1.0
# via carousel (pyproject.toml:dev)
pyyaml==6.0.2
# via
# marimo
# pymdown-extensions
referencing==0.36.2
# via
# jsonschema
# jsonschema-specifications
rich==14.0.0
# via carousel (pyproject.toml)
rpds-py==0.27.1
# via
# jsonschema
# referencing
ruff==0.11.6
# via carousel (pyproject.toml:dev)
# via
# carousel (pyproject.toml:dev)
# marimo
setuptools==78.1.0
# via
# pyinstaller
@ -66,7 +175,39 @@ shiboken6==6.9.0
# pyside6
# pyside6-addons
# pyside6-essentials
sniffio==1.3.1
# via
# anyio
# openai
sortedcontainers==2.4.0
# via hypothesis
sqlglot==27.12.0
# via marimo
starlette==0.47.3
# via marimo
tomlkit==0.13.3
# via marimo
tqdm==4.67.1
# via openai
traitlets==5.14.3
# via
# jupyter-core
# nbformat
ty==0.0.1a20
# via carousel (pyproject.toml:dev)
typing-extensions==4.13.2
# via pyright
# via
# altair
# openai
# pydantic
# pydantic-core
# pyright
# typing-inspection
typing-inspection==0.4.1
# via pydantic
tzdata==2025.2
# via faker
uvicorn==0.35.0
# via marimo
websockets==15.0.1
# via marimo

View file

@ -1,14 +1,14 @@
import logging, rich
from rich.logging import RichHandler
rich.traceback.install()
import itertools as it
import numpy as np
import polars as pl
import polars.selectors as pls
rich.traceback.install()
logging.basicConfig(
level=logging.INFO,
format="%(message)s",
@ -24,126 +24,7 @@ logging.basicConfig(
)
log = logging.getLogger(__name__)
def rank_to_pref(ranking):
"""Converts a ranking to a preference."""
id_col_name = ranking.select(pls.by_index(0)).to_series().name
preferences = ranking.select(
[
pl.col(id_col_name).sort_by(c).alias(c)
for c in ranking.columns
if c != id_col_name
]
)
return preferences
def pref_to_rank(preferences):
"""Converts a preference to a ranking."""
o = preferences.select(
pl.concat_list(preferences.columns).explode().unique().sort().alias("")
) # .with_row_index(offset=1)
ranking = pl.concat(
[
o.join(
preferences.with_row_index(offset=1),
how="full",
left_on="",
right_on=c,
maintain_order="left",
).select(pl.col("index").alias(c))
for c in preferences.columns
],
how="horizontal",
)
return pl.concat([o, ranking], how="horizontal")
""""
def ranking_matrix(A, B):
T = pl.concat([A, B], how="horizontal")
TT = T.with_columns(pl.concat_list(A.columns[0], B.columns[0]))
for ab in zip(A.columns[1:], B.columns[1:]):
TT = TT.with_columns(pl.concat_list(*ab))
TTT = TT.select(pl.col(A.columns))
return TTT.insert_column(0, pl.Series("names", B.columns))
"""
def check_valid_pref(preferences):
"""A valid set of preferences has all unique entries in each column, TODO"""
repeats = preferences.select(
(~pl.all_horizontal((pl.all().is_unique() | pl.all().is_null()).all())).alias(
"repeats"
)
).get_column("repeats")[0]
return not repeats
def check_valid_rank(ranking):
"""A valid ranking has no ties, TODO"""
ties = ranking.select(
(~pl.all_horizontal((pl.all().is_unique() | pl.all().is_null()).all())).alias(
"ties"
)
).get_column("ties")[0]
return not ties
def check_valid_match(match, applicants, reviewers):
# TODO
pass
def check_valid_assgn(assgn, applicants, reviewers):
# TODO
pass
def assgn_to_match(assgn):
# TODO
pass
def match_to_assgn(match):
# TODO
pass
def get_rank(ranking, ranker, ranked):
idx = ranking.select(pl.arg_where(pl.col("") == ranked)).item()
return ranking[ranker][idx]
def check_unstable(match, applicant_ranking, reviewer_ranking):
applicants = applicant_ranking.columns[1:] # assume unique applicants
for a, b in it.permutations(applicants, 2):
A = (
match.select(c for c in match.iter_columns() if a in c).to_series().name
) # the reviewer a is matched to
B = (
match.select(c for c in match.iter_columns() if b in c).to_series().name
) # the reviewer b is matched to
b_prefers_A = get_rank(applicant_ranking, b, A) < get_rank(
applicant_ranking, b, B
)
A_prefers_b = get_rank(reviewer_ranking, A, b) < get_rank(
reviewer_ranking, A, a
)
if b_prefers_A and A_prefers_b:
return True
# else
return False
def check_stable(*args, **kwargs):
return not check_unstable(*args, **kwargs)
from .util import *
from .brute import *
from .def_acc import *

View file

@ -1,3 +1,3 @@
def brute(applicant_rankings, reviewer_rankings):
"""Brute force search for stable matches."""
def brute_match(applicant_rankings, reviewer_rankings):
"""Brute force combinatoric search for stable matches."""
pass

View file

@ -1,5 +1,126 @@
def deferred_acceptance(applicant_rankings, reviewer_rankings):
"""Find Gale-Shapley deferred-acceptance stable matchings for preferences A, R."""
from .util import *
def preparefor_def_acc(applicant_rankings, reviewer_rankings):
"""Sanitize/Format applicant and reviewer rankings for the deferred acceptance solver."""
pass
# return app_ranks, rev_ranks
def matchby_deferred_acceptance(applicant_rankings, reviewer_rankings, revfirst=False):
"""Find the Gale-Shapley deferred-acceptance stable matching for rankings A, R. Default to A-first unless `revfirst=true`."""
app_prio = rank_to_prio(
applicant_rankings, prioritizer="applicant", priority="reviewer"
)
rev_prio = rank_to_prio(
reviewer_rankings, prioritizer="reviewer", priority="applicant"
)
state = app_prio.select(
[
pl.col("applicant").unique().alias("applicant"),
]
).with_columns([pl.lit(0).alias("next_rank"), pl.lit(True).alias("is_free")])
matches = pl.DataFrame(
{
"applicant": pl.Series([]),
"reviewer": pl.Series([]),
"current": pl.Series([], dtype=pl.Int64),
}
)
max_iters = len(state) * len(rev_prio.select("applicant").unique())
for _ in range(max_iters):
props = (
state.filter(pl.col("is_free"))
.join(app_prio, on="applicant")
.filter(pl.col("rank") == pl.col("next_rank"))
.select(["applicant", "reviewer"])
)
if len(props) == 0:
break
props = props.join(rev_prio, on=["reviewer", "applicant"], how="left")
props = props.join(
matches.select(
["reviewer", pl.col("applicant").alias("proposer"), pl.col("current")]
),
on="reviewer",
how="left",
)
props = props.with_columns(
[
(
pl.col("proposer").is_null() | (pl.col("rank") < pl.col("current"))
).alias("accepted")
]
)
accepted = proposals.filter(pl.col("accepted"))
rejected = proposals.filter(~pl.col("accepted"))
displaced = accepted.filter(pl.col("proposer").is_not_null()).select(
pl.col("proposer").alias("applicant")
)
matches = matches.join(accepted.select("reviewer"), on="reviewer", how="anti")
matches = pl.concat(
[
matches,
accepted.select(
["reviewer", "applicant", pl.col("rank").alias("current")]
),
]
)
rejected_apps = rejected.select("applicant")
accepted_apps = accepted.select("applicant")
state = (
state.join(
rejected_apps.with_columns(pl.lit(True).alias("was_rejected")),
on="applicant",
how="left",
)
.join(
accepted_apps.with_columns(pl.lit(True).alias("was_accepted")),
on="applicant",
how="left",
)
.join(
displaced.with_columns(pl.lit(True).alias("was_displaced")),
on="applicant",
how="left",
)
.with_columns(
[
pl.when(pl.col("was_rejected").fill_null(False))
.then(pl.col("next_rank") + 1)
.otherwise(pl.col("next_rank"))
.alias("next_rank"),
pl.when(pl.col("was_accepted").fill_null(False))
.then(False)
.when(pl.col("was_displaced").full_null(False))
.then(True)
.otherwise(pl.col("is_free"))
.alias("is_free"),
]
)
.select(["applicant", "next_rank", "is_free"])
)
return matches.select(["applicant", "reviewer"])
def deferred_acceptance_match(applicant_rankings, reviewer_rankings):
"""Find Gale-Shapley deferred-acceptance stable matching for rankings A, R."""
reviewer_rankings = reviewer_rankings.rename(
{reviewer_rankings.columns[0]: "applicant"}
)

152
src/carousel/util.py Normal file
View file

@ -0,0 +1,152 @@
import itertools as it
import numpy as np
import polars as pl
import polars.selectors as pls
# Stability
def get_rank(ranking, ranker, ranked):
idx = ranking.select(pl.arg_where(pl.col("") == ranked)).item()
return ranking[ranker][idx]
def check_match_unstable(match, applicant_ranking, reviewer_ranking):
applicants = applicant_ranking.columns[1:] # assume unique applicants
for a, b in it.permutations(applicants, 2):
A = (
match.select(c for c in match.iter_columns() if a in c).to_series().name
) # the reviewer a is matched to
B = (
match.select(c for c in match.iter_columns() if b in c).to_series().name
) # the reviewer b is matched to
b_prefers_A = get_rank(applicant_ranking, b, A) < get_rank(
applicant_ranking, b, B
)
A_prefers_b = get_rank(reviewer_ranking, A, b) < get_rank(
reviewer_ranking, A, a
)
if b_prefers_A and A_prefers_b:
return True
# else
return False
def check_match_stable(*args, **kwargs):
return not check_match_unstable(*args, **kwargs)
# Conversions
def rank_to_pref(ranking):
"""Converts a ranking to a preference."""
id_col_name = ranking.select(pls.by_index(0)).to_series().name
preferences = ranking.select(
[
pl.col(id_col_name).sort_by(c).alias(c)
for c in ranking.columns
if c != id_col_name
]
)
return preferences
def pref_to_rank(preferences):
"""Converts a preference to a ranking."""
o = preferences.select(
pl.concat_list(preferences.columns).explode().unique().sort().alias("")
) # .with_row_index(offset=1)
ranking = pl.concat(
[
o.join(
preferences.with_row_index(offset=1),
how="full",
left_on="",
right_on=c,
maintain_order="left",
).select(pl.col("index").alias(c))
for c in preferences.columns
],
how="horizontal",
)
return pl.concat([o, ranking], how="horizontal")
def ranks_to_mat(rankA, rankB):
return prefs_to_mat(rank_to_pref(rankA), rank_to_pref(rankB))
def prefs_to_mat(prefA, prefB):
return rank_to_mat(pref_to_rank(preferences), pref_to_rank(preferences[0]))
def mat_to_ranks(matrix):
pass # TODO
def mat_to_prefs(matrix):
rankA, rankB = mat_to_ranks(matrix)
return rank_to_pref(rankA), rank_to_preft(rankB)
def pref_to_prio(pref, prioritizer="prioritizer", priority="subject"):
return pref.with_row_index("rank").unpivot(
index="rank", variable_name=prioritizer, value_name=priority
)
def rank_to_prio(rank, **kwargs):
return pref_to_prio(rank_to_pref(rank), **kwargs)
""""
def ranking_matrix(A, B):
T = pl.concat([A, B], how="horizontal")
TT = T.with_columns(pl.concat_list(A.columns[0], B.columns[0]))
for ab in zip(A.columns[1:], B.columns[1:]):
TT = TT.with_columns(pl.concat_list(*ab))
TTT = TT.select(pl.col(A.columns))
return TTT.insert_column(0, pl.Series("names", B.columns))
"""
## Output
def match_to_assignment(matching):
pass
def assgn_to_match(assignment):
pass
## Internal
# Validity
def check_pref_allunique(preferences):
"""A valid set of preferences has all unique entries in each column."""
repeats = preferences.select(
(~pl.all_horizontal((pl.all().is_unique() | pl.all().is_null()).all())).alias(
"repeats"
)
).get_column("repeats")[0]
return not repeats
def check_rank_noties(ranking):
"""A valid ranking has no ties."""
ties = ranking.select(
(~pl.all_horizontal((pl.all().is_unique() | pl.all().is_null()).all())).alias(
"ties"
)
).get_column("ties")[0]
return not ties

View file

@ -40,7 +40,7 @@ def test_invalid_pref():
pp = pl.DataFrame(
{"a": ["A", "A", "B"], "b": ["B", "A", "C"], "c": ["C", "B", "A"]}
)
assert crsl.check_valid_pref(pp) is False
assert crsl.check_pref_allunique(pp) is False
def test_pref_to_rank():
@ -51,7 +51,7 @@ def test_invalid_rank():
rr = pl.DataFrame(
{"": ["A", "B", "C"], "a": [1, 1, 2], "b": [2, 1, 3], "c": [3, 2, 1]}
)
assert crsl.check_valid_pref(rr) is False
assert crsl.check_pref_allunique(rr) is False
def test_rank_to_pref():
@ -60,7 +60,7 @@ def test_rank_to_pref():
@given(rankings())
def test_valid_rank(R):
assert crsl.check_valid_rank(R)
assert crsl.check_rank_noties(R)
@given(rankings())
@ -70,7 +70,7 @@ def test_ranks_tofrom_prefs(R):
@given(preferences())
def test_valid_pref(P):
assert crsl.check_valid_pref(P)
assert crsl.check_pref_allunique(P)
@given(preferences())
@ -99,7 +99,7 @@ def test_eg2_unstable():
)
match = pl.DataFrame({"A": ["a"], "B": ["b"], "C": ["c"], "D": ["d"]})
assert crsl.check_unstable(match, ar, rr)
assert crsl.check_match_unstable(match, ar, rr)
def test_eg2_isstable():
@ -123,7 +123,7 @@ def test_eg2_isstable():
)
match = pl.DataFrame({"A": ["c"], "B": ["d"], "C": ["a"], "D": ["b"]})
assert crsl.check_stable(match, ar, rr)
assert crsl.check_match_stable(match, ar, rr)
@given(
@ -131,5 +131,5 @@ def test_eg2_isstable():
rankings(names=["A", "B", "C", "D"], choices=["a", "b", "c", "d"]),
)
def test_defacc_isstable(applicant_rankings, reviewer_rankings):
match = crsl.deferred_acceptance(applicant_rankings, reviewer_rankings)
assert crsl.check_stable(match, applicant_rankings, reviewer_rankings)
match = crsl.matchby_deferred_acceptance(applicant_rankings, reviewer_rankings)
assert crsl.check_match_stable(match, applicant_rankings, reviewer_rankings)

View file

@ -0,0 +1,124 @@
{
"version": "1",
"metadata": {
"marimo_version": "0.18.3"
},
"cells": [
{
"id": "Hbol",
"code_hash": "bc65c35c6fad59890b50c502bb8affa4",
"outputs": [
{
"type": "data",
"data": {
"text/markdown": "<span class=\"markdown prose dark:prose-invert contents\"><h1 id=\"caltech-hovse-rotation-example\">Caltech Hovse Rotation Example</h1></span>"
}
}
],
"console": []
},
{
"id": "MJUe",
"code_hash": "ccb62cc29f2cec640b063832a90adfec",
"outputs": [
{
"type": "data",
"data": {
"text/markdown": "<span class=\"markdown prose dark:prose-invert contents\"><h2 id=\"imports\">Imports</h2></span>"
}
}
],
"console": []
},
{
"id": "vblA",
"code_hash": "9588e2b4004a32c73b7ecc0a968b666f",
"outputs": [
{
"type": "data",
"data": {
"text/plain": ""
}
}
],
"console": []
},
{
"id": "bkHC",
"code_hash": "4821038400db1f5dc63daf7ca5b26279",
"outputs": [
{
"type": "data",
"data": {
"text/markdown": "<span class=\"markdown prose dark:prose-invert contents\"><h2 id=\"generating-rotation-rankings\">Generating Rotation Rankings</h2></span>"
}
}
],
"console": []
},
{
"id": "lEQa",
"code_hash": "ecc9ae14c8c8f3875656fc620665c6b4",
"outputs": [
{
"type": "data",
"data": {
"text/html": "<pre class='text-xs'>True</pre>"
}
}
],
"console": []
},
{
"id": "NdeS",
"code_hash": "c582dec943ff7b743aa0691df291cea6",
"outputs": [
{
"type": "data",
"data": {
"text/html": "<marimo-ui-element object-id='NdeS-0' random-id='7040b6c9-c598-beaa-38c6-9f1afe159be0'><marimo-table data-initial-value='[]' data-label='null' data-data='&quot;[{&#92;&quot;&#92;&quot;:&#92;&quot;A&#92;&quot;,&#92;&quot;a&#92;&quot;:1,&#92;&quot;b&#92;&quot;:1,&#92;&quot;c&#92;&quot;:2,&#92;&quot;d&#92;&quot;:4},{&#92;&quot;&#92;&quot;:&#92;&quot;B&#92;&quot;,&#92;&quot;a&#92;&quot;:2,&#92;&quot;b&#92;&quot;:4,&#92;&quot;c&#92;&quot;:1,&#92;&quot;d&#92;&quot;:2},{&#92;&quot;&#92;&quot;:&#92;&quot;C&#92;&quot;,&#92;&quot;a&#92;&quot;:3,&#92;&quot;b&#92;&quot;:3,&#92;&quot;c&#92;&quot;:3,&#92;&quot;d&#92;&quot;:3},{&#92;&quot;&#92;&quot;:&#92;&quot;D&#92;&quot;,&#92;&quot;a&#92;&quot;:4,&#92;&quot;b&#92;&quot;:2,&#92;&quot;c&#92;&quot;:4,&#92;&quot;d&#92;&quot;:1}]&quot;' data-total-rows='4' data-total-columns='5' data-max-columns='50' data-banner-text='&quot;&quot;' data-pagination='true' data-page-size='10' data-field-types='[[&quot;&quot;,[&quot;string&quot;,&quot;str&quot;]],[&quot;a&quot;,[&quot;integer&quot;,&quot;i64&quot;]],[&quot;b&quot;,[&quot;integer&quot;,&quot;i64&quot;]],[&quot;c&quot;,[&quot;integer&quot;,&quot;i64&quot;]],[&quot;d&quot;,[&quot;integer&quot;,&quot;i64&quot;]]]' data-show-filters='true' data-show-download='true' data-show-column-summaries='false' data-show-data-types='true' data-show-page-size-selector='false' data-show-column-explorer='true' data-show-chart-builder='true' data-row-headers='[]' data-has-stable-row-id='false' data-lazy='false' data-preload='false'></marimo-table></marimo-ui-element>"
}
}
],
"console": []
},
{
"id": "PKri",
"code_hash": "b9f0f3a28a20d94d4b173bbbd49db37f",
"outputs": [],
"console": []
},
{
"id": "Xref",
"code_hash": "ccc8305d08e563d4fb1c9df31e9b7b69",
"outputs": [
{
"type": "data",
"data": {
"text/html": "<marimo-ui-element object-id='Xref-0' random-id='b6beae57-9d1c-9561-11cf-94cfd5b3aa06'><marimo-table data-initial-value='[]' data-label='null' data-data='&quot;[{&#92;&quot;a&#92;&quot;:&#92;&quot;A&#92;&quot;,&#92;&quot;b&#92;&quot;:&#92;&quot;A&#92;&quot;,&#92;&quot;c&#92;&quot;:&#92;&quot;B&#92;&quot;,&#92;&quot;d&#92;&quot;:&#92;&quot;D&#92;&quot;},{&#92;&quot;a&#92;&quot;:&#92;&quot;B&#92;&quot;,&#92;&quot;b&#92;&quot;:&#92;&quot;D&#92;&quot;,&#92;&quot;c&#92;&quot;:&#92;&quot;A&#92;&quot;,&#92;&quot;d&#92;&quot;:&#92;&quot;B&#92;&quot;},{&#92;&quot;a&#92;&quot;:&#92;&quot;C&#92;&quot;,&#92;&quot;b&#92;&quot;:&#92;&quot;C&#92;&quot;,&#92;&quot;c&#92;&quot;:&#92;&quot;C&#92;&quot;,&#92;&quot;d&#92;&quot;:&#92;&quot;C&#92;&quot;},{&#92;&quot;a&#92;&quot;:&#92;&quot;D&#92;&quot;,&#92;&quot;b&#92;&quot;:&#92;&quot;B&#92;&quot;,&#92;&quot;c&#92;&quot;:&#92;&quot;D&#92;&quot;,&#92;&quot;d&#92;&quot;:&#92;&quot;A&#92;&quot;}]&quot;' data-total-rows='4' data-total-columns='4' data-max-columns='50' data-banner-text='&quot;&quot;' data-pagination='true' data-page-size='10' data-field-types='[[&quot;a&quot;,[&quot;string&quot;,&quot;str&quot;]],[&quot;b&quot;,[&quot;string&quot;,&quot;str&quot;]],[&quot;c&quot;,[&quot;string&quot;,&quot;str&quot;]],[&quot;d&quot;,[&quot;string&quot;,&quot;str&quot;]]]' data-show-filters='true' data-show-download='true' data-show-column-summaries='false' data-show-data-types='true' data-show-page-size-selector='false' data-show-column-explorer='true' data-show-chart-builder='true' data-row-headers='[]' data-has-stable-row-id='false' data-lazy='false' data-preload='false'></marimo-table></marimo-ui-element>"
}
}
],
"console": []
},
{
"id": "taaO",
"code_hash": "b712cc6f3d50763308dd5bc1c6703f77",
"outputs": [
{
"type": "data",
"data": {
"text/html": "<marimo-ui-element object-id='taaO-0' random-id='1a8ede2c-9d90-fbec-5f1c-6258f3189f72'><marimo-table data-initial-value='[]' data-label='null' data-data='&quot;[{&#92;&quot;rank&#92;&quot;:0,&#92;&quot;prioritizer&#92;&quot;:&#92;&quot;A&#92;&quot;,&#92;&quot;subject&#92;&quot;:&#92;&quot;d&#92;&quot;},{&#92;&quot;rank&#92;&quot;:1,&#92;&quot;prioritizer&#92;&quot;:&#92;&quot;A&#92;&quot;,&#92;&quot;subject&#92;&quot;:&#92;&quot;c&#92;&quot;},{&#92;&quot;rank&#92;&quot;:2,&#92;&quot;prioritizer&#92;&quot;:&#92;&quot;A&#92;&quot;,&#92;&quot;subject&#92;&quot;:&#92;&quot;a&#92;&quot;},{&#92;&quot;rank&#92;&quot;:3,&#92;&quot;prioritizer&#92;&quot;:&#92;&quot;A&#92;&quot;,&#92;&quot;subject&#92;&quot;:&#92;&quot;b&#92;&quot;},{&#92;&quot;rank&#92;&quot;:0,&#92;&quot;prioritizer&#92;&quot;:&#92;&quot;B&#92;&quot;,&#92;&quot;subject&#92;&quot;:&#92;&quot;b&#92;&quot;},{&#92;&quot;rank&#92;&quot;:1,&#92;&quot;prioritizer&#92;&quot;:&#92;&quot;B&#92;&quot;,&#92;&quot;subject&#92;&quot;:&#92;&quot;d&#92;&quot;},{&#92;&quot;rank&#92;&quot;:2,&#92;&quot;prioritizer&#92;&quot;:&#92;&quot;B&#92;&quot;,&#92;&quot;subject&#92;&quot;:&#92;&quot;a&#92;&quot;},{&#92;&quot;rank&#92;&quot;:3,&#92;&quot;prioritizer&#92;&quot;:&#92;&quot;B&#92;&quot;,&#92;&quot;subject&#92;&quot;:&#92;&quot;c&#92;&quot;},{&#92;&quot;rank&#92;&quot;:0,&#92;&quot;prioritizer&#92;&quot;:&#92;&quot;C&#92;&quot;,&#92;&quot;subject&#92;&quot;:&#92;&quot;d&#92;&quot;},{&#92;&quot;rank&#92;&quot;:1,&#92;&quot;prioritizer&#92;&quot;:&#92;&quot;C&#92;&quot;,&#92;&quot;subject&#92;&quot;:&#92;&quot;a&#92;&quot;}]&quot;' data-total-rows='16' data-total-columns='3' data-max-columns='50' data-banner-text='&quot;&quot;' data-pagination='true' data-page-size='10' data-field-types='[[&quot;rank&quot;,[&quot;integer&quot;,&quot;u32&quot;]],[&quot;prioritizer&quot;,[&quot;string&quot;,&quot;str&quot;]],[&quot;subject&quot;,[&quot;string&quot;,&quot;str&quot;]]]' data-show-filters='true' data-show-download='true' data-show-column-summaries='true' data-show-data-types='true' data-show-page-size-selector='true' data-show-column-explorer='true' data-show-chart-builder='true' data-row-headers='[]' data-has-stable-row-id='false' data-lazy='false' data-preload='false'></marimo-table></marimo-ui-element>"
}
}
],
"console": []
},
{
"id": "wJzy",
"code_hash": null,
"outputs": [],
"console": []
}
]
}

View file

@ -1,18 +1,22 @@
import marimo
__generated_with = "0.13.6"
__generated_with = "0.18.3"
app = marimo.App(width="medium")
@app.cell(hide_code=True)
def _(mo):
mo.md(r"""# Caltech Hovse Rotation Example""")
mo.md(r"""
# Caltech Hovse Rotation Example
""")
return
@app.cell(hide_code=True)
def _(mo):
mo.md(r"""## Imports""")
mo.md(r"""
## Imports
""")
return
@ -22,27 +26,73 @@ def _():
import polars as pl, polars.selectors as pls
import numpy as np, faker as fk
import carousel as crsl
return crsl, mo, pl
@app.cell(hide_code=True)
def _(mo):
mo.md(r"""## Generating Rotation Rankings""")
mo.md(r"""
## Generating Rotation Rankings
""")
return
@app.cell
def _(crsl, pl):
ar = pl.DataFrame({ "": ["A", "B", "C", "D"], "a": [1, 2, 3, 4], "b": [1, 4, 3, 2], "c": [2, 1, 3, 4], "d": [4, 2, 3, 1]})
rr = pl.DataFrame({ "": ["a", "b", "c", "d"], "A": [3, 4, 2, 1], "B": [3, 1, 4, 2], "C": [2, 3, 4, 1], "D": [3, 2, 1, 4]})
ar = pl.DataFrame(
{
"": ["A", "B", "C", "D"],
"a": [1, 2, 3, 4],
"b": [1, 4, 3, 2],
"c": [2, 1, 3, 4],
"d": [4, 2, 3, 1],
}
)
rr = pl.DataFrame(
{
"": ["a", "b", "c", "d"],
"A": [3, 4, 2, 1],
"B": [3, 1, 4, 2],
"C": [2, 3, 4, 1],
"D": [3, 2, 1, 4],
}
)
m = pl.DataFrame({"A": ["c"], "B": ["d"], "C": ["a"], "D": ["b"]})
crsl.check_stable(m, ar, rr)
crsl.check_match_stable(m, ar, rr)
return ar, rr
@app.cell
def _(ar):
ar
return
@app.cell
def _():
hovses = ["Blacker", "Dabney", "Ricketts", "Fleming", "Page", "Lloyd", "Venerable", "Avery"]
hovses = [
"Blacker",
"Dabney",
"Ricketts",
"Fleming",
"Page",
"Lloyd",
"Venerable",
"Avery",
]
return
@app.cell
def _(ar, crsl):
crsl.rank_to_pref(ar)
return
@app.cell
def _(crsl, rr):
crsl.pref_to_prio(crsl.rank_to_pref(rr))
return

1578
uv.lock generated

File diff suppressed because it is too large Load diff