算法优化---向量数组计算替代判断
目录
前言
接着之前降到运行pandas效率(时间复杂度)的优化。有一次用户抱怨说我的脚本太慢了,仔细打断点统计了,存取其实也才三分钟,重头全都压在了计算。一开始觉得没有任何优化空间,直到有一天我无聊,开始逐行代码分析倒地哪里慢。于是乎发现了以前一直很爱用的方法的惊天大坑。
元素级别迭代与series,ndarray的迭代
前者与后者在运行效率(速度)上有天然的差别。python在ndarray和series整体运算远远快于元素逐一的运算。因此我们应该多利用这个特点去避开针对元素的判断和计算。
比如:在做固定资产折旧,上一月末NBV-当月折旧=当月NBV。这种迭代需要通过数据结构的变化来避开元素判断。
方法1:
如图,我们按照这种数据结构计算,避免不了在行上,每一条资产,元素级别按照判断P几,然后再计算NBV和Dep,再shift,那可想而知速度很慢。不过节省空间。代码如下:
##此算法为迭代计算,即前一个运算的结果为后一个运算的参数。eg: a=b-c --> b=a --> a2=b-c-c
def iteration_cal(dataframe, new_col, Period_col, Begining_col, cal_col, num_of_period=None, Begining_Period=None):
df = dataframe
df[str(new_col)] = 0
iter_count = int(num_of_period - Begining_Period + 1)
count = 0
for i in range(0, iter_count):
if count < iter_count:
df[str(new_col)] = df.apply(lambda x: float(x[str(Begining_col)]) - float(x[str(cal_col)])
if x[str(Period_col)][1:] == str(Begining_Period)
else (df.loc[x.name - 1, str(new_col)] - x[str(cal_col)]
if (x.name + 1) % num_of_period > Begining_Period or (x.name + 1) % num_of_period == 0
else 'Pending'), axis=1)
count += 1
return df
方法2:
我们不如换一种数据结构,如图。这个方式是之前写的小文章,stack结构的应用。一切迭代都是针对列的运算(series or ndarray)。只要你没有那么多的Period(成百上千)(不然会内存爆掉),一般足够了。代码如下:
列计算的代码:
由于用户订的需求逻辑很多,下面挑选了个简单的例子。
##NBV_1单独先算出来。。。。
for i in range(1, 49):
dep_calc_df['Depr_'+str(i+1)] = dep_calc_df.apply(lambda x: x['NBV_'+str(i)] if x['NBV_'+str(i)]<x['Depr_'+str(i+1)] == 1 else x['Depr_' + str(i+1)], axis=1)
后面stack结构转换参考之前的文章,当然你也可以选择pd.melt/pd.pivot兄弟。
而今天谈到的坑,就是在列计算时候遇到的。'axis=1’其实就是逐行计算,而且还有判断的逻辑,60万行的情况,差不多这一套下来就好几分钟。
测试
我们可以找一组作对比。控制变量,我们用同一组数据,干同一件事:
算法A代码:
start = time.time()
calc_df_test['Col_C'] = calc_df_test.apply(lambda x: x['Col_A']+x['Col_B'], axis=1)
end = time.time()
print('测试耗时:'+str(end-start)+'秒')
算法B代码:
start = time.time()
calc_df_test['Col_C'] = calc_df_test.eval('Col_A+Col_B')
end = time.time()
print('测试耗时:'+str(end-start)+'秒')
算法C代码:
start = time.time()
calc_df_test['Col_C'] = calc_df_test['Col_A'].values + calc_df_test['Col_B'].values
#这个.values要不要无所谓
end = time.time()
print('测试耗时:'+str(end-start)+'秒')
三种算法运算速度如下:
因此我们有一种想法,将所有针对元素级别的遍历判断,都转化为ndarray之间的数学计算,这样时间效率至少提升几十倍。
series, ndarray之间的数学计算,替代元素遍历判断。
那么如何将上述的逻辑判断,转化为纯数学计算呢?
换句话说,我们如何把判断这件事情,做成数学运算?那么,我们先来想一个问题,如何判断位置?对了,位置就是向量坐标对吧。那么我们可以参考这样的思想,去把我们用于判断位置的逻辑,替换成向量坐标。
举两个栗子:
栗子1:单组向量(一层批判断)参与计算。
上图可以看出,其实我们只需要找到那一个需要“修正”的月份,将它改成正确的金额就可以了。换句话也就是锁定那个位置,再去替换,而这一切需要避开axis=1的元素遍历。
我们可以这样:
如下图用制作好的向量组(可以选择用作差的金额 ),乘以作差的数值,再把结果和原始数据相加,这样刚好被锁定的“位置”那个月的数变成了我们判定后想要的数。全程都是运算没有针对元素判断循环。
代码案例如下:全部都是数学运算替代元素级别循环,但是注意“!!”叹号下面的还是用了if, else的判断,之后我们会讲如何用纯数学方法替换它,以及,为什么在这里不做替换。
##单独计算好NBV_1和Depr1的数据
for y in range(1, 52):
df_base_output_3['Rem' + str(y + 1)] = df_base_output_3['NBV_M' + str(y)] - df_base_output_3[
'Depr_M' + str(y + 1)]
#!!!!!
df_base_output_3['Rem' + str(y + 1)] = df_base_output_3['Rem' + str(y + 1)].apply(
lambda x: 0 if x >= 0 else x)
df_base_output_3['Depr_M' + str(y + 1)] = df_base_output_3['Depr_M' + str(y + 1)] + df_base_output_3[
'Rem' + str(y + 1)]
栗子2:多组向量(多层判断)
判断的逻辑越多,我们需要的向量组也越多。比如,下面这条逻辑:判断某一家店的开店时间如果小于当年则为0,等于当年而小于当月则为0,其他情况为两个时间点的相隔月数。
同样的理论,二维数组里找下图红线范围以上的坐标,都满足以上的筛选判断要求。以此类推,那么三位向量空间中则是找满足两点距离的特定坐标。
根据数量级大小,选择适合的算法
刚才说到的:
df_base_output_3['Rem' + str(y + 1)] = df_base_output_3['Rem' + str(y + 1)].apply(
lambda x: 0 if x >= 0 else x)
这段代码,我们依然用了判断。但是可以接受,为什么呢?因为他是针对单独series,进行判断,x此时代表series or ndarray,而不是dataframe。不过,我们依然可以选择用纯数学计算来替换掉这层series的判断。
我们来像一件事,这句话的本质就是在制作向量组,输入所有正数和0,返还0;输入负数返还1。其实有很多种方法可以到达这个目的,下面举个例子:
##方法1强烈不推荐,但是稳定性好!
def Num1(num):
value = []
num=math.floor(num)+2
for i in range(2, num + 1):
for j in range(2, i):
if i % j == 0:
break
else:
value.append(i)
alist=value+[3]
value=alist[0]-2
return value
##方法2可以尝试
def Num2(num):
# num=math.floor(num)+0.9
num=num+0.0000001
num=((num/abs(num))-1)/(-2)
return num
方法1强烈不推荐,本质意义就是调整输入数字的质因数分解后的第一个数(2或者-2).但是有点稳定性好,不过速度一定很慢。
方法2貌似不错,本质思想就是输入的这个数字,除以它自身的绝对值,要么是1要么是-1,在这基础上减去1则为0或者-2,再除以-2那么就是0或者1.
唯一的顾略在于如果输入0的话,0不能为分母,那么我们调整输入值,让它+0.0000001,这个调整的数字必须保证原始NBV-dep不能出现绝对值比这个数更小的存在。否则可能就错了
这时候看,一般数据库后台都会设置类似decimal(20,6),看好你字段的小数限制.如上述设置,就可以放心的设置调整的参数,不会有超过6位的情况出现.
我们来看一看性能表现吧:
原始判断方法:
df['A']=df['Cost'].apply(lambda x: 0 if x>=0 else 1)
纯数学运算代替判断:
def Num2(num):
# num=math.floor(num)+0.9
num=num+0.0000001
num=((num/abs(num))-1)/(-2)
return num
df['A']=df['Cost'].apply(lambda x: Num2(x))
测试结果如下:
不难看出再数据量小的时候,原始判断更优。但是当数据量逐渐变大的时候,纯数学计算的方式更快。当然中受限于你的算法。因此我们可以再实际操作中,去尝试找到特定场景下那个数据量的阈值,从而选择最优的算法。
那么如果还想优化这段算法,换一种数学方式去替换判断的方法,还有别的方法嘛?
肯定有的。比如,我发现对数函数可以用以下哦。 因为log以a为底1的对数为0(a为常数) 恒过点(1,0)。
那么我们代码可以改成如下,似乎更简单:
try:
df['A']=df['B'].apply(lambda x: math.log(1,x))
except:
ValueError
为什么要try?因为B有可能是负数…这个try还是可以经常用到的。
有时候人觉得复杂的,计算机不一定觉得复杂,相反它可能会认为一层层判断很费脑。这是一个观念的转变。
*补充:感谢周老师~!
上面关于制作向量组那些呼呼轩轩的操作,什么绝对值调整,质因数,对数函数,真的很尬。周老师说他做只有一句话:
(df['A']<0)*1
没错,小于0的为1,其他为0。所以我总要反思一个问题,py是为了减轻工作员的工作量,而不是让你去创造。能有内置就尽量用内置,不知道就去查!py的内置语法底层都是C语言在执行,比我们快的多的多。比如,之前我还总去写那些排序算法,冒泡排序,希尔排序,插入排序。。。。其实py就一句话sorted()快而精准。。。。感慨下!