職場(chǎng)人員使用 Excel 進(jìn)行數(shù)據(jù)處理已經(jīng)成為家常便飯。不過(guò)相信大家一定有過(guò)很無(wú)助的情況,比如復(fù)雜計(jì)算、重復(fù)計(jì)算、自動(dòng)處理等,再遇上個(gè)死機(jī)沒(méi)保存,整個(gè)人崩潰掉也不是完全不可能。
如果學(xué)會(huì)了程序語(yǔ)言,這些問(wèn)題就都不是事了。那么,該學(xué)什么呢?
無(wú)數(shù)培訓(xùn)機(jī)構(gòu)和網(wǎng)上資料都會(huì)告訴我們:Python!
Python 代碼看起來(lái)很簡(jiǎn)單,只要幾行就能解決許多麻煩的 Excel 計(jì)算,看起來(lái)真不錯(cuò)。
但真是如此嗎?作為非專(zhuān)業(yè)人員,真能學(xué)得會(huì) Python 來(lái)協(xié)助我們工作嗎?
Python DataFrame
日常職場(chǎng)業(yè)務(wù)主要是處理表格類(lèi)數(shù)據(jù)(用專(zhuān)業(yè)的說(shuō)法是結(jié)構(gòu)化數(shù)據(jù)),比如這樣的:

表里除第一行外的每行數(shù)據(jù)稱(chēng)為一條記錄,對(duì)應(yīng)了一件事、一個(gè)人、一張訂單……,第一行是標(biāo)題,說(shuō)明記錄由哪些屬性構(gòu)成,這些記錄都有相同的屬性,整個(gè)表就是這樣一些記錄的集合。
Python 主要是用一個(gè)叫 DataFrame 的東西來(lái)處理這類(lèi)表格數(shù)據(jù),我們來(lái)看看 DataFrame 是怎么做的。
比如上面的表格,讀入 DataFrame 后是這樣的:

看起來(lái)和 Excel 差不多,只是行號(hào)是從 0 開(kāi)始的。
但是,DataFrame 的本質(zhì)是一個(gè)矩陣(大學(xué)時(shí)代的線(xiàn)性代數(shù)還想得起來(lái)嗎?),Python 也沒(méi)有記錄這樣的概念,它的運(yùn)算都要繞到矩陣可以執(zhí)行的方法上才行。
我們來(lái)看一些簡(jiǎn)單運(yùn)算。
過(guò)濾
過(guò)濾是個(gè)簡(jiǎn)單常見(jiàn)的運(yùn)算,就是把滿(mǎn)足某一條件的子集取出來(lái),比如還是上面的表格:

問(wèn)題一:取出 R&D 部門(mén)的員工。
Python 代碼是這樣的:

運(yùn)行結(jié)果:

代碼很簡(jiǎn)單,結(jié)果也沒(méi)問(wèn)題。但是:
1.???? 用到的函數(shù)叫 loc,是 location(定位)的縮寫(xiě),完全沒(méi)有過(guò)濾的意思。事實(shí)上,這里的過(guò)濾也是通過(guò)定位(location)滿(mǎn)足條件的行的索引來(lái)實(shí)現(xiàn)的,函數(shù)里面的 data[‘DEPT’]==’R&D’會(huì)算出一個(gè)布爾值構(gòu)成的 Series:

和 data 的索引相同,滿(mǎn)足條件的行為 True 否則為 False
然后 loc 就是根據(jù)取值為 True 的行對(duì)應(yīng)的索引再取出 data 中相應(yīng)的行再得到一個(gè)新的 DataFrame,本質(zhì)上是從矩陣中抽取指定行的運(yùn)算,用來(lái)對(duì)付過(guò)濾就有點(diǎn)繞。
2.???? 過(guò)濾 DataFrame 并不只可以使用 loc 函數(shù)過(guò)濾,還可以用 query(…) 等方法,但結(jié)果都是定位到矩陣的行列索引,然后按行列索引取數(shù)據(jù),大體上是這樣的 matrix.loc[row,col]
無(wú)論如何,基本的過(guò)濾還算簡(jiǎn)單吧,講明白了也能理解。下面我們?cè)賴(lài)L試對(duì)過(guò)濾后的子集做兩個(gè)算不上復(fù)雜的運(yùn)算看看。
修改子集中的數(shù)據(jù)
問(wèn)題二:將 R&D 部門(mén)員工的工資上調(diào) 5%
自然的想法,只要過(guò)濾出 R&D 部門(mén)員工,然后對(duì)這些員工的工資進(jìn)行修改就可以了。
按照這種邏輯寫(xiě)出代碼:

運(yùn)行結(jié)果:
SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead
See the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
? rd['SALARY']=rd['SALARY']*1.05

可以看到,不僅觸發(fā)了警告,修改值也沒(méi)有成功。
這是因?yàn)閞d = data.loc[data['DEPT']=='R&D']是一個(gè)過(guò)濾后的矩陣,再使用rd['SALARY']=rd['SALARY']*1.05這個(gè)語(yǔ)句修改 SALARY 值的時(shí)候,rd['SALARY']又是一個(gè)新的矩陣了,因此修改它其實(shí)是修改的 rd 這個(gè)子矩陣,并沒(méi)有修改 data 這個(gè)最初的矩陣。
這話(huà)說(shuō)著很繞,聽(tīng)著也繞。
正確的代碼怎么寫(xiě)呢?

運(yùn)行結(jié)果:

這次對(duì)了。不可以先取出子集再修改,要對(duì)著原矩陣,找到要修改的成員的定位再來(lái)修改,即 loc[row=data['DEPT']=='R&D',column='SALARY'],按照行列索引取到要修改的數(shù)據(jù),對(duì)著這個(gè)矩陣賦值。想要上調(diào) 5% 還要在此之前先拿到這份數(shù)據(jù)(rd_salary=…這句)。這種寫(xiě)法要進(jìn)行重復(fù)的過(guò)濾,效率低也就罷了,但實(shí)在是太繞了。
子集求交
問(wèn)題三:找出既是紐約州又是 R&D 部門(mén)的員工
這個(gè)問(wèn)題更簡(jiǎn)單,只要算出兩個(gè)子集做個(gè)交集運(yùn)算就完了。我們看看 Python 是如何處理的:

運(yùn)行結(jié)果:

集合求交是非常基本的運(yùn)算,很多程序語(yǔ)言都提供了,事實(shí)上 python 也提供了(上面有 intersection 函數(shù))。然而, DataFrame 的本質(zhì)是矩陣,兩個(gè)矩陣求交集卻沒(méi)有什么意義,Python 也就沒(méi)有提供矩陣求交集的運(yùn)算。想要做到用兩個(gè) dataframe 表示的集合的交集運(yùn)算,只能繞道去求兩個(gè)矩陣索引的交集,最后再利用索引的交集從原數(shù)據(jù)上定位截取,有種舍近求遠(yuǎn)的感覺(jué),不按“套路”出牌。
工作中最常用的過(guò)濾運(yùn)算都這么令人費(fèi)解,繞的腦袋暈,可以想象其他更復(fù)雜的運(yùn)算,一股酸爽的感覺(jué)“悠然而生”。
下面看下稍微復(fù)雜一點(diǎn)的分組運(yùn)算:
分組
分組運(yùn)算是日常數(shù)據(jù)處理中最常用的運(yùn)算了,Python 也提供了豐富的分組運(yùn)算函數(shù),能夠完成大多數(shù)的分組運(yùn)算,但在理解和使用上并沒(méi)有那么容易。
分組理解
分組就是把一個(gè)大集合按某種規(guī)則分成一些小集合,結(jié)果是個(gè)由集合構(gòu)成的集合,然后再對(duì)分組后的集合進(jìn)行運(yùn)算,如下圖:

先來(lái)看下最常用的分組聚合運(yùn)算。
問(wèn)題四:匯總各部門(mén)的人數(shù)
Python 代碼:

運(yùn)行結(jié)果:

結(jié)果好像有點(diǎn)尷尬,本來(lái)只需要記錄每個(gè)分組中的成員數(shù)量,只要有一列就行了,為什么出來(lái)這么多列,它像是對(duì)每一列都重復(fù)做了同樣的動(dòng)作,好奇怪。
別急,這個(gè)問(wèn)題 Python 還是可以解決的,只不過(guò)不是用 count 函數(shù),而是 size 函數(shù):

運(yùn)行結(jié)果:

這個(gè)結(jié)果看起來(lái)就正常多了,不過(guò),還是感覺(jué)哪里怪怪的。
是滴,這個(gè)結(jié)果不再是二維的 DataFrame 了,而是個(gè)單維的 Seriese。
count 函數(shù)計(jì)算的結(jié)果之所以奇怪,是因?yàn)樗菍?duì)每一列計(jì)數(shù),而 size 函數(shù)是查看各組的大小,但其實(shí)我們自然的邏輯還是用 count 來(lái)計(jì)數(shù),size 很難用自然的邏輯想到(還要上網(wǎng)搜資料)。
如前所述,分組結(jié)果應(yīng)該是集合的集合,我們看看 Python 中的 DataFrame 分組后是什么樣子呢?把上面代碼中 data.groupby(“DEPT”) 的結(jié)果打印出來(lái)看。

運(yùn)行結(jié)果:
<pandas.core.groupby.generic.DataFrameGroupBy object at 0x000001ADBC9CE0F0>
哇,這是個(gè)什么東東?
第一次看到這個(gè)東西,直接就蒙圈了,分組的結(jié)果不應(yīng)該是集合的集合嗎,為什么會(huì)是這樣?這對(duì)于非專(zhuān)業(yè)程序人員來(lái)說(shuō)簡(jiǎn)直如同夢(mèng)魘。
不過(guò)上網(wǎng)搜搜還是可以看到它是一個(gè)所謂的可迭代對(duì)象,迭代以后發(fā)現(xiàn)它的每一條都是以分組索引 + DataFrame 構(gòu)成的,可以使用一些方法看到里邊的內(nèi)容,如使用 list(group) 就可以看到分組的結(jié)果了。如下圖:

看到上圖以后就會(huì)明白,被稱(chēng)為“對(duì)象”的東西里面原來(lái)是這樣的。本質(zhì)上它也確實(shí)是個(gè)集合的集合(姑且把矩陣?yán)斫獬杉习桑?,但它并不能像普通的集合那樣直接取某個(gè)成員 (如 group[0]),這在使用上迫使用戶(hù)強(qiáng)行記憶這類(lèi)“對(duì)象”的 N 種運(yùn)算規(guī)則,理解不了就只能死記硬背了。
看到這里,估計(jì)已經(jīng)有很多讀者開(kāi)始暈菜了,徹底不明白上面這段話(huà)是在胡說(shuō)八道些什么。嗯,這就對(duì)了,因?yàn)檫@才是職場(chǎng)人員的正常狀態(tài)。
分組中簡(jiǎn)單的聚合運(yùn)算都如此難以理解,我們?cè)贌裏X,看下稍微復(fù)雜一點(diǎn)的分組后子集合的運(yùn)算。
分組子集處理
雖然分組后經(jīng)常用于聚合運(yùn)算,但有時(shí)我們并不關(guān)心聚合結(jié)果,而是關(guān)心分組后的集合本身。比如分組后的集合按某一列排序。
問(wèn)題五:將各部門(mén)員工按照入職時(shí)間從早到晚進(jìn)行排序 。
問(wèn)題分析:分組后對(duì)子集按照入職時(shí)間排序即可。
Python 代碼

運(yùn)行結(jié)果:

結(jié)果沒(méi)問(wèn)題,各個(gè)部門(mén)員工都按照入職時(shí)間從早到晚排序了。但我們觀(guān)察下代碼中最核心的一句employee.groupby('DEPT',as_index=False).apply(lambda x:x.sort_values('HIREDATE')),把這句代碼抽象一下就是這樣:
df.groupby(c).apply(lambda x:f(x))
df:數(shù)據(jù)框 DataFrame
groupby:分組函數(shù)
c:分組依據(jù)的列
以上三個(gè)還是比較好理解的,可是 apply 配合 lambda 就十分晦澀難懂了,超出了大多數(shù)非專(zhuān)業(yè)程序人員理解的范疇,這需要明白所謂“函數(shù)語(yǔ)言”的原理才能搞懂(自己去搜索,俺懶得解釋了)。
如果不使用這“二位”(apply+lambda)呢?也能做,就是會(huì)很麻煩。得用 for 循環(huán),對(duì)每個(gè)分組子集分別排序,最后還得把結(jié)果合并起來(lái)。

運(yùn)行結(jié)果相同,但代碼復(fù)雜了很多,而且運(yùn)行效率也變低了。你愿意用哪一種呢?
Python 對(duì)于類(lèi)似但不完全一樣的數(shù)據(jù)設(shè)計(jì)了不同的數(shù)據(jù)類(lèi)型,也對(duì)應(yīng)有不同的操作方式,并不能簡(jiǎn)單地把對(duì)某種數(shù)據(jù)的知識(shí)復(fù)制到另一個(gè)類(lèi)似數(shù)據(jù)上,搞得人暈死。
說(shuō)了這么多,總結(jié)下來(lái)就是一句話(huà):Python 真的挺難懂的,它就不是一個(gè)面向非專(zhuān)業(yè)選手的東西。具體來(lái)說(shuō)大概就是三點(diǎn):
1.???? DataFrame 本質(zhì)是矩陣
所有的運(yùn)算都要想辦法按矩陣的方法來(lái)計(jì)算,經(jīng)常會(huì)很繞。
2.???? 數(shù)據(jù)類(lèi)型多而且運(yùn)算規(guī)則差別很大
Python 中設(shè)計(jì)了 Series,DataFrame,分組對(duì)象等等不同的數(shù)據(jù)類(lèi)型,而且不同的數(shù)據(jù)類(lèi)型,計(jì)算方法也不完全相同,如 DataFrame 可以使用 query 函數(shù)過(guò)濾,而 Series 不可以,分組對(duì)象的本質(zhì)完全不同于 Series 和 DataFrame,計(jì)算方法更是難以捉摸。
3.???? 知其然而不知其所以然
數(shù)據(jù)類(lèi)型過(guò)多,計(jì)算方法差別又大,無(wú)形之中增加了用戶(hù)的記憶量,死記硬背的成分更多,想要靈活運(yùn)用太難了,這就造成了一種奇怪的現(xiàn)象:一個(gè)簡(jiǎn)單的運(yùn)算,上網(wǎng)搜索 Python 代碼的時(shí)間可能比用 excel 計(jì)算還要長(zhǎng)。
Python 代碼看起來(lái)簡(jiǎn)單,但你上了培訓(xùn)班也大概率學(xué)不會(huì),結(jié)果只會(huì)抄例子。
那么,是不是就沒(méi)有適合職場(chǎng)人員進(jìn)行日常數(shù)據(jù)處理的工具了嗎?
還是有的。
esProc SPL
esProc SPL 也是一種程序設(shè)計(jì)語(yǔ)言,專(zhuān)注于結(jié)構(gòu)化數(shù)據(jù)計(jì)算。SPL 中提供了豐富的基礎(chǔ)計(jì)算方法,其概念邏輯也是符合我們的思維習(xí)慣的。
1.???? 序表是記錄的集合
SPL 使用序表承載結(jié)構(gòu)化數(shù)據(jù),接近于日常處理的 excel 表。
2.???? 數(shù)據(jù)類(lèi)型少且規(guī)則一致
SPL 進(jìn)行結(jié)構(gòu)化數(shù)據(jù)處理時(shí)幾乎只有集合和記錄兩種數(shù)據(jù)類(lèi)型,涉及到的方法也大體一致。
3.???? 知其然且知其所以然
只要記住兩種數(shù)據(jù)類(lèi)型,掌握基本的運(yùn)算法則,更復(fù)雜的運(yùn)算就只是簡(jiǎn)單運(yùn)算規(guī)則的組合。不熟練時(shí)可能寫(xiě)的代碼不好看,但不太可能寫(xiě)不出來(lái),不會(huì)出現(xiàn) Python 那種花費(fèi)大量時(shí)間搜索代碼寫(xiě)法的現(xiàn)象。
下面我們就使用 SPL 來(lái)解決上述介紹的問(wèn)題,大家認(rèn)真體會(huì)下 SPL 是多么“平易近人”:
序表
esProc SPL 中用于承載二維結(jié)構(gòu)化數(shù)據(jù)的數(shù)據(jù)結(jié)構(gòu)是序表,它和 excel 中呈現(xiàn)的結(jié)果一致,如下圖:

上表中除了第一行(標(biāo)題行)外,其他每一行表示一條記錄,而序表就是記錄的集合,相較于 Python 中的 DataFrame 更加直觀(guān)。
過(guò)濾
SPL 中并不是按照矩陣定位的方式過(guò)濾,而是 select(篩選)出滿(mǎn)足條件的記錄。
問(wèn)題一:查看 R&D 部門(mén)的員工信息

A2 結(jié)果:

SPL 過(guò)濾后的結(jié)果非常好理解,就是原始數(shù)據(jù)集合的一個(gè)子集。
再來(lái)看看 SPL 對(duì)子集修改和求交集運(yùn)算
1.???? 修改子集中的數(shù)據(jù)
問(wèn)題二:將 R&D 部門(mén)員工的工資上調(diào) 5%

A4 結(jié)果:

SPL 完全是按照我們正常的思維方式來(lái)計(jì)算的,過(guò)濾出結(jié)果,對(duì)著結(jié)果修改工資,而不像 Python 那么費(fèi)勁。
2.???? 子集求交
問(wèn)題三:找出既是紐約州又是 R&D 部門(mén)的員工

A4 結(jié)果:

SPL 中的交集運(yùn)算就是對(duì)著集合求交集,是真正的集合運(yùn)算,只使用一個(gè)簡(jiǎn)單的交集運(yùn)算符“^”即可。易于理解,而且書(shū)寫(xiě)簡(jiǎn)單,而不用像 Python 那樣因?yàn)闊o(wú)法求矩陣的交集而去求索引的交集,然后再?gòu)脑瓟?shù)據(jù)中截取。SPL 中的其他集合運(yùn)算如并集、差集、異或集也都有對(duì)應(yīng)的運(yùn)算符,使用起來(lái)簡(jiǎn)單,方便。
分組
分組理解
SPL 的分組運(yùn)算也是符合自然邏輯的,即分組后結(jié)果是集合的集合,顯而易見(jiàn)。
先來(lái)看看 SPL 的分組聚合運(yùn)算。
問(wèn)題四:匯總各部門(mén)的人數(shù)

A2 結(jié)果:

分組聚合的結(jié)果,仍然是序表,可以繼續(xù)使用序表的方法。并不像 Python 聚合結(jié)果成了單維的 Series。
再來(lái)看下 SPL 的分組結(jié)果

A2 結(jié)果:

上圖是序表的集合,每個(gè)集合是一個(gè)部門(mén)的成員構(gòu)成的序表;下圖是點(diǎn)開(kāi)第一個(gè)分組的成員——Administration 部門(mén)成員的序表。
這種結(jié)果符合我們的正常邏輯,也容易查看分組的結(jié)果,更容易對(duì)分組結(jié)果進(jìn)行接下來(lái)的運(yùn)算。
分組子集處理
分組的結(jié)果是集合的集合,只要把每個(gè)子集進(jìn)行處理即可。
問(wèn)題五:將各部門(mén)員工按照入職時(shí)間從早到晚進(jìn)行排序

A3 結(jié)果:

由于 SPL 的分組結(jié)果還是個(gè)集合,因此它可以使用集合的計(jì)算方法計(jì)算,并不需要強(qiáng)行記憶分組后的計(jì)算方法,更不需要使用 apply()+lambda 這種天書(shū)般的組合,非常自然的就完成了分組 + 排序 + 合并的工作,簡(jiǎn)單的 3 行代碼,既好寫(xiě),又好理解,而且效率很高。
小結(jié)
1.???? Python 進(jìn)行結(jié)構(gòu)化處理時(shí),本質(zhì)都是矩陣運(yùn)算,簡(jiǎn)單的集合運(yùn)算需要繞到矩陣上去運(yùn)算;esProc SPL 本質(zhì)是記錄的集合,集合運(yùn)算簡(jiǎn)單便捷。
2.???? Python 數(shù)據(jù)類(lèi)型復(fù)雜多樣,運(yùn)算規(guī)則不可預(yù)測(cè),往往是知其然而不知其所以然,不太可能舉一反三,寫(xiě)代碼記憶的成分更多,想理解其原理太難了;esProc SPL 數(shù)據(jù)類(lèi)型少,而且計(jì)算規(guī)則固定,只需要掌握基本的運(yùn)算規(guī)則就可以舉一反三的完成復(fù)雜的運(yùn)算。
3.???? 學(xué)習(xí) SPL,可以到: