Python資料處理庫pandas進階教程
在前面一篇文章中,我們對pandas做了一些入門介紹。本文是它的進階篇。在這篇文章中,我們會講解一些更深入的知識。
前言
本文緊接著前一篇的入門教程,會介紹一些關於pandas的進階知識。建議讀者在閱讀本文之前先看完pandas入門教程。
資料訪問
在入門教程中,我們已經使用過訪問資料的方法。這裡我們再集中看一下。
注:這裡的資料訪問方法既適用於
Series
,也適用於DataFrame
。
基礎方法:[]
和.
這是兩種最直觀的方法,任何有面向物件程式設計經驗的人應該都很容易理解。下面是一個程式碼示例:
# select_data.py import pandas as pd import numpy as np series1 = pd.Series([1, 2, 3, 4, 5, 6, 7], index=["C", "D", "E", "F", "G", "A", "B"]) print("series1['E'] = {} \n".format(series1['E'])); print("series1.E = {} \n".format(series1.E));
這段程式碼輸出如下:
series1['E'] = 3
series1.E = 3
注1:對於類似屬性的訪問方式
.
來說,要求索引元素必須是有效的Python識別符號的時候才可以,而對於series1.1
這樣的索引是不行的。
注2:
[]
和.
提供了簡單和快速訪問pands資料結構的方法。這種方法非常的直觀。然而,由於要訪問的資料型別並不是事先知道的,因此使用這兩種方法方式存在一些優化限制。因此對於產品級的程式碼來說,pandas官方建議使用pandas庫中提供的資料訪問方法。
loc與iloc
在入門教程中,我們已經提到了這兩個操作符:
loc
:通過行和列的索引來訪問資料iloc
:通過行和列的下標來訪問資料
注意:索引的型別可能是整數。
實際上,當DataFrame
通過這兩個操作符訪問資料,可以只指定一個索引來訪問一行的資料,例如:
# select_data.py df1 = pd.DataFrame({"note" : ["C", "D", "E", "F", "G", "A", "B"], "weekday": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]}, index=['1', '2', '3', '4', '5', '6', '7']) print("df1.loc['2']:\n{}\n".format(df1.loc['2']))
這裡通過索引'2'
可以方法到第2行的所有資料,因此它的輸出如下:
df1.loc['2']:
note D
weekday Tue
Name: 2, dtype: object
除此之外,通過這兩個操作符我們還可以訪問某個範圍之內的資料,例如這樣:
# select_data.py
print("series1.loc['E':'A']=\n{}\n".format(series1.loc['E':'A']));
print("df1.iloc[2:4]=\n{}\n".format(df1.iloc[2:4]))
這段程式碼輸出如下:
series1.loc['E':'A']=
E 3
F 4
G 5
A 6
dtype: int64
df1.iloc[2:3]=
note weekday
3 E Wed
4 F Thu
at與iat
這兩個操作符用來訪問單個的元素值(Scalar Value)。類似的:
at
:通過行和列的索引來訪問資料iat
:通過行和列的下標來訪問資料
# select_data.py
print("series1.at['E']={}\n".format(series1.at['E']));
print("df1.iloc[4,1]={}\n".format(df1.iloc[4,1]))
這兩行程式碼輸出如下:
series1.at['E']=3
df1.iloc[4,1]=Fri
Index物件
在入門教程我們也已經簡單介紹過Index,Index提供了查詢,資料對齊和重新索引所需的基礎資料結構。
最直接的,我們可以通過一個數組來建立Index物件。在建立的同時我們還可以通過name
指定索引的名稱:
# index.py
index = pd.Index(['C','D','E','F','G','A','B'], name='note')
Index類提供了很多的方法進行各種操作,這個建議讀者直接查詢API說明即可,這裡不多做說明。稍微提一下的是,Index物件可以互相之間做集合操作,例如:
# index.py
a = pd.Index([1,2,3,4,5])
b = pd.Index([3,4,5,6,7])
print("a|b = {}\n".format(a|b))
print("a&b = {}\n".format(a&b))
print("a.difference(b) = {}\n".format(a.difference(b)))
這幾個運算的結果如下:
a|b = Int64Index([1, 2, 3, 4, 5, 6, 7], dtype='int64')
a&b = Int64Index([3, 4, 5], dtype='int64')
a.difference(b) = Int64Index([1, 2], dtype='int64')
Index類有很多的子類,下面是最常見的一些:
MultiIndex
MultiIndex,或者稱之為Hierarchical Index是指資料的行或者列通過多層次的標籤來進行索引。
例如,我們要通過一個MultiIndex描述三個公司在三年內每個季度的營業額,可以這樣:
# multiindex.py
import pandas as pd
import numpy as np
multiIndex = pd.MultiIndex.from_arrays([
['Geagle', 'Geagle', 'Geagle', 'Geagle',
'Epple', 'Epple', 'Epple', 'Epple', 'Macrosoft',
'Macrosoft', 'Macrosoft', 'Macrosoft', ],
['S1', 'S2', 'S3', 'S4', 'S1', 'S2', 'S3', 'S4', 'S1', 'S2', 'S3', 'S4']],
names=('Company', 'Turnover'))
這段程式碼輸出如下:
multiIndex =
MultiIndex(levels=[['Epple', 'Geagle', 'Macrosoft'], ['S1', 'S2', 'S3', 'S4']],
labels=[[1, 1, 1, 1, 0, 0, 0, 0, 2, 2, 2, 2], [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3]],
names=['Company', 'Turnover'])
從這個輸出可以看出,MultiIndex中的levels
陣列數量對應了索引的級別數量,labels
對應了levels
中元素的下標。
下面我們用一個隨機數來構造一個DataFrame:
# multiindex.py
df = pd.DataFrame(data=np.random.randint(0, 1000, 36).reshape(-1, 12),
index=[2016, 2017, 2018],
columns=multiIndex)
print("df = \n{}\n".format(df))
這裡創建出了36個[0, 1000)
之間的隨機數,然後組裝成3行12列的矩陣(如果你對NumPy不熟悉可以訪問NumPy官網學習,或者看一下我之前寫過的:Python 機器學習庫 NumPy 教程)。
上面這段程式碼輸出如下:
df =
Company Geagle Epple Macrosoft
Turnover S1 S2 S3 S4 S1 S2 S3 S4 S1 S2 S3 S4
2016 329 25 553 852 833 710 247 990 215 991 535 846
2017 734 368 28 161 187 444 901 858 244 915 261 485
2018 769 707 458 782 948 169 927 237 279 438 738 708
這個輸出很直觀的可以看出三個公司在三年內每個季度的營業額。
有了多級索引,我們可以方便的進行資料的篩選,例如:
- 通過
df.loc[2017, (['Geagle', 'Epple', 'Macrosoft'] ,'S1')])
篩選出三個公司2017年第一季度的營業額 - 通過
df.loc[2018, 'Geagle']
篩選出Geagle公司2018年每個季度的營業額
它們輸出如下:
2017 S1:
Company Turnover
Geagle S1 734
Epple S1 187
Macrosoft S1 244
Name: 2017, dtype: int64
Geagle 2018:
Turnover
S1 769
S2 707
S3 458
S4 782
Name: 2018, dtype: int64
資料整合
Concatenate:串聯,連線,級連
Append:附加,增補
Merge:融合,歸併,合併
Join:合併,接合,交接
Concat與Append
concat
函式的作用是將多個數據串聯到一起。例如,某個多條資料分散在3個地方記錄,最後我們將三個資料新增到一起。下面是一個程式碼示例:
# concat_append.py
import pandas as pd
import numpy as np
df1 = pd.DataFrame({'Note': ['C', 'D'],
'Weekday': ['Mon', 'Tue']},
index=[1, 2])
df2 = pd.DataFrame({'Note': ['E', 'F'],
'Weekday': ['Wed', 'Thu']},
index=[3, 4])
df3 = pd.DataFrame({'Note': ['G', 'A', 'B'],
'Weekday': ['Fri', 'Sat', 'Sun']},
index=[5, 6, 7])
df_concat = pd.concat([df1, df2, df3], keys=['df1', 'df2', 'df3'])
print("df_concat=\n{}\n".format(df_concat))
這裡我們通過keys
指定了三個資料的索引劃分,最後的資料中會由此存在MultiIndex。這段程式碼輸出如下:
df_concat=
Note Weekday
df1 1 C Mon
2 D Tue
df2 3 E Wed
4 F Thu
df3 5 G Fri
6 A Sat
7 B Sun
請仔細思考一下df_concat
結構與原先三個資料結構的關係:其實它就是將原先三個資料縱向串聯起來了。另外,請關注一下MultiIndex結構。
concat
函式預設是以axis=0
(行)為主進行串聯。如果需要,我們可以指定axis=1
(列)為主進行串聯:
# concat_append.py
df_concat_column = pd.concat([df1, df2, df3], axis=1)
print("df_concat_column=\n{}\n".format(df_concat_column))
這個結構輸出如下:
df_concat_column=
Note Weekday Note Weekday Note Weekday
1 C Mon NaN NaN NaN NaN
2 D Tue NaN NaN NaN NaN
3 NaN NaN E Wed NaN NaN
4 NaN NaN F Thu NaN NaN
5 NaN NaN NaN NaN G Fri
6 NaN NaN NaN NaN A Sat
7 NaN NaN NaN NaN B Sun
請再次觀察一下這裡的結果和原先三個資料結構之間的關係。
concat
是將多個數據串聯在一起。類似的,對於某個具體的資料來說,我們可以在其資料基礎上新增(append)其他資料來進行串聯:
# concat_append.py
df_append = df1.append([df2, df3])
print("df_append=\n{}\n".format(df_append))
這個操作的結果和之前的concat
是一樣的:
df_append=
Note Weekday
1 C Mon
2 D Tue
3 E Wed
4 F Thu
5 G Fri
6 A Sat
7 B Sun
Merge與Join
pandas中的Merge操作和SQL語句中的Join操作是類似的。Join操作可以分為下面幾種:
- INNER
- LEFT OUTER
- RIGHT OUTER
- FULL OUTER
- CROSS
關於這幾種的Join操作的含義請參閱其他資料,例如維基百科:Join SQL。
使用pandas進行Merge操作很簡單,下面是一段程式碼示例:
# merge_join.py
import pandas as pd
import numpy as np
df1 = pd.DataFrame({'key': ['K1', 'K2', 'K3', 'K4'],
'A': ['A1', 'A2', 'A3', 'A8'],
'B': ['B1', 'B2', 'B3', 'B8']})
df2 = pd.DataFrame({'key': ['K3', 'K4', 'K5', 'K6'],
'A': ['A3', 'A4', 'A5', 'A6'],
'B': ['B3', 'B4', 'B5', 'B6']})
print("df1=\n{}\n".format(df1))
print("df2=\n{}\n".format(df2))
merge_df = pd.merge(df1, df2)
merge_inner = pd.merge(df1, df2, how='inner', on=['key'])
merge_left = pd.merge(df1, df2, how='left')
merge_left_on_key = pd.merge(df1, df2, how='left', on=['key'])
merge_right_on_key = pd.merge(df1, df2, how='right', on=['key'])
merge_outer = pd.merge(df1, df2, how='outer', on=['key'])
print("merge_df=\n{}\n".format(merge_df))
print("merge_inner=\n{}\n".format(merge_inner))
print("merge_left=\n{}\n".format(merge_left))
print("merge_left_on_key=\n{}\n".format(merge_left_on_key))
print("merge_right_on_key=\n{}\n".format(merge_right_on_key))
print("merge_outer=\n{}\n".format(merge_outer))
這段程式碼說明如下:
merge
函式的join
引數的預設值是“inner”,因此merge_df是兩個資料的inner join
的結果。另外,在不指明的情況下,merge
函式使用所有同名的列名作為key來進行運算。- merge_inner是指定了列的名稱進行
inner join
。 - merge_left是
left outer join
的結果 - merge_left_on_key是指定了列名進行
left outer join
的結果 - merge_right_on_key是指定了列名進行
right outer join
的結果 - merge_outer是
full outer join
的結果
這裡的結果如下,請觀察一下結果與你的預算是否一致:
df1=
A B key
0 A1 B1 K1
1 A2 B2 K2
2 A3 B3 K3
3 A8 B8 K4
df2=
A B key
0 A3 B3 K3
1 A4 B4 K4
2 A5 B5 K5
3 A6 B6 K6
merge_df=
A B key
0 A3 B3 K3
merge_inner=
A_x B_x key A_y B_y
0 A3 B3 K3 A3 B3
1 A8 B8 K4 A4 B4
merge_left=
A B key
0 A1 B1 K1
1 A2 B2 K2
2 A3 B3 K3
3 A8 B8 K4
merge_left_on_key=
A_x B_x key A_y B_y
0 A1 B1 K1 NaN NaN
1 A2 B2 K2 NaN NaN
2 A3 B3 K3 A3 B3
3 A8 B8 K4 A4 B4
merge_right_on_key=
A_x B_x key A_y B_y
0 A3 B3 K3 A3 B3
1 A8 B8 K4 A4 B4
2 NaN NaN K5 A5 B5
3 NaN NaN K6 A6 B6
merge_outer=
A_x B_x key A_y B_y
0 A1 B1 K1 NaN NaN
1 A2 B2 K2 NaN NaN
2 A3 B3 K3 A3 B3
3 A8 B8 K4 A4 B4
4 NaN NaN K5 A5 B5
5 NaN NaN K6 A6 B6
DataFrame也提供了join
函式來根據索引進行資料合併。它可以被用於合併多個DataFrame,這些DataFrame有相同的或者類似的索引,但是沒有重複的列名。預設情況下,join
函式執行left join
。另外,假設兩個資料有相同的列名,我們可以通過lsuffix
和rsuffix
來指定結果中列名的字首。下面是一段程式碼示例:
# merge_join.py
df3 = pd.DataFrame({'key': ['K1', 'K2', 'K3', 'K4'],
'A': ['A1', 'A2', 'A3', 'A8'],
'B': ['B1', 'B2', 'B3', 'B8']},
index=[0, 1, 2, 3])
df4 = pd.DataFrame({'key': ['K3', 'K4', 'K5', 'K6'],
'C': ['A3', 'A4', 'A5', 'A6'],
'D': ['B3', 'B4', 'B5', 'B6']},
index=[1, 2, 3, 4])
print("df3=\n{}\n".format(df3))
print("df4=\n{}\n".format(df4))
join_df = df3.join(df4, lsuffix='_self', rsuffix='_other')
join_left = df3.join(df4, how='left', lsuffix='_self', rsuffix='_other')
join_right = df1.join(df4, how='outer', lsuffix='_self', rsuffix='_other')
print("join_df=\n{}\n".format(join_df))
print("join_left=\n{}\n".format(join_left))
print("join_right=\n{}\n".format(join_right))
這段程式碼輸出如下:
df3=
A B key
0 A1 B1 K1
1 A2 B2 K2
2 A3 B3 K3
3 A8 B8 K4
df4=
C D key
1 A3 B3 K3
2 A4 B4 K4
3 A5 B5 K5
4 A6 B6 K6
join_df=
A B key_self C D key_other
0 A1 B1 K1 NaN NaN NaN
1 A2 B2 K2 A3 B3 K3
2 A3 B3 K3 A4 B4 K4
3 A8 B8 K4 A5 B5 K5
join_left=
A B key_self C D key_other
0 A1 B1 K1 NaN NaN NaN
1 A2 B2 K2 A3 B3 K3
2 A3 B3 K3 A4 B4 K4
3 A8 B8 K4 A5 B5 K5
join_right=
A B key_self C D key_other
0 A1 B1 K1 NaN NaN NaN
1 A2 B2 K2 A3 B3 K3
2 A3 B3 K3 A4 B4 K4
3 A8 B8 K4 A5 B5 K5
4 NaN NaN NaN A6 B6 K6
資料集合和分組操作
很多時候,我們會需要對批量的資料進行分組統計或者再處理,groupby
,agg
,apply
就是用來做這件事的。
groupby
將資料分組,分組後得到pandas.core.groupby.DataFrameGroupBy
型別的資料。agg
用來進行合計操作,agg
是aggregate
的別名。apply
用來將函式func分組化並將結果組合在一起。
這些概念都很抽象,我們還是通過程式碼來進行說明。
# groupby.py
import pandas as pd
import numpy as np
df = pd.DataFrame({
'Name': ['A','A','A','B','B','B','C','C','C'],
'Data': np.random.randint(0, 100, 9)})
print('df=\n{}\n'.format(df))
groupby = df.groupby('Name')
print("Print GroupBy:")
for name, group in groupby:
print("Name: {}\nGroup:\n{}\n".format(name, group))
在這段程式碼中,我們生成了9個[0, 100)之間的隨機數,資料的第一列是['A','A','A','B','B','B','C','C','C']
。然後我們以Name
列進行groupby
,得到的結果會根據將Name
列值一樣的分組在一起,我們將得到的結果進行了列印。這段程式碼的輸出如下:
df=
Data Name
0 34 A
1 44 A
2 57 A
3 81 B
4 78 B
5 65 B
6 73 C
7 16 C
8 1 C
Print GroupBy:
Name: A
Group:
Data Name
0 34 A
1 44 A
2 57 A
Name: B
Group:
Data Name
3 81 B
4 78 B
5 65 B
Name: C
Group:
Data Name
6 73 C
7 16 C
8 1 C
groupby
並不是我們的最終目的,我們的目的是希望分組後還要對這些資料進行進一步的統計或者處理。pandas庫本身就提供了很多進行操作的函式,例如:count,sum,mean,median,std,var,min,max,prod,first,last
。這些函式的名稱很容易明白它的作用。
例如:groupby.sum()
就是對結果進行求和執行。
除了直接呼叫這些函式之外,我們也可以通過agg
函式來達到這個目的,這個函式接收其他函式的名稱,例如這樣:groupby.agg(['sum'])
。
通過agg
函式,可以一次性呼叫多個函式,並且可以為結果列指定名稱。
像這樣:groupby.agg([('Total', 'sum'), ('Min', 'min')])
。
這裡的三個呼叫輸出結果如下:
# groupby.py
Sum:
Data
Name
A 135
B 224
C 90
Agg Sum:
Data
sum
Name
A 135
B 224
C 90
Agg Map:
Data
Total Min
Name
A 135 34
B 224 65
C 90 1
除了對資料集合進行統計,我們也可以通過apply
函式進行分組資料的處理。像這樣:
# groupby.py
def sort(df):
return df.sort_values(by='Data', ascending=False)
print("Sort Group: \n{}\n".format(groupby.apply(sort)))
在這段程式碼中,我們定義了一個排序函式,並應用在分組資料上,這裡最終的輸出如下:
Sort Group:
Data
Name
A 2 57
1 44
0 34
B 3 81
4 78
5 65
C 6 73
7 16
8 1
時間相關
時間是應用程式中很頻繁需要處理的邏輯,尤其是對於金融,科技,商業等領域。
當我們在討論時間,我們討論的可能是下面三種情況中的一種:
- 某個具體的時間點(Timestamp),例如:今天下午一點整
- 某個時間範圍(Period),例如:整個這個月
- 某個時間間隔(Interval),例如:每週二上午七點整
Python語言提供了時間日期相關的基本API,它們位於datetime
, time
, calendar
幾個模組中。下面是一個程式碼示例:
# time.py
import datetime as dt
import numpy as np
import pandas as pd
now = dt.datetime.now();
print("Now is {}".format(now))
yesterday = now - dt.timedelta(1);
print("Yesterday is {}\n".format(yesterday.strftime('%Y-%m-%d')))
在這段程式碼中,我們列印了今天的日期,並通過timedelta
進行了日期的減法運算。這段程式碼輸出如下:
藉助pandas提供的介面,我們可以很方便的獲得以某個時間間隔的時間序列,例如這樣:
# time.py
this_year = pd.date_range(dt.datetime(2018, 1, 1),
dt.datetime(2018, 12, 31), freq='5D')
print("Selected days in 2018: \n{}\n".format(this_year))
這段程式碼獲取了整個2018年中從元旦開始,每隔5天的日期序列。
這段程式碼的輸出如下:
Selected days in 2018:
DatetimeIndex(['2018-01-01', '2018-01-06', '2018-01-11', '2018-01-16',
'2018-01-21', '2018-01-26', '2018-01-31', '2018-02-05',
'2018-02-10', '2018-02-15', '2018-02-20', '2018-02-25',
'2018-03-02', '2018-03-07', '2018-03-12', '2018-03-17',
'2018-03-22', '2018-03-27', '2018-04-01', '2018-04-06',
'2018-04-11', '2018-04-16', '2018-04-21', '2018-04-26',
'2018-05-01', '2018-05-06', '2018-05-11', '2018-05-16',
'2018-05-21', '2018-05-26', '2018-05-31', '2018-06-05',
'2018-06-10', '2018-06-15', '2018-06-20', '2018-06-25',
'2018-06-30', '2018-07-05', '2018-07-10', '2018-07-15',
'2018-07-20', '2018-07-25', '2018-07-30', '2018-08-04',
'2018-08-09', '2018-08-14', '2018-08-19', '2018-08-24',
'2018-08-29', '2018-09-03', '2018-09-08', '2018-09-13',
'2018-09-18', '2018-09-23', '2018-09-28', '2018-10-03',
'2018-10-08', '2018-10-13', '2018-10-18', '2018-10-23',
'2018-10-28', '2018-11-02', '2018-11-07', '2018-11-12',
'2018-11-17', '2018-11-22', '2018-11-27', '2018-12-02',
'2018-12-07', '2018-12-12', '2018-12-17', '2018-12-22',
'2018-12-27'],
dtype='datetime64[ns]', freq='5D')
我們得到的返回值是DatetimeIndex
型別的,我們可以建立一個DataFrame並以此作為索引:
# time.py
df = pd.DataFrame(np.random.randint(0, 100, this_year.size), index=this_year)
print("Jan: \n{}\n".format(df['2018-01']))
在這段程式碼中,我們建立了與索引數量一樣多的[0, 100)間的隨機整數,並用this_year
作為索引。用DatetimeIndex
作索引的好處是,我們可以直接指定某個範圍來選擇資料,例如,通過df['2018-01']
選出所有1月份的資料。
這段程式碼輸出如下:
Jan:
0
2018-01-01 61
2018-01-06 85
2018-01-11 66
2018-01-16 11
2018-01-21 34
2018-01-26 2
2018-01-31 97
圖形展示
pandas的圖形展示依賴於matplotlib
庫。對於這個庫,我們在後面會專門講解,因為這裡僅僅提供一個簡單的程式碼示例,讓大家感受一下圖形展示的樣子。
程式碼示例如下:
# plot.py
import matplotlib.pyplot as plt
import pandas as pd
data = pd.read_csv("data/housing.csv")
data.hist(bins=50, figsize=(15, 12))
plt.show()
這段程式碼讀取了一個CSV檔案,這個檔案中包含了一些關於房價的資訊。在讀取完之後,通過直方圖(hist)將其展示了出來。
直方圖結果如下所示:
結束語
雖然本文的標題是“進階篇”,我們也討論了一些更深入的知識。但很顯然,這對於pandas來說,仍然是很皮毛的東西。由於篇幅所限,更多的內容在今後的時候,有機會我們再來一起探討。
讀者朋友也可以根據官網上的文件進行更深入的學習。