Mục đích

Bài viết này sẽ đề cập đến một số phương pháp cơ bản trong việc phân tích dữ liệu, cho các bạn bắt đầu học và nắm các khải niệm cơ bản (train, test, features, items) trong machine learning.

Trong bất kỳ dự án nào liên quan đến machine learning, việc đầu tiên cần phải làm là nghiên cứu dữ liệu. Nghiên cứu dữ liệu rất quan trọng để:

  1. Tìm hiểu qua dữ liệu trông như thế nào.
  2. Tìm hiểu xem dữ liệu có gì lạ không? Nếu như items có dữ liệu lạ thì đây là noise, nên loại bỏ để không gây ảnh hưởng đến thuật toán.
  3. Dữ liệu có đủ, và chất lượng tốt để giải quyết vấn đề không?
  4. Dữ liệu có những đặc thù gì?
  5. Dữ liệu cần phải xử lý, tạo thêm, loại bỏ ra sao?
  6. Cuối cùng, đối với dữ liệu này ta có thể sử dụng thuật toán gì?

Nếu như ta chỉ làm bước 1) và nhảy lên bước 6) ngay lập tức để giải quyết vấn đề nhanh nhất có thể, bạn sẽ mất nhiều thời gian hơn để thử và tối ưu hóa các thuật toán (nhiều lúc vô hiệu quả). Từ việc nghiên cứu dữ liệu một cách chi tiết, ta sẽ có định hướng tốt về những cải thiện cần thiết, cũng như thuật toán có thể sử dụng. Sau đây, tôi sẽ đề cập đến một số phương pháp cơ bản qua tập dữ liệu Titanic trên Kaggle.

Vấn đề

Titanic là một con tàu nổi tiếng với mục đích vượt Đại Tây Dương, từ Anh(Southmampton) sang Mỹ (New York). Vào 15/04/1912, trong lộ trình đầu, Titanic đã đâm vào một tảng băng và chìm. 1502 trên 2224 người đã thiệt mạng. Tuy may mắn là một yếu tố, một số nhóm người như phụ nữ, trẻ con, giai cấp có nhiều khả năng lên thuyền cứu hộ và sống sót hơn.

Nhiệm vụ của các bạn là, từ dữ liệu tìm hiểu xem những người nào có khả năng sống cao hơn.

Tuy bài viết này sẽ không đề cập tới, mục đích cuối cùng là áp dụng một thuật toán để đoán trên một tập hành khách chưa được biết đến, họ có sống sót hay không.

Cài đặt môi trường

  1. Tạo folder bạn làm việc
  2. Install python (nếu chưa làm: https://www.python.org/downloads/)
  3. Install các package dưới đây trên command line/terminal:
pip install jupyter-notebook 
pip install scikit-learn
pip install matplotlib
pip install seaborn
pip install pandas

Để bắt đầu lập trình bằng Jupyter Notebook, trên command line/terminal:

jupyter notebook

Lấy dữ liệu

Dữ liệu các bạn có thể download từ đây vào folder bạn làm việc:
https://www.kaggle.com/c/3136/download-all

Khi unzip file này, các bạn sẽ thấy các file train và test đã được tách sẵn:

train.csv
test.csv

Bình thường, khi có dữ liệu mới thì tôi sẽ cắt 10% dữ liệu để test trước. May thay, Kaggle đã cắt sẵn train và test rồi. Khi tìm hiểu dữ liệu ta sẽ chỉ cần sử dụng train.csv.

Thông tin cơ bản

Lấy dữ liệu từ file csv vào dataframe để lưu dữ liệu:

import pandas as pd 
train_data = pd.read_csv("./train.csv") 

Lấy 10 items đầu và xem qua dữ liệu:

train_data.head(10)

Bạn sẽ thấy kết quả như sau:
uc?id=1J_-ki1c3igMIxX8tfTALDAWsE03XenNJ&export=download
Hình 1

Tóm tắt qua các features:

Tên Feature Định Nghĩa Key
PassengerId Id của hành khách
Survived sống sót 0 = Không, 1 = Có
Pclass loại thẻ 1 = hạng cao, 2 = hạng trung bình, 3 = hạng thấp
Sex giới tính
Age tuổi theo năm
SibSp số anh em, hoặc vợ chồng trên thuyền
Parch số phụ huynh, con trên thuyền
Ticket Số thẻ
Fare tiền vé
Cabin số cabin
Embarked bến khởi hành C = Cherbourg, Q = Queenstown, S = Southampton

Thông tin cơ bản được dịch từ Tiếng Anh, bạn có thể xem ở đây:
https://www.kaggle.com/c/titanic/data

Ngay lập tức, một vấn đề có thể trong dữ liệu có thể xử lý là kết hợp Parch và SibSp cùng với nhau vì nó mang cùng ý nghĩa là người nhà. Tạm thời, tôi để riêng vì có thể người làm dữ liệu này chia như vậy vì có mục đích gì đó.

Đầu tiên ta có thể tìm hiểu trong dữ liệu train có tổng cộng bao nhiêu items và bao nhiêu features:

train_data.shape

Kết quả là:
(891, 12)
Trong dữ liệu đang có 891 items, và 12 features.

Dựa vào Hình 1, bạn có thể thấy có một số items không có features Age, và Cabin. Bạn có thể tìm hiểu kỹ hơn, feature nào, thiếu bao nhiêu items:

train_data.info()

Kết quả là:

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 891 entries, 0 to 890
Data columns (total 12 columns):
PassengerId    891 non-null int64
Survived       891 non-null int64
Pclass         891 non-null int64
Name           891 non-null object
Sex            891 non-null object
Age            714 non-null float64
SibSp          891 non-null int64
Parch          891 non-null int64
Ticket         891 non-null object
Fare           891 non-null float64
Cabin          204 non-null object
Embarked       889 non-null object
dtypes: float64(2), int64(5), object(5)
memory usage: 83.6+ KB

Hình 2

Từ Hình 2, bạn có thể thấy có items thiếu features: Age, Cabin, Embarked.
Trong đó feature Cabin có rất ít dữ liệu, bạn có thể cân nhắc loại bỏ feature này.

Một số thông tin cơ bản khác trong features có thể được xem bằng lệnh:

explore_data.describe().round(3)

uc?id=1qmzgwLAYcMIcbU5WbBQn_ej4rwgRENZ4&export=download
Hình 3
Nó sẽ cho biết thông tin về:

  • số items
  • trung bình giá trị của từng feature
  • độ lệch chuẩn: kiểm tra các giá trị trong feature có khác nhau bình thường là bao nhiêu. Ví dụ: tuổi và giá vé của hành khách có độ khác nhau cao.
  • giá trị thấp nhất
  • giá trị của feature, từ thấp đến cao tại 25%, 50%, 75%: Có thể biết qua về dạng phân bố của các giá trị trong feature này. Ví dụ: 75% hành khách có số vợ chồng hoặc anh em (SibSp) <= 1
  • giá trị cao nhất

Histogram và Bar Chart

Bạn có thể vẽ histogram cho những features có biến liên tục, để xem qua dữ liệu được phân bố như thế nào:

explore_data.hist(bins=20, figsize=(15, 10), layout=(-1, 2))
plt.plot()

uc?id=1iKnu2oy5l8tBwgxLmzUGv3vd_qlY6LPG&export=download
Hình 3

Một số quan sát có thể thấy được từ Hình 3 là:

  • Age theo dạng Normal Distribution, Fare theo dạng Skewed Distribution
  • Trong Age: Phần lớn hành khách có độ tuổi từ 18 -> 35
  • Trong Fare: Với giá vé tăng, số khách hàng giảm
  • Trong Parch: Phần lớn hành khách không có bố mẹ và con cái đi cùng
  • Trong Pclass: Nhiều hành khách thuộc giai cấp cao
  • Trong SibSp: Phần lớn hành khách không có anh chị em và vợ chồng đi cùng
  • Trong Survived: Số lượng người sống sót và thiệt mạng có sự chênh lệch. Tuy chênh lệch không quá nhiều, có khả năng bạn sẽ phải sử dụng một số biện pháp để ngăn ngừa các vấn đề về class imbalance trong machine learning.

Bây giờ, bạn có thể vẽ histograms cho từng features, để kiểm tra riêng những khách hàng sống sót và không sống sót có gì khác nhau:

not_survived = explore_data.loc[explore_data["Survived"]==0.0]
survived = explore_data.loc[explore_data["Survived"]==1.0]
for column in ["Age", "Parch", "SibSp", "Fare"]:
    plt.figure(figsize=(15, 10))
    not_survived_data = not_survived[column]
    survived_data = survived[column]
    plt.hist([not_survived_data, survived_data], bins=20, color=["r", "g"], label=["Not survived", "Survived"])
    plt.legend(loc="upper left")
    plt.title(column)
    plt.show()

Biểu đồ vẽ riêng labels (sống sót, không sống sót) có hai lợi ích:

  • Về tổng quát, bạn có thể xem phân bố features giữa các labels có khác nhau không? Nếu khác nhau thì feature này đáng để sử dụng.
  • Với những giá trị nào của features, thì label nào có khả năng xuất hiện cao hơn. Cho ví dụ này bạn sẽ phải xem từng giá trị một giữa hai label và so sánh.

uc?id=1JHSwaPxpCc8hPofRpCinBC4O6JGXtm3o&export=download
Hình 4

Từ Hình 4, trẻ con dưới 10 tuổi và người khoảng 50 -55 tuổi có khả năng sống sót cao.

uc?id=1ACPGTPmcW0sOszttXoSj_U8UTb6xrdBf&export=download
Hình 5

Từ Hình 5, những người có phụ huynh hoặc trẻ có từ 1 dến 3 người có khả năng sống sót cao

uc?id=1khlXBJ4sdIamSK89M5s3PIuncOsAUbDj&export=download
Hình 6

Từ Hình 6, những người có số phụ huynh + trẻ có từ 1 dến 3 người có khả năng sống sót cao

Về tổng quát phân bố features giữa người sống sót và thiệt mạng không khác nhau nhiều. Các features có biến liên tục đều có thể sử dụng được để đoán người sống sót.

uc?id=1SXodycFsAO8uwtOp90twSJc7AnY3Jbdu&export=download
Hình 7
Những người có số anh em + vợ chồng 1 và 2 có khả năng sống sót cao

Tương tự như trên, bạn có thể vẽ barchart cho những features có biến phân loại, để kiểm tra những khách hàng sống sót và không sống sót có gì khác nhau :

import seaborn as sns
for feature in ["Sex", "Pclass", "Embarked"]:
    feature_data = train_data.groupby(["Survived", feature])["PassengerId"].count().reset_index(name="Count")
    plt.figure(figsize=(15, 10))
    sns.barplot(x=feature, y="Count", hue="Survived", data=feature_data)
    plt.title(feature)
    plt.show()

uc?id=1iuREkV0gCaSCUG4NebBZWUXIgv2fh5ld&export=download
Hình 8
Từ Từ Hình 8, ta thấy phụ nữ có khả năng sống sót cao hơn đàn ông.

uc?id=1cTlf2jjiNrqK4a04u1MR8kdlZvOCkvT0&export=download
Hình 9
Người có giai cấp cao (Pclass thấp dần) có khả năng sống sót cao dần.

uc?id=1laDVgpszx70QLY8LdVYHC7HSp4kfsdaL&export=download
Hình 10
Người xuất phát từ bến Cherbourg có khả năng sống sót cao.

Các features có biến phân loại đều có thể sử dụng được để đoán người sống sót.

Correlation Diagram

Để kiểm tra mỗi quan hệ giữa các features, giữa feature và label, bạn có thể vẽ correlation diagram. Correlation diagram sẽ biểu diễn mối tương quan giữa từng cặp feature.

corr = explore_data.corr()
corr.style.background_gradient(cmap="binary", low=0, high=0.2).set_precision(2)

uc?id=18aYXpLGXS2JzRdqLMwbhXsa-Ap993cnd&export=download
Hình 11

Trên Hình 11 nếu bạn chú ý đến các ô trắng, xám đậm, và bỏ qua các ô đen (toàn 1), xám nhạt (gần bằng 0):

  1. Theo cặp features
  • PClass và Fare có tỉ lệ nghịch khá cao. Nhiều khả năng vì giai cấp càng cao (PClass càng thấp) thì trả tiền vé càng cao.
  • SibSp (số anh em + vợ chồng) và Parch (số phụ huynh và con cái) có tỉ lệ thuận khá cao.
  1. Giữa label (Survived) và các feature
  • Giai cấp của hành khách (PClass) và khả năng sống sót có tỷ lệ nghịch. Vì PClass thấp thì giai cấp cao, những người giai cấp cao có khả năng lên thuyền cứu hộ hơn
  • Giá vé và khả năng sống sót có tỷ lệ thuận.

Mối quan hệ giữa các features có vẻ hợp lý, và không có gì bất thường.

Scatter Diagram

Để kiểm tra hành khách sống sót hay thiệt mạng có thể phân tách rõ hơn theo kết hợp giữa features, bản có thể vẽ scatter diagram:

from pandas.plotting import scatter_matrix
color_map = {
    0: "red", # Not survived
    1: "blue" # Survived
}
colors = explore_data["Survived"].map(lambda x: color_map.get(x))
scatter_matrix(explore_data, figsize=(50, 50), alpha=0.6, color=colors, s=20*4)
plt.legend()
plt.savefig("scatter_matrix.png")

uc?id=19o3Ndz4OBrcFjedLYNHYTqwf8TpICwnX&export=download
Hình 11: Not survived là màu đỏ, Survived là màu xanh nước biển. Hình hơi bé nên các bạn có thể download về

Cách xem scatter diagram là tìm những hình có phân tách rõ ràng nhất có thể giữa cặp features. Scatter diagram sẽ có các hình hướng chéo, biểu diễn histogram có thể xem mật độ số items cho từng giá trị. Từ đường chéo, bạn tách được 2 phần có hình tương đương nhau: dưới trái, phải trên. Bạn chỉ cần nhìn phần bên dưới trái, sang phải.

Sử dụng scatter diagram cho ví dụ này không thật sự tốt lắm, vì nhiều features có biến phân loại, nên cho cùng khoảng giá trị, sẽ có nhiều items với labels khác nhau.

Tuy nhiên nếu bạn xem kỹ Hình 11, có thể thấy một số điểm đặc biệt:

  • Cho cặp (PClass, Fare): Những người sống sót có PClass=1 và Fare cao.
  • Cho cặp (PClass, Parch): Những người sống sót cao có PClass=1, và Parch từ 0 -> 2 người. Ngưỡi người sống sót cao có PClass=2 có Parch từ 1 -> 3 người. Pclass=3 và Parch=1 có khả năng sống sót cao, nhưng khách hàng với các số Parch khác hầu như không sống sót.
    ...
  • Cho cặp (Age, Fare): Người từ 15 đến 60 tuổi, nếu tiền vé càng cao thì có khả năng sống sót cao. Đối với hành khách ngoài độ tuổi này thì tiền vé không quan trọng.
    ...
  • Cho cặp (SibSp, Parch): với những người có Sibsp 0-2 và Parch=1 thì có khả năng sống sót cao

Những cặp kết hợp features nêu trên, có thể sẽ rất quan trọng trong việc đoán người sống sót.

TSNE

Để sử dụng TSNE, việc đầu tiên là phải xử lý NaN ở trong dataframe. Bạn có thể loại bỏ feature Cabin vì nó thiếu nhiều dữ liệu. Sau đó, bạn có thể thay các giá trị NaN trong Age và Embarked, bằng giá trị có số lượng nhiều nhất.

processed_explore_data = pd.get_dummies(explore_data, columns=["Embarked", "Sex", "Pclass"], drop_first=True)
processed_explore_data.drop(["Cabin", "Name", "Ticket"], axis=1, inplace=True)
processed_explore_data = processed_explore_data.apply(lambda x:x.fillna(x.value_counts().index[0]))
processed_explore_data.head(10)

Sau đó bạn cần phải đồi những features có biến liên tục về cùng một scale.

from sklearn.preprocessing import StandardScaler
# Before put data in TSNE, we must scale the columns
continuous_columns = ["Age", "Fare", "SibSp", "Parch"]
scaled_continous_columns = ["scaled_" + continous_column for continous_column in continuous_columns]
std_scaler = StandardScaler()
scaled_explore_data = processed_explore_data.copy()
scaled_column_data = std_scaler.fit_transform(processed_explore_data[continuous_columns]).transpose()
for scaled_continous_column, continuous_column, scaled_column in zip(scaled_continous_columns, continuous_columns, scaled_column_data):
    scaled_explore_data[scaled_continous_column] = scaled_column
scaled_explore_data.drop(continuous_columns, axis=1, inplace=True)

Chạy TSNE và lưu kết quả vào bảng có lưu trữ thông tin người sống sót hay không.

from sklearn.manifold import TSNE
tsne = TSNE(n_components=2, perplexity=30)
tsne_vectors = tsne.fit_transform(scaled_explore_data[visual_columns])
processed_explore_data["tsne_x"] = tsne_vectors[:, 0]
processed_explore_data["tsne_y"] = tsne_vectors[:, 1]

Cuối cùng bạn có thể vẽ kết quả từ TSNE bằng scatter diagram. Khi vẽ TSNE bạn cũng nên ký hiệu label trên hình để kiểm tra xem label được phân bố như thế nào.

survived_df = visual_data.loc[visual_data["Survived"]  == 1]
not_survived_df = visual_data.loc[visual_data["Survived"]  == 0]
df_list = [survived_df, not_survived_df]
df_labels = ["Survived", "Not Survived"]
df_colors = ["Blue", "Red"]
plt.figure(figsize=(20, 15))
for df, label, color in zip(df_list, df_labels, df_colors):    
    plt.scatter(df["tsne_x"], df["tsne_y"], label=label, color=color, alpha=0.6)
plt.legend()
plt.show()

uc?id=162L2koyFb2oaYBn5CqMuxpHq0F9CjSna&export=download
Hình 13

Trên Hình 13, có thể thấy được khi sử dụng tất cả các features,
Không có cluster nào phân tách label một cách rõ ràng. Có nhiều phần trùng lặp cho người sống sót và thiệt mạng. Có khả năng số lượng features hiện giờ chưa đủ để phân tách chính xác hoàn toàn người có và không sống sót.

Tuy nhiên, ta vẫn có thể đoán phần lớn người có khả năng sống sót. Trên hình 13 những phần có nhiều người sống sót đã được khoanh tròn.

Bạn cũng có thể vẽ TSNE trên 3 chiều, để kiểm tra các điểm cho label khác nhau vẫn là thật sự trùng, hay nó tách nhau xa nếu có thêm một chiều để biểu diễn nữa.

Tổng kết

Hi vọng các bạn có thể thấy để phân tích dữ liệu có nhiều phương pháp khác nhau. Mỗi một phương pháp sẽ cho bạn biết thêm thông tin về dữ liệu bạn đang làm việc cùng. Nhìn chung các phương pháp trên có thể được áp dụng cho phần lớn các vấn đề. Có rất nhiều phương pháp hay khác các bạn có thể nghiên cứu trên mạng.

Code trên được tổng hợp ở đây:
https://gitlab.com/longlm/blog_titanic/tree/blog_titanic_data_explore

Nếu có thời gian, các bạn có thể tự làm quen với các các phương pháp này trên một bộ dữ liệu khác:
https://www.kaggle.com/mlg-ulb/creditcardfraud
Cho bộ dữ liệu CreditFraud này, các bạn không cần phải tách train và test. Nhưng nếu có các bạn muốn tách train và test (thói quen tốt) nên sử dụng Stratified Sampling. Stratified Sampling có nghĩa là, nếu bạn có 10% item là fraud trong train data, thì cũng phải có 10% item là fraud trong test data. Có rất nhiều câu trả lời (bằng tiếng anh) trong kernel về phân tích dữ liệu.