Spaces:
Sleeping
Sleeping
| """ | |
| 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(""" | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap'); | |
| :root { | |
| --accent: #6C63FF; --accent2: #00D2FF; --bg: #0E1117; | |
| --card: #161B22; --card2: #1A1D29; --text: #E0E0E0; | |
| --success: #00E676; --warn: #FFD600; --danger: #FF5252; | |
| } | |
| html, body, [class*="css"] { font-family: 'Inter', sans-serif; } | |
| /* Hero banner */ | |
| .hero { | |
| background: linear-gradient(135deg, #6C63FF 0%, #00D2FF 100%); | |
| border-radius: 16px; padding: 2.5rem 2rem; margin-bottom: 1.5rem; | |
| text-align: center; position: relative; overflow: hidden; | |
| } | |
| .hero::before { | |
| content: ''; position: absolute; inset: 0; | |
| background: radial-gradient(circle at 30% 50%, rgba(255,255,255,.12) 0%, transparent 60%); | |
| } | |
| .hero h1 { color: #fff; font-size: 2.2rem; font-weight: 800; margin: 0; position: relative; } | |
| .hero p { color: rgba(255,255,255,.85); font-size: 1rem; margin: .5rem 0 0; position: relative; } | |
| /* Stat cards */ | |
| .stat-row { display: flex; gap: 1rem; margin-bottom: 1.5rem; flex-wrap: wrap; } | |
| .stat-card { | |
| flex: 1; min-width: 160px; background: var(--card); border-radius: 14px; | |
| padding: 1.4rem; text-align: center; border: 1px solid rgba(108,99,255,.25); | |
| transition: transform .2s, box-shadow .2s; | |
| } | |
| .stat-card:hover { transform: translateY(-4px); box-shadow: 0 8px 24px rgba(108,99,255,.2); } | |
| .stat-card .num { font-size: 2rem; font-weight: 700; background: linear-gradient(135deg,#6C63FF,#00D2FF); | |
| -webkit-background-clip: text; -webkit-text-fill-color: transparent; } | |
| .stat-card .lbl { color: #9CA3AF; font-size: .85rem; margin-top: .3rem; } | |
| /* Section headers */ | |
| .sec-hdr { font-size: 1.35rem; font-weight: 700; margin: 1.5rem 0 .8rem; | |
| padding-left: .6rem; border-left: 4px solid var(--accent); } | |
| /* Info banner */ | |
| .info-banner { | |
| background: linear-gradient(135deg, rgba(108,99,255,.12), rgba(0,210,255,.08)); | |
| border: 1px solid rgba(108,99,255,.3); border-radius: 12px; | |
| padding: 1rem 1.2rem; margin: 1rem 0; font-size: .9rem; color: var(--text); | |
| } | |
| /* Footer */ | |
| .footer { text-align: center; padding: 2rem 0 1rem; color: #6B7280; font-size: .82rem; } | |
| .footer a { color: var(--accent); text-decoration: none; } | |
| .footer a:hover { text-decoration: underline; } | |
| /* Table tweaks */ | |
| .stDataFrame { border-radius: 12px; overflow: hidden; } | |
| </style> | |
| """, 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( | |
| '<div class="info-banner">' | |
| '<b>π Python + Streamlit Edition</b><br>' | |
| 'File-system storage (no MySQL needed).<br><br>' | |
| 'For the <b>C + MySQL</b> version:<br>' | |
| '<a href="https://github.com/Harshwardhan-Deshmukh03/Roommate-allocation-using-Gale-Shapley-Algorithm.git" ' | |
| 'target="_blank">View on GitHub β</a></div>', | |
| 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'<div class="stat-card"><div class="num">{num}</div>' | |
| f'<div class="lbl">{lbl}</div></div>', | |
| unsafe_allow_html=True, | |
| ) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # PAGES | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # ββ Dashboard βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if page == "π Dashboard": | |
| st.markdown( | |
| '<div class="hero"><h1>π Roommate Allocation System</h1>' | |
| '<p>Gale-Shapley Stable-Matching Algorithm Β· Python & Streamlit</p></div>', | |
| 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('<div class="sec-hdr">How It Works</div>', 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('<div class="sec-hdr">Quick Start</div>', 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( | |
| '<div class="footer">Built with Python & Streamlit Β· ' | |
| 'Algorithm by Gale & Shapley (1962) Β· ' | |
| '<a href="https://github.com/Harshwardhan-Deshmukh03/Roommate-allocation-using-Gale-Shapley-Algorithm.git" ' | |
| 'target="_blank">Original C + MySQL version</a></div>', | |
| unsafe_allow_html=True, | |
| ) | |
| # ββ Manage Students ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| elif page == "π₯ Manage Students": | |
| st.markdown('<div class="sec-hdr">π₯ Manage Students</div>', 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('<div class="sec-hdr">πͺ Manage Rooms</div>', 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('<div class="sec-hdr">π CSV Import</div>', unsafe_allow_html=True) | |
| st.markdown( | |
| '<div class="info-banner">Upload CSV files to bulk-import students and rooms. ' | |
| 'This is useful when manual entry is tedious or when the allocation is complex.</div>', | |
| 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('<div class="sec-hdr">βοΈ Run Allocation</div>', 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( | |
| '<div class="info-banner">' | |
| '<b>Stage 1:</b> Gale-Shapley matches students into roommate pairs.<br>' | |
| '<b>Stage 2:</b> Pairs ranked by CGPA select rooms via Gale-Shapley.</div>', | |
| 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('<div class="sec-hdr">π Allocation Results</div>', 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( | |
| '<div class="footer">Built with <b>Python & Streamlit</b> Β· ' | |
| 'For the <b>C + MySQL</b> version, visit ' | |
| '<a href="https://github.com/Harshwardhan-Deshmukh03/Roommate-allocation-using-Gale-Shapley-Algorithm.git" ' | |
| 'target="_blank">GitHub β</a></div>', | |
| unsafe_allow_html=True, | |
| ) | |