This commit is contained in:
Thomas (Tom) C. Gorordo 2025-12-20 08:08:47 -08:00
parent 6cdc579270
commit cbedbbeb48
12 changed files with 1582 additions and 591 deletions

View file

@ -3,7 +3,7 @@ bibliography: REFERENCES.bib
... ...
# Carousel # Carousel
*A simple Stable Matching Solver.* *A simple Stable Matching solver.*
`carousel` is a solver for the `carousel` is a solver for the
[Envy-free](https://en.wikipedia.org/wiki/Envy-free_matching) [Envy-free](https://en.wikipedia.org/wiki/Envy-free_matching)
@ -14,30 +14,29 @@ bibliography: REFERENCES.bib
### Gale-Shapley Deferred Acceptance ### 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 - Gives a solution to the basic problem.
[2012 Nobel Prize in Economics](https://www.nobelprize.org/prizes/economic-sciences/2012/popular-information/).
The basic problem being solved is as follows: ### GS + Rotation Enumeration of Solutions
#### Stable Marriage Problem - Can implement post-selection measures/constraints/criteria.
The "Stable Marriage problem" TODO
TODO ### Integer Programming (Huang++)
#### College Admissions Problem ### Polytope Solution Sampling (Large Problems)
Solving the stable marriage problem also provides a solution to the "College Admissions Problem",
with just a little more work.
TODO ### Brute-Force Combinatoric Search
A benchmark/testbed implementation. Beware.
### More?
Contribute!
## Data - Input/Output ## 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), 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. which accepts a few inter-related tabular schemes for the input/output data.
### Input ### 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"), 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.
as well as the preferences/rankings for the reviwers of applicants. Input should be in one of two forms:
Input should be in one of three forms:
#### Preferences #### Preferences
Preferences enumerate by-name some preferences in descending order, 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 | | banana | 3 | 1 | 2 |
| cherry | 2 | 3 | 1 | | cherry | 2 | 3 | 1 |
### Intermediate
#### Ranking Matrix #### Ranking Matrix
In order to perform a matching, `carousel` either needs a pair of preferences 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), (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 | | 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) | | CaseMed | (3, 2) | (1, 1) | (2, 3) |
| Emory | (2, 1) | (3, 3) | (1, 2) | | 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 ### Output
#### Matching #### 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, 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. 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 ### Command Line Binary
TODO TODO
@ -141,13 +165,13 @@ TODO
It's often desirable to enforce additional criteria on solutions It's often desirable to enforce additional criteria on solutions
that are not well-posed within the core optimization problem. 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 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 ## Examples
Here are some usage examples: Here are some usage examples:
### Departmental TA Assignments ### University of Oregon Physics Department Graduate TA Assignments
TODO TODO
### Caltech Housing Rotation ### Caltech Housing Rotation

View file

@ -1,5 +1,3 @@
@article{gale&shapley1962, @article{gale&shapley1962,
ISSN = {0002989, 19300972}, ISSN = {0002989, 19300972},
URL = {https://www.jstor.org/stable/2312726}, URL = {https://www.jstor.org/stable/2312726},
@ -14,3 +12,64 @@
year = {1962}, 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: list:
just --list just --list
run: #run:
uv run carousel # uv run carousel
python *arguments: python *arguments:
uv run python -c "import code; from rich import pretty; pretty.install(); code.interact()" {{arguments}} uv run python -c {{arguments}}
check:
uv run pyright src
test: test:
uv run pytest -vvv --tb=short --log-cli-level=INFO uv run pytest -vvv --tb=short --log-cli-level=INFO
@ -16,7 +13,16 @@ test:
format: format:
uv run ruff format src test 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/cli.py
uv run pyinstaller src/gui.py uv run pyinstaller src/gui.py
@ -29,5 +35,3 @@ wipe:
just clean just clean
rm -rf .venv 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: # This file was autogenerated by uv via the following command:
# uv pip compile pyproject.toml -o requirements.txt --group dev # uv pip compile pyproject.toml -o requirements.txt --group dev
altair==5.5.0
# via marimo
altgraph==0.17.4 altgraph==0.17.4
# via pyinstaller # via pyinstaller
annotated-types==0.7.0
# via pydantic
anyio==4.10.0
# via
# httpx
# openai
# starlette
attrs==25.3.0 attrs==25.3.0
# via hypothesis # via
# hypothesis
# jsonschema
# referencing
certifi==2025.8.3
# via
# httpcore
# httpx
click==8.1.8 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 hypothesis==6.131.5
# via carousel (pyproject.toml:dev) # via carousel (pyproject.toml:dev)
idna==3.10
# via
# anyio
# httpx
iniconfig==2.1.0 iniconfig==2.1.0
# via pytest # 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 markdown-it-py==3.0.0
# via rich # via rich
markupsafe==3.0.2
# via jinja2
mdurl==0.1.2 mdurl==0.1.2
# via markdown-it-py # via markdown-it-py
narwhals==2.3.0
# via
# altair
# marimo
nbformat==5.10.4
# via marimo
nodeenv==1.9.1 nodeenv==1.9.1
# via pyright # via pyright
numpy==2.2.4 numpy==2.2.4
# via carousel (pyproject.toml) # via carousel (pyproject.toml)
openai==1.106.1
# via marimo
packaging==24.2 packaging==24.2
# via # via
# altair
# marimo
# pyinstaller # pyinstaller
# pyinstaller-hooks-contrib # pyinstaller-hooks-contrib
# pytest # pytest
parso==0.8.5
# via jedi
platformdirs==4.4.0
# via jupyter-core
pluggy==1.5.0 pluggy==1.5.0
# via pytest # via pytest
polars==1.27.1 polars==1.27.1
# via carousel (pyproject.toml) # via
# carousel (pyproject.toml)
# marimo
psutil==7.0.0
# via marimo
py-cpuinfo==9.0.0 py-cpuinfo==9.0.0
# via pytest-benchmark # via pytest-benchmark
pyarrow==21.0.0
# via polars
pyclean==3.1.0 pyclean==3.1.0
# via carousel (pyproject.toml:dev) # via carousel (pyproject.toml:dev)
pydantic==2.11.7
# via openai
pydantic-core==2.33.2
# via pydantic
pygments==2.19.1 pygments==2.19.1
# via rich # via
# marimo
# rich
pyinstaller==6.13.0 pyinstaller==6.13.0
# via carousel (pyproject.toml:dev) # via carousel (pyproject.toml:dev)
pyinstaller-hooks-contrib==2025.3 pyinstaller-hooks-contrib==2025.3
# via pyinstaller # via pyinstaller
pymdown-extensions==10.16.1
# via marimo
pyright==1.1.399 pyright==1.1.399
# via carousel (pyproject.toml:dev) # via carousel (pyproject.toml:dev)
pyside6==6.9.0 pyside6==6.9.0
@ -53,10 +148,24 @@ pytest==8.3.5
# pytest-benchmark # pytest-benchmark
pytest-benchmark==5.1.0 pytest-benchmark==5.1.0
# via carousel (pyproject.toml:dev) # 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 rich==14.0.0
# via carousel (pyproject.toml) # via carousel (pyproject.toml)
rpds-py==0.27.1
# via
# jsonschema
# referencing
ruff==0.11.6 ruff==0.11.6
# via carousel (pyproject.toml:dev) # via
# carousel (pyproject.toml:dev)
# marimo
setuptools==78.1.0 setuptools==78.1.0
# via # via
# pyinstaller # pyinstaller
@ -66,7 +175,39 @@ shiboken6==6.9.0
# pyside6 # pyside6
# pyside6-addons # pyside6-addons
# pyside6-essentials # pyside6-essentials
sniffio==1.3.1
# via
# anyio
# openai
sortedcontainers==2.4.0 sortedcontainers==2.4.0
# via hypothesis # 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 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 import logging, rich
from rich.logging import RichHandler from rich.logging import RichHandler
rich.traceback.install()
import itertools as it import itertools as it
import numpy as np import numpy as np
import polars as pl import polars as pl
import polars.selectors as pls import polars.selectors as pls
rich.traceback.install()
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=logging.INFO,
format="%(message)s", format="%(message)s",
@ -24,126 +24,7 @@ logging.basicConfig(
) )
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
from .util import *
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 .brute import * from .brute import *
from .def_acc import * from .def_acc import *

View file

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

View file

@ -1,5 +1,126 @@
def deferred_acceptance(applicant_rankings, reviewer_rankings): from .util import *
"""Find Gale-Shapley deferred-acceptance stable matchings for preferences A, R."""
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 = reviewer_rankings.rename(
{reviewer_rankings.columns[0]: "applicant"} {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( pp = pl.DataFrame(
{"a": ["A", "A", "B"], "b": ["B", "A", "C"], "c": ["C", "B", "A"]} {"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(): def test_pref_to_rank():
@ -51,7 +51,7 @@ def test_invalid_rank():
rr = pl.DataFrame( rr = pl.DataFrame(
{"": ["A", "B", "C"], "a": [1, 1, 2], "b": [2, 1, 3], "c": [3, 2, 1]} {"": ["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(): def test_rank_to_pref():
@ -60,7 +60,7 @@ def test_rank_to_pref():
@given(rankings()) @given(rankings())
def test_valid_rank(R): def test_valid_rank(R):
assert crsl.check_valid_rank(R) assert crsl.check_rank_noties(R)
@given(rankings()) @given(rankings())
@ -70,7 +70,7 @@ def test_ranks_tofrom_prefs(R):
@given(preferences()) @given(preferences())
def test_valid_pref(P): def test_valid_pref(P):
assert crsl.check_valid_pref(P) assert crsl.check_pref_allunique(P)
@given(preferences()) @given(preferences())
@ -99,7 +99,7 @@ def test_eg2_unstable():
) )
match = pl.DataFrame({"A": ["a"], "B": ["b"], "C": ["c"], "D": ["d"]}) 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(): def test_eg2_isstable():
@ -123,7 +123,7 @@ def test_eg2_isstable():
) )
match = pl.DataFrame({"A": ["c"], "B": ["d"], "C": ["a"], "D": ["b"]}) 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( @given(
@ -131,5 +131,5 @@ def test_eg2_isstable():
rankings(names=["A", "B", "C", "D"], choices=["a", "b", "c", "d"]), rankings(names=["A", "B", "C", "D"], choices=["a", "b", "c", "d"]),
) )
def test_defacc_isstable(applicant_rankings, reviewer_rankings): def test_defacc_isstable(applicant_rankings, reviewer_rankings):
match = crsl.deferred_acceptance(applicant_rankings, reviewer_rankings) match = crsl.matchby_deferred_acceptance(applicant_rankings, reviewer_rankings)
assert crsl.check_stable(match, 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 import marimo
__generated_with = "0.13.6" __generated_with = "0.18.3"
app = marimo.App(width="medium") app = marimo.App(width="medium")
@app.cell(hide_code=True) @app.cell(hide_code=True)
def _(mo): def _(mo):
mo.md(r"""# Caltech Hovse Rotation Example""") mo.md(r"""
# Caltech Hovse Rotation Example
""")
return return
@app.cell(hide_code=True) @app.cell(hide_code=True)
def _(mo): def _(mo):
mo.md(r"""## Imports""") mo.md(r"""
## Imports
""")
return return
@ -22,27 +26,73 @@ def _():
import polars as pl, polars.selectors as pls import polars as pl, polars.selectors as pls
import numpy as np, faker as fk import numpy as np, faker as fk
import carousel as crsl import carousel as crsl
return crsl, mo, pl return crsl, mo, pl
@app.cell(hide_code=True) @app.cell(hide_code=True)
def _(mo): def _(mo):
mo.md(r"""## Generating Rotation Rankings""") mo.md(r"""
## Generating Rotation Rankings
""")
return return
@app.cell @app.cell
def _(crsl, pl): 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]}) ar = pl.DataFrame(
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]}) {
"": ["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"]}) 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 return
@app.cell @app.cell
def _(): 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 return

1265
uv.lock generated

File diff suppressed because it is too large Load diff