38 minute read

로지스틱 회귀 소개

로지스틱 회귀(Logistic Regression) (또는 Logit Regression)는 데이터 과학의 새로운 이진 분류 문제를 해결할 때, 생각할 수 있는 대표적인 알고리즘이다.

로지스틱 회귀는 이산적인 값을 가지는 클래스를 예측할 때 사용되는 지도 학습 알고리즘으로, 실제로 서로 다른 특성들의 관측치를 분류할 때 사용된다.

여기서 “이산적인 값”이란 불연속적인 값 또는 구분되어 셀 수 있는 것을 의미하는데, 로지스틱 회귀의 결과는 입력 변수에 따라 해당되는 클래스에 대한 롹률을 계산하여 가장 높은 확률을 가진 클래스를 예측하고 이러한 예측은 불연속적인 특성으로 표현된다.

로지스틱 회귀 분석

통계학에 있어, 로지스틱 회귀 모델은 주로 분류 목적으로 사용되는 통계학 모델에서 폭넓게 사용된다.

관측치의 집합이 주어졌을 때, 로지스틱 회귀 알고리즘은 두 개 이상의 이산적인 클래스로 분류하는데 도움을 주고, 목표 또한 이산적인 형태를 가진다.

다음은 로지스틱 회귀의 작동 방식이다.

선형 방정식 구현

로지스틱 회귀는 독립 변수 or 설명 변수로 응답 값을 예측하는 선형 방정식을 구현함으로써 작동한다.

예를 들어, 공부한 시간과 시험 합격률의 예시를 보자.

x1은 공부한 시간(설명 변수)이고, z는 시험 합격률(목표 변수)이다.

만약 하나의 설명 변수(x1) 및 목표 변수(z)를 가지고 있다면, 이에 대한 선형 방정식은 다음과 같다.


z = β0 + β1x1

위의 식에서 β0 와 β1은 모델의 파라미터인데, 만약 설명 변수가 여러 개가 주어진다면 다음과 같이 작성할 수 있다.


z = β0 + β1x1+ β2x2+……..+ βnxn

여기서 β0, β1, β2 및 βn은 모델의 파라미터이다.

따라서 예측값은 위의 선형 방정식으로 주어지며, z로 표현될 수 있다.

Sigmoid 함수

예측된 값 z는 0과 1사이의 확률 값으로 변환되는데 이 때 예측 값에서 확률 값으로 변환하기 위해서 sigmoid function을 사용해야 한다.

sigmoid 함수는 주어진 실수 값을 0과 1사이의 확률 값으로 매핑한다.

sigmoid 함수는 S자 모양의 곡선을 가지고 해당 곡선은 sigmoid 곡선이라고도 한다.

다음 그래프는 sigmoid 함수이다.

image.png

결정 경계

sigmoid 함수는 0과 1사이의 확률 값으로 반환하는데, 해당 확률 값은 0 또는 1의 이산적인 클래스로 매핑된다.

확률 값을 이산적인 클래스로 매핑하기 위해서 임계값을 선택해야 하고, 해당 임계값 보다 높다면 1로 매핑되고, 해당 임계값 보다 낮다면 0으로 매핑된다.

이를 수학적으로 표현하면 다음과 같다.


p  0.5  class = 1

p < 0.5  class = 0

일반적으로 임계값은 0.5로 설정된다.

만약 확률 값이 0.8이라면 관측치는 클래스 1로 매핑되고 확률 값이 0.2라면 관측치는 0으로 매핑된다.

이를 그래프로 표현하면 다음과 같다.

image.png

예측 함수

로지스틱 회귀에서 sigmoid 함수와 임계값을 이용하여 예측 함수를 만들 수 있다.

예측 함수는 양성(yes 또는 True)관측치의 확률값을 반환하는데 이를 class 1이라 하며 P(class = 1)로 표현한다.

만약 확률이 1로 가까워진다면 이는 관측치가 class 1에 속할 가능성이 높음을 의미하고, 반대의 경우 관측치가 class 0에 속할 가능성이 높음을 의미한다.

로지스틱 회귀의 가정

로지스틱 회귀 모델은 몇개의 가정을 요구하는데 다음과 같다.

  1. 종속 변수가 이항, 다항 또는 순서형일 경우 사용할 수 있다.

  2. 관측치가 각각 독립적이어야한다. 이는 반복적인 측정에서 나온 관측치는 사용할 수 없음을 의미한다.

  3. 독립 변수 사이에 다중선형성이 적거나 없어야한다. 이는 독립 변수들이 서로 연관성이 적어야 함을 의미한다.

  4. 독립 변수와 로그 확률의 선형성을 가정한다.

  5. 샘플의 크기가 클수록 높은 정확성을 얻을 수 있다.

로지스틱 회귀의 유형

로지스틱 회귀 모델은 목표 변수에 따라 세 가지의 유형으로 분류된다.

  1. 이항 로지스틱 회귀

이항 로지스틱 회귀에서 목표 변수는 두개의 특성값을 갖는다.

예시로 yes/no, good/bad, true/false, spam/no spam, pass/fail 등이 있다.

  1. 다항 로지스틱 회귀

다항 로지스틱 회귀에서 목표 변수는 특정한 순서가 없는 세개 이상의 특성(명목적인 특성)를 갖는다.

예시로 과일 - 사과/망고/오렌지/바나나 가 있다.

  1. 순서형 로지스틱 회귀

순서형 로지스틱 회귀에서 목표 변수는 순서가 있는 세개 이상의 특성을 갖는다.

예시로 성적 - 나쁨/보통/좋음/우수 가 있다.

라이브러리

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline

import os
for dirname, _, filenames in os.walk('/kaggle/input'):
    for filename in filenames:
        print(os.path.join(dirname, filename))
/kaggle/input/weatheraus/weatherAUS.csv
import warnings

warnings.filterwarnings('ignore')

데이터셋 불러오기

data = '/kaggle/input/weatheraus/weatherAUS.csv'

df = pd.read_csv(data)

데이터셋 분석

# 데이터셋의 차원 확인

df.shape
(142193, 24)

142193개의 인스턴스와 24개의 특성으로 이루어진 데이터셋이다.

# 데이터셋의 첫 5개 정보 확인

df.head()
Date Location MinTemp MaxTemp Rainfall Evaporation Sunshine WindGustDir WindGustSpeed WindDir9am ... Humidity3pm Pressure9am Pressure3pm Cloud9am Cloud3pm Temp9am Temp3pm RainToday RISK_MM RainTomorrow
0 2008-12-01 Albury 13.4 22.9 0.6 NaN NaN W 44.0 W ... 22.0 1007.7 1007.1 8.0 NaN 16.9 21.8 No 0.0 No
1 2008-12-02 Albury 7.4 25.1 0.0 NaN NaN WNW 44.0 NNW ... 25.0 1010.6 1007.8 NaN NaN 17.2 24.3 No 0.0 No
2 2008-12-03 Albury 12.9 25.7 0.0 NaN NaN WSW 46.0 W ... 30.0 1007.6 1008.7 NaN 2.0 21.0 23.2 No 0.0 No
3 2008-12-04 Albury 9.2 28.0 0.0 NaN NaN NE 24.0 SE ... 16.0 1017.6 1012.8 NaN NaN 18.1 26.5 No 1.0 No
4 2008-12-05 Albury 17.5 32.3 1.0 NaN NaN W 41.0 ENE ... 33.0 1010.8 1006.0 7.0 8.0 17.8 29.7 No 0.2 No

5 rows × 24 columns

RISK_MM 특성 삭제

col_names = df.columns
col_names
Index(['Date', 'Location', 'MinTemp', 'MaxTemp', 'Rainfall', 'Evaporation',
       'Sunshine', 'WindGustDir', 'WindGustSpeed', 'WindDir9am', 'WindDir3pm',
       'WindSpeed9am', 'WindSpeed3pm', 'Humidity9am', 'Humidity3pm',
       'Pressure9am', 'Pressure3pm', 'Cloud9am', 'Cloud3pm', 'Temp9am',
       'Temp3pm', 'RainToday', 'RISK_MM', 'RainTomorrow'],
      dtype='object')
# RISK_MM 특성 삭제

df.drop(["RISK_MM"], axis=1, inplace=True)
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 142193 entries, 0 to 142192
Data columns (total 23 columns):
 #   Column         Non-Null Count   Dtype  
---  ------         --------------   -----  
 0   Date           142193 non-null  object 
 1   Location       142193 non-null  object 
 2   MinTemp        141556 non-null  float64
 3   MaxTemp        141871 non-null  float64
 4   Rainfall       140787 non-null  float64
 5   Evaporation    81350 non-null   float64
 6   Sunshine       74377 non-null   float64
 7   WindGustDir    132863 non-null  object 
 8   WindGustSpeed  132923 non-null  float64
 9   WindDir9am     132180 non-null  object 
 10  WindDir3pm     138415 non-null  object 
 11  WindSpeed9am   140845 non-null  float64
 12  WindSpeed3pm   139563 non-null  float64
 13  Humidity9am    140419 non-null  float64
 14  Humidity3pm    138583 non-null  float64
 15  Pressure9am    128179 non-null  float64
 16  Pressure3pm    128212 non-null  float64
 17  Cloud9am       88536 non-null   float64
 18  Cloud3pm       85099 non-null   float64
 19  Temp9am        141289 non-null  float64
 20  Temp3pm        139467 non-null  float64
 21  RainToday      140787 non-null  object 
 22  RainTomorrow   142193 non-null  object 
dtypes: float64(16), object(7)
memory usage: 25.0+ MB

데이터셋을 불러올 때 설명을 보면 RISK_MM 특성을 삭제하라고 주어진다.

특성 확인

데이터셋에 범주형 특성과 수치형 특성이 존재하는데, 이를 분리해야한다.

범주형 특성은 데이터 타입이 object로 주어지고, 수치형 특성은 float64로 주어진다.

# 범주형 특성 탐색

categorical = [var for var in df.columns if df[var].dtype=='O']

print(f'범주형 특성의 개수 : {len(categorical)}개')
print(f'범주형 특성 : {categorical}')
범주형 특성의 개수 : 7개
범주형 특성 : ['Date', 'Location', 'WindGustDir', 'WindDir9am', 'WindDir3pm', 'RainToday', 'RainTomorrow']
# 범주형 특성 확인

df[categorical].head()
Date Location WindGustDir WindDir9am WindDir3pm RainToday RainTomorrow
0 2008-12-01 Albury W W WNW No No
1 2008-12-02 Albury WNW NNW WSW No No
2 2008-12-03 Albury WSW W WSW No No
3 2008-12-04 Albury NE SE E No No
4 2008-12-05 Albury W ENE NW No No

범주형 특성 요약

  • 날짜 데이터가 존재

  • 날짜 데이터 이외의 6개의 특성 존재

  • 이진 범주형 변수 존재 - RainToday / RainTomorrow

  • 목표 변수는 RainTomorrow

범주형 특성 문제 확인

# 결측치가 존재하는지 확인

df[categorical].isnull().sum()
Date                0
Location            0
WindGustDir      9330
WindDir9am      10013
WindDir3pm       3778
RainToday        1406
RainTomorrow        0
dtype: int64

4개의 특성에 결측치가 존재한다는 것을 알 수 있다.

# 빈도수 확인

for var in categorical:
    print(df[var].value_counts())
2013-12-01    49
2014-01-09    49
2014-01-11    49
2014-01-12    49
2014-01-13    49
              ..
2007-11-29     1
2007-11-28     1
2007-11-27     1
2007-11-26     1
2008-01-31     1
Name: Date, Length: 3436, dtype: int64
Canberra            3418
Sydney              3337
Perth               3193
Darwin              3192
Hobart              3188
Brisbane            3161
Adelaide            3090
Bendigo             3034
Townsville          3033
AliceSprings        3031
MountGambier        3030
Launceston          3028
Ballarat            3028
Albany              3016
Albury              3011
PerthAirport        3009
MelbourneAirport    3009
Mildura             3007
SydneyAirport       3005
Nuriootpa           3002
Sale                3000
Watsonia            2999
Tuggeranong         2998
Portland            2996
Woomera             2990
Cairns              2988
Cobar               2988
Wollongong          2983
GoldCoast           2980
WaggaWagga          2976
Penrith             2964
NorfolkIsland       2964
SalmonGums          2955
Newcastle           2955
CoffsHarbour        2953
Witchcliffe         2952
Richmond            2951
Dartmoor            2943
NorahHead           2929
BadgerysCreek       2928
MountGinini         2907
Moree               2854
Walpole             2819
PearceRAAF          2762
Williamtown         2553
Melbourne           2435
Nhil                1569
Katherine           1559
Uluru               1521
Name: Location, dtype: int64
W      9780
SE     9309
E      9071
N      9033
SSE    8993
S      8949
WSW    8901
SW     8797
SSW    8610
WNW    8066
NW     8003
ENE    7992
ESE    7305
NE     7060
NNW    6561
NNE    6433
Name: WindGustDir, dtype: int64
N      11393
SE      9162
E       9024
SSE     8966
NW      8552
S       8493
W       8260
SW      8237
NNE     7948
NNW     7840
ENE     7735
ESE     7558
NE      7527
SSW     7448
WNW     7194
WSW     6843
Name: WindDir9am, dtype: int64
SE     10663
W       9911
S       9598
WSW     9329
SW      9182
SSE     9142
N       8667
WNW     8656
NW      8468
ESE     8382
E       8342
NE      8164
SSW     8010
NNW     7733
ENE     7724
NNE     6444
Name: WindDir3pm, dtype: int64
No     109332
Yes     31455
Name: RainToday, dtype: int64
No     110316
Yes     31877
Name: RainTomorrow, dtype: int64
# 빈도수의 분포

for var in categorical:
    print(df[var].value_counts()/np.float(len(df)))
2013-12-01    0.000345
2014-01-09    0.000345
2014-01-11    0.000345
2014-01-12    0.000345
2014-01-13    0.000345
                ...   
2007-11-29    0.000007
2007-11-28    0.000007
2007-11-27    0.000007
2007-11-26    0.000007
2008-01-31    0.000007
Name: Date, Length: 3436, dtype: float64
Canberra            0.024038
Sydney              0.023468
Perth               0.022455
Darwin              0.022448
Hobart              0.022420
Brisbane            0.022230
Adelaide            0.021731
Bendigo             0.021337
Townsville          0.021330
AliceSprings        0.021316
MountGambier        0.021309
Launceston          0.021295
Ballarat            0.021295
Albany              0.021211
Albury              0.021175
PerthAirport        0.021161
MelbourneAirport    0.021161
Mildura             0.021147
SydneyAirport       0.021133
Nuriootpa           0.021112
Sale                0.021098
Watsonia            0.021091
Tuggeranong         0.021084
Portland            0.021070
Woomera             0.021028
Cairns              0.021014
Cobar               0.021014
Wollongong          0.020979
GoldCoast           0.020957
WaggaWagga          0.020929
Penrith             0.020845
NorfolkIsland       0.020845
SalmonGums          0.020782
Newcastle           0.020782
CoffsHarbour        0.020768
Witchcliffe         0.020761
Richmond            0.020753
Dartmoor            0.020697
NorahHead           0.020599
BadgerysCreek       0.020592
MountGinini         0.020444
Moree               0.020071
Walpole             0.019825
PearceRAAF          0.019424
Williamtown         0.017954
Melbourne           0.017125
Nhil                0.011034
Katherine           0.010964
Uluru               0.010697
Name: Location, dtype: float64
W      0.068780
SE     0.065467
E      0.063794
N      0.063526
SSE    0.063245
S      0.062936
WSW    0.062598
SW     0.061867
SSW    0.060552
WNW    0.056726
NW     0.056283
ENE    0.056205
ESE    0.051374
NE     0.049651
NNW    0.046142
NNE    0.045241
Name: WindGustDir, dtype: float64
N      0.080123
SE     0.064434
E      0.063463
SSE    0.063055
NW     0.060144
S      0.059729
W      0.058090
SW     0.057928
NNE    0.055896
NNW    0.055136
ENE    0.054398
ESE    0.053153
NE     0.052935
SSW    0.052380
WNW    0.050593
WSW    0.048125
Name: WindDir9am, dtype: float64
SE     0.074990
W      0.069701
S      0.067500
WSW    0.065608
SW     0.064574
SSE    0.064293
N      0.060952
WNW    0.060875
NW     0.059553
ESE    0.058948
E      0.058667
NE     0.057415
SSW    0.056332
NNW    0.054384
ENE    0.054321
NNE    0.045319
Name: WindDir3pm, dtype: float64
No     0.768899
Yes    0.221213
Name: RainToday, dtype: float64
No     0.775819
Yes    0.224181
Name: RainTomorrow, dtype: float64

카디널리티 : 범주형 변수내의 레이블 수

카디널리티가 높다면 머신 러닝에 있어 심각한 문제를 일으킬 수 있는데, 이를 확인한다.

for var in categorical:
    print(var, " contains ", len(df[var].unique()), " labels")
Date  contains  3436  labels
Location  contains  49  labels
WindGustDir  contains  17  labels
WindDir9am  contains  17  labels
WindDir3pm  contains  17  labels
RainToday  contains  3  labels
RainTomorrow  contains  2  labels

전처리 과정이 필요한 Date 특성이 존재한다.

df['Date'].dtypes
dtype('O')

Date 특성의 데이터 타입은 object이다.

해당 특성은 현재 날짜로 되어있는데 이를 날짜/시간으로 변경한다.

df['Date'] = pd.to_datetime(df['Date'])
# 데이터셋에 연도 추가

df['Year'] = df['Date'].dt.year
df['Year'].head()
0    2008
1    2008
2    2008
3    2008
4    2008
Name: Year, dtype: int64
# 데이터셋에 월 추가

df['Month'] = df['Date'].dt.month
df['Month']
0         12
1         12
2         12
3         12
4         12
          ..
142188     6
142189     6
142190     6
142191     6
142192     6
Name: Month, Length: 142193, dtype: int64
# 데이터셋에 일 추가

df['Day'] = df['Date'].dt.day
df['Day']
0          1
1          2
2          3
3          4
4          5
          ..
142188    20
142189    21
142190    22
142191    23
142192    24
Name: Day, Length: 142193, dtype: int64
# 전체 데이터셋 확인

df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 142193 entries, 0 to 142192
Data columns (total 26 columns):
 #   Column         Non-Null Count   Dtype         
---  ------         --------------   -----         
 0   Date           142193 non-null  datetime64[ns]
 1   Location       142193 non-null  object        
 2   MinTemp        141556 non-null  float64       
 3   MaxTemp        141871 non-null  float64       
 4   Rainfall       140787 non-null  float64       
 5   Evaporation    81350 non-null   float64       
 6   Sunshine       74377 non-null   float64       
 7   WindGustDir    132863 non-null  object        
 8   WindGustSpeed  132923 non-null  float64       
 9   WindDir9am     132180 non-null  object        
 10  WindDir3pm     138415 non-null  object        
 11  WindSpeed9am   140845 non-null  float64       
 12  WindSpeed3pm   139563 non-null  float64       
 13  Humidity9am    140419 non-null  float64       
 14  Humidity3pm    138583 non-null  float64       
 15  Pressure9am    128179 non-null  float64       
 16  Pressure3pm    128212 non-null  float64       
 17  Cloud9am       88536 non-null   float64       
 18  Cloud3pm       85099 non-null   float64       
 19  Temp9am        141289 non-null  float64       
 20  Temp3pm        139467 non-null  float64       
 21  RainToday      140787 non-null  object        
 22  RainTomorrow   142193 non-null  object        
 23  Year           142193 non-null  int64         
 24  Month          142193 non-null  int64         
 25  Day            142193 non-null  int64         
dtypes: datetime64[ns](1), float64(16), int64(3), object(6)
memory usage: 28.2+ MB

추가한 특성 Year, Month, Day가 있는 것을 볼 수 있다.

이제 필요없어진 Date 특성을 삭제한다.

df.drop('Date', axis=1, inplace=True)
df.head()
Location MinTemp MaxTemp Rainfall Evaporation Sunshine WindGustDir WindGustSpeed WindDir9am WindDir3pm ... Pressure3pm Cloud9am Cloud3pm Temp9am Temp3pm RainToday RainTomorrow Year Month Day
0 Albury 13.4 22.9 0.6 NaN NaN W 44.0 W WNW ... 1007.1 8.0 NaN 16.9 21.8 No No 2008 12 1
1 Albury 7.4 25.1 0.0 NaN NaN WNW 44.0 NNW WSW ... 1007.8 NaN NaN 17.2 24.3 No No 2008 12 2
2 Albury 12.9 25.7 0.0 NaN NaN WSW 46.0 W WSW ... 1008.7 NaN 2.0 21.0 23.2 No No 2008 12 3
3 Albury 9.2 28.0 0.0 NaN NaN NE 24.0 SE E ... 1012.8 NaN NaN 18.1 26.5 No No 2008 12 4
4 Albury 17.5 32.3 1.0 NaN NaN W 41.0 ENE NW ... 1006.0 7.0 8.0 17.8 29.7 No No 2008 12 5

5 rows × 25 columns

범주형 특성 문제 해결

categorical = [var for var in df.columns if df[var].dtype=='O']

print(f"범주형 특성의 개수 : {len(categorical)}")
print(f"범주형 특성 : {categorical}")
범주형 특성의 개수 : 6
범주형 특성 : ['Location', 'WindGustDir', 'WindDir9am', 'WindDir3pm', 'RainToday', 'RainTomorrow']

Location 특성 문제 해결

# Location 특성 카디널리티 개수 확인

print('Location contains', len(df.Location.unique()), 'labels')
Location contains 49 labels
# 카디널리티 확인

df.Location.unique()
array(['Albury', 'BadgerysCreek', 'Cobar', 'CoffsHarbour', 'Moree',
       'Newcastle', 'NorahHead', 'NorfolkIsland', 'Penrith', 'Richmond',
       'Sydney', 'SydneyAirport', 'WaggaWagga', 'Williamtown',
       'Wollongong', 'Canberra', 'Tuggeranong', 'MountGinini', 'Ballarat',
       'Bendigo', 'Sale', 'MelbourneAirport', 'Melbourne', 'Mildura',
       'Nhil', 'Portland', 'Watsonia', 'Dartmoor', 'Brisbane', 'Cairns',
       'GoldCoast', 'Townsville', 'Adelaide', 'MountGambier', 'Nuriootpa',
       'Woomera', 'Albany', 'Witchcliffe', 'PearceRAAF', 'PerthAirport',
       'Perth', 'SalmonGums', 'Walpole', 'Hobart', 'Launceston',
       'AliceSprings', 'Darwin', 'Katherine', 'Uluru'], dtype=object)
# 특성 값의 빈도수 확인

df.Location.value_counts()
Canberra            3418
Sydney              3337
Perth               3193
Darwin              3192
Hobart              3188
Brisbane            3161
Adelaide            3090
Bendigo             3034
Townsville          3033
AliceSprings        3031
MountGambier        3030
Launceston          3028
Ballarat            3028
Albany              3016
Albury              3011
PerthAirport        3009
MelbourneAirport    3009
Mildura             3007
SydneyAirport       3005
Nuriootpa           3002
Sale                3000
Watsonia            2999
Tuggeranong         2998
Portland            2996
Woomera             2990
Cairns              2988
Cobar               2988
Wollongong          2983
GoldCoast           2980
WaggaWagga          2976
Penrith             2964
NorfolkIsland       2964
SalmonGums          2955
Newcastle           2955
CoffsHarbour        2953
Witchcliffe         2952
Richmond            2951
Dartmoor            2943
NorahHead           2929
BadgerysCreek       2928
MountGinini         2907
Moree               2854
Walpole             2819
PearceRAAF          2762
Williamtown         2553
Melbourne           2435
Nhil                1569
Katherine           1559
Uluru               1521
Name: Location, dtype: int64
# get_dummies메소드로 원-핫 인코딩

pd.get_dummies(df.Location, drop_first=True).head()
Albany Albury AliceSprings BadgerysCreek Ballarat Bendigo Brisbane Cairns Canberra Cobar ... Townsville Tuggeranong Uluru WaggaWagga Walpole Watsonia Williamtown Witchcliffe Wollongong Woomera
0 0 1 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
1 0 1 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
2 0 1 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
3 0 1 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
4 0 1 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0

5 rows × 48 columns

WindGustDir 특성 문제 해결

# WindGustDir 특성 카디널리티 개수 확인

print('WindGustDir contains', len(df.WindGustDir.unique()), 'labels')
WindGustDir contains 17 labels
# 카디널리티 확인

df['WindGustDir'].unique()
array(['W', 'WNW', 'WSW', 'NE', 'NNW', 'N', 'NNE', 'SW', 'ENE', 'SSE',
       'S', 'NW', 'SE', 'ESE', nan, 'E', 'SSW'], dtype=object)
# 특성 값의 빈도수 확인

df.WindGustDir.value_counts()
W      9780
SE     9309
E      9071
N      9033
SSE    8993
S      8949
WSW    8901
SW     8797
SSW    8610
WNW    8066
NW     8003
ENE    7992
ESE    7305
NE     7060
NNW    6561
NNE    6433
Name: WindGustDir, dtype: int64
# get_dummies메소드로 원-핫 인코딩
# 결측치가 얼마나 있는지 확인하는 특성 추가

pd.get_dummies(df.WindGustDir, drop_first=True, dummy_na=True).head()
ENE ESE N NE NNE NNW NW S SE SSE SSW SW W WNW WSW NaN
0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0
1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0
2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0
3 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0
4 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0
# 행 기준으로 1의 개수 출력

pd.get_dummies(df.WindGustDir, drop_first=True, dummy_na=True).sum(axis=0)
ENE    7992
ESE    7305
N      9033
NE     7060
NNE    6433
NNW    6561
NW     8003
S      8949
SE     9309
SSE    8993
SSW    8610
SW     8797
W      9780
WNW    8066
WSW    8901
NaN    9330
dtype: int64

WindGustDir특성 확인 결과 9330개의 결측치가 존재한다.

WindDir9am 특성 문제 해결

# WindDir9am 특성 카디널리티 개수 확인

print('WindDir9am contains', len(df['WindDir9am'].unique()), 'labels')
WindDir9am contains 17 labels
# 카디널리티 확인

df.WindDir9am.unique()
array(['W', 'NNW', 'SE', 'ENE', 'SW', 'SSE', 'S', 'NE', nan, 'SSW', 'N',
       'WSW', 'ESE', 'E', 'NW', 'WNW', 'NNE'], dtype=object)
# 특성 값의 빈도수 확인

df['WindDir9am'].value_counts()
N      11393
SE      9162
E       9024
SSE     8966
NW      8552
S       8493
W       8260
SW      8237
NNE     7948
NNW     7840
ENE     7735
ESE     7558
NE      7527
SSW     7448
WNW     7194
WSW     6843
Name: WindDir9am, dtype: int64
# 원-핫 인코딩

pd.get_dummies(df.WindDir9am, drop_first=True, dummy_na=True).head()
ENE ESE N NE NNE NNW NW S SE SSE SSW SW W WNW WSW NaN
0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0
1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0
2 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0
3 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0
4 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
# 행 기준으로 1의 개수 출력

pd.get_dummies(df.WindDir9am, drop_first=True, dummy_na=True).sum(axis=0)
ENE     7735
ESE     7558
N      11393
NE      7527
NNE     7948
NNW     7840
NW      8552
S       8493
SE      9162
SSE     8966
SSW     7448
SW      8237
W       8260
WNW     7194
WSW     6843
NaN    10013
dtype: int64

WindDir9am특성에 10013개의 결측치가 존재한다.

WindDir3pm 특성 문제 해결

# WindDir3pm 특성 카디널리티 개수 확인

print('WindDir3pm contains', len(df['WindDir3pm'].unique()), 'labels')
WindDir3pm contains 17 labels
# 카디널리티 확인

df.WindDir3pm.unique()
array(['WNW', 'WSW', 'E', 'NW', 'W', 'SSE', 'ESE', 'ENE', 'NNW', 'SSW',
       'SW', 'SE', 'N', 'S', 'NNE', nan, 'NE'], dtype=object)
# 특성 값 빈도수 확인

df['WindDir3pm'].value_counts()
SE     10663
W       9911
S       9598
WSW     9329
SW      9182
SSE     9142
N       8667
WNW     8656
NW      8468
ESE     8382
E       8342
NE      8164
SSW     8010
NNW     7733
ENE     7724
NNE     6444
Name: WindDir3pm, dtype: int64
# 원-핫 인코딩

pd.get_dummies(df.WindDir3pm, drop_first=True, dummy_na=True).head()
ENE ESE N NE NNE NNW NW S SE SSE SSW SW W WNW WSW NaN
0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0
2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0
3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
4 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0
# 행 기준으로 1의 개수 출력

pd.get_dummies(df.WindDir3pm, drop_first=True, dummy_na=True).sum(axis=0)
ENE     7724
ESE     8382
N       8667
NE      8164
NNE     6444
NNW     7733
NW      8468
S       9598
SE     10663
SSE     9142
SSW     8010
SW      9182
W       9911
WNW     8656
WSW     9329
NaN     3778
dtype: int64

WindDir3pm특성에는 3778개의 결측치가 존재한다.

RainToday 특성 문제 해결

# RainToday 특성 카디널리티 개수 확인

print('RainToday contains', len(df['RainToday'].unique()), 'labels')
RainToday contains 3 labels
# 카디널리티 확인

df['RainToday'].unique()
array(['No', 'Yes', nan], dtype=object)
# 특성 값 확인

df.RainToday.value_counts()
No     109332
Yes     31455
Name: RainToday, dtype: int64
# 원-핫 인코딩

pd.get_dummies(df.RainToday, drop_first=True, dummy_na=True).head()
Yes NaN
0 0 0
1 0 0
2 0 0
3 0 0
4 0 0
# 행 기준 1의 개수 출력

pd.get_dummies(df.RainToday, drop_first=True, dummy_na=True).sum(axis=0)
Yes    31455
NaN     1406
dtype: int64

RainToday특성에는 1406개의 결측치가 존재한다.

수치형 특성 문제 해결

# 수치형 특성 탐색

numerical = [var for var in df.columns if df[var].dtype!='O']

print(f"수치형 특성의 개수 : {len(numerical)}")
print(f"수치형 특성 : {numerical}")
수치형 특성의 개수 : 19
수치형 특성 : ['MinTemp', 'MaxTemp', 'Rainfall', 'Evaporation', 'Sunshine', 'WindGustSpeed', 'WindSpeed9am', 'WindSpeed3pm', 'Humidity9am', 'Humidity3pm', 'Pressure9am', 'Pressure3pm', 'Cloud9am', 'Cloud3pm', 'Temp9am', 'Temp3pm', 'Year', 'Month', 'Day']
df[numerical].head()
MinTemp MaxTemp Rainfall Evaporation Sunshine WindGustSpeed WindSpeed9am WindSpeed3pm Humidity9am Humidity3pm Pressure9am Pressure3pm Cloud9am Cloud3pm Temp9am Temp3pm Year Month Day
0 13.4 22.9 0.6 NaN NaN 44.0 20.0 24.0 71.0 22.0 1007.7 1007.1 8.0 NaN 16.9 21.8 2008 12 1
1 7.4 25.1 0.0 NaN NaN 44.0 4.0 22.0 44.0 25.0 1010.6 1007.8 NaN NaN 17.2 24.3 2008 12 2
2 12.9 25.7 0.0 NaN NaN 46.0 19.0 26.0 38.0 30.0 1007.6 1008.7 NaN 2.0 21.0 23.2 2008 12 3
3 9.2 28.0 0.0 NaN NaN 24.0 11.0 9.0 45.0 16.0 1017.6 1012.8 NaN NaN 18.1 26.5 2008 12 4
4 17.5 32.3 1.0 NaN NaN 41.0 7.0 20.0 82.0 33.0 1010.8 1006.0 7.0 8.0 17.8 29.7 2008 12 5

수치형 특성 요약

  • 16개의 수치형 특성 존재(새로 추가한 특성 Year, Month, Day 제외)

  • 모든 수치형 변수는 연속적

수치형 특성 문제 해결

# 수치형 특성의 결측치 확인

df[numerical].isnull().sum()
MinTemp            637
MaxTemp            322
Rainfall          1406
Evaporation      60843
Sunshine         67816
WindGustSpeed     9270
WindSpeed9am      1348
WindSpeed3pm      2630
Humidity9am       1774
Humidity3pm       3610
Pressure9am      14014
Pressure3pm      13981
Cloud9am         53657
Cloud3pm         57094
Temp9am            904
Temp3pm           2726
Year                 0
Month                0
Day                  0
dtype: int64

16개의 특성 모두 결측치가 존재한다.

# 이상치 확인

print(round(df[numerical].describe()),2)
        MinTemp   MaxTemp  Rainfall  Evaporation  Sunshine  WindGustSpeed  \
count  141556.0  141871.0  140787.0      81350.0   74377.0       132923.0   
mean       12.0      23.0       2.0          5.0       8.0           40.0   
std         6.0       7.0       8.0          4.0       4.0           14.0   
min        -8.0      -5.0       0.0          0.0       0.0            6.0   
25%         8.0      18.0       0.0          3.0       5.0           31.0   
50%        12.0      23.0       0.0          5.0       8.0           39.0   
75%        17.0      28.0       1.0          7.0      11.0           48.0   
max        34.0      48.0     371.0        145.0      14.0          135.0   

       WindSpeed9am  WindSpeed3pm  Humidity9am  Humidity3pm  Pressure9am  \
count      140845.0      139563.0     140419.0     138583.0     128179.0   
mean           14.0          19.0         69.0         51.0       1018.0   
std             9.0           9.0         19.0         21.0          7.0   
min             0.0           0.0          0.0          0.0        980.0   
25%             7.0          13.0         57.0         37.0       1013.0   
50%            13.0          19.0         70.0         52.0       1018.0   
75%            19.0          24.0         83.0         66.0       1022.0   
max           130.0          87.0        100.0        100.0       1041.0   

       Pressure3pm  Cloud9am  Cloud3pm   Temp9am   Temp3pm      Year  \
count     128212.0   88536.0   85099.0  141289.0  139467.0  142193.0   
mean        1015.0       4.0       5.0      17.0      22.0    2013.0   
std            7.0       3.0       3.0       6.0       7.0       3.0   
min          977.0       0.0       0.0      -7.0      -5.0    2007.0   
25%         1010.0       1.0       2.0      12.0      17.0    2011.0   
50%         1015.0       5.0       5.0      17.0      21.0    2013.0   
75%         1020.0       7.0       7.0      22.0      26.0    2015.0   
max         1040.0       9.0       9.0      40.0      47.0    2017.0   

          Month       Day  
count  142193.0  142193.0  
mean        6.0      16.0  
std         3.0       9.0  
min         1.0       1.0  
25%         3.0       8.0  
50%         6.0      16.0  
75%         9.0      23.0  
max        12.0      31.0   2

Rainfall, Evaporation, WindSpeed9am, WindSpeed3pm 특성에 이상치가 존재한다.

이상치를 boxplot으로 시각화한다.

plt.figure(figsize=(15,10))


plt.subplot(2, 2, 1)
fig = df.boxplot(column='Rainfall')
fig.set_title('')
fig.set_ylabel('Rainfall')


plt.subplot(2, 2, 2)
fig = df.boxplot(column='Evaporation')
fig.set_title('')
fig.set_ylabel('Evaporation')


plt.subplot(2, 2, 3)
fig = df.boxplot(column='WindSpeed9am')
fig.set_title('')
fig.set_ylabel('WindSpeed9am')


plt.subplot(2, 2, 4)
fig = df.boxplot(column='WindSpeed3pm')
fig.set_title('')
fig.set_ylabel('WindSpeed3pm')
Text(0, 0.5, 'WindSpeed3pm')

특성의 분포 확인

히스토그램으로 정규 분포인지 왜곡된 분포인지 확인한다.

정규 분포를 따를 경우 극값 분석을, 왜곡된 분포일 경우 IQR을 찾는다.

plt.figure(figsize=(15,10))


plt.subplot(2, 2, 1)
fig = df.Rainfall.hist(bins=10)
fig.set_xlabel('Rainfall')
fig.set_ylabel('RainTomorrow')


plt.subplot(2, 2, 2)
fig = df.Evaporation.hist(bins=10)
fig.set_xlabel('Evaporation')
fig.set_ylabel('RainTomorrow')


plt.subplot(2, 2, 3)
fig = df.WindSpeed9am.hist(bins=10)
fig.set_xlabel('WindSpeed9am')
fig.set_ylabel('RainTomorrow')


plt.subplot(2, 2, 4)
fig = df.WindSpeed3pm.hist(bins=10)
fig.set_xlabel('WindSpeed3pm')
Text(0.5, 0, 'WindSpeed3pm')

네 특성 모두 정규 분포를 따르지 않고 왜곡된 분포이다.

따라서 IQR을 이용하여 이상치를 찾는다.

# Rainfall 특성 이상치 탐색

IQR = df.Rainfall.quantile(0.75) - df.Rainfall.quantile(0.25)

Lower_fence = df.Rainfall.quantile(0.25) - (IQR * 3)
Upper_fence = df.Rainfall.quantile(0.75) + (IQR * 3)

print(f"Rainfall 이상치 < {Lower_fence} or > {Upper_fence}")
Rainfall 이상치 < -2.4000000000000004 or > 3.2

RainFall 특성은 0부터 371 사이의 값으로 이루어져있다.

따라서 이상치의 범위는 3.2보다 큰 값이 해당된다.

# Evaporation 특성 이상치 탐색

IQR = df.Evaporation.quantile(0.75) - df.Evaporation.quantile(0.25)

Lower_fence = df.Evaporation.quantile(0.25) - (IQR * 3)
Upper_fence = df.Evaporation.quantile(0.75) + (IQR * 3)

print(f"Evaporation 이상치 < {Lower_fence} or > {Upper_fence}")
Evaporation 이상치 < -11.800000000000002 or > 21.800000000000004

Evaporation 특성은 0부터 145 사이의 값으로 이루어져있다.

따라서 이상치의 범위는 21.8보다 큰 값이 해당된다.

# WindSpeed9am 특성 이상치 탐색

IQR = df.WindSpeed9am.quantile(0.75) - df.WindSpeed9am.quantile(0.25)

Lower_fence = df.WindSpeed9am.quantile(0.25) - (IQR * 3)
Upper_fence = df.WindSpeed9am.quantile(0.75) + (IQR * 3)

print(f"WindSpeed9am 이상치 < {Lower_fence} or > {Upper_fence}")
WindSpeed9am 이상치 < -29.0 or > 55.0

WindSpeed9am 특성은 0부터 130 사이의 값으로 이루어져있다.

따라서 이상치의 범위는 55보다 큰 값이 해당된다.

# WindSpeed3pm 특성 확인

IQR = df.WindSpeed3pm.quantile(0.75) - df.WindSpeed3pm.quantile(0.25)

Lower_fence = df.WindSpeed3pm.quantile(0.25) - (IQR * 3)
Upper_fence = df.WindSpeed3pm.quantile(0.75) + (IQR * 3)

print(f"WindSpeed3pm 이상치 < {Lower_fence} or > {Upper_fence}")
WindSpeed3pm 이상치 < -20.0 or > 57.0

WindSpeed3pm 특성은 0부터 87 사이의 값으로 이루어져있다.

따라서 이상치의 범위는 57보다 큰 값이 해당된다.

입력 데이터셋과 타깃 데이터셋 선언

X = df.drop(['RainTomorrow'], axis=1)
y = df['RainTomorrow']

훈련셋과 테스트셋 분할

# 훈련셋과 테스트셋의 비율을 8:2로 지정

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, random_state = 0)
# 훈련셋과 테스트셋의 크기 확인

X_train.shape, X_test.shape
((113754, 24), (28439, 24))

특성 정제

특성을 정제함으로써 처음 데이터셋을 유용한 데이터셋으로 변환하여 더 잘 이해하고 예측 정확도를 높이는데 도움이 된다.

먼저 범주형과 수치형 특성을 구분한다.

X_train.dtypes
Location          object
MinTemp          float64
MaxTemp          float64
Rainfall         float64
Evaporation      float64
Sunshine         float64
WindGustDir       object
WindGustSpeed    float64
WindDir9am        object
WindDir3pm        object
WindSpeed9am     float64
WindSpeed3pm     float64
Humidity9am      float64
Humidity3pm      float64
Pressure9am      float64
Pressure3pm      float64
Cloud9am         float64
Cloud3pm         float64
Temp9am          float64
Temp3pm          float64
RainToday         object
Year               int64
Month              int64
Day                int64
dtype: object
# 범주형 특성

categorical = [col for col in X_train.columns if X_train[col].dtypes == 'O']
categorical
['Location', 'WindGustDir', 'WindDir9am', 'WindDir3pm', 'RainToday']
# 수치형 특성

numerical = [col for col in X_train.columns if X_train[col].dtypes != 'O']
numerical
['MinTemp',
 'MaxTemp',
 'Rainfall',
 'Evaporation',
 'Sunshine',
 'WindGustSpeed',
 'WindSpeed9am',
 'WindSpeed3pm',
 'Humidity9am',
 'Humidity3pm',
 'Pressure9am',
 'Pressure3pm',
 'Cloud9am',
 'Cloud3pm',
 'Temp9am',
 'Temp3pm',
 'Year',
 'Month',
 'Day']
# 수치형 특성의 결측치 확인

X_train[numerical].isnull().sum()
MinTemp            495
MaxTemp            264
Rainfall          1139
Evaporation      48718
Sunshine         54314
WindGustSpeed     7367
WindSpeed9am      1086
WindSpeed3pm      2094
Humidity9am       1449
Humidity3pm       2890
Pressure9am      11212
Pressure3pm      11186
Cloud9am         43137
Cloud3pm         45768
Temp9am            740
Temp3pm           2171
Year                 0
Month                0
Day                  0
dtype: int64
X_test[numerical].isnull().sum()
MinTemp            142
MaxTemp             58
Rainfall           267
Evaporation      12125
Sunshine         13502
WindGustSpeed     1903
WindSpeed9am       262
WindSpeed3pm       536
Humidity9am        325
Humidity3pm        720
Pressure9am       2802
Pressure3pm       2795
Cloud9am         10520
Cloud3pm         11326
Temp9am            164
Temp3pm            555
Year                 0
Month                0
Day                  0
dtype: int64
# 결측치의 퍼센트 확인

for col in numerical:
    if X_train[col].isnull().mean() > 0:
        print(col, round(X_train[col].isnull().mean(),4))
MinTemp 0.0044
MaxTemp 0.0023
Rainfall 0.01
Evaporation 0.4283
Sunshine 0.4775
WindGustSpeed 0.0648
WindSpeed9am 0.0095
WindSpeed3pm 0.0184
Humidity9am 0.0127
Humidity3pm 0.0254
Pressure9am 0.0986
Pressure3pm 0.0983
Cloud9am 0.3792
Cloud3pm 0.4023
Temp9am 0.0065
Temp3pm 0.0191

데이터가 무작위로 누락되었다는 가정하에 결측치를 추정값으로 대체하는 방법에는 두 가지가 존재한다.

하나는 평균값 또는 중앙값으로 대체하는 것이고, 다른 하나는 무작위 샘플 추정이다.

데이터셋에 이상치가 존재하는 경우 중앙값으로 대체하는 것이 좋다.

따라서 중앙값으로 결측치를 대체한다.

과적합을 피하기 위해서는 추정값으로 결측치를 대체할 때 훈련셋은 훈련셋에 대하여, 테스트셋은 테스트셋에 대하여 추정해야한다.

for df1 in [X_train, X_test]:
    for col in numerical:
        col_median=X_train[col].median()
        df1[col].fillna(col_median, inplace=True)
# 결측치가 존재하는지 다시한번 확인

X_train[numerical].isnull().sum()
MinTemp          0
MaxTemp          0
Rainfall         0
Evaporation      0
Sunshine         0
WindGustSpeed    0
WindSpeed9am     0
WindSpeed3pm     0
Humidity9am      0
Humidity3pm      0
Pressure9am      0
Pressure3pm      0
Cloud9am         0
Cloud3pm         0
Temp9am          0
Temp3pm          0
Year             0
Month            0
Day              0
dtype: int64
X_test[numerical].isnull().sum()
MinTemp          0
MaxTemp          0
Rainfall         0
Evaporation      0
Sunshine         0
WindGustSpeed    0
WindSpeed9am     0
WindSpeed3pm     0
Humidity9am      0
Humidity3pm      0
Pressure9am      0
Pressure3pm      0
Cloud9am         0
Cloud3pm         0
Temp9am          0
Temp3pm          0
Year             0
Month            0
Day              0
dtype: int64
# 범주형 특성의 결측치 확인

X_train[categorical].isnull().sum()
Location          0
WindGustDir    7407
WindDir9am     7978
WindDir3pm     3008
RainToday      1139
dtype: int64
# 결측치의 퍼센트 확인

for col in categorical:
    if X_train[col].isnull().mean() > 0:
        print(col, (X_train[col].isnull().mean()))
WindGustDir 0.06511419378659213
WindDir9am 0.07013379749283542
WindDir3pm 0.026443026179299188
RainToday 0.01001283471350458
# 결측치를 빈도수가 가장 높은 값으로 대체

for df2 in [X_train, X_test]:
    df2['WindGustDir'].fillna(X_train['WindGustDir'].mode()[0], inplace=True)
    df2['WindDir9am'].fillna(X_train['WindDir9am'].mode()[0], inplace=True)
    df2['WindDir3pm'].fillna(X_train['WindDir3pm'].mode()[0], inplace=True)
    df2['RainToday'].fillna(X_train['RainToday'].mode()[0], inplace=True)
# 결측치가 있는지 다시한번 확인

X_train[categorical].isnull().sum()
Location       0
WindGustDir    0
WindDir9am     0
WindDir3pm     0
RainToday      0
dtype: int64
X_test[categorical].isnull().sum()
Location       0
WindGustDir    0
WindDir9am     0
WindDir3pm     0
RainToday      0
dtype: int64
# 전체 데이터셋의 결측치 확인

X_train.isnull().sum()
Location         0
MinTemp          0
MaxTemp          0
Rainfall         0
Evaporation      0
Sunshine         0
WindGustDir      0
WindGustSpeed    0
WindDir9am       0
WindDir3pm       0
WindSpeed9am     0
WindSpeed3pm     0
Humidity9am      0
Humidity3pm      0
Pressure9am      0
Pressure3pm      0
Cloud9am         0
Cloud3pm         0
Temp9am          0
Temp3pm          0
RainToday        0
Year             0
Month            0
Day              0
dtype: int64
X_test.isnull().sum()
Location         0
MinTemp          0
MaxTemp          0
Rainfall         0
Evaporation      0
Sunshine         0
WindGustDir      0
WindGustSpeed    0
WindDir9am       0
WindDir3pm       0
WindSpeed9am     0
WindSpeed3pm     0
Humidity9am      0
Humidity3pm      0
Pressure9am      0
Pressure3pm      0
Cloud9am         0
Cloud3pm         0
Temp9am          0
Temp3pm          0
RainToday        0
Year             0
Month            0
Day              0
dtype: int64

수치형 특성의 이상치 문제 해결

앞서 Rainfall, Evaporation, WindSpeed9am, WindSpeed3pm 각각의 특성에 이상치가 존재하는 것을 알았다.

top-coding 방식을 이용하여 최댓값을 제한하고 이상치를 제거한다.

def max_value(df3, variable, top):
    return np.where(df3[variable]>top, top, df3[variable])

for df3 in [X_train, X_test]:
    df3['Rainfall'] = max_value(df3, 'Rainfall', 3.2)
    df3['Evaporation'] = max_value(df3, 'Evaporation', 21.8)
    df3['WindSpeed9am'] = max_value(df3, 'WindSpeed9am', 55)
    df3['WindSpeed3pm'] = max_value(df3, 'WindSpeed3pm', 57)
X_train.Rainfall.max(), X_test.Rainfall.max()
(3.2, 3.2)
X_train.Evaporation.max(), X_test.Evaporation.max()
(21.8, 21.8)
X_train.WindSpeed9am.max(), X_test.WindSpeed9am.max()
(55.0, 55.0)
X_train.WindSpeed3pm.max(), X_test.WindSpeed3pm.max()
(57.0, 57.0)
X_train[numerical].describe()
MinTemp MaxTemp Rainfall Evaporation Sunshine WindGustSpeed WindSpeed9am WindSpeed3pm Humidity9am Humidity3pm Pressure9am Pressure3pm Cloud9am Cloud3pm Temp9am Temp3pm Year Month Day
count 113754.000000 113754.000000 113754.000000 113754.000000 113754.000000 113754.000000 113754.000000 113754.000000 113754.000000 113754.000000 113754.000000 113754.000000 113754.000000 113754.000000 113754.000000 113754.000000 113754.000000 113754.000000 113754.000000
mean 12.193497 23.237216 0.675080 5.151606 8.041154 39.884074 13.978155 18.614756 68.867486 51.509547 1017.640649 1015.241101 4.651801 4.703588 16.995062 21.688643 2012.759727 6.404021 15.710419
std 6.388279 7.094149 1.183837 2.823707 2.769480 13.116959 8.806558 8.685862 18.935587 20.530723 6.738680 6.675168 2.292726 2.117847 6.463772 6.855649 2.540419 3.427798 8.796821
min -8.200000 -4.800000 0.000000 0.000000 0.000000 6.000000 0.000000 0.000000 0.000000 0.000000 980.500000 977.100000 0.000000 0.000000 -7.200000 -5.400000 2007.000000 1.000000 1.000000
25% 7.600000 18.000000 0.000000 4.000000 8.200000 31.000000 7.000000 13.000000 57.000000 37.000000 1013.500000 1011.000000 3.000000 4.000000 12.300000 16.700000 2011.000000 3.000000 8.000000
50% 12.000000 22.600000 0.000000 4.800000 8.500000 39.000000 13.000000 19.000000 70.000000 52.000000 1017.600000 1015.200000 5.000000 5.000000 16.700000 21.100000 2013.000000 6.000000 16.000000
75% 16.800000 28.200000 0.600000 5.400000 8.700000 46.000000 19.000000 24.000000 83.000000 65.000000 1021.800000 1019.400000 6.000000 6.000000 21.500000 26.300000 2015.000000 9.000000 23.000000
max 33.900000 48.100000 3.200000 21.800000 14.500000 135.000000 55.000000 57.000000 100.000000 100.000000 1041.000000 1039.600000 9.000000 8.000000 40.200000 46.700000 2017.000000 12.000000 31.000000

이상치가 존재하였던 특성들이 상한선을 넘은 것을 볼 수 있다.

# 범주형 특성 인코딩

categorical
['Location', 'WindGustDir', 'WindDir9am', 'WindDir3pm', 'RainToday']
X_train[categorical].head()
Location WindGustDir WindDir9am WindDir3pm RainToday
110803 Witchcliffe S SSE S No
87289 Cairns ENE SSE SE Yes
134949 AliceSprings E NE N No
85553 Cairns ESE SSE E No
16110 Newcastle W N SE No
# RainToday 특성 인코딩

import category_encoders as ce

encoder = ce.BinaryEncoder(cols=['RainToday'])

X_train = encoder.fit_transform(X_train)
X_test = encoder. fit_transform(X_test)
X_train.head()
Location MinTemp MaxTemp Rainfall Evaporation Sunshine WindGustDir WindGustSpeed WindDir9am WindDir3pm ... Pressure3pm Cloud9am Cloud3pm Temp9am Temp3pm RainToday_0 RainToday_1 Year Month Day
110803 Witchcliffe 13.9 22.6 0.2 4.8 8.5 S 41.0 SSE S ... 1013.4 5.0 5.0 18.8 20.4 0 1 2014 4 25
87289 Cairns 22.4 29.4 2.0 6.0 6.3 ENE 33.0 SSE SE ... 1013.1 7.0 5.0 26.4 27.5 1 0 2015 11 2
134949 AliceSprings 9.7 36.2 0.0 11.4 12.3 E 31.0 NE N ... 1013.6 1.0 1.0 28.5 35.0 0 1 2014 10 19
85553 Cairns 20.5 30.1 0.0 8.8 11.1 ESE 37.0 SSE E ... 1010.8 2.0 3.0 27.3 29.4 0 1 2010 10 30
16110 Newcastle 16.8 29.2 0.0 4.8 8.5 W 39.0 N SE ... 1015.2 5.0 8.0 22.2 27.0 0 1 2012 11 8

5 rows × 25 columns

RainToday 특성이 RainToday_0RainToday_1으로 대체된 것을 확인할 수 있다.

나머지 범주형 특성들은 원-핫 인코딩을 이용하고 훈련셋을 구성한다.

X_train = pd.concat([X_train[numerical], X_train[['RainToday_0', 'RainToday_1']],
                   pd.get_dummies(X_train.Location),
                   pd.get_dummies(X_train.WindGustDir),
                   pd.get_dummies(X_train.WindDir9am),
                   pd.get_dummies(X_train.WindDir3pm)], axis=1)

테스트셋 또한 똑같은 과정을 적용한다.

X_test = pd.concat([X_test[numerical], X_test[['RainToday_0', 'RainToday_1']],
                     pd.get_dummies(X_test.Location), 
                     pd.get_dummies(X_test.WindGustDir),
                     pd.get_dummies(X_test.WindDir9am),
                     pd.get_dummies(X_test.WindDir3pm)], axis=1)
X_train.head()
MinTemp MaxTemp Rainfall Evaporation Sunshine WindGustSpeed WindSpeed9am WindSpeed3pm Humidity9am Humidity3pm ... NNW NW S SE SSE SSW SW W WNW WSW
110803 13.9 22.6 0.2 4.8 8.5 41.0 20.0 28.0 65.0 55.0 ... 0 0 1 0 0 0 0 0 0 0
87289 22.4 29.4 2.0 6.0 6.3 33.0 7.0 19.0 71.0 59.0 ... 0 0 0 1 0 0 0 0 0 0
134949 9.7 36.2 0.0 11.4 12.3 31.0 15.0 11.0 6.0 2.0 ... 0 0 0 0 0 0 0 0 0 0
85553 20.5 30.1 0.0 8.8 11.1 37.0 22.0 19.0 59.0 53.0 ... 0 0 0 0 0 0 0 0 0 0
16110 16.8 29.2 0.0 4.8 8.5 39.0 0.0 7.0 72.0 53.0 ... 0 0 0 1 0 0 0 0 0 0

5 rows × 118 columns

X_test.head()
MinTemp MaxTemp Rainfall Evaporation Sunshine WindGustSpeed WindSpeed9am WindSpeed3pm Humidity9am Humidity3pm ... NNW NW S SE SSE SSW SW W WNW WSW
86232 17.4 29.0 0.0 3.6 11.1 33.0 11.0 19.0 63.0 61.0 ... 0 0 0 0 0 0 0 0 0 0
57576 6.8 14.4 0.8 0.8 8.5 46.0 17.0 22.0 80.0 55.0 ... 0 0 1 0 0 0 0 0 0 0
124071 10.1 15.4 3.2 4.8 8.5 31.0 13.0 9.0 70.0 61.0 ... 0 0 0 0 1 0 0 0 0 0
117955 14.4 33.4 0.0 8.0 11.6 41.0 9.0 17.0 40.0 23.0 ... 0 0 0 0 0 0 1 0 0 0
133468 6.8 14.3 3.2 0.2 7.3 28.0 15.0 13.0 92.0 47.0 ... 0 0 0 0 0 0 0 0 0 0

5 rows × 118 columns

특성 스케일링

X_train.describe()
MinTemp MaxTemp Rainfall Evaporation Sunshine WindGustSpeed WindSpeed9am WindSpeed3pm Humidity9am Humidity3pm ... NNW NW S SE SSE SSW SW W WNW WSW
count 113754.000000 113754.000000 113754.000000 113754.000000 113754.000000 113754.000000 113754.000000 113754.000000 113754.000000 113754.000000 ... 113754.000000 113754.000000 113754.000000 113754.000000 113754.000000 113754.000000 113754.000000 113754.000000 113754.000000 113754.000000
mean 12.193497 23.237216 0.675080 5.151606 8.041154 39.884074 13.978155 18.614756 68.867486 51.509547 ... 0.054530 0.060288 0.067259 0.101605 0.064059 0.056402 0.064464 0.069334 0.060798 0.065483
std 6.388279 7.094149 1.183837 2.823707 2.769480 13.116959 8.806558 8.685862 18.935587 20.530723 ... 0.227061 0.238021 0.250471 0.302130 0.244860 0.230698 0.245578 0.254022 0.238960 0.247378
min -8.200000 -4.800000 0.000000 0.000000 0.000000 6.000000 0.000000 0.000000 0.000000 0.000000 ... 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
25% 7.600000 18.000000 0.000000 4.000000 8.200000 31.000000 7.000000 13.000000 57.000000 37.000000 ... 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
50% 12.000000 22.600000 0.000000 4.800000 8.500000 39.000000 13.000000 19.000000 70.000000 52.000000 ... 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
75% 16.800000 28.200000 0.600000 5.400000 8.700000 46.000000 19.000000 24.000000 83.000000 65.000000 ... 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
max 33.900000 48.100000 3.200000 21.800000 14.500000 135.000000 55.000000 57.000000 100.000000 100.000000 ... 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000

8 rows × 118 columns

cols_train = X_train.columns
cols_test = X_test.columns
from sklearn.preprocessing import MinMaxScaler

scaler = MinMaxScaler()

X_train = scaler.fit_transform(X_train)
X_test = scaler.fit_transform(X_test)
X_train = pd.DataFrame(X_train, columns=[cols_train])
X_test = pd.DataFrame(X_test, columns=[cols_test])
X_train.describe()
MinTemp MaxTemp Rainfall Evaporation Sunshine WindGustSpeed WindSpeed9am WindSpeed3pm Humidity9am Humidity3pm ... NNW NW S SE SSE SSW SW W WNW WSW
count 113754.000000 113754.000000 113754.000000 113754.000000 113754.000000 113754.000000 113754.000000 113754.000000 113754.000000 113754.000000 ... 113754.000000 113754.000000 113754.000000 113754.000000 113754.000000 113754.000000 113754.000000 113754.000000 113754.000000 113754.000000
mean 0.484406 0.530004 0.210962 0.236312 0.554562 0.262667 0.254148 0.326575 0.688675 0.515095 ... 0.054530 0.060288 0.067259 0.101605 0.064059 0.056402 0.064464 0.069334 0.060798 0.065483
std 0.151741 0.134105 0.369949 0.129528 0.190999 0.101682 0.160119 0.152384 0.189356 0.205307 ... 0.227061 0.238021 0.250471 0.302130 0.244860 0.230698 0.245578 0.254022 0.238960 0.247378
min 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 ... 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
25% 0.375297 0.431002 0.000000 0.183486 0.565517 0.193798 0.127273 0.228070 0.570000 0.370000 ... 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
50% 0.479810 0.517958 0.000000 0.220183 0.586207 0.255814 0.236364 0.333333 0.700000 0.520000 ... 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
75% 0.593824 0.623819 0.187500 0.247706 0.600000 0.310078 0.345455 0.421053 0.830000 0.650000 ... 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
max 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 ... 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000

8 rows × 118 columns

모델 훈련

from sklearn.linear_model import LogisticRegression

logreg = LogisticRegression(solver='liblinear', random_state=0)

logreg.fit(X_train, y_train)
LogisticRegression(random_state=0, solver='liblinear')

모델 예측

y_pred_test = logreg.predict(X_test)
y_pred_test
array(['No', 'No', 'No', ..., 'No', 'No', 'Yes'], dtype=object)

predict_proba 메소드

해당 메소드는 목표 변수(타깃 데이터)에 대한 확률을 배열 형태로 보여준다.

logreg.predict_proba(X_test)[:,0]
array([0.92190214, 0.85735432, 0.8511671 , ..., 0.98224021, 0.82899741,
       0.34815758])
logreg.predict_proba(X_test)[:,1]
array([0.07809786, 0.14264568, 0.1488329 , ..., 0.01775979, 0.17100259,
       0.65184242])

정확도 측정

from sklearn.metrics import accuracy_score

print(f"모델 정확도 : {accuracy_score(y_test, y_pred_test):0.4f}")
모델 정확도 : 0.8488

y_test는 실제 데이터, y_pred_test는 예측한 데이터이다.

훈련셋과 테스트셋의 정확도를 비교하여 과적합 여부를 판단한다.

y_pred_train = logreg.predict(X_train)
y_pred_train
array(['No', 'No', 'No', ..., 'No', 'No', 'No'], dtype=object)
print(f"훈련 셋 정확도 : {accuracy_score(y_train, y_pred_train):0.4f}")
훈련 셋 정확도 : 0.8477

과대적합 및 과소적합 여부 확인

print(f"훈련 셋 점수 : {logreg.score(X_train, y_train):0.4f}")
print(f"테스트 셋 점수 : {logreg.score(X_test, y_test):0.4f}")
훈련 셋 점수 : 0.8477
테스트 셋 점수 : 0.8488

훈련 셋과 테스트 셋의 점수 차이가 약 0.01밖에 차이가 나지 않으므로 과대적합은 아니다.

로지스틱 회귀에서 C는 기본값을 1로 사용한다.

훈련 셋 및 테스트 셋의 정확도는 약 85%로 우수한 성능이지만 두 셋의 정확도 차이가 얼마 나지 않으므로 과소적합을 의심해 볼 수 있다.

C 값에 변화를 주면서 살펴본다.

logreg100 = LogisticRegression(C=100, solver='liblinear', random_state=0)

logreg100.fit(X_train, y_train)
LogisticRegression(C=100, random_state=0, solver='liblinear')
print(f"훈련 셋 점수 : {logreg100.score(X_train, y_train):0.4f}")
print(f"테스트 셋 점수 : {logreg100.score(X_test, y_test):0.4f}")
훈련 셋 점수 : 0.8478
테스트 셋 점수 : 0.8478
logreg001 = LogisticRegression(C=0.01, solver='liblinear', random_state=0)

logreg001.fit(X_train, y_train)
LogisticRegression(C=0.01, random_state=0, solver='liblinear')
print(f"훈련 셋 점수 : {logreg001.score(X_train, y_train):0.4f}")
print(f"테스트 셋 점수 : {logreg001.score(X_test, y_test):0.4f}")
훈련 셋 점수 : 0.8409
테스트 셋 점수 : 0.8441

C 값을 증가시켜 100으로 지정하였을 때 훈련 셋 점수는 아주 조금 올라갔고, 테스트 셋 점수는 내려갔다.

0.01로 지정하였을 때는 두 훈련 셋 모두 점수가 내려간 결과를 볼 수 있다.

모델 정확도와 null 정확도 비교

현재 모델 정확도는 0.8488로, 모델 정확도만을 가지고 매우 우수하다고 볼 수 없다.

null 정확도와 비교해야 하는데, 가장 빈번한 클래스를 예측함으로써 얻을 수 있다.

y_test.value_counts()
No     22067
Yes     6372
Name: RainTomorrow, dtype: int64

클래스에서 가장 빈번하게 발생하는 횟수가 22067번임을 알 수 있다.

이를 총 발생횟수로 나누면 null 정확도를 계산할 수 있다.

null_accuracy = (22067/(22067+7372))

print(f"Null 정확도 : {null_accuracy:0.4f}")
Null 정확도 : 0.7496

모델 정확도는 0.8488, null 정확도는 0.7496으로 로지스틱 회귀 모델이 타깃을 예측하는데 잘 작동되고 있음을 알 수 있다.

아를 통해 분류 모델의 정확도가 좋음을 알 수 있고, 타깃을 예측하는 면에서도 잘 작동한다.

하지만, 기본 값의 분포는 주지않고, 또한 분류기가 어떤 유형의 오류를 범하는지 알려주지 않는다.

혼동 행렬(Confusion Matrix)

혼동 행렬을 이용하여 분류 모델의 성능과 모델에서 발생하는 오류 유형을 파악할 수 있다.

각 특성별로 분류된 올바른 예측과 잘못된 예측에 대한 요약을 표 형식으로 제공해준다.

분류 모델의 성능을 평가하는 동안 4가지 유형의 결과가 나올 수 있는데, 다음과 같다.

  1. TP

관찰이 특정 클래스에 속할 것으로 예측하고 실제로 해당 클래스에 속할 때 발생

  1. TN

관찰이 특정 글래스에 속하지 않는다고 예측하고 실제로 해당 클래스에 속하지 않을 때 발생

  1. FP

관찰이 특정 클래스에 속할 것으로 예측하였으나 실제로 해당 클래스에 속하지 않을 때 발생

  1. FN

관찰이 특정 클래스에 속하지 않는다고 예측하였으나 실제로 해당 클래스에 속할 때 발생

이 중 FN의 경우 심각한 오류이며 유형 Ⅱ오류라고 한다.

from sklearn.metrics import confusion_matrix

cm = confusion_matrix(y_test, y_pred_test)

print('Confusion matrix\n\n', cm)

print('\nTrue Positives(TP) = ', cm[0,0])

print('\nTrue Negatives(TN) = ', cm[1,1])

print('\nFalse Positives(FP) = ', cm[0,1])

print('\nFalse Negatives(FN) = ', cm[1,0])
Confusion matrix

 [[21109   958]
 [ 3343  3029]]

True Positives(TP) =  21109

True Negatives(TN) =  3029

False Positives(FP) =  958

False Negatives(FN) =  3343

24177개의 올바른 예측과 4262개의 잘못된 예측을 보여준다.

# heatmap으로 시각화

cm_matrix = pd.DataFrame(data=cm, columns=['Actual Positive:1', 'Actual Negative:0'], 
                                 index=['Predict Positive:1', 'Predict Negative:0'])

sns.heatmap(cm_matrix, annot=True, fmt='d', cmap='YlGnBu')
<AxesSubplot:>

분류 행렬

분류 보고서는 분류 모델의 성능을 평가하는 방법 중 하나이다.

모델의 정밀도, 재검색, F1 및 지원 점수가 표시된다.

다음의 코드로 표현할 수 있다.

from sklearn.metrics import classification_report

print(classification_report(y_test, y_pred_test))
              precision    recall  f1-score   support

          No       0.86      0.96      0.91     22067
         Yes       0.76      0.48      0.58      6372

    accuracy                           0.85     28439
   macro avg       0.81      0.72      0.75     28439
weighted avg       0.84      0.85      0.84     28439

분류 정확도

TP = cm[0,0]
TN = cm[1,1]
FP = cm[0,1]
FN = cm[1,0]

classification_accuracy = (TP + TN) / float(TP + TN + FP + FN)

print('분류 정확도 : {0:0.4f}'.format(classification_accuracy))
분류 정확도 : 0.8488

분류 오류

classification_error = (FP + FN) / float(TP + TN + FP + FN)

print('분류 오류 : {0:0.4f}'.format(classification_error))
분류 오류 : 0.1512

정확도

정확도는 예측된 모든 양성(P) 결과 중 올바르게 예측된 양성 결과의 비율로 정의한다.

즉, TP + FP에 대한 TP의 비율이다.

이는 정확하게 예측된 긍정적인 결과의 비율을 식별하는 것으로, N 보다는 P에 집중된다.

precision = TP / float(TP + FP)


print('정확도 : {0:0.4f}'.format(precision))
정확도 : 0.9566

리콜(민감도)

리콜은 실제 양성 결과 중 올바르게 예측된 양성 결과의 비율로 정의된다.

즉, TP + FN에 대한 TP의 비율이다.

recall = TP / float(TP + FN)

print('리콜(민감도) : {0:0.4f}'.format(recall))
리콜(민감도) : 0.8633

TP 비율

TP 비율과 리콜(민감도)는 동의어이다.

true_positive_rate = TP / float(TP + FN)


print('TP 비율 : {0:0.4f}'.format(true_positive_rate))
TP 비율 : 0.8633

FP 비율

false_positive_rate = FP / float(FP + TN)


print('FP 비율 : {0:0.4f}'.format(false_positive_rate))
FP 비율 : 0.2403

특이점

specificity = TN / (TN + FP)

print('특이점 : {0:0.4f}'.format(specificity))
특이점 : 0.7597

f1-score

f1-score는 정확도와 리콜의 조화평균으로, 최댓값 1, 최저값 0을 가진다.

따라서 f1-score는 정확도와 리콜을 계산에 포함하기에 정확도 측정값보다 항상 낮은 값을 갖는다.

분류기 모델을 비교할 때에는 f1-score의 가중 평균을 사용하여 비교해야한다.

Support

Support는 데이터 셋에서 클래스의 실제 발생 횟수를 의미한다.

임계값 레벨 조정

# 처음 10개의 예측된 확률값 출력

y_pred_prob = logreg.predict_proba(X_test)[0:10]

y_pred_prob
array([[0.92190214, 0.07809786],
       [0.85735432, 0.14264568],
       [0.8511671 , 0.1488329 ],
       [0.99167847, 0.00832153],
       [0.9649014 , 0.0350986 ],
       [0.98227531, 0.01772469],
       [0.2080994 , 0.7919006 ],
       [0.26033723, 0.73966277],
       [0.91874581, 0.08125419],
       [0.8849659 , 0.1150341 ]])
  • 각 행 별로 합은 1이다.

  • 0과 1을 특성으로 가지는 2개의 열이 존재한다.

    • 0 : 내일 비가 내리지 않을 것으로 예상되는 확률

    • 1 : 내일 비가 올 것으로 예상되는 확률

  • 예측된 확률의 중요성

    • 비가 올 확률 or 오지 않을 확률에 따라 순위를 매길 수 있다.
  • predict_proba 프로세스

    • 확률을 예측

    • 가장 높은 확률을 가진 클래스 선택

  • 분류 임계값 레벨

    • 분류 임계값의 기본값은 0.5이다.

    • 1 : 확률이 0.5를 초과하면 비가 온다고 예측

    • 0 : 확률이 0.5 미만이면 비가 내리지 않는다고 예측

y_pred_prob_df = pd.DataFrame(data=y_pred_prob, columns=['Prob of - No rain tomorrow (0)', 'Prob of - Rain tomorrow (1)'])

y_pred_prob_df
Prob of - No rain tomorrow (0) Prob of - Rain tomorrow (1)
0 0.921902 0.078098
1 0.857354 0.142646
2 0.851167 0.148833
3 0.991678 0.008322
4 0.964901 0.035099
5 0.982275 0.017725
6 0.208099 0.791901
7 0.260337 0.739663
8 0.918746 0.081254
9 0.884966 0.115034
# 비가 온다고 예측한 것에  처음 10개의 예측 확률

logreg.predict_proba(X_test)[0:10, 1]
array([0.07809786, 0.14264568, 0.1488329 , 0.00832153, 0.0350986 ,
       0.01772469, 0.7919006 , 0.73966277, 0.08125419, 0.1150341 ])
# 저장

y_pred1 = logreg.predict_proba(X_test)[:, 1]

히스토그램으로 보면 다음과 같다.

plt.rcParams['font.size'] = 12

plt.hist(y_pred1, bins = 10)

plt.title('Histogram of predicted probabilities of rain')

plt.xlim(0,1)

plt.xlabel('Predicted probabilities of rain')
plt.ylabel('Frequency')
Text(0, 0.5, 'Frequency')

  • 위의 히스토그램에서 P쪽으로 매우 치우쳐 있다.

  • 첫 번째 열은 확률이 0.0 ~ 0.1 사이의 값이 약 15000개임을 알려준다.

  • 확률이 0.5를 넘는 값도 존재한다.

  • 0.5를 넘는 값은 내일 비가 온다고 예측한다.

  • 대부분의 값은 내일 비가 오지 않는다고 예측한다.

import sklearn.preprocessing

for i in range(1,5):
    
    cm1=0
    
    y_pred1 = logreg.predict_proba(X_test)[:,1]
    
    y_pred1 = y_pred1.reshape(-1,1)
    
    y_pred2 = sklearn.preprocessing.binarize(y_pred1, threshold=i/10)
    
    y_pred2 = np.where(y_pred2 == 1, 'Yes', 'No')
    
    cm1 = confusion_matrix(y_test, y_pred2)
        
    print ('임계값이 ',i/10,' 일 때 분류 행렬 ','\n\n',cm1,'\n\n',
           
            '올바른 예측의 개수 : ',cm1[0,0]+cm1[1,1], '\n\n', 
           
            '유형 1 오류( False Positives), ', cm1[0,1],'\n\n',
           
            '유형 2 오류( False Negatives), ', cm1[1,0],'\n\n',
           
           '정확도 : ', (accuracy_score(y_test, y_pred2)), '\n\n',
           
           '민감도: ',cm1[1,1]/(float(cm1[1,1]+cm1[1,0])), '\n\n',
           
           '특이점: ',cm1[0,0]/(float(cm1[0,0]+cm1[0,1])),'\n\n',
          
            '====================================================', '\n\n')
임계값이  0.1  일 때 분류 행렬  

 [[13876  8191]
 [  678  5694]] 

 올바른 예측의 개수 :  19570 

 유형 1 오류( False Positives),  8191 

 유형 2 오류( False Negatives),  678 

 정확도 :  0.6881395267062836 

 민감도:  0.8935969868173258 

 특이점:  0.6288122535913355 

 ==================================================== 


임계값이  0.2  일 때 분류 행렬  

 [[17849  4218]
 [ 1458  4914]] 

 올바른 예측의 개수 :  22763 

 유형 1 오류( False Positives),  4218 

 유형 2 오류( False Negatives),  1458 

 정확도 :  0.8004149231688878 

 민감도:  0.7711864406779662 

 특이점:  0.8088548511351792 

 ==================================================== 


임계값이  0.3  일 때 분류 행렬  

 [[19590  2477]
 [ 2161  4211]] 

 올바른 예측의 개수 :  23801 

 유형 1 오류( False Positives),  2477 

 유형 2 오류( False Negatives),  2161 

 정확도 :  0.8369140968388481 

 민감도:  0.6608600125549278 

 특이점:  0.8877509403181221 

 ==================================================== 


임계값이  0.4  일 때 분류 행렬  

 [[20506  1561]
 [ 2752  3620]] 

 올바른 예측의 개수 :  24126 

 유형 1 오류( False Positives),  1561 

 유형 2 오류( False Negatives),  2752 

 정확도 :  0.8483420654734696 

 민감도:  0.5681104833647207 

 특이점:  0.929260887297775 

 ==================================================== 


  • 이진 문제에서는 기본적으로 0.5의 임계값이 사용되어 예측 확률을 클래스 확률로 변환한다.

  • 임계값을 조정하여 민감도 or 특이점을 높일 수 있다.

  • 민감도와 특이점은 반비례 관계이다.

  • 임계값 수준을 높이면 정확도가 높아진다.

  • 임계값 레벨을 조정하는 것은 모델 구축의 마지막 단계에서 수행해야한다.

ROC

분류 모델의 성능을 시각적으로 측정하는 다른 방법은 ROC 곡선이다.

ROC 곡선은 다양한 임계값 레벨에서 분류 모델의 성능을 보여주는 도표이고, 이는 FPR에 대한 TPR을 배치한다.

또한 단일 포인트의 TPR과 FPR에 초점을 맞춰, 다양한 임계값에 대한 일반적인 성능을 알 수 있고 이를 그래프로 표현한다.

여기서 임계값을 낮추면 더 많은 항목이 P로 분류되어 TP와 FP가 증가한다.

따라서 특정 부분에 대한 민감도와 특이점의 균형을 맞추는 임계값을 지정하는데 도움을 줄 수 있다.

from sklearn.metrics import roc_curve

fpr, tpr, thresholds = roc_curve(y_test, y_pred1, pos_label = 'Yes')

plt.figure(figsize=(6,4))

plt.plot(fpr, tpr, linewidth=2)

plt.plot([0,1], [0,1], 'k--' )

plt.rcParams['font.size'] = 12

plt.title('ROC curve for RainTomorrow classifier')

plt.xlabel('False Positive Rate (1 - Specificity)')

plt.ylabel('True Positive Rate (Sensitivity)')

plt.show()

ROC의 AUC

ROC의 AUC는 ROC 곡선 아래의 면적을 의미하는데 분류기 성능을 비교할 수 있다.

완벽한 분류기는 AUC가 1이고, 순수 무작위 분류기는 0.5이다.

from sklearn.metrics import roc_auc_score

ROC_AUC = roc_auc_score(y_test, y_pred1)

print('ROC AUC : {:.4f}'.format(ROC_AUC))
ROC AUC : 0.8730

ROC의 AUC 값이 높을수록 더 좋은 분류기이다.

현재 0.8730으로 괜찮은 성능을 내고 있다.

밑의 코드는 cross_val_score 메소드를 이용하여 계산한다.

from sklearn.model_selection import cross_val_score

Cross_validated_ROC_AUC = cross_val_score(logreg, X_train, y_train, cv=5, scoring='roc_auc').mean()

print('Cross validated ROC AUC : {:.4f}'.format(Cross_validated_ROC_AUC))
Cross validated ROC AUC : 0.8695

k-겹 교차검증

# 폴드 5개를 적용하여 교차검증

from sklearn.model_selection import cross_val_score

scores = cross_val_score(logreg, X_train, y_train, cv = 5, scoring='accuracy')

print('Cross-validation 점수 : {}'.format(scores))
Cross-validation 점수 : [0.84686387 0.84624852 0.84633642 0.84963298 0.84773626]

교차 검증의 정확도는 평균을 계산하여 정리가능하다.

print('cross-validation 평균 점수: {:.4f}'.format(scores.mean()))
cross-validation 평균 점수: 0.8474

그리드탐색으로 하이퍼파라미터 미세조정

from sklearn.model_selection import GridSearchCV


parameters = [{'penalty':['l1','l2']}, 
              {'C':[1, 10, 100, 1000]}]



grid_search = GridSearchCV(estimator = logreg,  
                           param_grid = parameters,
                           scoring = 'accuracy',
                           cv = 5,
                           verbose=0)


grid_search.fit(X_train, y_train)
GridSearchCV(cv=5,
             estimator=LogisticRegression(random_state=0, solver='liblinear'),
             param_grid=[{'penalty': ['l1', 'l2']}, {'C': [1, 10, 100, 1000]}],
             scoring='accuracy')
print(f"GridSearch CV best score : {grid_search.best_score_:.4f}\n\n")

print("Best score에 대한 Parameters : ", grid_search.best_params_)

print("\n\n탐색에 의해 선택된 Estimator : ", grid_search.best_estimator_)
GridSearch CV best score : 0.8474


Best score에 대한 Parameters :  {'penalty': 'l2'}


탐색에 의해 선택된 Estimator :  LogisticRegression(random_state=0, solver='liblinear')
print(f"테스트 셋에 대한 GridSearch CV 점수 : {grid_search.score(X_test, y_test):.4f}")
테스트 셋에 대한 GridSearch CV 점수 : 0.8488
  • 처음 모델 테스트 정확도와 그리드 탐색을 통한 정확도는 0.8488로 동일하다.

  • 특정 모델에서는 그리드 탐색을 수행할 경우 성능을 향상시킬 수 있다.

결과/결론

  • 로지스틱 회귀 모델의 정확도는 0.8488로 내일 호주에 비가 올지 예측하는데 잘 작동한다.

  • 일부 관측값은 내일 비가 올 것이라고 예측하고, 대부분의 관측값은 비가 오지 않을 것이라고 예측한다.

  • 이번 데이터에 대한 모델은 과대적합을 보이지 않는다.

  • C 값을 적당한 값으로 조정하여 테스트 셋의 정확도를 높이도록 설계할 수 있다.

  • 임계값을 높이면 정확도가 높아진다.

  • ROC 곡선의 AUC가 1에 가까우므로 내일 비가 올지 안올지 예측하는데에 잘 작동된다는 것을 알 수 있다.

  • 원래 모델의 점수는 0.8488이고 평균 교차 검증을 수행하였을 경우 0.8474로 교차 검증이 성능 향상으로 이어지지는 않는다.

  • 원래 모델 테스트 정확도는 0.8488이고, 그리드 탐색을 통한 정확도는 0.8488로 동일한 결과값을 얻었다.