본문 바로가기
인공지능/데이터분석

서울시 공공자전거 실시간 대여정보

by hyunji00pj 2025. 1. 24.

1. 서울 열린데이터 광장

서울 열린데이터 광장(Seoul Open Data Plaza)은 서울시에서 운영하는 공공데이터 개방 플랫폼입니다. 시민, 연구자, 기업 등이 서울시에서 생성한 다양한 공공데이터를 자유롭게 활용할 수 있도록 제공하고 있습니다. 이를 통해 데이터 기반의 창의적인 아이디어와 혁신을 촉진하며, 시민들의 정보 접근성을 높이고 공공서비스를 개선하는 데 기여하고 있습니다.

 

https://data.seoul.go.kr/

 

열린데이터광장 메인

데이터분류,데이터검색,데이터활용

data.seoul.go.kr

2. 서울시 공공자전거 실시간 대여정보

  1. 인증키를 발급 받습니다.
  2. "서울시 공공자전거 실시간 대여정보" 를 검색합니다.

필요한 모듈 import

import requests
import folium
import json
import pandas as pd
import warnings
warnings.filterwarnings('ignore') #warning 감추기

 

응답 데이터를 JSON형태로 파싱

base_url = 'http://openapi.seoul.go.kr:8088/5465637658746c6139374b63447951/json/bikeList/1/2/'
response = requests.get(base_url)
# print(response)
json_data = response.json()
json_data

 

아래와 같이  데이터를 불러올 경우 값이 없거나 키가 없으면 KeyError가 발생하게된다.

json_data["rentBikeStatus"]["RESULT"]["CODE"]
#값이 없으면 바로 에러남 그래서 try catch 써야함

개선된 접근법: dict.get() 사용

dict.get() 메서드를 사용하면 키가 없을 경우 기본값을 반환하도록 설정할 수 있습니다. 이렇게 하면 코드가 더 간결하고 안전해집니다:

json_data.get('rentBikeStatus',{}).get("RESULT", {}).get("CODE", "")
#값이 없어도 에러 안남

 

서울시 공공자전거 API 데이터 가져오는 함수 설정

import time

def fetch_bike_data():
    base_url = 'http://openapi.seoul.go.kr:8088/5465637658746c6139374b63447951/json/bikeList/'
    start = 1
    end = 1000
    step = 1000
    data_frames = []

    while True:
        url = f'{base_url}{start}/{end}/'
        response = requests.get(url)
        time.sleep(1)

        if response.status_code != 200:
            print(f'Status Code: {response.status_code}')
            break

        json_data = response.json()

        try:
            rent_bike_status = json_data['rentBikeStatus']
            result_code = rent_bike_status['RESULT']['CODE']


        except KeyError:
            print('json 오류')
            break

        if result_code == 'INFO-200':
            print('데이터 없음')
            break
        elif result_code == 'INFO-000':
            print(f'시작{start}, 끝{end}')

            try:
                bike_data = rent_bike_status['row']

                if bike_data:
                    df = pd.DataFrame(bike_data)
                    data_frames.append(df)

            except KeyError:
                print('데이터를 찾을 수 없음')

        else:
            print(f'result_code: {result_code}')
            break

        start += step
        end += step

    if data_frames:
        final_df = pd.concat(data_frames, ignore_index=True)
        return final_df
    else:
        return pd.DataFrame()

코드 구조 및 흐름

기본 설정

    base_url = 'http://openapi.seoul.go.kr:8088/5465637658746c6139374b63447951/json/bikeList/'
    start = 1
    end = 1000
    step = 1000
    data_frames = []

 

  • base_url: 서울시 공공자전거 API 엔드포인트.
  • start: 데이터를 가져오는 시작 인덱스.
  • end: 데이터를 가져오는 종료 인덱스.
  • step: 한 번에 가져올 데이터의 범위(여기서는 1,000개).
  • data_frames: 가져온 데이터를 임시로 저장할 DataFrame 리스트.

데이터를 가져오는 반복문

    while True:
        url = f'{base_url}{start}/{end}/'
        print(f"Fetching data from: {url}")
        response = requests.get(url)
        time.sleep(1)  # 요청 간 간격 유지

 

  • URL 생성: API 호출을 위한 URL을 start와 end로 동적으로 생성.
  • requests.get: API 요청을 보내 데이터 가져오기.
  • time.sleep(1): API 호출 간 1초 대기(서버 과부하 방지 및 Rate Limit 준수).

응답 코드 확인

        if response.status_code != 200:
            print(f"HTTP Error: Status Code {response.status_code}")
            break

HTTP 상태 코드가 200이 아니면 오류를 출력하고 반복문 종료.

 

JSON데이터 파싱 및 오류 처리

        try:
            json_data = response.json()
            rent_bike_status = json_data['rentBikeStatus']
            result_code = rent_bike_status['RESULT']['CODE']
        except KeyError:
            print("JSON Parsing Error: Key not found")
            break

 

  • JSON 데이터 파싱: API 응답 데이터를 JSON 형식으로 변환.
  • rentBikeStatus: 자전거 대여소 관련 데이터를 포함하는 JSON의 최상위 키.
  • KeyError 처리: JSON 구조가 예상과 다를 경우 예외 처리.

결과 코드에 따른 처리

        if result_code == 'INFO-200':
            print("No more data available.")
            break
        elif result_code == 'INFO-000':
            print(f"Fetching data: Start={start}, End={end}")

 

  • INFO-200: 데이터가 더 이상 없는 경우 반복문 종료.
  • INFO-000: 데이터가 정상적으로 있는 경우 처리.

데이터 저장

            try:
                bike_data = rent_bike_status['row']

                if bike_data:
                    df = pd.DataFrame(bike_data)
                    data_frames.append(df)
                else:
                    print("Empty bike data received.")
                    break

 

 

  • row: 대여소 데이터를 포함하는 리스트.
  • pd.DataFrame: 데이터를 pandas.DataFrame으로 변환하여 저장.
  • 빈 데이터 처리: row가 비어 있으면 반복문 종료.

인덱스 증가

        start += step
        end += step

 

다음 페이지의 데이터를 가져오기 위해 start와 end 값을 증가

 

최종 데이터프레임 반환

    if data_frames:
        final_df = pd.concat(data_frames, ignore_index=True)
        return final_df
    else:
        print("No data collected.")
        return pd.DataFrame()

 

  • 여러 페이지에서 가져온 데이터를 하나의 DataFrame으로 결합.
  • 데이터가 없는 경우 빈 DataFrame 반환.

실행 흐름

 

  • API를 호출하여 한 번에 최대 1,000개의 데이터를 가져옵니다.
  • 응답 상태 코드와 결과 코드를 확인하여 데이터가 유효한지 판단합니다.
  • 데이터를 가져와 pandas.DataFrame으로 변환 후 저장합니다.
  • 데이터가 더 이상 없으면 반복문을 종료합니다.
  • 모든 데이터를 합쳐 하나의 DataFrame으로 반환합니다.

실행

bike_data_df =  fetch_bike_data()
bike_data_df

 

  • 코드 결과
  • 시작1, 끝1000
  • 시작1001, 끝2000
  • 시작2001, 끝3000
  • json 오류

 

데이터 프레임 잘 만들어진거 확인했으니까 데이터의 info를 확인해보도록 하자

bike_data_df.info()

info를 확인해 보니 null값은 따로 없다 전부 object타입이다

 

info로 확인해본 콜럼은 다음과 같다

'''
rackTotCnt	거치대개수
parkingBikeTotCnt	자전거주차총건수
shared	거치율
stationLatitude	위도
stationLongitude	경도
stationId	대여소ID
stationName	대여소이름
'''
bike_data_df.columns

 

데이터는의 shape를 확인해 보았다

bike_data_df.shape는 pandas.DataFrame 객체의 행(row)과 열(column)의 개수를 튜플 형태로 반환한다

bike_data_df.shape

#(2711, 7)

총 2711개의 데이터가 있고 콜럼은 7개이다

 

folium map활용하기

folium map를 활용하기 위해 위도와 경도 열의 데이터를 object타입이 아닌 float 타입으로 변환한 뒤 데이터 프레임 정보를 출력하는 과정이다.

#위경도를 folium맵에 찍기 위해서 float 타입으로 바꿔줌
bike_data_df['stationLatitude'] = bike_data_df['stationLatitude'].astype(float) 
bike_data_df['stationLongitude'] = bike_data_df['stationLongitude'].astype(float)
bike_data_df.info()

 

지도 출력하기

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

#iterrows : 데이터프레임의 메서드 Pandas 데이터프레임에서 행을 반복(iterate)하면서 해당 행의 인덱스와 데이터를 반환하는 메서드입니다. 이를 통해 데이터프레임의 각 행에 접근하고, 행별로 작업을 수행할 수 있음
for index, data in bike_data_df.iterrows():
    popup_str = '{} 자전거주차총건수:{}대'.format(
        data['stationName'], data['parkingBikeTotCnt']
    )
    popup = folium.Popup(popup_str, max_width=600)
    folium.Marker(location=[data['stationLatitude'], data['stationLongitude']],
                  popup=popup).add_to(bike_map)

bike_map

결과

  • 지도에는 모든 대여소의 위치가 마커로 표시됩니다.
  • 마커를 클릭하면 팝업 창이 열리며, 대여소 이름과 현재 주차된 자전거 수를 확인할 수 있습니다.

코드설명

 

지도 생성

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

 

  • folium.Map: Folium 지도를 생성하는 메서드입니다.
  • location: 지도 중심 좌표입니다. 여기서는 bike_data_df의 stationLatitude(위도)와 stationLongitude(경도) 평균값을 사용하여 중심 위치를 설정합니다.
  • zoom_start=12: 지도의 초기 확대 수준을 설정합니다. 값이 작을수록 멀리서 보이고, 값이 클수록 확대됩니다.

데이터 프레임 행 반복 (iterrows)

for index, data in bike_data_df.iterrows():

bike_data_df.iterrows(): bike_data_df 데이터프레임의 각 행을 반복합니다.

  • index: 각 행의 인덱스 값.
  • data: 현재 행의 데이터를 포함한 시리즈 객체.

팝업 문자열 생성

popup_str = '{} 자전거주차총건수:{}대'.format(
    data['stationName'], data['parkingBikeTotCnt']
)

 

 

  • data['stationName']: 현재 대여소의 이름.
  • data['parkingBikeTotCnt']: 현재 대여소에 주차된 자전거 수.
  • popup_str: 대여소 이름과 주차된 자전거 수를 포함한 문자열을 생성합니다. 예:
    • 시청역 1번 출구 자전거주차총건수:3대

팝업 생성

popup = folium.Popup(popup_str, max_width=600)

 

folium.Popup: 팝업 창을 생성합니다.

  • popup_str: 팝업에 표시할 내용.
  • max_width=600: 팝업의 최대 너비를 설정합니다.

마커 추가

folium.Marker(location=[data['stationLatitude'], data['stationLongitude']],
              popup=popup).add_to(bike_map)

 

  • folium.Marker: 지도에 마커(위치 표시)를 추가합니다.
    • location: 마커의 위치로, 위도(stationLatitude)와 경도(stationLongitude)를 사용합니다.
    • popup: 마커를 클릭했을 때 나타날 팝업 창.
  • .add_to(bike_map): 생성된 마커를 bike_map에 추가합니다.