戰(zhàn):零售銷(xiāo)售數(shù)據(jù)分析的完整流程與優(yōu)化技巧)
這個(gè)系列的024期按計(jì)劃應(yīng)該繼續(xù)深入NumPy的應(yīng)用。前面幾期我們把數(shù)組創(chuàng)建、索引切片、廣播機(jī)制這些基礎(chǔ)用法都過(guò)了一遍也做了幾個(gè)小案例從今天開(kāi)始要進(jìn)入真正的業(yè)務(wù)場(chǎng)景了。這次用的是我手頭整理的一份零售門(mén)店年度銷(xiāo)售匯總數(shù)據(jù)共9家門(mén)店、20個(gè)SKU、12個(gè)月2160條記錄。別小看這種二維表格日常數(shù)據(jù)分析里80%的場(chǎng)景都是這種結(jié)構(gòu)而NumPy做這類(lèi)結(jié)構(gòu)化數(shù)據(jù)的讀取、清洗、聚合、統(tǒng)計(jì)計(jì)算本身就是一個(gè)高價(jià)值練習(xí)。本篇適合兩種人一是已經(jīng)會(huì)Python基礎(chǔ)語(yǔ)法、裝好了NumPy但不知道拿它做什么的二是用Pandas很久但從來(lái)沒(méi)搞明白底層邏輯的人。我盡量把所有分析動(dòng)作都落在NumPy上一方面讓大家感受純數(shù)組操作怎么解決業(yè)務(wù)問(wèn)題另一方面也方便后面切換到Pandas、Spark時(shí)能理解它們的底層原理。純NumPy做項(xiàng)目分析和直接用Pandas做差別其實(shí)很大哪個(gè)更適合什么場(chǎng)景我在第5節(jié)專(zhuān)門(mén)聊。1. 案例背景與數(shù)據(jù)設(shè)計(jì)說(shuō)明1.1 這次案例要解決的問(wèn)題這次模擬的場(chǎng)景是一家連鎖零售企業(yè)數(shù)據(jù)是從區(qū)域財(cái)務(wù)系統(tǒng)導(dǎo)出的月度銷(xiāo)售匯總表。注意這不是流水明細(xì)而是每個(gè)月、每個(gè)門(mén)店、每個(gè)商品已經(jīng)匯總好的記錄所以一行代表一個(gè)聚合單元。文件結(jié)構(gòu)很簡(jiǎn)單門(mén)店編號(hào)、月份、商品編號(hào)、銷(xiāo)售額、銷(xiāo)量、訂單數(shù)。我給自己定的分析需求有6條全年銷(xiāo)售額和訂單量的月度趨勢(shì)判斷哪個(gè)月是旺季哪個(gè)月是淡季。各門(mén)店年度業(yè)績(jī)排序找出頭部和尾部門(mén)店。商品銷(xiāo)售結(jié)構(gòu)看哪些SKU貢獻(xiàn)了大頭收入。月度銷(xiāo)售額的集中程度和離散情況用分位數(shù)和標(biāo)準(zhǔn)差衡量。異常月份和異常門(mén)店識(shí)別比如突然飆高的銷(xiāo)售額是不是數(shù)據(jù)錄錯(cuò)了。銷(xiāo)量與銷(xiāo)售額的相關(guān)性判斷業(yè)績(jī)是走量驅(qū)動(dòng)還是高客單驅(qū)動(dòng)。這6個(gè)問(wèn)題基本覆蓋了日常經(jīng)營(yíng)分析里最常被老板問(wèn)到的幾個(gè)方向。案例本身不復(fù)雜但貴在完整從原始CSV文件開(kāi)始到清洗、聚合、指標(biāo)計(jì)算、結(jié)果輸出一條線(xiàn)走完。做完之后你能明顯感覺(jué)到NumPy在“處理規(guī)則明確的結(jié)構(gòu)化數(shù)據(jù)”這件事上是真的夠用且趁手的。1.2 模擬數(shù)據(jù)表結(jié)構(gòu)設(shè)計(jì)數(shù)據(jù)是我按真實(shí)業(yè)務(wù)習(xí)慣模擬生成的字段類(lèi)型和含義如下字段名類(lèi)型含義store_idint門(mén)店編號(hào)取值范圍1-9monthint月份取值范圍1-12product_idint商品SKU編號(hào)取值范圍101-120sales_amtfloat銷(xiāo)售額單位元volumefloat銷(xiāo)量單位件order_cntint訂單數(shù)CSV文件的前幾行長(zhǎng)這樣store_id,month,product_id,sales_amt,volume,order_cnt 1,1,101,12800.50,52,86 1,1,102,8600.00,41,63 1,1,103,15200.80,73,92 1,1,104,5400.20,28,45 1,1,105,22100.00,96,120一共9個(gè)門(mén)店 × 12個(gè)月 × 20個(gè)商品正好2160行。銷(xiāo)售額我刻意制造了一部分異常值比如某個(gè)月某個(gè)商品銷(xiāo)售額離群用于演示異常檢測(cè)。如果你要復(fù)現(xiàn)直接把上面的結(jié)構(gòu)用循環(huán)生成就行核心是關(guān)注NumPy的處理手法數(shù)據(jù)本身是模擬的還是真實(shí)的不影響分析流程。1.3 為什么不用Pandas而用NumPy很多初學(xué)者看到這里會(huì)疑惑數(shù)據(jù)分析不是都用Pandas嗎確實(shí)Pandas處理這種表格會(huì)更順手一行g(shù)roupby就能完成分組求和。但我想說(shuō)的是NumPy才是這一切的底層基礎(chǔ)。Pandas的Series和DataFrame內(nèi)部存儲(chǔ)結(jié)構(gòu)就是NumPy的ndarray。你把NumPy的數(shù)組操作搞明白了之后學(xué)Pandas基本就是學(xué)語(yǔ)法糖。反過(guò)來(lái)如果一上來(lái)就只會(huì)groupby遇到性能問(wèn)題、內(nèi)存問(wèn)題、或者需要自定義復(fù)雜算法的時(shí)候你會(huì)發(fā)現(xiàn)根本無(wú)從下手。因?yàn)檫@層抽象太舒服了反而把底層邏輯遮住了。另外從工程角度講NumPy更輕量。如果數(shù)據(jù)量在幾十萬(wàn)行以下、分析邏輯以數(shù)學(xué)計(jì)算為主純NumPy方案的執(zhí)行速度和內(nèi)存占用都優(yōu)于引入Pandas。我實(shí)際項(xiàng)目里有個(gè)小習(xí)慣能用NumPy解決的就不開(kāi)Pandas除非是要處理異構(gòu)表格、多級(jí)索引、時(shí)間序列重采樣這類(lèi)復(fù)雜數(shù)據(jù)操作。Pandas負(fù)責(zé)數(shù)據(jù)整理NumPy負(fù)責(zé)數(shù)學(xué)計(jì)算兩者配合才是正解。本篇刻意不用Pandas是為了讓大家把基本功練扎實(shí)。2. 數(shù)據(jù)讀取與預(yù)處理NumPy的讀取細(xì)節(jié)2.1 用np.genfromtxt讀取CSVNumPy讀取CSV最常用的兩個(gè)函數(shù)是np.loadtxt和np.genfromtxt?,F(xiàn)實(shí)中我更推薦后者因?yàn)樗鼘?duì)缺失值的容忍度更高不會(huì)一遇到空單元格就報(bào)錯(cuò)。讀取代碼如下import numpy as np raw np.genfromtxt( sales_2024.csv, delimiter,, skip_header1, dtypefloat, encodingutf-8 ) print(raw.shape) # (2160, 6)這里解釋幾個(gè)容易踩坑的參數(shù)。skip_header1表示跳過(guò)第一行表頭delimiter,指定分隔符dtypefloat讓所有列都按浮點(diǎn)數(shù)讀入encodingutf-8解決中文環(huán)境下的編碼問(wèn)題。如果不加encoding在Windows上很容易遇到UnicodeDecodeError這個(gè)我后面詳細(xì)說(shuō)。讀取之后raw是一個(gè)二維ndarray我們可以按列拆開(kāi)方便后續(xù)運(yùn)算store_id raw[:, 0].astype(int) month raw[:, 1].astype(int) product_id raw[:, 2].astype(int) sales_amt raw[:, 3] volume raw[:, 4] order_cnt raw[:, 5].astype(int)這一步拆列看似簡(jiǎn)單實(shí)際上隱藏了一個(gè)重要原則盡量把數(shù)據(jù)組織成“同類(lèi)型的一維數(shù)組”因?yàn)镹umPy對(duì)一維數(shù)組的廣播計(jì)算是最快、最不容易出錯(cuò)的。后續(xù)所有分組、聚合操作都是基于這些一維數(shù)組的布爾掩碼來(lái)完成的。2.2 處理缺失值和臟數(shù)據(jù)現(xiàn)實(shí)數(shù)據(jù)不可能全是干凈的我這份模擬數(shù)據(jù)里有少量缺失值。第一步永遠(yuǎn)是檢查缺失情況而不是上來(lái)就算均值# 按列統(tǒng)計(jì)缺失值數(shù)量 missing_count np.isnan(raw).sum(axis0) print(missing_count)如果發(fā)現(xiàn)某一列缺失值很多比如銷(xiāo)售額缺失了80條就要決定是刪除還是填充。刪除直接用布爾索引過(guò)濾填充則可以這樣寫(xiě)# 用該列的中位數(shù)填充缺失值 col_median np.nanmedian(sales_amt) sales_amt np.where(np.isnan(sales_amt), col_median, sales_amt)np.where是三元判斷的向量化版本滿(mǎn)足條件的位置取中位數(shù)不滿(mǎn)足的位置保留原值。這里用中位數(shù)而不是均值是因?yàn)橹形粩?shù)對(duì)離群點(diǎn)更穩(wěn)健不會(huì)因?yàn)槟硞€(gè)異常大值把填充值拉偏。臟數(shù)據(jù)也比你想的更常見(jiàn)。我檢查了一下這份數(shù)據(jù)里存在負(fù)銷(xiāo)售額、訂單數(shù)為0的記錄。碰到負(fù)銷(xiāo)售額第一反應(yīng)不是刪而是去確認(rèn)是不是退貨沖銷(xiāo)。如果業(yè)務(wù)上明確負(fù)值就是異常那就過(guò)濾掉# 篩掉銷(xiāo)售額為負(fù)的記錄 valid_mask sales_amt 0 store_id store_id[valid_mask] month month[valid_mask] product_id product_id[valid_mask] sales_amt sales_amt[valid_mask] volume volume[valid_mask] order_cnt order_cnt[valid_mask]注意過(guò)濾時(shí)必須把所有關(guān)聯(lián)的一維數(shù)組一起過(guò)濾只過(guò)濾一個(gè)數(shù)組會(huì)導(dǎo)致數(shù)據(jù)錯(cuò)位。這個(gè)錯(cuò)誤我見(jiàn)過(guò)太多新手在犯。2.3 數(shù)據(jù)類(lèi)型轉(zhuǎn)換和內(nèi)存控制拆列的時(shí)候我已經(jīng)做了類(lèi)型轉(zhuǎn)換整數(shù)字段轉(zhuǎn)int金額字段保持float。為什么在意這個(gè)因?yàn)轭?lèi)型決定了內(nèi)存占用和計(jì)算精度。一個(gè)float64占8字節(jié)2160行 × 6列約100KB感覺(jué)不到差異。但如果是1000萬(wàn)行數(shù)據(jù)那就是480MB直接讓內(nèi)存告急。實(shí)際生產(chǎn)環(huán)境里對(duì)于不需要極高精度的字段我會(huì)主動(dòng)降精度raw32 raw.astype(np.float32) print(raw32.itemsize) # 4內(nèi)存直接減半但有一個(gè)原則中間計(jì)算過(guò)程我通常保留float64避免累積誤差只有存檔、落庫(kù)時(shí)才用float32。比如大數(shù)據(jù)量下求方差、相關(guān)性如果你在float32下計(jì)算結(jié)果可能和float64差出不少。這不是玄學(xué)是浮點(diǎn)數(shù)表示精度問(wèn)題。3. 核心分析任務(wù)拆解與NumPy實(shí)現(xiàn)3.1 門(mén)店維度聚合用unique和bincount做分組先按門(mén)店匯總銷(xiāo)售額。最直觀的寫(xiě)法是循環(huán)加布爾掩碼store_ids np.unique(store_id) store_sales [] for sid in store_ids: mask store_id sid store_sales.append(sales_amt[mask].sum()) store_sales np.array(store_sales) print(store_ids) print(store_sales)這段代碼邏輯清晰但有一個(gè)性能問(wèn)題循環(huán)次數(shù)等于門(mén)店數(shù)。如果門(mén)店數(shù)只有9個(gè)無(wú)所謂但如果分組鍵有10萬(wàn)個(gè)取值循環(huán)就會(huì)非常慢。NumPy里更快的方式是np.bincount# bincount的下標(biāo)從0開(kāi)始所以門(mén)店1的數(shù)據(jù)落在結(jié)果下標(biāo)1的位置 store_sales_fast np.bincount(store_id, weightssales_amt) print(store_sales_fast)np.bincount第一個(gè)參數(shù)是分組鍵第二個(gè)參數(shù)是權(quán)重返回的結(jié)果就是每個(gè)分組的加權(quán)和。它比循環(huán)快一個(gè)數(shù)量級(jí)但要注意兩點(diǎn)分組鍵必須是非負(fù)整數(shù)結(jié)果長(zhǎng)度等于分組鍵最大值加1。這也是我在代碼里把store_id轉(zhuǎn)成int的原因。有了各門(mén)店的銷(xiāo)售額排名就很簡(jiǎn)單了sort_idx np.argsort(store_sales_fast)[::-1] for i in sort_idx: if i 0: continue # 跳過(guò)無(wú)意義的下標(biāo)0 print(f門(mén)店{i}: {store_sales_fast[i]:,.0f}元)市場(chǎng)份額也能順手算出來(lái)total_sales store_sales_fast.sum() share store_sales_fast / total_sales * 100這一步做完你已經(jīng)能看到頭部門(mén)店和尾部門(mén)店的差距了。3.2 時(shí)間趨勢(shì)分析按月份聚合門(mén)店聚合是橫向維度時(shí)間趨勢(shì)是縱向維度。按月份匯總可以這樣寫(xiě)months np.arange(1, 13) monthly_sales np.array([ sales_amt[month m].sum() for m in months ]) monthly_orders np.array([ order_cnt[month m].sum() for m in months ])這個(gè)寫(xiě)法很直白12個(gè)月的循環(huán)完全能接受。如果你想更優(yōu)雅一點(diǎn)可以用np.add.at做累加操作monthly_sales_v2 np.zeros(12) np.add.at(monthly_sales_v2, month - 1, sales_amt)np.add.at是不經(jīng)過(guò)中間數(shù)組的累加函數(shù)在多月份索引重復(fù)的情況下也能正確累加。它比循環(huán)快而且代碼更短。不過(guò)理解難度稍高所以我通常先講循環(huán)版本再說(shuō)優(yōu)化版本。有了月度銷(xiāo)售額環(huán)比率是個(gè)很自然的指標(biāo)mo_growth np.zeros(12) mo_growth[1:] np.diff(monthly_sales) / monthly_sales[:-1] * 100 for m, g in enumerate(mo_growth, 1): print(f{m}月: {g:.2f}%)np.diff是求相鄰元素差結(jié)果長(zhǎng)度比原數(shù)組少1。把第一個(gè)位置補(bǔ)0就能對(duì)齊月份下標(biāo)。注意用monthly_sales[:-1]做分母避免越界這個(gè)細(xì)節(jié)我在踩坑部分會(huì)再次強(qiáng)調(diào)。3.3 商品銷(xiāo)售結(jié)構(gòu)與異常值識(shí)別商品維度的聚合和門(mén)店完全一樣只是把分組鍵換成product_id。做完之后我習(xí)慣用np.argsort找出TOP5和BOTTOM5product_ids np.unique(product_id) product_sales np.bincount(product_id, weightssales_amt) top5_idx np.argsort(product_sales)[::-1][:5] bottom5_idx np.argsort(product_sales)[:5] print(銷(xiāo)售額TOP5商品, [(pid, product_sales[pid]) for pid in top5_idx if pid ! 0]) print(銷(xiāo)售額BOTTOM5商品, [(pid, product_sales[pid]) for pid in bottom5_idx])異常值識(shí)別我推薦用中位數(shù)絕對(duì)偏差法也就是MAD而不是普通Z-score。原因是Z-score的均值和標(biāo)準(zhǔn)差本身會(huì)被異常值污染一個(gè)極端大值會(huì)把標(biāo)準(zhǔn)差拉高導(dǎo)致真正的異常點(diǎn)被掩蓋。MAD的計(jì)算方式更穩(wěn)健med np.median(sales_amt) mad np.median(np.abs(sales_amt - med)) threshold 3 * 1.4826 * mad outliers np.abs(sales_amt - med) threshold print(異常記錄數(shù), outliers.sum()) print(異常銷(xiāo)售額, sales_amt[outliers])這里面的1.4826是個(gè)常數(shù)作用是讓MAD在正態(tài)分布下和標(biāo)準(zhǔn)差等價(jià)。如果你不想記這個(gè)數(shù)也可以用四分位距法IQR效果類(lèi)似q1, q3 np.percentile(sales_amt, [25, 75]) iqr q3 - q1 outliers_iqr (sales_amt q1 - 1.5 * iqr) | (sales_amt q3 1.5 * iqr)兩種方法選一種即可我實(shí)際項(xiàng)目里更常用MAD因?yàn)樗跇颖玖啃〉臅r(shí)候也穩(wěn)定。4. 統(tǒng)計(jì)指標(biāo)計(jì)算與業(yè)務(wù)解讀4.1 均值、標(biāo)準(zhǔn)差、分位數(shù)描述性統(tǒng)計(jì)是分析報(bào)告的骨架。NumPy一次性可以算出這些指標(biāo)overall_mean sales_amt.mean() overall_std sales_amt.std() quartiles np.percentile(sales_amt, [25, 50, 75]) q1, q2, q3 quartiles iqr q3 - q1 print(f銷(xiāo)售額均值{overall_mean:,.2f}) print(f標(biāo)準(zhǔn)差{overall_std:,.2f}) print(f四分位數(shù)Q1{q1:,.2f}, Q2{q2:,.2f}, Q3{q3:,.2f}) print(f四分位距{iqr:,.2f})均值反映平均水平標(biāo)準(zhǔn)差反映波動(dòng)程度分位數(shù)反映分布形態(tài)。比如Q1和Q3距離越遠(yuǎn)說(shuō)明中間50%數(shù)據(jù)的跨度越大。如果均值遠(yuǎn)大于中位數(shù)說(shuō)明存在右側(cè)長(zhǎng)尾也就是少數(shù)高銷(xiāo)售額記錄拉高了整體水平。光看整體不夠我還想看每個(gè)門(mén)店的月度波動(dòng)。把數(shù)據(jù)組織成二維矩陣門(mén)店為行、月份為列store_month_matrix np.zeros((9, 12)) for sid in store_ids: for m in range(1, 13): store_month_matrix[sid - 1, m - 1] sales_amt[(store_id sid) (month m)].sum()然后按行計(jì)算變異系數(shù)CV即標(biāo)準(zhǔn)差除以均值store_cv store_month_matrix.std(axis1) / store_month_matrix.mean(axis1)變異系數(shù)是個(gè)無(wú)量綱指標(biāo)適合比較不同門(mén)店的穩(wěn)定性。CV越小說(shuō)明門(mén)店月度銷(xiāo)售越平穩(wěn)CV大說(shuō)明某幾個(gè)月起伏特別劇烈。結(jié)合前面算的月度趨勢(shì)能快速定位問(wèn)題門(mén)店。4.2 相關(guān)性與增長(zhǎng)率分析銷(xiāo)量和銷(xiāo)售額的相關(guān)性很有意思。如果兩者高度正相關(guān)說(shuō)明業(yè)績(jī)主要由走量驅(qū)動(dòng)如果相關(guān)系數(shù)很低說(shuō)明不同商品的單價(jià)差異極大銷(xiāo)售結(jié)構(gòu)里可能有高客單商品在起作用。計(jì)算相關(guān)系數(shù)只需一行corr_matrix np.corrcoef(volume, sales_amt) corr corr_matrix[0, 1] print(f銷(xiāo)量與銷(xiāo)售額相關(guān)系數(shù){corr:.4f})np.corrcoef返回的是相關(guān)系數(shù)矩陣對(duì)角線(xiàn)都是1非對(duì)角線(xiàn)就是兩兩之間的相關(guān)系數(shù)。我見(jiàn)過(guò)很多新手直接print(corr_matrix)然后看著輸出發(fā)懵這里明確說(shuō)一下取值方式。增長(zhǎng)率方面除了前面算的月度環(huán)比還可以算全年增長(zhǎng)total_growth (monthly_sales[-1] - monthly_sales[0]) / monthly_sales[0] * 100 print(f全年銷(xiāo)售額增長(zhǎng)率{total_growth:.2f}%)不過(guò)環(huán)比更能看出業(yè)務(wù)節(jié)奏比如3月環(huán)比2月漲了多少、9月環(huán)比8月漲了多少。這些數(shù)字最后都要落到業(yè)務(wù)解釋上漲是因?yàn)楣?jié)日促銷(xiāo)跌是因?yàn)榇汗?jié)放假而不是空對(duì)空。分析結(jié)果如果脫離業(yè)務(wù)背景就只是一堆數(shù)字。4.3 結(jié)果導(dǎo)出與可視化銜接分析結(jié)果最終要交付給業(yè)務(wù)方。用np.savetxt導(dǎo)出月度匯總summary np.column_stack((months, monthly_sales, monthly_orders)) np.savetxt( monthly_summary.csv, summary, delimiter,, headermonth,sales_amt,order_cnt, comments, fmt%.2f )這里fmt%.2f控制導(dǎo)出格式避免輸出一長(zhǎng)串浮點(diǎn)數(shù)。header參數(shù)會(huì)在CSV第一行寫(xiě)表頭comments是去掉默認(rèn)的#注釋前綴。可視化方面NumPy的數(shù)組可以直接喂給matplotlib不需要轉(zhuǎn)成Python列表import matplotlib.pyplot as plt plt.plot(months, monthly_sales, markero) plt.title(2024月度銷(xiāo)售額趨勢(shì)) plt.xlabel(月份) plt.ylabel(銷(xiāo)售額) plt.show()如果你后續(xù)想畫(huà)更復(fù)雜的圖比如門(mén)店Top10柱狀圖、商品貢獻(xiàn)餅圖從NumPy數(shù)組到matplotlib是零成本的。這也是我一直強(qiáng)調(diào)先學(xué)NumPy的原因它和整個(gè)Python數(shù)據(jù)生態(tài)的接口是天然對(duì)齊的。5. 踩坑實(shí)錄與性能優(yōu)化技巧5.1 我遇到過(guò)的典型坑這個(gè)案例我跑了很多遍也踩過(guò)很多坑。以下幾條是個(gè)人實(shí)戰(zhàn)中最高頻的問(wèn)題整理成速查表問(wèn)題現(xiàn)象根本原因解決辦法讀取CSV報(bào)UnicodeDecodeError文件編碼不是系統(tǒng)默認(rèn)編碼np.genfromtxt加encodingutf-8聚合結(jié)果下標(biāo)錯(cuò)位忘記np.bincount從0開(kāi)始門(mén)店編號(hào)從1開(kāi)始時(shí)結(jié)果[0]直接忽略求均值得到nan數(shù)據(jù)里有缺失值沒(méi)先排查先np.isnan().sum()檢查再填充或過(guò)濾np.diff結(jié)果錯(cuò)位結(jié)果長(zhǎng)度比原數(shù)組少1先初始化一個(gè)全0數(shù)組再錯(cuò)位賦值過(guò)濾條件后數(shù)據(jù)全亂只過(guò)濾了部分?jǐn)?shù)組所有關(guān)聯(lián)數(shù)組必須同步應(yīng)用同一掩碼浮點(diǎn)數(shù)比較不穩(wěn)定精度問(wèn)題金額統(tǒng)一乘以100轉(zhuǎn)整數(shù)處理或用np.isclose其中下標(biāo)錯(cuò)位這個(gè)問(wèn)題是我自己在用np.bincount時(shí)踩得最多的一回。門(mén)店編號(hào)從1開(kāi)始結(jié)果數(shù)組下標(biāo)0沒(méi)有任何數(shù)據(jù)導(dǎo)致所有后續(xù)排名和中位數(shù)的理解都偏了一位。所以我現(xiàn)在只要用到bincount第一件事就是打印len(result)確認(rèn)是否多了一個(gè)無(wú)用下標(biāo)。5.2 讓NumPy跑得更快的小習(xí)慣代碼能跑通和跑得快是兩回事。以下幾個(gè)習(xí)慣我是在處理百萬(wàn)級(jí)數(shù)據(jù)時(shí)才真正體會(huì)到的第一能用向量化運(yùn)算就不寫(xiě)for循環(huán)。NumPy的加減乘除、比較、邏輯運(yùn)算都是按元素執(zhí)行的速度極快。比如分組求和用np.bincount替代循環(huán)速度能提升幾十倍。第二避免在循環(huán)里拼接數(shù)組。很多人會(huì)寫(xiě)arr np.append(arr, x)這是性能殺手。正確做法是先分配一個(gè)足夠大的結(jié)果數(shù)組再用索引賦值填進(jìn)去。舉個(gè)例子# 不推薦循環(huán)內(nèi)append result np.array([]) for x in range(10000): result np.append(result, np.sin(x)) # 推薦預(yù)分配空間 result np.zeros(10000) for i in xrange(10000): result[i] np.sin(i)第三注意復(fù)制和視圖的區(qū)別。b a只是把b指向同一個(gè)數(shù)組修改b會(huì)影響a這經(jīng)常導(dǎo)致莫名其妙的bug。如果確實(shí)要獨(dú)立副本用b a.copy()。第四隨機(jī)數(shù)生成統(tǒng)一用np.random.default_rng()而不是老的np.random.seed()。前者生成的隨機(jī)數(shù)質(zhì)量更好而且線(xiàn)程安全數(shù)據(jù)模擬、抽樣都會(huì)用到。5.3 如何擴(kuò)展成自動(dòng)化腳本分析做一次不難難的是每天、每周重復(fù)做。所以我會(huì)把代碼封裝成函數(shù)輸入是CSV路徑輸出是幾個(gè)匯總數(shù)組def build_sales_summary(csv_path): raw np.genfromtxt(csv_path, delimiter,, skip_header1, dtypefloat, encodingutf-8) store_id raw[:, 0].astype(int) month raw[:, 1].astype(int) product_id raw[:, 2].astype(int) sales_amt raw[:, 3] volume raw[:, 4] order_cnt raw[:, 5].astype(int) # 缺失值填充、異常值過(guò)濾… monthly_sales np.array([sales_amt[month m].sum() for m in range(1, 13)]) store_sales np.bincount(store_id, weightssales_amt) product_sales np.bincount(product_id, weightssales_amt) return monthly_sales, store_sales, product_sales后續(xù)接定時(shí)任務(wù)、寫(xiě)報(bào)告、發(fā)郵件都是圍繞這個(gè)函數(shù)展開(kāi)。我在真實(shí)項(xiàng)目里就是這么干的NumPy負(fù)責(zé)所有核心計(jì)算Pandas或openpyxl負(fù)責(zé)生成Excel報(bào)表matplotlib負(fù)責(zé)出圖。最后再分享一個(gè)小技巧分析過(guò)程中多打印數(shù)組形狀和前幾行數(shù)據(jù)能避免一半以上的邏輯錯(cuò)誤。NumPy的數(shù)組運(yùn)算很強(qiáng)大但一旦形狀對(duì)不上報(bào)錯(cuò)信息往往不直觀。養(yǎng)成print(arr.shape)的習(xí)慣很多時(shí)候能省下半小時(shí)的排查時(shí)間。我個(gè)人在實(shí)際操作中的體會(huì)是這個(gè)案例的價(jià)值不在統(tǒng)計(jì)數(shù)字本身而在把一個(gè)業(yè)務(wù)問(wèn)題拆成數(shù)組操作的過(guò)程。數(shù)據(jù)量小的時(shí)候感覺(jué)不到差距一旦上了幾十萬(wàn)行寫(xiě)不寫(xiě)向量化就是秒級(jí)和分鐘級(jí)的差別。所以我在項(xiàng)目里一直堅(jiān)持先用NumPy把核心邏輯驗(yàn)證通過(guò)再?zèng)Q定要不要引入更上層的工具?;A(chǔ)打牢了往上走反而更快。