四段程式、兩道關卡:值要對、記憶體要省。本站 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 分散式聚合的單機前傳。
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 倍)——讀檔瘦身是串流的好搭檔。點擊後出現漸進式說明:白話說明 → 說清楚一點 → 常見錯誤與考點。
點擊後出現漸進式說明。
五段,一段一段走完。右上角的「看位置」可以把這一段放回完整程式裡看。
total, count = 0.0, 0 # 合計與筆數——桌上只需要這兩個數字
一行指派兩個變數:total(金額合計,浮點起跳)與 count(非缺值筆數,整數起跳)。這就是串流演算法的精髓:不管檔案幾億列,桌上永遠只有兩個數字——所有疊讀完,答案就是 total ÷ count。
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 MB;dtype——直接告訴 pandas 這欄是 float64,跳過型態推斷;chunksize——最關鍵:read_csv 不再回傳 DataFrame,而是 TextFileReader 迭代器(實跑驗明正身),for 迴圈每繞一圈吐一疊 (100000, 1) 的 DataFrame。
x = c['amount'].dropna() # 洞不進帳——「忽略缺值」的直譯
題目說「忽略 amount 的缺值」——dropna() 就是這句話的直譯:把這疊的 NaN 全部丟掉再進帳。實跑四疊的非缺值筆數:88,000 / 94,000 / 97,000 / 49,500——各疊缺值率不同,這正是待會 B 選項翻車的伏筆。
total += x.sum(); count += x.count() # 抄兩個數字、把這疊還回去
sum() 是這疊的合計、count() 是這疊的非缺值筆數,+= 各自累加(分號讓兩句併一行)。抄完這兩個數字,這疊 DataFrame 就沒人引用了——記憶體回收,下一圈迭代器再吐新的一疊。這就是「mean 可合併」的具體動作:不搬資料、只搬小計——第 20 頁 Spark「資料不動、運算下鄉」的單機版。
result = total / count if count else float('nan') # 整體平均;count=0 回 nan 不噴錯
核心數學一句話:整體平均 = 全部合計 ÷ 全部筆數——不是「各疊平均再平均」。三元式是守門員:整欄都缺值(count=0)時直接回 nan,而不是撞上 ZeroDivisionError(實跑空檔案 → nan)。實跑結果:784.866 元,與全載版一字不差(差 < 1e-6)。
chunksize=100000 之後,read_csv 到底交出什麼東西?實跑驗明正身:
選項 B 把四疊各自的平均「再平均一次」。同一份資料實跑對帳:
把 D 的迴圈打開,看每讀完一疊,桌上的兩個數字與「目前平均」怎麼變:
四段程式各跑一遍,用 tracemalloc 量峰值記憶體——值對不對、桌子爆不爆,一次看清:
「能不能串流」的判準只有一條:統計量可不可以拆成小零件合併。考場小抄:
| 統計量 | 可合併嗎 | 怎麼拆 |
|---|---|---|
| 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 同一個靈魂
數學完全正確(實跑 784.866 元、本題的對帳基準),但整檔上桌——實跑峰值 35.5 MB、整檔在記憶體 119.1 MB;題目第一句就是「大到無法一次載入記憶體」——前提直接判死。真實場景幾億列時,這行的下場是 MemoryError。
記憶體及格(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 的老戲——等權只在各疊同筆數時僥倖答對,考場不能賭。
值正確(784.866、與 A 逐位相同),但 list() 把迭代器一口吞——4 疊全堆在記憶體、concat 再拷貝一份:實跑峰值 42.8 MB,比 A 還高。用了 chunksize 卻不放手任何一疊——串流的形、全載的心;記憶體效率這關直接出局。
三重實跑背書:值——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() # 抄兩個數字就放手
八題,全部都是本題的延伸。答錯會直接告訴你錯在哪。
「大檔案分塊處理」是中級科目二大數據處理分析與應用的資料工程招牌考點,chunksize、串流累加、平均的平均輪流出場。最常見的六種問法:① 給四段程式選「正確且記憶體有效率」(本題——值與效率雙關卡);② 問 chunksize 回傳什麼(迭代器,不是 DataFrame);③ 問「疊平均再平均」哪裡錯(權重——各疊筆數不同);④ 問怎麼修 B(加權 Σmean×n/Σn 或直接拆 sum/count);⑤ 問 usecols/dtype 的作用(讀檔瘦身、跳過推斷);⑥ 問哪些統計量能串流(可合併:sum/count/mean/var;不可:median——用草圖)。一句口訣:能拆 sum/count 才能串流;平均的平均是陷阱。
不是。實跑驗明正身:pd.read_csv('sales.csv', chunksize=100000) 回傳 TextFileReader——一個迭代器(閱讀器),不是資料本身。它的本事是「要一疊才讀一疊」:for 迴圈每繞一圈,才從硬碟讀下一個 10 萬列、生成一疊 (100000, 8) 的 DataFrame 交給你;上一疊沒人引用就被記憶體回收。所以整個過程中,記憶體裡永遠只掛著一疊——這就是「一次載入」與「串流」的分水嶺。實跑 35 萬列 → 4 疊(10萬×3+5萬)——注意末疊不滿一疊:chunksize 是上限不是保證,最後一疊裝剩下的。這個「不整除的尾巴」正是 B 選項等權平均翻車的第一個放大器。
因為平均是「合計 ÷ 筆數」——把四疊的平均再平均,等於假設每疊筆數一樣。本題實跑兩個放大器同時作用:末疊只有 5 萬列(其他疊 10 萬)、各疊缺值率又不同(非缺值 88,000/94,000/97,000/49,500)——結果金額最高的末疊(972.85)被灌了 1/4 的權重,B 高估 18.86 元。剛好相等的條件只有一個:每疊的非缺值筆數完全相同——考場上這是賭博不是答案。解藥兩帖:① 加權平均 Σ(mean_i×n_i)/Σn_i(實跑 784.866、與正解一字不差);② 更乾脆——別算疊平均,直接累加 (sum, count)(選項 D)。這正是第 13 頁 macro(等權)vs weighted(加權)在資料工程的重演。
實跑對帳:兩邊都是 784.866 元、差 < 1e-6——在報表精度下一字不差。數學上兩邊算的本來就是同一條式子:Σ(各疊合計) ÷ Σ(各疊筆數) ≡ 全體合計 ÷ 全體筆數——mean 是「可合併統計量」,拆開算與合著算恆等。浮點層面,加總順序不同可能差最後幾個 ulp(本題實跑連這都沒出現),對兩位小數的金額報表毫無影響。附帶一提:pandas 的 sum() 預設就會跳過 NaN,所以 D 的 dropna() 嚴格說是雙保險——但它讓「忽略缺值」的語意白紙黑字,且 count() 數的正是非缺值筆數,兩行對仗工整,是值得學的寫法。
因為 list(...) 把迭代器一口吞:強迫閱讀器把 4 疊全部讀出來、同時抓在一個清單裡——此刻記憶體已經等於整檔;接著 pd.concat(parts) 又要拷貝一份拼成大表——高峰時「4 疊+拼好的大表」同時在桌上。實跑峰值 42.8 MB,比 A 的 35.5 MB 還高。這是考題最愛的陷阱形狀:用了串流的語法、做著全載的事——判斷標準不是「有沒有 chunksize」,是「每疊用完有沒有放手」。D 的迴圈每圈只抄兩個數字就放手;C 的 list() 一疊都沒放。
不是正確性的必要條件(不加也算得對),但它們是「記憶體效率」的重要配角。usecols=['amount']:sales.csv 有 8 欄,字串欄(city/channel/product…)最吃記憶體——只讀需要的一欄,實跑整檔在記憶體從 119.1 MB 瘦成 2.7 MB(44 倍);串流時每疊同樣瘦 44 倍。dtype={'amount':'float64'}:跳過型態推斷——pandas 不用邊讀邊猜這欄是什麼,速度更穩,也避免髒資料造成各疊推斷不一致(一疊推成 float、另一疊混到字串變 object)的驚喜。兩個參數合起來是一句考場心法:讀檔就開始省——只讀需要的欄、先說好型態。
判準一條:能不能拆成「小零件」在疊之間合併。可以的:sum、count、min、max(小計直接併);mean(拆 (sum, count));var/std(拆 (sum, sum of squares, count) 三件套)。不行的:median 與任意分位數——它們是「位置統計量」,需要全體排隊才知道誰在中間;兩疊各自的中位數無法合併出全體中位數(想像一疊全是小額、一疊全是大額)。所以大數據世界對分位數只有兩條路:全體排序(貴),或草圖近似——這正是第 20 頁 Spark approxQuantile(relativeError 保證名次)的存在理由。考題若問「chunk 版的 median」——答案是「不能這樣算」,不是硬湊。
三條線。「加權的老戲」:B 的平均的平均=第 13 頁 macro F1(等權)在資料工程的翻版——weighted(按筆數加權)才對得上整體;同一個數學、兩個考場。「資料不動、運算下鄉」:D 的逐疊 (sum, count) 就是第 20 頁 Spark 分散式聚合的單機前傳——Spark 把疊換成分區、把 for 換成引擎排程,agg(F.avg) 底層做的就是這件事;而 median 不可合併,正是 approxQuantile 草圖的伏筆。「缺值三部曲」:第 21 頁 fillna 補洞(把洞補起來用)、本頁 dropna+count(把洞排除在帳外)——兩種缺值策略各有戰場;sum/count 預設 skipna 的家風則是兩頁共用的地基。