3. 탐색한 데이터로 모델성능 개선1

3.1.1 연속 수치 데이터를 범주형 변수로 변경하기

  • Overfitting: Train set에 대해선 Good, Test set에 대해선 bad
  • Underfitting: Train/Test 둘 다 bad -> 그냥 학습 자체가 제대로 안됨
  • 둘 다 일반화 성능 자체는 Bad. 결국 일반화가 중요한 것이므로 Under와 Over의 경계에서 optimize된 지점을 찾아야 함.
  • sample의 수가 적거나, 모델이 복잡하거나, 학습에 Target이 포함되었거나 등의 이유로 Overfitting이 발생.
  • 모델이 너무 간단하거나, 데이터의 수가 적거나 등의 이유로 Underfitting 발생

datasets

df = pd.read_csv('/content/drive/MyDrive/edwith/프로젝트로 배우는 데이터 사이언스/diabetes.csv')
df['Pregnancies_high'] = df['Pregnancies'] > 6
df[['Pregnancies', 'Pregnancies_high']].head()
train = df[:int(df.shape[0] * 0.8)]
test = df[int(df.shape[0] * 0.8):]

  • 이전의 분석에서 임신 횟수가 7회가 넘어가면 발병이 더 높다는 사실을 발견했었고 이를 범주화
feature_names = train.columns.tolist()
feature_names.remove('Pregnancies')
feature_names.remove('Outcome')
feature_names

  • remove 매서드는 리스트에 적용되는 매서드인데 입력한 원소에 대해 있으면 삭제를 해줌.
  • 존재하지 않는다면 에러 발생. 또한 중복된 값이 있다면 순서상으로 가장 빠른 원소를 제거
label_name = 'Outcome'
X_train = train[feature_names]
y_train = train[label_name]
print(X_train.shape)
X_train.head()

  • X_train은 8개의 컬럼을 가지고 614개의 관측치를 가짐. 원래 데이터 셋의 80%만큼만 할당했기 때문

모델 생성, 학습, 변수 중요도

model = DecisionTreeClassifier()
model.fit(X_train, y_train)
plt.figure(figsize = (10, 8))
sns.barplot(x = model.feature_importances_, y = feature_names)
plt.show()

  • 이전과는 다르게 Pregnancies의 중요도가 높지 않음
### 실제값 - 예측값을 빼주면 제대로 예측했다면 0, 틀리다면 -1
### 절대값을 취해 틀린 개수를 계산 후 Accuracy 계산
X_test = test[feature_names]
y_test = test[label_name]
y_predict = model.predict(X_test)
diff_count = abs(y_test - y_predict).sum()
print('틀린 개수는:', diff_count)
print('Accuracy:', (len(y_test) - diff_count) /len(y_test))

3.1.2 범주형 변수를 수치형 변수로 변환하기

원핫인코딩(One-hot-encoding)

  • 범주형 변수의 범주들을 컬럼으로 변환하고 해당되면 1, 해당되지 않으면 0으로. dummy화 한다고 생각

정수인코딩(Label-encoding)

  • 범주가 문자열로 되어있는 변수를 단순 명목형 수치로 변환해 줌
    - ex) ['남', '여', '여', '여'] -> [0, 1, 1, 1]

Age < 25, 25 <= Age < 60, Age > 60 총 세개의 구간으로 나눠줌

train['Age_low'] = train['Age'] < 25
train['Age_middle'] = (train['Age'] >= 25) & (df['Age'] <= 60)
train['Age_high'] = train['Age'] > 60
train[['Age', 'Age_low', 'Age_middle', 'Age_high']].head()

  • 총 세개의 파생변수가 생성되었고 One-hot 완성. 어차피 True는 1, False는 0
  • test에도 똑같이 원핫 인코딩
test['Age_low'] = test['Age'] < 25
test['Age_middle'] = (test['Age'] >= 25) & (test['Age'] <= 60)
test['Age_high'] = test['Age'] > 60
feature_names += ['Age_low', 'Age_middle', 'Age_high']
feature_names.remove('Age')
model.fit(train[feature_names], y_train)
y_predict = model.predict(X_test)
diff_count = abs(y_test - y_predict).sum()
print('틀린 개수는:', diff_count)
print('Accuracy:', (len(y_test) - diff_count) /len(y_test))
plt.figure(figsize = (10, 8))
sns.barplot(y = feature_names, x = model.feature_importances_)
plt.show()

  • 글루코스가 피쳐 중 가장 큰 역할을 하는건 변함이 없고, 파생된 변수들도 별다른 영향은 딱히..
  • Acc는 68%.

범주의 구간을 조금 다르게 수정

## train one-hot
train['Age_low'] = train['Age'] < 30
train['Age_middle'] = (train['Age'] >= 30) & (df['Age'] <= 60)
train['Age_high'] = train['Age'] > 60
## test one-hot
test['Age_low'] = test['Age'] < 30
test['Age_middle'] = (test['Age'] >= 30) & (test['Age'] <= 60)
test['Age_high'] = test['Age'] > 60
## model fit
model.fit(train[feature_names], y_train)
y_predict = model.predict(test[feature_names])
diff_count = abs(y_test - y_predict).sum()
print('틀린 개수는:', diff_count)
print('Accuracy:', (len(y_test) - diff_count) /len(y_test))
plt.figure(figsize = (10, 8))
sns.barplot(y = feature_names, x = model.feature_importances_)
plt.show()

  • 정확도가 조금 줄어들었다.
  • 수치형 변수를 세 개의 구간으로 나누고 one-hot을 해줬는데 수치형 구간을 잡아주는 기준에 따라 성능이 좀 달라질 수도 있음 => 그 자체의 영향력을 전체로 묶어주기 때문에
  • 카테고리의 수가 적다면 원핫이 별 상관 없을 수 있지만, 만약 10개라고 치면 1인 변수와 10인 변수의 차이는 10배 만큼 차이가 나게 되는 상황. 명목형 변수들의 수치 비교는 불가능함. 따라서 원핫 인코딩을 해주는 것.

3.1.3 결측치 평균으로 대체하기

df.isnull().sum()

  • 인슐린의 값이 0인 null로 대체해서 새로운 컬럼을 생성
df['Insulin_nan'] = df['Insulin'].replace(0, np.nan)
df[['Insulin', 'Insulin_nan']].head()

df.isna()['Insulin_nan'].sum()

  • 총 374개의 관측치가 null에 해당
df.groupby('Outcome')['Insulin', 'Insulin_nan'].agg(['mean', 'median', 'count'])

  • Outcome을 기준으로 그룹바이 하고 인슐린의 0값을 nan으로 처리한 것과 안한 것의 비교
  • 평균과 median 값의 차이가 많이 난다.
  • 결측치로 처리하니 count가 1/2 가량 줄어들었다. 결측치를 없애고 분석을 한다면 train set이 반으로 줄어들었기 때문에 under or over fitting이 발생할 수 있음
  • 데이터 셋이 충분히 많다면 결측치를 그냥 제거해도 되겠지만, 이처럼 부족한 상황에선 무작정 제거할 수는 없음.
  • 결측치를 채워주거나 해야함. 제일 좋은건 실제 관측치에 해당되는 값으로 채워주면 좋겠지만 이는 쉽지 않기 때문에 평균 혹은 median 등등 분석자의 판단에 따라 채워줌.
  • 해당 도메인에서 사용하는 값들이나.. 아무튼 합리적인 값으로 보통 채워서 학습.
### 결측치 대체(발병 여부별 평균값으로)
df.loc[(df.Outcome == 0) & (df['Insulin_nan'].isnull()), 'Insulin_nan'] = 68.7
df.loc[(df.Outcome == 1) & (df['Insulin_nan'].isnull()), 'Insulin_nan'] = 100.3
### 새롭게 features 만들어 주고 학습
features = df.columns.tolist()
features.remove('Insulin')
features.remove('Age_low')
features.remove('Age_middle')
features.remove('Age_high')
features.remove('Outcome')
train = df[:int(df.shape[0]*0.8)]
test = df[int(df.shape[0]*0.8):]
model.fit(train[features], y_train)
plt.figure(figsize = (10, 8))
sns.barplot(x = model.feature_importances_, y = features)
plt.show()

  • Insulin의 null을 평균값으로 대체해준 값이 가장 큰 영향으로 바뀜
y_predict = model.predict(test[features])
diff_count = abs(y_test - y_predict).sum()
print('틀린 개수는:', diff_count)
print('Accuracy:', (len(y_test) - diff_count) /len(y_test))

  • 정확도가 70% 아래에 머물다가 결측치를 처리해준 것 만으로 88%까지 상승
  • 학습에 포함되지 않았던 Test set으로 성능 평가를 한 것이므로 일반화의 가능성이 상승함
  • 결측치를 어떻게 처리할 지, 수치형 변수들을 구간으로 나눌지 등 여러가지를 고려해보며 성능을 향상
  • 너무 범주가 많은 변수의 one-hot을 하게되면 features의 수가 확 늘어나기 때문에 모델이 복잡해짐
  • 이는 오버피팅을 유발하게 될 수도 있고, 학습하는데 시간이 더 소요되고 무조건 장점은 아님
  • 그러므로 항상 Trade-off를 잘 생각해서 모델링을 해야함.

3.1.4 결측치 중앙값으로 대체하기

결측치 대체의 중요성을 느꼈고, 이번엔 median으로 대체

### fill the missing values
df['Insulin_nan'] = df['Insulin'].replace(0, np.nan)
df.loc[(df.Outcome == 0) & (df['Insulin_nan'].isnull()), 'Insulin_nan'] = 102.5
df.loc[(df.Outcome == 1) & (df['Insulin_nan'].isnull()), 'Insulin_nan'] = 169.5
### train / test split
train = df[:int(df.shape[0]*0.8)]
test = df[int(df.shape[0]*0.8):]
model.fit(train[features], y_train)

from sklearn.tree import plot_tree

plt.figure(figsize = (20, 20))
tree = plot_tree(model, feature_names = features, filled = True, fontsize = 10)

plt.figure(figsize = (10, 8))
sns.barplot(x = model.feature_importances_, y = features)
plt.show()

  • 인슐린 결측치 대체해준 값이 중요도가 가장 높음
y_predict = model.predict(test[features])
diff_count = abs(y_test - y_predict).sum()
print('틀린 개수는:', diff_count)
print('Accuracy:', (len(y_test) - diff_count) /len(y_test))

  • mean보다 median으로 대체해준 경우가 좀 더 성능이 좋아졌다.
  • 이처럼 결측치를 채워주는 부분이 정말 중요하고 보통 보수적으로 채워주기 위해 median이나 mean을 많이 넣어준다.
  • mean의 장점은 전체 값이 다 영향을 주는 대신, 이상치가 있다면 영향을 크게 받는다.
  • median은 순서대로 정렬했을 때 가운데 값을 넣어주므로 이상치에 별 영향을 안 받는다. 하지만 모든 값을 다 활용하는 것이 아니기 때문에 이게 단점이 됨.

좋은 웹페이지 즐겨찾기