본문 바로가기

Theory/DataScience

서울시 범죄현황 통계자료에 대한 확인

데이터 과학이라고 거창하게 부르지 않아도 요즘은 데이터를 이용해서 실제 원하는 결과를 검증하고 이를 블로그에 올리거나, Github page에 올리는 경우를 많이 봅니다. 그 주제가 참 멋지고 그 과정이 아름다운 분들도 많구요^^. 저도 그냥 가벼운 마음에 통계자료를 가지고 살짝 뭔가를 해볼려고 합니다. 뭐 거창한 알고리즘을 쓴 건 아니구요. 그저 그래프나 깨작거리고 그리고, 데이터의 순서나 좀 바꾸던지.. 혹은 조금 만지작 거리는 수준입니다.^^. 

살짝... "서울 강남 3구 체감안전도 높아"라는 위 기사를 보고~~~ 실제 통계자료도 그렇게 나타나는지를 볼려고 했습니다. 사람들이 생각하는 체감안전도와 혹시 통계자료에서 보는 안전도가 같을지 확인해 보는거죠^^

데이터 가져오기

공공데이터포털이라는 사이트에 가보면 아주아주 많은 통계 자료를 얻을 수 있습니다.

거기서 서울시 관서별 5대 범죄 발생 검거 현황이라는 자료가 있습니다. 그걸 받아서 사용해 볼려구요^^

데이터 다듬기 - 뭐 전처리라고 해둘까요^^

import numpy as np
import pandas as pd

import matplotlib.pyplot as plt
import seaborn as sns

import platform

from matplotlib import font_manager, rc
if platform.system() == 'Darwin':
    rc('font', family='AppleGothic')
elif platform.system() == 'Windows':
    font_name = font_manager.FontProperties(fname="c:/Windows/Fonts/malgun.ttf").get_name()
    rc('font', family=font_name)
else:
    print('Unknown system... sorry~~~~') 
    
%matplotlib inline

먼저 들어가기에 앞서~ Jupyter Notebook에서 작성할 거구요... Anaconda 4.1.1에서 테스트되었습니다^^. 그리고 자료형은 기본적으로 제가 연재로 설명한 적이 있는 pandas[바로가기]를 사용합니다. 일단, matplotlib의 한글문제를 해결하기 위해 최근 이야기한 적[바로가기]이 있는 platformimport해서 맥인지 윈도우인지를 확인하고 있습니다. 그리고...

df = pd.read_excel('data/2016서울범죄현황.xlsx', convert_float=True, encoding='euc-kr')
df.head()

아까 공공데이터 포털에서 받은 데이터는 "2016서울범죄현황.xlsx"로 저장했구요. 그 내용은 ...

이렇네요... 5대 범죄에 대해 검거와 발생 건수가 각 경찰서 별로 정리가 되어 있군요^^ 문제는 저는 각 경찰서를 각 구별로 정리를 하고 싶다는 거죠^^ 위키백과의 서울시지방경찰청[바로가기] 항목에는 서울시의 각 경찰청 리스트가 글 하단에 있습니다. 여기서 서울시 경찰청의 소속 구를 확인하구요...

SeoulGu_name = {'서대문서': '서대문구', '수서서': '강남구', '강서서': '강서구', '서초서': '서초구',
                '서부서': '은평구', '중부서': '중구', '종로서': '종로구', '남대문서': '중구',
                '혜화서': '종로구', '용산서': '용산구', '성북서': '성북구', '동대문서': '동대문구',
                '마포서': '마포구', '영등포서': '영등포구', '성동서': '성동구', '동작서': '동작구',
                '광진서': '광진구', '강북서': '강북구', '금천서': '금천구', '중랑서': '중랑구',
                '강남서': '강남구', '관악서': '관악구', '강동서': '강동구', '종암서': '성북구', 
                '구로서': '구로구', '양천서': '양천구', '송파서': '송파구', '노원서': '노원구', 
                '방배서': '서초구', '은평서': '은평구', '도봉서': '도봉구'}

df['구별'] = df['관서명'].apply(lambda v: SeoulGu_name.get(v, v))
df.head()

위에서 처럼 dict 자료형을 이용해서 구별~~이라는 column을 만들었습니다.

이제 위 결과처럼 df['구별']도 만들어 졌네요^^ 이제 '구별'로 구분은 되었지만... 구별 데이터를 쉽게 확인하기 위해서는 경찰서위주로 데이터가 되어 있는것 보다 구별로 되어있는것이 좋겠죠... 즉, 서초구처럼 경찰서가 두 개있는 구도 있으니, 전 구별로 모아서 데이터를 보고 싶다는 거죠^^. 그렇게 해주는 꽤 편리한 명령이 있습니다. .pivot_table[바로가기]입니다.^^

guDF = pd.pivot_table(df, index='구별', aggfunc=np.sum)
guDF = guDF.drop(['계'])
guDF.head()

이렇게 pivot_table을 사용하면 각 구별로 데이터를 쉽게 모아서 볼 수 있습니다. 그 결과는...

입니다.~~^^ 괜찮게 되었죠^^

guDF['강간검거율'] = guDF['강간(검거)']/guDF['강간(발생)']*100
guDF['강도검거율'] = guDF['강도(검거)']/guDF['강도(발생)']*100
guDF['살인검거율'] = guDF['살인(검거)']/guDF['살인(발생)']*100
guDF['절도검거율'] = guDF['절도(검거)']/guDF['절도(발생)']*100
guDF['폭력검거율'] = guDF['폭력(검거)']/guDF['폭력(발생)']*100

del guDF['강간(검거)']
del guDF['강도(검거)']
del guDF['살인(검거)']
del guDF['절도(검거)']
del guDF['폭력(검거)']

guDF.head()

이제 관심있는 데이터를 좀 만들고, 관심없는 데이터는 지우죠...

이렇게 되었습니다. 발생건수와 검거율만 남았네요~~~ 그런데 검거율이 100%가 넘는게 있네요... 아마 발생건수는 2016이고, 그 전해에 발생한 건수에 대한 검거가 2016에 이뤄지면 검거에 그게 반영된 모양입니다. 뭐 여기서는 그냥 100넘는건 100으로 하죠^^

guDF[guDF[['강간검거율', '강도검거율', '살인검거율', '절도검거율', '폭력검거율']] > 100] = 100
guDF.head(10)

넵~

수정되었습니다.^^.

guDF['검거율'] = guDF['소계(검거)']/guDF['소계(발생)']*100
guDF.head()

아차.. 전체 검거율도 만들어야죠~

음.. 가지고 놀만큼 되었네요^^그러나... 그냥 표가 넓어지는게 마음에 안들어서^^

guDF.rename(columns = {'강간(발생)':'강간', 
                       '강도(발생)':'강도', 
                       '살인(발생)':'살인', 
                       '절도(발생)':'절도', 
                       '폭력(발생)':'폭력'}, inplace=True)
del guDF['소계(발생)']
del guDF['소계(검거)']

guDF.head()

Column의 이름을 바꾸도록 하겠습니다.^^

앗.. 표가 확 줄어든게 마음에 드네요 ㅋㅋㅋㅋ. (이상한거에 만족합니다^^)

pop_kor.csv

위 파일을 받아서

popDF = pd.read_csv('data/pop_kor.csv', encoding='UTF-8', index_col='구별')
popDF.head()

보면...

구별 인구수가 있습니다. 어디 다른데서 사용하던건데... 필요해서 가져다 옵니다. 아마 2015년 인구라 지금이랑 맞지는 않지만, 경향정도를 확인하는 걸로 사용하도록 하겠습니다.

guDF = guDF.join(popDF)
guDF.head()

그렇게 받은 두 데이터의 index가 같기 때문에 쉽게 join 명령으로 합치도록 하겠습니다.

잘 합쳐졌죠^^ 

guDF.sort_values(by='검거율', ascending=False, inplace=True)
guDF.head()

일단.. 전체 검거율을 가지고 순위를 한 번 매겨 보겠습니다.

어때요... 종로, 용산, 서초, 등등의 구가 검거율이 높은게 아니네요... 강서구, 금천구, 강북구가.. 검거율이 높습니니다.. 응? 뭐.. 검거율이 체감안전도랑 좀 다르긴 할 수도 있겠죠.. 이제 조금더 가볼까요^^

그래프로 각 구별 현황 확인해보기

target_col = ['강간', '강도', '살인', '절도', '폭력']
weight_col = guDF[target_col].max()

crime_count_norm = guDF[target_col]/weight_col
crime_count_norm.head()

먼저... 5대범죄의 발생 건수만 대상으로 하고... 표현할려는 그래프의 특성을 잘 살리기 위해 각 범죄의 최댓값으로 각 column을 나눠서 정규화시키도록 하겠습니다.

넵... 각 범죄별 경중을 이야기할려는 것이 아니라 종합적인 시각화효과를 위해서 입니다^^

plt.figure(figsize = (10,10))
sns.heatmap(crime_count_norm.sort_values(by='살인', ascending=False), annot=True, fmt='f', linewidths=.5)
plt.title('범죄 발생(살인발생으로 정렬) - 각 항목별 최대값으로 나눠 정규화')
plt.show()

예전에 제가 연재한 적이 있는 seaborn의 heatmap[바로가기]을 사용했습니다.

비록 살인 발생 건수로 정렬했지만, 위로 갈수록 전반적으로 범죄 발생 건수가 높다는 것을 알 수 있습니다. 강남3구는 어디에 있나요^^ 네.. 결코 낮지 않네요.. 강남구는 누가봐도 5대 범죄 전체에 있어서 상위권입니다.

crime_ratio = crime_count_norm.div(guDF['인구수'], axis=0)*100000

plt.figure(figsize = (10,10))
sns.heatmap(crime_ratio.sort_values(by='살인', ascending=False), annot=True, fmt='f', linewidths=.5)
plt.title('범죄 발생(살인발생으로 정렬) - 각 항목을 정규화한 후 인구로 나눔')
plt.show()

이제 단순히 범죄건수만 보지 말고 이를 인구수로 나눠서 인구대비 발생비율로 보겠습니다.

이번에는 중구가 눈에 확~ 보이네요. 애초 기사에서 종로구에 있는 종로서가 체감안전도 1위였는데.. 종로구의 범죄발생 비율은 상위권이네요ㅠㅠ.

crime_ratio['전체발생비율'] = crime_ratio.mean(axis=1)

plt.figure(figsize = (10,10))
sns.heatmap(crime_ratio.sort_values(by='전체발생비율', ascending=False), annot=True, fmt='f', linewidths=.5)
plt.title('범죄 발생(전체발생비율로 정렬) - 각 항목을 정규화한 후 인구로 나눔')
plt.show()

이번에는 전체발생비율로 정렬하고 다시 보죠^^

확실히 알 수 있습니다. 중구, 종로구, 영등포구가 인구대비 발생비율이 높습니다. 강남구와 서초구도 만만치 않네요. 인구대비로 보니 송파는 그래도 하위권이긴 합니다.

지도에 데이터를 표현하기...

지도에 데이터를 표현하는 것도 한 번 해볼까 합니다. 예전에 제가 소개한 적이 있는 Folium[바로가기]을 이용할려구요. 이 부분을 따라가기 전에 필요한 데이터가 하나 있는데....

skorea_municipalities_geo_simple.json

입니다. 출처는 [바로가기]에 있는 한국 지도 데이터 중 서울만 제가 따로 추려낸 것입니다.

import json
import folium
import warnings
warnings.simplefilter(action = "ignore", category = FutureWarning)

geo_path = 'data/skorea_municipalities_geo_simple.json'
geo_str = json.load(open(geo_path, encoding='utf-8'))

일단 위 코드로 지도를 사용하기 위한 준비를 하구요

map = folium.Map(location=[37.5502, 126.982], zoom_start=11, tiles='Stamen Toner')

map.choropleth(geo_str = geo_str,
               data = guDF['살인'],
               columns = [guDF.index, guDF['살인']],
               fill_color = 'PuRd', #PuRd, YlGnBu
               key_on = 'feature.id')
map

심플하게 살인사건의 발생 건수를 표시해보죠

어떤거요... 갑가지 강남3구가 확 들어옵니다. 그리고 영등포구와 중랑구도 눈에 들어오네요... 저런...

map = folium.Map(location=[37.5502, 126.982], zoom_start=11, tiles='Stamen Toner')

map.choropleth(geo_str = geo_str,
               data = crime_ratio['전체발생비율'],
               columns = [crime_ratio.index, crime_ratio['전체발생비율']],
               fill_color = 'PuRd', #PuRd, YlGnBu
               key_on = 'feature.id')
map

인구대비 발생율로 계산한걸 합산한 전체발생비율로 다시 확인해 보겠습니다.

흠.. 종로구와 중구가 높네요... 강남구도 높구요... 아마 종로구와 중구는 관광 집중 지역이니 인구대비 비율이 높게 나타나는 것일 수도 있겠습니다.

map = folium.Map(location=[37.5502, 126.982], zoom_start=11, tiles='Stamen Toner')

map.choropleth(geo_str = geo_str,
               data = guDF['검거율'],
               columns = [guDF.index, guDF['검거율']],
               fill_color = 'YlGnBu', #PuRd, YlGnBu
               key_on = 'feature.id')
map

이번에는 검거율을 보죠^^

흠.. 검거율은 강서구와... 금천구, 강북구가 높네요. 

경찰서의 위치 정보에 데이터를 포함시켜서 지도에 나타내기...

처음 데이터... df라고 저장했던 데이터에는 경찰서별 정보가 남아 있습니다. 여기서... 응? '계'는 없애구요^^

이걸 가지고 경찰서의 검거율을 지도에 같이 표현해보려구요..

station_name = []

for name in df['관서명']:
    station_name.append('서울'+str(name[:-1])+'경찰서')

station_name

일단 제가 사용할 것은 googlemaps입니다. 그럴려면 경찰서의 fullname이 필요하거든요^^

이렇게 말이죠^^...

df['경찰서'] = station_name
df['검거율'] = df['소계(검거)']/df['소계(발생)']*100
df.head()

그리고.. 경찰서별 검거율을 계산해두고...

그런데.. 검거율의 폭이 좀 좁아서 가장 낮은 검거율과 가장 높은 검거율을 가지는 경찰서를 일종의 점수 개념으로 간격을 좀 벌리겠습니다. 지도에 표기할때 눈에 잘 들어나게 할려구요^^

def reRange(x, oldMin, oldMax, newMin, newMax):
    return (x - oldMin)*(newMax - newMin) / (oldMax - oldMin) + newMin

df['점수'] = reRange(df['검거율'], min(df['검거율']), max(df['검거율']), 1, 100)
df.head()

그래서 위와 같이 점수로 표기합니다. 단순히 경찰서의 능력을 검거율로만 볼수 없겠죠... 단지 얻은 데이터를 기준으로 보는 것이니까요...

이렇게 되었네요^^ 이제 sort를 시켜서 결과를 보면~

어~ 강서경찰서, 금천경찰서가 검거율 1, 2위네요... 그 뒤를 강북서, 도봉서, 수서서가 따라가고 있습니다. 이제... [바로가기]에서 소개한 데로 googlemaps를 사용해서 각 경찰서의 위도, 경도 정보를 얻을 겁니다.

import googlemaps
gmaps = googlemaps.Client(key="-- input your key --")

lat = []
lng = []

for name in df['경찰서']:
    tmpMap = gmaps.geocode(name)
    tmpLoc = tmpMap[0].get('geometry')
    lat.append(tmpLoc['location']['lat'])
    lng.append(tmpLoc['location']['lng'])
    
df['lat'] = lat
df['lng'] = lng

df.head()

위 코드의 결과는 각 경찰서의 위도 경도 정보를 얻을 수 있는거죠...

이렇게 말이죠^^...

map = folium.Map(location=[37.5502, 126.982], zoom_start=11)

for n in df.index:
    folium.CircleMarker([df['lat'][n], df['lng'][n]], radius=df['점수'][n]*25, 
                        color='#3186cc', fill_color='#3186cc').add_to(map)
    
map

이제 각 경찰서의 위치에 검거율을 환산한 점수를 원의 넓이로 표기하도록 하겠습니다.

아하.. 어떤가요.. 괜찮죠^^

map = folium.Map(location=[37.5502, 126.982], zoom_start=11)

map.choropleth(geo_str = geo_str,
               data = crime_ratio['전체발생비율'],
               columns = [crime_ratio.index, crime_ratio['전체발생비율']],
               fill_color = 'PuRd', #PuRd, YlGnBu
               key_on = 'feature.id')

for n in df.index:
    folium.CircleMarker([df['lat'][n], df['lng'][n]], radius=df['점수'][n]*25, 
                        color='#3186cc', fill_color='#3186cc').add_to(map)
    
map

마지막으로...

이렇게 아까 했던 범죄발생비율과 경찰서의 검거율을 같이 지도에 그려보았습니다. 어떤가요^^

  • 강남3구의 체감 안전도가 높다는 기사의 내용을 가지고, 실제 통계적으로도 그런 결과가 도출되는지 확인함
  • 강남3구의 5대 범죄 발생 건수는 다른 구와 비교해서 높음
  • 인구대비 발생 비율도 강남3구가 낮지는 않음
  • 단, 강남구와 서초구는 유흥업소 밀집지역에서 범죄 발생이 높을 수 있음

아무튼.. 서울시 5대 범죄 발생건수와 검거율을 가지고 잠시 즐거운 분석 시간을 가져 보았습니다. 요즘.. 제 취미활동이거든요^^

반응형