Tự Học Data Science · 22/11/2023 0

03.04 Handling Missing Data

Sự đánh đồng trong ý kiến của các phong cách dữ liệu còn thiếu

Có một số kế hoạch đã được phát triển để chỉ ra sự hiện diện của dữ liệu bị thiếu trong một bảng hoặc DataFrame. Thông thường, chúng xoay quanh một trong hai chiến lược: sử dụng một mask (mặt nạ) mà tổng quát chỉ ra giá trị bị thiếu, hoặc chọn một sentinel value (giá trị biểu tượng) để chỉ ra một mục nhập bị thiếu.

Trong phương pháp che phủ, mặt nạ có thể là một mảng Boolean hoàn toàn riêng biệt, hoặc nó có thể liên quan đến việc chiếm dụng một bit trong biểu diễn dữ liệu để chỉ ra trạng thái null cục bộ của một giá trị.

Trong phương pháp sentinel, giá trị sentinel có thể là một quy ước cụ thể cho dữ liệu, chẳng hạn như chỉ ra giá trị số nguyên bị thiếu bằng -9999 hoặc một dạng mẫu bit hiếm, hoặc có thể là một quy ước toàn cầu hơn, như chỉ ra giá trị số thực bị thiếu bằng NaN (Không phải là một số), một giá trị đặc biệt là một phần của quy ước số thực điểm động IEEE.

Không có cách tiếp cận nào mà không có nhược điểm: việc sử dụng một mảng mask riêng yêu cầu việc cấp phát một mảng Boolean thêm, tốn thêm bộ nhớ và tính toán. Giá trị gửi đi giới hạn phạm vi của các giá trị hợp lệ mà có thể được biểu diễn và có thể yêu cầu các logic thêm không được tối ưu trong phép tính của CPU và GPU. Những giá trị đặc biệt thông thường như NaN không có sẵn cho tất cả các kiểu dữ liệu.

Giống như hầu hết các trường hợp khác khi không có sự lựa chọn tối ưu phổ biến, các ngôn ngữ và hệ thống khác nhau sử dụng các quy ước khác nhau.Ví dụ, ngôn ngữ R sử dụng các mẫu bit dành riêng trong mỗi loại dữ liệu như giá trị thủ kỵ để chỉ ra dữ liệu bị thiếu, trong khi hệ thống SciDB sử dụng một byte phụ được gắn với mỗi ô dữ liệu để chỉ ra trạng thái NA.

Dữ liệu thiếu trong Pandas

Cách mà Pandas xử lý giá trị thiếu được hạn chế bởi sự phụ thuộc của nó vào gói NumPy, mà không có khái niệm NA tích hợp sẵn cho các kiểu dữ liệu không phải kiểu dấu chấm động.

Pandas có thể đã làm theo hướng dẫn của R trong việc chỉ định mẫu bit cho mỗi loại dữ liệu riêng lẻ để chỉ ra tính không hợp lệ, nhưng hướng tiếp cận này thì phức tạp hơn.Trong khi R chứa bốn loại dữ liệu cơ bản, thì NumPy hỗ trợ nhiều hơn thế: ví dụ, trong khi R chỉ có một loại số nguyên, thì NumPy hỗ trợ 14 loại số nguyên cơ bản tổng cộng khi tính đến độ chính xác, có dấu và thứ tự máy tính.Dành một mẫu bit cụ thể trong tất cả các loại NumPy sẽ dẫn đến một lượng công việc cồng kềnh khi phải xử lý các phép toán khác nhau cho các loại khác nhau, có thể thậm chí cần một phiên bản riêng của gói NumPy. Hơn nữa, đối với các kiểu dữ liệu nhỏ hơn (như số nguyên 8-bit), hy sinh một bit để sử dụng làm mặt nạ sẽ giảm đáng kể phạm vi giá trị có thể biểu diễn được.

NumPy có hỗ trợ cho các mảng đã che – tức là mảng có một mảng mask Boolean riêng để đánh dấu dữ liệu là “tốt” hay “xấu”. Pandas có thể đã phát sinh từ điều này, nhưng chi phí cho việc lưu trữ, tính toán và bảo trì mã là một lựa chọn không hấp dẫn.

Với những ràng buộc này trong tâm trí, Pandas đã chọn sử dụng sentinel cho dữ liệu thiếu, và tiếp tục chọn sử dụng hai giá trị null hiện có đã tồn tại trong Python: giá trị floating-point đặc biệt NaN, và đối tượng Python None.

None: Thiếu dữ liệu theo cách Python

Giá trị cảnh báo đầu tiên được sử dụng bởi Pandas là None, một đối tượng đơn của Python thường được sử dụng để thể hiện dữ liệu bị thiếu trong mã Python.Bởi vì nó là một đối tượng Python, None không thể được sử dụng trong bất kỳ mảng NumPy/Pandas tùy ý nào, mà chỉ có thể được sử dụng trong các mảng có kiểu dữ liệu 'object' (tức là mảng của các đối tượng Python):

import numpy as npimport pandas as pd
vals1 = np.array([1, None, 3, 4])vals1
array([1, None, 3, 4], dtype=object)

Đoạn mã HTML trên đề cập đến việc định dạng dữ liệu trong mảng NumPy. Theo đó, thuộc tính dtype=object được sử dụng để chỉ ra rằng loại dữ liệu phổ biến nhất mà NumPy có thể suy luận được cho nội dung của mảng là các đối tượng Python.Trong khi kiểu mảng này hữu ích cho một số mục đích, các hoạt động trên dữ liệu sẽ được thực hiện ở mức độ Python, với chi phí xử lý cao hơn so với các hoạt động nhanh chóng và tiêu chuẩn thấy trong các mảng với các loại dữ liệu nguyên thủy:

for dtype in ['object', 'int']:    print("dtype =", dtype)    %timeit np.arange(1E6, dtype=dtype).sum()    print()
dtype = object10 loops, best of 3: 78.2 ms per loopdtype = int100 loops, best of 3: 3.06 ms per loop

Việc sử dụng các đối tượng Python trong một mảng cũng có nghĩa là nếu bạn thực hiện các phép tổng hợp như sum() hoặc min() trên một mảng có giá trị None, bạn thường sẽ gặp lỗi:

vals1.sum()
---------------------------------------------------------------------------TypeError                                 Traceback (most recent call last)<ipython-input-4-749fd8ae6030> in <module>()----> 1 vals1.sum()/Users/jakevdp/anaconda/lib/python3.5/site-packages/numpy/core/_methods.py in _sum(a, axis, dtype, out, keepdims)     30      31 def _sum(a, axis=None, dtype=None, out=None, keepdims=False):---> 32     return umr_sum(a, axis, dtype, out, keepdims)     33      34 def _prod(a, axis=None, dtype=None, out=None, keepdims=False):TypeError: unsupported operand type(s) for +: 'int' and 'NoneType'

Điều này phản ánh thực tế rằng phép cộng giữa một số nguyên và None là không xác định.

NaN: Dữ liệu số bị thiếu

Cách biểu diễn dữ liệu còn thiếu khác, NaN (viết tắt của Not a Number, có nghĩa là “không phải là con số”), khác biệt; đây là một giá trị số chấm động đặc biệt được nhận diện bởi tất cả các hệ thống sử dụng biểu diễn số chấm động chuẩn IEEE:

vals2 = np.array([1, np.nan, 3, 4]) vals2.dtype
dtype('float64')

Lưu ý rằng NumPy đã chọn một kiểu số chấm động tự nhiên cho mảng này: điều này có nghĩa là khác với mảng object trước đó, mảng này hỗ trợ các phép toán nhanh được thực hiện trong mã đã được biên dịch.Bạn cần nhận thức rằng NaN giống như một loại virus dữ liệu – nó lây nhiễm vào bất kỳ đối tượng nào nó tiếp xúc.Bất kể phép tính nào, kết quả của phép tính với NaN sẽ là NaN khác:

1 + np.nan
nan
0 *  np.nan
nan

Lưu ý rằng điều này có nghĩa là tổng hợp các giá trị là được định nghĩa rõ ràng (tức là, chúng không gây ra lỗi) nhưng không luôn hữu ích:

vals2.sum(), vals2.min(), vals2.max()
(nan, nan, nan)

NumPy thực sự cung cấp một số phép tổng hợp đặc biệt sẽ bỏ qua những giá trị thiếu này:

np.nansum(vals2), np.nanmin(vals2), np.nanmax(vals2)
(8.0, 1.0, 4.0)

Hãy nhớ rằng NaN đặc biệt là một giá trị dấu chấm động; không có giá trị NaN tương đương cho số nguyên, chuỗi hoặc các kiểu dữ liệu khác.

NaN và None trong Pandas

NaNNone đều có vai trò riêng của chúng, và Pandas được xây dựng để xử lý hai giá trị này gần như tương đương, chuyển đổi giữa chúng khi thích hợp:

pd.Series([1, np.nan, 2, None])
0    1.01    NaN2    2.03    NaNdtype: float64

Đối với các kiểu dữ liệu mà không có giá trị gọi là “sentinel value”, Pandas tự động thực hiện việc chuyển đổi kiểu dữ liệu khi có giá trị NA (không có giá trị).Ví dụ, nếu chúng ta thiết lập một giá trị trong mảng số nguyên thành np.nan, nó sẽ tự động được chuyển đổi thành kiểu dữ liệu số thực để phù hợp với giá trị NA:

x = pd.Series(range(2), dtype=int)x
0    01    1dtype: int64
x[0] = Nonex
0    NaN1    1.0dtype: float64

Lưu ý rằng bên cạnh việc chuyển đổi mảng số nguyên sang số thực, Pandas tự động chuyển đổi None thành giá trị NaN. (Hãy nhận biết rằng hiện tại có một đề xuất để thêm một loại NA số nguyên nguyên thuỷ vào Pandas trong tương lai; cho đến khoảng thời gian này, nó chưa được bao gồm).

Mặc dù kiểu ma thuật này có thể cảm thấy hơi gia công so với cách tiếp cận thống nhất hơn với giá trị NA trong ngôn ngữ đặc thù của miền như R, phương pháp gửi thông báo/casting của Pandas hoạt động rất tốt trong thực tế và theo kinh nghiệm của tôi chỉ gây ra vấn đề hiếm khi.

Dưới đây là bảng liệt kê các quy ước upcasting trong Pandas khi giá trị NA được giới thiệu:

Hãy ghi nhớ rằng trong Pandas, dữ liệu chuỗi luôn được lưu trữ với kiểu dữ liệu object.

Thao tác trên giá trị Null

Như chúng ta đã thấy, Pandas xem NoneNaN như là hai giá trị hoàn toàn có thể thay thế nhau để chỉ ra giá trị thiếu hoặc giá trị null.Để hỗ trợ quy ước này, có một số phương pháp hữu ích để phát hiện, loại bỏ và thay thế các giá trị null trong cấu trúc dữ liệu của Pandas.Chúng là:

  • isnull(): Tạo một mặt nạ BOOLEAN chỉ định các giá trị bị thiếu
  • notnull(): Đảo ngược của isnull()
  • dropna(): Trả về phiên bản đã lọc của dữ liệu
  • fillna(): Trả về một bản sao của dữ liệu đã được điền hoặc thay thế các giá trị bị thiếu

Chúng ta sẽ kết thúc phần này với một khám phá và mô phỏng ngắn gọn về các hàm này.

Phát hiện giá trị rỗng

Cấu trúc dữ liệu trong Pandas có hai phương thức hữu ích để phát hiện dữ liệu null: isnull()notnull().Cả hai đều trả về một Boolean mask dựa trên dữ liệu. Ví dụ:

data = pd.Series([1, np.nan, 'hello', None])
data.isnull()
0    False1     True2    False3     Truedtype: bool

Như đã đề cập trong Data Indexing and Selection, Boolean masks có thể được sử dụng trực tiếp như là một chỉ mục của Series hoặc DataFrame:

data[data.notnull()]
0        12    hellodtype: object

Các phương thức isnull()notnull() cho ra kết quả Boolean tương tự cho các đối tượng DataFrame.

Bỏ các giá trị null

Ngoài việc sử dụng phương pháp che giấu trước, còn có các phương thức tiện ích, dropna()(mà loại bỏ các giá trị NA) và fillna() (mà điền vào các giá trị NA). Đối với một Series,kết quả là rõ ràng:

data.dropna()
0        12    hellodtype: object

Đối với một DataFrame, có nhiều lựa chọn hơn. Xem xét DataFrame sau đây:

df = pd.DataFrame([[1,      np.nan, 2],                   [2,      3,      5],                   [np.nan, 4,      6]])df

Sách hướng dẫn lập trình và chuyên gia IT tại Việt Nam

Mặc định, dropna() sẽ loại bỏ tất cả các hàng trong đó có bất kỳ giá trị null nào hiện diện:

df.dropna()

Một cách khác, bạn có thể loại bỏ các giá trị NA theo trục khác; axis=1 loại bỏ tất cả các cột chứa giá trị null:

df.dropna(axis='columns')

Nhưng điều này cũng gây mất một số dữ liệu tốt; bạn có thể quan tâm hơn là xóa các hàng hoặc cột có tất cả giá trị NA, hoặc phần lớn các giá trị NA.Điều này có thể được chỉ định thông qua các tham số how hoặc thresh, cho phép kiểm soát chính xác số lượng giá trị null được cho phép đi qua.

Mặc định là how='any', có nghĩa là bất kỳ hàng hoặc cột (tuỳ thuộc vào thuộc tính axis) chứa giá trị null sẽ bị loại bỏ.Bạn cũng có thể chỉ định how='all', chỉ loại bỏ các hàng/cột có giá trị null toàn bộ:

df[3] = np.nandf
df.dropna(axis='columns', how='all')

Để điều khiển chi tiết hơn, tham số thresh cho phép bạn chỉ định một số lượng tối thiểu các giá trị không rỗng để giữ lại hàng/cột:

df.dropna(axis='rows', thresh=3)

Ở đây, hàng đầu tiên và hàng cuối cùng đã được loại bỏ, vì chúng chỉ chứa hai giá trị không rỗng.

Điền giá trị rỗng

Đôi khi thay vì loại bỏ các giá trị NA, bạn muốn thay thế chúng bằng một giá trị hợp lệ.Giá trị này có thể là một số duy nhất như số không, hoặc nó có thể là một sự đặt giá hoặc nội suy từ các giá trị tốt.Bạn có thể thực hiện điều này trực tiếp bằng cách sử dụng phương thức isnull() như một mặt nạ, nhưng vì nó là một thao tác phổ biến, Pandas cung cấp phương thức fillna(), trả về một bản sao của mảng với các giá trị null đã được thay thế.

Xem xét Series sau đây:

data = pd.Series([1, np.nan, 2, None, 3], index=list('abcde'))data
a    1.0b    NaNc    2.0d    NaNe    3.0dtype: float64

Chúng ta có thể điền vào các giá trị NA bằng một giá trị duy nhất, chẳng hạn như số không:

data.fillna(0)
a    1.0b    0.0c    2.0d    0.0e    3.0dtype: float64

Chúng ta có thể chỉ định một forward-fill để lan truyền giá trị trước đó tiếp tục tiến lên:

# forward-filldata.fillna(method='ffill')
a    1.0b    1.0c    2.0d    2.0e    3.0dtype: float64

Hoặc chúng ta có thể chỉ định một back-fill để truyền các giá trị tiếp theo ngược lại:

# back-filldata.fillna(method='bfill')
a    1.0b    2.0c    2.0d    3.0e    3.0dtype: float64

Đối với DataFrame, các tùy chọn tương tự nhưng chúng ta cũng có thể chỉ định một axis để thực hiện việc điền các giá trị:

df
df.fillna(method='ffill', axis=1)

Lưu ý rằng nếu giá trị trước đó không có sẵn trong quá trình điền tiếp theo, giá trị NA vẫn được giữ nguyên.