<jupyter_start><jupyter_text>ĐỒ ÁN MÔN HỌC CUỐI KÌ __TÊN MÔN HỌC:__ NHẬP MÔN KHOA HỌC DỮ LIỆU__ĐỀ TÀI:__ ÁP DỤNG MÔ HÌNH HỌC MÁY ĐỂ DỰ ĐOÁN DOANH SỐ TRÊN SÀN THƯƠNG MẠI ĐIỆN TỬ SHOPEE__GIẢNG VIÊN:__ TRẦN TRUNG KIÊN __THỨ TỰ NHÓM:__ 15 __THÀNH VIÊN:__- 18120184 - <NAME>- 18120189 - Trần Đăng Khoa__PHÂN CÔNG:__Công việc | Thực hiện | Mức độ hoàn thành------------ | ------------- | ------------Thu thập dữ liệu | Khang | 100%Khám phá dữ liệu | Khang, Khoa | 100%Tiền xử lý dữ liệu | Khang, Khoa | 100%Mô hình hóa dữ liệu | Khang, Khoa | 100%  Lời Cảm ƠnNhóm xin gửi lời cảm ơn chân thành đến: Thầy Trần Trung Kiên đã dạy và giúp đỡ chúng em hoàn thành đồ án này. Bạn Lê Hoàng Phương Nhi, bạn Lê Thị Như Quỳnh đã dành thời gian góp ý rất chi tiết cho đồ án.  Mục lục- [Giới thiệu đồ án](I.-Giới-thiệu-đồ-án)   - [Dữ liệu](1.-Dữ-liệu)   - [Câu hỏi](2.-Câu-hỏi)- [Thu thập dữ liệu](II.-Thu-thập-dữ-liệu)  - [Lấy dữ liệu từ Shopee bằng API](1.-Lấy-dữ-liệu-từ-Shopee-bằng-API)  - [Gộp dữ liệu](2.-Gộp-dữ-liệu)- [Khám phá dữ liệu, tiền xử lý đơn giản và tách tập](III.-Khám-phá-dữ-liệu,-tiền-xử-lý-đơn-giản-và-tách-tập)  - [Kiểm tra output hợp lệ](1.-Kiểm-tra-output-hợp-lệ)  - [Ý nghĩa của mỗi dòng](2.-Ý-nghĩa-của-mỗi-dòng)  - [Kiểm tra các giá trị trùng](3.-Kiểm-tra-các-giá-trị-trùng)     - [Kiểm tra các giá trị không hợp lệ](4.-Kiểm-tra-các-giá-trị-không-hợp-lệ)  - [Xử lý dữ liệu bị lệch](5.-Xử-lý-dữ-liệu-bị-lệch)  - [Tách các tập](6.-Tách-các-tập) - [Khám phá dữ liệu (tập huấn luyện)](IV.-Khám-phá-dữ-liệu-(tập-huấn-luyện))  - [Kiểm tra kiểu dữ liệu của input có phù hợp](1.-Kiểm-tra-kiểu-dữ-liệu-của-input-có-phù-hợp)  - [Phân bố giá trị của cột có kiểu dữ liệu dạng số](2.-Phân-bố-giá-trị-của-cột-có-kiểu-dữ-liệu-dạng-số)  - [Phân bố giá trị của cột có kiểu dữ liệu không phải dạng số](3.-Phân-bố-giá-trị-của-cột-có-kiểu-dữ-liệu-không-phải-dạng-số)- [Tiền xử lý (tập huấn luyện)](V.-Tiền-xử-lý-(tập-huấn-luyện))  - [Bỏ cột](1.-Bỏ-cột)  - [Sửa cột](2.-Sửa-cột)  - [Xử lý giá trị thiếu](3.-Xử-lý-giá-trị-thiếu)  - [Chuẩn hóa](4.-Chuẩn-hóa)  - [Tạo pipeline](5.-Tạo-pipeline)- [Mô hình hóa dữ liệu](VI.-Mô-hình-hóa-dữ-liệu)  - [Tìm mô hình tốt nhất](1.-Tìm-mô-hình-tốt-nhất)  - [Đánh giá mô hình tìm được](2.-Đánh-giá-mô-hình-tìm-được)- [Nhìn lại quá trình làm đồ án](VII.-Nhìn-lại-quá-trình-làm-đồ-án)  - [Khó khăn](1.-Khó-khăn)  - [Những thứ học được](2.-Những-thứ-học-được)  - [Những điều cần bổ sung nếu có thêm thời gian](3.-Những-điều-cần-bổ-sung-nếu-có-thêm-thời-gian)- [Tài liệu tham khảo](VIII.-Tài-liệu-tham-khảo)  I. Giới thiệu đồ ánMục đích chính của đồ án là xây dựng mô hình học máy để dự đoán doanh số bán hàng trên Shopee 1. Dữ liệuCác món hàng _Thời trang nam_ trên ShopeeInput là các thuộc tính của món hàng mà được biểu hiện ra trên giao diện của shopee.vn cho người dùng thấy như tên sản phẩm, giá thành,... và các thuộc tính ẩn bên dưới như thời gian sản phẩm được đăng, giá cao nhất,...Output là số lượng hàng đã bán được trong thời gian món hàng được đăng lên Shopee cho đến lúc được lấy dữ liệu về máyDữ liệu đúng vì được request từ API của Shopee, có nghĩa là Shopee cũng đang sử dụng dữ liệu này để hiển thị cho người tiêu dùng xem 2. Câu hỏi*Output - doanh số -* được tính từ *input - các thông tin của sản phẩm -* theo công thức nào?Cảm hứng từ thực tế các trang thương mại điện tử ngày càng phát triển và nhu cầu trao đổi, mua bán, kinh doanh trên mạng cũng ngày càng tăng lênVì thế, trả lời cho câu hỏi này giúp ta có những chiến lược phù hợp để nâng cao doanh số sản phẩm  Import  [`reviewModel`](Thử-nghiệm,-class-đánh-giá-các-model-(nếu-có-nhiều-thời-gian-hơn-em-sẽ-cải-thiện-class-này))<jupyter_code>%run ReviewModel.ipynb
import time
import re
import pandas as pd
import numpy as np
%matplotlib inline
import matplotlib.pyplot as plt

from sklearn.model_selection import train_test_split
from sklearn.model_selection import GridSearchCV
from sklearn.compose import ColumnTransformer
from sklearn.compose import make_column_transformer
from sklearn.preprocessing import OneHotEncoder
from sklearn.preprocessing import StandardScaler
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline, make_pipeline

from sklearn import set_config
set_config(display='diagram') # Để trực quan hóa pipeline

from sklearn.neural_network import MLPRegressor
from sklearn.linear_model import LinearRegression, SGDRegressor
from sklearn.utils import resample

#Set Option
pd.set_option('max_colwidth', 10000)
pd.set_option('display.max_rows', 500)
pd.set_option('display.max_columns', 500)
pd.set_option('display.width', 1000)<jupyter_output><empty_output><jupyter_text>II. Thu thập dữ liệu  1. Lấy dữ liệu từ Shopee bằng API Toàn bộ phần thu thập dữ liệu được nhóm đặt trong thư mục _Data Crawler_ và quá trình làm việc trong branch [_develope-Al27_](https://github.com/cstotodile/ds_final/tree/develope-Al27)Trong quá trình thu thập dữ liệu, nhóm đã tinh giản dữ liệu bằng cách giảm bớt chiều dữ liệu dựa trên kinh nghiệm của người mua hàng: Giảm bớt những thuộc tính mà nhóm nghĩ là không ảnh hưởng đến quyết định mua hàng của khách hàng.  2. Gộp dữ liệu (chỉ chạy khi có những file dữ liệu rời rạc) Do dữ liệu thu thập được nằm ở những file riêng lẻ, ta tiến hành gộp lại thành một tập dữ liệu duy nhất để khám pháPhần tích hợp dữ liệu do ta lấy dữ liệu từ cùng một trang, nên có sự nhất quán, đồng nhất về ngữ nghĩa, không có gì cần xử lý.<jupyter_code># dataset = ['AoNganTayKhongCo', 'AoSoMi', 'BaloTuiVi', 'GiayDep', 'PhuKien', 'Quan', 'AoKhoacAoVest', 'AoNi', 'AoNganTayCoCo', 'TrangSucNam']
# frames = [pd.read_csv(f'Data/ShopeeItem-ThoiTrangNam-{x}.tsv', sep='\t') for x in dataset]
# items_df = pd.concat(frames, ignore_index=True, sort=False)<jupyter_output><empty_output><jupyter_text>Lưu file để nộp bài<jupyter_code># items_df.to_csv('Data/ShopeeItem-ThoiTrangNam-Full.tsv', sep='\t', index=False)<jupyter_output><empty_output><jupyter_text>III. Khám phá dữ liệu, tiền xử lý đơn giản và tách tập Đầu tiên ta sẽ đọc file dữ liệu vào DataFrame sau đó sẽ khám phá dữ liệu<jupyter_code>items_df = pd.read_csv('Data/ShopeeItem-ThoiTrangNam-Full.tsv', sep='\t')
items_df.shape
items_df.head()<jupyter_output><empty_output><jupyter_text>Xử lý khác biệt ngữ nghĩa do parse data từ JSONDo dữ liệu trả về từ API của shopee, những dữ liệu thiếu có dạng string: 'None' nên pandas sẽ không đếm được lượng dữ liệu không có giá trị này. Ta tiến hành thay 'None' thành np.nan để có thể đếm lượng dữ liệu thiếu này chính xác.<jupyter_code>items_df = items_df.replace('None', np.nan)<jupyter_output><empty_output><jupyter_text>1. Kiểm tra output hợp lệTa kiểm tra xem dữ liệu thu được có đủ điều kiện để mô hình hóa hay không? Vì đây là bài toán hồi qui nên cột output bắt buộc phải có dạng số; nếu hiện chưa có dạng số (ví dụ, số nhưng được lưu dưới dạng chuỗi) thì ta cần chuyển sang dạng số rồi mới tách các tập<jupyter_code># Cột output hiện có kiểu dữ liệu gì?
items_df['historical_sold'].dtype
# Cột output có giá trị thiếu không?
items_df['historical_sold'].isna().sum()
# Xóa các mẫu có giá trị output thiếu
items_df.dropna(subset=['historical_sold'], inplace=True)
# Xem phân bố các giá trị của output
items_df['historical_sold'].describe()<jupyter_output><empty_output><jupyter_text>Ta có thể thấy giá trị 0 chiếm rất nhiều trong output khiến cho phân bố bị lệch trái rõ rệt, hãy xem biểu đồ phân bố dưới đây<jupyter_code>items_df['historical_sold'].plot.hist(bins=range(0, 300,10), edgecolor='black')
# Tính thử độ lệch của output
items_df['historical_sold'].skew()<jupyter_output><empty_output><jupyter_text>Ta sẽ quay lại xử lý vấn đề này sau  2. Ý nghĩa của mỗi dòngQuan sát sơ bộ dữ liệu và qua quá trình crawler data, ta thấy mỗi dòng chứa thông tin của một món hàng, và không có vấn đề các dòng có ý nghĩa khác nhau  3. Kiểm tra các giá trị trùngTa xem thử có bao nhiêu mẫu bị trùng giá trị<jupyter_code>num_duplicated_rows = items_df.duplicated().sum()
num_duplicated_rows<jupyter_output><empty_output><jupyter_text>Xóa các mẫu có giá trị trùng bằng phương thức drop_duplicates của pandas, mặc định giữ lại dòng có giá trị lặp đầu tiên<jupyter_code>items_df = items_df.drop_duplicates(ignore_index = True)<jupyter_output><empty_output><jupyter_text>4. Kiểm tra các giá trị không hợp lệ<jupyter_code>items_df.describe()<jupyter_output><empty_output><jupyter_text>Nhìn vào bảng trên, ta thấy có một số bất thường. Thật ra giá trị của những mẫu này đều đang hoạt động bình thường trên giao diện shopee nên khả năng lỗi là rất thấp. Ta vẫn xem xét các trường hợp như sau:1. Cột price_min_before_discount và cột price_max_before_discount có giá trị âm2. Cột sell_time có giá trị âm  2. So sánh price_min > price_max, price_min_before_discount > price_max_before_discount3. So sánh price < price_before_discount, price_min < price_min_before_discount, price_max < price_max_before_discount<jupyter_code># Xử lý trường hợp 1.

invalid_price_negative = (items_df.price_min_before_discount < 0) | (items_df.price_max_before_discount < 0)
invalid_price_negative_vals = invalid_price_negative.sum()
invalid_price_negative_vals/len(items_df)<jupyter_output><empty_output><jupyter_text>Ta thấy có đến xấp xỉ 39.07% các giá trị `price_min_before_discount` và `price_min_before_discount` bị âm.Do shopee vẫn hiển thị bình thường nên ta dự đoán có lẽ do tại những mẫu có giá trị âm này thì `discount` bị thiếu.<jupyter_code>print((items_df.discount.isnull()).sum()/len(items_df))
print((items_df.discount.isnull()).sum()/len(items_df) == invalid_price_negative_vals/len(items_df))<jupyter_output>0.39067733531819876
True<jupyter_text>Kiểm tra thấy hai giá trị là hoàn toàn giống nhau chứng tỏ dự đoán là đúng.Vậy code Shopee chịu lỗi này hoặc đây là quy ước data của shopee, khi lấy mẫu mới về từ Shopee trường hợp này vẫn sẽ là giá trị âm. Vậy nên những giá trị âm này ta xem như hợp lệ<jupyter_code># Xử lý trường hợp 2.
invalid_sell_time = (items_df['sell_time'] < 0)
invalid_sell_time.sum()<jupyter_output><empty_output><jupyter_text>Có khá nhiều mẫu có thuộc tính sell_time âm, ta xóa những mẫu này<jupyter_code>items_df = items_df[~invalid_sell_time]
# Xử lý trường hợp 3
invalid_price_min_max = (items_df.price_min_before_discount > items_df.price_max_before_discount) | (items_df.price_max < items_df.price_min)
invalid_price_min_max_vals = invalid_price_min_max.sum()
invalid_price_min_max_vals<jupyter_output><empty_output><jupyter_text>Có 1 mẫu bất hợp lệ trong trường hợp này, ta xóa nốt<jupyter_code>items_df = items_df[~invalid_price_min_max]
# Xử lý trường hợp 4
invalid_price_before_after = (items_df.price > items_df.price_before_discount) | (items_df.price_min > items_df.price_min_before_discount) | (items_df.price_max > items_df.price_max_before_discount)
invalid_price_before_after_vals = invalid_price_before_after.sum()
invalid_price_before_after_vals / len(items_df)<jupyter_output><empty_output><jupyter_text>Ta thấy phần trăm các giá trị lỗi giống như ở trường hợp 1 nên kiểm tra riêng lại cho điều kiện (items_df.price_min > items_df.price_min_before_discount) | (items_df.price_max > items_df.price_max_before_discount)<jupyter_code>print('(items_df.price_min > items_df.price_min_before_discount) | (items_df.price_max > items_df.price_max_before_discount):\n->', ((items_df.price_min > items_df.price_min_before_discount) | (items_df.price_max > items_df.price_max_before_discount)).sum()/len(items_df))
print('True if it equal to (items_df.discount == "None").sum()/len(items_df))\n->', ((items_df.price_min > items_df.price_min_before_discount) | (items_df.price_max > items_df.price_max_before_discount)).sum()/len(items_df) == (items_df.discount == 'None').sum()/len(items_df))<jupyter_output>(items_df.price_min > items_df.price_min_before_discount) | (items_df.price_max > items_df.price_max_before_discount):
-> 0.3993653235888066
True if it equal to (items_df.discount == "None").sum()/len(items_df))
-> False<jupyter_text>Vậy tương tự như trường hợp 1, những giá trị này là hợp lệ Kiểm tra lại số mẫu<jupyter_code>items_df.shape<jupyter_output><empty_output><jupyter_text>5. Xử lý dữ liệu bị lệch Ta sẽ viết một hàm để giảm độ lệch của dữ liệu, ý tưởng là giảm số lượng giá trị xuất hiện nhiều nhất bằng với số lượng của giá trị xuất hiện nhiều thứ 2 cho đến khi độ lệch đạt một mức nào đó, ta sẽ điều chỉnh tham số s là độ lệch của cột đó sau khi biến đổi bằng hàm logarithm tự nhiên với độ dời là 1 (hàm log(x + 1)) do dữ liệu có giá trị 0<jupyter_code>def reduce_skew(df, column, s=0.6):
    k = df.copy()
    while (np.log(k[column] + 1).skew() > s):
        vc = k[column].value_counts()
        mf = vc.index[0]
        if len(vc[vc < vc[0]]):
            sf_n = vc[vc < vc[0]].iloc[0]
        else:
            break
        mf_sample = k[k[column] == mf]
        not_mf_sample = k[k[column] != mf]
        undersampled = resample(mf_sample, replace = False,
                                    n_samples = sf_n, random_state = 0)
        k = pd.concat([undersampled, not_mf_sample])
    return k<jupyter_output><empty_output><jupyter_text>Gọi hàm reduce_skew với mỗi s tùy chọn, lưu ý rằng khi độ lệch càng nhỏ tức là phải bỏ đi nhiều mẫu, ta sẽ cố gắng cân bằng sao cho độ lệch và số mẫu ở mức chấp nhận được. Dữ liệu có độ lệch < 1 thì có thể coi là không lệch quá nhiều, vậy ta sẽ chọn s = 0.9<jupyter_code>x = reduce_skew(items_df, 'historical_sold', 0.9)
x.shape<jupyter_output><empty_output><jupyter_text>Số mẫu còn lại là 3900 từ hơn 10000 mẫu. Tiếp theo ta sẽ so sánh độ lệch của dữ liệu trước và sau khi giảm lệch<jupyter_code>print('Độ lệch trước:', items_df['historical_sold'].skew())
print('Độ lệch sau:', x['historical_sold'].skew())
# Vẽ biểu đồ phân phối giữa dữ liệu trước và sau khi giảm lệch
fig, (ax, ax2) = plt.subplots(ncols=2, sharey=True, figsize=(15,5))
items_df['historical_sold'].plot.hist(bins=range(0, 300, 10), edgecolor='black',ax=ax)
x['historical_sold'].plot.hist(bins=range(0, 300, 10), edgecolor='black', ax=ax2)<jupyter_output><empty_output><jupyter_text>Có thể thấy độ lệch được giảm đáng kể, ta hãy tiếp tục so sánh độ lệch của dữ liệu sau khi biến đổi<jupyter_code>transformed_before = np.log(items_df['historical_sold'] + 1)
transformed_after = np.log(x['historical_sold'] + 1)
print('Độ lệch trước:', transformed_before.skew())
print('Độ lệch sau:', transformed_after.skew())
fig, (ax, ax2) = plt.subplots(ncols=2, sharey=True, figsize=(15,5))
transformed_before.plot.hist(bins=range(0, 11, 1), edgecolor='black', ax=ax)
transformed_after.plot.hist(bins=range(0, 11, 1), edgecolor='black', ax=ax2)<jupyter_output><empty_output><jupyter_text>Ta thấy khi s = 0.9 thì phân phối có vẻ tốt, vậy ta quyết định biến đổi output bằng hàm log nói trên để dữ liệu phân bố đều hơn<jupyter_code>items_df = x
items_df['historical_sold'] = transformed_after
items_df['historical_sold'].describe()<jupyter_output><empty_output><jupyter_text>6. Tách các tậpVậy là dữ liệu đã sẵn sàng để tách và khám phá<jupyter_code># Tách X và y
y_sr = items_df["historical_sold"]
X_df = items_df.drop("historical_sold", axis=1)
# Tách train và test
rest_X_df, test_X_df, rest_y_sr, test_y_sr = train_test_split(X_df, y_sr, test_size=0.2, random_state=0)
# Tách train và validation
train_X_df, val_X_df, train_y_sr, val_y_sr = train_test_split(rest_X_df, rest_y_sr, test_size=0.2, random_state=0)
train_X_df.shape
train_y_sr.shape
val_X_df.shape
val_y_sr.shape
test_X_df.shape
test_y_sr.shape<jupyter_output><empty_output><jupyter_text>IV. Khám phá dữ liệu (tập huấn luyện)Quá trình làm việc của nhóm trong branch [Preprocessing](https://github.com/cstotodile/ds_final/tree/Preprocessing)  1. Kiểm tra kiểu dữ liệu của input có phù hợp<jupyter_code>train_X_df.dtypes<jupyter_output><empty_output><jupyter_text>Ta nhận thấy thuộc tính `discount` có kiểu dữ liệu không phù hợp, đáng lẽ phải ở dạng số. Ta hãy kiểm tra xem `discount` giữ những giá trị gì?<jupyter_code>train_X_df['discount'].unique()<jupyter_output><empty_output><jupyter_text>Nhận xét: `discount` gồm con số và theo sau là kí tự %, `discount` có giá trị nan  2. Phân bố giá trị của cột có kiểu dữ liệu dạng số- Tỉ lệ % (từ 0 đến 100) các giá trị thiếu - Giá trị min- Giá trị lower quartile (phân vị 25)- Giá trị median (phân vị 50)- Giá trị upper quartile (phân vị 75)- Giá trị maxKết quả được lưu vào DataFrame `num_col_info_df`, trong đó: - Tên của các cột là tên của các cột số trong `train_X_df`- Tên của các dòng là: "missing_ratio", "min", "lower_quartile", "median", "upper_quartile", "max"<jupyter_code>num_cols = ['item_public_time','item_be_got_time', 'sell_time', 'discount', 
            'options', 'rating_star' ,'five_star', 'four_star', 'three_star', 
            'two_star' ,'one_star' ,'price_min_before_discount', 'price_max', 'price_min', 
            'price_max_before_discount', 'price', 'price_before_discount', 
            'liked_count', 'view_count', 'cmt_count','images', 'video_info_list']
cat_cols = list(set(train_X_df.columns) - set(num_cols))
def missing_ratio(df):
    return (df.isna().mean() * 100).round(1)
def lower_quartile(df):
    return df.quantile(0.25)
def median(df):
    return df.quantile(0.5)
def upper_quartile(df):
    return df.quantile(0.75)
df = train_X_df[num_cols]
num_col_info_df = df.agg([missing_ratio, min, lower_quartile, median, upper_quartile, max]).round(1)
num_col_info_df<jupyter_output><empty_output><jupyter_text>Nhận xét: Tuy một số thuộc tính có phân bố bị lệch nhưng điều này không ảnh hưởng đến việc mô hình hóa, nhìn chung thì không có gì bất thường cả  3. Phân bố giá trị của cột có kiểu dữ liệu không phải dạng số- Tỉ lệ % (từ 0 đến 100) các giá trị thiếu - Số lượng các giá trị (các giá trị ở đây là các giá trị khác nhau và ta không xét giá trị thiếu)- Tỉ lệ % (từ 0 đến 100) của mỗi giá trị được sort theo tỉ lệ % giảm dần (ta không xét giá trị thiếu, tỉ lệ là tỉ lệ so với số lượng các giá trị không thiếu): dùng dictionary để lưu, key là giá trị, value là tỉ lệ %Kết quả được lưu vào DataFrame `cat_col_info_df`, trong đó: - Tên của các cột là tên của các cột không phải số trong `train_X_df`- Tên của các dòng là: "missing_ratio", "num_values", "value_ratios"   Trước hết kiểm tra có bao nhiêu giá trị `name`<jupyter_code>len(train_X_df['name'].unique())<jupyter_output><empty_output><jupyter_text>Do giá trị `name` rất nhiều ta sẽ không xét nó nữa và sẽ xóa trong phần tiền xử lý<jupyter_code>cat_cols = list(set(cat_cols) - set(['name']))
def missing_ratio(df):
    return (df.isna().mean() * 100).round(1)
def num_values(df):
    return df.nunique()
def value_ratios(c):
    return dict((c.value_counts(normalize=True) * 100).round(1))
df = train_X_df[cat_cols]
cat_col_info_df = df.agg([missing_ratio, num_values, value_ratios])
cat_col_info_df<jupyter_output><empty_output><jupyter_text>Nhận xét: - Thuộc tính `coin_earn_label`, `flash_sale` và `upcoming_flash_sale` thiếu 100% giá trị, ta sẽ xóa  ba thuộc tính này bằng câu lệnh sau:```pythonPandas.DataFrame.dropna(axis = 1, how = 'all')```- Thuộc tính `is_adult` có 100% giá trị False, ta xóa thuộc tính này- Thuộc tính `show_official_shop_label_in_title` thiếu 0.6%, có giá trị True False nhưng ở dạng chuỗi- Thuộc tính `shop_location` có 3 giá trị chiếm phần lớn mẫu là 'Hà Nội', 'TP. Hồ Chí Minh', 'Nước ngoài' với tỉ lệ chiếm là hơn 90% - Thuộc tính `shop_location` _thiếu_ 0.5%  - Ta có thể xóa các mẫu bị thiếu dữ liệu do thiếu ít, hoặc điền giá trị thiếu nhờ hỗ trợ của thư viện SciKit Learn nhưng ta sẽ thử xử lý ở thuộc tính này thay vì xóa hẳn.  - Lưu ý rằng các dữ liệu này vẫn hoạt động tốt trên shopee nên ta dùng lệnh sau để xem những mẫu nào bị _thiếu_ và đối chiếu với trên shopee.```pythonitems_df.loc[items_df['shop_location'].isnull()]```Sau khi đối chiếu ta được kết quả sau: Mẫu không thiếu:![shop_location_not_missing_value](images/shop_location_not_missing_value.jpg)Mẫu thiếu:![shop_location_missing_value](images/shop_location_missing_value.jpg)Tức là những mẫu có thuộc tính shop_location thiếu sẽ không hiển thị vị trí shop ở góc dưới.Vậy ta sẽ xử lý bằng cách thay các giá trị thiếu này thành chuỗi "None".<jupyter_code>items_df.loc[items_df['shop_location'].isnull()]<jupyter_output><empty_output><jupyter_text>V. Tiền xử lý (tập huấn luyện)  1. Bỏ cộtTa sẽ tiến hành bỏ những cột sau:- `name`: do quá nhiều giá trị, nếu mã hóa bằng OneHot hoặc sử dụng kỹ thuật Bag of Word thì số cột tăng lên rất nhiều- `flash_sale`, `upcoming_flash_sale`, `coin_earn_label`: 3 thuộc tính đều bị thiếu toàn bộ dữ liệu nên vô nghĩa- `is_adult`: chỉ có giá trị False nên cũng vô nghĩa  2. Sửa cộtTa tiến hành sửa những cột sau: 1. `shop_location`: Ta thấy có khá nhiều giá trị khác nhau trong tập huấn luyện, trong đó có các giá trị chỉ xuất hiện một hoặc một ít lần; nếu ta để nguyên thì khi chuyển sang dạng số bằng phương pháp one-hot sẽ có nhiều cột → dễ bị overfit. Ta sẽ xử lý vấn đề này như sau: chỉ lấy num_top_locations (ví dụ, 4) giá trị xuất hiện nhiều nhất, các giá trị còn lại sẽ được thay thế bằng giá trị "Others". Lúc sau, ta sẽ thí nghiệm để chọn ra giá trị num_top_locations2. `discount`: có thêm kí tự % đằng sau nên có kiểu chuỗi, vậy ta sẽ tách số ra đó và chuyển thành số. Ngoài ra, ta còn thấy `discount` có giá trị thiếu. Kiểm tra đối chiếu với các item trên Shopee, ta thấy thực tế những mẫu bị _thiếu_ dữ liệu ở thuộc tính `discount` là do không có giảm giá (`discount` = 0). Ta tiến hành sửa bằng cách thay giá trị nan tại cột này thành 03. `shopee_verified`, `show_official_shop_label_in_title`, `is_preferred_plus_seller`, `show_free_shipping`: những cột kiểu bool như này thì ta chuyển hết về 0 và 1 Class ColAdderDropper sẽ thực hiện các bước trên.<jupyter_code>class ColAdderDropper(BaseEstimator, TransformerMixin):
    def __init__(self, num_top_locations=3):
        self.num_top_locations = num_top_locations
    def fit(self, X_df, y=None):
        location_col = X_df['shop_location'].copy()
#         location_col = location_col.replace('None', np.nan)
        location_col = location_col.replace(np.nan, 'None')
        self.location_counts_ = location_col.value_counts()
        locations = list(self.location_counts_.index)
        self.top_locations_ = locations[:max(1, min(self.num_top_locations, len(locations)))]
        return self
    def transform(self, X_df, y=None):
        tf_df = X_df.copy()
        
        # Bước 1
        tf_df = tf_df.dropna(axis = 0, thresh = 15)
        tf_df = tf_df.dropna(axis = 1, how = 'all')
        tf_df = tf_df.drop(['name', 'is_adult'], axis=1)
        
        # Bước 2.1 Sửa cột location
        tf_df['shop_location'] = tf_df['shop_location'].replace(np.nan, 'None')
        locations = list(tf_df['shop_location'].value_counts().index)
        other_locations = list(set(locations) - set(self.top_locations_))
        tf_df['shop_location'] = tf_df['shop_location'].replace(other_locations, 'Others')
        
        # Bước 2.2 Sửa cột discount
        tf_df['discount'] = tf_df['discount'].replace(np.nan, 0)
        
        def apply_discount(discount):
            if type(discount) == int:
                return discount
            else:
                result = re.findall(r'\d+',discount)
                return result[0]
            pass

        tf_df['discount'] = tf_df.apply(lambda row: apply_discount(row['discount']), axis=1)
        
        def convert_col_dtype(col):
            if col.name in ['discount']:
                return pd.to_numeric(col, errors='coerce')
            return col

        tf_df = tf_df.apply(convert_col_dtype)
        
        # Bước 3.3 Sửa các cột boolean
        def apply_bool(FalseOrTrue):
            if FalseOrTrue == 'False' or FalseOrTrue == False:
                return 0
            if FalseOrTrue == 'True' or FalseOrTrue == True:
                return 1
            else:
                return FalseOrTrue
            pass

        tf_df['show_official_shop_label_in_title'] = tf_df.apply(lambda row: apply_bool(row['show_official_shop_label_in_title']), axis=1)
        tf_df['is_preferred_plus_seller'] = tf_df.apply(lambda row: apply_bool(row['is_preferred_plus_seller']), axis=1)
        tf_df['shopee_verified'] = tf_df.apply(lambda row: apply_bool(row['shopee_verified']), axis=1)
        tf_df['show_free_shipping'] = tf_df.apply(lambda row: apply_bool(row['show_free_shipping']), axis=1)
        return tf_df
        #raise NotImplementedError()
# Test
col_adderdropper = ColAdderDropper(num_top_locations=4)
fewer_cols_train_X_df = col_adderdropper.fit_transform(train_X_df)
fewer_cols_train_X_df.head()
# Kiểm tra các giá trị không phải số đã được xử lý đúng chưa
cat_cols = list(set(fewer_cols_train_X_df.columns) - set(num_cols))
df = fewer_cols_train_X_df[cat_cols]
cat_col_info_df = df.agg([missing_ratio, num_values, value_ratios])
cat_col_info_df<jupyter_output><empty_output><jupyter_text>3. Xử lý giá trị thiếu- Với các thuộc tính bị thiếu toàn bộ dữ liệu, các mẫu bị thiếu từ 15 thuộc tính ta đã xử lý ở class ColAdderDropper phía trên bằng 2 dòng lệnh sau:```pythontf_df = tf_df.dropna(axis = 0, thresh = 15)tf_df = tf_df.dropna(axis = 1, how = 'all')```- Với các cột dạng số, ta sẽ điền giá trị thiếu bằng giá trị mean của cột (gợi ý: dùng `SimpleImputer` trong Sklearn). Với *tất cả* các cột dạng số trong tập huấn luyện, ta đều cần tính mean, vì ta không biết được cột nào sẽ bị thiếu giá trị khi dự đoán với các véc-tơ input mới. - Với các cột không phải dạng số và không có thứ tự:    - Ta sẽ điền giá trị thiếu bằng giá trị mode (giá trị xuất hiện nhiều nhất) của cột (gợi ý: dùng `SimpleImputer` trong Sklearn). Với *tất cả* các cột không có dạng số và không có thứ tự, ta đều cần tính mode, vì ta không biết được cột nào sẽ bị thiếu giá trị khi dự đoán với các véc-tơ input mới.    - Sau đó, ta sẽ chuyển sang dạng số bằng phương pháp mã hóa one-hot (gợi ý: dùng `OneHotEncoder` trong Sklearn, để ý tham số `handle_unknown` vì khi dự đoán với các véc-tơ input mới ...).- Với cột không phải dạng số và có thứ tự:    - Ta sẽ điền giá trị thiếu bằng giá trị mode (giá trị xuất hiện nhiều nhất) của cột.    - Các giá trị của cột này đều là kiểu boolean và ta đã chuyển sang dạng 0, 1 trước đó nên không cần phải mã hóa nữa<jupyter_code>nume_cols = ['item_public_time','item_be_got_time', 'sell_time', 'discount', 
            'options', 'rating_star' ,'five_star', 'four_star', 'three_star', 
            'two_star' ,'one_star' ,'price_min_before_discount', 'price_max', 'price_min', 
            'price_max_before_discount', 'price', 'price_before_discount', 
            'liked_count', 'view_count', 'cmt_count','images', 'video_info_list']
unorder_cate_cols = ['category_id', 'shop_location']
order_cate_cols = ['is_preferred_plus_seller', 'shopee_verified', 'show_free_shipping', 'show_official_shop_label_in_title']

imp_mean = SimpleImputer(missing_values=np.nan, strategy='mean')
imp_mode = SimpleImputer(missing_values=np.nan, strategy='most_frequent')
enc = OneHotEncoder(handle_unknown='ignore')
unorder_cate_pipeline = make_pipeline(imp_mode, enc)
column_transformer = ColumnTransformer(transformers = [('numerical', imp_mean, nume_cols),
                                                      ('unorder_categorical', unorder_cate_pipeline, unorder_cate_cols),
                                                      ('order_categorical', imp_mode, order_cate_cols)], remainder='passthrough')<jupyter_output><empty_output><jupyter_text>4. Chuẩn hóaCuối cùng, khi tất cả các cột đã được điền giá trị thiếu và đã có dạng số, ta sẽ tiến hành chuẩn hóa bằng cách trừ đi mean và chia cho độ lệch chuẩn của cột để giúp cho các thuật toán cực tiểu hóa như Gradient Descent, LBFGS,... hội tụ nhanh hơn   5. Tạo pipeline Khi đã có những bước tiền xử lý cụ thể, ta sẽ tạo pipeline chứa chúng để cho quá trình tiền xử lý và mô hình hóa được dễ dàng hơn<jupyter_code>preprocess_pipeline = make_pipeline(ColAdderDropper(), column_transformer,  StandardScaler())
preprocessed_train_X = preprocess_pipeline.fit_transform(train_X_df)
preprocessed_val_X = preprocess_pipeline.transform(val_X_df)
preprocessed_train_X.shape
preprocessed_val_X.shape<jupyter_output><empty_output><jupyter_text>VI. Mô hình hóa dữ liệu  1. Tìm mô hình tốt nhấtDo output là kiểu số thực liên tục nên ta sẽ sử dụng mô hình hồi quy để dự đoán các input mới, ta sẽ sử dụng các model đã được học sau:- Hồi quy tuyến tính- Mạng neural nhân tạo Đầu tiên, đối với các mô hình hồi quy này thì ta sử dụng độ đo $R^2$ để đánh giá độ lỗi vì độ đo MSE không cho ta biết được cụ thể chất lượng của mô hình $R^2$ cho biết độ phù hợp của mô hình, người ta nghiên cứu được rằng, với $R^2$ > 0.5 thì một mô hình được đánh giá là phù hợp  Huấn luyện mô hình hồi quy tuyến tính<jupyter_code>linear_full_pipeline = make_pipeline(ColAdderDropper(), column_transformer, StandardScaler(), LinearRegression())
linear_full_pipeline.fit(train_X_df, train_y_sr);
# R^2 tập validation
linear_full_pipeline.score(val_X_df, val_y_sr)<jupyter_output><empty_output><jupyter_text>Score < 0.5, **không** thỏa tiêu chí đặt ra  Thử nghiệm, class đánh giá các model (nếu có nhiều thời gian hơn em sẽ cải thiện class này) Mục đích của class này: Giúp đánh giá các model tốt hơn, dễ dàng xem xét hơn**Hàm khởi tạo**```pythonreviewModel = ReviewModel(val_y_sr, linear_pred,baseline_preds = baseline_preds, DistanceType = 'r2') ```- Tham số 1: `val_y_sr` target chính xác để so sánh- Tham số 2: `linear_pred` mảng ba chiều có cấu trúc như sau:  - Ngoài cùng là các model thử nghiệm khác nhau [model1,model2,...,modeln]  - Trong mỗi model là kết quả trả về từ `predict` với các siêu tham số khác nhau: [predict1, predict2,..., predictn]  - Trong mỗi kết quả trả về từ `predict`chính là các output được dự đoán từ chương trình.- Tham số 3: baseline_preds, chỉ số baseline_preds cần truyền khi dùng mô hình $R^2$- Tham số 4: `DistanceType`, độ đo sử dụng, **hiện tại hỗ trợ các độ đo** Hausdoff, MSE và $R^2$: `Hausdoff`, `mse`, `r2`**Hàm tính toán:**```pythonreviewModel.calculate()```In ra màn hình kết quả bảng axb - với a: số cột: số siêu tham số thử cho model- b: số hàng - số model chạy thửMỗi một ô thể hiện độ đo giữa tập val_y_sr và tập predict của một model nhất định tại một siêu tham số nhất địnhTa có thể xem lại kết quả này bằng cách gọi: `reviewModel.distance`Kết quả trả về `binaryArray`,`bestIndexModel`- `binaryArray`: Mảng 2 chiều, lưu siêu tham số tốt nhất của mỗi model dưới dạng nhị phân, 1 = tốt nhất, còn lại = 0. (Mỗi model là một hàng)- `bestIndexModel`: Chỉ số của model tốt nhất trong các model, chỉ số ở đây là chỉ số đầu tiên trong `y_pred` cũng như `binaryArray`Ví dụ `bestIndexModel` = 0 thì: - model lưu tại `binaryArray[0]` là tốt nhất - với độ lỗi tốt nhất là `reviewModel.distance[bestIndexModel][binaryArray[bestIndexModel].index(max(binaryArray[bestIndexModel]))]`- alpha tốt nhất cho model tốt nhất: `alphas[binaryArray[bestIndexModel].index(max(binaryArray[bestIndexModel]))]`- alpha tốt nhất cho model thứ i: `alphas[binaryArray[i].index(max(binaryArray[i]))]`<jupyter_code>y_pred = linear_full_pipeline.predict(val_X_df)

y_pred = y_pred[np.newaxis,:]
y_pred = y_pred[np.newaxis,:]
y_pred.shape
baseline_preds = train_y_sr.mean()
reviewModel = ReviewModel(val_y_sr, y_pred,baseline_preds = baseline_preds, DistanceType = 'r2') 
binaryArray,bestIndexModel = reviewModel.calculate()
reviewModel.distance
binaryArray
bestIndexModel
reviewModel.distance[bestIndexModel][binaryArray[bestIndexModel].index(max(binaryArray[bestIndexModel]))]
#best alpha for best model:
# print(alphas[binaryArray[bestIndexModel].index(max(binaryArray[bestIndexModel]))])

# print('best alpha for model 1', alphas[binaryArray[0].index(max(binaryArray[0]))])
# print('best alpha for model 2', alphas[binaryArray[1].index(max(binaryArray[1]))])<jupyter_output><empty_output><jupyter_text>Huấn luyện mô hình mạng neural nhân tạoTa sẽ thử các siêu tham số khác nhau để tìm ra mô hình tốt nhất Nếu ta thử nhiều siêu tham số để tính độ lỗi trên **một** tập validation thì sẽ dẫn tới overfit và kết quả sẽ không khách quanVì thế ta sẽ dùng GridSearchCV để thử tham số và cross-validate các mô hình cụ thể, do tự động cross-validate nên ta sẽ fit trên toàn bộ tập train + validation, sau đó tìm ra siêu tham số tốt nhấtCác tham số thay đổi:- `alpha`: 0.001, 0.1, 1- `num_top_locations`: 1, 3, 5Chạy GridSearchCV với tham số cv=3, nghĩa là sẽ thử 3 x 3 x 3 = 27 lần Các tham số được cố định:- `hidden_layer_sizes`: 70- `activation`: `relu`- `solver`: Do dữ liệu rất nhiều nên sử dụng thuật toán `adam` sẽ hiệu quả hơn so với lbfgs (theo scikit-learn), tuy nhiên nhược điểm là train khá lâu- `max_iter`: 10000 để cho thuật toán có thể đạt được hội tụ, nếu set quá thấp sẽ không đủ lần lặp để hội tụ  Suy nghĩ về vấn đề giảm chi phí thời gianTa thấy khi train nếu chỉ dùng mô hình fit cho tập đã được tiền xử lý sẽ nhanh hơn là dùng _full pipeline_ để vừa tiền xử lý vừa fit- Với trường hợp chỉ thay đổi các tham số của mô hình ta có thể dùng phương án trên để giảm chi phí thời gianTuy nhiên do trong các tham số thay đổi có `num_top_locations` nên ta phải dùng đến `full_pipeline`, muốn giảm chi phí thời gian ta sẽ dùng vòng lặp `for` để chỉ tiền xử lý với mỗi giá trị của `num_top_locations` là 3 lần, thay vì tiền xử lý với số lần là 3 x 3 x 3 = 27 lầnTa thử đo thời gian mỗi lần tiền xử lý tập _rest_X_df_ (vì khi fit ta sử dụng tập *rest_X_df*) như ở dưới thì thấy kết quả nằm giữa 0 và 0.5: __Mean: 0.4152515888214111__ Kết quả này khá thấp khi nhân với 24Vậy ta có thể sử dụng `full_pipeline` để code được sạch hơn. Tuy nhiên đây vẫn là vấn đề đáng xem xét nếu thời gian tiền xử lý cao hoặc có nhiều giá trị tham số thay đổi<jupyter_code>t = 0.00
for i in range(0,10):
    t0 = time.time()
    preprocessed_rest_X = preprocess_pipeline.transform(rest_X_df)
    t1 = time.time()
    print('Time elapsed:', t1 - t0)
    t += (t1 - t0)
t /= 10
print('Mean:', t)
mlp = MLPRegressor(hidden_layer_sizes=70, solver='adam', random_state=0, max_iter=10000)
mlp_full_pipeline = make_pipeline(ColAdderDropper(), column_transformer, StandardScaler(), mlp)

params = {
    'mlpregressor__alpha': [0.001, 0.01, 1],
    'coladderdropper__num_top_locations': [1, 3, 5]
}

mlp_grid_search=GridSearchCV(mlp_full_pipeline, param_grid=params, cv=3)
t0 = time.time()
mlp_grid_search.fit(rest_X_df, rest_y_sr)
t1 = time.time()
print('Time elapsed:', t1 - t0)<jupyter_output>Time elapsed: 176.34570574760437<jupyter_text>Kết quả thử tham số<jupyter_code>mlp_grid_search.cv_results_<jupyter_output><empty_output><jupyter_text>Tham số tốt nhất<jupyter_code>mlp_grid_search.best_params_<jupyter_output><empty_output><jupyter_text>Score trung bình cao nhất<jupyter_code>mlp_grid_search.best_score_<jupyter_output><empty_output><jupyter_text>Như vậy $R^2$ > 0.5, mô hình này có thể phù hợp để dự đoán các dữ liệu mới. **Vậy ta sẽ chọn mô hình mạng neural để dự đoán**  2. Đánh giá mô hình tìm được Sau khi đã có mô hình tốt nhất, ta sẽ dùng mô hình đó để dự đoán tập test cuối cùng<jupyter_code>rr = mlp_grid_search.score(test_X_df, test_y_sr)
rr<jupyter_output><empty_output><jupyter_text>Và độ lỗi của mô hình trên tập test là<jupyter_code>(1 - rr) * 100<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.09655761024182077, 'fraction_numerical': 0.013115220483641537, 'mean_word_length': 4.47267631947688, 'pattern_counts': {'":': 0, '<': 170, '<?xml version=': 0, '>': 182, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '40', 'dir': 'jupyter-structured-clean-dedup', 'id': '583743', 'license': 'MIT', 'n_tokens_mistral': 17693, 'n_tokens_neox': 17490, 'n_words': 4330, 'path': 'main.ipynb', 'repo_name': 'comphilano/ds_final', 'size': '35163'}