In [1]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
import seaborn as sns
sns.set()
import os
根據數據的維度,Pandas可將資料存成序列(series) 或資料表(data frame)。以下將各別針對序列和資料表做介紹。
可將Python字典存成Pandas序列。字典格式為 {索引1:索引值1, 索引2:索引值2, 索引3:索引值3,...}。
In [2]:
adjs={'affordable': 1,
'comfortable': 3,
'comparable': 1,
'different': 1,
'disappointed': 1,
'fantastic': 1,
'good': 8,
'great': 15}
In [3]:
ser=pd.Series(adjs)
以[ ]方法取出序列中的前三筆資料
In [4]:
ser[:3]
Out[4]:
In [5]:
ser.index
Out[5]:
以[索引名稱]可提取和該索引名稱相應的索引值。
In [6]:
ser['comfortable']
Out[6]:
In [7]:
for ind in ser.index:
print(ind,ser[ind])
ser[索引名稱] 等同於使用 ser.loc[索引名稱]:
In [8]:
for ind in ser.index:
print(ind,ser.loc[ind])
以iloc[索引排序]可提取對應該索引排序的索引值。索引排序是整數,代表該筆資料在序列內的順位。
In [9]:
for i,ind in enumerate(ser.index):
print(ind,ser[ind])
print(ind,ser.iloc[i])
print('------------')
In [10]:
ser
Out[10]:
利用apply()將序列內的每個值+1。
In [11]:
ser.apply(lambda x:x+1)
Out[11]:
In [12]:
ser.plot.bar()
Out[12]:
以上等同於:
In [13]:
ser.plot(kind='bar')
Out[13]:
我們亦可不提供索引,直接輸入一個無索引, 非字典形式的序列給Pandas:
In [14]:
ser=pd.Series(np.random.normal(0,1,1000))
In [15]:
ser[:5]
Out[15]:
由於並無提供索引,索引會以流水號從0開始自動產生。
In [16]:
ser.shape
Out[16]:
In [17]:
ser.plot.box()
Out[17]:
以上結果等同於:
In [18]:
ser.plot(kind='box')
Out[18]:
我們先前使用了 pd.Series(序列) 讓序列自動產生由0開始遞增的索引。而若是我們想要自己提供索引,我們仍然可使用 pd.Series(序列,索引序列) 建立自帶索引的序列。
In [19]:
ser=pd.Series(np.random.normal(0,1,1000), index=np.random.choice(['A','B'],1000))
In [20]:
ser.head(5)
Out[20]:
複習
我們可以提供{行名稱1:行序列1,行名稱2:行序列2,行名稱3:行序列3,...}給pandas.DataFrame()建構資料表。
In [21]:
a=[1,2,3]*2
b=[1, 2, 3, 10, 20, 30]
c=np.array(b)*2
In [22]:
df=pd.DataFrame({'col1':a,'col2':b,'col3':c})
In [23]:
df
Out[23]:
In [24]:
df['col2']
Out[24]:
用loc或iloc時機:當你不只想選取欄位,也想選取特定索引範圍時。
例:選出索引0,1,2;列為col2的資料
In [25]:
df.loc[0:2,'col2']
Out[25]:
以上範例索引為數值,因此可以用$0:2$的方式,選出索引由$0$至$2$ ($0,1,2$) 的資料列。
例:選出索引為0,1,2, 列為col1和col2的資料
In [26]:
df.loc[0:2,['col1','col2']]
Out[26]:
In [27]:
df
Out[27]:
選取位於'col'行中等於1的列
In [28]:
df['col1']==1
Out[28]:
In [29]:
df[df['col1']==1]
Out[29]:
先依條件df['col1']==1選定資料,再指定要選取的欄位是'col2'
In [30]:
df.loc[df['col1']==1,'col2']
Out[30]:
亦可使用iloc(索引排序,行排序),根據索引排序和行排序來選取資料
In [31]:
df
Out[31]:
In [32]:
df.iloc[0:2,1]
Out[32]:
In [33]:
def filePathsGen(rootPath):
paths=[]
dirs=[]
for dirPath,dirNames,fileNames in os.walk(rootPath):
for fileName in fileNames:
fullPath=os.path.join(dirPath,fileName)
paths.append((int(dirPath[len(rootPath) ]),fullPath))
dirs.append(dirNames)
return dirs,paths
dirs,paths=filePathsGen('mnist/') #載入圖片路徑
dfPath=pd.DataFrame(paths,columns=['class','path']) #圖片路徑存成Pandas資料表
dfPath.head(5) # 看資料表前5個row
Out[33]:
In [ ]:
# 完成以下程式碼:
dfPath[...]
Pandas 可做各種等同於SQL查詢的處理,詳見Pandas和SQL指令的比較:
http://pandas.pydata.org/pandas-docs/stable/comparison_with_sql.html
現在,我們首先來介紹,SQL查詢指令中常見的groupby,於pandas是該怎麼做呢?
In [35]:
df
Out[35]:
以行'col1'來做groupby
In [36]:
grouped=df.groupby('col1')
In [37]:
grouped.indices
Out[37]:
現在,grouped這個物件是一個群集,裡面有三個群。其中,群為1的有索引是0和索引是3的這兩列,群為2的有索引是1和索引是4的這兩列,群為3的有索引是2和索引是5的這兩列。
In [38]:
for name,group in grouped:
print(name)
print(group)
print('--------------')
直接以* 將群集展開
In [39]:
print(*grouped)
In [40]:
grouped.get_group(2)
Out[40]:
In [41]:
grouped.sum()
Out[41]:
In [42]:
grouped.describe()
Out[42]:
現在,我們來製作一個亂數產生的時間序列。我們將利用它來學習,於群聚(groupby)之後,如何將得到的群集做聚合 (aggregate),轉換 (transform)或過濾 (filter),進而得出想要的結果。
以下我們要亂數產生一個時間序列的範例資料。首先,先產生$365\times2$個日期
In [43]:
dates = pd.date_range('2017/1/1', periods=365*2,freq='D')
建立一個時間序列,以剛剛產生的日期當做索引。序列的前365個亂數是由常態分佈$N(\mu=0,\sigma=1)$抽取出來的樣本,而後365個亂數則是由常態分佈$N(\mu=6,\sigma=1)$抽取出來的樣本。
以下,我們以水平方向堆疊(horizontal stack)兩個不同分佈的亂數序列,將其輸入給pd.Series()。
In [44]:
dat=pd.Series(np.hstack( (np.random.normal(0,1,365),np.random.normal(6,1,365) )) ,dates)
In [45]:
dat[:5]
Out[45]:
接著我們將序列以年來分群。
In [46]:
grouped=pd.Series(dat).groupby(lambda x:x.year)
In [47]:
grouped.indices
Out[47]:
顯而易見,因2017和2018年的資料是不同的亂數分佈產生的,所以畫出來這兩年的資料是分開的。
In [48]:
for name,group in grouped:
group.plot(label=name,legend=True)
畫直方圖(histogram),可發現兩組數據的確類似常態分佈,一個中心約在x=0處,另一個中心約在x=6處。
法一
In [49]:
grouped.plot.hist(15)
Out[49]:
法二(有畫出兩組資料的標籤)
In [50]:
for name,group in grouped:
group.plot.hist(15,label=name,legend=True)
盒鬚圖(boxplot)
In [51]:
fig,axes=plt.subplots(1,2)
for idx,(name,group) in enumerate(grouped):
group.plot(kind='box', label=name,ax=axes[idx])
接著我們想畫盒鬚圖(boxplot)。在這裡遇到一個問題,也就是boxplot()這個方法只有物件是DataFrame才有,物件是Series尚無此方法。因此我們只好將序列(Series)轉成DataFrame。
註:我們想用boxplot這個方法,因為它可以寫一行就幫我們自動分群,畫出盒鬚圖。
In [52]:
dat.name='random variables'
將序列dat轉成DataFrame並且將日期索引變成行。
In [53]:
datNew=dat.to_frame().reset_index()
將日期欄位取年的部份,存到一個欄位叫做'year'。
In [54]:
datNew['year']=datNew['index'].apply(lambda x:x.year)
將原先是索引的日期欄位刪除。
In [55]:
del datNew['index']
最終我們產生了新的資料表,可以用'year'這個欄位來做groupby。
In [56]:
datNew[:5]
Out[56]:
最後以boxplot()畫盒鬚圖並以年做groupby。
In [57]:
datNew.boxplot(by='year')
Out[57]:
從盒鬚圖可看出,2017年的亂數資料平均值靠近0, 而2018年的亂數資料平均值靠近6。這符合我們的預期。
用seaborn畫可能會漂亮些:
In [58]:
sns.boxplot(data=datNew,x='year',y='random variables',width=0.3)
Out[58]:
In [59]:
dat[:5]
Out[59]:
In [60]:
grouped=dat.groupby(lambda x:x.year)
In [61]:
transformed = grouped.transform(lambda x: (x - x.mean()) / x.std())
In [62]:
transformed[:5]
Out[62]:
In [63]:
len(transformed)
Out[63]:
畫圖比較轉換前和轉換後的序列
In [64]:
compare = pd.DataFrame({'before transformation': dat, 'after transformation': transformed})
In [65]:
compare.plot()
Out[65]:
轉換後,2018年的資料和2017年的資料有相同的分佈
將原資料做groupby
In [66]:
groups=dat.groupby(lambda x:x.year)
將轉換後資料做groupby
In [67]:
groupsTrans=transformed.groupby(lambda x:x.year)
計算原資料各群平均
In [68]:
groups.agg(np.mean)
Out[68]:
計算原資料各群標準差
In [69]:
groups.agg(np.std)
Out[69]:
計算轉換後資料各群平均
In [70]:
groupsTrans.agg(np.mean)
Out[70]:
計算轉換後資料各群標準差
In [71]:
groupsTrans.agg(np.std)
Out[71]:
例如說,我們想找出平均數是小於5的群集。由先前所畫的圖,或是由數據產生的方式,我們知道2017年這個群,平均數是小於五,因此我們可使用np.abs(x.mean())<5 這個條件過濾出2017年的群集資料。
In [72]:
filtered=groups.filter(lambda x: np.abs(x.mean())<5)
In [73]:
len(filtered)
Out[73]:
In [74]:
filtered[:5]
Out[74]:
相同的,我們知道2018年這個群,平均數是大於五,因此我們可使用np.abs(x.mean())>5 這個條件過濾出2018年的群集資料。
In [75]:
filtered=groups.filter(lambda x: np.abs(x.mean())>5)
In [76]:
len(filtered)
Out[76]:
In [77]:
filtered[:5]
Out[77]:
使用np.abs(x.mean())>6 將找不到任何群集的資料。
In [78]:
filtered=groups.filter(lambda x: np.abs(x.mean())>6)
In [79]:
len(filtered)
Out[79]:
使用np.abs(x.mean())<6 將得到屬於任何群集的資料。
In [80]:
filtered=groups.filter(lambda x: np.abs(x.mean())<6)
In [81]:
len(filtered)
Out[81]:
In [82]:
filtered[:5]
Out[82]:
In [83]:
filtered[-1-5:-1]
Out[83]:
In [84]:
dat=pd.Series(np.random.normal(0,1,6))
In [85]:
dat
Out[85]:
將索引2的值換成NaN
In [86]:
dat[2]=np.NAN
In [87]:
dat
Out[87]:
將序列所有值往後挪動(shift)一格。此舉會導致最前面的數變成NaN。
In [88]:
shifted=dat.shift(1)
In [89]:
shifted
Out[89]:
用NaN後的數來填補該NaN (backward fill)
In [90]:
shifted.bfill()
Out[90]:
用NaN前的數來填補該NaN (forward fill)
In [91]:
shifted.ffill()
Out[91]:
用0來填補該NaN
In [92]:
shifted.fillna(0)
Out[92]:
In [93]:
shifted.mean()
Out[93]:
用平均數來填補該NaN
In [94]:
shifted.fillna( shifted.mean() )
Out[94]:
將序列中的NaN丟棄(drop)
In [95]:
shifted.dropna()
Out[95]:
In [3]:
def filePathsGen(rootPath):
paths=[]
dirs=[]
for dirPath,dirNames,fileNames in os.walk(rootPath):
for fileName in fileNames:
fullPath=os.path.join(dirPath,fileName)
paths.append((int(dirPath[len(rootPath) ]),fullPath))
dirs.append(dirNames)
return dirs,paths
dirs,paths=filePathsGen('mnist/') #載入圖片路徑
dfPath=pd.DataFrame(paths,columns=['class','path']) #圖片路徑存成Pandas資料表
dfPath.head(5) # 看資料表前5個row
Out[3]:
In [ ]:
# 完成以下程式碼:
...
...
groups.count()
In [ ]: