서울시 범죄 현황

서울시 자치구별 5대 범죄(살인, 강도, 강간, 절도, 폭력) 발생과 검거 현황을 분석해 본다.

목표

  • 압축 파일(zip)을 활용한다.
  • 주소를 검색 API를 이용해서 검색하여 정확한 주소명(도로명, 지번명)과 위도/경도를 알아낸다.

자료 불러오기

구글에서 서울시 관서별 5대 범죄 현황을 검색하여 data.go.kr 사이트에서 zip 파일을 다운받아 data 폴더에 저장한다.

zip 파일로부터 직접 읽어오자.

In [1]: import zipfile
   ...: import re
   ...: import pandas as pd
   ...: import numpy as np
   ...: pd.set_option('max_rows', 10)
   ...: 
   ...: zipref = zipfile.ZipFile('data/관서별_5대범죄_발생_및_검거_현황_2000_2016_.zip', 'r')
   ...: 

파일 인코딩이 cp949이므로 옵션을 설정해야 하고 한글 파일 이름이 깨져서 보이지 않으므로 숫자만 이름으로 선택한다. 사전형으로 저장을 하자.

In [7]: dfs = {re.sub(r'(\d{4}).*', r'\1', fname): pd.read_csv(zipref.open(fname), encoding='cp949') for fname in zipref.namelist() if fname.endswith('.csv')}
   ...: zipref.close()
   ...: dfs
   ...: 
Out[9]: 
{'2000':      구분  죄종 발생검거    건수  Unnamed: 4  Unnamed: 5  Unnamed: 6     ...       Unnamed: 27  Unnamed: 28  Unnamed: 29  Unnamed: 30  Unnamed: 31  Unnamed: 32  Unnamed: 33
 0    중부  살인   발생     1         NaN         NaN         NaN     ...               NaN          NaN          NaN          NaN          NaN          NaN          NaN
 1    중부  살인   검거     1         NaN         NaN         NaN     ...               NaN          NaN          NaN          NaN          NaN          NaN          NaN
 2    중부  강도   발생    17         NaN         NaN         NaN     ...               NaN          NaN          NaN          NaN          NaN          NaN          NaN
 3    중부  강도   검거    15         NaN         NaN         NaN     ...               NaN          NaN          NaN          NaN          NaN          NaN          NaN
 4    중부  강간   발생    14         NaN         NaN         NaN     ...               NaN          NaN          NaN          NaN          NaN          NaN          NaN
 ..   ..  ..  ...   ...         ...         ...         ...     ...               ...          ...          ...          ...          ...          ...          ...
 305  수서  강간   검거    29         NaN         NaN         NaN     ...               NaN          NaN          NaN          NaN          NaN          NaN          NaN
 306  수서  절도   발생  1183         NaN         NaN         NaN     ...               NaN          NaN          NaN          NaN          NaN          NaN          NaN
 307  수서  절도   검거   352         NaN         NaN         NaN     ...               NaN          NaN          NaN          NaN          NaN          NaN          NaN
 308  수서  폭력   발생  3351         NaN         NaN         NaN     ...               NaN          NaN          NaN          NaN          NaN          NaN          NaN
 309  수서  폭력   검거  2983         NaN         NaN         NaN     ...               NaN          NaN          NaN          NaN          NaN          NaN          NaN
 
 [310 rows x 34 columns], '2001':      구분  죄종 발생검거    건수
 0    중부  살인   발생     0
 1    중부  살인   검거     0
 2    중부  강도   발생    17
 3    중부  강도   검거     6
 4    중부  강간   발생    80
 ..   ..  ..  ...   ...
 305  수서  강간   검거    43
 306  수서  절도   발생  1665
 307  수서  절도   검거   632
 308  수서  폭력   발생  3164
 309  수서  폭력   검거  2629
 
 [310 rows x 4 columns], '2002':         구분   죄종 발생검거    건수
 0       중부   살인   발생   1.0
 1       중부   살인   검거   2.0
 2       중부   강도   발생  21.0
 3       중부   강도   검거  18.0
 4       중부   강간   발생  59.0
 ...    ...  ...  ...   ...
 65529  NaN  NaN  NaN   NaN
 65530  NaN  NaN  NaN   NaN
 65531  NaN  NaN  NaN   NaN
 65532  NaN  NaN  NaN   NaN
 65533  NaN  NaN  NaN   NaN
 
 [65534 rows x 4 columns], '2003':      구분  죄종 발생검거    건수
 0    중부  살인   발생     0
 1    중부  살인   검거     0
 2    중부  강도   발생    46
 3    중부  강도   검거    50
 4    중부  강간   발생    70
 ..   ..  ..  ...   ...
 305  수서  강간   검거    30
 306  수서  절도   발생  1694
 307  수서  절도   검거  1000
 308  수서  폭력   발생  2693
 309  수서  폭력   검거  2431
 
 [310 rows x 4 columns], '2004':         구분   죄종 발생검거     건수
 0       중부   살인   발생    2.0
 1       중부   살인   검거    2.0
 2       중부   강도   발생   13.0
 3       중부   강도   검거   17.0
 4       중부   강간   발생  128.0
 ...    ...  ...  ...    ...
 65529  NaN  NaN  NaN    NaN
 65530  NaN  NaN  NaN    NaN
 65531  NaN  NaN  NaN    NaN
 65532  NaN  NaN  NaN    NaN
 65533  NaN  NaN  NaN    NaN
 
 [65534 rows x 4 columns], '2005':         구분   죄종 발생검거    건수
 0       중부   살인   발생   2.0
 1       중부   살인   검거   3.0
 2       중부   강도   발생  10.0
 3       중부   강도   검거   8.0
 4       중부   강간   발생  89.0
 ...    ...  ...  ...   ...
 65529  NaN  NaN  NaN   NaN
 65530  NaN  NaN  NaN   NaN
 65531  NaN  NaN  NaN   NaN
 65532  NaN  NaN  NaN   NaN
 65533  NaN  NaN  NaN   NaN
 
 [65534 rows x 4 columns], '2006':         구분   죄종 발생검거     건수
 0       중부   살인   발생    4.0
 1       중부   살인   검거    4.0
 2       중부   강도   발생   28.0
 3       중부   강도   검거   18.0
 4       중부   강간   발생  121.0
 ...    ...  ...  ...    ...
 65529  NaN  NaN  NaN    NaN
 65530  NaN  NaN  NaN    NaN
 65531  NaN  NaN  NaN    NaN
 65532  NaN  NaN  NaN    NaN
 65533  NaN  NaN  NaN    NaN
 
 [65534 rows x 4 columns], '2007':         구분   죄종 발생검거    건수
 0       중부   살인   발생   4.0
 1       중부   살인   검거   4.0
 2       중부   강도   발생  13.0
 3       중부   강도   검거  12.0
 4       중부   강간   발생  83.0
 ...    ...  ...  ...   ...
 65529  NaN  NaN  NaN   NaN
 65530  NaN  NaN  NaN   NaN
 65531  NaN  NaN  NaN   NaN
 65532  NaN  NaN  NaN   NaN
 65533  NaN  NaN  NaN   NaN
 
 [65534 rows x 4 columns], '2008':         구분   죄종 발생검거    건수
 0       중부   살인   발생   4.0
 1       중부   살인   검거   4.0
 2       중부   강도   발생  20.0
 3       중부   강도   검거  26.0
 4       중부   강간   발생  76.0
 ...    ...  ...  ...   ...
 65529  NaN  NaN  NaN   NaN
 65530  NaN  NaN  NaN   NaN
 65531  NaN  NaN  NaN   NaN
 65532  NaN  NaN  NaN   NaN
 65533  NaN  NaN  NaN   NaN
 
 [65534 rows x 4 columns], '2009':         구분   죄종 발생검거    건수
 0       중부   살인   발생   1.0
 1       중부   살인   검거   1.0
 2       중부   강도   발생  28.0
 3       중부   강도   검거  24.0
 4       중부   강간   발생  94.0
 ...    ...  ...  ...   ...
 65529  NaN  NaN  NaN   NaN
 65530  NaN  NaN  NaN   NaN
 65531  NaN  NaN  NaN   NaN
 65532  NaN  NaN  NaN   NaN
 65533  NaN  NaN  NaN   NaN
 
 [65534 rows x 4 columns], '2010':         구분   죄종 발생검거     건수
 0       중부   살인   발생    5.0
 1       중부   살인   검거    5.0
 2       중부   강도   발생   16.0
 3       중부   강도   검거   11.0
 4       중부   강간   발생  117.0
 ...    ...  ...  ...    ...
 65529  NaN  NaN  NaN    NaN
 65530  NaN  NaN  NaN    NaN
 65531  NaN  NaN  NaN    NaN
 65532  NaN  NaN  NaN    NaN
 65533  NaN  NaN  NaN    NaN
 
 [65534 rows x 4 columns], '2011':         구분   죄종 발생검거    검거
 0       중부   살인   발생   2.0
 1       중부   살인   검거   3.0
 2       중부   강도   발생  14.0
 3       중부   강도   검거  19.0
 4       중부   강간   발생  89.0
 ...    ...  ...  ...   ...
 65529  NaN  NaN  NaN   NaN
 65530  NaN  NaN  NaN   NaN
 65531  NaN  NaN  NaN   NaN
 65532  NaN  NaN  NaN   NaN
 65533  NaN  NaN  NaN   NaN
 
 [65534 rows x 4 columns], '2012':         구분   죄종 발생검거     검거
 0       중부   살인   발생    4.0
 1       중부   살인   검거    5.0
 2       중부   강도   발생    9.0
 3       중부   강도   검거    8.0
 4       중부   강간   발생  100.0
 ...    ...  ...  ...    ...
 65529  NaN  NaN  NaN    NaN
 65530  NaN  NaN  NaN    NaN
 65531  NaN  NaN  NaN    NaN
 65532  NaN  NaN  NaN    NaN
 65533  NaN  NaN  NaN    NaN
 
 [65534 rows x 4 columns], '2013':         구분   죄종 발생검거     건수
 0       중부   살인   발생    0.0
 1       중부   살인   검거    0.0
 2       중부   강도   발생    6.0
 3       중부   강도   검거    7.0
 4       중부   강간   발생  128.0
 ...    ...  ...  ...    ...
 65529  NaN  NaN  NaN    NaN
 65530  NaN  NaN  NaN    NaN
 65531  NaN  NaN  NaN    NaN
 65532  NaN  NaN  NaN    NaN
 65533  NaN  NaN  NaN    NaN
 
 [65534 rows x 4 columns], '2014':         구분   죄종 발생검거     건수
 0       중부   살인   발생    3.0
 1       중부   살인   검거    2.0
 2       중부   강도   발생    8.0
 3       중부   강도   검거    8.0
 4       중부   강간   발생  143.0
 ...    ...  ...  ...    ...
 65529  NaN  NaN  NaN    NaN
 65530  NaN  NaN  NaN    NaN
 65531  NaN  NaN  NaN    NaN
 65532  NaN  NaN  NaN    NaN
 65533  NaN  NaN  NaN    NaN
 
 [65534 rows x 4 columns], '2015':         구분   죄종 발생검거     건수
 0       중부   살인   발생    2.0
 1       중부   살인   검거    2.0
 2       중부   강도   발생    3.0
 3       중부   강도   검거    2.0
 4       중부   강간   발생  105.0
 ...    ...  ...  ...    ...
 65529  NaN  NaN  NaN    NaN
 65530  NaN  NaN  NaN    NaN
 65531  NaN  NaN  NaN    NaN
 65532  NaN  NaN  NaN    NaN
 65533  NaN  NaN  NaN    NaN
 
 [65534 rows x 4 columns], '2016':         구분   죄종 발생검거     건수
 0       중부   살인   발생    2.0
 1       중부   살인   검거    2.0
 2       중부   강도   발생    3.0
 3       중부   강도   검거    3.0
 4       중부   강간   발생  141.0
 ...    ...  ...  ...    ...
 65529  NaN  NaN  NaN    NaN
 65530  NaN  NaN  NaN    NaN
 65531  NaN  NaN  NaN    NaN
 65532  NaN  NaN  NaN    NaN
 65533  NaN  NaN  NaN    NaN
 
 [65534 rows x 4 columns]}

2000년부터 2016년까지 자료가 들어가 있는데 필요없는 열과 행이 들어가 있다. 이것을 제거하자.

자료 정제

Unnamed 열이름들이 몇 개나 있는지를 조사해보자.

In [10]: [(key, sum(df.columns.str.contains('Unn'))) for key, df in dfs.items()]
Out[10]: 
[('2009', 0),
 ('2010', 0),
 ('2011', 0),
 ('2012', 0),
 ('2013', 0),
 ('2014', 0),
 ('2015', 0),
 ('2016', 0),
 ('2000', 30),
 ('2001', 0),
 ('2002', 0),
 ('2004', 0),
 ('2005', 0),
 ('2006', 0),
 ('2007', 0),
 ('2008', 0),
 ('2003', 0)]

2000년도에만 30개가 있는 것을 알 수 있다.

Unnamed 열 이름을 제거하자.

In [11]: [(key, df.drop([x for x in df.columns if 'Un' in x], axis=1, inplace=True)) for key, df in dfs.items()]
Out[11]: 
[('2009', None),
 ('2010', None),
 ('2011', None),
 ('2012', None),
 ('2013', None),
 ('2014', None),
 ('2015', None),
 ('2016', None),
 ('2000', None),
 ('2001', None),
 ('2002', None),
 ('2004', None),
 ('2005', None),
 ('2006', None),
 ('2007', None),
 ('2008', None),
 ('2003', None)]

NA 행으로만 이루어진 행들이 각 파일에 몇 개씩 있나를 조사해보자.

In [12]: [(key, sum(df.isna().all(axis=1))) for key, df in dfs.items()]
Out[12]: 
[('2009', 65224),
 ('2010', 65224),
 ('2011', 65224),
 ('2012', 65224),
 ('2013', 65224),
 ('2014', 65224),
 ('2015', 65224),
 ('2016', 65224),
 ('2000', 0),
 ('2001', 0),
 ('2002', 65224),
 ('2004', 65224),
 ('2005', 65224),
 ('2006', 65224),
 ('2007', 65224),
 ('2008', 65224),
 ('2003', 0)]

2000, 2001, 2003년을 제외하고 동일하게 65224개의 NA행들이 있음을 알 수 있다.

NA 행들을 모두 제거하자. thresh=4를 이용하면 NA가 아닌 항목이 4개 이상인 행들을 남겨둔다.

In [13]: [(key, df.dropna(inplace=True, thresh=4)) for key, df in dfs.items()]
Out[13]: 
[('2009', None),
 ('2010', None),
 ('2011', None),
 ('2012', None),
 ('2013', None),
 ('2014', None),
 ('2015', None),
 ('2016', None),
 ('2000', None),
 ('2001', None),
 ('2002', None),
 ('2004', None),
 ('2005', None),
 ('2006', None),
 ('2007', None),
 ('2008', None),
 ('2003', None)]

각 파일에 연도 열을 추가하자.

In [14]: for key, df in dfs.items():
   ....:   df['연도'] = key
   ....: dfs
   ....: 
Out[14]: 
{'2000':      구분  죄종 발생검거    건수    연도
 0    중부  살인   발생     1  2000
 1    중부  살인   검거     1  2000
 2    중부  강도   발생    17  2000
 3    중부  강도   검거    15  2000
 4    중부  강간   발생    14  2000
 ..   ..  ..  ...   ...   ...
 305  수서  강간   검거    29  2000
 306  수서  절도   발생  1183  2000
 307  수서  절도   검거   352  2000
 308  수서  폭력   발생  3351  2000
 309  수서  폭력   검거  2983  2000
 
 [310 rows x 5 columns], '2001':      구분  죄종 발생검거    건수    연도
 0    중부  살인   발생     0  2001
 1    중부  살인   검거     0  2001
 2    중부  강도   발생    17  2001
 3    중부  강도   검거     6  2001
 4    중부  강간   발생    80  2001
 ..   ..  ..  ...   ...   ...
 305  수서  강간   검거    43  2001
 306  수서  절도   발생  1665  2001
 307  수서  절도   검거   632  2001
 308  수서  폭력   발생  3164  2001
 309  수서  폭력   검거  2629  2001
 
 [310 rows x 5 columns], '2002':      구분  죄종 발생검거      건수    연도
 0    중부  살인   발생     1.0  2002
 1    중부  살인   검거     2.0  2002
 2    중부  강도   발생    21.0  2002
 3    중부  강도   검거    18.0  2002
 4    중부  강간   발생    59.0  2002
 ..   ..  ..  ...     ...   ...
 305  수서  강간   검거    32.0  2002
 306  수서  절도   발생  2120.0  2002
 307  수서  절도   검거  1156.0  2002
 308  수서  폭력   발생  2803.0  2002
 309  수서  폭력   검거  2449.0  2002
 
 [310 rows x 5 columns], '2003':      구분  죄종 발생검거    건수    연도
 0    중부  살인   발생     0  2003
 1    중부  살인   검거     0  2003
 2    중부  강도   발생    46  2003
 3    중부  강도   검거    50  2003
 4    중부  강간   발생    70  2003
 ..   ..  ..  ...   ...   ...
 305  수서  강간   검거    30  2003
 306  수서  절도   발생  1694  2003
 307  수서  절도   검거  1000  2003
 308  수서  폭력   발생  2693  2003
 309  수서  폭력   검거  2431  2003
 
 [310 rows x 5 columns], '2004':      구분  죄종 발생검거      건수    연도
 0    중부  살인   발생     2.0  2004
 1    중부  살인   검거     2.0  2004
 2    중부  강도   발생    13.0  2004
 3    중부  강도   검거    17.0  2004
 4    중부  강간   발생   128.0  2004
 ..   ..  ..  ...     ...   ...
 305  수서  강간   검거    53.0  2004
 306  수서  절도   발생  1529.0  2004
 307  수서  절도   검거   600.0  2004
 308  수서  폭력   발생  2600.0  2004
 309  수서  폭력   검거  2338.0  2004
 
 [310 rows x 5 columns], '2005':      구분  죄종 발생검거      건수    연도
 0    중부  살인   발생     2.0  2005
 1    중부  살인   검거     3.0  2005
 2    중부  강도   발생    10.0  2005
 3    중부  강도   검거     8.0  2005
 4    중부  강간   발생    89.0  2005
 ..   ..  ..  ...     ...   ...
 305  수서  강간   검거    81.0  2005
 306  수서  절도   발생  1441.0  2005
 307  수서  절도   검거   893.0  2005
 308  수서  폭력   발생  2207.0  2005
 309  수서  폭력   검거  2010.0  2005
 
 [310 rows x 5 columns], '2006':      구분  죄종 발생검거      건수    연도
 0    중부  살인   발생     4.0  2006
 1    중부  살인   검거     4.0  2006
 2    중부  강도   발생    28.0  2006
 3    중부  강도   검거    18.0  2006
 4    중부  강간   발생   121.0  2006
 ..   ..  ..  ...     ...   ...
 305  수서  강간   검거    87.0  2006
 306  수서  절도   발생   890.0  2006
 307  수서  절도   검거   716.0  2006
 308  수서  폭력   발생  1931.0  2006
 309  수서  폭력   검거  1771.0  2006
 
 [310 rows x 5 columns], '2007':      구분  죄종 발생검거      건수    연도
 0    중부  살인   발생     4.0  2007
 1    중부  살인   검거     4.0  2007
 2    중부  강도   발생    13.0  2007
 3    중부  강도   검거    12.0  2007
 4    중부  강간   발생    83.0  2007
 ..   ..  ..  ...     ...   ...
 305  수서  강간   검거   117.0  2007
 306  수서  절도   발생   815.0  2007
 307  수서  절도   검거   506.0  2007
 308  수서  폭력   발생  1881.0  2007
 309  수서  폭력   검거  1805.0  2007
 
 [310 rows x 5 columns], '2008':      구분  죄종 발생검거      건수    연도
 0    중부  살인   발생     4.0  2008
 1    중부  살인   검거     4.0  2008
 2    중부  강도   발생    20.0  2008
 3    중부  강도   검거    26.0  2008
 4    중부  강간   발생    76.0  2008
 ..   ..  ..  ...     ...   ...
 305  수서  강간   검거    99.0  2008
 306  수서  절도   발생  1365.0  2008
 307  수서  절도   검거   539.0  2008
 308  수서  폭력   발생  2138.0  2008
 309  수서  폭력   검거  1853.0  2008
 
 [310 rows x 5 columns], '2009':      구분  죄종 발생검거      건수    연도
 0    중부  살인   발생     1.0  2009
 1    중부  살인   검거     1.0  2009
 2    중부  강도   발생    28.0  2009
 3    중부  강도   검거    24.0  2009
 4    중부  강간   발생    94.0  2009
 ..   ..  ..  ...     ...   ...
 305  수서  강간   검거    80.0  2009
 306  수서  절도   발생  1570.0  2009
 307  수서  절도   검거   745.0  2009
 308  수서  폭력   발생  2201.0  2009
 309  수서  폭력   검거  1908.0  2009
 
 [310 rows x 5 columns], '2010':      구분  죄종 발생검거      건수    연도
 0    중부  살인   발생     5.0  2010
 1    중부  살인   검거     5.0  2010
 2    중부  강도   발생    16.0  2010
 3    중부  강도   검거    11.0  2010
 4    중부  강간   발생   117.0  2010
 ..   ..  ..  ...     ...   ...
 305  수서  강간   검거    84.0  2010
 306  수서  절도   발생  1584.0  2010
 307  수서  절도   검거   604.0  2010
 308  수서  폭력   발생  1944.0  2010
 309  수서  폭력   검거  1646.0  2010
 
 [310 rows x 5 columns], '2011':      구분  죄종 발생검거      검거    연도
 0    중부  살인   발생     2.0  2011
 1    중부  살인   검거     3.0  2011
 2    중부  강도   발생    14.0  2011
 3    중부  강도   검거    19.0  2011
 4    중부  강간   발생    89.0  2011
 ..   ..  ..  ...     ...   ...
 305  수서  강간   검거   109.0  2011
 306  수서  절도   발생  1780.0  2011
 307  수서  절도   검거   422.0  2011
 308  수서  폭력   발생  2000.0  2011
 309  수서  폭력   검거  1516.0  2011
 
 [310 rows x 5 columns], '2012':      구분  죄종 발생검거      검거    연도
 0    중부  살인   발생     4.0  2012
 1    중부  살인   검거     5.0  2012
 2    중부  강도   발생     9.0  2012
 3    중부  강도   검거     8.0  2012
 4    중부  강간   발생   100.0  2012
 ..   ..  ..  ...     ...   ...
 305  수서  강간   검거    86.0  2012
 306  수서  절도   발생  1820.0  2012
 307  수서  절도   검거   464.0  2012
 308  수서  폭력   발생  1920.0  2012
 309  수서  폭력   검거  1519.0  2012
 
 [310 rows x 5 columns], '2013':      구분  죄종 발생검거      건수    연도
 0    중부  살인   발생     0.0  2013
 1    중부  살인   검거     0.0  2013
 2    중부  강도   발생     6.0  2013
 3    중부  강도   검거     7.0  2013
 4    중부  강간   발생   128.0  2013
 ..   ..  ..  ...     ...   ...
 305  수서  강간   검거    94.0  2013
 306  수서  절도   발생  1696.0  2013
 307  수서  절도   검거   470.0  2013
 308  수서  폭력   발생  1817.0  2013
 309  수서  폭력   검거  1512.0  2013
 
 [310 rows x 5 columns], '2014':      구분  죄종 발생검거      건수    연도
 0    중부  살인   발생     3.0  2014
 1    중부  살인   검거     2.0  2014
 2    중부  강도   발생     8.0  2014
 3    중부  강도   검거     8.0  2014
 4    중부  강간   발생   143.0  2014
 ..   ..  ..  ...     ...   ...
 305  수서  강간   검거   115.0  2014
 306  수서  절도   발생  1555.0  2014
 307  수서  절도   검거   407.0  2014
 308  수서  폭력   발생  1709.0  2014
 309  수서  폭력   검거  1394.0  2014
 
 [310 rows x 5 columns], '2015':      구분  죄종 발생검거      건수    연도
 0    중부  살인   발생     2.0  2015
 1    중부  살인   검거     2.0  2015
 2    중부  강도   발생     3.0  2015
 3    중부  강도   검거     2.0  2015
 4    중부  강간   발생   105.0  2015
 ..   ..  ..  ...     ...   ...
 305  수서  강간   검거   124.0  2015
 306  수서  절도   발생  1439.0  2015
 307  수서  절도   검거   666.0  2015
 308  수서  폭력   발생  1819.0  2015
 309  수서  폭력   검거  1559.0  2015
 
 [310 rows x 5 columns], '2016':      구분  죄종 발생검거      건수    연도
 0    중부  살인   발생     2.0  2016
 1    중부  살인   검거     2.0  2016
 2    중부  강도   발생     3.0  2016
 3    중부  강도   검거     3.0  2016
 4    중부  강간   발생   141.0  2016
 ..   ..  ..  ...     ...   ...
 305  수서  강간   검거   144.0  2016
 306  수서  절도   발생  1149.0  2016
 307  수서  절도   검거   789.0  2016
 308  수서  폭력   발생  1666.0  2016
 309  수서  폭력   검거  1431.0  2016
 
 [310 rows x 5 columns]}

디비 저장

각 파일의 열 이름을 살펴보자.

In [15]: [(key, df.columns) for key, df in dfs.items()]
Out[15]: 
[('2009', Index(['구분', '죄종', '발생검거', '건수', '연도'], dtype='object')),
 ('2010', Index(['구분', '죄종', '발생검거', '건수', '연도'], dtype='object')),
 ('2011', Index(['구분', '죄종', '발생검거', '검거', '연도'], dtype='object')),
 ('2012', Index(['구분', '죄종', '발생검거', '검거', '연도'], dtype='object')),
 ('2013', Index(['구분', '죄종', '발생검거', '건수', '연도'], dtype='object')),
 ('2014', Index(['구분', '죄종', '발생검거', '건수', '연도'], dtype='object')),
 ('2015', Index(['구분', '죄종', '발생검거', '건수', '연도'], dtype='object')),
 ('2016', Index(['구분', '죄종', '발생검거', '건수', '연도'], dtype='object')),
 ('2000', Index(['구분', '죄종', '발생검거', '건수', '연도'], dtype='object')),
 ('2001', Index(['구분', '죄종', '발생검거', '건수', '연도'], dtype='object')),
 ('2002', Index(['구분', '죄종', '발생검거', '건수', '연도'], dtype='object')),
 ('2004', Index(['구분', '죄종', '발생검거', '건수', '연도'], dtype='object')),
 ('2005', Index(['구분', '죄종', '발생검거', '건수', '연도'], dtype='object')),
 ('2006', Index(['구분', '죄종', '발생검거', '건수', '연도'], dtype='object')),
 ('2007', Index(['구분', '죄종', '발생검거', '건수', '연도'], dtype='object')),
 ('2008', Index(['구분', '죄종', '발생검거', '건수', '연도'], dtype='object')),
 ('2003', Index(['구분', '죄종', '발생검거', '건수', '연도'], dtype='object'))]

2011, 2012년도 열 이름이 다른 해와 다르게 검거라고 되어 있다. 이것을 건수로 변경하자.

In [16]: [(key, df.rename(columns={'검거': '건수'}, inplace=True)) for key, df in dfs.items()]
Out[16]: 
[('2009', None),
 ('2010', None),
 ('2011', None),
 ('2012', None),
 ('2013', None),
 ('2014', None),
 ('2015', None),
 ('2016', None),
 ('2000', None),
 ('2001', None),
 ('2002', None),
 ('2004', None),
 ('2005', None),
 ('2006', None),
 ('2007', None),
 ('2008', None),
 ('2003', None)]

다음은 pd.concat 함수를 이용해서 데이터프레임을 모두 하나로 합친다. 사전형이 입력되면 사전형의 키값이 인덱스로 변환된다.

In [17]: crime = pd.concat(dfs)
   ....: crime.info()
   ....: 
<class 'pandas.core.frame.DataFrame'>
MultiIndex: 5270 entries, (2000, 0) to (2016, 309)
Data columns (total 5 columns):
구분      5270 non-null object
죄종      5270 non-null object
발생검거    5270 non-null object
건수      5270 non-null float64
연도      5270 non-null object
dtypes: float64(1), object(4)
memory usage: 223.9+ KB

연도 열이 문자열이므로 정수형으로 변경하고 건수 열도 정수형으로 변경하자.

In [19]: crime.연도 = crime.연도.astype('int')
   ....: crime.건수 = crime.건수.astype('int')
   ....: 

구분 열이름을 경찰서로 변경하자.

In [21]: crime.rename(columns={'구분': '경찰서'}, inplace=True)

각 열의 내용을 살펴보자.

In [22]: crime.select_dtypes('object').apply(lambda x: pd.Series(x.unique()))
Out[22]: 
    경찰서   죄종 발생검거
0    중부   살인   발생
1    종로   강도   검거
2   남대문   강간  NaN
3   서대문   절도  NaN
4    혜화   폭력  NaN
..  ...  ...  ...
26   노원  NaN  NaN
27   방배  NaN  NaN
28   은평  NaN  NaN
29   도봉  NaN  NaN
30   수서  NaN  NaN

[31 rows x 3 columns]

커다란 문제는 보이지 않는 것 같다.

NA가 있는지 살펴보자.

In [23]: sum(crime.isna().any())
Out[23]: 0

디비에 저장하자.

In [24]: import sqlite3
   ....: conn = sqlite3.connect('./data/seoul.sqlite3')
   ....: crime.to_sql('crime', conn, index=False)
   ....: conn.close()
   ....: 

편의를 위해서 앞 장에서 pop.sqlite3 디비에 저장했던 테이블들을 seoul.sqlite3 디비에 저장하자.

In [25]: con_pop = sqlite3.connect('./data/pop.sqlite3')
   ....: pop = pd.read_sql("select * from pop", con_pop)
   ....: cctv = pd.read_sql("select * from cctv", con_pop)
   ....: con_pop.close()
   ....: conn = sqlite3.connect('./data/seoul.sqlite3')
   ....: pop.to_sql("pop", conn)
   ....: cctv.to_sql("cctv", conn)
   ....: conn.close()
   ....: 

VWorld API 이용 주소 검색

VWorld 지도 검색 API를 이용해서 주소를 입력하면 지번 및 도로명 및 위/경도를 알아낼 수 있다. VWorld API를 이용하기 위해서는 먼저 회원가입 후 인증키를 받아야 한다. 사용법은 웹사이트를 참고하면 된다.

인증키를 받았다고 가정하면 다음과 같이 검색과 결과를 받을 수 있다.

In [26]: import requests
   ....: resp = requests.get('http://api.vworld.kr/req/search?key=인증키입력부분&reque
   ....: st=search&size=10&type=place&query=강남경찰서&format=json')
   ....: resp.json()
   ....: 
Out[26]: 
{'response': {'page': {'current': '1', 'size': '10', 'total': '2'},
  'record': {'current': '10', 'total': '16'},
  'result': {'crs': 'EPSG:4326',
   'items': [{'address': {'parcel': '서울특별시 강남구 삼성동 171-2',
      'road': '서울특별시 강남구 테헤란로113길 12'},
     'category': '정부행정기관 > 치안/안보부처 > 경찰청',
     'id': 'AA0000557912',
     'point': {'x': '127.067194351542', 'y': '37.5090497296493'},
     'title': '서울강남경찰서'},
    {'address': {'parcel': '', 'road': ''},
     'category': '교통시설 > 버스터미널/정류장 > 정류장',
     'id': 'AA0009688456',
     'point': {'x': '127.067120000476', 'y': '37.5098699998996'},
     'title': '강남경찰서.강남운전면허시험장정류장'},
    {'address': {'parcel': '', 'road': ''},
     'category': '교통시설 > 버스터미널/정류장 > 정류장',
     'id': 'AA0009688883',
     'point': {'x': '127.065949999596', 'y': '37.509770000102'},
     'title': '강남경찰서면허시험장정류장'},
    {'address': {'parcel': '', 'road': ''},
     'category': '교통시설 > 버스터미널/정류장 > 정류장',
     'id': 'AA0009684034',
     'point': {'x': '127.065989999507', 'y': '37.5095599998675'},
     'title': '강남경찰서.강남운전면허시험장정류장'},
    {'address': {'parcel': '서울특별시 강남구 논현동 42-6',
      'road': '서울특별시 강남구 학동로17길 13'},
     'category': '정부행정기관 > 치안/안보부처 > 경찰청',
     'id': 'AA0009154677',
     'point': {'x': '127.026024819286', 'y': '37.5135511178908'},
     'title': '서울강남경찰서논현1파출소'},
    {'address': {'parcel': '서울특별시 강남구 논현동 58-13', 'road': '서울특별시강남구     학동로 169'},
     'category': '정부행정기관 > 치안/안보부처 > 경찰청',
     'id': 'AA0010029010',
     'point': {'x': '127.029322774658', 'y': '37.5138635184868'},
     'title': '서울강남경찰서논현1파출소'},
    {'address': {'parcel': '서울특별시 강남구 대치동 997', 'road': '서울특별시강남구     테헤란로 624'},
     'category': '교통시설 > 버스터미널/정류장 > 정류장',
     'id': 'AA0009645290',
     'point': {'x': '127.066001999827', 'y': '37.5095529998435'},
     'title': '강남경찰서.강남운전면허시험장정류장'},
    {'address': {'parcel': '서울특별시 강남구 삼성동 107-3',
      'road': '서울특별시 강남구 영동대로112길 6'},
     'category': '정부행정기관 > 치안/안보부처 > 경찰청',
     'id': 'AA0000562500',
     'point': {'x': '127.060689561917', 'y': '37.5149115385337'},
     'title': '서울강남경찰서삼성1파출소'},
    {'address': {'parcel': '서울특별시 강남구 삼성동 170-5',
      'road': '서울특별시 강남구 테헤란로113길 7'},
     'category': '도로시설 > 진출입시설 > 미분류',
     'id': 'AA0009770678',
     'point': {'x': '127.066222045683', 'y': '37.5108711388763'},
     'title': '강남경찰서(임시)입구'},
    {'address': {'parcel': '서울특별시 강남구 삼성동 170-8',
      'road': '서울특별시 강남구 테헤란로 623'},
     'category': '교통시설 > 버스터미널/정류장 > 정류장',
     'id': 'AA0009641451',
     'point': {'x': '127.065794000155', 'y': '37.5099129996285'},
     'title': '강남경찰서면허시험장정류장'}],
   'type': 'place'},
  'service': {'name': 'search',
   'operation': 'search',
   'time': '13(ms)',
   'version': '2.0'},
  'status': 'OK'}}

요청 매개변수

검색 API 2.0을 사용하고 있으며 key는 발급받은 인증키, format은 응답 받을 형식으로 json과 xml을 사용할 수 있다. request는 search이어야 한다. size는 한 페이지에 출력될 갯수를 나타낸다. 최소 1, 최대 1000까지 사용할 수 있다. page는 결과값이 많아서 여러 페이지일 때 어떤 페이지를 선택할 것인가를 나타내는 것이다. 기본값은 1, 즉 첫 페이지이다. query는 검색할 키워드를 넣는 부분이다. type는 place와 address 두 가지를 선택할 수 있는데 place는 장소 이름으로 검색하는 것이고 address는 비교적 정확한 주소를 넣어야 검색이 된다. category는 type에서 address를 선택했을 때 필수적으로 입력해야 하는 부분으로 도로명일 때는 road를 선택하고 지번 주소는 parcel을 선택한다. type이 place일 때는 자연수를 입력해야하는데 아직 정해지지 않은 것 같다.

응답 변수

응답 결과 정보 중에서 record의 total은 전체 검색된 결과의 수를 나타내고 current는 현재 페이지에 보여지고 있는 결과의 수를 나타낸다. page의 total은 검색 결과의 전체 페이지 수이고 current는 현재 보여지고 있는 페이지 번호, size는 페이지당 보여지는 결과의 수를 나타낸다. result의 crs는 응답결과 좌표계의 종류를 의미한다. status의 OK는 결과가 있는 경우, NOT_FOUND는 검색 결과가 없는 경우, ERROR는 오류가 발생된 경우이다.

질의 문자열 주소 검색

문자열을 입력하면 문자열 반환된 결과의 항목 중 title과 정확히 일치하는 것 중에서 주소와 경위도 리스트를 반환하는 함수를 만들자. 매개변수 key에는 발급받은 본인의 인증키를 입력해야 한다.

In [27]: def getAddressCoords(key, queryString, page=1, size=50):
   ....:   qStmt = "key=" + key + "&request=search&type=place&page=" + str(page) + "&size=" + str(size) + "&query=" + queryString + "&format=json"
   ....:   url = "http://api.vworld.kr/req/search?" + qStmt
   ....: 
   ....:   resp = requests.get(url)
   ....:   res_json = resp.json()
   ....:   response = res_json['response']
   ....:   itemlist = []
   ....:   if response['status'] == 'OK':
   ....:     items = response['result']['items']
   ....:     for item in items:
   ....:       if item['title'].strip() == queryString:
   ....:         itemlist.append(dict(경찰서=item['title'], 주소=item['address']['road'], 경위도=item['point']))
   ....:   return itemlist
   ....: 

다음과 같이 사용하면 된다.

In [28]: import requests
   ....: getAddressCoords("본인인증키", "서울강북경찰서")
   ....: 
Out[28]: 
[{'경위도': {'x': '127.027356706417', 'y': '37.6374446425296'},
  '경찰서': '서울강북경찰서',
  '주소': '서울특별시 강북구 오패산로 406'}]

경찰서 소속 자치구 및 경위도

위에서 만든 함수를 이용해서 서울시 경찰서가 속한 자치구와 경위도 자료를 만들어 보자.

디비에서 경찰서 이름만 뽑아낸다.

In [29]: import sqlite3
   ....: conn = sqlite3.connect('./data/seoul.sqlite3')
   ....: plc_stn = pd.read_sql("SELECT DISTINCT 경찰서 FROM crime", conn)
   ....: conn.close()
   ....: plc_stn
   ....: 
Out[33]: 
    경찰서
0    중부
1    종로
2   남대문
3   서대문
4    혜화
..  ...
26   노원
27   방배
28   은평
29   도봉
30   수서

[31 rows x 1 columns]

지도 검색 사이트에 정확한 질의를 위해서 이름을 수정한다.

In [34]: stn_name = "서울" + plc_stn + "경찰서"
   ....: stn_name
   ....: 
Out[35]: 
         경찰서
0    서울중부경찰서
1    서울종로경찰서
2   서울남대문경찰서
3   서울서대문경찰서
4    서울혜화경찰서
..       ...
26   서울노원경찰서
27   서울방배경찰서
28   서울은평경찰서
29   서울도봉경찰서
30   서울수서경찰서

[31 rows x 1 columns]

모든 경찰서에 대해서 주소 및 경위도 리스트는 다음과 같다. 마찬가지로 아래 인증키 부분에 자신의 인증키를 입력한다.

In [36]: res = []
   ....: for name in stn_name.squeeze():
   ....:   res.extend(getAddressCoords("인증키", name))
   ....: res
   ....: 
Out[36]: 
[{'경위도': {'x': '126.990020918921', 'y': '37.5636320332824'},
  '경찰서': '서울중부경찰서',
  '주소': '서울특별시 중구 수표로 27'},
 {'경위도': {'x': '126.984669638716', 'y': '37.5752966541743'},
  '경찰서': '서울종로경찰서',
  '주소': '서울특별시 종로구 율곡로 46'},
 {'경위도': {'x': '126.973548995951', 'y': '37.5548113303817'},
  '경찰서': '서울남대문경찰서',
  '주소': '서울특별시 중구 남대문로5가 한강대로 410'},
 {'경위도': {'x': '126.967010318625', 'y': '37.5646882605718'},
  '경찰서': '서울서대문경찰서',
  '주소': '서울특별시 서대문구 통일로 113'},
 {'경위도': {'x': '126.998912193137', 'y': '37.5718852513784'},
  '경찰서': '서울혜화경찰서',
  '주소': '서울특별시 종로구 창경궁로 112-16'},
 {'경위도': {'x': '126.966858988641', 'y': '37.5409370273683'},
  '경찰서': '서울용산경찰서',
  '주소': '서울특별시 용산구 원효로89길 24'},
 {'경위도': {'x': '127.016215814566', 'y': '37.5899801887989'},
  '경찰서': '서울성북경찰서',
  '주소': '서울특별시 성북구 보문로 170'},
 {'경위도': {'x': '127.045794546032', 'y': '37.5851575211424'},
  '경찰서': '서울동대문경찰서',
  '주소': '서울특별시 동대문구 약령시로21길 29'},
 {'경위도': {'x': '126.953979398128', 'y': '37.5508321344531'},
  '경찰서': '서울마포경찰서',
  '주소': '서울특별시 마포구 마포대로 183'},
 {'경위도': {'x': '126.901006328686', 'y': '37.5259558027604'},
  '경찰서': '서울영등포경찰서',
  '주소': '서울특별시 영등포구 국회대로 608 영등포경찰서'},
 {'경위도': {'x': '127.036377645509', 'y': '37.5617654993203'},
  '경찰서': '서울성동경찰서',
  '주소': '서울특별시 성동구 왕십리광장로 9'},
 {'경위도': {'x': '126.942813709977', 'y': '37.5131633261019'},
  '경찰서': '서울동작경찰서',
  '주소': '서울특별시 동작구 노량진로 148'},
 {'경위도': {'x': '127.079268260508', 'y': '37.5458138364559'},
  '경찰서': '서울광진경찰서',
  '주소': '서울특별시 광진구 광나루로 447'},
 {'경위도': {'x': '126.921268264495', 'y': '37.6021987191145'},
  '경찰서': '서울서부경찰서',
  '주소': '서울특별시 은평구 은평로9길 15'},
 {'경위도': {'x': '127.027356706417', 'y': '37.6374446425296'},
  '경찰서': '서울강북경찰서',
  '주소': '서울특별시 강북구 오패산로 406'},
 {'경위도': {'x': '126.90997796841', 'y': '37.481417602623'},
  '경찰서': '서울금천경찰서',
  '주소': '서울특별시 관악구 남부순환로 1435'},
 {'경위도': {'x': '127.104468066862', 'y': '37.6182542869542'},
  '경찰서': '서울중랑경찰서',
  '주소': '서울특별시 중랑구 신내역로3길 40-10'},
 {'경위도': {'x': '127.067194351542', 'y': '37.5090497296493'},
  '경찰서': '서울강남경찰서',
  '주소': '서울특별시 강남구 테헤란로113길 12'},
 {'경위도': {'x': '126.950979422302', 'y': '37.4743761890679'},
  '경찰서': '서울관악경찰서',
  '주소': '서울특별시 관악구 관악로5길 33'},
 {'경위도': {'x': '126.850605671634', 'y': '37.551287755715'},
  '경찰서': '서울강서경찰서',
  '주소': '서울특별시 강서구 화곡로 308'},
 {'경위도': {'x': '127.127014346882', 'y': '37.5286751523262'},
  '경찰서': '서울강동경찰서',
  '주소': '서울특별시 강동구 성내로13길 12'},
 {'경위도': {'x': '127.03221766715', 'y': '37.6017154977445'},
  '경찰서': '서울종암경찰서',
  '주소': '서울특별시 성북구 종암로 135'},
 {'경위도': {'x': '126.886723746883', 'y': '37.494790994579'},
  '경찰서': '서울구로경찰서',
  '주소': '서울특별시 구로구 가마산로 235'},
 {'경위도': {'x': '127.005161641666', 'y': '37.4957431981887'},
  '경찰서': '서울서초경찰서',
  '주소': '서울특별시 서초구 반포대로 179'},
 {'경위도': {'x': '126.865594675255', 'y': '37.5166761915281'},
  '경찰서': '서울양천경찰서',
  '주소': '서울특별시 양천구 목동동로 99'},
 {'경위도': {'x': '127.127006491873', 'y': '37.5020022551097'},
  '경찰서': '서울송파경찰서',
  '주소': '서울특별시 송파구 중대로 221 송파경찰서'},
 {'경위도': {'x': '127.071268346957', 'y': '37.6423087554946'},
  '경찰서': '서울노원경찰서',
  '주소': '서울특별시 노원구 노원로 283 노원경찰서'},
 {'경위도': {'x': '126.982996672093', 'y': '37.481568037107'},
  '경찰서': '서울방배경찰서',
  '주소': '서울특별시 서초구 방배천로 54'},
 {'경위도': {'x': '126.928154701241', 'y': '37.6285962717838'},
  '경찰서': '서울은평경찰서',
  '주소': '서울특별시 은평구 연서로 365'},
 {'경위도': {'x': '127.052640617791', 'y': '37.6537663696334'},
  '경찰서': '서울도봉경찰서',
  '주소': '서울특별시 도봉구 노해로 403'},
 {'경위도': {'x': '127.077143894471', 'y': '37.4935487332215'},
  '경찰서': '서울수서경찰서',
  '주소': '서울특별시 강남구 개포로 617'}]

데이터프레임으로 변경하자.

In [37]: stn_info = pd.DataFrame(res)

주소에서 구를 추출하여 자치구 열을 추가하자.

In [38]: stn_info['자치구'] = stn_info.주소.str.extract(r'\s(.*구)')

자료를 합치기 전에 이름을 변경하자.

In [39]: stn_info.rename(columns={'경찰서': '검색명'}, inplace=True)

자료를 합치자.

In [40]: police = pd.concat([plc_stn, stn_info], axis=1)

디비 저장

디비에 저장하기 위해 자료를 손질한다. 경위도를 경도와 위도로 분리해서 저장하자. 디비에 사전형으로는 저장할 수 없기 때문이다.

In [41]: police['경도'] = police.경위도.map(lambda x: x['x']).astype('float')
   ....: police['위도'] = police.경위도.map(lambda x: x['y']).astype('float')
   ....: 

경위도 열을 제거하자.

In [43]: police.drop(columns='경위도', inplace=True)
   ....: police
   ....: 
Out[44]: 
    경찰서       검색명                        주소   자치구          경도         위도
0    중부   서울중부경찰서           서울특별시 중구 수표로 27    중구  126.990021  37.563632
1    종로   서울종로경찰서          서울특별시 종로구 율곡로 46   종로구  126.984670  37.575297
2   남대문  서울남대문경찰서  서울특별시 중구 남대문로5가 한강대로 410    중구  126.973549  37.554811
3   서대문  서울서대문경찰서        서울특별시 서대문구 통일로 113  서대문구  126.967010  37.564688
4    혜화   서울혜화경찰서     서울특별시 종로구 창경궁로 112-16   종로구  126.998912  37.571885
..  ...       ...                       ...   ...         ...        ...
26   노원   서울노원경찰서   서울특별시 노원구 노원로 283 노원경찰서   노원구  127.071268  37.642309
27   방배   서울방배경찰서         서울특별시 서초구 방배천로 54   서초구  126.982997  37.481568
28   은평   서울은평경찰서         서울특별시 은평구 연서로 365   은평구  126.928155  37.628596
29   도봉   서울도봉경찰서         서울특별시 도봉구 노해로 403   도봉구  127.052641  37.653766
30   수서   서울수서경찰서         서울특별시 강남구 개포로 617   강남구  127.077144  37.493549

[31 rows x 6 columns]

경찰서 관할구 중 누락된 구가 없는지를 살펴보자. 우선 인구 테이블에 있는 자치구를 불러와서 관할구와 비교해본다.

In [45]: conn = sqlite3.connect('./data/seoul.sqlite3')
   ....: gu = pd.read_sql("SELECT DISTINCT 자치구 FROM pop", conn)
   ....: 

np.setdiff1d 함수를 이용해서 두 배열들의 집합차를 살펴보자.

In [47]: np.setdiff1d(gu.squeeze().unique(), police.자치구.unique())
Out[47]: array(['금천구'], dtype=object)

금천구가 누락된 것을 알 수 있다. 금천경찰서를 살펴보면 주소지가 관악구로 되어있는 것을 알 수 있다. 하지만 관할구역은 금천구이므로 금천구로 자치구를 변경하자.

In [48]: police.loc[police.경찰서 == '금천', '자치구'] = "금천구"
   ....: police.자치구
   ....: 
Out[49]: 
0       중구
1      종로구
2       중구
3     서대문구
4      종로구
      ... 
26     노원구
27     서초구
28     은평구
29     도봉구
30     강남구
Name: 자치구, Length: 31, dtype: object

자치구별 관할 경찰서를 살펴보자.

In [50]: police.set_index(['자치구', '경찰서']).sort_index()['검색명']
Out[50]: 
자치구  경찰서
강남구  강남      서울강남경찰서
     수서      서울수서경찰서
강동구  강동      서울강동경찰서
강북구  강북      서울강북경찰서
강서구  강서      서울강서경찰서
              ...   
종로구  종로      서울종로경찰서
     혜화      서울혜화경찰서
중구   남대문    서울남대문경찰서
     중부      서울중부경찰서
중랑구  중랑      서울중랑경찰서
Name: 검색명, Length: 31, dtype: object

갯수를 세보자.

In [51]:  police.pivot_table(index='자치구', values='검색명', aggfunc=lambda x: x.count()).sort_values('검색명', ascending=False)
Out[51]: 
     검색명
자치구     
강남구    2
중구     2
종로구    2
은평구    2
성북구    2
..   ...
광진구    1
관악구    1
강서구    1
강북구    1
중랑구    1

[25 rows x 1 columns]

디비에 저장하자.

In [52]: conn = sqlite3.connect('./data/seoul.sqlite3')
   ....: police.to_sql('police_station', conn)
   ....: conn.close()
   ....: 

TODO: 범죄 현황 그래프

지역 경계

지역 경계 지도를 이용해서 지도에 표시한다. 지역 경계 정보는 브이월드 또는 통계지리정보서비스를 통해서 얻을 수 있다. 여기서는 브이월드 API를 이용해본다.

법정동 코드

지역 경계 지도를 구하기 위해서는 법정동 코드가 필요하다.

정부 행정표준코드관리시스템에 접속한 후 코드 검색 > 자주 찾는 코드에서 법정동을 선택하여 법정동 코드 전체 자료를 다운받아 data 폴더에 저장한다.

압축을 해제한다. 압축 파일 이름이 한글로 되어 있고 인코딩이 utf-8이 아니어서 깨져 보인다. 따라서 파일 이름을 다음과 같이 변경한다.

In [53]: import zipfile
   ....: import os
   ....: zipref = zipfile.ZipFile('./data/법정동코드 전체자료.zip', 'r')
   ....: for fname in zipref.namelist():
   ....:   zipref.extract(fname, './data')
   ....:   os.rename('./data/' + fname, './data/' + fname.encode('cp437').decode('cp949'))
   ....: 

파일을 읽어 들인다.

In [54]: df = pd.read_csv('./data/법정동코드 전체자료.txt', sep='\t', dtype={'법정동코드': str}, engine
   ....: ='python')
   ....: 

폐지된 코드를 제거한다.

In [55]: df = df[df.폐지여부.str.contains('존재')]

폐지여부 열을 제거하자.

In [56]: df.drop(columns='폐지여부', inplace=True)

법정동을 시도/시군구/읍면동/리 별로 구분을 하자. 모든 구획 열의 값을 로 설정한다.

In [57]: df['구획'] = '리'

법정동은 처음 2자리는 시도, 다음 3자리는 시군구, 다음 3자리는 읍면동, 마지막 2자리는 리를 나타낸다. 따라서 마지막 2자리가 모두 0인 코드는 읍면동 이상이다. 즉, 읍면동, 시군구, 시도가 될 수 있다.

In [58]: df['구획'][df.법정동코드.str.match(r'\d{8}0{2}')] = '읍면동'

마지막 5자리가 모두 0이면 시군구 이상이다.

In [59]: df['구획'][df.법정동코드.str.match(r'\d{5}0{5}')] = '시군구'

뒤 8자리가 모두 0이면 시도를 나타내는 것이고 세종특별자치시는 예외적으로 3611000000이다.

In [60]: df['구획'][df.법정동코드.str.match(r'\d{2}0{8}|36110{6}')] = '시도'

읍면동 구획의 법정동명의 끝 문자를 살펴보자.

In [61]: df[df.구획.str.match(r'읍면동')]['법정동명'].map(lambda x: re.search(r'.*(.)$', x)[1]).unique(
   ....: )
   ....: 
Out[61]: array(['동', '로', '가', '읍', '면', ' '], dtype=object)

끝 문자중 공백문자가 있는 것을 알 수 있다. 이것을 제거하자.

In [62]: df.법정동명 = df.법정동명.str.strip()
   ....: df[df.구획.str.match(r'읍면동')]['법정동명'].map(lambda x: re.search(r'.*(.)$', x)[1]).unique(
   ....: )
   ....: 
Out[63]: array(['동', '로', '가', '읍', '면'], dtype=object)

디비에 저장하자.

In [64]: conn = sqlite3.connect('./data/seoul.sqlite3')
   ....: df.to_sql('legal_dong', conn)
   ....: conn.close()
   ....: 

지역 경계 획득

브이월드 API를 이용해서 법정동 코드를 사용해 지역 경계 정보를 얻어 온다. 브이월드 서비스 중 데이터 API 레퍼런스를 이용해서 지역 경계 정보를 얻어 올 수 있다. 4개의 서비스로 구분해서 질의를 할 수 있다. 기본 url은 다음과 같다.:

http://api.vworld.kr/req/data?service=data&request=GetFeature&

시도는 data 값에 LT_C_ADSIDO_INFO, 시군구는 LT_C_ADSIGG_INFO, 읍면동은 LT_C_ADEMD_INFO, 리는 LT_C_ADRI_INFO를 대입하면 된다. domain 값에는 인증할 때 입력한 도메인 주소를 적어 주어야 한다. key에는 본인 인증키 값을 넣어 주고 attrFilter값에 속성명과 연산자 속성값을 콜론으로 구분해서 대입한다. 더 자세한 사항들은 웹페이지를 참조한다.

읍면동 경계

읍면동 지역 경계를 얻기 위해서는 data=LT_C_ADEMD_INFO을 사용하고 attrFilter=emd_cd 또는 attrFilter=emd_kor_nm을 이용한다. emd_cd는 법정동 코드이고 emd_kor_nm은 법정동 이름이다.

예를 들어 속성명이 emd_cd``(읍면동 법정코드)이고 천안시 신부동(법정 코드명은 ``44131118)을 검색한다면 attrFilter는 다음과 같이 설정 한다.

attrFilter=emd_cd:=:44131118

또는 이름으로 검색을 하고 싶으면 다음과 같이 한다.

attrFilter=emd_kor_nm:=:신부동

충남 천안시 신부동을 법정동 코드를 이용하여 검색하려면 다음과 같다. 여기서 key와 domain 값을 본인의 키와 도메인 이름으로 대입하는 것을 잊지 말자.

In [65]: import requests
   ....: url = "http://api.vworld.kr/req/data?service=data&request=GetFeature&data=LT_C_ADEMD_INFO&key=본인인증키&domain=인증시입력한도메인&attrFilter=emd_cd:=:44131118"
   ....: res = requests.get(url)
   ....: res.json()
   ....: 
Out[39]
{'response': {'page': {'current': '1', 'size': '10', 'total': '1'}
  'record': {'current': '1', 'total': '1'}
  'result': {'featureCollection': {'bbox': [127.14905068928854
     36.813623388526494
     127.17636925005063
     36.83769590850596]
    'features': [{'geometry': {'coordinates': [[[[127.17522234436002
           36.82270325471661]
          [127.17517087238659, 36.822488660509]
          [127.17508430881621, 36.82217545589959]
          ... 중간 생략 ...
          [127.17479339720188, 36.82379635782791]
          [127.17513122120647, 36.822917147077035]
          [127.17522234436002, 36.82270325471661]]]]
       'type': 'MultiPolygon'}
      'id': 'LT_C_ADEMD_INFO.28013'
      'properties': {'emd_cd': '44131118'
       'emd_eng_nm': 'Sinbu-dong'
       'emd_kor_nm': '신부동'
       'full_nm': '충청남도 천안시동남구 신부동'}
      'type': 'Feature'}]
    'type': 'FeatureCollection'}}
  'service': {'name': 'data'
   'operation': 'GetFeature'
   'time': '29(ms)'
   'version': '2.0'}
  'status': 'OK'}}

서울시 자치구 경계

서울특별시 자치구들에 대한 경계를 얻어와서 디비에 저장하자.

법정동 테이블에서 서울시 자치구만 뽑아내자.

In [66]: import sqlite3
   ....: conn = sqlite3.connect('./data/seoul.sqlite3')
   ....: seoul = conn.execute("select 법정동코드, 법정동명 from legal_dong where 법정동명 like '%서울%' and \
   ....: 구획 = '시군구'").fetchall()
   ....: conn.close()
   ....: 

각 자치구의 지역 경계 정보를 가져와서 사전형으로 저장한다.

In [70]: dfs = {}
   ....: url = "http://api.vworld.kr/req/data?service=data&request=GetFeature&data=LT_C_ADSIGG_INFO&key=본인인증키&domain=인증시입력한도메인&attrFilter=full_nm:=:{gu}"
   ....: for code, fullname in seoul:
   ....:   gu = re.sub(r".*\s(.*)", r"\1", fullname)
   ....:   dfs[gu] = requests.get(url.format(gu=fullname)).json()
   ....: 

필요한 정보만 뽑아서 JSON 문자열로 저장한다.

In [71]: import json
   ....: for gu, geoinfo in dfs.items():
   ....:   dfs[gu] = json.dumps(geoinfo['response']['result']['featureCollection'])
   ....: 

시리즈 형식으로 변형 후 데이터프레임으로 변형한다.

In [72]: geo = pd.Series(dfs).reset_index()
   ....: geo.columns = ['자치구', 'geojson']
   ....: 

디비에 저장하자.

In [74]: conn = sqlite3.connect('./data/seoul.sqlite3')
   ....: geo.to_sql('geoinfo', conn)
   ....: conn.close()
   ....: 

Folium 이용 지도 표시

Folium 은 Open Street Map과 같은 지도데이터에 Leaflet.js를 이용하여 위치정보를 시각화하기 위한 라이브러리다. 기본적으로 GeoJSON 형식 또는 topoJSON 형식으로 데이터를 지정하면, 오버레이를 통해 마커의 형태로 위치 정보를 지도상에 표현할 수 있다.

folium 설치

In [75]: conda install folium -c conda-forge

폴리움 사용은 location에 위도, 경도를 지정함으로 지도의 가운데 위치를 설정한다. 다음은 서울시 중구를 지도의 중앙에 위치시킨 것이다.

In [76]: import folium
   ....: m = folium.Map(location=(37.557939871241885, 126.99417336148036), zoom_start=13,)
   ....: m
   ....: 

다음은 디비에서 종로구 경계 정보를 가져와 폴리움을 이용해서 지도에 표시하는 예이다.

In [78]: import sqlite3
   ....: import json
   ....: conn = sqlite3.connect('./data/seoul.sqlite3')
   ....: jongro = pd.read_sql('select geojson from geoinfo where 자치구 = "종로구"', conn)
   ....: conn.close()
   ....: jongro_geo = json.loads(jongro.geojson[0])
   ....: (lon, lat) = ((jongro_geo['bbox'][0] + jongro_geo['bbox'][2]) / 2, (jongro_geo['bbox'][1] + jongro_geo['bbox'][3]) / 2 )
   ....: m.location = (lat, lon)
   ....: m.zoom_start = 12
   ....: folium.GeoJson(jongro_geo, name='geojson').add_to(m)
   ....: m
   ....: 

2015년 자료 분석

2015년도 자료를 분석해 보자.

2015년 범죄 자료와 경찰서 자료를 받아 온다. 각 자치구별로 분석을 원하기 때문에 경찰서 자료에 있는 자치구 정보가 필요하다.

In [80]: import sqlite3
   ....: conn = sqlite3.connect('./data/seoul.sqlite3')
   ....: crime = pd.read_sql("select * from crime where 연도 = 2015", conn)
   ....: stn = pd.read_sql("select 경찰서, 자치구 from police_station", conn)
   ....: conn.close()
   ....: 

두 자료를 합치자.

In [85]: df = crime.merge(stn, on='경찰서')

pivot_table을 이용해서 자치구별로 합산을 계산한다.

In [86]: dfp = df.pivot_table(index=['자치구'], columns=['발생검거', '죄종'], values='건수', \
   ....: aggfunc=np.sum)
   ....: dfp
   ....: 
Out[87]: 
발생검거   검거                       발생                    
죄종     강간  강도  살인    절도    폭력   강간  강도  살인    절도    폭력
자치구                                                   
강남구   349  18  10  1650  3705  449  21  13  3850  4284
강동구   123   8   3   789  2248  156   6   4  2366  2712
강북구   126  13   8   618  2348  153  14   7  1434  2649
강서구   191  13   8  1260  2718  262  13   7  2096  3207
관악구   221  14   8   827  2642  320  12   9  2706  3298
..    ...  ..  ..   ...   ...  ...  ..  ..   ...   ...
용산구   173  14   5   587  1704  194  14   5  1557  2050
은평구   141   6   3   711  2306  166   9   3  1914  2653
종로구   161   9   5   837  1931  211  11   6  2184  2293
중구    111   6   2   859  1964  170   9   3  2548  2224
중랑구   148   9  12   829  2407  187  11  13  2135  2847

[25 rows x 10 columns]

검거율을 계산하자.

In [88]: arrest = dfp['검거'] / dfp['발생'] * 100
   ....: arrest
   ....: 
Out[89]: 
죄종          강간          강도          살인         절도         폭력
자치구                                                         
강남구  77.728285   85.714286   76.923077  42.857143  86.484594
강동구  78.846154  133.333333   75.000000  33.347422  82.890855
강북구  82.352941   92.857143  114.285714  43.096234  88.637222
강서구  72.900763  100.000000  114.285714  60.114504  84.752105
관악구  69.062500  116.666667   88.888889  30.561715  80.109157
..         ...         ...         ...        ...        ...
용산구  89.175258  100.000000  100.000000  37.700706  83.121951
은평구  84.939759   66.666667  100.000000  37.147335  86.920467
종로구  76.303318   81.818182   83.333333  38.324176  84.212822
중구   65.294118   66.666667   66.666667  33.712716  88.309353
중랑구  79.144385   81.818182   92.307692  38.829040  84.545135

[25 rows x 5 columns]

검거율이 100이 넘는 것도 보인다. 100으로 맞춘다.

In [90]: arrest[arrest > 100] = 100

범죄 발생 건수를 최대, 최소 스케일링을 해서 각 열마다 0부터 1사이의 값으로 변경하자.

In [91]: scaled = dfp - dfp.min()
   ....: scaled /= scaled.max()
   ....: scaled
   ....: 
Out[93]: 
발생검거        검거                                           발생                                        
죄종          강간        강도   살인        절도        폭력        강간        강도        살인        절도        폭력
자치구                                                                                                
강남구   1.000000  0.652174  0.8  1.000000  1.000000  1.000000  0.941176  0.916667  1.000000  1.000000
강동구   0.073770  0.217391  0.1  0.265358  0.393422  0.155620  0.058824  0.166667  0.467528  0.437969
강북구   0.086066  0.434783  0.6  0.119454  0.435054  0.146974  0.529412  0.416667  0.133118  0.415445
강서구   0.352459  0.434783  0.6  0.667235  0.589092  0.461095  0.470588  0.416667  0.370649  0.614945
관악구   0.475410  0.478261  0.6  0.297782  0.557452  0.628242  0.411765  0.583333  0.589523  0.647479
..         ...       ...  ...       ...       ...       ...       ...       ...       ...       ...
용산구   0.278689  0.478261  0.3  0.093003  0.166944  0.265130  0.529412  0.250000  0.177252  0.201287
은평구   0.147541  0.130435  0.1  0.198805  0.417569  0.184438  0.235294  0.083333  0.305346  0.416875
종로구   0.229508  0.260870  0.3  0.306314  0.261449  0.314121  0.352941  0.333333  0.402225  0.288166
중구    0.024590  0.130435  0.0  0.325085  0.275187  0.195965  0.235294  0.083333  0.532831  0.263497
중랑구   0.176230  0.260870  1.0  0.299488  0.459617  0.244957  0.352941  0.916667  0.384643  0.486235

[25 rows x 10 columns]

인덱스를 다중으로 변경하자.

In [94]: midx = pd.MultiIndex.from_product([['검거율'], arrest.columns.values])
   ....: arrest.columns = midx
   ....: 

발생 건수에 대한 스케일링과 검거율을 합치자.

In [96]: pd.concat([scaled[['발생']], arrest], axis=1)
Out[96]: 
발생검거        발생                                                검거율                                              
죄종          강간        강도        살인        절도        폭력         강간          강도          살인         절도         폭력
자치구                                                                                                            
강남구   1.000000  0.941176  0.916667  1.000000  1.000000  77.728285   85.714286   76.923077  42.857143  86.484594
강동구   0.155620  0.058824  0.166667  0.467528  0.437969  78.846154  100.000000   75.000000  33.347422  82.890855
강북구   0.146974  0.529412  0.416667  0.133118  0.415445  82.352941   92.857143  100.000000  43.096234  88.637222
강서구   0.461095  0.470588  0.416667  0.370649  0.614945  72.900763  100.000000  100.000000  60.114504  84.752105
관악구   0.628242  0.411765  0.583333  0.589523  0.647479  69.062500  100.000000   88.888889  30.561715  80.109157
..         ...       ...       ...       ...       ...        ...         ...         ...        ...        ...
용산구   0.265130  0.529412  0.250000  0.177252  0.201287  89.175258  100.000000  100.000000  37.700706  83.121951
은평구   0.184438  0.235294  0.083333  0.305346  0.416875  84.939759   66.666667  100.000000  37.147335  86.920467
종로구   0.314121  0.352941  0.333333  0.402225  0.288166  76.303318   81.818182   83.333333  38.324176  84.212822
중구    0.195965  0.235294  0.083333  0.532831  0.263497  65.294118   66.666667   66.666667  33.712716  88.309353
중랑구   0.244957  0.352941  0.916667  0.384643  0.486235  79.144385   81.818182   92.307692  38.829040  84.545135

[25 rows x 10 columns]

heatmap을 이용해서 검거율 상황을 살펴보자. 정렬을 위해 임시로 order 열을 하나 만들자.

In [97]: arrest['order'] = arrest.sum(axis=1)

seaborn을 이용해서 heatmap을 그리자.

In [98]: from matplotlib import font_manager, rc
   ....: font_name = font_manager.FontProperties(fname="c:/Windows/Fonts/malgun.ttf").get_name()
   ....: rc('font', family=font_name)
   ....: import seaborn as sns
   ....: ax = sns.heatmap(arrest.sort_values('order', ascending=False).iloc[:, :5], annot=True, fmt='.2f')
   ....: ax.set_xlabel('')
   ....: ax.set_title('범죄 검거 비율')
   ....: ax.figure.tight_layout()
   ....: 
Out[103]: Text(0.5,27.4219,'')
Out[104]: Text(0.5,1,'범죄 검거 비율')
_images/plot_arrest_heatmap.png

정규화된 발생 건수에 대한 히트맵을 그려보자.

In [106]: nrm = scaled['발생']
   .....: nrm['order'] = nrm.sum(axis=1)
   .....: ax.figure.clear()
   .....: ax = sns.heatmap(nrm.sort_values('order', ascending=False).iloc[:, :5], annot=True, fmt='.2f')
   .....: ax.set_title('범죄 비율')
   .....: ax.figure.tight_layout()
   .....: 
Out[110]: Text(0.5,1,'범죄 비율')
_images/plot_occur_heatmap.png

자료 지도 표시

폴리움을 이용해서 각 자치구별 자료를 지도에 표시한다.

디비에 있는 서울시 모든 자치구 경계 자료를 가져온다.

In [112]: conn = sqlite3.connect('./data/seoul.sqlite3')
   .....: cur = conn.cursor()
   .....: geo = cur.execute("SELECT geojson from geoinfo").fetchall()
   .....: 

자치구 경계를 하나로 합친다.

In [115]: import json
   .....: seoul_geo = None
   .....: for item, in geo:
   .....:   gu = json.loads(item)
   .....:   if seoul_geo == None:
   .....:     seoul_geo = gu
   .....:   else:
   .....:     seoul_geo['features'].extend(gu['features'])
   .....: 

서울시 모든 구의 경계 지도이다.

In [118]: import folium
   .....: choro_map = folium.Map(location=(37.557939871241885, 126.99417336148036), zoom_start=11,)
   .....: choro_map.choropleth(geo_data=seoul_geo)
   .....: 

범죄 발생 건수로 지도에 표시하자.

In [120]: crime_occurence = dfp['발생'].reset_index()
   .....: crime_occurence.head()
   .....: 
Out[121]: 
죄종  자치구   강간  강도  살인    절도    폭력
0   강남구  449  21  13  3850  4284
1   강동구  156   6   4  2366  2712
2   강북구  153  14   7  1434  2649
3   강서구  262  13   7  2096  3207
4   관악구  320  12   9  2706  3298

2015년 서울시 자치구별 강간 발생 건수이다.

In [122]: choro_map = folium.Map(location=(lat, lon), zoom_start=10)
   .....: choro_map.choropleth(geo_data=seoul_geo, \
   .....:       data=crime_occurence, \
   .....:       columns=['자치구', '강간'], \
   .....:       fill_color='YlOrRd', \
   .....:       key_on='feature.properties.sig_kor_nm', \
   .....:       name="강간", \
   .....:       legend_name='2015년 서울시 자치구 강간 발생 건수')
   .....: choro_map
   .....: 

2015년 서울시 자치구별 강간, 살인 발생 건수이다.

In [123]: choro_map.choropleth(geo_data=seoul_geo, \
   .....:       data=crime_occurence, \
   .....:       columns=['자치구', '살인'], \
   .....:       fill_color='YlOrRd', \
   .....:       key_on='feature.properties.sig_kor_nm', \
   .....:       name="살인 건수", \
   .....:       legend_name='2015년 서울시 자치구 살인 발생 건수')
   .....: folium.LayerControl().add_to(choro_map)
   .....: choro_map
   .....: 

경찰서 위치를 지도에 표시하자.

경찰서 경위도 정보를 가져온다.

In [124]: conn = sqlite3.connect('./data/seoul.sqlite3')
   .....: stn = pd.read_sql("select 경찰서, 자치구, 위도, 경도 from police_station", conn)
   .....: conn.close()
   .....: 
In [127]: map_police = folium.Map(location=(lat, lon), zoom_start=10)
   .....: for _, row in stn.iterrows():
   .....:   folium.Marker(
   .....:     location=(row['위도'], row['경도']),
   .....:     popup=row['경찰서']
   .....:   ).add_to(map_police)
   .....: map_police
   .....: 

강간 검거율을 지도에 표시하자.

검거율 자료에서 0레벨을 삭제하자.

In [128]: arrest.columns = arrest.columns.droplevel(level=0)
   .....: arrest
   .....: 
Out[129]: 
            강간          강도          살인         절도         폭력            
자치구                                                                     
강남구  77.728285   85.714286   76.923077  42.857143  86.484594  369.707384
강동구  78.846154  100.000000   75.000000  33.347422  82.890855  370.084431
강북구  82.352941   92.857143  100.000000  43.096234  88.637222  406.943540
강서구  72.900763  100.000000  100.000000  60.114504  84.752105  417.767372
관악구  69.062500  100.000000   88.888889  30.561715  80.109157  368.622261
..         ...         ...         ...        ...        ...         ...
용산구  89.175258  100.000000  100.000000  37.700706  83.121951  409.997915
은평구  84.939759   66.666667  100.000000  37.147335  86.920467  375.674229
종로구  76.303318   81.818182   83.333333  38.324176  84.212822  363.991830
중구   65.294118   66.666667   66.666667  33.712716  88.309353  320.649519
중랑구  79.144385   81.818182   92.307692  38.829040  84.545135  376.644434

[25 rows x 6 columns]

경찰서 위치 정보와 검거율을 합치자.

In [130]: stn_arrest = arrest.merge(stn, on='자치구')

CircleMarker의 반지름의 단위는 픽셀이다.

In [131]: map_stn_arrest = folium.Map(location=(lat, lon), zoom_start=12)
   .....: for _, row in stn_arrest.iterrows():
   .....:   folium.CircleMarker(
   .....:     location=(row['위도'], row['경도']),
   .....:     radius=int(row['강간']) / 2,
   .....:     popup=row['경찰서'],
   .....:     tooltip=str(int(row['강간']))+'%',
   .....:     color='#3186cc',
   .....:     fill=True,
   .....:     fill_color='#3186cc',
   .....:   ).add_to(map_stn_arrest)
   .....: map_stn_arrest
   .....: