Gale_shapely / app.py
daemon03's picture
Initial commit: Streamlit UI for Gale-Shapley Algorithm
f804bd5
"""
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 &amp; 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 &amp; Streamlit Β· '
'Algorithm by Gale &amp; 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 &amp; 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,
)