🍀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
float64và sử dụngnp.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]: TrueChú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: stringNhữ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
🍀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 index và find:
In [158]: "guido" in val
Out[158]: True
In [159]: val.index(",")
Out[159]: 1
In [160]: val.find(":")
Out[160]: -1Lưu ý rằng sự khác biệt giữa find và index là index 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 foundLiê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]: 2Phươ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'
📙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.
match và search 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))
NoneLiê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 REDACTEDGiả 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 và \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
📙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: boolCá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: objectLư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: booleanTa 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: objectCó 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: objectBạ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: objectPhươ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
🍀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ư unique và value_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: int64Nhiề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: objectChú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: objectThậ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 categorical và categories để 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
categorymới mà không thay đổi thứ tự hoặc vị trí của cáccategorieshiệ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.795525Ta 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à categories và codes:
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:
What is accessor?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())
Một mánh khá là hay để thể hiện phần mapping giữa codes và categories 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.211418Cộ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]: 10000540Tấ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 msCá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 categories và codes. 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: int64Trong 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.

📙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

