[Data Analysis] 서울시 공공자전거 '따릉이' 이용 패턴 분석 및 시각화

감자·2026년 1월 14일

microsoft_dataschool

목록 보기
7/28
post-thumbnail

서울시 열린 데이터 광장의 공공데이터를 활용하여 따릉이의 대여 이력과 대여소 위치 정보를 결합하고, 요일별/시간대별 이용 패턴을 시각화함.


01. 데이터 수집 및 읽기

분석을 위해 서울 열린 데이터 광장(https://data.seoul.go.kr/)에서 두 가지 핵심 자료를 다운로드함.

  • 서울시 공공자전거 대여이력 정보: bike.csv
  • 서울시 공공자전거 대여소 정보: bike_rent_location.xlsx
import 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", "운영방식"],
)

02. 데이터 전처리: 위치 정보 결합

대여 이력에는 대여소 번호만 존재하므로, 대여소 정보 테이블과 머지(Merge)하여 자치구와 위/경도 정보를 추가해야 함.

발생 가능한 오류 및 해결책

  1. 데이터 타입 불일치: 대여소번호가 한쪽은 문자열, 한쪽은 숫자인 경우 머지 불가.
  2. 결측치 포함: 데이터 내 \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)

03. 요일 및 주말 정보 생성

대여일시로부터 요일을 한글로 매핑하고, 주말 여부를 판별하는 파생 변수를 생성함.

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)

04. 시각화 분석

1) 요일별 대여 건수 (Bar Chart)

전체적인 요일별 대여 빈도를 확인함. 폰트는 맑은 고딕을 사용함.

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)

2) 시간대별/요일별 이용 패턴 (Heatmap)

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()

분석: 평일 출/퇴근 시간대에 이용량이 집중되는 경향을 보임.

3) 평일 vs 주말 이용 패턴 비교 (Line Chart)

자치구별로 평일과 주말의 시간대별 이용 패턴 차이를 선 그래프로 비교함.

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시)가 뚜렷한 반면, 주말은 오후 시간대에 완만한 곡선을 그림.

4) 지도 시각화 (Folium Choropleth)

서울시 자치구별 대여 건수를 지도 위에 색상으로 시각화함. 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')

profile
제 별명이 감자입니다

0개의 댓글