Metadata-Version: 2.5
Name: preference-grouping-solver
Version: 0.2.0
Summary: Optimal assignment of students to capacity-limited groups based on ranked preferences
Author-email: Simon Münker <muenker@uni-trier.de>
Requires-Python: >=3.11
Requires-Dist: numpy>=2.4.6
Requires-Dist: pandas>=3.0.6
Requires-Dist: pydantic>=2.13.0
Requires-Dist: rich>=15.0.0
Requires-Dist: scipy>=1.17.1
Provides-Extra: fake
Requires-Dist: faker>=40.0.0; extra == 'fake'
Description-Content-Type: text/markdown

# preference-grouping-solver

Assigns students to capacity-limited groups so that the sum of their preference ranks is minimal,
solved exactly as a mixed-integer linear program with SciPy/HiGHS.

## Installation

```bash
uv sync                  # runtime
uv sync --all-extras     # + faker for generating synthetic data
```

## Input

Each entry ranks groups from most to least preferred:

```json
{
    "student_id": 1000000,
    "group_order": [2, 1, 6, 14, 3, 8, 4, 7, 9, 5]
}
```

- `group_order` may be partial; every unranked group receives the worst rank (`groups`).
- Groups must be unique and within `1..groups`.
- Input is either a **directory** of `*.json` files (one entry or a list of entries per file)
  or a **single JSON file** containing a list of entries.

Invalid entries are skipped and reported with their location (`file` or `file[row]`) and kind:

| kind        | cause                                             |
|-------------|---------------------------------------------------|
| `parse`     | unreadable file or invalid JSON                   |
| `schema`    | missing/invalid field, repeated or unknown group  |
| `duplicate` | `student_id` already loaded from an earlier entry |

## Configuration

| field            | default                 | description                                                  |
|------------------|-------------------------|--------------------------------------------------------------|
| `input`          | required                | directory or JSON file                                       |
| `output`         | `group_assignments.csv` | CSV export (`student,group,rank`)                            |
| `groups`         | `16`                    | number of groups, numbered `1..groups`                       |
| `max_group_size` | `9`                     | maximum students per group                                   |
| `min_group_size` | `0`                     | minimum students per group                                   |
| `rank_exponent`  | `1.0`                   | cost is `rank ** exponent`; `> 1` avoids very bad placements |

## Usage

### CLI

```bash
preference-grouping-solver --input preferences/ --groups 16 --max-group-size 9
preference-grouping-solver --config examples/config.json --rank-exponent 1
python -m preference_grouping_solver --help
```

Values from `--config` (JSON, paths relative to the working directory) are overridden by CLI flags.
The exit code is `0` on success, `1` if no assignment exists and `2` for invalid arguments.

### Python

```python
from preference_grouping_solver import Solver, SolverConfig, load

config = SolverConfig(groups=8, max_group_size=9)
loaded = load("examples/preferences.json", groups=config.groups)

solution = Solver(config).solve(loaded.preferences)
solution.assignments        # student -> group, rank
solution.groups             # size, mean_rank, first_choices per group
solution.rank_distribution  # students per assigned rank
solution.to_csv("group_assignments.csv")
```

## Output example

```text
groups
┏━━━━━━━┳━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ group ┃ size ┃ mean_rank ┃ first_choices ┃
┡━━━━━━━╇━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ 1     │ 9/9  │ 1.00      │ 11            │
│ 2     │ 6/9  │ 1.17      │ 5             │
│ …     │ …    │ …         │ …             │
└───────┴──────┴───────────┴───────────────┘
assigned ranks
┏━━━━━━┳━━━━━━━━━━┓
┃ rank ┃ students ┃
┡━━━━━━╇━━━━━━━━━━┩
│ 1    │ 54       │
│ 2    │ 6        │
└──────┴──────────┘
students: 60, mean rank: 1.10
assignments written to examples/group_assignments.csv
```

## Examples

`examples/` contains 60 synthetic students for 8 groups, both as a directory and as a combined file:

```bash
uv run python examples/generate.py    # regenerate the data with faker
uv run python examples/run.py         # Python API
uv run preference-grouping-solver --config examples/config.json
```

## Development

```bash
uv sync --all-extras
uv run pytest
```

Tests use faker-generated data and verify optimality against brute force on small instances.
