目录:
1. 前言
2. 概览
3. 肮脏代码与相对改良(例)
4. @pyspark,SQL优化...
1.前言
笔者小白,从事SAAS开发工作(其实是个写脚本的初级菜鸟),在工作中饱受挫折,于是下定决心,更新自己操作中的心德,一来可以分享给其他朋友让大家避免我的歧途,二来可以随时总结,加深记忆。文章如有问题,欢迎大神及时指正,互相学习进步~!
2.概览
当下主流操作亿兆级别大数据主要还是Apache他们家的Spark,Hadoop等,通过内存直接运算数据,速度明显优于其他方式。但是很多情况下,我们还是老老实实写一些本职工作的东西(毕竟也就只有千万级别数据),所以要求我们满足客户需求的前提,准确的前提下,提高程序运行效率(时间、空间),保持代码整洁干净,方便他人阅读。
3. 肮脏代码(例)【A为差,B为好】
我刚开始接触的时候,写的一坨SHI(虽然现在也不是很干净),具体表现为,胡乱定义变量,重复的function,重复语句,重复定义赋值,满篇循环,通篇遍历,简直辣眼睛,下面请“欣赏”!
I-A.“无限月读(循环)"
def stage0():
for i in range(len(df_NEW2['StoreCode'])):
if pd.isnull(df_NEW2.loc[i,'CloseDate_y']) and df_NEW2.loc[i,'OpenDate']>DAY1 and df_NEW2.loc[i,'OpenDate']<=DAY2:
num11=((df_NEW2.loc[i,'OpenDate']-DAY1).days)//7
num1_1=(num11)%7
if num11==52:
df_NEW2.loc[i,12:]=0
if num11==0:
df_NEW2.loc[i,12:]=7
else:
for j in range(num11):
df_NEW2.loc[i,str('week')+str(j+1)]=0
df_NEW2.loc[i,str('week')+str(num11+1)]=7-num1_1
for t in range(num11+1,Weeks):
df_NEW2.loc[i,str('week')+str(t+1)]=7
else:
pass
return df_NEW2
首先说这样写可不可以达到预期的结果呢?答案是可以的。但是会很耗时。我记得当时光跑这一段,就花费了将近1分钟。很多时候设计的程序,需要很快的运行速度,才能给用户最好的体验。设想下,如果你点一下“提交”按钮,网页卡了一个小时不能动的尴尬场景,估计5秒钟就烦了吧。
昂,回归正题,第一种恶心的写法就是过多的循环,看下这段代码遍历了多少次,时间复杂度是2n^2+2,大概是n方级别的,这很可怕,如果需要读取的主键元素很多很多,电脑就会直接崩掉。
尽量减少不必要的遍历,会极大的优化程序的运行效率。
I-B.减少不必要的遍历
其实减少不要的遍历,可以做到以下几点:
(1)多用py的内置函数。python毕竟是高级语言,写源代码固然锻炼码子能力,可是很多时候多是徒劳,遇到情况可以先check(github, csdn,简书,博客园,身边大神...)下有无更加方便的思路、语法是你实现不了解的,这样既节省时间,还提高效率。python的内置函数远远优于你自己在Py上写源代码,之前学了很多排序算法,什么冒泡排序,希尔排序,插入排序,堆排序....其实内置语法一句话sorted()就解决了(除非你需要针对数组去做运算,或者对稳定性没有要求)。如果是用Pandas, 那么更加不需要自己去写算法。
(2)计量避免针对元素计算,选择分模块整体运算大大提升效率。比如遍历一列中每一个元素去做数学运算,那比如事先groupy好,直接apply(lambda x:...),这样避免了循环还要快得多。
(3)事先整理好逻辑,避免不必要的步骤。很多时候我是老老实实按照客户的思路去做,但是客户可能用vba,用excel,用oracle做,他的思路是他那个途径所必须的,而我不一定完完全全按照他的思路step1-2-3....只要最终得到一样的结果就好,完全可以选择最适合自己的思路和数据结构。
下面简单介绍下比较好的方式,顺便整理下我用过比较好的内置函数。
dataframe的列直接运算
如果只是简单的A列+B列-C列=新的一列,就可以直接写成如下。切勿循环每一行去做,这样如果你有几千万行,会卡死。
##新建一列你想要的resut,记得调整数据类型。
df_1['A','B','C']=pd.to_numeric(df_1['A','B','C'])-
df_1['result']=df_1['A']+df_1['B']-df_1['C']
print(df_1)
apply/map/applymap(lambda x: x......)
对于一些附带条件的运算,或者转换数据类型,拆分字符串,总而言之就是我们希望主键之间的任何加工,我们都可以尝试以上语法。(详细见代码注释)
##对dataframe本身进行apply,x为dataframe本身,条件if...elif..else
df_temp_p['ACTUAL_AREA']=df_temp_p.apply(lambda x: 0 if x['COMPLETION_DATE'] > x['PERIOD_TIME'] else x['OCCUPIED_LAND_AREA'],axis=1)
##当然也可以针对groupby之后的dataframe进行apply,此外x也可以是sum某个主键的所有value并不附加条件。
df_temp['SUM_AREA_COMPANY']=df_sum.groupby(['ENTITY_CODE','PERIOD_TIME']).apply(lambda x: x['ACTUAL_AREA'].sum())
##也可以针对某一个主键,转换数据类型。
df_e['ENTITY_CODE'] = df_e['ENTITY_CODE'].apply(lambda x: str(x))
##applymap,map与apply不同。apply是针对整个数据矩阵,而map是针对个别元素。
temp_go[w_list[i]]=temp_go[w_list[i]].map(lambda x: 7 if x>=7 else (x if x>=0 else 0))
eval('数学公式字符串')
隆重引入我们的高能函数eval()。这个短短的一句话可以胜过千万行代码,真的是大神为我们铺好了所有的路。
它有什么用呢?举个简单的例子,上面我们想运用的某些简单的数学公式,你大可把公式直接写进去,直接出结果。
##创建一个新列A
df1['A']=0
df1=df1.eval('A=C*B+D-E')
当然,你也可以写更复杂的数学公式,甚至数据模型。举例:比如这个标准正态分布反函数。
R= 𝜱((𝜱^(−𝟏) (Rate)+√𝝆 *Z )/√(𝟏−𝝆))
import math
from scipy import stats
from sklearn import datasets
df=df.eval('R=norm.cdf((norm.ppf(Rate+math.sqrt(B)*Z)/math.sqrt(1-B))')
另外,如果你脚本运行的是系统传给你的字符串,也可以对字符串进行拆分加工,作为一个变量传入eval函数。
##传入的字符串
Formula='Acc{AS970100}+Acc{AISM0101}+Acc{AS0451}'
##加工字符串
##拆分算是字符串
def split_it():
operator=list()
split=formula.split('Acc{')
for i in range(len(split)):
if i !=0 and i !=(len(split)):
a=split[i]
b=a.split('}')
operator.append(b)
return [a for a in operator]
####得到公式字符串
def append_strings():
for j in range(0,len(M),1):
Q=M[j][0]
T=M[j][1]
FF=Q+T
F.append(FF)
return [v for v in F]
def make_formula():
K=Formula_1()
K.insert(0,'Data=')
Key=''.join(K)
return Key
##将成型的公式作为key变量,传入eval函数
key=make_formula()
DF=DF.eval(key)
II. 尽量减少耗时的函数
如同SQL中,HAVING的使用,会影响效率,对于python,有一些函数本来就会比较复杂。我个人发现while loop很可怕,但是并不敢一棒子打死。遇到while,我都会用创建新list和append/ for loop if else判断结果的方式去进行。虽然while和for各有意义,但是我还是害怕回避while的使用。while循环中,只要为true则记录,if为单一条件判断。
下面我们来对比,让这两种方法干同一件事,速度的对比。干什么事呢?很简单,从0数到1000万。
II-A
import time
##while loop方式,用时107秒。
start=time.clock()
count=0
while (count < 10000000):
print('the loop is %s' %count)
count+=1
end=time.clock()
print(end-start)
while loop结果如图:107秒
下面我们看看创建新list和append/ for loop if else判断,的表现
import time
##创建新list和append/ for loop if else判断,结果为4秒。
start=time.clock()
alist=list()
for i in range(0,10000000):
if (i < 10000000):
alist.append(i)
else:
pass
print(alist)
end=time.clock()
print(end-start)
for+if+appendlist方法结果如图:4秒。
两者相差27倍
所以我还是很害怕使用while loop的。当然只有几十个数,无所谓啦~
但是,某些语言,好像并无大碍,所以我刚才说不清楚是不是只有py这样。。。下面看一段笔者之前写的PHP代码,while无伤大雅...当然有可能是数据库里元素不多?...
<?php
$fetchVideos = mysqli_query($con, "SELECT location FROM videos ORDER BY id DESC");
while ($row = mysqli_fetch_assoc($fetchVideos)){
$location = $row['location'];
echo "<div >";
echo "<video src='".$location."' controls width='320px' height='200px' >";
echo "</div>";
}
?>
III. 应用更高效的数据结构
进一步提升代码运行效率,我们可以考虑更加高效的数据结构,是更宏观的层面,举个例子。
假如现在我们想要将一个千万行的dataframe的某一个主键的某几类值,转为新的主键。(数据升维后再降维)
一般我们要学会在结构中找特点,根据特点来改善算法。
比如我们可以用pd.merge, concat 来进行多组数据整合,用groupby filter的方法来将大数据先分类再运算,避免逐条运算。基本语法如下:
先filter分类,再每块整体运算, 在merge回去。
class Merge():
def __init__(self,accountlist,data,content):
self.a = accountlist
self.d = data
self.c = content
##选取需要筛选的account
def filter_account(self,iaccount):
df1=self.d[self.d['Account']==iaccount]
df1.columns = df1.columns.str.replace('Data', iaccount) ##给每一个新的data以其account名字命名
return df1
##将需要参与运算的account拿出来合并在一个数据矩阵
def merge_account(self):
for i in range(len(self.a)):
df_temp=self.filter_account(self.a[i])
if i == 0:
df_merge_final=df_temp
if i > 0:
df_merge_final=pd.merge(df_merge_final,df_temp,how='outer',on=self.c)
return df_merge_final
###传入参数
if __name__=='__main__':
content=['Entity','Years','Period','View','Currency','Type','Product','Misc','id','Scenario','Version']
##与filter不同在于,filter是维度划分,这个则是固定维度(如果不固定,一边会出现重复值)
##一级维度筛选product类型
data=pd.read_excel('data.xlsx')
accountlist=['AAAAAA','BBBBBB','CCCCCC']
笛卡尔积
这个笛卡尔积也可以很好的做到维度计算。
具体代码如下,用到了numpy里面的np.diag,取对角线为一个新的List。
###用到了numpy里面的np.diag,取对角线为一个新的List.
def Cartesian():
list_bill=list()
for i in range(len(K1_list)):
df1=HGdf[K1_list[i]]
df1_N=np.array(df1)
df1_nn=np.diag(df1_N)
list_bill.append(df1_nn)
return [a for a in list_bill]
CART=Cartesian()
IV. 打包,避免重复赋值。
这个应该很好理解,只是很多时候在写的时候因为偷懒,就换命名变量,赋值。要养成写注释,有顺序的命名变量。
此外,不要重复引用function,能打包的就写一次。
这里有一个小的心德,以前总喜欢用conda-base,逐段代码运行,很舒服方便,但是不利于整体的全局观和整体逻辑规划。学会写完整project,将能够多次利用的功能、对象放在一个def或者class里,传入对应参数,会使得代码很美观,别人看了也舒服,自己清bug也舒服~~!
4. @pyspark,SQL优化...
提供个新的思路,也是我自己在学习琢磨的。spark也有pyspark的包,不用直接装java的jdk环境,也不用Hadoop, 被称为伪分布式。可以直接
pip install pyspark。
但是国内的朋友注意,可能会中断,因为外网比较慢,这里推荐利用清华大学tuna镜像库,然后设置default time out。
pip --default-timeout=100 install -i https://pypi.tuna.tsinghua.edu.cn/simple pyspark
SQL优化,我个人理解在SQL读入数据和Insert回去的时候,有些逻辑运算可以再SQL里做,但是要去试,比如:(以读取为例)
此处sql2为SQL查询语句,可以再其中加入运算,比如排个序,选个表,union下等等.....
from pymysql import *
##读取SQL数据
class MysqlHelper:
def __init__(self, host, user, passwd, port, db):
self.host = host
self.user = user
self.passwd = passwd
self.port = port
self.db = db
def open(self):
self.conn = connect(host=self.host,
user=self.user,
passwd=self.passwd,
port=self.port,
db=self.db,)
self.cursor = self.conn.cursor()
def close(self):
self.cursor.close()
self.conn.close()
def cud(self, sql):
try:
self.open()
self.cursor.execute(sql)
self.conn.commit()
self.close()
except Exception as e:
print(e)
def all(self, sql):
try:
self.open()
self.cursor.execute(sql)
result = self.cursor.fetchall()
self.close()
return result
except Exception as e:
print(e)
##输入参数
sql2 = "SELECT * FROM as_data WHERE zzz=qqq'"
result2 = MysqlHelper('XXXXX', 'YYYY', 'ZZZZ', 3306, 'QQQQ').all(sql2)
未完待续...........