目录:
1. 前言
2. 栈机构的概览
3. 应用案例及分析
4. 局限性
1.前言
前段时间写了几个迭代的脚本,有感而发,于是打算系统的记录一下关于这方面的思考。顺便做一个中二的搬运工~
迭代算法我们经常会遇到,它本身并不难,通俗讲就是重复反馈过程。下面是一个简单的Java迭代,每个元素+1,重复反馈。
/* 建立一个数组 */
int[] integers = {1, 2, 3, 4, 5};
/* 开始遍历 */
for (int j = 0; j < integers.length; j++) {
int i = integers[j]+1;
System.out.println(i);
}
无论什么情况,总会有最直接的办法,遍历每一个元素(类似穷举法)。但是当我们对时间复杂度O有所要求时候,就会去想办法加快这一进程的运算效率。下面讲述的栈结构是我自己比较习惯运用,在迭代处理中相对效率较高的方法。当然会有其他更多种方法,希望多多留言大家一起进步!
2. 栈结构的概述
上面链接是比较详细的讲解,作为搬运工的我,来画画重点~
1. 理解: 这就好比食堂大妈收盘子,她不会一个一个收,而是落在一起,一堆一堆收,快!(当然有局限性,最后会讲到。)
2. 它是一种常见的受限的线性结构。什么限制呢?只允许在一端插入和删除数据。
这个也好理解,一堆100个盘子落在一起,在中间第38个插一个盘子...此时食堂大妈表情凝重!大喊"臣妾做不到啊!"
3. LIFO(last in first out)后进先出的数据结构
听起来很像会计里的存货后进先出法,先进先出法....体现了谨慎性原则。好的吧,这里木有谨慎性原则,不像会计LIFO、FIFO,在期间中不同的采购价格变化,操控存货账面价值。这里只是栈的特点,就像子弹上膛123,打出来永远是321。
(后面有案例给大家展示)
4. 写python的同学们,庆幸没有指针!所以,后面案例展示大家怎样打出132,231, 213...的子弹顺序
3. 应用案例分析(附代码)
此文中案例,以财务中计算固定资产Depreciation与NBV为例。当然其实运用的地方有很多。
在开始前,先附上一个从MySql查询,读取,存入,删除数据的方法,自己搬运整理的简化版,玩玩就好。具体如何配置本地电脑装Mysql,推荐wampserver64, 上学时候总用,还附带PHPadmin,一键式安装,安利下~!
"""
MySQL存取过程
"""
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)
#写入数据到数据库中
def InsertData(dataframe, table):
if not dataframe.empty:
host = 'XXXX'
port = 3306
user = 'Taylor'
password = '123'
schema = 'test'
paramdict = {'dbuser': user,'dbpwd':password, 'dbip':host, 'dbport':port, 'dbname': schema}
connect_info = 'mysql+pymysql://{}:{}@{}:{}/{}?charset=utf8'.format(paramdict['dbuser'], paramdict['dbpwd'], paramdict['dbip'], paramdict['dbport'], paramdict['dbname']) # 1
engine = create_engine(connect_info)
con = engine.connect()
dataframe.to_sql(name=table, con=con, if_exists='append', index=False)
engine.dispose()
##删除数据
def Delete_From(Table=None):
#打开数据库链接
db = pymysql.connect('XXXX', 'Taylor', '123', 'test')
# 使用cursor()方法获取操作游标
cursor = db.cursor()
# SQL语句更新数据
sql = """DELETE FROM %s"""%(Table)
try:
# 执行SQL语句
cursor.execute(sql)
# 提交到数据库执行
db.commit()
print("删除数据成功")
except Exception as e:
print("删除数据失败:case%s"%e)
#发生错误时回滚
db.rollback()
finally:
# 关闭游标连接
cursor.close()
# 关闭数据库连接
db.close()
"""
(*)查询,插入,删除
"""
##查询读取数据
sql1 = "Select * from I_love_taylor"
df_1 = MysqlHelper('XXX', 'Taylor', '123', 3306, 'test').all(sql1)
##插入算好的数据
InsertData(df_Final, 'I_love_taylor')
##删除数据
Delete_From(Table='I_love_taylor')
案例要求1:
截止至2016年6月30日,某企业有10万条固定资产,要求在下一年期间,每个月对其计提折旧,并计算出当月的NBV, 采用直线法。
已知条件:每条资产的(1)固定资产原值;(2)预计可使用寿命;(3)剩余可使用寿命;(4)2016年6月30日NBV。
直线法公式:Dep = Cost/Total Life
预计净残值:假设为0
为了能够更加清晰的展示,这是最简单的一种情况,现实中我们还会遇到加速折旧,会遇到净残值不同的组合,遇到新增/减少固定资产的情况,但是那些只需套用逻辑筛选即可。
传统Pythonic做法
这是最直接,但是也是最不推荐的做法,因为很慢。
将所有数据装进一个dataframe, 然后复制出所要计算的期间,遍历每一条资产的每一个期间,分别计算其折旧和NBV。这样的方法稳定,但是效率过低。
读取的源数据:
for index in df_1.index:
df_temp=df_1.iloc[index].copy()
...........
后面不用写了,谨记永远不要在dataframe用loc遍历,Loc的本质是location, 相当耗时,运气好,电脑可能不会崩。如果没有特别需要,要把某个指揪出来,不要用Loc。即便要用,我们最好要是通过做字典dic,或者json的方式。
升级版pandas内置函数
首先,每一条资产copy,复制成需要计算的12个月期间,再迭代计算。
##添加n列:将period放在列上
class add_row_col():
def __init__(self, dataframe, new_col_horizontal, new_col_vertical, counts=None):
self.d = dataframe
self.h = new_col_horizontal
self.n = new_col_vertical
self.c = counts
# 创建一个p1---p?的字符串,逗号间隔。
def create_str(self):
P_list = list()
for i in range(1, self.c + 1):
N = str(str(self.h)) + str(i)
P_list.append(N)
PStr = ','.join(P_list)
return PStr
# 创建P1-P12的list,增加每个FA的维度。(12*N行)
def create_matrix(self):
P_list = list()
for i in range(1, self.c + 1):
N = str(str(self.h)) + str(i)
P_list.append(N)
return [a for a in P_list]
# 增加维度!(复制N行)
def multi_dimension(self):
str_list = self.create_str()
df_1 = self.d
df_1.reset_index(drop=True, inplace=True)
df_1[str(self.n)] = str(str_list)
df_temp = df_1[str(self.n)].str.split(',', expand=True)
df_temp = df_temp.stack()
df_temp = df_temp.reset_index(level=1, drop=True)
df_temp = pd.DataFrame(df_temp, columns=[str(self.n)])
df_2 = df_1.drop([str(self.n)], axis=1).join(df_temp)
df_2[str(self.n)] = df_2[str(self.n)].apply(lambda x: x.replace("'", ""))
df_2.reset_index(drop=True, inplace=True)
return df_2
# 增加维度!(复制N列)
def create_col(self):
df = self.d.copy()
matrix = self.create_matrix()
for g in matrix:
df[g] = 0
return df
df_2=add_row_col(df_1, 'Period', 'Period_list', counts=12).multi_dimension()
通过上述方法可以得到以下新dataframe:
题外话,如果说我不想Period1,2,3,4,5,6....想要以具体时间来判定怎么办?那就需要加一个新字段规定起始日期2016年7月1日,时间戳,定义结尾日期2017年6月30日。通过以下算法,来处理,即计算间隔。以下不多做说明。
def get_month_range(start_day,end_day):
months = (end_day.year - start_day.year)*12 + end_day.month - start_day.month
month_range = ['%s-%s-%s'%(start_day.year + mon//12,mon%12+1,'1') for mon in range(start_day.month-1,start_day.month + months)]
return month_range
那么传统方法,核心的迭代就是以下算法:
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
这是一套比较通用的算法,做一个图比较好理解。
它其实就是每个资产12期规定为一个模块,规定一个起始期限如7,10000个资产对应10000个模块,每个模块同时迭代,迭代多少以count虚数为定义。当时写完这段,觉得大事可定,轻松了很多。直到跑全量数据后,才意识到这个方法依旧很慢。慢就慢在依旧还要在自己定义的模块里通过虚数遍历。
栈结构stack
下面重点介绍它。stack()会很快,多快呢?线性效率,每增加10倍数据,时间只增加8倍。前两种方法算10000条记录分别是10分钟,1分钟。栈结构只要4秒。
核心思想:将需要计算的都放在列,因此需要上面写的那个class:add_row_col 创建多个列,对列循环运用eval或者apply(lambda公式)。在将每一个结算好的结果,当作一落落得盘子,放在原始数据里。
df_1['Dep_Amount']=df_1.eval('Cost/Total_Life')
df_2 = add_row_col(df_1, 'Depr_M', 'Period_list', counts=12).create_col()
df_2['Remaining_Life'] = df_2['Remaining_Life'].astype(int)
##判断remaining month 得到default dep
##此处需要断定scenario 对应的remaining month
for i in range(1, 13):
df_2['Depr_M'+str(i)] = df_2.apply(lambda x: 0 if x['Remaining_Life']-i <0 else x['Dep_Amount'], axis=1)
df_3=add_row_col(df_2, 'NBV_M', 'Period_list', counts=12).create_col()
df_3['NBV_M1']=df_3['NBV_Beginning']-df_3['Depr_M1']
for i in range(1, 12):
df_3['NBV_M'+str(i+1)]=df_3.apply(lambda x: x['NBV_M'+str(i)]-x['Depr_M'+str(i+1)], axis=1)
开始装盘子!!
##开始装盘子
df_temp_dep=df_3[[所有dep]]
df_temp_nbv=df_3[[所有nbv]]
df_temp_dep=df_temp_dep.stack()
df_temp_nbv=df_temp_nbv.stack()
df_temp_dep=df_temp_dep.reset_index(level=1, drop=True)
df_temp_nbv=df_temp_nbv.reset_index(level=1, drop=True)
df_temp_dep=pd.DataFrame(df_temp_dep, columns=['dep'])
df_temp_nbv=pd.DataFrame(df_temp_nbv, columns=['nbv'])
df_1=df_1.drop(columns=[没用得新加字段])
df_1=df_1.join(df_temp_dep)
df_1['nbv']=..........
##注意,这里join默认按照索引join。
df_1。reset_index(drop=True,inplace=True)
大致核心就是这样,具体有很多细节,可以自行修改。stack可以做很多,只要是类似beginning-thisamount=ending得滚动迭代,都可以用栈结构。
4. 局限性
最后讲讲局限性。就像之前提到得,它是后进先出,受限得线性结构。那么就会导致,无法个别差异计算。未来将这些落起来得盘子,也只能按顺序摆放。如果是做数据模型得同学,前期一定做好聚类,否则就杯具了。其他用途得,如只是的单纯得财务,人力管理,为了想让它打出231, 132等不同顺序得子弹,我们需要再做一个引用多维度得包。通过唯一主键,对其进行重新排序。另外一个问题,就是它占用空间较大,且对索引得排列要求较高。这也就意味着,一旦你发现数据没有算全,那就从头开始落吧!
And......
如果有何错误,请大家多多指正。如果有其他方法,希望分享,共同进步~!