算法优化---向量数组计算替代元素级别判断

目录

前言

接着之前降到运行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()快而精准。。。。感慨下!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值