正解 B 一句話三個零件:reader 是可迭代的 TextFileReader(不是 DataFrame)、每次迭代載入 100 萬筆的 DataFrame 分塊處理、usecols 與 category 再壓記憶體。本站 pandas 3.0.2 實跑(自造 3.6M 列 × 8 欄、238.9 MB):4 塊(100萬×3+60萬)、串流總和 5,401,597,601 與全載完全相等;記憶體帳——全載 1,304.8 MB vs 串流峰值 41.1 MB(縮 20 倍)、region 欄 category 縮 93 倍。A「reader 是完整 DataFrame」——實跑 TypeError: not subscriptable;C「等同 Spark」——同一顆行程循序跑 4 塊、單機串流不是分散式;D「region 全轉成 0」——category 是字典編碼、value_counts 與 object 版 array_equal=True、資訊零遺失。口訣:一次一塊、邊讀邊加;挑欄壓字省記憶體——單機串流不是 Spark。
工程師執行下列程式處理 50GB 檔案,敘述何者正確?
reader = pd.read_csv('logs.csv', chunksize=1_000_000, # 一次只讀 100 萬筆 usecols=['user_id','region','amount'], # 只挑三欄 dtype={'region': 'category'}) # 字串欄字典編碼 total = 0 # 帳本 for chunk in reader: # 一塊一塊接 total += chunk['amount'].sum()# 邊讀邊加、加完就放
50GB 的檔案、幾 GB 的記憶體——整缸水倒不進小杯子。這題的解法是把 read_csv 從「水庫模式」切成水龍頭模式:加了 chunksize,回來的 reader 就不是裝滿水的水庫(DataFrame),而是一顆水龍頭(TextFileReader)——本身沒有水,轉一下才流出一杯(100 萬筆的小 DataFrame)。for chunk in reader 就是「接一杯、記個帳、倒掉,再接下一杯」——帳房先生手上永遠只有一杯水和一本帳(total),50GB 流完、帳也記完了。另外兩顆旋鈕在幫杯子瘦身:usecols——8 欄的檔案只裝需要的 3 欄;dtype='category'——把「北區、南區……」這種重複幾十萬次的字串改成字典編碼(字典寫一次、格子裡放小代碼)。三個錯誤選項是三種誤會:A 把水龍頭當水庫直接舀水(當場 TypeError);C 以為水龍頭一轉就召喚出一排機器(chunksize 是單機省水術、不是 Spark 的分散式艦隊);D 以為字典編碼會把字全部塗掉變成 0(字典就在旁邊、一個字都沒丟)。
type(reader)=TextFileReader——舀水 reader['amount'] 當場 TypeError: 'TextFileReader' object is not subscriptable。② 記憶體帳:全載 8 欄 1,304.8 MB、串流峰值 41.1 MB——縮 20 倍;三件套單塊帳 369.1 → 104.0 → 16.2 MB。③ 串流總和與全載總和完全相等(5,401,597,601、整數元一字不差)——省的是記憶體、不是精度。點擊後出現漸進式說明:白話說明 → 說清楚一點 → 常見錯誤與考點。
點擊後出現漸進式說明。
五段,一段一段走完。右上角的「看位置」可以把這一段放回完整程式裡看。
import pandas as pd # 單機資料處理的老朋友 reader = pd.read_csv('logs.csv', chunksize=1_000_000, # 一次只讀 100 萬筆 usecols=['user_id','region','amount'], # 8 欄只挑 3 欄 dtype={'region': 'category'})# 字串欄字典編碼——三件套到齊
關鍵在 chunksize=1_000_000(底線只是千分位隔線、就是一百萬):這個參數一給,read_csv 的回傳從「水庫」變「水龍頭」——檔案一個位元組都還沒進記憶體,你拿到的是一顆待轉開的閥門。另外兩顆旋鈕先掛好:usecols 聲明「8 欄裡我只要 3 欄」(不要的欄連解析都省了);dtype={'region': 'category'} 聲明「這欄重複值超多、用字典編碼存」。本站的 logs.csv:3.6M 列 × 8 欄、238.9 MB(實跑自造、金額整數元)。
print(type(reader)) # TextFileReader——水龍頭不是水庫 total = 0 # 帳本只有一個數字 for chunk in reader: # 每次迭代:載入一塊 (1_000_000, 3) 的 DataFrame total += chunk['amount'].sum()# 這一塊記完帳、下一圈就被回收
type(reader) 實跑:pandas.io.parsers.readers.TextFileReader——B 的第一個零件驗明正身。for chunk in reader 每轉一圈,pandas 才從磁碟讀下一個 100 萬筆、做成一個小 DataFrame 交給你;chunk['amount'].sum() 把這塊的金額加進 total,然後這塊就功成身退、記憶體釋放。塊內是完整的 DataFrame——sum、groupby、篩選都能用(B 的第二個零件);塊與塊之間靠帳本銜接。
print(total) # 5,401,597,601——與全載 sum 完全相等(整數元一字不差) # 實跑塊帳:4 塊 = 100萬 × 3 + 最後一塊 60 萬(不滿照樣出貨) # 記憶體帳:全載 8 欄 1,304.8 MB vs 串流峰值 41.1 MB——縮 20 倍 # 三件套單塊帳:8 欄 369.1 → usecols 3 欄 104.0 → +category 16.2 MB
三本帳攤開:塊帳——3.6M 列 ÷ 100 萬=4 塊,前三塊整整 100 萬、最後一塊 60 萬(餘數塊不滿照樣交貨,別假設每塊等長)。總和帳——串流 total 與全載 df['amount'].sum() 完全相等:分批加總不損精度(金額整數元、一字不差)。記憶體帳——tracemalloc 實測:全載峰值 810.1 MB(deep 記憶體 1,304.8 MB)vs 串流峰值 41.1 MB;三件套逐層瘦身:8 欄 369.1 → 挑 3 欄 104.0 → region 換 category 16.2 MB——B 後半句「進一步降低記憶體」是可以量出來的。
# A 世界:reader['amount'] → TypeError: 'TextFileReader' object is not subscriptable # hasattr(reader, 'sum') → False——水龍頭本身沒有水、也沒有 sum # C 世界:os.getpid() 全程同一顆——4 塊在同一個 Python 行程裡循序處理 # chunksize 是單機省水術;分散式要換 Spark/Dask(第 20 頁本尊)
A 世界:把水龍頭當水庫舀——reader['amount'] 實跑 TypeError: 'TextFileReader' object is not subscriptable;它也沒有 .sum()(hasattr 實跑 False)。想用 DataFrame 的功能,得先 for 出一塊、或 get_chunk() 手動接一杯。C 世界:實跑在迴圈裡收集 os.getpid()——4 塊全程同一顆行程、一塊接一塊循序跑;chunksize 沒有叢集、沒有分散式、沒有「多台機器」——它是單機記憶體管理。真的分散式長什麼樣?第 20 頁的 Spark approxQuantile 才是本尊——兩者解的是同一類問題(資料比記憶體大),走的是完全不同的路。
# D 世界:categories 字典 ['中區','北區','南區','東區','離島']——值一個都沒變 # cat.codes 值域 0~4 是「內部代碼」;value_counts 與 object 版 array_equal=True # 一次性:迴圈跑完再 next(reader) → StopIteration——要重開水龍頭 # 50GB 換算:約 771 塊——峰值記憶體仍是每塊 16.2 MB 級 # 口訣:一次一塊、邊讀邊加;挑欄壓字省記憶體——單機串流不是 Spark
D 世界三連拆:.cat.categories 實跑印出完整字典 ['中區','北區','南區','東區','離島'];head(5) 印出來還是字串;value_counts、groupby 金額與 object 版分毫不差(array_equal=True)——「全部轉成數字 0」的世界不存在,cat.codes 的 0~4 是存放層的代碼、不是你的資料。兩個收尾知識點:一次性——reader 是迭代器,跑完再 next() 實跑 StopIteration(要再算一輪就重開一顆 reader);50GB 的帳——以實測每列 69.6 bytes 換算約 771 塊,一塊一塊流過去、峰值記憶體始終是十幾 MB 級——檔案大小跟記憶體占用徹底脫鉤,這正是整題的靈魂。
它是什麼、怎麼流、流完會怎樣——三面向逐一實跑:
type(reader)=pandas.io.parsers.readers.TextFileReader——不是 DataFrame;reader['amount'] 當場 TypeError: 'TextFileReader' object is not subscriptable、hasattr(reader, 'sum')=False(A 陣亡)。接水帳:4 塊=(1000000, 3) × 3 + (600000, 3)——每塊是貨真價實的 DataFrame(sum/groupby/篩選都能用)、最後一塊不滿照樣出貨。一次性:迴圈跑完再 next(reader) → StopIteration——水龍頭是流過就沒的迭代器,要再算一輪就重開一顆 reader。chunksize、usecols、category——一顆一顆旋鈕拆開量:
pd.read_csv('logs.csv') 峰值 810.1 MB(8 欄 deep 記憶體 1,304.8 MB);串流版全程峰值 41.1 MB——縮 20 倍。三件套單塊帳(100 萬列):8 欄全裝 369.1 MB → usecols 挑 3 欄 104.0 MB → region 換 category 16.2 MB——合計縮 23 倍;region 欄本身 88.7 → 0.95 MB(93 倍)。50GB 換算(以實測每列 69.6 bytes):約 771 塊——塊數變多、峰值不變:檔案大小與記憶體占用徹底脫鉤。「全部轉成數字 0、資訊遺失」——值、代碼、記憶體三面開庭:
.cat.categories 實跑 ['中區','北區','南區','東區','離島']、每個字串只存一次)+代碼欄(.cat.codes 值域 0~4、每格一個小整數)。對帳三連全過:head(5) 印出來還是字串(['中區','南區','南區','中區','北區'])、value_counts 與 object 版 array_equal=True、逐值比對 (astype(str)==object版).all()=True、groupby 金額分毫不差。D 把「內部存法」當成「資料被改」——代碼確實是數字、但那是倉庫的貨架編號,貨(你的字串)一件都沒少。「自動啟動多台機器、效果等同 Spark」——宣稱、真身、本尊三面對質:
os.getpid():4 塊全程同一顆 Python 行程、一塊處理完才輪下一塊(循序);chunksize 從頭到尾沒有啟動任何東西——沒有叢集、沒有第二台機器、連第二顆行程都沒有。它解決的是「記憶體不夠」;Spark 解決的是「一台機器不夠」——資料切成分區撒到多台 executor 平行算、由 driver 彙總(第 20 頁 approxQuantile 的地盤)。兩者常被一起考:單機先用 chunksize/Dask、資料量再上去才動用叢集——「效果等同 Spark」把梯子的兩階說成同一階。分塊處理的進階姿勢速查(全部本站實跑):
| 姿勢 | 寫法 | 本頁實跑 |
|---|---|---|
| 分塊加總 | total += chunk['amount'].sum() | 5,401,597,601 與全載完全相等 |
| 分塊分組聚合 | acc = acc.add(chunk.groupby(...).sum(), fill_value=0) | 五區金額與全載 groupby 分毫不差 |
| 手動接一杯 | reader.get_chunk(5) | 回 (5, 3) 的 DataFrame |
| 另一種開法 | iterator=True+get_chunk(n) | 與 chunksize 同族、自由控制杯量 |
# 陷阱①:pd.concat(list(reader))——把全部塊接回一張大表=自殺(記憶體原地爆回全載) # 陷阱②:平均值不能用「各塊平均再平均」——要累加 (sum, count)(第 22 頁整頁在講) # 陷阱③:最後一塊只有 60 萬筆——任何「假設每塊等長」的算法都會在餘數塊翻車
加了 chunksize 之後回傳的就不是 DataFrame——type(reader) 實跑 TextFileReader;reader['amount'] 當場 TypeError: 'TextFileReader' object is not subscriptable、hasattr(reader, 'sum')=False。水在迴圈裡,一次一杯。
三個零件全實跑:4 塊(100萬×3+60萬)、每塊是完整 DataFrame、總和與全載完全相等;瘦身帳 369.1 → 104.0 → 16.2 MB(region 欄縮 93 倍)、串流峰值 41.1 MB vs 全載 1,304.8 MB。
實跑 os.getpid() 全程同一顆行程、4 塊循序處理——chunksize 是單機記憶體管理,不啟動叢集、不分散、不平行。分散式是 Spark/Dask 的地盤(第 20 頁本尊)——解同一類問題、走完全不同的路。
category=字典編碼:字典 ['中區','北區','南區','東區','離島'] 完整保存、cat.codes 的 0~4 只是內部貨架編號;value_counts/groupby 與 object 版 array_equal=True、head 印出來還是字串——資訊零遺失、記憶體省 93 倍。
再看一次同一道題。這次你手上有一句口訣了:一次一塊、邊讀邊加;挑欄壓字省記憶體——單機串流不是 Spark。
reader = pd.read_csv('logs.csv', chunksize=1_000_000, ...) # 水龍頭不是水庫 for chunk in reader: total += chunk['amount'].sum() # 一次一塊、邊讀邊加
八題,全部都是本題的延伸。答錯會直接告訴你錯在哪。
「大型檔案分塊處理」是中級科目二機率統計與資料分析的大數據處理招牌考點,chunksize、TextFileReader、記憶體優化輪流出場。最常見的六種問法:① 給程式問 reader 的身分與行為(本題——迭代器 vs DataFrame);② 問塞不進記憶體的統計怎麼算(第 22 頁——串流累加);③ 問 usecols/dtype 的省記憶體效果(可量化);④ 問 category dtype 的原理(字典編碼、值不變);⑤ 問 chunksize 與 Spark 的差別(單機 vs 分散式);⑥ 問迭代器特性(一次性、最後一塊不滿)。一句口訣:一次一塊、邊讀邊加。
回傳的是 TextFileReader——一顆「待轉開的水龍頭」,不是裝滿資料的 DataFrame(實跑 type() 驗明正身:pandas.io.parsers.readers.TextFileReader)。此刻檔案一個位元組都還沒進記憶體——真正的讀取發生在 for chunk in reader 每轉一圈時:pandas 從磁碟讀下一個 100 萬筆、做成小 DataFrame 交給你。所以 A 的 reader['amount'].sum() 實跑當場 TypeError: 'TextFileReader' object is not subscriptable——想用 DataFrame 的功能,得先接出一塊(for 迴圈或 get_chunk(n))。同族寫法:iterator=True 也回 TextFileReader、再用 get_chunk 自由控制杯量。
用 memory_usage(deep=True) 一層一層量(實跑、單塊 100 萬列):什麼都不做——8 欄全裝 369.1 MB(timestamp/ip 這些字串欄超肥);usecols 挑 3 欄——104.0 MB(不要的欄連解析都省了);region 再換 category——16.2 MB:region 欄本身從 object 的 88.7 MB 壓到 0.95 MB(93 倍——「北區」重複三十幾萬次、字典編碼只存一次字串+每格一個小代碼)。全流程 tracemalloc 對照:全載峰值 810.1 MB vs 串流峰值 41.1 MB——縮 20 倍。順帶:deep=True 才會真的去量字串內容,不加 deep 會嚴重低估 object 欄。
不會改——category 是存法不是值(實跑對帳三連)。它把欄位拆成兩件事存:字典(.cat.categories=['中區','北區','南區','東區','離島'],每個字串只存一次)+代碼欄(.cat.codes=0~4 的小整數,標記每格是字典裡第幾號)。你看得到的一切照舊:head(5) 印出來還是字串、value_counts 與 object 版 array_equal=True、逐值比對 (astype(str)==object 版).all()=True、groupby('region') 金額分毫不差。D 的錯誤在把「倉庫貨架編號」當成「貨被換掉」——代碼是內部實作、字典就在旁邊、資訊零遺失。但書:category 欄做字串操作(.str.xxx)前常要 astype(str);新值不在字典裡會變 NaN(賦值前要先 add_categories)。
切塊只是表象、執行模型完全不同(實跑+觀念)。chunksize:單機、單行程、循序——實跑收集 os.getpid(),4 塊全在同一顆 Python 行程裡一塊接一塊處理;它解決的是「記憶體比檔案小」,CPU 還是那一顆、時間不會變快。Spark:叢集框架——資料切成分區撒到多台機器的 executor 平行計算、driver 彙總結果;它解決的是「一台機器不夠」(算力與記憶體都能水平擴充)——第 20 頁的 approxQuantile 就是本尊、還有 lazy 執行等一整套語意。階梯順序:pandas 全載 → chunksize 串流 → Dask(單機/多機平行)→ Spark 叢集——C 把第二階說成第四階,「效果等同」四個字當庭陣亡。
本頁實跑完全相等:串流 total=5,401,597,601=全載 df['amount'].sum()——因為金額是整數元,整數加法怎麼分批都一字不差。兩個但書:① 浮點欄位分批加總,因為加法順序不同會有 1e-7 級的浮點誤差(allclose 會過、== 可能不過)——財報等級的對帳請用整數分/Decimal 先定精度;② 不是所有統計都能這樣拆——可合併統計量(sum、count、min、max、按組 sum)可以塊塊累加;平均、標準差、分位數不行直接拆(平均要累加 (sum, count) 再除——第 22 頁整頁在講「平均的平均」高估 18.86 元的翻車現場;分位數要 Spark 的 approxQuantile 這類近似演算法)。
不能再用——它是一次性的迭代器(實跑):迴圈跑完再 next(reader) 當場 StopIteration;檔案指標已經走到底,要再算一輪就重開一顆 reader(再呼叫一次 read_csv)。這也是為什麼「先看一眼再正式跑」要用 get_chunk(5) 接一小杯、或另開一顆 nrows=1000 的預覽版。最後一塊:3.6M ÷ 100 萬=3 塊整、餘 60 萬——餘數塊不滿照樣出貨(實跑 shapes:(1000000,3)×3+(600000,3))。考場陷阱:任何「總筆數=塊數 × chunksize」或「每塊權重相同」的算法都在餘數塊翻車——第 22 頁「平均的平均」錯得更兇的原因之一就是最後一塊權重不同。
三條線。「大數據處理三部曲」:第 22 頁 chunksize 串流平均(可合併統計量 (sum, count)、tracemalloc 四方案 35.5/17.3/42.8/5.4 MB)→ 本頁 TextFileReader 本體+瘦身三件套(usecols/category 量化到 MB)→ 第 20 頁 Spark approxQuantile(真的分散式、relativeError 的帳)——單機省水、單機瘦身、叢集分工三階梯一次看懂。「型別家族」:第 21 頁 Int64(大 I 能裝 <NA>、小 i 會爆)+本頁 category(字典編碼省 93 倍)——dtype 參數是 pandas 的省錢與防錯開關。「迭代器線」:TextFileReader 與第 22 頁同一位主角,本頁補上 TypeError/StopIteration/get_chunk 三張身分證——考卷兩頁都認得。