Logistic Regression Classifier - weatherAUC
로지스틱 회귀 소개
로지스틱 회귀(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 함수이다.

결정 경계
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으로 매핑된다.
이를 그래프로 표현하면 다음과 같다.

예측 함수
로지스틱 회귀에서 sigmoid 함수와 임계값을 이용하여 예측 함수를 만들 수 있다.
예측 함수는 양성(yes 또는 True)관측치의 확률값을 반환하는데 이를 class 1이라 하며 P(class = 1)로 표현한다.
만약 확률이 1로 가까워진다면 이는 관측치가 class 1에 속할 가능성이 높음을 의미하고, 반대의 경우 관측치가 class 0에 속할 가능성이 높음을 의미한다.
로지스틱 회귀의 가정
로지스틱 회귀 모델은 몇개의 가정을 요구하는데 다음과 같다.
-
종속 변수가 이항, 다항 또는 순서형일 경우 사용할 수 있다.
-
관측치가 각각 독립적이어야한다. 이는 반복적인 측정에서 나온 관측치는 사용할 수 없음을 의미한다.
-
독립 변수 사이에 다중선형성이 적거나 없어야한다. 이는 독립 변수들이 서로 연관성이 적어야 함을 의미한다.
-
독립 변수와 로그 확률의 선형성을 가정한다.
-
샘플의 크기가 클수록 높은 정확성을 얻을 수 있다.
로지스틱 회귀의 유형
로지스틱 회귀 모델은 목표 변수에 따라 세 가지의 유형으로 분류된다.
- 이항 로지스틱 회귀
이항 로지스틱 회귀에서 목표 변수는 두개의 특성값을 갖는다.
예시로 yes/no, good/bad, true/false, spam/no spam, pass/fail 등이 있다.
- 다항 로지스틱 회귀
다항 로지스틱 회귀에서 목표 변수는 특정한 순서가 없는 세개 이상의 특성(명목적인 특성)를 갖는다.
예시로 과일 - 사과/망고/오렌지/바나나 가 있다.
- 순서형 로지스틱 회귀
순서형 로지스틱 회귀에서 목표 변수는 순서가 있는 세개 이상의 특성을 갖는다.
예시로 성적 - 나쁨/보통/좋음/우수 가 있다.
라이브러리
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_0 과 RainToday_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가지 유형의 결과가 나올 수 있는데, 다음과 같다.
- TP
관찰이 특정 클래스에 속할 것으로 예측하고 실제로 해당 클래스에 속할 때 발생
- TN
관찰이 특정 글래스에 속하지 않는다고 예측하고 실제로 해당 클래스에 속하지 않을 때 발생
- FP
관찰이 특정 클래스에 속할 것으로 예측하였으나 실제로 해당 클래스에 속하지 않을 때 발생
- 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로 동일한 결과값을 얻었다.