Upload app.py
Browse files
app.py
CHANGED
|
@@ -12,16 +12,16 @@ try:
|
|
| 12 |
except Exception:
|
| 13 |
HAS_FUZZ = False
|
| 14 |
|
| 15 |
-
APP_TITLE = "Ward Ranking Random Assigner"
|
| 16 |
DESCRIPTION = """
|
| 17 |
**Flow**
|
| 18 |
1) Upload .csv/.xlsx
|
| 19 |
2) Choose wards + set capacity
|
| 20 |
3) Check Available columns
|
| 21 |
4) Map by Auto-detect (Thai/English + fuzzy) or by numbers (1-based)
|
| 22 |
-
5) Clean → keep NAME/ID + selected wards; convert ranks to integers
|
| 23 |
-
6)
|
| 24 |
-
|
| 25 |
"""
|
| 26 |
|
| 27 |
WARD_CHOICES = [
|
|
@@ -56,9 +56,9 @@ AUTO_MAP = {
|
|
| 56 |
"NAME": ["ชื่อ-สกุล", "ชื่อ - สกุล", "fullname", "full name", "name", "student name"],
|
| 57 |
"ID": ["รหัสนักศึกษา", "รหัส", "student id", "id", "studentid"],
|
| 58 |
"Medical": ["อายุรศาสตร์", "medical"],
|
| 59 |
-
"Medical_1": ["อายุรศาสตร์_1", "medical_1", "med_1"
|
| 60 |
-
"Medical_2": ["อายุรศาสตร์_2", "medical_2", "med_2"
|
| 61 |
-
"Surgical": ["ศัลยศาสตร์", "surgical", "surgery"
|
| 62 |
"Pediatric": ["เด็ก", "pediatric", "pediatrics"],
|
| 63 |
"Community": ["ชุมชน", "community"],
|
| 64 |
"Psychiatric": ["จิตเวช", "psychiatric"],
|
|
@@ -176,20 +176,92 @@ def build_cleaned_from_indices(df: pd.DataFrame,
|
|
| 176 |
cleaned = cleaned[ordered]
|
| 177 |
return cleaned
|
| 178 |
|
| 179 |
-
def
|
| 180 |
-
capacities: Dict[str, int]) -> Tuple[pd.DataFrame, pd.DataFrame, Dict[str, int]]:
|
| 181 |
wards = [w for w in cleaned.columns if w not in ("NAME", "ID")]
|
| 182 |
-
cap = {w: int(capacities.get(w, 0)) for w in wards}
|
| 183 |
-
|
| 184 |
-
assigned = pd.Series(index=cleaned.index, data=pd.NA, dtype="object")
|
| 185 |
-
choice_no = pd.Series(index=cleaned.index, data=pd.NA, dtype="Int64")
|
| 186 |
-
|
| 187 |
mr = 0
|
| 188 |
for w in wards:
|
| 189 |
m = cleaned[w].max(skipna=True)
|
| 190 |
if pd.notna(m):
|
| 191 |
mr = max(mr, int(m))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
for r in range(1, mr + 1):
|
| 194 |
if all(c <= 0 for c in cap.values()):
|
| 195 |
break
|
|
@@ -211,112 +283,9 @@ def random_assign(cleaned: pd.DataFrame,
|
|
| 211 |
result = cleaned.copy()
|
| 212 |
result["AssignedWard"] = assigned
|
| 213 |
result["ChoiceNumber"] = choice_no
|
| 214 |
-
|
| 215 |
not_assigned = result[result["AssignedWard"].isna()].copy()
|
| 216 |
return result.fillna(""), not_assigned.fillna(""), cap
|
| 217 |
|
| 218 |
-
# ===== Reporting helpers =====
|
| 219 |
-
def ward_display(ward_key: str) -> str:
|
| 220 |
-
en, th = WARD_LABELS.get(ward_key, (ward_key, ward_key))
|
| 221 |
-
return f"{en} ({th})"
|
| 222 |
-
|
| 223 |
-
def max_rank_in(cleaned: pd.DataFrame) -> int:
|
| 224 |
-
wards = [w for w in cleaned.columns if w not in ("NAME", "ID")]
|
| 225 |
-
mr = 0
|
| 226 |
-
for w in wards:
|
| 227 |
-
m = cleaned[w].max(skipna=True)
|
| 228 |
-
if pd.notna(m):
|
| 229 |
-
mr = max(mr, int(m))
|
| 230 |
-
return int(mr)
|
| 231 |
-
|
| 232 |
-
def make_rank1_report(cleaned: pd.DataFrame, capacities: Dict[str, int]) -> str:
|
| 233 |
-
wards = [w for w in cleaned.columns if w not in ("NAME", "ID")]
|
| 234 |
-
total_students = len(cleaned)
|
| 235 |
-
total_capacity = sum(int(capacities.get(w, 0)) for w in wards)
|
| 236 |
-
lines = []
|
| 237 |
-
lines.append("## Rank 1 Results (การแสดงผลอันดับที่ 1)")
|
| 238 |
-
lines.append("")
|
| 239 |
-
lines.append(f"- **Total Students (จำนวนนักศึกษาทั้งหมด):** {total_students} students (คน)")
|
| 240 |
-
lines.append(f"- **Total Capacity (ความจุรวม):** {total_capacity} people (คน)")
|
| 241 |
-
lines.append("")
|
| 242 |
-
header = "| Ward (วอร์ด) | Capacity (ความจุ) | Rank 1 Count (จำนวนเลือกอันดับ 1) | Students (รายชื่อนักศึกษา) |"
|
| 243 |
-
sep = "|---|---:|---:|---|"
|
| 244 |
-
lines += [header, sep]
|
| 245 |
-
over = []
|
| 246 |
-
under = []
|
| 247 |
-
for w in wards:
|
| 248 |
-
cap = int(capacities.get(w, 0))
|
| 249 |
-
rank1_students = cleaned.loc[cleaned[w] == 1, "NAME"].astype(str).tolist()
|
| 250 |
-
r1_count = len(rank1_students)
|
| 251 |
-
display_students = ", ".join(rank1_students[:3]) + ("..." if r1_count > 3 else "")
|
| 252 |
-
lines.append(f"| {ward_display(w)} | {cap} | {r1_count} | {display_students} |")
|
| 253 |
-
if r1_count > cap:
|
| 254 |
-
over.append((w, r1_count, cap))
|
| 255 |
-
elif r1_count < cap:
|
| 256 |
-
under.append((w, r1_count, cap))
|
| 257 |
-
lines.append("")
|
| 258 |
-
lines.append("### Additional Statistics (สถิติเพิ่มเติม)")
|
| 259 |
-
lines.append("")
|
| 260 |
-
if over:
|
| 261 |
-
lines.append("**Wards where Rank 1 count exceeds capacity (วอร์ดที่มีคนเลือกอันดับ 1 เกินความจุ):**")
|
| 262 |
-
for w, c, cap in over:
|
| 263 |
-
lines.append(f"- {ward_display(w)}: {c} selected (capacity {cap})")
|
| 264 |
-
else:
|
| 265 |
-
lines.append("- No wards exceed capacity at Rank 1. (ไม่มีวอร์ดใดเกินความจุในอันดับ 1)")
|
| 266 |
-
if under:
|
| 267 |
-
lines.append("")
|
| 268 |
-
lines.append("**Wards where Rank 1 count below capacity (วอร์ดที่มีคนเลือกอันดับ 1 น้อยกว่าความจุ):**")
|
| 269 |
-
for w, c, cap in under:
|
| 270 |
-
lines.append(f"- {ward_display(w)}: {c} selected (capacity {cap})")
|
| 271 |
-
return "\n".join(lines)
|
| 272 |
-
|
| 273 |
-
def make_rank_report(cleaned: pd.DataFrame, capacities: Dict[str, int], rank: int) -> str:
|
| 274 |
-
wards = [w for w in cleaned.columns if w not in ("NAME", "ID")]
|
| 275 |
-
lines = []
|
| 276 |
-
lines.append(f"## Rank {rank} Results (การแสดงผลอันดับที่ {rank})")
|
| 277 |
-
total_students = len(cleaned)
|
| 278 |
-
total_capacity = sum(int(capacities.get(w, 0)) for w in wards)
|
| 279 |
-
lines.append(f"- **Total Students (จำนวนนักศึกษาทั้งหมด):** {total_students} students (คน)")
|
| 280 |
-
lines.append(f"- **Total Capacity (ความจุรวม):** {total_capacity} people (คน)")
|
| 281 |
-
lines.append("")
|
| 282 |
-
header = "| Ward (วอร์ด) | Capacity (ความจุ) | Rank {rank} Count (จำนวนเลือกอันดับ {rank}) | Students (รายชื่อนักศึกษา) |".format(rank=rank)
|
| 283 |
-
sep = "|---|---:|---:|---|"
|
| 284 |
-
lines += [header, sep]
|
| 285 |
-
over, under = [], []
|
| 286 |
-
for w in wards:
|
| 287 |
-
cap = int(capacities.get(w, 0))
|
| 288 |
-
names = cleaned.loc[cleaned[w] == rank, "NAME"].astype(str).tolist()
|
| 289 |
-
cnt = len(names)
|
| 290 |
-
sample = ", ".join(names[:3]) + ("..." if cnt > 3 else "")
|
| 291 |
-
lines.append(f"| {ward_display(w)} | {cap} | {cnt} | {sample} |")
|
| 292 |
-
if cnt > cap:
|
| 293 |
-
over.append((w, cnt, cap))
|
| 294 |
-
elif cnt < cap:
|
| 295 |
-
under.append((w, cnt, cap))
|
| 296 |
-
lines.append("")
|
| 297 |
-
lines.append("**Additional Statistics (สถิติเพิ่มเติม):**")
|
| 298 |
-
if over:
|
| 299 |
-
lines.append("- Wards where count exceeds capacity (เกินความจุ):")
|
| 300 |
-
for w, c, cap in over:
|
| 301 |
-
lines.append(f" - {ward_display(w)}: {c} selected (capacity {cap})")
|
| 302 |
-
else:
|
| 303 |
-
lines.append("- No wards exceed capacity at this rank. (ไม่มีวอร์ดเกินความจุในอันดับนี้)")
|
| 304 |
-
if under:
|
| 305 |
-
lines.append("- Wards where count below capacity (ต่ำกว่าความจุ):")
|
| 306 |
-
for w, c, cap in under:
|
| 307 |
-
lines.append(f" - {ward_display(w)}: {c} selected (capacity {cap})")
|
| 308 |
-
return "\n".join(lines)
|
| 309 |
-
|
| 310 |
-
def make_all_ranks_report(cleaned: pd.DataFrame, capacities: Dict[str, int]) -> str:
|
| 311 |
-
mr = max_rank_in(cleaned)
|
| 312 |
-
if mr == 0:
|
| 313 |
-
return "No ranking numbers found. (ไม่พบข้อมูลอันดับเป็นตัวเลข)"
|
| 314 |
-
parts = []
|
| 315 |
-
for r in range(1, mr + 1):
|
| 316 |
-
parts.append(make_rank_report(cleaned, capacities, r))
|
| 317 |
-
parts.append("\n---\n")
|
| 318 |
-
return "\n".join(parts)
|
| 319 |
-
|
| 320 |
# ===== Helpers for temp file paths =====
|
| 321 |
def _tmp(name: str) -> str:
|
| 322 |
os.makedirs("/tmp", exist_ok=True)
|
|
@@ -424,29 +393,9 @@ def _capacities_from_df(cleaned: pd.DataFrame, capacity_df: Optional[pd.DataFram
|
|
| 424 |
capacities[str(row["Ward"])] = 0
|
| 425 |
return capacities
|
| 426 |
|
| 427 |
-
def
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
if df is None:
|
| 431 |
-
return "Please upload a valid file."
|
| 432 |
-
n_cols = len(df.columns)
|
| 433 |
-
ward_nums = {
|
| 434 |
-
"Medical": med_num, "Medical_1": med1_num, "Medical_2": med2_num,
|
| 435 |
-
"Surgical": surg_num, "Pediatric": ped_num, "Community": comm_num,
|
| 436 |
-
"Psychiatric": psy_num, "Obstetrics": obs_num
|
| 437 |
-
}
|
| 438 |
-
errors, mapping_idx = collect_mapping_numbers(name_num, id_num, ward_nums, selected_wards, n_cols)
|
| 439 |
-
if errors:
|
| 440 |
-
return "❌ Mapping invalid:\n" + "\n".join(errors)
|
| 441 |
-
try:
|
| 442 |
-
cleaned = build_cleaned_from_indices(df, mapping_idx)
|
| 443 |
-
except Exception as e:
|
| 444 |
-
return f"❌ Error building cleaned data: {e}"
|
| 445 |
-
capacities = _capacities_from_df(cleaned, capacity_df)
|
| 446 |
-
return make_rank1_report(cleaned, capacities)
|
| 447 |
-
|
| 448 |
-
def on_all_ranks_report(file, selected_wards, capacity_df, name_num, id_num,
|
| 449 |
-
med_num, med1_num, med2_num, surg_num, ped_num, comm_num, psy_num, obs_num):
|
| 450 |
df, msg = read_table(file)
|
| 451 |
if df is None:
|
| 452 |
return "Please upload a valid file."
|
|
@@ -464,7 +413,7 @@ def on_all_ranks_report(file, selected_wards, capacity_df, name_num, id_num,
|
|
| 464 |
except Exception as e:
|
| 465 |
return f"❌ Error building cleaned data: {e}"
|
| 466 |
capacities = _capacities_from_df(cleaned, capacity_df)
|
| 467 |
-
return
|
| 468 |
|
| 469 |
def on_assign(file, selected_wards, capacity_df, name_num, id_num,
|
| 470 |
med_num, med1_num, med2_num, surg_num, ped_num, comm_num, psy_num, obs_num):
|
|
@@ -482,7 +431,6 @@ def on_assign(file, selected_wards, capacity_df, name_num, id_num,
|
|
| 482 |
}
|
| 483 |
_errors, mapping_idx = collect_mapping_numbers(name_num, id_num, ward_nums, selected_wards, n_cols)
|
| 484 |
cleaned = build_cleaned_from_indices(df, mapping_idx)
|
| 485 |
-
|
| 486 |
capacities = _capacities_from_df(cleaned, capacity_df)
|
| 487 |
|
| 488 |
total_capacity = sum(capacities.values())
|
|
@@ -500,9 +448,9 @@ def on_assign(file, selected_wards, capacity_df, name_num, id_num,
|
|
| 500 |
not_assigned.to_csv(not_assigned_path, index=False, encoding="utf-8-sig")
|
| 501 |
|
| 502 |
leftover_text = "Remaining capacity (จำนวนรับที่เหลือ):\n" + "\n".join([f"- {ward_display(k)}: {v}" for k, v in leftover.items()])
|
| 503 |
-
|
| 504 |
|
| 505 |
-
return status, assigned.head(30), assigned_path, not_assigned_path, leftover_text,
|
| 506 |
|
| 507 |
with gr.Blocks(title=APP_TITLE) as demo:
|
| 508 |
gr.Markdown(f"# {APP_TITLE}")
|
|
@@ -553,28 +501,20 @@ with gr.Blocks(title=APP_TITLE) as demo:
|
|
| 553 |
outputs=[status, available, name_num, id_num,
|
| 554 |
med_num, med1_num, med2_num, surg_num, ped_num, comm_num, psy_num, obs_num])
|
| 555 |
|
| 556 |
-
#
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
med_num, med1_num, med2_num, surg_num, ped_num, comm_num, psy_num, obs_num],
|
| 563 |
-
outputs=rank1_report
|
| 564 |
-
)
|
| 565 |
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
all_ranks_btn.click(
|
| 569 |
-
fn=on_all_ranks_report,
|
| 570 |
inputs=[file, selected_wards, capacity_df, name_num, id_num,
|
| 571 |
med_num, med1_num, med2_num, surg_num, ped_num, comm_num, psy_num, obs_num],
|
| 572 |
-
outputs=
|
| 573 |
)
|
| 574 |
|
| 575 |
-
with gr.Row():
|
| 576 |
-
clean_btn = gr.Button("Clean data (ดูพรีวิว)", variant="primary")
|
| 577 |
-
|
| 578 |
preview = gr.Dataframe(label="Cleaned preview (first 30 rows)", visible=True)
|
| 579 |
cleaned_file = gr.File(label="Download cleaned.csv")
|
| 580 |
|
|
@@ -590,7 +530,7 @@ with gr.Blocks(title=APP_TITLE) as demo:
|
|
| 590 |
assigned_file = gr.File(label="Download assigned.csv")
|
| 591 |
not_assigned_file = gr.File(label="Download not_assigned.csv")
|
| 592 |
leftover_text = gr.Textbox(label="Remaining capacity summary", interactive=False)
|
| 593 |
-
allocation_report = gr.Markdown(label="
|
| 594 |
|
| 595 |
assign_btn.click(
|
| 596 |
fn=on_assign,
|
|
|
|
| 12 |
except Exception:
|
| 13 |
HAS_FUZZ = False
|
| 14 |
|
| 15 |
+
APP_TITLE = "Ward Ranking Cleaner & Random Assigner (Auto-map + Stepwise Reports)"
|
| 16 |
DESCRIPTION = """
|
| 17 |
**Flow**
|
| 18 |
1) Upload .csv/.xlsx
|
| 19 |
2) Choose wards + set capacity
|
| 20 |
3) Check Available columns
|
| 21 |
4) Map by Auto-detect (Thai/English + fuzzy) or by numbers (1-based)
|
| 22 |
+
5) **Clean** → keep NAME/ID + selected wards; convert ranks to integers
|
| 23 |
+
6) **Show Stepwise Report** → Rank 1 results → random assign rank 1 → Rank 2 results → random assign … until done
|
| 24 |
+
7) (Optional) Assign button runs the full allocation too and provides CSVs to download
|
| 25 |
"""
|
| 26 |
|
| 27 |
WARD_CHOICES = [
|
|
|
|
| 56 |
"NAME": ["ชื่อ-สกุล", "ชื่อ - สกุล", "fullname", "full name", "name", "student name"],
|
| 57 |
"ID": ["รหัสนักศึกษา", "รหัส", "student id", "id", "studentid"],
|
| 58 |
"Medical": ["อายุรศาสตร์", "medical"],
|
| 59 |
+
"Medical_1": ["อายุรศาสตร์_1", "medical_1", "med_1"],
|
| 60 |
+
"Medical_2": ["อายุรศาสตร์_2", "medical_2", "med_2"],
|
| 61 |
+
"Surgical": ["ศัลยศาสตร์", "surgical", "surgery"],
|
| 62 |
"Pediatric": ["เด็ก", "pediatric", "pediatrics"],
|
| 63 |
"Community": ["ชุมชน", "community"],
|
| 64 |
"Psychiatric": ["จิตเวช", "psychiatric"],
|
|
|
|
| 176 |
cleaned = cleaned[ordered]
|
| 177 |
return cleaned
|
| 178 |
|
| 179 |
+
def max_rank_in(cleaned: pd.DataFrame) -> int:
|
|
|
|
| 180 |
wards = [w for w in cleaned.columns if w not in ("NAME", "ID")]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
mr = 0
|
| 182 |
for w in wards:
|
| 183 |
m = cleaned[w].max(skipna=True)
|
| 184 |
if pd.notna(m):
|
| 185 |
mr = max(mr, int(m))
|
| 186 |
+
return int(mr)
|
| 187 |
+
|
| 188 |
+
# ===== Stepwise simulation & reports (random each round) =====
|
| 189 |
+
def simulate_stepwise_report(cleaned: pd.DataFrame, capacities: Dict[str, int]) -> str:
|
| 190 |
+
"""Round-by-round report: show rank r results, then randomly assign rank r, update remaining capacity, continue."""
|
| 191 |
+
wards = [w for w in cleaned.columns if w not in ("NAME", "ID")]
|
| 192 |
+
total_students = len(cleaned)
|
| 193 |
+
cap = {w: int(capacities.get(w, 0)) for w in wards}
|
| 194 |
+
assigned = pd.Series(index=cleaned.index, data=False) # True if assigned already
|
| 195 |
+
assigned_ward = pd.Series(index=cleaned.index, data="", dtype="object")
|
| 196 |
+
|
| 197 |
+
mr = max_rank_in(cleaned)
|
| 198 |
+
lines = []
|
| 199 |
+
lines.append(f"### Total Students (จำนวนนักศึกษาทั้งหมด): {total_students} students (คน)")
|
| 200 |
+
lines.append(f"### Total Capacity (ความจุรวม): {sum(cap.values())} people (คน)")
|
| 201 |
+
lines.append("")
|
| 202 |
+
|
| 203 |
+
for r in range(1, mr + 1):
|
| 204 |
+
lines.append("\n---\n")
|
| 205 |
+
lines.append(f"## Rank {r} Results (การแสดงผลอันดับที่ {r})\n")
|
| 206 |
+
header = "| Ward (วอร์ด) | Remaining Capacity (ความจุคงเหลือ) | Rank {r} Count (จำนวนเลือกอันดับ {r}) | Students (รายชื่อนักศึกษา) |".format(r=r)
|
| 207 |
+
sep = "|---|---:|---:|---|"
|
| 208 |
+
lines += [header, sep]
|
| 209 |
+
|
| 210 |
+
# Show BEFORE assignment
|
| 211 |
+
for w in wards:
|
| 212 |
+
names = cleaned.loc[(~assigned) & (cleaned[w] == r), "NAME"].astype(str).tolist()
|
| 213 |
+
cnt = len(names)
|
| 214 |
+
sample = ", ".join(names[:3]) + ("..." if cnt > 3 else "")
|
| 215 |
+
lines.append(f"| {ward_display(w)} | {cap[w]} | {cnt} | {sample} |")
|
| 216 |
+
|
| 217 |
+
# Now perform random assignment at this rank
|
| 218 |
+
lines.append("")
|
| 219 |
+
lines.append(f"### Allocation at Rank {r} (การสุ่มจัดสรรในอันดับที่ {r})")
|
| 220 |
+
for w in wards:
|
| 221 |
+
candidates_idx = cleaned.index[(~assigned) & (cleaned[w] == r)].tolist()
|
| 222 |
+
if not candidates_idx or cap[w] <= 0:
|
| 223 |
+
lines.append(f"- {ward_display(w)}: No allocation (ไม่มีการจัดสรร)")
|
| 224 |
+
continue
|
| 225 |
+
if len(candidates_idx) <= cap[w]:
|
| 226 |
+
chosen = candidates_idx
|
| 227 |
+
else:
|
| 228 |
+
chosen = list(np.random.choice(candidates_idx, size=cap[w], replace=False))
|
| 229 |
+
assigned.loc[chosen] = True
|
| 230 |
+
assigned_ward.loc[chosen] = w
|
| 231 |
+
cap[w] -= len(chosen)
|
| 232 |
|
| 233 |
+
chosen_names = cleaned.loc[chosen, "NAME"].astype(str).tolist()
|
| 234 |
+
sample = ", ".join(chosen_names[:10]) + ("..." if len(chosen_names) > 10 else "")
|
| 235 |
+
lines.append(f"- {ward_display(w)} → Selected (คัดเลือก): {len(chosen_names)} | {sample}")
|
| 236 |
+
|
| 237 |
+
# After assignment at this rank
|
| 238 |
+
lines.append("")
|
| 239 |
+
lines.append("**Remaining capacity (จำนวนรับที่เหลือหลังรอบนี้):**")
|
| 240 |
+
for w in wards:
|
| 241 |
+
lines.append(f"- {ward_display(w)}: {cap[w]}")
|
| 242 |
+
|
| 243 |
+
# Final summary
|
| 244 |
+
lines.append("\n---\n")
|
| 245 |
+
lines.append("## Final Summary (สรุปสุดท้าย)")
|
| 246 |
+
for w in wards:
|
| 247 |
+
sel_names = assigned_ward[assigned_ward == w]
|
| 248 |
+
lines.append(f"- {ward_display(w)}: {len(sel_names)} assigned (จัดสรรแล้ว)")
|
| 249 |
+
unassigned = cleaned.loc[~assigned, "NAME"].astype(str).tolist()
|
| 250 |
+
lines.append(f"- Not assigned (ยังไม่ได้รับการจัดสรร): {len(unassigned)}")
|
| 251 |
+
if unassigned:
|
| 252 |
+
sample_un = ", ".join(unassigned[:15]) + ("..." if len(unassigned) > 15 else "")
|
| 253 |
+
lines.append(f" - {sample_un}")
|
| 254 |
+
return "\n".join(lines)
|
| 255 |
+
|
| 256 |
+
def random_assign(cleaned: pd.DataFrame,
|
| 257 |
+
capacities: Dict[str, int]) -> Tuple[pd.DataFrame, pd.DataFrame, Dict[str, int]]:
|
| 258 |
+
"""Full allocation using the same stepwise logic (for CSV outputs)."""
|
| 259 |
+
wards = [w for w in cleaned.columns if w not in ("NAME", "ID")]
|
| 260 |
+
cap = {w: int(capacities.get(w, 0)) for w in wards}
|
| 261 |
+
assigned = pd.Series(index=cleaned.index, data=pd.NA, dtype="object")
|
| 262 |
+
choice_no = pd.Series(index=cleaned.index, data=pd.NA, dtype="Int64")
|
| 263 |
+
|
| 264 |
+
mr = max_rank_in(cleaned)
|
| 265 |
for r in range(1, mr + 1):
|
| 266 |
if all(c <= 0 for c in cap.values()):
|
| 267 |
break
|
|
|
|
| 283 |
result = cleaned.copy()
|
| 284 |
result["AssignedWard"] = assigned
|
| 285 |
result["ChoiceNumber"] = choice_no
|
|
|
|
| 286 |
not_assigned = result[result["AssignedWard"].isna()].copy()
|
| 287 |
return result.fillna(""), not_assigned.fillna(""), cap
|
| 288 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
# ===== Helpers for temp file paths =====
|
| 290 |
def _tmp(name: str) -> str:
|
| 291 |
os.makedirs("/tmp", exist_ok=True)
|
|
|
|
| 393 |
capacities[str(row["Ward"])] = 0
|
| 394 |
return capacities
|
| 395 |
|
| 396 |
+
def on_stepwise_report(file, selected_wards, capacity_df, name_num, id_num,
|
| 397 |
+
med_num, med1_num, med2_num, surg_num, ped_num, comm_num, psy_num, obs_num):
|
| 398 |
+
# Requires cleaned mapping to be valid
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 399 |
df, msg = read_table(file)
|
| 400 |
if df is None:
|
| 401 |
return "Please upload a valid file."
|
|
|
|
| 413 |
except Exception as e:
|
| 414 |
return f"❌ Error building cleaned data: {e}"
|
| 415 |
capacities = _capacities_from_df(cleaned, capacity_df)
|
| 416 |
+
return simulate_stepwise_report(cleaned, capacities)
|
| 417 |
|
| 418 |
def on_assign(file, selected_wards, capacity_df, name_num, id_num,
|
| 419 |
med_num, med1_num, med2_num, surg_num, ped_num, comm_num, psy_num, obs_num):
|
|
|
|
| 431 |
}
|
| 432 |
_errors, mapping_idx = collect_mapping_numbers(name_num, id_num, ward_nums, selected_wards, n_cols)
|
| 433 |
cleaned = build_cleaned_from_indices(df, mapping_idx)
|
|
|
|
| 434 |
capacities = _capacities_from_df(cleaned, capacity_df)
|
| 435 |
|
| 436 |
total_capacity = sum(capacities.values())
|
|
|
|
| 448 |
not_assigned.to_csv(not_assigned_path, index=False, encoding="utf-8-sig")
|
| 449 |
|
| 450 |
leftover_text = "Remaining capacity (จำนวนรับที่เหลือ):\n" + "\n".join([f"- {ward_display(k)}: {v}" for k, v in leftover.items()])
|
| 451 |
+
stepwise_md = simulate_stepwise_report(cleaned, capacities)
|
| 452 |
|
| 453 |
+
return status, assigned.head(30), assigned_path, not_assigned_path, leftover_text, stepwise_md
|
| 454 |
|
| 455 |
with gr.Blocks(title=APP_TITLE) as demo:
|
| 456 |
gr.Markdown(f"# {APP_TITLE}")
|
|
|
|
| 501 |
outputs=[status, available, name_num, id_num,
|
| 502 |
med_num, med1_num, med2_num, surg_num, ped_num, comm_num, psy_num, obs_num])
|
| 503 |
|
| 504 |
+
# >>> Moved CLEAN button here (before reports) <<<
|
| 505 |
+
clean_btn = gr.Button("Clean data (ดูพรีวิว)", variant="primary")
|
| 506 |
+
|
| 507 |
+
# Reports (stepwise randomized)
|
| 508 |
+
step_btn = gr.Button("Show Stepwise Report (ดูรายงานทีละอันดับแบบสุ่ม)")
|
| 509 |
+
step_report = gr.Markdown(label="Stepwise Report (รายงานรอบต่อรอบ)")
|
|
|
|
|
|
|
|
|
|
| 510 |
|
| 511 |
+
step_btn.click(
|
| 512 |
+
fn=on_stepwise_report,
|
|
|
|
|
|
|
| 513 |
inputs=[file, selected_wards, capacity_df, name_num, id_num,
|
| 514 |
med_num, med1_num, med2_num, surg_num, ped_num, comm_num, psy_num, obs_num],
|
| 515 |
+
outputs=step_report
|
| 516 |
)
|
| 517 |
|
|
|
|
|
|
|
|
|
|
| 518 |
preview = gr.Dataframe(label="Cleaned preview (first 30 rows)", visible=True)
|
| 519 |
cleaned_file = gr.File(label="Download cleaned.csv")
|
| 520 |
|
|
|
|
| 530 |
assigned_file = gr.File(label="Download assigned.csv")
|
| 531 |
not_assigned_file = gr.File(label="Download not_assigned.csv")
|
| 532 |
leftover_text = gr.Textbox(label="Remaining capacity summary", interactive=False)
|
| 533 |
+
allocation_report = gr.Markdown(label="Stepwise Report (ผลการสุ่มรอบต่อรอบ)")
|
| 534 |
|
| 535 |
assign_btn.click(
|
| 536 |
fn=on_assign,
|