ShinyaJ commited on
Commit
b682ac6
·
verified ·
1 Parent(s): 23f7e5a

Upload 3 files

Browse files
Files changed (3) hide show
  1. README.md +19 -12
  2. app.py +377 -0
  3. requirements.txt +4 -0
README.md CHANGED
@@ -1,12 +1,19 @@
1
- ---
2
- title: Gradio 2025 Ward Assignment System CMU
3
- emoji: 📊
4
- colorFrom: green
5
- colorTo: indigo
6
- sdk: gradio
7
- sdk_version: 5.47.2
8
- app_file: app.py
9
- pinned: false
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
1
+
2
+ # Ward Ranking Cleaner & Random Assigner (Gradio)
3
+
4
+ ## วิธีใช้งานใน Hugging Face Spaces
5
+ 1. สร้าง Space ใหม่ เลือก SDK = **Gradio**
6
+ 2. อัปโหลดไฟล์เหล่านี้:
7
+ - `app.py`
8
+ - `requirements.txt`
9
+ - (ถ้ามีข้อมูลประกอบอื่น ๆ ให้ใส่เพิ่มได้)
10
+ 3. กด Commit → ระบบจะ build และให้ลิงก์ถาวร
11
+
12
+ ## ขั้นตอนใช้งาน
13
+ 1) อัปโหลดไฟล์ .csv หรือ .xlsx ที่มีข้อมูลนักศึกษา/ผู้เรียน
14
+ 2) เลือกวอร์ดที่จะใช้ แล้วกำหนด Capacity
15
+ 3) ใส่ชื่อคอลัมน์จริงในไฟล์ของคุณสำหรับ NAME, ID และคอลัมน์วอร์ดแต่ละอัน (ใส่ชื่อจริง หรือใช้ regex/คำบางส่วนร่วมกับโหมดยืดหยุ่น)
16
+ 4) กด **Clean data** เพื่อดูพรีวิว (ระบบจะ keep เฉพาะ NAME, ID, และคอลัมน์วอร์ดที่เลือก พร้อมแปลงอันดับให้เป็นตัวเลข)
17
+ 5) กด **Assign** เพื่อสุ่มจัดสรรทีละอันดับ 1,2,3,... ตาม Capacity
18
+
19
+ > การดึง "อันดับ" จะใช้ตัวเลขที่พบในสตริง เช่น `1st`, `อันดับ 3`, `4th` เป็นต้น
app.py ADDED
@@ -0,0 +1,377 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import gradio as gr
3
+ import pandas as pd
4
+ import numpy as np
5
+ import re
6
+ from io import BytesIO
7
+ from typing import List, Dict, Tuple, Optional
8
+
9
+ APP_TITLE = "Ward Ranking Cleaner & Random Assigner (Flexible Columns)"
10
+ DESCRIPTION = """
11
+ 1) เลือก **วอร์ด** ที่จะใช้ (จากรายการ 8 วอร์ดด้านล่าง) และใส่ **capacity** แต่ละวอร์ด
12
+ 2) ระบุ **หัวคอลัมน์ในไฟล์** ของคุณสำหรับ: NAME, ID และคอลัมน์คะแนน/อันดับของแต่ละวอร์ด (ชื่อคอลัมน์จริงในไฟล์)
13
+ 3) อัปโหลดไฟล์ .csv หรือ .xlsx แล้วกด **Clean data** เพื่อดูตารางที่เหลือเฉพาะ NAME, ID และคอลัมน์วอร์ดที่เลือก (คอลัมน์อื่นจะถูก drop)
14
+ 4) กด **Assign (สุ่มตามลำดับอันดับ)** เพื่อสุ่มจัดสรรทีละอันดับ 1 → 2 → 3 ... ตาม capacity ของแต่ละวอร์ด
15
+ 5) ดาวน์โหลด CSV ผลลัพธ์ได้
16
+
17
+ - การอ่าน "อันดับ" จะดึง **ตัวเลข** จากสตริง (เช่น `1st`, `อันดับ 3`, `4th`) — ถ้าหาเลขไม่เจอจะถือว่าเป็นค่าว่าง
18
+ - ถ้าคุณมีคอลัมน์ชื่อไม่แน่นอน สามารถใส่ชื่อที่แน่ใจลงไป หรือใช้ชื่อบางส่วนแล้วยกให้ **โหมดจับคู่ยืดหยุ่น** (regex) ช่วยค้นหา
19
+ """
20
+
21
+ WARD_CHOICES = [
22
+ ("Medical", "อายุรศาสตร์ ภาคปกติ"),
23
+ ("Medical_1", "อายุรศาสตร์_1"),
24
+ ("Medical_2", "อายุรศาสตร์_2"),
25
+ ("Surgical", "ศัลยศาสตร์"),
26
+ ("Pediatric", "เด็ก"),
27
+ ("Community", "ชุมชน"),
28
+ ("Psychiatric", "จิตเวช"),
29
+ ("Obstetrics", "สูติศาสตร์"),
30
+ ]
31
+
32
+ def read_table(file) -> Tuple[Optional[pd.DataFrame], str]:
33
+ if file is None:
34
+ return None, "กรุณาอัปโหลดไฟล์ก่อน (.csv หรือ .xlsx)"
35
+ name = file.name.lower() if hasattr(file, "name") else ""
36
+ try:
37
+ if name.endswith(".csv"):
38
+ df = pd.read_csv(file.name if hasattr(file, "name") else file)
39
+ elif name.endswith(".xlsx"):
40
+ df = pd.read_excel(file.name if hasattr(file, "name") else file)
41
+ else:
42
+ # ลองเดาว่าเป็น csv
43
+ try:
44
+ df = pd.read_csv(file)
45
+ except Exception:
46
+ return None, "รองรับเฉพาะ .csv หรือ .xlsx เท่านั้น"
47
+ except Exception as e:
48
+ return None, f"อ่านไฟล์ไม่สำเร็จ: {e}"
49
+ # ปรับชื่อคอลัมน์ (trim)
50
+ df.columns = [str(c).strip() for c in df.columns]
51
+ return df, ""
52
+
53
+ def find_column(df: pd.DataFrame, key: str, flexible: bool) -> Optional[str]:
54
+ """
55
+ ค้นหาคอลัมน์ตามชื่อที่ผู้ใช้กรอก:
56
+ - ถ้า flexible=False → ค้นหาแบบตรงตัว (case-sensitive แบบเดิม แต่เราทำ trim แล้ว)
57
+ - ถ้า flexible=True → จับคู่แบบยืดหยุ่น: ถ้า key มีอักขระพิเศษ ถือเป็น regex; ถ้าไม่ ก็มองเป็นสตริงย่อยที่ต้องพบในชื่อคอลัมน์
58
+ คืนชื่อคอลัมน์จริงถ้าพบ (ตัวแรกที่พบ), ไม่งั้นคืน None
59
+ """
60
+ cols = list(df.columns)
61
+ if not flexible:
62
+ return key if key in cols else None
63
+ # โหมดยืดหยุ่น
64
+ # ถ้า key เป็นสตริงธรรมดา ให้ค้นหาแบบ "มี key เป็นส่วนหนึ่งของชื่อคอลัมน์" (case-insensitive)
65
+ try:
66
+ pattern = re.compile(key, flags=re.IGNORECASE)
67
+ for c in cols:
68
+ if re.search(pattern, c):
69
+ return c
70
+ except re.error:
71
+ # ถ้า regex ไม่ valid ให้ fallback เป็น contains (case-insensitive)
72
+ low = key.lower()
73
+ for c in cols:
74
+ if low in c.lower():
75
+ return c
76
+ return None
77
+
78
+ def parse_rank(value) -> Optional[int]:
79
+ """
80
+ รับค่าจากคอลั��น์อันดับ เช่น '1st', 'อันดับ 3', '2', 'third' (จะไม่รองรับคำภาษาอังกฤษเต็ม)
81
+ คืนเป็น int ถ้าพบเลข, ถ้าไม่พบคืน None
82
+ """
83
+ if pd.isna(value):
84
+ return None
85
+ s = str(value)
86
+ m = re.search(r'(\d+)', s)
87
+ if m:
88
+ try:
89
+ return int(m.group(1))
90
+ except ValueError:
91
+ return None
92
+ return None
93
+
94
+ def build_cleaned(df: pd.DataFrame,
95
+ name_key: str,
96
+ id_key: str,
97
+ ward_to_key: Dict[str, str],
98
+ flexible_match: bool) -> Tuple[pd.DataFrame, List[str]]:
99
+ """
100
+ สร้างตาราง cleaned: เก็บเฉพาะ NAME, ID, และคอลัมน์วอร์ดที่เลือก
101
+ แปลงค่าคอลัมน์วอร์ดเป็น int (ตัวเลขอันดับ) ถ้าทำไม่ได้จะเป็น NaN
102
+ """
103
+ messages = []
104
+ # หา NAME / ID
105
+ name_col = find_column(df, name_key.strip(), flexible_match)
106
+ id_col = find_column(df, id_key.strip(), flexible_match)
107
+ if name_col is None or id_col is None:
108
+ missing = []
109
+ if name_col is None: missing.append("NAME")
110
+ if id_col is None: missing.append("ID")
111
+ raise ValueError(f"หาไม่พบคอลัมน์บังคับ: {', '.join(missing)}")
112
+
113
+ keep_cols = [name_col, id_col]
114
+ renamed = {name_col: "NAME", id_col: "ID"}
115
+ # หาและแปลงคอลัมน์วอร์ด
116
+ for ward, key in ward_to_key.items():
117
+ key = key.strip()
118
+ if not key:
119
+ continue
120
+ col = find_column(df, key, flexible_match)
121
+ if col is None:
122
+ messages.append(f"⚠️ ไม่พบคอลัมน์ของวอร์ด '{ward}' จากคีย์ '{key}' (ข้ามวอร์ดนี้)")
123
+ continue
124
+ keep_cols.append(col)
125
+ renamed[col] = ward # เปลี่ยนชื่อคอลัมน์เป็นชื่อวอร์ดมาตรฐาน
126
+
127
+ # unique และรักษาลำดับ
128
+ seen = set()
129
+ keep_unique = []
130
+ for c in keep_cols:
131
+ if c not in seen:
132
+ seen.add(c)
133
+ keep_unique.append(c)
134
+
135
+ cleaned = df[keep_unique].rename(columns=renamed).copy()
136
+
137
+ # แปลงอันดับเป็น int
138
+ ward_cols = [c for c in cleaned.columns if c not in ("NAME", "ID")]
139
+ for c in ward_cols:
140
+ cleaned[c] = cleaned[c].apply(parse_rank).astype("Int64")
141
+
142
+ # จัดเรียงคอลัมน์
143
+ cleaned = cleaned[["NAME", "ID"] + ward_cols]
144
+
145
+ return cleaned, messages
146
+
147
+ def random_assign(cleaned: pd.DataFrame,
148
+ capacities: Dict[str, int],
149
+ seed: Optional[int] = None) -> Tuple[pd.DataFrame, pd.DataFrame, Dict[str, int]]:
150
+ """
151
+ สุ่มจัดสรรแบบรอบเลือกอันดับ: เริ่มจากอันดับ 1 → 2 → 3 → ...
152
+ - ในแต่ละอันดับและแต่ละวอร์ด: ถ้าเกิน capacity ที่เหลือ ให้สุ่มเลือก
153
+ - คืนผลลัพธ์: assignments, not_assigned, leftover_capacities
154
+ """
155
+ rng = np.random.default_rng(seed)
156
+ wards = [w for w in cleaned.columns if w not in ("NAME", "ID")]
157
+ # กำหนด capacity ที่ใช้จริง เฉพาะวอร์ดที่อยู่ในตาราง
158
+ cap = {w: int(capacities.get(w, 0)) for w in wards}
159
+
160
+ # เตรียมข้อมูลทำงาน
161
+ assigned = pd.Series(index=cleaned.index, data=pd.NA, dtype="object") # ชื่อวอร์ดที่ได้
162
+ choice_no = pd.Series(index=cleaned.index, data=pd.NA, dtype="Int64") # อันดับที่ได้
163
+
164
+ # หาค่า max rank ที่ปรากฏ (เช่น 1..6)
165
+ max_rank = 0
166
+ for w in wards:
167
+ max_w = cleaned[w].max(skipna=True)
168
+ if pd.notna(max_w):
169
+ max_rank = max(max_rank, int(max_w))
170
+
171
+ # วนทีละอันดับ
172
+ for r in range(1, max_rank + 1):
173
+ # ข้ามถ้าทุกวอร์ดเต็มแล้ว
174
+ if all(c <= 0 for c in cap.values()):
175
+ break
176
+ # สำหรับแต่ละวอร์ด
177
+ for w in wards:
178
+ if cap[w] <= 0:
179
+ continue
180
+ # ผู้สมัครที่ยังไม่ได้รับการจัดสรร และเลือกวอร์ดนี้ที่อันดับ r
181
+ mask = (assigned.isna()) & (cleaned[w] == r)
182
+ candidates = cleaned.index[mask].tolist()
183
+ if len(candidates) == 0:
184
+ continue
185
+ if len(candidates) <= cap[w]:
186
+ pick = candidates
187
+ else:
188
+ pick = list(rng.choice(candidates, size=cap[w], replace=False))
189
+ # ทำการจัดสรร
190
+ assigned.loc[pick] = w
191
+ choice_no.loc[pick] = r
192
+ cap[w] -= len(pick)
193
+
194
+ result = cleaned.copy()
195
+ result["AssignedWard"] = assigned
196
+ result["ChoiceNumber"] = choice_no
197
+
198
+ not_assigned = result[result["AssignedWard"].isna()].copy()
199
+ # แปลง NA ให้ดูง่ายขึ้นใน preview
200
+ result_preview = result.copy()
201
+ result_preview = result_preview.fillna("")
202
+
203
+ return result_preview, not_assigned.fillna(""), cap
204
+
205
+ def update_capacity_table(selected_wards: List[str]) -> pd.DataFrame:
206
+ rows = []
207
+ for w, th in WARD_CHOICES:
208
+ if selected_wards and w in selected_wards:
209
+ rows.append([w, th, 0])
210
+ if not rows:
211
+ return pd.DataFrame(columns=["Ward", "Thai Name", "Capacity"])
212
+ return pd.DataFrame(rows, columns=["Ward", "Thai Name", "Capacity"])
213
+
214
+ def update_mapping_table(selected_wards: List[str]) -> pd.DataFrame:
215
+ rows = [["NAME", ""], ["ID", ""]]
216
+ for w, th in WARD_CHOICES:
217
+ if selected_wards and w in selected_wards:
218
+ rows.append([w, ""])
219
+ return pd.DataFrame(rows, columns=["Field", "Your Column Header (exact or regex)"])
220
+
221
+ def on_clean(file, selected_wards, capacity_df, mapping_df, flexible):
222
+ if not selected_wards:
223
+ return gr.update(value="กรุณาเลือกวอร์ดอย่างน้อย 1", visible=True), None, None
224
+ # อ่านไฟล์
225
+ df, msg = read_table(file)
226
+ if df is None:
227
+ return gr.update(value=msg, visible=True), None, None
228
+
229
+ # ดึงชื่อคอลัมน์ที่ผู้ใช้ระบุ
230
+ mapping_df = mapping_df.copy()
231
+ mapping_df.columns = ["Field", "Key"]
232
+ mapping = {row["Field"]: str(row["Key"]).strip() for _, row in mapping_df.iterrows() if str(row["Field"]).strip()}
233
+
234
+ name_key = mapping.get("NAME", "")
235
+ id_key = mapping.get("ID", "")
236
+
237
+ if not name_key or not id_key:
238
+ return gr.update(value="กรุณาใส่หัวคอลัมน์ของ NAME และ ID", visible=True), None, None
239
+
240
+ ward_to_key = {}
241
+ for w in selected_wards:
242
+ ward_to_key[w] = mapping.get(w, "")
243
+
244
+ try:
245
+ cleaned, messages = build_cleaned(df, name_key, id_key, ward_to_key, bool(flexible))
246
+ except Exception as e:
247
+ return gr.update(value=f"❌ เกิดข้อผิดพลาด: {e}", visible=True), None, None
248
+
249
+ info = "✓ Cleaning สำเร็จ"
250
+ if messages:
251
+ info += "\n" + "\n".join(messages)
252
+
253
+ # เตรียมไฟล์ดาวน์โหลด
254
+ buf = BytesIO()
255
+ cleaned.to_csv(buf, index=False, encoding="utf-8-sig")
256
+ buf.seek(0)
257
+
258
+ return gr.update(value=info, visible=True), cleaned.head(30), ("cleaned.csv", buf)
259
+
260
+ def on_assign(file, selected_wards, capacity_df, mapping_df, flexible, seed):
261
+ # ต้อง clean ก่อน (เราอ่านไฟล์เดิมแล้ว clean ในฟังก์ชันนี้อีกครั้งเพื่อความแน่นอน)
262
+ status, cleaned_preview, cleaned_file = on_clean(file, selected_wards, capacity_df, mapping_df, flexible)
263
+ if cleaned_preview is None:
264
+ return status, None, None, None, None
265
+
266
+ # โหลด cleaned จากไฟล์ใน memory อีกครั้งเพื่อความแม่นยำ
267
+ # แต่เรามีเฉพาะ preview; จึง clean ซ้ำเพื่อได้ dataframe เต็ม
268
+ df, _ = read_table(file)
269
+ mapping_df = mapping_df.copy()
270
+ mapping_df.columns = ["Field", "Key"]
271
+ mapping = {row["Field"]: str(row["Key"]).strip() for _, row in mapping_df.iterrows() if str(row["Field"]).strip()}
272
+ name_key = mapping.get("NAME", "")
273
+ id_key = mapping.get("ID", "")
274
+ ward_to_key = {w: mapping.get(w, "") for w in selected_wards}
275
+ cleaned, _ = build_cleaned(df, name_key, id_key, ward_to_key, bool(flexible))
276
+
277
+ # capacities
278
+ if capacity_df is None or len(capacity_df) == 0:
279
+ return gr.update(value="กรุณากรอก capacity ก่อน", visible=True), None, None, None, None
280
+
281
+ # ทำให้แน่ใจว่ามีคอลัมน์ตามชื่อที่เราคาด
282
+ cap_df = capacity_df.copy()
283
+ cap_df.columns = ["Ward", "Thai Name", "Capacity"]
284
+ cap_df = cap_df[cap_df["Ward"].isin([c for c in cleaned.columns if c not in ("NAME", "ID")])]
285
+ cap_map = {}
286
+ for _, row in cap_df.iterrows():
287
+ try:
288
+ cap_map[str(row["Ward"])] = int(row["Capacity"])
289
+ except Exception:
290
+ cap_map[str(row["Ward"])] = 0
291
+
292
+ assigned, not_assigned, leftover = random_assign(cleaned, cap_map, seed=seed if seed not in (None, "") else None)
293
+
294
+ # สร้างไฟล์ด��วน์โหลด
295
+ out_all = BytesIO()
296
+ assigned.to_csv(out_all, index=False, encoding="utf-8-sig")
297
+ out_all.seek(0)
298
+
299
+ out_un = BytesIO()
300
+ not_assigned.to_csv(out_un, index=False, encoding="utf-8-sig")
301
+ out_un.seek(0)
302
+
303
+ leftover_text = "ความจุคงเหลือ:\n" + "\n".join([f"- {k}: {v}" for k, v in leftover.items()])
304
+
305
+ return status, assigned.head(30), ("assigned.csv", out_all), ("not_assigned.csv", out_un), leftover_text
306
+
307
+
308
+ with gr.Blocks(title=APP_TITLE) as demo:
309
+ gr.Markdown(f"# {APP_TITLE}")
310
+ gr.Markdown(DESCRIPTION)
311
+
312
+ with gr.Row():
313
+ file = gr.File(file_count="single", file_types=[".csv", ".xlsx"], label="อัปโหลดข้อมูลนักศึกษา/ผู้เรียน (.csv / .xlsx)")
314
+
315
+ with gr.Accordion("1) เลือกวอร์ดที่ต้องใช้", open=True):
316
+ selected_wards = gr.CheckboxGroup(
317
+ choices=[w for w, _ in WARD_CHOICES],
318
+ label="เลือกวอร์ด (เลือกได้หลายข้อ)",
319
+ value=["Medical", "Surgical"] # ค่าเริ่มต้นเล็กน้อย
320
+ )
321
+ gr.Markdown(
322
+ "คำแปล (อ้างอิง): " +
323
+ ", ".join([f"**{w}** = {th}" for w, th in WARD_CHOICES])
324
+ )
325
+
326
+ with gr.Accordion("2) กำหนด Capacity ต่อวอร์ด", open=True):
327
+ capacity_df = gr.Dataframe(
328
+ headers=["Ward", "Thai Name", "Capacity"],
329
+ value=[],
330
+ row_count=(0, "dynamic"),
331
+ col_count=3,
332
+ interactive=True,
333
+ wrap=True,
334
+ label="กรอกแค่แถวของวอร์ดที่เลือก"
335
+ )
336
+ selected_wards.change(fn=update_capacity_table, inputs=selected_wards, outputs=capacity_df)
337
+
338
+ with gr.Accordion("3) ระบุหัวคอลัมน์จริงในไฟล์ของคุณ", open=True):
339
+ gr.Markdown("ใส่ชื่อคอลัมน์ **จริง** ที่อยู่ในไฟล์ของคุณ (จะใช้แมตช์ตรงตัว หรือเปิดโหมดยืดหยุ่นก็ได้)")
340
+ mapping_df = gr.Dataframe(
341
+ headers=["Field", "Your Column Header (exact or regex)"],
342
+ value=[["NAME",""],["ID",""]],
343
+ row_count=(2, "dynamic"),
344
+ col_count=2,
345
+ interactive=True,
346
+ wrap=True
347
+ )
348
+ selected_wards.change(fn=update_mapping_table, inputs=selected_wards, outputs=mapping_df)
349
+ flexible = gr.Checkbox(label="เปิดโหมดจับคู่คอลัมน์แบบยืดหยุ่น (regex / contains)", value=True)
350
+
351
+ with gr.Row():
352
+ clean_btn = gr.Button("Clean data (ดูพรีวิว)")
353
+ assign_btn = gr.Button("Assign (สุ่มตามลำดับอันดับ)")
354
+
355
+ info = gr.Markdown(visible=False)
356
+ preview = gr.Dataframe(label="พรีวิวข้อมูลที่ผ่านการ clean (แสดงหัว 30 แถว)", visible=True)
357
+ cleaned_file = gr.File(label="ดาวน์โหลดไฟล์ cleaned.csv")
358
+ assigned_preview = gr.Dataframe(label="ตัวอย่างผลการจัดสรร (หัว 30 แถว)", visible=True)
359
+ assigned_file = gr.File(label="ดาวน์โหลดไฟล์ assigned.csv")
360
+ not_assigned_file = gr.File(label="ดาวน์โหลดไฟล์ not_assigned.csv")
361
+ leftover_text = gr.Textbox(label="สรุปความจุคงเหลือ", interactive=False)
362
+
363
+ seed = gr.Textbox(label="Random seed (เว้นว่างเพื่อให้สุ่มใหม่ทุกครั้ง)", value="")
364
+
365
+ clean_btn.click(
366
+ fn=on_clean,
367
+ inputs=[file, selected_wards, capacity_df, mapping_df, flexible],
368
+ outputs=[info, preview, cleaned_file]
369
+ )
370
+ assign_btn.click(
371
+ fn=on_assign,
372
+ inputs=[file, selected_wards, capacity_df, mapping_df, flexible, seed],
373
+ outputs=[info, assigned_preview, assigned_file, not_assigned_file, leftover_text]
374
+ )
375
+
376
+ if __name__ == "__main__":
377
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ gradio==4.44.0
2
+ pandas==2.2.2
3
+ openpyxl==3.1.5
4
+ numpy==2.0.2