一、前言
在前幾天的學習中,我已經完成健康紀錄的新增、查詢、修改與刪除,也學會使用 Pandas 計算平均喝水量與平均睡眠時間。
不過,除了喝水量與睡眠時間之外,運動也是個人健康習慣追蹤系統的重要指標。
假設我每天都有記錄運動時間,單純查看每筆資料,只能知道某一天運動了多久。如果將這些紀錄整理成每週統計,就可以進一步了解自己一週總共運動了多少分鐘、運動了幾次,以及不同週之間是否出現變化。
因此,今天的學習目標是使用 Python 的 Pandas 套件,將每日運動紀錄依照週次分組,計算每週的運動總時間與運動次數,並將結果呈現在 Streamlit 網頁中。
二、學習目標
認識時間序列資料與每週統計的概念。
使用 Pandas 將日期轉換成日期格式。
使用 groupby() 依照每週進行資料分組。
使用 sum() 計算每週運動總時間。
使用 count() 計算每週具有有效運動紀錄的天數。
將每週運動統計整合至健康習慣追蹤系統。
三、認識每週運動統計
在目前的系統中,每筆健康紀錄都包含日期與運動時間兩個重要欄位:

其中,date 表示紀錄日期,exercise_minutes 表示當天記錄的運動分鐘數。
假設以上五筆資料屬於同一週,則:
運動總時間:30 + 45 + 0 + 20 + 60 = 155 分鐘。
有運動的天數:4 天。
有效紀錄天數:5 天。
這裡需要區分「有運動的天數」與「有效紀錄天數」。如果當天填寫 0 分鐘,代表有完成紀錄,只是沒有運動;如果當天沒有留下任何紀錄,就不能直接認定當天完全沒有運動。
因此,本次實作會以有效紀錄作為統計基礎,避免將沒有資料的日期誤認為零分鐘。
四、使用 Pandas 整理日期資料
在進行每週統計之前,必須先確保日期欄位可以正確識別。
可以使用 pd.to_datetime() 將日期文字轉換成 Pandas 的日期格式:
import pandas as pd
records = pd.DataFrame({
"date": [
"2026-10-19",
"2026-10-20",
"2026-10-21",
"2026-10-22",
"2026-10-23"
],
"exercise_minutes": [30, 45, 0, 20, 60]
})
records["date"] = pd.to_datetime(
records["date"],
errors="coerce"
)
print(records)
執行後,date 欄位會轉換成日期時間格式。如果遇到無法辨識的日期,errors="coerce" 會將它轉成缺失值,方便後續排除。
接著,將運動時間轉換成數值:
records["exercise_minutes"] = pd.to_numeric(
records["exercise_minutes"],
errors="coerce"
)
這樣可以降低資料格式不一致對統計造成的影響。
五、使用 groupby() 進行每週分組
Pandas 的 groupby() 可以將符合相同條件的資料分成不同群組,再對各群組進行計算。
例如,想依照每週統計運動時間,可以先建立一個代表每週的欄位。
records["week"] = (
records["date"]
.dt.to_period("W-SUN")
.astype(str)
)
weekly_summary = records.groupby("week").agg(
total_exercise_minutes=("exercise_minutes", "sum"),
exercise_record_count=("exercise_minutes", "count")
)
print(weekly_summary)
這段程式使用 W-SUN,將星期一至星期日視為一週,並以星期日作為週期結束日。
agg() 可以一次執行多種統計:
sum:計算運動時間總和。
count:計算具有有效運動時間數值的紀錄筆數。
在這個例子中,exercise_record_count 代表有效運動紀錄筆數,不一定等於有運動的天數。若同一天有多筆紀錄,也會被計算多次。
如果希望計算有運動的天數,就必須先將同一天的紀錄整理成單日資料,再計算運動時間大於零的日期數量。
六、實作:建立每週運動分析網頁
接下來,將每週運動分析整合至 Streamlit。
請先備份原本的 app.py,再將以下程式碼放入檔案中。這個版本會讀取既有的 health_records.csv,並顯示每週運動總時間、有效紀錄筆數及有運動的天數。
from pathlib import Path
import pandas as pd
import streamlit as st
FILE_PATH = Path("health_records.csv")
st.set_page_config(
page_title="個人健康習慣追蹤系統",
page_icon="🌱",
layout="wide"
)
st.title("🌱 個人健康習慣追蹤系統")
st.subheader("每週運動分析")
if not FILE_PATH.exists():
st.info("目前尚無健康紀錄,請先新增每日健康資料。")
st.stop()
try:
records = pd.read_csv(FILE_PATH)
except (OSError, pd.errors.ParserError, UnicodeDecodeError):
st.error("讀取健康紀錄失敗,請檢查 CSV 檔案。")
st.stop()
if records.empty:
st.info("目前沒有可供分析的健康紀錄。")
st.stop()
required_columns = ["date", "exercise_minutes"]
missing_columns = [
column
for column in required_columns
if column not in records.columns
]
if missing_columns:
st.error(
"健康紀錄缺少必要欄位:" + ", ".join(missing_columns)
)
st.stop()
records["date"] = pd.to_datetime(
records["date"],
errors="coerce"
)
records["exercise_minutes"] = pd.to_numeric(
records["exercise_minutes"],
errors="coerce"
)
records = records.dropna(
subset=["date", "exercise_minutes"]
).copy()
records = records[
records["exercise_minutes"] >= 0
].copy()
if records.empty:
st.info("目前沒有有效的日期與運動時間資料。")
st.stop()
daily_summary = records.groupby(
records["date"].dt.normalize()
).agg(
daily_exercise_minutes=("exercise_minutes", "sum")
).reset_index(names="date")
daily_summary["week"] = (
daily_summary["date"]
.dt.to_period("W-SUN")
.astype(str)
)
weekly_summary = daily_summary.groupby("week").agg(
total_exercise_minutes=(
"daily_exercise_minutes",
"sum"
),
recorded_days=(
"date",
"count"
),
exercise_days=(
"daily_exercise_minutes",
lambda values: (values > 0).sum()
)
).reset_index()
latest_week = weekly_summary.iloc[-1]
col1, col2, col3 = st.columns(3)
with col1:
st.metric(
"最新一週運動總時間",
f"{latest_week['total_exercise_minutes']:.0f} 分鐘"
)
with col2:
st.metric(
"最新一週有運動的天數",
f"{latest_week['exercise_days']:.0f} 天"
)
with col3:
st.metric(
"最新一週有紀錄的天數",
f"{latest_week['recorded_days']:.0f} 天"
)
st.divider()
st.subheader("每週運動統計表")
st.dataframe(
weekly_summary.sort_values(
"week",
ascending=False
),
use_container_width=True,
hide_index=True
)
st.caption(
"每週以星期一至星期日計算;"
"未記錄的日期不會自動視為零分鐘。"
)
程式碼重點說明
daily_summary = records.groupby(
records["date"].dt.normalize()
).agg(
daily_exercise_minutes=("exercise_minutes", "sum")
).reset_index(names="date")
如果同一天有兩筆運動紀錄,例如上午運動 20 分鐘、晚上運動 30 分鐘,系統會先加總成當日 50 分鐘,再進行每週分析。
這可以避免同一天被重複計算成多個運動日。
total_exercise_minutes=(
"daily_exercise_minutes",
"sum"
)
這會將同一週內每天的運動時間加總,得到該週的運動總分鐘數。
exercise_days=(
"daily_exercise_minutes",
lambda values: (values > 0).sum()
)
這段程式會判斷每日運動時間是否大於零。如果大於零,就將該日計算為有運動的一天。
latest_week = weekly_summary.iloc[-1]
由於資料在這個版本中依照週期由早到晚建立,因此最後一筆是最新一週。若未來需要更完整的日期篩選功能,也可以再增加週次選擇器。
七、實際執行與測試
完成程式碼後,開啟終端機,進入專案資料夾,執行:
python -m streamlit run app.py
接著,可以利用以下測試案例確認統計是否正確。

測試時,建議先使用備份的 CSV 或另外建立測試檔案,避免修改原本的健康紀錄。
另外要注意,目前系統只統計有保存的日期。若某週只有兩天留下紀錄,系統就只能確認這兩天的情況,不能將其餘五天直接判定為沒有運動。
八、今日成果與學習心得
今天完成了每週運動分析的基本功能,學會使用 Pandas 的日期處理、groupby()、sum() 與 count() 等方法,將每日健康紀錄整理成每週統計結果。
透過這次實作,我了解到資料分析不只是進行加總,也需要先思考資料應該如何分組,以及每個統計數字實際代表什麼。
例如,「運動紀錄筆數」、「有紀錄的天數」與「有運動的天數」看起來相似,但實際上具有不同意義。如果沒有先定義清楚,就可能得到容易誤解的結果。
此外,每週運動總時間也可以作為後續趨勢分析的基礎。當系統持續累積資料,就能比較不同週的運動量,觀察自己的生活習慣是否有所變化。
九、下一步學習方向
第二十一天的主題是「健康資料視覺化——使用圖表呈現紀錄趨勢」。
下一階段將學習如何使用圖表呈現喝水量、睡眠時間與運動時間的變化,讓使用者不只看到統計數字,也能透過圖形更直觀地理解自己的健康習慣。