Python 和 SPL 对比 10——一对 N 关联
数据分析中,经常会遇到两表或多表关联,表间关联关系可以分为以下几种:一对一、一对多、多对一和多对多。一对一关联,是指一张表的一条记录只与另外一张表的一条记录对应;一对多关联,是指一张表的一条记录与另外一张表的多条记录对应;本文先对比 Python 和 SPL 一对N关联的运算能力。
同维关联
一对一关联也叫同维关联,关联的两表互为同维表。如:
现有学生期末成绩表,平时成绩表,请统计学生的学期成绩(期末考试成绩、平时成绩所占比例分别是 0.7,0.3)。
期末考试成绩表
studentid |
fscore |
… |
s1087 |
68.36422 |
… |
s1049 |
78.17515 |
… |
s1018 |
82.61762 |
… |
… |
… |
… |
平时成绩表
studentid |
uscore |
… |
s1050 |
75.11157 |
… |
s1039 |
88.02989 |
… |
s1091 |
87.95997 |
… |
… |
… |
… |
Python
import pandas as pd fexam_file="D:/data/FExamRes.csv" uperf_file="D:/data/UPerformance.csv" fexam=pd.read_csv(fexam_file) uperf=pd.read_csv(uperf_file) scores=pd.merge(fexam,uperf,on="studentid",how="outer").fillna(0) scores["total"]=0.7*scores["fscore"]+0.3*scores["uscore"] print(scores) |
期末考试成绩 平时成绩 关联后补 0 计算成绩
|
Python的 merge 函数可以关联两个 Dataframe,因为有可能有的同学缺少某项成绩,所以使用外连接方式,没有成绩时,成绩记为 0,最后再加权求和就得到最终成绩了。Python关联后会得到一个新的Dataframe,数据会被复制,耗时且耗内存。
SPL
A |
B |
|
1 |
D:/data/FExamRes.csv |
|
2 |
D:/data/UPerformance.csv |
|
3 |
=file(A1).import@tc() |
|
4 |
=file(A2).import@tc() |
|
5 |
=join@f(A3:fexam,studentid;A4:uperf,studentid) |
同维关联 |
6 |
=A5.new([fexam.studentid,uperf.studentid].conj().id().~:studentid,fexam.fscore:fscore,uperf.uscore:uscore,0.7*fexam.fscore+0.3*uperf.uscore:total) |
SPL的join函数完成关联,@f选项是外连接,A6完成成绩的计算。SPL关联只是利用主键(本例中的学号studentid)将两表建立关联,并没有复制数据,内存占用小且运算很快。
多同维表
关联运算经常会遇到三张甚至更多的同维表关联,如:
学生成绩除期末考试成绩和平时成绩外,还有选修课成绩,三者的占比分别是 0.6,0.3,0.1,请统计学生的最终成绩。
期末考试成绩和平时成绩续用上例数据,选修课成绩表如下:
选修课成绩表
studentid |
escore |
… |
s1051 |
74.66936 |
… |
s1035 |
57.33134 |
… |
s1047 |
61.29318 |
… |
… |
… |
… |
Python
#续用 fexam 和 uperf elec_file="D:/data/ElectiveScore.csv" fexam=fexam.set_index(keys="studentid") uperf=uperf.set_index(keys="studentid") elec = pd.read_csv(elec_file,index_col='studentid') student_score = pd.concat([fexam,uperf,elec],axis=1,join='outer',sort=True).fillna(0).reset_index() student_score['total'] = 0.6*student_score.fscore+0.3*student_score.uscore+0.1*student_score.escore print(student_score) |
设置索引
读数据时设置索引 多同维表关联
汇总成绩
|
Python在多个同维表关联时就不能使用 merge 函数了,因为 merge 函数只支持两表关联,本例中的三表关联则要 merge 两次,有点麻烦,不过 Python 提供了 concat 函数,可以根据索引关联,这使得多同维表关联没有那么麻烦了,但需要多掌握一个函数的用法。
SPL
A |
B |
|
… |
/A3是期末成绩表,A4 是平时成绩表 |
|
8 |
D:/data/ElectiveScore.csv |
|
9 |
=file(A8).import@tc() |
|
10 |
=A3.keys(studentid) |
/设置主键 |
11 |
=A4.keys(studentid) |
|
12 |
=A9.keys(studentid) |
|
13 |
=join@f(A3:fexam;A4:uperf;A9:elec) |
/多同维表关联 |
14 |
=A13.new([fexam.studentid,uperf.studentid,elec.studentid].conj().id().~:studentid, fexam.fscore:fscore,uperf.uscore:uscore,elec.escore:escore, 0.6*fexam.fscore+0.3*uperf.uscore+0.1*escore:total) |
/计算总成绩 |
SPL 的 join 函数既可以关联两表也可以三表或更多表,设置主键后,关联时会自动按主键关联,后续的运算三表和两表也没有什么区别,还是 SPL 容易举一反三,不需要记忆那么多函数的用法。
主子表
一对多关联,是指一张表的一条记录能与另外一张表的任意记录进行对应。在一对多的关系中,我们把“一”的这端表叫做主表,“多”的这端表叫子表(从表)。例如订单表和订单明细表,每笔订单对应唯一的订单orderid,但是每个订单orderid可能对应多条订单明细信息。我们称订单表是主表,订单明细表是子表。
现有订单表和订单明细表,请查询 s150 门店各省客户的消费情况。
订单表(订单明细表的主表):
storeid |
orderid |
client |
location |
… |
s101 |
o10001 |
c104231 |
Beijing |
… |
s101 |
o10002 |
c107032 |
Beijing |
… |
s101 |
o10003 |
c108875 |
Beijing |
… |
… |
… |
… |
… |
… |
订单明细表(订单表的子表)
storeid |
orderid |
productid |
price |
… |
s101 |
o10001 |
p1078 |
1273 |
… |
s101 |
o10002 |
p1091 |
131 |
… |
s101 |
o10002 |
p1008 |
780 |
… |
… |
… |
… |
… |
… |
Python
detail_file="D:/data/Detail.csv" order_file="D:/data/Order.csv" detail=pd.read_csv(detail_file) order=pd.read_csv(order_file) detail150=detail.query("storeid=='s150'") order150=order.query("storeid=='s150'") order_detail=pd.merge(order150,detail150,on="orderid",how="inner") loc_consume=order_detail.groupby("location").price.sum() print(loc_consume) |
s150门店订单明细 s150门店订单 两表关联 分组聚合
|
本例中只有两表关联,关联关系明确,只要用订单号 orderid 将两表关联即可,然后按照地区 location 分组汇总价格就可以了。这里的关联是一对多的关系,但我们也可以将其转换为一对一的关系,
1. 汇总订单明细中每个订单的消费金额;
2. 关联订单表和第 1 步的汇总结果,此时就是一对一的关联关系了;
3. 按地区 location 分组聚合。代码写出来是这样的:
1. order_sum=detail150.groupby(“orderid”).price.sum()
2. order_detail=pd.merge(order150, order_sum,on="orderid",how="inner")
3. loc_consume=order_detail.groupby("location").price.sum()
SPL
A |
B |
|
… |
… |
|
16 |
D:/data/Detail.csv |
|
17 |
D:/data/Order.csv |
|
18 |
=file(A16).import@tc() |
|
19 |
=file(A17).import@tc() |
|
20 |
=A18.select(storeid=="s150") |
/选出 |
21 |
=A19.select(storeid=="s150") |
|
22 |
=A20.groups(orderid;sum(price):sprice) |
/分组聚合 |
23 |
=join(A21:order,orderid;A22:consume,orderid) |
/关联 |
24 |
=A23.groups(order.location;sum(consume.sprice):consume) |
/分组聚合 |
SPL 是精心设计过的,对于这种主子表关联,鼓励分组聚合——同维关联的方式完成关联运算,因为这样关联关系更清晰,当关联表很多很复杂时这样关联不容易出错。当然 SPL 同样可以完成 Python 先关联在汇总的方法,代码这样写:
A25=join(A21:order,orderid;A20:detail,orderid)
A26=A25.groups(order.location;sum(detail.price):consume)
多层主子表
有时会遇到多层主子表的情况,如:
续用上例的订单表和订单明细表,现在新增门店信息表。
门店信息表(主表):
storeid |
province |
… |
s101 |
Heilongjiang |
… |
s102 |
Beijing |
… |
s103 |
Tianjin |
… |
… |
… |
… |
请根据门店信息表、订单表和订单明细表,汇总各省门店的销售情况。
Python
#续用 detail 和 order 两表 store_file="D:/data/Store.csv" store = pd.read_csv(store_file) detail_sum = detail.groupby(['storeid','orderid']).price.sum() order_detai = pd.merge(order,detail_sum,on=['storeid','orderid']) store_amount = order_detai.groupby('storeid',as_index=False).price.sum() store_inf = pd.merge(store,store_amount,on=["storeid"]) province_sale = store_inf.groupby('province',as_index=False).price.sum() print(province_sale) |
汇总每个订单的金额
关联订单和汇总后的订单金额 汇总每个门店的销售额
关联门店信息和门店销售额 按门店所在省份分组汇总
|
订单明细表是订单表的子表,订单表又是门店表的子表,两层主子表。都遵循子表汇总——关联主表这一关联方式,会使关联关系更清晰,而且不会使关联后的表过大或者出现多对多的错误关联。本例中出现了多字段关联的方式,Python 也可以轻松完成。
SPL
A |
B |
|
… |
… |
|
28 |
D:/data/Store.csv |
|
29 |
=file(A28).import@tc() |
|
30 |
=A18.groups(storeid,orderid;sum(price):sprice) |
/汇总每个订单的金额 |
31 |
=join(A19:order,[storeid,orderid];A30:detail_sum) |
/关联订单和汇总后的订单金额 |
32 |
=A31.groups(order.storeid;sum(detail_sum.sprice):sales) |
/汇总每个门店的销售额 |
33 |
=join(A29:store,storeid;A32:store_amount) |
/关联门店信息和门店销售额 |
34 |
=A33.groups(store.province;sum(store_amount.sales):sales) |
/按门店所在省份分组汇总 |
SPL 按相同的思路完成运算也不在话下。
一主表多子表
一张主表多张子表的情况也比较常见,例如订单表、订单明细表和订单回款表。订单明细表和订单回款表都是订单表的子表。如:
订单表,订单明细表,回款表部分内容如下:
订单表(主表)
orderid |
clientid |
date |
… |
10012 |
100658 |
2019/2/13 |
… |
10023 |
103478 |
2019/1/12 |
… |
10040 |
108013 |
2019/1/4 |
… |
… |
… |
… |
… |
订单明细表(子表)
orderid |
productid |
price |
… |
10012 |
3018 |
428.5 |
… |
10012 |
3019 |
349.2 |
… |
10023 |
3019 |
349.2 |
… |
… |
… |
… |
… |
订单回款表(子表)
orderid |
term |
payment |
… |
10012 |
1 |
259.2 |
… |
10012 |
2 |
259.2 |
… |
10012 |
3 |
259.3 |
… |
… |
… |
… |
… |
请根据订单表、订单明细表和订单回款表,找出未完全回款的订单。
Python
detail_file="D:/data/Detail_2.csv" order_file="D:/data/Order_2.csv" pay_file="D:/data/Payment_2.csv" detail_2 = pd.read_csv(detail_file) order_2 = pd.read_csv(order_file,index_col='orderid') pay = pd.read_csv(pay_file) detail_order = detail_2.groupby('orderid').price.sum() pay_order = pay.groupby('orderid').payment.sum() order_d_p = order_2.join([pay_order,detail_order]) nopay_order = order_d_p.query('price>payment') print(nopay_order) |
汇总订单金额 汇总回款金额 关联主表和两张子表 选出 |
订单明细表是订单表的子表,回款表也是订单表的子表,一张主表两张子表。还是按照子表汇总——关联主表这一关联方式。Python 在实现上和上例不同了,因为 merge 不可以同时关联三张表,换用另一个函数 join,它可以利用索引将三表关联,建立一张新的宽表,最后拿这张宽表选出结果,在关联方式上和之前的思路略有不同。
SPL
A |
B |
|
… |
… |
|
36 |
D:/data/Detail_2.csv |
|
37 |
D:/data/Order_2.csv |
|
38 |
D:/data/Payment_2.csv |
|
39 |
=file(A36).import@tc() |
|
40 |
=file(A37).import@tc() |
|
41 |
=file(A38).import@tc() |
|
42 |
=A39.groups(orderid;sum(price):amount) |
/汇总订单金额 |
43 |
=A41.groups(orderid;sum(payment):payment) |
/汇总回款金额 |
44 |
=join(A40:order,orderid;A42:amount;A43:payment) |
关联主表和两张子表 |
45 |
=A44.select(amount.amount>payment.payment).(order) |
/选出 |
SPL 仍然是按照之前的思路,一步一步完成,关联多张表时也还是 join 函数,只在选出时将订单表展示出来就行了。
小结
Python 进行关联时,是将两张表复制,产生一张新的宽表,然后利用宽表来完成后续的运算,这会增加内存消耗且效率不高;在遇到多表关联时,Python 需要使用其他的函数,并且有一些特殊的条件才可以完成,否则只能使用 merge 函数关联多次,略显麻烦。
SPL 的关联只是将两表建立关联关系,不会复制数据,运算时利用建立好的关联关系迅速完成运算,内存占用小,运行效率高;在遇到多表关联时,SPL 同样从容,两张表和 N 张表的关联对于 SPL 来说是一样的,同样的问题用同样的方式解决,不需要烧脑绕路。
主子关联(一对多)时,将子表汇总后再与主表关联就成了同维关联,因此将两者放在一起介绍。
Nice!
英文版