머신러닝을 통한 자전거 대여 수요 예측: (1) EDA
자전거 대여 시스템 데이터를 이용해 자전거 대여 패턴을 분석하고, 대여 수요를 가장 잘 예측하는 모델을 찾아내고자 한다.
데이터는 Kaggle의 Bike Sharing Demand 대회 데이터를 이용했다.
데이터 불러오기 및 확인
# 데이터 불러오기
df_train = pd.read_csv('./src/train.csv')
df_test = pd.read_csv('./src/test.csv')
df_train.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 10886 entries, 0 to 10885
Data columns (total 12 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 datetime 10886 non-null object
1 season 10886 non-null int64
2 holiday 10886 non-null int64
3 workingday 10886 non-null int64
4 weather 10886 non-null int64
5 temp 10886 non-null float64
6 atemp 10886 non-null float64
7 humidity 10886 non-null int64
8 windspeed 10886 non-null float64
9 casual 10886 non-null int64
10 registered 10886 non-null int64
11 count 10886 non-null int64
dtypes: float64(3), int64(8), object(1)
memory usage: 1020.7+ KB
df_test.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 6493 entries, 0 to 6492
Data columns (total 9 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 datetime 6493 non-null object
1 season 6493 non-null int64
2 holiday 6493 non-null int64
3 workingday 6493 non-null int64
4 weather 6493 non-null int64
5 temp 6493 non-null float64
6 atemp 6493 non-null float64
7 humidity 6493 non-null int64
8 windspeed 6493 non-null float64
dtypes: float64(3), int64(5), object(1)
memory usage: 456.7+ KB
train 데이터와 test 데이터 모두 결측치가 존재하지 않는다.
datetime
변수가 object(string) type으로 되어 있어 이를 datetime type으로 변환하고 파생변수들을 생성하였다.
datetime 변수 형변환 및 파생변수 생성
# string to datetime
df_train['datetime'] = pd.to_datetime(df_train['datetime'])
df_test['datetime'] = pd.to_datetime(df_test['datetime'])
# year, month, day, hour, weekday 컬럼 생성
df_train['year'] = df_train['datetime'].dt.year
df_train['month'] = df_train['datetime'].dt.month
df_train['day'] = df_train['datetime'].dt.day
df_train['hour'] = df_train['datetime'].dt.hour
df_train['weekday'] = df_train['datetime'].dt.day_name()
df_test['year'] = df_test['datetime'].dt.year
df_test['month'] = df_test['datetime'].dt.month
df_test['day'] = df_test['datetime'].dt.day
df_test['hour'] = df_test['datetime'].dt.hour
df_test['weekday'] = df_test['datetime'].dt.day_name()
df_train.head()
datetime | season | holiday | workingday | weather | temp | atemp | humidity | windspeed | casual | registered | count | year | month | day | hour | weekday | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 2011-01-01 00:00:00 | 1 | 0 | 0 | 1 | 9.84 | 14.395 | 81 | 0.0 | 3 | 13 | 16 | 2011 | 1 | 1 | 0 | Saturday |
1 | 2011-01-01 01:00:00 | 1 | 0 | 0 | 1 | 9.02 | 13.635 | 80 | 0.0 | 8 | 32 | 40 | 2011 | 1 | 1 | 1 | Saturday |
2 | 2011-01-01 02:00:00 | 1 | 0 | 0 | 1 | 9.02 | 13.635 | 80 | 0.0 | 5 | 27 | 32 | 2011 | 1 | 1 | 2 | Saturday |
3 | 2011-01-01 03:00:00 | 1 | 0 | 0 | 1 | 9.84 | 14.395 | 75 | 0.0 | 3 | 10 | 13 | 2011 | 1 | 1 | 3 | Saturday |
4 | 2011-01-01 04:00:00 | 1 | 0 | 0 | 1 | 9.84 | 14.395 | 75 | 0.0 | 0 | 1 | 1 | 2011 | 1 | 1 | 4 | Saturday |
로그 변환
# train 데이터 변수들 히스토그램
# 사용할 변수 목록
columns_to_plot = [
"season", "holiday", "workingday", "weather",
"temp", "atemp", "humidity", "windspeed",
"casual", "registered", "count", "year"
]
def plot_selected_histograms(df, columns):
num_columns = len(columns)
rows = (num_columns + 3) // 4
plt.figure(figsize=(16, rows * 4))
for i, column in enumerate(columns, 1):
plt.subplot(rows, 4, i)
plt.hist(df[column], bins=30, color='blue', alpha=0.7, edgecolor='black')
plt.title(column)
plt.tight_layout()
plt.show()
plot_selected_histograms(df_train, columns_to_plot)
모델에서 종속변수로 사용될 casual
, registered
, count
변수의 데이터의 분포가 왼쪽으로 치우쳐져 있다.
따라서 로그 변환을 통해 데이터 정규화를 진행하였다.
# 로그 변환
def apply_log_transformation(df, columns):
for column in columns:
df[column] = np.log1p(df[column])
return df
columns_to_log_transform = ["casual", "registered", "count"]
df_train = apply_log_transformation(df_train, columns_to_log_transform)
columns_to_plot = ["casual", "registered", "count"]
plot_selected_histograms(df_train, columns_to_plot)
season 변수 재정의
# season별 casual, registered, count 박스플롯
def plot_boxplots_by_season_horizontal(df, variables, group_by_column):
num_variables = len(variables)
plt.figure(figsize=(18, 6))
for i, var in enumerate(variables, 1):
plt.subplot(1, num_variables, i)
grouped_data = [df[df[group_by_column] == season][var] for season in sorted(df[group_by_column].unique())]
plt.boxplot(
grouped_data,
tick_labels=sorted(df[group_by_column].unique()),
patch_artist=True,
boxprops=dict(facecolor='skyblue', color='blue'),
medianprops=dict(color='red'),
whiskerprops=dict(color='blue')
)
plt.title(f"{var} by {group_by_column}")
plt.xlabel(group_by_column.capitalize())
plt.ylabel(var.capitalize())
plt.grid(axis='y', linestyle='--', alpha=0.7)
plt.tight_layout()
plt.show()
variables_to_plot = ["casual", "registered", "count"]
plot_boxplots_by_season_horizontal(df_train, variables_to_plot, "season")
season
변수의 1이 봄이고 4가 겨울인데, 봄의 자전거 대여 수가 겨울보다 적게 나타나는 것에 대해 의문이 들었다.
# season이 1(봄)일 때 month
df_train[df_train['season'] == 1]['month'].unique()
array([1, 2, 3], dtype=int32)
# season이 4(겨울)일 때 month
df_train[df_train['season'] == 4]['month'].unique()
array([10, 11, 12], dtype=int32)
확인해보니 1월~3월이 season
변수의 1(봄)로 할당되어 있었고, 10월~12월이 season
변수의 4(겨울)로 할당되어 있었다.
보통 12월~2월을 겨울, 3월~5월을 봄으로 보기 때문에 season
변수를 재정의할 필요가 있는지 확인하기 위해 월별 온도를 시각화했다.
# 월별 평균 기온 시각화
month_avg_temp = df_train.groupby("month")["temp"].mean()
plt.figure(figsize=(10, 6))
plt.plot(month_avg_temp.index, month_avg_temp.values, marker='o', linestyle='-', color='blue')
plt.title("월별 평균 기온", fontsize=16)
plt.xlabel("월", fontsize=12)
plt.ylabel("기온", fontsize=12)
plt.xticks(month_avg_temp.index)
plt.grid(axis='y', linestyle='--', alpha=0.7)
plt.show()
예상대로 12월~2월에 연중 온도가 가장 낮게 나타나
season
변수를 재정의하였다.
# season 변수 재정의
conditions = [
df_train['month'].isin([3, 4, 5]),
df_train['month'].isin([6, 7, 8]),
df_train['month'].isin([9, 10, 11]),
df_train['month'].isin([12, 1, 2])
]
choices = [1, 2, 3, 4]
df_train['season'] = np.select(conditions, choices)
# season별 casual, registered, count 박스플롯
def plot_boxplots_by_season_horizontal(df, variables, group_by_column):
num_variables = len(variables)
plt.figure(figsize=(18, 6))
for i, var in enumerate(variables, 1):
plt.subplot(1, num_variables, i)
grouped_data = [df[df[group_by_column] == season][var] for season in sorted(df[group_by_column].unique())]
plt.boxplot(
grouped_data,
tick_labels=sorted(df[group_by_column].unique()),
patch_artist=True,
boxprops=dict(facecolor='skyblue', color='blue'),
medianprops=dict(color='red'),
whiskerprops=dict(color='blue')
)
plt.title(f"{var} by {group_by_column}")
plt.xlabel(group_by_column.capitalize())
plt.ylabel(var.capitalize())
plt.grid(axis='y', linestyle='--', alpha=0.7)
plt.tight_layout()
plt.show()
variables_to_plot = ["casual", "registered", "count"]
plot_boxplots_by_season_horizontal(df_train, variables_to_plot, "season")
재정의 후 casual
, registered
, count
모두 겨울에 가장 적게 나타난다.
데이터 시각화
실제 값의 뚜렷한 추이를 보기 위해 시각화할 때는 로그 변환 전의 데이터를 사용하였다.
# 시각화를 위해 스케일링(로그 변환)되지 않은 train 데이터 준비
df_train_unscaled = pd.read_csv('./src/train.csv')
# string to datetime
df_train_unscaled['datetime'] = pd.to_datetime(df_train['datetime'])
# year, month, day, hour 컬럼 생성
df_train_unscaled['year'] = df_train_unscaled['datetime'].dt.year
df_train_unscaled['month'] = df_train_unscaled['datetime'].dt.month
df_train_unscaled['day'] = df_train_unscaled['datetime'].dt.day
df_train_unscaled['hour'] = df_train_unscaled['datetime'].dt.hour
df_train_unscaled['weekday'] = df_train_unscaled['datetime'].dt.day_name()
# season 변수 재정의
conditions = [
df_train_unscaled['month'].isin([3, 4, 5]),
df_train_unscaled['month'].isin([6, 7, 8]),
df_train_unscaled['month'].isin([9, 10, 11]),
df_train_unscaled['month'].isin([12, 1, 2])
]
choices = [1, 2, 3, 4]
df_train_unscaled['season'] = np.select(conditions, choices)
# 월별 평균 대여 수
monthly_avg = df_train_unscaled.groupby('month')['count'].mean()
plt.figure(figsize=(10, 5))
monthly_avg.plot(kind='bar', color='skyblue')
plt.title('월별 평균 대여 수')
plt.ylabel('평균 대여 수')
plt.xlabel('월')
plt.xticks(ticks=range(12), labels=range(1, 13), rotation=0)
plt.tight_layout()
plt.show()
여름철에 자전거 대여 수가 증가하고 겨울철에 자전거 대여 수가 감소한다.
# 계절에 따른 시간대별 평균 대여 수
season_hour_avg = df_train_unscaled.groupby(['season', 'hour'])['count'].mean().reset_index()
season_map = {1: 'Spring', 2: 'Summer', 3: 'Fall', 4: 'Winter'}
season_hour_avg['season'] = season_hour_avg['season'].map(season_map)
plt.figure(figsize=(10, 5))
sns.lineplot(data=season_hour_avg, x='hour', y='count', hue='season', marker='o')
plt.title('계절에 따른 시간대별 평균 대여 수')
plt.ylabel('평균 대여 수')
plt.xlabel('시간대(하루)')
plt.xticks(range(0, 24))
plt.grid(axis='y')
plt.tight_layout()
plt.show()
여름, 가을, 봄, 겨울 순으로 자전거 대여 수가 많다.
출퇴근 시간대에 자전거 대여 수가 급증한다.
# 요일에 따른 시간대별 평균 대여 수
weekday_hour_avg = df_train_unscaled.groupby(['weekday', 'hour'])['count'].mean().reset_index()
weekday_hour_avg['weekday'] = pd.Categorical(
weekday_hour_avg['weekday'],
categories=['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'],
ordered=True
)
plt.figure(figsize=(10, 5))
sns.lineplot(data=weekday_hour_avg, x='hour', y='count', hue='weekday', marker='o')
plt.title('요일에 따른 시간대별 평균 대여 수')
plt.ylabel('평균 대여 수')
plt.xlabel('시간대(하루)')
plt.xticks(range(0, 24))
plt.grid(axis='y')
plt.tight_layout()
plt.show()
평일에는 출퇴근 시간대에 자전거 대여 수가 급증하고, 주말에는 오후 시간대에 자전거 대여 수가 많게 유지된다.
평일에는 실용적으로, 주말에는 여가용으로 대여가 많이 발생한다고 볼 수 있다.
# 사용자 타입에 따른 시간대별 평균 대여 수
user_type_hour_avg = df_train_unscaled.groupby(['hour']).agg({'casual': 'mean', 'registered': 'mean'}).reset_index()
plt.figure(figsize=(10, 5))
sns.lineplot(data=user_type_hour_avg, x='hour', y='casual', label='Casual', marker='o')
sns.lineplot(data=user_type_hour_avg, x='hour', y='registered', label='Registered', marker='o')
plt.title('사용자 타입에 따른 시간대별 평균 대여 수')
plt.ylabel('평균 대여 수')
plt.xlabel('시간대(하루)')
plt.xticks(range(0, 24))
plt.legend(title='User Type')
plt.grid(axis='y')
plt.tight_layout()
plt.show()
주로 등록 사용자는 출퇴근 용도로, 미등록 사용자는 여가 용도로 자전거를 대여함을 알 수 있다.
등록 사용자의 대여 수가 미등록 사용자의 대여 수보다 많은 것도 볼 수 있다.
Leave a comment