
【서울시 범죄 현황 데이터 분석】
실습 목표 : 구별 범죄 현황과 경찰서별 검거율을 지도에 시각화
import pandas as pd
crime_raw_data = pd.read_csv(
"../data/02. crime_in_Seoul.csv",
thousands=',',
encoding='euc-kr'
)
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
*/
👉 자료는 310개인데, 인덱스는 6만개가 넘는다.
crime_raw_data["죄종"].unique()
/*실행결과
array(['살인', '강도', '강간', '절도', '폭력', nan], dtype=object)
*/
👉 "죄종" 컬럼에 'nan'값이 들어가 있음을 확인할 수 있다.
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
*/
👉 Index 값도 데이터 수와 동일하고 데이터 크기도 2MB에서 12.1KB로 줄었다.

이 데이터의 형태로는 의미있는 insight를 도출하기 어렵다.
데이터의 재편성이 필요하다.
pivot_table()을 이용해 원하는 DataFrame 형태로 데이터를 재편성한다.
import pandas as pd
import numpy as np
crime_station = crime_raw_data.pivot_table(
index="구분",
columns=['죄종', '발생검거'],
aggfunc=np.sum
)
crime_station.head()
aggfunc param에 ‘합(sum)’을 사용한다.
문제는 pivot_table()의 결과 컬럼이 멀티로 표현된다는 것이다.
필요 없는 “건수” 컬럼을 제거한다.
crime_station.columns = crime_station.columns.droplevel([0])
현재 Index는 경찰서의 이름으로 되어 있다.
우리의 목적은 "구별" 범죄 현황이므로 경찰서 이름을 구 이름으로 변경해야 한다.
googlemaps를 활용해 경찰서의 이름으로 소속된 구 이름을 알아낸 후 구별로 데이터를 정리한다.
crime_station["구별"] = np.nan
crime_station["lat"] = np.nan # latitude, 위도
crime_station["lng"] = np.nan # longitude, 경도

import googlemaps
gmaps = googlemaps.Client(key="google_api_key")
cnt = 0
for idx, row in crime_station.iterrows():
if idx == "동작":
station_name = idx + "경찰서"
else:
station_name = "서울" + idx + "경찰서"
temp = gmaps.geocode(station_name, language="ko")
// 주소 정보 얻어오기
tmp_gu = temp[0].get("formatted_address")
// 위치(위도, 경도) 가져오기
lat = temp[0].get("geometry")["location"]["lat"]
lng = temp[0].get("geometry")["location"]["lng"]
// 각 컬럼에 값 채우기
crime_station.loc[idx, "구별"] = tmp_gu.split()[2]
crime_station.loc[idx, "lat"] = lat
crime_station.loc[idx, "lng"] = lng
print(cnt)
cnt += 1

"죄종"과 "발생검거" 두 줄의 컬럼을 하나로 합친다. -> "강도검거", 강도발생", ..
새로운 컬럼명 리스트를 만들고 데이터에 반영한다.
tmp = [crime_station.columns.get_level_values(0)[n]
+ crime_station.columns.get_level_values(1)[n]
for n in range(len(crime_station.columns.get_level_values(0)))]
/*실행결과
['강간검거', '강간발생', '강도검거', '강도발생', '살인검거', '살인발생',
'절도검거', '절도발생', '폭력검거', '폭력발생', '구별', 'lat', 'lng']
*/
crime_station.columns = tmp

crime_anal_gu = pd.pivot_table(crime_anal_station, index="구별", aggfunc=np.sum)
crime_anal_gu.head()

num = ["강간검거", "강도검거", "살인검거", "절도검거", "폭력검거"]
den = ["강간발생", "강도발생", "살인발생", "절도발생", "폭력발생"]
target = ["강간검거율", "강도검거율", "살인검거율", "절도검거율", "폭력검거율"]
crime_anal_gu[target] = crime_anal_gu[num].div(crime_anal_gu[den].values) * 100
crime_anal_gu[crime_anal_gu[target] > 100] = 100

구별 데이터에서 범죄별 발생 건수를 0과 1 사이 값으로 정규화 한다.
범죄의 경중에 따라 발생 건수의 차이가 크다.
(살인은 한 자리 수 발생, 절도나 폭력은 네 자리 수 발생)
발생 건수를 0과 1 사이로 정규화하여 최대값이 1이 되면 상호 비교가 용이해진다.
col1 = ["살인", "강도", "강간", "절도", "폭력"]
crime_anal_norm = crime_anal_gu[col1] / crime_anal_gu[col1].max()
col2 = ["강간검거율", "강도검거율", "살인검거율", "절도검거율", "폭력검거율"]
crime_anal_norm[col2] = crime_anal_gu[col2]

result_CCTV = pd.read_csv(
"../01_Seoul_CCTV/result_data/04_merge_data_result.csv",
index_col="구별", encoding="utf-8"
)
crime_anal_norm[["인구수", "CCTV"]] = result_CCTV[["인구수", "소계"]]
crime_anal_norm.head()

col = ["살인", "강도", "강간", "절도", "폭력"]
crime_anal_norm["범죄"] = np.mean(crime_anal_norm[col], axis=1)
col = ["강간검거율", "강도검거율", "살인검거율", "절도검거율", "폭력검거율"]
crime_anal_norm["검거"] = np.mean(crime_anal_norm[col], axis=1)
구별 5대 범죄 발행 현황을 지도에 시각화하고 경찰서별 검거율을 경찰서 위치 기준으로 원으로 표시한다.
import pandas as pd
import numpy as np
import folium
import json
crime_anal_norm = pd.read_csv(
"./result_data/06_crime_in_seoul_final.csv", index_col=0, encoding="utf-8"
)
// 행정 구역에 대한 경계선 좌표가 들어 있는 json 파일
geo_path = "../data/02. skorea_municipalities_geo_simple.json"
geo_str = json.load(open(geo_path, encoding="utf-8"))

// 인구수 대비 범죄발생 비율
tmp_criminal = crime_anal_norm["범죄"] / crime_anal_norm["인구수"]
my_map = folium.Map(location=[37.5502, 126.982], zoom_start=11)
folium.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="인구 대비 범죄 발생 건수",
).add_to(my_map)
my_map

crime_anal_station = pd.read_csv(
"./result_data/03_crime_in_seoul_raw_data.csv", encoding="utf-8"
)
col = ["살인검거", "강도검거", "강간검거", "절도검거", "폭력검거"]
// 정규화 0 ~ 1
tmp = crime_anal_station[col] / crime_anal_station[col].max()
// 평균
crime_anal_station["검거"] = np.mean(tmp, axis=1)

my_map = folium.Map(location=[37.5502, 126.982], zoom_start=11)
for idx, row in crime_anal_station.iterrows():
folium.Marker([row["lat"], row["lng"]]).add_to(my_map)
folium.CircleMarker(
[row["lat"], row["lng"]],
radius=row["검거"] * 50,
popup=row["구분"] + " : " + "%.2f" % row["검거"],
color="purple",
fill=True,
fill_color="purple"
).add_to(my_map)
my_map

my_map = folium.Map(location=[37.5502, 126.982]<, zoom_start=11)
folium.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="5대 범죄 발생 비율"
).add_to(my_map)
for idx, row in crime_anal_station.iterrows():
folium.Marker([row["lat"], row["lng"]]).add_to(my_map)
folium.CircleMarker(
location=[row["lat"], row["lng"]],
radius=row["검거"] * 50,
popup=row["구분"] + " : " + "%.2f" % row["검거"],
color="Green",
fill=True,
fill_color="Green"
).add_to(my_map)
my_map
