數據組合與合并:Pandas 數據整合全指南
在進行數據分析之前,數據清洗與整合是關鍵步驟。
遵循“整潔數據”(Tidy Data)原則:
- 每個觀測值占一行
- 每個變量占一列
- 每種觀測單元構成一張獨立的表格
整理好數據后,常常需要將多個數據集組合起來,才能回答復雜問題。
一、連接數據(Concatenation)
當多個數據集結構相似(列名一致或相近),可以使用 pd.concat()
將它們按行或列方向拼接。
1. 添加行:DataFrame 連接
import pandas as pd# 加載多個結構相似的 CSV 文件
df1 = pd.read_csv('data/demo1.csv')
df2 = pd.read_csv('data/demo2.csv')
df3 = pd.read_csv('data/demo3.csv')# 沿行方向(垂直)連接
row_concat = pd.concat([df1, df2, df3], ignore_index=True) # 重置索引
print(row_concat)
參數說明:
ignore_index=True
:重新生成從 0 開始的整數索引,避免重復行標簽。- 默認
axis=0
表示按行連接(上下拼接)。
2. 添加行:DataFrame 與 Series 連接
df1 = pd.read_csv('data/demo1.csv')
new_series = pd.Series(['n1', 'n2', 'n3', 'n4'], index=['A', 'B', 'C', 'D']) # 推薦指定索引# 錯誤方式(不推薦):
# pd.concat([df1, new_series]) # Series 被當作新列添加,造成 NaN 填充# 正確方式:將 Series 轉為 DataFrame 再連接
new_row_df = pd.DataFrame([new_series], index=['new_row']) # 包裝成一行的 DataFrame
result = pd.concat([df1, new_row_df], ignore_index=False)
關鍵點:
- 直接
concat([df, series])
會嘗試按列對齊,導致Series
成為新列,原數據缺失處填NaN
。 - 應先將
Series
轉換為單行DataFrame
,并確保其index
與目標DataFrame
的列名匹配。
3. 添加行:使用 append()
方法(已棄用,建議用 concat)
注意:
DataFrame.append()
方法在 Pandas 1.4+ 中已被 棄用(deprecated),官方推薦使用pd.concat()
替代。
舊寫法(不推薦):
# 已棄用
new_row = {'A': 'n1', 'B': 'n2', 'C': 'n3', 'D': 'n4'}
df1 = df1.append(new_row, ignore_index=True)
新寫法(推薦):
new_row_df = pd.DataFrame([{'A': 'n1', 'B': 'n2', 'C': 'n3', 'D': 'n4'}])
df1 = pd.concat([df1, new_row_df], ignore_index=True)
4. 添加行:重置索引
當多次連接后,行索引可能出現重復或無序:
result = pd.concat([df1, df2, df3])
result = result.reset_index(drop=True) # 丟棄原索引,生成新整數索引
建議:在垂直連接時始終使用
ignore_index=True
或后續調用reset_index(drop=True)
。
5. 添加列:使用 concat()
水平連接(axis=1
)可實現列拼接:
col_concat = pd.concat([df1, df2], axis=1) # 按列拼接(左右)
注意:
- 默認按行索引對齊,若索引不一致會導致
NaN
。 - 若兩個 DataFrame 行數不同但索引相同,也能對齊。
6. 添加列:直接賦值(最常用)
# 添加標量(廣播)
df1['new_col'] = 0# 添加列表或數組(長度需匹配)
df1['new_col'] = [1, 2, 3, 4]# 添加 Series(按索引自動對齊)
s = pd.Series([10, 20, 30, 40], index=df1.index)
df1['new_col'] = s
這是最簡潔高效的方式,適用于大多數場景。
7. 添加列:重置索引的影響
如果 Series
的索引與 DataFrame
不一致,直接賦值可能導致 NaN
:
s = pd.Series([10, 20, 30, 40], index=[0, 1, 2, 3])
df1['new_col'] = s # 若 df1 索引為 [0,1,2,3],則正常;否則部分為 NaN
解決方案:
- 使用
.values
強制忽略索引:df1['new_col'] = s.values # 忽略索引,按順序賦值
- 或先對齊索引:
s.reindex(df1.index)
二、合并多個數據集(Merge & Join)
當數據集之間有共同鍵(key),但結構不同(如主表+屬性表),應使用 merge
或 join
。
方法 | 類型 | 用途 | 對齊方式 | 默認連接類型 |
---|---|---|---|---|
pd.concat() | 函數 | 拼接多個對象(行/列) | 按索引對齊 | 外連接(outer) |
DataFrame.join() | 方法 | 水平合并多個 DataFrame | 左表列/行索引 vs 右表行索引 | 左連接(left) |
pd.merge() | 函數 | 靈活合并兩個 DataFrame | 基于列或索引 | 內連接(inner) |
1. pd.merge()
:最靈活的合并方式
left = pd.DataFrame({'key': ['K0', 'K1', 'K2'],'A': ['A0', 'A1', 'A2']
})right = pd.DataFrame({'key': ['K0', 'K1', 'K3'],'B': ['B0', 'B1', 'B3']
})# 基于列 'key' 合并
merged = pd.merge(left, right, on='key', how='inner')
參數詳解:
on
:指定連接鍵(列名)how
:連接方式'inner'
:交集(默認)'outer'
:并集'left'
:保留左表所有行'right'
:保留右表所有行
left_on
,right_on
:左右表鍵名不同時使用left_index=True
,right_index=True
:基于索引合并
示例:不同列名合并
pd.merge(left, right, left_on='key', right_on='key_r', how='outer')
2. DataFrame.join()
:基于索引的便捷合并
result = left.join(right.set_index('key'), on='key', how='left')
# 或者 right 的索引已經是 'key'
# result = left.join(right, on='key')
特點:
- 默認以左表為基準(
how='left'
) - 通常用于:主表 + 多個屬性表(如公司信息 + 股價、行業等)
- 支持多表連接:
df.join([df2, df3, df4])
注意:
join()
默認是左連接,而merge()
默認是內連接!
3. concat()
vs merge()
vs join()
對比總結
功能 | concat() | merge() | join() |
---|---|---|---|
是否支持多對象 | 支持多個 | 僅兩個 | 支持多個 |
連接方向 | 行或列 | 僅列(水平) | 僅列(水平) |
對齊依據 | 索引 | 列或索引 | 左表列/索引 vs 右表索引 |
默認連接方式 | outer | inner | left |
典型用途 | 日志文件合并、時間序列拼接 | 主鍵關聯表(如訂單+用戶) | 屬性擴展(如 ID + 特征) |
三、常見實踐建議
推薦流程:
- 清理單個數據集 → 遵循整潔數據原則
- 統一列名與數據類型
- 根據關系選擇合并方式:
- 結構相同 →
pd.concat()
- 有公共鍵 →
pd.merge()
或df.join()
- 結構相同 →
- 合并后檢查:
- 行數是否合理?
- 是否出現意外的
NaN
? - 索引是否需要重置?
常見錯誤避免:
- 忘記
ignore_index=True
導致索引重復 - 直接
concat(df, series)
而未包裝成 DataFrame - 使用已棄用的
.append()
merge
時未指定on
,導致笛卡爾積- 忽視
how
參數,丟失數據(如默認 inner 丟掉不匹配行)
四、總結
數據整合三劍客
1. pd.concat()
- 用途:拼接多個對象(行/列)
- axis=0: 上下拼(常用)
- axis=1: 左右拼
- ignore_index=True 重置索引
2. pd.merge()
- 用途:基于列或索引合并兩個表
- on=‘key’: 指定連接列
- how: inner, outer, left, right
- 最靈活,推薦用于主鍵關聯
3. df.join()
- 用途:基于索引合并多個表
- 默認左連接
- 常用于主表 + 多個屬性表
Tip: 優先使用
concat
和merge
,join
適合索引對齊場景。
五、補充
1. 合并時處理重復列名(suffixes 參數)
當兩個 DataFrame 有相同列名但不是連接鍵時,merge
或 join
會自動添加后綴避免沖突。
left = pd.DataFrame({'key': ['K0', 'K1'], 'value': [1, 2]})
right = pd.DataFrame({'key': ['K0', 'K1'], 'value': [3, 4]})merged = pd.merge(left, right, on='key', suffixes=('_left', '_right'))
print(merged)
# 輸出:
# key value_left value_right
# 0 K0 1 3
# 1 K1 2 4
用途:比較同一指標在不同時間/來源的數據。
建議:始終顯式設置
suffixes
,避免默認的_x
,_y
造成混淆。
2. 基于多列合并(復合鍵合并)
有時需要多個字段共同作為“主鍵”來合并。
pd.merge(df1, df2, on=['date', 'city', 'product_id'], how='inner')
典型場景:
- 按“日期+地區”合并天氣與銷售數據
- 按“用戶ID+商品ID”合并評分與評論
注意:確保多列組合后能唯一標識一條記錄,否則可能產生笛卡爾積(行數暴增)。
3. 使用索引進行合并(left_index / right_index)
當數據以索引為唯一標識時(如時間序列、ID),可以直接用索引合并。
# 基于索引合并
result = pd.merge(df1, df2, left_index=True, right_index=True, how='outer')# 等價于:
result = df1.join(df2, how='outer')
優勢:避免創建冗余的 ID 列;適合時間對齊。
示例:對齊不同頻率的股票數據(日頻 + 周頻)
4. concat 的 join 參數:inner vs outer
pd.concat(..., axis=1)
默認是 outer
(外連接),但可通過 join='inner'
只保留共有的行。
# 只保留所有表都存在的行
pd.concat([df1, df2, df3], axis=1, join='inner')# 保留所有行(默認)
pd.concat([df1, df2, df3], axis=1, join='outer')
用途:
join='inner'
:嚴格對齊,避免 NaNjoin='outer'
:保留全部信息,后續填充缺失值
5. 合并性能優化建議
場景 | 建議 |
---|---|
大數據集合并 | 確保連接鍵是 category 或 int 類型(比 object 快) |
多次合并 | 先合并小表,再與大表連接 |
時間序列拼接 | 使用 pd.concat() + ignore_index=False 保持時間索引 |
內存不足 | 考慮使用 dask 或 polars 替代 |
小技巧:合并前檢查數據類型:
print(df1['key'].dtype)
print(df2['key'].dtype)
# 確保類型一致,否則可能導致匹配失敗!
6. 合并后的質量檢查清單(QA)
每次合并后建議檢查以下幾點:
def check_merge_result(left, right, merged, how):print(f"左表行數: {len(left)}")print(f"右表行數: {len(right)}")print(f"合并后行數: {len(merged)}")if how == 'inner':assert len(merged) <= min(len(left), len(right))elif how == 'left':assert len(merged) >= len(left)elif how == 'right':assert len(merged) >= len(right)elif how == 'outer':assert len(merged) >= max(len(left), len(right))print(f"新增 NaN 數量: {merged.isna().sum().sum()}")
關鍵問題:
- 行數是否合理?
- 是否出現大量
NaN
?是否預期? - 索引是否重復或混亂?
- 數據類型是否被意外轉換?
缺失值處理
為什么會出現缺失值?
在數據整合過程中,以下操作極易引入
NaN
:
pd.concat(..., axis=1)
拼接列時,行索引不完全對齊pd.merge(..., how='left')
左連接時,右表無匹配記錄join
擴展屬性時,某些 ID 沒有對應信息- 不同來源數據字段覆蓋不全
Pandas 使用 NaN
(Not a Number)表示浮點型缺失值,None
表示對象型缺失值,兩者在大部分操作中被視為等價。
一、識別缺失值
1. 檢查缺失情況
# 查看每個字段缺失數量
df.isnull().sum()# 查看整體缺失比例
df.isnull().mean() * 100# 查看是否有任意缺失
df.isnull().any().any()# 可視化缺失模式(需 seaborn)
import seaborn as sns
sns.heatmap(df.isnull(), cbar=True, yticklabels=False, cmap='viridis')
建議:在每次合并后立即檢查缺失情況。
二、缺失值的產生場景與應對策略
整合操作 | 缺失原因 | 建議處理方式 |
---|---|---|
concat(axis=0) | 不同文件字段不一致 | 統一列名 / 補充默認值 |
concat(axis=1) | 行索引不對齊 | 對齊索引 / 使用 join='inner' |
merge(how='left') | 右表無匹配鍵 | 檢查數據完整性 / 改用 outer |
join() | 某些 ID 無擴展信息 | 補充默認屬性 / 標記為“未知” |
三、處理缺失值的常用方法
1. 刪除缺失值(dropna
)
適用于:缺失嚴重且無法填補,或樣本足夠多。
# 刪除任意含缺失的行
df.dropna(axis=0, how='any')# 刪除所有值都缺失的列
df.dropna(axis=1, how='all')# 只在關鍵列缺失時刪除
df.dropna(subset=['user_id', 'order_date'])
風險:可能導致樣本偏差或信息丟失。
2. 填充缺失值(fillna
)
(1)填充固定值
df['category'] = df['category'].fillna('Unknown')
df['price'] = df['price'].fillna(0)
(2)前向/后向填充(適合時間序列)
df['value'] = df['value'].fillna(method='ffill') # 用前一個值填充
df['value'] = df['value'].fillna(method='bfill') # 用后一個值填充
method
參數已棄用,推薦使用ffill
/bfill
方法:df['value'] = df['value'].ffill()
(3)用統計量填充
# 數值型:均值、中位數
df['age'] = df['age'].fillna(df['age'].mean())
df['income'] = df['income'].fillna(df['income'].median())# 分類型:眾數
mode_value = df['gender'].mode()[0] # 取第一個眾數
df['gender'] = df['gender'].fillna(mode_value)
(4)用模型預測填充(高級)
from sklearn.impute import KNNImputer
imputer = KNNImputer(n_neighbors=5)
df[['age', 'income']] = imputer.fit_transform(df[['age', 'income']])
3. 插值法填充(interpolate
)
適用于:時間序列或有序數據。
# 線性插值
df['value'] = df['value'].interpolate(method='linear')# 時間序列插值(考慮時間間隔)
df['value'] = df.set_index('date')['value'].interpolate(method='time').values# 多項式插值(更平滑)
df['value'] = df['value'].interpolate(method='polynomial', order=2)
4. 標記缺失(創建指示變量)
有時“是否缺失”本身就是一個重要特征。
# 創建缺失標志列
df['age_missing'] = df['age'].isnull().astype(int)
df['price_missing'] = df['price'].isnull().astype(int)
用途:在機器學習中作為額外特征,幫助模型理解數據質量。
四、結合數據整合的完整流程示例
# 示例:合并用戶信息與訂單數據
users = pd.read_csv('users.csv') # 包含 user_id, age, city
orders = pd.read_csv('orders.csv') # 包含 order_id, user_id, amount# 步驟1:合并
merged = pd.merge(orders, users, on='user_id', how='left')# 步驟2:檢查缺失
print("合并后缺失情況:")
print(merged.isnull().sum())# 步驟3:處理缺失
merged['city'] = merged['city'].fillna('Unknown')
merged['age'] = merged['age'].fillna(merged['age'].median())# 步驟4:標記缺失(可選)
merged['age_was_missing'] = (merged['age'] == merged['age'].median()).astype(int)# 步驟5:驗證
assert merged.isnull().sum().sum() == 0 # 確保無缺失
五、缺失值處理策略選擇指南
場景 | 推薦方法 |
---|---|
缺失率 < 5% | 可考慮刪除行 |
數值型變量 | 均值/中位數填充,或插值 |
分類型變量 | 眾數填充,或新增“Unknown”類別 |
時間序列 | 前向填充、時間插值 |
關鍵字段無匹配 | 檢查數據源,避免錯誤合并 |
機器學習建模 | 結合 fillna + 缺失標志列 |
六、最佳實踐建議
黃金法則:
“寧可標記,也不要隨意刪除或填充。”
推薦流程
- 合并前:統一字段、數據類型、索引
- 合并后:立即檢查缺失分布
- 分析缺失機制:
- MCAR(完全隨機缺失)
- MAR(隨機缺失)
- MNAR(非隨機缺失)
- 選擇合適策略:根據業務邏輯決定如何處理
- 記錄處理過程:便于復現和審計
七、缺失值處理速查表
檢查
df.isnull().sum()
→ 查看各列缺失數df.isnull().mean()
→ 查看缺失比例
刪除
df.dropna()
→ 刪除含缺失的行df.dropna(subset=['col'])
→ 指定列刪除
填充
df['col'].fillna(0)
→ 填固定值df['col'].fillna(df['col'].mean())
→ 填均值df['col'].ffill()
→ 前向填充df['col'].interpolate()
→ 插值
標記
df['col_missing'] = df['col'].isnull().astype(int)