Thông báo: Download 4 khóa học Python từ cơ bản đến nâng cao tại đây.
Xử lý dữ liệu trong 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 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:
Bài viết này được đăng tại [free tuts .net]
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: \n", sr) print("\nDataFrame: \n", df)
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):
print("Series: \n", np.cos(sr)) print("\nDataFrame: \n", np.exp(df / np.e))
# 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:
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: \n", area) print("\n5 tỉnh / thành phố đông dân nhất Việt Nam: \n", population)
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:
density = area / population print("Mật độ dân số: \n", density)
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:
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: \n", X + Y) print("\nX + Y: (filled) \n", X.add(Y, fill_value=0))
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:
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: \n", Xdf) print("\nYdf: \n", Ydf) print("\nXdf + Ydf: \n", Xdf + Ydf)
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("\nXdf + Ydf (filled): \n", Xdf.add(Ydf, fill_value=avg))
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:
Toán tử trong Python | Phương thức trong Pandas |
+ | add() |
- | sub(), subtract() |
* | mul(), multiply() |
/ | truediv(), div(), divide() |
// | floordiv() |
% | mod() |
** | pow() |
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:
A = np.random.randint(0, 10, (3, 4)) B = np.array([2, 2, 2, 2]) print("A: \n", A) print("B: \n", B) print("\nA * B: \n", A * B) Adf = pd.DataFrame(A, columns=['A', 'B', 'C', 'D']) Bs = pd.Series(B, index=['A', 'B', 'C', 'D']) print("\nAdf * Bs\n", Adf * Bs)
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:
print("Nhân các phần tử trong DataFrame với cột A: \n", Adf.multiply(Adf['A'], axis=0))
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:
even = Adf[Adf % 2 == 0].iloc[:, ::2] print("Các phần tử chẵn trong cột A và C: \n", even) print("\nAdf * even: \n", Adf * even)
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 và 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:
n = np.array([1, 2, None, 3]) print(n.dtype)
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:
n.mean()
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-13-e454570d7e3f> in <module> ----> 1 n.mean() ~\anaconda3\lib\site-packages\numpy\core\_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:
na = np.array([1, 2, np.nan, 4]) print(na.dtype)
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:
# 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())
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:
print("na = ", na) print("mean: ", np.nanmean(na)) print("max: ", np.nanmax(na))
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:
p = pd.Series([1, 2, 3, np.nan, 5, None]) print(p)
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:
# 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)
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:
pd.Series([1, 2, np.nan, 4], dtype=pd.Int64Dtype())
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:
Kiểu dữ liệu | Chuyển kiểu dữ liệu khi thêm giá trị NA | Giá trị NA tương ứng |
float | Không thay đổi | np.nan |
object | Không thay đổi | None hoặc np.nan |
integer | Chuyển sang float64 | np.nan |
boolean | Chuyển sang object | None hoặc np.nan |
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:
pd.Series(['Freetuts', '.net', np.nan, None])
0 Freetuts 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 và 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:
s = pd.Series(['Freetuts', '.net', np.nan, None]) s.isnull()
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:
s[s.notnull()]
0 Freetuts 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:
s.dropna()
0 Freetuts 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:
m = pd.DataFrame([[2, np.nan, 5], [3, 7, 9], [4, 4, np.nan]]) print(m)
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 đó:
print("Xoá hàng chứa NaN: \n", m.dropna()) # có thể dùng dropna(axis=0) hoặc dropna(axis='rows') thay thế print("\nXoá cột chứa NaN: \n", m.dropna(axis=1)) # có thể dùng dropna(axis='columns') thay thế
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 và 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' và 'all'. Giá trị mặc định của how là '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:
# Chuyển toàn bộ cột đầu tiên thành giá trị NaN m[0] = np.nan print("m: \n", m) print("\ndropna với tham số how = all :\n", m.dropna(how='all', axis=1))
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:
f = pd.Series([1, 2, np.nan, 5, np.nan, 3, np.nan, 9], index=list('Freetuts')) print("f series: \n", f) # Điền vào các giá trị NA bằng một số bất kỳ print("\nThay thế NA = 0: \n", f.fillna(0)) # Điền vào các giá trị NA bằng giá trị của phần tử trước print("\nf (thay thế NA = phần tử đứng trước): \n", f.fillna(method='ffill')) # Điền vào các giá trị NA bằng giá trị của phần tử sau print("\nf: (thay thế NA = phần tử đứng sau)\n", f.fillna(method='bfill'))
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):
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: \n", df) print("\nffill theo axis=1: \n", df.fillna(method='ffill', axis=0)) print("\nbfill theo axis=1: \n", df.fillna(method='bfill', axis=1))
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 và 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.