Python 並不適合職場程式設計

cainiao_M發表於2020-11-24

職場人員使用 Excel 進行資料處理已經成為家常便飯。不過相信大家一定有過很無助的情況,比如複雜計算、重複計算、自動處理等,再遇上個當機沒儲存,整個人崩潰掉也不是完全不可能。

如果學會了程式語言,這些問題就都不是事了。那麼,該學什麼呢?

無數培訓機構和網上資料都會告訴我們:Python!

Python 程式碼看起來很簡單,只要幾行就能解決許多麻煩的 Excel 計算,看起來真不錯。

但真是如此嗎?作為非專業人員,真能學得會 Python 來協助我們工作嗎?

日常職場業務主要是處理表格類資料(用專業的說法是結構化資料),比如這樣的:

..

表裡除第一行外的每行資料稱為一條記錄,對應了一件事、一個人、一張訂單……,第一行是標題,說明記錄由哪些屬性構成,這些記錄都有相同的屬性,整個表就是這樣一些記錄的集合。

Python 主要是用一個叫 DataFrame 的東西來處理這類表格資料,我們來看看 DataFrame 是怎麼做的。

比如上面的表格,讀入 DataFrame 後是這樣的:

..

看起來和 Excel 差不多,只是行號是從 0 開始的。

但是,DataFrame 的本質是一個矩陣(大學時代的線性代數還想得起來嗎?),Python 也沒有記錄這樣的概念,它的運算都要繞到矩陣可以執行的方法上才行。

我們來看一些簡單運算。

過濾是個簡單常見的運算,就是把滿足某一條件的子集取出來,比如還是上面的表格:

..

問題一:取出 R&D 部門的員工。

Python 程式碼是這樣的:

| import pandas as pddata = pd.read_csv(‘Employees.csv’)rd = data.loc[data[‘DEPT’]==’R&D’]print(rd) | 匯入 Pandas讀取資料過濾 R&D 部門檢視 rd 資料 |

執行結果:

..

程式碼很簡單,結果也沒問題。但是:

  1. 用到的函式叫 loc,是 location(定位)的縮寫,完全沒有過濾的意思。事實上,這裡的過濾也是通過定位(location)滿足條件的行的索引來實現的,函式裡面的 data[‘DEPT’]==’R&D’會算出一個布林值構成的 Series:

..

和 data 的索引相同,滿足條件的行為 True 否則為 False

然後 loc 就是根據取值為 True 的行對應的索引再取出 data 中相應的行再得到一個新的 DataFrame,本質上是從矩陣中抽取指定行的運算,用來對付過濾就有點繞。

  1. 過濾 DataFrame 並不只可以使用 loc 函式過濾,還可以用 query(…) 等方法,但結果都是定位到矩陣的行列索引,然後按行列索引取資料,大體上是這樣的 matrix.loc[row,col]

無論如何,基本的過濾還算簡單吧,講明白了也能理解。下面我們再嘗試對過濾後的子集做兩個算不上覆雜的運算看看。

修改子集中的資料

問題二:將 R&D 部門員工的工資上調 5%

自然的想法,只要過濾出 R&D 部門員工,然後對這些員工的工資進行修改就可以了。

按照這種邏輯寫出程式碼:

| import pandas as pddata = pd.read_csv(‘Employees.csv’)rd = data.loc[data[‘DEPT’]==’R&D’]rd[‘SALARY’]=rd[‘SALARY’]*1.05print(data) | 匯入 Pandas讀取資料過濾 R&D 部門修改 SALARY |

執行結果:

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: pandas.pydata.org/pandas-docs/stabl...

  • rd[‘SALARY’]=rd[‘SALARY’]1.05

..

可以看到,不僅觸發了警告,修改值也沒有成功。

這是因為rd = data.loc[data[‘DEPT’]==’R&D’]是一個過濾後的矩陣,再使用rd[‘SALARY’]=rd[‘SALARY’]*1.05這個語句修改 SALARY 值的時候,rd[‘SALARY’]又是一個新的矩陣了,因此修改它其實是修改的 rd 這個子矩陣,並沒有修改 data 這個最初的矩陣。

這話說著很繞,聽著也繞。

正確的程式碼怎麼寫呢?

| import pandas as pddata = pd.read_csv(‘Employees.csv’)rd_salary = data.loc[data[‘DEPT’]==’R&D’,’SALARY’]data.loc[data[‘DEPT’]==’R&D’,’SALARY’] = rd_salary*1.05print(data) | 找到 R&D 部門的員工工資擷取 R&D 部門的員工工資並修改 |

執行結果:

..

這次對了。不可以先取出子集再修改,要對著原矩陣,找到要修改的成員的定位再來修改,即 loc[row=data[‘DEPT’]==’R&D’,column=’SALARY’],按照行列索引取到要修改的資料,對著這個矩陣賦值。想要上調 5% 還要在此之前先拿到這份資料(rd_salary=…這句)。這種寫法要進行重複的過濾,效率低也就罷了,但實在是太繞了。

子集求交

問題三:找出既是紐約州又是 R&D 部門的員工

這個問題更簡單,只要算出兩個子集做個交集運算就完了。我們看看 Python 是如何處理的:

| import pandas as pddata = pd.read_csv(‘Employees.csv’)rd = data[data[‘DEPT’]==’R&D’]ny = data[data[‘STATE’]==’New York’]isect_idx = rd.index.intersection(ny.index)rd_isect_ny = data.loc[isect_idx]print(rd_isect_ny) | R&D部門員工紐約州員工索引求交集按索引交集擷取資料 |

執行結果:

..

集合求交是非常基本的運算,很多程式語言都提供了,事實上 python 也提供了(上面有 intersection 函式)。然而, DataFrame 的本質是矩陣,兩個矩陣求交集卻沒有什麼意義,Python 也就沒有提供矩陣求交集的運算。想要做到用兩個 dataframe 表示的集合的交集運算,只能繞道去求兩個矩陣索引的交集,最後再利用索引的交集從原資料上定位擷取,有種捨近求遠的感覺,不按“套路”出牌。

工作中最常用的過濾運算都這麼令人費解,繞的腦袋暈,可以想象其他更復雜的運算,一股酸爽的感覺“悠然而生”。

下面看下稍微複雜一點的分組運算:

分組運算是日常資料處理中最常用的運算了,Python 也提供了豐富的分組運算函式,能夠完成大多數的分組運算,但在理解和使用上並沒有那麼容易。

分組理解

分組就是把一個大集合按某種規則分成一些小集合,結果是個由集合構成的集合,然後再對分組後的集合進行運算,如下圖:

..

先來看下最常用的分組聚合運算。

問題四:彙總各部門的人數

Python 程式碼:

| import pandas as pddata = pd.read_csv(‘Employees.csv’)group = data.groupby(“DEPT”)dept_num = group.count()print(dept_num) | 按照部門分組彙總各部門人數 |

執行結果:

..

結果好像有點尷尬,本來只需要記錄每個分組中的成員數量,只要有一列就行了,為什麼出來這麼多列,它像是對每一列都重複做了同樣的動作,好奇怪。

別急,這個問題 Python 還是可以解決的,只不過不是用 count 函式,而是 size 函式:

| import pandas as pddata = pd.read_csv(‘Employees.csv’)group = data.groupby(“DEPT”)dept_num = group.size()print(dept_num) | 按照部門分組彙總各部門人數 |

執行結果:

..

這個結果看起來就正常多了,不過,還是感覺哪裡怪怪的。

是滴,這個結果不再是二維的 DataFrame 了,而是個單維的 Seriese。

count 函式計算的結果之所以奇怪,是因為它是對每一列計數,而 size 函式是檢視各組的大小,但其實我們自然的邏輯還是用 count 來計數,size 很難用自然的邏輯想到(還要上網搜資料)。

如前所述,分組結果應該是集合的集合,我們看看 Python 中的 DataFrame 分組後是什麼樣子呢?把上面程式碼中 data.groupby(“DEPT”) 的結果列印出來看。

| import pandas as pddata = pd.read_csv(‘Employees.csv’)group = data.groupby(“DEPT”)print(group) | 按照部門分組 |

執行結果:

<pandas.core.groupby.generic.DataFrameGroupBy object at 0x000001ADBC9CE0F0>

哇,這是個什麼東東?

第一次看到這個東西,直接就蒙圈了,分組的結果不應該是集合的集合嗎,為什麼會是這樣?這對於非專業程式人員來說簡直如同夢魘。

不過上網搜搜還是可以看到它是一個所謂的可迭代物件,迭代以後發現它的每一條都是以分組索引 + DataFrame 構成的,可以使用一些方法看到裡邊的內容,如使用 list(group) 就可以看到分組的結果了。如下圖:

..

看到上圖以後就會明白,被稱為“物件”的東西里面原來是這樣的。本質上它也確實是個集合的集合(姑且把矩陣理解成集合吧),但它並不能像普通的集合那樣直接取某個成員 (如 group[0]),這在使用上迫使使用者強行記憶這類“物件”的 N 種運算規則,理解不了就只能死記硬背了。

看到這裡,估計已經有很多讀者開始暈菜了,徹底不明白上面這段話是在胡說八道些什麼。嗯,這就對了,因為這才是職場人員的正常狀態。

分組中簡單的聚合運算都如此難以理解,我們再燒燒腦,看下稍微複雜一點的分組後子集合的運算。

分組子集處理

雖然分組後經常用於聚合運算,但有時我們並不關心聚合結果,而是關心分組後的集合本身。比如分組後的集合按某一列排序。

問題五:將各部門員工按照入職時間從早到晚進行排序 。

問題分析:分組後對子集按照入職時間排序即可。

Python 程式碼

| import pandas as pdemployee = pd.read_csv(“Employees.csv”)employee[‘HIREDATE’]=pd.to_datetime(employee[‘HIREDATE’])employee_new = employee.groupby(‘DEPT’,as_index=False).apply(lambda x:x.sort_values(‘HIREDATE’)).reset_index(drop=True)print(employee_new) | 修改入職時間格式按 DEPT 分組,並對各組按照 HIREDATE 排序,最後重置索引 |

執行結果:

..

結果沒問題,各個部門員工都按照入職時間從早到晚排序了。但我們觀察下程式碼中最核心的一句employee.groupby(‘DEPT’,as_index=False).apply(lambda x:x.sort_values(‘HIREDATE’)),把這句程式碼抽象一下就是這樣:

df.groupby(c).apply(lambda x:f(x))

df:資料框 DataFrame

groupby:分組函式

c:分組依據的列

以上三個還是比較好理解的,可是 apply 配合 lambda 就十分晦澀難懂了,超出了大多數非專業程式人員理解的範疇,這需要明白所謂“函式語言”的原理才能搞懂(自己去搜尋,俺懶得解釋了)。

如果不使用這“二位”(apply+lambda)呢?也能做,就是會很麻煩。得用 for 迴圈,對每個分組子集分別排序,最後還得把結果合併起來。

| import pandas as pdemployee = pd.read_csv(“Employees.csv”)employee[‘HIREDATE’]=pd.to_datetime(employee[‘HIREDATE’])dept_g = employee.groupby(‘DEPT’,as_index=False)dept_list = []for index,group in dept_g: group = group.sort_values(‘HIREDATE’) dept_list.append(group)employee_new = pd.concat(dept_list,ignore_index=True)print(employee_new) | 修改入職時間格式按 DEPT 分組初始化列表for迴圈每個分組排序排序結果放入列表合併各組結果 |

執行結果相同,但程式碼複雜了很多,而且執行效率也變低了。你願意用哪一種呢?

Python 對於類似但不完全一樣的資料設計了不同的資料型別,也對應有不同的操作方式,並不能簡單地把對某種資料的知識複製到另一個類似資料上,搞得人暈死。

說了這麼多,總結下來就是一句話:Python 真的挺難懂的,它就不是一個面向非專業選手的東西。具體來說大概就是三點:

  1. DataFrame 本質是矩陣

所有的運算都要想辦法按矩陣的方法來計算,經常會很繞。

  1. 資料型別多而且運算規則差別很大

Python 中設計了 Series,DataFrame,分組物件等等不同的資料型別,而且不同的資料型別,計算方法也不完全相同,如 DataFrame 可以使用 query 函式過濾,而 Series 不可以,分組物件的本質完全不同於 Series 和 DataFrame,計算方法更是難以捉摸。

  1. 知其然而不知其所以然

資料型別過多,計算方法差別又大,無形之中增加了使用者的記憶量,死記硬背的成分更多,想要靈活運用太難了,這就造成了一種奇怪的現象:一個簡單的運算,上網搜尋 Python 程式碼的時間可能比用 excel 計算還要長。

Python 程式碼看起來簡單,但你上了培訓班也大概率學不會,結果只會抄例子。

那麼,是不是就沒有適合職場人員進行日常資料處理的工具了嗎?

還是有的。

esProc SPL 也是一種程式設計語言,專注於結構化資料計算。SPL 中提供了豐富的基礎計算方法,其概念邏輯也是符合我們的思維習慣的。

  1. 序表是記錄的集合

SPL 使用序表承載結構化資料,接近於日常處理的 excel 表。

  1. 資料型別少且規則一致

SPL 進行結構化資料處理時幾乎只有集合和記錄兩種資料型別,涉及到的方法也大體一致。

  1. 知其然且知其所以然

只要記住兩種資料型別,掌握基本的運演算法則,更復雜的運算就只是簡單運算規則的組合。不熟練時可能寫的程式碼不好看,但不太可能寫不出來,不會出現 Python 那種花費大量時間搜尋程式碼寫法的現象。

下面我們就使用 SPL 來解決上述介紹的問題,大家認真體會下 SPL 是多麼“平易近人”:

esProc SPL 中用於承載二維結構化資料的資料結構是序表,它和 excel 中呈現的結果一致,如下圖:

..

上表中除了第一行(標題行)外,其他每一行表示一條記錄,而序表就是記錄的集合,相較於 Python 中的 DataFrame 更加直觀。

SPL 中並不是按照矩陣定位的方式過濾,而是 select(篩選)出滿足條件的記錄。

問題一:檢視 R&D 部門的員工資訊

| | A | B |
| 1 | =file(“Employees.csv”).import@tc() | /匯入資料 |
| 2 | =A1.select(DEPT==”R&D”) | /過濾 |

A2 結果:

..

SPL 過濾後的結果非常好理解,就是原始資料集合的一個子集。

再來看看 SPL 對子集修改和求交集運算

  1. 修改子集中的資料

問題二:將 R&D 部門員工的工資上調 5%

| | A | B |
| 1 | =file(“Employees.csv”).import@tc() | /匯入資料 |
| 2 | =A1.select(DEPT==”R&D”) | /過濾 |
| 3 | =A2.run(SALARY=SALARY*1.05) | /修改工資 |
| 4 | =A1 | /檢視結果 |

A4 結果:

..

SPL 完全是按照我們正常的思維方式來計算的,過濾出結果,對著結果修改工資,而不像 Python 那麼費勁。

  1. 子集求交

問題三:找出既是紐約州又是 R&D 部門的員工

| | A | B |
| 1 | =file(“Employees.csv”).import@tc() | /匯入資料 |
| 2 | =A1.select(DEPT==”R&D”) | /R&D部門員工 |
| 3 | =A1.select(STATE==”New York”) | /紐約州員工 |
| 4 | =A2^A3 | /交集 |

A4 結果:

..

SPL 中的交集運算就是對著集合求交集,是真正的集合運算,只使用一個簡單的交集運算子“^”即可。易於理解,而且書寫簡單,而不用像 Python 那樣因為無法求矩陣的交集而去求索引的交集,然後再從原資料中擷取。SPL 中的其他集合運算如並集、差集、異或集也都有對應的運算子,使用起來簡單,方便。

分組理解

SPL 的分組運算也是符合自然邏輯的,即分組後結果是集合的集合,顯而易見。

先來看看 SPL 的分組聚合運算。

問題四:彙總各部門的人數

| | A | B |
| 1 | =file(“Employees.csv”).import@tc() | |
| 2 | =A1.groups(DEPT;count(~):cnt) | /分組 |

A2 結果:

..

分組聚合的結果,仍然是序表,可以繼續使用序表的方法。並不像 Python 聚合結果成了單維的 Series。

再來看下 SPL 的分組結果

| | A | B |
| 1 | =file(“Employees.csv”).import@tc() | |
| 2 | =A1.group(DEPT) | /分組 |

A2 結果:

..

上圖是序表的集合,每個集合是一個部門的成員構成的序表;下圖是點開第一個分組的成員——Administration 部門成員的序表。

這種結果符合我們的正常邏輯,也容易檢視分組的結果,更容易對分組結果進行接下來的運算。

分組子集處理

分組的結果是集合的集合,只要把每個子集進行處理即可。

問題五:將各部門員工按照入職時間從早到晚進行排序

| | A | B |
| 1 | =file(“Employees.csv”).import@tc() | |
| 2 | =A1.group(DEPT) | /分組 |
| 3 | =A2.conj(~.sort(HIREDATE)) | /子集排序併合並 |

A3 結果:

..

由於 SPL 的分組結果還是個集合,因此它可以使用集合的計算方法計算,並不需要強行記憶分組後的計算方法,更不需要使用 apply()+lambda 這種天書般的組合,非常自然的就完成了分組 + 排序 + 合併的工作,簡單的 3 行程式碼,既好寫,又好理解,而且效率很高。

  1. Python 進行結構化處理時,本質都是矩陣運算,簡單的集合運算需要繞到矩陣上去運算;esProc SPL 本質是記錄的集合,集合運算簡單便捷。

  2. Python 資料型別複雜多樣,運算規則不可預測,往往是知其然而不知其所以然,不太可能舉一反三,寫程式碼記憶的成分更多,想理解其原理太難了;esProc SPL 資料型別少,而且計算規則固定,只需要掌握基本的運算規則就可以舉一反三的完成複雜的運算。

  3. 學習 SPL,可以到:

http://www.raqsoft.com.cn/wx/SPL-programming.html

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章