1. 程式人生 > >Python資料處理庫pandas進階教程

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。另外,假設兩個資料有相同的列名,我們可以通過lsuffixrsuffix來指定結果中列名的字首。下面是一段程式碼示例:

# 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

資料集合和分組操作

很多時候,我們會需要對批量的資料進行分組統計或者再處理,groupbyaggapply就是用來做這件事的。

  • groupby將資料分組,分組後得到pandas.core.groupby.DataFrameGroupBy型別的資料。
  • agg用來進行合計操作,aggaggregate的別名。
  • 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,它們位於datetimetimecalendar幾個模組中。下面是一個程式碼示例:

# 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來說,仍然是很皮毛的東西。由於篇幅所限,更多的內容在今後的時候,有機會我們再來一起探討。

讀者朋友也可以根據官網上的文件進行更深入的學習。

參考資料與推薦讀物