🍀Tóm tắt buổi trước

Ở buổi trước chúng ta tìm hiểu về các cách xử lý dữ liệu bị thiếu, sau đó làm quen với các bước xử lý khác như chống trùng lặp, thay thế giá trị, Discretization và Binning, tìm ra các Outlier

Trong bài này ta tiếp tục đi qua các thao tác còn lại trong quá trình làm sạch dữ liệu. Dù chưa phải là tất các bước nhưng đây là những bước cơ bản và quan trọng mà hầu như chúng ta đều phải thực hiện.

📁Bạn có thể coi lại bài trước của mình:

📁Buổi Live ngày 15:

🍀Các kiểu dữ liệu mở rộng – Extension Data Types

Pandas ban đầu được xây dựng dựa trên khả năng có sẵn trong NumPy, một thư viện tính toán mảng chủ yếu được sử dụng để làm việc với dữ liệu số. Nhiều khái niệm của pandas, như cách định danh dữ liệu bị thiếu, được thừa kế những gì có sẵn trong NumPy, đồng thời cố gắng tối đa hóa tính tương thích giữa các thư viện sử dụng NumPy và pandas cùng nhau.

Tuy nhiên nó lại kèm theo một số hạn chế, như:

  • Thiếu sót khi “Xử lý dữ liệu bị thiếu” cho một số kiểu dữ liệu dạng số, như số nguyên và Booleans. Kết quả là, khi dữ liệu bị thiếu xuất hiện, pandas tự ép kiểu thành float64 và sử dụng np.nan để biểu diễn giá trị null. Điều này gây ra những lỗi “tinh vi” trong nhiều thuật toán pandas dẫn tới giảm độ chính xác.
  • Bộ dữ liệu chứa nhiều dữ liệu kiểu chuỗi (string data) tốn nhiều chi phí tính toán và bộ nhớ.
  • Một số loại dữ liệu, như "time intervals", "timedeltas", và "timestamps with time zone", không được tối ưu mà không cần sử dụng các đối tượng mảng hiệu suất cao ("computationally expensive arrays").

Gần đây, pandas đã phát triển một hệ thống các kiểu dữ liệu mở rộng mới cho phép thêm các loại dữ liệu mới ngay cả khi chúng không được hỗ trợ chính thức bởi NumPy. Những loại dữ liệu mới này có thể được xử lý như là "first class" cùng với dữ liệu từ mảng NumPy.

Hãy xem một ví dụ sau, ta có một Series của các số nguyên với một giá trị thiếu:

In [133]: s = pd.Series([1, 2, 3, None])

In [134]: s
Out[134]: 
0    1.0
1    2.0
2    3.0
3    NaN
dtype: float64

In [135]: s.dtype
Out[135]: dtype('float64')

Vì lý do tương thích ngược ("backward comaptibility"), đối tượng Series trong pandas tiếp tục sử dụng kiểu dữ liệu float64 và giá trị np.nan để đại diện cho int và giá trị None. Thay vào đó, chúng ta có thể tạo ra Series mang đúng kiểu dữ liệu gốc bằng cách khai báo thêm tham số sau pandas.Int64Dtype:

In [136]: s = pd.Series([1, 2, 3, None], dtype=pd.Int64Dtype())

In [137]: s
Out[137]: 
0       1
1       2
2       3
3    <NA>
dtype: Int64

In [138]: s.isna()
Out[138]: 
0    False
1    False
2    False
3     True
dtype: bool

In [139]: s.dtype
Out[139]: Int64Dtype()

Kết quả <NA> đại diện chho giá trị bị thiếu. Nó sử dụng giá trị đặc biệt pandas.NA làm sentinel value thay vì np.nan:

In [140]: s[3]
Out[140]: <NA>

In [141]: s[3] is pd.NA
Out[141]: True

Chúng ta cũng có thể sử dụng cách viết tắt "Int64" thay vì pd.Int64Dtype() để chỉ định kiểu dữ liệu. Việc viết hoa là cần thiết nhé nếu không nó sẽ trở thành một dạng NumPy-based nonextension type:

In [142]: s = pd.Series([1, 2, 3, None], dtype="Int64")

Pandas cũng có một kiểu dữ liệu mở rộng được chuyên biệt cho dữ liệu chuỗi mà không sử dụng mảng đối tượng NumPy (nó yêu cầu thư viện pyarrow mà bạn có thể cần cài đặt riêng):

In [143]: s = pd.Series(['one', 'two', None, 'three'], dtype=pd.StringDtype())

In [144]: s
Out[144]: 
0      one
1      two
2     <NA>
3    three
dtype: string

Những mảng chuỗi này thông thường sử dụng ít bộ nhớ hơn và thường hiệu quả tính toán hơn khi thực hiện các phép toán trên bộ dữ liệu lớn.

Còn một kiểu dữ liệu mở rộng quan trọng nữa là Categorical nhưng ta sẽ đề cập sau.

Các kiểu dữ liệu mở rộng có thể được truyền vào qua phương thức astype của Series, giúp bạn chuyển đổi một cách dễ dàng trong quá trình làm sạch dữ liệu của bạn:

In [145]: df = pd.DataFrame({"A": [1, 2, None, 4],
   .....:                    "B": ["one", "two", "three", None],
   .....:                    "C": [False, None, False, True]})

In [146]: df
Out[146]: 
     A      B      C
0  1.0    one  False
1  2.0    two   None
2  NaN  three  False
3  4.0   None   True

In [147]: df["A"] = df["A"].astype("Int64")

In [148]: df["B"] = df["B"].astype("string")

In [149]: df["C"] = df["C"].astype("boolean")

In [150]: df
Out[150]: 
      A      B      C
0     1    one  False
1     2    two   <NA>
2  <NA>  three  False
3     4   <NA>   True
 pandas extension data types

🍀String Manipulation – Xử lý chuỗi

Python từ lâu đã là một ngôn ngữ phổ biến cho việc xử lý dữ liệu thô, đặc biệt là do sự thuận tiện trong việc xử lý chuỗi và văn bản.

Hầu hết các thao tác với dữ liệu văn bản trở nên đơn giản với các phương thức tích hợp sẵn trong đối tượng chuỗi của Python (str).

Đối với các mẫu ký tự phức tạp hơn khi cần so khớp và xử lý văn bản, bạn có thể cần đến biểu thức chính quy (regular expression). pandas đã làm rất tốt trong việc kết hợp công cụ xử lý chuỗi và biểu thức chính quy một cách ngắn gọn và có thể áp vào toàn bộ mảng dữ liệu, đồng thời thuận tiện xử lý các dữ liệu bị thiếu cùng lúc.

📙Python Built-In String Object Methods

Trong nhiều ứng dụng xử lý chuỗi và kịch bản (ý chỉ dùng scripting language -tự động hóa các nhiệm vụ cụ thể), các phương thức chuỗi tích hợp ("built-in string methods") thường đủ để sử dụng. Như một 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 phương thức split:

In [151]: val = "a,b,  guido"

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


Phương thức split thường được kết hợp với strip để loại bỏ khoảng trắng (bao gồm cả dấu xuống dòng):

In [153]: pieces = [x.strip() for x in val.split(",")]

In [154]: pieces
Out[154]: ['a', 'b', 'guido']

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

In [155]: first, second, third = pieces

In [156]: first + "::" + second + "::" + third
Out[156]: 'a::b::guido'

Nhưng đây không phải là một phương pháp tổng quát trên thực tế. Một cách nhanh chóng và “Pythonic” hơn là truyền một danh sách hoặc tuple vào phương thức join của chuỗi "::":

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

Các phương thức khác liên quan đến việc xác định vị trí các chuỗi con. Việc 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, tuy nhiên cũng có thể sử dụng indexfind:

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

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

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

Lưu ý rằng sự khác biệt giữa findindexindex sẽ ném một exception (ngoại lệ – lỗi) nếu chuỗi không được tìm thấy (thay vì trả về -1 như find):

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

Liên quan đến vấn đề tìm kiếm chuỗi con thì phương thức count trả về số lần xuất hiện của một chuỗi con cụ thể:

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

Phương thức replace sẽ thay thế các mẫu chuỗi mong muốn liên tiếp thành một mẫu khác. Một cách dùng khác là ta sử dụng để xóa các mẫu xác định bằng cách truyền một chuỗi rỗng:

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

In [164]: val.replace(",", "")
Out[164]: 'ab  guido'
Python built-in string methods

📙Regular Expressions – 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 so khớp mẫu phức tạp đại diện cho các ký tự xác định (gọi tắt là "mẫu tự") trong văn bản. Với một biểu thức duy nhất, thường được gọi là regex, là một chuỗi được tạo theo ngôn ngữ biểu thức chính quy.

Module re được tích hợp sẵn trong Python và chịu trách nhiệm cho việc áp dụng biểu thức chính quy vào chuỗi; dưới đây là một số ví dụ về cách sử dụng:

Thật sự việc viết biểu thức chính quy đòi hỏi nhiều kinh nghiệm và nó thực sự là cách ghép nối nghệ thuật. Thậm chí có những trang kiểm tra online nên mình sẽ đề cập phần này sau. Ở đây ta đi qua những các đơn giản trước


Các hàm trong module re có ba danh mục chính: tìm kiếm mẫu tự, thay thế, và phân chia. Về bản chất, tất cả chúng liên quan với nhau vì trước tiên đều mô tả một mẫu tự cần định vị trong văn bản, sau đó có thể được sử dụng cho nhiều mục đích.

Hãy xem một ví dụ đơn giản: giả sử chúng ta muốn phân chia một chuỗi với một biến chứa một hoặc nhiều ký tự khoảng trắng (tab, khoảng trắng và dấu xuống dòng).
Biểu thức chính quy mô tả một hoặc nhiều ký tự khoảng trắng là \s+:

In [165]: import re

In [166]: text = "foo    bar\t baz  \tqux"

In [167]: 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, sau đó phương thức split của nó được gọi trên đoạn văn bản được chuyển vào.

Bạn cũng có thể tự biên dịch biểu thức chính quy bằng cách sử dụng re.compile, tạo ra một đối tượng biểu thức chính quy có thể tái sử dụng lại sau:

In [168]: regex = re.compile(r"\s+")

In [169]: regex.split(text)
Out[169]: ['foo', 'bar', 'baz', 'qux']

Nếu, thay vào đó, bạn muốn nhận được một danh sách tất cả các mẫu tự khớp với biểu thức chính quy, bạn có thể sử dụng phương thức findall:

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

Trong trường hợp mẫu regex liên quan tới URL, để tránh việc gây nhầm lẫn dấu \thành ký tự escapse thì ta có thể sử dụng chuỗi chưa được xử lý (raw string literals) như r"C:\x" thay vì biểu thức tương đương "C:\\x" hoặc để dấu ngược lại "C:/x"


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 biểu thức cho nhiều chuỗi; làm điều này sẽ tiết kiệm tài nguyên hơn.

matchsearch có mối quan hệ 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. Cụ thể, match chỉ khớp ở đầu chuỗi. Hãy xem xét một đoạn 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 địa chỉ email:

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 makes the regex case insensitive
regex = re.compile(pattern, flags=re.IGNORECASE)

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

In [172]: 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 trùng khớp đặc biệt cho địa chỉ email đầu tiên trong văn bản. Đối tượng khớp này chỉ có thể cung cấp cho chúng ta vị trí bắt đầu và kết thúc của mẫu tự trong chuỗi với biểu thức chính quy đã nêu trước:

In [173]: m = regex.search(text)

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

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

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

In [176]: print(regex.match(text))
None

Liên quan đến điều này, phương thức sub sẽ trả về một chuỗi mới trong đó các các “mẫu tự” liên tiếp được thay thế bằng một chuỗi mới:

In [177]: print(regex.sub("REDACTED", text))
Dave REDACTED
Steve REDACTED
Rob REDACTED
Ryan REDACTED

Giả sử bạn muốn tìm địa chỉ email và đồng thời phân đoạn mỗi địa chỉ thành ba thành phần: tên người dùng (username), tên miền (domain name), và hậu tố miền (domain suffix). Để thực hiện điều này, đặt dấu ngoặc đơn "(" xung quanh các phần của mẫu tự để phân đoạn:

In [178]: pattern = r"([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\.([A-Z]{2,4})"

In [179]: regex = re.compile(pattern, flags=re.IGNORECASE)

Một đối tượng khớp được tạo ra bởi biểu thức chính quy đã được sửa đổi này sẽ trả về một bộ các thành phần của mẫu tự thông qua phương thức groups:

In [180]: m = regex.match("wesm@bright.net")

In [181]: m.groups()
Out[181]: ('wesm', 'bright', 'net')

findall sẽ trả về một danh sách tuples khi biểu thức chính quy có các nhóm:

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

Phương thức 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 biểu tượng đặc biệt như \1\2.

Biểu tượng \1 tương ứng với nhóm đầu tiên được khớp, \2 tương ứng với nhóm thứ hai, và cứ tiếp tục như vậy:

In [183]: print(regex.sub(r"Username: \1, Domain: \2, Suffix: \3", text))
Dave Username: dave, Domain: google, Suffix: com
Steve Username: steve, Domain: gmail, Suffix: com
Rob Username: rob, Domain: gmail, Suffix: com
Ryan Username: ryan, Domain: yahoo, Suffix: com
Regular expression methods

📙String Functions in pandas

Việc làm sạch một bộ dữ liệu lộn xộn để phân tích thường đòi hỏi nhiều xử lý chuỗi. Trong trường hợp phức tạp, một cột chứa chuỗi đôi khi có thể chứa dữ liệu bị thiếu:

In [184]: data = {"Dave": "dave@google.com", "Steve": "steve@gmail.com",
   .....:         "Rob": "rob@gmail.com", "Wes": np.nan}

In [185]: data = pd.Series(data)

In [186]: data
Out[186]: 
Dave     dave@google.com
Steve    steve@gmail.com
Rob        rob@gmail.com
Wes                  NaN
dtype: object

In [187]: data.isna()
Out[187]: 
Dave     False
Steve    False
Rob      False
Wes       True
dtype: bool

Các phương thức chuỗi và biểu thức chính quy có thể được sử dụng (truyền vào hàm lambda hoặc hàm khác) cho mỗi giá trị bằng cách sử dụng data.map, nhưng nó sẽ gặp sự cố khi gặp giá trị NA (null).

Để giải quyết vấn đề này, Series có các phương thức định mảng cho các phép tính chuỗi sẽ bỏ qua và "propagate" luân chuyển (đến cuối) các giá trị NA. Chúng có thể được truy cập thông qua thuộc tính str của Series; ví dụ, chúng ta có thể kiểm tra xem mỗi địa chỉ email có chứa "gmail" không bằng cách sử dụng str.contains:

In [188]: data.str.contains("gmail")
Out[188]: 
Dave     False
Steve     True
Rob       True
Wes        NaN
dtype: object

Lưu ý rằng kết quả trên có kiểu dữ liệu là object. pandas có các kiểu mở rộng (extension types) cung cấp xử lý chuyên biệt cho chuỗi, số nguyên và dữ liệu Boolean, những loại dữ liệu này cho đến gần đây đã gặp một số vấn đề khi làm việc với dữ liệu bị thiếu:

In [189]: data_as_string_ext = data.astype('string')

In [190]: data_as_string_ext
Out[190]: 
Dave     dave@google.com
Steve    steve@gmail.com
Rob        rob@gmail.com
Wes                 <NA>
dtype: string

In [191]: data_as_string_ext.str.contains("gmail")
Out[191]: 
Dave     False
Steve     True
Rob       True
Wes       <NA>
dtype: boolean

Ta cũng có thể dùng kết hợp với regex bằng cách thêm tham số sau re.IGNORECASE:

In [192]: pattern = r"([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\.([A-Z]{2,4})"

In [193]: data.str.findall(pattern, flags=re.IGNORECASE)
Out[193]: 
Dave     [(dave, google, com)]
Steve    [(steve, gmail, com)]
Rob        [(rob, gmail, com)]
Wes                        NaN
dtype: object

Có một vài cách để thực hiện việc truy xuất phần tử theo vec-tơ. Bạn có thể sử dụng str.get hoặc sử dụng chỉ mục kèm theo thuộc tính str:

In [194]: matches = data.str.findall(pattern, flags=re.IGNORECASE).str[0]

In [195]: matches
Out[195]: 
Dave     (dave, google, com)
Steve    (steve, gmail, com)
Rob        (rob, gmail, com)
Wes                      NaN
dtype: object

In [196]: matches.str.get(1)
Out[196]: 
Dave     google
Steve     gmail
Rob       gmail
Wes         NaN
dtype: object

Bạn cũng có thể cắt chuỗi tương tự bằng cú pháp này:

In [197]: data.str[:5]
Out[197]: 
Dave     dave@
Steve    steve
Rob      rob@g
Wes        NaN
dtype: object

Phương thức str.extract sẽ trả về các nhóm theo regex về dạng Dataframe:

In [198]: data.str.extract(pattern, flags=re.IGNORECASE)
Out[198]: 
           0       1    2
Dave    dave  google  com
Steve  steve   gmail  com
Rob      rob   gmail  com
Wes      NaN     NaN  NaN
Partial listing of Series string methods

🍀Categorical Data – Dữ liệu phân nhóm

Phần này giới thiệu về kiểu dữ liệu Categorical trong pandas. Mình sẽ chỉ ra làm thế nào bạn có thể đạt được hiệu năng tố hơn mà lại sử dụng bộ nhớ tiết kiệm hơn trong một số phép tính trong pandas bằng cách chuyển kiểu dữ liệu sang Categorical.

Bên cạnh đó mình cũng giới thiệu một số công cụ có thể hỗ trợ việc sử dụng dữ liệu phân loại trong các ứng dụng về thống kê và máy học.

📙Background & Motivation – Đôi điều muốn nói

Không hiếm gặp khi một cột trong một bảng có thể chứa các dòng lặp lại của một tập hợp nhỏ các giá trị phân biệt. Các hàm quen thuộc như uniquevalue_countsvốn là công cụ giúp chúng ta trích xuất các giá trị phân biệt ("distinct values") từ một mảng và tính toán tần suất của chúng, như ví dụ sau:

In [199]: values = pd.Series(['apple', 'orange', 'apple',
   .....:                     'apple'] * 2)

In [200]: values
Out[200]: 
0     apple
1    orange
2     apple
3     apple
4     apple
5    orange
6     apple
7     apple
dtype: object

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

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

Nhiều hệ thống dữ liệu ( dùng để data warehousing, statistical computing, hoặc các mục đích khác) đã 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 để lưu trữ và tính toán hiệu quả hơn.

Tong data warehouse, người ta dùng các bảng dimension (bảng chiều) chứa các giá trị phân biệt.

Trong ví dụ bên dưới ta sẽ lưu trữ các primary observations (quan sát) thành các khóa integer trong series để tham chiếu tới một một series khác tượng trưng cho một bảng dimension:

In [203]: values = pd.Series([0, 1, 0, 0] * 2)

In [204]: dim = pd.Series(['apple', 'orange'])

In [205]: values
Out[205]: 
0    0
1    1
2    0
3    0
4    0
5    1
6    0
7    0
dtype: int64

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

Chúng ta có thể sử dụng phương thức take để tái hiện lại series values trên bằng giá trị phân loại trong series dim

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

Thật ra thì phương thức .take() có công năng dùng để lấy các phần tử của mảng dọc theo một trục xác định (default là 0:index – chọn dòng theo trục)


Biểu diễn dưới dạng số nguyên được gọi là categorical representation hay dictionary-encoded representation. Mảng các giá trị phân biệt có thể được gọi là categories of data, dictionary of data hoặc levels of data.

Để đơn giản mình thống nhất sẽ dùng khái niệm categoricalcategories để chỉ các phân nhóm và các giá trị số nguyên tham chiếu tới các phân nhóm là category codes hay codes.

Việc phân nhóm có thể mang lại cải thiện đáng kể về hiệu suất khi bạn đang thực hiện phân tích dữ liệu. Bạn cũng có thể thực hiện các biến đổi trên categories trong khi vẫn giữ nguyên các codes. Một số ví dụ về các biến đổi có thể được thực hiện với chi phí tương đối thấp bao gồm:

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

📙Kiểu dữ liệu về Categories

pandas cung cấp kiểu dữ liệu Categorical bằng cách thay thế dạng string-based categorical thành integer-based categorical – bản chất là encoding cột phân loại. Kỹ thuật nén dữ liệu này nhìn quá cũng thấy sẽ giúp mang lại hiệu suất cao hơn vì máy tính làm việc tốt hơn với kiểu dữ liệu numerics.

Xem qua ví dụ đối tượng Series sau:

In [208]: fruits = ['apple', 'orange', 'apple', 'apple'] * 2

In [209]: N = len(fruits)

In [210]: rng = np.random.default_rng(seed=12345)

In [211]: 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'])

In [212]: 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

Ta thấy df['fruit'] là một mảng chứa các chuỗi str. Để ép kiểu sang categorical chúng ta làm như sau:

In [213]: fruit_cat = df['fruit'].astype('category')

In [214]: 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']

Lưu ý là giờ fruit_cat hay chính là df['fruit'] không còn chứa danh sách str nữa mà là category. Nói cách khác fruit_cat là instance của pandas.Categorical.

Bạn có thể truy cập đối tượng này thông qua thuộc tính .array vì đối tượng Categorical có hai thuộc tính quan trọng là categoriescodes:

In [215]: c = fruit_cat.array

In [216]: type(c)
Out[216]: pandas.core.arrays.categorical.Categorical

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

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

Ở phần sau ta còn biết tới accessor cat của đối tượng Categorical

Accessor có thể hiểu là phương thức hoặc hàm được sử dụng để nhận giá trị của một thuộc tính thuộc đối tượng đó – ở đây là đối tượng. Thật ra accessor là một khái niệm quen thuộc trong OOP, xem ví dụ sau:

class Person:
    def __init__(self, name, age):
        self._name = name
        self._age = age

    def get_name(self):
        return self._name

    def get_age(self):
        return self._age

# Creating an instance of the Person class
person1 = Person("Alice", 30)

# Using accessor methods to retrieve attribute values
print("Name:", person1.get_name())
print("Age:", person1.get_age())
What is accessor?

Một mánh khá là hay để thể hiện phần mapping giữa codescategories bằng cách như sau:

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

Quay trở lại dataframe ở trên nếu muốn ép kiểu cho cột thành kiểu Categorical thì ta cứ ghi đè lên là được:

In [220]: df['fruit'] = df['fruit'].astype('category')

In [221]: 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 đối tượng pandas.Categorical trực tiếp từ các loại chuỗi trình tự khác bằng chính hàm tương tự pd.Categorical(), như ví dụ dưới đây là từ một array

In [222]: my_categories = pd.Categorical(['foo', 'bar', 'baz', 'foo', 'bar'])

In [223]: my_categories
Out[223]: 
['foo', 'bar', 'baz', 'foo', 'bar']
Categories (3, object): ['bar', 'baz', 'foo']

Nếu bạn đã nhận được dữ liệu hạng mục được mã hóa từ một nguồn khác, bạn có thể sử dụng constructor from_codes để mapping với danh sách hạng mục đã có:

In [224]: categories = ['foo', 'bar', 'baz']

In [225]: codes = [0, 1, 2, 0, 0, 1]

In [226]: my_cats_2 = pd.Categorical.from_codes(codes, categories)

In [227]: my_cats_2
Out[227]: 
['foo', 'bar', 'baz', 'foo', 'foo', 'bar']
Categories (3, object): ['foo', 'bar', 'baz']

Cách làm này làm ta nhớ tới hàm .take() ở trên nhưng đối tượng trả về của cả hai là khác nhau.

Trong mảng categories khi sử dụng constructor from_codes sẽ có thêm parameter ordered để chỉ định thứ tự của các giá trị trong biến Categorical. Lưu ý là thứ tự này không phải thứ tự Alpha – Beta mà là thứ tự nhập phân nhóm vào biến, xem ví dụ sau:

In [228]: ordered_cat = pd.Categorical.from_codes(codes, categories,
   .....:                                         ordered=True)

In [229]: ordered_cat
Out[229]: 
['foo', 'bar', 'baz', 'foo', 'foo', 'bar']
Categories (3, object): ['foo' < 'bar' < 'baz']

Kết quả [foo < bar < baz] cho thấy ‘foo' đứng trước 'bar' trong thứ tự trong mảng categories ở trên. Ngoài ra ta có thể chuyển từ dạng "unordered" sang dạng "ordered" bằng phương thức as_ordered:

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

Lưu ý categorical data không nhất thiết phải là chuỗi str, trên thực tế nó có thể bao gồm bất kỳ loại giá trị không thể thay đổi ("immutable value types").

📙Computations with Categoricals

Sử dụng Categorical trong pandas so với phiên bản không được mã hóa "nonencoded version" (như mảng str) nhìn chung giống nhau. Một số phần trong pandas, như hàm groupby, hoạt động tốt hơn khi làm việc với categoricals.

Bạn còn nhớ parameter ordered không? Cũng có một số hàm có thể tận dụng sử dụng option này.

Hãy xem xét một số dữ liệu số ngẫu nhiên sau đây và sử dụng hàm pandas.qcut để chia các bins khác nhau. Điều thú vị là hàm này trả về đối tượng pandas.Categorical; trước đó chúng ta chỉ tìm hiểu về quantiles nên bỏ sót đặc tính quan trọng này

In [231]: rng = np.random.default_rng(seed=12345)

In [232]: draws = rng.standard_normal(1000)

In [233]: draws[:5]
Out[233]: array([-1.4238,  1.2637, -0.8707, -0.2592, -0.0753])

Giờ thử tính các quartile bins và làm một vài thống kê:

In [234]: bins = pd.qcut(draws, 4)

In [235]: 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]]

Ta biến đổi chút thay vì để label là các số thì ta để tên để dễ nhìn hơn bằng cách dùng tham số labels trong qcut:

In [236]: bins = pd.qcut(draws, 4, labels=['Q1', 'Q2', 'Q3', 'Q4'])

In [237]: bins
Out[237]: 
['Q1', 'Q4', 'Q1', 'Q2', 'Q2', ..., 'Q3', 'Q3', 'Q2', 'Q3', 'Q2']
Length: 1000
Categories (4, object): ['Q1' < 'Q2' < 'Q3' < 'Q4']

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

Cơ mà ở trên ta thấy các quartiles trả về không có thông tin về các biên (edges) trái phải của từng quartiles nên ta có thể dùng groupby để trích xuất một số thống kê tổng quát như sau:

In [239]: bins = pd.Series(bins, name='quartile')

In [240]: results = (pd.Series(draws)
   .....:            .groupby(bins)
   .....:            .agg(['count', 'min', 'max'])
   .....:            .reset_index())

In [241]: 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ữ thông tin gốc của các hạng mục (chính là các bin) bao gồm cả thứ tự các bin:

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

📙Better performance with categoricals

Hồi đầu giờ mình nói Categorical giúp cải thiện hiệu suất và bộ nhớ, để chứng minh 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 categories không trùng lặp:

In [243]: N = 10_000_000

In [244]: labels = pd.Series(['foo', 'bar', 'baz', 'qux'] * (N // 4))

Giờ ta ép kiểu của biến labels thành Categorical:

In [245]: categories = labels.astype('category')

Giờ thử kiểm tra xem tên nào sử dụng nhiều bộ nhớ hơn:

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

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

Tất nhiên chúng ta vẫn tốn chi phí khi ép kiểu nhưng ta chỉ cần làm một lần:

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

Các phép tính GroupBy có thể nhanh hơn đáng kể với categoricals vì các thuật toán sử dụng mảng codes số nguyên thay vì một mảng chuỗi ký tự. Ở đây, chúng ta so sánh hiệu suất khi dùng hàm value_counts(), mà thực tế bên trong sử dụng cơ chế GroupBy:

In [249]: %timeit labels.value_counts()
331 ms +- 5.39 ms per loop (mean +- std. dev. of 7 runs, 1 loop each)

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

331 ms so với 15.6 ms – Kết quả đã rõ

📙Categorical Methods

Các Series chứa dữ liệu categorical có một số phương thức đặc biệt "accessor" tương tự như các phương thức chuỗi đặc biệt được tối ưu hóa trong Series.str. Điều này cũng cung cấp cách truy cập thuận tiện hơn vào categoriescodes. Hãy xem xét Series sau:

In [251]: s = pd.Series(['a', 'b', 'c', 'd'] * 2)

In [252]: cat_s = s.astype('category')

In [253]: 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 accessor đặc biệt cat cung cấp truy cập vào các phương thức categorical:

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

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

Giả sử chúng ta biết rằng tập hợp thực sự của các hạng mục cho dữ liệu này mở rộng hơn bốn giá trị 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:

In [256]: actual_categories = ['a', 'b', 'c', 'd', 'e']

In [257]: cat_s2 = cat_s.cat.set_categories(actual_categories)

In [258]: 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ù dữ liệu không thay đổi, các hạng mục mới sẽ được ánh xạ lại trong các phép tính sử dụng chúng. Ví dụ, value_counts phản ảnh thực các categories đang có:

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

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

Trong các bộ dữ liệu lớn, categoricals thường được sử dụng như một công cụ thuận tiện để 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 categories có thể không xuất hiện trong dữ liệu. Chúng ta có thể sử dụng phương thức remove_unused_categories để loại bỏ các categories không dùng tới:

In [261]: cat_s3 = cat_s[cat_s.isin(['a', 'b'])]

In [262]: cat_s3
Out[262]: 
0    a
1    b
4    a
5    b
dtype: category
Categories (4, object): ['a', 'b', 'c', 'd']

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

Biểu thức cat_s[cat_s.isin(['a', 'b'])] được sử dụng để lọc các giá trị trong Series cat_s mà thuộc vào danh sách [‘a’, ‘b’] bằng cách tạo ra Series boolean.

Categorical methods for Series in pandas

📙Creating dummy variables for modeling

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

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

In [265]: 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

🍀Kết luận

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 giú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 buổi trước và buổi này, nhưng chắc chắn vẫn còn rất nhiều kỹ thuật nâng cao khác.

Trong phần kế ta sẽ tìm hiểu về các kết nối dữ liệu join hay các hàm grouping

Các kênh cộng đồng của mình

  • Facebook: https://www.facebook.com/bocuoilamdata
  • Youtube: https://www.youtube.com/@CarrY4U_VN
  • Website: https://carry4u.org/

© Bản quyền thuộc về CarrY4U và Bò Cười Làm Data
© Copyright by CarrY4U and bocuoilamdata ☞ Do not reup