deneve07 commited on
Commit
664fc5f
·
verified ·
1 Parent(s): c169c78

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +90 -88
app.py CHANGED
@@ -6,7 +6,37 @@ from openpyxl import Workbook
6
  from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
7
  from openpyxl.utils import get_column_letter
8
 
9
- # --- 1. 輔助函式:劑量排序邏輯 ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  def parse_dosage_to_numeric(dose_str):
11
  if not isinstance(dose_str, str):
12
  return 0.0
@@ -18,87 +48,57 @@ def parse_dosage_to_numeric(dose_str):
18
  return val
19
  return 0.0
20
 
21
- # --- 2. 讀取網頁上傳的資料並更新選項 (動態載入) ---
22
- def process_uploaded_file(file_info):
23
- if file_info is None:
24
- return pd.DataFrame(), gr.update(choices=[]), "請上傳健保申報量資料。"
25
-
26
- try:
27
- # 支援讀取 CSV 或 Excel
28
- if file_info.name.endswith('.csv'):
29
- df_raw = pd.read_csv(file_info.name)
30
- else:
31
- df_raw = pd.read_excel(file_info.name)
32
-
33
- df_raw.columns = df_raw.columns.str.strip()
34
-
35
- # 確保必要欄位存在並去除空白
36
- for col in ['成分', '劑型', '劑量', '廠商', '年度']:
37
- if col in df_raw.columns:
38
- df_raw[col] = df_raw[col].astype(str).str.strip()
39
-
40
- # 抓取所有成分
41
- if '成分' not in df_raw.columns:
42
- return pd.DataFrame(), gr.update(choices=[]), "❌ 檔案格式錯誤,找不到「成分」欄位。"
43
-
44
- all_comps = sorted(df_raw['成分'].dropna().unique())
45
- return df_raw, gr.update(choices=all_comps), f"✅ 資料載入成功!共 {len(df_raw)} 筆資料,發現 {len(all_comps)} 種成分。"
46
-
47
- except Exception as e:
48
- return pd.DataFrame(), gr.update(choices=[]), f"❌ 載入失敗:{str(e)}"
49
-
50
- # --- 3. 動態搜尋成分 ---
51
- def update_component_choices(search_text, df_raw):
52
- # 若還沒上傳資料,回傳空選項
53
- if df_raw is None or df_raw.empty:
54
  return gr.update(choices=[])
55
-
56
- all_comps = sorted(df_raw['成分'].dropna().unique())
57
  if not search_text or search_text.strip() == "":
58
- return gr.update(choices=all_comps)
59
 
60
  search_text = search_text.strip().lower()
61
- filtered = [c for c in all_comps if search_text in c.lower()]
62
  return gr.update(choices=filtered)
63
 
64
- # --- 4. 產出完全對齊範本的標準化 Excel ---
65
- def generate_standard_excel(selected_components, df_raw):
66
- if df_raw is None or df_raw.empty:
67
- return None, "❌ 請先在上方上傳並載入您的資料檔案"
68
  if not selected_components:
69
  return None, "❌ 請至少勾選一個成分品項!"
70
 
71
- # 篩選資料
72
- df_filtered = df_raw[df_raw['成分'].isin(selected_components)].copy()
73
  if df_filtered.empty:
74
  return None, "❌ 找不到相關資料!"
75
 
76
- # 如果有 數量(顆) 欄位就用它,不然找包含數量的欄位
77
  qty_col = '數量(顆)' if '數量(顆)' in df_filtered.columns else [col for col in df_filtered.columns if '數量' in col][0]
78
 
79
- # 樞紐分析
80
  pivot_df = df_filtered.groupby(['成分', '劑型', '劑量', '廠商', '年度'])[qty_col].sum().unstack(fill_value=0)
81
 
 
82
  for year_col in ['2022年', '2023年', '2024年']:
83
  if year_col not in pivot_df.columns:
84
  pivot_df[year_col] = 0
85
 
86
  pivot_df = pivot_df.reindex(columns=['2022年', '2023年', '2024年']).reset_index()
87
 
88
- # 排序:劑量由小到大 -> 2024年數量由大到小
89
  pivot_df['dose_numeric'] = pivot_df['劑量'].apply(parse_dosage_to_numeric)
90
  pivot_df = pivot_df.sort_values(by=['dose_numeric', '2024年'], ascending=[True, False]).drop(columns=['dose_numeric'])
91
 
92
- # 建立 Excel
93
  wb = Workbook()
94
  ws = wb.active
95
  ws.title = "廠商排名報表"
96
  ws.views.sheetView[0].showGridLines = True
97
 
 
98
  font_family = "微軟正黑體"
99
- header_fill = PatternFill(start_color="1F497D", end_color="1F497D", fill_type="solid")
100
- subtotal_fill = PatternFill(start_color="DCE6F1", end_color="DCE6F1", fill_type="solid")
101
- total_fill = PatternFill(start_color="B8CCE4", end_color="B8CCE4", fill_type="solid")
102
 
103
  header_font = Font(name=font_family, size=12, bold=True, color="FFFFFF")
104
  data_font = Font(name=font_family, size=12)
@@ -111,7 +111,7 @@ def generate_standard_excel(selected_components, df_raw):
111
  thin_side = Side(border_style="thin", color="D9D9D9")
112
  cell_border = Border(left=thin_side, right=thin_side, top=thin_side, bottom=thin_side)
113
 
114
- # 表頭
115
  headers = ["成分", "劑型", "劑量", "廠商", "2022年\n數量", "2023年\n數量", "2024年\n數量", "2024年\n占比(%)"]
116
  ws.append(headers)
117
  ws.row_dimensions[1].height = 30
@@ -130,6 +130,7 @@ def generate_standard_excel(selected_components, df_raw):
130
 
131
  current_row = 2
132
 
 
133
  for dose in unique_doses:
134
  dose_group = pivot_df[pivot_df['劑量'] == dose]
135
  dose_2024_sum = dose_group['2024年'].sum()
@@ -138,7 +139,7 @@ def generate_standard_excel(selected_components, df_raw):
138
  dose_subtotal_2023 = 0
139
  dose_subtotal_2024 = 0
140
 
141
- is_first_row = True # 用來判斷是否為該劑量的第一家廠商
142
 
143
  for _, row in dose_group.iterrows():
144
  qty_2022 = row['2022年']
@@ -146,13 +147,14 @@ def generate_standard_excel(selected_components, df_raw):
146
  qty_2024 = row['2024年']
147
  ratio = (qty_2024 / dose_2024_sum) if dose_2024_sum > 0 else 0.0
148
 
149
- # 【重要排版優化】:若是同劑量的第二家廠商以上,成分、劑型、劑量直接留白,對齊範本!
150
  c_val = row['成分'] if is_first_row else ""
151
  f_val = row['劑型'] if is_first_row else ""
152
  d_val = row['劑量'] if is_first_row else ""
153
 
154
  ws.append([c_val, f_val, d_val, row['廠商'], qty_2022, qty_2023, qty_2024, ratio])
155
 
 
156
  for col_idx in range(1, 9):
157
  cell = ws.cell(row=current_row, column=col_idx)
158
  cell.font = data_font
@@ -163,18 +165,18 @@ def generate_standard_excel(selected_components, df_raw):
163
  cell.alignment = left_align
164
  elif col_idx in [5, 6, 7]:
165
  cell.alignment = right_align
166
- cell.number_format = '#,##0'
167
  elif col_idx == 8:
168
  cell.alignment = right_align
169
- cell.number_format = '0.0%'
170
 
171
  dose_subtotal_2022 += qty_2022
172
  dose_subtotal_2023 += qty_2023
173
  dose_subtotal_2024 += qty_2024
174
  current_row += 1
175
- is_first_row = False # 寫完第一筆後改為 False
176
 
177
- # 寫入合計列
178
  ws.append(["", "", f"{dose} 合計", "", dose_subtotal_2022, dose_subtotal_2023, dose_subtotal_2024, 1.0])
179
  for col_idx in range(1, 9):
180
  cell = ws.cell(row=current_row, column=col_idx)
@@ -195,7 +197,7 @@ def generate_standard_excel(selected_components, df_raw):
195
  grand_total_2024 += dose_subtotal_2024
196
  current_row += 1
197
 
198
- # 寫入總計列
199
  ws.append(["", "", "總計", "", grand_total_2022, grand_total_2023, grand_total_2024, 1.0])
200
  for col_idx in range(1, 9):
201
  cell = ws.cell(row=current_row, column=col_idx)
@@ -224,10 +226,10 @@ def generate_standard_excel(selected_components, df_raw):
224
  max_len = line_len
225
  ws.column_dimensions[col_letter].width = max(max_len + 4, 12)
226
 
227
- # 版面與列印設定
228
  ws.sheet_properties.pageSetUpPr.fitToPage = True
229
- ws.page_setup.fitToWidth = 1
230
- ws.page_setup.fitToHeight = 0
231
  ws.page_margins.top = 2.7 / 2.54
232
  ws.page_margins.bottom = 2.5 / 2.54
233
  ws.page_margins.left = 1.5 / 2.54
@@ -235,66 +237,66 @@ def generate_standard_excel(selected_components, df_raw):
235
  ws.page_margins.header = 1.5 / 2.54
236
  ws.page_margins.footer = 1.0 / 2.54
237
 
 
238
  comp_names = "、".join(df_filtered['成分'].unique())
239
  form_mapping = {"注射劑": "Inj.", "一般錠劑膠囊劑": "Tab./Cap.", "膜衣錠": "F.C. Tab.", "膠囊劑": "Cap.", "錠劑": "Tab."}
240
  unique_forms = df_filtered['劑型'].unique()
241
  form_abbr = f" {form_mapping.get(unique_forms[0], unique_forms[0])}" if len(unique_forms) == 1 else ""
242
  doses_str = "、".join(unique_doses)
243
 
 
244
  header_string = f"&16&B{comp_names}{form_abbr} {doses_str}廠商申報量排名"
245
  ws.oddHeader.center.text = header_string
246
  ws.oddFooter.left.text = "&12中央健康保險署 政府資料開放平台 2024年資料"
247
  ws.oddFooter.right.text = "&12https://data.gov.tw/dataset/22131"
 
248
  ws.page_setup.scaleWithDoc = True
249
  ws.page_setup.alignWithMargins = True
250
 
 
251
  output_filename = "Standardized_Report.xlsx"
252
  wb.save(output_filename)
253
 
254
- return output_filename, f"🎉 成功!已匯出報表。"
255
 
256
- # --- 5. 全新:包含「檔案上傳」網頁介面 ---
257
  with gr.Blocks(title="健保申報量標準化 Excel 產出工具") as demo:
258
  gr.Markdown("# 💊 健保申報量標準化 Excel 自動化產出工具")
 
259
 
260
- # 建立一個隱藏的狀態儲存區,用來暫存您上傳的資料表
261
- df_state = gr.State()
262
-
263
  with gr.Row():
264
  with gr.Column(scale=1):
265
- gr.Markdown("### 📥 1 步:上傳您的健保資料")
266
- file_input = gr.File(label="請上傳 2022-2024年申報量 (CSV或Excel檔)", file_types=[".csv", ".xlsx"])
267
- load_status = gr.Textbox(label="資料載狀態", interactive=False)
 
 
 
268
 
269
- gr.Markdown("### 🔍 第 2 步:搜尋並勾選成分")
270
- search_input = gr.Textbox(label="入成分關鍵字(需先上傳資料)", placeholder="例如: Levofloxacin")
271
- component_choices = gr.CheckboxGroup(label="📋 勾選欲納入報表的完整成分品項", choices=[])
 
 
272
 
273
- submit_btn = gr.Button("🚀 第 3 步:產生標準化 Excel 報表", variant="primary")
274
 
275
  with gr.Column(scale=1):
276
- gr.Markdown("### 📤 產出結果區")
277
- status_output = gr.Textbox(label="系統狀態回報", interactive=False)
278
- file_output = gr.File(label="📥 下載產出的 Excel 檔案")
279
 
280
- # 事件綁定:上傳檔案後自動解析
281
- file_input.upload(
282
- fn=process_uploaded_file,
283
- inputs=file_input,
284
- outputs=[df_state, component_choices, load_status]
285
- )
286
-
287
- # 事件綁定:搜尋框過濾選項
288
  search_input.change(
289
  fn=update_component_choices,
290
- inputs=[search_input, df_state],
291
  outputs=component_choices
292
  )
293
 
294
- # 事件綁定:點擊產出報表
295
  submit_btn.click(
296
  fn=generate_standard_excel,
297
- inputs=[component_choices, df_state],
298
  outputs=[file_output, status_output]
299
  )
300
 
 
6
  from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
7
  from openpyxl.utils import get_column_letter
8
 
9
+ # --- 1. 固定讀取同目錄下的資料庫檔案 ---
10
+ DATA_FILE = "data.csv"
11
+
12
+ def load_fixed_database():
13
+ if os.path.exists(DATA_FILE):
14
+ try:
15
+ # 讀取固定的健保資料庫
16
+ df = pd.read_csv(DATA_FILE)
17
+ df.columns = df.columns.str.strip()
18
+ # 清理欄位前後空白
19
+ for col in ['成分', '劑型', '劑量', '廠商', '年度']:
20
+ if col in df.columns:
21
+ df[col] = df[col].astype(str).str.strip()
22
+ return df
23
+ except Exception as e:
24
+ print(f"讀取本地資料庫失敗: {e}")
25
+ return pd.DataFrame()
26
+ else:
27
+ print(f"錯誤:找不到固定資料庫檔案 {DATA_FILE},請確保檔案已上傳至同目錄。")
28
+ return pd.DataFrame()
29
+
30
+ # 程式啟動時載入全域資料
31
+ df_global = load_fixed_database()
32
+ if not df_global.empty:
33
+ ALL_COMPONENTS = sorted(df_global['成分'].dropna().unique())
34
+ INIT_MESSAGE = f"✅ 成功自動載入固定資料庫!共 {len(df_global)} 筆數據,包含 {len(ALL_COMPONENTS)} 種成分。"
35
+ else:
36
+ ALL_COMPONENTS = []
37
+ INIT_MESSAGE = "❌ 警告:未在目錄下找到 data.csv 檔案,請先將資料庫檔案上傳至 Space 檔案庫中。"
38
+
39
+ # --- 2. 輔助函式:劑量排序邏輯 ---
40
  def parse_dosage_to_numeric(dose_str):
41
  if not isinstance(dose_str, str):
42
  return 0.0
 
48
  return val
49
  return 0.0
50
 
51
+ # --- 3. 動態搜尋成分(如同 Excel 篩選功能) ---
52
+ def update_component_choices(search_text):
53
+ if not ALL_COMPONENTS:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  return gr.update(choices=[])
 
 
55
  if not search_text or search_text.strip() == "":
56
+ return gr.update(choices=ALL_COMPONENTS)
57
 
58
  search_text = search_text.strip().lower()
59
+ filtered = [c for c in ALL_COMPONENTS if search_text in c.lower()]
60
  return gr.update(choices=filtered)
61
 
62
+ # --- 4. 產出標準化 Excel 報表 ---
63
+ def generate_standard_excel(selected_components):
64
+ if df_global.empty:
65
+ return None, "❌ 系統未成功載入 data.csv 資料庫,請檢查 Space 中的檔案是否存在。"
66
  if not selected_components:
67
  return None, "❌ 請至少勾選一個成分品項!"
68
 
69
+ # 篩選選定的成分資料
70
+ df_filtered = df_global[df_global['成分'].isin(selected_components)].copy()
71
  if df_filtered.empty:
72
  return None, "❌ 找不到相關資料!"
73
 
74
+ # 自動判斷數量欄位名稱
75
  qty_col = '數量(顆)' if '數量(顆)' in df_filtered.columns else [col for col in df_filtered.columns if '數量' in col][0]
76
 
77
+ # 進行樞紐分析 (Pivot Table)
78
  pivot_df = df_filtered.groupby(['成分', '劑型', '劑量', '廠商', '年度'])[qty_col].sum().unstack(fill_value=0)
79
 
80
+ # 確保三年欄位皆完整
81
  for year_col in ['2022年', '2023年', '2024年']:
82
  if year_col not in pivot_df.columns:
83
  pivot_df[year_col] = 0
84
 
85
  pivot_df = pivot_df.reindex(columns=['2022年', '2023年', '2024年']).reset_index()
86
 
87
+ # 高階排序:劑量由小到大 -> 2024年數量由大到小
88
  pivot_df['dose_numeric'] = pivot_df['劑量'].apply(parse_dosage_to_numeric)
89
  pivot_df = pivot_df.sort_values(by=['dose_numeric', '2024年'], ascending=[True, False]).drop(columns=['dose_numeric'])
90
 
91
+ # 建立全新 Excel 工作簿
92
  wb = Workbook()
93
  ws = wb.active
94
  ws.title = "廠商排名報表"
95
  ws.views.sheetView[0].showGridLines = True
96
 
97
+ # 樣式與底色定義
98
  font_family = "微軟正黑體"
99
+ header_fill = PatternFill(start_color="1F497D", end_color="1F497D", fill_type="solid") # 標題深藍
100
+ subtotal_fill = PatternFill(start_color="DCE6F1", end_color="DCE6F1", fill_type="solid") # 合計淡藍
101
+ total_fill = PatternFill(start_color="B8CCE4", end_color="B8CCE4", fill_type="solid") # 總計中藍
102
 
103
  header_font = Font(name=font_family, size=12, bold=True, color="FFFFFF")
104
  data_font = Font(name=font_family, size=12)
 
111
  thin_side = Side(border_style="thin", color="D9D9D9")
112
  cell_border = Border(left=thin_side, right=thin_side, top=thin_side, bottom=thin_side)
113
 
114
+ # 寫入標題列 (欄位依序排列,包含換行)
115
  headers = ["成分", "劑型", "劑量", "廠商", "2022年\n數量", "2023年\n數量", "2024年\n數量", "2024年\n占比(%)"]
116
  ws.append(headers)
117
  ws.row_dimensions[1].height = 30
 
130
 
131
  current_row = 2
132
 
133
+ # 按劑量拆分群組寫入
134
  for dose in unique_doses:
135
  dose_group = pivot_df[pivot_df['劑量'] == dose]
136
  dose_2024_sum = dose_group['2024年'].sum()
 
139
  dose_subtotal_2023 = 0
140
  dose_subtotal_2024 = 0
141
 
142
+ is_first_row = True
143
 
144
  for _, row in dose_group.iterrows():
145
  qty_2022 = row['2022年']
 
147
  qty_2024 = row['2024年']
148
  ratio = (qty_2024 / dose_2024_sum) if dose_2024_sum > 0 else 0.0
149
 
150
+ # 同劑量重複廠商:前三欄留白不重複顯示
151
  c_val = row['成分'] if is_first_row else ""
152
  f_val = row['劑型'] if is_first_row else ""
153
  d_val = row['劑量'] if is_first_row else ""
154
 
155
  ws.append([c_val, f_val, d_val, row['廠商'], qty_2022, qty_2023, qty_2024, ratio])
156
 
157
+ # 設定一般資料列格式
158
  for col_idx in range(1, 9):
159
  cell = ws.cell(row=current_row, column=col_idx)
160
  cell.font = data_font
 
165
  cell.alignment = left_align
166
  elif col_idx in [5, 6, 7]:
167
  cell.alignment = right_align
168
+ cell.number_format = '#,##0' # 千分位、至個位數
169
  elif col_idx == 8:
170
  cell.alignment = right_align
171
+ cell.number_format = '0.0%' # 占比至小數點第一位
172
 
173
  dose_subtotal_2022 += qty_2022
174
  dose_subtotal_2023 += qty_2023
175
  dose_subtotal_2024 += qty_2024
176
  current_row += 1
177
+ is_first_row = False
178
 
179
+ # 寫入「劑量合計
180
  ws.append(["", "", f"{dose} 合計", "", dose_subtotal_2022, dose_subtotal_2023, dose_subtotal_2024, 1.0])
181
  for col_idx in range(1, 9):
182
  cell = ws.cell(row=current_row, column=col_idx)
 
197
  grand_total_2024 += dose_subtotal_2024
198
  current_row += 1
199
 
200
+ # 寫入總計
201
  ws.append(["", "", "總計", "", grand_total_2022, grand_total_2023, grand_total_2024, 1.0])
202
  for col_idx in range(1, 9):
203
  cell = ws.cell(row=current_row, column=col_idx)
 
226
  max_len = line_len
227
  ws.column_dimensions[col_letter].width = max(max_len + 4, 12)
228
 
229
+ # 專業列印與邊界設定 (公分轉英吋)
230
  ws.sheet_properties.pageSetUpPr.fitToPage = True
231
+ ws.page_setup.fitToWidth = 1 # 欄位強迫縮放放入單頁
232
+ ws.page_setup.fitToHeight = 0 # 高度自動延伸
233
  ws.page_margins.top = 2.7 / 2.54
234
  ws.page_margins.bottom = 2.5 / 2.54
235
  ws.page_margins.left = 1.5 / 2.54
 
237
  ws.page_margins.header = 1.5 / 2.54
238
  ws.page_margins.footer = 1.0 / 2.54
239
 
240
+ # 動態變數計算 (用於頁首)
241
  comp_names = "、".join(df_filtered['成分'].unique())
242
  form_mapping = {"注射劑": "Inj.", "一般錠劑膠囊劑": "Tab./Cap.", "膜衣錠": "F.C. Tab.", "膠囊劑": "Cap.", "錠劑": "Tab."}
243
  unique_forms = df_filtered['劑型'].unique()
244
  form_abbr = f" {form_mapping.get(unique_forms[0], unique_forms[0])}" if len(unique_forms) == 1 else ""
245
  doses_str = "、".join(unique_doses)
246
 
247
+ # 頁首頁尾設定 (&16&B 為 Excel 內建 16級字+粗體 語法)
248
  header_string = f"&16&B{comp_names}{form_abbr} {doses_str}廠商申報量排名"
249
  ws.oddHeader.center.text = header_string
250
  ws.oddFooter.left.text = "&12中央健康保險署 政府資料開放平台 2024年資料"
251
  ws.oddFooter.right.text = "&12https://data.gov.tw/dataset/22131"
252
+
253
  ws.page_setup.scaleWithDoc = True
254
  ws.page_setup.alignWithMargins = True
255
 
256
+ # 儲存 Excel
257
  output_filename = "Standardized_Report.xlsx"
258
  wb.save(output_filename)
259
 
260
+ return output_filename, f"🎉 成功!已為您匯出完整的廠商排名 Excel 報表。"
261
 
262
+ # --- 5. 簡潔 Gradio 使用者介面 (移除上傳區塊) ---
263
  with gr.Blocks(title="健保申報量標準化 Excel 產出工具") as demo:
264
  gr.Markdown("# 💊 健保申報量標準化 Excel 自動化產出工具")
265
+ gr.Markdown(f"**系統狀態:** {INIT_MESSAGE}")
266
 
 
 
 
267
  with gr.Row():
268
  with gr.Column(scale=1):
269
+ gr.Markdown("### 🔍步:輸入成分篩選(支援單方、複方模糊比對)")
270
+ search_input = gr.Textbox(
271
+ label="成分關鍵字",
272
+ placeholder="例如:Levofloxacin 或 Cilostazol",
273
+ value=""
274
+ )
275
 
276
+ component_choices = gr.CheckboxGroup(
277
+ label="📋 第二步:勾選欲納報表的成分品項 (可多選)",
278
+ choices=ALL_COMPONENTS,
279
+ value=[]
280
+ )
281
 
282
+ submit_btn = gr.Button("🚀 產生標準化 Excel 報表", variant="primary")
283
 
284
  with gr.Column(scale=1):
285
+ gr.Markdown("### 📥 第三步:下載產出檔案")
286
+ status_output = gr.Textbox(label="系統處理結果", interactive=False)
287
+ file_output = gr.File(label="點擊下載產出的 Excel 報表")
288
 
289
+ # 監聽搜尋輸入框變更,即時過濾全域資料庫的成分
 
 
 
 
 
 
 
290
  search_input.change(
291
  fn=update_component_choices,
292
+ inputs=search_input,
293
  outputs=component_choices
294
  )
295
 
296
+ # 點擊按鈕一鍵生成報表
297
  submit_btn.click(
298
  fn=generate_standard_excel,
299
+ inputs=component_choices,
300
  outputs=[file_output, status_output]
301
  )
302