【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
,除了必須的on
和how
之外,可以對重複的列指定左右後綴lsuffix
和rsuffix
。其中,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
前面介紹了關係型連線,其中最重要的引數是on
和how
,但有時候使用者並不關心以哪一列為鍵來合併,只是希望把兩個表或者多個表按照縱向或者橫向拼接,為這種需求,pandas
中提供了concat
函式來實現。
在concat
中,最常用的有三個引數,它們是axis, join, keys
,分別表示拼接方向,連線形式,以及在新表中指示來自於哪一張舊錶的名字。這裡需要特別注意,join
和keys
與之前提到的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
可以實現多個表之間的方向拼接,如果想要把一個序列追加到表的行末或者列末,則可以分別使用append
和assign
方法。
在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
,其中other
和self
分別指代傳入的引數表和被呼叫的表自身。
如果想要完整顯示錶中所有元素的比較情況,可以設定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 York
的Confirmed, 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等