一、前言
在第十六天的學習中,我已經完成歷史紀錄查詢功能,能夠讀取 CSV 檔案中的健康資料,並透過 Streamlit 將紀錄呈現在網頁上。此外,也學習了依照日期排序,以及使用日期範圍篩選資料,讓使用者可以更方便地查找過去的健康紀錄。
然而,實際使用系統時,仍然可能遇到資料輸入錯誤的情況。例如,使用者可能將喝水量輸入成 1,800 毫升,事後才發現實際紀錄應該是 1,500 毫升;也可能不小心重複新增同一天的資料,或是希望移除不再需要的測試紀錄。
如果系統只能新增與查詢資料,就無法直接修正這些問題。因此,今天將學習如何建立資料修改與刪除功能,讓使用者能夠管理已經保存的健康紀錄。
今天的目標是完成健康習慣追蹤系統的基本資料管理流程,讓使用者不只能新增與查看資料,也能在需要時修正或移除紀錄。
二、資料修改與刪除的設計需求
在開始撰寫程式之前,需要先確認修改與刪除功能應該如何運作。

其中最重要的是,修改與刪除都必須針對正確的紀錄進行。如果系統沒有清楚辨識資料,就可能修改錯誤的內容,或意外刪除其他紀錄。
因此,本次練習會使用資料表中的列索引作為暫時的識別方式,讓使用者先選擇目標紀錄,再進行操作。這種方式適合目前的單機練習專案;若未來要建立更完整的系統,則建議為每筆紀錄增加唯一識別碼。
三、讀取並整理現有健康紀錄
在修改或刪除資料之前,必須先讀取原本保存的 CSV 檔案。
以下程式可以檢查檔案是否存在,並將資料讀取成 Pandas DataFrame。
import pandas as pd
from pathlib import Path
FILE_PATH = Path("health_records.csv")
def load_health_records():
if FILE_PATH.exists():
return pd.read_csv(FILE_PATH)
return pd.DataFrame(
columns=[
"date",
"water_ml",
"sleep_hours",
"exercise_minutes",
"vegetables_fruits",
"notes"
]
)
這段程式建立了 load_health_records() 函式,負責讀取健康紀錄。
如果 CSV 檔案存在,就讀取其中的資料;如果檔案尚未建立,就回傳一個具有固定欄位的空資料表。
透過這個函式,後續的修改與刪除功能就不必重複撰寫讀取檔案的程式碼。
四、建立資料保存函式
當資料被修改或刪除後,需要將更新結果寫回 CSV,才能讓變更持續保存。
def save_health_records(records):
records.to_csv(
FILE_PATH,
index=False,
encoding="utf-8-sig"
)
這個函式會將目前的 DataFrame 寫入 health_records.csv。
其中,index=False 可以避免將 Pandas 的列索引額外寫入檔案,而 encoding="utf-8-sig" 則有助於讓中文內容在常見的試算表軟體中正確顯示。
需要注意的是,這個函式會以目前的資料覆寫原有 CSV,因此呼叫之前必須先確認 DataFrame 內容正確。
五、建立資料修改功能
修改功能的主要流程是:先選擇一筆紀錄,再將原有內容放入輸入元件,讓使用者修改後重新保存。
以下使用 Streamlit 建立一個簡單的修改介面:
import streamlit as st
import pandas as pd
from pathlib import Path
FILE_PATH = Path("health_records.csv")
def load_health_records():
if FILE_PATH.exists():
return pd.read_csv(FILE_PATH)
return pd.DataFrame(
columns=[
"date",
"water_ml",
"sleep_hours",
"exercise_minutes",
"vegetables_fruits",
"notes"
]
)
def save_health_records(records):
records.to_csv(
FILE_PATH,
index=False,
encoding="utf-8-sig"
)
st.title("個人健康習慣追蹤系統")
st.header("修改健康紀錄")
records = load_health_records()
if records.empty:
st.info("目前沒有可供修改的健康紀錄。")
else:
selected_index = st.selectbox(
"請選擇要修改的紀錄",
options=records.index.tolist(),
format_func=lambda i: (
f"第 {i + 1} 筆|"
f"{records.loc[i, 'date']}"
)
)
selected_record = records.loc[selected_index]
with st.form("edit_record_form"):
record_date = st.text_input(
"紀錄日期(YYYY-MM-DD)",
value=str(selected_record["date"])
)
water = st.number_input(
"喝水量(毫升)",
min_value=0,
max_value=10000,
value=int(selected_record["water_ml"])
)
sleep = st.number_input(
"睡眠時間(小時)",
min_value=0.0,
max_value=24.0,
value=float(selected_record["sleep_hours"]),
step=0.5
)
exercise = st.number_input(
"運動時間(分鐘)",
min_value=0,
max_value=1440,
value=int(selected_record["exercise_minutes"])
)
vegetables_fruits = st.checkbox(
"今天有攝取蔬菜水果",
value=str(
selected_record["vegetables_fruits"]
).lower() == "true"
)
notes = st.text_area(
"備註",
value=str(selected_record["notes"])
if pd.notna(selected_record["notes"])
else ""
)
submitted = st.form_submit_button("保存修改")
if submitted:
try:
from datetime import date
date.fromisoformat(record_date)
records.loc[selected_index, "date"] = record_date
records.loc[selected_index, "water_ml"] = water
records.loc[selected_index, "sleep_hours"] = sleep
records.loc[selected_index, "exercise_minutes"] = exercise
records.loc[
selected_index,
"vegetables_fruits"
] = vegetables_fruits
records.loc[selected_index, "notes"] = notes.strip()
save_health_records(records)
st.success("健康紀錄已成功修改!")
st.dataframe(
records,
use_container_width=True
)
except ValueError:
st.error("日期格式錯誤,請輸入有效的 YYYY-MM-DD 日期。")
except OSError:
st.error("資料保存失敗,請檢查檔案與執行權限。")
這段程式示範了幾個重要概念:
st.selectbox():讓使用者選擇要修改的紀錄。
records.loc[]:根據指定的列索引存取或修改資料。
st.form():將多個輸入欄位組成一份表單。
date.fromisoformat():檢查日期是否符合有效的 ISO 日期格式。
save_health_records():將修改後的資料重新保存至 CSV。
本例的日期欄位使用文字輸入框,方便示範日期驗證;未來可以改用 st.date_input(),讓使用者透過日曆選擇日期。
另外,這段程式假設 CSV 的欄位名稱與資料格式都符合先前的設計。若檔案中存在空白數字、格式錯誤或其他不完整資料,還需要增加更完整的資料驗證處理。
六、建立資料刪除功能
除了修改資料,系統也需要讓使用者可以刪除不再需要的紀錄。
刪除資料的基本流程如下:
讀取現有健康紀錄。
選擇要刪除的紀錄。
顯示刪除確認按鈕。
使用者確認後,從 DataFrame 移除指定資料。
將剩餘資料重新保存至 CSV。
顯示操作結果。
以下是簡單的刪除功能範例:
import streamlit as st
import pandas as pd
from pathlib import Path
FILE_PATH = Path("health_records.csv")
def load_health_records():
if FILE_PATH.exists():
return pd.read_csv(FILE_PATH)
return pd.DataFrame()
def save_health_records(records):
records.to_csv(
FILE_PATH,
index=False,
encoding="utf-8-sig"
)
st.title("個人健康習慣追蹤系統")
st.header("刪除健康紀錄")
records = load_health_records()
if records.empty:
st.info("目前沒有可供刪除的健康紀錄。")
else:
selected_index = st.selectbox(
"請選擇要刪除的紀錄",
options=records.index.tolist(),
format_func=lambda i: (
f"第 {i + 1} 筆|"
f"{records.loc[i, 'date']}"
),
key="delete_record"
)
st.warning("刪除後,該筆紀錄將從 CSV 檔案中移除。")
if st.button("確認刪除"):
updated_records = records.drop(
index=selected_index
).reset_index(drop=True)
try:
save_health_records(updated_records)
st.success("健康紀錄已成功刪除!")
if updated_records.empty:
st.info("目前已沒有健康紀錄。")
else:
st.dataframe(
updated_records,
use_container_width=True
)
except OSError:
st.error("資料刪除失敗,請檢查檔案與執行權限。")
其中,drop() 可以移除指定索引的資料列,而 reset_index(drop=True) 則會重新整理索引,讓剩餘資料從零開始排列。
本例以按鈕作為簡化的刪除確認機制。若未來要提供更安全的操作體驗,可以加入額外的二次確認流程,降低誤刪資料的風險。
七、測試資料修改與刪除功能
完成程式後,需要透過實際操作確認資料是否能正確更新。
建議進行以下測試:
測試時,除了觀察網頁上的訊息,也應該重新讀取 health_records.csv,確認資料確實被修改或刪除。
此外,今天的修改與刪除範例是分開展示的功能片段。如果要整合進同一個 app.py,應將它們合併成同一份程式,並避免重複建立相同的函式或標題。
八、今日實作需要注意的地方
今天使用 DataFrame 的列索引來選擇紀錄,主要是為了讓初學者理解資料修改與刪除的基本原理。
不過,列索引並不是穩定的資料識別方式。當資料重新排序、篩選或重新載入時,索引可能改變,未來也可能造成選錯紀錄的問題。
因此,較完整的系統可以在每筆健康紀錄中加入唯一識別碼,例如 record_id,並使用這個識別碼來定位資料。
另外,直接覆寫 CSV 雖然容易實作,但如果寫入過程發生問題,可能影響原有檔案。因此,正式系統還需要考慮備份、錯誤處理與資料復原機制。
這些限制也說明了,資料管理功能不只是讓按鈕能夠運作,還需要確保操作對象正確、資料能持續保存,以及使用者不會因為誤操作而失去重要紀錄。
九、今日成果與學習心得
透過今天的學習,我進一步完成健康習慣追蹤系統的資料管理概念,理解如何使用 Pandas 修改指定資料列、移除不需要的紀錄,並將更新後的結果重新寫入 CSV。
目前系統已經逐步具備新增、查詢、修改與刪除四項基本資料操作能力,讓使用者可以管理自己的健康紀錄,而不必每次都直接開啟 CSV 檔案修改內容。
同時,我也發現資料操作必須搭配輸入驗證、操作確認與錯誤處理,才能降低錯誤資料與誤刪紀錄造成的影響。
十、下一步學習方向
Day 18 將學習「個人健康儀表板——呈現每日健康概況」。
接下來會把目前已經保存的健康資料轉換成更容易理解的資訊,運用 Streamlit 的數值指標、摘要文字與資料整理功能,呈現喝水量、睡眠時間、運動時間與其他健康習慣的概況,讓使用者能夠更快速地了解自己的生活紀錄,逐步完成健康習慣追蹤系統的第一階段展示功能。