背景:
我有這樣一個需要審核的飛書題目表,按日期分成多個sheet,有初審——復核——質檢三個環節,這三個環節是不同的同學在作業,并且領到同一個題目的人選是隨機的,也就是說,完成一道題的三個人之間完全是沒有任何聯系。
我現在需要指定sheet表的范圍,統計一段時間內各環節的數據指標:
初審同學,需要統計每個同學的做題總步數,有效做題步數,還有合格率。
復核同學,需要統計每個同學的做題總步數,有效做題步數,還有合格率。
質檢同學,需要統計每個質檢同學的檢查題目數量。一行就是一道題目,所以質檢同學只需要看寫著他名字的共有幾行就可以。
其中,有效做題步數是指質檢同學檢查后判定為正確的步數,比如一道題共25步,如果質檢認為初審同學做對15步,復核同學做對18步,那么初審有效步數 = 15,復核有效步數 = 18.
初審合格率 = 總初審有效步數 / 初審做題總步數;
復核合格率= 總復核有效步數 / 復核做題總步數;
編寫代碼:
理清上述邏輯之后,下面的工作直接交給GPT:
import pandas as pddef analyze_qc_records(file_path, sheet_names):initial_stats = {}relabel_stats = {}checker_counts = {}for sheet in sheet_names:df = pd.read_excel(file_path, sheet_name=sheet)for _, row in df.iterrows():# 初審init_name = row.get('初審同學姓名') # 取該行“初審同學姓名”,若是空值 (NaN) 則跳過。total_steps = row.get('單題步數', 0) or 0 # 當題目總步數(“單題步數”)缺失或為 NaN 時,設置為 0;否則讀取數值。init_valid = row.get('初審同學單題有效步數', 0) or 0 # 讀取該行“初審同學單題有效步數”if pd.notna(init_name):st = initial_stats.setdefault(init_name, {'總步數': 0, '有效步數': 0}) # 用 setdefault 保證 initial_stats[姓名] 一定存在,并初始化為 { '總步數':0, '有效步數':0 }st['總步數'] += total_steps # 每行累加“總步數”和“有效步數”st['有效步數'] += init_valid# 復核relabel_name = row.get('復核同學姓名')relabel_valid = row.get('復核同學單題有效步數', 0) or 0if pd.notna(relabel_name): # 如果不是缺失值st = relabel_stats.setdefault(relabel_name, {'總步數': 0, '有效步數': 0}) # 不管 relabel_name 之前在不在 relabel_stats 字典里,調用 setdefault 后,relabel_stats[relabel_name] 一定存在st['總步數'] += total_steps # 總步數依然用當行的 total_steps —— 即復核同學也「認定」原題的總步數與初審相同st['有效步數'] += relabel_valid# 質檢checker = row.get('質檢同學姓名')if pd.notna(checker): # 每遇到一行有“質檢同學姓名”,就讓對應姓名的計數 +1checker_counts[checker] = checker_counts.get(checker, 0) + 1# 構建 DataFrame(保留數值合格率)initial_df = pd.DataFrame([{'初審同學姓名': name,'總步數': stats['總步數'],'有效步數': stats['有效步數'],'合格率': stats['有效步數'] / stats['總步數'] if stats['總步數'] else 0} # 用“有效步數 ÷ 總步數” 得到一個 0–1 之間的小數;若總步數為 0 則直接賦 0,避免除零錯誤。for name, stats in initial_stats.items()])relabel_df = pd.DataFrame([{'復核同學姓名': name,'總步數': stats['總步數'],'有效步數': stats['有效步數'],'合格率': stats['有效步數'] / stats['總步數'] if stats['總步數'] else 0}for name, stats in relabel_stats.items()])checker_df = pd.DataFrame([{'質檢同學姓名': name, '檢查題目數': count}for name, count in checker_counts.items()])# 百分比格式化initial_df['合格率'] = initial_df['合格率'].map(lambda x: f"{x:.2%}")relabel_df['合格率'] = relabel_df['合格率'].map(lambda x: f"{x:.2%}")# 排序 初審/復核 按姓名字母序;質檢按檢查量從高到低。initial_df = initial_df.sort_values(by='初審同學姓名').reset_index(drop=True)relabel_df = relabel_df.sort_values(by='復核同學姓名').reset_index(drop=True)checker_df = checker_df.sort_values(by='檢查題目數', ascending=False).reset_index(drop=True)return initial_df, relabel_df, checker_dfif __name__ == '__main__':# 示例:讀取文件并指定 sheet 范圍file_path = '目錄/質檢記錄.xlsx'xls = pd.ExcelFile(file_path)# 假如想統計第 1 到第 3 張 sheet(索引從 0 開始)selected_sheets = xls.sheet_names[0:3]init_df, relabel_df, checker_df = analyze_qc_records(file_path, selected_sheets)# 打印結果print("初審同學統計:")print(init_df)print("\n復核同學統計:")print(relabel_df)print("\n質檢同學統計:")print(checker_df)# 如需導出到 Excel:with pd.ExcelWriter('目錄/質檢統計結果.xlsx') as writer:init_df.to_excel(writer, sheet_name='初審統計', index=False)relabel_df.to_excel(writer, sheet_name='復核統計', index=False)checker_df.to_excel(writer, sheet_name='質檢統計', index=False)print("統計已導出到 質檢統計結果.xlsx")
輸出結果:
結果會直接寫入XLSX文件保存。
并且初審/復核 按姓名字母序排列;質檢按檢查量從高到低排序。
截至目前圓滿實現了我的需求,完美!!
需要注意的點&容易困惑的代碼片段:
1.飛書必須導出為XLSX
因為CSV沒有多個sheet,無法實現跨多張表統計。如果導出成了CSV,你會驚喜地發現只剩下第一張表,后面的數據全丟了。
下載為CSV的后果…
2.if pd.notna(relabel_name),pd.notna是什么意思?
pd.notna(…) 的作用:
功能:判斷一個值是否 不是 “缺失值”(NaN / None / NaT 等)。
返回:布爾值——如果傳入的 relabel_name 既不是 NaN 也不是 None,則返回 True;否則返回 False。
為什么要用:在讀取 Excel 時,空單元格會被 Pandas 識別為 NaN。只有當姓名字段確實有值時,才要執行后續的統計操作;如果是空的,就跳過這一行,避免在字典里注冊一個空字符串或 NaN 作為鍵。
示例代碼:
import pandas as pdprint(pd.notna("Alice")) # True,說明 "Alice" 是個有效字符串
print(pd.notna(None)) # False,說明 None 是缺失值
print(pd.notna(float('nan'))) # False,NaN 也是缺失值
這點確實很好用,有的同學填表容易漏項,沒有把信息寫全,留出一些空值在表里,會導致統計數據出現異常,非常的棘手。
3.dict.setdefault(key, default) 的用法?
所屬類型:這是 Python 內置字典 (dict) 的一個方法。
功能:
如果字典中已經有了 key,就直接返回 dict[key];
如果沒有 key,就先把 key: default 這對鍵值對插入字典,然后返回這個 default。
為什么要用:
這樣寫可以保證:不管 relabel_name 之前在不在 relabel_stats 字典里,調用 setdefault 后,relabel_stats[relabel_name] 一定存在,且是一個形如 {‘總步數’: 0, ‘有效步數’: 0} 的初始結構。
之后就可以安心地做 st[‘總步數’] += total_steps、st[‘有效步數’] += relabel_valid這樣步數統計的操作,不用每次都寫一大段 “如果 key 不在,就先賦值” 的邏輯。
示例代碼:
d = {}
x = d.setdefault('張三', {'總步數': 0, '有效步數': 0})
# 這時 d == {'張三': {'總步數': 0, '有效步數': 0}}
# 變量 x 就是 {'總步數': 0, '有效步數': 0}# 再次調用
y = d.setdefault('張三', {'總步數': 100, '有效步數': 50})
# 因為 '張三' 已存在,d 不變,y 仍是原來的 {'總步數': 0, '有效步數': 0}print(d) # {'張三': {'總步數': 0, '有效步數': 0}}
那么回到我們解決問題的代碼:
結合第二點,這段代碼的作用是:
先用 pd.notna 確保 relabel_name 真有值。
再用 setdefault 拿到或初始化統計結構,省去了手寫“鍵不存在就先賦值”的繁瑣。
最后在 st 上累加步數,就完成了該同學在“初審”或者”復核“環節的統計。
這樣寫既簡潔又安全,不用擔心 KeyError 或把空姓名也算進去。
4.for name, stats in initial_stats.items() ])這個stats是什么?
initial_stats 是你在前面遍歷所有行時累積的一個字典,形如:
{"張三": {"總步數": 120, "有效步數": 110},"李四": {"總步數": 95, "有效步數": 90},...
}
initial_stats.items() 會返回一個由 (key, value) 對組成的可迭代對象,這里 key 就是每個同學的姓名,value 就是對應的那個小字典(包含 “總步數” 和 “有效步數” 兩個字段)。
所以在列表推導里:
for name, stats in initial_stats.items()
name —— 對應同學姓名(例如 “張三”)
stats —— 對應該同學的那個子字典(例如 {“總步數”: 120, “有效步數”: 110})
接下來你就可以用 stats[‘總步數’]、stats[‘有效步數’] 來取出這兩個值,計算合格率:
'合格率': stats['有效步數'] / stats['總步數'] if stats['總步數'] else 0
如果覺得 stats 這個名字不直觀,也可以任意改成別的,比如 data、counts、vals,邏輯完全一樣:它只是“每個同學對應的那份統計數據”的一個變量名。