Để thực hành viết code theo tài liệu này cũng như để làm các bài tập, sử dụng Google Colab notebook sau đây: https://colab.research.google.com/drive/1b7Tb6MAacgKyviJF2TsMV3N18Ys8n4Nc?usp=sharing
Numpy và Pandas là hai thư viện mạnh mẽ trong Python và được sử dụng rộng rãi trong phân tích dữ liệu.
Numpy (Numerical Python) là 1 thư viện đóng vai trò nền tảng cho việc tính toán số học trong Python. Nó cung cấp ndarray (mảng đa chiều) và các công cụ để làm việc với mảng này như thực hiện các phép tính toán số học trên mảng, xử lý ma trận, tạo các dãy số ngẫu nhiên và nhiều chức năng khác. Các tính toán được thực hiện trên toàn bộ mảng (thay vì phải dùng vòng lặp và thực hiện trên từng phần tử) nên tốc độ tính toán nhanh hơn là sử dụng Python list.
Pandas là 1 thư viện cung cấp các cấu trúc dữ liệu hiệu suất cao và các công cụ phân tích dữ liệu cho Python. Trong Pandas ta cần lưu ý 2 cấu trúc dữ liệu mà Python cơ bản không có, đó là Series và DataFrame, cả hai cấu trúc này đều được xây dựng trên Numpy. Pandas rất hữu ích cho việc xử lý và phân tích dữ liệu có cấu trúc (dạng bảng, ví dụ như load từ bảng Excel hoặc SQL databases, …).
Series: Một series là 1 object dạng mảng 1 chiều và có index
DataFrame: DataFrame trong Pandas là cấu trúc dữ liệu hai chiều, nó giống như 1 bảng dữ liệu trong SQL, cũng có hàng và cột. Hàng và cột đều có index.
Ví dụ ta tạo DataFrame từ Series và Python list như sau:
import numpy as np
import pandas as pd
cust_id_ndarray = np.array((1, 2, 3, 4, 5, 6, 7, 8, 9, 10))
cust_name_list = ["Nguyen Van A", "Nguyen Van B", "Le Thi C", "Le Thi D", "Le Thi E", "Nguyen Van F", "Le Thi G", "Le Thi H", "Le Thi I", "Le Thi J"]
cust_income_list = [50, 50, 600, 200, None, None, None, 100, None, None]
cust_vip_flag = [True, False, None, None, None, None, None, False, None, False]
customer_data_dict = {"customer_id":cust_id_ndarray,
"customer_name":cust_name_list,
"customer_income":cust_income_list,
"vip_flag":cust_vip_flag}
my_df = pd.DataFrame(customer_data_dict)
my_df## customer_id customer_name customer_income vip_flag
## 0 1 Nguyen Van A 50.0 True
## 1 2 Nguyen Van B 50.0 False
## 2 3 Le Thi C 600.0 None
## 3 4 Le Thi D 200.0 None
## 4 5 Le Thi E NaN None
## 5 6 Nguyen Van F NaN None
## 6 7 Le Thi G NaN None
## 7 8 Le Thi H 100.0 False
## 8 9 Le Thi I NaN None
## 9 10 Le Thi J NaN False
Bài học này sẽ tập trung vào các nội dung kiến thức quan trọng trong sử dụng Pandas để biến đổi và xử lý dữ liệu.
Cũng giống như khi sử dụng bảng trong SQL, trước khi sử dụng dữ liệu trong một Pandas DataFrame thì ta cũng cần kiểm tra dữ liệu trong DataFrame đó. Pandas DataFrame có sẵn các attribute và method giúp ta kiểm tra dữ liệu nhanh gọn và tiện lợi hơn khi sử dụng SQL.
Hiện nay đã có nhiều library giúp việc data profiling nhanh và tự động hơn, như Pandas Profiling, AutoViz, … tuy nhiên chúng sẽ không được đề cập đến trong khuôn khổ khoá học này vì chúng ta cần biết cách thực hiện data profiling với những công cụ có sẵn trong pandas để vừa hiểu rõ hơn cách pandas hoạt động và vừa chủ động hơn, không bị lệ thuộc vào thêm nhiều library khác (đề phòng môi trường làm việc được cài đặt tại cơ quan của bạn không cho phép cài đặt thêm các library này chẳng hạn).
Method này giúp ta xem nhanh một vài dòng đầu tiên của DataFrame, mặc định là 5 dòng
## customer_id customer_name customer_income vip_flag
## 0 1 Nguyen Van A 50.0 True
## 1 2 Nguyen Van B 50.0 False
## 2 3 Le Thi C 600.0 None
## 3 4 Le Thi D 200.0 None
## 4 5 Le Thi E NaN None
Method này giúp ta xem nhanh một vài dòng cuối cùng của DataFrame, mặc định là 5 dòng
## customer_id customer_name customer_income vip_flag
## 7 8 Le Thi H 100.0 False
## 8 9 Le Thi I NaN None
## 9 10 Le Thi J NaN False
Attribute này giúp ta xem tên các cột và kiểu dữ liệu của chúng
## customer_id int64
## customer_name object
## customer_income float64
## vip_flag object
## dtype: object
2 method này thực chất là 1, và chúng kiểm tra tất cả các giá trị trong DataFrame có phải là giá trị trống hay không (missing values hay còn gọi là NA values)
## customer_id 0
## customer_name 0
## customer_income 5
## vip_flag 6
## dtype: int64
## customer_id 0
## customer_name 0
## customer_income 5
## vip_flag 6
## dtype: int64
Attribute này giúp ta kiểm tra số dòng và số cột của bảng
## (10, 4)
Method này giúp ta xem được các thông tin về thống kê mô tả (descriptive statistics) của dữ liệu trong tất cả các cột của bảng (ta có thể lựa chọn trả ra thông tin của tất cả các cột hoặc chỉ của các cột có kiểu dữ liệu nhất định).
## customer_id customer_name customer_income vip_flag
## count 10.00000 10 5.000000 4
## unique NaN 10 NaN 2
## top NaN Nguyen Van A NaN False
## freq NaN 1 NaN 3
## mean 5.50000 NaN 200.000000 NaN
## std 3.02765 NaN 231.840462 NaN
## min 1.00000 NaN 50.000000 NaN
## 25% 3.25000 NaN 50.000000 NaN
## 50% 5.50000 NaN 100.000000 NaN
## 75% 7.75000 NaN 200.000000 NaN
## max 10.00000 NaN 600.000000 NaN
Kết quả trả ra bao gồm các thông tin:
Với các cột dạng số (tất cả các chỉ số đều được tính toán sau khi loại bỏ các giá trị bị thiếu: None, NaN):
count: đếm số dòng
mean: giá trị trung bình
std: standard deviation (độ lệch chuẩn)
min: giá trị nhỏ nhất
25%: giá trị của tứ phân vị thứ nhất (first quartile - Q1)
50%: giá trị của trung vị (median)
75%: giá trị của tứ phân vị thứ 3 (third quartile - Q3)
max: giá trị lớn nhất
Với các cột dạng không phải số:
count: đếm số dòng (không tính các giá trị None)
unique: số lượng các giá trị duy nhất (không trùng lặp) (không tính giá trị None)
top: giá trị xuất hiện nhiều nhất (không tính giá trị None)
freq: số lần xuất hiện của giá trị ở mục “top”
Attribute này giúp ta kiểm tra tên các cột trong bảng
## Index(['customer_id', 'customer_name', 'customer_income', 'vip_flag'], dtype='object')
Khi làm việc với pandas DataFrame ta sẽ rất hay gặp khái niệm axis (vd: lệnh DataFrame.drop() có tham số axis nhận giá trị 0 hoặc 1, nếu nhận 0 thì thực hiện xoá dòng, nhận 1 thì thực hiện xoá cột, …). Ta chỉ cần nhìn vào hình minh hoạ dưới đây và nhớ rằng khi nói đến axis=0 thì là đang nói đến dòng, còn axis=1 là đang nói đến cột trong DataFrame
Khi làm việc với Pandas DataFrame, thường các DataFrame methods sẽ biến đổi dữ liệu và tạo ra 1 DataFrame mới. Tuy nhiên với 1 số methods ta có thể khai báo parameter inplace=True để thực hiện biến đổi dữ liệu trên chính DataFrame đó. Việc thực hiện biến đổi ngay trên chính DataFrame (inplace) trên lý thuyết là để tránh việc tạo ra 1 bản copy nữa của DataFrame, làm cho việc biến đổi dữ liệu chậm hơn (vì phải copy data) và tốn thêm bộ nhớ (memory). Tuy nhiên nó tiềm ẩn rủi ro khiến ta xử lý biến đổi dữ liệu sai và không quay lại trạng thái lúc trước của DataFrame được mà phải biến đổi lại từ đầu. Ngoài ra, nhiều inplace operation thực tế vẫn ngầm tạo ra các bản copy của dữ liệu nên không giúp ích được gì cho việc tăng tốc độ xử lý. Vì vậy, ta hết sức lưu ý khi dùng các biến đổi inplace, và với người mới bắt đầu, hãy cố gắng tránh sử dụng.
Best practice để lọc dữ liệu trong pandas là sử dụng 2 properties là .loc và .iloc. Chúng giúp ta lọc dữ liệu theo nhiều cách thức khác nhau và giúp code lọc dữ liệu trở nên rõ ràng hơn.
Dù dùng .loc hay .iloc thì thứ tự trong syntax sẽ luôn là điều kiện lọc dòng đứng trước rồi mới đến điều kiện lọc cột
# lọc dữ liệu khách hàng có income < 200 hoặc income >= 600
# và lấy ra 2 cột là customer_id và customer_income
my_df.loc[(my_df.customer_income < 200) | (my_df.customer_income >= 600), ["customer_id", "customer_income"]]## customer_id customer_income
## 0 1 50.0
## 1 2 50.0
## 2 3 600.0
## 7 8 100.0
Lưu ý: khi sử dụng nhiều hơn 1 điều kiện lọc (ví dụ: A và B, A hoặc B, …) thì hãy luôn tuân theo best practice là bọc từng điều kiện lọc trong ngoặc đơn. Điều này giúp câu lệnh không bị lỗi do Python có thứ tự ưu tiên xử lý các toán tử (operator) nhất định, và nếu ta không để ý khi viết điều kiện thì rất dễ là thứ tự mà Python xử lý sẽ không giống với thứ tự mà ta mong muốn.
Ví dụ: hãy thử chạy câu lệnh trên nhưng không có ngoặc đơn để phân tách 2 điều kiện, câu lệnh sẽ báo lỗi. Lí do là vì thứ tự xử lý của Python sẽ là: điều kiện my_df.customer_income < 200, sau đó đến toán tử “|” (hoặc), rồi mới đến toán tử >=
Xem code dưới đây để hiểu rõ vì sao câu lệnh bị lỗi
# 2 câu lệnh sau đây sẽ bị lỗi giống hệt nhau
# lệnh 1
my_df.loc[my_df.customer_income < 200 | my_df.customer_income >= 600, ["customer_id", "customer_income"]]## Cannot perform 'ror_' with a dtyped [float64] array and scalar of type [bool]
# lệnh 2
my_df.loc[(my_df.customer_income < 200 | my_df.customer_income) >= 600, ["customer_id", "customer_income"]]## Cannot perform 'ror_' with a dtyped [float64] array and scalar of type [bool]
# lọc dữ liệu khách hàng có income >= 100 và < 600 và lấy ra tất cả các cột
my_df.loc[(my_df.customer_income >= 100) & (my_df.customer_income < 600), :]## customer_id customer_name customer_income vip_flag
## 3 4 Le Thi D 200.0 None
## 7 8 Le Thi H 100.0 False
## customer_id customer_income
## 0 1 50.0
## 1 2 50.0
## 2 3 600.0
## 3 4 200.0
## 4 5 NaN
## 5 6 NaN
## 6 7 NaN
## 7 8 100.0
## 8 9 NaN
## 9 10 NaN
# tạo DataFrame my_df_2 giống hệt my_df nhưng không có index = 2
my_df_2 = my_df.copy()
my_df_2 = my_df_2.set_index(pd.Index([0, 1, 3, 4, 5, 6, 7, 8, 9, 10]))
my_df_2## customer_id customer_name customer_income vip_flag
## 0 1 Nguyen Van A 50.0 True
## 1 2 Nguyen Van B 50.0 False
## 3 3 Le Thi C 600.0 None
## 4 4 Le Thi D 200.0 None
## 5 5 Le Thi E NaN None
## 6 6 Nguyen Van F NaN None
## 7 7 Le Thi G NaN None
## 8 8 Le Thi H 100.0 False
## 9 9 Le Thi I NaN None
## 10 10 Le Thi J NaN False
# lọc dữ liệu của my_df_2, lấy ra các dòng có index với giá trị từ 1 đến 4
# (bao gồm cả 1 và 4) và lấy các cột từ customer_id đến cột customer_income
my_df_2.loc[1:4, "customer_id":"customer_income"]## customer_id customer_name customer_income
## 1 2 Nguyen Van B 50.0
## 3 3 Le Thi C 600.0
## 4 4 Le Thi D 200.0
Lưu ý: hãy nhớ rằng .loc giúp ta lọc dữ liệu theo giá trị của index chứ không phải theo vị trí của index, nên câu lệnh bên trên giúp ta lọc ra các dòng có index “với giá trị” trong khoảng từ 1 đến 4 (là các dòng có index bằng 1, 3, 4) chứ không phải là lọc ra các dòng với index có thứ tự số 1, 2, 3, 4 (tương ứng với các dòng có index là 1, 3, 4, 5 trong my_df_2)
# lọc lấy các dòng có index thứ tự từ 0 đến 1
# (không lấy 2, giống cách slicing trong Numpy và trong Python list)
# và lấy các cột có vị trí (index) từ 1 đến 2 (không lấy 3).
# Cột customer_id là cột có index = 0
my_df_2.iloc[0:2, 1:3]## customer_name customer_income
## 0 Nguyen Van A 50.0
## 1 Nguyen Van B 50.0
# lọc lấy các dòng có index thứ tự từ 0 đến 1
# (không lấy 2, giống cách slicing trong Numpy và trong Python list)
# và lấy các cột có index = 1 và = 3
my_df_2.iloc[0:2, [1, 3]]## customer_name vip_flag
## 0 Nguyen Van A True
## 1 Nguyen Van B False
Nhờ .loc và .iloc mà ta có thể lọc được dữ liệu rất linh hoạt và dễ dàng hơn nhiều so với khi sử dụng SQL như trong một số yêu cầu dưới đây
## customer_id customer_name customer_income
## 0 1 Nguyen Van A 50.0
## 1 2 Nguyen Van B 50.0
## 2 3 Le Thi C 600.0
## 3 4 Le Thi D 200.0
## 4 5 Le Thi E NaN
## 5 6 Nguyen Van F NaN
## 6 7 Le Thi G NaN
## 7 8 Le Thi H 100.0
## 8 9 Le Thi I NaN
## 9 10 Le Thi J NaN
# lấy ra tất cả các cột chỉ trừ 2 cột là customer_name và vip_flag
my_df_2.loc[:, ~my_df_2.columns.isin(["customer_name", "vip_flag"])]## customer_id customer_income
## 0 1 50.0
## 1 2 50.0
## 3 3 600.0
## 4 4 200.0
## 5 5 NaN
## 6 6 NaN
## 7 7 NaN
## 8 8 100.0
## 9 9 NaN
## 10 10 NaN
# lấy ra tất cả các cột với kiểu dữ liệu dạng số (integer hay float đều là dạng số)
my_df_2.select_dtypes(include="number")## customer_id customer_income
## 0 1 50.0
## 1 2 50.0
## 3 3 600.0
## 4 4 200.0
## 5 5 NaN
## 6 6 NaN
## 7 7 NaN
## 8 8 100.0
## 9 9 NaN
## 10 10 NaN
Khi thực hiện nhóm dữ liệu và tính toán với pandas thì ta cần lưu ý những điểm sau đây:
Tương tự như với SQL, ta nên lọc dữ liệu trước (ở mệnh đề WHERE của SQL) rồi mới nhóm dữ liệu (mệnh đề GROUP BY) khi có thể vì nó sẽ làm giảm số lượng bản ghi cần phải xử lý, từ đó làm tăng tốc độ xử lý của câu lệnh. Tuy rằng Python lưu dữ liệu vào memory (hay ta hiểu chính là vào RAM của máy tính) nên tốc độ xử lý nhìn chung là nhanh, nhưng điều này cũng dẫn tới hạn chế đó là nếu có quá nhiều dữ liệu được lưu vào memory sẽ có thể dẫn đến tình trạng tràn bộ nhớ, khiến Python bị crash và ta phải reset lại. Vì vậy, hãy luôn lọc dữ liệu để làm nhỏ bớt lượng dữ liệu trước khi xử lý nếu có thể.
Có nhiều cách viết code để nhóm dữ liệu của DataFrame và tính toán, tuy nhiên cách sau đây là một trong những cách tiện lợi, dễ hiểu và có tính ứng dụng cao nhất. Đoạn code sau đây cũng thể hiện các hàm dùng để tính toán những chỉ số hay dùng khi groupby:
đếm số dòng: size
đếm số dòng nhưng loại trừ các giá trị bị thiếu (None, NaN, NA): count
đếm số lượng các giá trị unique: nunique
tính tổng: sum (tương tự là các hàm tính min, max, mean, …)
Lưu ý: tham số as_index mặc định nhận giá trị True, khi đó cột được sử dụng để làm key khi nhóm dữ liệu sẽ trở thành index của DataFrame. Ta set as_index = False khi muốn cột dùng làm key khi nhóm dữ liệu không trở thành index của DataFrame mà trở thành 1 cột bình thường trong DataFrame.
my_df_2.groupby(
"vip_flag", as_index=False
).agg(
row_count = ("customer_income", "size"),
rn_cnt_not_none = ("customer_income", "count"),
cnt_unique_not_none = ("customer_income", "nunique"),
total_income = ("customer_income", "sum"),
).sort_values(
by="total_income", ascending=True
)## vip_flag row_count rn_cnt_not_none cnt_unique_not_none total_income
## 1 True 1 1 1 50.0
## 0 False 3 2 2 150.0
Để làm việc với kiểu dữ liệu ngày tháng và thời gian trong Python thì ta sẽ cần biết sử dụng cơ bản module datetime.
Thường ta sẽ cần import Class datetime từ module datetime, viết lệnh như sau:
Trong class datetime ta có một số kiểu dữ liệu chính như sau: date, time, datetime, timedelta, tzinfo, timezone
Đọc thêm tại đây: https://docs.python.org/3/library/datetime.html#
Ta xem ví dụ dưới đây hiểu được nhanh chóng và căn bản cách sử dụng các kiểu dữ liệu này
from datetime import datetime
# ta có date và datetime dưới dạng string
my_date_str = "2023-11-21"
my_datetime_str = "2023-12-01 05:03:42"
# convert string thành kiểu dữ liệu date và datetime
my_date = datetime.strptime(my_date_str, "%Y-%m-%d").date()
my_datetime = datetime.strptime(my_datetime_str, "%Y-%m-%d %H:%M:%S")
print(my_date)## 2023-11-21
## <class 'datetime.date'>
## 2023-12-01 05:03:42
## <class 'datetime.datetime'>
# tính khoảng cách giữa 2 ngày, kết quả trả ra là 1 timedelta object
print(my_datetime.date() - my_date)## 10 days, 0:00:00
## <class 'datetime.timedelta'>
# tính khoảng cách giữa 2 datetime (ngày giờ), kết quả trả ra là 1 timedelta object
my_datetime - datetime.strptime("2023-11-21 22:13:25", "%Y-%m-%d %H:%M:%S")## datetime.timedelta(days=9, seconds=24617)
## <class 'datetime.timedelta'>
# cộng, trừ ngày giờ bằng cách sử dụng timedelta object
from datetime import timedelta
my_datetime + timedelta(days=1, seconds=10)## datetime.datetime(2023, 12, 2, 5, 3, 52)
Tương tự như đã đề cập tại khoá học Job Seekers, best practice là nên lưu và làm việc với dữ liệu ngày tháng và thời gian theo tiêu chuẩn ISO standard 8601 for date format.
Thường ta sẽ load dữ liệu từ 1 nguồn nào đó vào pandas DataFrame, có thể là từ file .csv hoặc từ SQL table. Nếu pandas tự động gán cho các cột dạng thời gian kiểu dữ liệu object (tức là string) thì ta nên chuyển kiểu dữ liệu đó sang dạng thời gian (kiểu dữ liệu datetime64 của Numpy) để làm việc với các cột đó dễ dàng hơn. datetime64 sẽ là kiểu dữ liệu thời gian mà ta sử dụng khi làm việc với pandas dataframe. Đọc thêm tại đây: https://numpy.org/doc/stable/reference/arrays.datetime.html
Ví dụ: ta load dữ liệu từ file .csv và biến đổi cột dữ liệu từ dạng chữ sang dạng thời gian như sau
import requests
from bs4 import BeautifulSoup
import urllib
import pandas as pd
from datetime import datetime
# URL of file to be loaded
github_url = 'https://raw.githubusercontent.com/tuanphan92/datatute-datasets/main/JobSeeker/sakila%20dataset/sakila-db/sakila_film.csv'
# load file to pandas dataframe
df = pd.read_csv(github_url, encoding = "utf-8", sep=";")
df = df.loc[:, ["film_id", "last_update"]]
df.head()## film_id last_update
## 0 1 2006-02-15 05:03:42
## 1 2 2006-02-15 05:03:42
## 2 3 2006-02-15 05:03:42
## 3 4 2006-02-15 05:03:42
## 4 5 2006-02-15 05:03:42
## film_id int64
## last_update object
## dtype: object
# change last_update from object type to datetime type
df["last_update"] = pd.to_datetime(df["last_update"], format='%Y-%m-%d %H:%M:%S')
df.dtypes## film_id int64
## last_update datetime64[ns]
## dtype: object
Ta có bảng dữ liệu thời gian giao dịch ngân hàng như sau:
# URL of file to be loaded
github_url = "https://raw.githubusercontent.com/tuanphan92/datatute-datasets/main/data-analytics-booster/bank_transaction_table.csv"
# load file to pandas dataframe
df = pd.read_csv(github_url, encoding = "utf-8", sep=",")
df.head()## trans_id cust_id trans_timestamp trans_amt_vnd trans_final_status
## 0 1 74452 1670307686000 178061487 failed
## 1 2 58217 1691936364000 4043095 failed
## 2 3 39259 1694704270000 80445236 success
## 3 4 28824 1681570267000 217118584 success
## 4 5 57355 1671758825000 38983836 failed
Ta thấy rằng cột trans_timestamp không ở dạng ngày giờ thông thường mà lại có dạng 1 chuỗi số. Đây là dạng “unix timestamp”, là dạng dữ liệu thời gian khá thường gặp (đọc thêm tại đây: https://www.unixtimestamp.com/). Ta cần convert về dạng datetime64 để tiện sử dụng trong pandas DataFrame.
Lưu ý: unix timestamp thường được lưu dưới dạng second (s), milisecond (ms) hoặc nanosecond (ns) nên ta cần kiểm tra tài liệu mô tả về dữ liệu mà ta đang sử dụng để biết xem dữ liệu được lưu dưới dạng nào, sau đó kiểm tra thử cho chắc chắn. Ví dụ dữ liệu của bảng giao dịch này là ở dạng milisecond. Trong câu lệnh dưới nếu bạn khai báo unit=“s” thì sẽ báo lỗi còn để mặc định là unit=“ns” thì sẽ trả ra kết quả sai (sẽ ra toàn là ngày 1970-01-01)
## trans_id cust_id ... trans_amt_vnd trans_final_status
## 0 1 74452 ... 178061487 failed
## 1 2 58217 ... 4043095 failed
## 2 3 39259 ... 80445236 success
## 3 4 28824 ... 217118584 success
## 4 5 57355 ... 38983836 failed
## .. ... ... ... ... ...
## 995 996 78329 ... 251891929 success
## 996 997 60573 ... 188692659 success
## 997 998 555 ... 220844250 failed
## 998 999 1144 ... 396626525 failed
## 999 1000 14855 ... 247175635 success
##
## [1000 rows x 5 columns]
Yêu cầu:
Tạo cột “hours_from_now” để tính số giờ kể từ khi giao dịch được thực hiện cho tới thời điểm hiện tại (tương tự ta cũng có thể tính số ngày, tháng, năm, …).
Tạo cột “trans_year” để lưu năm mà giao dịch được thực hiện (tương tự ta cũng có thể lưu ngày, tháng, …)
Lưu ý: unix timestamp luôn thuộc time zone là UTC (vì bản chất nó là số giây tính từ 1970-01-01 lúc 00:00 UTC)
# tạo cột hours_from_now
df["hours_from_now"] = (pd.Timestamp.now(tz="UTC") - df["trans_timestamp"]) / pd.Timedelta(1, "h")
# tạo cột trans_year
df["trans_year"] = df["trans_timestamp"].dt.year
df[["trans_id", "trans_timestamp", "hours_from_now", "trans_year"]].head()## trans_id trans_timestamp hours_from_now trans_year
## 0 1 2022-12-06 06:21:26+00:00 14683.731747 2022
## 1 2 2023-08-13 14:19:24+00:00 8675.765636 2023
## 2 3 2023-09-14 15:11:10+00:00 7906.902858 2023
## 3 4 2023-04-15 14:51:07+00:00 11555.237025 2023
## 4 5 2022-12-23 01:27:05+00:00 14280.637580 2022
Tương tự như đã đề cập tại khoá học Job Seekers, best practice là lưu dữ liệu về cùng 1 time zone, tốt nhất là UTC.
Nếu những ai đã quen thuộc với SQL thì có thể hiểu nội dung này cũng giống như là tạo ra 1 cột mới sử dụng lệnh CASE trong SQL vậy
Khi làm việc với Pandas DataFrame thì có nhiều cách để tạo cột mới dựa trên điều kiện if else, tuy nhiên 2 cách sau đây là rõ ràng, tiện lợi nhất về cả về syntax, chức năng và performance (hiệu năng):
import numpy as np
df['trans_amt_group'] = np.where(df['trans_amt_vnd']<1e6, '0 - 1mil VND',
np.where((df['trans_amt_vnd']>=1e6) & (df['trans_amt_vnd']<5e7), '1 - 50mil VND',
np.where((df['trans_amt_vnd']>=5e7) & (df['trans_amt_vnd']<2e8), '50 - 200mil VND',
np.where(df['trans_amt_vnd']>=2e8, '200mil VND and above', 'NA'))))
df.loc[:, ["trans_id", "trans_amt_vnd", "trans_amt_group"]].head(10)## trans_id trans_amt_vnd trans_amt_group
## 0 1 178061487 50 - 200mil VND
## 1 2 4043095 1 - 50mil VND
## 2 3 80445236 50 - 200mil VND
## 3 4 217118584 200mil VND and above
## 4 5 38983836 1 - 50mil VND
## 5 6 317618334 200mil VND and above
## 6 7 358803602 200mil VND and above
## 7 8 487137306 200mil VND and above
## 8 9 291619443 200mil VND and above
## 9 10 421195030 200mil VND and above
import numpy as np
df["trans_amt_group"] = np.select(
# conditon list
[
df['trans_amt_vnd']<1e6,
(df['trans_amt_vnd']>=1e6) & (df['trans_amt_vnd']<5e7),
(df['trans_amt_vnd']>=5e7) & (df['trans_amt_vnd']<2e8),
df['trans_amt_vnd']>=2e8
], \
# choice list
[
'0 - 1mil VND',
'1 - 50mil VND',
'50 - 200mil VND',
'200mil VND and above'
], \
#default value
default='NA'
)
df.loc[:, ["trans_id", "trans_amt_vnd", "trans_amt_group"]].head(10)## trans_id trans_amt_vnd trans_amt_group
## 0 1 178061487 50 - 200mil VND
## 1 2 4043095 1 - 50mil VND
## 2 3 80445236 50 - 200mil VND
## 3 4 217118584 200mil VND and above
## 4 5 38983836 1 - 50mil VND
## 5 6 317618334 200mil VND and above
## 6 7 358803602 200mil VND and above
## 7 8 487137306 200mil VND and above
## 8 9 291619443 200mil VND and above
## 9 10 421195030 200mil VND and above
Để join các DataFrame lại với nhau (giống như join các bảng trong SQL) thì ta dùng hàm “merge” như sau. Hãy để ý kĩ cách khai báo cột dùng làm key để join cho mỗi DataFrame, và cách xử lý để phân biệt các cột bị trùng tên giữa 2 DataFrame
df1 = pd.DataFrame(
{
'id': ['1', '2', '3', '4', '5'],
'feature1': ['A', 'B', 'C', 'D', 'E'],
'feature2': ['F', 'G', 'H', 'I', 'J']
},
columns = ['id', 'feature1', 'feature2']
)
df1## id feature1 feature2
## 0 1 A F
## 1 2 B G
## 2 3 C H
## 3 4 D I
## 4 5 E J
df2 = pd.DataFrame(
{
'id_col': ['2', '2', '3', '4', '7'],
'feature2': ['K', 'L', 'M', 'N', 'O'],
'feature4': ['P', 'Q', 'R', 'S', 'T']
},
columns = ['id_col', 'feature2', 'feature4']
)
df2## id_col feature2 feature4
## 0 2 K P
## 1 2 L Q
## 2 3 M R
## 3 4 N S
## 4 7 O T
df_merged = pd.merge(
df1,
df2,
left_on="id",
right_on="id_col",
# on="id" (dùng khi cột để join của 2 df cùng tên)
how="outer", # cách join
suffixes=("_df1", "_df2"), # đưa thêm hậu tố vào tên cột để phân biệt các cột trùng tên sau khi join
validate="one_to_many" # kiểm tra key được dùng để join ở df1 có unique hay không
)
df_merged## id feature1 feature2_df1 id_col feature2_df2 feature4
## 0 1 A F NaN NaN NaN
## 1 2 B G 2 K P
## 2 2 B G 2 L Q
## 3 3 C H 3 M R
## 4 4 D I 4 N S
## 5 5 E J NaN NaN NaN
## 6 NaN NaN NaN 7 O T
Pandas đã cung cấp cho ta 1 công cụ rất tiện lợi khi join các DataFrame đó là tham số “validate” trong hàm merge giúp kiểm tra mối quan hệ giữa 2 bảng khi join, giúp ta đảm bảo kết quả join trả ra đúng với những gì ta muốn. Ví dụ, nếu kiểm tra mối quan hệ “one_to_many” thì pandas sẽ kiểm tra cột dùng làm key để join ở bảng bên trái có các giá trị đều duy nhất hay không, nếu không thì sẽ báo lỗi. Tuy không thể nói đây là best practice, nhưng với người mới bắt đầu, để tránh sai sót khi join các DataFrame, hãy tận dụng tính năng này để có thêm 1 chốt kiểm tra xem 2 DataFrame được join với nhau có thực sự là có những quan hệ “one_to_many”, “many_to_one”, và “one_to_one” hay không.
Khi join 2 pandas DataFrame, kết quả trả ra sẽ là tất cả các cột ở trong 2 DataFrame đó. Nếu cột được dùng làm key để join có tên giống nhau trong 2 DataFrame thì chỉ 1 cột xuất hiện trong kết quả trả ra, còn với các cột mà không phải là key để join, nếu trùng tên thì sẽ được tự động gắn hậu tố (suffix) là “_x” với bảng bên trái và “_y” với bảng bên phải.
Để xử lý các cột bị trùng tên dẫn tới có các hậu tố “_x” và “_y”, ta làm theo 1 trong các cách sau:
# chỉ lấy các cột ở bảng bên trái nếu cột trùng tên
df_merged_2 = pd.merge(
df1,
df2.rename(columns={"id_col":"id"}),
on="id",
how="outer",
suffixes=(None, "_duplicated"), # đưa thêm hậu tố vào tên cột ở bảng bên phải
validate="one_to_many"
).loc[:, lambda x: ~x.columns.str.endswith('_duplicated')] # chỉ lấy các cột không có hậu tố _duplicated
df_merged_2## id feature1 feature2 feature4
## 0 1 A F NaN
## 1 2 B G P
## 2 2 B G Q
## 3 3 C H R
## 4 4 D I S
## 5 5 E J NaN
## 6 7 NaN NaN T
Cho dù theo lý thuyết thì method này có thể giúp tăng performance của việc join 2 DataFrame, tuy nhiên để dùng method này ta phải chuyển hết tất cả các cột dùng làm key để join thành index. Như vậy ta dễ nhầm lẫn khi viết code, và cũng làm code khó hiểu hơn. Ngoài ra, để performance gain nhận thấy rõ rệt được thì các DataFrame phải rất lớn, mà nếu chúng đã rất lớn thì bạn cũng không nên thuần tuý chỉ dùng pandas để xử lý, mà nên tiền xử lý (có thể là với SQL) trước, rồi dùng pandas, hoặc sử dụng một processing engine khác như Spark chẳng hạn. Vì vậy, ta nên tránh sử dụng method .join() này. Ví dụ nếu join df1 và df2 bên trên sử dụng method này sẽ như sau:
# sử dụng DataFrame.join() để join các DataFrame thay vì pd.merge làm câu lệnh phức tạp và khó hiểu hơn
df_merged_using_join = df1\
.set_index("id")\
.join(
df2.set_index("id_col"),
on="id",
how="outer",
lsuffix="_df1",
rsuffix="_df2"
).reset_index(drop=True)
df_merged_using_join## id feature1 feature2_df1 feature2_df2 feature4
## 0 1 A F NaN NaN
## 1 2 B G K P
## 2 2 B G L Q
## 3 3 C H M R
## 4 4 D I N S
## 5 5 E J NaN NaN
## 6 7 NaN NaN O T
Với những ai đã quen với việc sử dụng window function trong SQL thì khi chuyển sang sử dụng pandas sẽ không tránh khỏi bỡ ngỡ khi không tìm thấy các hàm rất tiện lợi như ROW_NUMBER(), LEAD(), LAG(), … Tuy nhiên, trong pandas ta hoàn toàn có thể thực hiện được các window function như trong SQL bằng cách sử dụng combo 2 method là DataFrame.sort_values và DataFrame.groupby
Có bảng customer_phone như sau
data = {
"customer_id":[1, 1, 2, 3, 3, 3],
"phone":["0913266654", "0984331988", "0943524647", "0942221817", "0942464764", "0915298756"],
"source":["CRM", "Mobile App Form", "CRM", "Mobile App Form", "Mobile App Form", "CRM"],
"updated_date":["2023-06-01", "2021-07-12", "2019-12-24", "2023-06-29", "2022-02-08", "2023-01-29"]
}
customer_phone_df = pd.DataFrame(data)
customer_phone_df## customer_id phone source updated_date
## 0 1 0913266654 CRM 2023-06-01
## 1 1 0984331988 Mobile App Form 2021-07-12
## 2 2 0943524647 CRM 2019-12-24
## 3 3 0942221817 Mobile App Form 2023-06-29
## 4 3 0942464764 Mobile App Form 2022-02-08
## 5 3 0915298756 CRM 2023-01-29
Yêu cầu: lấy ra dữ liệu số điện thoại mới nhất của từng khách hàng dựa theo thời gian cập nhật trên hệ thống
# đánh số dòng - tương đương ROW_NUMBER() trong SQL
customer_phone_df["rn"] = customer_phone_df\
.sort_values(by="updated_date", ascending=True)\
.groupby("customer_id")\
.cumcount()+1
# do hàm cumcount sẽ đếm từ 0 đến độ dài của Series -1, nên ta phải + 1 vào
# để ra kết quả giống ROW_NUMBER
customer_phone_df## customer_id phone source updated_date rn
## 0 1 0913266654 CRM 2023-06-01 2
## 1 1 0984331988 Mobile App Form 2021-07-12 1
## 2 2 0943524647 CRM 2019-12-24 1
## 3 3 0942221817 Mobile App Form 2023-06-29 3
## 4 3 0942464764 Mobile App Form 2022-02-08 1
## 5 3 0915298756 CRM 2023-01-29 2
Bài tập: có bảng google_analytics_pageview thể hiện các trang mà người dùng (user) đã xem trên một website (chính là bảng google_analytics_pageview ở Lesson 2 khoá học Job Seekers).
data = {
"session_id":[1, 1, 1, 1, 1, 1, 2, 2, 2, 3, 3, 3, 3],
"user":[
"thang@mail.com", "thang@mail.com", "thang@mail.com", "thang@mail.com", "thang@mail.com", "thang@mail.com",
"huyen@gmail.com", "huyen@gmail.com", "huyen@gmail.com",
"thang@mail.com", "thang@mail.com", "thang@mail.com", "thang@mail.com"
],
"time_stamp":[
"2023-01-15 12:28:30", "2023-01-15 12:28:40", "2023-01-15 12:29:56", "2023-01-15 12:41:02", "2023-01-15 12:42:40", "2023-01-15 12:44:04",
"2023-01-15 12:33:08", "2023-01-15 12:33:15", "2023-01-15 12:38:30",
"2023-01-16 09:05:31", "2023-01-16 09:05:35", "2023-01-16 09:06:01", "2023-01-16 09:25:03"
],
"page_url":[
"https://datatute.vn/", "https://datatute.vn/about/", "https://datatute.vn/portfolio/",
"https://datatute.vn/portfolio/job-seekers-course/", "https://datatute.vn/about/", "https://datatute.vn/portfolio/job-seekers-course/",
"https://datatute.vn/portfolio/", "https://datatute.vn/portfolio/interview-training/", "https://datatute.vn/portfolio/free-cv-review/",
"https://datatute.vn/", "https://datatute.vn/blog/", "https://datatute.vn/sql-ve-viec-tim-kiem-thong-tin-chua-xay-ra/", "https://datatute.vn/contact/"
]
}
google_analytics_pageview = pd.DataFrame(data)
# convert string to datetime
google_analytics_pageview["time_stamp"] = pd.to_datetime(
google_analytics_pageview["time_stamp"], format='%Y-%m-%d %H:%M:%S'
)Ta có bảng google_analytics_sessions thể hiện các session mà người dùng đã thực hiện trên website. 1 session có thể hiểu là 1 lần người dùng truy cập vào website, từ lúc mở website cho đến khi tắt trình duyệt web đi. Trong 1 session thì người dùng có thể truy cập và xem nhiều trang của website.
data = {
"session_id":[1, 2, 3],
"session_start":["2023-01-15 12:28:27", "2023-01-15 12:33:08", "2023-01-16 09:05:29"],
"session_end":["2023-01-15 12:45:23", "2023-01-15 12:42:11", "2023-01-16 09:28:51"]
}
google_analytics_sessions = pd.DataFrame(data)
# convert string to datetime
google_analytics_sessions["session_start"] = pd.to_datetime(
google_analytics_sessions["session_start"], format='%Y-%m-%d %H:%M:%S'
)
google_analytics_sessions["session_end"] = pd.to_datetime(
google_analytics_sessions["session_end"], format='%Y-%m-%d %H:%M:%S'
)Yêu cầu: Tính thời gian (bằng số giây) của mỗi lần khách hàng xem 1 trang (page) trên website. Để cho bài toán đỡ phức tạp, ta giả sử khách hàng chỉ xem 1 trang tại 1 thời điểm, và xem xong trang này mới chuyển sang trang khác (tức là không mở nhiều trang cùng 1 lúc).
Hãy thử tự làm trước khi xem đáp án. Gợi ý sử dụng method sort_values() kết hợp groupby() như ví dụ 1, nhưng thay method cumcount() bằng method shift(). Có thể tham khảo đáp án bằng SQL ở Lesson 2 khoá học Job Seekers và dùng pandas để triển khai lại đáp án SQL.
Đáp án:
# join bảng google_analytics_pageview với bảng google_analytics_sessions để lấy cột session_end
# page_start_time chính là cột time_stamp
page_df = pd.merge(
google_analytics_pageview,
google_analytics_sessions,
on="session_id",
how="left",
validate="many_to_one"
).rename(columns={"time_stamp":"page_start_time"})
# tạo cột page_end_time lưu giá trị thời gian kết thúc của pageview
# sử dụng hàm shift(-1) kết hợp với sort_values() và groupby()
# để tạo kết quả giống hàm LEAD() trong SQL
page_df["page_end_time"] = (
page_df.sort_values(
by="page_start_time", ascending=True
).groupby(
"session_id"
)["page_start_time"]
.shift(-1)
)
# pageview cuối cùng trong session sẽ có page_end_time = session_end
page_df["page_end_time"] = (
np.where(
page_df['page_end_time'].isnull(),
page_df["session_end"],
page_df['page_end_time']
)
)
# tính time_on_page bằng page_end_time - page_start_time (đổi ra đơn vị giây)
page_df["time_on_page"] = (page_df["page_end_time"] - page_df["page_start_time"]).dt.secondsTrong thực tế công việc ta thường gặp những tình huống (đặc biệt là để phục vụ phân tích hoặc vẽ biểu đồ) mà ta cần biến đổi dữ liệu từ dạng wide về dạng long và ngược lại. Việc biến đổi này gọi là “reshape data”.
Ví dụ 1: Ta thường gặp dữ liệu gốc ở định dạng sau (đặc biệt là nếu dữ liệu gốc được cung cấp từ 1 file excel hay google spreadsheet)
data = {
"Item":["Revenue", "Cost"],
"FY2019":[2850000, 2450000],
"FY2020":[2680000, 2150000],
"FY2021":[2775000, 2285000],
"FY2022":[3001000, 2785000]
}
revenue_and_cost_df = pd.DataFrame(data)
revenue_and_cost_df## Item FY2019 FY2020 FY2021 FY2022
## 0 Revenue 2850000 2680000 2775000 3001000
## 1 Cost 2450000 2150000 2285000 2785000
Dữ liệu ở dạng này gọi là dạng “wide”. Để thuận tiện cho việc phân tích cũng như vẽ đồ thị thì ta thường cần chuyển nó về dạng “long”. Pandas có sẵn hàm để ta chuyển qua lại giữa 2 dạng “wide” và “long” rất thuận tiện.
Chuyển về dạng “long”: dùng hàm pd.melt()
# convert from wide to long
long_df = pd.melt(
revenue_and_cost_df,
# cột/các cột được dùng làm id:
id_vars=["Item"],
# các cột sẽ được chuyển vào thành 2 cột "variable" và "value"":
# trong trường hợp này là tất cả các cột còn lại trừ cột "Item
value_vars=revenue_and_cost_df.columns.drop("Item")
).rename(columns={"variable":"Year", "value":"Value"}) # đổi tên cột
long_df## Item Year Value
## 0 Revenue FY2019 2850000
## 1 Cost FY2019 2450000
## 2 Revenue FY2020 2680000
## 3 Cost FY2020 2150000
## 4 Revenue FY2021 2775000
## 5 Cost FY2021 2285000
## 6 Revenue FY2022 3001000
## 7 Cost FY2022 2785000
Ví dụ 2: chuyển từ dạng “long” về dạng “wide”
# convert from long to wide
wide_df = (
long_df.pivot(
index="Item",
columns="Year",
values="Value"
)
).rename_axis( # bỏ tên (Year) của trục nằm ngang (Year, FY2019, FY2020, ...)
None,
axis=1
).reset_index() # reset lại index để trở về dạng DataFrame thông thường
wide_df## Item FY2019 FY2020 FY2021 FY2022
## 0 Cost 2450000 2150000 2285000 2785000
## 1 Revenue 2850000 2680000 2775000 3001000
Nếu bạn là người thường xuyên sử dụng numpy và pandas làm công cụ chính cho việc xử lý dữ liệu hàng ngày của mình thì tốt nhất nên đọc pandas documentation và nắm thật vững về các kiểu dữ liệu mà pandas cung cấp. Đọc tại đây: https://pandas.pydata.org/docs/reference/arrays.html
Vì pandas sử dụng kiểu dữ liệu trong numpy cho một số kiểu dữ liệu và mở rộng type system của numpy cho một số kiểu dữ liệu khác, nên trong phạm vi khoá học này không thể (và có lẽ cũng chưa cần thiết với người mới bắt đầu) giải thích chi tiết hết được. Ngoài ra, tài liệu chính thức của pandas (link bên trên) là đầy đủ và chính xác.
Khi load và xử lý dữ liệu bằng Pandas ta sẽ thường gặp các loại giá trị trống sau đây:
None: đây là kiểu giá trị trống của Python, thường xuất hiện ở các cột trong pandas DataFrame có data type = object
np.nan: đây là kiểu giá trị trống của numpy
pd.NaT: đây là kiểu giá trị trống với dữ liệu thời gian của pandas
pd.NA: đây là kiểu giá trị trống mới được đưa vào từ pandas 1.0 với mục đích để có 1 kiểu giá trị trống duy nhất cho tất cả các datatypes thay vì chỗ thì dùng np.nan, chỗ thì dùng pd.NaT, chỗ thì dùng None, …
Tuỳ vào datatype của Series (cột trong pandas DataFrame) mà sẽ có kiểu dữ liệu trống được sử dụng tương ứng. Hãy xem các câu lệnh dưới đây và kết quả của chúng để hiểu rõ hơn vấn đề này.
# cách thể hiện missing value của kiểu dữ liệu boolean
# Kiểu dữ liệu này chỉ cho phép nhận giá trị True và False
# nên khi ta truyền missing value None và np.nan vào thì chúng sẽ bị convert
# sang True hoặc False tương ứng (để ý là cách chúng bị convert ngược nhau)
# Nếu ta truyền pd.NA vào sẽ bị báo lỗi
s = pd.Series([1, 0, None, np.nan, ""], dtype="bool")
s## 0 True
## 1 False
## 2 False
## 3 True
## 4 False
## dtype: bool
# cách thể hiện missing value của kiểu dữ liệu BooleanDtype
# Kiểu dữ liệu này cho phép nhận missing value
# và thể hiện dưới dạng pd.NA
s = pd.Series([1, 0, None, np.nan, pd.NA], dtype=pd.BooleanDtype())
s## 0 True
## 1 False
## 2 <NA>
## 3 <NA>
## 4 <NA>
## dtype: boolean
# cách thể hiện missing value của kiểu dữ liệu integer
# Ta thấy lệnh báo lỗi rằng kiểu dữ liệu này không cho phép nhận missing value
# --> vậy nếu ta thấy cột nào trong pandas DataFrame có kiểu dữ liệu int64
# thì chắc chắn cột đó không có missing value
# Nếu thay vì None và pd.NA, ta truyền vào giá trị np.nan thì kiểu dữ liệu của Series sẽ
# tự động bị convert sang float64 thay vì int64 (vì np.nan chỉ sử dụng được với kiểu float)
s = pd.Series([1, 0, None, pd.NA], dtype="int64")## int() argument must be a string, a bytes-like object or a number, not
## 'NoneType'
# cách thể hiện missing value của kiểu dữ liệu Int64Dtype
# Kiểu dữ liệu này cho phép nhận missing value và thể hiện dưới dạng pd.NA
# Vì vậy, nếu trong pandas DataFrame cần có cột integer mà chứa được missing value
# thì ta nên dùng kiểu pd.Int64Dtype()
s = pd.Series([1, 0, None, np.nan, pd.NA], dtype=pd.Int64Dtype())
s## 0 1
## 1 0
## 2 <NA>
## 3 <NA>
## 4 <NA>
## dtype: Int64
# cách thể hiện missing value của kiểu dữ liệu float
# Kiểu dữ liệu này cho phép nhận missing value
# và thể hiện dưới dạng np.nan
# (giá trị None đã tự động bị convert sang np.nan)
# Lệnh sẽ báo lỗi nếu ta truyền vào pd.NA
s = pd.Series([1, 0.2, None, np.nan], dtype="float64")
s## 0 1.0
## 1 0.2
## 2 NaN
## 3 NaN
## dtype: float64
# cách thể hiện missing value của kiểu dữ liệu Float64Dtype
# Kiểu dữ liệu này cho phép nhận missing value
# và thể hiện dưới dạng pd.NA
s = pd.Series([1, 0.2, None, np.nan, pd.NA], dtype=pd.Float64Dtype())
s## 0 1.0
## 1 0.2
## 2 <NA>
## 3 <NA>
## 4 <NA>
## dtype: Float64
# cách thể hiện missing value của kiểu dữ liệu object
# Kiểu dữ liệu này cho phép nhận missing value ở bất kỳ kiểu nào ta muốn
# và giữ nguyên các kiểu dữ liệu missing value được truyền vào
s = pd.Series(["nguyen van a", [], None, np.nan, pd.NA], dtype="object")
s## 0 nguyen van a
## 1 []
## 2 None
## 3 NaN
## 4 <NA>
## dtype: object
# cách thể hiện missing value của kiểu dữ liệu string
# Kiểu dữ liệu này cho phép nhận missing value
# và thể hiện dưới dạng pd.NA
s = pd.Series(["nguyen van a", "blabla", None, np.nan, pd.NA], dtype="string")
s## 0 nguyen van a
## 1 blabla
## 2 <NA>
## 3 <NA>
## 4 <NA>
## dtype: string
xem Lesson 2 khoá Job Seekers, mục Xử lý các giá trị NULL, các phần sau vẫn đúng khi join (hàm merge) trong pandas:
giá trị NULL ở điều kiện lọc dữ liệu
giá trị NULL ở lệnh not in
giá trị NULL khi concatenate string
giá trị NULL khi sử dụng các phép tính toán học
giá trị NULL khi sử dụng các hàm aggregate
giá trị NULL khi INNER JOIN
giá trị NULL khi dùng điều kiện if else (np.where, …)
Pandas cung cấp các công cụ mạnh mẽ cho ta trong việc thay thế các missing values. Ta lưu ý việc này thường chỉ dùng khi xử lý dữ liệu chuẩn bị cho train model, còn trong phân tích, đặc biệt là trong báo cáo cần rà soát số liệu chi tiết, phản ánh đúng số liệu, … thì nên hạn chế tối đa việc thay thế các giá trị trống, nhất là khi chưa hiểu rõ nguyên nhân dữ liệu bị thiếu/trống để tránh đưa ra kết quả phân tích sai hoặc người dùng báo cáo hiểu sai bản chất vấn đề.
Trong phạm vi khoá học này, ta sẽ chỉ cùng nhau review một số hàm cơ bản trong pandas và cách thay thế các giá trị trống cơ bản như sau:
Ta dùng 2 method này để thay thế các giá trị trống trong một hoặc nhiều cột hoặc trong toàn bộ DataFrame.
Một số cách thay thế giá trị trống thường gặp:
Trong những trường hợp ta hiểu rõ bản chất của việc thiếu dữ liệu, ta có thể chọn thay thế các giá trị trống bằng những giá trị cũng có ý nghĩa thể hiện việc không có dữ liệu. Ví dụ:
Với cột kiểu số (ví dụ giá trị hợp đồng chẳng hạn), ta có thể thay giá trị trống bằng số 0 để dữ liệu thể hiện ra đẹp hơn (và phần nào đó là tiện cho các thao tác tính toán hơn) nếu ta biết chắc chắn rằng đó là loại hợp đồng không có giá trị tiền chứ không phải là lỗi dữ liệu
Với cột kiểu chữ (text, string) ta có thể thay giá trị trống bằng giá trị “” để khi hiển thị dữ liệu trông sẽ giống như một ô trắng tinh, và ngoài ra tránh một số trường hợp phải viết code quá phức tạp khi thực hiện các xử lý trên string do có giá trị trống (missing value)
Xem ví dụ với bảng dữ liệu sau đây:
data = {
"float_col":pd.Series([np.nan, 1.2, np.nan, 1.4, np.nan, 1.5], dtype="float64"),
"string_col":pd.Series(["str1", pd.NA, "str3", np.nan, None, "str6"], dtype="string"),
"object_col":pd.Series(["obj1", pd.NA, "obj3", np.nan, None, "obj6"], dtype="object"),
"datetime_col":pd.Series(["2023-06-01", "2021-07-12", "2019-12-24", "2023-06-29", "2022-02-08", pd.NaT], dtype="datetime64[ns]")
}
df = pd.DataFrame(data)
df## float_col string_col object_col datetime_col
## 0 NaN str1 obj1 2023-06-01
## 1 1.2 <NA> <NA> 2021-07-12
## 2 NaN str3 obj3 2019-12-24
## 3 1.4 <NA> NaN 2023-06-29
## 4 NaN <NA> None 2022-02-08
## 5 1.5 str6 obj6 NaT
## 0 0.0
## 1 1.2
## 2 0.0
## 3 1.4
## 4 0.0
## 5 1.5
## Name: float_col, dtype: float64
# fill missing values cột string bằng giá trị ""
df2 = df.copy()
df2["string_col"].fillna("", inplace=True)
df2## float_col string_col object_col datetime_col
## 0 NaN str1 obj1 2023-06-01
## 1 1.2 <NA> 2021-07-12
## 2 NaN str3 obj3 2019-12-24
## 3 1.4 NaN 2023-06-29
## 4 NaN None 2022-02-08
## 5 1.5 str6 obj6 NaT
Đây là 2 cách thay thế giá trị trống rất phổ biến cho các cột dạng số để chuẩn bị dữ liệu cho việc training các mô hình machine learning. Median thường được dùng thay cho mean khi mà dữ liệu của cột đó có các outliers.
## float_col string_col object_col datetime_col
## 0 1.366667 str1 obj1 2023-06-01
## 1 1.200000 <NA> <NA> 2021-07-12
## 2 1.366667 str3 obj3 2019-12-24
## 3 1.400000 <NA> NaN 2023-06-29
## 4 1.366667 <NA> None 2022-02-08
## 5 1.500000 str6 obj6 NaT
# fill with median
df2 = df.copy()
df2["float_col"].fillna(df2["float_col"].median(), inplace=True)
df2## float_col string_col object_col datetime_col
## 0 1.4 str1 obj1 2023-06-01
## 1 1.2 <NA> <NA> 2021-07-12
## 2 1.4 str3 obj3 2019-12-24
## 3 1.4 <NA> NaN 2023-06-29
## 4 1.4 <NA> None 2022-02-08
## 5 1.5 str6 obj6 NaT
Mode là giá trị xuất hiện nhiều nhất. Ta thường dùng mode để thay thế các giá trị trống với các cột thuộc kiểu dữ liệu categorical.
Ví dụ có dữ liệu gender như sau:
## 0 M
## 1 F
## 2 F
## 3 <NA>
## 4 F
## 5 M
## 6 <NA>
## dtype: object
Thay thế giá trị trống bằng mode
## 0 M
## 1 F
## 2 F
## 3 F
## 4 F
## 5 M
## 6 F
## dtype: object
Cách thay thế giá trị trống này thường được dùng trong một số bài toán liên quan tới dữ liệu time series.
## float_col string_col object_col datetime_col
## 0 NaN str1 obj1 2023-06-01
## 1 1.2 str1 obj1 2021-07-12
## 2 1.2 str3 obj3 2019-12-24
## 3 1.4 str3 obj3 2023-06-29
## 4 1.4 str3 obj3 2022-02-08
## 5 1.5 str6 obj6 2022-02-08
## float_col string_col object_col datetime_col
## 0 1.2 str1 obj1 2023-06-01
## 1 1.2 str3 obj3 2021-07-12
## 2 1.4 str3 obj3 2019-12-24
## 3 1.4 str6 obj6 2023-06-29
## 4 1.5 str6 obj6 2022-02-08
## 5 1.5 str6 obj6 NaT
Trong một số ít trường hợp, ta muốn xoá những dòng mà có giá trị trống (trống ở cột nào không quan trọng), ta có thể làm như sau:
# trong trường hợp này, dòng nào của df cũng có missing value
# (không missing ở cột này thì ở cột khác)
# nên kết quả là tất cả các dòng ở df đều bị xoã
df.dropna(axis=0, inplace=False)## Empty DataFrame
## Columns: [float_col, string_col, object_col, datetime_col]
## Index: []
Method này có thể được dùng để xoá cột hoặc dòng. Thường khi ta có những cột mà toàn bộ cột đều không có dữ liệu, ta nên xoá toàn bộ cột đi trước khi thực hiện các bước xử lý, phân tích tiếp theo.
# tạo một cột trống toàn bộ
df["empty_col"] = pd.Series([pd.NA, pd.NA, pd.NA, pd.NA, pd.NA, pd.NA], dtype="string")
df## float_col string_col object_col datetime_col empty_col
## 0 NaN str1 obj1 2023-06-01 <NA>
## 1 1.2 <NA> <NA> 2021-07-12 <NA>
## 2 NaN str3 obj3 2019-12-24 <NA>
## 3 1.4 <NA> NaN 2023-06-29 <NA>
## 4 NaN <NA> None 2022-02-08 <NA>
## 5 1.5 str6 obj6 NaT <NA>
# xoá cột trống toàn bộ đi
# axis=1 để lệnh thực hiện xoá cột thay vì xoá dòng
df.drop("empty_col", axis=1, inplace=True)
df## float_col string_col object_col datetime_col
## 0 NaN str1 obj1 2023-06-01
## 1 1.2 <NA> <NA> 2021-07-12
## 2 NaN str3 obj3 2019-12-24
## 3 1.4 <NA> NaN 2023-06-29
## 4 NaN <NA> None 2022-02-08
## 5 1.5 str6 obj6 NaT
Khi viết code Python ta nên tuân thủ theo PEP-8. Đây là một bộ quy chuẩn và hướng dẫn để viết code Python sao cho dễ hiểu và đồng nhất (consistent).
Đọc bản full tại đây: https://peps.python.org/pep-0008/
Hoặc đọc bài viết sau của Real Python về PEP-8 (ngắn gọn hơn): https://realpython.com/python-pep8/
Bạn không cần thiết phải đọc hết full PEP-8 cũng như trong thực tế bạn cũng không nhất thiết phải tuân thủ quá chặt chẽ PEP-8, tuy nhiên khi làm việc với các dự án và các code base càng phức tạp thì bạn sẽ càng thấy sự quan trọng của những bộ hướng dẫn và quy chuẩn (giúp bạn hiểu code nhanh hơn, debug dễ hơn, việc review code những dễ và nhanh hơn, người khác phối hợp coding với bạn dễ hơn, …) và tốt nhất ta nên tuân theo các hướng dẫn này nhiều nhất có thể.