
서울시 열린 데이터 광장의 공공데이터를 활용하여 따릉이의 대여 이력과 대여소 위치 정보를 결합하고, 요일별/시간대별 이용 패턴을 시각화함.
분석을 위해 서울 열린 데이터 광장(https://data.seoul.go.kr/)에서 두 가지 핵심 자료를 다운로드함.
bike.csvbike_rent_location.xlsximport pandas as pd
# 대여 이력 데이터 로드
df = pd.read_csv(
"./data/bike.csv",
encoding="cp949",
parse_dates=["대여일시", "반납일시"],
date_format="%Y-%m-%d %H:%M:%S",
)
# 대여소 위치 정보 로드 (불필요한 행 제외 및 컬럼명 재정의)
rent_location = pd.read_excel(
"./data/bike_rent_location.xlsx",
sheet_name="대여소현황",
skiprows=5,
engine="openpyxl",
header=None,
names=["대여소번호", "대여소명", "자치구", "상세주소", "위도", "경도", "설치시기", "LCD", "QR", "운영방식"],
)
대여 이력에는 대여소 번호만 존재하므로, 대여소 정보 테이블과 머지(Merge)하여 자치구와 위/경도 정보를 추가해야 함.
대여소번호가 한쪽은 문자열, 한쪽은 숫자인 경우 머지 불가.\N 등 숫자로 변환 불가능한 문자열이 섞여 있는 경우.위 문제를 해결하기 위해 errors='coerce' 옵션을 사용하여 비수치 데이터를 NaN으로 처리한 후, 5자리 포맷(00123)으로 통일함.
def add_lat_lon_to_rent(rent, location):
rent = rent.copy()
location = location.copy()
# 번호 형식을 5자리 문자열로 통일하는 함수 (예: 123 -> '00123')
def format_station_id(series):
# 숫자로 변환 불가한 값(\N 등)은 NaN 처리
nums = pd.to_numeric(series, errors='coerce')
return nums.dropna().astype(int).apply(lambda x: f"{x:05d}")
# 양쪽 데이터프레임의 번호 형식을 통일
location["대여소번호"] = format_station_id(location["대여소번호"])
rent["대여 대여소번호"] = format_station_id(rent["대여 대여소번호"])
rent["반납대여소번호"] = format_station_id(rent["반납대여소번호"])
loc_cols = ["대여소번호", "자치구", "위도", "경도"]
# 대여 지점 위치 정보 병합 (Left Join)
rent = rent.merge(
location[loc_cols].rename(columns={"대여소번호": "대여 대여소번호", "자치구": "대여 자치구", "위도": "대여 위도", "경도": "대여 경도"}),
on="대여 대여소번호", how="left"
)
# 반납 지점 위치 정보 병합 (Left Join)
rent = rent.merge(
location[loc_cols].rename(columns={"대여소번호": "반납대여소번호", "자치구": "반납 자치구", "위도": "반납 위도", "경도": "반납 경도"}),
on="반납대여소번호", how="left"
)
return rent
combined_with_location = add_lat_lon_to_rent(df, rent_location)
대여일시로부터 요일을 한글로 매핑하고, 주말 여부를 판별하는 파생 변수를 생성함.
def add_dayofweek_and_weekend(df):
days_kr = ["월", "화", "수", "목", "금", "토", "일"]
# dt.dayofweek는 월(0)~일(6) 반환
df["요일"] = df["대여일시"].dt.dayofweek.map(lambda x: days_kr[x])
# 토(5), 일(6)은 주말로 간주 (True)
df["주말"] = df["대여일시"].dt.dayofweek >= 5
return df
combined_with_location = add_dayofweek_and_weekend(combined_with_location)
전체적인 요일별 대여 빈도를 확인함. 폰트는 맑은 고딕을 사용함.
import seaborn as sns
import matplotlib.pyplot as plt
import matplotlib
# 한글 폰트 설정
matplotlib.rc("font", family="Malgun Gothic")
def plot_dayofweek_count(df):
plt.figure(figsize=(8, 5))
order = ["월", "화", "수", "목", "금", "토", "일"]
sns.countplot(data=df, x="요일", order=order)
plt.title("요일별 대여 건수")
plt.xlabel("요일")
plt.ylabel("건수")
plt.show()
plot_dayofweek_count(combined_with_location)

groupby와 unstack을 활용해 피벗 테이블을 생성한 후 히트맵으로 표현함.
def make_pivot_by_hour_and_day(df):
df["대여시간대"] = df["대여일시"].dt.hour
# 시간대/요일 기준 집계
pivot = df.groupby(["대여시간대", "요일"]).size().unstack(fill_value=0)
# 요일 순서 재정렬
day_order = ["월", "화", "수", "목", "금", "토", "일"]
return pivot.reindex(columns=day_order)
pivot_df = make_pivot_by_hour_and_day(combined_with_location)
# 히트맵 시각화
plt.figure(figsize=(10, 6))
sns.heatmap(pivot_df, annot=True, fmt="d", cmap="YlGnBu", cbar_kws={"label": "건수"})
plt.title("대여시간대별 요일별 대여 건수 Heatmap")
plt.show()

분석: 평일 출/퇴근 시간대에 이용량이 집중되는 경향을 보임.
자치구별로 평일과 주말의 시간대별 이용 패턴 차이를 선 그래프로 비교함.
def plot_usage_by_weekend(df):
# 데이터 집계
df_grouped = df.groupby(["대여시간대", "주말", "대여 자치구"]).size().reset_index(name="count")
weekday = df_grouped[df_grouped["주말"] == False]
weekend = df_grouped[df_grouped["주말"] == True]
# Subplot 생성
fig, axes = plt.subplots(1, 2, figsize=(18, 7), sharey=True)
# 평일 그래프
sns.lineplot(data=weekday, x="대여시간대", y="count", hue="대여 자치구", ax=axes[0])
axes[0].set_title("평일 대여시간대별 자치구별 대여 건수")
# 주말 그래프
sns.lineplot(data=weekend, x="대여시간대", y="count", hue="대여 자치구", ax=axes[1])
axes[1].set_title("주말 대여시간대별 자치구별 대여 건수")
plt.tight_layout()
plt.show()
plot_usage_by_weekend(combined_with_location)

분석: 평일은 출퇴근 피크(8시, 18시)가 뚜렷한 반면, 주말은 오후 시간대에 완만한 곡선을 그림.
서울시 자치구별 대여 건수를 지도 위에 색상으로 시각화함. seoul.json 파일이 필요함.
import folium
def draw_choropleth_by_gu(df, geojson_path, value_col, legend_name):
# 자치구별 대여 건수 합산
data_by_gu = df.groupby("대여 자치구")[value_col].count().reset_index()
# 서울 중심 좌표
seoul_center = [37.5665, 126.9780]
m = folium.Map(location=seoul_center, zoom_start=11)
folium.Choropleth(
geo_data=geojson_path,
data=data_by_gu,
columns=["대여 자치구", value_col],
key_on="feature.properties.name", # GeoJSON 속성 키 확인 필요
fill_color="Purples",
fill_opacity=0.7,
line_opacity=0.2,
legend_name=legend_name,
).add_to(m)
return m
m = draw_choropleth_by_gu(combined_with_location, "data/seoul.json", "자전거번호", "대여 건수")
m.save('seoul_bike_choropleth.html')
