🗺️ AI 學習與考證地圖
中級科目二程式實戰 · 大數據處理

水龍頭不是水庫:
一次一塊、邊讀邊加

正解 B 一句話三個零件:reader 是可迭代的 TextFileReader(不是 DataFrame)、每次迭代載入 100 萬筆的 DataFrame 分塊處理、usecolscategory 再壓記憶體。本站 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 subscriptableC「等同 Spark」——同一顆行程循序跑 4 塊、單機串流不是分散式D「region 全轉成 0」——category 是字典編碼、value_counts 與 object 版 array_equal=True、資訊零遺失。口訣:一次一塊、邊讀邊加;挑欄壓字省記憶體——單機串流不是 Spark

閱讀模式

00題目

工程師執行下列程式處理 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、整數元一字不差)——省的是記憶體、不是精度

先點開看:TextFileReaderchunksize 分塊category dtype

不熟分塊與大數據處理?你需要先認識下列名詞

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

水龍頭本體
chunksize 分塊TextFileReader串流處理迭代器一次性最後一塊不滿
瘦身三件套
usecols 挑欄category dtype字典編碼cat.codesdtype 指定
帳房防身
記憶體 vs 磁碟50GB 的帳可合併統計量分塊分組聚合concat 自殺陷阱
分散式分辨
單機 vs 分散式Spark 是什麼Dask 中繼站

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

點擊後出現漸進式說明

起手式
pd.read_csvfor chunk in reader.sum() 聚合1_000_000 底線
驗身分
type() 驗身分讀 not subscriptableStopIterationget_chunk 手動接水
對帳工具
memory_usage(deep)tracemalloc 峰值value_counts 對帳array_equal 對帳

01逐行拆解:一顆水龍頭、三顆瘦身旋鈕

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

第 1 段開水龍頭:chunksize 一下、回來的就不是 DataFrame
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(實跑自造、金額整數元)。

相關名詞:chunksize 分塊usecols 挑欄1_000_000 底線

第 2 段驗身分與接水迴圈:一塊一塊來、邊讀邊加
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 的第二個零件);塊與塊之間靠帳本銜接。

相關名詞:TextFileReaderfor chunk in readertype() 驗身分

第 3 段實跑塊帳與記憶體帳:4 塊、41 MB、縮 20 倍
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 後半句「進一步降低記憶體」是可以量出來的。

相關名詞:最後一塊不滿memory_usage(deep)tracemalloc 峰值

第 4 段A 與 C 的世界:舀水舀了個空、召喚不出艦隊
# 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 才是本尊——兩者解的是同一類問題(資料比記憶體大),走的是完全不同的路

相關名詞:讀 not subscriptable單機 vs 分散式Spark 是什麼

第 5 段D 的世界與收尾:字典就在旁邊、一個字都沒丟
# 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 級——檔案大小跟記憶體占用徹底脫鉤,這正是整題的靈魂。

一句話記住本題:一次一塊、邊讀邊加;挑欄壓字省記憶體——單機串流不是 Spark。B 三個零件全對;A 舀空水龍頭、C 幻想艦隊、D 誣賴字典編碼。

相關名詞:字典編碼StopIteration50GB 的帳

02水龍頭實驗室:reader 的三張身分證

它是什麼、怎麼流、流完會怎樣——三面向逐一實跑:

互動實驗室:水龍頭實驗室TextFileReader · 4 塊逐塊帳 · 一次性 StopIteration
本站實跑判決:type(reader)pandas.io.parsers.readers.TextFileReader——不是 DataFrame;reader['amount'] 當場 TypeError: 'TextFileReader' object is not subscriptablehasattr(reader, 'sum')=False(A 陣亡)。接水帳:4 塊=(1000000, 3) × 3 + (600000, 3)——每塊是貨真價實的 DataFrame(sum/groupby/篩選都能用)、最後一塊不滿照樣出貨。一次性:迴圈跑完再 next(reader)StopIteration——水龍頭是流過就沒的迭代器,要再算一輪就重開一顆 reader

相關名詞:TextFileReader迭代器一次性get_chunk 手動接水

03記憶體帳房:三件套各省多少

chunksize、usecols、category——一顆一顆旋鈕拆開量:

互動實驗室:記憶體帳房全載 1,304.8 vs 串流 41.1 MB · 369→104→16.2 · 50GB 換算
本站實跑判決:tracemalloc 實測——全載 pd.read_csv('logs.csv') 峰值 810.1 MB(8 欄 deep 記憶體 1,304.8 MB);串流版全程峰值 41.1 MB——縮 20 倍。三件套單塊帳(100 萬列):8 欄全裝 369.1 MBusecols 挑 3 欄 104.0 MB → region 換 category 16.2 MB——合計縮 23 倍;region 欄本身 88.7 → 0.95 MB(93 倍)。50GB 換算(以實測每列 69.6 bytes):約 771 塊——塊數變多、峰值不變:檔案大小與記憶體占用徹底脫鉤。

相關名詞:記憶體 vs 磁碟usecols 挑欄50GB 的帳

04category 闢謠室:D 誣賴了字典編碼

「全部轉成數字 0、資訊遺失」——值、代碼、記憶體三面開庭:

互動實驗室:category 闢謠室值一個都沒變 · cat.codes 是內部代碼 · region 欄縮 93 倍
字典編碼的真相(實跑):category 把「北區」這種重複幾十萬次的字串改成兩件事存——字典.cat.categories 實跑 ['中區','北區','南區','東區','離島']、每個字串只存一次)+代碼欄.cat.codes 值域 0~4、每格一個小整數)。對帳三連全過:head(5) 印出來還是字串(['中區','南區','南區','中區','北區'])、value_counts 與 object 版 array_equal=True、逐值比對 (astype(str)==object版).all()=True、groupby 金額分毫不差。D 把「內部存法」當成「資料被改」——代碼確實是數字、但那是倉庫的貨架編號,貨(你的字串)一件都沒少。

相關名詞:category dtypecat.codesvalue_counts 對帳

05分散式闢謠庭:C 的艦隊幻覺

「自動啟動多台機器、效果等同 Spark」——宣稱、真身、本尊三面對質:

互動實驗室:分散式闢謠庭C 的宣稱 · chunksize 真身=單機循序 · Spark 真身=叢集框架
單機與分散式的分界(實跑+觀念):實跑證據——迴圈裡收集 os.getpid():4 塊全程同一顆 Python 行程、一塊處理完才輪下一塊(循序);chunksize 從頭到尾沒有啟動任何東西——沒有叢集、沒有第二台機器、連第二顆行程都沒有。它解決的是「記憶體不夠」;Spark 解決的是「一台機器不夠」——資料切成分區撒到多台 executor 平行算、由 driver 彙總(第 20 頁 approxQuantile 的地盤)。兩者常被一起考:單機先用 chunksize/Dask、資料量再上去才動用叢集——「效果等同 Spark」把梯子的兩階說成同一階。

相關名詞:單機 vs 分散式Spark 是什麼串流處理

06考場加碼:帳房進階與三個陷阱

分塊處理的進階姿勢速查(全部本站實跑):

姿勢寫法本頁實跑
分塊加總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 萬筆——任何「假設每塊等長」的算法都會在餘數塊翻車
串起系列:「大數據處理三部曲」:第 22 頁 chunksize 串流平均(可合併統計量 (sum, count)、tracemalloc 四方案)→ 本頁 TextFileReader 本體+瘦身三件套 → 第 20 頁 Spark approxQuantile(真的分散式)——單機省水、單機瘦身、叢集分工三階梯。「型別家族」:第 21 頁 Int64 補值(大 I 能裝 <NA>)+本頁 category 字典編碼——dtype 是 pandas 的省錢與防錯開關。「一字不差工法」:金額用整數元設計、串流總和與全載完全相等——分批不損精度是設計出來的(浮點欄位分批加總會有 1e-7 級誤差、報表對帳請先定精度)。

相關名詞:concat 自殺陷阱可合併統計量分塊分組聚合

07四個選項收工

Areader 是一個完整的 DataFrame,可直接執行 reader['amount'].sum()舀空水龍頭

加了 chunksize 之後回傳的就不是 DataFrame——type(reader) 實跑 TextFileReaderreader['amount'] 當場 TypeError: 'TextFileReader' object is not subscriptable、hasattr(reader, 'sum')=False。水在迴圈裡,一次一杯。

Breader 是可迭代的 TextFileReader,每次迭代載入 100 萬筆的 DataFrame 分塊處理;usecols 與 category dtype 可進一步降低每塊的記憶體占用正確

三個零件全實跑:4 塊(100萬×3+60萬)、每塊是完整 DataFrame、總和與全載完全相等;瘦身帳 369.1 → 104.0 → 16.2 MB(region 欄縮 93 倍)、串流峰值 41.1 MB vs 全載 1,304.8 MB。

Cchunksize 會自動啟動多台機器的分散式運算,效果等同 Spark艦隊幻覺

實跑 os.getpid() 全程同一顆行程、4 塊循序處理——chunksize 是單機記憶體管理,不啟動叢集、不分散、不平行。分散式是 Spark/Dask 的地盤(第 20 頁本尊)——解同一類問題、走完全不同的路。

Ddtype={'region': 'category'} 會把 region 欄位的值全部轉成數字 0,導致資訊遺失誣賴字典

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()   # 一次一塊、邊讀邊加

08自我檢測

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

09重點整理

  1. 水龍頭比喻:50GB 整缸水倒不進小杯子——chunksize 把 read_csv 從「水庫」切成「水龍頭」,一次流一杯(100 萬筆的小 DataFrame)、接一杯記一筆帳、倒掉再接。
  2. 正解 B 三零件(實跑):reader 是可迭代的 TextFileReader;每次迭代載入一塊完整 DataFrame(4 塊=100萬×3+60萬);usecols+category 讓每塊 369.1 → 104.0 → 16.2 MB
  3. A 的死因(實跑):reader 不是 DataFrame——reader['amount'] 當場 TypeError: 'TextFileReader' object is not subscriptable、hasattr(reader, 'sum')=False。
  4. C 的死因(實跑):os.getpid() 全程同一顆——單機、單行程、循序;chunksize 不啟動叢集、不等同 Spark——分散式是另一座梯階(第 20 頁)。
  5. D 的死因(實跑):category=字典編碼——categories ['中區','北區','南區','東區','離島'] 完整保存、cat.codes 0~4 是內部代碼;value_counts/groupby/逐值比對與 object 版全部相等、資訊零遺失。
  6. 總和對帳(實跑):串流 total=5,401,597,601=全載 sum——整數元一字不差;分批加總不損精度(浮點欄要先定精度)。
  7. 記憶體帳(實跑):全載 8 欄 deep 1,304.8 MB/tracemalloc 峰值 810.1 MB vs 串流峰值 41.1 MB——縮 20 倍;region 欄 object 88.7 → category 0.95 MB(93 倍)。
  8. 一次性(實跑):迭代器跑完再 next(reader) → StopIteration——要再算一輪就重開一顆 reader;get_chunk(5) 可手動接一杯。
  9. 最後一塊不滿(實跑):3.6M ÷ 100 萬=3 塊整+餘 60 萬——任何「假設每塊等長」的算法都會在餘數塊翻車。
  10. 50GB 換算(實測每列 69.6 bytes):約 771 塊——塊數變多、峰值不變:檔案大小與記憶體占用徹底脫鉤,這是整題的靈魂。
  11. 三個陷阱:pd.concat 全塊接回=自殺;平均不能「各塊平均再平均」(第 22 頁的 (sum, count) 解法);跨塊統計要用可合併統計量。
  12. 考場口訣一次一塊、邊讀邊加;挑欄壓字省記憶體——單機串流不是 Spark
完整程式碼