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 来说是一样的,同样的问题用同样的方式解决,不需要烧脑绕路。

主子关联(一对多)时,将子表汇总后再与主表关联就成了同维关联,因此将两者放在一起介绍。