針對數據集合的每個成員進行計算是很常見的任務,用循環語句當然能實現,但比較麻煩,算個簡單的求和都要寫很多句代碼。
編程語言經常把這些運算封裝成函數,比如 Python 的 sum 函數,求訂單價格總和是這樣寫的:
total_price = orders['price'].sum()
SQL 也可以寫成:
select sum(price) total_price from orders
SPL 當然也沒問題:
total_price = orders.sum(price)
看起來都很簡潔。
任務當然不會總是這么簡單,看一個更復雜的例子:對員工計算標簽列,薪酬在 5000 以上的經理標簽為 yes,其他員工為 no。
這個計算可以用一個不太復雜的表達式來描述,但不能在循環外部事先計算出結果,而要在循環中針對每個集合成員計算。這時候,可以把這個表達式定義成函數,再把這個函數作為參數傳遞給循環計算的函數,Python 可以這么寫:
def calc_flag(row):
return 'yes' if row['position'] == 'manager' and row['salary'] > 5000 else 'no'employee['flag']=employee.apply(calc_flag, axis=1)
這段代碼先定義了一個函數 calc_flag,對傳入的記錄 row 計算表達式。
apply 函數以 calc_flag 為參數,在循環中將集合的當前成員(記錄)傳遞給 calc_flag 計算,并用結果組成新序列。
顯然,每次都預先定義成一個函數實在是麻煩,特別是對這么一個簡單表達式就能搞定的任務。于是業界發明了 lambda 語法,可以在參數中定義函數,代碼簡潔很多:
employee['flag'] = employee.apply(lambda row: 'yes' if row['position'] == 'manager' and row['salary'] > 5000 else 'no', axis=1)
apply 函數的參數中用 lambda 關鍵字定義了一個匿名函數,傳入參數是記錄 row。在循環過程中,apply 函數將每個成員(記錄)傳給 lambda 函數,計算得到新的序列。
Python 的這種寫法是顯式的 lambda 語法,有 lambda 關鍵字,要定義參數,還要寫函數體。
SQL 又是如何處理這種問題的呢?
SELECT *,CASE WHEN position = 'manager' AND salary > 5000 THEN 'yes' ELSE 'no' END AS flag
FROM employee;
沒有 lambda,似乎更簡單了。
其實,CASE WHEN 表達式還是相當于定義了一個函數。這還是把表達式定義的函數當成循環運算的參數,本質上仍是 lambda 語法。只是 SQL 更簡潔,已經看不出 lambda 語法的形式了。
SQL 專業面向結構化數據計算,僅支持二維數據表這一種集合,lambda 函數傳入參數只能是記錄。SQL 就不需要像 Python 那樣顯式定義一個 row 參數,而可以直接訪問字段,這會更便捷。大多數情況下,lambda 函數中可以直接使用字段名,只有存在同名字段時才需要冠以表名(或表別名)以示區分。這樣,表名和記錄參數都省了,SQL 就把 lambda 函數寫成了簡單表達式,將 lambda 語法化于無形了。
esProc SPL 繼承了 SQL 這些優點,同樣把 lambda 化于無形:
employee.derive(if(position == "manager" && salary > 5000, "yes","no"))
數據集合并不只有數據表這一種。SQL 不支持其他形式的集合,處理起來會麻煩了。對于單值成員組成的集合,SQL 還可以用只有一個字段的數據表來對付。比如求一組數值的平方和,SQL 這樣寫:
create table numbers asselect value as nfrom (select 4.3 as value union all select 2.3 union all select 6.5 union all select 44.1) t;
select sum(n*n) from numbers;
這組數值還要額外起一個字段名和表名,有點啰嗦了。
Python 支持單值組成的集合,可以用 lambda 語法寫出這樣的運算:
numbers=pd.Series([4.3,2.3,6.5,44.1])
result=numbers.apply(lambda x: x * x).sum()
SPL 也有單值集合:
numbers=[4.3,2.3,6.5,44.1]
numbers.sum(~*~)
這里,sum 函數對集合循環計算的時候,將當前成員 ~ 傳遞給 lambda 函數求平方,再由 sum 函數求和。~ 就相當于前面 Python 代碼中的 x。
在循環中,lambda 函數幾乎總是用到集合的當前成員,SPL 把這個參數固化為 ~ 符號,這樣就省去了參數的定義,從而把 lambda 寫成簡單表達式,繼續保持將 lambda 化于無形的優點。
不過,對于這種相對簡單的情況,Python 更提倡對位集合運算,可以避免使用 lambda 語法:
numbers=pd.Series([4.3,2.3,6.5,44.1])
result = (numbers * numbers).sum()
這顯得更簡潔。
前面那個員工標簽的例子也可以寫出來:
employee['flag'] = np.where((employee['position'] == 'manager') & (employee['salary'] > 5000), 'yes', 'no')
當表達式較復雜的時候,看著就不如 lamdba 語法簡潔了。而且這種寫法只適用于針對數組做過優化的運算 (比如加減乘除和這里的 if),大部分數學函數都沒有做過這種優化,碰到也只能寫成 lambda 語法。
說句題外話,這種寫法要用 where 函數,邏輯運算符是 &,而前面 lambda 語法中是用 if 函數和 and,Python 語法經常會表現出這種不一致。lambda 語法也有這種不一致問題,在 apply 函數中能用,但 sort_values 中就不能用,這些都會加大學習難度。
SPL 也支持對位集合運算的書寫形式:
(numbers ** numbers).sum()
看起來和 Python 差不多,但不如化于無形的 lambda 語法簡單了。
SPL 還支持集合的集合。實際上,只要是集合,SPL 就都可以使用化于無形的 lambda 語法。比如求員工超過 10 個的部門有哪些員工:
employee.group(department).select(~.len()>10)
group 函數按部門分組后得到一個大集合,其成員是同一部門員工組成的子集合。表達式 ~.len()>10 是一個 lambda 函數,其中的 ~ 是集合的當前成員,也就是分組子集。
select 函數對大集合循環計算時,將當前成員(子集)傳遞給 lambda 函數,判斷子集長度是否大于 10,再由 select 函數保留或舍棄這個子集。這是很自然的解題思路。
Python 一定程度也可以表示集合的集合,可以寫出類似代碼:
result=employee.groupby('department').filter(lambda x: len(x) >10)
對于集合的集合,就不能再使用對位運算了,采用 lambda 語法是 Python 最簡單的寫法,換其它方法,思路和代碼都會變得更復雜。
SQL 不能描述集合的集合,對于這個問題要換種思路去實現(麻煩很多),lamdba 語法對這個問題已經無能為力了。
SPL 不僅僅是針對結構化數據計算的,但由于結構化數據過于常見,SPL 和 SQL 一樣專門做了語法簡化。再看一下前面計算員工標簽列的例子,其實 SPL 引用字段的完整寫法應該是 ~.position、~.salary,而 SPL 也提供了直接訪問字段的便捷機制,就能把 lambda 函數寫的和 SQL 一樣簡潔:
if(position == "manager" && salary > 5000, "yes","no")
Python 沒做這種簡化,只能寫成下面這樣,會導致這種最常見的情況寫起來比較啰嗦。
lambda row: 'yes' if row['position'] == 'manager' and row['salary'] > 5000 else 'no'
除了當前成員之外,針對有序集合的循環計算經常還會用到成員的序號。比如:要取出一組數值中,第偶數個成員。簡單思路是循環計算這個集合,如果成員的序號能被 2 整除就保留,否則就舍棄。
Python 只能給 lambda 函數傳入當前成員這一個參數,必須改造這個集合,給每個成員附加上序號,才能實現這個思路:
result = filter(lambda x: x[0] % 2 == 1, enumerate(number))
even_index_members = [x[1] for x in result]
enumerate 函數將 number 的每個成員都變成數組,數組的第 0 個成員是序號,第 1 個成員是原來的數值。這樣,lambda 函數才能用 x[0] 取得成員序號。過濾后還要把原來的數值拆出來,這個過程很繞,代碼也繁瑣。
SQL 基于無序集合,成員序號沒有意義,這個問題又得繞路實現。
SPL 用 #表示當前成員的序號,用上述簡單思路寫出的代碼非常簡潔:
number.select(# % 2 ==0)
#和 ~ 一樣,也是 SPL 循環函數中 lambda 函數的傳入參數。
小結一下:
SQL 把二維數據表運算的 lambda 語法化于無形,用于描述常規結構化數據集合運算還是比較方便簡捷的,但不能支持結構化數據以外的集合。Python 支持各種形式的集合,但 lambda 函數代碼寫起來也有些啰嗦,適應面不全面,語法風格也不太一致。SPL 繼承了 SQL 的所有優勢,且對各種形式的集合都可以使用化于無形的 lambda 語法,引入了 ~、# 等符號,進一步簡化代碼,而且適應面廣且風格統一,是三者中最強的。
SPL是開源免費的,歡迎下載試用~~