daemon03 commited on
Commit
f804bd5
·
0 Parent(s):

Initial commit: Streamlit UI for Gale-Shapley Algorithm

Browse files
.gitignore ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Environments
55
+ .env
56
+ .venv
57
+ env/
58
+ venv/
59
+ ENV/
60
+ env.bak/
61
+ venv.bak/
62
+
63
+ # Jupyter Notebook
64
+ .ipynb_checkpoints
65
+
66
+ # IPython
67
+ profile_default/
68
+ ipython_config.py
69
+
70
+ # pyenv
71
+ # For a library or package, you might want to ignore these files since the code is
72
+ # intended to run in multiple environments; otherwise, check them in:
73
+ # .python-version
74
+
75
+ # pipenv
76
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
77
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
78
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
79
+ # install all needed dependencies.
80
+ #Pipfile.lock
81
+
82
+ # poetry
83
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
84
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
85
+ # commonly ignored for libraries.
86
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
87
+ #poetry.lock
88
+
89
+ # pdm
90
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
91
+ #pdm.lock
92
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
93
+ # in version control.
94
+ # https://pdm.fming.dev/#use-with-ide
95
+ .pdm.toml
96
+
97
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
98
+ __pypackages__/
99
+
100
+ # Celery stuff
101
+ celerybeat-schedule
102
+ celerybeat.pid
103
+
104
+ # SageMath parsed files
105
+ *.sage.py
106
+
107
+ # Environments
108
+ .env
109
+ .venv
110
+ env/
111
+ venv/
112
+ ENV/
113
+ env.bak/
114
+ venv.bak/
115
+
116
+ # Spyder project settings
117
+ .spyderproject
118
+ .spyproject
119
+
120
+ # Rope project settings
121
+ .ropeproject
122
+
123
+ # mkdocs documentation
124
+ /site
125
+
126
+ # mypy
127
+ .mypy_cache/
128
+ .dmypy.json
129
+ dmypy.json
130
+
131
+ # Pyre type checker
132
+ .pyre/
133
+
134
+ # pytype static type analyzer
135
+ .pytype/
136
+
137
+ # Cython debug symbols
138
+ cython_debug/
139
+
140
+ # OS generated files
141
+ .DS_Store
142
+ .DS_Store?
143
+ ._*
144
+ .Spotlight-V100
145
+ .Trashes
146
+ ehthumbs.db
147
+ Thumbs.db
148
+
149
+ # Editors
150
+ .vscode/
151
+ .idea/
152
+ *.swp
153
+ *.swo
.streamlit/config.toml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ [theme]
2
+ primaryColor = "#6C63FF"
3
+ backgroundColor = "#0E1117"
4
+ secondaryBackgroundColor = "#1A1D29"
5
+ textColor = "#E0E0E0"
6
+ font = "sans serif"
7
+
8
+ [server]
9
+ headless = true
10
+ port = 8501
README.md ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🏠 Roommate Allocation System — Python & Streamlit Edition
2
+
3
+ [![Python](https://img.shields.io/badge/Python-3.9+-3776AB?logo=python&logoColor=white)](https://python.org)
4
+ [![Streamlit](https://img.shields.io/badge/Streamlit-1.30+-FF4B4B?logo=streamlit&logoColor=white)](https://streamlit.io)
5
+ [![Algorithm](https://img.shields.io/badge/Algorithm-Gale--Shapley-6C63FF)](https://en.wikipedia.org/wiki/Gale%E2%80%93Shapley_algorithm)
6
+
7
+ A modern **Streamlit UI** for the Gale-Shapley Roommate Allocation algorithm.
8
+ This version uses **Python** for the algorithm and **file-system storage** (JSON) instead of MySQL.
9
+
10
+ > 🔗 **For the original C + MySQL version**, see:
11
+ > [https://github.com/Harshwardhan-Deshmukh03/Roommate-allocation-using-Gale-Shapley-Algorithm.git](https://github.com/Harshwardhan-Deshmukh03/Roommate-allocation-using-Gale-Shapley-Algorithm.git)
12
+
13
+ ---
14
+
15
+ ## ✨ Features
16
+
17
+ - **Stable Matching** via the Nobel Prize-winning Gale-Shapley algorithm
18
+ - **Two-Stage Allocation**: Roommate matching → CGPA-ranked room assignment
19
+ - **CSV Import**: Bulk-upload students & rooms via CSV files
20
+ - **File-System Storage**: No database needed — data stored as JSON
21
+ - **Interactive Charts**: Plotly visualizations of CGPA distributions
22
+ - **Premium UI**: Dark theme with glassmorphism, gradients, and animations
23
+
24
+ ---
25
+
26
+ ## 🚀 Quick Start
27
+
28
+ ```bash
29
+ # 1. Create virtual environment
30
+ python -m venv venv
31
+ venv\Scripts\activate # Windows
32
+ # source venv/bin/activate # macOS/Linux
33
+
34
+ # 2. Install dependencies
35
+ pip install -r requirements.txt
36
+
37
+ # 3. Run the app
38
+ streamlit run app.py
39
+ ```
40
+
41
+ ---
42
+
43
+ ## 📁 Project Structure
44
+
45
+ ```
46
+ streamlit_gale_shapely/
47
+ ├── app.py # Main Streamlit UI application
48
+ ├── gale_shapley.py # Gale-Shapley algorithm (Python port)
49
+ ├── db.py # File-system database layer (JSON)
50
+ ├── requirements.txt # Python dependencies
51
+ ├── .streamlit/
52
+ │ └── config.toml # Streamlit theme configuration
53
+ ├── data/
54
+ │ ├── students.json # Student records (replaces MySQL 'main' table)
55
+ │ ├── rooms.json # Room records (replaces MySQL 'RoomNum' table)
56
+ │ └── allocations.json # Allocation results
57
+ └── sample_csv/
58
+ ├── sample_students_10.csv # 10 students (5 pairs)
59
+ ├── sample_rooms_5.csv # 5 rooms for 10 students
60
+ ├── sample_students_26.csv # 26 students (13 pairs)
61
+ └── sample_rooms_13.csv # 13 rooms for 26 students
62
+ ```
63
+
64
+ ---
65
+
66
+ ## 📊 CSV Format
67
+
68
+ ### Students CSV
69
+ | Column | Type | Description |
70
+ |--------|------|-------------|
71
+ | id | int | Unique student ID (0-indexed) |
72
+ | name | str | Student name |
73
+ | cgpa | float | CGPA (0.0–10.0) |
74
+ | pref_roommate | str | Space-separated preferred roommate IDs |
75
+ | pref_room | str | Space-separated preferred room IDs |
76
+
77
+ ### Rooms CSV
78
+ | Column | Type | Description |
79
+ |--------|------|-------------|
80
+ | room_id | int | Unique room ID (0-indexed) |
81
+ | room_number | str | Room label (e.g., "A101") |
82
+
83
+ ---
84
+
85
+ ## 🧠 Algorithm
86
+
87
+ 1. **Stage 1 — Roommate Matching**: Gale-Shapley pairs students into stable roommate matches.
88
+ 2. **Stage 2 — Room Allocation**: Pairs ranked by max CGPA select rooms via Gale-Shapley.
89
+
90
+ Higher CGPA pairs get priority in room selection.
app.py ADDED
@@ -0,0 +1,456 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Roommate Allocation System — Streamlit UI
3
+ Uses the Gale-Shapley algorithm (Python implementation).
4
+ For the original C + MySQL version, see:
5
+ https://github.com/Harshwardhan-Deshmukh03/Roommate-allocation-using-Gale-Shapley-Algorithm.git
6
+ """
7
+
8
+ import streamlit as st
9
+ import pandas as pd
10
+ import os, json
11
+
12
+ from db import (
13
+ get_all_students, get_all_rooms, get_all_allocations,
14
+ save_all_students, save_all_rooms, save_allocations,
15
+ clear_all_students, clear_all_rooms, clear_allocations,
16
+ add_student, delete_student, add_room, delete_room,
17
+ import_students_from_csv, import_rooms_from_csv,
18
+ get_student_count, get_room_count,
19
+ )
20
+ from gale_shapley import run_full_allocation
21
+
22
+ # ── Page Config ───────────────────────────────────────────────────────────────
23
+ st.set_page_config(
24
+ page_title="Roommate Allocation · Gale-Shapley",
25
+ page_icon="🏠",
26
+ layout="wide",
27
+ initial_sidebar_state="expanded",
28
+ )
29
+
30
+ # ── Custom CSS ────────────────────────────────────────────────────────────────
31
+ st.markdown("""
32
+ <style>
33
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
34
+ :root {
35
+ --accent: #6C63FF; --accent2: #00D2FF; --bg: #0E1117;
36
+ --card: #161B22; --card2: #1A1D29; --text: #E0E0E0;
37
+ --success: #00E676; --warn: #FFD600; --danger: #FF5252;
38
+ }
39
+ html, body, [class*="css"] { font-family: 'Inter', sans-serif; }
40
+
41
+ /* Hero banner */
42
+ .hero {
43
+ background: linear-gradient(135deg, #6C63FF 0%, #00D2FF 100%);
44
+ border-radius: 16px; padding: 2.5rem 2rem; margin-bottom: 1.5rem;
45
+ text-align: center; position: relative; overflow: hidden;
46
+ }
47
+ .hero::before {
48
+ content: ''; position: absolute; inset: 0;
49
+ background: radial-gradient(circle at 30% 50%, rgba(255,255,255,.12) 0%, transparent 60%);
50
+ }
51
+ .hero h1 { color: #fff; font-size: 2.2rem; font-weight: 800; margin: 0; position: relative; }
52
+ .hero p { color: rgba(255,255,255,.85); font-size: 1rem; margin: .5rem 0 0; position: relative; }
53
+
54
+ /* Stat cards */
55
+ .stat-row { display: flex; gap: 1rem; margin-bottom: 1.5rem; flex-wrap: wrap; }
56
+ .stat-card {
57
+ flex: 1; min-width: 160px; background: var(--card); border-radius: 14px;
58
+ padding: 1.4rem; text-align: center; border: 1px solid rgba(108,99,255,.25);
59
+ transition: transform .2s, box-shadow .2s;
60
+ }
61
+ .stat-card:hover { transform: translateY(-4px); box-shadow: 0 8px 24px rgba(108,99,255,.2); }
62
+ .stat-card .num { font-size: 2rem; font-weight: 700; background: linear-gradient(135deg,#6C63FF,#00D2FF);
63
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
64
+ .stat-card .lbl { color: #9CA3AF; font-size: .85rem; margin-top: .3rem; }
65
+
66
+ /* Section headers */
67
+ .sec-hdr { font-size: 1.35rem; font-weight: 700; margin: 1.5rem 0 .8rem;
68
+ padding-left: .6rem; border-left: 4px solid var(--accent); }
69
+
70
+ /* Info banner */
71
+ .info-banner {
72
+ background: linear-gradient(135deg, rgba(108,99,255,.12), rgba(0,210,255,.08));
73
+ border: 1px solid rgba(108,99,255,.3); border-radius: 12px;
74
+ padding: 1rem 1.2rem; margin: 1rem 0; font-size: .9rem; color: var(--text);
75
+ }
76
+
77
+ /* Footer */
78
+ .footer { text-align: center; padding: 2rem 0 1rem; color: #6B7280; font-size: .82rem; }
79
+ .footer a { color: var(--accent); text-decoration: none; }
80
+ .footer a:hover { text-decoration: underline; }
81
+
82
+ /* Table tweaks */
83
+ .stDataFrame { border-radius: 12px; overflow: hidden; }
84
+ </style>
85
+ """, unsafe_allow_html=True)
86
+
87
+ # ── Sidebar ───────────────────────────────────────────────────────────────────
88
+ with st.sidebar:
89
+ st.markdown("## 🧭 Navigation")
90
+ page = st.radio(
91
+ "Go to",
92
+ ["🏠 Dashboard", "👥 Manage Students", "🚪 Manage Rooms",
93
+ "📂 CSV Import", "⚙️ Run Allocation", "📊 Results"],
94
+ label_visibility="collapsed",
95
+ )
96
+ st.markdown("---")
97
+ st.markdown(
98
+ '<div class="info-banner">'
99
+ '<b>🐍 Python + Streamlit Edition</b><br>'
100
+ 'File-system storage (no MySQL needed).<br><br>'
101
+ 'For the <b>C + MySQL</b> version:<br>'
102
+ '<a href="https://github.com/Harshwardhan-Deshmukh03/Roommate-allocation-using-Gale-Shapley-Algorithm.git" '
103
+ 'target="_blank">View on GitHub ↗</a></div>',
104
+ unsafe_allow_html=True,
105
+ )
106
+
107
+ # ── Helper ────────────────────────────────────────────────────────────────────
108
+ def stat_cards(items):
109
+ cols = st.columns(len(items))
110
+ for col, (num, lbl) in zip(cols, items):
111
+ col.markdown(
112
+ f'<div class="stat-card"><div class="num">{num}</div>'
113
+ f'<div class="lbl">{lbl}</div></div>',
114
+ unsafe_allow_html=True,
115
+ )
116
+
117
+ # ══════════════════════════════════════════════════════════════════════════════
118
+ # PAGES
119
+ # ══════════════════════════════════════════════════════════════════════════════
120
+
121
+ # ── Dashboard ─────────────────────────────────────────────────────────────────
122
+ if page == "🏠 Dashboard":
123
+ st.markdown(
124
+ '<div class="hero"><h1>🏠 Roommate Allocation System</h1>'
125
+ '<p>Gale-Shapley Stable-Matching Algorithm · Python &amp; Streamlit</p></div>',
126
+ unsafe_allow_html=True,
127
+ )
128
+ n_stu = get_student_count()
129
+ n_rm = get_room_count()
130
+ allocs = get_all_allocations()
131
+ stat_cards([
132
+ (n_stu, "Students"), (n_rm, "Rooms"),
133
+ (n_stu // 2 if n_stu else 0, "Possible Pairs"),
134
+ (len(allocs), "Allocations"),
135
+ ])
136
+
137
+ st.markdown('<div class="sec-hdr">How It Works</div>', unsafe_allow_html=True)
138
+ c1, c2 = st.columns(2)
139
+ with c1:
140
+ st.markdown("""
141
+ **Stage 1 — Roommate Matching**
142
+ 1. Each student submits an ordered preference list of roommates.
143
+ 2. The Gale-Shapley algorithm pairs students into **stable matches**
144
+ (no two students would rather swap partners).
145
+ """)
146
+ with c2:
147
+ st.markdown("""
148
+ **Stage 2 — Room Allocation**
149
+ 1. Pairs are ranked by the **higher CGPA** in each pair.
150
+ 2. Ranked pairs select rooms via Gale-Shapley, so top performers
151
+ get priority for their preferred rooms.
152
+ """)
153
+
154
+ st.markdown('<div class="sec-hdr">Quick Start</div>', unsafe_allow_html=True)
155
+ st.markdown("""
156
+ 1. **Add Students** — manually or via CSV upload.
157
+ 2. **Add Rooms** — manually or via CSV upload.
158
+ 3. **Run Allocation** — click one button to get stable assignments.
159
+ 4. **View Results** — see the final roommate + room table & charts.
160
+ """)
161
+
162
+ st.markdown(
163
+ '<div class="footer">Built with Python &amp; Streamlit · '
164
+ 'Algorithm by Gale &amp; Shapley (1962) · '
165
+ '<a href="https://github.com/Harshwardhan-Deshmukh03/Roommate-allocation-using-Gale-Shapley-Algorithm.git" '
166
+ 'target="_blank">Original C + MySQL version</a></div>',
167
+ unsafe_allow_html=True,
168
+ )
169
+
170
+ # ── Manage Students ──────────────────────────────────────────────────────────
171
+ elif page == "👥 Manage Students":
172
+ st.markdown('<div class="sec-hdr">👥 Manage Students</div>', unsafe_allow_html=True)
173
+
174
+ students = get_all_students()
175
+ stat_cards([(len(students), "Total Students")])
176
+
177
+ # Show current students
178
+ if students:
179
+ df = pd.DataFrame(students)
180
+ df["pref_roommate"] = df["pref_roommate"].apply(lambda x: " ".join(map(str, x)))
181
+ df["pref_room"] = df["pref_room"].apply(lambda x: " ".join(map(str, x)))
182
+ st.dataframe(df, use_container_width=True, hide_index=True)
183
+ else:
184
+ st.info("No students yet. Add below or import via CSV.")
185
+
186
+ st.markdown("---")
187
+ st.markdown("#### ➕ Add a Student")
188
+ with st.form("add_student", clear_on_submit=True):
189
+ ac1, ac2, ac3 = st.columns(3)
190
+ sid = ac1.number_input("Student ID", min_value=0, step=1)
191
+ name = ac2.text_input("Name")
192
+ cgpa = ac3.number_input("CGPA", min_value=0.0, max_value=10.0, step=0.1)
193
+ pref_r = st.text_input("Roommate Preferences (space-separated IDs)", placeholder="5 6 7 8 9 ...")
194
+ pref_rm = st.text_input("Room Preferences (space-separated room IDs)", placeholder="0 1 2 3 4 ...")
195
+ submitted = st.form_submit_button("Add Student", type="primary")
196
+ if submitted:
197
+ if not name.strip():
198
+ st.error("Name cannot be empty.")
199
+ else:
200
+ try:
201
+ pr = [int(x) for x in pref_r.strip().split()] if pref_r.strip() else []
202
+ pm = [int(x) for x in pref_rm.strip().split()] if pref_rm.strip() else []
203
+ ok = add_student({"id": int(sid), "name": name.strip(), "cgpa": float(cgpa),
204
+ "pref_roommate": pr, "pref_room": pm})
205
+ if ok:
206
+ st.success(f"✅ Added **{name}** (ID {sid})")
207
+ st.rerun()
208
+ else:
209
+ st.error(f"Student ID {sid} already exists.")
210
+ except ValueError:
211
+ st.error("Preferences must be space-separated integers.")
212
+
213
+ # Delete
214
+ if students:
215
+ st.markdown("#### 🗑️ Remove a Student")
216
+ dc1, dc2 = st.columns([3, 1])
217
+ del_id = dc1.selectbox("Select student to remove",
218
+ [(s["id"], s["name"]) for s in students],
219
+ format_func=lambda x: f"ID {x[0]} — {x[1]}")
220
+ if dc2.button("Delete", type="secondary"):
221
+ delete_student(del_id[0])
222
+ st.success(f"Removed student ID {del_id[0]}")
223
+ st.rerun()
224
+
225
+ if st.button("🗑️ Clear ALL Students", type="secondary"):
226
+ clear_all_students()
227
+ st.warning("All students cleared.")
228
+ st.rerun()
229
+
230
+ # ── Manage Rooms ──────────────────────────────────────────────────────────────
231
+ elif page == "🚪 Manage Rooms":
232
+ st.markdown('<div class="sec-hdr">🚪 Manage Rooms</div>', unsafe_allow_html=True)
233
+
234
+ rooms = get_all_rooms()
235
+ stat_cards([(len(rooms), "Total Rooms")])
236
+
237
+ if rooms:
238
+ st.dataframe(pd.DataFrame(rooms), use_container_width=True, hide_index=True)
239
+ else:
240
+ st.info("No rooms yet. Add below or import via CSV.")
241
+
242
+ st.markdown("---")
243
+ st.markdown("#### ➕ Add a Room")
244
+ with st.form("add_room", clear_on_submit=True):
245
+ rc1, rc2 = st.columns(2)
246
+ rid = rc1.number_input("Room ID", min_value=0, step=1)
247
+ rnum = rc2.text_input("Room Number", placeholder="e.g. A101")
248
+ if st.form_submit_button("Add Room", type="primary"):
249
+ if not rnum.strip():
250
+ st.error("Room number cannot be empty.")
251
+ else:
252
+ ok = add_room({"room_id": int(rid), "room_number": rnum.strip()})
253
+ if ok:
254
+ st.success(f"✅ Added room **{rnum}** (ID {rid})")
255
+ st.rerun()
256
+ else:
257
+ st.error(f"Room ID {rid} already exists.")
258
+
259
+ if rooms:
260
+ st.markdown("#### 🗑️ Remove a Room")
261
+ drc1, drc2 = st.columns([3, 1])
262
+ del_rid = drc1.selectbox("Select room to remove",
263
+ [(r["room_id"], r["room_number"]) for r in rooms],
264
+ format_func=lambda x: f"ID {x[0]} — {x[1]}")
265
+ if drc2.button("Delete", type="secondary"):
266
+ delete_room(del_rid[0])
267
+ st.success(f"Removed room ID {del_rid[0]}")
268
+ st.rerun()
269
+
270
+ if st.button("🗑️ Clear ALL Rooms", type="secondary"):
271
+ clear_all_rooms()
272
+ st.warning("All rooms cleared.")
273
+ st.rerun()
274
+
275
+ # ── CSV Import ────────────────────────────────────────────────────────────────
276
+ elif page == "📂 CSV Import":
277
+ st.markdown('<div class="sec-hdr">📂 CSV Import</div>', unsafe_allow_html=True)
278
+ st.markdown(
279
+ '<div class="info-banner">Upload CSV files to bulk-import students and rooms. '
280
+ 'This is useful when manual entry is tedious or when the allocation is complex.</div>',
281
+ unsafe_allow_html=True,
282
+ )
283
+
284
+ tab1, tab2, tab3 = st.tabs(["📥 Import Students", "📥 Import Rooms", "📄 Sample CSVs"])
285
+
286
+ with tab1:
287
+ st.markdown("**Expected columns:** `id, name, cgpa, pref_roommate, pref_room`")
288
+ st.caption("Preferences are space-separated integer IDs.")
289
+ f = st.file_uploader("Upload Students CSV", type=["csv"], key="stu_csv")
290
+ if f:
291
+ content = f.getvalue().decode("utf-8")
292
+ st.markdown("**Preview:**")
293
+ st.dataframe(pd.read_csv(f), use_container_width=True, hide_index=True)
294
+ f.seek(0)
295
+ if st.button("✅ Import Students", type="primary"):
296
+ cnt, errs = import_students_from_csv(content)
297
+ if errs:
298
+ for e in errs:
299
+ st.error(e)
300
+ st.success(f"Imported **{cnt}** students.")
301
+ st.rerun()
302
+
303
+ with tab2:
304
+ st.markdown("**Expected columns:** `room_id, room_number`")
305
+ f2 = st.file_uploader("Upload Rooms CSV", type=["csv"], key="room_csv")
306
+ if f2:
307
+ content2 = f2.getvalue().decode("utf-8")
308
+ st.markdown("**Preview:**")
309
+ st.dataframe(pd.read_csv(f2), use_container_width=True, hide_index=True)
310
+ f2.seek(0)
311
+ if st.button("✅ Import Rooms", type="primary"):
312
+ cnt2, errs2 = import_rooms_from_csv(content2)
313
+ if errs2:
314
+ for e in errs2:
315
+ st.error(e)
316
+ st.success(f"Imported **{cnt2}** rooms.")
317
+ st.rerun()
318
+
319
+ with tab3:
320
+ st.markdown("#### Sample CSV Files")
321
+ st.markdown("Download these to understand the expected format, then modify and re-upload.")
322
+ sample_dir = os.path.join(os.path.dirname(__file__), "sample_csv")
323
+ for fname in sorted(os.listdir(sample_dir)):
324
+ fpath = os.path.join(sample_dir, fname)
325
+ with open(fpath, "r") as sf:
326
+ st.download_button(f"⬇️ {fname}", sf.read(), file_name=fname, mime="text/csv")
327
+ with open(fpath, "r") as sf:
328
+ st.markdown(f"**`{fname}` preview:**")
329
+ st.code(sf.read(), language="csv")
330
+
331
+ # ── Run Allocation ────────────────────────────────────────────────────────────
332
+ elif page == "⚙️ Run Allocation":
333
+ st.markdown('<div class="sec-hdr">⚙️ Run Allocation</div>', unsafe_allow_html=True)
334
+
335
+ students = get_all_students()
336
+ rooms = get_all_rooms()
337
+ n_stu = len(students)
338
+ n_rm = len(rooms)
339
+
340
+ stat_cards([(n_stu, "Students"), (n_rm, "Rooms"), (n_stu // 2, "Pairs Needed")])
341
+
342
+ # Validation
343
+ issues = []
344
+ if n_stu < 2:
345
+ issues.append("Need at least 2 students.")
346
+ if n_stu % 2 != 0:
347
+ issues.append("Number of students must be even.")
348
+ if n_rm < n_stu // 2:
349
+ issues.append(f"Need at least {n_stu // 2} rooms (have {n_rm}).")
350
+
351
+ if issues:
352
+ for iss in issues:
353
+ st.error(f"❌ {iss}")
354
+ st.info("Fix the above issues before running the algorithm.")
355
+ else:
356
+ st.success("✅ All checks passed — ready to allocate!")
357
+ st.markdown(
358
+ '<div class="info-banner">'
359
+ '<b>Stage 1:</b> Gale-Shapley matches students into roommate pairs.<br>'
360
+ '<b>Stage 2:</b> Pairs ranked by CGPA select rooms via Gale-Shapley.</div>',
361
+ unsafe_allow_html=True,
362
+ )
363
+
364
+ if st.button("🚀 Run Gale-Shapley Allocation", type="primary", use_container_width=True):
365
+ with st.spinner("Running Gale-Shapley algorithm..."):
366
+ try:
367
+ allocs = run_full_allocation(students, rooms)
368
+ save_allocations(allocs)
369
+ st.success(f"🎉 Allocation complete — **{len(allocs)} pairs** assigned!")
370
+ st.balloons()
371
+
372
+ df = pd.DataFrame(allocs)
373
+ display_cols = ["roommate1_name", "roommate1_cgpa",
374
+ "roommate2_name", "roommate2_cgpa",
375
+ "room_number", "pair_max_cgpa"]
376
+ st.dataframe(df[display_cols], use_container_width=True, hide_index=True)
377
+ except Exception as e:
378
+ st.error(f"Allocation failed: {e}")
379
+ st.info("Try importing data via CSV if manual entry is causing issues.")
380
+
381
+ # ── Results ───────────────────────────────────────────────────────────────────
382
+ elif page == "📊 Results":
383
+ st.markdown('<div class="sec-hdr">📊 Allocation Results</div>', unsafe_allow_html=True)
384
+
385
+ allocs = get_all_allocations()
386
+ if not allocs:
387
+ st.info("No allocation results yet. Go to **⚙️ Run Allocation** first.")
388
+ else:
389
+ df = pd.DataFrame(allocs)
390
+
391
+ stat_cards([
392
+ (len(df), "Pairs Allocated"),
393
+ (f"{df['pair_max_cgpa'].mean():.2f}", "Avg Pair CGPA"),
394
+ (df["room_number"].nunique(), "Rooms Used"),
395
+ ])
396
+
397
+ # Main table
398
+ st.markdown("#### 📋 Final Allocation Table")
399
+ display = df[["roommate1_name", "roommate1_cgpa",
400
+ "roommate2_name", "roommate2_cgpa",
401
+ "room_number", "pair_max_cgpa"]].copy()
402
+ display.columns = ["Roommate 1", "CGPA 1", "Roommate 2", "CGPA 2", "Room", "Pair CGPA"]
403
+ st.dataframe(display, use_container_width=True, hide_index=True)
404
+
405
+ # Download
406
+ csv_out = display.to_csv(index=False)
407
+ st.download_button("⬇️ Download Results CSV", csv_out,
408
+ file_name="allocation_results.csv", mime="text/csv")
409
+
410
+ # Charts
411
+ st.markdown("#### 📈 CGPA Distribution")
412
+ import plotly.express as px
413
+
414
+ fig = px.bar(
415
+ display, x="Room", y="Pair CGPA",
416
+ color="Pair CGPA",
417
+ color_continuous_scale=["#6C63FF", "#00D2FF"],
418
+ title="Pair CGPA by Room Assignment",
419
+ )
420
+ fig.update_layout(
421
+ plot_bgcolor="rgba(0,0,0,0)", paper_bgcolor="rgba(0,0,0,0)",
422
+ font_color="#E0E0E0", title_font_size=16,
423
+ )
424
+ st.plotly_chart(fig, use_container_width=True)
425
+
426
+ # Roommate comparison
427
+ st.markdown("#### 🤝 Roommate CGPA Comparison")
428
+ comp = pd.DataFrame({
429
+ "Room": display["Room"],
430
+ "Roommate 1": display["CGPA 1"],
431
+ "Roommate 2": display["CGPA 2"],
432
+ })
433
+ fig2 = px.bar(
434
+ comp.melt(id_vars="Room", var_name="Roommate", value_name="CGPA"),
435
+ x="Room", y="CGPA", color="Roommate", barmode="group",
436
+ color_discrete_sequence=["#6C63FF", "#00D2FF"],
437
+ title="CGPA Comparison per Room",
438
+ )
439
+ fig2.update_layout(
440
+ plot_bgcolor="rgba(0,0,0,0)", paper_bgcolor="rgba(0,0,0,0)",
441
+ font_color="#E0E0E0", title_font_size=16,
442
+ )
443
+ st.plotly_chart(fig2, use_container_width=True)
444
+
445
+ if st.button("🗑️ Clear Results", type="secondary"):
446
+ clear_allocations()
447
+ st.warning("Results cleared.")
448
+ st.rerun()
449
+
450
+ st.markdown(
451
+ '<div class="footer">Built with <b>Python &amp; Streamlit</b> · '
452
+ 'For the <b>C + MySQL</b> version, visit '
453
+ '<a href="https://github.com/Harshwardhan-Deshmukh03/Roommate-allocation-using-Gale-Shapley-Algorithm.git" '
454
+ 'target="_blank">GitHub ↗</a></div>',
455
+ unsafe_allow_html=True,
456
+ )
data/allocations.json ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "roommate1_id": 2,
4
+ "roommate1_name": "Charlie",
5
+ "roommate1_cgpa": 9.5,
6
+ "roommate2_id": 7,
7
+ "roommate2_name": "Henry",
8
+ "roommate2_cgpa": 8.6,
9
+ "room_number": "A103",
10
+ "room_id": 2,
11
+ "pair_max_cgpa": 9.5
12
+ },
13
+ {
14
+ "roommate1_id": 1,
15
+ "roommate1_name": "Bob",
16
+ "roommate1_cgpa": 8.9,
17
+ "roommate2_id": 6,
18
+ "roommate2_name": "Grace",
19
+ "roommate2_cgpa": 9.3,
20
+ "room_number": "A102",
21
+ "room_id": 1,
22
+ "pair_max_cgpa": 9.3
23
+ },
24
+ {
25
+ "roommate1_id": 0,
26
+ "roommate1_name": "Alice",
27
+ "roommate1_cgpa": 9.2,
28
+ "roommate2_id": 5,
29
+ "roommate2_name": "Frank",
30
+ "roommate2_cgpa": 8.8,
31
+ "room_number": "A101",
32
+ "room_id": 0,
33
+ "pair_max_cgpa": 9.2
34
+ },
35
+ {
36
+ "roommate1_id": 3,
37
+ "roommate1_name": "Diana",
38
+ "roommate1_cgpa": 8.7,
39
+ "roommate2_id": 8,
40
+ "roommate2_name": "Ivy",
41
+ "roommate2_cgpa": 9.1,
42
+ "room_number": "B201",
43
+ "room_id": 3,
44
+ "pair_max_cgpa": 9.1
45
+ },
46
+ {
47
+ "roommate1_id": 4,
48
+ "roommate1_name": "Eve",
49
+ "roommate1_cgpa": 9.0,
50
+ "roommate2_id": 9,
51
+ "roommate2_name": "Jack",
52
+ "roommate2_cgpa": 8.5,
53
+ "room_number": "B202",
54
+ "room_id": 4,
55
+ "pair_max_cgpa": 9.0
56
+ }
57
+ ]
data/rooms.json ADDED
@@ -0,0 +1 @@
 
 
1
+ []
data/students.json ADDED
@@ -0,0 +1 @@
 
 
1
+ []
db.py ADDED
@@ -0,0 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ File-System Database Layer
3
+
4
+ Replaces MySQL with JSON file storage for students, rooms, and allocations.
5
+ Mirrors the original MySQL schema:
6
+ - main table → data/students.json
7
+ - RoomNum → data/rooms.json
8
+ - results → data/allocations.json
9
+ """
10
+
11
+ import json
12
+ import os
13
+ from typing import List, Optional
14
+
15
+ # ─── Paths ────────────────────────────────────────────────────────────────────
16
+
17
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
18
+ DATA_DIR = os.path.join(BASE_DIR, "data")
19
+
20
+ STUDENTS_FILE = os.path.join(DATA_DIR, "students.json")
21
+ ROOMS_FILE = os.path.join(DATA_DIR, "rooms.json")
22
+ ALLOCATIONS_FILE = os.path.join(DATA_DIR, "allocations.json")
23
+
24
+
25
+ def _ensure_data_dir():
26
+ """Create data directory if it doesn't exist."""
27
+ os.makedirs(DATA_DIR, exist_ok=True)
28
+
29
+
30
+ def _read_json(filepath: str) -> list:
31
+ """Read a JSON file and return the parsed list."""
32
+ _ensure_data_dir()
33
+ if not os.path.exists(filepath):
34
+ with open(filepath, "w") as f:
35
+ json.dump([], f)
36
+ return []
37
+ try:
38
+ with open(filepath, "r") as f:
39
+ data = json.load(f)
40
+ return data if isinstance(data, list) else []
41
+ except (json.JSONDecodeError, IOError):
42
+ return []
43
+
44
+
45
+ def _write_json(filepath: str, data: list):
46
+ """Write a list to a JSON file."""
47
+ _ensure_data_dir()
48
+ with open(filepath, "w") as f:
49
+ json.dump(data, f, indent=2)
50
+
51
+
52
+ # ─── Student Operations ──────────────────────────────────────────────────────
53
+
54
+
55
+ def get_all_students() -> List[dict]:
56
+ """Get all students from the database."""
57
+ return _read_json(STUDENTS_FILE)
58
+
59
+
60
+ def get_student_by_id(student_id: int) -> Optional[dict]:
61
+ """Get a specific student by ID."""
62
+ students = get_all_students()
63
+ for s in students:
64
+ if s["id"] == student_id:
65
+ return s
66
+ return None
67
+
68
+
69
+ def add_student(student: dict) -> bool:
70
+ """
71
+ Add a new student.
72
+
73
+ student dict should contain:
74
+ - id: int
75
+ - name: str
76
+ - cgpa: float
77
+ - pref_roommate: List[int] (ordered list of preferred roommate IDs)
78
+ - pref_room: List[int] (ordered list of preferred room IDs)
79
+ """
80
+ students = get_all_students()
81
+ # Check for duplicate ID
82
+ for s in students:
83
+ if s["id"] == student["id"]:
84
+ return False
85
+ students.append(student)
86
+ _write_json(STUDENTS_FILE, students)
87
+ return True
88
+
89
+
90
+ def update_student(student_id: int, updated_data: dict) -> bool:
91
+ """Update an existing student's data."""
92
+ students = get_all_students()
93
+ for i, s in enumerate(students):
94
+ if s["id"] == student_id:
95
+ students[i].update(updated_data)
96
+ _write_json(STUDENTS_FILE, students)
97
+ return True
98
+ return False
99
+
100
+
101
+ def delete_student(student_id: int) -> bool:
102
+ """Delete a student by ID."""
103
+ students = get_all_students()
104
+ new_students = [s for s in students if s["id"] != student_id]
105
+ if len(new_students) == len(students):
106
+ return False
107
+ _write_json(STUDENTS_FILE, new_students)
108
+ return True
109
+
110
+
111
+ def clear_all_students():
112
+ """Remove all students."""
113
+ _write_json(STUDENTS_FILE, [])
114
+
115
+
116
+ def save_all_students(students: List[dict]):
117
+ """Overwrite the entire students database."""
118
+ _write_json(STUDENTS_FILE, students)
119
+
120
+
121
+ def get_student_count() -> int:
122
+ """Get the total number of students."""
123
+ return len(get_all_students())
124
+
125
+
126
+ # ─── Room Operations ─────────────────────────────────────────────────────────
127
+
128
+
129
+ def get_all_rooms() -> List[dict]:
130
+ """Get all rooms from the database."""
131
+ return _read_json(ROOMS_FILE)
132
+
133
+
134
+ def add_room(room: dict) -> bool:
135
+ """
136
+ Add a new room.
137
+
138
+ room dict should contain:
139
+ - room_id: int
140
+ - room_number: str (e.g., 'A101')
141
+ """
142
+ rooms = get_all_rooms()
143
+ for r in rooms:
144
+ if r["room_id"] == room["room_id"]:
145
+ return False
146
+ rooms.append(room)
147
+ _write_json(ROOMS_FILE, rooms)
148
+ return True
149
+
150
+
151
+ def delete_room(room_id: int) -> bool:
152
+ """Delete a room by ID."""
153
+ rooms = get_all_rooms()
154
+ new_rooms = [r for r in rooms if r["room_id"] != room_id]
155
+ if len(new_rooms) == len(rooms):
156
+ return False
157
+ _write_json(ROOMS_FILE, new_rooms)
158
+ return True
159
+
160
+
161
+ def clear_all_rooms():
162
+ """Remove all rooms."""
163
+ _write_json(ROOMS_FILE, [])
164
+
165
+
166
+ def save_all_rooms(rooms: List[dict]):
167
+ """Overwrite the entire rooms database."""
168
+ _write_json(ROOMS_FILE, rooms)
169
+
170
+
171
+ def get_room_count() -> int:
172
+ """Get the total number of rooms."""
173
+ return len(get_all_rooms())
174
+
175
+
176
+ # ─── Allocation Operations ───────────────────────────────────────────────────
177
+
178
+
179
+ def get_all_allocations() -> List[dict]:
180
+ """Get all allocation results."""
181
+ return _read_json(ALLOCATIONS_FILE)
182
+
183
+
184
+ def save_allocations(allocations: List[dict]):
185
+ """Save allocation results (overwrites previous results)."""
186
+ _write_json(ALLOCATIONS_FILE, allocations)
187
+
188
+
189
+ def clear_allocations():
190
+ """Clear all allocation results."""
191
+ _write_json(ALLOCATIONS_FILE, [])
192
+
193
+
194
+ # ─── Bulk Import from CSV ────────────────────────────────────────────────────
195
+
196
+
197
+ def import_students_from_csv(csv_content: str) -> tuple:
198
+ """
199
+ Import students from CSV content.
200
+
201
+ Expected columns: id, name, cgpa, pref_roommate, pref_room
202
+ pref_roommate and pref_room should be space-separated integers.
203
+
204
+ Returns:
205
+ (success_count, error_messages)
206
+ """
207
+ import csv
208
+ import io
209
+
210
+ reader = csv.DictReader(io.StringIO(csv_content))
211
+ students = []
212
+ errors = []
213
+
214
+ for row_num, row in enumerate(reader, start=2):
215
+ try:
216
+ student = {
217
+ "id": int(row["id"].strip()),
218
+ "name": row["name"].strip(),
219
+ "cgpa": float(row["cgpa"].strip()),
220
+ "pref_roommate": [
221
+ int(x) for x in row["pref_roommate"].strip().split()
222
+ ],
223
+ "pref_room": [
224
+ int(x) for x in row["pref_room"].strip().split()
225
+ ],
226
+ }
227
+ students.append(student)
228
+ except (KeyError, ValueError) as e:
229
+ errors.append(f"Row {row_num}: {str(e)}")
230
+
231
+ if students:
232
+ save_all_students(students)
233
+
234
+ return len(students), errors
235
+
236
+
237
+ def import_rooms_from_csv(csv_content: str) -> tuple:
238
+ """
239
+ Import rooms from CSV content.
240
+
241
+ Expected columns: room_id, room_number
242
+
243
+ Returns:
244
+ (success_count, error_messages)
245
+ """
246
+ import csv
247
+ import io
248
+
249
+ reader = csv.DictReader(io.StringIO(csv_content))
250
+ rooms = []
251
+ errors = []
252
+
253
+ for row_num, row in enumerate(reader, start=2):
254
+ try:
255
+ room = {
256
+ "room_id": int(row["room_id"].strip()),
257
+ "room_number": row["room_number"].strip(),
258
+ }
259
+ rooms.append(room)
260
+ except (KeyError, ValueError) as e:
261
+ errors.append(f"Row {row_num}: {str(e)}")
262
+
263
+ if rooms:
264
+ save_all_rooms(rooms)
265
+
266
+ return len(rooms), errors
gale_shapley.py ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Gale-Shapley Algorithm Implementation
3
+
4
+ Python port of the C implementation from the original project.
5
+ Implements the Stable Marriage Problem for roommate matching and room allocation.
6
+
7
+ Original C version: https://github.com/Harshwardhan-Deshmukh03/Roommate-allocation-using-Gale-Shapley-Algorithm.git
8
+ """
9
+
10
+ from typing import Dict, List, Tuple, Optional
11
+
12
+
13
+ def gale_shapley(pref_matrix: List[List[int]], n: int) -> Dict[int, int]:
14
+ """
15
+ Run the Gale-Shapley algorithm for stable matching.
16
+
17
+ This is a direct Python translation of the C galeShapley() function.
18
+ Students 0..n-1 are "proposers" and students n..2n-1 are "acceptors".
19
+
20
+ Args:
21
+ pref_matrix: 2D preference list. pref_matrix[i] is the ordered preference
22
+ list for person i. For proposers (0..n-1), preferences are
23
+ indices in the acceptor range (n..2n-1). For acceptors,
24
+ preferences are proposer indices (0..n-1).
25
+ n: Number of pairs (half the total participants).
26
+
27
+ Returns:
28
+ Dictionary mapping each person to their matched partner.
29
+ """
30
+ # match[i] = partner of person i, -1 means unmatched
31
+ match = [-1] * (2 * n)
32
+ free_count = n
33
+
34
+ # Track which preference index each proposer is up to
35
+ next_proposal = [0] * n
36
+
37
+ while free_count > 0:
38
+ # Find the first free proposer
39
+ m = -1
40
+ for i in range(n):
41
+ if match[i] == -1:
42
+ m = i
43
+ break
44
+
45
+ if m == -1:
46
+ break
47
+
48
+ # Proposer m proposes to their next preferred acceptor
49
+ while next_proposal[m] < n:
50
+ w = pref_matrix[m][next_proposal[m]]
51
+ next_proposal[m] += 1
52
+
53
+ if match[w] == -1:
54
+ # w is free, accept the proposal
55
+ match[w] = m
56
+ match[m] = w
57
+ free_count -= 1
58
+ break
59
+ else:
60
+ # w is already matched, check if w prefers m over current partner
61
+ m1 = match[w]
62
+ if _prefers_new_over_current(pref_matrix, w, m, m1):
63
+ # w prefers m over m1 — switch
64
+ match[w] = m
65
+ match[m] = w
66
+ match[m1] = -1 # m1 becomes free
67
+ break
68
+ # else w rejects m, m tries next preference
69
+
70
+ return match
71
+
72
+
73
+ def _prefers_new_over_current(
74
+ pref_matrix: List[List[int]], w: int, m_new: int, m_current: int
75
+ ) -> bool:
76
+ """
77
+ Check if acceptor w prefers m_new over m_current.
78
+
79
+ Mirrors the C function wPrefersm1Overm() but with clearer naming.
80
+ Returns True if w prefers m_new over m_current.
81
+ """
82
+ for pref in pref_matrix[w]:
83
+ if pref == m_new:
84
+ return True
85
+ if pref == m_current:
86
+ return False
87
+ return False
88
+
89
+
90
+ def run_roommate_matching(students: List[dict]) -> List[Tuple[int, int]]:
91
+ """
92
+ Stage 1: Match students into roommate pairs using Gale-Shapley.
93
+
94
+ Args:
95
+ students: List of student dicts with 'id', 'name', 'cgpa', 'pref_roommate'.
96
+
97
+ Returns:
98
+ List of (student_id_1, student_id_2) roommate pairs.
99
+ """
100
+ n_students = len(students)
101
+ if n_students < 2 or n_students % 2 != 0:
102
+ raise ValueError(
103
+ f"Need an even number of students (≥ 2). Got {n_students}."
104
+ )
105
+
106
+ n = n_students // 2 # Number of pairs
107
+
108
+ # Build preference matrix: proposers are 0..n-1, acceptors are n..2n-1
109
+ # Each student's pref_roommate contains IDs of other students they prefer
110
+ pref_matrix = []
111
+ for student in students:
112
+ prefs = student["pref_roommate"]
113
+ # Filter out only valid preferences (should be IDs of other students)
114
+ # For proposers (0..n-1): their prefs should reference acceptors (n..2n-1)
115
+ # For acceptors (n..2n-1): their prefs should reference proposers (0..n-1)
116
+ sid = student["id"]
117
+ if sid < n:
118
+ # Proposer: filter prefs to only include acceptor IDs (n..2n-1)
119
+ filtered = [p for p in prefs if n <= p < 2 * n]
120
+ else:
121
+ # Acceptor: filter prefs to only include proposer IDs (0..n-1)
122
+ filtered = [p for p in prefs if 0 <= p < n]
123
+ pref_matrix.append(filtered)
124
+
125
+ # Run Gale-Shapley
126
+ match = gale_shapley(pref_matrix, n)
127
+
128
+ # Extract unique pairs (only from proposer side to avoid duplicates)
129
+ pairs = []
130
+ seen = set()
131
+ for i in range(n):
132
+ partner = match[i]
133
+ if partner != -1 and i not in seen and partner not in seen:
134
+ pairs.append((i, partner))
135
+ seen.add(i)
136
+ seen.add(partner)
137
+
138
+ return pairs
139
+
140
+
141
+ def run_room_allocation(
142
+ students: List[dict],
143
+ roommate_pairs: List[Tuple[int, int]],
144
+ rooms: List[dict],
145
+ ) -> List[dict]:
146
+ """
147
+ Stage 2: Allocate rooms to roommate pairs based on CGPA ranking.
148
+
149
+ Higher CGPA pairs get priority in room selection (Gale-Shapley on rooms).
150
+
151
+ Args:
152
+ students: List of student dicts with 'id', 'name', 'cgpa', 'pref_room'.
153
+ roommate_pairs: List of (id1, id2) roommate pairs from Stage 1.
154
+ rooms: List of room dicts with 'room_id' and 'room_number'.
155
+
156
+ Returns:
157
+ List of allocation dicts: {roommate1, roommate2, room_number, room_id, pair_cgpa}
158
+ """
159
+ n = len(roommate_pairs)
160
+ n_rooms = len(rooms)
161
+
162
+ if n_rooms < n:
163
+ raise ValueError(
164
+ f"Not enough rooms ({n_rooms}) for {n} pairs."
165
+ )
166
+
167
+ # Build student lookup
168
+ student_map = {s["id"]: s for s in students}
169
+
170
+ # Rank pairs by the higher CGPA in each pair (descending)
171
+ pair_cgpas = []
172
+ for id1, id2 in roommate_pairs:
173
+ cgpa1 = student_map[id1]["cgpa"]
174
+ cgpa2 = student_map[id2]["cgpa"]
175
+ max_cgpa = max(cgpa1, cgpa2)
176
+ pair_cgpas.append((max_cgpa, id1, id2))
177
+
178
+ # Sort descending by max CGPA
179
+ pair_cgpas.sort(key=lambda x: x[0], reverse=True)
180
+
181
+ # Build room preference matrix for Gale-Shapley
182
+ # Proposers: ranked pairs (0..n-1) → these map to pair_cgpas indices
183
+ # Acceptors: rooms (n..2n-1) → these map to rooms indices (offset by n)
184
+ room_id_to_index = {rooms[j]["room_id"]: j + n for j in range(n)}
185
+
186
+ pref_matrix = [[] for _ in range(2 * n)]
187
+
188
+ # For each ranked pair, get the higher-CGPA student's room preferences
189
+ for rank_idx, (max_cgpa, id1, id2) in enumerate(pair_cgpas):
190
+ # Use the higher CGPA student's room preferences
191
+ if student_map[id1]["cgpa"] >= student_map[id2]["cgpa"]:
192
+ prefs = student_map[id1].get("pref_room", [])
193
+ else:
194
+ prefs = student_map[id2].get("pref_room", [])
195
+
196
+ # Map room IDs to room indices (offset by n for acceptor range)
197
+ mapped_prefs = []
198
+ for room_id in prefs:
199
+ if room_id in room_id_to_index:
200
+ mapped_prefs.append(room_id_to_index[room_id])
201
+ pref_matrix[rank_idx] = mapped_prefs
202
+
203
+ # Rooms accept any student in order (no real preference)
204
+ for j in range(n):
205
+ pref_matrix[n + j] = list(range(n))
206
+
207
+ # Run Gale-Shapley for room allocation
208
+ match = gale_shapley(pref_matrix, n)
209
+
210
+ # Build index-to-room mapping
211
+ index_to_room = {j + n: rooms[j] for j in range(n)}
212
+
213
+ # Build final allocation
214
+ allocations = []
215
+ for rank_idx, (max_cgpa, id1, id2) in enumerate(pair_cgpas):
216
+ room_index = match[rank_idx]
217
+ room = index_to_room.get(room_index, {"room_number": "N/A", "room_id": -1})
218
+
219
+ allocations.append({
220
+ "roommate1_id": id1,
221
+ "roommate1_name": student_map[id1]["name"],
222
+ "roommate1_cgpa": student_map[id1]["cgpa"],
223
+ "roommate2_id": id2,
224
+ "roommate2_name": student_map[id2]["name"],
225
+ "roommate2_cgpa": student_map[id2]["cgpa"],
226
+ "room_number": room["room_number"],
227
+ "room_id": room["room_id"],
228
+ "pair_max_cgpa": max_cgpa,
229
+ })
230
+
231
+ return allocations
232
+
233
+
234
+ def run_full_allocation(students: List[dict], rooms: List[dict]) -> List[dict]:
235
+ """
236
+ Run the complete two-stage allocation pipeline.
237
+
238
+ Stage 1: Gale-Shapley for roommate matching.
239
+ Stage 2: CGPA-ranked Gale-Shapley for room allocation.
240
+
241
+ Args:
242
+ students: List of student dicts.
243
+ rooms: List of room dicts.
244
+
245
+ Returns:
246
+ List of allocation result dicts.
247
+ """
248
+ # Stage 1: Roommate matching
249
+ roommate_pairs = run_roommate_matching(students)
250
+
251
+ # Stage 2: Room allocation
252
+ allocations = run_room_allocation(students, roommate_pairs, rooms)
253
+
254
+ return allocations
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ streamlit>=1.30.0
2
+ pandas>=2.0.0
3
+ plotly>=5.18.0
sample_csv/sample_rooms_13.csv ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ room_id,room_number
2
+ 0,A101
3
+ 1,A102
4
+ 2,A103
5
+ 3,A104
6
+ 4,A105
7
+ 5,B201
8
+ 6,B202
9
+ 7,B203
10
+ 8,B204
11
+ 9,B205
12
+ 10,C301
13
+ 11,C302
14
+ 12,C303
sample_csv/sample_rooms_5.csv ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ room_id,room_number
2
+ 0,A101
3
+ 1,A102
4
+ 2,A103
5
+ 3,B201
6
+ 4,B202
sample_csv/sample_students_10.csv ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ id,name,cgpa,pref_roommate,pref_room
2
+ 0,Alice,9.2,5 6 7 8 9 1 2 3 4,0 1 2 3 4
3
+ 1,Bob,8.9,5 6 7 8 9 0 2 3 4,1 0 2 3 4
4
+ 2,Charlie,9.5,5 6 7 8 9 3 1 0 4,2 1 0 3 4
5
+ 3,Diana,8.7,5 6 7 8 9 2 0 1 4,3 2 1 0 4
6
+ 4,Eve,9.0,5 6 7 8 9 0 1 2 3,4 3 2 1 0
7
+ 5,Frank,8.8,0 1 2 3 4 6 7 8 9,0 1 2 3 4
8
+ 6,Grace,9.3,0 1 2 3 4 7 5 8 9,1 0 2 3 4
9
+ 7,Henry,8.6,0 1 2 3 4 6 5 8 9,2 1 0 3 4
10
+ 8,Ivy,9.1,0 1 2 3 4 9 5 6 7,3 2 1 0 4
11
+ 9,Jack,8.5,0 1 2 3 4 8 5 6 7,4 3 2 1 0
sample_csv/sample_students_26.csv ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ id,name,cgpa,pref_roommate,pref_room
2
+ 0,Aarav Sharma,9.4,13 14 15 16 17 18 19 20 21 22 23 24 25 1 2 3 4 5 6 7 8 9 10 11 12,0 1 2 3 4 5 6 7 8 9 10 11 12
3
+ 1,Priya Patel,8.8,13 14 15 16 17 18 19 20 21 22 23 24 25 0 2 3 4 5 6 7 8 9 10 11 12,1 0 2 3 4 5 6 7 8 9 10 11 12
4
+ 2,Rohan Mehta,9.1,13 14 15 16 17 18 19 20 21 22 23 24 25 3 0 1 4 5 6 7 8 9 10 11 12,2 1 0 3 4 5 6 7 8 9 10 11 12
5
+ 3,Sneha Kulkarni,8.5,13 14 15 16 17 18 19 20 21 22 23 24 25 2 0 1 4 5 6 7 8 9 10 11 12,3 2 1 0 4 5 6 7 8 9 10 11 12
6
+ 4,Vikram Singh,9.0,13 14 15 16 17 18 19 20 21 22 23 24 25 5 6 7 0 1 2 3 8 9 10 11 12,4 5 6 7 0 1 2 3 8 9 10 11 12
7
+ 5,Ananya Desai,8.7,13 14 15 16 17 18 19 20 21 22 23 24 25 4 6 7 0 1 2 3 8 9 10 11 12,5 4 6 7 0 1 2 3 8 9 10 11 12
8
+ 6,Karan Joshi,9.3,13 14 15 16 17 18 19 20 21 22 23 24 25 7 5 4 0 1 2 3 8 9 10 11 12,6 5 4 7 0 1 2 3 8 9 10 11 12
9
+ 7,Meera Nair,8.9,13 14 15 16 17 18 19 20 21 22 23 24 25 6 5 4 0 1 2 3 8 9 10 11 12,7 6 5 4 0 1 2 3 8 9 10 11 12
10
+ 8,Arjun Reddy,9.2,13 14 15 16 17 18 19 20 21 22 23 24 25 9 10 11 0 1 2 3 4 5 6 7 12,8 9 10 11 0 1 2 3 4 5 6 7 12
11
+ 9,Divya Iyer,8.6,13 14 15 16 17 18 19 20 21 22 23 24 25 8 10 11 0 1 2 3 4 5 6 7 12,9 8 10 11 0 1 2 3 4 5 6 7 12
12
+ 10,Nikhil Gupta,9.5,13 14 15 16 17 18 19 20 21 22 23 24 25 11 8 9 0 1 2 3 4 5 6 7 12,10 11 8 9 0 1 2 3 4 5 6 7 12
13
+ 11,Ritu Agarwal,8.4,13 14 15 16 17 18 19 20 21 22 23 24 25 10 8 9 0 1 2 3 4 5 6 7 12,11 10 8 9 0 1 2 3 4 5 6 7 12
14
+ 12,Siddharth Rao,8.3,13 14 15 16 17 18 19 20 21 22 23 24 25 0 1 2 3 4 5 6 7 8 9 10 11,12 0 1 2 3 4 5 6 7 8 9 10 11
15
+ 13,Pooja Verma,9.0,0 1 2 3 4 5 6 7 8 9 10 11 12 14 15 16 17 18 19 20 21 22 23 24 25,0 1 2 3 4 5 6 7 8 9 10 11 12
16
+ 14,Harsh Deshmukh,8.7,0 1 2 3 4 5 6 7 8 9 10 11 12 13 15 16 17 18 19 20 21 22 23 24 25,1 0 2 3 4 5 6 7 8 9 10 11 12
17
+ 15,Kavya Menon,9.2,0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 16 17 18 19 20 21 22 23 24 25,2 1 0 3 4 5 6 7 8 9 10 11 12
18
+ 16,Aditya Bhatt,8.5,0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 17 18 19 20 21 22 23 24 25,3 2 1 0 4 5 6 7 8 9 10 11 12
19
+ 17,Tanvi Shah,8.8,0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 18 19 20 21 22 23 24 25,4 3 2 1 0 5 6 7 8 9 10 11 12
20
+ 18,Parth Mane,9.1,0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 19 20 21 22 23 24 25,5 4 3 2 1 0 6 7 8 9 10 11 12
21
+ 19,Ishita Roy,8.6,0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 20 21 22 23 24 25,6 5 4 3 2 1 0 7 8 9 10 11 12
22
+ 20,Jia Johnson,9.4,0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 21 22 23 24 25,7 6 5 4 3 2 1 0 8 9 10 11 12
23
+ 21,Rahul Kapoor,8.3,0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 22 23 24 25,8 7 6 5 4 3 2 1 0 9 10 11 12
24
+ 22,Neha Saxena,8.9,0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 23 24 25,9 8 7 6 5 4 3 2 1 0 10 11 12
25
+ 23,Yash Tiwari,8.2,0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 24 25,10 9 8 7 6 5 4 3 2 1 0 11 12
26
+ 24,Shweta Mishra,9.3,0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 25,11 10 9 8 7 6 5 4 3 2 1 0 12
27
+ 25,Amit Pandey,8.0,0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24,12 11 10 9 8 7 6 5 4 3 2 1 0