[44일차] 서울시 공공자전거 실시간 대여정보 API

송정근·2026년 8월 6일

1. 분석 개요

서울 열린데이터광장의 서울시 공공자전거 실시간 대여정보 API를 호출해 대여소별 자전거 현황을 수집하고, Pandas로 데이터프레임을 만든 뒤 Folium 지도에 대여소 위치를 표시한다.

이 데이터는 호출 시점의 현황을 제공하는 실시간 데이터다. 같은 코드를 실행해도 시간에 따라 대여 가능 자전거 수와 거치율이 달라질 수 있다.

분석 흐름은 다음과 같다.

서울 열린데이터광장 API 요청
-> JSON 응답 확인
-> 페이지 단위로 전체 데이터 수집
-> DataFrame 변환 및 자료형 정리
-> Folium 지도에 대여소 위치와 자전거 수 표시

2. 서울 열린데이터광장 API

서울 열린데이터광장은 서울시가 제공하는 공공데이터 플랫폼이다. 교통, 환경, 안전, 복지, 문화 등 다양한 데이터를 API와 파일 형태로 제공한다.

공공자전거 실시간 대여정보를 사용하려면 다음 순서로 준비한다.

  1. 서울 열린데이터광장에서 서울시 공공자전거 실시간 대여정보를 검색한다.
  2. 인증키를 발급받는다.
  3. 서비스 이름과 요청 형식, 응답 구조를 확인한다.
  4. 발급받은 인증키로 API를 요청한다.

인증키는 코드에 직접 작성하지 않는다

노트북이나 Git 저장소에 인증키를 직접 넣으면 키가 외부에 노출될 수 있다. 환경 변수로 관리하면 코드와 비밀 정보를 분리할 수 있다.

import os

API_KEY = os.environ["SEOUL_OPEN_API_KEY"]

macOS 또는 Linux 터미널에서는 다음처럼 현재 터미널 세션에 환경 변수를 설정할 수 있다.

export SEOUL_OPEN_API_KEY="발급받은_인증키"

인증키가 이미 공개된 상태라면 해당 키를 폐기하거나 재발급받는 것이 좋다.


3. 필요한 라이브러리

import os

import folium
import pandas as pd
import requests
라이브러리역할
requestsHTTP 요청을 보내 API 데이터를 받는다.
pandasJSON 목록을 표 형태의 DataFrame으로 정리한다.
folium위도와 경도를 사용해 인터랙티브 지도를 만든다.
os환경 변수에서 인증키를 읽는다.

설치되어 있지 않다면 다음 명령어를 사용한다.

python -m pip install requests pandas folium

4. API 요청과 JSON 응답 확인

서울 열린데이터광장 API의 요청 주소는 인증키, 응답 형식, 서비스 이름, 시작 행, 끝 행을 포함한다.

http://openapi.seoul.go.kr:8088/
{인증키}/json/bikeList/{시작번호}/{끝번호}/

먼저 1~5행만 요청해 응답 구조를 확인한다.

API_KEY = os.environ["SEOUL_OPEN_API_KEY"]
BASE_URL = "http://openapi.seoul.go.kr:8088"

url = (
    f"{BASE_URL}/{API_KEY}/json/"
    "bikeList/1/5/"
)

response = requests.get(url, timeout=10)
response.raise_for_status()

json_data = response.json()
json_data

requests.get()은 API에 HTTP GET 요청을 보낸다. timeout=10은 서버 응답이 너무 오래 걸릴 때 무한정 기다리지 않도록 제한하는 값이다. raise_for_status()는 HTTP 상태 코드가 200이 아닐 때 예외를 발생시켜 요청 실패를 빠르게 확인하게 한다.

노트북의 실행 결과에서는 다음과 같은 구조를 확인할 수 있다.

{
    "rentBikeStatus": {
        "list_total_count": 5,
        "RESULT": {
            "CODE": "INFO-000",
            "MESSAGE": "정상 처리되었습니다."
        },
        "row": [
            {
                "stationName": "102. 망원역 1번출구 앞",
                "parkingBikeTotCnt": "4",
                "stationLatitude": "37.55564880",
                "stationLongitude": "126.91062927"
            }
        ]
    }
}

응답에서 상태 코드 안전하게 읽기

딕셔너리 인덱싱을 여러 번 사용하면 중간 키가 없을 때 KeyError가 발생한다. .get()을 사용하면 키가 없을 때 기본값을 반환하도록 만들 수 있다.

result_code = (
    json_data
    .get("rentBikeStatus", {})
    .get("RESULT", {})
    .get("CODE")
)

print(result_code)  # INFO-000

노트북에서 확인한 INFO-000은 정상 처리 상태다. 반대로 응답 구조가 예상과 다르거나 오류 코드가 반환되면 row를 읽기 전에 오류 메시지를 확인해야 한다.


5. 응답 데이터 구조

rentBikeStatus 안에는 전체 행 수, 처리 결과, 실제 대여소 목록이 들어 있다.

키의미
list_total_count현재 응답 기준으로 조회 가능한 전체 데이터 수다.
RESULT요청 처리 상태 코드와 메시지다.
row대여소별 실시간 정보 목록이다.

각 row에는 다음과 같은 정보가 포함된다.

컬럼의미
stationId대여소 식별자다.
stationName대여소 이름이다.
rackTotCnt대여소의 전체 거치대 수다.
parkingBikeTotCnt현재 대여소에 거치된 자전거 수다.
shared전체 거치대 대비 현재 거치 자전거의 비율이다.
stationLatitude대여소 위도다.
stationLongitude대여소 경도다.

API 응답의 수치 값은 문자열로 제공될 수 있으므로, 계산이나 지도 표시 전에 숫자 자료형으로 변환하는 과정이 필요하다.


6. 페이지 단위로 전체 데이터 수집하기

API는 한 번에 가져올 수 있는 행 수가 제한될 수 있다. 따라서 1~1000, 1001~2000처럼 범위를 나누어 반복 요청하고, 받은 데이터를 하나의 DataFrame으로 합친다.

def fetch_bike_data(page_size=1000):
    api_key = os.environ["SEOUL_OPEN_API_KEY"]
    base_url = "http://openapi.seoul.go.kr:8088"

    start = 1
    data_frames = []

    while True:
        end = start + page_size - 1
        url = (
            f"{base_url}/{api_key}/json/"
            f"bikeList/{start}/{end}/"
        )

        response = requests.get(url, timeout=10)
        response.raise_for_status()

        json_data = response.json()
        bike_status = json_data.get("rentBikeStatus", {})
        result = bike_status.get("RESULT", {})
        result_code = result.get("CODE")

        if result_code != "INFO-000":
            message = result.get("MESSAGE", "알 수 없는 오류")
            print(f"수집 종료: {result_code} - {message}")
            break

        rows = bike_status.get("row", [])
        if not rows:
            break

        data_frames.append(pd.DataFrame(rows))

        total_count = int(bike_status["list_total_count"])
        if end >= total_count:
            break

        start = end + 1

    if not data_frames:
        return pd.DataFrame()

    return pd.concat(data_frames, ignore_index=True)
bike_data_df = fetch_bike_data()

print(bike_data_df.shape)
print(bike_data_df.head())

노트북 실행 시점에는 총 2,744개 대여소 행이 수집되었다. 실시간 데이터이므로 실행 시점에 따라 행 수와 각 대여소의 자전거 수는 달라질 수 있다.

반복 수집에서 확인할 점

  • 마지막 페이지를 넘어서 요청하면 데이터 서비스가 예상과 다른 오류 형식을 반환할 수 있다.
  • list_total_count를 확인하면 필요한 범위까지만 요청할 수 있다.
  • 네트워크 오류나 API 서버 오류가 발생할 수 있으므로 timeout과 상태 코드 확인이 필요하다.
  • API의 전체 데이터 수는 실시간으로 변할 수 있으므로 수집 중 행 수가 달라질 가능성도 고려해야 한다.

7. 자료형 변환과 데이터 점검

위도와 경도는 Folium 지도에서 숫자로 사용해야 한다. pd.to_numeric()을 사용하면 숫자로 변환할 수 없는 값은 NaN으로 처리할 수 있다.

numeric_columns = [
    "rackTotCnt",
    "parkingBikeTotCnt",
    "shared",
    "stationLatitude",
    "stationLongitude",
]

for column in numeric_columns:
    bike_data_df[column] = pd.to_numeric(
        bike_data_df[column],
        errors="coerce",
    )

bike_data_df = bike_data_df.dropna(
    subset=["stationLatitude", "stationLongitude"]
)

이후 기본 정보를 확인한다.

print(bike_data_df.info())
print(bike_data_df.isna().sum())
print(bike_data_df.describe())

astype(float)도 사용할 수 있지만, 잘못된 문자열이 하나라도 있으면 변환 과정에서 바로 오류가 발생한다. 공공데이터처럼 입력 형식을 완전히 신뢰하기 어려운 경우에는 pd.to_numeric(errors="coerce")가 더 안전하다.


8. Folium으로 대여소 지도 만들기

Folium은 Leaflet.js를 기반으로 HTML 형태의 인터랙티브 지도를 만든다. 지도 중심은 모든 대여소의 평균 위도와 평균 경도로 설정한다.

bike_map = folium.Map(
    location=[
        bike_data_df["stationLatitude"].mean(),
        bike_data_df["stationLongitude"].mean(),
    ],
    zoom_start=12,
)

각 대여소에 마커를 추가하고, 마커를 클릭했을 때 대여소 이름과 현재 거치 자전거 수가 보이도록 팝업을 만든다.

for _, row in bike_data_df.iterrows():
    popup_text = (
        f"{row['stationName']}<br>"
        f"현재 거치 자전거: {row['parkingBikeTotCnt']}대"
    )

    folium.Marker(
        location=[
            row["stationLatitude"],
            row["stationLongitude"],
        ],
        popup=folium.Popup(popup_text, max_width=300),
    ).add_to(bike_map)

bike_map

Jupyter Notebook에서는 마지막 줄의 bike_map을 실행하면 지도가 바로 표시된다. HTML 파일로 저장하면 브라우저에서 독립적으로 열 수 있다.

bike_map.save("seoul_bike_map.html")

마커가 많을 때의 개선 방법

대여소가 수천 개이면 일반 마커를 모두 표시할 때 지도 렌더링이 느려질 수 있다. 이때는 작은 원 모양의 CircleMarker를 사용하거나, 가까운 마커를 묶어 주는 MarkerCluster를 적용할 수 있다.

from folium.plugins import MarkerCluster

marker_cluster = MarkerCluster().add_to(bike_map)

for _, row in bike_data_df.iterrows():
    folium.Marker(
        location=[row["stationLatitude"], row["stationLongitude"]],
        popup=row["stationName"],
    ).add_to(marker_cluster)

9. 데이터 해석 시 주의점

parkingBikeTotCnt가 낮다고 해서 해당 대여소의 운영 상태가 항상 나쁘다는 뜻은 아니다. 출퇴근 시간처럼 이용 수요가 높은 시간대에는 대여가 활발해서 자전거 수가 적을 수 있다. 반대로 자전거 수가 많아도 반납이 몰린 결과일 수 있다.

실제 분석에서는 한 시점의 값만 보기보다, 일정 간격으로 수집한 데이터를 쌓아 다음과 같은 질문을 분석하는 편이 더 의미 있다.

  • 출퇴근 시간대에 자전거가 부족한 대여소는 어디인가?
  • 요일과 시간대별 거치율은 어떻게 달라지는가?
  • 특정 지역에서 대여와 반납이 불균형한가?
  • 대여소 증설 또는 재배치가 필요한 지역은 어디인가?

10. 핵심 정리

  • 서울 열린데이터광장 API를 사용하면 서울시 공공자전거의 실시간 대여 정보를 JSON 형태로 받을 수 있다.
  • API 응답의 RESULT 상태 코드와 row 목록을 확인한 뒤 데이터를 처리해야 한다.
  • 페이지 단위로 데이터를 수집하고 pd.concat()으로 합치면 전체 대여소 정보를 DataFrame으로 만들 수 있다.
  • 위도와 경도는 숫자로 변환한 뒤 Folium 지도에 사용한다.
  • Folium의 마커와 팝업을 이용하면 대여소별 위치와 현재 거치 자전거 수를 인터랙티브하게 확인할 수 있다.
  • 실시간 데이터는 시간에 따라 변하므로, 추세 분석을 하려면 여러 시점의 데이터를 저장해 비교해야 한다.
profile
기록하며 성장하는 개발자

0개의 댓글