8. Làm sạch và chuẩn bị dữ liệu#


Trong quá trình phân tích và xây dựng mô hình dữ liệu, một phần đáng kể thời gian thường được dành cho công việc chuẩn bị dữ liệu, bao gồm: thu thập, làm sạch, chuyển đổi và cấu trúc lại dữ liệu. Nhiều nghiên cứu thực nghiệm đã ghi nhận rằng các tác vụ này có thể chiếm tới 80% hoặc hơn tổng thời gian làm việc của một nhà phân tích dữ liệu.

Một trong những thách thức phổ biến là dữ liệu được lưu trữ dưới các định dạng không phù hợp với yêu cầu của bài toán phân tích cụ thể. Do đó, các nhà phân tích thường phải thực hiện các thao tác chuyển đổi linh hoạt giữa các cấu trúc dữ liệu, sử dụng những ngôn ngữ lập trình như Python, R, hoặc Java, cũng như các công cụ dòng lệnh truyền thống trong môi trường Unix.

Tuy nhiên, sự kết hợp giữa thư viện pandas và các khả năng tích hợp sẵn của ngôn ngữ Python đã cung cấp một bộ công cụ thao tác dữ liệu có hiệu quả cao, có thể mở rộng, giúp giải quyết nhanh chóng hầu hết các nhu cầu xử lý dữ liệu phổ biến trong thực tiễn nghiên cứu và ứng dụng.

Trong trường hợp người dùng phát hiện một thao tác xử lý dữ liệu chưa được đề cập trong tài liệu này hoặc chưa được hỗ trợ trong pandas, việc chia sẻ trường hợp sử dụng cụ thể lên cộng đồng phát triển pandas trên GitHub hoặc các danh sách thư tín Python là rất được khuyến khích. Trên thực tế, nhiều cải tiến trong thiết kế và triển khai pandas đã trực tiếp xuất phát từ các yêu cầu thực tế như vậy.

Chương này sẽ tập trung trình bày các công cụ hỗ trợ xử lý dữ liệu thiếu, dữ liệu trùng lặp, thao tác trên chuỗi ký tự, cùng với một số phép biến đổi dữ liệu phục vụ mục tiêu phân tích. Trong chương kế tiếp, chúng ta sẽ tiếp tục tìm hiểu về các kỹ thuật kết hợp và tái tổ chức tập dữ liệu, qua đó hỗ trợ cho quá trình phân tích và trực quan hóa dữ liệu một cách hiệu quả hơn.

import numpy as np
import pandas as pd
PREVIOUS_MAX_ROWS = pd.options.display.max_rows
pd.options.display.max_rows = 20
np.random.seed(12345) # để ví dụ có thể lặp lại
import matplotlib.pyplot as plt
plt.rc("figure", figsize=(10, 6))
np.set_printoptions(precision=4, suppress=True)

8.1. Xử lý dữ liệu không quan sát được#

Dữ liệu không quan sát được (missing data) thường xuyên xuất hiện trong nhiều ứng dụng phân tích dữ liệu. Một trong những mục tiêu của pandas là làm cho việc xử lý dữ liệu không quan sát được trở nên dễ dàng nhất có thể. Ví dụ, tất cả các thống kê mô tả trên các đối tượng của pandas đều loại trừ dữ liệu này theo mặc định.

Cách biểu diễn dữ liệu không quan sát được trong các đối tượng pandas còn nhiều vấn đề, nhưng cũng đủ cho hầu hết các mục đích sử dụng trong thực tế. Đối với dữ liệu có kiểu float64, pandas sử dụng giá trị dấu phẩy động NaN (Not a Number) để biểu diễn dữ liệu bị thiếu.

Chúng ta gọi đây là một giá trị sentinel: khi có mặt, nó chỉ ra một giá trị bị không quan sát được (hoặc null):

float_data = pd.Series([1.2, -3.5, np.nan, 0])
float_data
# Out[15]: 
# 0    1.2
# 1   -3.5
# 2    NaN
# 3    0.0
# dtype: float64
0    1.2
1   -3.5
2    NaN
3    0.0
dtype: float64

Phương thức isna trả về một mảng kiểu logical với giá trị True ở những nơi giá trị là không quan sát được:

float_data.isna()
# Out[16]: 
# 0    False
# 1    False
# 2     True
# 3    False
# dtype: bool
0    False
1    False
2     True
3    False
dtype: bool

Trong pandas cũng áp dụng một quy ước được sử dụng trong ngôn ngữ lập trình R bằng cách gọi dữ liệu không quan sát được là NA, viết tắt của not available. Trong các ứng dụng thống kê, dữ liệu NA có thể là dữ liệu không tồn tại hoặc tồn tại nhưng không được quan sát (ví dụ, do các vấn đề trong quá trình thu thập dữ liệu). Khi dọn dẹp dữ liệu để phân tích, việc phân tích chính dữ liệu NA thường rất quan trọng để xác định các vấn đề thu thập dữ liệu hoặc các sai lệch tiềm ẩn trong dữ liệu do dữ liệu bị thiếu gây ra.

Giá trị None tích hợp sẵn của Python cũng được coi là NA:

string_data = pd.Series(["aardvark", np.nan, None, "avocado"])
string_data
# Out[18]:
# 0    aardvark
# 1         NaN
# 2        None
# 3     avocado
# dtype: object

string_data.isna()
# Out[19]:
# 0    False
# 1     True
# 2     True
# 3    False
# dtype: bool
0    False
1     True
2     True
3    False
dtype: bool
float_data = pd.Series([1, 2, None], dtype='float64')
float_data
# Out[21]:
# 0    1.0
# 1    2.0
# 2    NaN
# dtype: float64

float_data.isna()
# Out[22]:
# 0    False
# 1    False
# 2     True
# dtype: bool
0    False
1    False
2     True
dtype: bool

Thư viện pandas đã cố gắng làm cho việc xử lý dữ liệu không quan sát được nhất quán giữa các kiểu dữ liệu. Xem Bảng … để biết danh sách một số hàm liên quan đến xử lý dữ liệu không quan sát được.

Bảng …: Các phương thức xử lý NA

Phương thức

Mô tả

dropna

Lọc các chỉ số dựa trên việc các giá trị cho chỉ số có dữ liệu bị thiếu hay không

fillna

Điền vào dữ liệu NA bằng một giá trị nào đó hoặc sử dụng một phương pháp nội suy như "ffill" hoặc "bfill".

isna

Trả về các giá trị boolean cho biết giá trị nào là NA.

notna

Phủ định của isna, trả về True cho các giá trị không phải NA và False cho các giá trị NA.

8.1.1. Lọc bỏ dữ liệu bị thiếu#

Có nhiều cách để lọc bỏ dữ liệu bị thiếu. Mặc dù bạn luôn có thể tự làm điều đó bằng cách sử dụng pandas.notna và lọc dữ liệu bằng chỉ số kiểu logical. Ngoài ra bạn có thể dùng phương thức dropna như đã mô tả ở trên:

data = pd.Series([1, np.nan, 3.5, np.nan, 7])
data.dropna()
# Out[24]:
# 0    1.0
# 2    3.5
# 4    7.0
# dtype: float64
0    1.0
2    3.5
4    7.0
dtype: float64

Điều này tương đương với:

data[data.notna()]
# Out[25]:
# 0    1.0
# 2    3.5
# 4    7.0
# dtype: float64
0    1.0
2    3.5
4    7.0
dtype: float64

Với các đối tượng DataFrame, có nhiều cách khác nhau để loại bỏ dữ liệu không quan sát được. Bạn có thể muốn loại bỏ các hàng hoặc cột hoàn toàn là NA, hoặc chỉ những hàng hoặc cột chứa bất kỳ NA nào. dropna theo mặc định sẽ loại bỏ bất kỳ hàng nào chứa một giá trị không quan sát được:

data = pd.DataFrame([[1., 6.5, 3.], [1., np.nan, np.nan],
                     [np.nan, np.nan, np.nan], [np.nan, 6.5, 3.]])
data
# Out[27]:
#      0    1    2
# 0  1.0  6.5  3.0
# 1  1.0  NaN  NaN
# 2  NaN  NaN  NaN
# 3  NaN  6.5  3.0

data.dropna()
# Out[28]:
#      0    1    2
# 0  1.0  6.5  3.0
0 1 2
0 1.0 6.5 3.0

Sử dụng tham số how="all" sẽ chỉ loại bỏ các hàng mà tất cả các giá trị đều là NA:

data.dropna(how="all")
# Out[29]:
#      0    1    2
# 0  1.0  6.5  3.0
# 1  1.0  NaN  NaN
# 3  NaN  6.5  3.0
0 1 2
0 1.0 6.5 3.0
1 1.0 NaN NaN
3 NaN 6.5 3.0

Hãy lưu ý rằng các hàm này trả về các đối tượng mới theo mặc định và không sửa đổi nội dung của đối tượng ban đầu.

Để loại bỏ các cột theo cách tương tự, hãy sử dụng tham số axis="columns":

data[4] = np.nan
data
# Out[31]:
#      0    1    2   4
# 0  1.0  6.5  3.0 NaN
# 1  1.0  NaN  NaN NaN
# 2  NaN  NaN  NaN NaN
# 3  NaN  6.5  3.0 NaN

data.dropna(axis="columns", how="all")
# Out[32]:
#      0    1    2
# 0  1.0  6.5  3.0
# 1  1.0  NaN  NaN
# 2  NaN  NaN  NaN
# 3  NaN  6.5  3.0
0 1 2
0 1.0 6.5 3.0
1 1.0 NaN NaN
2 NaN NaN NaN
3 NaN 6.5 3.0

Giả sử bạn muốn giữ lại chỉ những hàng chứa tối đa một số lượng NA nhất định. Bạn có thể chỉ ra điều này bằng tham số thresh:

df = pd.DataFrame(np.random.standard_normal((7, 3)))
df.iloc[:4, 1] = np.nan
df.iloc[:2, 2] = np.nan
df
# Out[36]:
#           0         1         2
# 0 -0.204708       NaN       NaN
# 1 -0.555730       NaN       NaN
# 2  0.092908       NaN  0.769023
# 3  1.246435       NaN -1.296221
# 4  0.274992  0.228913  1.352917
# 5  0.886429 -2.001637 -0.371843
# 6  1.669025 -0.438570 -0.539741

df.dropna()
# Out[37]:
#           0         1         2
# 4  0.274992  0.228913  1.352917
# 5  0.886429 -2.001637 -0.371843
# 6  1.669025 -0.438570 -0.539741

df.dropna(thresh=2)
# Out[38]:
#           0         1         2
# 2  0.092908       NaN  0.769023
# 3  1.246435       NaN -1.296221
# 4  0.274992  0.228913  1.352917
# 5  0.886429 -2.001637 -0.371843
# 6  1.669025 -0.438570 -0.539741
0 1 2
2 0.092908 NaN 0.769023
3 1.246435 NaN -1.296221
4 0.274992 0.228913 1.352917
5 0.886429 -2.001637 -0.371843
6 1.669025 -0.438570 -0.539741

8.1.2. Điền vào dữ liệu bị thiếu#

Thay vì lọc bỏ dữ liệu không quan sát được và có khả năng loại bỏ dữ liệu khác cùng hàng hoặc cùng cột, bạn có giải pháp là điền vào các giá trị này theo nhiều cách. Đối với hầu hết các mục đích, phương thức fillna là hàm chính được sử dụng. Phương pháp đơn giản nhất là gọi fillna với một hằng số để thay thế các giá trị NA bằng giá trị đó:

df.fillna(0)
# Out[39]:
#           0         1         2
# 0 -0.204708  0.000000  0.000000
# 1 -0.555730  0.000000  0.000000
# 2  0.092908  0.000000  0.769023
# 3  1.246435  0.000000 -1.296221
# 4  0.274992  0.228913  1.352917
# 5  0.886429 -2.001637 -0.371843
# 6  1.669025 -0.438570 -0.539741
0 1 2
0 -0.204708 0.000000 0.000000
1 -0.555730 0.000000 0.000000
2 0.092908 0.000000 0.769023
3 1.246435 0.000000 -1.296221
4 0.274992 0.228913 1.352917
5 0.886429 -2.001637 -0.371843
6 1.669025 -0.438570 -0.539741

Gọi fillna với tham số là một dictionary, bạn có thể điền các giá trị khác nhau cho mỗi cột:

df.fillna({1: 0.5, 2: 0})
# Out[40]:
#           0         1         2
# 0 -0.204708  0.500000  0.000000
# 1 -0.555730  0.500000  0.000000
# 2  0.092908  0.500000  0.769023
# 3  1.246435  0.500000 -1.296221
# 4  0.274992  0.228913  1.352917
# 5  0.886429 -2.001637 -0.371843
# 6  1.669025 -0.438570 -0.539741
0 1 2
0 -0.204708 0.500000 0.000000
1 -0.555730 0.500000 0.000000
2 0.092908 0.500000 0.769023
3 1.246435 0.500000 -1.296221
4 0.274992 0.228913 1.352917
5 0.886429 -2.001637 -0.371843
6 1.669025 -0.438570 -0.539741

Các phương pháp nội suy tương tự có sẵn cho việc sắp xếp lại (xem Bảng 5.3) có thể được sử dụng với fillna:

df = pd.DataFrame(np.random.standard_normal((6, 3)))
df.iloc[2:, 1] = np.nan
df.iloc[4:, 2] = np.nan
df
# Out[44]:
#           0         1         2
# 0  0.476985  3.248944 -1.021228
# 1 -0.577087  0.124121  0.302614
# 2  0.523772       NaN  1.343810
# 3 -0.713544       NaN -2.370232
# 4 -1.860761       NaN       NaN
# 5 -1.265934       NaN       NaN

df.fillna(method="ffill")
# Out[45]:
#           0         1         2
# 0  0.476985  3.248944 -1.021228
# 1 -0.577087  0.124121  0.302614
# 2  0.523772  0.124121  1.343810
# 3 -0.713544  0.124121 -2.370232
# 4 -1.860761  0.124121 -2.370232
# 5 -1.265934  0.124121 -2.370232

df.fillna(method="ffill", limit=2)
# Out[46]:
#           0         1         2
# 0  0.476985  3.248944 -1.021228
# 1 -0.577087  0.124121  0.302614
# 2  0.523772  0.124121  1.343810
# 3 -0.713544  0.124121 -2.370232
# 4 -1.860761       NaN -2.370232
# 5 -1.265934       NaN -2.370232
0 1 2
0 0.476985 3.248944 -1.021228
1 -0.577087 0.124121 0.302614
2 0.523772 0.124121 1.343810
3 -0.713544 0.124121 -2.370232
4 -1.860761 NaN -2.370232
5 -1.265934 NaN -2.370232

Với fillna, bạn có thể làm điền dữ liệu bằng các giá trị thống kê đặc trưng như trung vị hoặc trung bình:

data = pd.Series([1., np.nan, 3.5, np.nan, 7])
data.fillna(data.mean())
# Out[48]:
# 0    1.000000
# 1    3.833333
# 2    3.500000
# 3    3.833333
# 4    7.000000
# dtype: float64
0    1.000000
1    3.833333
2    3.500000
3    3.833333
4    7.000000
dtype: float64

Xem Bảng … để tham khảo về các tham số của hàm fillna.

Bảng…: các tham số của hàm fillna

Tham số

Mô tả

value

Giá trị vô hướng hoặc đối tượng giống dictionary được sử dụng để điền vào các giá trị bị thiếu.

method

Phương pháp nội suy; một trong "bfill" (điền lùi) hoặc "ffill" (điền tiến); mặc định là None.

axis

Trục cần điền ("index" hoặc "columns"); mặc định là axis="index".

limit

Đối với điền tiến và lùi, số lượng tối đa các khoảng trống liên tiếp cần điền.

8.2. Chuyển đổi Dữ Liệu#

Cho đến nay trong chương này, chúng ta đã thảo luận về xử lý dữ liệu bị thiếu. Ngoài ra, lọc dữ liệu và làm sạch làm sạch dữ liệu cũng là các bước xử lý quan trọng khác.

8.2.1. Loại bỏ các giá trị trùng lặp#

Các hàng trùng lặp có thể được tìm thấy trong một DataFrame vì nhiều lý do. Đây là một ví dụ:

data = pd.DataFrame({"k1": ["one", "two"] * 3 + ["two"],
                     "k2": [1, 1, 2, 3, 3, 4, 4]})
data
# Out[50]:
#      k1  k2
# 0   one   1
# 1   two   1
# 2   one   2
# 3   two   3
# 4   one   3
# 5   two   4
# 6   two   4
k1 k2
0 one 1
1 two 1
2 one 2
3 two 3
4 one 3
5 two 4
6 two 4

Phương thức duplicated của DataFrame trả về một mảng kiểu logical cho biết liệu mỗi hàng có phải là một bản sao, nghĩa là các giá trị của nó hoàn toàn bằng với các giá trị trong một hàng trước đó, hay không:

data.duplicated()
# Out[51]:
# 0    False
# 1    False
# 2    False
# 3    False
# 4    False
# 5    False
# 6     True
# dtype: bool
0    False
1    False
2    False
3    False
4    False
5    False
6     True
dtype: bool

Phương thức drop_duplicates trả về một DataFrame với các hàng mà mảng duplicatedFalse được lọc bỏ:

data.drop_duplicates()
# Out[52]:
#      k1  k2
# 0   one   1
# 1   two   1
# 2   one   2
# 3   two   3
# 4   one   3
# 5   two   4
k1 k2
0 one 1
1 two 1
2 one 2
3 two 3
4 one 3
5 two 4

Cả hai phương thức này theo mặc định xem xét tất cả các cột. Tuy nhiên, bạn cũng có thể chỉ định bất kỳ tập hợp con nào của các cột để phát hiện ra các giá trị bị trùng lặp. Giả sử chúng ta có một cột giá trị bổ sung và muốn lọc các bản sao chỉ dựa trên cột "k1":

data["v1"] = range(7)
data
# Out[54]:
#      k1  k2  v1
# 0   one   1   0
# 1   two   1   1
# 2   one   2   2
# 3   two   3   3
# 4   one   3   4
# 5   two   4   5
# 6   two   4   6

data.drop_duplicates(subset=["k1"])
# Out[55]:
#      k1  k2  v1
# 0   one   1   0
# 1   two   1   1
k1 k2 v1
0 one 1 0
1 two 1 1

duplicateddrop_duplicates theo mặc định giữ lại tổ hợp giá trị được quan sát đầu tiên. Sử dụng tham số keep="last" sẽ loại bỏ các hàng bị lặp xuất hiện ở phía trên:

data.drop_duplicates(["k1", "k2"], keep="last")
# Out[56]:
#      k1  k2  v1
# 0   one   1   0
# 1   two   1   1
# 2   one   2   2
# 3   two   3   3
# 4   one   3   4
# 6   two   4   6
k1 k2 v1
0 one 1 0
1 two 1 1
2 one 2 2
3 two 3 3
4 one 3 4
6 two 4 6

8.2.2. Chuyển đổi dữ liệu bằng hàm hoặc mapping#

Đối với nhiều tập dữ liệu, bạn có thể muốn thực hiện một số chuyển đổi dựa trên các giá trị trong một mảng, Series hoặc cột trong DataFrame. Hãy xem xét dữ liệu giả định sau được thu thập về các loại thực phẩm khác nhau:

data = pd.DataFrame({"food": ["bacon", "pulled pork", "bacon",
                              "pastrami", "corned beef", "bacon",
                              "pastrami", "honey ham", "nova lox"],
                     "ounces": [4, 3, 12, 6, 7.5, 8, 3, 5, 6]})
data
# Out[58]:
#           food  ounces
# 0        bacon     4.0
# 1  pulled pork     3.0
# 2        bacon    12.0
# 3     pastrami     6.0
# 4  corned beef     7.5
# 5        bacon     8.0
# 6     pastrami     3.0
# 7    honey ham     5.0
# 8     nova lox     6.0
food ounces
0 bacon 4.0
1 pulled pork 3.0
2 bacon 12.0
3 pastrami 6.0
4 corned beef 7.5
5 bacon 8.0
6 pastrami 3.0
7 honey ham 5.0
8 nova lox 6.0

Giả sử bạn muốn thêm một cột cho biết loại động vật mà mỗi loại thực phẩm đến từ đó. Cần có một mapping của từng loại thịt riêng biệt đến loại động vật:

meat_to_animal = {
  "bacon": "pig",
  "pulled pork": "pig",
  "pastrami": "cow",
  "corned beef": "cow",
  "honey ham": "pig",
  "nova lox": "salmon"
}

Phương thức map trên một mảng được chấp nhận như một hàm để thực hiện việc thêm một cột mới animal vào dữ liệu

data["animal"] = data["food"].map(meat_to_animal)
data
# Out[61]:
#           food  ounces  animal
# 0        bacon     4.0     pig
# 1  pulled pork     3.0     pig
# 2        bacon    12.0     pig
# 3     pastrami     6.0     cow
# 4  corned beef     7.5     cow
# 5        bacon     8.0     pig
# 6     pastrami     3.0     cow
# 7    honey ham     5.0     pig
# 8     nova lox     6.0  salmon
food ounces animal
0 bacon 4.0 pig
1 pulled pork 3.0 pig
2 bacon 12.0 pig
3 pastrami 6.0 cow
4 corned beef 7.5 cow
5 bacon 8.0 pig
6 pastrami 3.0 cow
7 honey ham 5.0 pig
8 nova lox 6.0 salmon

Hoặc chúng ta cũng có thể định nghĩa một hàm thay cho phương thức map

def get_animal(x):
    return meat_to_animal[x]

data["food"].map(get_animal)
# Out[63]:
# 0       pig
# 1       pig
# 2       pig
# 3       cow
# 4       cow
# 5       pig
# 6       cow
# 7       pig
# 8    salmon
# Name: food, dtype: object
0       pig
1       pig
2       pig
3       cow
4       cow
5       pig
6       cow
7       pig
8    salmon
Name: food, dtype: object

Nhìn chung, sử dụng map là một cách thuận tiện để thực hiện các phép biến đổi theo từng phần tử và các hoạt động liên quan đến làm sạch dữ liệu khác.

8.2.3. Thay thế các giá trị#

Điền vào dữ liệu bị thiếu bằng phương thức fillna là một trường hợp đặc biệt của việc thay thế giá trị trong trường hợp tổng quát hơn. Như chúng ta đã thấy, map có thể được sử dụng để sửa đổi một tập hợp con các giá trị trong một đối tượng, nhưng replace cung cấp một cách đơn giản và linh hoạt hơn để làm điều đó. Hãy xem xét Series này:

data = pd.Series([1., -999., 2., -999., -1000., 3.])
data
# Out[65]:
# 0       1.0
# 1    -999.0
# 2       2.0
# 3    -999.0
# 4   -1000.0
# 5       3.0
# dtype: float64
0       1.0
1    -999.0
2       2.0
3    -999.0
4   -1000.0
5       3.0
dtype: float64

Giá trị -999 có thể là giá trị sentinel cho dữ liệu không quan sát được. Để thay thế những giá trị này bằng các giá trị NA mà pandas hiểu, chúng ta có thể sử dụng replace, tạo ra một Series mới:

data.replace(-999, np.nan)
# Out[66]:
# 0       1.0
# 1       NaN
# 2       2.0
# 3       NaN
# 4   -1000.0
# 5       3.0
# dtype: float64
0       1.0
1       NaN
2       2.0
3       NaN
4   -1000.0
5       3.0
dtype: float64

Nếu bạn muốn thay thế nhiều giá trị cùng một lúc, bạn thay vào một danh sách và sau đó là giá trị thay thế:

data.replace([-999, -1000], np.nan)
# Out[67]:
# 0    1.0
# 1    NaN
# 2    2.0
# 3    NaN
# 4    NaN
# 5    3.0
# dtype: float64
0    1.0
1    NaN
2    2.0
3    NaN
4    NaN
5    3.0
dtype: float64

Để sử dụng một giá trị thay thế khác nhau cho mỗi giá trị, hãy sử dụng một danh sách các giá trị thay thế:

data.replace([-999, -1000], [np.nan, 0])
# Out[68]:
# 0    1.0
# 1    NaN
# 2    2.0
# 3    NaN
# 4    0.0
# 5    3.0
# dtype: float64
0    1.0
1    NaN
2    2.0
3    NaN
4    0.0
5    3.0
dtype: float64

Lưu ý: Phương thức data.replace khác với data.str.replace, phương thức này thực hiện thay thế chuỗi theo từng phần tử. Chúng ta sẽ xem xét các phương thức chuỗi này trên Series sau trong chương.

8.2.4. Đổi tên chỉ số hàng và cột#

Giống như các giá trị trong một Series, các tên hàng và tên cột trong một DataFrame có thể được chuyển đổi tương tự bằng một hàm hoặc mapping để tạo ra các chỉ số mới. Bạn cũng có thể sửa đổi các chỉ số mà không cần tạo ra một cấu trúc dữ liệu mới. Đây là một ví dụ đơn giản:

data = pd.DataFrame(np.arange(12).reshape((3, 4)),
                    index=["Ohio", "Colorado", "New York"],
                    columns=["one", "two", "three", "four"])
data
one two three four
Ohio 0 1 2 3
Colorado 4 5 6 7
New York 8 9 10 11

Giống như một Series, chỉ số của trục có một phương thức map:

def transform(x):
    return x[:4].upper()

data.index.map(transform)
# Out[72]: Index(['OHIO', 'COLO', 'NEW '], dtype='object')
Index(['OHIO', 'COLO', 'NEW '], dtype='object')

Bạn có thể gán cho thuộc tính index, sửa đổi DataFrame trực tiếp:

data.index = data.index.map(transform)
data
# Out[74]:
#        one  two  three  four
# OHIO     0    1      2     3
# COLO     4    5      6     7
# NEW      8    9     10    11
one two three four
OHIO 0 1 2 3
COLO 4 5 6 7
NEW 8 9 10 11

Nếu bạn muốn tạo một phiên bản đã được chuyển đổi của một tập dữ liệu mà không sửa đổi bản gốc, một phương thức hữu ích là rename:

data.rename(index=str.title, columns=str.upper)
# Out[75]:
#        ONE  TWO  THREE  FOUR
# Ohio     0    1      2     3
# Colo     4    5      6     7
# New      8    9     10    11
ONE TWO THREE FOUR
Ohio 0 1 2 3
Colo 4 5 6 7
New 8 9 10 11

Đáng chú ý là rename có thể được sử dụng kết hợp với một đối tượng giống từ điển, cung cấp các giá trị mới cho một tập hợp con các tên hàng hoặc tên cột:

data.rename(index={"OHIO": "INDIANA"},
            columns={"three": "peekaboo"})
# Out[76]:
#          one  two  peekaboo  four
# INDIANA    0    1         2     3
# COLO       4    5         6     7
# NEW        8    9        10    11
one two peekaboo four
INDIANA 0 1 2 3
COLO 4 5 6 7
NEW 8 9 10 11

rename giúp bạn không phải thực hiện công việc sao chép DataFrame thủ công và gán các giá trị mới cho các thuộc tính indexcolumns của nó.

8.2.5. Rời rạc hóa và phân nhóm#

Trong nhiều trường hợp, dữ liệu liên tục thường được rời rạc hóa hoặc tách thành các nhóm (bins) để phân tích. Giả sử bạn có dữ liệu về một nhóm người trong một nghiên cứu và bạn muốn nhóm họ thành các nhóm tuổi riêng biệt:

ages = [20, 22, 25, 27, 21, 23, 37, 31, 61, 45, 41, 32]

Yêu cầu đặt ra là chia những tuổi này thành các nhóm từ 18 đến 25, 26 đến 35, 36 đến 60 và cuối cùng là 61 tuổi trở lên. Để làm điều này, bạn phải sử dụng pandas.cut:

bins = [18, 25, 35, 60, 100]
age_categories = pd.cut(ages, bins)
age_categories
# Out[80]:
# [(18, 25], (18, 25], (18, 25], (25, 35], (18, 25], ..., (25, 35], (60, 100], (35, 60], (35, 60], (25, 35]]
# Length: 12
# Categories (4, interval[int64, right]): [(18, 25] < (25, 35] < (35, 60] < (60, 100]]
[(18, 25], (18, 25], (18, 25], (25, 35], (18, 25], ..., (25, 35], (60, 100], (35, 60], (35, 60], (25, 35]]
Length: 12
Categories (4, interval[int64, right]): [(18, 25] < (25, 35] < (35, 60] < (60, 100]]

Đối tượng mà pandas trả về là một đối tượng kiểu phân loại, hay categorical, đặc biệt. Kết quả hiển thị mô tả các nhóm được tính toán bởi pandas.cut. Mỗi nhóm được xác định bởi một kiểu giá trị khoảng đặc biệt chứa giới hạn dưới và giới hạn trên của mỗi nhóm:

age_categories.codes
# Out[81]: array([0, 0, 0, 1, 0, 0, 2, 1, 3, 2, 2, 1], dtype=int8)

age_categories.categories
# Out[82]: IntervalIndex([(18, 25], (25, 35], (35, 60], (60, 100]],
#           dtype='interval[int64, right]')

age_categories.categories[0]
# Out[83]: Interval(18, 25, closed='right')

pd.value_counts(age_categories)
# Out[84]:
# (18, 25]     5
# (25, 35]     3
# (35, 60]     3
# (60, 100]    1
# Name: count, dtype: int64
(18, 25]     5
(25, 35]     3
(35, 60]     3
(60, 100]    1
Name: count, dtype: int64

Lưu ý rằng pd.value_counts(categories) là số lượng nhóm cho kết quả của pandas.cut.

Trong biểu diễn chuỗi của một khoảng, một dấu ngoặc đơn có nghĩa là phía đó mở (không bao gồm), trong khi dấu ngoặc vuông có nghĩa là phía đó đóng (bao gồm). Bạn có thể thay đổi phía nào được đóng bằng cách truyền right=False:

pd.cut(ages, bins, right=False)
# Out[85]:
# [[18, 25), [18, 25), [25, 35), [25, 35), [18, 25), ..., [25, 35), [60, 100), [35, 60), [35, 60), [25, 35)]
# Length: 12
# Categories (4, interval[int64, left]): [[18, 25) < [25, 35) < [35, 60) < [60, 100)]
[[18, 25), [18, 25), [25, 35), [25, 35), [18, 25), ..., [25, 35), [60, 100), [35, 60), [35, 60), [25, 35)]
Length: 12
Categories (4, interval[int64, left]): [[18, 25) < [25, 35) < [35, 60) < [60, 100)]

Bạn có thể ghi đè tên nhóm dựa trên khoảng mặc định bằng cách truyền một danh sách hoặc mảng cho tùy chọn labels:

group_names = ["Youth", "YoungAdult", "MiddleAged", "Senior"]
pd.cut(ages, bins, labels=group_names)
# Out[87]:
# ['Youth', 'Youth', 'Youth', 'YoungAdult', 'Youth', ..., 'YoungAdult', 'Senior', 'MiddleAged', 'MiddleAged', 'YoungAdult']
# Length: 12
# Categories (4, object): ['Youth' < 'YoungAdult' < 'MiddleAged' < 'Senior']
['Youth', 'Youth', 'Youth', 'YoungAdult', 'Youth', ..., 'YoungAdult', 'Senior', 'MiddleAged', 'MiddleAged', 'YoungAdult']
Length: 12
Categories (4, object): ['Youth' < 'YoungAdult' < 'MiddleAged' < 'Senior']

Nếu bạn gán một số nguyên, truyền tham số số lượng nhóm vào pandas.cut thay vì các giới hạn nhóm rõ ràng, pandas sẽ tính toán các nhóm có chiều dài bằng nhau dựa trên các giá trị tối thiểu và tối đa trong dữ liệu. Hãy xem xét trường hợp một số dữ liệu được phân bố đều được chia thành bốn phần:

data = np.random.uniform(size=20)
pd.cut(data, 4, precision=2)
# Out[89]:
# [(0.34, 0.55], (0.34, 0.55], (0.76, 0.97], (0.76, 0.97], (0.34, 0.55], ...,
#  (0.34, 0.55], (0.34, 0.55], (0.55, 0.76], (0.34, 0.55], (0.12, 0.34]]
# Length: 20
# Categories (4, interval[float64, right]): [(0.12, 0.34] < (0.34, 0.55] < (0.55, 0.76] < (0.76, 0.97]]
[(0.34, 0.55], (0.34, 0.55], (0.76, 0.97], (0.76, 0.97], (0.34, 0.55], ..., (0.34, 0.55], (0.34, 0.55], (0.55, 0.76], (0.34, 0.55], (0.12, 0.34]]
Length: 20
Categories (4, interval[float64, right]): [(0.12, 0.34] < (0.34, 0.55] < (0.55, 0.76] < (0.76, 0.97]]

Tùy chọn precision=2 giới hạn độ chính xác thập phân đến hai chữ số.

Một hàm liên quan chặt chẽ đến phân nhóm dữ liệu là pandas.qcut có thể phân nhóm dữ liệu dựa trên các phân vị. Tùy thuộc vào sự phân bố của dữ liệu, việc sử dụng pandas.cut thường sẽ không dẫn đến việc mỗi nhóm có cùng số lượng điểm dữ liệu. Vì pandas.qcut sử dụng các phân vị mẫu thay vào đó, bạn sẽ thu được các nhóm có kích thước gần bằng nhau:

data = np.random.standard_normal(1000)
quartiles = pd.qcut(data, 4, precision=2)
quartiles
# Out[92]:
# [(-0.026, 0.62], (0.62, 3.93], (-0.68, -0.026], (0.62, 3.93], (-0.026, 0.62], ...,
#  (-0.68, -0.026], (-0.68, -0.026], (-2.96, -0.68], (0.62, 3.93], (-0.68, -0.026]]
# Length: 1000
# Categories (4, interval[float64, right]): [(-2.96, -0.68] < (-0.68, -0.026] < (-0.026, 0.62] < (0.62, 3.93]]

pd.value_counts(quartiles)
# Out[93]:
# (-2.96, -0.68]     250
# (-0.68, -0.026]    250
# (-0.026, 0.62]     250
# (0.62, 3.93]      250
# Name: count, dtype: int64
(-2.96, -0.68]     250
(-0.68, -0.026]    250
(-0.026, 0.62]     250
(0.62, 3.93]       250
Name: count, dtype: int64

Tương tự như pandas.cut, bạn có thể tự tạo các phân vị theo ý mình mình:

pd.qcut(data, [0, 0.1, 0.5, 0.9, 1.]).value_counts()
# Out[94]:
# (-2.9499999999999997, -1.187]    100
# (-1.187, -0.0265]               400
# (-0.0265, 1.286]                400
# (1.286, 3.928]                  100
# Name: count, dtype: int64
(-2.9499999999999997, -1.187]    100
(-1.187, -0.0265]                400
(-0.0265, 1.286]                 400
(1.286, 3.928]                   100
Name: count, dtype: int64

Chúng ta sẽ quay lại pandas.cutpandas.qcut sau trong chương này trong phần thảo luận về tổng hợp và các thao tác nhóm, vì các hàm rời rạc hóa này đặc biệt hữu ích cho phân tích phân vị và nhóm.

8.2.6. Phát hiện và xử lý các giá trị ngoại lai#

Việc lọc hoặc chuyển đổi các giá trị ngoại lai (outliers) phần lớn là được thao tác bằng các phép toán mảng. Hãy xem xét một DataFrame có một số dữ liệu phân phối chuẩn:

data = pd.DataFrame(np.random.standard_normal((1000, 4)))
data.describe()
# Out[96]:
#                0           1           2           3
# count  1000.000000  1000.000000  1000.000000  1000.000000
# mean      0.049091    0.026112   -0.002544   -0.051827
# std       0.996947    1.007458    0.995232    0.998311
# min      -3.645860   -3.184377   -3.745356   -3.428254
# 25%      -0.599807   -0.612162   -0.687373   -0.747478
# 50%       0.047101   -0.013609   -0.022158   -0.088274
# 75%       0.756646    0.695298    0.699046    0.623331
# max       2.653656    3.525865    2.735527    3.366626
0 1 2 3
count 1000.000000 1000.000000 1000.000000 1000.000000
mean 0.049091 0.026112 -0.002544 -0.051827
std 0.996947 1.007458 0.995232 0.998311
min -3.645860 -3.184377 -3.745356 -3.428254
25% -0.599807 -0.612162 -0.687373 -0.747478
50% 0.047101 -0.013609 -0.022158 -0.088274
75% 0.756646 0.695298 0.699046 0.623331
max 2.653656 3.525865 2.735527 3.366626

Giả sử bạn muốn tìm các giá trị trong một trong các cột vượt quá 3 về giá trị tuyệt đối:

col = data[2]
col[col.abs() > 3]
# Out[98]:
# 41    -3.399312
# 136   -3.745356
# Name: 2, dtype: float64
41    -3.399312
136   -3.745356
Name: 2, dtype: float64

Để chọn tất cả các hàng có một giá trị vượt quá 3 hoặc –3, bạn có thể sử dụng phương thức any trên một DataFrame chứa toàn các giá trị logical:

data[(data.abs() > 3).any(axis="columns")]
# Out[99]:
#             0         1         2         3
# 41   0.457246 -0.025907 -3.399312 -0.974657
# 60   1.951312  3.260383  0.963301  1.201206
# 136  0.508391 -0.196713 -3.745356 -1.520113
# 235 -0.242459 -3.056990  1.918403 -0.578828
# 258  0.682841  0.326045  0.425384 -3.428254
# 322  1.179227 -3.184377  1.369891 -1.074833
# 544 -3.548824  1.553205 -2.186301  1.277104
# 635 -0.578093  0.193299  1.397822  3.366626
# 782 -0.207434  3.525865  0.283070  0.544635
# 803 -3.645860  0.255475 -0.549574 -1.907459
0 1 2 3
41 0.457246 -0.025907 -3.399312 -0.974657
60 1.951312 3.260383 0.963301 1.201206
136 0.508391 -0.196713 -3.745356 -1.520113
235 -0.242459 -3.056990 1.918403 -0.578828
258 0.682841 0.326045 0.425384 -3.428254
322 1.179227 -3.184377 1.369891 -1.074833
544 -3.548824 1.553205 -2.186301 1.277104
635 -0.578093 0.193299 1.397822 3.366626
782 -0.207434 3.525865 0.283070 0.544635
803 -3.645860 0.255475 -0.549574 -1.907459

Dấu ngoặc đơn xung quanh data.abs() > 3 là cần thiết để gọi phương thức any trên kết quả của phép toán so sánh.

Các giá trị có thể được đặt dựa trên các tiêu chí này. Đoạn câu lệnh dưới đây là để giới hạn các giá trị nằm ngoài khoảng từ –3 đến 3:

data[data.abs() > 3] = np.sign(data) * 3
data.describe()
# Out[101]:
#                0           1           2           3
# count  1000.000000  1000.000000  1000.000000  1000.000000
# mean      0.050286    0.025567   -0.001399   -0.051765
# std       0.992920    1.004214    0.991414    0.995761
# min      -3.000000   -3.000000   -3.000000   -3.000000
# 25%      -0.599807   -0.612162   -0.687373   -0.747478
# 50%       0.047101   -0.013609   -0.022158   -0.088274
# 75%       0.756646    0.695298    0.699046    0.623331
# max       2.653656    3.000000    2.735527    3.000000
0 1 2 3
count 1000.000000 1000.000000 1000.000000 1000.000000
mean 0.050286 0.025567 -0.001399 -0.051765
std 0.992920 1.004214 0.991414 0.995761
min -3.000000 -3.000000 -3.000000 -3.000000
25% -0.599807 -0.612162 -0.687373 -0.747478
50% 0.047101 -0.013609 -0.022158 -0.088274
75% 0.756646 0.695298 0.699046 0.623331
max 2.653656 3.000000 2.735527 3.000000

Câu lệnh np.sign(data) tạo ra các giá trị 1 và –1 dựa trên việc các giá trị trong data là dương hay âm:

np.sign(data).head()
# Out[102]:
#      0    1    2    3
# 0 -1.0  1.0 -1.0  1.0
# 1  1.0 -1.0  1.0 -1.0
# 2  1.0  1.0  1.0 -1.0
# 3 -1.0 -1.0  1.0 -1.0
# 4 -1.0  1.0 -1.0 -1.0
0 1 2 3
0 -1.0 1.0 -1.0 1.0
1 1.0 -1.0 1.0 -1.0
2 1.0 1.0 1.0 -1.0
3 -1.0 -1.0 1.0 -1.0
4 -1.0 1.0 -1.0 -1.0

8.2.7. Tính toán biến chỉ báo#

Một kiểu biến đổi biến thường được sử dụng cho mục đích xây dựng mô hình thống kê hoặc các ứng dụng học máy là chuyển đổi một biến phân loại thành một ma trận biến giả hay biến chỉ báo. Nếu một cột trong DataFrame có J giá trị riêng biệt, bạn sẽ tạo ra một ma trận hoặc DataFrame mới có J cột chỉ chứa giá trị 0 và 1. Trong thư viện pandas có hàm pandas.get_dummies để thực hiện điều này:

df = pd.DataFrame({"key": ["b", "b", "a", "c", "a", "b"],
                   "data1": range(6)})
df
# Out[116]:
#   key  data1
# 0   b      0
# 1   b      1
# 2   a      2
# 3   c      3
# 4   a      4
# 5   b      5

pd.get_dummies(df["key"], dtype=float)
# Out[117]:
#      a    b    c
# 0  0.0  1.0  0.0
# 1  0.0  1.0  0.0
# 2  1.0  0.0  0.0
# 3  0.0  0.0  1.0
# 4  1.0  0.0  0.0
# 5  0.0  1.0  0.0
a b c
0 0.0 1.0 0.0
1 0.0 1.0 0.0
2 1.0 0.0 0.0
3 0.0 0.0 1.0
4 1.0 0.0 0.0
5 0.0 1.0 0.0

Trong hàm get_dummies đã sử dụng tham số dtype=float để thay đổi kiểu đầu ra sang số thực.

Trong một số trường hợp, bạn có thể muốn thêm một tiền tố vào tên các cột trong DataFrame. pandas.get_dummies có tham số prefix để thực hiện việc này:

dummies = pd.get_dummies(df["key"], prefix="key", dtype=float)
df_with_dummy = df[["data1"]].join(dummies)
df_with_dummy
# Out[120]:
#    data1  key_a  key_b  key_c
# 0      0    0.0    1.0    0.0
# 1      1    0.0    1.0    0.0
# 2      2    1.0    0.0    0.0
# 3      3    0.0    0.0    1.0
# 4      4    1.0    0.0    0.0
# 5      5    0.0    1.0    0.0
data1 key_a key_b key_c
0 0 0.0 1.0 0.0
1 1 0.0 1.0 0.0
2 2 1.0 0.0 0.0
3 3 0.0 0.0 1.0
4 4 1.0 0.0 0.0
5 5 0.0 1.0 0.0

Phương thức join sử dụng ở trên sẽ được giải thích chi tiết hơn trong chương tiếp theo.

Nếu một hàng trong DataFrame thuộc về nhiều danh mục, chúng ta phải sử dụng một cách tiếp cận khác để tạo các biến giả. Hãy xem ví dụ về dữ liệu dưới đây:

mnames = ["movie_id", "title", "genres"]
movies = pd.read_table("datasets/movielens/movies.dat", sep="::",
                       header=None, names=mnames, engine="python")
movies[:10]
# Out[123]:
#    movie_id                               title                        genres
# 0         1                    Toy Story (1995)   Animation|Children's|Comedy
# 1         2                      Jumanji (1995)  Adventure|Children's|Fantasy
# 2         3             Grumpier Old Men (1995)                Comedy|Romance
# 3         4            Waiting to Exhale (1995)                  Comedy|Drama
# 4         5  Father of the Bride Part II (1995)                        Comedy
# 5         6                         Heat (1995)          Action|Crime|Thriller
# 6         7                      Sabrina (1995)                Comedy|Romance
# 7         8                 Tom and Huck (1995)          Adventure|Children's
# 8         9                  Sudden Death (1995)                        Action
# 9        10                    GoldenEye (1995)      Action|Adventure|Thriller
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
Cell In[49], line 2
      1 mnames = ["movie_id", "title", "genres"]
----> 2 movies = pd.read_table("datasets/movielens/movies.dat", sep="::",
      3                        header=None, names=mnames, engine="python")
      4 movies[:10]
      5 # Out[123]:
      6 #    movie_id                               title                        genres
      7 # 0         1                    Toy Story (1995)   Animation|Children's|Comedy
   (...)     15 # 8         9                  Sudden Death (1995)                        Action
     16 # 9        10                    GoldenEye (1995)      Action|Adventure|Thriller

File ~\AppData\Local\Programs\Python\Python313\Lib\site-packages\pandas\io\parsers\readers.py:1405, in read_table(filepath_or_buffer, sep, delimiter, header, names, index_col, usecols, dtype, engine, converters, true_values, false_values, skipinitialspace, skiprows, skipfooter, nrows, na_values, keep_default_na, na_filter, verbose, skip_blank_lines, parse_dates, infer_datetime_format, keep_date_col, date_parser, date_format, dayfirst, cache_dates, iterator, chunksize, compression, thousands, decimal, lineterminator, quotechar, quoting, doublequote, escapechar, comment, encoding, encoding_errors, dialect, on_bad_lines, delim_whitespace, low_memory, memory_map, float_precision, storage_options, dtype_backend)
   1392 kwds_defaults = _refine_defaults_read(
   1393     dialect,
   1394     delimiter,
   (...)   1401     dtype_backend=dtype_backend,
   1402 )
   1403 kwds.update(kwds_defaults)
-> 1405 return _read(filepath_or_buffer, kwds)

File ~\AppData\Local\Programs\Python\Python313\Lib\site-packages\pandas\io\parsers\readers.py:620, in _read(filepath_or_buffer, kwds)
    617 _validate_names(kwds.get("names", None))
    619 # Create the parser.
--> 620 parser = TextFileReader(filepath_or_buffer, **kwds)
    622 if chunksize or iterator:
    623     return parser

File ~\AppData\Local\Programs\Python\Python313\Lib\site-packages\pandas\io\parsers\readers.py:1620, in TextFileReader.__init__(self, f, engine, **kwds)
   1617     self.options["has_index_names"] = kwds["has_index_names"]
   1619 self.handles: IOHandles | None = None
-> 1620 self._engine = self._make_engine(f, self.engine)

File ~\AppData\Local\Programs\Python\Python313\Lib\site-packages\pandas\io\parsers\readers.py:1880, in TextFileReader._make_engine(self, f, engine)
   1878     if "b" not in mode:
   1879         mode += "b"
-> 1880 self.handles = get_handle(
   1881     f,
   1882     mode,
   1883     encoding=self.options.get("encoding", None),
   1884     compression=self.options.get("compression", None),
   1885     memory_map=self.options.get("memory_map", False),
   1886     is_text=is_text,
   1887     errors=self.options.get("encoding_errors", "strict"),
   1888     storage_options=self.options.get("storage_options", None),
   1889 )
   1890 assert self.handles is not None
   1891 f = self.handles.handle

File ~\AppData\Local\Programs\Python\Python313\Lib\site-packages\pandas\io\common.py:873, in get_handle(path_or_buf, mode, encoding, compression, memory_map, is_text, errors, storage_options)
    868 elif isinstance(handle, str):
    869     # Check whether the filename is to be opened in binary mode.
    870     # Binary mode does not support 'encoding' and 'newline'.
    871     if ioargs.encoding and "b" not in ioargs.mode:
    872         # Encoding
--> 873         handle = open(
    874             handle,
    875             ioargs.mode,
    876             encoding=ioargs.encoding,
    877             errors=errors,
    878             newline="",
    879         )
    880     else:
    881         # Binary mode
    882         handle = open(handle, ioargs.mode)

FileNotFoundError: [Errno 2] No such file or directory: 'datasets/movielens/movies.dat'

pandas có phương thức dành cho chuỗi ký tự str.get_dummies có thể xử lý trường hợp này:

dummies = movies["genres"].str.get_dummies("|")
dummies.iloc[:10, :6]
# Out[125]:
#    Action  Adventure  Animation  Children's  Comedy  Crime
# 0       0          0          1           1       1      0
# 1       0          1          0           1       0      0
# 2       0          0          0           0       1      0
# 3       0          0          0           0       1      0
# 4       0          0          0           0       1      0
# 5       1          0          0           0       0      1
# 6       0          0          0           0       1      0
# 7       0          1          0           1       0      0
# 8       1          0          0           0       0      0
# 9       1          1          0           0       0      0

Lưu ý: Đối với dữ liệu lớn hơn, phương pháp xây dựng các biến phân loại nhận nhiều giá trị sẽ không hiệu quả về thời gian. Sẽ tốt hơn nếu viết chúng ta xử lý dưới dạng mảng trong NumPy, sau đó chuyển sang kiểu DataFrame.

Một công thức hữu ích cho các ứng dụng thống kê là kết hợp pandas.get_dummies với một hàm rời rạc hóa như pandas.cut:

np.random.seed(12345) # để làm cho ví dụ có thể lặp lại
values = np.random.uniform(size=10)
values
# Out[130]: array([0.9296, 0.3164, 0.1839, 0.2046, 0.5677, 0.5955, 0.9645, 0.6532,
#        0.7489, 0.6536])

bins = [0, 0.2, 0.4, 0.6, 0.8, 1]
pd.get_dummies(pd.cut(values, bins))
# Out[132]:
#    (0.0, 0.2]  (0.2, 0.4]  (0.4, 0.6]  (0.6, 0.8]  (0.8, 1.0]
# 0       False       False       False       False        True
# 1       False        True       False       False       False
# 2        True       False       False       False       False
# 3       False        True       False       False       False
# 4       False       False        True       False       False
# 5       False       False        True       False       False
# 6       False       False       False       False        True
# 7       False       False       False        True       False
# 8       False       False       False        True       False
# 9       False       False       False        True       False

Chúng ta sẽ xem lại pandas.get_dummies sau các phần tiếp theo của sách.

8.3. Thao tác trên chuỗi ký tự#


Python từ lâu đã là một ngôn ngữ phổ biến để thao tác dữ liệu thô một phần nhờ vào sự dễ dàng trong việc xử lý chuỗi và văn bản. Hầu hết các thao tác văn bản đều khả thi với các phương thức tích hợp sẵn của đối tượng chuỗi. Đối với các thao tác xử lý văn bản phức tạp hơn, có thể cần đến các biểu thức chính quy. pandas bổ sung vào đó bằng cách cho phép bạn áp dụng các biểu thức chuỗi và biểu thức chính quy một cách ngắn gọn trên toàn bộ mảng dữ liệu, đồng thời xử lý sự phiền phức của dữ liệu bị thiếu.

8.3.1. Các phương thức đối tượng chuỗi ký tự tích hợp sẵn của Python#

Trong nhiều ứng dụng xử lý chuỗi và viết kịch bản, các phương thức chuỗi tích hợp sẵn là đủ. Ví dụ, một chuỗi được phân tách bằng dấu phẩy có thể được chia thành các phần bằng split:

val = "a,b,  guido"
val.split(",")
# Out[152]: ['a', 'b', ' guido']

split thường được kết hợp với strip để loại bỏ khoảng trắng, bao gồm cả ngắt dòng:

pieces = [x.strip() for x in val.split(",")]
pieces
# Out[154]: ['a', 'b', 'guido']

Các chuỗi ký tự con này có thể được nối lại với nhau bằng một dấu hai chấm kép làm dấu phân cách bằng cách sử dụng phép cộng:

first, second, third = pieces
first + "::" + second + "::" + third
# Out[156]: 'a::b::guido'

Nhưng đây không phải là một phương pháp chung thực tế. Một cách nhanh hơn là sử dụng phương thức join trên chuỗi "::":

"::".join(pieces)
# Out[157]: 'a::b::guido'

Có các phương thức khác liên quan đến việc định vị chuỗi con. Sử dụng từ khóa in của Python là cách tốt nhất để phát hiện một chuỗi con, mặc dù indexfind cũng có thể được sử dụng:

"guido" in val
# Out[158]: True

val.index(",")
# Out[159]: 1

val.find(":")
# Out[160]: -1

Lưu ý rằng sự khác biệt giữa findindexindex sẽ báo lỗi nếu chuỗi ký tự không được tìm thấy thay vì trả về –1:

# val.index(":")
# ---------------------------------------------------------------------------
# ValueError                                Traceback (most recent call last)
# <ipython-input-161-bea4c4c30248> in <module>
# ----> 1 val.index(":")
# 
# ValueError: substring not found
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-161-bea4c4c30248> in <module>
----> 1 val.index(":")

ValueError: substring not found

Liên quan đến điều này, count trả về số lần xuất hiện của một chuỗi con cụ thể:

val.count(",")
# Out[162]: 2

Phương thức replace sẽ thay thế các lần xuất hiện của một chuỗi ký tự bằng một chuỗi ký tự khác. Phương thức này cũng thường được sử dụng để xóa các chuỗi ký tự bằng cách truyền một chuỗi ký tự trống:

val.replace(",", "::")
# Out[163]: 'a::b:: guido'

val.replace(",", "")
# Out[164]: 'ab guido'

Xem Bảng …. liệt kê danh sách các phương thức chuỗi của Python.

Bảng … các phương thức chuỗi tích hợp sẵn của Python

Phương thức

Mô tả

count

Trả về số lần xuất hiện không chồng chéo của chuỗi con trong chuỗi.

endswith

Trả về True nếu chuỗi kết thúc bằng hậu tố.

startswith

Trả về True nếu chuỗi bắt đầu bằng tiền tố.

join

Sử dụng chuỗi làm dấu phân cách để nối một chuỗi các chuỗi khác.

index

Trả về chỉ mục bắt đầu của lần xuất hiện đầu tiên của chuỗi con được truyền nếu tìm thấy trong chuỗi; ngược lại, gây ra ValueError nếu không tìm thấy.

find

Trả về vị trí của ký tự đầu tiên của lần xuất hiện đầu tiên của chuỗi con trong chuỗi; giống như index, nhưng trả về –1 nếu không tìm thấy.

rfind

Trả về vị trí của ký tự đầu tiên của lần xuất hiện cuối cùng của chuỗi con trong chuỗi; trả về –1 nếu không tìm thấy.

replace

Thay thế các lần xuất hiện của chuỗi bằng một chuỗi khác.

strip, rstrip, lstrip

Loại bỏ khoảng trắng, bao gồm cả ngắt dòng ở cả hai bên, ở bên phải, hoặc ở bên trái, tương ứng.

split

Chia chuỗi thành danh sách các chuỗi con bằng cách sử dụng dấu phân cách được truyền.

lower

Chuyển đổi các ký tự chữ cái thành chữ thường.

upper

Chuyển đổi các ký tự chữ cái thành chữ hoa.

casefold

Chuyển đổi các ký tự thành chữ thường, và chuyển đổi bất kỳ tổ hợp ký tự biến thể theo vùng cụ thể nào thành một dạng chung có thể so sánh được.

ljust, rjust

Căn lề trái hoặc căn lề phải, tương ứng; đệm phía đối diện của chuỗi bằng khoảng trắng (hoặc một ký tự điền khác) để trả về một chuỗi có chiều rộng tối thiểu.

8.3.2. Biểu thức chính quy#

Biểu thức chính quy cung cấp một cách linh hoạt để tìm kiếm hoặc mapping các chuỗi ký tự trong văn bản. Một biểu thức, thường được gọi là regex, là một chuỗi được hình thành theo ngôn ngữ biểu thức chính quy. Thư viện re tích hợp sẵn của Python cho phép áp dụng các biểu thức chính quy cho các chuỗi; chúng ta sẽ tìm hiểu về regex thông qua một số ví dụ về cách sử dụng.

Lưu ý: Trình bày đầy đủ về biểu thức chính quy có thể yêu cầu một cuốn sách riêng và do đó nằm ngoài phạm vi của cuốn sách này. Có rất nhiều hướng dẫn và tài liệu tham khảo tuyệt vời có sẵn trên internet và trong các cuốn sách khác.

Các hàm của thư viện re được chia thành ba loại: mapping, thay thế và chia tách chuỗi ký tự. Hãy xem một ví dụ đơn giản: giả sử chúng ta muốn chia một chuỗi được phân tách bằng một số lượng ký tự khoảng trắng, có thể là tab, dấu cách hoặc ngắt dòng.

Regex mô tả một hoặc nhiều ký tự khoảng trắng là \s+:

import re
text = "foo    bar\t baz  \tqux"
re.split(r"\s+", text)
# Out[167]: ['foo', 'bar', 'baz', 'qux']

Khi bạn gọi re.split(r"\s+", text), biểu thức chính quy được biên dịch trước, và sau đó phương thức split được gọi trên văn bản được truyền vào. Trong Python, thêm chữ r trước một chuỗi (ví dụ: r”\s+”) nghĩa là bạn đang viết raw string — chuỗi “thô”, để tránh việc phải sử dụng \\.

Bạn có thể tự tạo một đối tượng regex có thể tái sử dụng bằng phương thức compile như sau

regex = re.compile(r"\s+")
regex.split(text)
# Out[169]: ['foo', 'bar', 'baz', 'qux']

Nếu bạn muốn lấy một danh sách tất cả các mẫu khớp với regex, bạn có thể sử dụng phương thức findall:

regex.findall(text)
# Out[170]: ['    ', '\t ', '  \t']

Việc tạo một đối tượng regex bằng re.compile được khuyến khích nếu bạn có ý định áp dụng cùng một biểu thức cho nhiều chuỗi; làm như vậy sẽ tiết kiệm thời gian tính toán thay vì phải gọi thư viện re cho từng lần.

matchsearch có liên quan chặt chẽ với findall. Trong khi findall trả về tất cả các kết quả khớp trong một chuỗi, search chỉ trả về kết quả khớp đầu tiên. Chặt chẽ hơn, match chỉ khớp ở đầu chuỗi. Hãy xem xét một văn bản và một biểu thức chính quy có khả năng xác định hầu hết các địa chỉ email trong một văn bản:

text = """Dave dave@google.com
Steve steve@gmail.com
Rob rob@gmail.com
Ryan ryan@yahoo.com"""
pattern = r"[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}"

# re.IGNORECASE làm cho regex không phân biệt chữ hoa chữ thường
regex = re.compile(pattern, flags=re.IGNORECASE)

Sử dụng findall trên văn bản sẽ tạo ra một danh sách các địa chỉ email:

regex.findall(text)
# Out[172]: ['dave@google.com', 'steve@gmail.com', 'rob@gmail.com', 'ryan@yahoo.com']

search trả về một đối tượng khớp đặc biệt cho địa chỉ email đầu tiên trong văn bản. Đối với regex ở trên, đối tượng khớp chỉ có thể cho chúng ta biết vị trí bắt đầu và kết thúc của mẫu trong chuỗi:

m = regex.search(text)
m
# Out[174]: <re.Match object; span=(5, 20), match='dave@google.com'>

text[m.start():m.end()]
# Out[175]: 'dave@google.com'

regex.match trả về None, vì nó sẽ chỉ khớp nếu mẫu xuất hiện ở đầu chuỗi:

print(regex.match(text))
# Out: None
None

Cũng liên quan đến tìm kiếm và khớp văn bản, sub sẽ trả về một chuỗi mới với các lần xuất hiện của mẫu được thay thế bằng một chuỗi mới:

print(regex.sub("REDACTED", text))
# Out: 
# Dave REDACTED
# Steve REDACTED
# Rob REDACTED
# Ryan REDACTED
Dave REDACTED
Steve REDACTED
Rob REDACTED
Ryan REDACTED

Giả sử bạn muốn tìm các địa chỉ email và đồng thời phân đoạn từng địa chỉ thành ba thành phần của nó: tên người dùng, tên miền và hậu tố tên miền. Để làm điều này, hãy đặt dấu ngoặc đơn xung quanh các phần của mẫu để phân đoạn:

pattern = r"([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\.([A-Z]{2,4})"
regex = re.compile(pattern, flags=re.IGNORECASE)

Một đối tượng khớp được tạo bởi regex đã sửa đổi này trả về một tuple các thành phần mẫu bằng phương thức groups của nó:

m = regex.match("wesm@bright.net")
m.groups()
# Out[181]: ('wesm', 'bright.net', 'net') # Chú ý: trang web gốc có 'bright' thay vì 'bright.net', regex của tôi bắt 'bright.net'

findall trả về một danh sách các tuple khi mẫu có các nhóm:

regex.findall(text)
# Out[182]: [('dave', 'google.com', 'com'), ('steve', 'gmail.com', 'com'), ('rob', 'gmail.com', 'com'), ('ryan', 'yahoo.com', 'com')]

sub cũng có quyền truy cập vào các nhóm trong mỗi kết quả khớp bằng cách sử dụng các ký hiệu đặc biệt như \1\2. Ký hiệu \1 tương ứng với nhóm khớp đầu tiên, \2 tương ứng với nhóm thứ hai, v.v.:

print(regex.sub(r"Username: \1, Domain: \2, Suffix: \3", text))
# Out:
# Dave Username: dave, Domain: google.com, Suffix: com
# Steve Username: steve, Domain: gmail.com, Suffix: com
# Rob Username: rob, Domain: gmail.com, Suffix: com
# Ryan Username: ryan, Domain: yahoo.com, Suffix: com
Dave Username: dave, Domain: google.com, Suffix: com
Steve Username: steve, Domain: gmail.com, Suffix: com
Rob Username: rob, Domain: gmail.com, Suffix: com
Ryan Username: ryan, Domain: yahoo.com, Suffix: com

Có rất nhiều điều khác về biểu thức chính quy trong Python, hầu hết nằm ngoài phạm vi của cuốn sách này. Bảng 7.5 cung cấp một bản tóm tắt ngắn gọn.

Bảng … Các phương thức biểu thức chính quy

Phương thức

Mô tả

findall

Trả về tất cả các mẫu khớp không chồng chéo trong một chuỗi dưới dạng một danh sách.

finditer

Giống như findall, nhưng trả về một iterator.

match

Khớp mẫu ở đầu chuỗi và tùy chọn phân đoạn các thành phần mẫu thành các nhóm; nếu mẫu khớp, trả về một đối tượng khớp, ngược lại là None.

search

Quét chuỗi để tìm khớp với mẫu, trả về một đối tượng Match nếu có; không giống như match, khớp có thể ở bất kỳ đâu trong chuỗi thay vì chỉ ở đầu.

split

Chia chuỗi thành các phần ở mỗi lần xuất hiện của mẫu.

sub, subn

Thay thế tất cả (sub) hoặc n lần xuất hiện đầu tiên (subn) của mẫu trong chuỗi bằng biểu thức thay thế; sử dụng các ký hiệu \1, \2, … để tham chiếu đến các phần tử nhóm khớp trong chuỗi thay thế.

8.4. Dữ liệu kiểu phân loại#

Phần này của chương giới thiệu về kiểu dữ liệu phân loại, hay còn gọi là Categorical của pandas. Trước hết, chúng ta thảo luận về cách có thể đạt được hiệu suất tốt hơn và sử dụng bộ nhớ ít hơn trong một số hoạt động pandas bằng cách sử dụng nó. Sau đó, chúng ta sẽ được làm quen với một số công cụ có thể giúp sử dụng dữ liệu phân loại trong phân tích dữ liệu và xây dựng mô hình.

8.4.1. Dữ liệu kiểu phân loại là gì?#

Rất thường gặp trong thực tế dữ liệu là một cột trong bảng có thể chứa các giá trị lặp đi lặp lại của một tập nhỏ hơn các giá trị riêng biệt. Chúng ta đã thấy các hàm như uniquevalue_counts cho phép trích xuất các giá trị riêng biệt từ một mảng và tính toán tần suất của chúng, tương ứng:

values = pd.Series(['apple', 'orange', 'apple',
                    'apple'] * 2)
values
# Out[200]:
# 0     apple
# 1    orange
# 2     apple
# 3     apple
# 4     apple
# 5    orange
# 6     apple
# 7     apple
# dtype: object

pd.unique(values)
# Out[201]: array(['apple', 'orange'], dtype=object)

pd.value_counts(values)
# Out[202]:
# apple     6
# orange    2
# Name: count, dtype: int64

Nhiều hệ thống dữ liệu đã phát triển các phương pháp chuyên biệt để biểu diễn dữ liệu với các giá trị lặp lại nhằm lưu trữ và tính toán hiệu quả hơn. Một trong những phương pháp phổ biến là sử dụng các dimension tables chứa các giá trị riêng biệt và lưu trữ các quan sát chính dưới dạng các số nguyên tham chiếu đến bảng chiều:

values = pd.Series([0, 1, 0, 0] * 2)
dim = pd.Series(['apple', 'orange'])
values
# Out[205]:
# 0    0
# 1    1
# 2    0
# 3    0
# 4    0
# 5    1
# 6    0
# 7    0
# dtype: int64

dim
# Out[206]:
# 0     apple
# 1    orange
# dtype: object

Chúng ta có thể sử dụng phương thức take để khôi phục lại mảng ban đầu:

dim.take(values)
# Out[207]:
# 0     apple
# 1    orange
# 0     apple
# 0     apple
# 0     apple
# 1    orange
# 0     apple
# 0     apple
# dtype: object

Biểu diễn này dưới dạng số nguyên được gọi là biểu diễn phân loại hoặc mã hóa từ điển (dictionary-encoded). Mảng các giá trị riêng biệt có thể được gọi là các phân loại (categories), từ điển (dictionary), hoặc cấp độ (levels) của dữ liệu. Trong cuốn sách này, chúng ta sẽ sử dụng các thuật ngữ phân loại (categories). Các giá trị số nguyên tham chiếu đến các category được gọi là mã phân loại (category codes).

Biểu diễn phân loại có thể mang lại những cải thiện đáng kể về hiệu suất khi bạn thực hiện phân tích. Bạn cũng có thể thực hiện các phép biến đổi trên các phân loại trong khi vẫn giữ nguyên các câu lệnh. Một số ví dụ về các phép biến đổi có thể được thực hiện với chi phí tương đối thấp là:

  • Đổi tên các phân loại

  • Thêm một phân loại mới mà không thay đổi thứ tự hoặc vị trí của các phân loại hiện có

8.4.2. Kiểu phân loại trong pandas#

pandas có một kiểu mở rộng Categorical đặc biệt để lưu trữ dữ liệu sử dụng biểu diễn hoặc mã hóa phân loại dựa trên số nguyên. Đây là một kỹ thuật nén dữ liệu phổ biến cho dữ liệu có nhiều lần xuất hiện của các giá trị tương tự và có thể cung cấp hiệu suất nhanh hơn đáng kể với việc sử dụng bộ nhớ thấp hơn, đặc biệt đối với dữ liệu chuỗi.

Hãy xem xét ví dụ Series từ trước:

fruits = ['apple', 'orange', 'apple', 'apple'] * 2
N = len(fruits)
rng = np.random.default_rng(seed=12345)
df = pd.DataFrame({'fruit': fruits,
                   'basket_id': np.arange(N),
                   'count': rng.integers(3, 15, size=N),
                   'weight': rng.uniform(0, 4, size=N)},
                  columns=['basket_id', 'fruit', 'count', 'weight'])
df
# Out[212]:
#    basket_id   fruit  count    weight
# 0          0   apple     11  1.564438
# 1          1  orange      5  1.331256
# 2          2   apple     12  2.393235
# 3          3   apple      6  0.746937
# 4          4   apple      5  2.691024
# 5          5  orange     12  3.767211
# 6          6   apple     10  0.992983
# 7          7   apple     11  3.795525

Ở đây, df['fruit'] là một mảng các đối tượng chuỗi ký tự của Python. Chúng ta có thể chuyển đổi nó thành phân loại bằng cách gọi:

fruit_cat = df['fruit'].astype('category')
fruit_cat
# Out[214]:
# 0     apple
# 1    orange
# 2     apple
# 3     apple
# 4     apple
# 5    orange
# 6     apple
# 7     apple
# Name: fruit, dtype: category
# Categories (2, object): ['apple', 'orange']

Các giá trị cho fruit_cat bây giờ là một pandas.Categorical và có thể truy cập thông qua thuộc tính .array:

c = fruit_cat.array
type(c)
# Out[216]: pandas.core.arrays.categorical.Categorical

Đối tượng Categorical có các thuộc tính categoriescodes:

c.categories
# Out[217]: Index(['apple', 'orange'], dtype='object')

c.codes
# Out[218]: array([0, 1, 0, 0, 0, 1, 0, 0], dtype=int8)

Một cách để lấy ánh xạ giữa mã phân loại và giá trị là:

dict(enumerate(c.categories))
# Out[219]: {0: 'apple', 1: 'orange'}

Bạn có thể chuyển đổi một cột DataFrame thành phân loại bằng cách gán kết quả đã chuyển đổi:

df['fruit'] = df['fruit'].astype('category')
df["fruit"]
# Out[221]:
# 0     apple
# 1    orange
# 2     apple
# 3     apple
# 4     apple
# 5    orange
# 6     apple
# 7     apple
# Name: fruit, dtype: category
# Categories (2, object): ['apple', 'orange']

Bạn cũng có thể tạo pandas.Categorical trực tiếp từ các loại chuỗi Python khác:

my_categories = pd.Categorical(['foo', 'bar', 'baz', 'foo', 'bar'])
my_categories
# Out[223]:
# ['foo', 'bar', 'baz', 'foo', 'bar']
# Categories (3, object): ['bar', 'baz', 'foo']

Nếu bạn đã thu được dữ liệu được mã hóa phân loại từ một nguồn khác, bạn có thể sử dụng hàm tạo from_codes thay thế:

categories = ['foo', 'bar', 'baz']
codes = [0, 1, 2, 0, 0, 1]
my_cats_2 = pd.Categorical.from_codes(codes, categories)
my_cats_2
# Out[227]:
# ['foo', 'bar', 'baz', 'foo', 'foo', 'bar']
# Categories (3, object): ['foo', 'bar', 'baz']

Trừ khi được chỉ định rõ ràng, các chuyển đổi phân loại giả định không có thứ tự cụ thể của các phân loại. Vì vậy, mảng các phân loại có thể theo một thứ tự khác nhau tùy thuộc vào thứ tự của dữ liệu đầu vào. Khi sử dụng from_codes hoặc bất kỳ hàm tạo nào khác, bạn có thể chỉ ra rằng các phân loại có một thứ tự có ý nghĩa:

ordered_cat = pd.Categorical.from_codes(codes, categories,
                                         ordered=True)
ordered_cat
# Out[229]:
# ['foo', 'bar', 'baz', 'foo', 'foo', 'bar']
# Categories (3, object): ['foo' < 'bar' < 'baz']

Đầu ra [foo < bar < baz] cho biết ‘foo’ đứng trước ‘bar’ trong thứ tự, v.v. Một thể hiện phân loại không có thứ tự có thể được sắp xếp thứ tự bằng as_ordered:

my_cats_2.as_ordered()
# Out[230]:
# ['foo', 'bar', 'baz', 'foo', 'foo', 'bar']
# Categories (3, object): ['foo' < 'bar' < 'baz']

Lưu ý cuối cùng, dữ liệu phân loại không nhất thiết phải là chuỗi, mặc dù chúng ta chỉ hiển thị các ví dụ về chuỗi. Một mảng phân loại có thể bao gồm bất kỳ kiểu giá trị bất biến nào.

8.4.3. Tính toán với dữ liệu phân loại#

Việc sử dụng Categorical trong pandas so với phiên bản không mã hóa như một mảng các chuỗi thường hoạt động theo cùng một cách. Một số phần của pandas, như hàm groupby, hoạt động tốt hơn khi làm việc với dữ liệu phân loại. Cũng có một số hàm có thể sử dụng ordered.

Hãy xem xét một số dữ liệu số ngẫu nhiên và sử dụng hàm phân nhóm pandas.qcut. Hàm này trả về pandas.Categorical; chúng ta đã sử dụng pandas.cut trước đó trong sách nhưng đã bỏ qua chi tiết về cách hoạt động của dữ liệu phân loại:

rng = np.random.default_rng(seed=12345)
draws = rng.standard_normal(1000)
draws[:5]
# Out[233]: array([-1.4238,  1.2637, -0.8707, -0.2592, -0.0753])

Hãy tính toán một phân nhóm tứ phân vị của dữ liệu này và trích xuất một số thống kê:

bins = pd.qcut(draws, 4)
bins
# Out[235]:
# [(-3.121, -0.675], (0.687, 3.211], (-3.121, -0.675], (-0.675, 0.0134], (-0.675, 0.0134], ...,
# (0.0134, 0.687], (0.0134, 0.687], (-0.675, 0.0134], (0.0134, 0.687], (-0.675, 0.0134]]
# Length: 1000
# Categories (4, interval[float64, right]): [(-3.121, -0.675] < (-0.675, 0.0134] < (0.0134, 0.687] < (0.687, 3.211]]

Mặc dù hữu ích, các phân vị mẫu chính xác có thể ít hữu ích hơn để tạo báo cáo so với tên tứ phân vị. Chúng ta có thể đạt được điều này bằng đối số labels cho qcut:

bins = pd.qcut(draws, 4, labels=['Q1', 'Q2', 'Q3', 'Q4'])
bins
# Out[237]:
# ['Q1', 'Q4', 'Q1', 'Q2', 'Q2', ..., 'Q3', 'Q3', 'Q2', 'Q3', 'Q2']
# Length: 1000
# Categories (4, object): ['Q1' < 'Q2' < 'Q3' < 'Q4']

bins.codes[:10]
# Out[238]: array([0, 3, 0, 1, 1, 0, 0, 2, 2, 0], dtype=int8)

Dữ liệu phân loại bins được gắn nhãn không chứa thông tin về các cạnh nhóm trong dữ liệu, vì vậy chúng ta có thể sử dụng groupby để trích xuất một số thống kê tóm tắt:

bins = pd.Series(bins, name='quartile')
results = (pd.Series(draws)
             .groupby(bins)
             .agg(['count', 'min', 'max'])
             .reset_index())
results
# Out[241]:
#   quartile  count       min       max
# 0       Q1    250 -3.119609 -0.678494
# 1       Q2    250 -0.673305  0.008009
# 2       Q3    250  0.018753  0.686183
# 3       Q4    250  0.688282  3.211418

Cột 'quartile' trong kết quả giữ lại thông tin phân loại ban đầu, bao gồm cả thứ tự, từ bins:

results['quartile']
# Out[242]:
# 0    Q1
# 1    Q2
# 2    Q3
# 3    Q4
# Name: quartile, dtype: category
# Categories (4, object): ['Q1' < 'Q2' < 'Q3' < 'Q4']

8.4.4. Đo lường hiệu suất khi sử dụng dữ liệu phân loại#

Ở phần trước, chúng ta đã nói rằng các kiểu phân loại có thể cải thiện hiệu suất và việc sử dụng bộ nhớ, vì vậy hãy xem một số ví dụ. Hãy xem xét một số Series với 10 triệu phần tử và một số lượng nhỏ các phân loại riêng biệt:

N = 10_000_000
labels = pd.Series(['foo', 'bar', 'baz', 'qux'] * (N // 4))

Bây giờ chúng ta chuyển đổi labels thành phân loại:

categories = labels.astype('category')

Bây giờ chúng ta lưu ý rằng labels sử dụng bộ nhớ nhiều hơn đáng kể so với categories:

labels.memory_usage(deep=True)
# Out[246]: 600000128

categories.memory_usage(deep=True)
# Out[247]: 10000540

Tất nhiên, việc chuyển đổi sang category cũng sẽ cần thời gian tính toán, nhưng đó là tính toán một lần:

%time _ = labels.astype('category')
# CPU times: user 279 ms, sys: 6.06 ms, total: 285 ms
# Wall time: 285 ms

Các thao tác GroupBy có thể nhanh hơn đáng kể với dữ liệu phân loại vì các thuật toán cơ bản sử dụng mảng mã dựa trên số nguyên thay vì một mảng các chuỗi. Ở đây chúng ta so sánh hiệu suất của value_counts(), mà bên trong sử dụng cơ chế GroupBy:

%timeit labels.value_counts()
# 331 ms +- 5.39 ms per loop (mean +- std. dev. of 7 runs, 1 loop each)

%timeit categories.value_counts()
# 15.6 ms +- 152 us per loop (mean +- std. dev. of 7 runs, 100 loops each)

8.4.5. Phương thức phân loại#

Series chứa dữ liệu phân loại có một số phương thức đặc biệt tương tự như các phương thức chuỗi chuyên biệt Series.str. Điều này cũng cung cấp quyền truy cập thuận tiện vào các phân loại và mã. Hãy xem xét Series:

s = pd.Series(['a', 'b', 'c', 'd'] * 2)
cat_s = s.astype('category')
cat_s
# Out[253]:
# 0    a
# 1    b
# 2    c
# 3    d
# 4    a
# 5    b
# 6    c
# 7    d
# dtype: category
# Categories (4, object): ['a', 'b', 'c', 'd']

Thuộc tính truy cập đặc biệt cat cung cấp quyền truy cập vào các phương thức phân loại:

cat_s.cat.codes
# Out[254]:
# 0    0
# 1    1
# 2    2
# 3    3
# 4    0
# 5    1
# 6    2
# 7    3
# dtype: int8

cat_s.cat.categories
# Out[255]: Index(['a', 'b', 'c', 'd'], dtype='object')

Giả sử rằng chúng ta biết tập hợp các phân loại thực tế cho dữ liệu này mở rộng ra ngoài bốn giá trị được quan sát trong dữ liệu. Chúng ta có thể sử dụng phương thức set_categories để thay đổi chúng:

actual_categories = ['a', 'b', 'c', 'd', 'e']
cat_s2 = cat_s.cat.set_categories(actual_categories)
cat_s2
# Out[258]:
# 0    a
# 1    b
# 2    c
# 3    d
# 4    a
# 5    b
# 6    c
# 7    d
# dtype: category
# Categories (5, object): ['a', 'b', 'c', 'd', 'e']

Mặc dù có vẻ như dữ liệu không thay đổi, các phân loại mới sẽ được phản ánh trong các hoạt động sử dụng chúng. Ví dụ, value_counts giữ nguyên các phân loại, nếu có:

cat_s.value_counts()
# Out[259]:
# a    2
# b    2
# c    2
# d    2
# Name: count, dtype: int64

cat_s2.value_counts()
# Out[260]:
# a    2
# b    2
# c    2
# d    2
# e    0
# Name: count, dtype: int64

Trong các tập dữ liệu lớn, dữ liệu phân loại thường được sử dụng như một công cụ tiện lợi để tiết kiệm bộ nhớ và cải thiện hiệu suất. Sau khi bạn lọc một DataFrame hoặc Series lớn, nhiều phân loại có thể không xuất hiện trong dữ liệu. Để giải quyết vấn đề này, chúng ta có thể sử dụng phương thức remove_unused_categories để loại bỏ các phân loại không được quan sát:

cat_s3 = cat_s[cat_s.isin(['a', 'b'])]
cat_s3
# Out[262]:
# 0    a
# 1    b
# 4    a
# 5    b
# dtype: category
# Categories (4, object): ['a', 'b', 'c', 'd']

cat_s3.cat.remove_unused_categories()
# Out[263]:
# 0    a
# 1    b
# 4    a
# 5    b
# dtype: category
# Categories (2, object): ['a', 'b']

Xem Bảng … để biết danh sách các phương thức phân loại có sẵn.

Bảng …: Các phương thức phân loại cho Series trong pandas

Phương thức

Mô tả

add_categories

Thêm các phân loại mới (chưa sử dụng) vào cuối các phân loại hiện có.

as_ordered

Làm cho các phân loại có thứ tự.

as_unordered

Làm cho các phân loại không có thứ tự.

remove_categories

Loại bỏ các phân loại, đặt bất kỳ giá trị bị loại bỏ nào thành null.

remove_unused_categories

Loại bỏ bất kỳ giá trị phân loại nào không xuất hiện trong dữ liệu.

rename_categories

Thay thế các phân loại bằng tập hợp các tên phân loại mới được chỉ định; không thể thay đổi số lượng phân loại.

reorder_categories

Hoạt động giống như rename_categories, nhưng cũng có thể thay đổi kết quả để có các phân loại có thứ tự.

set_categories

Thay thế các phân loại bằng tập hợp các phân loại mới được chỉ định; có thể thêm hoặc loại bỏ các phân loại.

8.4.5.1. Tạo biến giả để lập mô hình#

Khi bạn sử dụng các công cụ thống kê hoặc học máy, bạn sẽ thường xuyên chuyển đổi dữ liệu phân loại thành các biến giả (dummy variables), còn được gọi là mã hóa one-hot (one-hot encoding). Điều này liên quan đến việc tạo một DataFrame với một cột cho mỗi phân loại riêng biệt; các cột này chứa 1 cho các lần xuất hiện của một phân loại nhất định và 0 cho các trường hợp khác.

Hãy xem xét ví dụ trước:

cat_s = pd.Series(['a', 'b', 'c', 'd'] * 2, dtype='category')

Như đã đề cập trước đó trong chương này, hàm pandas.get_dummies chuyển đổi dữ liệu phân loại một chiều này thành một DataFrame chứa biến giả:

pd.get_dummies(cat_s, dtype=float)
# Out[265]:
#      a    b    c    d
# 0  1.0  0.0  0.0  0.0
# 1  0.0  1.0  0.0  0.0
# 2  0.0  0.0  1.0  0.0
# 3  0.0  0.0  0.0  1.0
# 4  1.0  0.0  0.0  0.0
# 5  0.0  1.0  0.0  0.0
# 6  0.0  0.0  1.0  0.0
# 7  0.0  0.0  0.0  1.0

Như vậy, bạn đọc có thể thấy rằng việc chuẩn bị dữ liệu hiệu quả có thể cải thiện đáng kể năng suất bằng cách cho phép bạn dành nhiều thời gian hơn để phân tích dữ liệu và ít thời gian hơn để chuẩn bị dữ liệu cho phân tích. Chúng ta đã khám phá một số công cụ trong chương này, nhưng phạm vi bao phủ ở đây không hề toàn diện. Trong chương tiếp theo, chúng ta sẽ khám phá chức năng thêm các chức năng quan trọng khác của pandas trong phân tích dữ liệu.