[Applied Predictive Modeling] Choose your ML problems

Choose your ML problems

  • 초콜릿 바 평점 데이터세트 사용
1
2
3
4
import pandas as pd
import numpy as np
pd.options.display.max_columns = None
df = pd.read_csv('https://ds-lecture-data.s3.ap-northeast-2.amazonaws.com/chocolate_bar_ratings/flavors_of_cacao.csv')

데이터 과학자 실무 프로세스

  1. 비즈니스 문제
    • 실무자들과 대화를 통해 문제를 발견
  2. 데이터 문제
    • 문제와 관련된 데이터를 발견
  3. 데이터 문제 해결
    • 데이터 처리, 시각화
    • 머신러닝/통계
  4. 비즈니스 문제 해결
    • 데이터 문제 해결을 통해 실무자들과 함께 해결
  • 캐글 대회를 수행은 여러 모델을 검증해보며 기술을 익히는데 훌륭한 방법이지만 이 과정도 데이터 과학 업무의 한 부분이다.
  • 문제정의과정은 누군가에 의해 정해져 있었고 기술적으로 데이터로의 문제해결에만 집중했다.

Choose Target

  • 지도학습에서는 예측해야하는 타겟을 명확히 정하고 그 분포를 살펴본다.
  • 어떤 문제는 회귀/분류가 쉽게 구분되지 않는다.
    • 이산형, 순서형, 범주형 타겟특성도 회귀문제 또는 다중클래스분류문제로 볼 수 있다.
    • 회귀, 다중클래스분류 문제들도 이진분류문제로 바꿀 수 있다.
1
df. head()

스크린샷 2021-08-24 09 59 01

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
df.columns
'''
Index(['Company \n(Maker-if known)', 'Specific Bean Origin\nor Bar Name',
       'REF', 'Review\nDate', 'Cocoa\nPercent', 'Company\nLocation', 'Rating',
       'Bean\nType', 'Broad Bean\nOrigin'],
      dtype='object')
'''

# 컬럼명 정리
df.columns = ['company','specificOrigin','ref','reviewDate','cocoaPercent','companyLocation','rating','beanType','broadOrigin']

# 결측치 확인, 몇 개 안되어 후에 전처리 과정에서 제거
[(x, df[x].isnull().sum()) for x in df.columns if df[x].isnull().any()]
'''
[('beanType', 1), ('broadOrigin', 1)]
'''

# 타겟 확인
df.dtypes
'''
company             object
specificOrigin      object
ref                  int64
reviewDate           int64
cocoaPercent        object
companyLocation     object
rating             float64
beanType            object
broadOrigin         object
dtype: object
'''

df.describe(include='all').T

스크린샷 2021-08-24 10 00 58

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# rating이 타겟특성 : 실수형
df['rating'].describe()
'''
count    1795.000000
mean        3.185933
std         0.478062
min         1.000000
25%         2.875000
50%         3.250000
75%         3.500000
max         5.000000
Name: rating, dtype: float64
'''

# 분포확인
import seaborn as sns
import matplotlib.pyplot as plt
sns.displot(df['rating'],kde=True);
plt.axvline(3.7, color='red');

스크린샷 2021-08-24 10 02 08

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# rating을 이진타입으로 변형시켜 분류문제로 변경
# recommend 특성 만들어 이진분류문제로 변환
df['recommend'] = df['rating'] >= 3.7
df['recommend'].nunique()
'''
2
'''

df['recommend'].value_counts()
'''
False    1485
True      310
Name: recommend, dtype: int64
'''

# 데이터 확인 및 간단한 전처리
df['cocoaPercent'].head()
'''
0    63%
1    70%
2    70%
3    70%
4    70%
Name: cocoaPercent, dtype: object
'''

df['broadOrigin'].unique()
'''
array(['Sao Tome', 'Togo', 'Peru', 'Venezuela', 'Cuba', 'Panama',
       'Madagascar', 'Brazil', 'Ecuador', 'Colombia', 'Burma',
       'Papua New Guinea', 'Bolivia', 'Fiji', 'Mexico', 'Indonesia',
       'Trinidad', 'Vietnam', 'Nicaragua', 'Tanzania',
       'Dominican Republic', 'Ghana', 'Belize', '\xa0', 'Jamaica',
       'Grenada', 'Guatemala', 'Honduras', 'Costa Rica',
       'Domincan Republic', 'Haiti', 'Congo', 'Philippines', 'Malaysia',
       'Dominican Rep., Bali', 'Venez,Africa,Brasil,Peru,Mex', 'Gabon',
       'Ivory Coast', 'Carribean', 'Sri Lanka', 'Puerto Rico', 'Uganda',
       'Martinique', 'Sao Tome & Principe', 'Vanuatu', 'Australia',
       'Liberia', 'Ecuador, Costa Rica', 'West Africa', 'Hawaii',
       'St. Lucia', 'Cost Rica, Ven', 'Peru, Madagascar',
       'Venezuela, Trinidad', 'Trinidad, Tobago',
       'Ven, Trinidad, Ecuador', 'South America, Africa', 'India',
       'Africa, Carribean, C. Am.', 'Tobago', 'Ven., Indonesia, Ecuad.',
       'Trinidad-Tobago', 'Peru, Ecuador, Venezuela',
       'Venezuela, Dom. Rep.', 'Colombia, Ecuador', 'Solomon Islands',
       'Nigeria', 'Peru, Belize', 'Peru, Mad., Dom. Rep.', nan,
       'PNG, Vanuatu, Mad', 'El Salvador', 'South America', 'Samoa',
       'Ghana, Domin. Rep', 'Trinidad, Ecuador', 'Cameroon',
       'Venezuela, Java', 'Venezuela/ Ghana', 'Venezuela, Ghana',
       'Indonesia, Ghana', 'Peru(SMartin,Pangoa,nacional)', 'Principe',
       'Central and S. America', 'Ven., Trinidad, Mad.',
       'Carribean(DR/Jam/Tri)', 'Ghana & Madagascar',
       'Ven.,Ecu.,Peru,Nic.', 'Madagascar & Ecuador',
       'Guat., D.R., Peru, Mad., PNG', 'Peru, Dom. Rep',
       'Dom. Rep., Madagascar', 'Gre., PNG, Haw., Haiti, Mad',
       'Mad., Java, PNG', 'Ven, Bolivia, D.R.', 'DR, Ecuador, Peru',
       'Suriname', 'Peru, Ecuador', 'Ecuador, Mad., PNG',
       'Ghana, Panama, Ecuador', 'Venezuela, Carribean'], dtype=object)
'''

import re

# broadOrigin 텍스트 수정 함수
def txt_prep(text):
    replacements = [
        ['-', ', '], ['/ ', ', '], ['/', ', '], ['\(', ', '], [' and', ', '], [' &', ', '], ['\)', ''],
        ['Dom Rep|DR|Domin Rep|Dominican Rep,|Domincan Republic', 'Dominican Republic'],
        ['Mad,|Mad$', 'Madagascar, '],
        ['PNG', 'Papua New Guinea, '],
        ['Guat,|Guat$', 'Guatemala, '],
        ['Ven,|Ven$|Venez,|Venez$', 'Venezuela, '],
        ['Ecu,|Ecu$|Ecuad,|Ecuad$', 'Ecuador, '],
        ['Nic,|Nic$', 'Nicaragua, '],
        ['Cost Rica', 'Costa Rica'],
        ['Mex,|Mex$', 'Mexico, '],
        ['Jam,|Jam$', 'Jamaica, '],
        ['Haw,|Haw$', 'Hawaii, '],
        ['Gre,|Gre$', 'Grenada, '],
        ['Tri,|Tri$', 'Trinidad, '],
        ['C Am', 'Central America'],
        ['S America', 'South America'],
        [', $', ''], [',  ', ', '], [', ,', ', '], ['\xa0', ' '],[',\s+', ','],
        ['\.',''],
        [' Bali', ',Bali']
    ]
    for i, j in replacements:
        text = re.sub(i, j, text)
    return text

# 간단하게 수정할 수 있는 부분만 전처리
def preprocess (df):

    df.dropna(inplace=True)
    
    df['cocoaPercent'] = df['cocoaPercent'].str.replace('%','').astype(float)/100
    
    df['broadOrigin'] = df['broadOrigin'].apply(txt_prep)
    
    df['companyLocation'] = df['companyLocation']\
        .str.replace('Amsterdam', 'Holland')\
        .str.replace('U.K.', 'England')\
        .str.replace('Niacragua', 'Nicaragua')\
        .str.replace('Domincan Republic', 'Dominican Republic')
    
    df['beanType'] = df['beanType'].apply(lambda x : 'Missing' if (x is "\xa0") else x)
    
    df['is_blend'] = np.where(
    np.logical_or(
        np.logical_or(df['specificOrigin'].str.lower().str.contains(',|blend|;'),
                      df['broadOrigin'].str.len() == 1),
        df['broadOrigin'].str.lower().str.contains(',')
    )
    , 1
    , 0
)
    
    return df

df = preprocess(df)

df['is_blend'].value_counts()
'''
0    1095
1     698
Name: is_blend, dtype: int64
'''

정보의 누수(Leakage) 확인

  • 모델을 만들고 평가했을 때 예측을 100% 가깝게 잘 하는 경우를 보게 된다.
  • 이 때 정보의 누수가 존재할 가능성이 크다.
    • 타겟변수 외의 예측시점에 사용할 수 없는 데이터가 포함되어 학습
    • 훈련데이터와 검증데이터를 분리하지 못한 경우
  • 정보의 누수가 일어나 과적합을 일으키고 실제 테스트 데이터에서 성능이 급격히 떨어진다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# 데이터 정리
df.isna().sum().sort_values()
'''
company            0
specificOrigin     0
ref                0
reviewDate         0
cocoaPercent       0
companyLocation    0
rating             0
beanType           0
broadOrigin        0
recommend          0
is_blend           0
dtype: int64
'''

df['reviewDate'].value_counts().sort_index()
'''
2006     72
2007     77
2008     93
2009    123
2010    111
2011    164
2012    194
2013    184
2014    247
2015    285
2016    219
2017     24
Name: reviewDate, dtype: int64
'''

# 데이터 분리
from sklearn.model_selection import train_test_split
train, val = train_test_split(df, test_size=0.2, random_state=2)
train.shape, val.shape
'''
((1434, 11), (359, 11))
'''

# 누수가 일어난 특성을 제거하지 않았을 때의 결과
from category_encoders import OrdinalEncoder
from sklearn.pipeline import make_pipeline
from sklearn.tree import DecisionTreeClassifier

target = 'recommend'
features = df.columns.drop([target, 'reviewDate'])
X_train = train[features]
y_train = train[target]
X_val = val[features]
y_val = val[target]

pipe = make_pipeline(
    OrdinalEncoder(), 
    DecisionTreeClassifier(max_depth=5, random_state=2)
)

pipe.fit(X_train, y_train)
print('검증 정확도: ', pipe.score(X_val, y_val))
'''
검증 정확도:  1.0
'''

# 트리 확인
import graphviz
from sklearn.tree import export_graphviz

tree = pipe.named_steps['decisiontreeclassifier']

dot_data = export_graphviz(
    tree,
    feature_names=X_train.columns, 
    class_names=y_train.unique().astype(str), 
    filled=True, 
    proportion=True
)

graphviz.Source(dot_data)

스크린샷 2021-08-24 10 07 49

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 정보 누수가 일어난 컬럼 제거
features = df.columns.drop([target
                            , 'reviewDate'
                            , 'rating'
                            , 'ref'
                           ])
X_train = train[features]
y_train = train[target]
X_val = val[features]
y_val = val[target]

pipe = make_pipeline(
    OrdinalEncoder(), 
    DecisionTreeClassifier(max_depth=5, random_state=2)
)

pipe.fit(X_train, y_train)
print('검증 정확도', pipe.score(X_val, y_val))
'''
검증 정확도 0.83008356545961
'''

# 시각화 확인
tree = pipe.named_steps['decisiontreeclassifier']

dot_data = export_graphviz(
    tree, 
    feature_names=X_train.columns, 
    class_names=y_train.unique().astype(str), 
    filled=True, 
    proportion=True
)

graphviz.Source(dot_data)

스크린샷 2021-08-24 10 09 07

문제에 적합한 평가지표 선택

  • 예측모델 평가는 문제의 상황에 따라 다르다. 특히, 분류 & 회귀 모델의 평가지표는 더욱 다르다.
  • 분류문제에서 타겟 클래스 비율이 70% 이상 차이날 경우 정확도만 사용하면 판단을 정확히 할 수 없다.
  • Precision, Recall, ROC curve, AUC 등을 같이 사용해야 한다.
1
2
3
4
5
6
7
8
from sklearn.metrics import plot_confusion_matrix
import matplotlib.pyplot as plt

fig, ax = plt.subplots()
pcm = plot_confusion_matrix(pipe, X_val, y_val,
                            cmap=plt.cm.Blues,
                            ax=ax);
plt.title(f'Confusion matrix, n = {len(y_val)}', fontsize=15)

스크린샷 2021-08-24 10 14 10

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
pipe = make_pipeline(
    OrdinalEncoder(), 
    DecisionTreeClassifier(max_depth=5, random_state=2)
)

pipe.fit(X_train, y_train)
print('검증 정확도', pipe.score(X_val, y_val))
'''
검증 정확도 0.83008356545961
'''

# false 예측 정확도는 높지만 True는 현저히 낮다.
from sklearn.metrics import classification_report
y_pred = pipe.predict(X_val)
print(classification_report(y_val, y_pred))
'''
              precision    recall  f1-score   support

       False       0.84      0.98      0.91       302
        True       0.17      0.02      0.03        57

    accuracy                           0.83       359
   macro avg       0.50      0.50      0.47       359
weighted avg       0.73      0.83      0.77       359
'''

from sklearn.metrics import roc_auc_score

y_pred_proba = pipe.predict_proba(X_val)[:, -1]
print('AUC score: ', roc_auc_score(y_val, y_pred_proba))
'''
AUC score:  0.5991634715928895
'''

from sklearn.metrics import roc_curve
import matplotlib.pyplot as plt

fpr, tpr, thresholds = roc_curve(y_val, y_pred_proba)

plt.scatter(fpr, tpr, color='blue')
plt.plot(fpr, tpr, color='green')
plt.title('ROC curve')
plt.xlabel('FPR')
plt.ylabel('TPR')

스크린샷 2021-08-24 10 15 29

불균형 클래스

  • 타겟 특성의 클래스 비율이 차이가 나는 경우가 많다.
  • Scikit-learn 분류기들은 class_weight같은 클래스의 밸런스를 맞추는 파라미터를 갖고 있다.
    • 데이터가 적은 범주의 손실을 계산할 때 가중치를 곱하여 데이터의 균형을 맞추거나
    • 적은 범주 데이터를 추가샘플링(Oversampling)하거나 반대로 많은 범주 데이터를 적게 샘플링(Undersampling)하는 방법이 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 범주 비율 확인
# class_weight에서 원하는 비율을 적용하거나 class_weight='balance' 옵션을 사용
y_train.value_counts(normalize=True)
'''
False    0.824268
True     0.175732
Name: recommend, dtype: float64
'''

# class weights 계산
# n_samples / (n_classes * np.bincount(y))
custom = len(y_train)/(2*np.bincount(y_train))
custom
'''
array([0.60659898, 2.8452381 ])
'''

# 파이프라인
pipe = make_pipeline(
    OrdinalEncoder(), 
#     DecisionTreeClassifier(max_depth=5, class_weight='balanced', random_state=2)
    DecisionTreeClassifier(max_depth=5, class_weight={False:custom[0],True:custom[1]}, random_state=2)
)

pipe.fit(X_train, y_train)
print('검증 정확도: ', pipe.score(X_val, y_val))
'''
검증 정확도:  0.584958217270195
'''

fig, ax = plt.subplots()
pcm = plot_confusion_matrix(pipe, X_val, y_val,
                            cmap=plt.cm.Blues,
                            ax=ax);
plt.title(f'Confusion matrix, n = {len(y_val)}', fontsize=15)

스크린샷 2021-08-24 10 20 38

  • 완화되었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# True 범주의 수치 비교
y_pred = pipe.predict(X_val)
print(classification_report(y_val, y_pred))
'''
              precision    recall  f1-score   support

       False       0.86      0.60      0.71       302
        True       0.19      0.49      0.27        57

    accuracy                           0.58       359
   macro avg       0.53      0.55      0.49       359
weighted avg       0.76      0.58      0.64       359
'''

y_pred_proba = pipe.predict_proba(X_val)[:, -1]
print('AUC score: ', roc_auc_score(y_val, y_pred_proba))
fpr, tpr, thresholds = roc_curve(y_val, y_pred_proba)
plt.scatter(fpr, tpr, color='blue')
plt.plot(fpr, tpr, color='green')
plt.title('ROC curve')
plt.xlabel('FPR')
plt.ylabel('TPR')
'''
AUC score:  0.624056000929476
'''

스크린샷 2021-08-24 10 22 12

타겟의 분포

  • 회귀문제에서 타겟 분포를 주의깊게 살펴야한다.
1
2
3
4
5
# house price 사용
df = pd.read_csv('https://ds-lecture-data.s3.ap-northeast-2.amazonaws.com/house-prices/house_prices_train.csv')

# 타겟 선택
target = df['SalePrice']

비대칭 형태인지 확인

  • 선형회귀모델은 일반적으로 특성과 타겟간의 선형관계를 가정한다.
  • 특성 변수들과 타겟변수의 분포가 정규분포일때 좋은 성능을 보인다.
  • 타겟변수가 왜곡된 형태의 분포(skewed)일때 예측 성능에 부정적인 영향을 미친다.
  • 등분산성
    • 분산이 같다는 것이고, 특정한 패턴없이 고르게 분포했다는 의미
    • 등분산성의 주체는 잔차

스크린샷 2021-08-24 10 26 32

1
2
# 타겟 분포가 right(positively) skewed
sns.displot(target);

스크린샷 2021-08-24 10 27 45

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 이상치 처리
# 몇몇 가격이나 다른 수치는 높아서 문제될 수 있다.
import numpy as np

# 타겟 이상치(outlier)를 제거
df['SalePrice'] = df[df['SalePrice'] < np.percentile(df['SalePrice'], 99.5)]['SalePrice']

# 몇몇 변수 합치고 이상치 제거
df['All_Flr_SF'] = df['1stFlrSF'] + df['2ndFlrSF']
df['All_Liv_SF'] = df['All_Flr_SF'] + df['LowQualFinSF'] + df['GrLivArea']
df = df.drop(['1stFlrSF','2ndFlrSF','LowQualFinSF','GrLivArea'], axis=1)

df['All_Flr_SF'] = df[df['All_Flr_SF'] < np.percentile(df['All_Flr_SF'], 99.5)]['All_Flr_SF']
df['All_Liv_SF'] = df[df['All_Liv_SF'] < np.percentile(df['All_Liv_SF'], 99.5)]['All_Liv_SF']

df['SalePrice']
'''
0       208500.0
1       181500.0
2       223500.0
3       140000.0
4       250000.0
          ...   
1455    175000.0
1456    210000.0
1457    266500.0
1458    142125.0
1459    147500.0
Name: SalePrice, Length: 1460, dtype: float64
'''

# 분포의 치우침이 어느정도 개선되었지만 여전히 right-skewed 상태
target = df['SalePrice']
sns.displot(target);

스크린샷 2021-08-24 10 29 37

로그변환(Log-Transform)

  • 로그변환 사용 시 비대칭 분포형태를 정규분포형태로 변환시켜준다.

스크린샷 2021-08-24 10 30 25

  • log1p: ln(1 + x)
  • expm1: exp(x) - 1, the inverse of log1p.
1
2
3
4
5
6
7
8
9
plots=pd.DataFrame()
plots['original']=target
plots['transformed']=np.log1p(target)
plots['backToOriginal']=np.expm1(np.log1p(target))

fig, ax = plt.subplots(1,3,figsize=(15,5))
sns.histplot(plots['original'], ax=ax[0]);
sns.histplot(plots['transformed'], ax=ax[1]);
sns.histplot(plots['backToOriginal'], ax=ax[2]);

스크린샷 2021-08-24 10 31 09

Transformed TargetRegressor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
target = 'SalePrice'
from sklearn.model_selection import train_test_split

df = df[df[target].notna()]

train, val = train_test_split(df, test_size=260, random_state=2)

features = train.drop(columns=[target]).columns

X_train = train[features]
y_train = train[target]
X_val = val[features]
y_val = val[target]


from category_encoders import OrdinalEncoder
from sklearn.impute import SimpleImputer
from sklearn.ensemble import RandomForestRegressor
from sklearn.compose import TransformedTargetRegressor

pipe = make_pipeline(
    OrdinalEncoder(), 
    SimpleImputer(),
    RandomForestRegressor(random_state=2)
)


pipe.fit(X_train, y_train)
pipe.score(X_val, y_val)
'''
0.8853294698484703
'''


from category_encoders import OrdinalEncoder
from sklearn.impute import SimpleImputer
from sklearn.ensemble import RandomForestRegressor
from sklearn.compose import TransformedTargetRegressor

pipe = make_pipeline(
    OrdinalEncoder(), 
    SimpleImputer(),
    RandomForestRegressor(random_state=2)
)

tt = TransformedTargetRegressor(regressor=pipe,
                                func=np.log1p, inverse_func=np.expm1)

tt.fit(X_train, y_train)
tt.score(X_val, y_val)
'''
0.8886126974296943
'''

Reference

0%