"""
Roommate Allocation System — Streamlit UI
Uses the Gale-Shapley algorithm (Python implementation).
For the original C + MySQL version, see:
https://github.com/Harshwardhan-Deshmukh03/Roommate-allocation-using-Gale-Shapley-Algorithm.git
"""
import streamlit as st
import pandas as pd
import os, json
from db import (
get_all_students, get_all_rooms, get_all_allocations,
save_all_students, save_all_rooms, save_allocations,
clear_all_students, clear_all_rooms, clear_allocations,
add_student, delete_student, add_room, delete_room,
import_students_from_csv, import_rooms_from_csv,
get_student_count, get_room_count,
)
from gale_shapley import run_full_allocation
# ── Page Config ───────────────────────────────────────────────────────────────
st.set_page_config(
page_title="Roommate Allocation · Gale-Shapley",
page_icon="🏠",
layout="wide",
initial_sidebar_state="expanded",
)
# ── Custom CSS ────────────────────────────────────────────────────────────────
st.markdown("""
""", unsafe_allow_html=True)
# ── Sidebar ───────────────────────────────────────────────────────────────────
with st.sidebar:
st.markdown("## 🧭 Navigation")
page = st.radio(
"Go to",
["🏠 Dashboard", "👥 Manage Students", "🚪 Manage Rooms",
"📂 CSV Import", "⚙️ Run Allocation", "📊 Results"],
label_visibility="collapsed",
)
st.markdown("---")
st.markdown(
'
'
'
🐍 Python + Streamlit Edition'
'File-system storage (no MySQL needed).
'
'For the
C + MySQL version:
'
'
View on GitHub ↗ ',
unsafe_allow_html=True,
)
# ── Helper ────────────────────────────────────────────────────────────────────
def stat_cards(items):
cols = st.columns(len(items))
for col, (num, lbl) in zip(cols, items):
col.markdown(
f'',
unsafe_allow_html=True,
)
# ══════════════════════════════════════════════════════════════════════════════
# PAGES
# ══════════════════════════════════════════════════════════════════════════════
# ── Dashboard ─────────────────────────────────────────────────────────────────
if page == "🏠 Dashboard":
st.markdown(
'🏠 Roommate Allocation System
'
'
Gale-Shapley Stable-Matching Algorithm · Python & Streamlit
',
unsafe_allow_html=True,
)
n_stu = get_student_count()
n_rm = get_room_count()
allocs = get_all_allocations()
stat_cards([
(n_stu, "Students"), (n_rm, "Rooms"),
(n_stu // 2 if n_stu else 0, "Possible Pairs"),
(len(allocs), "Allocations"),
])
st.markdown('How It Works
', unsafe_allow_html=True)
c1, c2 = st.columns(2)
with c1:
st.markdown("""
**Stage 1 — Roommate Matching**
1. Each student submits an ordered preference list of roommates.
2. The Gale-Shapley algorithm pairs students into **stable matches**
(no two students would rather swap partners).
""")
with c2:
st.markdown("""
**Stage 2 — Room Allocation**
1. Pairs are ranked by the **higher CGPA** in each pair.
2. Ranked pairs select rooms via Gale-Shapley, so top performers
get priority for their preferred rooms.
""")
st.markdown('Quick Start
', unsafe_allow_html=True)
st.markdown("""
1. **Add Students** — manually or via CSV upload.
2. **Add Rooms** — manually or via CSV upload.
3. **Run Allocation** — click one button to get stable assignments.
4. **View Results** — see the final roommate + room table & charts.
""")
st.markdown(
'',
unsafe_allow_html=True,
)
# ── Manage Students ──────────────────────────────────────────────────────────
elif page == "👥 Manage Students":
st.markdown('👥 Manage Students
', unsafe_allow_html=True)
students = get_all_students()
stat_cards([(len(students), "Total Students")])
# Show current students
if students:
df = pd.DataFrame(students)
df["pref_roommate"] = df["pref_roommate"].apply(lambda x: " ".join(map(str, x)))
df["pref_room"] = df["pref_room"].apply(lambda x: " ".join(map(str, x)))
st.dataframe(df, use_container_width=True, hide_index=True)
else:
st.info("No students yet. Add below or import via CSV.")
st.markdown("---")
st.markdown("#### ➕ Add a Student")
with st.form("add_student", clear_on_submit=True):
ac1, ac2, ac3 = st.columns(3)
sid = ac1.number_input("Student ID", min_value=0, step=1)
name = ac2.text_input("Name")
cgpa = ac3.number_input("CGPA", min_value=0.0, max_value=10.0, step=0.1)
pref_r = st.text_input("Roommate Preferences (space-separated IDs)", placeholder="5 6 7 8 9 ...")
pref_rm = st.text_input("Room Preferences (space-separated room IDs)", placeholder="0 1 2 3 4 ...")
submitted = st.form_submit_button("Add Student", type="primary")
if submitted:
if not name.strip():
st.error("Name cannot be empty.")
else:
try:
pr = [int(x) for x in pref_r.strip().split()] if pref_r.strip() else []
pm = [int(x) for x in pref_rm.strip().split()] if pref_rm.strip() else []
ok = add_student({"id": int(sid), "name": name.strip(), "cgpa": float(cgpa),
"pref_roommate": pr, "pref_room": pm})
if ok:
st.success(f"✅ Added **{name}** (ID {sid})")
st.rerun()
else:
st.error(f"Student ID {sid} already exists.")
except ValueError:
st.error("Preferences must be space-separated integers.")
# Delete
if students:
st.markdown("#### 🗑️ Remove a Student")
dc1, dc2 = st.columns([3, 1])
del_id = dc1.selectbox("Select student to remove",
[(s["id"], s["name"]) for s in students],
format_func=lambda x: f"ID {x[0]} — {x[1]}")
if dc2.button("Delete", type="secondary"):
delete_student(del_id[0])
st.success(f"Removed student ID {del_id[0]}")
st.rerun()
if st.button("🗑️ Clear ALL Students", type="secondary"):
clear_all_students()
st.warning("All students cleared.")
st.rerun()
# ── Manage Rooms ──────────────────────────────────────────────────────────────
elif page == "🚪 Manage Rooms":
st.markdown('🚪 Manage Rooms
', unsafe_allow_html=True)
rooms = get_all_rooms()
stat_cards([(len(rooms), "Total Rooms")])
if rooms:
st.dataframe(pd.DataFrame(rooms), use_container_width=True, hide_index=True)
else:
st.info("No rooms yet. Add below or import via CSV.")
st.markdown("---")
st.markdown("#### ➕ Add a Room")
with st.form("add_room", clear_on_submit=True):
rc1, rc2 = st.columns(2)
rid = rc1.number_input("Room ID", min_value=0, step=1)
rnum = rc2.text_input("Room Number", placeholder="e.g. A101")
if st.form_submit_button("Add Room", type="primary"):
if not rnum.strip():
st.error("Room number cannot be empty.")
else:
ok = add_room({"room_id": int(rid), "room_number": rnum.strip()})
if ok:
st.success(f"✅ Added room **{rnum}** (ID {rid})")
st.rerun()
else:
st.error(f"Room ID {rid} already exists.")
if rooms:
st.markdown("#### 🗑️ Remove a Room")
drc1, drc2 = st.columns([3, 1])
del_rid = drc1.selectbox("Select room to remove",
[(r["room_id"], r["room_number"]) for r in rooms],
format_func=lambda x: f"ID {x[0]} — {x[1]}")
if drc2.button("Delete", type="secondary"):
delete_room(del_rid[0])
st.success(f"Removed room ID {del_rid[0]}")
st.rerun()
if st.button("🗑️ Clear ALL Rooms", type="secondary"):
clear_all_rooms()
st.warning("All rooms cleared.")
st.rerun()
# ── CSV Import ────────────────────────────────────────────────────────────────
elif page == "📂 CSV Import":
st.markdown('📂 CSV Import
', unsafe_allow_html=True)
st.markdown(
'Upload CSV files to bulk-import students and rooms. '
'This is useful when manual entry is tedious or when the allocation is complex.
',
unsafe_allow_html=True,
)
tab1, tab2, tab3 = st.tabs(["📥 Import Students", "📥 Import Rooms", "📄 Sample CSVs"])
with tab1:
st.markdown("**Expected columns:** `id, name, cgpa, pref_roommate, pref_room`")
st.caption("Preferences are space-separated integer IDs.")
f = st.file_uploader("Upload Students CSV", type=["csv"], key="stu_csv")
if f:
content = f.getvalue().decode("utf-8")
st.markdown("**Preview:**")
st.dataframe(pd.read_csv(f), use_container_width=True, hide_index=True)
f.seek(0)
if st.button("✅ Import Students", type="primary"):
cnt, errs = import_students_from_csv(content)
if errs:
for e in errs:
st.error(e)
st.success(f"Imported **{cnt}** students.")
st.rerun()
with tab2:
st.markdown("**Expected columns:** `room_id, room_number`")
f2 = st.file_uploader("Upload Rooms CSV", type=["csv"], key="room_csv")
if f2:
content2 = f2.getvalue().decode("utf-8")
st.markdown("**Preview:**")
st.dataframe(pd.read_csv(f2), use_container_width=True, hide_index=True)
f2.seek(0)
if st.button("✅ Import Rooms", type="primary"):
cnt2, errs2 = import_rooms_from_csv(content2)
if errs2:
for e in errs2:
st.error(e)
st.success(f"Imported **{cnt2}** rooms.")
st.rerun()
with tab3:
st.markdown("#### Sample CSV Files")
st.markdown("Download these to understand the expected format, then modify and re-upload.")
sample_dir = os.path.join(os.path.dirname(__file__), "sample_csv")
for fname in sorted(os.listdir(sample_dir)):
fpath = os.path.join(sample_dir, fname)
with open(fpath, "r") as sf:
st.download_button(f"⬇️ {fname}", sf.read(), file_name=fname, mime="text/csv")
with open(fpath, "r") as sf:
st.markdown(f"**`{fname}` preview:**")
st.code(sf.read(), language="csv")
# ── Run Allocation ────────────────────────────────────────────────────────────
elif page == "⚙️ Run Allocation":
st.markdown('⚙️ Run Allocation
', unsafe_allow_html=True)
students = get_all_students()
rooms = get_all_rooms()
n_stu = len(students)
n_rm = len(rooms)
stat_cards([(n_stu, "Students"), (n_rm, "Rooms"), (n_stu // 2, "Pairs Needed")])
# Validation
issues = []
if n_stu < 2:
issues.append("Need at least 2 students.")
if n_stu % 2 != 0:
issues.append("Number of students must be even.")
if n_rm < n_stu // 2:
issues.append(f"Need at least {n_stu // 2} rooms (have {n_rm}).")
if issues:
for iss in issues:
st.error(f"❌ {iss}")
st.info("Fix the above issues before running the algorithm.")
else:
st.success("✅ All checks passed — ready to allocate!")
st.markdown(
''
'Stage 1: Gale-Shapley matches students into roommate pairs.
'
'Stage 2: Pairs ranked by CGPA select rooms via Gale-Shapley.
',
unsafe_allow_html=True,
)
if st.button("🚀 Run Gale-Shapley Allocation", type="primary", use_container_width=True):
with st.spinner("Running Gale-Shapley algorithm..."):
try:
allocs = run_full_allocation(students, rooms)
save_allocations(allocs)
st.success(f"🎉 Allocation complete — **{len(allocs)} pairs** assigned!")
st.balloons()
df = pd.DataFrame(allocs)
display_cols = ["roommate1_name", "roommate1_cgpa",
"roommate2_name", "roommate2_cgpa",
"room_number", "pair_max_cgpa"]
st.dataframe(df[display_cols], use_container_width=True, hide_index=True)
except Exception as e:
st.error(f"Allocation failed: {e}")
st.info("Try importing data via CSV if manual entry is causing issues.")
# ── Results ───────────────────────────────────────────────────────────────────
elif page == "📊 Results":
st.markdown('📊 Allocation Results
', unsafe_allow_html=True)
allocs = get_all_allocations()
if not allocs:
st.info("No allocation results yet. Go to **⚙️ Run Allocation** first.")
else:
df = pd.DataFrame(allocs)
stat_cards([
(len(df), "Pairs Allocated"),
(f"{df['pair_max_cgpa'].mean():.2f}", "Avg Pair CGPA"),
(df["room_number"].nunique(), "Rooms Used"),
])
# Main table
st.markdown("#### 📋 Final Allocation Table")
display = df[["roommate1_name", "roommate1_cgpa",
"roommate2_name", "roommate2_cgpa",
"room_number", "pair_max_cgpa"]].copy()
display.columns = ["Roommate 1", "CGPA 1", "Roommate 2", "CGPA 2", "Room", "Pair CGPA"]
st.dataframe(display, use_container_width=True, hide_index=True)
# Download
csv_out = display.to_csv(index=False)
st.download_button("⬇️ Download Results CSV", csv_out,
file_name="allocation_results.csv", mime="text/csv")
# Charts
st.markdown("#### 📈 CGPA Distribution")
import plotly.express as px
fig = px.bar(
display, x="Room", y="Pair CGPA",
color="Pair CGPA",
color_continuous_scale=["#6C63FF", "#00D2FF"],
title="Pair CGPA by Room Assignment",
)
fig.update_layout(
plot_bgcolor="rgba(0,0,0,0)", paper_bgcolor="rgba(0,0,0,0)",
font_color="#E0E0E0", title_font_size=16,
)
st.plotly_chart(fig, use_container_width=True)
# Roommate comparison
st.markdown("#### 🤝 Roommate CGPA Comparison")
comp = pd.DataFrame({
"Room": display["Room"],
"Roommate 1": display["CGPA 1"],
"Roommate 2": display["CGPA 2"],
})
fig2 = px.bar(
comp.melt(id_vars="Room", var_name="Roommate", value_name="CGPA"),
x="Room", y="CGPA", color="Roommate", barmode="group",
color_discrete_sequence=["#6C63FF", "#00D2FF"],
title="CGPA Comparison per Room",
)
fig2.update_layout(
plot_bgcolor="rgba(0,0,0,0)", paper_bgcolor="rgba(0,0,0,0)",
font_color="#E0E0E0", title_font_size=16,
)
st.plotly_chart(fig2, use_container_width=True)
if st.button("🗑️ Clear Results", type="secondary"):
clear_allocations()
st.warning("Results cleared.")
st.rerun()
st.markdown(
'',
unsafe_allow_html=True,
)