Python 和 SPL 对比 7——对齐与枚举分组
通常的分组都是等值分组,有以下几个特点:
1) 原集合的所有成员都在且只在唯一的组中;
2) 没有一个组是空集;
满足这种特点分组在数学上又称为完全划分。
那么是不是还有不完全划分呢?
是的,完全划分这两个条件有可能都不被满足,比如分组中可能有空集存在,也可能有些原集合的成员不在任何一个组中,或者在多个组中。本文对比 Python 和 SPL 在不完全划分分组的运算能力。
对齐分组
有时会遇到指定集合分组后的聚合信息,如:
统计公司男员工在指定的部门 ['Administration', 'HR', 'Marketing', 'Sales'] 的人数。
公司员工信息如下:
Python
def align_group(g,l,by): d = pd.DataFrame(l,columns=[by]) m = pd.merge(d,g,on=by,how='left') return m.groupby(by,sort=False) file="D:\data\EMPLOYEE.csv" emp = pd.read_csv(file) emp_m=emp.query('GENDER=="M"') sub_dept = ['Administration', 'HR', 'Marketing', 'Sales'] res = align_group(emp_m,sub_dept,'DEPT').EID.count() print(res) |
自定义函数,对齐分组 对照 dataframe 对齐运算 分组
选出 指定序列 对齐分组聚合 |
因为部门可能不止案例中列出的 4 个,也可能有的部门没有男员工,也就是分组子集可能为空集,而 groupby 函数只能按所有部门分组且分组后没有空集,这样也就无法计算空集成员数量,只能借助 merge 函数对齐,绕一下再分组。
SPL
A |
B |
|
1 |
D:\data\EMPLOYEE.csv |
|
2 |
=file(A1).import@tc() |
|
3 |
=A2.select(GENDER=="M") |
/选出 |
4 |
[Administration, HR, Marketing, Sales] |
/指定部门 |
5 |
=A3.align@a(A4,DEPT).new(A4(#):DEPT,~.len():NUM) |
/对齐分组聚合 |
SPL中 align@a() 函数来完成对齐分组,而且结果也和 group 一致,都返回分组后的子集,不需要额外用 merge 来对齐,写起来更容易效率也更高。
align()函数更常用来解决非常规次序排序的问题,如:
中国的各省排名通常会把北京排到第一位,而不会根据 unicode 编码的次序把安徽排到前面。此时只要把省份的指定次序列出来再用 align() 函数来对齐即可,写出来是这样的。
province=[“北京”,”天津”,”黑龙江”,…]
A.align(province,LOCATION)
这样就会把 LOCATION 按指定的省份顺序排好,如果想返回分组的子集,只要加上 @a 选项即可。
枚举分组
事先指定一组条件,将待分组集合的成员作为参数计算这批条件,条件成立者被划分到与该条件对应的一个子集中,结果集的子集和事先指定的条件一一对应,我们称这类分组为枚举分组。如:
计算公司员工各世代(X世代:1965-1980年出生,Y世代:1980-1995年出生,Z世代:1995-2010年出生)的平均工资。
Python
#续用 emp year_seg=["1965-01-01","1980-01-01","1995-01-01","2010-01-01"] generation=["X","Y","Z"] year_seg=pd.to_datetime(year_seg) birth=pd.to_datetime(emp["BIRTHDAY"]) genner=pd.cut(birth,bins=year_seg,right=False,labels=generation) gen_salary=emp.groupby(genner).SALARY.mean() print(gen_salary) |
时间间隔
世代名
分段标记 分组聚合
|
Python提供了 cut() 函数来分段,功能很强大,既可以平均分,也可以自定义分段,分段后还可以标记,而且按 cut 以后的 Seiries 分组子集允许出现空集。
SPL
A |
B |
|
… |
/A2是员工信息 |
|
7 |
[?>=date("1965-01-01")&&?<date("1980-01-01"),?>=date("1980-01-01")&&?<date("1995-01-01"),?>=date("1995-01-01")&&?<date("2010-01-01")] |
/分组条件 |
8 |
[X,Y,Z] |
/组名 |
9 |
=A2.run(BIRTHDAY=date(BIRTHDAY)) |
/修改日期格式 |
10 |
=A2.enum(A7,BIRTHDAY).new(A8(#):GENERATION,~.avg(SALARY):SALARY) |
/枚举分组 |
SPL 中将不同世代的条件写成字符串构成一个序列,其中用? 表示将要代入计算的分组键值。enum 函数将会用待分组集合中每个成员的分组键去依次计算这些条件,如果得到 true,则将该成员分到相应的组中。最终的分组子集也会在数量和次序上和这些用作基准的条件一一对应,分组后只要对子集处理即可。枚举分组也可能分出空集,以及会发生有些成员不会分到任何组中的现象。
这种分段分组很常见,SPL 用 pseg转换成序号分组,写法更简单,执行速度也更快。
A |
B |
|
… |
/A2是员工信息 |
|
11 |
["1965-01-01","1980-01-01","1995-01-01","2010-01-01"] |
/分段 |
12 |
=A11.(date(~)) |
/修改日期格式 |
13 |
=A2.align@a(A8,A8(A12.pseg(BIRTHDAY))).new(A8(#):GENERATION,~.avg(SALARY):SALARY) |
/分组聚合 |
pseg()函数可以算出某个时间或数值属于哪一段,返回序号。因为可能没有员工属于 Z 世代,所以用 align@a() 函数来对齐分组,最后用同样的方式处理子集就行了,这种处理方式和 Python 的 cut 函数属于同一种计算方式了。
可重复枚举分组
有时我们还会遇到集合成员重复出现在不同子集中的情况。如:
按员工工龄将员工分组并统计每组的员工人数(分组条件重合时,列出所有满足条件的员工,分组的条件是 [工龄 <5 年,5 年 <= 工龄 <10 年,工龄 >=10 年,工龄 >=15 年])
Python
#续用 emp import datetime import numpy as np import math def eval_g(dd:dict,ss:str): return eval(ss,dd) employed_list=['Within five years','Five to ten years','More than ten years','Over fifteen years'] employed_str_list=["(s<5)","(s>=5) & (s<10)","(s>=10)","(s>=15)"] today=datetime.datetime.today() emp['HIREDATE']=pd.to_datetime(emp['HIREDATE']) employed=((today-emp['HIREDATE'])/np.timedelta64(1,'Y')).apply(math.floor) emp['EMPLOYED']=employed dd={'s':emp['EMPLOYED']} group_cond = [] for n in range(len(employed_str_list)): emp_g = emp.groupby(eval_g(dd,employed_str_list[n])) emp_g_index=[index for index in emp_g.size().index] if True not in emp_g_index: sum_emp=0 else: group=emp_g.get_group(True) sum_emp=len(group) group_cond.append([employed_list[n],sum_emp]) group_df=pd.DataFrame(group_cond,columns=['EMPLOYED','NUM']) print(group_df) |
函数,字符串转表达式
分组条件
计算入职时间
循环分组条件 按分组条件分组 分组索引
如果没有满足条件的成员 员工数为 0
满足条件 总员工数
汇总各个分组条件的计算结果 |
本例比较特殊,工龄 >=10 年,工龄 >=15 年可能存在重复成员,按常规的等值分组或对齐分组不能完成这个任务,而 Python 中的 cut 函数也不能一次完成这种分段,只能自己硬编码来完成:循环分组条件——按条件等值分组——分组子集处理——合并结果,绕了一大圈才解决这个问题。
SPL
A |
B |
|
… |
/A2是员工信息,A10 是世代名 |
|
15 |
[?<5,?>=5 && ?<10,?>=10,?>=15] |
/分组条件 |
16 |
[Within five years,Five to ten years,More than ten years,Over fifteen years] |
/组名 |
17 |
=A2.derive(age(HIREDATE):EMPLOYED) |
/计算工龄 |
18 |
=A17.enum@r(A15, EMPLOYED).new(A16(#):EMPLOYED,~.len():NUM) |
/枚举分组 |
SPL 就简单很多,enum@r() 中 @r 选项允许成员重复出现在不同的子集中,相比于Python的绕大圈方式,SPL简直太简单了。
小结
Python的对齐分组饶了一个小圈,借助 merge 函数来对齐,运行效率上打折扣,但所幸书写不算特别麻烦,数据量小时还可以接受;枚举分组饶了一大圈,效率大打折扣的同时书写也很费劲。。
SPL 就容易的多,align 函数来对齐分组,enum 函数来枚举分组,返回的结果和 group 也相同,都是分组子集,后续只要对子集处理就行。
再仔细看一下等值分组、对齐分组、枚举分组,SPL 的是统一的形式:A.f(…).(y),只不过等值分组的 f(…) 是 group,对齐分组的 f(…) 是 align,枚举分组的 f(…) 是 enum,后续分组子集的处理是完全一致的。Python 应对三种分组方式的方法就完全不同了,需要一一应对。
英文版