目錄:
1. 前言
2. 棧機(jī)構(gòu)的概覽
3. 應(yīng)用案例及分析
4. 局限性
1.前言
前段時間寫了幾個迭代的腳本,有感而發(fā),于是打算系統(tǒng)的記錄一下關(guān)于這方面的思考。順便做一個中二的搬運(yùn)工~
迭代算法我們經(jīng)常會遇到,它本身并不難,通俗講就是重復(fù)反饋過程。下面是一個簡單的Java迭代,每個元素+1,重復(fù)反饋。
/* 建立一個數(shù)組 */
int[] integers = {1, 2, 3, 4, 5};
/* 開始遍歷 */
for (int j = 0; j < integers.length; j++) {
int i = integers[j]+1;
System.out.println(i);
}
無論什么情況,總會有最直接的辦法,遍歷每一個元素(類似窮舉法)。但是當(dāng)我們對時間復(fù)雜度O有所要求時候,就會去想辦法加快這一進(jìn)程的運(yùn)算效率。下面講述的棧結(jié)構(gòu)是我自己比較習(xí)慣運(yùn)用,在迭代處理中相對效率較高的方法。當(dāng)然會有其他更多種方法,希望多多留言大家一起進(jìn)步!
2. 棧結(jié)構(gòu)的概述
上面鏈接是比較詳細(xì)的講解,作為搬運(yùn)工的我,來畫畫重點~
1. 理解: 這就好比食堂大媽收盤子,她不會一個一個收,而是落在一起,一堆一堆收,快?。ó?dāng)然有局限性,最后會講到。)
2. 它是一種常見的受限的線性結(jié)構(gòu)。什么限制呢?只允許在一端插入和刪除數(shù)據(jù)。
這個也好理解,一堆100個盤子落在一起,在中間第38個插一個盤子...此時食堂大媽表情凝重!大喊"臣妾做不到??!"
3. LIFO(last in first out)后進(jìn)先出的數(shù)據(jù)結(jié)構(gòu)
聽起來很像會計里的存貨后進(jìn)先出法,先進(jìn)先出法....體現(xiàn)了謹(jǐn)慎性原則。好的吧,這里木有謹(jǐn)慎性原則,不像會計LIFO、FIFO,在期間中不同的采購價格變化,操控存貨賬面價值。這里只是棧的特點,就像子彈上膛123,打出來永遠(yuǎn)是321。
(后面有案例給大家展示)
4. 寫python的同學(xué)們,慶幸沒有指針!所以,后面案例展示大家怎樣打出132,231, 213...的子彈順序
3. 應(yīng)用案例分析(附代碼)
此文中案例,以財務(wù)中計算固定資產(chǎn)Depreciation與NBV為例。當(dāng)然其實運(yùn)用的地方有很多。
在開始前,先附上一個從MySql查詢,讀取,存入,刪除數(shù)據(jù)的方法,自己搬運(yùn)整理的簡化版,玩玩就好。具體如何配置本地電腦裝Mysql,推薦wampserver64, 上學(xué)時候總用,還附帶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)
#寫入數(shù)據(jù)到數(shù)據(jù)庫中
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()
##刪除數(shù)據(jù)
def Delete_From(Table=None):
#打開數(shù)據(jù)庫鏈接
db = pymysql.connect('XXXX', 'Taylor', '123', 'test')
# 使用cursor()方法獲取操作游標(biāo)
cursor = db.cursor()
# SQL語句更新數(shù)據(jù)
sql = """DELETE FROM %s"""%(Table)
try:
# 執(zhí)行SQL語句
cursor.execute(sql)
# 提交到數(shù)據(jù)庫執(zhí)行
db.commit()
print("刪除數(shù)據(jù)成功")
except Exception as e:
print("刪除數(shù)據(jù)失?。篶ase%s"%e)
#發(fā)生錯誤時回滾
db.rollback()
finally:
# 關(guān)閉游標(biāo)連接
cursor.close()
# 關(guān)閉數(shù)據(jù)庫連接
db.close()
"""
(*)查詢,插入,刪除
"""
##查詢讀取數(shù)據(jù)
sql1 = "Select * from I_love_taylor"
df_1 = MysqlHelper('XXX', 'Taylor', '123', 3306, 'test').all(sql1)
##插入算好的數(shù)據(jù)
InsertData(df_Final, 'I_love_taylor')
##刪除數(shù)據(jù)
Delete_From(Table='I_love_taylor')
案例要求1:
截止至2016年6月30日,某企業(yè)有10萬條固定資產(chǎn),要求在下一年期間,每個月對其計提折舊,并計算出當(dāng)月的NBV, 采用直線法。
已知條件:每條資產(chǎn)的(1)固定資產(chǎn)原值;(2)預(yù)計可使用壽命;(3)剩余可使用壽命;(4)2016年6月30日NBV。
直線法公式:Dep = Cost/Total Life
預(yù)計凈殘值:假設(shè)為0
為了能夠更加清晰的展示,這是最簡單的一種情況,現(xiàn)實中我們還會遇到加速折舊,會遇到凈殘值不同的組合,遇到新增/減少固定資產(chǎn)的情況,但是那些只需套用邏輯篩選即可。
傳統(tǒng)Pythonic做法
這是最直接,但是也是最不推薦的做法,因為很慢。
將所有數(shù)據(jù)裝進(jìn)一個dataframe, 然后復(fù)制出所要計算的期間,遍歷每一條資產(chǎn)的每一個期間,分別計算其折舊和NBV。這樣的方法穩(wěn)定,但是效率過低。
讀取的源數(shù)據(jù):
for index in df_1.index:
df_temp=df_1.iloc[index].copy()
...........
后面不用寫了,謹(jǐn)記永遠(yuǎn)不要在dataframe用loc遍歷,Loc的本質(zhì)是location, 相當(dāng)耗時,運(yùn)氣好,電腦可能不會崩。如果沒有特別需要,要把某個指揪出來,不要用Loc。即便要用,我們最好要是通過做字典dic,或者json的方式。
升級版pandas內(nèi)置函數(shù)
首先,每一條資產(chǎn)copy,復(fù)制成需要計算的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
# 創(chuàng)建一個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
# 創(chuàng)建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]
# 增加維度?。◤?fù)制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
# 增加維度?。◤?fù)制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....想要以具體時間來判定怎么辦?那就需要加一個新字段規(guī)定起始日期2016年7月1日,時間戳,定義結(jié)尾日期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
那么傳統(tǒng)方法,核心的迭代就是以下算法:
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
這是一套比較通用的算法,做一個圖比較好理解。
它其實就是每個資產(chǎn)12期規(guī)定為一個模塊,規(guī)定一個起始期限如7,10000個資產(chǎn)對應(yīng)10000個模塊,每個模塊同時迭代,迭代多少以count虛數(shù)為定義。當(dāng)時寫完這段,覺得大事可定,輕松了很多。直到跑全量數(shù)據(jù)后,才意識到這個方法依舊很慢。慢就慢在依舊還要在自己定義的模塊里通過虛數(shù)遍歷。
棧結(jié)構(gòu)stack
下面重點介紹它。stack()會很快,多快呢?線性效率,每增加10倍數(shù)據(jù),時間只增加8倍。前兩種方法算10000條記錄分別是10分鐘,1分鐘。棧結(jié)構(gòu)只要4秒。
核心思想:將需要計算的都放在列,因此需要上面寫的那個class:add_row_col 創(chuàng)建多個列,對列循環(huán)運(yùn)用eval或者apply(lambda公式)。在將每一個結(jié)算好的結(jié)果,當(dāng)作一落落得盤子,放在原始數(shù)據(jù)里。
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 對應(yīng)的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默認(rèn)按照索引join。
df_1。reset_index(drop=True,inplace=True)
大致核心就是這樣,具體有很多細(xì)節(jié),可以自行修改。stack可以做很多,只要是類似beginning-thisamount=ending得滾動迭代,都可以用棧結(jié)構(gòu)。
4. 局限性
最后講講局限性。就像之前提到得,它是后進(jìn)先出,受限得線性結(jié)構(gòu)。那么就會導(dǎo)致,無法個別差異計算。未來將這些落起來得盤子,也只能按順序擺放。如果是做數(shù)據(jù)模型得同學(xué),前期一定做好聚類,否則就杯具了。其他用途得,如只是的單純得財務(wù),人力管理,為了想讓它打出231, 132等不同順序得子彈,我們需要再做一個引用多維度得包。通過唯一主鍵,對其進(jìn)行重新排序。另外一個問題,就是它占用空間較大,且對索引得排列要求較高。這也就意味著,一旦你發(fā)現(xiàn)數(shù)據(jù)沒有算全,那就從頭開始落吧!
And......
如果有何錯誤,請大家多多指正。如果有其他方法,希望分享,共同進(jìn)步~!