Python Pandas PK esProc SPL,谁才是数据预处理王者?

#取第1条记录
df.iloc[1]
#区间取第1-3条记录(左闭右开)
df.iloc[1:4]
#步进(偶数位置)
df.iloc[1::2]
#倒数第2条(从1开始)
df.iloc[-2]
#用记录序号和字段序号取值
df1.iloc[1,0]
#用记录序号和字段名取值
df.loc[1,‘two’]


SPL序表自带行号(从1开始)、字段号、字段名,可以通过下标和字段名方便地访问记录,这方面SPL和Pandas区别不大,用法都很方便:



//取行号列表,#是行号的字段名
T.(#)
//取第2条记录(可简写为T(2))
T.m(2)
//区间取第2-4条记录(左闭右闭)
T.m(2:4)
//步进(偶数位置)
T.step(2,2)
//倒数第二条(从1开始)
T.m(-2)
//用记录序号和字段序号取值
T.m(2).#1
//用记录序号和字段名取值
T.m(2).two


行号(下标)的本质是高性能地址索引,除了行号,Pandas和SPL还提供了其他种类的索引,以及对应的查询函数,包括唯一值的哈希索引,有序值的二分查找索引。性能不是本文重点,且两者功能类似,这里就不多说了。


**维护数据**


修改指定位置的记录。Pandas:



df.loc[4,[‘NAME’,‘SALARY’]]=[‘aaa’,1000]


Pandas没有直接提供修改函数,而是用Series对象取出记录的部分字段,再用List去修改。Series这里表示的是记录,但通常表示列,List通常表示记录,但也可以表示列,这些规则初学者容易混淆。


SPL:



T.modify(5,“aaa”:NAME,1000:SALARY)


SPL直接提供了修改函数,符合初学者的常识。当然,SPL也可以取出记录再修改,两种方法各自适合不同的场景。


在指定位置插入新记录。Pandas:



record=pd.DataFrame([[100,“wang”,“lao”,“Femal”,“CA”, pd.to_datetime(“1999-01-01”), pd.to_datetime(“2009-03-04”),“HR”,3000]],columns=df.columns)
df = pd.concat([df.loc[:2], record,df.loc[3:]],ignore_index=True)


Pandas没有真正的记录对象,也没有直接提供插入记录的方法,间接实现起来较麻烦,先构造一条单记录的DataFrame,再将原DataFrame按指定位置拆成前后两个DataFrame,最后把三个DataFrame拼起来。很多易忽略的细节也要处理好,否则无法获得理想结果,比如构造记录时要保证字段名与原DataFrame相同,拼接新DataFrame时不能保留原来的行号。


SPL:



T.insert(3,100,“wang”,“lao”,“Femal”,“CA”,date(“1999-1-1”),date(“2009-3-4”),“HR”,3000)


SPL对记录比较重视,直接提供了插入记录的方法,代码简洁易于理解。


添加计算列。Pandas:



today = datetime.datetime.today().year
df[“Age”] = today-pd.to_datetime(df[“BIRTHDAY”]).dt.year
df[“Fullname”]=df[“NAME”]+ " " +df[“SURNAME”]


Pandas没有提供添加计算列的函数,虽然实现起来问题不大,但添加多个列就要处理多次,还是比较麻烦。Pandas的时间函数也不够丰富,计算年龄比较麻烦。


SPL:



T.derive(age(BIRTHDAY):Age, NAME+“”+SURNAME:Fullname)


SPL提供了添加计算列的函数,一次可以添加多个列,且时间函数更加丰富。


### 结构化数据计算


**计算函数**


Pandas内置丰富的库函数,支持多种结构化数据计算,包括:遍历循环apply\map\transform\itertuples\iterrows\iteritems、过滤Filter\query\where\mask、排序sort\_values、唯一值unique、分组groupby、聚合agg(max\min\mean\count\median\ std\var\cor)、关联join\merge、合并append\concat、转置transpose、移动窗口rolling、shift整体移行。


Pandas没有专门的函数进行记录集合的交、并、差等运算,只能间接实现,代码比较繁琐。Pandas会为类似的计算提供多个函数,比如过滤,这些函数的主体功能互相覆盖,只是参数约定\输出类型\历史版本不同,学习时要注意区分。


SPL的计算函数也很丰富,包括:遍历循环.()、过滤select、排序sort、唯一值id、分组group、聚合max\min\avg\count\median\top\icount\iterate、关联join、合并conj、转置pivot。


SPL对记录集合的集合运算支持较好,针对来源于同一集合的子集,可使用高性能集合运算函数,包括交集isect、并集union、差集diff,对应的中缀运算符是^、&、\。对于来源不同的集合,可用merge函数搭配选项进行集合运算,包括交集@i、并集@u、差集@d。


除了集合运算,SPL还有以下独有的运算函数:分组汇总groups、外键切换switch、有序关联joinx、有序归并merge、迭代循环iterate、枚举分组enum、对齐分组align、计算序号pselect\psort\ptop\pmax\pmin。Pandas没有直接提供这些函数,需要硬编码实现。


有大量功能类似的函数时,Pandas要用不同的名字或者参数进行区分,使用不太方便。而SPL提供了非常独特的函数选项,使功能相似的函数可以共用一个函数名,只用函数选项区分差别。比如,select函数的基本功能是过滤,如果只过滤出符合条件的第1条记录,可使用选项@1:



T.select@1(Amount>1000)


对有序数据用二分法进行快速过滤,使用@b:



T.select@b(Amount>1000)


函数选项还可以组合搭配,比如:



Orders.select@1b(Amount>1000)


结构化运算函数的参数有些很复杂,Pandas需要用选项或参数名来区分复杂的参数,这样易于记忆和理解,但代码难免冗长,也使语法结构不统一。比如左关联:



pd.merge(Orders, Employees, left_on=‘SellerId’, right_on=‘EId’, how=‘left’, suffixes=[‘_o’,‘_e’])


SPL使用层次参数简化了复杂参数的表达,即通过分号、逗号、冒号自高而低将参数分为三层,不过这样会增加一些记忆难度。同样左关联:



join@1(Orders:o,SellerId ; Employees:e,EId)


层次参数的表达能力也很强,比如join函数里的分号用于区分顶层参数序表,如果进行多表关联,只要继续加分号就可以。Pandas参数的表达能力就差多了,merge函数里表示DataFrame的选项只有left和right,因此只能进行两表关联。


Pandas和SPL都提供了足够丰富的计算函数,进行单个函数的基础计算时,区别不算大。但实际工作中的数据准备通常有一定复杂度,需要灵活运用多个函数,且配合原生的语法才能实现,这种情况下,两者的区别就比较明显了。


**同期比**


先按年、月分组,统计每个月的销售额,再计算每个月比去年同月份的销售额的增长率。Pandas:



sales[‘y’]=sales[‘ORDERDATE’].dt.year
sales[‘m’]=sales[‘ORDERDATE’].dt.month
sales_g = sales[[‘y’,‘m’,‘AMOUNT’]].groupby(by=[‘y’,‘m’],as_index=False)
amount_df = sales_g.sum().sort_values([‘m’,‘y’])
yoy = np.zeros(amount_df.values.shape[0])
yoy=(amount_df[‘AMOUNT’]-amount_df[‘AMOUNT’].shift(1))/amount_df[‘AMOUNT’].shift(1)
yoy[amount_df[‘m’].shift(1)!=amount_df[‘m’]]=np.nan
amount_df[‘yoy’]=yoy


分组汇总时,Pandas很难像SQL那样边计算边分组,通常要先追加计算列再分组,这导致代码变复杂。计算同期比时,Pandas用shift函数进行整体移行,从而间接达到访问“上一条记录”的目的,再加上要处理零和空值等问题,整体代码就更长了。


SPL:




|  |  |
| --- | --- |
|  | A  |
| 2  | =sales.groups(year(ORDERDATE):y,month(ORDERDATE):m;sum(AMOUNT):x)  |
| 3  | =A2.sort(m)  |
| 4  | =A3.derive(if(m==m[-1],x/x[-1] -1,null):yoy)  |


分组汇总时,SPL可以像SQL那样边计算边分组,灵活的语法带来简练的代码。计算同期比时,SPL直接用[-1]表示“上一条记录”,且可自动处理数组越界和被零除等问题,整体代码较短。


除了用[x]表示相对位置,SPL还可以用[x:y]表示相对区间,比如股票的3日移动平均值:



T.derive(Amount[-2:0].avg():ma)


Pandas也可以表示相对区间,但由于语言整体性不佳,无法从语法层面直接支持,所以提供了一个新函数rolling。同样计算股票的3日移动平均值:



df[‘ma’]=df[‘Close’].rolling(3, min_periods=1).mean()


**贷款分期**


根据多项贷款的基本信息(金额、期数、利息),计算每项贷款每一期的还款明细(当期还款额、当期利息、当期本金、剩余本金)。Pandas:



loan_data = … #省略loan_data的取数过程
loan_data[‘mrate’] = loan_data[‘Rate’]/(100*12)
loan_data[‘mpayment’] = loan_data[‘LoanAmt’]*loan_data[‘mrate’]np.power(1+loan_data[‘mrate’],loan_data[‘Term’]) \ /(np.power(1+loan_data[‘mrate’],loan_data[‘Term’])-1)
loan_term_list = []
for i in range(len(loan_data)):
loanid = np.tile(loan_data.loc[i][‘LoanID’],loan_data.loc[i][‘Term’])
loanamt = np.tile(loan_data.loc[i][‘LoanAmt’],loan_data.loc[i][‘Term’])
term = np.tile(loan_data.loc[i][‘Term’],loan_data.loc[i][‘Term’])
rate = np.tile(loan_data.loc[i][‘Rate’],loan_data.loc[i][‘Term’])
payment = np.tile(np.array(loan_data.loc[i][‘mpayment’]),loan_data.loc[i][‘Term’])
interest = np.zeros(len(loanamt))
principal = np.zeros(len(loanamt))
principalbalance = np.zeros(len(loanamt))
loan_amt = loanamt[0]
for j in range(len(loanamt)):
interest[j] = loan_amt
loan_data.loc[i][‘mrate’]
principal[j] = payment[j] - interest[j]
principalbalance[j] = loan_amt - principal[j]
loan_amt = principalbalance[j]
loan_data_df = pd.DataFrame(np.transpose(np.array([loanid,loanamt,term,rate,payment,interest,principal,principalbalance])),columns = [‘loanid’,‘loanamt’,‘term’,‘rate’,‘payment’,‘interest’,‘principal’,‘principalbalance’])
loan_term_list.append(loan_data_df)
loan_term_pay = pd.concat(loan_term_list,ignore_index=True)


上面代码用两层循环作为主体结构,先循环每项贷款,再循环生成该项贷款的每一期,然后将各期明细转置为DataFrame,并追加到事先准备好的list里,继续循环下一项贷款,循环结束后将list里的多个小DataFrame合并为一个大DataFrame。业务逻辑是比较清晰的,就是按公式计算各项数据项,但因为两层循环的结构比较复杂,数据类型的转换比较麻烦,导致代码显得冗长。


SPL:




|  |  |
| --- | --- |
|  | A  |
| 1  | //省略loan\_data的取数过程  |
| 2  | =loan\_data.derive(Rate/100/12:mRate,LoanAmt\*mRate\*power((1+mRate),Term)/(power((1+mRate),Term)-1):mPayment)  |
| 3  | =A2.news((t=LoanAmt,Term);LoanID, LoanAmt, mPayment:payment, Term, Rate, t\* mRate:interest, payment-interest:principal, t=t-principal:principlebalance)  |


业务逻辑上SPL和Pandas几乎一样,但因为语言整体性强,两层循环可以用一个news函数实现,也不需要麻烦的类型转换,因此代码大幅简化。


**按工龄分组**


按员工工龄将员工分组,并统计每组的员工人数,有些组之间有重复。Pandas:



#省略员工信息emp的取数过程
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’])


Pandas擅长等值分组,也可实现简单的区间枚举分组,遇到本题这种可重复的枚举分组只能硬编码实现,大概过程:循环分组条件,转为等值分组解决问题,处理分组子集,最后合并结果。此外,Pandas没有计算工龄的函数,也要手工实现。


SPL:




|  |  |  |
| --- | --- | --- |
|  | A  | B  |
| 1  | /省略员工信息emp的取数过程  |  |
| 2  | [?<5,?>=5 && ?<10,?>=10,?>=15]  | /条件  |
| 3  | [Within five years,Five to ten years, More than ten years, Over fifteen years]  | /组名  |
| 4  | =emp.derive(age(HIREDATE):EMPLOYED)  | /计算工龄  |
| 5  | =A4.enum@r(A2, EMPLOYED).new(A3(#):EMPLOYED,~.len():NUM)  | /枚举分组  |


函数enum用于枚举分组,选项@r处理重复分组的情况,再配合SPL高效的表达能力,整体代码比Pandas简短得多。


通过上面的几个例子可以看出来,Pandas适合简单的数据准备场景,遇到复杂些的结构化数据计算,代码就很难写了。SPL语言整体性好,无论简单场景还是复杂计算,代码量都不多。


### 大数据量计算


如果文件或库表的数据量较大(指超出内存,而不是Big Data),最终都要用循环分段的办法来处理,即:每次读取并计算少量数据,再保留本次计算的中间计算结果,循环结束后合并多个中间计算结果(比如过滤),或对合并结果做二次计算(比如分组汇总)。即使是基本的结构化数据计算,数据量大时也很麻烦,如果涉及关联、归并、并集或综合性计算,代码将更加复杂。


**聚合**


Pandas:



chunk_data = pd.read_csv(“orders.txt”,sep=“\t”,chunksize=100000)
total=0
for chunk in chunk_data:
total+=chunk[‘amount’].sum()


对于聚合这种简单的大文件计算,Pandas代码还算简单。打开大文本时,Pandas提供了一个选项chunksize,用来指定每次读取的记录数,之后就可以用循环分段的办法处理大文本,每次读入一段并聚合,再将计算结果累加起来。


SPL:



=file(“orders.txt”).cursor@tc().total(sum(‘amount’))


SPL同样采用循环分段的办法处理大文本,但SPL封装了代码细节,提供了方便的游标机制,允许用类似处理小数据量的语法,直观地处理较大的数据量,所以代码里看不到循环累加的过程。


**过滤**


Pandas:



chunk_data = pd.read_csv(“d:/orders.txt”,sep=“\t”,chunksize=100000)
chunk_list = []
for chunk in chunk_data:
chunk_list.append(chunk[chunk.state==“New York”])
res = pd.concat(chunk_list)


Pandas没有提供游标,只能硬编码进行循环分段,每次将部分数据读入内存进行过滤,过滤的结果也存储于内存中。


上面的方法只适合结果集小于内存的场景,如果结果集大于大内存,就要把每次过滤的结果写入文件中,代码变化较大:



chunk_data = pd.read_csv(“d:/orders.txt”,sep=“\t”,chunksize=100000)
isNew=True
for chunk in chunk_data:
need_data = chunk[chunk.state==‘New York’]
if isNew == True:
need_data.to_csv(“orders_filter.txt”,index=None)
isNew =False
else:
need_data.to_csv(“orders_filter.txt”,index=None,mode=‘a’,header=None)


首次创建文件和后续追加记录不同,代码细节要小心处理,代码难度显著增加。


SPL:




|  |  |
| --- | --- |
|  | A  |
| 1  | =file(d:/orders.txt).cursor@tc()  |
| 2  | =A1.select(state=="New York")  |
| 3  | =A2.fetch()  |


游标机制隐藏了底层细节,解题难度显著降低,代码量显著缩小。不难看出,SPL语言的整体性较好,因此能够从底层提供游标机制。


结果集大于内存时,只要简单地把A3改为:



=file(“orders_filter.txt”).export@tc(A2)


得益于游标机制,SPL不必手工区分首次创建文件和后续追加,代码简短得多。


**排序**


pandas:



def parse_type(s):
if s.isdigit():
return int(s)
try:
res = float(s)
return res
except:
return s
def pos_by(by,head,sep):
by_num = 0
for col in head.split(sep):
if col.strip()==by:
break
else:
by_num+=1
return by_num
def merge_sort(directory,ofile,by,ascending=True,sep=“,”):
with open(ofile,‘w’) as outfile:
file_list = os.listdir(directory)
file_chunk = [open(directory+“/”+file,‘r’) for file in file_list]
k_row = [file_chunk[i].readline()for i in range(len(file_chunk))]
by = pos_by(by,k_row[0],sep)
outfile.write(k_row[0])
k_row = [file_chunk[i].readline()for i in range(len(file_chunk))]
k_by = [parse_type(k_row[i].split(sep)[by].strip())for i in range(len(file_chunk))]
with open(ofile,‘a’) as outfile:
while True:
for i in range(len(k_by)):
if i >= len(k_by):
break
sorted_k_by = sorted(k_by) if ascending else sorted(k_by,reverse=True)
if k_by[i] == sorted_k_by[0]:
outfile.write(k_row[i])
k_row[i] = file_chunk[i].readline()
if not k_row[i]:
file_chunk[i].close()
del(file_chunk[i])
del(k_row[i])
del(k_by[i])
else:
k_by[i] = parse_type(k_row[i].split(sep)[by].strip())
if len(k_by)==0:
break
def external_sort(file_path,by,ofile,tmp_dir,ascending=True,chunksize=50000,sep=‘,’,usecols=None,index_col=None):
os.makedirs(tmp_dir,exist_ok=True)
try:
data_chunk = pd.read_csv(file_path,sep=sep,usecols=usecols,index_col=index_col,chunksize=chunksize)
for chunk in data_chunk:
chunk = chunk.sort_values(by,ascending=ascending)
chunk.to_csv(tmp_dir+“/”+“chunk”+str(int(time.time()*10**7))+str(uuid.uuid4())+“.csv”,index=None,sep=sep)
merge_sort(tmp_dir,ofile=ofile,by=by,ascending=ascending,sep=sep)
except Exception:
print(traceback.format_exc())
finally:
shutil.rmtree(tmp_dir, ignore_errors=True)
infile = “D:/orders.txt”
ofile = “D:/extra_sort_res_py.txt”
tmp = “D:/tmp”
external_sort(infile,‘amount’,ofile,tmp,ascending=True,chunksize=1000000,sep=‘\t’)


将大文件分成多段,每段分别排序,分别写入N个临时文件;再打开N个临时文件,并维持一个N个成员的数组,指向每个临时文件的当前读取位置,初始位置是第一条记录;之后比较该数组对应的N条记录,将最小记录i写入结果文件,并下移i对应的临时文件的当前读取位置;继续比较N条记录,直至排序结束。这是大文件排序时常用的归并算法,实现过程比较复杂,Pandas缺乏方便的游标机制,只能硬编码实现,代码冗长且不易解读。


SPL:




|  |  |
| --- | --- |
|  | A  |
| 1  | =file("D:/orders.txt").cursor@tc()  |
| 2  | =A1.sortx(amount)  |
| 3  | =file("D:/extra\_sort\_res\_py.txt").export@tc(A2)  |


上面同样采用归并法实现大文件排序,由于SPL支持游标机制,复杂的细节被隐藏起来,只要写出简短的代码就能实现。


大数据量计算还有很多种,比如分组汇总、关联、交集等,很多都比排序复杂,比如分组汇总的第一步通常就是大排序,追求效率就要用更复杂的哈希分堆。Pandas的语言整体性差,不支持游标,只能硬编码实现这些计算,难度非常大,至于综合性的大数据量计算,基本就不用考虑Pandas了。SPL语言整体性较好,有方便的游标机制,代码都不难写,比如大结果集的分组汇总:




|  |  |
| --- | --- |
|  | A  |
| 1  | =file(file\_path).cursor@tc()  |
| 2  | =A1.groupx(key;sum(coli):total)  |
| 3  | =file(out\_file).export@tc(A2)  |


综合性的,计算每种商品销售额最大的3笔订单:




|  |  |
| --- | --- |
|  | A  |
| 1  | =file(file\_path).cursor@tc()  |
| 2  | =A1.groups(product;top(3; -amt):three)  |
| 3  | =A2.conj(three)  |


Pandas提供了丰富的库函数,但因为没有参与Python的统一设计,无法获得Python的底层支持,导致语言的整体性不佳,只擅长简单的数据准备工作,不适合一般的场景。esProc SPL的语言整体性较好,结构化数据类型更加专业,可以用简洁直观的代码实现一般的数据准备工作,包括解析不规则的数据源,表达多层数据,进行复杂的结构化数据计算,完成大数据量计算。


### SPL资料


* [SPL下载](https://bbs.csdn.net/topics/618545628)
* [SPL源代码](https://bbs.csdn.net/topics/618545628)



![img](https://img-blog.csdnimg.cn/img_convert/405c66feb5d1c573bb515b22fa3fdbec.png)
![img](https://img-blog.csdnimg.cn/img_convert/9dce62431fc2bc472c2493c332a9ce57.png)

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618545628)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

得Python的底层支持,导致语言的整体性不佳,只擅长简单的数据准备工作,不适合一般的场景。esProc SPL的语言整体性较好,结构化数据类型更加专业,可以用简洁直观的代码实现一般的数据准备工作,包括解析不规则的数据源,表达多层数据,进行复杂的结构化数据计算,完成大数据量计算。


### SPL资料


* [SPL下载](https://bbs.csdn.net/topics/618545628)
* [SPL源代码](https://bbs.csdn.net/topics/618545628)



[外链图片转存中...(img-nSjpUkiT-1714159316855)]
[外链图片转存中...(img-rrXbfvWQ-1714159316856)]

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618545628)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值