학습 내용
- 데이터 분석 및 모델링의 전 과정을 알 수 있다
- 시각화 기법들을 알 수 있다
자세한 코드보기 :
https://github.com/seongyeon1/aiffel/blob/main/Exploration/Quest2/project2_sykim.ipynb
https://github.com/seongyeon1/aiffel/blob/main/Exploration/Quest2/Bike_sharing.ipynb
- 첫 번째 링크는 두 번째 링크 이후에 작성한 코드입니다.
Bike sharing demand
대회 관련 링크
데이터 및 문제 설명
- 시계열과 수치형, 범주형으로 구성된 다양한 피처들로 자전거 대여량을 예측하는 회귀문제이다.
데이터 필드 구성
- datetime : 기록 일시(1시간 간격)
- season : 계절(1봄, 2여름, 3가을, 4겨울)
- holiday : 공휴일 여부(0공휴일 아님, 1공휴일)
- workingday : 근무일 여부(0근무일 아님, 1근무일_주말과 공휴일이 아니면 근무일로 간주)
- weather : 날씨(1맑음, 2옅은 안개 및 약간 흐림, 3약간의 눈이나 비 천둥번개와 흐림, 4폭우와 천둥번개 및 눈 짙은 안개)
- temp : 실제 온도
- atemp : 체감 온도
- humidity : 상대 습도
- windspeed : 풍속
train 데이터에만 존재하는 feature
- casual : 등록되지 않은 사용자(비회원) 대여 수량
- registered : 등록된 사용자(회원) 대여 수량
예측해야 할 타깃값
- count : 자전거 대여 수량 (회원 + 비회원)
데이터 분석 방향성
1. 시계열 데이터 (datetime) : 시계열 데이터 분석을 통해 시간적 특성에 따른 대여량의 변화 및 차이를 확인한다.
- datetime 으로 타입을 변환한 뒤 파생변수를 생성한다 (day, month, dayofweek 등)
2. 날씨 관련 연속형 데이터 (temp, atemp, humidity, windspeed)
- 이상치 파악
3. 범주형 데이터 (season, holiday, workingday, weather) : 명목형과 순서형을 구분한다
- 명목형 : holiday, workingday
- 순서형 : season, weather
4. target : train 데이터에만 있는 casual과 registered feature에 대해서 어떻게 다뤄야 할 지 고민해본다.
- 예측해야 하는 타겟값이 두 피처의 합으로 보여진다.
- 각각을 예측한 뒤에 합해서 결과를 낼지 혹은 한번에 예측해야 할 지 두 가지 예측 방향성을 생각해 볼 수 있다.
Datetime 컬럼을 datatime 형식으로 변환 후 파생변수 추출
- datetime 컬럼은
# train['datetime'] = pd.to_datetime(train['datetime'])
# train['year'] = train['datetime'].dt.year
# train['month'] = train['datetime'].dt.month
# train['day'] = train['datetime'].dt.day
# train['hour'] = train['datetime'].dt.hour
# train['minute'] = train['datetime'].dt.minute
# train['second'] = train['datetime'].dt.second
위 코드를 좀 더 간결하게 작성하는 방법을 소개하고자 한다.
- getattr()를 통해 간결하게 하였다.
# datetime 컬럼을 datetime 자료형으로 변환
train['datetime'] = pd.to_datetime(train['datetime'])
# 추출할 시간 관련 속성들
time_attrs = ['year', 'month', 'day', 'hour', 'minute', 'second']
# for 루프를 사용하여 각 속성을 추출하고 저장
for attr in time_attrs:
train[attr] = getattr(train['datetime'].dt, attr)
날씨 관련 수치형(연속형) 데이터 (temp, atemp, humidity, windspeed)
- 이상치 파악
numerical = ['temp','atemp', 'humidity', 'windspeed']
train[numerical].describe()
- humidity와 windspeed의 데이터에서 최솟값으로 0이 있는데, 이것이 실제로 가능한 수치인지 생각해 볼 필요가 있다.
- 체감온도와 실제온도는 관련성이 높은 feature로 보이기에 두 데이터 간의 차이가 너무 크다면 이 또한 이상치로 보아야할 것이다.
분포 파악
distplot을 활용한 데이터 분포 확인
# 4개의 feature에 대한 distplot 그리기
plt.figure(figsize=(12, 4))
for i, feature in enumerate(numerical):
plt.subplot(1, 4, i + 1)
sns.distplot(train[feature], kde=True)
plt.title(feature)
qq plot을 활용한 데이터 분포 확인
from scipy import stats
# 4개의 feature에 대한 qqplot 그리기
plt.figure(figsize=(12, 4))
for i, feature in enumerate(numerical):
plt.subplot(1, 4, i + 1)
stats.probplot(train[feature], dist="norm", plot=plt)
plt.title(feature)
- 풍속(windspeed)의 경우 왼쪽으로 치우쳐져 있는 것을 볼 수 있다.
- 로그 변환을 고려해볼 필요가 있다.
- 풍속(windspeed)은 0이 될 수 없으므로 사실상 이상치라 볼 수 있고 이를 다른 값으로 대체해 주는 것이 바람직 해 보인다.
수치형 데이터의 상관관계 파악
온도와 체감온도의 산점도
# 온도와 체감 온도에 대한 산점도 그리기
plt.scatter(train['temp'], train['atemp'])
plt.xlabel('Temperature')
plt.ylabel('Apparent Temperature')
plt.show()
- 온도와 체감온도가 상당히 차이가 많이 나는 데이터 확인 후 이상치 처리를 해볼 것이다.
- 유독 차이가 많이 나는 부분이 체감온도가 하나의 값인 것으로 보아 기록시 나타난 이상치로 보인다
이상치 처리 (체감온도)
- 실제온도 / 체감온도의 비율로 이상치를 파악해보았다.
- 이 비율이 2를 넘어가면 이상치로 처리해주었다
train.loc[train['temp'] / train['atemp'] > 2,'atemp_out'] = 1
train.atemp_out.fillna(0, inplace=True)
sns.scatterplot(data=train, x='temp', y='atemp', hue='atemp_out')
plt.xlabel('Temperature')
plt.ylabel('Apparent Temperature')
plt.show()
이상치 시각화
이상치 확인
- 데이터를 확인해보니 2012년 08월 17일의 체감온도가 12.12로 일정하게 잘 못 기록된 것으로 보인다.
- 랜덤 포레스트를 활용하여 이상치를 보간해 주었다.
이상치 보간 (imputation)
from sklearn.ensemble import RandomForestRegressor
# atemp_out이 1인 데이터의 인덱스를 추출
index_outlier = train[train['atemp_out'] == 1].index
# atemp_out이 0인 데이터를 학습 데이터로 사용
X_train = train[train['atemp_out'] == 0][['temp', 'humidity', 'windspeed','month','hour']]
y_train = train[train['atemp_out'] == 0]['atemp']
# 랜덤 포레스트 모델 생성
model = RandomForestRegressor()
# 모델 학습
model.fit(X_train, y_train)
# atemp_out이 1인 데이터의 온도, 습도, 풍속을 사용하여 체감온도 예측
y_pred = model.predict(train[train['atemp_out'] == 1][['temp', 'humidity', 'windspeed','month','hour']])
# 예측한 체감온도를 train 데이터에 업데이트
train.loc[index_outlier, 'atemp'] = y_pred
sns.scatterplot(data=train, x='temp', y='atemp', hue='atemp_out')
plt.xlabel('Temperature')
plt.ylabel('Apparent Temperature')
plt.show()
풍속 0인 데이터 처리
- 풍속이 0인 데이터는 1313개로 상당히 많아서 제거하기에는 너무 큰 숫자로 보인다
- 좀 더 자세한 분석을 통해 처리 방법을 고려해보자
'풍속'이 unique 값이 28개 밖에 안되는 것으로 보아 이산적으로 어떤 기준에 따라 나눠진 것 임을 예측할 수 있고, 0은 단순히 0이 아니라 6 미만 정도로 해석이 가능하다
범주형 데이터 분석
- season 데이터는 범주형 데이터이지만 날씨의 특성상 여름, 봄, 가을, 겨울 순으로 온도가 낮아지기에 이를 순서형으로도 볼 수 있다.
- 데이터 분석을 통해 단순 명목형으로 볼 것인지 혹은 순서형으로 변환해 볼지 고민해 볼 계정이다.
- weather 데이터는 1은 맑은 날씨고 4로 갈수록 자전거 타기 어려운 날씨로 보여진다.
- 따라서 순서형 데이터로 생각하고 데이터를 분석할 예정이다.
- 또한 4는 1개밖에 없으므로 학습하는 데 방해가 될 것으로 예상되어 3으로 변환한다
- weather 데이터는 1은 맑은 날씨고 4로 갈수록 자전거 타기 어려운 날씨로 보여진다.
시각화 시 의미가 잘 드러나도록 문자열로 바꾸고 진행한다.
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
# holiday
sns.countplot(x='holiday', data=train, ax=axes[0])
axes[0].set_title('Holiday')
axes[0].set_xticklabels(['No Holiday', 'Holiday'])
for p in axes[0].patches:
axes[0].annotate(format(p.get_height(), '.0f'), (p.get_x() + p.get_width() / 2., p.get_height()), ha = 'center', va = 'center', xytext = (0, 10), textcoords = 'offset points')
# workingday
sns.countplot(x='workingday', data=train, ax=axes[1])
axes[1].set_title('Working Day')
axes[1].set_xticklabels(['No Workingday', 'Workingday'])
for p in axes[1].patches:
axes[1].annotate(format(p.get_height(), '.0f'), (p.get_x() + p.get_width() / 2., p.get_height()), ha = 'center', va = 'center', xytext = (0, 10), textcoords = 'offset points')
# weather
sns.countplot(x='weather', data=train, ax=axes[2])
axes[2].set_title('Weather')
axes[2].set_xticklabels(['Clear', 'Mist, Few clouds', 'Light Snow, Rain, Thunderstorm', 'Heavy Rain, Thunderstorm, Snow, Fog'])
axes[2].set_xticklabels(['Clear', 'Mist, Few clouds', 'Light Snow, Rain, Thunderstorm', 'Heavy Rain, Thunderstorm, Snow, Fog'], rotation=15)
for p in axes[2].patches:
axes[2].annotate(format(p.get_height(), '.0f'), (p.get_x() + p.get_width() / 2., p.get_height()), ha = 'center', va = 'center', xytext = (0, 10), textcoords = 'offset points')
plt.tight_layout()
plt.show()
타겟 값 분포 확인
stats.probplot(train['count'], dist="norm", plot=plt)
- 분포가 왼쪽으로 많이 편향되어있는 것을 볼 수 있음.
- 회귀 모델이 좋은 성능을 내려면 데이터가 정규분포를 따라야 함
- 현재 타깃값인 count는 정규분포를 따르지 않아 현재 타깃값을 그대로 사용해 모델링하면 좋은 성능 기대 어려움
- 이에 따라 데이터 분포를 정규분포에 가깝게 만들기 위해 로그변환을 하고자 한다
범주형 데이터 분석
타겟 값과 범주형 변수 간의 관계
fig, axes = plt.subplots(nrows=2,ncols=2)
fig.set_size_inches(12, 10)
sns.boxplot(data=train, y="count", orient="v",ax=axes[0][0])
sns.boxplot(data=train, y="count", x="season",orient="v",ax=axes[0][1])
sns.boxplot(data=train, y="count", x="hour",orient="v",ax=axes[1][0])
sns.boxplot(data=train, y="count", x="workingday",orient="v",ax=axes[1][1])
axes[0][0].set(ylabel='Count',title="Box Plot On Count")
axes[0][1].set(xlabel='Season', ylabel='Count',title="Box Plot On Count Across Season")
axes[1][0].set(xlabel='Hour Of The Day', ylabel='Count',title="Box Plot On Count Across Hour Of The Day")
axes[1][1].set(xlabel='Working Day', ylabel='Count',title="Box Plot On Count Across Working Day")
- 계절(Season) : 대여량의 중앙값을 비교해보면, 계절의 따른 대여량 차이에는 봄이 다른 계절에 비해 대여량이 낮은 것으로 보인다.
- 시간대(Hour of the Day) : 대여량의 중앙값을 비교해보면, 아침 7-9시, 저녁 5-6시 시간대에 대여량이 다른 시간대에 비해 상당히 많음을 볼 수 있다. 이는 출퇴근 시간대에 자전거를 이용하여 대여량이 많은 것으로 유추할 수 있다.
- 이에 대한 증거로, 대부분의 이상치 값(대여량이 높은 데이터)들은 평일에 분포되어 있음을 볼 수 있다.
Holiday, Working Day 피처 분석
# Contingency Table 생성
contingency_table = pd.crosstab(train['holiday'], train['workingday'])
print("Contingency Table:")
# 시각화
import seaborn as sns
import matplotlib.pyplot as plt
plt.figure(figsize=(8, 6))
sns.heatmap(contingency_table, annot=True, cmap="YlGnBu", fmt='g')
plt.xlabel('Working Day')
plt.ylabel('Holiday')
plt.title('Contingency Table of Holiday vs Working Day')
plt.show()
피처 엔지니어링
# 0 : 휴일아닌 평일
# 1 : 휴일아닌 주말
# -1 : 휴일인 주말
train['day_off'] = train.holiday * (-1) + train.workingday + 1
날씨 변수 변경
train['weather'] = train['weather'].replace({4: 3})
원 핫 인코딩
# One-hot encoding for categorical variables
train = pd.get_dummies(train, columns=['season', 'day_off','year', 'month', 'day','hour']) # weather은 1,2,3이 순서가 있다고 생각해서 그대로사용
체감온도, 실제 온도 데이터는 상관관계 높으므로 PCA
from sklearn.decomposition import PCA
pca = PCA(n_components=1)
train['temperature'] = pca.fit_transform(train[['temp', 'atemp']])
train.drop(['temp','atemp'], axis=1, inplace=True)
테스트 데이터 셋과 학습데이터 셋으로 분리
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(train.drop('count', axis=1), train['count'], test_size=0.2, random_state=123)
print(f"학습 데이터: {X_train.shape} 레이블: {y_train.shape}")
print(f"테스트 데이터: {X_test.shape} 레이블: {y_test.shape}")
# scikit-learn 패키지의 MinMaxScaler 클래스를 불러옵니다.
from sklearn.preprocessing import MinMaxScaler
# MinMaxScaler 객체를 생성합니다.
minmax_scaler = MinMaxScaler()
# fit_transform()을 사용해서 학습과 스케일링을 한 번에 적용합니다.
train_minmax = minmax_scaler.fit_transform(X_train)
# Min-Max 스케일링이 완료된 데이터를 데이터프레임 형태로 변환합니다.
train_minmax = pd.DataFrame(train_minmax,
index=X_train.index,
columns=X_train.columns)
로그 변환
y_train = np.log1p(y_train)
모델링
- 모델링은 간단하게 선형 회귀 모델로만 진행하였다
from sklearn.linear_model import LinearRegression
# scikit-learn에서 제공하는 선형 회귀 모델
lr_model = LinearRegression()
# model을 x와 y 데이터셋을 이용하여 학습시킴
lr_model.fit(train_minmax, y_train)
학습된 모델로 X_test에 대한 예측값 출력 및 손실함수값 계산
test_minmax = minmax_scaler.fit_transform(X_test)
# model이 test 데이터에 대해 예측한 값을 array로 반환
y_pred = lr_model.predict(test_minmax)
y_pred = np.expm1(y_pred)
# 모델이 예측한 값과 정답 target 같의 손실함수 값 계산하기
mse = ((y_pred - y_test) ** 2).mean()
rmse = np.sqrt(mse)
print(f'mse: {mse}')
print(f'rmse: {rmse}')
mse: 9283.943270974496
rmse: 96.35322138348306
결론
- 배운 점 : 자주 하던 데이터셋이라서 새로운 것이 없을 줄 알았는데, windspeed 관련해서 새로운 인사이트를 얻은 것 같아서 기분이 좋다
- 아쉬운 점 : windspeed가 categorical 하다는 걸 알게 되었고, 이에 대한 고민을 더 해보지 못해서 아쉬웠다
- 느낀 점 : modeling을 직접 하드코딩으로 하는 연습을 해야 겠다
- 어려웠던 점 : windspeed 전처리에 대한 해결방안을 찾지못했다
참고자료
'AIFFLE > PROJECT' 카테고리의 다른 글
[CV] 인물사진 만들기 - 기본편 (OpenCV, semantic segmentation) (0) | 2024.06.08 |
---|---|
[CV] 고양이 수염 스티커 합성하기 (OpenCV 완전 정복기) (0) | 2024.06.08 |
[sklearn] 선형회귀와 경사하강법 직접 구현하기 (diabetes dataset) (0) | 2024.05.28 |
[kaggle] 포켓몬 분류하기 프로젝트 (Tree 모델들의 Feature importance를 확인해보자) (2) | 2024.05.28 |