""" 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'
{num}
' f'
{lbl}
', 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, )