Python 和 SPL 对比 11——多对一关联
《Python和 SPL对比系列 10——一对 N关联》介绍了关联关系中的一对一和一对 N 关联,本文对比 Python 和 SPL 多对一关联的运算能力。
外键关联
表 A 的某些字段与表 B 的主键关联,表 A 的关联字段可以不唯一,表 B 的关联字段唯一,这就是多对一关联,也称作外键关联,即表 A 是事实表,表 B 是维表,A 表与 B 表主键关联的字段称为 A 指向 B 的外键,B 也称为 A 的外键表。如:
现有销售记录表和产品信息表,请汇总各类产品的销售额。
销售记录表、城市信息表、产品信息表部分内容如下:
销售记录表 (事实表):
recordid  |  
   product  |  
   sale_city  |  
   amount  |  
   …  |  
  
sr100001  |  
   p1003  |  
   c104  |  
   380  |  
   …  |  
  
sr100002  |  
   p1005  |  
   c103  |  
   400  |  
   …  |  
  
sr100003  |  
   p1003  |  
   c104  |  
   626  |  
   …  |  
  
…  |  
   …  |  
   …  |  
   …  |  
   …  |  
  
产品信息表(维表):
productid  |  
   pclass  |  
   …  |  
  
p1001  |  
   A  |  
   …  |  
  
p1002  |  
   A  |  
   …  |  
  
p1003  |  
   B  |  
   …  |  
  
…  |  
   …  |  
   …  |  
  
Python
import pandas as pd sr_file1="D:\data\SaleRecord.csv" pt_file1="D:\data\Product.csv" record1=pd.read_csv(sr_file1) product1=pd.read_csv(pt_file1) r_pt=pd.merge(record1,product1,left_on="product",right_on="productid") pclass_sale=r_pt.groupby('pclass',as_index=False).amount.sum() print(pclass_sale)  |  
   
 
 
 事实表 
 维表 事实表与维表外键关联 
  |  
  
Python的 merge 函数关联两表,销售记录表 record1 是实事表,产品信息表 product1 是维表,record1 中的多条记录与 product1 的一条记录对应,两者的关联字段名不同,left_on 和 right_on 分别标出两表的关联字段,使两表关联成为一张宽表,最后分组汇总得到结果。
SPL
A  |  
   B  |  
  |
1  |  
   D:\data\SaleRecord.csv  |  
   |
2  |  
   D:\data\Product.csv  |  
   |
3  |  
   =file(A1).import@tc()  |  
   |
4  |  
   =file(A2).import@tc()  |  
   |
5  |  
   =A3.switch(product,A4:productid)  |  
   /外键转维表记录  |  
  
6  |  
   =A5.groups(product.pclass;sum(amount):amount)  |  
   
SPL的 switch 函数把外键字段转换成对应的维表记录,既然是记录当然可以引用字段,在分组时可以利用引用字段完成分组聚合运算。
一事实表,多维表
一张事实表可能关联多张维表,如:
续用上例的销售记录表(事实表)和产品信息表(维表 1),新增城市信息表(维表 2),请统计各省份各类产品的销售额。
城市信息表(维表 2):
cityid  |  
   name  |  
   province  |  
   …  |  
  
c101  |  
   Beijing  |  
   Beijing  |  
   …  |  
  
c102  |  
   Tianjin  |  
   Tianjin  |  
   …  |  
  
c103  |  
   Harbin  |  
   Heilongjiang  |  
   …  |  
  
…  |  
   …  |  
   …  |  
   …  |  
  
Python
#续用sr_file1和pt_file1 ct_file1="D:\data\City.csv" ct1=pd.read_csv(ct_file1) r_ct=pd.merge(record1,ct1,left_on="sale_city",right_on="cityid") r_ct_pdt=pd.merge(r_ct,product1,left_on="product",right_on="productid") ct_pdt_sale=r_ct_pdt.groupby(['province','pclass'],as_index=False).amount.sum() print(ct_pdt_sale)  |  
   
 
 
 事实表关联维表 2 事实表关联维表 1 分组聚合  |  
  
Python在关联多张维表时,通常是先关联一个再关联另一个,两次 merge 组成一张大宽表,最后利用宽表分组聚合。
SPL
A  |  
   B  |  
  |
…  |  
   /A3是销售记录表,A4 是产品信息表  |  
   |
8  |  
   D:\data\City.csv  |  
   |
9  |  
   =file(A8).import@tc()  |  
   |
10  |  
   =A3.switch(product,A4:productid;sale_city,A9:cityid)  |  
   /设置主键  |  
  
11  |  
   =A10.groups(sale_city.province,product.pclass;sum(amount):amount)  |  
   /分组聚合  |  
  
SPL 的 switch 函数可以同时建立多个外键关系,如本例中的产品号 product 与产品信息表中的 productid,城市号 sale_city 与城市信息表中的 cityid,如果需要还可以建立更多的关联关系,关联后可以利用记录的字段完成分组聚合。和 Python 不同,SPL 可以一次解析多个关联关系,这使得关联关系明确且效率更高。
维表复用
一张事实表可能多次利用同一张维表,如:
现有销售记录表和城市信息表,请找出产品销售地与产地在同一省份的销售记录。
销售记录表 2(事实表):
recordid  |  
   product  |  
   product_city  |  
   sale_city  |  
   amount  |  
   …  |  
  
sr100001  |  
   p1006  |  
   c105  |  
   c103  |  
   603  |  
   …  |  
  
sr100002  |  
   p1005  |  
   c105  |  
   c102  |  
   1230  |  
   …  |  
  
sr100003  |  
   p1003  |  
   c102  |  
   c102  |  
   885  |  
   …  |  
  
…  |  
   …  |  
   …  |  
   …  |  
   …  |  
   …  |  
  
城市信息表2(维表):
cityid  |  
   name  |  
   province  |  
   …  |  
  
c101  |  
   Beijing  |  
   Beijing  |  
   …  |  
  
c102  |  
   Tianjin  |  
   Tianjin  |  
   …  |  
  
c103  |  
   Harbin  |  
   Heilongjiang  |  
   …  |  
  
…  |  
   …  |  
   …  |  
   …  |  
  
Python
sr_file2="D:\data\SaleRecord2.csv" ct_file2="D:\data\City2.csv" record2=pd.read_csv(sr_file2) ct2=pd.read_csv(ct_file2) r_ct2=pd.merge(record2,ct2,left_on="sale_city",right_on="cityid") r_ct_ct=pd.merge(r_ct2,ct2,left_on="product_city",right_on="cityid",suffixes=('_s', '_p')) r_ct_p_ct= r_ct_ct[r_ct_ct['province_s']==r_ct_ct['province_p']].recordid print(r_ct_p_ct)  |  
   
 
 
 
 事实表第一次关联维表 事实表第二次关联维表 
 
 
  |  
  
本例中的销售记录包含了产品销售城市号和产品产地号,两者都关联城市信息表的 cityid 可以完成关联。Python 还是老方子,merge 两次,第二次 merge 时会产生相同的字段名,但 Python 通过增加不同的后缀很好的处理了。
SPL
A  |  
   B  |  
  |
…  |  
   …  |  
   |
13  |  
   D:\data\SaleRecord2.csv  |  
   |
14  |  
   D:\data\City2.csv  |  
   |
15  |  
   =file(A13).import@tc()  |  
   |
16  |  
   =file(A14).import@tc()  |  
   |
17  |  
   =A15.switch(sale_city,A16:cityid;product_city,A16:cityid)  |  
   /事实表关联维表  |  
  
18  |  
   =A17.select(sale_city.province==product_city.province).(recordid)  |  
   
SPL 通过 switch 关联同一张维表,只不过是不同的外键(sale_city 和 product_city),然后利用关联后的记录字段选出。
多层维表
外键关联可能不止一层维表,会遇到多层维表的情况,如:
现有销售记录表、产品信息表和城市信息表,请找出产品销售地与产地在同一省份的销售记录。
销售记录表 (事实表):
recordid  |  
   product  |  
   sale_city  |  
   amount  |  
   …  |  
  
sr100001  |  
   p1003  |  
   c104  |  
   380  |  
   …  |  
  
sr100002  |  
   p1005  |  
   c103  |  
   400  |  
   …  |  
  
sr100003  |  
   p1003  |  
   c104  |  
   626  |  
   …  |  
  
…  |  
   …  |  
   …  |  
   …  |  
   …  |  
  
城市信息表(维表 1):
cityid  |  
   name  |  
   province  |  
   …  |  
  
c101  |  
   Beijing  |  
   Beijing  |  
   …  |  
  
c102  |  
   Tianjin  |  
   Tianjin  |  
   …  |  
  
c103  |  
   Harbin  |  
   Heilongjiang  |  
   …  |  
  
…  |  
   …  |  
   …  |  
   …  |  
  
产品信息表(维表 2):
productid  |  
   product_city  |  
   …  |  
  
p1001  |  
   c104  |  
   …  |  
  
p1002  |  
   c103  |  
   …  |  
  
p1003  |  
   c102  |  
   …  |  
  
…  |  
   …  |  
   …  |  
  
Python
sr_file3="D:\data\SaleRecord3.csv" ct_file3="D:\data\City3.csv" pt_file3="D:\data\Product3.csv" record3=pd.read_csv(sr_file3) product3=pd.read_csv(pt_file3) ct3=pd.read_csv(ct_file3) pdt_ct=pd.merge(product3,ct3,left_on="product_city",right_on="cityid") r_pdt_ct=pd.merge(record3,pdt_ct,left_on="product",right_on="productid") r_pdt_ct_ct=pd.merge(r_pdt_ct,ct3,left_on="sale_city",right_on="cityid",suffixes=('_s', '_p')) r_ct_p_ct2=r_pdt_ct_ct[r_pdt_ct_ct['province_s']==r_pdt_ct_ct['province_p']].recordid print(r_ct_p_ct2)  |  
   
 
 
 
 
 
 关联产地和城市 关联销售记录和产品 关联销售地和城市 
 
  |  
  
城市信息既是产品的维表又是销售记录的维表,产品信息也是销售记录的维表,这就构成了多层维表,而且有维表被多次关联。Python 中三次关联三次 merge。
SPL
A  |  
   B  |  
  |
…  |  
   …  |  
   |
20  |  
   D:\data\SaleRecord3.csv  |  
   |
21  |  
   D:\data\City3.csv  |  
   |
22  |  
   D:\data\Product3.csv  |  
   |
23  |  
   =file(A20).import@tc()  |  
   |
24  |  
   =file(A21).import@tc()  |  
   |
25  |  
   =file(A22).import@tc()  |  
   |
26  |  
   =A25.switch(product_city,A24:cityid)  |  
   /产地关联城市  |  
  
27  |  
   =A23.switch(sale_city,A24:cityid;product,A26:productid)  |  
   /销售记录关联城市和产品  |  
  
28  |  
   =A27.select(sale_city.province==product.product_city.province).(recordid)  |  
   
SPL 建立关联关系后可以一直使用,即使是又被建立关联后,如 A26 中建立了产地与城市的关联关系,在 A27 中建立销售记录与产品的关联关系后,产地与城市的关联关系依旧在,所以 A28 中可以有 product.product_city.province 的引用,这在多表关联的关系中是很方便的。
自关联
有时会遇到一张表既是事实表又是维表,即自己和自己关联的情况。如:
现有员工信息表,请列出所有员工及其上级的姓名。
员工信息表部分内容如下:
empid  |  
   name  |  
   superior  |  
   …  |  
  
7902  |  
   FORD  |  
   7566  |  
   …  |  
  
7788  |  
   SCOTT  |  
   7566  |  
   …  |  
  
7900  |  
   JAMES  |  
   7698  |  
   …  |  
  
…  |  
   …  |  
   …  |  
   …  |  
  
Python
emp_file="D:\data\Employee_.csv" emp=pd.read_csv(emp_file) emp_s=pd.merge(emp,emp,left_on="superior",right_on="empid",suffixes=('', '_m'),how="left") emp_s_name=emp_s[['name','name_m']] print(emp_s_name)  |  
   
 
 自关联 
 
 
  |  
  
Python的本质还是两表关联。
SPL
A  |  
   B  |  
  |
…  |  
   …  |  
   |
30  |  
   D:\data\Employee_.csv  |  
   |
31  |  
   =file(A30).import@tc()  |  
   |
32  |  
   =A31.switch(superior,A31:empid)  |  
   /自关联  |  
  
33  |  
   =A32.new(name,superior.name:s_name)  |  
   
SPL 也没太大的区别,switch 建立 superior 和 empid 的关联。
环状关联
关联关系复杂时,可能会发生环装关联。如:
现有员工信息表和部门信息表,请找出北京经理的北京员工。
员工信息表:
empid  |  
   name  |  
   dept  |  
   province  |  
   …  |  
  
1  |  
   Rebecca  |  
   6  |  
   Beijing  |  
   …  |  
  
2  |  
   Ashley  |  
   2  |  
   Tianjin  |  
   …  |  
  
3  |  
   Rachel  |  
   7  |  
   Heilongjiang  |  
   …  |  
  
…  |  
   …  |  
   …  |  
   …  |  
   …  |  
  
部门信息表:
deptid  |  
   name  |  
   manager  |  
   …  |  
  
1  |  
   Administration  |  
   20  |  
   …  |  
  
2  |  
   Finance  |  
   2  |  
   …  |  
  
3  |  
   HR  |  
   162  |  
   …  |  
  
…  |  
   …  |  
   …  |  
   …  |  
  
Python
emp_file2="D:\data\Employee_2.csv" dept_file2="D:\data\Department2.csv" emp2=pd.read_csv(emp_file2) dept2=pd.read_csv(dept_file2) d_emp=pd.merge(dept2,emp2,left_on="manager",right_on="empid") emp_d_emp=pd.merge(emp2,d_emp,left_on="dept",right_on="deptid",suffixes=('', '_m')) beijing_emp_m=emp_d_emp[(emp_d_emp['province']=="Beijing") & (emp_d_emp['province_m']=="Beijing")].name print(beijing_emp_m)  |  
   
 
 
 
 部门表关联员工表 员工表关联部门表 
 选出 
  |  
  
Python的两步关联相对独立,两步构成一个环状关联,形成一个宽表,最后选出。
SPL
A  |  
   B  |  
  |
…  |  
   …  |  
   |
35  |  
   D:\data\Employee_2.csv  |  
   |
36  |  
   D:\data\Department2.csv  |  
   |
37  |  
   =file(A35).import@tc()  |  
   |
38  |  
   =file(A36).import@tc()  |  
   |
39  |  
   =A38.switch(manager,A37:empid)  |  
   /部门关联员工表  |  
  
40  |  
   =A37.switch(dept,A38:deptid)  |  
   /员工表关联部门表  |  
  
41  |  
   =A40.select(province=="Beijing"&&dept.manager.province=="Beijing").(name)  |  
   /选出  |  
  
SPL 第一步建立部门和员工的关联关系,第二步建立员工和部门的关联关系,第三步选出,建好的关联关系可以直接拿来使用。上述的所有案例都是用 switch 函数完成关联,它有一个特点:关联后本身的字段值会被替换为关联后的记录,本身的记录值就不存在了,如果想保留本身的记录值可以用 join 函数完成关联,如本例的 A40 可以这样写:A40=A37.join(dept,A38:deptid,~:dpt),此时的 dept 就是关联后的记录,利用该记录引用就可以完成后续计算了,之前例子中的 switch 都可以用这种方式完成计算。
混合关联
数据分析中,可能会遇到同维关联、主子关联、外键关联同时发生的混合关联,此时的关联关系复杂,要理清楚关联关系。如:
利用订单表,订单明细表,产品信息表,员工信息表,出差信息表,客户信息表,城市信息表,计算出差时间大于 10 天的 90 后销售员在各省份销售黑龙江商品的金额。
关联关系如下:

Python
emp4 = pd.read_csv("D:\data\Employee4.csv") trv4 = pd.read_csv("D:\data\Travel4.csv") emp_inf = pd.merge(emp4,trv4,on=["empid","name"]) years = pd.to_datetime(emp_inf.birthday).dt.year emp_inf_c = emp_inf[(years>=1990) & (years<2000)&(emp_inf.time>=10)] clt4 = pd.read_csv("D:\data\Client4.csv") city4 = pd.read_csv("D:\data\City4.csv") sale_location = pd.merge(clt4,city4,left_on='city',right_on='cityid') pdt4 = pd.read_csv("D:\data\Product4.csv") pdt_location = pd.merge(pdt4,city4,left_on='city',right_on='cityid') detail4 = pd.read_csv("D:\data\Detail4.csv") order4 = pd.read_csv("D:\data\Order4.csv") detail_pdt = pd.merge(detail4,pdt_location,on='productid',how="left") order_sale_location = pd.merge(order4,sale_location,on='clientid',how="left") order_sale_location_emp = pd.merge(order_sale_location,emp_inf_c,left_on='saleid',right_on='empid',how="left",suffixes=('_c', '_e')) order_inf = order_sale_location_emp[order_sale_location_emp.empid.notnull()] order_detail = pd.merge(order_inf,detail_pdt,on='orderid',how="left",suffixes=('_s', '_p')) order_detail_Hljp = order_detail[order_detail.province_p=="Heilongjiang"] res = order_detail_Hljp.groupby(['empid','name_e','province_s'],as_index=False).price.sum() print(res)  |  
   
 
 员工表和出差表 
 选出 90 后 
 
 客户表和城市表 
 产品表和城市表 
 
 订单明细和产品 订单和出售地 
 订单和员工 
 
 订单和订单明细 选出 
 分组聚合  |  
  
本例中表很多,关联关系很复杂,按照我们介绍的 3 种关系关联: 同维关联(一对一),主子关联(一对多),外键关联(多对一)进行关联,如果出现了多对多的关联关系,那多半是错了,需要重新检查关联关系,否则多对多关联后很可能导致内存爆炸。Python 面对如此复杂的关联关系时,最好就是用 merge 两两关联,一步解析一个关联关系,虽然稍麻烦,但不容易出错。
SPL
A  |  
   B  |  
  |
…  |  
   …  |  
   |
43  |  
   =file("D:/data/Employee4.csv").import@tc()  |  
   |
44  |  
   =file("D:/data/Travel4.csv").import@tc()  |  
   |
45  |  
   =A44.join(empid,A43:empid,birthday)  |  
   |
46  |  
   =A45.select((y=year(birthday),y>=1990&&y<2000&&time>=10))  |  
   |
47  |  
   =file("D:/data/Client4.csv").import@tc()  |  
   |
48  |  
   =file("D:/data/City4.csv").import@tc()  |  
   |
49  |  
   =A47.join(city,A48:cityid,province)  |  
   |
50  |  
   =file("D:/data/Product4.csv").import@tc()  |  
   |
51  |  
   =A50.join(city,A48:cityid,province)  |  
   |
52  |  
   =file("D:/data/Detail4.csv").import@tc()  |  
   |
53  |  
   =file("D:/data/Order4.csv").import@tc()  |  
   |
54  |  
   =A52.join(productid, A51:productid,province:product_province)  |  
   |
55  |  
   =A54.group(orderid)  |  
   |
56  |  
   =A53.switch(orderid, A55:orderid;saleid, A46:empid;clientid, A49:clientid)  |  
   |
57  |  
   =A56.select(saleid).new(saleid.empid:empid,saleid.name:sale_name,clientid.province:sale_location,orderid.select(product_province=="Heilongjiang").sum(price):price)  |  
   |
58  |  
   =A57.groups(empid,sale_name,sale_location;sum(price):price).select(price)  |  
   
SPL 面对如此复杂的关联关系时,就显得很从容,同维、主子、外键关联关系都很明了,需要同时关联两个关联关系时,SPL 也可以同时解决,快捷还不易出错。
小结
Python 进行外键关联时,同样存在复制数据、每次只能解析一个关联关系的问题,另外每一次关联都是独立的,之前建立的关联关系,后续不可以重复利用,只能重新关联,这使得关联效率比较低。
SPL 不存在类似的问题,建立一次关联关系后,可以重复利用,运算效率更高。
            
        

英文版