級(jí)數(shù)據(jù)分析的實(shí)戰(zhàn)入門與性能優(yōu)化)
上周幫一個(gè)零售團(tuán)隊(duì)處理他們的銷售流水一份兩百多兆的Excel文件打開就卡等了三分鐘才看到那一百多萬(wàn)行數(shù)據(jù)。列一多、表一寬Excel直接進(jìn)入半癱瘓狀態(tài)。我當(dāng)時(shí)的處理方案很簡(jiǎn)單把數(shù)據(jù)丟給Pandas用Python腳本批量清洗、聚合、輸出匯總表十分鐘不到跑完還順手給他們做了兩張趨勢(shì)圖。今天這篇就是想把這條從Excel到百萬(wàn)級(jí)數(shù)據(jù)分析的路盡量完整地拆開講清楚——Pandas處理入門到底要學(xué)什么哪些操作是高頻剛需數(shù)據(jù)量上來(lái)之后又有哪些坑等著你。這不是一篇只講API的入門文而是我實(shí)際處理過大幾十個(gè)數(shù)據(jù)文件之后沉淀下來(lái)的經(jīng)驗(yàn)。它適合兩類人一種是每天泡在Excel里做報(bào)表、但明顯感覺Excel已經(jīng)扛不住數(shù)據(jù)體量的同學(xué)另一種是剛學(xué)完P(guān)ython基礎(chǔ)、想知道怎么用它來(lái)正經(jīng)做數(shù)據(jù)分析的初學(xué)者。看完之后你至少能明白一件事為什么Pandas能撐起百萬(wàn)級(jí)數(shù)據(jù)分析這個(gè)場(chǎng)景以及你該從哪里下手。1. 為什么不建議繼續(xù)用Excel硬扛大數(shù)據(jù)先聲明一點(diǎn)我不是讓你把Excel丟掉。Excel的交互體驗(yàn)、可視化、日常表格操作至今仍然是辦公場(chǎng)景里效率最高的工具之一。但當(dāng)你手里的數(shù)據(jù)開始往幾十萬(wàn)行、上百萬(wàn)行走的時(shí)候Excel的兩大問題就會(huì)暴露得淋漓盡致。1.1 Excel的卡頓源頭在哪里Excel的卡頓核心在于它的內(nèi)存管理方式。一個(gè)單元格只要有內(nèi)容哪怕只是一個(gè)空字符串它都要為公式計(jì)算、格式渲染、條件格式預(yù)留資源。你想想Excel里一個(gè)100萬(wàn)行的表格有多少個(gè)單元格光指針和元信息就要吃掉好幾個(gè)GB內(nèi)存。更致命的是Excel對(duì)大數(shù)據(jù)執(zhí)行篩選、排序、透視表這類操作時(shí)是整表重算的。你對(duì)一份百萬(wàn)行的表做個(gè)數(shù)據(jù)透視后臺(tái)要重新索引全部數(shù)據(jù)CPU和硬盤都在被反復(fù)壓榨表現(xiàn)出來(lái)就是轉(zhuǎn)圈、無(wú)響應(yīng)。我印象特別深的一次是處理一份將近120萬(wàn)行的訂單明細(xì)列數(shù)不算多大概15列但其中夾雜著日期、時(shí)間、文本備注、金額字段。Excel打開花了40多秒之后隨便點(diǎn)擊一下都需要兩三秒才有反應(yīng)想按客戶維度做個(gè)匯總直接卡死彈窗Excel無(wú)法完成此操作。最后我導(dǎo)出CSV用Pandas讀進(jìn)來(lái)整個(gè)數(shù)據(jù)集的讀取、清洗、透視、導(dǎo)出一條管道下來(lái)7秒跑完。這不是Pandas特別神奇而是它的設(shè)計(jì)本來(lái)就是為了處理這種體量。1.2 Pandas處理大數(shù)據(jù)的三點(diǎn)優(yōu)勢(shì)Pandas相比之下有幾個(gè)天然優(yōu)勢(shì)對(duì)比維度ExcelPandas內(nèi)存占用方式每個(gè)單元格都帶格式元信息開銷巨大按列存儲(chǔ)每列同一類型緊湊得多計(jì)算方式界面操作觸發(fā)整表重算向量化運(yùn)算按列批量計(jì)算性能高一個(gè)量級(jí)可重復(fù)性步驟靠人記改了數(shù)據(jù)要重做腳本寫一次永久復(fù)用怎么清洗的清清楚楚規(guī)模上限百萬(wàn)行已是極限再往上直接崩處理千萬(wàn)行以內(nèi)都很常見極限取決于機(jī)器內(nèi)存擴(kuò)展能力缺乏與Python生態(tài)協(xié)作能力機(jī)器學(xué)習(xí)、可視化、數(shù)據(jù)庫(kù)對(duì)接隨手就能接其中最核心的一份優(yōu)勢(shì)是按列存儲(chǔ)。Pandas的DataFrame本質(zhì)上是個(gè)列優(yōu)先的數(shù)據(jù)容器你做計(jì)算的時(shí)候是整列整列地批量運(yùn)算底層用的又是C語(yǔ)言實(shí)現(xiàn)的numpy數(shù)組所以同樣是對(duì)100萬(wàn)行數(shù)據(jù)求平均Pandas只需要一次性掃過這一列Excel卻要逐單元格去解釋公式。這也是為什么能跑百萬(wàn)級(jí)的關(guān)鍵所在。1.3 什么樣的數(shù)據(jù)量適合用Pandas說句實(shí)在話數(shù)據(jù)量在5萬(wàn)行以內(nèi)我自己也經(jīng)常用Excel處理沒必要非上代碼。但一旦你的數(shù)據(jù)規(guī)模往50萬(wàn)行靠攏或者你需要每天、每周重復(fù)做同一套匯總邏輯那Pandas的腳本化優(yōu)勢(shì)就體現(xiàn)出來(lái)了。代碼寫一遍以后的數(shù)據(jù)丟進(jìn)去跑就行不用再拿鼠標(biāo)點(diǎn)半天。另外還有一種情況也建議用Pandas——數(shù)據(jù)需要跨表關(guān)聯(lián)。Excel里做多表匹配主要靠VLOOKUP兩張表都很大時(shí)VLOOKUP慢得令人絕望而且跨文件引用極易出錯(cuò)。Pandas里一個(gè)merge函數(shù)就解決了一會(huì)兒我細(xì)說。2. 入門階段必須掌握的五個(gè)DataFrame操作很多初學(xué)者學(xué)Pandas最大誤區(qū)是把API當(dāng)單詞表背??粗臋n里幾百個(gè)方法今天學(xué)drop_duplicates明天學(xué)fillna學(xué)完就忘。我的建議很直接你的真實(shí)場(chǎng)景只需要從幾個(gè)高頻操作開始。2.1 讀入數(shù)據(jù)是第一步也是第一個(gè)坑一切從讀數(shù)據(jù)開始。日常最常見的是這兩個(gè)函數(shù)import pandas as pd # 讀Excel df pd.read_excel(sales_2024.xlsx, sheet_name訂單明細(xì)) # 讀CSV df pd.read_csv(sales_2024.csv, encodingutf-8)如果你是從Excel轉(zhuǎn)過來(lái)的注意兩個(gè)細(xì)節(jié)。第一CSV和Excel文件讀進(jìn)來(lái)之后的DataFrame完全一樣但對(duì)百萬(wàn)級(jí)數(shù)據(jù)來(lái)說CSV的讀取速度通常比xlsx快很多因?yàn)閤lsx本質(zhì)是個(gè)壓縮的XML文件讀取時(shí)要層層解包解析CSV就是純文本逐行掃。第二編碼問題非常容易踩到read_csv的時(shí)候如果遇到中文亂碼先試試encodinggbk或者encodingutf-8-sigWindows出品的CSV多半要轉(zhuǎn)一下編碼。還有一個(gè)入門階段容易忽略的參數(shù)是usecols。假如原始文件有30列你只需要其中的5列那就別把30列全讀進(jìn)來(lái)占內(nèi)存df pd.read_csv(big_sales.csv, usecols[訂單號(hào), 日期, 金額, 客戶ID, 渠道])這個(gè)習(xí)慣在數(shù)據(jù)量小的時(shí)候看不出來(lái)到百萬(wàn)級(jí)以后能少讀一半內(nèi)存。2.2 先摸家底info、head、describe這一套體檢動(dòng)作拿到數(shù)據(jù)之后不要急著清洗先做體檢。我的固定操作是三連print(df.head()) print(df.info()) print(df.describe())head讓你看前五行長(zhǎng)什么樣用來(lái)判斷列名是否規(guī)范、數(shù)據(jù)長(zhǎng)得是否像你預(yù)期。info輸出的東西更有價(jià)值每一列的字段類型、非空數(shù)量、內(nèi)存占用都在里面了。你知道嗎info最后會(huì)顯示一行memory usage: 156.8 MB這一行是判斷你后續(xù)需不需要做內(nèi)存優(yōu)化的關(guān)鍵指標(biāo)。如果你的數(shù)據(jù)讀進(jìn)來(lái)只有十幾萬(wàn)行卻占了幾百M(fèi)B那大概率是列的類型沒優(yōu)化后面我專門講。describe適用于數(shù)值列會(huì)告訴你每列的均值、標(biāo)準(zhǔn)差、最小值、四分位數(shù)、最大值。這一段跑完哪些列有離群值哪些列明顯異常心里基本就有數(shù)了。2.3 篩選、分組、聚合從自然語(yǔ)言到一行代碼數(shù)據(jù)分析里最高頻的三個(gè)動(dòng)作就是篩選、分組、聚合。舉個(gè)例子你腦子里想的是華東區(qū)、渠道為線上的訂單里每個(gè)月的總銷售額是多少。這句話翻譯成Pandas就是三行filtered df[(df[區(qū)域] 華東) (df[渠道] 線上)] monthly_sales filtered.groupby(filtered[日期].dt.to_period(M))[金額].sum()第一行篩選用布爾條件組合。注意Pandas里多個(gè)條件必須用連接每個(gè)條件都要加括號(hào)否則報(bào)錯(cuò)。第二行按月份分組這里用到了dt.to_period(M)意思是把日期列轉(zhuǎn)成月份周期相當(dāng)于把日期按月份歸類。很多人第一步就栽在分組上是因?yàn)橥薵roupby之后對(duì)象的類型會(huì)變成DataFrameGroupBy不再是普通的DataFrame。你必須對(duì)它再做聚合操作sum、mean、count、agg才能得到結(jié)果。如果你要同時(shí)算多個(gè)指標(biāo)比如既要總銷售額又要訂單數(shù)就配合agg使用result filtered.groupby(filtered[日期].dt.to_period(M)).agg( 銷售額(金額, sum), 訂單數(shù)(訂單號(hào), nunique) )這里訂單數(shù)用nunique意思是去重計(jì)數(shù)如果你直接count它會(huì)數(shù)一下這個(gè)組里有幾條記錄而不是多少個(gè)不同訂單。這個(gè)區(qū)別在電商數(shù)據(jù)里特別容易造成報(bào)表失真我在實(shí)際交付中反復(fù)跟業(yè)務(wù)團(tuán)隊(duì)確認(rèn)過這個(gè)口徑的差異。2.4 排序和去重永遠(yuǎn)記住你處理的是一張表排序用sort_values去重用drop_duplicatestop_customers df.sort_values(銷售額, ascendingFalse).head(10) unique_orders df.drop_duplicates(subset[訂單號(hào)])這兩個(gè)操作很簡(jiǎn)單但實(shí)際場(chǎng)景里有個(gè)細(xì)節(jié)容易出錯(cuò)drop_duplicates默認(rèn)保留第一條重復(fù)記錄如果你想保留最后一條要加keeplast參數(shù)。另外如果原始數(shù)據(jù)里訂單號(hào)本身存在空值去重之前得先確認(rèn)一下空值占比避免把大量真實(shí)訂單誤刪。空了的數(shù)據(jù)不算重復(fù)但Pandas默認(rèn)也會(huì)把它當(dāng)作重復(fù)項(xiàng)之一保留一條處理的時(shí)候要留意。2.5 缺失值處理先搞清楚為什么缺再?zèng)Q定怎么填缺失值處理看起來(lái)簡(jiǎn)單其實(shí)最考驗(yàn)?zāi)銓?duì)業(yè)務(wù)的理解。處理手段無(wú)非就幾種刪除、填0、填均值、前向填充。但選哪一種取決于這個(gè)字段為什么為空。舉個(gè)例子訂單金額為空那大概率是異常單需要謹(jǐn)慎。用戶年齡為空你可以考慮用該渠道的平均年齡填充。庫(kù)存表里某個(gè)商品的補(bǔ)貨日期為空可能只是因?yàn)樗€沒有排期跟異常沒有關(guān)系。我在實(shí)戰(zhàn)中的固定做法是先跑一下缺失值統(tǒng)計(jì)missing_ratio df.isnull().sum() / len(df) print(missing_ratio[missing_ratio 0])然后根據(jù)缺失比例和字段含義分列處理絕不無(wú)腦fillna一把梭。對(duì)數(shù)值型字段最常見的方案是fillna(0)但你要想清楚0是否是一個(gè)有業(yè)務(wù)意義的填充值。3. 百萬(wàn)級(jí)數(shù)據(jù)真正要命的三個(gè)性能瓶頸到了百萬(wàn)行這個(gè)量級(jí)你會(huì)發(fā)現(xiàn)普通的Pandas代碼還能跑但就是慢。明明一個(gè)很簡(jiǎn)單的操作要等好幾分鐘這就是性能瓶頸。我踩過的三個(gè)最大的坑今天一次性說清楚。3.1 dtype是內(nèi)存大戶object類型會(huì)吞掉你的內(nèi)存很多人讀入數(shù)據(jù)之后從來(lái)不關(guān)心列類型。默認(rèn)情況下Pandas會(huì)把文本列都識(shí)別為object類型而object類型在內(nèi)存里存的其實(shí)是字符串指針比數(shù)值類型昂貴得多。我做過一次實(shí)測(cè)一個(gè)90萬(wàn)行的數(shù)據(jù)里面有三列是分類文本比如華東/華北/華南這種如果保持object類型光這三列就占了大約120MB內(nèi)存把它們轉(zhuǎn)成category后直接降到5MB左右內(nèi)存減少九成以上。所以大數(shù)據(jù)的第一個(gè)優(yōu)化動(dòng)作永遠(yuǎn)是類型壓縮。常用的招數(shù)df[區(qū)域] df[區(qū)域].astype(category) df[金額] df[金額].astype(float32) df[訂單數(shù)] df[訂單數(shù)].astype(int32)category類型適合那種重復(fù)值很多的列比如區(qū)域、渠道、產(chǎn)品大類這幾個(gè)列在百萬(wàn)行數(shù)據(jù)里往往只有幾十種取值用category再合適不過。數(shù)值列則用更小的子類型替代默認(rèn)的float64和int64哪怕float32在精度上損失一點(diǎn)點(diǎn)做匯總分析完全夠用。每次做完類型轉(zhuǎn)換看一眼df.info()的memory usage你會(huì)直觀感受到內(nèi)存從幾百M(fèi)B掉到幾十MB這會(huì)給后續(xù)所有操作騰出大量空間。3.2 循環(huán)是萬(wàn)惡之源向量化才是Pandas的舒適區(qū)新手最容易寫的慢代碼就是遍歷每一行# 千萬(wàn)別這么寫100萬(wàn)行會(huì)讓你等到懷疑人生 for i in range(len(df)): if df.loc[i, 金額] 1000: df.loc[i, 等級(jí)] 高 else: df.loc[i, 等級(jí)] 低這行代碼的邏輯沒錯(cuò)但對(duì)100萬(wàn)行數(shù)據(jù)來(lái)說循環(huán)加loc逐行定位雙重減速跑完起碼要十分鐘起步。Pandas的設(shè)計(jì)思想是向量化——對(duì)整列一次性操作底層用C語(yǔ)言的循環(huán)替你跑速度是一百倍以上的差距。同樣的邏輯用向量化一行搞定df[等級(jí)] np.where(df[金額] 1000, 高, 低)如果條件復(fù)雜多個(gè)列參與判斷可以用np.selectconditions [ df[金額] 5000, df[金額] 1000, df[金額] 0 ] choices [高, 中, 低] df[等級(jí)] np.select(conditions, choices, default未知)有很多人問我什么時(shí)候真的要用循環(huán)我的答案是當(dāng)你需要逐行依賴前一行計(jì)算結(jié)果的時(shí)候比如計(jì)算累加收益、持倉(cāng)狀態(tài)機(jī)這類有順序依賴的業(yè)務(wù)循環(huán)才不可避免。此時(shí)你可以考慮用numba加速或者盡量將這類操作拆解成累計(jì)函數(shù)。大多數(shù)想用循環(huán)的場(chǎng)景其實(shí)是沒找到對(duì)應(yīng)的向量化函數(shù)先找找不到再循環(huán)。3.3 groupby和merge在數(shù)據(jù)量上來(lái)之后的正確姿勢(shì)groupby在百萬(wàn)級(jí)數(shù)據(jù)上如果直接跑通常問題不大但如果你在這個(gè)基礎(chǔ)上又掛了多個(gè)agg還要排序再合并回原表那計(jì)算量會(huì)指數(shù)級(jí)上升。我常用的原則是能少合并就少合并groupby的結(jié)果如果只是最終報(bào)表就不concat回原表保持分組之后只輸出結(jié)果。merge同理。兩張百萬(wàn)級(jí)表做關(guān)聯(lián)默認(rèn)用howinner就行不要沒事就搞howouter外連接會(huì)在匹配不到時(shí)產(chǎn)生大量空行內(nèi)存和行數(shù)都會(huì)暴漲。另外merge之前建議先對(duì)被關(guān)聯(lián)列做類型統(tǒng)一比如一邊是int64一邊是object即使內(nèi)容一樣merge也會(huì)判定無(wú)法匹配。我吃過這個(gè)虧客戶ID一側(cè)是從Excel讀的數(shù)字被讀成了float64一側(cè)是字符串看起來(lái)都是10086merge出來(lái)全是NaN排查了很久才發(fā)現(xiàn)是類型不一致。所以merge前的類型檢查要養(yǎng)成習(xí)慣用df.dtypes排查一遍再操作。4. 一次完整的百萬(wàn)行數(shù)據(jù)實(shí)操?gòu)?fù)盤理論說了不少我們干脆從頭到尾跑一個(gè)完整案例。我構(gòu)造一個(gè)模擬場(chǎng)景一份某電商平臺(tái)的交易流水大約120萬(wàn)行包含訂單號(hào)、用戶ID、區(qū)域、渠道、商品ID、銷售日期、銷售額、成本、數(shù)量、優(yōu)惠金額這10列。目標(biāo)是算出每個(gè)區(qū)域每個(gè)渠道的月度銷售額、訂單量和利潤(rùn)。4.1 先做環(huán)境準(zhǔn)備與數(shù)據(jù)體檢我會(huì)先準(zhǔn)備環(huán)境import pandas as pd import numpy as np df pd.read_csv(sales_120w.csv, encodingutf-8, parse_dates[銷售日期]) df.info()這里parse_dates參數(shù)直接把日期列解析成datetime類型不要等后續(xù)再轉(zhuǎn)換。info出來(lái)之后看內(nèi)存占用我模擬的結(jié)果是大概980MB這個(gè)體量在普通配置的電腦上已經(jīng)有點(diǎn)吃緊了。于是做類型壓縮df[區(qū)域] df[區(qū)域].astype(category) df[渠道] df[渠道].astype(category) df[銷售額] df[銷售額].astype(float32) df[成本] df[成本].astype(float32) df[優(yōu)惠金額] df[優(yōu)惠金額].astype(float32) df[數(shù)量] df[數(shù)量].astype(int8) df[利潤(rùn)] (df[銷售額] - df[成本] - df[優(yōu)惠金額]).astype(float32)此時(shí)再跑一遍df.info()內(nèi)存占用已經(jīng)掉到差不多150MB這個(gè)數(shù)字對(duì)后續(xù)計(jì)算就很舒服了。4.2 深度清洗去重、缺失值、異常值三件套清洗環(huán)節(jié)是重頭戲先統(tǒng)計(jì)缺失再處理重復(fù)和異常print(df.isnull().sum())假設(shè)發(fā)現(xiàn)優(yōu)惠金額有2.3萬(wàn)行缺失這通常是訂單沒有優(yōu)惠填充0是合理的df[優(yōu)惠金額] df[優(yōu)惠金額].fillna(0)再看訂單號(hào)去重跑一個(gè)數(shù)量對(duì)比before len(df) after len(df.drop_duplicates(subset[訂單號(hào), 用戶ID])) print(f去重前后差異: {before - after})如果差異顯著說明有大量重復(fù)記錄這些通常來(lái)自同一個(gè)用戶重復(fù)下單產(chǎn)生的多條流水在訂單明細(xì)分析里屬于正常情況不需要?jiǎng)h除。但如果同一訂單號(hào)出現(xiàn)多次且你統(tǒng)計(jì)的是訂單量這種口徑就必須去重后再統(tǒng)計(jì)否則訂單量會(huì)虛高。異常值這一塊我習(xí)慣跑一個(gè)describe看分布print(df[[銷售額, 數(shù)量, 利潤(rùn)]].describe())如果發(fā)現(xiàn)銷售額最小值是負(fù)數(shù)或者利潤(rùn)出現(xiàn)了極度異常的極大值要單獨(dú)抽查這些記錄判斷是促銷沖正還是數(shù)據(jù)錄入錯(cuò)誤。模擬數(shù)據(jù)里我設(shè)定了少量銷售負(fù)值退款單這類記錄在匯總?cè)粘dN售額時(shí)應(yīng)該剔除但它們對(duì)退款分析有價(jià)值所以我會(huì)單獨(dú)建一張退款表而不是簡(jiǎn)單刪掉。4.3 管道化處理把清洗邏輯封裝進(jìn)函數(shù)清洗步驟一多臨時(shí)寫在腳本里就很亂。實(shí)際操作中我會(huì)把整套清洗封裝成一個(gè)函數(shù)用pipe一次性串起來(lái)def clean_sales_data(df): df df.copy() df[優(yōu)惠金額] df[優(yōu)惠金額].fillna(0) df df[df[銷售額] 0] df[利潤(rùn)] df[銷售額] - df[成本] - df[優(yōu)惠金額] df[銷售日期] pd.to_datetime(df[銷售日期]) return df df_clean ( pd.read_csv(sales_120w.csv, encodingutf-8) .pipe(clean_sales_data) )這么做的最大好處是以后每周收到新數(shù)據(jù)只需要重復(fù)跑一遍管道不會(huì)因?yàn)樯賵?zhí)行某一步清洗而造成結(jié)果前后不一致。數(shù)據(jù)交付里最忌諱的就是這次我手動(dòng)多過濾了一個(gè)條件這類操作結(jié)果下周對(duì)不上賬。4.4 按區(qū)域渠道匯聚月度核心指標(biāo)清洗完之后做匯總這是百萬(wàn)級(jí)數(shù)據(jù)里最典型的導(dǎo)出需求result ( df_clean .groupby([ df_clean[銷售日期].dt.to_period(M), 區(qū)域, 渠道 ]) .agg( 銷售額(銷售額, sum), 訂單量(訂單號(hào), nunique), 利潤(rùn)(利潤(rùn), sum) ) .reset_index() ) result.to_csv(月度區(qū)域渠道匯總.csv, indexFalse, encodingutf-8-sig)這段代碼在120萬(wàn)行數(shù)據(jù)上跑實(shí)測(cè)大概用了不到5秒。跑完之后result大約幾百行這已經(jīng)是可以直接交給業(yè)務(wù)方看的報(bào)表級(jí)輸出了。這里有一個(gè)細(xì)節(jié)很重要就是groupby里分組鍵用了df[銷售日期].dt.to_period(M)這是一個(gè)Series而不是列名字符串這樣寫是允許的而且能把日期直接按月歸類。如果按月分組的場(chǎng)景很常見更推薦提前把月份字段單獨(dú)生成一列這樣后續(xù)寫代碼更清晰df_clean[月份] df_clean[銷售日期].dt.to_period(M)4.5 結(jié)果校驗(yàn)別相信一次跑通的結(jié)果跑完匯總之后千萬(wàn)不要直接交差一定要做結(jié)果校驗(yàn)。最穩(wěn)妥的校驗(yàn)方法是抽一個(gè)你熟悉的維度跟原始數(shù)據(jù)對(duì)齊。比如拿華東區(qū)域線上渠道某一個(gè)月的數(shù)據(jù)手工用Excel篩選原始文件算一下總銷售額跟你代碼輸出的結(jié)果對(duì)一下。對(duì)不上的情況絕大多數(shù)是過濾條件不一致比如你有沒有把退款單算進(jìn)去有沒有漏掉空區(qū)域的分組。我另外的習(xí)慣是在腳本里做兩層自動(dòng)校驗(yàn)# 校驗(yàn)1匯總后的總銷售額等于過濾后的總銷售額 assert abs(result[銷售額].sum() - df_clean[銷售額].sum()) 0.01 # 校驗(yàn)2每個(gè)分組的行數(shù)占比要合理 group_counts df_clean.groupby([ 區(qū)域, 渠道, 月份 ]).size() assert len(group_counts) len(result)這兩層校驗(yàn)成本極低但你有沒有做這步?jīng)Q定了交付的質(zhì)量是看起來(lái)對(duì)還是真對(duì)。數(shù)據(jù)量越大的時(shí)候越要信不過自己的手要信校驗(yàn)邏輯。5. 兩年里踩過最深的四個(gè)Pandas坑坑這個(gè)東西不真正踩一遍光看文檔是記不住的。作為一個(gè)被坑過無(wú)數(shù)次的人我挑四個(gè)最傷的例子按慘痛程度排個(gè)序。5.1 inplaceTrue的虛假安全感剛學(xué)Pandas時(shí)特喜歡寫df.dropna(inplaceTrue)總覺得加個(gè)inplace參數(shù)就萬(wàn)事大吉不用重新賦值。但實(shí)際上inplace在很多方法里是有隱患的。第一Pandas 2.x開始已經(jīng)在官方文檔里不推薦這種用法未來(lái)有可能被移除。第二inplace依賴函數(shù)內(nèi)部修改原對(duì)象某些方法實(shí)際做的是復(fù)制一份再修改再回填根本沒有省內(nèi)存。第三鏈?zhǔn)秸{(diào)用的場(chǎng)景里inplace經(jīng)常失效——你對(duì)一個(gè)切片執(zhí)行inplace操作改的只是副本原DataFrame紋絲不動(dòng)。我現(xiàn)在一律寫成顯式賦值df df.drop_duplicates(subset[訂單號(hào)])這樣代碼意圖明確debug時(shí)也方便打斷點(diǎn)看中間狀態(tài)。5.2 鏈?zhǔn)劫x值引發(fā)的SettingWithCopyWarning鏈?zhǔn)劫x值是我見過最隱蔽的Bug來(lái)源。和inplace類似問題出在你對(duì)切片的理解。下面這段代碼df[df[金額] 100][折扣] 0很多新手以為這是把金額大于100的那幾行折扣改成0實(shí)際上Pandas會(huì)警告你這可能是在試圖修改一個(gè)副本。因?yàn)閐f[df[金額] 100]產(chǎn)生的是一個(gè)視圖或副本到底改沒改到原DataFrame取決于內(nèi)部?jī)?yōu)化你不該依賴它。正確的寫法永遠(yuǎn)是用.locdf.loc[df[金額] 100, 折扣] 0記住這條規(guī)則就好了在DataFrame里做條件修改一律df.loc[條件, 列名] 新值不要用鏈?zhǔn)嚼ㄌ?hào)。養(yǎng)成這個(gè)習(xí)慣之后我再?zèng)]遇到過SettingWithCopyWarning。5.3 索引沒有resetgroupby之后悄悄變了樣groupby之后忘了reset_index是我看別人代碼時(shí)最常見的問題。groupby的結(jié)果行數(shù)取決于分組鍵的取值組合數(shù)但索引是這些分組鍵的值你要是還想再繼續(xù)跟其他DataFrame合并或者做進(jìn)一步篩選就會(huì)發(fā)現(xiàn)索引亂七八糟對(duì)不上。如果你在groupby后直接執(zhí)行to_excel你會(huì)發(fā)現(xiàn)左邊多了一列看不到名字的索引。正確的姿勢(shì)是想清楚這個(gè)中間結(jié)果是要繼續(xù)參與計(jì)算還是作為最終報(bào)表。如果是中間結(jié)果分組之后加個(gè)reset_index()把分組鍵變成普通列后面怎么操作都不慌。這也是為什么我對(duì)agg結(jié)果的固定習(xí)慣是馬上reset_index()。5.4 read_excel在大型Excel文件上的隱藏慢速很多人遇到大文件第一步還是pd.read_excel殊不知這可能是最慢的讀法。Excel文件的底層是XML壓縮格式讀取時(shí)要實(shí)時(shí)解壓和解析速度遠(yuǎn)比CSV慢。我測(cè)過一個(gè)約80MB的xlsxread_excel用了30多秒同樣數(shù)據(jù)導(dǎo)出成CSV后read_csv只要5秒。如果是100MB以上的Excel差距還會(huì)更離譜。所以我的工作流是如果數(shù)據(jù)源是大型Excel文件第一步先用Excel另存為CSV或者用腳本把xlsx批量轉(zhuǎn)成csv再做Pandas處理。如果實(shí)在只能讀Excel可以考慮用openpyxl引擎配合usecols和nrows參數(shù)先只讀一部分看看結(jié)構(gòu)全量讀取留到最后。還有一個(gè)相關(guān)的坑read_excel默認(rèn)會(huì)讀取整個(gè)sheet的所有行和列如果sheet里有大量空行Pandas也會(huì)傻乎乎地讀進(jìn)來(lái)再丟掉白白浪費(fèi)內(nèi)存和讀取時(shí)間。加sheet_name參數(shù)指定要讀的sheet能省則省。寫在最后的一點(diǎn)個(gè)人經(jīng)驗(yàn)我剛開始用Pandas處理百萬(wàn)級(jí)數(shù)據(jù)時(shí)心態(tài)是代碼能跑就行后來(lái)才意識(shí)到干凈、可讀、可復(fù)現(xiàn)的腳本比跑得快重要得多。因?yàn)閿?shù)據(jù)分析工作真正的交付物不是那幾張報(bào)表而是如果有人想復(fù)現(xiàn)你的數(shù)據(jù)結(jié)論能不能照著你的腳本一步步跑出同樣的結(jié)果。哪怕你只是給自己用一周之后回看你也會(huì)感謝當(dāng)初寫了清晰注釋的自己。還有一個(gè)容易被低估的習(xí)慣每一步處理完都打印一下行數(shù)和shape數(shù)據(jù)從120萬(wàn)行變成90萬(wàn)行這一步到底是因?yàn)槿ブ剡€是因?yàn)檫^濾退款心里要有數(shù)。做數(shù)據(jù)分析最怕的不是報(bào)錯(cuò)而是結(jié)果看著挺合理但實(shí)際上有偏差那種錯(cuò)誤藏得最深也最難查。Pandas這條路入門不難難的是數(shù)據(jù)量上來(lái)之后那種感覺自己會(huì)用、但一跑就慢、一慢就不知道該優(yōu)化哪里的迷茫。我的建議是先跑通再想性能最后再優(yōu)化內(nèi)存。別一上手就背一堆優(yōu)化技巧等你真遇到那個(gè)讓你干等10分鐘的百萬(wàn)行文件時(shí)你自然會(huì)回來(lái)翻這篇。