06/04/2021, 14:46

Xử lý dữ liệu trong Pandas - Pandas

Ở trong series về NumPy ta đã được làm quen với các Ufuncs rất mạnh mẽ trong các thao tác tính toán và xử lý dữ liệu như các hàm số học (cộng, trừ, nhân, chia,...) và những phép toán phức tạp (lượng giác, luỹ thừa,...), vì Pandas vốn được xây dựng trên NumPy, nó kế thừa hầu hết các ưu điểm và các ...

Ở trong series về NumPy ta đã được làm quen với các Ufuncs rất mạnh mẽ trong các thao tác tính toán và xử lý dữ liệu như các hàm số học (cộng, trừ, nhân, chia,...) và những phép toán phức tạp (lượng giác, luỹ thừa,...), vì Pandas vốn được xây dựng trên NumPy, nó kế thừa hầu hết các ưu điểm và các phương thức mà NumPy có và đã được mình viết khá đầy đủ tại bài tính toán trên mảng với NumPy.

Một trong những điểm cần chú ý trong Pandas đó là khi sử dụng với unary functions, Pandas sẽ bảo toàn chỉ mục và tên cột (index preservation & column labels), còn khi thao tác với binary functions thì thư viện này sẽ tự động căn chỉnh chỉ mục (index alignment) với các tham số đầu vào. Điều này có nghĩa là ta có thể kết hợp các dữ liệu từ các nguồn khác nhau mà vẫn giữ được bản chất của bộ dữ liệu đó, nếu như dùng NumPy để làm việc này thì sẽ rất dễ xảy ra lỗi.

Trong bài này, ta sẽ cùng tìm hiểu về Index Preservation, Index Aligment, các thao tác giữa DataFrame và Series cũng như xử lý dữ liệu bị thiếu (missing data) nhé.

1. Giới thiệu về Index Preservation và Index Alignment

Index Preservation

Đầu tiên ta sẽ tạo một Series và một DataFrame từ mảng ngẫu nhiên NumPy như sau:

In[1]
sr = pd.Series(np.linspace(0, np.pi, 4), index=[2, 3, 5, 6])
df = pd.DataFrame(np.random.randint(0, 10, (3, 4)), columns=['A', 'B', 'C', 'D'])

print("Series: 
", sr)
print("
DataFrame: 
", df)
Out[2]
Series: 
2    0.000000
3    1.047198
5    2.094395
6    3.141593
dtype: float64

DataFrame: 
    A  B  C  D
0  9  7  8  9
1  9  8  0  1
2  7  8  7  4

Như chúng ta đã biết rằng Pandas được xây dựng dựa trên NumPy, nên ta có thể sử dụng bất kì ufuncs nào cho các object của Pandas, và nếu chúng ta sử dụng các unary function (các hàm tính toán chỉ có một tham số như lượng giác, mũ,...) thì kết quả trả về sẽ là một object Pandas mới với chỉ mục được bảo toàn (index preservation):

In[3]
print("Series: 
", np.cos(sr))
print("
DataFrame: 
", np.exp(df / np.e))
Out[3]
# Index & column labels của Series và DataFrame đều được giữ nguyên
Series: 
2    1.0
3    0.5
5   -0.5
6   -1.0
dtype: float64

DataFrame: 
            A          B          C          D
0  27.410194  13.133367  18.973353  27.410194
1  27.410194  18.973353   1.000000   1.444668
2  13.133367  18.973353  13.133367   4.355841

Index Aligment

Khi làm việc với các binary functions(các hàm tính toán có 2 tham số như cộng, trừ, nhân, chia,...) trên Series hay DataFrame, Pandas sẽ tự động căn chỉnh index trong quá trình tính toán, đây là một tính năng cực kì hữu ích khi phải làm việc với dữ liệu không đầy đủ. Ta sẽ đến với từng trường hợp trong Series và DataFrame để hiểu rõ hơn về cơ chế này.

Index Alignment trong Series

Giả sử ta đang có 2 bộ dữ liệu gồm 5 Tỉnh / Thành phố có số dân đông nhất và diện tích lớn nhất ở Việt Nam như sau:

In[4]
area = pd.Series({'Nghệ An': 16493, 'Gia Lai': 15510, 'Sơn La': 14125, 'Đăk Lăk': 13030, 'Thanh Hoá': 11130})
population = pd.Series({'TP.HCM': 8993, 'Hà Nội': 8053, 'Thanh Hoá': 3640, 'Nghệ An': 3237, 'Đồng Nai': 3097})

print("5 tỉnh / thành phố lớn nhất Việt Nam: 
", area)
print("
5 tỉnh / thành phố đông dân nhất Việt Nam: 
", population)
Out[4]
5 tỉnh / thành phố lớn nhất Việt Nam: 
Nghệ An      16493
Gia Lai      15510
Sơn La       14125
Đăk Lăk      13030
Thanh Hoá    11130
dtype: int64

5 tỉnh / thành phố đông dân nhất Việt Nam: 
TP.HCM       8993
Hà Nội       8053
Thanh Hoá    3640
Nghệ An      3237
Đồng Nai     3097
dtype: int64

Giờ nhiệm vụ của ta là tính mật độ dân số tương ứng, nhưng như ta thấy trong 2 bộ dữ liệu trên chỉ có 2 tỉnh là Thanh Hoá và Nghệ An là xuất hiện cả hai. Ta sẽ thử tính để xem kết quả như thế nào:

In[5]
density = area / population

print("Mật độ dân số: 
", density)
Out[5]
Mật độ dân số: 
Gia Lai           NaN
Hà Nội            NaN
Nghệ An      5.095150
Sơn La            NaN
TP.HCM            NaN
Thanh Hoá    3.057692
Đăk Lăk           NaN
Đồng Nai          NaN
dtype: float64

Như ta thấy trong output ở bên trên, kết quả trả về là một Series mới kết hợp tất cả các giá trị của 2 Series tham số. Với phần tử nào không xuất hiện trong Series còn lại sẽ được đánh dấu là NaN (not a number).

Cách xử lý này áp dụng cho bất kì phép toán nào được NumPy hỗ trợ. Nếu như ta không muốn để NaN xuất hiện trong kết quả trả về, ta có thể áp dụng bằng cách sử dụng ufuncs tương ứng với phép toán (đã nhắc đến ở bài Tính toán trên mảng với NumPy) và truyền tham số fill_value vào, chẳng hạn:

In[6]
X = pd.Series([1, 2, 3, 4], index=[0, 1, 2, 3])
Y = pd.Series([2, 3, 4, 5], index=[1, 2, 4, 5])

print("X + Y: 
", X + Y)

print("
X + Y: (filled) 
", X.add(Y, fill_value=0))
Out[6]
X + Y: 
0    NaN
1    4.0
2    6.0
3    NaN
4    NaN
5    NaN
dtype: float64

# fill_value sẽ được điền vào các giá trị bị khuyết ở từng Series tương ứng, sau đó thực hiện phép toán cộng như bình thường
X + Y: (filled) 
0    1.0
1    4.0
2    6.0
3    4.0
4    4.0
5    5.0
dtype: float64

Index Alignment trong DataFrame

Index Alignment trong DataFrame hoạt động khá tương tự như trong Series nhưng là với mảng 2 chiều với các hàng và cột tương ứng:

In[7]
Xdf = pd.DataFrame(np.random.randint(0, 10, (3, 3)), columns=['A', 'B', 'C'])
Ydf = pd.DataFrame(np.random.randint(0, 10, (4, 4)), columns=['A', 'C', 'B', 'D'])

print("Xdf: 
", Xdf)
print("
Ydf: 
", Ydf)

print("
Xdf + Ydf: 
", Xdf + Ydf)
Out[7]
Xdf: 
   A  B  C
0  7  7  4
1  6  6  1
2  9  7  1

Ydf: 
   A  C  B  D
0  0  2  4  5
1  2  0  0  2
2  4  4  0  9
3  2  0  7  8

Xdf + Ydf: 
     A     B    C   D
0   7.0  11.0  6.0 NaN
1   8.0   6.0  1.0 NaN
2  13.0   7.0  5.0 NaN
3   NaN   NaN  NaN NaN

Ở trong ví dụ trên, nếu bạn để ý thì mình đã cố ý để index của Xdf là [A, B, C] còn Ydf là [A, C, B, D], tuy nhiên kết quả trả về vẫn cho ta các cột A, B, C, D tương ứng. Điều này chứng tỏ Pandas không quan tâm đến thứ tự của các cột mà sẽ dựa vào tên của các cột tương ứng, và kết quả trả về thì các cột sẽ được sắp xếp theo các thứ tự cụ thể (A -> Z, từ bé đến lớn,...).

Ta cũng hoàn toàn có thể truyền fill_value vào cho DataFrame giống như Series:

# Tính giá trị trung bình của tất cả các phần tử trong Xdf và Ydf
avg = (Xdf.to_numpy().mean() + Ydf.to_numpy().mean()) / 2

print("
Xdf + Ydf (filled): 
", Xdf.add(Ydf, fill_value=avg))
Out[8]
Xdf + Ydf (filled): 
       A          B         C          D
0   7.000000  11.000000  6.000000   9.197917
1   8.000000   6.000000  1.000000   6.197917
2  13.000000   7.000000  5.000000  13.197917
3   6.197917  11.197917  4.197917  12.197917

Ta có bảng các toán tử trong Python tương đương với các phương thức trong Pandas sau:

2. Xử lý dữ liệu giữa Series và DataFrame

Khi chúng ta thao tác dữ liệu giữa Series và DataFrame thì index và column tương ứng ở 2 bên được căn chỉnh tương ứng như đã giới thiệu ở trên. Nhìn chung thì việc ta xử lý dữ liệu giữa Series và DataFrame khá là tương đồng với việc ta tính toán giữa mảng một chiều và mảng hai chiều trong NumPy, chẳng hạn:

In[9]
A = np.random.randint(0, 10, (3, 4))
B = np.array([2, 2, 2, 2])

print("A: 
", A)
print("B: 
", B)

print("
A * B: 
", A * B)

Adf = pd.DataFrame(A, columns=['A', 'B', 'C', 'D'])
Bs = pd.Series(B, index=['A', 'B', 'C', 'D'])

print("
Adf * Bs
", Adf * Bs)
Out[9]
A: 
 [[2 7 4 5]
 [0 8 8 0]
 [6 5 1 1]]
B: 
 [2 2 2 2]

A * B: 
 [[ 4 14  8 10]
 [ 0 16 16  0]
 [12 10  2  2]]

Adf * Bs
    A   B   C   D
0   4  14   8  10
1   0  16  16   0
2  12  10   2   2

Lưu ý là các phép toán với các mảng khác chiều đều được thực hiện theo các nguyên tắc của Broadcasting trong NumPy mà mình đã nhắc trong phần 3 đến ở đây: Tính toán trên mảng với NumPy, nếu các bạn không hiểu ví dụ trên hãy vào để đọc thêm nhé.

Quay lại với ví dụ trên, nếu ta muốn thực hiện phép tính theo các cột thì giống với NumPy, ta sẽ truyền vào tham số axis tương ứng với trục mà ta cần:

In[10]
print("Nhân các phần tử trong DataFrame với cột A: 
", Adf.multiply(Adf['A'], axis=0))
Out[10]
Nhân các phần tử trong DataFrame với cột A: 
    A   B  C   D
0   4  14  8  10
1   0   0  0   0
2  36  30  6   6

Và index cũng như column sẽ được căn chỉnh trong trường hợp dữ liệu bị thiếu:

In[11]
even = Adf[Adf % 2 == 0].iloc[:, ::2]
print("Các phần tử chẵn trong cột A và C: 
", even)

print("
Adf * even: 
", Adf * even)
Out[11]
Các phần tử chẵn trong cột A và C: 
    A    C
0  2  4.0
1  0  8.0
2  6  NaN

Adf * even: 
     A   B     C   D
0   4 NaN  16.0 NaN
1   0 NaN  64.0 NaN
2  36 NaN   NaN NaN

Với tính căn chỉnh và bảo toàn, Pandas sẽ luôn duy trì bản chất của dữ liệu và ngăn chặn các lỗi trong quá trình xử lý cũng như tính toán ,đặc biệt là làm việc với dữ liệu không đồng nhất hoặc là dữ liệu thô từ NumPy.

3. Xử lý dữ liệu bị thiếu (missing data)

Khi ta còn ngồi học trên trường và ra ngoài làm việc, sẽ có những thứ mà chúng ta chẳng bao giờ xuất hiện ở giảng đường hay trong bài kiểm tra cuối kỳ. Dữ liệu cũng khá giống vậy, giữa dữ liệu thực tế và các dữ liệu mẫu trong các bài hướng dẫn trên mạng sẽ khác nhau khá nhiều. Các bộ dữ liệu mà ta có khi thu thập được rất hiếm khi là dữ liệu đồng nhất cũng như đầy đủ không thiếu một hàng nào. Trong phần 3 này, ta sẽ làm quen với các cách để xử lý dữ liệu bị thiếu trong Pandas. Bên cạnh đó, kể từ bài này thì chúng ta sẽ quy ước các dữ liệu bị thiếu sẽ được gọi chung là NA.

Missing data trong Pandas

Missing data trong Pandas về cơ bản giống như trong NumPy. Pandas chọn 2 giá trị null có sẵn trong Python là NaN None để biểu diễn cho missing data trong các tập dữ liệu mà nó xử lý, mỗi cách chọn về cơ bản sẽ có một số lợi ích cũng như hạn chế khác nhau và ta sẽ cùng nhau tìm hiểu nhé.

None

Đầu tiên ta sẽ tìm hiểu về None trước, đây là một giá trị khá phổ biến trong Python khi ta muốn gán cho biến nào đó mà không có giá trị nhất định. Vì None bản chất là một object, ta không thể sử dụng cho bất kỳ một mảng NumPy hay Pandas tuỳ ý nào mà chỉ có thể dùng cho các mảng có kiểu dữ liệu là object:

In[12]
n = np.array([1, 2, None, 3])

print(n.dtype)
Out[13]
object

Kiểu dữ liệu object này khá hữu ích trong trường hợp mảng của bạn là dữ liệu không đồng nhất, đặc biệt với None cũng là một object. Tuy nhiên, vấn đề của object đó là nó khá chậm so với các kiểu dữ liệu khác, ta có thể làm một benchmark nho nhỏ sau:

for dtype in ['object', 'int']:
    print("dtype =", dtype)
    %timeit np.arange(1E8, dtype=dtype).sum()
    print()
Kiểu dữ liệu:  object
10.4 ms ± 1.29 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)

Kiểu dữ liệu:  int
235 µs ± 38.1 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Như ta thấy rằng sử dụng object chậm hơn khá nhiều so với int (dù là chỉ ở mức ms), với tập dữ liệu nhỏ thì ta sẽ không thấy được nhiều sự khác biệt, tuy nhiên nếu ở tập dữ liệu đủ lớn thì nó sẽ rất rõ ràng (bạn có thể đọc thêm ở bài Tính toán trên mảng với NumPy mục Vectorized operations, mình đã giải thích cơ chế tăng tốc tính toán của NumPy).

Một vấn đề của None nữa đó chính là vì đây là object, ta sẽ không thể thực hiện được các phép toán số học lên toàn mảng, chẳng hạn:

In[13]
n.mean()
Out[13]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-13-e454570d7e3f> in <module>
----> 1 n.mean()

~anaconda3libsite-packages
umpycore\_methods.py in _mean(a, axis, dtype, out, keepdims)
    158             is_float16_result = True
    159 
--> 160     ret = umr_sum(arr, axis, dtype, out, keepdims)
    161     if isinstance(ret, mu.ndarray):
    162         ret = um.true_divide(

TypeError: unsupported operand type(s) for +: 'int' and 'NoneType'

NaN (Not a Number)

Một cách biểu diễn dữ liệu bị thiếu khác là NaN (viết tắt cho Not a Number), khác với None, nó là một giá trị floating-point đặc biệt được công nhận bởi tất cả các hệ thống sử dụng tiêu chuẩn IEEE, điều này có nghĩa là về cơ bản nó không khác gì một số thực:

In[14]
na = np.array([1, 2, np.nan, 4])

print(na.dtype)
Out[14]
float64

Vì mảng trên cơ bản là một mảng số nên dĩ nhiên tốc độ sẽ nhanh hơn nhiều so với một mảng có kiểu dữ liệu là object (như ta đã benchmark ở trên). Với NaN, một điều mà ta cần phải chú ý đó là mọi thứ khi cộng, trừ, nhân, chia,.... hay thực hiện bất cứ phép toán nào với NaN đều sẽ trả về là NaN:

In[15]
# Một số cộng với NaN
print("999 + nan = ", 1 + np.nan)

# Một số nhân với NaN
print("0 * nan = ", 0 * np.nan)

# Một số phép toán lên mảng chứa NaN

print("na = ", na)
print("mean: ", na.mean())
print("max: ", na.max())
Out[15]
999 + nan =  nan
0 * nan =  nan
na =  [ 1.  2. nan  4.]
mean:  nan
max:  nan

Như mình đã nhắc ở trong chương NumPy, để bỏ qua các giá trị NaN trong mảng khi thực hiện các phép toán lên toàn bộ mảng thì ta chỉ cần dùng các phương thức có tiền tố "nan" ở đằng trước như sau:

In[16]
print("na = ", na)
print("mean: ", np.nanmean(na))
print("max: ", np.nanmax(na))
Out[16]
na =  [ 1.  2. nan  4.]
mean:  2.3333333333333335
max:  4.0

NaN và None trong Pandas

NaN và None đều được sử dụng trong Pandas tuỳ theo từng trường hợp, và một điều thú vị trong Pandas là nó được thiết kế để đưa ra kiểu giá trị phù hợp cho tập dữ liệu mà ta đang dùng, chẳng hạn:

In[17]
p = pd.Series([1, 2, 3, np.nan, 5, None])

print(p)
Out[17]
0    1.0
1    2.0
2    3.0
3    NaN
4    5.0
5    NaN
dtype: float64

Như ta thấy rằng phần tử mang giá trị None ở vị trí cuối cùng đã tự động đổi thành NaN để phù hợp với kiểu dữ liệu float trong mảng. Tuy nhiên, Pandas không chỉ thay đổi giá trị None sang NaN, mà nó còn có thể thay đổi luôn cả mảng số nguyên (int) sang số thực (float) nếu như xuất hiện giá trị NaN:

In[18]
# Mảng chứa toàn các phần tử số nguyên
int_array = pd.Series([1, 2, 3, 4, 5])

print(int_array)

# Mảng chứa toàn các phần tử số nguyên nhưng thêm NaN
nan_array = pd.Series([1, 2, 3, 4, 5, np.nan])

print(nan_array)
Out[18]
0    1
1    2
2    3
3    4
4    5
dtype: int64

0    1.0
1    2.0
2    3.0
3    4.0
4    5.0
5    NaN
dtype: float64

Ta có thể ngăn việc Pandas chuyển giá trị sang float bằng cách sử dụng một kiểu dtype đặc biệt sau:

In[19]
pd.Series([1, 2, np.nan, 4], dtype=pd.Int64Dtype())
Out[19]
0       1
1       2
2    <NA>
3       4
dtype: Int64

Dưới đây là bảng giá trị NA tương ứng của Pandas khi xử lý dữ liệu, để tránh nhầm lẫn khi thao tác với None và NaN thì các bạn nên ghi nhớ kĩ bảng này:

Một điều cần để ý đó là nếu mảng của bạn là mảng chứa các chuỗi (string) thì Pandas sẽ tự động hiểu dtype là object và nhận cả None và NaN:

In[20]
pd.Series(['Zaidap.com', '.net', np.nan, None])
Out[20]
0    Zaidap.com
1        .net
2         NaN
3        None
dtype: object

Các phép toán với các giá trị NA

Xác định giá trị NA

Pandas có 2 phương thức khá hữu ích trong việc xác định NA đó là isnull notnull, trong đó phương thức isnull sẽ trả về một mảng boolean đánh dấu các phần tử NA với giá trị là True, còn phương thức notnull chính là ngược lại với isnull. Ta có ví dụ sau:

In[21]
s = pd.Series(['Zaidap.com', '.net', np.nan, None])

s.isnull()
Out[21]
0    False
1    False
2     True
3     True
dtype: bool

Và vì giá trị trả về là một mảng boolean, ta có thể dùng mảng này như là một masks để lọc dữ liệu:

In[22]
s[s.notnull()]
Out[22]
0    Zaidap.com
1        .net
dtype: object

Loại bỏ các giá trị NA

Để loại bỏ các giá trị NA trong một Series hay DataFrame nhanh chóng mà không cần phải sử dụng đến masking như trên, Pandas cung cấp cho ta phương thức dropna(). Với Series thì việc loại bỏ các giá trị NA khá dễ dàng:

In[23]
s.dropna()
Out[23]
0    Zaidap.com
1        .net
dtype: object

Với DataFrame, vì là mảng 2 chiều nên sẽ có một số vấn đề. Giả sử ta có mảng sau:

In[24]
m = pd.DataFrame([[2, np.nan, 5],
                  [3, 7, 9],
                  [4, 4, np.nan]])
print(m)
Out[24]
   0    1    2
0  2  NaN  5.0
1  3  7.0  9.0
2  4  4.0  NaN

Ta không thể loại bỏ đi các phần tử mang giá trị NaN trong DataFrame được vì nó thuộc về một cột và một hàng nhất định, khác với Series chỉ là 1 hàng. Do vậy nếu ta áp dụng phương thức này lên DataFrame thì sẽ có 2 trường hợp chính là xoá đi hàng hoặc cột tương ứng chứa giá trị NA đó:

In[25]
print("Xoá hàng chứa NaN: 
", m.dropna()) # có thể dùng dropna(axis=0) hoặc dropna(axis='rows') thay thế

print("
Xoá cột chứa NaN: 
", m.dropna(axis=1)) # có thể dùng dropna(axis='columns') thay thế
Out[25]
Xoá hàng chứa NaN: 
    0    1    2
1  3  7.0  9.0

Xoá cột chứa NaN: 
   0
0  2
1  3
2  4

Vậy ta đã xoá được các giá trị NA ra khỏi DataFrame, tuy nhiên đồng thời ta cũng xoá đi những dữ liệu mang giá trị khác và việc xoá này có thể gây ảnh hưởng đến toàn bộ tập dữ liệu, giả sử một cột có hàng trăm ngàn giá trị và chỉ một giá trị NA mà ta phải xoá đi toàn bộ cột thì không đáng chút nào. Nhìn chung ta chỉ cần xoá đi những hàng hoặc cột mà nó chứa toàn bộ / phần lớn là các giá trị NA. Do đó Pandas cung cấp 2 tham số là how thresh để giải quyết việc này, nó cho phép chúng ta kiểm soát được số lượng phần tử NA tương ứng để xoá đi.

Tham số how nhận 2 giá trị là 'any' 'all'. Giá trị mặc định của how 'any', giá trị này sẽ chỉ định xoá đi toàn bộ cột hoặc hàng chứa giá trị NA (giống với ví dụ ở trên). Với 'any', hàng / cột tương ứng sẽ chỉ bị xoá đi khi toàn bộ hàng / cột đó đều mang giá trị NA:

In[26]
# Chuyển toàn bộ cột đầu tiên thành giá trị NaN
m[0] = np.nan
print("m: 
", m)

print("
dropna với tham số how = all :
", m.dropna(how='all', axis=1))
Out[26]
m: 
     0    1    2
0 NaN  NaN  5.0
1 NaN  7.0  9.0
2 NaN  4.0  NaN

dropna với tham số how = all :
     1    2
0  NaN  5.0
1  7.0  9.0
2  4.0  NaN

Thay thế NA bằng một giá trị khác

Trong một số trường hợp, thay vì xoá đi NA thì ta có thể thay nó bằng một số khác. Pandas hỗ trợ vấn đề này bằng phương thức fillna().

Ta có một ví dụ nhỏ với Series:

In[27]
f = pd.Series([1, 2, np.nan, 5, np.nan, 3, np.nan, 9], index=list('Zaidap.com'))

print("f series: 
", f)

# Điền vào các giá trị NA bằng một số bất kỳ
print("
Thay thế NA = 0: 
", f.fillna(0))

# Điền vào các giá trị NA bằng giá trị của phần tử trước
print("
f (thay thế NA = phần tử đứng trước): 
", f.fillna(method='ffill'))

# Điền vào các giá trị NA bằng giá trị của phần tử sau
print("
f: (thay thế NA = phần tử đứng sau)
", f.fillna(method='bfill'))
Out[27]
f series: 
F    1.0
r    2.0
e    NaN
e    5.0
t    NaN
u    3.0
t    NaN
s    9.0
dtype: float64

Thay thế NA = 0: 
F    1.0
r    2.0
e    0.0
e    5.0
t    0.0
u    3.0
t    0.0
s    9.0
dtype: float64

f (thay thế NA = phần tử đứng trước): 
F    1.0
r    2.0
e    2.0
e    5.0
t    5.0
u    3.0
t    3.0
s    9.0
dtype: float64

f: (thay thế NA = phần tử đứng sau)
F    1.0
r    2.0
e    5.0
e    5.0
t    3.0
u    3.0
t    9.0
s    9.0
dtype: float64

Với DataFrame thì các thao tác cũng giống như Series tuy nhiên thì ngoài ta sẽ có thêm các tuỳ chọn liên quan đến các cột và hàng (giống với dropna):

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

print("df: 
", df)

print("
ffill theo axis=1: 
", df.fillna(method='ffill', axis=0))

print("
bfill theo axis=1: 
", df.fillna(method='bfill', axis=1))
Out[28]
df: 
      0    1  2    3
0  1.0  2.0  4  NaN
1  NaN  2.0  1  9.0
2  NaN  NaN  2  3.0
3  2.0  4.0  5  NaN

ffill theo axis=1: 
      0    1  2    3
0  1.0  2.0  4  NaN
1  1.0  2.0  1  9.0
2  1.0  2.0  2  3.0
3  2.0  4.0  5  3.0

bfill theo axis=1: 
      0    1    2    3
0  1.0  2.0  4.0  NaN
1  2.0  2.0  1.0  9.0
2  2.0  2.0  2.0  3.0
3  2.0  4.0  5.0  NaN

Lưu ý rằng với bfill ffill, nếu giá trị phía sau và phía trước tương ứng không tồn tại thì giá trị NA vẫn sẽ được giữ nguyên.

4. Tổng kết

Vậy là chúng ta đã hoàn thành một bài khá dài trong chương Pandas này, trong bài ta đã làm quen với những khái niệm cơ bản khi thao tác với mảng, làm việc giữa DataFrame & Series cũng như một điều rất quan trọng đó là xử lý với dữ liệu bị thiếu trong Pandas. Đây là một trong những bài quan trọng nhất của series, đặc biệt bạn hãy thử nhiều ví dụ với xử lý dữ liệu bị thiếu bằng các phương thức đã giới thiệu ở trên để hiểu rõ về bản chất của nó. Trong bài tiếp theo, ta sẽ tìm hiểu về Hierarchical Indexing trong Pandas nhé, hẹn gặp lại bạn ở trong bài tiếp theo.

0