基於XGBoost模型的幸福度預測——阿里天池學習賽

極客鋒行發表於2020-12-20

本文根據阿里天池學習賽《快來一起挖掘幸福感!》撰寫

載入資料

載入的是完整版的資料 happiness_train_complete.csv

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
import seaborn as sns
sns.set_style('whitegrid')
# 將 id 列作為 DataFrame 的 index 並且指定 survey_time 為時間序列
data_origin = pd.read_csv('./data/happiness_train_complete.csv', index_col='id', parse_dates=['survey_time'], encoding='gbk')

資料集基本資訊的探索

下面簡單輸出前5行檢視。

data_origin.head()
happiness survey_type province city county survey_time gender birth nationality religion ... neighbor_familiarity public_service_1 public_service_2 public_service_3 public_service_4 public_service_5 public_service_6 public_service_7 public_service_8 public_service_9
id
1 4 1 12 32 59 2015-08-04 14:18:00 1 1959 1 1 ... 4 50 60 50 50 30.0 30 50 50 50
2 4 2 18 52 85 2015-07-21 15:04:00 1 1992 1 1 ... 3 90 70 70 80 85.0 70 90 60 60
3 4 2 29 83 126 2015-07-21 13:24:00 2 1967 1 0 ... 4 90 80 75 79 80.0 90 90 90 75
4 5 2 10 28 51 2015-07-25 17:33:00 2 1943 1 1 ... 3 100 90 70 80 80.0 90 90 80 80
5 4 1 7 18 36 2015-08-10 09:50:00 2 1994 1 1 ... 2 50 50 50 50 50.0 50 50 50 50

5 rows × 139 columns

檢視資料的詳細資訊,共8000條記錄,139個特徵。

第二列為特證名、第三列為非空記錄個數、第四列為特徵的資料格式。

data_origin.info(verbose=True, null_counts=True)
<class 'pandas.core.frame.DataFrame'>
Int64Index: 8000 entries, 1 to 8000
Data columns (total 139 columns):
 #   Column                Non-Null Count  Dtype         
---  ------                --------------  -----         
 0   happiness             8000 non-null   int64         
 1   survey_type           8000 non-null   int64         
 2   province              8000 non-null   int64         
 3   city                  8000 non-null   int64         
 4   county                8000 non-null   int64         
 5   survey_time           8000 non-null   datetime64[ns]
 6   gender                8000 non-null   int64         
 7   birth                 8000 non-null   int64         
 8   nationality           8000 non-null   int64         
 9   religion              8000 non-null   int64         
 10  religion_freq         8000 non-null   int64         
 11  edu                   8000 non-null   int64         
 12  edu_other             3 non-null      object        
 13  edu_status            6880 non-null   float64       
 14  edu_yr                6028 non-null   float64       
 15  income                8000 non-null   int64         
 16  political             8000 non-null   int64         
 17  join_party            824 non-null    float64       
 18  floor_area            8000 non-null   float64       
 19  property_0            8000 non-null   int64         
 20  property_1            8000 non-null   int64         
 21  property_2            8000 non-null   int64         
 22  property_3            8000 non-null   int64         
 23  property_4            8000 non-null   int64         
 24  property_5            8000 non-null   int64         
 25  property_6            8000 non-null   int64         
 26  property_7            8000 non-null   int64         
 27  property_8            8000 non-null   int64         
 28  property_other        66 non-null     object        
 29  height_cm             8000 non-null   int64         
 30  weight_jin            8000 non-null   int64         
 31  health                8000 non-null   int64         
 32  health_problem        8000 non-null   int64         
 33  depression            8000 non-null   int64         
 34  hukou                 8000 non-null   int64         
 35  hukou_loc             7996 non-null   float64       
 36  media_1               8000 non-null   int64         
 37  media_2               8000 non-null   int64         
 38  media_3               8000 non-null   int64         
 39  media_4               8000 non-null   int64         
 40  media_5               8000 non-null   int64         
 41  media_6               8000 non-null   int64         
 42  leisure_1             8000 non-null   int64         
 43  leisure_2             8000 non-null   int64         
 44  leisure_3             8000 non-null   int64         
 45  leisure_4             8000 non-null   int64         
 46  leisure_5             8000 non-null   int64         
 47  leisure_6             8000 non-null   int64         
 48  leisure_7             8000 non-null   int64         
 49  leisure_8             8000 non-null   int64         
 50  leisure_9             8000 non-null   int64         
 51  leisure_10            8000 non-null   int64         
 52  leisure_11            8000 non-null   int64         
 53  leisure_12            8000 non-null   int64         
 54  socialize             8000 non-null   int64         
 55  relax                 8000 non-null   int64         
 56  learn                 8000 non-null   int64         
 57  social_neighbor       7204 non-null   float64       
 58  social_friend         7204 non-null   float64       
 59  socia_outing          8000 non-null   int64         
 60  equity                8000 non-null   int64         
 61  class                 8000 non-null   int64         
 62  class_10_before       8000 non-null   int64         
 63  class_10_after        8000 non-null   int64         
 64  class_14              8000 non-null   int64         
 65  work_exper            8000 non-null   int64         
 66  work_status           2951 non-null   float64       
 67  work_yr               2951 non-null   float64       
 68  work_type             2951 non-null   float64       
 69  work_manage           2951 non-null   float64       
 70  insur_1               8000 non-null   int64         
 71  insur_2               8000 non-null   int64         
 72  insur_3               8000 non-null   int64         
 73  insur_4               8000 non-null   int64         
 74  family_income         7999 non-null   float64       
 75  family_m              8000 non-null   int64         
 76  family_status         8000 non-null   int64         
 77  house                 8000 non-null   int64         
 78  car                   8000 non-null   int64         
 79  invest_0              8000 non-null   int64         
 80  invest_1              8000 non-null   int64         
 81  invest_2              8000 non-null   int64         
 82  invest_3              8000 non-null   int64         
 83  invest_4              8000 non-null   int64         
 84  invest_5              8000 non-null   int64         
 85  invest_6              8000 non-null   int64         
 86  invest_7              8000 non-null   int64         
 87  invest_8              8000 non-null   int64         
 88  invest_other          29 non-null     object        
 89  son                   8000 non-null   int64         
 90  daughter              8000 non-null   int64         
 91  minor_child           6934 non-null   float64       
 92  marital               8000 non-null   int64         
 93  marital_1st           7172 non-null   float64       
 94  s_birth               6282 non-null   float64       
 95  marital_now           6230 non-null   float64       
 96  s_edu                 6282 non-null   float64       
 97  s_political           6282 non-null   float64       
 98  s_hukou               6282 non-null   float64       
 99  s_income              6282 non-null   float64       
 100 s_work_exper          6282 non-null   float64       
 101 s_work_status         2565 non-null   float64       
 102 s_work_type           2565 non-null   float64       
 103 f_birth               8000 non-null   int64         
 104 f_edu                 8000 non-null   int64         
 105 f_political           8000 non-null   int64         
 106 f_work_14             8000 non-null   int64         
 107 m_birth               8000 non-null   int64         
 108 m_edu                 8000 non-null   int64         
 109 m_political           8000 non-null   int64         
 110 m_work_14             8000 non-null   int64         
 111 status_peer           8000 non-null   int64         
 112 status_3_before       8000 non-null   int64         
 113 view                  8000 non-null   int64         
 114 inc_ability           8000 non-null   int64         
 115 inc_exp               8000 non-null   float64       
 116 trust_1               8000 non-null   int64         
 117 trust_2               8000 non-null   int64         
 118 trust_3               8000 non-null   int64         
 119 trust_4               8000 non-null   int64         
 120 trust_5               8000 non-null   int64         
 121 trust_6               8000 non-null   int64         
 122 trust_7               8000 non-null   int64         
 123 trust_8               8000 non-null   int64         
 124 trust_9               8000 non-null   int64         
 125 trust_10              8000 non-null   int64         
 126 trust_11              8000 non-null   int64         
 127 trust_12              8000 non-null   int64         
 128 trust_13              8000 non-null   int64         
 129 neighbor_familiarity  8000 non-null   int64         
 130 public_service_1      8000 non-null   int64         
 131 public_service_2      8000 non-null   int64         
 132 public_service_3      8000 non-null   int64         
 133 public_service_4      8000 non-null   int64         
 134 public_service_5      8000 non-null   float64       
 135 public_service_6      8000 non-null   int64         
 136 public_service_7      8000 non-null   int64         
 137 public_service_8      8000 non-null   int64         
 138 public_service_9      8000 non-null   int64         
dtypes: datetime64[ns](1), float64(25), int64(110), object(3)
memory usage: 8.5+ MB

檢視資料總體統計量。

data_origin.describe()
happiness survey_type province city county gender birth nationality religion religion_freq ... neighbor_familiarity public_service_1 public_service_2 public_service_3 public_service_4 public_service_5 public_service_6 public_service_7 public_service_8 public_service_9
count 8000.000000 8000.000000 8000.000000 8000.000000 8000.000000 8000.00000 8000.000000 8000.00000 8000.000000 8000.000000 ... 8000.000000 8000.000000 8000.000000 8000.000000 8000.000000 8000.000000 8000.000000 8000.00000 8000.000000 8000.000000
mean 3.850125 1.405500 15.155375 42.564750 70.619000 1.53000 1964.707625 1.37350 0.772250 1.427250 ... 3.722250 70.809500 68.170000 62.737625 66.320125 62.794187 67.064000 66.09625 65.626750 67.153750
std 0.938228 0.491019 8.917100 27.187404 38.747503 0.49913 16.842865 1.52882 1.071459 1.408441 ... 1.143358 21.184742 20.549943 24.771319 22.049437 23.463162 21.586817 23.08568 23.827493 22.502203
min -8.000000 1.000000 1.000000 1.000000 1.000000 1.00000 1921.000000 -8.00000 -8.000000 -8.000000 ... -8.000000 -3.000000 -3.000000 -3.000000 -3.000000 -3.000000 -3.000000 -3.00000 -3.000000 -3.000000
25% 4.000000 1.000000 7.000000 18.000000 37.000000 1.00000 1952.000000 1.00000 1.000000 1.000000 ... 3.000000 60.000000 60.000000 50.000000 60.000000 55.000000 60.000000 60.00000 60.000000 60.000000
50% 4.000000 1.000000 15.000000 42.000000 73.000000 2.00000 1965.000000 1.00000 1.000000 1.000000 ... 4.000000 79.000000 70.000000 70.000000 70.000000 70.000000 70.000000 70.00000 70.000000 70.000000
75% 4.000000 2.000000 22.000000 65.000000 104.000000 2.00000 1977.000000 1.00000 1.000000 1.000000 ... 5.000000 80.000000 80.000000 80.000000 80.000000 80.000000 80.000000 80.00000 80.000000 80.000000
max 5.000000 2.000000 31.000000 89.000000 134.000000 2.00000 1997.000000 8.00000 1.000000 9.000000 ... 5.000000 100.000000 100.000000 100.000000 100.000000 100.000000 100.000000 100.00000 100.000000 100.000000

8 rows × 135 columns

資料預處理

缺失值處理

檢視子特徵的缺失情況,其中

  • required_list 表示特徵中的必填項
  • continuous_list 表示特徵屬性為連續型變數
  • categorical_list 表示分型別變數

其餘特徵均為等級(ordinal)型的分類變數。

required_list = ['survey_type', 'province', 'city', 'county', 'survey_time', 'gender', 'birth', 'nationality', 'religion',
                 'religion_freq', 'edu', 'income', 'political', 'floor_area', 'height_cm', 'weight_jin', 'health', 'health_problem',
                 'depression', 'hukou', 'socialize', 'relax', 'learn', 'equity', 'class', 'work_exper', 'work_status', 'work_yr', 'work_type',
                 'work_manage', 'family_income', 'family_m', 'family_status', 'house', 'car', 'marital', 'status_peer', 'status_3_before', 
                 'view', 'inc_ability']
continuous_list = ['birth', 'edu_yr', 'income', 'floor_area', 'height_cm', 'weight_jin', 'work_yr', 'family_income', 'family_m', 'house', 'son', 
                   'daughter', 'minor_child', 'marital_1st', 's_birth', 'marital_now', 's_income', 'f_birth', 'm_birth', 'inc_exp',
                  'public_service_1', 'public_service_2', 'public_service_3', 'public_service_4', 'public_service_5', 'public_service_6',
                  'public_service_7', 'public_service_8', 'public_service_9']
categorical_list = ['survey_type', 'province', 'gender', 'nationality']

必填項的缺失值分析

檢視必填項中缺失值的情況。

data_origin[required_list].isna().sum()[data_origin[required_list].isna().sum() > 0].to_frame().T
work_status work_yr work_type work_manage family_income
0 5049 5049 5049 5049 1

其中

  • work_status 表示 目前工作的狀況
  • work_yr 表示 一共工作了多少年
  • work_type 表示 目前工作的性質
  • work_manage 表示 目前工作的管理活動情況
  • family_income 表示 去年全年家庭總收入

首先分析 work_ 開頭的四項特徵的缺失情況,它們的缺失計數一樣,可能說明調查問卷的填寫方式,可能被跳過了。

首先檢查調查問卷,找到對應的問卷問題,發現在 work_exper 特徵中,即 工作經歷及狀況,根據不同的工作經歷,將上面四個問題跳過。

檢視 work_exper 對應的問卷。

圖片

可以發現 work_exper 除了 1 分類,其它問題均被跳問;所以將上面四列的缺失記錄的 work_exper 輸出,檢視是否都為非 1 類的記錄。

通過下面的輸出可以看到,在上面四項特徵為缺失值的情況下,其記錄對應的 work_exper 的取值大部分不為 1

data_origin.loc[data_origin[required_list].isna().sum(axis=1)[data_origin[required_list].isna().sum(axis=1) > 0].index, 'work_exper'].to_frame().plot.hist()
pd.value_counts(data_origin.loc[data_origin[required_list].isna().sum(axis=1)[data_origin[required_list].isna().sum(axis=1) > 0].index, 'work_exper'])
5    1968
3    1242
4    1065
2     387
6     380
1       7
Name: work_exper, dtype: int64

output_15_1

進一步檢視取值為 1 的記錄。

(data_origin[data_origin[required_list].isna().sum(axis=1) > 0])[(data_origin[data_origin[required_list].isna().sum(axis=1) > 0].work_exper == 1)]
happiness survey_type province city county survey_time gender birth nationality religion ... neighbor_familiarity public_service_1 public_service_2 public_service_3 public_service_4 public_service_5 public_service_6 public_service_7 public_service_8 public_service_9
id
692 4 2 21 64 101 2015-07-20 11:12:00 2 1975 1 1 ... 5 80 70 80 80 80.0 80 80 80 80
841 4 2 31 88 133 2015-08-17 13:49:00 2 1971 1 0 ... 4 50 30 -2 -2 -2.0 50 50 50 70
1411 4 2 2 2 9 2015-07-23 09:25:00 1 1967 8 1 ... 4 90 85 80 90 90.0 92 93 94 90
3117 4 1 4 7 18 2015-10-03 16:02:00 1 1980 1 1 ... 2 30 35 30 40 60.0 40 30 70 70
4783 5 2 22 65 103 2015-07-08 18:45:00 1 1955 1 1 ... 5 90 90 90 90 80.0 90 80 90 90
5589 5 2 16 46 78 2015-07-29 11:34:00 2 1964 1 1 ... 3 89 63 67 75 74.0 67 65 78 79
7368 4 2 21 64 101 2015-07-19 08:32:00 2 1963 1 1 ... 5 70 70 70 60 70.0 70 60 60 60

7 rows × 139 columns

可以發現 work_exper1 的記錄存在7條,故將此刪除。

data_origin.drop((data_origin[data_origin[required_list].isna().sum(axis=1) > 0])[(data_origin[data_origin[required_list].isna().sum(axis=1) > 0].work_exper == 1)].index, inplace=True)

因為 family_income 缺失個數只有1條,不影響資料規模,所以直接將其刪除。

data_origin.drop(data_origin['family_income'].isna()[data_origin['family_income'].isna()].index, inplace=True)

連續型特徵缺失值分析

檢視連續型特徵的卻失情況。

data_origin[continuous_list].isna().sum()[data_origin[continuous_list].isna().sum() > 0].to_frame().T
edu_yr work_yr minor_child marital_1st s_birth marital_now s_income
0 1970 5041 1066 828 1718 1770 1718

其中

  • edu_yr 表示 已經完成的最高學歷是哪一年獲得的
  • work_yr 表示 第一份非農工作到目前的工作一共工作了多少年
  • minor_child 表示 有幾個18週歲以下未成年子女
  • marital_1st 表示 第一次結婚的時間
  • s_birth 表示 目前的配偶或同居伴侶是哪一年出生的
  • martital_now 表示 與目前的配偶是哪一年結婚的
  • s_income 表示 配偶或同居伴侶去年全年的總收入

對於 edu_yr 即 已經完成的最高學歷是哪一年獲得的,檢視缺失記錄的 edu_status 取值分佈情況。

data_origin[data_origin['edu_yr'].isna()]['edu_status'].plot.hist()
pd.value_counts(data_origin[data_origin['edu_yr'].isna()]['edu_status'])
2.0    746
3.0    103
4.0      1
1.0      1
Name: edu_status, dtype: int64

output_26_1

檢視 edu_yr 缺失的記錄的 edu_status 特徵後,只有選項 4 即 畢業的記錄才應該填寫 edu_yr 的畢業年份,所以應該刪除記錄。

data_origin.drop(data_origin[(data_origin['edu_status'] == 4) & (data_origin['edu_yr'].isna())].index, inplace=True)
data_origin.shape
(7991, 139)

對於 minor_child 特徵,可以檢查這個特徵缺失的記錄另外兩項特徵 sondaughter 分別表示兒子、女兒的數量,如果為0,則將 minor_child 也填充為0。

print(data_origin[np.array(data_origin['minor_child'].isna())].loc[:, 'son'].sum())
print(data_origin[np.array(data_origin['minor_child'].isna())].loc[:, 'daughter'].sum())
data_origin[np.array(data_origin['minor_child'].isna())].loc[:, 'son':'daughter']
0
0
son daughter
id
2 0 0
5 0 0
9 0 0
29 0 0
31 0 0
... ... ...
7967 0 0
7972 0 0
7991 0 0
7999 0 0
8000 0 0

1066 rows × 2 columns

可以看對 minor_child 缺失的記錄,其兒子和女兒的個數也為0,所以將 minor_child 缺失值填充為0。

data_origin['minor_child'].fillna(0, inplace=True)

對於 marital_1st 的記錄的缺失情況,可以檢視對應的記錄的 marital 的取值是否為 1 表示 未婚。

print(data_origin[np.array(data_origin['marital_1st'].isna())]['marital'].sum() == data_origin[np.array(data_origin['marital_1st'].isna())]['marital'].shape[0])
data_origin[np.array(data_origin['marital_1st'].isna())]['marital'].plot.hist()
pd.value_counts(data_origin[np.array(data_origin['marital_1st'].isna())]['marital'])
True





1    828
Name: marital, dtype: int64

output_35_2

可以看到輸出結果表明對於 marital_1st 缺失的記錄都是未婚人士,所以缺失值正常。

下面檢視 s_birth 即 目前的配偶或同居伴侶是哪一年出生的的缺失情況,首先檢視缺失的記錄的 marital 狀態,檢視是否滿足無配偶或同居伴侶的情況。

data_origin[data_origin['s_birth'].isna()]['marital'].plot.hist()
pd.value_counts(data_origin[data_origin['s_birth'].isna()]['marital'])
1    828
7    718
6    171
2      1
Name: marital, dtype: int64

output_37_1

根據輸出可以看到,marital 取值為 167 分別表示 未婚、離婚和喪偶,所以 s_birth 缺失屬於正常;而且取值為 2 表示同居的缺失記錄只有一條,所以直接將其刪除即可。

data_origin.drop(data_origin[data_origin['s_birth'].isna()]['marital'][data_origin[data_origin['s_birth'].isna()]['marital'] == 2].index, inplace=True)

對於 marital_now 即 與目前的配偶是哪一年結婚的,首先輸出 marital 檢視婚姻的狀態,是否滿足沒結婚的條件。

data_origin[data_origin['marital_now'].isna()]['marital'].plot.hist()
pd.value_counts(data_origin[data_origin['marital_now'].isna()]['marital'])
1    828
7    718
6    171
2     51
3      1
Name: marital, dtype: int64

output_41_1

根據輸出可以得到 12 表示沒有結婚的情況,所以缺失屬於正常;

對於 367 分別表示 初婚有配偶、離婚、喪偶;只有 3 屬於目前有配偶並結婚的情況,所以應該刪除。

data_origin.drop(data_origin[data_origin['marital_now'].isna()].loc[data_origin[data_origin['marital_now'].isna()]['marital'] == 3].index, inplace=True)
data_origin.shape
(7989, 139)

對於 s_income 即 配偶或同居伴侶去年全年的總收入的缺失情況,可以檢查對於 marital 檢視其是否滿足無配偶或伴侶的條件。

data_origin[data_origin['s_income'].isna()]['marital'].plot.hist()
pd.value_counts(data_origin[data_origin['s_income'].isna()]['marital'])
1    828
7    718
6    171
Name: marital, dtype: int64

output_46_1

可以看到對於 s_income 的缺失值,其記錄對應的婚姻狀態都為未婚、離婚或喪偶,所以 s_income 缺失是正常的。

分類變數缺失值分析

檢視分型別(categorical)變數的缺失情況,全部為0,則沒有缺失值。

data_origin[categorical_list].isna().sum().to_frame().T
survey_type province gender nationality
0 0 0 0 0

所有特徵缺失值分析

檢視所有特徵的缺失情況。

data_origin.isna().sum()[data_origin.isna().sum() > 0].to_frame().T
edu_other edu_status edu_yr join_party property_other hukou_loc social_neighbor social_friend work_status work_yr ... marital_1st s_birth marital_now s_edu s_political s_hukou s_income s_work_exper s_work_status s_work_type
0 7986 1119 1969 7167 7923 4 795 795 5038 5038 ... 828 1717 1768 1717 1717 1717 1717 1717 5427 5427

1 rows × 23 columns

首先對於 edu_other 特徵,只有在 edu 填寫了 14 的情況下才填寫,首先檢查 edu_other 缺失的記錄的 edu 是否為 14 若為 14 則說明 edu_other 不應該為缺失,應該將其刪除。

data_origin[data_origin['edu_other'].isna()][data_origin[data_origin['edu_other'].isna()]['edu'] == 14]
happiness survey_type province city county survey_time gender birth nationality religion ... neighbor_familiarity public_service_1 public_service_2 public_service_3 public_service_4 public_service_5 public_service_6 public_service_7 public_service_8 public_service_9
id
1242 4 2 3 6 13 2015-09-24 17:58:00 1 1971 1 1 ... 5 100 90 60 80 70.0 80 70 60 50
3651 3 2 3 6 13 2015-09-24 20:25:00 1 1953 1 1 ... 5 100 100 60 50 70.0 50 30 70 40
5330 2 2 3 6 13 2015-09-25 07:57:00 1 1953 1 1 ... 5 100 100 100 100 100.0 100 30 100 50

3 rows × 139 columns

可以看到 edu14 的記錄中,有3條記錄 edu_other 也為缺失;所以將3條記錄刪除。

data_origin.drop(data_origin[data_origin['edu_other'].isna()][data_origin[data_origin['edu_other'].isna()]['edu'] == 14].index, inplace=True)

對於 edu_status 的缺失記錄,可以先檢查記錄對應的 edu 是取的何值。

data_origin[data_origin['edu_status'].isna()]['edu'].plot.hist()
pd.value_counts(data_origin[data_origin['edu_status'].isna()]['edu'])
1    1052
2      65
3       2
Name: edu, dtype: int64

output_57_1

可以看到對於 edu_status 缺失的記錄,其對應的 edu 教育程度為別為沒有受過任何教育、私塾、掃盲班和小學;對於取值為 12 的情況,屬於跳問選項,對應的 edu_status 屬於缺失是正常的;所以將 edu 取值為 3 的記錄刪除。

data_origin.drop(data_origin[data_origin['edu_status'].isna()][data_origin[data_origin['edu_status'].isna()]['edu'] == 3].index, inplace=True)

對於 join_party 即 目前政治面貌是黨員的入黨時間,只有政治面貌不是黨員的缺失值才算正確,檢視分佈情況。

data_origin[data_origin['join_party'].isna()]['political'].plot.hist()
pd.value_counts(data_origin[data_origin['join_party'].isna()]['political'])
 1    6703
 2     402
-8      41
 3      11
 4       5
Name: political, dtype: int64

output_61_1

根據直方圖看到,有5條記錄的 partical 的取值是 4 而入黨時間沒有填寫,所以將這5條記錄刪除。

data_origin.drop(data_origin[data_origin['join_party'].isna()][data_origin[data_origin['join_party'].isna()]['political'] == 4].index, inplace=True)

對於 hukou_loc 即 目前的戶口登記地,檢視缺失記錄的 hukou 登記情況,發現取值都為 7 即 沒有戶口,所以缺失屬於正常。

data_origin[data_origin['hukou_loc'].isna()]['hukou'].to_frame()
hukou
id
589 7
3657 7
3799 7
7811 7

對於 social_neighborsocial_friend 即與與其他朋友進行社交娛樂活動的頻繁程度和有多少個晚上是因為出去度假或者探訪親友而沒有在家過夜,首先檢視缺失記錄的 socialize 的分佈情況。

data_origin[data_origin['social_neighbor'].isna()]['socialize'].plot.hist()
pd.value_counts(data_origin[data_origin['social_neighbor'].isna()]['socialize'])
1    793
Name: socialize, dtype: int64

output_67_1

可以發現所有的 social_neighborsocial_friend 缺失記錄的 socialize 即 是否經常在空閒時間做社交的事情全部均為 1 即 從不社交,所以兩個特徵的缺失值可以使用 1 填充。

data_origin['social_neighbor'].fillna(1, inplace=True)
data_origin['social_friend'].fillna(1, inplace=True)

對於 s_edus_work_exper 的特徵,缺失值的記錄數都一樣,所以存在可能這幾項特徵的缺失記錄都來自同一批問卷物件。

首先檢視 s_edu 的缺失記錄的 marital 的分佈情況。

data_origin[data_origin['s_edu'].isna()]['marital'].plot.hist()
pd.value_counts(data_origin[data_origin['s_edu'].isna()]['marital'])
1    827
7    717
6    171
Name: marital, dtype: int64

output_71_1

可以發現 s_edu 缺失的記錄的婚姻情況全部均為未婚、離婚或喪偶,均屬於沒有配偶或同居伴侶的情況,所以屬於正常的缺失。

對於 s_politicals_work_exper 全部均屬於上述情況。

對於 s_work_status 即 配偶或同居伴侶目前的工作狀況,首先檢視調查問卷。

圖片

可以得知只有 s_work_exper 填寫了 1 的情況下才應該填寫 s_work_statuss_work_type 其它選項均需要跳過,所以屬於正常缺失值。

下面檢視 s_work_status 缺失記錄的 s_work_exper 的分佈情況。

data_origin[data_origin['s_work_status'].isna()]['s_work_exper'].plot.hist()
pd.value_counts(data_origin[data_origin['s_work_status'].isna()]['s_work_exper'])
5.0    1424
3.0    1017
4.0     823
6.0     221
2.0     217
1.0       1
Name: s_work_exper, dtype: int64

output_73_1

檢視得知 s_work_exper1 的記錄只有1條,直接刪除即可。

data_origin.drop(data_origin[data_origin['s_work_status'].isna()][data_origin[data_origin['s_work_status'].isna()]['s_work_exper'] == 1].index, inplace=True)

在調查問卷中,每個選項通用含義,其 -1 表示不適用;-2 表示不知道;-3 表示拒絕回答;-8 表示無法回答。

在這裡將所有的特徵的負數使用每一個特徵的中位數進行填充。

data_origin.shape
(7978, 139)
no_ne_rows_index = (data_origin.drop(['survey_time', 'edu_other', 'property_other', 'invest_other'], axis=1) < 0).sum(axis=1)[(data_origin.drop(['survey_time', 'edu_other', 'property_other', 'invest_other'], axis=1) < 0).sum(axis=1) == 0].index
for column, content in data_origin.items():
    if pd.api.types.is_numeric_dtype(content):
        data_origin[column] = data_origin[column].apply(lambda x : pd.Series(data_origin.loc[no_ne_rows_index, :][column].unique()).median() if(x < 0 and x != np.nan) else x)

將所有的負數填充完成後,再將 NaN 數值全部使用統一的一個值 -1 填充。

data_origin.fillna(-1, inplace=True)

至此,所有特徵的缺失值已經全部處理完畢。

文字資料處理

在所有的特徵中,有3個特徵分別是 edu_otherproperty_otherinvest_other 是字串資料,需要將其轉換成序號編碼(Ordinal Encoding)。

首先檢視 edu_other 的填寫情況。

data_origin[data_origin['edu_other'] != -1]['edu_other'].to_frame()
edu_other
id
1170 夜校
2513 夜校
4926 夜校

可以看到 edu_other 的填寫情況全都是夜校,將字串轉換成序號編碼。

data_origin['edu_other'] = data_origin['edu_other'].astype('category').values.codes + 1

檢視 property_other 即 房子產權歸屬誰,首先檢查調查問卷的填寫情況。

data_origin[data_origin['property_other'] != -1]['property_other'].to_frame()
property_other
id
76 無產權
92 已購買,但未過戶
99 家庭共同所有
132 待辦
455 沒有產權
... ...
7376 家人共有
7746 全家人共有
7776 兄弟共有
7821 未分家,全家所有
7917 家人共有

66 rows × 1 columns

根據填寫情況來看,其中有很多填寫資訊都是一個意思,例如 家庭共同所有全家所有 是同一個意思,但是在python處理中只能一個個的手動處理。

#data_origin.loc[[8009, 9212, 9759, 10517], 'property_other'] = '多人擁有'
#data_origin.loc[[8014, 8056, 10264], 'property_other'] = '未過戶'
#data_origin.loc[[8471, 8825, 9597, 9810, 9842, 9967, 10069, 10166, 10203, 10469], 'property_other'] = '全家擁有'
#data_origin.loc[[8553, 8596, 9605, 10421, 10814], 'property_other'] = '無產權'
data_origin.loc[[76, 132, 455, 495, 1415, 2511, 2792, 2956, 3647, 4147, 4193, 4589, 5023, 5382, 5492, 6102, 6272, 6339, 
                6507, 7184, 7239], 'property_other'] = '無產權'
data_origin.loc[[92, 1888, 2703, 3381, 5654], 'property_other'] = '未過戶'
data_origin.loc[[99, 619, 2728, 3062, 3222, 3251, 3696, 5283, 6191, 7295, 7376, 7746, 7821, 7917], 'property_other'] = '全家擁有'
data_origin.loc[[1597, 4993, 5398, 5899, 7240, 7776], 'property_other'] = '多人擁有'
data_origin.loc[[6469, 6891], 'property_other'] = '小產權'

將字串編碼為整數型的序號(ordinal)型別。

data_origin['property_other'] = data_origin['property_other'].astype('category').values.codes + 1

檢視 invest_other 即 從事的投資活動的填寫情況。

pd.DataFrame(data_origin[data_origin['invest_other'] != -1]['invest_other'].unique())
0
0 理財產品
1 民間借貸
2 銀行理財
3 儲蓄存款
4 理財
5 銀行存款利息
6 活期儲蓄
7 投資服務業、傢俱業
8 銀行存款
9 個人融資
10 租房
11 老人家不清楚
12 家中有部分土地承包出去
13 沒有
14 高利貸
15 彩票
16 自己沒有,兒女不清楚
17 網上理財
18 統籌
19 福利車票
20 其他理財產品
21 商業萬能保險
22 投資開發區
23 字畫、茶壺

同樣地,將其轉換成整數型別的序號(ordinal)編碼。

data_origin['invest_other'] = data_origin['invest_other'].astype('category').values.codes + 1

離群值處理

data_nona = data_origin.copy()

畫出箱型圖分析特徵的異常值。

並刪除離群記錄。

sns.boxplot(x=data_nona['house'])
<AxesSubplot:xlabel='house'>

output_100_1

data_nona.drop(data_nona[data_nona['house'] > 25].index, inplace=True)
sns.boxplot(x=data_nona['family_m'])
<AxesSubplot:xlabel='family_m'>

output_102_1

data_nona.drop(data_nona[data_nona['family_m'] > 40].index, inplace=True)
sns.boxplot(x=data_nona['inc_exp'])
<AxesSubplot:xlabel='inc_exp'>

output_104_1

data_nona.drop(data_nona[data_nona['inc_exp'] > 0.6e8].index, inplace=True)

檢視調查時間的月份分佈情況,因為調查問卷都是在2015年填寫,只需要檢視月份的離群點。

圖片

由圖可知調查問卷是從6月開始的,記錄中2月的問卷屬於異常資料,應該刪除。

sns.boxplot(x=data_nona['survey_time'].dt.month)
<AxesSubplot:xlabel='survey_time'>

output_107_1

data_nona.drop(data_nona[data_nona['survey_time'].dt.month < 6].index, inplace=True)

特徵構造

特徵構造也可稱為特徵交叉、特徵組合、資料變換。

連續變數離散化

離散化除了一些計算方面等等好處,還可以引入非線性特性,也可以很方便的做cross-feature。離散特徵的增加和減少都很容易,易於模型的快速迭代。此外,噪聲很大的環境中,離散化可以降低特徵中包含的噪聲,提升特徵的表達能力。

pd.DataFrame(continuous_list)
0
0 birth
1 edu_yr
2 income
3 floor_area
4 height_cm
5 weight_jin
6 work_yr
7 family_income
8 family_m
9 house
10 son
11 daughter
12 minor_child
13 marital_1st
14 s_birth
15 marital_now
16 s_income
17 f_birth
18 m_birth
19 inc_exp
20 public_service_1
21 public_service_2
22 public_service_3
23 public_service_4
24 public_service_5
25 public_service_6
26 public_service_7
27 public_service_8
28 public_service_9

將連續型變數全部進行分箱,然後對每個區間進行編碼,生成新的離散的特徵。

for column in continuous_list:
    cut = pd.qcut(data_nona[column], q=5, duplicates='drop')
    cat = cut.values
    codes = cat.codes
    data_nona[column + '_discrete'] = codes
for column, content in data_nona.items():
    if pd.api.types.is_numeric_dtype(content):
        data_nona[column] = content.astype('int')

特徵選擇

將連續變數離散化後,生成以字尾 _discrete 的新特徵,所以將原來的連續變數的特徵刪除掉。

data_nona.to_csv('./data/happiness_train_complete_analysis.csv')
data_nona.drop(continuous_list, axis=1, inplace=True)
data_nona.to_csv('./data/happiness_train_complete_nona.csv')

特徵分析

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
import seaborn as sns
data = pd.read_csv('./data/happiness_train_complete_analysis.csv', index_col='id', parse_dates=['survey_time'])
data.head()
happiness survey_type province city county survey_time gender birth nationality religion ... inc_exp_discrete public_service_1_discrete public_service_2_discrete public_service_3_discrete public_service_4_discrete public_service_5_discrete public_service_6_discrete public_service_7_discrete public_service_8_discrete public_service_9_discrete
id
1 4 1 12 32 59 2015-08-04 14:18:00 1 1959 1 1 ... 2 0 0 0 0 0 0 0 0 0
2 4 2 18 52 85 2015-07-21 15:04:00 1 1992 1 1 ... 2 4 1 2 3 4 1 4 0 0
3 4 2 29 83 126 2015-07-21 13:24:00 2 1967 1 0 ... 3 4 2 3 3 3 4 4 4 2
4 5 2 10 28 51 2015-07-25 17:33:00 2 1943 1 1 ... 0 4 3 2 3 3 4 4 3 2
5 4 1 7 18 36 2015-08-10 09:50:00 2 1994 1 1 ... 4 0 0 0 0 0 0 0 0 0

5 rows × 168 columns

data.describe()
happiness survey_type province city county gender birth nationality religion religion_freq ... inc_exp_discrete public_service_1_discrete public_service_2_discrete public_service_3_discrete public_service_4_discrete public_service_5_discrete public_service_6_discrete public_service_7_discrete public_service_8_discrete public_service_9_discrete
count 7968.000000 7968.000000 7968.000000 7968.000000 7968.000000 7968.000000 7968.000000 7968.000000 7968.000000 7968.000000 ... 7968.000000 7968.000000 7968.000000 7968.000000 7968.000000 7968.000000 7968.000000 7968.000000 7968.000000 7968.000000
mean 3.866466 1.405120 15.158258 42.572164 70.631903 1.530748 1964.710216 1.399724 0.880271 1.452560 ... 1.725653 1.665537 1.272214 1.841365 1.613328 1.848519 1.643449 1.651732 1.654869 1.302962
std 0.818844 0.490946 8.915876 27.183764 38.736751 0.499085 16.845155 1.466409 0.324665 1.358444 ... 1.338535 1.420309 1.108440 1.342524 1.499494 1.297290 1.533445 1.544477 1.511468 1.078601
min 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1921.000000 1.000000 0.000000 1.000000 ... 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
25% 4.000000 1.000000 7.000000 18.000000 37.000000 1.000000 1952.000000 1.000000 1.000000 1.000000 ... 1.000000 0.000000 0.000000 1.000000 0.000000 1.000000 0.000000 0.000000 0.000000 0.000000
50% 4.000000 1.000000 15.000000 42.000000 73.000000 2.000000 1965.000000 1.000000 1.000000 1.000000 ... 1.000000 2.000000 1.000000 2.000000 1.000000 2.000000 1.000000 1.000000 1.000000 1.000000
75% 4.000000 2.000000 22.000000 65.000000 104.000000 2.000000 1977.000000 1.000000 1.000000 1.000000 ... 3.000000 2.000000 2.000000 3.000000 3.000000 3.000000 3.000000 3.000000 3.000000 2.000000
max 5.000000 2.000000 31.000000 89.000000 134.000000 2.000000 1997.000000 8.000000 1.000000 9.000000 ... 4.000000 4.000000 3.000000 4.000000 4.000000 4.000000 4.000000 4.000000 4.000000 3.000000

8 rows × 167 columns

data.info(verbose=True, null_counts=True)
<class 'pandas.core.frame.DataFrame'>
Int64Index: 7968 entries, 1 to 8000
Data columns (total 168 columns):
 #   Column                     Non-Null Count  Dtype         
---  ------                     --------------  -----         
 0   happiness                  7968 non-null   int64         
 1   survey_type                7968 non-null   int64         
 2   province                   7968 non-null   int64         
 3   city                       7968 non-null   int64         
 4   county                     7968 non-null   int64         
 5   survey_time                7968 non-null   datetime64[ns]
 6   gender                     7968 non-null   int64         
 7   birth                      7968 non-null   int64         
 8   nationality                7968 non-null   int64         
 9   religion                   7968 non-null   int64         
 10  religion_freq              7968 non-null   int64         
 11  edu                        7968 non-null   int64         
 12  edu_other                  7968 non-null   int64         
 13  edu_status                 7968 non-null   int64         
 14  edu_yr                     7968 non-null   int64         
 15  income                     7968 non-null   int64         
 16  political                  7968 non-null   int64         
 17  join_party                 7968 non-null   int64         
 18  floor_area                 7968 non-null   int64         
 19  property_0                 7968 non-null   int64         
 20  property_1                 7968 non-null   int64         
 21  property_2                 7968 non-null   int64         
 22  property_3                 7968 non-null   int64         
 23  property_4                 7968 non-null   int64         
 24  property_5                 7968 non-null   int64         
 25  property_6                 7968 non-null   int64         
 26  property_7                 7968 non-null   int64         
 27  property_8                 7968 non-null   int64         
 28  property_other             7968 non-null   int64         
 29  height_cm                  7968 non-null   int64         
 30  weight_jin                 7968 non-null   int64         
 31  health                     7968 non-null   int64         
 32  health_problem             7968 non-null   int64         
 33  depression                 7968 non-null   int64         
 34  hukou                      7968 non-null   int64         
 35  hukou_loc                  7968 non-null   int64         
 36  media_1                    7968 non-null   int64         
 37  media_2                    7968 non-null   int64         
 38  media_3                    7968 non-null   int64         
 39  media_4                    7968 non-null   int64         
 40  media_5                    7968 non-null   int64         
 41  media_6                    7968 non-null   int64         
 42  leisure_1                  7968 non-null   int64         
 43  leisure_2                  7968 non-null   int64         
 44  leisure_3                  7968 non-null   int64         
 45  leisure_4                  7968 non-null   int64         
 46  leisure_5                  7968 non-null   int64         
 47  leisure_6                  7968 non-null   int64         
 48  leisure_7                  7968 non-null   int64         
 49  leisure_8                  7968 non-null   int64         
 50  leisure_9                  7968 non-null   int64         
 51  leisure_10                 7968 non-null   int64         
 52  leisure_11                 7968 non-null   int64         
 53  leisure_12                 7968 non-null   int64         
 54  socialize                  7968 non-null   int64         
 55  relax                      7968 non-null   int64         
 56  learn                      7968 non-null   int64         
 57  social_neighbor            7968 non-null   int64         
 58  social_friend              7968 non-null   int64         
 59  socia_outing               7968 non-null   int64         
 60  equity                     7968 non-null   int64         
 61  class                      7968 non-null   int64         
 62  class_10_before            7968 non-null   int64         
 63  class_10_after             7968 non-null   int64         
 64  class_14                   7968 non-null   int64         
 65  work_exper                 7968 non-null   int64         
 66  work_status                7968 non-null   int64         
 67  work_yr                    7968 non-null   int64         
 68  work_type                  7968 non-null   int64         
 69  work_manage                7968 non-null   int64         
 70  insur_1                    7968 non-null   int64         
 71  insur_2                    7968 non-null   int64         
 72  insur_3                    7968 non-null   int64         
 73  insur_4                    7968 non-null   int64         
 74  family_income              7968 non-null   int64         
 75  family_m                   7968 non-null   int64         
 76  family_status              7968 non-null   int64         
 77  house                      7968 non-null   int64         
 78  car                        7968 non-null   int64         
 79  invest_0                   7968 non-null   int64         
 80  invest_1                   7968 non-null   int64         
 81  invest_2                   7968 non-null   int64         
 82  invest_3                   7968 non-null   int64         
 83  invest_4                   7968 non-null   int64         
 84  invest_5                   7968 non-null   int64         
 85  invest_6                   7968 non-null   int64         
 86  invest_7                   7968 non-null   int64         
 87  invest_8                   7968 non-null   int64         
 88  invest_other               7968 non-null   int64         
 89  son                        7968 non-null   int64         
 90  daughter                   7968 non-null   int64         
 91  minor_child                7968 non-null   int64         
 92  marital                    7968 non-null   int64         
 93  marital_1st                7968 non-null   int64         
 94  s_birth                    7968 non-null   int64         
 95  marital_now                7968 non-null   int64         
 96  s_edu                      7968 non-null   int64         
 97  s_political                7968 non-null   int64         
 98  s_hukou                    7968 non-null   int64         
 99  s_income                   7968 non-null   int64         
 100 s_work_exper               7968 non-null   int64         
 101 s_work_status              7968 non-null   int64         
 102 s_work_type                7968 non-null   int64         
 103 f_birth                    7968 non-null   int64         
 104 f_edu                      7968 non-null   int64         
 105 f_political                7968 non-null   int64         
 106 f_work_14                  7968 non-null   int64         
 107 m_birth                    7968 non-null   int64         
 108 m_edu                      7968 non-null   int64         
 109 m_political                7968 non-null   int64         
 110 m_work_14                  7968 non-null   int64         
 111 status_peer                7968 non-null   int64         
 112 status_3_before            7968 non-null   int64         
 113 view                       7968 non-null   int64         
 114 inc_ability                7968 non-null   int64         
 115 inc_exp                    7968 non-null   int64         
 116 trust_1                    7968 non-null   int64         
 117 trust_2                    7968 non-null   int64         
 118 trust_3                    7968 non-null   int64         
 119 trust_4                    7968 non-null   int64         
 120 trust_5                    7968 non-null   int64         
 121 trust_6                    7968 non-null   int64         
 122 trust_7                    7968 non-null   int64         
 123 trust_8                    7968 non-null   int64         
 124 trust_9                    7968 non-null   int64         
 125 trust_10                   7968 non-null   int64         
 126 trust_11                   7968 non-null   int64         
 127 trust_12                   7968 non-null   int64         
 128 trust_13                   7968 non-null   int64         
 129 neighbor_familiarity       7968 non-null   int64         
 130 public_service_1           7968 non-null   int64         
 131 public_service_2           7968 non-null   int64         
 132 public_service_3           7968 non-null   int64         
 133 public_service_4           7968 non-null   int64         
 134 public_service_5           7968 non-null   int64         
 135 public_service_6           7968 non-null   int64         
 136 public_service_7           7968 non-null   int64         
 137 public_service_8           7968 non-null   int64         
 138 public_service_9           7968 non-null   int64         
 139 birth_discrete             7968 non-null   int64         
 140 edu_yr_discrete            7968 non-null   int64         
 141 income_discrete            7968 non-null   int64         
 142 floor_area_discrete        7968 non-null   int64         
 143 height_cm_discrete         7968 non-null   int64         
 144 weight_jin_discrete        7968 non-null   int64         
 145 work_yr_discrete           7968 non-null   int64         
 146 family_income_discrete     7968 non-null   int64         
 147 family_m_discrete          7968 non-null   int64         
 148 house_discrete             7968 non-null   int64         
 149 son_discrete               7968 non-null   int64         
 150 daughter_discrete          7968 non-null   int64         
 151 minor_child_discrete       7968 non-null   int64         
 152 marital_1st_discrete       7968 non-null   int64         
 153 s_birth_discrete           7968 non-null   int64         
 154 marital_now_discrete       7968 non-null   int64         
 155 s_income_discrete          7968 non-null   int64         
 156 f_birth_discrete           7968 non-null   int64         
 157 m_birth_discrete           7968 non-null   int64         
 158 inc_exp_discrete           7968 non-null   int64         
 159 public_service_1_discrete  7968 non-null   int64         
 160 public_service_2_discrete  7968 non-null   int64         
 161 public_service_3_discrete  7968 non-null   int64         
 162 public_service_4_discrete  7968 non-null   int64         
 163 public_service_5_discrete  7968 non-null   int64         
 164 public_service_6_discrete  7968 non-null   int64         
 165 public_service_7_discrete  7968 non-null   int64         
 166 public_service_8_discrete  7968 non-null   int64         
 167 public_service_9_discrete  7968 non-null   int64         
dtypes: datetime64[ns](1), int64(167)
memory usage: 10.3 MB

首先,檢視 happiness 幸福程度的分佈,可以發現多數人都屬於 比較幸福 的程度。

sns.set_theme(style="darkgrid")
sns.displot(data, x="happiness", facet_kws=dict(margin_titles=True))
<seaborn.axisgrid.FacetGrid at 0x25128009850>

output_129_1

檢視每個人的收入和幸福度的散點圖,通過散點圖可以看出隨著收入的提高,大多數點都落在了較高的幸福程度上;即使如此,也會發現存在一些收入非常高的人也處在一個說不上幸福不幸福的程度。

sns.set_theme(style="whitegrid")
f, ax = plt.subplots()
sns.despine(f, left=True, bottom=True)
sns.scatterplot(x="happiness", y="income",
                size="income",
                palette="ch:r=-.2,d=.3_r",
                data=data, ax=ax)
<AxesSubplot:xlabel='happiness', ylabel='income'>

output_131_1

檢視性別男女的幸福程度的分佈直方圖,在性別特徵上沒有過多的類別不平衡情況。

sns.set_theme(style="darkgrid")
sns.displot(
    data, x="happiness", col="gender",
    facet_kws=dict(margin_titles=True)
)
<seaborn.axisgrid.FacetGrid at 0x25128b8e1f0>

output_133_1

通過直線圖,可以看出,隨著 edu 受到的教育的提高,幸福程度也隨之提升。

sns.set_theme(style="ticks")
palette = sns.color_palette("rocket_r")
sns.relplot(
    data=data,
    x="edu", y="happiness",
    kind="line", size_order=["T1", "T2"], palette=palette,
    facet_kws=dict(sharex=False)
)
<seaborn.axisgrid.FacetGrid at 0x251284bdeb0>

output_135_0

檢視每個幸福程度的出生日期,可以看出,不同幸福程度的年代的人分佈都是大同小異的。

sns.set_theme(style="ticks", palette="pastel")
sns.boxplot(x="happiness", y="birth",
            data=data)
sns.despine(offset=10, trim=True)

output_135_0

將記錄分為是否信仰宗教信仰,檢視幸福度和健康狀況的分裂小提琴圖,也可以看出一個趨勢,幸福度高的人大多數都分佈在較高的健康狀況上,而且也可以看出一個現象,隨著健康狀況和幸福度的提高,信仰宗教信仰的人數也慢慢增加。

sns.set_theme(style="whitegrid")
sns.violinplot(data=data, x="happiness", y="health", hue="religion",
               split=True, inner="quart", linewidth=1)
sns.despine(left=True)

output_137_0

繪製一個多變數分佈直方圖,可以看出大多數比較幸福的人,房產的數量也不會大幅增加。

import seaborn as sns
sns.set_theme(style="ticks")
g = sns.JointGrid(data=data, x="happiness", y="house", marginal_ticks=True)

# Set a log scaling on the y axis
g.ax_joint.set(yscale="linear")

# Create an inset legend for the histogram colorbar
cax = g.fig.add_axes([.15, .55, .02, .2])

# Add the joint and marginal histogram plots
g.plot_joint(
    sns.histplot, discrete=(True, False),
    cmap="light:#03012d", pmax=.8, cbar=True, cbar_ax=cax
)
g.plot_marginals(sns.histplot, element="step", color="#03012d")
<seaborn.axisgrid.JointGrid at 0x251288fee20>

output_139_1

繪製幸福度和住房建築面積的核密度估計圖,可以看出同樣的現象,多數比較幸福的人的房屋建築面積也不會集中在很高的一個水平,但是也會有一個隨著房屋建築面積的增加幸福度也增加的現象。

sns.set_theme(style="ticks")
g = sns.jointplot(
    data=data[data['floor_area'] < 600],
    x="happiness", y="floor_area",
    kind="kde",
)

output_141_0

檢視各個特徵的熱力圖,可以根據圖中的顏色深度看出兩兩特徵之間的相關性的高低。

sns.set_theme(style="whitegrid")
corr_list = ['survey_type', 'province', 'city', 'county', 'survey_time', 'gender', 'birth', 'nationality', 'religion',
                 'religion_freq', 'edu', 'income', 'political', 'floor_area', 'height_cm', 'weight_jin', 'health', 'health_problem',
                 'depression', 'hukou', 'socialize', 'relax', 'learn', 'equity', 'class', 'work_exper', 'work_status', 'work_yr', 'work_type',
                 'work_manage', 'family_income', 'family_m', 'family_status', 'house', 'car', 'marital', 'status_peer', 'status_3_before', 
                 'view', 'inc_ability']
df = data
corr_mat = data[corr_list].corr().stack().reset_index(name="correlation")
g = sns.relplot(
    data=corr_mat,
    x="level_0", y="level_1", hue="correlation", size="correlation",
    palette="vlag", hue_norm=(-1, 1), edgecolor=".7",
    height=10, sizes=(50, 250), size_norm=(-.2, .8),
)
g.set(xlabel="", ylabel="", aspect="equal")
g.despine(left=True, bottom=True)
g.ax.margins(.02)
for label in g.ax.get_xticklabels():
    label.set_rotation(90)
for artist in g.legend.legendHandles:
    artist.set_edgecolor(".7")

output_143_0

檢視全國省會城市的幸福人數的佔比條形圖,通過圖中可以看出,湖北省調查人數最多但幸福人數不算高;河南省和山東省的幸福人數的佔比非常之高;即使內蒙古自治區的調查人數最少,但是幸福人數的佔比卻是非常高的。

sns.set_theme(style="whitegrid")

province_total = data['province'].groupby(data['province']).count().sort_values(ascending=False).to_frame()
province_total.columns = ['total']
happiness_involved = []
for index in province_total.index:
    happiness_involved.append((data[data['province'] == index][data[data['province'] == index]['happiness'] > 3].shape[0]))
happiness_involved = pd.DataFrame(happiness_involved, index=province_total.index)
happiness_involved.columns = ['involved']
province_total['province'] = province_total.index.map({
    1 : 'Shanghai', 2 : 'Yunnan', 3 : 'Neimeng', 4 : 'Beijing', 5 : 'Jilin', 6 : 'Sichuan', 7 : 'Tianjin', 8 : 'Ningxia',
    9 : 'Anhui', 10 : 'Shandong', 11 : 'Shanxi', 12 : 'Guangdong', 13 : 'Guangxi', 14 : 'Xinjiang', 15 : 'Jiangsu',
    16 : 'Jiangxi', 17 : 'Hebei', 18 : 'Henan', 19 : 'Zhejiang', 20 : 'Hainan', 21 : 'Hubei', 22 : 'Hunan', 23 : 'Gansu',
    24 : 'Fujian', 25 : 'XIzang', 26 : 'Guizhou', 27 : 'Liangning', 28 : 'Chongqing', 29 : 'Shaanxi', 30 : 'Qinghai', 31 : 'Heilongjiang'})
happiness_involved['province'] = province_total['province']

f, ax = plt.subplots(figsize=(6, 15))

sns.set_color_codes("pastel")
sns.barplot(x="total", y="province", data=province_total,
            label="Total", color="b")

sns.set_color_codes("muted")
sns.barplot(x="involved", y="province", data=happiness_involved,
            label="Alcohol-involved", color="b")

ax.legend(ncol=2, loc="lower right", frameon=True)
ax.set(ylabel="", xlabel="Happiness of every province")
sns.despine(left=True, bottom=True)

output_145_0

檢視調查物件認為的當今社會的公平度中的幸福人數佔比的直方圖,多數調查物件認為當今社會是出於一個比較公平的,但仍有近半數人認為不算太公平。

sns.set_theme(style="ticks")
f, ax = plt.subplots(figsize=(7, 5))
sns.despine(f)
sns.histplot(
    data, hue='happiness',
    x="equity",
    multiple="stack",
    palette="light:m_r",
    edgecolor=".3",
    linewidth=.5
)
<AxesSubplot:xlabel='equity', ylabel='Count'>

output_147_1

根據多變數的散點圖,幸福度高的人的都均勻地分佈在了不同身高、體重的地方;體形沒有太大地影響幸福度。

sns.set_theme(style="white")
sns.relplot(x="height_cm", y="weight_jin", hue="happiness", size="health",
             alpha=.5, palette="muted", data=data)
<seaborn.axisgrid.FacetGrid at 0x2512b5a2ca0>

output_149_1

繪製一個帶有誤差帶的直線圖,橫軸表示幸福度的提升,縱軸表示期待的年收入的提升,可以看出,在幸福度比較低的人期待的年收入通常會很高並帶有非常大的誤差,隨著幸福度的提升每個人期待的年收入也沒有變得更高,並且隨之誤差帶也變小了。

sns.set_theme(style="ticks")
palette = sns.color_palette("rocket_r")
sns.relplot(
    data=data,
    x="happiness", y="inc_exp",
    kind="line", palette=palette,
    aspect=.75, facet_kws=dict(sharex=False)
)
<seaborn.axisgrid.FacetGrid at 0x2512b5e8d90>

output_151_1


模型建立

XGBoost 模型介紹

XGBoost 是一個具有高效、靈活和可移植性的經過優化的分散式 梯度提升 庫。它的實現是基於機器學習演算法梯度提升框架。XGBoost 提供了並行的提升樹(例如GBDT、GBM)以一個非常快速並且精準的方法解決了許多的資料科學問題。相同的程式碼可以執行在主流的分散式環境(如Hadoop、SGE、MPI)並且可以處理數十億的樣本。

XGBoost代表了極端梯度提升(Extreme Gradient Boosting)。

整合決策樹

首先了解XGBoost的模型選擇:整合決策樹。樹的整合模型是由CART(classification and regression trees)的集合組成。下面一張圖簡單說明了一個CART分出某個人是否喜歡玩電腦遊戲的例子。

圖片

將每個家庭成員分到不同的葉子結點上,並賦給他們一個分數,每一個葉結點對應了一個分數。CART與決策樹是略有不同的,決策樹中每個葉結點只包含了一個決策值。在CART上,真實的分數是與葉結點關聯的,可以給出比分類更豐富的解釋。這也允許了更具有原則、更一致性的優化方法。

通常,在實踐中一個單獨的樹是不夠強大的。實際上使用的是整合模型,將多個樹的預測結果彙總到一起。

圖片

上圖中是一個由兩棵樹整合在一起的例子。每一個樹的預測分數被加到一起得到最終的分數。一個重要的因素是兩棵樹努力補足彼此。可以寫出模型:

\[\hat{y}_i=\sum_{k=1}^Kf_k\left (x_i\right ),f_k\in\mathcal{F} \]

其中,\(K\) 是樹的數量,\(f\) 是一個在函式空間 \(\mathcal{F}\) 的函式,並且 \(\mathcal{F}\) 是一個所有可能的CART的集合。可被優化的目標函式為:

\[\mathit{obj}\left (\theta\right )=\sum_i^n\ell\left (y_i,\hat{y}_i\right )+\sum_{k=1}^K\Omega\left (f_k\right ) \]

隨機森林和提升樹實際上都是相同的模型;不同之處是如何去訓練它們。如果需要一個用來預測的整合樹,只需要寫出一個並其可以工作在隨機森林和提升樹上。

提升樹

正如同所有的監督學習一樣,想要訓練樹就要先定義目標函式並優化它。

一個目標函式要總是包含訓練的損失度和正則化項。

\[\mathit{obj}=\sum_i^n\ell\left (y_i,\hat{y}_i\right )+\sum_{k=1}^K\Omega\left (f_k\right ) \]

加性訓練

樹需要訓練的引數有 \(f_i\) 每一個都包含了樹的結構和葉結點的得分。訓練樹的結構是比傳統的可以直接採用梯度的優化問題更難。一次性訓練並學習到所有的樹是非常棘手的。相反地,可以採取一個附加的策略,修正已經學習到的,同時增加一課新樹。可以寫出在第 \(t\) 步的預測值 \(\hat{y}_i^\left(t\right )\)

\[\begin{split} \hat{y}_i^{\left (0\right )}&=0\\ \hat{y}_i^{\left (1\right )}&=f_1\left (x_i\right )=\hat{y}_i^{\left (0\right )}+f_1\left (x_i\right )\\ \hat{y}_i^{\left (2\right )}&=f_1\left (x_i\right )+f_2\left (x_i\right )=\hat{y}_i^{\left (1\right )}+f_2\left (x_i\right )\\ &\dots\\ \hat{y}_i^{\left (t\right )}&=\sum_{k=1}^Kf_k\left (x_i\right )=\hat{y}_i^{\left (t-1\right )}+f_t\left (x_i\right )\\ \end{split} \]

在每一步需要什麼的樹,增加一棵樹,優化目標函式。

\[\begin{split} \mathit{obj}^{(t)}&=\sum_{i=1}^n\ell(y_i,\hat{y}_i^{(t)})+\sum_{i=1}^t\Omega(f_i)\\ &=\sum_{i=1}^n\ell(y_i,\hat{y}^{(t-1)}_i+f_t(x_i))+\Omega(f_t)+C \end{split} \]

如果考慮使用均方誤差(MSE)作為損失函式,目標函式將會變成:

\[\begin{split} \mathit{obj}^{(t)}&=\sum_{i=1}^n\ell(y_i,\hat{y}^{(t-1)}_i+f_t(x_i))+\Omega(f_t)+C\\ &=\sum_{i=1}^n(y_i-(\hat{y}_i^{(t-1)}+f_t(x_i)))^2+\Omega(f_t)+C\\ &=\sum_{i=1}^n((y_i-\hat{y}_i^{(t-1)})-f_t(x_i))^2+\Omega(f_t)+C\\ &=\sum_{i=1}^n((y_i-\hat{y}_i^{(t-1)})^2-2(y_i-\hat{y}_i^{(t-1)})f_t(x_i)+f_t(x_i)^2)+\Omega(f_t)+C\\ &=\sum_{i=1}^n(-2(y_i-\hat{y}_i^{(t-1)})f_t(x_i)+f_t(x_i)^2)+\Omega(f_t)+C\\ \end{split} \]

MSE的形式是非常優雅的,其中有一個一階項(通常稱作殘差)和一個二階項。對於其它的損失函式(例如logistic的損失函式)而言,是沒有那麼輕易就可以得到如此優雅的形式。因此,通常會使用泰勒公式損失函式展開到二階項:

泰勒公式:函式 \(f(x)\) 在開區間 \((a,b)\) 上具有 \((n+1)\) 階導數,對於任一 \(x\in(a,b)\)

\[f(x)=\frac{f(x_0)}{0!}+\frac{f'(x_0)}{1!}(x-x_0)+\frac{f''(x_0)}{2!}(x-x_0)^2+\dots+\frac{f^{(n)}(x_0)}{n!}(x-x_0)^n+R_n(x) \]

\[\begin{split} \mathit{obj}^{(t)}&=\sum_{i=1}^n\ell(y_i,\hat{y}^{(t-1)}_i+f_t(x_i))+\Omega(f_t)+C\\ &=\sum_{i=1}^n[\frac{\ell(y_i,\hat{y}^{(t-1)}_i)}{0!}+\frac{\ell'(y_i,\hat{y}^{(t-1)}_i)}{1!}(\hat{y}^{(t)}_i-\hat{y}^{(t-1)}_i)+\frac{\ell''(y_i,\hat{y}^{(t-1)}_i)}{2!}(\hat{y}^{(t)}_i-\hat{y}^{(t-1)}_i)^2]+\Omega(f_t)+C\\ &=\sum_{i=1}^n[\ell(y_i,\hat{y}^{(t-1)}_i)+\ell'(y_i,\hat{y}^{(t-1)}_i)f_t(x_i)+\frac{1}{2}\ell''(y_i,\hat{y}^{(t-1)}_i)f_t(x_i)^2]+\Omega(f_t)+C\\ &=\sum_{i=1}^n[\ell(y_i,\hat{y}^{(t-1)}_i)+g_if_t(x_i)+\frac{1}{2}h_if_t(x_i)^2]+\Omega(f_t)+C\\ \end{split} \]

其中,\(g_i\)\(h_i\) 被定義為:

\[\begin{split} g_i&=\partial_{\hat{y}_i^{(t-1)}}\ell(y_i,\hat{y}^{(t-1)}_i)\\ h_i&=\partial^2_{\hat{y}^{(t-1)}_i}\ell(y_i,\hat{y}^{(t-1)}_i) \end{split} \]

移除所有的常量,在第 \(t\) 步的目標函式就成了:

\[\sum_{i=1}^n[g_if_t(x_i)+\frac{1}{2}h_if_t(x_i)^2]+\Omega(f_t) \]

這就成了對於一顆新樹的優化目標。一個非常重要的優勢就是這個定義的目標函式的值只依賴於 \(g_i\)\(h_i\) 這正是XGBoost支援自定義損失函式。可以優化各種損失函式,包括邏輯迴歸和成對排名(pairwise ranking),使用 \(g_i\)\(h_i\) 作為輸入的完全相同的求解器求解。

模型複雜度

定義樹的複雜度 \(\Omega(f)\) 。首先提煉出樹的定義 \(f(x)\) 為:

\[f_t(x)=w_{q(x)},w\in\mathbb{R}^T,q:\mathbb{R}^d\rightarrow \{1,2,\dots,T\}. \]

其中 \(w\) 是葉結點上的得分向量,\(q\) 是一個將每一個資料點分配到對應的葉結點上的函式,\(T\) 是葉結點的數量。在XGBoost中,定義複雜度為:

\[\Omega(f)=\gamma T+\frac{1}{2}\lambda\sum_{j=1}^Tw_j^2 \]

有不止一個方法定義複雜度,但是這種方式在實踐中可以表現的很好。正則化項是大多數樹包都會被忽略的一部分。這是因為傳統的樹學習的對待僅僅強調改善雜質,模型的複雜度的控制留給了啟發式。通過正式的定義它,可以更好的理解模型並使模型的表現更具有泛化能力。

樹的結構分數

通過對樹模型的目標函式的推導,可以得到在第 \(t\) 步的樹的目標值:

\[\begin{split} \mathit{obj}^{(t)}&\approx\sum_{i=1}^n[g_iw_{q(x_i)}+\frac{1}{2}h_iw^2_{q(x_i)}]+\gamma T+\frac{1}{2}\lambda\sum_{j=1}^Tw_j^2\\ &=[g_1w_{q(x_1)}+\frac{1}{2}h_1w^2_{q(x_1)}+g_2w_{q(x_2)}+\frac{1}{2}h_2w^2_{q(x_2)}+\dots+g_nw_{q(x_n)}+\frac{1}{2}h_nw^2_{q(x_n)}]+\gamma T+\frac{1}{2}\lambda\sum_{j=1}^Tw_j^2\\ &=\sum_{j=1}^T[(\sum_{i\in I_j}g_i)w_j+\frac{1}{2}(\sum_{i\in I_j}h_i)w^2_j]+\gamma T+\frac{1}{2}\lambda\sum_{j=1}^Tw_j^2\\ &=\sum_{j=1}^T[(\sum_{i\in I_j}g_i)w_j+\frac{1}{2}(\sum_{i\in I_j}h_i)w^2_j+\frac{1}{2}\lambda w_j^2]+\gamma T\\ &=\sum_{j=1}^T[(\sum_{i\in I_j}g_i)w_j+\frac{1}{2}(\sum_{i\in I_j}h_i+\lambda)w^2_j]+\gamma T\\ \end{split} \]

其中 \(I_j=\{i|q(x_i)=j\}\) 是第 \(i\) 個資料點被分配到第 \(j\) 個葉結點上的下標集合。改變了其累加的索引,因為被分配到相同的葉結點上的資料點得到的分數是統一的。進一步壓縮表達令 \(G_j=\sum_{i\in I_j}g_i\)\(H_j=\sum_{i\in I_j}h_i\)

\[\mathit{obj}^{(t)}=\sum_{j=1}^T[G_jw_j+\frac{1}{2}(H_j+\lambda)w_j^2]+\gamma T \]

其中,\(w_j\) 是彼此獨立的,式子 \(G_jw_j+\frac{1}{2}(H_j+\lambda)w_j^2\) 是二次的,並且對於給定的結構 \(q(x)\) 最好的 \(w_j\) 和可以得到的最佳的目標規約為:

\[\begin{split} w_j\ast&=-\frac{G_j}{H_j+\lambda}\\ \mathit{obj}\ast&=-\frac{1}{2}\sum_{j=1}^T\frac{G_j^2}{H_j+\lambda}+\gamma T \end{split} \]

此公式衡量了一棵樹的結構 \(q(x)\) 有多好。

圖片

基本上,對於一顆給定的樹結構,將統計量 \(g_i\)\(h_i\) 推到它們所屬的葉結點上,並將它們累加到一起,使用公式計算衡量這棵樹多好。這個分數類似於決策樹中的不純度度量(impurity measure),區別之處在於它還將模型複雜度考慮在內。

學習樹的結構

現在已經有了衡量一棵樹好壞的指標,一個典型的想法是列舉所有可能的樹並從中挑出最好的一個。實際上這是非常棘手的,所以應該嘗試一次優化樹的一個級別。具體來說,是將一個子結點分割成兩個葉結點,得分增益為:

\[\mathit{Gain}=\frac{1}{2}\left [\frac{G_L^2}{H_L+\lambda}+\frac{G_R^2}{H_R+\lambda}-\frac{(G_L+G_R)^2}{H_L+H_R+\lambda}\right]-\gamma \]

這個公式可以被分解為幾個部分,一部分是在新左子結點的得分,第二部分是在新右子結點上的得分,第三部分是原先葉結點上的得分,第四部分是在新葉結點上的正則化項。可以看到非常重要的因素是,如果增益小於 \(\gamma\) 更好的選擇是不去分割出一個新分支。這就是基本的樹模型的剪枝(pruning)技術。

對於實際中的資料,通常想要搜尋一個最優的分割點。一個高效率的做法是,將所有的例項(記錄)排好序,如下圖示。

圖片

從左到右掃描計算所有分割方案的結構分數是非常高效的,並且可以快速地找出最優的分割點。

加性數訓練的限制

因為將所有可能的樹結構列舉出來是非常棘手的,所以每次增加一個分割點(split)。這個方法在大多數情況下執行的很好,但是有一些邊緣案例導致這個方法失效。對於退化模型的訓練結果,每次僅僅考慮一個特徵維度。參考Can Gradient Boosting Learn Simple Arithmetic?

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
train = pd.read_csv('./data/happiness_train_complete_nona.csv', index_col='id', parse_dates=['survey_time'])
test = pd.read_csv('./data/happiness_test_complete_nona.csv', index_col='id', parse_dates=['survey_time'])
submit = pd.read_csv('./data/happiness_submit.csv', index_col='id')
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error

X = train.drop(['happiness', 'survey_time'], axis=1)
y = train['happiness']

X_train, X_test, y_train, y_test = train_test_split(X, y)
from xgboost import XGBRegressor
from xgboost import plot_importance

model = XGBRegressor(gamma=0.1, learning_rate=0.1)
model.fit(X_train, y_train)
mean_squared_error(y_test, model.predict(X_test))
0.4596381608913307
predict = pd.DataFrame({'happiness' : model.predict(test.drop('survey_time', axis=1))}, index=test.index)
submit.loc[predict.index, 'happiness'] = predict['happiness']
submit.to_csv('./data/predict.csv')

相關文章