[제로베이스][EDA] 서울 범죄 현황 분석

한결·2024년 1월 5일
0
post-thumbnail

이번 글은 서울시에 있는 각 구의 범죄 현황과 인구수 대비 얼마나 치안이 좋은지 데이터 분석을 통해 알아보겠습니다.


데이터 불러오기

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
from matplotlib import rc
rc('font', family='Arial Unicode MS')

crime_raw_data = pd.read_csv('data/02. crime_in_Seoul.csv', thousands=',', encoding = 'euc-kr')
crime_raw_data.head(3)


crime_raw_data['죄종'].unique()
array(['살인', '강도', '강간', '절도', '폭력', nan], dtype=object)

crime_raw_data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 65534 entries, 0 to 65533
Data columns (total 4 columns):
 #   Column  Non-Null Count  Dtype  
---  ------  --------------  -----  
 0   구분      310 non-null    object 
 1   죄종      310 non-null    object 
 2   발생검거    310 non-null    object 
 3   건수      310 non-null    float64
dtypes: float64(1), object(3)
memory usage: 2.0+ MB

# 널값 제거
crime_raw_data = crime_raw_data[crime_raw_data['죄종'].notnull()]
crime_raw_data.info()
<class 'pandas.core.frame.DataFrame'>
Index: 310 entries, 0 to 309
Data columns (total 4 columns):
 #   Column  Non-Null Count  Dtype  
---  ------  --------------  -----  
 0   구분      310 non-null    object 
 1   죄종      310 non-null    object 
 2   발생검거    310 non-null    object 
 3   건수      310 non-null    float64
dtypes: float64(1), object(3)
memory usage: 12.1+ KB

널 값을 제거했더니 데이터의 용량이 매우 줄은 것을 확인 할 수 있습니다.


crime_station = crime_raw_data.pivot_table(
    crime_raw_data, index = ['구분'], columns=['죄종','발생검거'], aggfunc=[np.sum]
)
crime_station.head()


# 필요없는 행 삭제
crime_station.columns = crime_station.columns.droplevel([0,1])

경찰서 위치 정보 얻기

import googlemaps
gmaps_key = 'AIzaSyDaMnoVWZaz1CPPRVFb4vl8wv6Mny2Pt_o'
gmaps = googlemaps.Client(key = gmaps_key)

tmp = gmaps.geocode('서울영등포경찰서',language='ko')
print(tmp[0].get('geometry')['location']['lat'])
print(tmp[0].get('geometry')['location']['lng'])
print(tmp[0].get('formatted_address'))
37.5223245
126.9101692
대한민국 서울특별시 영등포구

# 정보를 얻을 열 미리 생성
crime_station['구별'] = np.nan
crime_station['lat'] = np.nan
crime_station['lng'] = np.nan

# 경찰서 위치 정보 채우기
count = 0
for idx, rows in crime_station.iterrows():
    if str(idx) == '동작':
        station_name = '서울동작구경찰서'
    else:
        station_name = '서울' + str(idx) + '경찰서'
    tmp = gmaps.geocode(station_name, language = 'ko')
    tmp[0].get('formatted_address')
    tmp_gu = tmp[0].get('formatted_address')

    lat = tmp[0].get('geometry')['location']['lat']
    lng = tmp[0].get('geometry')['location']['lng']

    crime_station.loc[idx, 'lat'] = lat
    crime_station.loc[idx, 'lng'] = lng
    if str(idx) == '마포':
        crime_station.loc[idx, '구별'] = '마포구'
    else :
        crime_station.loc[idx, '구별'] = tmp_gu.split()[2]

    count = count + 1

# 강서구 경찰서의 위치가 임시 위치로 표현되어 현재 위치를 찾아서 수기로 수정하겠습니다.
crime_station.loc['강서', '구별'] = '강서구'
crime_station.loc['강서', 'lat'] =37.5511433
crime_station.loc['강서', 'lng'] =126.8497936

정보 취합하기

tmp = [
    crime_station.columns.get_level_values(0)[n] # 첫번째 줄 인덱스
    + crime_station.columns.get_level_values(1)[n] # 두번째 줄 인덱스 합치기
    for n in range(0, len(crime_station.columns.get_level_values(0)))
]
crime_station.columns = tmp
crime_station.head()


crime_anal_gu = pd.pivot_table(crime_station, index = '구별', aggfunc = np.sum)
del crime_anal_gu['lat']
del crime_anal_gu['lng']

# 검거율 열 만들기
target = ['강간검거율','강도검거율','살인검거율','절도검거율','폭력검거율']
num = ['강간검거','강도검거','살인검거','절도검거','폭력검거']
den = ['강간발생','강도발생','살인발생','절도발생','폭력발생']

crime_anal_gu[target] = crime_anal_gu[num].div(crime_anal_gu[den].values) * 100

# 필요없는 열 삭제
del crime_anal_gu['강간검거']
del crime_anal_gu['강도검거']
crime_anal_gu.drop(['살인검거','절도검거','폭력검거'],axis = 1, inplace=True)

# 데이터에서 100을 넘는 값은 100으로 설정
crime_anal_gu[crime_anal_gu[target]>100] =100

검거율이 100프로가 넘는건 발생건수보다 검거수가 더 많아서 그렇습니다. 검거수는 작년이나 제작년에 일어난 범죄들도 해당하기에 발생건수보다 많을 수 있습니다.


crime_anal_gu.rename(
    columns = {'강간발생': '강간','강도발생':'강도','살인발생':'살인','절도발생':'절도','폭력발생':'폭력'},
    inplace = True
)

crime_anal_gu.head()


# 새로운 데이터 프레임 생성
col = ['살인','강도','강간','절도','폭력']
crime_anal_norm = crime_anal_gu[col] /crime_anal_gu[col].max()

col2 = ['강간검거율','강도검거율','살인검거율','절도검거율','폭력검거율']
crime_anal_norm[col2] = crime_anal_gu[col2]
crime_anal_norm.head()

# 각 구별 인구수와 CCTV 개수 불러오기
result_CCTV = pd.read_csv('data/01. CCTV_result.csv', encoding = 'UTF-8', index_col = '구별')

crime_anal_norm[['인구수','CCTV']] = result_CCTV[['인구수','소계']]

col = ['강간','강도','살인','절도','폭력']
crime_anal_norm['범죄'] = np.mean(crime_anal_norm[col],axis = 1)
crime_anal_norm.head()

col = ['강간검거율','강도검거율','살인검거율','절도검거율','폭력검거율']
crime_anal_norm['검거'] = np.mean(crime_anal_norm[col], axis=1)

crime_anal_norm


시각화를 통한 분석

import seaborn as sns
get_ipython().run_line_magic('matplotlib','inline')

sns.pairplot(crime_anal_norm, vars =['강도','살인','폭력'],kind = 'reg', height =3);

폭력과 강도 사이의 기울기가 가파른 것으로 보아 둘의 상관관계가 높은 것을 확인할 수 있습니다.


sns.pairplot(
    crime_anal_norm, x_vars=['인구수','CCTV'],
    y_vars=['살인','강도'],
    kind = 'reg',
    height =4)

plt.show()

회귀선을 매우 벗어난 이상치들 때문에 정확한 파악이 어렵습니다.


sns.pairplot(
    crime_anal_norm, x_vars=['인구수','CCTV'],
    y_vars=['살인검거율','폭력검거율'],
    kind = 'reg',
    height =4)

plt.show()

이것 또한 이상치들 때문에 상관관계 파악이 어렵습니다.


sns.pairplot(
    crime_anal_norm, x_vars=['인구수','CCTV'],
    y_vars=['절도검거율','강도검거율'],
    kind = 'reg',
    height =4)

plt.show()


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,
    cmap='RdPu'
)
plt.title('범죄 검거 비율 (정규화된 검거의 합으로 정렬)')
plt.show()

송파구, 서초구 처럼 치안이 매우 좋을 것 같은 동네도 검거율로만 따졌을 때는 생각보다 높지 않다는 사실을 확인할 수 있습니다.


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,
    cmap='RdPu'
)
plt.title('범죄 비율 (정규화된 발생 건수로 정렬)')
plt.show()

정규화된 발생건수로 따지면 강남구가 제일 높은것을 확인할 수 있습니다.


지도로 시각화 하기

import json
import folium

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

# 구별 살인 발생 시각화
my_map = folium.Map(location=[37.5502, 126.982], zoom_start=11, tiles='cartodbpositron')
my_map.choropleth(
    geo_data = geo_str,
    data = crime_anal_norm['살인'],
    columns=[crime_anal_norm.index, crime_anal_norm['살인']],
    fill_color='PuRd',
    key_on = 'feature.id',
    fill_opacity = 0.7,
    line_opacity = 0.2,
    legend_name = '정규화된 살인 발생 건수'
)

my_map


# 구별 범죄 총 발생 시각화
my_map = folium.Map(location=[37.5502, 126.982], zoom_start=11, tiles='cartodbpositron')
my_map.choropleth(
    geo_data = geo_str,
    data = crime_anal_norm['범죄'],
    columns=[crime_anal_norm.index, crime_anal_norm['범죄']],
    fill_color='PuRd',
    key_on = 'feature.id',
    fill_opacity = 0.7,
    line_opacity = 0.2,
    legend_name = '정규화된 범죄 발생 건수'
)

my_map

발생 건수로만 따지면 인구수가 많은 구가 불리할 수 있습니다. 따라서 인구수 대비 범죄 발생건수를 파악해보겠습니다.


# 구별 인구 대비 범죄 발생 건수
tmp_criminal = crime_anal_norm['범죄'] / crime_anal_norm['인구수']

my_map = folium.Map(location=[37.5502, 126.982], zoom_start = 11, tiles = 'cartodbpositron')
my_map.choropleth(
    geo_data = geo_str,
    data = tmp_criminal,
    columns=[crime_anal_norm.index, tmp_criminal],
    fill_color='PuRd',
    key_on = 'feature.id',
    fill_opacity = 0.7,
    line_opacity = 0.2,
    legend_name = '정규화된 인구대비 범죄 발생 건수'
)

my_map


각 구별 검거 상황을 경찰서 위치를 통해 표현하기

col = ['살인검거','강도검거','강간검거','절도검거','폭력검거']
tmp = crime_station[col]/crime_station[col].max()
crime_station['검거'] = np.mean(tmp, axis = 1)
crime_station.head()


my_map = folium.Map(location=[37.5502, 126.982], zoom_start=11, tiles='cartodbpositron')

# 각 구별 범죄건수 시각화
my_map.choropleth(
    geo_data = geo_str,
    data = crime_anal_norm['범죄'],
    columns=[crime_anal_norm.index, crime_anal_norm['범죄']],
    fill_color='PuRd',
    key_on = 'feature.id',
    fill_opacity = 0.7,
    line_opacity = 0.2,
    legend_name = '정규화된 범죄 발생 건수'
)

# 각 구별 경찰서 위치 표현 및 구석 건수 시각화
for idx, rows in crime_station.iterrows():
    folium.CircleMarker(
        [rows['lat'],rows['lng']],
        radius=rows['검거'] * 50,
        popup=rows['구분'] + ":" + "%.2f" % rows['검거'],
        color = '#3186cc',
        fill = True,
        fill_color = '#3186cc',
    ).add_to(my_map)

my_map


추가 검증

crime_loc_raw = pd.read_csv('data/02. crime_in_Seoul_location.csv', thousands=',', encoding = 'euc-kr')
crime_loc_raw.head()


#범죄명 파악
crime_loc_raw['범죄명'].unique()
array(['살인', '강도', '강간.추행', '절도', '폭력'], dtype=object)

crime_loc_raw['장소'].unique()
array(['아파트, 연립 다세대', '단독주택', '노상', '상점', '숙박업소, 목욕탕', '유흥 접객업소', '사무실',
       '역, 대합실', '교통수단', '유원지 ', '학교', '금융기관', '기타'], dtype=object)

crime_loc = crime_loc_raw.pivot_table(
    crime_loc_raw, index=['장소'], columns=['범죄명'], aggfunc=[np.sum]
)
crime_loc.columns = crime_loc.columns.droplevel([0, 1])
crime_loc.head()


# 정규화
col=['살인','강도','강간','절도','폭력']
crime_loc_norm = crime_loc/ crime_loc.max()

# 종합열 추가
crime_loc_norm['종합'] = np.mean(crime_loc_norm, axis = 1)

crime_loc_norm.head()


crime_loc_norm_sort = crime_loc_norm.sort_values(by='종합', ascending = False)

plt.figure(figsize = (10, 10))
sns.heatmap(crime_loc_norm_sort, annot = True, fmt = 'f', linewidths=0.5, cmap = 'RdPu')
plt.title('범죄와 발생 장소')
plt.show()

  • 성추행,폭행은 주로 교통수단에서 빈번하게 일어나는 것을 확인할 수 있습니다.
  • 강도는 상점에서 주로 일어나는 것을 확인할 수 있습니다.
  • 살인은 단독주택에서 빈번하게 일어나는 것을 확인할 수 있습니다.
  • 폭력은 노상에서 빈번하게 일어나는 것을 확인할 수 있습니다.
profile
낭만젊음사랑

0개의 댓글