[zero-base/] DS Part 4. EDA - 25일차 스터디 노트

손윤재·2024년 1월 5일

제로베이스 DS 22기

목록 보기
26/55
post-thumbnail

실습 프로젝트 2️⃣

【서울시 범죄 현황 데이터 분석】

실습 목표 : 구별 범죄 현황과 경찰서별 검거율을 지도에 시각화


1. 분석 데이터 개요 확인

🔰 기본 정보 확인

    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만개가 넘는다.

🔰 데이터 편집

  1. 특정 컬럼의 unique를 조사
    crime_raw_data["죄종"].unique()

    /*실행결과
        array(['살인', '강도', '강간', '절도', '폭력', nan], dtype=object)
    */
    👉 "죄종" 컬럼에 'nan'값이 들어가 있음을 확인할 수 있다.
  1. 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를 도출하기 어렵다.
데이터의 재편성이 필요하다.


2. 분석 데이터 정리

pivot_table()을 이용해 원하는 DataFrame 형태로 데이터를 재편성한다.

🔰 DataFrame 재편성

    import pandas as pd
    import numpy as np

    crime_station = crime_raw_data.pivot_table(
            index="구분",
            columns=['죄종', '발생검거'],
            aggfunc=np.sum
    )
    crime_station.head()
  • 경찰서 이름인 "구분" 컬럼을 Index로 설정해 정리한다.
  • “죄종”과 “발생검거” 컬럼을 columns로 지정한다.
  • 집계 함수의 Default가 '평균(mean)' 이므로 사건의 합이 기록되도록 aggfunc param에 ‘합(sum)’을 사용한다.

🔰 Multi Column 편집

  • 문제는 pivot_table()의 결과 컬럼이 멀티로 표현된다는 것이다.

  • 필요 없는 “건수” 컬럼을 제거한다.

    crime_station.columns = crime_station.columns.droplevel([0])

현재 Index는 경찰서의 이름으로 되어 있다.
우리의 목적은 "구별" 범죄 현황이므로 경찰서 이름을 구 이름으로 변경해야 한다.


3. 분석 데이터 추가

googlemaps를 활용해 경찰서의 이름으로 소속된 구 이름을 알아낸 후 구별로 데이터를 정리한다.

🔰 컬럼 추가

  • 경찰서가 소속된 구 이름과 위도, 경도 정보를 저장할 새로운 컬럼을 생성한다.
  • 새로 추가한 컬럼의 values를 모두 NaN으로 채운다.
    crime_station["구별"] = np.nan
    crime_station["lat"] = np.nan # latitude, 위도
    crime_station["lng"] = np.nan # longitude, 경도

🔰 새로운 데이터 추가

  • 경찰서 이름으로 googlemaps에서 위치 정보를 얻어와 필요한 데이터로 새로 추가한 컬럼 values를 채운다.
    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


4. "구별" 데이터로 정리

🔰 "구별" Index 지정

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

🔰 검거율 데이터 생성

  • 각 죄종별로 검거율(검거/발생*100)을 연산해 새로운 컬럼으로 추가한다.
    num = ["강간검거", "강도검거", "살인검거", "절도검거", "폭력검거"]
    den = ["강간발생", "강도발생", "살인발생", "절도발생", "폭력발생"]
    target = ["강간검거율", "강도검거율", "살인검거율", "절도검거율", "폭력검거율"]
    
    crime_anal_gu[target] = crime_anal_gu[num].div(crime_anal_gu[den].values) * 100
  • 검거율 값 중 100이 넘는 것들을 강제로 100을 만들어 준다.
    crime_anal_gu[crime_anal_gu[target] > 100] = 100


5. 범죄 데이터 정규화

🔰 정규화 데이터 생성

  • 구별 데이터에서 범죄별 발생 건수를 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]

🔰 서울시 CCTV 데이터 추가

  • 서울시 CCTV 자료에서 구별 인구수와 CCTV수를 가져와 추가한다.
    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)
  • 5대 범죄 검거율의 평균을 구해 '검거'의 대표값으로 사용한다.
    col = ["강간검거율", "강도검거율", "살인검거율", "절도검거율", "폭력검거율"]

    crime_anal_norm["검거"] = np.mean(crime_anal_norm[col], axis=1)

6. 분석 데이터 지도 시각화

구별 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

🔰 경찰서별 검거율

  • 5대 범죄 검거 건수를 정규화한 후 평균을 낸 값을 “검거” 대표값으로 사용한다.
    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)

  • 검거율이 높을수록 원이 커지도록 검거율을 원의 radius로 사용한다.
    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

profile
ISTP(정신승리), To Be Data Scientist

0개의 댓글