나의 첫 해커톤 참가이다.
수상이 목표가 아니라 그동안 배웠던 것을 실제로 잘 쓸 수 있는지 스스로 테스트해보기 위함이고, 목표는 100등 안에 드는 것이다.
💡프로젝트 개요
- 일정: 2023. 12. 16. ~ 24. 1. 7.
- 주제: 서울시의 평균기온을 예측하는 AI 알고리즘 개발
💻 기술스택: Python, Tensorflow, Keras, matplotlib, pandas, numpy, seaborn, scikit learn, ML/DL
🗃️ 사용 알고리즘
- LSTM: 기존의 RNN에서 출력과 멀리 있는 정보를 기억할 수 없다는 단점을 보완하여 장/단기 기억을 가능하게 설계한 신경망의 구조이다. 주로 시계열 처리나, 자연어 처리에 사용
- Prophet: Meta(舊Facebook)에서 개발한 시계열 예측 모델이다. 간단하면서도 강력한 모델로, 일상적인 시계열 데이터에 대한 예측을 수행하는 데 사용된다. 이 모델은 주로 계절성, 휴일 효과, 이상치 등을 처리하는데 특화
📍프로젝트 순서
데이터수집 ▶️ 결측치 처리 및 컬럼 수정 ▶️ EDA ▶️ Feature Engineering ▶️ 모델학습 ▶️평가 ▶️예측
데이콘 제38회 해커톤 '서울시 평균기온 예측' 데이터 활용
https://dacon.io/competitions/official/236200/data
data = pd.read_csv('/content/drive/MyDrive/data/train.csv')
data.describe()
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
import seaborn as sns
from google.colab import drive
import datetime
import sys
import matplotlib
import warnings
warnings.filterwarnings(action='ignore')
import tensorflow as tf
# 한글 폰트
if 'google.colab' in sys.modules:
!sudo apt-get -qq -y install fonts-nanum
import matplotlib.font_manager as fm
font_files = fm.findSystemFonts(fontpaths=['/usr/share/fonts/truetype/nanum'])
for fpath in font_files:
fm.fontManager.addfont(fpath)
matplotlib.rc('font', family='NanumBarunGothic')
matplotlib.rcParams['axes.unicode_minus'] = False
from statsmodels.datasets.longley import load_pandas
import statsmodels.api as sm
from statsmodels.stats.outliers_influence import variance_inflation_factor
plt.rc('font', family='NanumBarunGothic') ## 한글 폰트
# 날짜 데이터 변환
data['일시'] = pd.to_datetime(data['일시'])
data = data.set_index('일시')
# 데이터의 시간 간격 지정
data.index.freq = 'D'
# 일시 컬럼이 인덱스로 할당
data.info()
# 컬럼별 결측치 확인
data.columns
for i in data.columns:
print(data[i].isnull().value_counts())
print()
print(data[data[i].isna()])
# 결측치 처리
# 최고기온 : 2월, 10월이 결측치가 있으니 각 년도 2월, 10월 각 평균으로 입력
data.loc['1967-02-19', '최고기온'] = data.loc['1967-02-01':'1967-02-28', '최고기온'].mean()
data.loc['1973-10-16', '최고기온'] = data.loc['1973-10-01':'1973-10-31', '최고기온'].mean()
data.loc['2017-10-12', '최고기온'] = data.loc['2017-10-01':'2017-10-31', '최고기온'].mean()
# 최저기온 : 각 년도 2, 8, 10월 평균으로 결측치 처리
data.loc['1967-02-19', '최저기온'] = data.loc['1967-02-01':'1967-02-28', '최저기온'].mean()
data.loc['1973-10-16', '최저기온'] = data.loc['1973-10-01':'1973-10-31', '최저기온'].mean()
data.loc['2022-08-08', '최저기온'] = data.loc['2022-08-01':'2022-08-31', '최저기온'].mean()
data['최저기온'].isnull().sum()
data.drop('일교차', axis = 1, inplace = True)
data.isnull().sum()
# 강수량 : 결측치 : 13861개
data[data['강수량'].isna()].index
data['강수량'].fillna(0.0, inplace = True)
data['강수량'].isnull().sum()
data.loc['1983-07-16', '평균풍속'] = data.loc['1983-07-01':'1983-07-30', '평균풍속'].mean()
data.loc['2017-10-14', '평균풍속'] = data.loc['2017-10-01':'2017-10-31', '평균풍속'].mean()
data.loc['2017-12-05':'2017-12-07', '평균풍속'] = data.loc['2017-12-01':'2017-12-31', '평균풍속'].mean()
data['평균풍속'].isnull().sum()
# 일조합 결측 : 118개, 일사합 결측 : 4862개
# 선형보간 사용(interpolate) -> method = 'linear'
cols = ['일조합', '일사합', '일조율']
for col in cols:
data[col].interpolate(method = 'linear', inplace = True)
# 일사합 및 일조율이 1972년까지 결측치 처리가 안 됨 -> 1972년까지의 행을 drop
data = data.loc['1973-01-01':,:]
for i in data.index:
if i.month in [3, 4, 5]:
data.loc[i, '계절'] = "봄"
elif i.month in [6, 7, 8]:
data.loc[i, '계절'] = '여름'
elif i.month in [9, 10, 11]:
data.loc[i, '계절'] = '가을'
else:
data.loc[i, '계절'] = '겨울'
plt.figure(figsize = (1,1))
plot_cols = ['최고기온', '최저기온', '강수량', '평균기온']
plot_features = data[plot_cols]
plot_features.index = data.index
_ = plot_features.plot(subplots = True)
# 2000년 월별 추이(1972~2022년 중 중간 년도 선택)
plot_features = data[plot_cols][9861:10227]
plot_features.index = data.index[9861:10227]
_ = plot_features.plot(subplots=True)
[최고/최저기온, 강수량, 평균기온 추이]
[2000년 월별 추이(1972~2022년 중 중간 년도 선택)]
☞ 평균기온별 각 컬럼을 분석 결과
→ 평균기온 -10도에서 모든 컬럼들의 편차가 크게 나타났다. 이상치처럼 보일 수 있으나, 실제 기후데이터의 경향이며 삭제하거나 fitting을 하면 추후 모델 성능이 떨어지거나 overfitting 가능성이 있으므로 그대로 유지하기로 판단
mask = np.triu(np.ones_like(data.corr(), dtype=np.bool))
sns.heatmap(data.corr(method = 'pearson', min_periods =1), annot = True, mask = mask)
plt.xticks(rotation = 45)
[ IQR 기반 이상치 제거]
Q1 = data[['평균습도','일사합', '일조율','최고기온', '최저기온']].quantile(q=0.25)
Q3 = data[['평균습도', '일사합', '일조율', '최고기온', '최저기온']].quantile(q=0.75)
print(Q1, Q3)
IQR = Q3 - Q1
IQR_df = data[(data['평균습도'] <= Q3['평균습도'] + 1.5 * IQR['평균습도']) & (data['평균습도'] >= Q1['평균습도'] - 1.5 * IQR['평균습도'])]
IQR_df = IQR_df[(IQR_df['일사합'] <= Q3['일사합'] + 1.5 * IQR['일사합']) & (IQR_df['일사합'] >= Q1['일사합'] - 1.5 * IQR['일사합'])]
IQR_df = IQR_df[(IQR_df['일조율'] <= Q3['일조율'] + 1.5 * IQR['일조율']) & (IQR_df['일조율'] >= Q1['일조율'] - 1.5 * IQR['일조율'])]
IQR_df = IQR_df[(IQR_df['최고기온'] <= Q3['최고기온'] + 1.5 * IQR['최고기온']) & (IQR_df['최고기온'] >= Q1['최고기온'] - 1.5 * IQR['최고기온'])]
IQR_df = IQR_df[(IQR_df['최저기온'] <= Q3['최저기온'] + 1.5 * IQR['최저기온']) & (IQR_df['최저기온'] >= Q1['최저기온'] - 1.5 * IQR['최저기온'])]
💡 IQR 개념
- 제1사분위수 = Q1(25%) - 제3사분위수 = Q3(75%) - IQR = Q3 - Q1 * IQR 범위 값 구하기 Q1 - 1.5 * IQR <= values <= Q3 + 1.5 * IQR ☞ 해당 범위 안에 있는 값만 보존
from sklearn.preprocessing import LabelEncoder
encoder = LabelEncoder()
IQR_df['계절'] = encoder.fit_transform(IQR_df['계절'])
import prophet
from prophet import Prophet
from sklearn.metrics import mean_absolute_error
df = df.reset_index()
df = df.rename(columns={'일시': 'ds', '평균기온': 'y'})
train_size = int(0.8 * len(df))
val_size = int(0.1 * len(df))
test_size = len(df) - train_size - val_size
train_df = df[:train_size]
val_df = df[train_size:train_size+val_size]
test_df = df[train_size+val_size:]
prophet = Prophet(changepoint_prior_scale=0.5,
daily_seasonality=True,
seasonality_mode = 'additive',
seasonality_prior_scale = 10)
prophet.fit(train_df)
- 파라미터 설명
plt.figure(figsize = (8, 3))
# validation 데이터를 사용하여 모델 예측(optional)
future_val = prophet.make_future_dataframe(periods = val_size)
forecast_val = prophet.predict(future_val)
# 검증 데이터와 예측값 비교
fig = prophet.plot(forecast_val)
plt.title('validaition')
# 검증 데이터에 대한 평가 지표 계산 (예: 평균제곱오차)
mae = mean_absolute_error(val_df['y'], forecast_val['yhat'][:val_size])
print(f'Mean Squared Error on Validation Data: {mae}')
# test 데이터를 사용하여 최종 예측 수행
future_test = prophet.make_future_dataframe(periods=test_size)
forecast_test = prophet.predict(future_test)
# test 데이터와 예측값 비교
fig = prophet.plot(forecast_test)
plt.title('test')
# test 데이터에 대한 평가 지표 계산D
mae_test = mean_absolute_error(test_df['y'], forecast_test['yhat'][-test_size:])
print(f'Mean Squared Error on Test Data: {mae_test}')
검증결과 : 2.8194 / 평가결과 : 2.8751
* Blue Line = 예측값 / Black dot = 실제값
[Validation 데이터 성능 시각화]
[Test 데이터 성능 시각화]
future_data = prophet.make_future_dataframe(periods = 358, freq = 'd') #periods는 예측할 기간
forecast_data = prophet.predict(future_data)
forecast_data[['ds','yhat']].tail()
submission_df = pd.read_csv('/content/drive/MyDrive/data/sample_submission.csv')
submission_df['평균기온'] = forecast_data.yhat[-358:].values
submission_df.tail()
#결과 저장
submission_df.to_csv("submission_prophet_final2.csv", index=False)
column_indices = {name: i for i, name in enumerate(df.columns)}
n = len(df)
train_df = df[0:int(n*0.7)]
val_df = df[int(n*0.7):int(n*0.9)]
test_df = df[int(n*0.9):]
num_features = df.shape
print(num_features)
train_mean = train_df.mean()
train_std = train_df.std()
train_df = (train_df - train_mean) / train_std
val_df = (val_df - train_mean) / train_std
test_df = (test_df - train_mean) / train_std
- Window 개념: 시계열 데이터나 시퀀스 데이터를 처리할 때 사용되는 개념이며, 데이터를 일정한 크기의 창 또는 기간으로 나누어서 처리하는 방법. Window 기법은 데이터를 일정한 크기의 부분 집합으로 분할하여 모델에 입력하는 방식으로 사용.
- LSTM은 입력으로 고정된 크기의 시퀀스를 요구됨. window를 사용하면 가변 길이의 시퀀스를 고정된 크기로 자를 수 있어 LSTM에 입력으로 사용하기 편리
[Window 생성 class 정의]
class WindowGenerator():
def __init__(self, input_width, label_width, shift,
train_df=train_df, val_df=val_df, label_columns=None):
# Store the raw data.
self.train_df = train_df
self.val_df = val_df
# Work out the label column indices.
self.label_columns = label_columns
if label_columns is not None:
self.label_columns_indices = {name: i for i, name in
enumerate(label_columns)}
self.column_indices = {name: i for i, name in
enumerate(train_df.columns)}
# Work out the window parameters.
self.input_width = input_width
self.label_width = label_width
self.shift = shift
self.total_window_size = input_width + shift
self.input_slice = slice(0, input_width)
self.input_indices = np.arange(self.total_window_size)[self.input_slice]
self.label_start = self.total_window_size - self.label_width
self.labels_slice = slice(self.label_start, None)
self.label_indices = np.arange(self.total_window_size)[self.labels_slice]
def __repr__(self):
return '\n'.join([
f'Total window size: {self.total_window_size}',
f'Input indices: {self.input_indices}',
f'Label indices: {self.label_indices}',
f'Label column name(s): {self.label_columns}'])
[분할함수 정의]
def split_window(self, features):
inputs = features[:, self.input_slice, :]
labels = features[:, self.labels_slice, :]
if self.label_columns is not None:
labels = tf.stack(
[labels[:, :, self.column_indices[name]] for name in self.label_columns],
axis=-1)
# Slicing doesn't preserve static shape information, so set the shapes
# manually. This way the `tf.data.Datasets` are easier to inspect.
inputs.set_shape([None, self.input_width, None])
labels.set_shape([None, self.label_width, None])
return inputs, labels
WindowGenerator.split_window = split_window
[tf.data.Dataset 만들기]
def make_dataset(self, data):
data = np.array(data, dtype=np.float32)
ds = tf.keras.utils.timeseries_dataset_from_array(
data=data,
targets=None,
sequence_length=self.total_window_size,
sequence_stride=1,
shuffle=True,
batch_size=32,)
ds = ds.map(self.split_window)
return ds
WindowGenerator.make_dataset = make_dataset
[@property를 통한 메서드 엑세스 간편화]
@property
def train(self):
return self.make_dataset(self.train_df)
@property
def val(self):
return self.make_dataset(self.val_df)
@property
def example(self):
"""Get and cache an example batch of `inputs, labels` for plotting."""
result = getattr(self, '_example', None)
if result is None:
# No example batch was found, so get one from the `.train` dataset
result = next(iter(self.train))
# And cache it for next time
self._example = result
return result
WindowGenerator.train = train
WindowGenerator.val = val
WindowGenerator.example = example
MAX_EPOCHS = 110
def compile_and_fit(model, window, patience=5):
early_stopping = tf.keras.callbacks.EarlyStopping(monitor='val_loss',
patience=patience,
mode='min',
restore_best_weights=True)
model.compile(loss=tf.keras.losses.MeanSquaredError(),
optimizer=tf.keras.optimizers.SGD(),
metrics=[tf.keras.metrics.MeanAbsoluteError()])
history = model.fit(window.train, epochs=MAX_EPOCHS,
validation_data=window.val,
callbacks=[early_stopping])
return history
OUT_STEPS = 358
INPUT_WIDTH = OUT_STEPS * 3
multi_window = WindowGenerator(input_width=INPUT_WIDTH,
label_width=OUT_STEPS,
shift=OUT_STEPS,
label_columns=['평균기온'])
multi_window.train.element_spec
multi_val_performance = {}
multi_performance = {}
multi_lstm_model = tf.keras.Sequential([
# Shape [batch, time, features] => [batch, lstm_units].
tf.keras.layers.LSTM(128, return_sequences=True),
tf.keras.layers.LSTM(128, return_sequences=True),
tf.keras.layers.LSTM(128, return_sequences=False),
# Shape => [batch, out_steps*features].
tf.keras.layers.Dense(OUT_STEPS,
kernel_initializer=tf.initializers.zeros()),
# Shape => [batch, out_steps, features].
tf.keras.layers.Reshape([OUT_STEPS, 1])
])
history = compile_and_fit(multi_lstm_model, multi_window)
# IPython.display.clear_output()
multi_val_performance['LSTM'] = multi_lstm_model.evaluate(multi_window.val)
multi_performance['LSTM'] = multi_lstm_model.evaluate(multi_window.val, verbose=0)
- 모델평가 결과: loss율 : 0.1029 / MAE : 0.2519
input_submit = test_df[-INPUT_WIDTH:].values.reshape(1, INPUT_WIDTH, 9)
pred = multi_lstm_model.predict(input_submit)
plt.plot(pred)
prediction = pred[-1].reshape(-1,) * train_std['평균기온'] + train_mean['평균기온']
⭐ 결론
- Prophet은 회귀분석을 기반으로 한 시계열 예측모델이며 머신러닝에 속함. Prophet은 복잡한 튜닝 없이도 간편하게 모델을 설정하고 예측 가능
- LSTM은 순환 신경망(RNN)의 한 종류로, 시계열 데이터 및 순차적인 데이터를 모델링하는데 효과적인 모델이며 장기 기억과 단기 기억을 관리하여 시퀀스 데이터의 장기 의존성을 감지하고 유지할 수 있음
- Prophet에 비해 LSTM이 모델링 하는데에 있어, 더욱 정교하며 파라미터의 종류도 다양함
- 기후데이터를 여러방법으로 전처리를 수행하고 모델을 학습시켰지만 성능에 있어서 큰 차이는 보이지 않음
- 프로젝트 진행시, Prophet 모델의 성능이 더 잘 나왔으며, LSTM의 성능을 높이기 위해서는 정교한 하이퍼파라미터 튜닝이 필요
- 데이터 전처리에 있어서, 평균풍속 및 강수량의 이상치 처리가 keypoint이며 각 컬럼의 결측치 처리의 방식도 중요하다고 분석
🏆DACON 해커톤 결과
- Public : 90/638등
- Private : 44/624등
🤔 첫 대회치고는 꽤 괜찮은 성적인것 같다. 다만 아쉬웠던 점은 모델들을 완벽히 이해하고 사용한 것이 아니라 예제 코드들을 보면서 사용했던 것이 조금 아쉽다. 다음 대회나 프로젝트에서는 모델에 대한 연구를 깊게 하고, 전처리 방식도 고민을 해서 진행을 해야겠다.
📑참고문헌
- https://www.tensorflow.org (텐서플로우 공식 홈페이지)
- https://mikulskibartosz.name/prophet-plot-explained
- https://pseudo-lab.github.io/Tutorial-Book/chapters/time-series/Ch4-LSTM.html
- (책) 위키북스, 정석으로 배우는 딥러닝 - 5장. 신경 순환망
- https://facebook.github.io/prophet/docs/quick_start.html
- https://paperswithcode.com/method/lstm (paperswithcode)
- https://dacon.io/competitions/official/236200/overview/description (데이콘)