Py) API(공공) 보행노인 교통사고 다발지역

Py) API(공공) 보행노인 교통사고 다발지역

한국도로교통공단(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의 파라미터 목록

파라미터 목록에서 “시도코드”와 “시군구코드”는 행정표준코드관리시스템에서 법정동 코드를 확인하고 적절한 값을 할당하면 된다.

API 최초 요청

보행노인 사고다발지역은 지역에 따라 연도에 따라 없는 경우도 있기 때문에 그 결과를 잘 보고 정상 응답인데도 결과가 나오지 않아 잘못 응답을 받은 것으로 착각하지 않도록 한다.

예를 들어 부산(26) 중구(110)의 2020년 기준 정보를 호출한 결과는 다음과 같다.
API 호출 결과 예제 1

하지만 부산(26) 중구(110)의 2024년 기준 정보를 호출한 결과는 다음과 같다.
API 호출 결과 예제 2

특정 지역과 년도에 데이터가 있는 경우 “resultCode”에 값이 00으로 되어있고 데이터가 “items”태그 내에 있는 반면, 데이터가 없는 경우 “resultCode”애 값이 “03”이 들어있으며 “resultMsg”에도 “NODATA ERROR”라는 값이 들어있다.

파이썬 코드는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import requests

key = "본인 Decoding Key를 할당!!"
url_base = "http://apis.data.go.kr/B552061/frequentzoneOldman/getRestFrequentzoneOldman"
dic_params = {"ServiceKey": key,
"searchYearCd": 2020,
"siDo": 26,
"guGun": 110,
"type": "xml",
"numOfRows": 10,
"pageNo": 1}

res = requests.get(url_base, params = dic_params)
res.content.decode()
## '<response>\r\n <header>\r\n <resultCode>00</resultCode>\r\n <resultMsg>NORMAL_CODE</resultMsg>\r\n
## </header>\r\n <body>\r\n <items>\r\n <item>\r\n <afos_fid>6680636</afos_fid>\r\n
## <afos_id>2021024</afos_id>\r\n <bjd_cd>2611013000</bjd_cd>\r\n <spot_cd>26110001</spot_cd>\r\n
## <sido_sgg_nm>부산광역시 중구1</sido_sgg_nm>\r\n <spot_nm>부산광역시 중구 신창동4가(대청사거리 부근)</spot_nm>\r\n
## ...

데이터 정제

이제 BeautifulSoup 라이브러리를 활용하여 결과를 조금 정제해보자. 이번에는 서울(11) 중구(140)의 2021년도 데이터를 호출해보았다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import requests
from bs4 import BeautifulSoup as bs

key = "본인 Decoding Key를 할당!!"
url_base = "http://apis.data.go.kr/B552061/frequentzoneOldman/getRestFrequentzoneOldman"
dic_params = {"ServiceKey": key,
"searchYearCd": 2021,
"siDo": 11,
"guGun": 140,
"type": "xml",
"numOfRows": 10,
"pageNo": 1}

res = requests.get(url_base, params = dic_params)
bs_res = bs(res.content)

val_res_total = bs_res.select_one("totalcount").text
val_res_total
## '3'

bs_items = bs_res.select("item")
bs_items[0]
## <item>
## <afos_fid>6780565</afos_fid>
## <afos_id>2022042</afos_id>
## <bjd_cd>1114016200</bjd_cd>
## <spot_cd>11140001</spot_cd>
## <sido_sgg_nm>서울특별시 중구1</sido_sgg_nm>
## <spot_nm>서울특별시 중구 신당동(약수역서울3호선 부근)</spot_nm>
## <occrrnc_cnt>6</occrrnc_cnt>
## <caslt_cnt>6</caslt_cnt>
## <dth_dnv_cnt>0</dth_dnv_cnt>
## <se_dnv_cnt>6</se_dnv_cnt>
## <sl_dnv_cnt>0</sl_dnv_cnt>
## <wnd_dnv_cnt>0</wnd_dnv_cnt>
## <geom_json>{"type":"Polygon","coordinates":[[[127.01175097,37.5535695],[127.01173371,37.55343056],[127.01168259,37.55329696],[127.01159958,37.55317384],[127.01148786,37.55306592],[127.01135173,37.55297735],[127.01119643,37.55291154],[127.01102791,37.55287101],[127.01085266,37.55285733],[127.0106774,37.55287101],[127.01050889,37.55291154],[127.01035358,37.55297735],[127.01021745,37.55306592],[127.01010573,37.55317384],[127.01002272,37.55329696],[127.0099716,37.55343056],[127.00995434,37.5535695],[127.0099716,37.55370844],[127.01002272,37.55384203],[127.01010573,37.55396516],[127.01021745,37.55407308],[127.01035358,37.55416164],[127.01050889,37.55422746],[127.0106774,37.55426798],[127.01085266,37.55428167],[127.01102791,37.55426798],[127.01119643,37.55422746],[127.01135173,37.55416164],[127.01148786,37.55407308],[127.01159958,37.55396516],[127.01168259,37.55384203],[127.01173371,37.55370844],[127.01175097,37.5535695]]]}</geom_json>
## <lo_crd>127.010852655926</lo_crd>
## <la_crd>37.553569498834</la_crd>
## </item>

“totalcount” 태그에서 총 개수가 “3”인 것을 알 수 있었고 그 결과 중 첫번째는 “서울특별시 중구 신당동(약수역서울3호선 부근)”의 데이터인 것을 알 수 있다. 그런데 “geom_json”태그를 보면 텍스트가 JSON 형태로 되어있고 이를 잘 처리하기 위해서는 json 라이브러리가 필요하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
import json

json.loads(bs_items[0].select_one("geom_json").text)
## {'type': 'Polygon',
## 'coordinates': [[[127.01175097, 37.5535695],
## [127.01173371, 37.55343056],
## [127.01168259, 37.55329696],
## [127.01159958, 37.55317384],
## [127.01148786, 37.55306592],
## ...
## [127.01168259, 37.55384203],
## [127.01173371, 37.55370844],
## [127.01175097, 37.5535695]]]}

JSON의 경우 json 라이브러리를 사용하면 텍스트를 딕셔너리 객체로 바꿀 수 있는데 여기서 “coordinates” key에 있는 value를 뽑을 수 있다.
그런데 “coordinates”키에 할당된 value를 보면 3중첩 리스트 객체인 것을 알 수 있는데 이는 2개 이상의 polygon으로 영역이 지정될 수 있음을 시사한다. 하지만 예외적인 상황을 대비하기 위해 작업한 것인지 실제로 2개 이상의 polygon이 존재하는지 확인해보긴 해야 한다. 우선 다음과 같이 JSON 텍스트를 “dic_geom”객체에 저장하도록 하자.

1
2
3
dic_geom = json.loads(bs_items[0].select_one("geom_json").text)
len(dic_geom["coordinates"])
## 1

일단 “coordinates”에 1개의 polygon이 있다는 가정 하에 다음과 같이 코드를 작성하여 해당 좌표를 데이터프레임 객체로 만들 수 있다.

1
2
df_geom = pd.DataFrame(dic_geom["coordinates"][0], columns = ["latitude", "longitude"])
df_geom.head()
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
2
3
df_geom = pd.DataFrame(dic_geom["coordinates"][0], columns = ["latitude", "longitude"])
df_geom["afos_fid"] = bs_items[0].select_one("afos_fid").text
df_geom.head()
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
res = requests.get(url_base, params = dic_params)
bs_res = bs(res.content)

val_res_total = int(bs_res.select_one("totalcount").text)
bs_items = bs_res.select("item")

df_bind = pd.DataFrame()
df_geom_bind = pd.DataFrame()
for n_item in range(val_res_total):
bs_item_single = bs_items[n_item]
dic_geom = json.loads(bs_item_single.select_one("geom_json").text)
df_geom_single = pd.DataFrame(dic_geom["coordinates"][0], columns = ["latitude", "longitude"]) # p1
df_geom_single["afos_fid"] = bs_item_single.select_one("afos_fid").text # p2

df_geom_bind = pd.concat([df_geom_bind, df_geom_single])

bs_item_single.find("geom_json").decompose() # p3

df_single = pd.DataFrame([{e.name: e.text for e in bs_item_single.find_all()}]) # p4
df_bind = pd.concat([df_bind, df_single])

코드 중에서 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
2
3
4
5
6
7
8
9
10
11
12
13
url_base = "http://apis.data.go.kr/B552061/frequentzoneOldman/getRestFrequentzoneOldman"
dic_params = {"ServiceKey": key,
"searchYearCd": 2024,
"siDo": 11,
"guGun": 140,
"type": "xml",
"numOfRows": 10,
"pageNo": 1}

res = requests.get(url_base, params = dic_params)
bs_res = bs(res.content)
bs_res.select_one("resultmsg").text
## 'NODATA_ERROR'

그래서 결과 메세지(resultmsg)를 가져와서 if 조건문으로 확인하는 절차를 추가해야 하겠다. 조건문 추가, 너무 빠른 호출 방지를 위한 time sleep 추가, 년도 변경에 따른 결과 취합 객체 추가를 한 코드는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import time

ls_year = [2020, 2021, 2022, 2023, 2024]

df_bind_total = pd.DataFrame()
df_geom_bind_total = pd.DataFrame()
for n_year in range(len(ls_year)):
url_base = "http://apis.data.go.kr/B552061/frequentzoneOldman/getRestFrequentzoneOldman"
dic_params = {"ServiceKey": key,
"searchYearCd": ls_year[n_year],
"siDo": 11,
"guGun": 140,
"type": "xml",
"numOfRows": 10,
"pageNo": 1}

res = requests.get(url_base, params = dic_params)
bs_res = bs(res.content)
val_res_msg = bs_res.select_one("resultmsg").text

if val_res_msg != "NODATA_ERROR":
val_res_total = int(bs_res.select_one("totalcount").text)
bs_items = bs_res.select("item")

df_bind = pd.DataFrame()
df_geom_bind = pd.DataFrame()
for n_item in range(val_res_total):
bs_item_single = bs_items[n_item]
dic_geom = json.loads(bs_item_single.select_one("geom_json").text)
df_geom_single = pd.DataFrame(dic_geom["coordinates"][0], columns = ["latitude", "longitude"])
df_geom_single["afos_fid"] = bs_item_single.select_one("afos_fid").text

df_geom_bind = pd.concat([df_geom_bind, df_geom_single])

bs_item_single.find("geom_json").decompose()

df_single = pd.DataFrame([{e.name: e.text for e in bs_item_single.find_all()}])
df_bind = pd.concat([df_bind, df_single])

df_bind_total = pd.concat([df_bind_total, df_bind])
df_geom_bind_total = pd.concat([df_geom_bind_total, df_geom_bind])

time.sleep(0.5)

전체 수집 결과가 있는 객체 중 “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
2
len(df_bind_total), len(df_geom_bind_total)
## (13, 429)

상기 두 개의 객체는 시각화 작업을 위해 다음과 같이 저장하는 것을 권장한다.

1
2
df_bind_total.to_csv("API_KOROAD_freqarea_old_list_sample.csv", index = False)
df_geom_bind_total.to_csv("API_KOROAD_freqarea_old_geom_sample.csv", index = False)

지역 변경

예를 들어 서울시의 종로구(11110), 중구(11140), 중랑구(11260), 마포구(11440)의 데이터를 2020년부터 2024년까지 5개년도에 걸쳐 수집하고자 한다. 이를 위해 행정구역명과 코드가 있는 데이터프레임 객체 “df_loc”를 준비해보자.
※ 전국단위로 작업을 하는 경우는 행정표준코드관리시스템에서 법정동 코드파일을 받아 처리하는 것을 권장한다.

1
2
3
4
5
6
df_loc = pd.DataFrame([["서울특별시", "종로구", 11110],
["서울특별시", "중구", 11140],
["서울특별시", "중랑구", 11260],
["서울특별시", "마포구", 11440]],
columns = ["sido", "sgg", "cd"])
df_loc
sido sgg cd
0 서울특별시 종로구 11110
1 서울특별시 중구 11140
2 서울특별시 중랑구 11260
3 서울특별시 마포구 11440

이제 앞의 코드를 모두 조합해서 만든 최종 수집코드는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import json
import pandas as pd

import time
from tqdm.notebook import tqdm

import requests
from bs4 import BeautifulSoup as bs

key = "본인 Decoding Key를 할당!!"
ls_year = [2020, 2021, 2022, 2023, 2024]
df_loc = pd.DataFrame([["서울특별시", "종로구", 11110],
["서울특별시", "중구", 11140],
["서울특별시", "중랑구", 11260],
["서울특별시", "마포구", 11440]],
columns = ["sido", "sgg", "cd"])

df_bind_total = pd.DataFrame()
df_geom_bind_total = pd.DataFrame()
for n_loc in tqdm(range(len(df_loc))):
val_cd = str(df_loc["cd"].iloc[n_loc])
val_sido_cd = val_cd[:2]
val_sgg_cd = val_cd[2:]

for n_year in range(len(ls_year)):
url_base = "http://apis.data.go.kr/B552061/frequentzoneOldman/getRestFrequentzoneOldman"
dic_params = {"ServiceKey": key,
"searchYearCd": ls_year[n_year],
"siDo": val_sido_cd,
"guGun": val_sgg_cd,
"type": "xml",
"numOfRows": 10,
"pageNo": 1}

res = requests.get(url_base, params = dic_params)
bs_res = bs(res.content)
val_res_msg = bs_res.select_one("resultmsg").text

if val_res_msg != "NODATA_ERROR":
val_res_total = int(bs_res.select_one("totalcount").text)
bs_items = bs_res.select("item")

df_bind = pd.DataFrame()
df_geom_bind = pd.DataFrame()
for n_item in range(val_res_total):
bs_item_single = bs_items[n_item]
dic_geom = json.loads(bs_item_single.select_one("geom_json").text)
df_geom_single = pd.DataFrame(dic_geom["coordinates"][0], columns = ["latitude", "longitude"])
df_geom_single["afos_fid"] = bs_item_single.select_one("afos_fid").text

df_geom_bind = pd.concat([df_geom_bind, df_geom_single])

bs_item_single.find("geom_json").decompose()

df_single = pd.DataFrame([{e.name: e.text for e in bs_item_single.find_all()}])
df_single["year"] = ls_year[n_year]
df_single["loc_cd"] = val_cd
df_bind = pd.concat([df_bind, df_single])

df_bind_total = pd.concat([df_bind_total, df_bind])
df_geom_bind_total = pd.concat([df_geom_bind_total, df_geom_bind])

time.sleep(0.5)

수집 결과를 확인하면 다음과 같다.

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
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×