Phân Tích Giỏ Hàng Của Website Instacart

Lời mở đầu

Instacart là một startup cung ứng đồ tạp hóa qua website và ứng dụng di động. Người dùng chỉ cần chọn đồ muốn mua tại các chuỗi bán lẻ và đặt đồ, Instacart sẽ đi mua và giao đến tận tay họ. Đến nay, Instacart hoạt động tại 15.000 cửa hàng tạp hoá tại 4.000 thành phố với khoảng 50.000 “trợ lý mua sắm”. Team data science của instacart đóng vai trò rất quan trọng trong việc cung cấp trải nghiệm người dùng trong việc sử dụng app để mua hàng. Hiện tại, họ đang sử dụng các dữ liệu của khách hàng để tạo nên mô hình dự đoán sản phẩm nào người dùng sẽ mua lại, sẽ mua thử lần đầu tiên, hoặc sẽ thêm vào giỏ hàng. Hiện họ đã publish khoảng 3 triệu đơn hàng của họ để các nhà khoa học dữ liệu khác sử dụng và nghiên cứu.

Dẫn nhập

Phân tích dữ liệu

Các bạn có thể download dữ liệu ở https://www.instacart.com/datasets/grocery-shopping-2017.

Các file bao gồm:

File aisles.csv (134 dòng) có 2 cột là aisle_id,aisle

1aisle_id,aisle
21,prepared soups salads
32,specialty cheeses
43,energy granola bars
5 ...

File departments.csv (21 dòng) gồm 2 cột là department_id,department

1department_id,department
21,frozen
32,other
43,bakery
5 ...

File order_products__(prior|train).csv (trên 30 triệu dòng)

Tập này chứa danh sách sản phẩm được mua trong mỗi đơn hàng. File order_products__prior.csv chứa sản phẩm của đơn hàng trước đó của khách hàng. ‘reordered’ nói rằng sản phẩm này trong đơn hàng hiện tại đã được mua ở đơn hàng trước đó. Vì vậy, sẽ có đơn hàng không được gán là ‘reordered’ (chúng ta có thể gán nhãn là None hoặc cái gì đó cũng được để chỉ các sản phẩm này). ‘add_to_cart_order’ là thứ tự của sp được thêm vào giỏ hàng.

1order_id,product_id,add_to_cart_order,reordered
2 1,49302,1,1
3 1,11109,2,1
4 1,10246,3,0
5 ...

File orders.csv (3.4 triệu dòng, 206k users): chứa thông tin của đơn hàng, trong đó, order_dow là ngày trong tuần, eval_set thuộc một trong 3 loại là prior, train, test. order_number là thứ tự của đơn hàng của user này.

1order_id,user_id,eval_set,order_number,order_dow,order_hour_of_day,days_since_prior_order
2 2539329,1,prior,1,2,08,
3 2398795,1,prior,2,3,07,15.0
4 473747,1,prior,3,3,12,21.0
5 ...

File products.csv ((50k dòng) chứa thông tin sản phẩm:

1 product_id,product_name,aisle_id,department_id
2 1,Chocolate Sandwich Cookies,61,19
3 2,All-Seasons Salt,104,13
4 3,Robust Golden Unsweetened Oolong Tea,94,7
5 ...

Với mỗi order_id trong tập test ở file orders.csv, chúng ta phải dự đoán các sản phẩm nào người dùng sẽ mua lại (“reorder”) thuộc đơn hàng đó. Nếu bạn dự đoán đó là đơn hàng không có sản phẩm nào được mua lại, thì ta sẽ điền vào giá trị ‘None’

Ví dụ về kết quả dự đoán:

1order_id,products
217,1 2
334,None
4137,1 2 3

Thực hành

Đầu tiên, ta sẽ import một số thư viện cơ bản để sử dụng, và load tất cả các file lên. Lưu ý một chút là ở đây, mình để tất cả các file trong thư mục data

 1import pandas as pd
 2import numpy as np
 3from collections import OrderedDict
 4
 5from sklearn.linear_model import LogisticRegression
 6from sklearn.metrics import f1_score
 7
 8from sklearn import metrics, cross_validation
 9from sklearn.metrics import f1_score
10from sklearn.preprocessing import MinMaxScaler
11
12#Import the files
13aisles_df = pd.read_csv('data/aisles.csv')
14products_df = pd.read_csv('data/products.csv')
15orders_df = pd.read_csv('data/orders.csv')
16order_products_prior_df = pd.read_csv('data/order_products__prior.csv')
17departments_df = pd.read_csv('data/departments.csv')
18order_products_train_df = pd.read_csv('data/order_products__train.csv')

Sau đó, mình sẽ merge đơn hàng vào chi tiết đơn hàng của tập train và tập prior

1order_products_train_df = order_products_train_df.merge(orders_df.drop('eval_set', axis=1), on='order_id')
2order_products_prior_df = order_products_prior_df.merge(orders_df.drop('eval_set', axis=1), on='order_id')

show ra 5 dòng đầu tiên của order_products_train_df

1print(order_products_train_df.head())
1   order_id  product_id  add_to_cart_order  reordered  user_id  order_number  order_dow  order_hour_of_day  days_since_prior_order
20         1       49302                  1          1   112108             4          4                 10                     9.0
31         1       11109                  2          1   112108             4          4                 10                     9.0
42         1       10246                  3          0   112108             4          4                 10                     9.0
53         1       49683                  4          0   112108             4          4                 10                     9.0
64         1       43633                  5          1   112108             4          4                 10                     9.0
7
8[5 rows x 9 columns]

Tổng cộng mình có 9 cột, ý nghĩa các cột mình có giải thích ở trên rồi nha.

Tiếp theo, chúng ta tạo tập tập dữ liệu đếm số lượng sản phẩm của từng người mua

1user_product_df = (order_products_prior_df.groupby(['product_id','user_id'],as_index=False)
2                                          .agg({'order_id':'count'})
3                                          .rename(columns={'order_id':'user_product_total_orders'}))
4
5train_ids = order_products_train_df['user_id'].unique()
6df_X = user_product_df[user_product_df['user_id'].isin(train_ids)]
7print(df_X.head())
1   product_id  user_id  user_product_total_orders
20           1      138                          2
31           1      709                          1
43           1      777                          1
56           1     1052                          2
69           1     1494                          3

Ở đây, người 138 mua sản phẩm 1 2 lần, người 709 mua sản phẩm 1 1 lần, … tương tự như vậy cho các user và product khác.

Bước tiếp theo, chúng ta sẽ liệt kê các sản phẩm người dùng đã mua:

1train_carts = (order_products_train_df.groupby('user_id',as_index=False)
2                                      .agg({'product_id':(lambda x: set(x))})
3                                      .rename(columns={'product_id':'latest_cart'}))

print(train_carts.head())

1  user_id                                        latest_cart
20        1  {196, 26405, 27845, 46149, 13032, 39657, 26088...
31        2  {24838, 11913, 45066, 31883, 48523, 38547, 248...
42        5  {40706, 21413, 20843, 48204, 21616, 19057, 201...
53        7  {17638, 29894, 47272, 45066, 13198, 37999, 408...
64        8  {27104, 15937, 5539, 41540, 31717, 48230, 2224...

Mối tương quan giữa sản phẩm được add to card và sản phẩm được mua

1df_X = df_X.merge(train_carts, on='user_id')
2df_X['in_cart'] = (df_X.apply(lambda row: row['product_id'] in row['latest_cart'], axis=1).astype(int))
3
4print(df_X.head())
5
6print(df_X['in_cart'].value_counts())
 1# df_X.head()
 2   product_id  user_id  user_product_total_orders latest_cart  in_cart
 30           1      138                          2     {42475}        0
 41         907      138                          2     {42475}        0
 52        1000      138                          1     {42475}        0
 63        3265      138                          1     {42475}        0
 74        4913      138                          1     {42475}        0
 8
 9# df_X['in_cart'].value_counts()
100    7645837
111     828824
12Name: in_cart, dtype: int64

Tỷ lệ khoảng 9.7%. Điều này nói lên rằng, người dùng trong 1 phiên mua hàng có thể add rất nhiều sản phẩm vào giỏ, nhưng chỉ khoảng 10% sản phẩm họ mua thật sự, hơn 90% sản phẩm còn lại sẽ bị remove trước khi nọ nhấn nút thanh toán.

Xây dựng tập đặc trưng

Đặc trưng sản phẩm

Với đặc trưng sản phẩm, chúng ta sẽ rút trích 2 đặc trưng đơn giản là tổng số lượng đơn hàng của một sản phẩm và trung bình số lượng đơn hàng có chứa sản phẩm.

1prod_features = ['product_total_orders','product_avg_add_to_cart_order']
2
3prod_features_df = (order_products_prior_df.groupby(['product_id'],as_index=False)
4                                           .agg(OrderedDict(
5                                                   [('order_id','nunique'),
6                                                    ('add_to_cart_order','mean')])))
7prod_features_df.columns = ['product_id'] + prod_features
8print(prod_features_df.head())
1
2   product_id  product_total_orders  product_avg_add_to_cart_order
30           1                  1852                       5.801836
41           2                    90                       9.888889
52           3                   277                       6.415162
63           4                   329                       9.507599
74           5                    15                       6.466667

Add thêm đặc trưng sản phẩm vào trong tập huấn luyện

1
2df_X = df_X.merge(prod_features_df, on='product_id')
3
4#note that dropping rows with NA product_avg_days_since_prior_order is likely a naive choice
5df_X = df_X.dropna()
6print(df_X.head())
1   product_id  user_id              ...               product_total_orders product_avg_add_to_cart_order
20           1      138              ...                               1852                      5.801836
31           1      709              ...                               1852                      5.801836
42           1      777              ...                               1852                      5.801836
53           1     1052              ...                               1852                      5.801836
64           1     1494              ...                               1852                      5.801836

Đặc trưng người dùng

Với người dùng, chúng sa sử dụng các đặc trưng là: Tổng số lượng đơn hàng, trung bình số sản phẩm trong 1 đơn hàng, tổng số lượng sản phẩm người dùng mua, Trung bình số ngày user sẽ mua đơn hàng tiếp theo

 1user_features = ['user_total_orders','user_avg_cartsize','user_total_products','user_avg_days_since_prior_order']
 2
 3user_features_df = (order_products_prior_df.groupby(['user_id'],as_index=False)
 4                                           .agg(OrderedDict(
 5                                                   [('order_id',['nunique', (lambda x: x.shape[0] / x.nunique())]),
 6                                                    ('product_id','nunique'),
 7                                                    ('days_since_prior_order','mean')])))
 8
 9user_features_df.columns = ['user_id'] + user_features
10print(user_features_df.head())

Và chúng ta merge tiếp đặc trưng user vào trong tập huấn luyện.

1
2df_X = df_X.merge(user_features_df, on='product_id')
3
4#note that dropping rows with NA product_avg_days_since_prior_order is likely a naive choice
5df_X = df_X.dropna()

Đặc trưng mối tương quan giữa người dùng và sản phẩm

Ở đây, chúng ta sử dụng đặc trưng trung bình số sản phẩm của 1 người được thêm vào đơn hàng và tần suất 1 sản phẩm 1 user add vào đơn hàng.

1user_prod_features = ['user_product_avg_add_to_cart_order']
2
3user_prod_features_df = (order_products_prior_df.groupby(['product_id','user_id'],as_index=False) \
4                                                .agg(OrderedDict(
5                                                     [('add_to_cart_order','mean')])))
6
7user_prod_features_df.columns = ['product_id','user_id'] + user_prod_features
8df_X = df_X.merge(user_prod_features_df,on=['user_id','product_id'])
9df_X['user_product_order_freq'] = df_X['user_product_total_orders'] / df_X['user_total_orders']

Bổ sung thêm đặc trưng

Ngoài các đặc trưng cơ bản ở trên, ta sẽ bổ sung thêm một số đặc trưng khác:

Đặc trưng sản phẩm: bổ sung thêm 3 đặc trưng trung bình ngày trong tuần được đặt hàng (cột order_down), trung bình giờ đặt hàng (cột order_hour_of_day), trung bình ngày đặt hàng kể từ lần đặt trước đó (cột days_since_prior_order) theo sản phẩm.

 1prod_features = ['product_avg_order_dow', 'product_avg_order_hour_of_day', 'product_avg_days_since_prior_order']
 2
 3prod_features_df = (order_products_prior_df.groupby(['product_id'], as_index=False)
 4                                     .agg(OrderedDict(
 5                                     [('order_dow','mean'),
 6                                      ('order_hour_of_day', 'mean'),
 7                                      ('days_since_prior_order', 'mean')])))
 8
 9prod_features_df.columns = ['product_id'] + prod_features
10
11df_X = df_X.merge(prod_features_df, on='product_id')
12df_X = df_X.dropna()

Đặc trưng người dùng: bổ sung thêm 2 cột đặc trung trung bình ngày trong tuần được đặt hàng (cột order_down) và trung bình giờ đặt hàng (cột order_hour_of_day) theo người dùng

 1user_features = ['user_avg_order_dow','user_avg_order_hour_of_day']
 2
 3user_features_df = (order_products_prior_df.groupby(['user_id'],as_index=False)
 4                                           .agg(OrderedDict(
 5                                                   [('order_dow','mean'),
 6                                                    ('order_hour_of_day','mean')])))
 7
 8user_features_df.columns = ['user_id'] + user_features
 9df_X = df_X.merge(user_features_df, on='user_id')
10df_X = df_X.dropna()

Đặc trung người dùng - sản phẩm: Bổ sung thêm đặc trưng tung bình trên cột order_down, order_hour_of_day, days_since_prior_order theo người dùng và sản phẩm

 1
 2user_prod_features = ['user_product_avg_days_since_prior_order',
 3                      'user_product_avg_order_dow',
 4                      'user_product_avg_order_hour_of_day']
 5
 6user_prod_features_df = (order_products_prior_df.groupby(['product_id','user_id'],as_index=False) \
 7                                                .agg(OrderedDict(
 8                                                     [('days_since_prior_order','mean'),
 9                                                     ('order_dow','mean'),
10                                                     ('order_hour_of_day','mean')])))
11
12user_prod_features_df.columns = ['product_id','user_id'] + user_prod_features
13
14df_X = df_X.merge(user_prod_features_df, on=['user_id', 'product_id'])
15df_X = df_X.dropna()

Đặc trưng độ lệch: Tính độ lệch của của một số đặc trưng so với trung bình của chúng

 1#Create delta columns to compare how users perform against averages
 2df_X['product_total_orders_delta_per_user'] = df_X['product_total_orders'] - df_X['user_product_total_orders']
 3
 4df_X['product_avg_add_to_cart_order_delta_per_user'] = df_X['product_avg_add_to_cart_order'] - \
 5                                                            df_X['user_product_avg_add_to_cart_order']
 6
 7df_X['product_avg_order_dow_per_user'] = df_X['product_avg_order_dow'] - df_X['user_product_avg_order_dow']
 8
 9df_X['product_avg_order_hour_of_day_per_user'] = df_X['product_avg_order_hour_of_day'] - \
10                                                            df_X['user_product_avg_order_hour_of_day']
11
12df_X['product_avg_days_since_prior_order_per_user'] = df_X['product_avg_days_since_prior_order'] - \
13                                                            df_X['user_product_avg_days_since_prior_order']

Bổ sung thêm đặc trưng department name

1f_departments_df = products_df.merge(departments_df, on = 'department_id')
2f_departments_df = f_departments_df[['product_id', 'department']]
3
4df_X = df_X.merge(f_departments_df, on = 'product_id')
5df_X = df_X.dropna()
6df_X = pd.concat([df_X, pd.get_dummies(df_X['department'])], axis=1)
7del df_X['department']

Chúng ta có tổng cộng 21 department name, vậy chúng ta thêm 21 cột, một cột tương ứng với một department name, ví dụ: alcohol,babies ,bakery, … Sản phẩm thuộc department name thì sẽ được đánh số 1, không thuộc department name thì đánh số 0.

Huấn luyện mô hình

Chia tập dữ liệu thành 80/20 trong đó 80% là tập train, 20% là tập test. Sử dụng k-fold-cross_validation với k=10

 1
 2np.random.seed(99)
 3total_users = df_X['user_id'].unique()
 4test_users = np.random.choice(total_users, size=int(total_users.shape[0] * .20), replace=False)
 5
 6
 7
 8test_user_sets = []
 9length = len(test_users)
10cv = 10
11
12
13for x in range (0, cv):
14    start = int(x/cv*length)
15    finish = int((x+1)/cv*length)
16    test_user_sets.append(test_users[start:finish])
17
18cv_f1_scores = []
19cv_f1_scores_balanced = []
20cv_f1_scores_10fit = []
21
22for test_user_set in test_user_sets:
23    df_X_tr, df_X_te = df_X[~df_X['user_id'].isin(test_user_set)], df_X[df_X['user_id'].isin(test_user_set)]
24
25    y_tr, y_te = df_X_tr['in_cart'], df_X_te['in_cart']
26    X_tr, X_te = df_X_tr.drop(['product_id','user_id','latest_cart','in_cart'],axis=1), \
27             df_X_te.drop(['product_id','user_id','latest_cart','in_cart'],axis=1), \
28
29    scaler = MinMaxScaler()
30    X_tr = pd.DataFrame(scaler.fit_transform(X_tr), columns=X_tr.columns)
31    X_te = pd.DataFrame(scaler.fit_transform(X_te), columns=X_te.columns)
32
33    lr = LogisticRegression(C=10000000)
34    lr_balanced = LogisticRegression(class_weight='balanced', C=10000000)
35    lr_10x = LogisticRegression(class_weight={1 : 6, 0 : 1}, C=10000000)
36
37    lr.fit(X_tr, y_tr)
38    cv_f1_scores.append(f1_score(lr.predict(X_te), y_te))
39
40    lr_balanced.fit(X_tr, y_tr)
41    cv_f1_scores_balanced.append(f1_score(lr_balanced.predict(X_te), y_te))
42
43    lr_10x.fit(X_tr, y_tr)
44    cv_f1_scores_10fit.append(f1_score(lr_10x.predict(X_te), y_te))
45
46print("cv_f1_scores: " +str( np.mean(cv_f1_scores)))
47print("cv_f1_scores_balanced: "+str(np.mean(cv_f1_scores_balanced)))
48print("cv_f1_scores_10fit: "+str(np.mean(cv_f1_scores_10fit)))
49
50df_X_tr, df_X_te = df_X[~df_X['user_id'].isin(test_users)], df_X[df_X['user_id'].isin(test_users)]
51
52y_tr, y_te = df_X_tr['in_cart'], df_X_te['in_cart']
53X_tr, X_te = df_X_tr.drop(['product_id','user_id','latest_cart','in_cart'],axis=1), \
54             df_X_te.drop(['product_id','user_id','latest_cart','in_cart'],axis=1), \
55
56lr_10x = LogisticRegression(class_weight={1 : 6, 0 : 1}, C=10000000)
57lr_10x.fit(X_tr, y_tr)
58print("F1 store all: "+str(f1_score(lr_10x.predict(X_te), y_te)))
1cv_f1_scores: 0.2026889989037295
2cv_f1_scores_balanced: 0.3816810646496983
3cv_f1_scores_10fit: 0.3899595078917494
4
5F1 store all: 0.3808374055616213

Thử in ra hệ số của hàm hồi quy

1coefficients = pd.DataFrame(lr_10x.coef_, columns = X_tr.columns)
2coefficients = np.exp(coefficients)
3print(coefficients.T)
 1user_product_total_orders                     1.160475
 2product_total_orders                          1.077254
 3product_avg_add_to_cart_order                 0.915343
 4user_total_orders                             0.983272
 5user_avg_cartsize                             1.059655
 6user_total_products                           0.993839
 7user_avg_days_since_prior_order               0.993513
 8user_product_avg_add_to_cart_order            0.950418
 9user_product_order_freq                       1.051246
10product_avg_order_dow                         0.994744
11product_avg_order_hour_of_day                 1.010971
12product_avg_days_since_prior_order            0.994498
13user_avg_order_dow                            0.997298
14user_avg_order_hour_of_day                    1.012958
15user_product_avg_days_since_prior_order       1.003382
16user_product_avg_order_dow                    0.994477
17user_product_avg_order_hour_of_day            1.003457
18product_total_orders_delta_per_user           0.928288
19product_avg_add_to_cart_order_delta_per_user  0.963095
20product_avg_order_dow_per_user                1.000268
21product_avg_order_hour_of_day_per_user        1.007489
22product_avg_days_since_prior_order_per_user   0.991147
23alcohol                                       0.998866
24babies                                        1.000313
25bakery                                        1.003098
26beverages                                     1.007733
27breakfast                                     1.000117
28bulk                                          0.999980
29canned goods                                  0.995017
30dairy eggs                                    1.018069
31deli                                          1.002720
32dry goods pasta                               0.997379
33frozen                                        1.000752
34household                                     0.992164
35international                                 0.996822
36meat seafood                                  1.000340
37missing                                       1.001953
38other                                         0.999607
39pantry                                        0.972038
40personal care                                 0.992072
41pets                                          1.000466
42produce                                       1.017809
43snacks                                        1.004893

Thử show confusion matrix của dữ liệu:

 1from sklearn.metrics import confusion_matrix
 2import seaborn as sns
 3import matplotlib.pyplot as plt
 4%matplotlib inline
 5plt.style.use('fivethirtyeight')
 6
 7def plot_confusion_matrix(cm,title='Confusion matrix', cmap=plt.cm.Reds):
 8    plt.imshow(cm, interpolation='nearest',cmap=cmap)
 9    plt.title(title)
10    plt.colorbar()
11    plt.tight_layout()
12    plt.ylabel('True label')
13    plt.xlabel('Predicted label')
14
15#y_tr=np.ravel(y_tr)
16
17train_acc=lr_10x.score(X_tr, y_tr)
18test_acc=lr_10x.score(X_te, y_te)
19print("Training Data Accuracy: %0.2f" %(train_acc))
20print("Test Data Accuracy:     %0.2f" %(test_acc))
21
22y_true = y_te
23y_pred = lr_10x.predict(X_te)
24
25
26conf = confusion_matrix(y_true, y_pred)
27print(conf)
28
29print ('\n')
30print ("Precision:              %0.2f" %(conf[1, 1] / (conf[1, 1] + conf[0, 1])))
31print ("Recall:                 %0.2f"% (conf[1, 1] / (conf[1, 1] + conf[1, 0])))
32
33cm=confusion_matrix(y_true, y_pred, labels=[0, 1])
34
35plt.figure()
36plot_confusion_matrix(cm)

Kết quả

1Training Data Accuracy: 0.83
2Test Data Accuracy:     0.83
3[[1236979  190126]
4 [  78107   82493]]
5
6
7Precision:              0.30
8Recall:                 0.51

Hình ảnh

Show đường cong ROC của dữ liệu

 1
 2from sklearn.metrics import roc_curve, auc
 3
 4y_score = lr_10x.predict_proba(X_te)[:,1]
 5
 6fpr, tpr,_ = roc_curve(y_te, y_score)
 7roc_auc = auc(fpr, tpr)
 8
 9plt.figure()
10# Plotting our Baseline..
11plt.plot([0,1],[0,1], linestyle='--', color = 'black')
12plt.plot(fpr, tpr, color = 'green')
13plt.xlabel('False Positive Rate')
14plt.ylabel('True Positive Rate')
15plt.gca().set_aspect('equal', adjustable='box')

Hình ảnh

Cảm ơn các bạn đã theo dõi. Hẹn gặp bạn ở các bài viết tiếp theo.

Comments