1. 程式人生 > 實用技巧 >【12月DW打卡】joyful-pandas - 06 - joyful-pands - pandas連線(join、concat、compare連線、combine連線)

【12月DW打卡】joyful-pandas - 06 - joyful-pands - pandas連線(join、concat、compare連線、combine連線)

第六章 pandas連線

小結

  • 原文指路:joyful-pandas
  • jupyter nbconvert --to markdown E:\PycharmProjects\TianChiProject\00_山楓葉紛飛\competitions\008_joyful-pandas\06_pandas連線.ipynb
import numpy as np
import pandas as pd

1. 連線的基本概念 (說白了就是SQL的join)

在 pandas 中的關係型連線函式 merge 和 join 中提供了 how 引數來代表連線形式,分為左連線 left 、右連線 right 、內連線 inner 、外連線 outer .

2. 值連線

在上面示意圖中的例子中,兩張表根據某一列的值來連線,事實上還可以通過幾列值的組合進行連線,這種基於值的連線在 pandas 中可以由 merge 函式實現,例如第一張圖的左連線:

df1 = pd.DataFrame({'Name':['San Zhang','Si Li'],
                      'Age':[20,30]})

df2 = pd.DataFrame({'Name':['Si Li','Wu Wang'],
                      'Gender':['F','M']})

# 實現left join
left_df = df1.merge(df2, on='Name', how='left')
left_df
Name Age Gender
0 San Zhang 20 NaN
1 Si Li 30 F

如果兩個表中想要連線的列不具備相同的列名,可以通過 left_on 和 right_on 指定:

df1 = pd.DataFrame({'df1_name':['San Zhang', 'Si Li'],
                      'Age':[20,30]})

df2 = pd.DataFrame({'df2_name':['Si Li', 'Wu Wang'],
                     'Gender':['F','M']})
df1.merge(df2, left_on='df1_name', right_on='df2_name', how='left')
df1_name Age df2_name Gender
0 San Zhang 20 NaN NaN
1 Si Li 30 Si Li F

如果兩個表中的列出現了重複的列名,那麼可以通過 suffixes(字尾) 引數指定。例如合併考試成績的時候,第一個表記錄了語文成績,第二個是數學成績:

df1 = pd.DataFrame({'Name':['San Zhang'],'Grade':[70]})
df2 = pd.DataFrame({'Name':['San Zhang'],'Grade':[80]})
df1.merge(df2, on='Name', how='left', suffixes=['_Chinese','_Math'])
Name Grade_Chinese Grade_Math
0 San Zhang 70 80

在某些時候出現重複元素是麻煩的,例如兩位同學來自不同的班級,但是姓名相同,這種時候就要指定 on 引數為多個列使得正確連線:

df1 = pd.DataFrame({'Name':['San Zhang', 'San Zhang'],
                     'Age':[20, 21],
                     'Class':['one', 'two']})
df2 = pd.DataFrame({'Name':['San Zhang', 'San Zhang'],
                     'Gender':['F', 'M'],
                     'Class':['two', 'one']})
df1.merge(df2, on=['Name', 'Class'], how='left') # 正確的結果
Name Age Class Gender
0 San Zhang 20 one M
1 San Zhang 21 two F

基於唯一性的連線下,如果鍵不是唯一的,那麼結果就會產生問題。舉例中的行數很少,但如果實際資料中有幾十萬到上百萬行的進行合併時,如果想要保證唯一性,除了用 duplicated 檢查是否重複外, merge 中也提供了 validate 引數來檢查連線的唯一性模式。

這裡共有三種模式,即一對一連線 1:1,一對多連線 1:m,多對一連線 m:1 連線,第一個是指左右表的鍵都是唯一的,後面兩個分別指左表鍵唯一和右表鍵唯一。

練一練
上面以多列為鍵的例子中,錯誤寫法顯然是一種多對多連線,而正確寫法是一對一連線,請修改原表,使得以多列為鍵的正確寫法能夠通過 validate='1:m' 的檢驗,但不能通過 validate='m:1' 的檢驗。

# 錯誤的寫法
df1.merge(df2, on='Name', how='left')
Name Age Class_x Gender Class_y
0 San Zhang 20 one F two
1 San Zhang 20 one M one
2 San Zhang 21 two F two
3 San Zhang 21 two M one
# 加 validate='1:1' 的檢驗的寫法
try:
    df1.merge(df2, on='Name', how='left', validate='1:1')
except Exception as e:
    print(e)
Merge keys are not unique in either left or right dataset; not a one-to-one merge
df1.merge(df2, on=['Name', 'Class'], how='left', validate='1:m')
不用修改原表,使得以多列為鍵的正確寫法能夠通過`validate='1:m'`的檢驗
print('不用修改原表,使得以多列為鍵的正確寫法能夠通過`validate=\'1:m\'`的檢驗')

3. 索引連線

所謂索引連線,就是把索引當作鍵,因此這和值連線本質上沒有區別,pandas中利用join函式來處理索引連線,它的引數選擇要少於merge,除了必須的onhow之外,可以對重複的列指定左右後綴lsuffixrsuffix。其中,on引數指索引名,單層索引時省略引數表示按照當前索引連線。

如果想要進行類似於merge中以多列為鍵的操作的時候,join需要使用多級索引。

df1 = pd.DataFrame({'Age':[20,30]}, index=pd.Series(['San Zhang','Si Li'],name='Name'))
df2 = pd.DataFrame({'Gender':['F','M']}, index=pd.Series(['Si Li','Wu Wang'],name='Name'))
df1.join(df2, how='left')


Age Gender
Name
San Zhang 20 NaN
Si Li 30 F

仿照第2小節的例子,寫出語文和數學分數合併的join版本:

df1 = pd.DataFrame({'Grade':[70]}, index=pd.Series(['San Zhang'], name='Name'))
df2 = pd.DataFrame({'Grade':[80]}, index=pd.Series(['San Zhang'], name='Name'))
df1.join(df2, on='Name', how='left', lsuffix='_Chinese', rsuffix='_Math')

Grade_Chinese Grade_Math
Name
San Zhang 70 80

二、方向連線

1. concat

前面介紹了關係型連線,其中最重要的引數是onhow,但有時候使用者並不關心以哪一列為鍵來合併,只是希望把兩個表或者多個表按照縱向或者橫向拼接,為這種需求,pandas中提供了concat函式來實現。

concat中,最常用的有三個引數,它們是axis, join, keys,分別表示拼接方向,連線形式,以及在新表中指示來自於哪一張舊錶的名字。這裡需要特別注意,joinkeys與之前提到的join函式和鍵的概念沒有任何關係。

在預設狀態下的axis=0,表示縱向拼接多個表,常常用於同特徵的多個樣本的直接拼接;而axis=1表示橫向拼接多個表,常用於多個欄位或特徵的拼接。


concat仍然是關於索引進行連線的。

縱向拼接會根據列索引對齊,預設狀態下join=outer,表示保留所有的列,並將不存在的值設為缺失;join=inner,表示保留兩個表都出現過的列。橫向拼接則根據行索引對齊,join引數可以類似設定。

df1 = pd.DataFrame({'Name':['San Zhang','Si Li'], 'Age':[20,30]})
df1
Name Age
0 San Zhang 20
1 Si Li 30
df2 = pd.DataFrame({'Name':['Wu Wang'], 'Gender':['M']})
pd.concat([df1, df2])
Name Age Gender
0 San Zhang 20.0 NaN
1 Si Li 30.0 NaN
0 Wu Wang NaN M
df2 = pd.DataFrame({'Grade':[80, 90]}, index=[1, 2])
pd.concat([df1, df2], 1)
Name Age Grade
0 San Zhang 20.0 NaN
1 Si Li 30.0 80.0
2 NaN NaN 90.0
pd.concat([df1, df2], axis=1, join='inner')
Name Age Grade
1 Si Li 30 80

因此,當確認要使用多表直接的方向合併時,尤其是橫向的合併,可以先用reset_index方法恢復預設整數索引再進行合併,或者手動去重,防止出現由索引的誤對齊和重複索引的笛卡爾積帶來的錯誤結果。

最後,keys引數的使用場景在於多個表合併後,使用者仍然想要知道新表中的資料來自於哪個原表,這時可以通過keys引數產生多級索引進行標記。例如,第一個表中都是一班的同學,而第二個表中都是二班的同學,可以使用如下方式合併:

df1 = pd.DataFrame({'Name':['San Zhang','Si Li'], 'Age':[20,21]})
df2 = pd.DataFrame({'Name':['Wu Wang'],'Age':[21]})
pd.concat([df1, df2], keys=['one', 'two'])

Name Age
one 0 San Zhang 20
1 Si Li 21
two 0 Wu Wang 21

2. 序列與表的合併

利用concat可以實現多個表之間的方向拼接,如果想要把一個序列追加到表的行末或者列末,則可以分別使用appendassign方法。

append中,如果原表是預設整數序列的索引,那麼可以使用ignore_index=True對新序列對應索引的自動標號,否則必須對Series指定name屬性。

df1
Name Age
0 San Zhang 20
1 Si Li 21
s = pd.Series(['Wu Wang', 21], index = df1.columns)
df1.append(s, ignore_index=True)

Name Age
0 San Zhang 20
1 Si Li 21
2 Wu Wang 21

對於assign而言,雖然可以利用其新增新的列,但一般通過df['new_col'] = ...的形式就可以等價地新增新列。同時,使用[]修改的缺點是它會直接在原表上進行改動,而assign返回的是一個臨時副本

s = pd.Series([666, 999])
df1.assign(Grade=s)
Name Age Grade
0 San Zhang 20 666
1 Si Li 21 999

三、類連線操作

除了上述介紹的若干連線函式之外,pandas中還設計了一些函式能夠對兩個表進行某些操作,這裡把它們統稱為類連線操作。

1. 比較 compare

compare是在1.1.0後引入的新函式,它能夠比較兩個表或者序列的不同處並將其彙總展示:

結果中返回了不同值所在的行列,如果相同則會被填充為缺失值NaN,其中otherself分別指代傳入的引數表和被呼叫的表自身。

如果想要完整顯示錶中所有元素的比較情況,可以設定keep_shape=True

df1 = pd.DataFrame({'Name':['San Zhang', 'Si Li', 'Wu Wang'],
                    'Age':[20, 21 ,21],
                    'Class':['one', 'two', 'three']})
df2 = pd.DataFrame({'Name':['San Zhang', 'Li Si', 'Wu Wang'],
                    'Age':[20, 21 ,21],
                    'Class':['one', 'two', 'Three']})
df1.compare(df2)

Name Class
self other self other
1 Si Li Li Si NaN NaN
2 NaN NaN three Three

如果想要完整顯示錶中所有元素的比較情況,可以設定keep_shape=True

df1.compare(df2, keep_shape=True)
Name Age Class
self other self other self other
0 NaN NaN NaN NaN NaN NaN
1 Si Li Li Si NaN NaN NaN NaN
2 NaN NaN NaN NaN three Three

2. 組合

combine函式能夠讓兩張表按照一定的規則進行組合,在進行規則比較時會自動進行列索引的對齊。對於傳入的函式而言,每一次操作中輸入的引數是來自兩個表的同名Series,依次傳入的列是兩個表列名的並集,例如下面這個例子會依次傳入A,B,C,D四組序列,每組為左右表的兩個序列。同時,進行A列比較的時候,s1指代的就是一個全空的序列,因為它在被呼叫的表中並不存在,並且來自第一個表的序列索引會被reindex成兩個索引的並集。具體的過程可以通過在傳入的函式中插入適當的print方法檢視。

def choose_min(s1, s2):
    s2 = s2.reindex_like(s1)
    res = s1.where(s1<s2, s2) # #返回一個同樣shape的df,當滿足條件為TRUE時,從本身返回結果,否則從返回其他df的結果 (也就是條件不符合再進行替換)
    # print('res,:\n', res)
    # print('s1.isna():\n', s1.isna())
    res = res.mask(s1.isna()) # isna表示是否為缺失值,返回布林序列; mask是條件符合進行替換
    print(res)
    return res
df1 = pd.DataFrame({'A':[1,2], 'B':[3,4], 'C':[5,6]})
df2 = pd.DataFrame(           {'B':[5,6], 'C':[7,8], 'D':[9,10]}, index=[1,2])
df1.combine(df2, choose_min)

0   NaN
1   NaN
2   NaN
Name: A, dtype: float64
0    NaN
1    4.0
2    NaN
Name: B, dtype: float64
0    NaN
1    6.0
2    NaN
Name: C, dtype: float64
0   NaN
1   NaN
2   NaN
Name: D, dtype: float64
A B C D
0 NaN NaN NaN NaN
1 NaN 4.0 6.0 NaN
2 NaN NaN NaN NaN

【練一練】

請在上述程式碼的基礎上修改,保留df2中4個未被df1替換的相應位置原始值。

def choose_min_plus(s1, s2):
    s2 = s2.reindex_like(s1)
    res = s1.where(s1<s2, s2) # #返回一個同樣shape的df,當滿足條件為TRUE時,從本身返回結果,否則從返回其他df的結果 (也就是條件不符合再進行替換)
    # print('res,:\n', res)
    # print('s1.isna():\n', s1.isna())
    res = res.mask(s1.isna(), s2) # isna表示是否為缺失值,返回布林序列; mask是條件符合進行替換
    # print(res)
    return res
df1 = pd.DataFrame({'A':[1,2], 'B':[3,4], 'C':[5,6]}) # index=[0,1]
df2 = pd.DataFrame(           {'B':[5,6], 'C':[7,8], 'D':[9,10]}, index=[1,2])
df1.combine(df2, choose_min_plus)



A B C D
0 NaN NaN NaN NaN
1 NaN 4.0 6.0 9.0
2 NaN 6.0 8.0 10.0

【END】

此外,設定overtwrite引數為False可以保留\(\color{red}{被呼叫表}\)中未出現在傳入的引數表中的列,而不會設定未缺失值:

df1.combine(df2, choose_min, overwrite=False)
0    NaN
1    4.0
2    NaN
Name: B, dtype: float64
0    NaN
1    6.0
2    NaN
Name: C, dtype: float64
0   NaN
1   NaN
2   NaN
Name: D, dtype: float64
A B C D
0 1.0 NaN NaN NaN
1 2.0 4.0 6.0 NaN
2 NaN NaN NaN NaN

【練一練】

除了combine之外,pandas中還有一個combine_first方法,其功能是在對兩張表組合時,若第二張表中的值在第一張表中對應索引位置的值不是缺失狀態,那麼就使用第一張表的值填充。下面給出一個例子,請用combine函式完成相同的功能。

【END】

df1 = pd.DataFrame({'A':[1,2], 'B':[3,np.nan]})
df2 = pd.DataFrame({'A':[5,6], 'B':[7,8]}, index=[1,2])
df1.combine_first(df2)
A B
0 1.0 3.0
1 2.0 7.0
2 6.0 8.0
print('用`combine`函式完成相同的功能')
def choose_plus_ultra(s1, s2):
    s2 = s2.reindex_like(s1) # index: [1,2]  => [0,1,2]
    print(s2)
    # res = s1.where(s1<s2, s2) # #返回一個同樣shape的df,當滿足條件為TRUE時,從本身返回結果,否則從返回其他df的結果 (也就是條件不符合再進行替換)
    # print('res,:\n', res)
    # print('s1.isna():\n', s1.isna())
    res = s1.mask(s1.isna(), s2) # isna表示是否為缺失值,返回布林序列; mask是條件符合進行替換
    # print(res)
    return res
df1 = pd.DataFrame({'A':[1,2], 'B':[3,4], 'C':[5,6]}) # index=[0,1]
df2 = pd.DataFrame(           {'B':[5,6], 'C':[7,8], 'D':[9,10]}, index=[1,2])
df1.combine(df2, choose_plus_ultra)
用`combine`函式完成相同的功能
0   NaN
1   NaN
2   NaN
Name: A, dtype: float64
0    NaN
1    5.0
2    6.0
Name: B, dtype: float64
0    NaN
1    7.0
2    8.0
Name: C, dtype: float64
0     NaN
1     9.0
2    10.0
Name: D, dtype: float64
A B C D
0 1.0 3.0 5.0 NaN
1 2.0 4.0 6.0 9.0
2 NaN 6.0 8.0 10.0

四、練習

Ex1:美國疫情資料集

現有美國4月12日至11月16日的疫情報表,請將New YorkConfirmed, Deaths, Recovered, Active合併為一張表,索引為按如下方法生成的日期字串序列:

date = pd.date_range('20200412', '20201116').to_series()
date = date.dt.month.astype('string').str.zfill(2) +'-'+ date.dt.day.astype('string').str.zfill(2) +'-'+ '2020'
date = date.tolist()
date[:5]
['04-12-2020', '04-13-2020', '04-14-2020', '04-15-2020', '04-16-2020']
dfs=[]
for a_date in date:
    one_df = pd.read_csv('E:\\PycharmProjects\\DatawhaleChina\\joyful-pandas\\data\\us_report\\{}.csv'.format(str(a_date)))
    dfs.append(one_df)
    one_df['Last_Update'] = a_date
batch_df = pd.concat(dfs)
batch_df.columns
Index(['Province_State', 'Country_Region', 'Last_Update', 'Lat', 'Long_',
       'Confirmed', 'Deaths', 'Recovered', 'Active', 'FIPS', 'Incident_Rate',
       'People_Tested', 'People_Hospitalized', 'Mortality_Rate', 'UID', 'ISO3',
       'Testing_Rate', 'Hospitalization_Rate', 'Total_Test_Results',
       'Case_Fatality_Ratio'],
      dtype='object')
batch_df.set_index('Last_Update', inplace=True)
end_df = batch_df[batch_df['Province_State']=='New York'][['Confirmed', 'Deaths', 'Recovered', "Active"]]
end_df.tail()
Confirmed Deaths Recovered Active
Last_Update
11-12-2020 545762 33975 81198.0 430589.0
11-13-2020 551163 33993 81390.0 435780.0
11-14-2020 556551 34010 81585.0 440956.0
11-15-2020 560200 34032 81788.0 444380.0
11-16-2020 563690 34054 81908.0 447728.0

Ex2:實現join函式

請實現帶有how引數的join函式

  • 假設連線的兩表無公共列
  • 呼叫方式為 join(df1, df2, how="left")
  • 給出測試樣例
df1 = pd.DataFrame({'A':[1,2], 'B':[3,4], 'C':[5,6]}, index=[0,1]) # index=[0,1]
df2 = pd.DataFrame({'D':[5,6], 'E':[7,8], 'F':[9,10]}, index=[1,2])
print('標準')
df1.join(df2, how="left")
標準
A B C D E F
0 1 3 5 NaN NaN NaN
1 2 4 6 5.0 7.0 9.0

個人設想的思路

  • how有多類引數,如left/right/inner/outer等