Machine Learning Data Preparation

DP4ML - Missing Data - Phần 2 - Statistical Imputation

DP4ML - Missing Data - Phần 2 - Statistical Imputation

Bài thứ 5 trong chuỗi các bài viết về chủ đề Data Preparation cho các mô hình ML và là bài thứ 2 về chủ đề Missing Data. Trong bài này, chúng ta sẽ tìm hiểu phương pháp tiếp theo để giải quyết vấn đề Missing Data, đó là phương pháp Statistical Imputation.

Imputation, nói chung là các phương pháp thay thế các Missing Data bằng một giá trị khác. Có 3 phương pháp Imputation phổ biến là Statistical Imputation, KNN Imputation, và Iterative Imputation. Phương pháp đầu tiên sẽ được tìm hiểu trong bài này.

1. Statistic Imputation

Đây là phương pháp sử dụng các giá trị thống kê để thay thế cho Missing Data. Ưu điểm của nó là đơn giản, tính toán nhanh. Một số phương án thay thế Missing Data bằng giá trị thống kê có thể sử dụng ở đây là:

  • Thay thế Missing Data trong 1 cột bằng giá trị trung bình của cột chứa Missing Data đó.
  • Thay thế Missing Data trong 1 cột bằng giá trị trung vị của cột chứa Missing Data đó.
  • Thay thế Missing Data trong 1 cột bằng giá trị xuất hiện nhiều nhất (mode) trong cột chứa Missing Data đó.
  • Thay thế Missing Data trong 1 cột bằng giá trị là hằng số khác.

2. Thực hàng Statistical Imputation

Chúng ta sẽ thử thực hành phương pháp Statistical Imputation trên một bộ dữ liệu cụ thể.

2.1 Giới thiệu bộ dữ liệu Horse Colic Dataset

Horse Colic Dataset là bộ dữ liệu mô tả các đặc điểm y học của những con ngựa bị đau bụng và cho ra kết quả là chúng còn sống hay đã chết. Có tổng cộng 300 con ngựa, mỗi con có 26 đặc điểm được ghi nhận. Thông tin chi tiết về các trường dữ liệu được trình bày trong file này. Đây là bài toán Binary Classification, kết quả dự đoán là 1 nếu con ngựa còn sống và 2 nếu con ngựa đã chết. Tập dữ liệu có nhiều giá trị bị thiếu được đánh dấu bằng một ký tự dấu chấm hỏi (“?”).

2,1,530101,38.50,66,28,3,3,?,2,5,4,4,?,?,?,3,5,45.00,8.40,?,?,2,2,11300,00000,00000,2
1,1,534817,39.2,88,20,?,?,4,1,3,4,2,?,?,?,4,2,50,85,2,2,3,2,02208,00000,00000,2
2,1,530334,38.30,40,24,1,1,3,1,3,3,1,?,?,?,1,1,33.00,6.70,?,?,1,2,00000,00000,00000,1
1,9,5290409,39.10,164,84,4,1,6,2,2,4,4,1,2,5.00,3,?,48.00,7.20,3,5.30,2,1,02208,00000,00000,1
...

Mình sẽ tiến hành đọc bộ dữ liệu này vào một Dataframe, thay thế Missing Data bằng giá trị NaN, sau đó đếm số lượng Missing Data trong mỗi cột kèm theo phần trăm tương ứng:

# summarize the horse colic dataset
from pandas import read_csv
# load dataset
dataframe = read_csv('horse-colic.csv' , header=None, na_values= '?')
# summarize the first few rows
print(dataframe.head())
# summarize the number of rows with missing values for each column
for i in range(dataframe.shape[1]):
    # count number of rows with missing values
    n_miss = dataframe[[i]].isnull().sum()
    perc = n_miss / dataframe.shape[0] * 100
    print('> %d, Missing: %d (%.1f%%)' % (i, n_miss, perc))

Kết quả thực hiện:

    0   1        2     3      4     5    6    7    8    9    10   11   12   13   14   15   16   17    18    19   20   21   22  23     24  25  26  27
0  2.0   1   530101  38.5   66.0  28.0  3.0  3.0  NaN  2.0  5.0  4.0  4.0  NaN  NaN  NaN  3.0  5.0  45.0   8.4  NaN  NaN  2.0   2  11300   0   0   2
1  1.0   1   534817  39.2   88.0  20.0  NaN  NaN  4.0  1.0  3.0  4.0  2.0  NaN  NaN  NaN  4.0  2.0  50.0  85.0  2.0  2.0  3.0   2   2208   0   0   2
2  2.0   1   530334  38.3   40.0  24.0  1.0  1.0  3.0  1.0  3.0  3.0  1.0  NaN  NaN  NaN  1.0  1.0  33.0   6.7  NaN  NaN  1.0   2      0   0   0   1
3  1.0   9  5290409  39.1  164.0  84.0  4.0  1.0  6.0  2.0  2.0  4.0  4.0  1.0  2.0  5.0  3.0  NaN  48.0   7.2  3.0  5.3  2.0   1   2208   0   0   1
4  2.0   1   530255  37.3  104.0  35.0  NaN  NaN  6.0  2.0  NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN  74.0   7.4  NaN  NaN  2.0   2   4300   0   0   2
> 0, Missing: 1 (0.3%)
> 1, Missing: 0 (0.0%)
> 2, Missing: 0 (0.0%)
> 3, Missing: 60 (20.0%)
> 4, Missing: 24 (8.0%)
> 5, Missing: 58 (19.3%)
> 6, Missing: 56 (18.7%)
> 7, Missing: 69 (23.0%)
> 8, Missing: 47 (15.7%)
> 9, Missing: 32 (10.7%)
> 10, Missing: 55 (18.3%)
> 11, Missing: 44 (14.7%)
> 12, Missing: 56 (18.7%)
> 13, Missing: 104 (34.7%)
> 14, Missing: 106 (35.3%)
> 15, Missing: 247 (82.3%)
> 16, Missing: 102 (34.0%)
> 17, Missing: 118 (39.3%)
> 18, Missing: 29 (9.7%)
> 19, Missing: 33 (11.0%)
> 20, Missing: 165 (55.0%)
> 21, Missing: 198 (66.0%)
> 22, Missing: 1 (0.3%)
> 23, Missing: 0 (0.0%)
> 24, Missing: 0 (0.0%)
> 25, Missing: 0 (0.0%)
> 26, Missing: 0 (0.0%)
> 27, Missing: 0 (0.0%)

Có thể thấy rằng, chỉ có cột 1, 2, 23, 24, 25, 25, 27 là không có Missing Data. Các cột còn lại đều có. Đặc biệt, cột cột 15 và 21, số lượng Missing Data chiếm phần lớn.

2.2 Thực hiện Statistical Imputation

Thư viện Scikit-learn cung cấp lớp SimpleImputer giúp chúng ta thực hiện Statistical Imputation rất dễ dàng.

a, SimpleImputer và Data Transform

Đầu tiên, khai báo một Instance của lớp SimpleImputer, truyền vào loại thống kê muốn sử dụng: mean, median, mode, …

# define imputer
imputer = SimpleImputer(strategy= 'mean')

Tiếp theo, dùng imputer vừa khai báo fit trên tập dữ liệu để tính toán trung bình của mỗi cột.

...
# fit on the dataset
imputer.fit(X)

Cuối cùng, mang imputer áp dụng lên toàn bộ tập dữ liệu để tạo ra một phiên bản mới của tập dữ liệu, trong đó, tất cả Missing Data được thay thể bởi giá trị trung bình của cột chứa nó.

...
# transform the dataset
Xtrans = imputer.transform(X)

Code đầy đủ áp dụng vào tập dữ liệu Horse Colic Dataset như sau:

# statistical imputation transform for the horse colic dataset
from numpy import isnan
from pandas import read_csv
from sklearn.impute import SimpleImputer

# load dataset
dataframe = read_csv('horse-colic.csv' , header=None, na_values= '?' )
# split into input and output elements. #23 is label column
data = dataframe.values
ix = [i for i in range(data.shape[1]) if i != 23]
X, y = data[:, ix], data[:, 23]
# summarize total missing
print( 'Missing: %d' % sum(isnan(X).flatten()))
# define imputer
imputer = SimpleImputer(strategy= 'mean' )
# fit on the dataset
imputer.fit(X)
# transform the dataset
Xtrans = imputer.transform(X)
# summarize total missing
print( 'Missing: %d' % sum(isnan(Xtrans).flatten()))

Kết quả thực thi:

Missing: 1605
Missing: 0

Ban đầu, có 1605 Missing Data. Sau khi thực hiện Statistical Imputation, không còn Missing Data nữa.

b, SimpleImputer và Model Evaluation

Phần này, chúng ta sẽ thực hiện đầy đủ việc áp dụng Statistical Imputation vào mô hình hóa thuật toán RandomForest trên tập dữ liệu Horse Colic Dataset theo phương pháp k-Fold Cross-validation.

Hãy nhớ lại cách tiếp cận chuẩn khi thực hiện Data Preparation mà chúng ta đã đề cập ở bài đầu tiên. Cụ thể, với bài toán này, các bước thực hiện sẽ như sau:

  • Chia tập dữ liệu thành 2 tập Train và Test theo kiểu k-Fold Cross-validation.
  • Tính toán giá trị Statistic trên mỗi cột của tập Train.
  • Áp dụng phần tính toán đó vào cả 2 tập Train và Test.

Để thực hiện các công viêc này một cách đơn giản, mình sẽ sử dụng lớp Pipeline của Scikit-learn. Có thể hiểu đó là một cách thực hiện các bước tuần tự nhau theo một thứ tự được quy định trước. Như ví dụ dưới đây, Pipeline gồm 2 bước: bước 1 là khai báo SimpleImputer, bước 2 là khai báo RandomForest model:

...
# define modeling pipeline
model = RandomForestClassifier()
imputer = SimpleImputer(strategy= 'mean')
pipeline = Pipeline(steps=[('i' , imputer), ('m' , model)])

Code đây đủ của bài toán như dưới đây:

# evaluate mean imputation and random forest for the horse colic dataset
from numpy import mean
from numpy import std
from pandas import read_csv
from sklearn.ensemble import RandomForestClassifier
from sklearn.impute import SimpleImputer
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import RepeatedStratifiedKFold
from sklearn.pipeline import Pipeline

# load dataset
dataframe = read_csv('horse-colic.csv' , header=None, na_values= '?')
# split into input and output elements
data = dataframe.values
ix = [i for i in range(data.shape[1]) if i != 23]
X, y = data[:, ix], data[:, 23]
# define modeling pipeline
model = RandomForestClassifier()
imputer = SimpleImputer(strategy='mean')
pipeline = Pipeline(steps=[('i' , imputer), ('m' , model)])
# define model evaluation
cv = RepeatedStratifiedKFold(n_splits=10, n_repeats=3, random_state=1)
# evaluate model
scores = cross_val_score(pipeline, X, y, scoring= 'accuracy' , cv=cv, n_jobs=-1)
print('Mean Accuracy: %.3f (%.3f)' % (mean(scores), std(scores)))

Pipeline được đánh giá sử dụng 3 lần lặp của 10-Fold Cross-validation. Độ chính xác trung bình đo được là khoảng 86.4%, một kết quả tương đối tốt.

Mean Accuracy: 0.864 (0.054)

Chú ý rằng kết quả thực hiện của bạn có thể không giống ở đây. Nên chạy thử vài lần để tính kết quá trung bình.

c, So sánh các loại Statistical Imputation khác nhau

Như đã nói ở trên, Statistical Imputation có thể sử dụng Mean, Median, Mode, Constant … để thay thế cho Missing Data. Câu hỏi đặt ra là làm sao biết được cái nào là tốt nhất đối với bài toán của chúng ta? Câu trả lời là chúng ta không thể khẳng định ngay được, mà phải thông qua phép thử-sai mới có được kết quả chính xác.

Code dưới đây thực hiện áp dụng 4 loại Statistical Imputation khác nhau cho bài toán ở mục b, sau đó so sách các độ chính xác trung bình của mỗi loại với nhau:

# compare statistical imputation strategies for the horse colic dataset
from numpy import mean
from numpy import std
from pandas import read_csv
from sklearn.ensemble import RandomForestClassifier
from sklearn.impute import SimpleImputer
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import RepeatedStratifiedKFold
from sklearn.pipeline import Pipeline
from matplotlib import pyplot

# load dataset
dataframe = read_csv('horse-colic.csv' , header=None, na_values= '?')
# split into input and output elements
data = dataframe.values
ix = [i for i in range(data.shape[1]) if i != 23]
X, y = data[:, ix], data[:, 23]
# evaluate each strategy on the dataset
results = list()
strategies = ['mean' , 'median' , 'most_frequent' , 'constant']
for s in strategies:
    # create the modeling pipeline
    pipeline = Pipeline(steps=[('i' , SimpleImputer(strategy=s)), ('m', RandomForestClassifier())])
    # evaluate the model
    cv = RepeatedStratifiedKFold(n_splits=10, n_repeats=3, random_state=1)
    scores = cross_val_score(pipeline, X, y, scoring= 'accuracy' , cv=cv, n_jobs=-1)
    # store results
    results.append(scores)
    print('>%s %.3f (%.3f)' % (s, mean(scores), std(scores)))
# plot model performance for comparison
pyplot.boxplot(results, labels=strategies, showmeans=True)
pyplot.show()
  • Kết quả so sánh độ chính xác trung bình và độ lệch chuẩn:
>mean 0.868 (0.055)
>median 0.870 (0.055)
>most_frequent 0.873 (0.056)
>constant 0.880 (0.048)

Khá ngạc nhiên là Statictical Imputation với hằng số 0 lại cho kết quả cao nhất, 88%.

  • Kết quả so sánh sự phân phối kết quả:

Quan sát đồ thị box-plot ta thấy Statistical Imputation với hằng số 0 có độ phân tán kết quả thấp nhất giữa các lần thực hiện của nó.

Từ 2 nhận xét trên, có thể kết luận rằng Statistical Imputation với hằng số 0 là chiến thuật phù hợp nhất đối với bài toán của chúng ta.

d, Sử dụng SimpleImputer Transform khi dự đoán dữ liệu mới

Sau khi đã biết được loại Statistical Imputation phù hợp nhất, chúng ta mong muốn sử dụng nó để thực hiện việc dự đoán trên mẫu dữ liệu mới. Điều này có thể đạt được bằng cách định nghĩa một Pipeline, fitting nó trên toàn bộ dữ liệu, sau đó gọi hàm predict() và truyền vào mẫu dữ liệu mới cần dự đoán nhãn.

# constant imputation strategy and prediction for the horse colic dataset
from numpy import nan
import joblib
from pandas import read_csv
from sklearn.ensemble import RandomForestClassifier
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
# load dataset
dataframe = read_csv('horse-colic.csv', header=None, na_values='?')
# split into input and output elements
data = dataframe.values
ix = [i for i in range(data.shape[1]) if i != 23]
X, y = data[:, ix], data[:, 23]
# create the modeling pipeline
pipeline = Pipeline(steps=[('i', SimpleImputer(strategy='constant')), ('m', RandomForestClassifier())])
# fit the model
pipeline.fit(X, y)

# save pipeline as model file
joblib.dump(pipeline, 'model.mod') 
# load model from file
model = joblib.load('model.mod')

# define new data
row = [2, 1, 530101, 38.50, 66, 28, 3, 3, nan, 2, 5, 4, 4, nan, nan, nan, 3, 5, 45.00, 8.40, nan, nan, 2, 11300, 00000, 00000, 2]
# make a prediction
yhat = model.predict([row])
# summarize prediction
print('Predicted Class: %d' % yhat[0])

Chạy code trên thu được kết quả:

Predicted Class: 2

Chú ý quan trọng là Missing Data trong mẫu dữ liệu mới phải được đánh dấu là nan thì model mới có thể hiểu được.

3. Kết luận

Hôm nay, chúng ta đã tìm hiểu về phương pháp Statistical Imputation trong việc giải quyết vấn đề Missing Data. Đây là một phương pháp đơn giản, dễ thực hiện, và tỏ ra hiệu quả cao trong một số trường hợp cụ thể.

Toàn bộ code của bài này, các bạn có thể tham khảo tại đây.

Trong bài tiếp theo, chúng ta sẽ tìm hiểu về phương pháp tiếp theo trong việc xử lý Missing Data, đó là kNN Imputation. Mời các bạn đón đọc.

4. Tham khảo

[1] Jason Brownlee, “Data Preparation for Machine Learning”, Book: https://machinelearningmastery.com/data-preparation-for-machine-learning/.