-
[DAYCON][오늘의 파이썬] Lv3. 교차검증과 LGBM 모델을 활용한 와인 품질 분류하기python/오늘의 파이썬 2021. 11. 13. 15:10
이번 챕터에서는 딥러닝 모델로 와인의 품질을 예측해볼 것이다.
[EDA]
1. read_csv(), info(), shape(), head()
먼저 가장 기본적인 데이터 불러오기, 데이터 정보 관측하기, 데이터 크기 파악하기, 데이터 피쳐 알아보기를 진행한다.
- read_csv() 메서드를 사용해서 csv 파일을 Pandas DataFrame class로 불러온다.
train = pd.read_csv('data/train.csv')
- info() 매서드를 사용하여 데이터의 피쳐수와 칼럼명, 결측치 여부, Dtype에 대한 정보를 알아낸다.
train.info()
- shape 어트리뷰트를 사용해 데이터의 행갯수, 열 개수를 출력해 데이터의 크기를 파악한다.
train.shape
- head() 매서드를 통해 데이터의 대략적인 정보를 알아낸다.
train.head()
2. 결측치 유무 확인하기
EDA 과정에서는 우선적으로 결측치의 유무에 대해서 반드시 파악을 해야 한다. 결측치가 없다면 다음 과정으로 넘어가도 되지만, 결측치가 존재한다면 이에 대한 조치를 취해야 한다.
- isnull() 매서드로 결측치 존재 여부를 확인한다.
train.isnull()
test.isnull()
- sum() 매서드로 결측치의 개수를 출력한다.
train.isnull().sum()
test.isnull().sum()
3. 수치 데이터 특성 보기
describe() 메서드를 통계량을 요약할 수 있다.
describe() 메서 드을 실행 시키면 결측치는 제외하고 수치형 데이터에 한해 각 열에 대한 요약이 수행된다.
train.describe()
4. 타깃 변수 분포 시각화
가장 중요한 변수, 예측하고자 하는 변수인 종속변수(quality)의 분포를 시각화한 결과를 통해 머신러닝의 방향성을 잡을 수 있다.
- matplotlib, seaborn 라이브러리로 시각화를 출력할 수 있다.
import matplotlib import matplotlib.pyplot as plt import seaborn as sns
- 시각화를 진행할 때는 보통 copy() 매서드로 복사본을 생성한 후 진행한다.
traindata = train.copy()
- seaborn의 distplot() 메서드를 사용해서 타깃 변수(와인 품질) 분포를 시각화한다.
sns.distplot(traindata['quality'], kde=False, bins=10) plt.axis([0, 10, 0, 2500]) # [x 축 최솟값, x 축 최댓값, y 축 최솟값, y 축 최댓값] plt.title("와인 품질") # 그래프 제목 지정 plt.show() # 그래프 그리기
5. Matplotlib 선 그래프 그리기
Matplotlib 은 데이터 시각화 라이브러리다. 라이브러리를 불러오고 plot() 매서드를 활용해서 선 그래프를 그려보는 연습을 해보자.
- 라이브러리를 불러온다.
from matplotlib import pyplot as plt
- x축괴 y축 지점의 값들로 정할 리스트를 생성한다.
x_values = [0, 1, 2, 3, 4] y_values = [0, 1, 4, 9, 16]
- plot() 매서드를 사용해서 그래프를 그린다.
plt.plot(x_values, y_values)
- 그래프를 화면에 보여준다.
plt.show()
6. Matplotlib 히스토그램 그리기
히스토그램은 도수분포표를 그래프로 나타낸 것이다. 변수들의 분포 정도를 볼 때 유용하다.
- 변수 분포를 갖는 리스트를 생성한다.
a = [1,2,3,3,3,4,4,4,4,5,5,5,5,5,6,6,7]
- hist() 메서드를 활용해 그래프를 그린다.
plt.hist(a)
- 그래프를 화면에 보여준다.
plt.show()
[전처리]
1. 이상치 탐지
데이터 중에는 이상치(Outlier)가 존재한다.
이런 데이터는 일반적인 데이터 패턴과 매우 다른 패턴을 갖는 데이터가 된다.
이러한 이상치 데이터는 모델의 성능을 크게 떨어트리게 되기 때문에 이러한 이상치를 탐지하여야 한다.
이상치를 탐지하는 대표적인 방법은 IQR (Inter Qunatile Range) 로, 사분위 값의 편차를 이용한다. 이를 boxplot 그래프로 볼 수 있다.
- seaborn 라이브러리를 불러온다.
import seaborn as sns
- boxplot() 매서드로 'fixed acidity' 피쳐의 이상치를 확인한다.
sns.boxplot(data=train['fixed acidity'])
2. 이상치 제거
IQR을 통해서 이상치를 제거할 수 있다. IQR이란, 3분 위수(75%에 위치한 값) - 1분위수(25%에 위치한 값)를 의미한다.
- 10~8 사이의 실선 : 3분위수 + 1.5 * IQR
- 보라색 박스의 위쪽 실선 : 3분위수 (75%)
- 보라색 박스의 중앙 실선 : 2분 위수 (중앙값)
- 보라색 박스의 아래 실선 : 1분 위수 (25%)
- 4~6 사이의 실선 : 1분위수 - 1.5 * IQR
- 보라색 박스의 길이 : 3분 위수 - 1분 위수 = IQR
우리는 이 boxplot의 4와 6 사이의 실선보다 작고, 8과 10 사이의 실선보다 큰 데이터 포인트들을 이상치로 판단하고 제거할 것이다.
- 먼저 train에는 현재 몇 개의 행이 있는지 확인한다.
train.shape
- 각 범위의 값들을 변수로 만들어 준다.
quantile_25 = np.quantile(train['fixed acidity'], 0.25) #25%에 위치한 값 quantile_75 = np.quantile(train['fixed acidity'],0.75) #75%에 위치한 값 IQR = quantile_75 - quantile_25 minimum = quantile_25 - 1.5 * IQR maximum = quantile_75 + 1.5 * IQR
- 'fixed acidity'이 minimum보다 크고 maximum보다 작은 값들만 'train2'에 저장한다.
train2 = train[(minimum <= train['fixed acidity']) & (train['fixed acidity'] <= maximum)]
- 총 몇개의 행이 되었는지 확인한다.
train2.shape
- 몇 개의 이상치가 있었는지 계산해본다.
train.shape[0] - train2.shape[0]
3. 수치형 데이터 정규화
의사결정 나무나, 랜덤 포레스트 같은 “트리 기반의 모델”들은 대소 비교를 통해서 구분하기 때문에, 숫자의 단위에 크게 영향을 받지 않는다.
하지만 Logistic Regression, Lasso 등과 같은 “평활 함수 모델”들은 숫자의 크기와 단위에 영향을 많이 받는다.
따라서 수치형 데이터 정규화를 통해 모든 모델에 잘 어울리는 데이터를 만들어줄 것이다.
다양한 수치형 데이터 정규화 방법 중에서, 비교적 간단한 "Min Max Scailing"기법은
가장 작은 값은 0으로, 가장 큰 값은 1로 만들어주는 방법이다.
그리고 그 사이의 값들은 비율에 따라서 0~1 사이에 분포하게 된다.
이 기법은 상대적으로 굉장히 큰 값이나, 작은 값을 1이나 0으로 만들기 때문에 문제가 발생할 수도 있다.
즉 이상치에 민감하다.
- describe()를 통해 데이터의 분포가 어떻게 생겼는지 짐작해본다.
train.describe()
- seaborn의 displot을 통해 "fixed acidity"의 distplot을 그려본다.
sns.distplot(train['fixed acidity'])
- MinMaxScaler를 'scaler'라는 변수에 지정한다.
scaler = MinMaxScaler()
- 'scaler'를 학습시킨다.
scaler.fit(train[['fixed acidity']])
- 'scaler'를 통해 train의 "fixed acidity"를 바꾸어 "Scaled fixed acidity"라는 column에 저장한다.
train['Scaled fixed acidity'] = scaler.transform(train[['fixed acidity']])
- seaborn의 displot을 통해 "Scaled fixed acidity"의 distplot을 그려본다.
sns.distplot(train['Scaled fixed acidity'])
4. 원-핫 인코딩
컴퓨터는 “문자”로 된 데이터를 학습할 수 없다. 그래서 “type”같은 피쳐들은 컴퓨터가 읽어서 학습할 수 있도록 “인코딩”을 해주어야 한다.
인코딩의 방법 중 하나인 One-Hot Encoding은 말 그대로, ‘하나만 Hot 하고, 나머지는 Cold한 데이터”라는 의미다.
즉, 자신에게 맞는 것은 1로, 나머지는 0으로 바꾸어 준다.
자신에 해당 되는 값은 1인 "Hot"한 값을 주고, 나머지는 0인 "Cold"한 값을 준다. - 'OneHotEncoder'를 'encoder'라는 변수에 저장한다.
encoder = OneHotEncoder()
- 'encoder'를 사용해 train의 'type' 피쳐를 학습시킨다.
encoder.fit(train[['type']])
- 'encoder'를 사용해 train의 'type' 피쳐를 변환해 'onehot'이라는 변수에 저장한다.
onehot = encoder.transform(train[['type']]) onehot
- 'onehot'이라는 변수에 array 형태로 변환한다.
onehot = onehot.toarray() onehot
- 'onehot'이라는 변수를 DataFrame 형태롤 변환해 본다.
onehot = pd.DataFrame(onehot) onehot.head()
- encoder의 "get_feature_names()"를 사용해 column 이름을 바꾼다.
onehot.columns = encoder.get_feature_names() onehot.head()
- onehot을 원본데이터인 train에 병합시킨다.
onehot = pd.concat([train, onehot], axis = 1) onehot.head()
- train의 'type' 변수를 제거한다.
train = train.drop(columns = ['type']) train.head()
[모델링]
1. 모델 정의
지금까지의 내용을 통해 모델에 데이터를 넣을 준비를 마쳤다. 이제 모델링을 통해서 실제로 모델을 학습하고 성능을 높여보자.
Lv3에서 사용할 모델은 지난 Lv2 모델링에서 다루었던 “RandomForest” 모형이다.
이번에는 '회귀 모형'이 아닌 '분류 모형'을 사용해볼 것이다.
회귀 모형은 집값, 주가, 시가 등등 특정한 값을 맞추는 모형이라면, 분류 모형은 어떤 그룹에 속할지를 예측하는 모형이다.
우리가 다루고 있는 “와인 품질 분류”는 말 그대로, 와인의 품질이 어느 정도 일지를 예측하는 문제이기 때문에, “분류 모델”을 사용해서 예측해볼 것이다.
Random Forest 모형은 위에서 불러왔던 것과 같이, sklearn.ensemble 안에 있다.
- 랜덤포레스트 분류 모형을 불러온다.
from sklearn.ensemble import RandomForestClassifier
- 랜덤포레스트 분류 모형을 "random_forest"라는 변수에 저장한다.
random_forest = RandomForestClassifier() random_forest
2. 모델 실습
전 단계에서 불러왔던 "RandomForestClassifier()"모델을 이용하여 모델 실습을 진행해보자.
- 랜덤포레스트 분류 모형을 "random_classifier"라는 변수에 저장한다.
random_classifier = RandomForestClassifier()
- 'X'라는 변수에 train의 'quality'피쳐를 제거하고 저장한다.
X = train.drop(columns = ['quality'])
- 'y'라는 변수에 정답인 train의 'quality'피쳐를 저장한다.
y = train['quality']
- "random_classifier"를 X와 y를 이용해 학습시킨다.
random_classifier.fit(X,y)
3. 교차 검증 정의
Hold-out
Hold-out은 단순하게 Train 데이터를 (train, valid)라는 이름의 2개의 데이터로 나누는 작업이다.
예측 성능을 가늠해보기 위해, 데이터를 이렇게 나눈다.
보통 train : valid = 8:2 혹은 7:3의 비율로 데이터를 나눈다.
Train이 train.csv를 통해서 불러온 데이터라면, train은 Train의 거대한 데이터를 8:2로 쪼갠 큰 부분이다.
test(=valid)는 Train의 거대한 데이터를 8:2로 쪼갠 작은 부분이다.
모델이 80%의 데이터를 통해서 학습하고, 20%의 데이터를 예측한다면,
어느 정도의 성능이 나올지 가늠할 수 있을 것이다.
다만 Hold-out의 문제점은 데이터의 낭비다.
데이터 사이언스에 있어서, 데이터는 소중한 자원이다.
하지만 단순하게 trian과 test로 분할하게 된다면, 20%의 데이터는 모델이 학습할 기회도 없이, 예측만 하고 버려지게 된다.
그래서 "모든 데이터를 학습하게 해 보자"라는 생각에서 나온 것이 "교차검증", 즉 K-Fold이다.
교차검증
"모든 데이터를 최소한 한 번씩 다 학습하게 하자"는 것이 K-Fold의 아이디어다.
그래서 valid 데이터를 겹치지 않게 나누어 N개의 데이터셋을 만들어 낸다.
만약 데이터셋을 5개로 만든다고 하면, (== valid size가 20%) 겹치지 않게 위와 같은 모양으로 만들 수 있다.
그리고 반복문을 통해서 1번부터 5번 데이터들에 들어갔다가 나오면서, 데이터를 모두 최소한 한 번씩은 학습한다.
- sklearn에 model_selection 부분 속 KFold를 불러온다.
from sklearn.model_selection import KFold
- kFlod에 n_splits = 5, shuffle = True, random_state = 0이라는 인자를 추가해 "kf"라는 변수에 저장한다.
kf = KFold(n_splits = 5, shuffle = True, random_state = 0)
- 반복문을 통해서 1번부터 5번까지의 데이터에 접근한다.
for train_idx, valid_idx in kf.split(train) : train_data = train.iloc[train_idx] valid_data = train.iloc[valid_idx]
4. 교차 검증 실습
실습의 내용을 위의 이미지와 함께 정리하면 다음과 같다.
- K-Fold를 이용해서 Train과 Valid Data를 나눈다.
- Model을 이용해서 train 데이터를 학습한다.
- Model을 이용해서 valid 데이터를 예측해 성능을 확인한다.
- Model을 이용해서 test 데이터를 예측한다.
- n_splits를 5로 설정한다면, 5개의 결괏값들에 대한 “최빈값”을 이용해 가장 등장할 가능성이 높은 결괏값으로 결정한다.
- 결과를 제출한다.
- "X"라는 변수에 train의 "index"와 "quality"를 제외하고 지정한다. "y"라는 변수에는 "quality"를 지정한다.
X = train.drop(columns = ['index','quality']) y = train['quality']
- "kf"라는 변수에 KFold를 지정해 준다. n_splits는 5, shuffle은 True, random_state는 0으로 설정해준다.
kf = KFold(n_splits = 5, shuffle = True, random_state = 0)
- "model"# "model"이라는 변수에 RandomForestClassifier를 지정해 준다.
valid_scores와 test_predictions라는 빈 리스트를 하나씩 만들어준다.
model = RandomForestClassifier(random_state = 0) valid_scores = [] test_predictions = []
- 지난 시간에 다루었던 kf.split()을 활용해, 반복문으로 X_tr, y_tr, X_val, y_val을 설정한다.
for train_idx, valid_idx in kf.split(X,y) : X_tr = X.iloc[train_idx] y_tr = y.iloc[train_idx] X_val = X.iloc[valid_idx] y_val = y.iloc[valid_idx]
- 앞에 문제이 이어서 반복문 속에서 model.fit(X_tr, y_tr)을 활용해 모델을 학습한다.
for train_idx, valid_idx in kf.split(X,y) : X_tr = X.iloc[train_idx] y_tr = y.iloc[train_idx] X_val = X.iloc[valid_idx] y_val = y.iloc[valid_idx] model.fit(X_tr, y_tr)
- 앞에 문제에 이어서 반복문 속에서 "valid_prediction"이라는 변수에 model.predict(X_val)의 결과를 저장한다.
for train_idx, valid_idx in kf.split(X,y) : X_tr = X.iloc[train_idx] y_tr = y.iloc[train_idx] X_val = X.iloc[valid_idx] y_val = y.iloc[valid_idx] model.fit(X_tr, y_tr) valid_prediction = model.predict(X_val)
- 앞에 문제에 이어서 반복문 속에서 accuracy_score를 이용해, 모델이 어느 정도의 예측 성능이 나올지 확인해보자. 그리고 "valid_prediction"의 점수를 scores에 저장한다. 반복문에서 빠져나온 후에 np.mean()을 활용해 평균 점수를 예측해본다.
for train_idx, valid_idx in kf.split(X,y) : X_tr = X.iloc[train_idx] y_tr = y.iloc[train_idx] X_val = X.iloc[valid_idx] y_val = y.iloc[valid_idx] model.fit(X_tr, y_tr) valid_prediction = model.predict(X_val) score = accuracy_score(y_val, valid_prediction) valid_scores.append(score) print(score) print('평균 점수 : ', np.mean(valid_scores))
- 반복문 속에서 test를 예측해 "test_prediction"이라는 변수에 지정한다. test_prediction을 지정했다면, "test_precitions"라는 빈 리스트에 넣어준다.
for train_idx, valid_idx in kf.split(X,y) : X_tr = X.iloc[train_idx] y_tr = y.iloc[train_idx] X_val = X.iloc[valid_idx] y_val = y.iloc[valid_idx] model.fit(X_tr, y_tr) test_prediction = model.predict(test.drop(columns = ['index'])) test_predictions.append(test_prediction)
- 이제 결과 값을 만들어 보자. "test_precitions"를 Data Frame으로 만들어준다.
test_predictions = pd.DataFrame(test_predictions) test_predictions
- DF.mode()를 활용해 열 별 최빈값을 확인하고, "test_prediction"이라는 변수에 지정 한다. "test_prediction"의 첫 행을 최종 결괏값으로 사용한다.
test_prediction = test_predictions.mode() test_prediction = test_predictions.values[0] test_prediction
- data의 sample_submission 파일을 불러와 "quality"라는 변수에 "test_precition"을 저장한다. 그 이후에는, "data/submission_KFOLD.csv"에 저장하고, 제출한다.
sample_submission = pd.read_csv('data/sample_submission.csv') sample_submission['quality'] = test_prediction sample_submission.to_csv('data/submission_KFOLD.csv', index=False)
[튜닝]
1. Bayesian Optimization
Bayesian Optmization은 하이퍼 파라미터 튜닝과 관련된 내용이다.
우리가 흔히 알고 있는 하이퍼 파라미터 튜닝은 Grid Search, Random Search이다.
하지만 그 2가지에는 "최적의 값을 찾아갈 수 없다"라는 공통된 문제점이 있다.
이를 해결하기 위한 방법 중 하나가 "Bayesian Optimization"이다.
Bayesian Optimization은 보통
- "Gausain Process"라는 통계학을 기반으로 만들어진 모델로,
- 여러 개의 하이퍼 파라미터들에 대해서,
- "Acquisition Fucntion"을 적용했을 때,
- "가장 큰 값"이 나올 확률이 높은 지점을 찾아낸다.
우리가 다룰 Bayesian Optimization 패키지에서는 다음과 같은 단계가 필요하다.
- 변경할 하이퍼 파라미터의 범위를 설정한다.
- Bayesian Optimization 패키지를 통해, 하이퍼 파라미터의 범위 속 값들을 랜덤 하게 가져온다.
- 처음 R번은 정말 Random 하게 좌표를 꺼내 성능을 확인한다.
- 이후 B번은 Bayesian Optimization을 통해 B번만큼 최적의 값을 찾는다.
이번 단계에서는 Bayesian Optimization을 사용하기 위한 준비를 해보자.
- bayesian-optimization을 설치한다.
pip install bayesian-optimization
- bayes_opt 패키지에서 BayesianOptimization을 불러온다.
from bayes_opt import BayesianOptimization
2. 그리드, 랜덤 서치 vs Bayesian Optimization
Hyper Parameter의 3가지 튜닝 방법을 비교해보자.
1.Grid Search
- 기법 : Grid Search는 사전에 탐색할 값들을 미리 지정해주고, 그 값들의 모든 조합을 바탕으로 성능의 최고점을 찾아낸다.
- 장점 :
- 내가 원하는 범위를 정확하게 비교 분석이 가능하다.
- 단점 :
- 시간이 오래 걸린다.
(4개의 파라미터에 대해서, 4가지 값들을 지정해두고, 한 번 탐색하는데 1분이 걸린다면 -> 4*4*1분 = 16분 소요)
- 성능의 최고점이 아닐 가능성이 높다.
- "최적화 검색" (여러 개 들을 비교 분석해서 최고를 찾아내는 기법)이지, "최적화 탐색"(성능이 가장 높은 점으로 점차 찾아가는 기법)이 아니다.
2.Random Search
- 기법 : 사전에 탐색할 값들의 범위를 지정해주고, 그 범위 속에서 가능한 조합을 바탕으로 최고점을 찾아낸다.
- 장점 :
- Grid Search에 비해 시간이 짧게 걸린다.
- Grid Search보다, 랜덤 하게 점들을 찍으니, 성능이 더 좋은 점으로 갈 가능성이 높다.
- 단점 :
- 반대로 성능이 Grid Search보다 낮을 수 있다.
- 하이퍼 파라미터의 범위가 너무 넓으면, 일반화된 결과가 나오지 않는다. (할 때마다 달라진다)
- seed를 고정하지 않으면, 할 때 마다 결과가 달라진다.
- 마찬가지로, "최적 값 검색"의 느낌이지, "최적화 탐색"의 개념이 아니다.
3.Bayeisan Optimization
- 기법 : 하이퍼 파라미터의 범위를 지정한 후, Random 하게 R 번 탐색한 후, B번만큼 최적의 값을 찾아간다.
- 장점 :
- 정말 "최적의 값"을 찾아갈 수 있다.
- 상대적으로 시간이 덜 걸린다.
- 엔지니어가 그 결괏값을 신뢰할 수 있다.
- 단점 :
- Random 하게 찍은 값이 달라질 경우, 최적화 하는데 오래 걸릴 수 있다.
- Random하게 찍은 값이 부족하면, 최적의 값을 탐색하는 게 불가능할 수 있다.
- Rnadom 하게 찍은 값이 너무 많으면, 최적화 이전에 이미 최적 값을 가지고 있을 수도 있다.
그럼에도, Bayesian Optimization은 수동적으로 하이퍼 파라미터를 튜닝하는데 좋은 결과를 가져온다.
3. Bayesian Optimization 실습
- X에 학습할 데이터를 y에 목표 변수를 저장한다.
X = train.drop(columns = ['index', 'quality']) y = train['quality']
- 랜덤 포레스트의 하이퍼 파라미터의 범위를 dictionary 형태로 지정해준다. 이때 Key는 랜덤 포레스트의 hyperparameter이름이고, value는 탐색할 범위다.
rf_parameter_bounds = { 'max_depth' : (1,3), # 나무의 깊이 'n_estimators' : (30,100), }
- 함수를 만든다.
- 함수에 들어가는 인자 = 위에서 만든 함수의 key값들
- 함수 속 인자를 통해 받아와 새롭게 하이퍼 파라미터 딕셔너리 생성
- 그 딕셔너리를 바탕으로 모델 생성
- train_test_split을 통해 데이터 train-valid 나누기
- 모델 학습
- 모델 성능 측정
- 모델의 점수 변환
def rf_bo(max_depth, n_estimators): rf_params = { 'max_depth' : int(round(max_depth)), 'n_estimators' : int(round(n_estimators)), } rf = RandomForestClassifier(**rf_params) X_train, X_valid, y_train, y_valid = train_test_split(X,y,test_size = 0.2, ) rf.fit(X_train,y_train) score = accuracy_score(y_valid, rf.predict(X_valid)) return score
- 이제 Bayesian Optimization을 사용할 준비가 끝났다. "BO_rf"라는 변수에 Bayesian Optmization을 저장한다.
BO_rf = BayesianOptimization(f = rf_bo, pbounds = rf_parameter_bounds,random_state = 0)
- Bayesian Optmization을 실행한다.
BO_rf.maximize(init_points = 5, n_iter = 5)
- 하이퍼 파라미터의 결괏값을 불러와 "max_params"라는 변수에 저장한다.
max_params = BO_rf.max['params'] max_params['max_depth'] = int(max_params['max_depth']) max_params['n_estimators'] = int(max_params['n_estimators']) print(max_params)
- Bayesian Optmization의 결과를 "BO_tuend_rf"라는 변수에 저장한다.
BO_tuend_rf = RandomForestClassifier(**max_params)
'python > 오늘의 파이썬' 카테고리의 다른 글
[DAYCON] 타이타닉 생존자 예측 (0) 2021.11.24 [DAYCON][오늘의 파이썬] Lv4 교차검증과 모델 앙상블을 활용한 와인 품질 분류하기 (0) 2021.11.22 [DAYCON][오늘의 파이썬] Lv.2 결측치 보간법과 랜덤포레스트로 따릉이 데이터 예측하기 (0) 2021.11.04 [DACON 오늘의 파이썬][Lv1. 의사결정회귀나무로 따릉이 데이터 예측하기] (0) 2021.10.28