Kết hợp các tập dữ liệu trong Pandas - Pandas
Một trong những điều mà ai cũng phải làm trong Data Science đó là kết hợp các bộ dữ liệu từ các nguồn khác nhau. Tuỳ thuộc vào kiểu dữ liệu mà ta sẽ có các thao tác khác nhau, trong đó phổ biến nhất là việc kết hợp giữa những dữ liệu nhỏ, đơn giản hay kết hợp những tập dữ liệu lớn như database-style ...
Một trong những điều mà ai cũng phải làm trong Data Science đó là kết hợp các bộ dữ liệu từ các nguồn khác nhau. Tuỳ thuộc vào kiểu dữ liệu mà ta sẽ có các thao tác khác nhau, trong đó phổ biến nhất là việc kết hợp giữa những dữ liệu nhỏ, đơn giản hay kết hợp những tập dữ liệu lớn như database-style (giống như cơ sở dữ liệu SQL) để xử lý việc chồng chéo các tập dữ liệu với nhau.
Pandas xây dựng Series và DataFrame tương thích hoàn toàn với 2 kiểu kết hợp trên, với những phương thức và hàm hỗ trợ xử lý một cách nhanh chóng. Trong bài này chúng ta sẽ tìm hiểu về cách kết hợp dữ liệu trong Pandas với các phương thức concat, join và merge từ cơ bản đến nâng cao.
1. Kết hợp dữ liệu đơn giản (Concatenating)
Giới thiệu về phương thức concat trong Pandas
Ở bài Xử lý dữ liệu trên mảng cơ bản với Numpy, tại phần 5 mình đã giới thiệu về phương thức concatenate cũng như cơ chế hoạt động của nó trong NumPy. Về cơ bản thì Series và DataFrame khi nối với nhau không khác biệt mấy so với phương thức trên. Trong NumPy, ta có thể nối 3 mảng đơn giản như sau:
import pandas as pd import numpy as np x = [1, 2, 3, 4, 5] y = [2, 4, 5, 1, 2, 2] z = [2, 1, 4, 5, 6, 7, 8] print("Nối 3 mảng x y z: ", np.concatenate([x, y, z]))
Nối 3 mảng x y z: [1 2 3 4 5 2 4 5 1 2 2 2 1 4 5 6 7 8]
Hoặc với dữ liệu nhiều chiều thì ta có thể nối theo trục tương ứng:
x = [[1, 2], [2, 3]] y = [[3, 4], [5, 6]] print("Nối 2 mảng x y theo hàng: ", np.concatenate([x, y], axis=0)) print(" Nối 2 mảng x y theo cột: ", np.concatenate([x, y], axis=1))
Nối 2 mảng x y theo hàng: [[1 2] [2 3] [3 4] [5 6]] Nối 2 mảng x y theo cột: [[1 2 3 4] [2 3 5 6]]
Trong Pandas thì phương thức tương tự là concat và có cú pháp khá tương tự như concatenate trong NumPy:
pd.concat(objs, axis=0, join='outer', ignore_index=False, keys=None, levels=None, names=None, verify_integrity=False, copy=True)
- objs: Object tương ứng (Series / DataFrame)
- axis: {0,1,...} trục cần ghép nối
- join: {‘inner’, ‘outer’}, mặc định là ‘outer’
- ignore_index: {True, False}, mặc định là False. Nếu là True thì khi ghép nối sẽ bỏ qua index của object và đặt index theo thứ tự tương ứng với object kia.
- keys: mặc định là None. Tham số này nhận giá trị là một mảng hoặc tương tự, có tác dụng xây dựng đa chỉ mục (MultiIndex) khi ghép nối.
- levels: mặc định là None. Tham số này đặt các cấp cho MultiIndex tương ứng.
- names: mặc định là None. Tham số này đặt tên cho các MultiIndex tương ứng.
- verify_integrity: {True, False}, mặc định là False. Nếu là True thì sẽ kiểm tra xem có trùng lặp index không khi ghép hai object và sẽ báo lỗi nếu có.
- copy: {True, False}, mặc định là True. Nếu là False thì sẽ không sao chép những dữ liệu không cần thiết
Ta sẽ tìm hiểu từng tham số và tác dụng của nó cụ thể trong các mục nhỏ ở bên dưới. Giờ ta sẽ thử một ví dụ đơn giản là nối 2 series với phương thức này:
# Ghép 2 series đơn giản s1 = pd.Series(['F', 'r', 'e', 'e'], index=[1, 2, 3, 4]) s2 = pd.Series(['t', 'u', 't', 's'], index=[5, 6, 7, 8]) pd.concat([s1, s2])
1 F 2 r 3 e 4 e 5 t 6 u 7 t 8 s dtype: object
Hoặc nối 2 DataFrame với nhau:
A = pd.DataFrame([['A0', 'B0'], ['A1', 'B1']], columns=['A', 'B']) B = pd.DataFrame([['A3', 'B3'], ['A4', 'B4']], columns=['A', 'B']) print("Nối A và B theo hàng: ", pd.concat([A, B])) print(" Nối A và B theo cột: ", pd.concat([A, B], axis=1))
Nối A và B theo hàng: A B 0 A0 B0 1 A1 B1 0 A3 B3 1 A4 B4 Nối A và B theo cột: A B A B 0 A0 B0 A3 B3 1 A1 B1 A4 B4
Như ta có thể thấy là cách nối của Series / DataFrame không khác NumPy lắm, bên cạnh đó Pandas còn cung cấp khá nhiều tính năng mở rộng khác và chúng ta sẽ tìm hiểu trong các mục dưới đây.
Bỏ qua chỉ mục
Nếu bạn không quan tâm index của mình và chỉ cần nối dữ liệu thôi thì có thể sử dụng truyền vào tham số ignore_index với giá trị tương ứng là True, khi đó index sẽ được đặt lại theo thứ tự từ nhỏ đến lớn:
print("A: ", A) print(" A: ", B) print(" Nối theo hàng: ", pd.concat([A, B], ignore_index=True)) print(" Nối theo cột: ", pd.concat([A, B], ignore_index=True, axis=1))
A: A B 0 A0 B0 1 A1 B1 A: A B 0 A3 B3 1 A4 B4 Nối theo hàng: A B 0 A0 B0 1 A1 B1 2 A3 B3 3 A4 B4 Nối theo cột: 0 1 2 3 0 A0 B0 A3 B3 1 A1 B1 A4 B4
Kiểm tra index trùng lặp
Trong một số trường hợp ta không muốn index giống nhau của các object được nối vào vì sẽ gây chống chéo dữ liệu, thì ta có thể sử dụng đặt tham số verify_integrity=True, khi đó nếu như sự trùng lặp index xảy ra thì lập tức sẽ báo lỗi:
pd.concat([A, B], verify_integrity=True)
... ValueError: Indexes have overlapping values: Int64Index([0, 1], dtype='int64')
Thêm đa chỉ mục
Nếu ta muốn đặc nhãn cho từng object thêm vào để tiện phân biệt thì khi đó ta sẽ cần đến tham số keys:
print("A: ", A) print(" A: ", B) pd.concat([A, B], keys=['Dữ liệu A', 'Dữ liệu B'])
A: A B 0 A0 B0 1 A1 B1 A: A B 0 A3 B3 1 A4 B4 A B Dữ liệu A 0 A0 B0 1 A1 B1 Dữ liệu B 0 A3 B3 1 A4 B4
Sử dụng phương thức append
Nếu như muốn nối nhanh object trực tiếp từ object khác, ta có thể sử dụng phương thức append:
print("A: ", A) print(" A: ", B) print(" Append: ", A.append(B))
A: A B 0 A0 B0 1 A1 B1 A: A B 0 A3 B3 1 A4 B4 Append: A B 0 A0 B0 1 A1 B1 0 A3 B3 1 A4 B4
Một điều mà ta hay nhầm đó là khác với hàm append() của List trong Python, append trong Pandas không thay đổi giá trị của object gốc mà tạo một object mới:
C = A.append(B) print("C: ", C) print(" A: ", A)
C: A B 0 A0 B0 1 A1 B1 0 A3 B3 1 A4 B4 A: A B 0 A0 B0 1 A1 B1
Đây không phải là một phương pháp hiệu quả vì nó sẽ tốn thêm nhiều bộ nhớ hơn, do vậy nếu bạn cần nối nhiều object, đặc biệt là các object chứa dữ liệu lớn thì nên xây dựng tất cả chúng thành các DataFrame và nối chúng lại với nhau bằng phương thức concat.
2. Kết hợp dữ liệu dạng database-style (Merging)
Nếu bạn đã làm quen với dữ liệu SQL thì bạn sẽ nhanh chóng hiểu được cách mà dữ liệu được ghép nối với nhau theo cách này. Nếu chưa thì không sao, mình sẽ nói rõ bên dưới. Kiểu nối dữ liệu này được thực hiện bằng phương thức merge trong Pandas, các thao tác với merge dựa trên một số tập hợp quy tắc tuân theo Đại số tập hợp (Relational algebra) và có 3 trường hợp quan trọng mà các bạn cần phải biết:
- One-to-one: ví dụ: khi nối hai DataFrame trên các index của chúng (phải chứa các giá trị duy nhất).
- Many-to-one và one-to-many: ví dụ: khi nối một index (duy nhất) với một hoặc nhiều cột trong một DataFrame
- Many-to-many: nối cột với cột
Chúng ta sẽ cùng tìm hiểu các ví dụ về 3 kiểu nối này.
One-to-one (1-1)
Phép nối này là một trong những kiểu nối đơn giản nhất, nó về cơ bản khá giống với việc dùng phương thức concat theo các cột với nhau. Quan hệ one-to-one (1-1) về bản chất nghĩa là với mỗi bản ghi ở bảng này thì ta sẽ một bạn ghi tương ứng ở bảng kia. Giả sử ta có 2 DataFrame chứa dữ liệu CTV làm việc ở Zaidap.com như sau:
# ctv1: dữ liệu ctv tương ứng với các mảng đang phụ trách ctv1 = pd.DataFrame({'ctv': ['Minh', 'Tuan', 'An', 'Hoa'], 'category': ['Python', 'Vue', 'Javascript', 'React Native']}) # ctv2: dữ liệu ctv tương ứng với thời gian tham gia ctv2 = pd.DataFrame({'ctv': ['Tuan', 'Minh', 'Hoa', 'An'], 'join_date': [2020, 2019, 2007, 2014]}) print("ctv1: ", ctv1) print("ctv2: ", ctv2)
ctv1: ctv category 0 Minh Python 1 Tuan Vue 2 An Javascript 3 Hoa React Native ctv2: ctv join_date 0 Tuan 2020 1 Minh 2019 2 Hoa 2007 3 An 2014
Để kết hợp 2 DataFrame này với nhau, ta sẽ dùng phương thức merge đơn giản như sau:
ctv = pd.merge(ctv1, ctv2) ctv
ctv category join_date 0 Minh Python 2019 1 Tuan Vue 2020 2 An Javascript 2014 3 Hoa React Native 2007
Như ta thế với mỗi CTV của Zaidap.com làm việc ở một chủ đề nhất định (bảng 1) thì sẽ có giá trị tương ứng với thời gian họ tham gia (bảng 2). Phương thức merge sẽ tự động nhận ra cột "ctv" giống nhau ở hai DataFrame và dùng cột này làm key (key column) để ghép các cột và hàng khác tương đương lại với nhau. Một điều bạn nên nhớ là khi merge theo kiểu này thì index sẽ bị loại bỏ, chỉ trừ một số trường hợp sẽ nói ở phần sau.
One-to-many (1-n) và Many-to-one (n-1)
Quan hệ many-to-one (n-1) về cơ bản là ngược lại với quan hệ one-to-many (1-n). 1-n nghĩa là một bản ghi ở bảng này có thể liên kết với nhiều bản ghi ở bảng kia, chẳng hạn mỗi khách hàng có thể có nhiều đơn hàng, còn n-1 chính là nhiều đơn hàng cùng thuộc về một khách hàng (nhiều bản ghi ở bảng này liên kết với một bản ghi ở bảng kia). Trong Zaidap.com, nhiều chuyên mục có thể thuộc cùng 1 nhóm bài (n-1), vậy để ta có thể tìm nhóm bài của từng CTV đang viết như sau:
group = pd.DataFrame({'category': ['Python', 'Vue', 'Javascript, 'React Native'], 'group': ['Programming', 'Web Frontend', 'Web Frontend', 'Mobile Dev']}) print("group: ", group) print(" merge ctv & group: ", pd.merge(ctv, group))
group: category group 0 Python Programming 1 Vue Web Frontend 2 Javascript Web Frontend 3 React Native Mobile Dev merge ctv & group: ctv category join_date group 0 Minh Python 2019 Programming 1 Tuan Vue 2020 Web Frontend 2 An Javascript 2014 Web Frontend 3 Hoa React Native 2007 Mobile Dev
Many-to-many
Quan hệ many-to-many (n-n) xảy ra khi nhiều bản ghi trong bảng được liên kết với nhiều bản ghi trong bảng khác, ví dụ như mỗi nhóm sẽ yêu cầu CTV phải có 1 số kỹ năng, và 1 kỹ năng cũng có thể xuất hiện ở nhiều nhóm khác nhau, chẳng hạn ta có DataFrame skill chứa dữ liệu các kỹ năng cần khi làm CTV ở từng nhóm:
skills = pd.DataFrame({'group': ['Programming', 'Programming', 'Programming', 'Web Frontend', 'Web Frontend', 'Web Frontend', 'Mobile Dev', 'Mobile Dev', 'Mobile Dev'], 'skills': ['Coding', 'Algorithm', 'Data Structure', 'Coding', 'HTML5', 'CSS3', 'Coding', 'Android', 'iOS' ]}) skills
group skills 0 Programming Coding 1 Programming Algorithm 2 Programming Data Structure 3 Web Frontend Coding 4 Web Frontend HTML5 5 Web Frontend CSS3 6 Mobile Dev Coding 7 Mobile Dev Android 8 Mobile Dev iOS
Và để biết được kỹ năng của từng CTV, ta chỉ cần ghép 2 DataFrame lại:
ctv3 = pd.merge(ctv, group) ctv4 = pd.merge(ctv3, skills) print("Nhóm tương ứng của các CTV: ", ctv3) print(" Kỹ năng của các CTV: ", ctv4)
Nhóm tương ứng của các CTV: ctv category join_date group 0 Minh Python 2019 Programming 1 Tuan Vue 2020 Web Frontend 2 An Javascript 2014 Web Frontend 3 Hoa React Native 2007 Mobile Dev Kỹ năng của các CTV: ctv category join_date group skills 0 Minh Python 2019 Programming Coding 1 Minh Python 2019 Programming Algorithm 2 Minh Python 2019 Programming Data Structure 3 Tuan Vue 2020 Web Frontend Coding 4 Tuan Vue 2020 Web Frontend HTML5 5 Tuan Vue 2020 Web Frontend CSS3 6 An Javascript 2014 Web Frontend Coding 7 An Javascript 2014 Web Frontend HTML5 8 An Javascript 2014 Web Frontend CSS3 9 Hoa React Native 2007 Mobile Dev Coding 10 Hoa React Native 2007 Mobile Dev Android 11 Hoa React Native 2007 Mobile Dev iOS
Các tuỳ chọn khi sử dụng phương thức merge
Tham số on, left_on và right_on
Ta dùng tham số on khi muốn chỉ định rõ cột nào trở thành key column khi ghép các DataFrame lại với nhau:
pd.merge(ctv1, ctv2, on='ctv')
ctv category join_date 0 Minh Python 2019 1 Tuan Vue 2020 2 An Javascript 2014 3 Hoa React Native 2007
Điều kiện để tham số này sử dụng được là ở 2 object phải có 2 cột giống tên với nhau. Nếu bạn muốn ghép 2 object có tên 2 cột khác nhau thì ta sẽ dùng tham số left_on và right_on:
ctv_salary = pd.DataFrame({'name': ['Minh', 'An', 'Tuan', 'Hoa'], 'salary': [5000, 3000, 3500, 2000]}) print("Lương của CTV: ", ctv_salary) print(" Lương của CTV tương ứng với từng chuyên mục: ", pd.merge(ctv1, ctv_salary, left_on='ctv', right_on='name'))
Lương của CTV: name salary 0 Minh 5000 1 An 3000 2 Tuan 3500 3 Hoa 2000 Lương của CTV tương ứng với từng chuyên mục: ctv category name salary 0 Minh Python Minh 5000 1 Tuan Vue Tuan 3500 2 An Javascript An 3000 3 Hoa React Native Hoa 2000
Để loại bỏ cột "name" dư thừa ta dùng phương thức drop:
pd.merge(ctv1, ctv_salary, left_on='ctv', right_on='name').drop('name', axis=1)
ctv category salary 0 Minh Python 5000 1 Tuan Vue 3500 2 An Javascript 3000 3 Hoa React Native 2000
Tham số left_index và right_index
Thi thoảng ta không muốn ghép dữ liệu trên cột mà lại muốn dùng index, ví dụ như dữ liệu của ta thế này:
ctv1i = ctv1.set_index('ctv') ctv2i = ctv2.set_index('ctv') print("ctv1i: ", ctv1i) print(" ctv2i: ", ctv2i)
ctv1i: category ctv Minh Python Tuan Vue An Javascript Hoa React Native ctv2i: join_date ctv Tuan 2020 Minh 2019 Hoa 2007 An 2014
Và lúc này ta sẽ dùng index làm key để ghép 2 DataFrame lại với nhau:
ctv3i = pd.merge(ctv1i, ctv2i, left_index=True, right_index=True) ctv3i
category join_date ctv Minh Python 2019 Tuan Vue 2020 An Javascript 2014 Hoa React Native 2007
Sử dụng phương thức join
Khá giống với append bên trên, ta dùng join khi muốn ghép nhanh trực tiếp trên object khi dùng index:
ctv1i.join(ctv2i)
category join_date ctv Minh Python 2019 Tuan Vue 2020 An Javascript 2014 Hoa React Native 2007
Kết hợp giữa index và cột
Nếu ta muốn ghép mà sử dụng cả index và cột thì chỉ cần kết hợp 2 kiểu tham số trên, chẳng hạn:
print("ctv2i: ", ctv2i) print("ctv_salary: ", ctv_salary) pd.merge(ctv2i, ctv_salary, left_index=True, right_on='name')
ctv2i: join_date ctv Tuan 2020 Minh 2019 Hoa 2007 An 2014 ctv_salary: name salary 0 Minh 5000 1 An 3000 2 Tuan 3500 3 Hoa 2000 join_date name salary 2 2020 Tuan 3500 0 2019 Minh 5000 3 2007 Hoa 2000 1 2014 An 3000
Tham số how
Ta xét ví dụ sau:
ctv_location = pd.DataFrame({'ctv': ['Minh', 'Tuan', 'Hoa'], 'location': ['Ha Noi', 'TP.HCM', 'Lam Dong']}) ctv_age = pd.DataFrame({'ctv': ['Minh', 'An', 'Hoa'], 'age': [18, 19, 20]}) print("Địa chỉ của các CTV: ", ctv_location) print(" Tuổi của các CTV: ", ctv_age) print(" Địa chỉ và tuổi của CTV: ", pd.merge(ctv_location, ctv_age))
Địa chỉ của các CTV: ctv location 0 Minh Ha Noi 1 Tuan TP.HCM 2 Hoa Lam Dong Tuổi của các CTV: ctv age 0 Minh 18 1 An 19 2 Hoa 20 Địa chỉ và tuổi của CTV: ctv location age 0 Minh Ha Noi 18 1 Hoa Lam Dong 20
Mặc định khi ghép dữ liệu bằng merge, Pandas sẽ chỉ chọn những dữ liệu xuất hiện ở cả 2 DataFrame. Như ví dụ trên, output chỉ có 2 CTV là Minh và Hoa do chỉ có dữ liệu của 2 người đầy đủ. Chúng ta có thể chỉ định kiểu ghép này rõ ràng bằng cách dùng tham số how='inner':
print("Địa chỉ và tuổi của CTV: ", pd.merge(ctv_location, ctv_age, how='inner'))
Địa chỉ và tuổi của CTV: ctv location age 0 Minh Ha Noi 18 1 Hoa Lam Dong 20
Nếu như muốn output chứa tất cả dữ liệu từ 2 DataFrame thì ta có thể dùng tham số how='outer', khi đó các dữ liệu bị thiếu sẽ được điền giá trị NA tương ứng, trong trường hợp này là NaN:
print("Địa chỉ và tuổi của CTV: ", pd.merge(ctv_location, ctv_age, how='outer'))
Địa chỉ và tuổi của CTV: ctv location age 0 Minh Ha Noi 18.0 1 Tuan TP.HCM NaN 2 Hoa Lam Dong 20.0 3 An NaN 19.0
Hoặc nếu như ta muốn chỉ định output phụ thuôc vào DataFrame bên trái hoặc bên phải thì khi đó ta dùng tham số how='left' hoặc how='right' tương ứng:
print("Địa chỉ và tuổi của CTV (left) ", pd.merge(ctv_location, ctv_age, how='left')) print(" Địa chỉ và tuổi của CTV (right) ", pd.merge(ctv_location, ctv_age, how='right'))
Địa chỉ và tuổi của CTV (left) ctv location age 0 Minh Ha Noi 18.0 1 Tuan TP.HCM NaN 2 Hoa Lam Dong 20.0 Địa chỉ và tuổi của CTV (right) ctv location age 0 Minh Ha Noi 18 1 An NaN 19 2 Hoa Lam Dong 20
Tham số suffixes
Một số trường hợp mà dữ liệucủa bạn có 2 cột trùng tên nhưng không tương tự về mục đích (xung đột) thì nếu ghép 2 DataFrame lại thì Pandas sẽ tự động thêm hậu tố tương ứng để phân biệt, chẳng hạn ta có dữ liệu xếp hạng số lượt view và like của các CTV ở Zaidap.com như sau:
view_rank = pd.DataFrame({'ctv': ['Minh', 'Tuan', 'An', 'Hoa'], 'rank': [1, 3, 2, 4]}) like_rank = pd.DataFrame({'ctv': ['Minh', 'Tuan', 'An', 'Hoa'], 'rank': [4, 2, 1, 3]}) print("Xếp hạng CTV trên lượt xem: ", view_rank) print(" Xếp hạng CTV trên lượt xem: ", like_rank) print(" Bảng xếp hạng: ", pd.merge(view_rank, like_rank, on='ctv'))
Xếp hạng CTV trên lượt xem: ctv rank 0 Minh 1 1 Tuan 3 2 An 2 3 Hoa 4 Xếp hạng CTV trên lượt xem: ctv rank 0 Minh 4 1 Tuan 2 2 An 1 3 Hoa 3 Bảng xếp hạng: ctv rank_x rank_y 0 Minh 1 4 1 Tuan 3 2 2 An 2 1 3 Hoa 4 3
Như ta thấy Pandas đã thêm hậu tố _x và _y vào hai cột "rank" để tránh xung đột. Ta có thể tuỳ chỉnh hậu tố bằng cách sử dụng tham số suffixes như sau:
print("Bảng xếp hạng: ", pd.merge(view_rank, like_rank, on='ctv', suffixes=['(view)', '(like)']))
Bảng xếp hạng: ctv rank(view) rank(like) 0 Minh 1 4 1 Tuan 3 2 2 An 2 1 3 Hoa 4 3
3. Tổng kết
Vậy là chúng ta đã hoàn thành và làm quen được với các phương thức ghép nối dữ liệu trong Pandas. Thư viện này hỗ trợ rất mạnh mẽ các kiểu ghép nối dữ liệu, dù là đơn giản hay phức tạp. Trong bài tiếp theo chúng ta sẽ học về Thống kê và nhóm dữ liệu với Pandas, hẹn gặp các bạn ở bài tiếp nhé.