한국도로교통공단(KOROAD)에서 제공하는 보행노인 교통사고 다발지역 API를 수집하는 방법을 알아본다.
개요
한국도로교통공단(KOROAD)에서는 교통사고 다발지역과 관련해서 보행어린이/보행노인/이륜차/자전거 등 다양한 기준에 대한 정보를 API를 통해 제공하고 있다.
보행노인 교통사고 다발지역의 경우 선정기준은 다음과 같다.
1) 2012~2020년 사고다발지역
- (대상사고) 1년간 발생한 65세이상 노인보행자가 다치거나 사망한 교통사고
- (다발지역선정조건) 반경 200m내, 대상사고 3건 이상 발생지역(사망사고 포함시 2건 이상)
2) 2021년 이후 사고다발지역
- (대상사고) 최근3년간 발생한 65세이상 노인보행자가 사망, 중상 교통사고
- (다발지역선정조건) 반경 100m내, 대상사고 5건 이상 발생지역
API 사용
API를 사용하기 위해서는 해당 API 키(key) 발급, API 호출을 위한 URL 그리고 호출 시 필요한 파라미터를 알아야 한다.
API Key 발급
API를 사용하기 위해서 [✏️활용신청] 버튼을 클릭하여 신청을 해야 한다. 신청 페이지에서 활용 목적을 적당히 기입하고 신청을 하면 해당 API를 사용할 수 있게 된다.
사이트 우상단의 [마이페이지]를 클릭하면 다음과 같은 화면을 볼 수 있다.
API 키는 두 종류(Encoding, Decoding)이 발급되며 상황에 맞게 적절한 키를 사용해야 한다. 그리고 신청하는 API에 관계 없이 여기서는 하나의 API 키를 사용할 수 있다.
마이페이지에서 [API 신청] 메뉴를 눌러보면 확실하게 API를 사용할 수 있게 되었는지 그 목록을 확인할 수 있으며 경우에 따라 신청 즉시 사용할 수 없는 API도 있으니 참고하도록 한다.
요청 URL
이 API의 경우 서비스 URL은 http://apis.data.go.kr/B552061/frequentzoneOldman
으로 되어있는데 요청주소는 http://apis.data.go.kr/B552061/frequentzoneOldman/getRestFrequentzoneOldman
로 되어있다. 그래서 초심자는 혼란이 있을 수 있는데 정확하게는 “요청주소”의 URL을 사용해야 한다.
파라미터
API 문서인 “기술문서_한국도로교통공단_보행노인사고다발지역정보.hwp”를 확인해보면 다음과 같이 요청 메세지 명세를 볼 수 있다.
파라미터 목록에서 “시도코드”와 “시군구코드”는 행정표준코드관리시스템에서 법정동 코드를 확인하고 적절한 값을 할당하면 된다.
API 최초 요청
보행노인 사고다발지역은 지역에 따라 연도에 따라 없는 경우도 있기 때문에 그 결과를 잘 보고 정상 응답인데도 결과가 나오지 않아 잘못 응답을 받은 것으로 착각하지 않도록 한다.
예를 들어 부산(26) 중구(110)의 2020년 기준 정보를 호출한 결과는 다음과 같다.
하지만 부산(26) 중구(110)의 2024년 기준 정보를 호출한 결과는 다음과 같다.
특정 지역과 년도에 데이터가 있는 경우 “resultCode”에 값이 00으로 되어있고 데이터가 “items”태그 내에 있는 반면, 데이터가 없는 경우 “resultCode”애 값이 “03”이 들어있으며 “resultMsg”에도 “NODATA ERROR”라는 값이 들어있다.
파이썬 코드는 다음과 같다.
1 | import requests |
데이터 정제
이제 BeautifulSoup 라이브러리를 활용하여 결과를 조금 정제해보자. 이번에는 서울(11) 중구(140)의 2021년도 데이터를 호출해보았다.
1 | import requests |
“totalcount” 태그에서 총 개수가 “3”인 것을 알 수 있었고 그 결과 중 첫번째는 “서울특별시 중구 신당동(약수역서울3호선 부근)”의 데이터인 것을 알 수 있다. 그런데 “geom_json”태그를 보면 텍스트가 JSON 형태로 되어있고 이를 잘 처리하기 위해서는 json 라이브러리가 필요하다.
1 | import json |
JSON의 경우 json 라이브러리를 사용하면 텍스트를 딕셔너리 객체로 바꿀 수 있는데 여기서 “coordinates” key에 있는 value를 뽑을 수 있다.
그런데 “coordinates”키에 할당된 value를 보면 3중첩 리스트 객체인 것을 알 수 있는데 이는 2개 이상의 polygon으로 영역이 지정될 수 있음을 시사한다. 하지만 예외적인 상황을 대비하기 위해 작업한 것인지 실제로 2개 이상의 polygon이 존재하는지 확인해보긴 해야 한다. 우선 다음과 같이 JSON 텍스트를 “dic_geom”객체에 저장하도록 하자.
1 | dic_geom = json.loads(bs_items[0].select_one("geom_json").text) |
일단 “coordinates”에 1개의 polygon이 있다는 가정 하에 다음과 같이 코드를 작성하여 해당 좌표를 데이터프레임 객체로 만들 수 있다.
1 | df_geom = pd.DataFrame(dic_geom["coordinates"][0], columns = ["latitude", "longitude"]) |
latitude | longitude | |
---|---|---|
0 | 127.011751 | 37.553570 |
1 | 127.011734 | 37.553431 |
2 | 127.011683 | 37.553297 |
3 | 127.011600 | 37.553174 |
4 | 127.011488 | 37.553066 |
일단 데이터프레임으로 만드는 것 까지는 괜찮지만, API로 반환된 결과를 처리하는데 있어 조금 더 고민이 필요하다. 왜냐하면 결과를 정리 했을 때 1개의 행(row)으로 깔끔하게 정리가 되지 않는 형태이기 때문이다. 그래서 지리좌표정보는 별도의 데이터프레임으로 정리하는 것이 좋다. 단, 별도로 정리하더라도 필요 시 어떤 좌표가 어느 영역에 매칭되는지 구분을 할 수 있어야 하기 때문에 “다발지역FID”인 “afos_fid”태그의 텍스트를 따서 붙여주는 것이 좋다. 즉, 다음과 같이 정리할 수 있겠다.
1 | df_geom = pd.DataFrame(dic_geom["coordinates"][0], columns = ["latitude", "longitude"]) |
latitude | longitude | afos_fid | |
---|---|---|---|
0 | 127.011751 | 37.553570 | 6780565 |
1 | 127.011734 | 37.553431 | 6780565 |
2 | 127.011683 | 37.553297 | 6780565 |
3 | 127.011600 | 37.553174 | 6780565 |
4 | 127.011488 | 37.553066 | 6780565 |
그럼 최종적으로 특정 지역과 년도의 API결과가 있을 때, 정리하는 코드는 다음과 같다.
1 | res = requests.get(url_base, params = dic_params) |
코드 중에서 p1
의 경우 앞에서도 살짝 언급했던 polygon 관련이며 여기서는 여러개의 polygon이 있더라도 첫 번째 polygon을 대상으로 정리하는 줄이며, p2
의 경우 지리좌표를 향후 다른 정보와 Join할 수 있도록 key값을 추가하는데 여기서는 “다발지역FID”인 “afos_fid”태그의 값을 신규 변수로 넣어주었다. 그리고 p3
의 경우 p4
에서 dictionary comprehension을 사용했을 때 데이터프레임으로 정제하기 용이하도록 불필요한 “geom_json”태그를 .decompose()
메서드로 제거하는 코드이다.
상기 코드에서 생성한 “df_bind”객체의 경우 다음과 같다.
1 | df_bind.iloc[:, :8] |
afos_fid | afos_id | bjd_cd | spot_cd | sido_sgg_nm | spot_nm | occrrnc_cnt | caslt_cnt | |
---|---|---|---|---|---|---|---|---|
0 | 6780565 | 2022042 | 1114016200 | 11140001 | 서울특별시 중구1 | 서울특별시 중구 신당동(약수역서울3호선 부근) | 6 | 6 |
0 | 6780537 | 2022042 | 1114017400 | 11140002 | 서울특별시 중구2 | 서울특별시 중구 만리동2가(만리시장 부근) | 5 | 5 |
0 | 6780629 | 2022042 | 1114016500 | 11140003 | 서울특별시 중구3 | 서울특별시 중구 황학동(신당중앙시장 부근) | 5 | 5 |
API 사용 실무
예외 처리
API를 사용해서 제대로 데이터를 수집하려면 특정 지역과 연도를 설정하여 API를 호출하였을 때 정보가 없을 경우 예외처리를 할 수 있어야 하겠다.
여기서는 API 자체에서 결과로 데이터가 없다는 메세지를 출력해주는데 다시 확인해보면 다음과 같다.
1 | url_base = "http://apis.data.go.kr/B552061/frequentzoneOldman/getRestFrequentzoneOldman" |
그래서 결과 메세지(resultmsg)를 가져와서 if 조건문으로 확인하는 절차를 추가해야 하겠다. 조건문 추가, 너무 빠른 호출 방지를 위한 time sleep 추가, 년도 변경에 따른 결과 취합 객체 추가를 한 코드는 다음과 같다.
1 | import time |
전체 수집 결과가 있는 객체 중 “df_geom_bind_total” 객체의 일부를 확인해보면 다음과 같다.
1 | df_bind_total.iloc[:6, :7] |
afos_fid | afos_id | bjd_cd | spot_cd | sido_sgg_nm | spot_nm | occrrnc_cnt | |
---|---|---|---|---|---|---|---|
0 | 6679882 | 2021024 | 1114016500 | 11140001 | 서울특별시 중구1 | 서울특별시 중구 황학동(신당5동주민센터 부근) | 6 |
0 | 6679883 | 2021024 | 1114016200 | 11140002 | 서울특별시 중구2 | 서울특별시 중구 신당동(약수역서울3호선 부근) | 5 |
0 | 6679884 | 2021024 | 1114016500 | 11140003 | 서울특별시 중구3 | 서울특별시 중구 황학동(동대문중앙교회앞 부근) | 4 |
0 | 6678207 | 2021024 | 1114016000 | 11140004 | 서울특별시 중구4 | 서울특별시 중구 인현동1가(을지로4가 부근) | 4 |
0 | 6780565 | 2022042 | 1114016200 | 11140001 | 서울특별시 중구1 | 서울특별시 중구 신당동(약수역서울3호선 부근) | 6 |
0 | 6780537 | 2022042 | 1114017400 | 11140002 | 서울특별시 중구2 | 서울특별시 중구 만리동2가(만리시장 부근) | 5 |
그리고 각 객체의 행 개수를 확인해보면 다음과 같다.
1 | len(df_bind_total), len(df_geom_bind_total) |
상기 두 개의 객체는 시각화 작업을 위해 다음과 같이 저장하는 것을 권장한다.
1 | df_bind_total.to_csv("API_KOROAD_freqarea_old_list_sample.csv", index = False) |
지역 변경
예를 들어 서울시의 종로구(11110), 중구(11140), 중랑구(11260), 마포구(11440)의 데이터를 2020년부터 2024년까지 5개년도에 걸쳐 수집하고자 한다. 이를 위해 행정구역명과 코드가 있는 데이터프레임 객체 “df_loc”를 준비해보자.
※ 전국단위로 작업을 하는 경우는 행정표준코드관리시스템에서 법정동 코드파일을 받아 처리하는 것을 권장한다.
1 | df_loc = pd.DataFrame([["서울특별시", "종로구", 11110], |
sido | sgg | cd | |
---|---|---|---|
0 | 서울특별시 | 종로구 | 11110 |
1 | 서울특별시 | 중구 | 11140 |
2 | 서울특별시 | 중랑구 | 11260 |
3 | 서울특별시 | 마포구 | 11440 |
이제 앞의 코드를 모두 조합해서 만든 최종 수집코드는 다음과 같다.
1 | import json |
수집 결과를 확인하면 다음과 같다.
1 | df_bind_total.groupby(["loc_cd", "year"])["afos_fid"].count().reset_index() |
loc_cd | year | afos_fid | |
---|---|---|---|
0 | 11110 | 2020 | 4 |
1 | 11110 | 2021 | 1 |
2 | 11110 | 2022 | 3 |
3 | 11140 | 2020 | 4 |
4 | 11140 | 2021 | 3 |
5 | 11140 | 2022 | 2 |
6 | 11140 | 2023 | 4 |
7 | 11260 | 2020 | 8 |
8 | 11260 | 2021 | 1 |
9 | 11260 | 2022 | 5 |
10 | 11260 | 2023 | 14 |
11 | 11440 | 2020 | 2 |
12 | 11440 | 2021 | 1 |
13 | 11440 | 2022 | 1 |
14 | 11440 | 2023 | 2 |