🗺️ AI 學習與考證地圖
中級科目二程式實戰 · 大檔案串流處理

塞不進記憶體的平均:
一次一疊、只記兩個數字

四段程式、兩道關卡:值要對、記憶體要省。本站 pandas 3.0.2 實跑(35 萬列 × 8 欄縮影檔):D 的串流累加與全載版一字不差(784.866 元),峰值記憶體 5.4 MB vs 35.5 MB(6.6 倍);B 的「疊平均再平均」高估 18.86 元——末疊 5 萬列被灌 1/4 權重(平均的平均 ≠ 整體平均);C 的 list()+concat 峰值 42.8 MB 比 A 還高。核心一句:mean 拆成 (sum, count) 就可合併——第 20 頁 Spark 分散式聚合的單機前傳。

閱讀模式

00題目

sales.csv 大到無法一次載入記憶體。若要忽略 amount 的缺值並求全檔案平均,下列程式何者正確且具記憶體效率?

# sales.csv 欄位很多,amount 為數值欄位
# 目標:計算所有非缺值 amount 的整體平均

先說:超厚帳本與一張小桌子

想像一本超厚的流水帳要算平均消費,但你的桌子(記憶體)很小、整本攤不開。聰明的做法:一次撕一疊下來,只抄兩個數字——「這疊的合計」與「這疊的筆數」——就把那疊還回去;全部撕完,合計 ÷ 筆數,答案跟整本攤開算的一字不差。這就是選項 D:撕一疊=chunksize、抄兩個數字=total += sum、count += count

三個失敗者各有死法:A 整本攤開——桌子直接爆(題目前提就是攤不開);C 把每疊都影印堆在桌上再合併——桌子照樣爆(實跑峰值還比 A 高);B 最陰險——每疊各算平均、最後再「平均這些平均」:大疊小疊一視同仁,末疊只有半疊厚、聲音卻跟全疊一樣大——實跑高估 18.86 元。能不能只抄兩個數字就放手,取決於統計量「可不可合併」——mean 可拆 (sum, count)、median 不行(所以第 20 頁的 Spark 得用草圖近似)。

三個先立好的事實(全部實跑): chunksize 讓 read_csv 回傳迭代器(TextFileReader)——一次吐一疊 DataFrame,用完即釋放。 平均的平均 ≠ 整體平均——每疊筆數不同時要加權:Σ(mean×n)/Σn 實跑 784.866、與正解一字不差。 usecols 只讀需要的欄:整檔 119.1 MB 瘦成 2.7 MB(44 倍)——讀檔瘦身是串流的好搭檔。

先點開看:chunksize 一次一疊平均的平均陷阱可合併統計量

不熟大檔案處理?你需要先認識下列名詞

點擊後出現漸進式說明:白話說明 → 說清楚一點 → 常見錯誤與考點。

大檔案的世界
塞不進記憶體chunksize 一次一疊迭代器不是表串流處理
缺值與計數
dropna 丟缺值sum 合計count 數非缺值累加器
平均的數學
平均的平均陷阱加權平均可合併統計量MapReduce 精神Spark 對照
效率的證據
usecols 只讀一欄dtype 指定型態concat 堆疊tracemalloc 實測峰值記憶體

不熟 Python?你需要先認識下列名詞

點擊後出現漸進式說明

起手式
import pandas as pd一行指派兩個具名參數
迴圈與累加
for c in 迴圈+= 累加分號一行兩句c['amount'] 選欄
收尾守門
if else 三元式float('nan')除以零守門
選項法醫室
B 的清單生成式C 的 list() 一口吞

01逐行拆解:正解 D 的五個零件

五段,一段一段走完。右上角的「看位置」可以把這一段放回完整程式裡看。

第 1 段兩個累加器:整個演算法的全部狀態
total, count = 0.0, 0   # 合計與筆數——桌上只需要這兩個數字

一行指派兩個變數:total(金額合計,浮點起跳)與 count(非缺值筆數,整數起跳)。這就是串流演算法的精髓:不管檔案幾億列,桌上永遠只有兩個數字——所有疊讀完,答案就是 total ÷ count。

相關名詞:一行指派兩個累加器

第 2 段read_csv 三參數:瘦身、定型、分疊
for c in pd.read_csv('sales.csv', usecols=['amount'],   # 只讀這一欄——整檔 119.1 → 2.7 MB(44 倍)
                     dtype={'amount': 'float64'},      # 指定型態——跳過推斷、不怕混型
                     chunksize=100000):                # 一次一疊 10 萬列——回傳迭代器

三個具名參數三件事:usecols——8 欄只讀 1 欄,實跑整檔記憶體 119.1 MB 瘦成 2.7 MBdtype——直接告訴 pandas 這欄是 float64,跳過型態推斷;chunksize——最關鍵:read_csv 不再回傳 DataFrame,而是 TextFileReader 迭代器(實跑驗明正身),for 迴圈每繞一圈吐一疊 (100000, 1) 的 DataFrame。

迭代器的本事:它不是「已經切好的一堆表」,是「要一疊才讀一疊」的閱讀器——上一疊沒人引用就釋放,記憶體永遠只掛著一疊。這正是「一次載入」與「串流」的分水嶺。

相關名詞:usecols 只讀一欄dtype 指定型態chunksize 一次一疊迭代器不是表具名參數

第 3 段dropna:這疊先丟缺值
    x = c['amount'].dropna()   # 洞不進帳——「忽略缺值」的直譯

題目說「忽略 amount 的缺值」——dropna() 就是這句話的直譯:把這疊的 NaN 全部丟掉再進帳。實跑四疊的非缺值筆數:88,000 / 94,000 / 97,000 / 49,500——各疊缺值率不同,這正是待會 B 選項翻車的伏筆。

相關名詞:dropna 丟缺值c['amount'] 選欄

第 4 段累加兩個數字,這疊功成身退
    total += x.sum(); count += x.count()   # 抄兩個數字、把這疊還回去

sum() 是這疊的合計、count() 是這疊的非缺值筆數,+= 各自累加(分號讓兩句併一行)。抄完這兩個數字,這疊 DataFrame 就沒人引用了——記憶體回收,下一圈迭代器再吐新的一疊。這就是「mean 可合併」的具體動作:不搬資料、只搬小計——第 20 頁 Spark「資料不動、運算下鄉」的單機版。

相關名詞:sum 合計count 數非缺值+= 累加分號一行兩句

第 5 段合計 ÷ 筆數+空檔案守門
result = total / count if count else float('nan')   # 整體平均;count=0 回 nan 不噴錯

核心數學一句話:整體平均 = 全部合計 ÷ 全部筆數——不是「各疊平均再平均」。三元式是守門員:整欄都缺值(count=0)時直接回 nan,而不是撞上 ZeroDivisionError(實跑空檔案 → nan)。實跑結果:784.866 元,與全載版一字不差(差 < 1e-6)

一句話記住本題:能拆 (sum, count) 才能串流;平均的平均是陷阱——大疊小疊要加權。選項 D 的每個零件都在這五段裡。

相關名詞:if else 三元式float('nan')除以零守門

02chunk 流水線觀察站:一次一疊是什麼感覺

chunksize=100000 之後,read_csv 到底交出什麼東西?實跑驗明正身:

互動實驗室:chunk 流水線觀察站迭代器 → 一疊一疊的 DataFrame——實跑身分驗證
本站實跑判決:加了 chunksize 的 read_csv 回傳 TextFileReader(迭代器)——不是 DataFrame、也不是「切好的一堆表」;for 每繞一圈才讀下一疊。35 萬列 ÷ 每疊 10 萬 → 4 疊(10萬 × 3 + 5萬)——末疊只有半疊厚,這個不整除的尾巴就是 B 選項的死穴。

相關名詞:chunksize 一次一疊迭代器不是表串流處理

03平均的平均驗屍間:B 高估 18.86 元的完整解剖

選項 B 把四疊各自的平均「再平均一次」。同一份資料實跑對帳:

互動實驗室:平均的平均驗屍間四疊成績單 → B 的等權平均 → 加權解藥——逐步對帳
B 的死因(實跑):四疊平均 [618.60, 746.04, 877.40, 972.85] 直接平均 = 803.72 元,高估 18.86 元。兩個放大器疊在一起: 末疊只有 5 萬列(且金額最高——年末衝刺)卻拿到 1/4 的權重; 各疊缺值率不同(非缺值 88,000/94,000/97,000/49,500)——就算列數相同,非缺值筆數也不同。平均的平均 ≠ 整體平均——這正是第 13 頁 macro(等權)vs weighted(加權)的老戲:B 犯的就是「不該用 macro 的地方用了 macro」。

相關名詞:平均的平均陷阱加權平均B 的清單生成式

04累加器追蹤器:D 逐疊逼近真值

把 D 的迴圈打開,看每讀完一疊,桌上的兩個數字與「目前平均」怎麼變:

互動實驗室:累加器追蹤器total / count / 目前平均——四疊逐站實跑
累計合計 total
累計筆數 count
目前平均
收斂軌跡(實跑):618.60 → 684.42 → 751.51 → 784.87——因為檔案越後面金額越大(年末衝刺),目前平均一路上修,讀完最後一疊正好落在真值。注意全程桌上永遠只有兩個數字——這就是 5.4 MB 峰值的秘密:狀態小到可以忽略,記憶體都花在「當前那一疊」上。

相關名詞:累加器sum 合計count 數非缺值

05記憶體對決:tracemalloc 四選項實測

四段程式各跑一遍,用 tracemalloc 量峰值記憶體——值對不對、桌子爆不爆,一次看清:

互動實驗室:記憶體對決A 35.5 / B 17.3 / C 42.8 / D 5.4 MB——實測峰值
對決成績單(實跑峰值):A 35.5 MB(值對、桌子爆——與前提矛盾);B 17.3 MB(桌子及格、數學不及格);C 42.8 MB——比 A 還高(chunk 清單+concat 拷貝,雙倍搬運);D 5.4 MB(值對、桌子最小——6.6 倍優勢)。誠實聲明:本站 35 萬列只是縮影(14.8 MB 檔案當然塞得進)——重點是比例:列數放大一百倍,A/C 跟著爆一百倍,D 的峰值幾乎不動(永遠一疊+兩個數字)。

相關名詞:tracemalloc 實測峰值記憶體concat 堆疊C 的 list() 一口吞

06考場加碼:可合併統計量速查表與 Spark 對照

「能不能串流」的判準只有一條:統計量可不可以拆成小零件合併。考場小抄:

統計量可合併嗎怎麼拆
sum / count / min / max○ 直接合併各疊小計再 sum/取 min、max
mean(本題)○ 拆零件(sum, count) 各自累加 → 最後相除
var / std○ 拆零件(sum, sum of squares, count) 三件套
median / 分位數× 不可精確合併要全體排隊——大數據用草圖近似(第 20 頁 approxQuantile)
# 同一件事的 Spark 方言(第 20 頁)——引擎自動幫你做 D
df = spark.read.csv('sales.csv', header=True, inferSchema=True)   # 分散式讀檔
df.agg(F.avg('amount'))   # 各節點局部 (sum, count)、再合併——與本題 D 同一個靈魂
串起系列:本題 D 就是單機版的「資料不動、運算下鄉」——第 20 頁 Spark 分散式聚合的前傳:Spark 把「疊」換成「節點上的分區」、把你的 for 迴圈換成引擎排程,數學核心一模一樣(局部 (sum, count) → 合併)。而 median 不可合併正是第 20 頁 approxQuantile 存在的理由——要精確就得全體排隊,塞不進記憶體時只能草圖近似。B 的翻車則是第 13 頁 macro vs weighted 在資料工程的重演:等權平均只在「各組同筆數」時僥倖等於整體平均——考場上永遠選加權(或直接拆 sum/count)

相關名詞:可合併統計量MapReduce 精神Spark 對照

07四個選項收工

Adf = pd.read_csv('sales.csv'); result = df['amount'].mean()值對、前提爆

數學完全正確(實跑 784.866 元、本題的對帳基準),但整檔上桌——實跑峰值 35.5 MB、整檔在記憶體 119.1 MB;題目第一句就是「大到無法一次載入記憶體」——前提直接判死。真實場景幾億列時,這行的下場是 MemoryError。

Bmeans = [各疊平均]; result = sum(means) / len(means)平均的平均

記憶體及格(17.3 MB),數學不及格:四疊平均 [618.60, 746.04, 877.40, 972.85] 等權平均 = 803.72 元、高估 18.86 元——末疊 5 萬列被灌 1/4 權重、各疊缺值數也不同。解藥是加權:Σ(mean×n)/Σn = 784.866 與正解一字不差。第 13 頁 macro/weighted 的老戲——等權只在各疊同筆數時僥倖答對,考場不能賭。

Cparts = list(chunks); result = pd.concat(parts)['amount'].mean()假串流

值正確(784.866、與 A 逐位相同),但 list() 把迭代器一口吞——4 疊全堆在記憶體、concat 再拷貝一份:實跑峰值 42.8 MB,比 A 還高。用了 chunksize 卻不放手任何一疊——串流的形、全載的心;記憶體效率這關直接出局。

D逐疊累加 total/count,最後 total / count(含空檔守門)正確

三重實跑背書:——784.866 元、與全載版一字不差(差 < 1e-6)記憶體——峰值 5.4 MB(A 的 1/6.6,靠 usecols 再瘦 44 倍);穩健——dropna 忽略缺值、count=0 守門回 nan。核心是 mean 拆成 (sum, count) 可合併——每疊抄兩個數字就放手,檔案再大桌子永遠不爆。

回到題目:現在再作答一次

再看一次同一道題。這次你手上有一句口訣了:能拆 sum/count 才能串流;平均的平均是陷阱

for c in pd.read_csv('sales.csv', usecols=['amount'], chunksize=100000):   # 一次一疊
    x = c['amount'].dropna(); total += x.sum(); count += x.count()   # 抄兩個數字就放手

08自我檢測

八題,全部都是本題的延伸。答錯會直接告訴你錯在哪。

09重點整理

  1. 題目兩道關卡:值要對(忽略缺值的整體平均)+記憶體要省(塞不進就不能全載)——只有 D 同時過關。
  2. 正確答案 D:chunksize 逐疊讀、每疊 dropna 後累加 (sum, count)、最後 total/count+空檔守門——實跑 784.866 元、與全載版一字不差(差 < 1e-6)。
  3. chunksize 的身分(實跑):read_csv 回傳 TextFileReader 迭代器——要一疊才讀一疊;35 萬列 → 4 疊(10萬×3+5萬),每疊是 DataFrame。
  4. A 的死因:值對(它是對帳基準)但整檔上桌——峰值 35.5 MB、整檔在記憶體 119.1 MB;題目前提「塞不進」直接判死。
  5. B 的死因(實跑):平均的平均 = 803.72 元、高估 18.86 元——四疊平均 [618.60/746.04/877.40/972.85] 等權,末疊 5 萬列被灌 1/4 權重、各疊非缺值數(88,000/94,000/97,000/49,500)也不同。
  6. B 的解藥:加權 Σ(mean×n)/Σn = 784.866——與正解一字不差;第 13 頁 macro vs weighted 的老戲
  7. C 的死因(實跑):list() 一口吞+concat 拷貝——峰值 42.8 MB 比 A 還高;串流的形、全載的心。
  8. D 的效率(實跑):峰值 5.4 MB——A 的 1/6.6;usecols 只讀一欄再瘦 44 倍(119.1 → 2.7 MB);dtype 指定跳過推斷。
  9. 收斂軌跡(實跑):618.60 → 684.42 → 751.51 → 784.87——桌上永遠只有 total 與 count 兩個數字。
  10. 可合併統計量:sum/count/min/max 直接併;mean 拆 (sum,count);var 拆三件套;median 不可精確合併——第 20 頁 approxQuantile 草圖的存在理由。
  11. Spark 對照:df.agg(F.avg('amount')) = 引擎自動版的 D——局部 (sum, count) 再合併;本題是「資料不動、運算下鄉」的單機前傳。
  12. 考場口訣能拆 sum/count 才能串流;平均的平均是陷阱——大疊小疊要加權
完整程式碼