-
chapter-2 서울시 범죄 현황 분석파이썬으로 데이터 주무르기 2021. 10. 21. 20:57
2장 서울시 범죄 현황 분석¶
우리는 1장에서 꽤 기나긴 여정을 거치며 많은 일을 했습니다. 데이터를 다루는 사람들은 항상 객관적으로 데이터를 이용해서 검증 가능한 범위 안에서 판단하게 됩니다. 그래서 데이터를 가지고 가설 혹은 인식을 검증하는 일도 할 수 있게 됩니다. 사실 이런 일들이 쾌감도 잇고 즐겁습니다.
2-2 pandas를 이용하여 데이터 정리하기¶
In [1]:import numpy as np import pandas as pd
먼저 numpy와 pandas는 항상 import하는 모듈이라고 생각하면 됩니다. 이제 다운받은 데이터를 pandas로 읽어보겠습니다. crime_anal_police라는 변수에 저장합니다. 그 내용을 보면 서울시 경찰서별로 살인,강도,강간,절도,폭력이라는 5대 범죄에 대해 발생 건수와 검거 건수를 가지고 있습니다.
In [2]:crime_anal_police = pd.read_csv('data/02. crime_in_Seoul.csv', thousands=',',encoding='euc-kr') crime_anal_police.head()
Out[2]:관서명 살인 발생 살인 검거 강도 발생 강도 검거 강간 발생 강간 검거 절도 발생 절도 검거 폭력 발생 폭력 검거 0 중부서 2 2 3 2 105 65 1395 477 1355 1170 1 종로서 3 3 6 5 115 98 1070 413 1278 1070 2 남대문서 1 0 6 4 65 46 1153 382 869 794 3 서대문서 2 2 5 4 154 124 1812 738 2056 1711 4 혜화서 3 2 5 4 96 63 1114 424 1015 861 그런데 여기서 문제가 하나 생겼습니다. 우리는 강남 3구가 안전한지를 확인하려는 것인데 데이터가 관서별로 되어 있습니다. 서울시에는 한 구에 하나 혹은 두 군데의 결찰서가 위치해 있고, 구 이름과 다른 결찰서도 있습니다. 이 결찰서 목록을 소속 구별로 변경하고 싶습니다. 그러기 위해서는 먼저 경찰서 이름으로 구 정보를 알아야 합니다. 양이 많지 않으니 직접 입력해도 되지만
2-3 지도 정보를 얻을 수 있는 Google Maps¶
In [3]:import googlemaps
In [4]:gmaps_key = "Google Maps APL key" gmaps = googlemaps.Client(key=gmaps_key)
In [5]:gmaps.geocode('서울중부경찰서', language='ko')
Out[5]:[{'address_components': [{'long_name': '27', 'short_name': '27', 'types': ['premise']}, {'long_name': '수표로', 'short_name': '수표로', 'types': ['political', 'sublocality', 'sublocality_level_4']}, {'long_name': '을지로동', 'short_name': '을지로동', 'types': ['political', 'sublocality', 'sublocality_level_2']}, {'long_name': '중구', 'short_name': '중구', 'types': ['political', 'sublocality', 'sublocality_level_1']}, {'long_name': '서울특별시', 'short_name': '서울특별시', 'types': ['administrative_area_level_1', 'political']}, {'long_name': '대한민국', 'short_name': 'KR', 'types': ['country', 'political']}, {'long_name': '100-032', 'short_name': '100-032', 'types': ['postal_code']}], 'formatted_address': '대한민국 서울특별시 중구 을지로동 수표로 27', 'geometry': {'location': {'lat': 37.5636465, 'lng': 126.9895796}, 'location_type': 'ROOFTOP', 'viewport': {'northeast': {'lat': 37.56499548029149, 'lng': 126.9909285802915}, 'southwest': {'lat': 37.56229751970849, 'lng': 126.9882306197085}}}, 'place_id': 'ChIJc-9q5uSifDURLhQmr5wkXmc', 'plus_code': {'compound_code': 'HX7Q+FR 대한민국 서울특별시', 'global_code': '8Q98HX7Q+FR'}, 'types': ['establishment', 'point_of_interest', 'police']}]
Coogle Maps를 사용해서 '서울중부경찰서'라는 단어를 검색해 보겠습니다. 그러면 formatted_address항목에 주소가 나오구요. lng와 lat에서 위도 경도 정보도 확인해 볼 수 있습니다. 나중에 지도 시각화에서 유용하게 사용할 수 있는 정보입니다.
In [6]:station_name = [] for name in crime_anal_police['관서명']: station_name.append('서울' + str(name[:-1]) + '경찰서') station_name
Out[6]:['서울중부경찰서', '서울종로경찰서', '서울남대문경찰서', '서울서대문경찰서', '서울혜화경찰서', '서울용산경찰서', '서울성북경찰서', '서울동대문경찰서', '서울마포경찰서', '서울영등포경찰서', '서울성동경찰서', '서울동작경찰서', '서울광진경찰서', '서울서부경찰서', '서울강북경찰서', '서울금천경찰서', '서울중랑경찰서', '서울강남경찰서', '서울관악경찰서', '서울강서경찰서', '서울강동경찰서', '서울종암경찰서', '서울구로경찰서', '서울서초경찰서', '서울양천경찰서', '서울송파경찰서', '서울노원경찰서', '서울방배경찰서', '서울은평경찰서', '서울도봉경찰서', '서울수서경찰서']
그런데 코드 [2]의 결과를 보면 관서, 즉 경찰서의 이름이 중부서,수서서처럼 되어 있습니다. 그러면 구글 검색에서 주소가 제대로 나오지 않아서 위 코드처럼 서울**결찰서로 만들어야 겠습니다. 코드 [6]을 실행하면 다음과 같이 나옵니다.
이름이 잘 만들어 졌습니다.
In [7]:station_addreess = [] station_lat = [] station_lng = [] for name in station_name: tmp = gmaps.geocode(name, language='ko') station_addreess.append(tmp[0].get("formatted_address")) tmp_loc = tmp[0].get("geometry") station_lat.append(tmp_loc['location']['lat']) station_lng.append(tmp_loc['location']['lng']) print(name + '-->' + tmp[0].get("formatted_address"))
서울중부경찰서-->대한민국 서울특별시 중구 을지로동 수표로 27 서울종로경찰서-->대한민국 서울특별시 종로구 종로1.2.3.4가동 율곡로 46 서울남대문경찰서-->대한민국 서울특별시 중구 회현동 한강대로 410 서울서대문경찰서-->대한민국 서울특별시 서대문구 충현동 통일로 113 서울혜화경찰서-->대한민국 서울특별시 종로구 인의동 창경궁로 112-16 서울용산경찰서-->대한민국 서울특별시 용산구 원효로1가 백범로 329 서울성북경찰서-->대한민국 서울특별시 성북구 삼선동 보문로 170 서울동대문경찰서-->대한민국 서울특별시 동대문구 청량리동 약령시로21길 29 서울마포경찰서-->대한민국 서울특별시 마포구 아현동 마포대로 183 서울영등포경찰서-->대한민국 서울특별시 영등포구 영등포동1가 618-7 서울성동경찰서-->대한민국 서울특별시 성동구 행당동 왕십리광장로 9 서울동작경찰서-->대한민국 서울특별시 동작구 노량진1동 노량진로 148 서울광진경찰서-->대한민국 서울특별시 광진구 구의동 자양로 167 서울서부경찰서-->대한민국 서울특별시 은평구 녹번동 진흥로 58 서울강북경찰서-->대한민국 서울특별시 강북구 번1동 오패산로 406 서울금천경찰서-->대한민국 서울특별시 금천구 시흥1동 시흥대로73길 50 서울중랑경찰서-->대한민국 서울특별시 중랑구 신내동 신내역로3길 40-10 서울강남경찰서-->대한민국 서울특별시 강남구 대치동 테헤란로114길 11 서울관악경찰서-->대한민국 서울특별시 관악구 청룡동 관악로5길 33 서울강서경찰서-->대한민국 서울특별시 양천구 신월동 화곡로 73 서울강동경찰서-->대한민국 서울특별시 강동구 성내1동 성내로 57 서울종암경찰서-->대한민국 서울특별시 성북구 종암동 종암로 135 서울구로경찰서-->대한민국 서울특별시 구로구 구로동 가마산로 235 서울서초경찰서-->대한민국 서울특별시 서초구 서초3동 반포대로 179 서울양천경찰서-->대한민국 서울특별시 양천구 신정6동 목동동로 99 서울송파경찰서-->대한민국 서울특별시 송파구 가락동 중대로 221 서울노원경찰서-->대한민국 서울특별시 노원구 하계동 노원로 283 서울방배경찰서-->대한민국 서울특별시 서초구 방배본동 동작대로 204 서울은평경찰서-->대한민국 서울특별시 은평구 불광동 연서로 365 서울도봉경찰서-->대한민국 서울특별시 도봉구 창4동 노해로 403 서울수서경찰서-->대한민국 서울특별시 강남구 개포동 개포로 617
위 결과를 얻었으니 각 경찰서별 주소를 모두 얻었습니다.
In [8]:station_addreess
Out[8]:['대한민국 서울특별시 중구 을지로동 수표로 27', '대한민국 서울특별시 종로구 종로1.2.3.4가동 율곡로 46', '대한민국 서울특별시 중구 회현동 한강대로 410', '대한민국 서울특별시 서대문구 충현동 통일로 113', '대한민국 서울특별시 종로구 인의동 창경궁로 112-16', '대한민국 서울특별시 용산구 원효로1가 백범로 329', '대한민국 서울특별시 성북구 삼선동 보문로 170', '대한민국 서울특별시 동대문구 청량리동 약령시로21길 29', '대한민국 서울특별시 마포구 아현동 마포대로 183', '대한민국 서울특별시 영등포구 영등포동1가 618-7', '대한민국 서울특별시 성동구 행당동 왕십리광장로 9', '대한민국 서울특별시 동작구 노량진1동 노량진로 148', '대한민국 서울특별시 광진구 구의동 자양로 167', '대한민국 서울특별시 은평구 녹번동 진흥로 58', '대한민국 서울특별시 강북구 번1동 오패산로 406', '대한민국 서울특별시 금천구 시흥1동 시흥대로73길 50', '대한민국 서울특별시 중랑구 신내동 신내역로3길 40-10', '대한민국 서울특별시 강남구 대치동 테헤란로114길 11', '대한민국 서울특별시 관악구 청룡동 관악로5길 33', '대한민국 서울특별시 양천구 신월동 화곡로 73', '대한민국 서울특별시 강동구 성내1동 성내로 57', '대한민국 서울특별시 성북구 종암동 종암로 135', '대한민국 서울특별시 구로구 구로동 가마산로 235', '대한민국 서울특별시 서초구 서초3동 반포대로 179', '대한민국 서울특별시 양천구 신정6동 목동동로 99', '대한민국 서울특별시 송파구 가락동 중대로 221', '대한민국 서울특별시 노원구 하계동 노원로 283', '대한민국 서울특별시 서초구 방배본동 동작대로 204', '대한민국 서울특별시 은평구 불광동 연서로 365', '대한민국 서울특별시 도봉구 창4동 노해로 403', '대한민국 서울특별시 강남구 개포동 개포로 617']
이렇게 전체 주소가 저장되었습니다.
In [9]:station_lat
Out[9]:[37.5636465, 37.575548, 37.5547584, 37.5647439, 37.5718529, 37.5387099, 37.58977830000001, 37.58506149999999, 37.550814, 37.5153176, 37.5617303, 37.5130866, 37.542873, 37.6020914, 37.63730390000001, 37.4568722, 37.6186095, 37.5094352, 37.4743945, 37.5397827, 37.528511, 37.6020592, 37.494931, 37.4956054, 37.5167711, 37.5016941, 37.6425238, 37.4945959, 37.6280204, 37.6533589, 37.49349]
In [10]:station_lng
Out[10]:[126.9895796, 126.9847471, 126.9734981, 126.9667705, 126.9989143, 126.9659183, 127.016589, 127.0457679, 126.954028, 126.905728, 127.0364217, 126.9428498, 127.083821, 126.9213528, 127.0273399, 126.8970429, 127.1045734, 127.0669578, 126.9513489, 126.8299968, 127.1268224, 127.0321577, 126.886731, 127.0052504, 126.8656996, 127.1272481, 127.0717076, 126.9831279, 126.9287899, 127.052682, 127.0772119]
경도를 모두 얻었습니다.
In [11]:gu_name = [] for name in station_addreess: tmp = name.split() tmp_gu = [gu for gu in tmp if gu[-1] == '구'][0] gu_name.append(tmp_gu) crime_anal_police['구별'] = gu_name crime_anal_police.head()
Out[11]:관서명 살인 발생 살인 검거 강도 발생 강도 검거 강간 발생 강간 검거 절도 발생 절도 검거 폭력 발생 폭력 검거 구별 0 중부서 2 2 3 2 105 65 1395 477 1355 1170 중구 1 종로서 3 3 6 5 115 98 1070 413 1278 1070 종로구 2 남대문서 1 0 6 4 65 46 1153 382 869 794 중구 3 서대문서 2 2 5 4 154 124 1812 738 2056 1711 서대문구 4 혜화서 3 2 5 4 96 63 1114 424 1015 861 종로구 이제 코드 [8]에서 저장한 주소를 띄어쓰기, 공백으로 나누고(split) 두 번째 단어(얼핏 보니 구 이름)를 선택해서 구별이라는 컬럼으로 저장합니다. 이렇게 하면 관서명에서 google maps의 도음을 받아 구별 이름으로 저장할 수 있게 되었습니다.
단, 금천경찰서의 경우는 관악구에 위치해 있어서 금천서는 예외 처리를 해야 합니다.In [12]:crime_anal_police[crime_anal_police['관서명']=='금천서']
Out[12]:관서명 살인 발생 살인 검거 강도 발생 강도 검거 강간 발생 강간 검거 절도 발생 절도 검거 폭력 발생 폭력 검거 구별 15 금천서 3 4 6 6 151 122 1567 888 2054 1776 금천구 In [13]:crime_anal_police.loc[crime_anal_police['관서명']=='금천서',['구별']] = '금천구' crime_anal_police[crime_anal_police['관서명']=='금천서']
Out[13]:관서명 살인 발생 살인 검거 강도 발생 강도 검거 강간 발생 강간 검거 절도 발생 절도 검거 폭력 발생 폭력 검거 구별 15 금천서 3 4 6 6 151 122 1567 888 2054 1776 금천구 관악구로 되어 있는 것을 금천구로 변경합니다.
In [14]:crime_anal_police.to_csv('data/02. crime_in_Seoul_include_gu_name.csv', sep=',', encoding='utf-8')
인터넷에서 자료를 가져오는 경우는 이 과정을 단순한 프로그램 오류로 다시 수행할 때 발생하는 정신적 스트레스를 피하기 위해서 적당한 이름으로 저장합니다. 이제 한 가지 고민이 생깁니다.
In [15]:crime_anal_police.head()
Out[15]:관서명 살인 발생 살인 검거 강도 발생 강도 검거 강간 발생 강간 검거 절도 발생 절도 검거 폭력 발생 폭력 검거 구별 0 중부서 2 2 3 2 105 65 1395 477 1355 1170 중구 1 종로서 3 3 6 5 115 98 1070 413 1278 1070 종로구 2 남대문서 1 0 6 4 65 46 1153 382 869 794 중구 3 서대문서 2 2 5 4 154 124 1812 738 2056 1711 서대문구 4 혜화서 3 2 5 4 96 63 1114 424 1015 861 종로구 현재까지 우리가 확보한 데이터의 구조가 관서명을 기초로 했기 때문에 구별 컬럼에서는 같은 구 이름이 두 번 있을 수 있습니다. 예를 들면 강남구에는 경찰서가 두 개이니 코드[15]의 테이블에서는 강남구가 두 번 등장하게 됩니다. 우리는 이 부분을 어떻게 해야 할지 공부해야 합니다.
2-5 pandas의 pivot_table 학습하기¶
여기서 pandas 관련 많은 예제가 있는 한 github을 운영하고 있는 chris1610 님이 공개한 데이터와 예제를 인용하도록 하겠습니다. 주소는 다음과 같습니다.
In [16]:import pandas as pd import numpy as np
In [17]:df = pd.read_excel('data/02. sales-funnel.xlsx') df.head()
Out[17]:Account Name Rep Manager Product Quantity Price Status 0 714466 Trantow-Barrows Craig Booker Debra Henley CPU 1 30000 presented 1 714466 Trantow-Barrows Craig Booker Debra Henley Software 1 10000 presented 2 714466 Trantow-Barrows Craig Booker Debra Henley Maintenance 2 5000 pending 3 737550 Fritsch, Russel and Anderson Craig Booker Debra Henley CPU 1 35000 declined 4 146832 Kiehn-Spinka Daniel Hilton Debra Henley CPU 2 65000 won 위 데이터에서 Name 항목으로만 정렬할 때 pivot_table을 사용합니다. 그러면 코드 [18]처럼 Name컬럼이 index가 되고 특별히 지정하지 않았다면 숫자형 데이터 컬럼들이 남게 됩니다. 그리고 중복된 Name의 항목은 하나로 합쳐지고 value들은 평균을 갖게 됩니다.
In [18]:pd.pivot_table(df,index=["Name"])
Out[18]:Account Price Quantity Name Barton LLC 740150 35000 1.000000 Fritsch, Russel and Anderson 737550 35000 1.000000 Herman LLC 141962 65000 2.000000 Jerde-Hilpert 412290 5000 2.000000 Kassulke, Ondricka and Metz 307599 7000 3.000000 Keeling LLC 688981 100000 5.000000 Kiehn-Spinka 146832 65000 2.000000 Koepp Ltd 729833 35000 2.000000 Kulas Inc 218895 25000 1.500000 Purdy-Kunde 163416 30000 1.000000 Stokes LLC 239344 7500 1.000000 Trantow-Barrows 714466 15000 1.333333 index를 여러 개 지정할 수 있습니다.
In [19]:pd.pivot_table(df,index=["Name","Rep","Manager"])
Out[19]:Account Price Quantity Name Rep Manager Barton LLC John Smith Debra Henley 740150 35000 1.000000 Fritsch, Russel and Anderson Craig Booker Debra Henley 737550 35000 1.000000 Herman LLC Cedric Moss Fred Anderson 141962 65000 2.000000 Jerde-Hilpert John Smith Debra Henley 412290 5000 2.000000 Kassulke, Ondricka and Metz Wendy Yule Fred Anderson 307599 7000 3.000000 Keeling LLC Wendy Yule Fred Anderson 688981 100000 5.000000 Kiehn-Spinka Daniel Hilton Debra Henley 146832 65000 2.000000 Koepp Ltd Wendy Yule Fred Anderson 729833 35000 2.000000 Kulas Inc Daniel Hilton Debra Henley 218895 25000 1.500000 Purdy-Kunde Cedric Moss Fred Anderson 163416 30000 1.000000 Stokes LLC Cedric Moss Fred Anderson 239344 7500 1.000000 Trantow-Barrows Craig Booker Debra Henley 714466 15000 1.333333 특정 value만 지정해서 나타나도록 할 수도 있습니다.
In [20]:pd.pivot_table(df,index=["Manager","Rep"],values=["Price"])
Out[20]:Price Manager Rep Debra Henley Craig Booker 20000.000000 Daniel Hilton 38333.333333 John Smith 20000.000000 Fred Anderson Cedric Moss 27500.000000 Wendy Yule 44250.000000 value를 pivot_table로 합친 경우 평균치가 기본이 됩니다. 여기에 합계를 사용하려면 aggfunc옵션을 사용해서 np.sum을 사용하면 됩니다.
In [21]:pd.pivot_table(df,index=["Manager","Rep"],values=["Price"],aggfunc=np.sum)
Out[21]:Price Manager Rep Debra Henley Craig Booker 80000 Daniel Hilton 115000 John Smith 40000 Fred Anderson Cedric Moss 110000 Wendy Yule 177000 코드 [27]과 같이 원 데이터를 이용해서 아주 그럴싸한 결과를 얻을 수 있습니다. index를 지정하고, values를 지정합니다. 그리고 합산(np.sum)과 평군(np.mean)을 표시하도록 하고, 그 과정에서 빈 칸이 나타나면 NaN으로 두지 말고 fill_value 옵션을 이용해서 0을 채웁니다.
In [22]:pd.pivot_table(df,index=["Manager","Rep","Product"], values=["Price","Quantity"], aggfunc=[np.sum,np.mean],fill_value=0,margins=True)
Out[22]:sum mean Price Quantity Price Quantity Manager Rep Product Debra Henley Craig Booker CPU 65000 2 32500.000000 1.000000 Maintenance 5000 2 5000.000000 2.000000 Software 10000 1 10000.000000 1.000000 Daniel Hilton CPU 105000 4 52500.000000 2.000000 Software 10000 1 10000.000000 1.000000 John Smith CPU 35000 1 35000.000000 1.000000 Maintenance 5000 2 5000.000000 2.000000 Fred Anderson Cedric Moss CPU 95000 3 47500.000000 1.500000 Maintenance 5000 1 5000.000000 1.000000 Software 10000 1 10000.000000 1.000000 Wendy Yule CPU 165000 7 82500.000000 3.500000 Maintenance 7000 3 7000.000000 3.000000 Monitor 5000 2 5000.000000 2.000000 All 522000 30 30705.882353 1.764706 이때의 결과는 다음과 같습니다.
pivot_table 자체는 그리 큰 비중을 차지하지 않지만 어떤 상황에서 내가 원하는 것을 얻기 위해 pivot_table을 떠올리지 못하고 그걸 여러 반목문과 조건문으로 처리하려고 하면 꽤 머리가 아픕니다. 물론 함수 하나를 더 많이 아는 것보다 그것을 구현할 줄 아는 능력이 더더욱 소프트웨어 엔지니어답지만, 그렇다고 모든 것을 다 손으로 직접 구현할 수는 없는 놋릇입니다. 지금 우리는 지금 기초를 학습하는 중이니까요. 아무튼 이제 다시 본론으로 돌아가겠습니다.
2-6 Pivot_table을 이용해서 데이터 정리하기¶
In [23]:crime_anal_raw = pd.read_csv('data/02. crime_in_Seoul_include_gu_name.csv',encoding='utf-8') crime_anal_raw.head()
Out[23]:Unnamed: 0 관서명 살인 발생 살인 검거 강도 발생 강도 검거 강간 발생 강간 검거 절도 발생 절도 검거 폭력 발생 폭력 검거 구별 0 0 중부서 2 2 3 2 105 65 1395 477 1355 1170 중구 1 1 종로서 3 3 6 5 115 98 1070 413 1278 1070 종로구 2 2 남대문서 1 0 6 4 65 46 1153 382 869 794 중구 3 3 서대문서 2 2 5 4 154 124 1812 738 2056 1711 서대문구 4 4 혜화서 3 2 5 4 96 63 1114 424 1015 861 종로구 앞 절에서 배운 pandas의 pivot_table 이용하여 원 데이터를 관서별에서 구별로 바꾸면 다음과 같습니다.
In [24]:crime_anal_raw = pd.read_csv('data/02. crime_in_Seoul_include_gu_name.csv', encoding='utf-8', index_col=0) crime_anal = pd.pivot_table(crime_anal_raw, index='구별',aggfunc=np.sum) crime_anal.head()
Out[24]:강간 검거 강간 발생 강도 검거 강도 발생 살인 검거 살인 발생 절도 검거 절도 발생 폭력 검거 폭력 발생 구별 강남구 349 449 18 21 10 13 1650 3850 3705 4284 강동구 123 156 8 6 3 4 789 2366 2248 2712 강북구 126 153 13 14 8 7 618 1434 2348 2649 관악구 221 320 14 12 8 9 827 2706 2642 3298 광진구 220 240 26 14 4 4 1277 3026 2180 2625 멋지지 않나요? 그냥 이렇게 손쉽게 우리는 원하는 데이터를 얻게 되었습니다.
In [25]:crime_anal['강간검거율'] = crime_anal['강간 검거']/crime_anal['강간 발생']*100 crime_anal['강도검거율'] = crime_anal['강도 검거']/crime_anal['강도 발생']*100 crime_anal['살인검거율'] = crime_anal['살인 검거']/crime_anal['살인 발생']*100 crime_anal['절도검거율'] = crime_anal['절도 검거']/crime_anal['절도 발생']*100 crime_anal['폭력검거율'] = crime_anal['폭력 검거']/crime_anal['폭력 발생']*100 del crime_anal['강간 검거'] del crime_anal['강도 검거'] del crime_anal['살인 검거'] del crime_anal['절도 검거'] del crime_anal['폭력 검거'] crime_anal.head()
Out[25]:강간 발생 강도 발생 살인 발생 절도 발생 폭력 발생 강간검거율 강도검거율 살인검거율 절도검거율 폭력검거율 구별 강남구 449 21 13 3850 4284 77.728285 85.714286 76.923077 42.857143 86.484594 강동구 156 6 4 2366 2712 78.846154 133.333333 75.000000 33.347422 82.890855 강북구 153 14 7 1434 2649 82.352941 92.857143 114.285714 43.096234 88.637222 관악구 320 12 9 2706 3298 69.062500 116.666667 88.888889 30.561715 80.109157 광진구 240 14 4 3026 2625 91.666667 185.714286 100.000000 42.200925 83.047619 추가로 각 범죄별 검거율을 계산하고, 검거 건수는 검거율로 대체할 수 있어서 삭제하기로 합니다. 그리고 pandas의 결과표를 한 화면에 표현하고 싶으니까요
그런데 검거율에 이상한 점이 있습니다. 바로 100이 넘는 숫자들이 보입니다. 아마도 그 전년도 발생 건수에 대한 검거도 포함되니 그런 듯합니다. 우리는 학습이 목적으로 진행하고 있으니 여기서는 깊은 고민 없이 그냥 100이 넘는 숫자는 다 100으로 처리합니다.
In [26]:con_list = ['강간검거율','강도검거율','살인검거율','절도검거율','폭력검거율'] for column in con_list: crime_anal.loc[crime_anal[column] > 100, column] = 100 crime_anal.head()
Out[26]:강간 발생 강도 발생 살인 발생 절도 발생 폭력 발생 강간검거율 강도검거율 살인검거율 절도검거율 폭력검거율 구별 강남구 449 21 13 3850 4284 77.728285 85.714286 76.923077 42.857143 86.484594 강동구 156 6 4 2366 2712 78.846154 100.000000 75.000000 33.347422 82.890855 강북구 153 14 7 1434 2649 82.352941 92.857143 100.000000 43.096234 88.637222 관악구 320 12 9 2706 3298 69.062500 100.000000 88.888889 30.561715 80.109157 광진구 240 14 4 3026 2625 91.666667 100.000000 100.000000 42.200925 83.047619 그러면 다음과 같은 결과를 얻을 수 있습니다.
이제 뒤에 붙은 발생이라는 단어를 삭제하겠습니다.
In [27]:crime_anal.rename(columns = {'강간 발생':'강간', '강도 발생':'강도', '살인 발생':'살인', '절도 발생':'절도', '폭력 발생':'폭력'}, inplace=True) crime_anal.head()
Out[27]:강간 강도 살인 절도 폭력 강간검거율 강도검거율 살인검거율 절도검거율 폭력검거율 구별 강남구 449 21 13 3850 4284 77.728285 85.714286 76.923077 42.857143 86.484594 강동구 156 6 4 2366 2712 78.846154 100.000000 75.000000 33.347422 82.890855 강북구 153 14 7 1434 2649 82.352941 92.857143 100.000000 43.096234 88.637222 관악구 320 12 9 2706 3298 69.062500 100.000000 88.888889 30.561715 80.109157 광진구 240 14 4 3026 2625 91.666667 100.000000 100.000000 42.200925 83.047619 이렇게 컬럼의 이름은 rename으로 병경할 수 있습니다.
잘 되었습니다. 이제 우리는 서울시 구별 5대 범죄의 발생건수와 검거율을 데이터로 갖게 되었습니다.2-7 데이터 표현을 위해 다듬기¶
코드 [32]의 결과를 보면 강도, 살인 사건은 두 자릿수인데, 절도와 폭력은 네 자리수입니다. 물론 숫자 그 자체로도 중요하지만 각각을 비슷한 범위에 놓고 비교하는 것이 편리할 수 있습니다. 개념적으로는 살인 1건과 절도 1천 건이 같은 비중이라고 이야기하는 것이 절대 아닙니다만, 각 항목의 최대값을 1로 두면 추후 범죄 발생 건수를 종합적으로 비교할 때 편리할 것입니다. 그래서 강간, 강도,살인,절도,폭력에 대해 각 컬럼별로 '정규화(normalize)'하도록 하겠습니다.
In [28]:from sklearn import preprocessing col = ['강간', '강도', '살인', '절도', '폭력'] x = crime_anal[col].values min_max_scaler = preprocessing.MinMaxScaler() x_scaled = min_max_scaler.fit_transform(x.astype(float)) crime_anal_norm = pd.DataFrame(x_scaled, columns = col, index = crime_anal.index) col2 = ['강간검거율','강도검거율','살인검거율','절도검거율','폭력검거율'] crime_anal_norm[col2] = crime_anal[col2] crime_anal_norm.head()
Out[28]:강간 강도 살인 절도 폭력 강간검거율 강도검거율 살인검거율 절도검거율 폭력검거율 구별 강남구 1.000000 0.941176 0.916667 0.953472 0.661386 77.728285 85.714286 76.923077 42.857143 86.484594 강동구 0.155620 0.058824 0.166667 0.445775 0.289667 78.846154 100.000000 75.000000 33.347422 82.890855 강북구 0.146974 0.529412 0.416667 0.126924 0.274769 82.352941 92.857143 100.000000 43.096234 88.637222 관악구 0.628242 0.411765 0.583333 0.562094 0.428234 69.062500 100.000000 88.888889 30.561715 80.109157 광진구 0.397695 0.529412 0.166667 0.671570 0.269094 91.666667 100.000000 100.000000 42.200925 83.047619 파이썬의 머신러닝에 관한 모듈로 유명한 scikit learn에 있는 전처리(preprocessing)도구에는 최소값, 최대값을 이용해서 정규화시키는 함수가 있습니다.
이제 다양한 시각화에서 편리해지도록 각 발생 건수는 정규화시켰습니다.
In [29]:result_CCTV = pd.read_csv('data/01. CCTV_result.csv', encoding='UTF-8', index_col='구별') crime_anal_norm[['인구수', 'CCTV']] = result_CCTV[['인구수','소계']] crime_anal_norm.head()
Out[29]:강간 강도 살인 절도 폭력 강간검거율 강도검거율 살인검거율 절도검거율 폭력검거율 인구수 CCTV 구별 강남구 1.000000 0.941176 0.916667 0.953472 0.661386 77.728285 85.714286 76.923077 42.857143 86.484594 570500.0 2780 강동구 0.155620 0.058824 0.166667 0.445775 0.289667 78.846154 100.000000 75.000000 33.347422 82.890855 453233.0 773 강북구 0.146974 0.529412 0.416667 0.126924 0.274769 82.352941 92.857143 100.000000 43.096234 88.637222 330192.0 748 관악구 0.628242 0.411765 0.583333 0.562094 0.428234 69.062500 100.000000 88.888889 30.561715 80.109157 525515.0 1496 광진구 0.397695 0.529412 0.166667 0.671570 0.269094 91.666667 100.000000 100.000000 42.200925 83.047619 372164.0 707 이제 1장에서 학습했던 결과인 01.CCTV_result.csv를 읽어서 그 속에서 구별 인구수와 CCTV개수를 가지고 오겠습니다.
In [30]:col = ['강간','강도','살인','절도','폭력'] crime_anal_norm['범죄'] = np.sum(crime_anal_norm[col], axis=1) crime_anal_norm.head()
Out[30]:강간 강도 살인 절도 폭력 강간검거율 강도검거율 살인검거율 절도검거율 폭력검거율 인구수 CCTV 범죄 구별 강남구 1.000000 0.941176 0.916667 0.953472 0.661386 77.728285 85.714286 76.923077 42.857143 86.484594 570500.0 2780 4.472701 강동구 0.155620 0.058824 0.166667 0.445775 0.289667 78.846154 100.000000 75.000000 33.347422 82.890855 453233.0 773 1.116551 강북구 0.146974 0.529412 0.416667 0.126924 0.274769 82.352941 92.857143 100.000000 43.096234 88.637222 330192.0 748 1.494746 관악구 0.628242 0.411765 0.583333 0.562094 0.428234 69.062500 100.000000 88.888889 30.561715 80.109157 525515.0 1496 2.613667 광진구 0.397695 0.529412 0.166667 0.671570 0.269094 91.666667 100.000000 100.000000 42.200925 83.047619 372164.0 707 2.034438 또한 발생 건수의 합을 '범죄'라는 항목으로 두고 이를 합하겠습니다. 만약 정규화하지 않았다면 몇 천건의 절도에 수십 건의 살인의 비중이 애매했겠지만 정규화를 통해 그 부분은 유리해졌습니다. 단, 여기서는 범죄의 경중을 녹하자는 것이 절대 아닙니다.
In [31]:col = ['강간검거율','강도검거율','살인검거율','절도검거율','폭력검거율'] crime_anal_norm['검거'] = np.sum(crime_anal_norm[col], axis=1) crime_anal_norm
Out[31]:강간 강도 살인 절도 폭력 강간검거율 강도검거율 살인검거율 절도검거율 폭력검거율 인구수 CCTV 범죄 검거 구별 강남구 1.000000 0.941176 0.916667 0.953472 0.661386 77.728285 85.714286 76.923077 42.857143 86.484594 570500.0 2780 4.472701 369.707384 강동구 0.155620 0.058824 0.166667 0.445775 0.289667 78.846154 100.000000 75.000000 33.347422 82.890855 453233.0 773 1.116551 370.084431 강북구 0.146974 0.529412 0.416667 0.126924 0.274769 82.352941 92.857143 100.000000 43.096234 88.637222 330192.0 748 1.494746 406.943540 관악구 0.628242 0.411765 0.583333 0.562094 0.428234 69.062500 100.000000 88.888889 30.561715 80.109157 525515.0 1496 2.613667 368.622261 광진구 0.397695 0.529412 0.166667 0.671570 0.269094 91.666667 100.000000 100.000000 42.200925 83.047619 372164.0 707 2.034438 416.915211 구로구 0.515850 0.588235 0.500000 0.435169 0.359423 58.362989 73.333333 75.000000 38.072805 80.877951 447874.0 1561 2.398678 325.647079 금천구 0.141210 0.058824 0.083333 0.172426 0.134074 80.794702 100.000000 100.000000 56.668794 86.465433 255082.0 1015 0.589867 423.928929 노원구 0.273775 0.117647 0.666667 0.386589 0.292268 61.421320 100.000000 100.000000 36.525308 85.530665 569384.0 1265 1.736946 383.477292 도봉구 0.000000 0.235294 0.083333 0.000000 0.000000 100.000000 100.000000 100.000000 44.967074 87.626093 348646.0 485 0.318627 432.593167 동대문구 0.204611 0.470588 0.250000 0.314061 0.250887 84.393064 100.000000 100.000000 41.090358 87.401884 369496.0 1294 1.490147 412.885306 동작구 0.527378 0.235294 0.250000 0.274376 0.100024 48.771930 55.555556 100.000000 35.442359 83.089005 412520.0 1091 1.387071 322.858850 마포구 0.553314 0.529412 0.500000 0.510434 0.353748 84.013605 71.428571 100.000000 31.819961 84.445189 389649.0 574 2.446908 371.707327 서대문구 0.149856 0.000000 0.000000 0.256244 0.134547 80.519481 80.000000 100.000000 40.728477 83.219844 327163.0 962 0.540647 384.467802 서초구 0.838617 0.235294 0.500000 0.537804 0.215654 63.358779 66.666667 75.000000 41.404175 87.453105 450310.0 1930 2.327368 333.882725 성동구 0.069164 0.235294 0.166667 0.186110 0.029558 94.444444 88.888889 100.000000 37.149969 86.538462 311244.0 1062 0.686793 407.021764 성북구 0.138329 0.000000 0.250000 0.247007 0.170726 82.666667 80.000000 100.000000 41.512605 83.974649 461260.0 1464 0.806061 388.153921 송파구 0.340058 0.470588 0.750000 0.744441 0.427524 80.909091 76.923077 90.909091 34.856437 84.552352 667483.0 618 2.732611 368.150048 양천구 0.806916 0.823529 0.666667 1.000000 1.000000 77.486911 84.210526 100.000000 48.469644 83.065080 479978.0 2034 4.297113 393.232162 영등포구 0.556196 1.000000 1.000000 0.650359 0.493024 62.033898 90.909091 85.714286 32.995951 82.894737 402985.0 904 3.699580 354.547963 용산구 0.265130 0.529412 0.250000 0.169004 0.133128 89.175258 100.000000 100.000000 37.700706 83.121951 244203.0 1624 1.346674 409.997915 은평구 0.184438 0.235294 0.083333 0.291139 0.275715 84.939759 66.666667 100.000000 37.147335 86.920467 494388.0 1873 1.069920 375.674229 종로구 0.314121 0.352941 0.333333 0.383510 0.190589 76.303318 81.818182 83.333333 38.324176 84.212822 162820.0 1002 1.574494 363.991830 중구 0.195965 0.235294 0.083333 0.508040 0.174273 65.294118 66.666667 66.666667 33.712716 88.309353 133240.0 671 1.196905 320.649519 중랑구 0.244957 0.352941 0.916667 0.366746 0.321589 79.144385 81.818182 92.307692 38.829040 84.545135 414503.0 660 2.202900 376.644434 그 결과는 위와 같습니다. 그럼 이결과를 어떻게 하면 효과적으로 인식할 수 있게 시각화할 수 있을까요? 혹시 정렬(sort)하는 것을 상상하셨다면, 아닙니다. 그건 효과적이지 않습니다.
2-7 좀 더 편리한 시각화 도구 - Seaborn¶
Seaborn이라는 뭔가 니모를 연상하게 하는 단어의 시각화 도구가 있습니다. Matplotlib과 함께 사용하는 것인데 정말 괜찮습니다. 이 모듈도 터미널에서 pip install seaborn으로 설치하면 됩니다.
In [32]:import matplotlib.pyplot as plt %matplotlib inline import seaborn as sns x = np.linspace(0, 14, 100) y1 = np.sin(x) y2 = 2*np.sin(x+0.5) y3 = 3*np.sin(x+1.0) y4 = 4*np.sin(x+1.5) plt.figure(figsize=(10,6)) plt.plot(x,y1, x,y2, x,y3, x,y4) plt.show()
간단하게 몇 개의 사인 함수를 그려보겠습니다. seaborn을 import할 때는 matplotlib도 같이 import되어 있어야 합니다.
In [33]:sns.set_style("whitegrid") plt.figure(figsize=(10,6)) plt.plot(x,y1, x,y2, x,y3, x,y4) plt.show()
Seaborn은 whitegrid라는 스타일을 지원합니다.
또한 Seaborn은 연습할 만한 데이터셋을 몇 개 가지고 있습니다.
In [34]:import matplotlib.pyplot as plt import numpy as np import seaborn as sns sns.set_style("whitegrid") %matplotlib inline
In [35]:tips = sns.load_dataset("tips") tips.head(5)
Out[35]:total_bill tip sex smoker day time size 0 16.99 1.01 Female No Sun Dinner 2 1 10.34 1.66 Male No Sun Dinner 3 2 21.01 3.50 Male No Sun Dinner 3 3 23.68 3.31 Male No Sun Dinner 2 4 24.59 3.61 Female No Sun Dinner 4 Tips라는 데이터셋인데요, 요일별 점심, 저녁, 흡연 여부와 식사 금액과 팁을 정이한 데이터입니다.
In [36]:plt.figure(figsize=(8,6)) sns.boxplot(x='day', y='total_bill', data=tips) plt.show()
이렇게 boxplot을 그리는데 x축에는 요일로, y축에는 전체 금액을 그릴 수 있습니다. 이미 이것만으로도 꽤 편리하다는 것을 눈치챘을 것입니다.
In [37]:plt.figure(figsize=(8,6)) sns.boxplot(x="day", y="total_bill", hue="smoker", data=tips, palette="Set3") plt.show()
더 놀라운 것은 hue라는 옵션을 이용해서 구분할 수 있습니다. 코드 [46]의 경우는 흡연 여부로 구분한 것입니다.
In [38]:sns.set_style("darkgrid") sns.lmplot(x="total_bill", y="tip", data=tips, size=7) plt.show()
/home/ubuntu/anaconda3/lib/python3.8/site-packages/seaborn/regression.py:581: UserWarning: The `size` parameter has been renamed to `height`; please update your code. warnings.warn(msg, UserWarning)
이번에는 darkgrid 스타일로 하고 lmplot을 그렸습니다. 데이터를 scatter처럼 그리고 직석으로 regression한 그림도 같이 그려주고 유효범위도 ci로 잡아줍니다.
In [40]:sns.lmplot(x="total_bill", y="tip", hue="smoker", data=tips, palette="Set1", size=7) plt.show()
/home/ubuntu/anaconda3/lib/python3.8/site-packages/seaborn/regression.py:581: UserWarning: The `size` parameter has been renamed to `height`; please update your code. warnings.warn(msg, UserWarning)
또, lmplor도 hue옵션을 가질 수 있으며 미리 준비된 palette로 색상을 지정할 수 있습니다.
In [41]:flights = sns.load_dataset("flights") flights.head(5)
Out[41]:year month passengers 0 1949 Jan 112 1 1949 Feb 118 2 1949 Mar 132 3 1949 Apr 129 4 1949 May 121 이번에는 연도 및 월별 항공기 승객수를 기록한 데이터를 가져오겠습니다.
In [42]:flights = flights.pivot("month", "year", "passengers") flights.head(5)
Out[42]:year 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 month Jan 112 115 145 171 196 204 242 284 315 340 360 417 Feb 118 126 150 180 196 188 233 277 301 318 342 391 Mar 132 141 178 193 236 235 267 317 356 362 406 419 Apr 129 135 163 181 235 227 269 313 348 348 396 461 May 121 125 172 183 229 234 270 318 355 363 420 472 pivot 기능으로 간편하게 월별, 연도별로 구분할 수 있습니다. 앞서 언급했다시피 pivot을 상상할 수만 있다면 꽤 유용한 결과를 얻습니다.
In [43]:plt.figure(figsize=(10,8)) sns.heatmap(flights, annot=True, fmt="d") plt.show()
heatmap이라는 도구를 이용하면 이런 종류의 데이터는 그 경향을 설명하기 참 좋습니다.
In [45]:sns.set(style="ticks") iris = sns.load_dataset("iris") iris.head(10)
Out[45]:sepal_length sepal_width petal_length petal_width species 0 5.1 3.5 1.4 0.2 setosa 1 4.9 3.0 1.4 0.2 setosa 2 4.7 3.2 1.3 0.2 setosa 3 4.6 3.1 1.5 0.2 setosa 4 5.0 3.6 1.4 0.2 setosa 5 5.4 3.9 1.7 0.4 setosa 6 4.6 3.4 1.4 0.3 setosa 7 5.0 3.4 1.5 0.2 setosa 8 4.4 2.9 1.4 0.2 setosa 9 4.9 3.1 1.5 0.1 setosa 이번에는 머신러닝에서 중요하게 다뤄지는 아이리스 꽃에 대한 데이터를 가지고 옵니다. 꽃잎, 꽃받침의 너비와 폭을 가지고 그 종을 그분할 수 있는지를 알아보겠습니다.
In [46]:sns.pairplot(iris, hue="species") plt.show()
여기서는 pairplot이라는 정말 깔끔한 명령이 있습니다.
2-8 범죄 데이터 시각화하기¶
방금 학습한 시각화 도구인 Seaborn을 이용해서 뭔가 성과를 얻어보려고 합니다.
In [48]:import matplotlib.pyplot as plt import seaborn as sns %matplotlib inline
In [49]:import platform from matplotlib import font_manager, rc plt.rcParams['axes.unicode_minus'] = False if platform.system() == 'Darwin': rc('font', family='AppleGothic') print('Mac version') elif platform.system() == 'Windows': path = "c:/Windows/Fonts/malgun.ttf" font_name = font_manager.FontProperties(fname=path).get_name() rc('font', family=font_name) print('Windows version') elif platform.system() == 'Linux': path = "/usr/share/fonts/NanumFont/NanumGothicBold.ttf" font_name = font_manager.FontProperties(fname=path).get_name() plt.rc('font', family=font_name) print('Linux version') else: print('Unknown system... sorry~~~~')
Linux version
일단 그래프에 대한 한글 폰트 문제를 해결합니다.
In [51]:sns.pairplot(crime_anal_norm, vars=["강도", "살인", "폭력"], kind='reg', size=3) plt.show()
/home/ubuntu/anaconda3/lib/python3.8/site-packages/seaborn/axisgrid.py:2076: UserWarning: The `size` parameter has been renamed to `height`; please update your code. warnings.warn(msg, UserWarning)
pairplot으로 강도, 살인, 폭력 간의 상관관계를 그래프로 보겠습니다.
강도와 폭력, 살인과 폭력, 강도와 살인 모두 양의 상관관계가 보입니다.In [53]:sns.pairplot(crime_anal_norm, x_vars=["인구수", "CCTV"], y_vars=["살인","강도"], kind="reg", size=3) plt.show()
/home/ubuntu/anaconda3/lib/python3.8/site-packages/seaborn/axisgrid.py:2076: UserWarning: The `size` parameter has been renamed to `height`; please update your code. warnings.warn(msg, UserWarning)
인구수와 CCTV 개수, 그리고 살인과 강도에 대해 조사했습니다. 전체적인 상관계수는 CCTV와 살인의 관계가 낮을지 몰라도 CCTV가 없을 때 살인이 많이 일어나는 구간이 있습니다. 즉,CCTV개수를 기준으로 좌측면에 살인과 강도의 높은 수를 갖는 데이터가 보입니다.
In [55]:sns.pairplot(crime_anal_norm, x_vars=["인구수","CCTV"], y_vars=["살인검거율", "폭력검거율"], kind="reg", size=3) plt.show()
/home/ubuntu/anaconda3/lib/python3.8/site-packages/seaborn/axisgrid.py:2076: UserWarning: The `size` parameter has been renamed to `height`; please update your code. warnings.warn(msg, UserWarning)
그런데 살인 및 폭력 검거율과 CCTV의 관계가 양의 상관관계가 아닙니다. 오히려 음의 상관계수도 보입니다. 또 인구수와 살인 및 폭력 검거율도 음의 상관과계가 관찰됩니다.
In [56]:tmp_max = crime_anal_norm['검거'].max() crime_anal_norm['검거'] = crime_anal_norm['검거'] / tmp_max * 100 crime_anal_norm_sort = crime_anal_norm.sort_values(by='검거', ascending=False) crime_anal_norm_sort.head()
Out[56]:강간 강도 살인 절도 폭력 강간검거율 강도검거율 살인검거율 절도검거율 폭력검거율 인구수 CCTV 범죄 검거 구별 도봉구 0.000000 0.235294 0.083333 0.000000 0.000000 100.000000 100.0 100.0 44.967074 87.626093 348646.0 485 0.318627 100.000000 금천구 0.141210 0.058824 0.083333 0.172426 0.134074 80.794702 100.0 100.0 56.668794 86.465433 255082.0 1015 0.589867 97.997139 광진구 0.397695 0.529412 0.166667 0.671570 0.269094 91.666667 100.0 100.0 42.200925 83.047619 372164.0 707 2.034438 96.375820 동대문구 0.204611 0.470588 0.250000 0.314061 0.250887 84.393064 100.0 100.0 41.090358 87.401884 369496.0 1294 1.490147 95.444250 용산구 0.265130 0.529412 0.250000 0.169004 0.133128 89.175258 100.0 100.0 37.700706 83.121951 244203.0 1624 1.346674 94.776790 이쯤에서 검거율의 합계인 검거 항복 최고 값을 100으로 한정하고 그 값으로 정렬한 다음, heatmap을 그려보겠습니다.
In [58]:target_col = ['강간검거율','강도검거율','살인검거율','절도검거율','폭력검거율'] crime_anal_norm_sort = crime_anal_norm.sort_values(by='검거', ascending=False) plt.figure(figsize = (10,10)) sns.heatmap(crime_anal_norm_sort[target_col], annot=True, fmt='f', linewidths=.5) plt.title('범죄 검거 비율 (정규화된 검거의 합으로 정렬)') plt.show()
결과를 보면 절도 검거율은 다른 검거율에 비해 낮다는 것을 알 수 있습니다. 그리고 그래프의 하단으로 갈수록 검거율이 낮은데 그 속에 강남3구 중에서'서초구'가 보입니다. 전반적으로 검거율이 우수한 구는'도봉구','광진구','성동구'로보입니다.
In [60]:target_col = ['강간', '강도', '살인', '절도', '폭력', '범죄'] crime_anal_norm['범죄'] = crime_anal_norm['범죄'] / 5 crime_anal_norm_sort = crime_anal_norm.sort_values(by='범죄', ascending=False) plt.figure(figsize = (10,10)) sns.heatmap(crime_anal_norm_sort[target_col], annot=True, fmt='f', linewidths=.5) plt.title('범죄비율 (정규화된 발생 건수로 정렬)') plt.show()
발생 건수로 보니'강남구','양천구','연등포구'가 발생 건수가 높습니다. 그리고'송파구'와'성초구'도 낮다고 볼 수 없습니다. 그렇다면 정말 강남 3구가 안전하다고 할 수 있을지 의문이 생깁니다.
In [61]:crime_anal_norm.to_csv('data/02. crime_in_Seoul_final.csv', sep=',', encoding='utf-8')
일단 여기까지 하고 저장합니다. 이제 또 우리는 신세계로 진입해야 합니다.
2-9 지도 시각화 도구 - folium¶
지도를 가지고 뭔가 원하는 데이터를 표현할 수 있다는 것은 매력적인 일입니다. 특히 지금처럼 강남 3구의 안전성을 이야기하면서 범죄율이나 검거율을 지도에 그릴 수 있다는 것은 더더욱 그렇습니다. 많은 지도 시각화 도구가 있지만 여기서는 Folium라이브러리를 다루도록 하겠습니다. 먼저 터미널을 열고 pip install folium이라고 입력해서 folium을 설치합니다. 그리고 간편하게 folium의 공식 페이지에 있는 튜토리얼을 확인합니다.
In [63]:import folium
In [65]:map_osm = folium.Map(location=[45.5236, -122.6750]) map_osm
Out[65]:Make this Notebook Trusted to load map: File -> Trust Notebook그냥 위도와 경도 정보를 주면 지도를 그려줍니다. 대단하죠. 혹시 저 부분이 하얗게 표시된다면 0장에서 추천한 구글 크롬 브라우저를 사용하기 바랍니다. 마이크로소프트의 익스플로러 브라우저에서는 지도가 파일로 저장은 되는데 브라우저에 저렇게 표현되지는 않습니다.
In [68]:stamen = folium.Map(location=[45.5236, -122.6750], zoom_start=13) stamen
Out[68]:Make this Notebook Trusted to load map: File -> Trust Notebook또 zoom_start라는 옵션으로 확대 비율을 정의할 수도 있습니다.
In [67]:stamen = folium.Map(location=[45.5236, -122.6750], tiles='Stamen Toner', zoom_start=13) stamen
Out[67]:Make this Notebook Trusted to load map: File -> Trust Notebooktiles 옵션으로 저런 모양의 지도도 만들 수 있습니다.
In [71]:map_2 = folium.Map(location=[45.5236, -122.6750], tiles='Stamen Toner', zoom_start=13) folium.Marker([45.5244, -122.6699], popup='The Waterfront' ).add_to(map_2) folium.CircleMarker([45.5215, -122.6261], radius=50, popup='Laurelhurst Park', color='#3186cc', fill_color='#3186cc', ).add_to(map_2) map_2
Out[71]:Make this Notebook Trusted to load map: File -> Trust Notebook이번에는 지도를 그리고 그 상태에서 원하는 좌표(위도, 경도)에 Marker 명령으로 마크를 찍을 수 있습니다. CircleMarker 명령으로 반경(radius)과 색상(color)을 지정하면 원을 그려줍니다.
In [72]:import folium import pandas as pd
이번에는 pandas도 import 합니다. 같은 장 안에서도 같은 모둘을 import하는 이유는 혹시 여기부터 실습을 진행하는 경우에 코드의 문제가 없도록 하려는 것입니다.
In [73]:state_unemployment = 'data/02. folium_US_Unemployment_Oct2012.csv' state_data = pd.read_csv(state_unemployment) state_data.head()
Out[73]:State Unemployment 0 AL 7.1 1 AK 6.8 2 AZ 8.1 3 AR 7.2 4 CA 10.1 - folium_US_Unemployment_Oct2012.csv라는 파일에는 2012년 10월 기준 미국 주별 실업률이 있습니다. 이것을 지도에 시각화하려고 합니다. 역시 02. folium_us=states.json이라는 json파일이 필요합니다.
In [78]:state_geo = 'data/02. folium_us-states.json' map = folium.Map(location=[40, -98], zoom_start=4) map.choropleth(geo_data=state_geo, data=state_data, columns=['State', 'Unemployment'], key_on='feature.id', fill_color='YlGn', legend_name='Unemployment Rate (%)') map
Out[78]:Make this Notebook Trusted to load map: File -> Trust Notebook그래서 state_geo라는 변수에 json파일 경로를 담고 folium에서 choropleth 명령으로 json 파일과 지도에 표현하고 싶은 데이터를 입력하고, key_on 옵션으로 지도의 id를 알려주면 됩니다. 여기서 지도의 id가 서로 중복되지 않아야합니다.
그러면 위 그림처럼 실업률이 colormap으로 표현된 결과를 얻을 수 있습니다.2-10 서울시 범죄율에 대한 지도 시각화¶
이제 지도를 다루는 법도 알았으니 우리가 열심히 다듬은 자료를 시각화해보겠습니다. 물론 언제나 그렇듯이 난관은 있습니다. 서울시 구별 경계선을 그릴 수 있는 json파일이 있어야 합니다. 이건 우리가 직접 만들수 있는 범위를 벗어납니다. 그러나 다행이 조금만 검색을 해보면 그래도 뭔가 답이 있는데 Github에서 e9t라는 아이디로 활동하는 Lucy Park님이 있습니다. 아무튼 http://github.com/southkorea/southkorea-maps에 방문하면 한국 지도에 대해 json파일을 얻을 수 있습니다.
In [79]:import json geo_path = 'data/02. skorea_municipalities_geo_simple.json' geo_str = json.load(open(geo_path, encoding='utf-8'))
먼저 json파일을 로딩합니다.
In [81]:map = folium.Map(location=[37.5502, 126.982], zoom_start=11, tiles='Stamen Toner') map.choropleth(geo_data = geo_str, data = crime_anal_norm['살인'], columns = [crime_anal_norm.index, crime_anal_norm['살인']], fill_color = 'PuRd', # PuRd, YlGnBu key_on = 'feature.id') map
Out[81]:Make this Notebook Trusted to load map: File -> Trust Notebook그리고 서울시의 중심의 위도와 경도 정보를 먼저 입력하고 경계선을 그리는데, 컬러맵은 살인 발생건수로 지정합니다.
그결과를 보면 살인 발생 건수에서 강남 3구가 안전하다고 보기는 어려울 것 같습니다.In [82]:map = folium.Map(location=[37.5502, 126.982], zoom_start=11, tiles='Stamen Toner') map.choropleth(geo_data = geo_str, data = crime_anal_norm['강간'], columns = [crime_anal_norm.index, crime_anal_norm['강간']], fill_color = 'PuRd', #PuRd, YlGnBu key_on = 'feature.id') map
Out[82]:Make this Notebook Trusted to load map: File -> Trust Notebook특수 강간 발생 건수로 다시 그려보면, 더더욱 강남3구가 안전한지 의심이 듭니다.
In [84]:map = folium.Map(location=[37.5502, 126.982], zoom_start=11, tiles='Stamen Toner') map.choropleth(geo_data = geo_str, data = crime_anal_norm['범죄'], columns = [crime_anal_norm.index, crime_anal_norm['범죄']], fill_color = 'PuRd', # PuRd, YlGnBu key_on = 'feature.id') map
Out[84]:Make this Notebook Trusted to load map: File -> Trust Notebook이제 이전에 만들어둔 범죄 발생 건수 전체에 대해 살펴보면, 역시 강남3구와 강서구 주변이 범죄 발생 건수가 높은 것으로 나타나고 있습니다. 그러나 인구수를 고려해야 할 것 같습니다.즉 인구 대비 범죄 발생 비율을 알아보는 것입니다. 그래서 범죄 전체 발생 건수에 인구수를 나누고 소수점 밑으로 가서 적절한 값을 곱하는 것으로 하겠습니다.
In [85]:tmp_criminal = crime_anal_norm['살인'] / crime_anal_norm['인구수'] * 1000000 map = folium.Map(location=[37.5502, 126.982], zoom_start=11, tiles='Stamen Toner') map.choropleth(geo_data = geo_str, data = tmp_criminal, colimns = [crime_anal.index, tmp_criminal], fill_color = 'PuRd', #PuRd, YlGnBu key_on = 'feature.id') map
Out[85]:Make this Notebook Trusted to load map: File -> Trust Notebook인구 대비 범죄 발생 건수로 보면 강남 3구가 1위는 아니지만 안전도 가 제일 높다고 말할 수는 없을것 같습니다. 그런데 중구와 종로구의 범죄율이 엄청 높아졌습니다. 아마 거주 인구는 적고, 관광지여서 그런게 아닐지 추측해봅니다.
2-11 서울시 경찰서별 검거율과 구별 범죄 발생율을 동시에 시각화하기¶
이제 조금 더 진행해서 경찰서별 검거율과 방금 전까지 수행한 범죄 발생율을 동시에 표현하는 게 효과적일 것 같습니다.
In [87]:crime_anal_raw['lat'] = station_lat crime_anal_raw['lng'] = station_lng col = ['살인 검거', '강도 검거', '강간 검거', '절도 검거', '폭력 검거'] tmp = crime_anal_raw[col] / crime_anal_raw[col].max() crime_anal_raw['검거'] = np.sum(tmp, axis=1) crime_anal_raw.head()
Out[87]:관서명 살인 발생 살인 검거 강도 발생 강도 검거 강간 발생 강간 검거 절도 발생 절도 검거 폭력 발생 폭력 검거 구별 lat lng 검거 0 중부서 2 2 3 2 105 65 1395 477 1355 1170 중구 37.563646 126.989580 1.275416 1 종로서 3 3 6 5 115 98 1070 413 1278 1070 종로구 37.575548 126.984747 1.523847 2 남대문서 1 0 6 4 65 46 1153 382 869 794 중구 37.554758 126.973498 0.907372 3 서대문서 2 2 5 4 154 124 1812 738 2056 1711 서대문구 37.564744 126.966770 1.978299 4 혜화서 3 2 5 4 96 63 1114 424 1015 861 종로구 37.571853 126.998914 1.198382 검거만 따로 모아둡니다. 그리고 이미 앞서 수집해둔 각 경찰서의 위도와 경도 정보를 이용하겠습니다.
In [88]:map = folium.Map(location=[37.5502, 126.982], zoom_start=11) for n in crime_anal_raw.index: folium.Marker([crime_anal_raw['lat'][n], crime_anal_raw['lng'][n]]).add_to(map) map
Out[88]:Make this Notebook Trusted to load map: File -> Trust Notebook이렇게 경찰서의 위치만 먼저 확인했습니다.
In [90]:map = folium.Map(location=[37.5502, 126.982], zoom_start=11) for n in crime_anal_raw.index: folium.CircleMarker([crime_anal_raw['lat'][n], crime_anal_raw['lng'][n]], radius = crime_anal_raw['검거'][n]*10, color='#3186cc', fill_color='#3186cc').add_to(map) map
Out[90]:Make this Notebook Trusted to load map: File -> Trust Notebook이제 검거에 정당한 값(10)을 곱해서 원 넓이를 정하고, 경찰서의 검거율을 원의 넓이로 표현하겠습니다.
이러면 이제 각 경찰서의 위치에서 넓은 원을 가지면 검거율이 높다고 보면 됩니다. 마치 경찰서별 범죄에 대한 방어력이 미치는 범위처럼 보이네요. 이제 색상을 붉은 색으로 해서 범죄 발생 건수를 넣으면 될듯합니다.In [92]:map = folium.Map(location=[37.5502, 126.982], zoom_start=11) map.choropleth(geo_data = geo_str, data = crime_anal_norm['범죄'], columns = [crime_anal_norm.index, crime_anal_norm['범죄']], fill_color = 'PuRd', #PuRd, YlGnBu key_on = 'feature.id') for n in crime_anal_raw.index: folium.CircleMarker([crime_anal_raw['lat'][n], crime_anal_raw['lng'][n]], radius = crime_anal_raw['검거'][n]*10, color='#3186cc', fill_color='#3186cc').add_to(map) map
/home/ubuntu/anaconda3/lib/python3.8/site-packages/folium/folium.py:409: FutureWarning: The choropleth method has been deprecated. Instead use the new Choropleth class, which has the same arguments. See the example notebook 'GeoJSON_and_choropleth' for how to do this. warnings.warn(
Out[92]:Make this Notebook Trusted to load map: File -> Trust Notebook이렇게 구현할 수 있습니다. 큰 어려움 없이 두 종류의 코드를 단순히 배치만 시켰습니다.
그 결과입니다. 범죄가 많이 일어날수록 붉은색이고, 그 속에서 방어력이 높을수록 큰 원을 가진 경찰서들이 배치됩니다. 그러면 서울 서부는 범죄는 많이 발생하지만 방어력 또한 높습니다. 서울 강북의 중앙부(중구, 중랑구 등)는 경찰서의 검거율도 높지 않지만, 범죄 발생 건수도 높지 않습니다.
비록 파이썬을 연습하는 과정에서 수행하는 주제입니다만, 우리는 이번 장에서 서울시 강남 3구 주민들이 자신의 구가 안전하다고 느낀다는 기사에 대해 검증했습니다. 이것저것 조사하고 관찰해도 강남 3구가 실제로 안전한지는 의문이 듭니다. 아마 많은 유흥업소들이 밀집해 있으니 범죄 발생율은 높겠지만 거주 지역에서는 발생 건수가 낮을 수도 있을 것 같다는 생각도 듭니다. 그 이유가지 조사하는 건 숙제로 남겨두겠습니다.출처 : " 파이썬으로 데이터 주무르기"
'파이썬으로 데이터 주무르기' 카테고리의 다른 글
chapter-7 시계열 데이터를 다뤄보자 (0) 2021.10.25 chapter-5 우리나라 인구 소멸 위기 지역 분석 (0) 2021.10.25 chapter-3 시카고 샌드위치 맛집 분석 & 네이버 영화 평점 변화,날짜 변화 평점 확인하기 (0) 2021.10.23 chapter-01 서울시 구별 CCTV 현황 분석 (0) 2021.10.21