from IPython.display import Image

3.2. 공간자료 시각화#

공간데이터#

  1. 공간 자료(공간 데이터)란 각 관측치마다 공간에 대한 정보를 가지고 있는 데이터이며, 공간 정보(위치 정보)는 위도(lattitude)와 경도(longitude)로 표현한다.

  2. 위경도는 지리 좌표계(Geographic coordinate system)을 기반으로 생성된다. ex) TM 좌표계(EPSG:2097), WGS84좌표계(EPSG:4326), UTM-K좌표계(EPSG:5179)등이 존재한다.

위경도

  1. 같은 위경도 좌표더라도 각 좌표계에 따라 다른 값을 가진다. 그러므로 공간데이터를 다루는데 있어 가장 중요한 것이 이 좌표계이다.

  2. 예를들어, 서울 롯데타워의 위치의 좌표는 각 좌표계에 따라 다음과 같이 나타난다.

    1. 위도 : 37.67777777777778 N, 경도 : 127.09805555555555 E

    2. TM좌표계 : (565937.9846, 32211.3849)

    3. UTM-K좌표계 : (1964325.7452, 964556.428)

  3. 공간 정보는 지도에 위치를 표현할때, 기본적으로 점(point)로 표현을 한다. 또한 이 점들을 연결하여 선(line), 이 선들을 연결한 다각형(polygon)형태로 저장된다. 이 개념은 다음의 그림으로 표현할 수 있다.

기본

파이썬에서는 이런 공간데이터를 다루기 위해 geopandas 패키지가 구현되어 있고, 좌표계를 다루기 위해 pyproj 패키지가 구현되어 있다.

사용할 패키지 불러오기#

import pandas as pd
import geopandas as gpd
import pyproj
from pyproj import crs
import matplotlib.pyplot as plt

좌표계가 포함된 데이터 읽기#

화재출동 현황 데이터를 이용하여 좌표를 입력하고, 이를 지도에 표현해 보자.

fire_dat = pd.read_csv("https://uos-bigdata.github.io/lab_data/docs/assets/data_lab_fire/fire.csv")
fire_dat.columns
Index(['wrinvstg_no', 'fire_type_nm', 'buld_srtfrm', 'buld_strctr',
       'buld_srtrf', 'buld_strct_dong_cnt', 'ground_nof', 'bstory_cnt',
       'totar', 'bottom_area', 'buld_sttus_nm', 'dth_cnt', 'injpsn_cnt',
       'dth_hnl_dmge_cnt', 'prprty_dmge_amt', 'fire_ocrn_yr', 'season_se_nm',
       'qtr_se', 'fire_ocrn_ymd', 'fire_ocrn_tm', 'fire_ocrn_mnth',
       'fire_ocrn_day', 'fire_ocrn_hour', 'fire_ocrn_min', 'daywk', 'frstt_nm',
       'ward_nm', 'lfdau_nm', 'dsp_reqre_time', 'fire_supesn_time', 'sido_nm',
       'sigungu_nm', 'emd_nm', 'cty_frmvl_se_nm', 'emd_se_nm', 'gis_x_axis',
       'gis_y_axis', 'longitude', 'la', 'spt_frstt_dist',
       'spt_safe_cnter_dist', 'spt_lfdau_dist', 'ign_htsrc_nm',
       'ign_htsrc_sclas_nm', 'ign_fctr_lclas_nm', 'ign_fctr_sclas_nm',
       'frst_igobj_lclas_nm', 'frst_igobj_sclas_nm', 'ign_mhrls_lclas_nm',
       'ign_mhrls_sclas_nm', 'cmbs_expobj_lclas_nm', 'cmbs_expobj_sclas_nm',
       'fclty_place_lclas_nm', 'fclty_place_mclas_nm', 'fclty_place_sclas_nm',
       'ign_floor_nm', 'soot_area', 'fio_join_trgt_yn', 'frmng_trgt_yn',
       'mub_yn', 'emrg_crg_oper_yn', 'vhcle_place', 'vhcle_ign_bhf',
       'fld_fire_se', 'fld_fire_ign_bhf', 'time_unit_tmprt',
       'time_unit_rainqty', 'time_unit_ws', 'time_unit_wd',
       'time_unit_humidity', 'time_unit_msnf', 'time_unit'],
      dtype='object')

화재출동 현황 데이터를 보면, longitude변수, la변수가 각각 경도, 위도를 의미하는 변수이다.

fire_dat.crs
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[4], line 1
----> 1 fire_dat.crs

File D:\anaconda3\lib\site-packages\pandas\core\generic.py:5902, in NDFrame.__getattr__(self, name)
   5895 if (
   5896     name not in self._internal_names_set
   5897     and name not in self._metadata
   5898     and name not in self._accessors
   5899     and self._info_axis._can_hold_identifiers_and_holds_name(name)
   5900 ):
   5901     return self[name]
-> 5902 return object.__getattribute__(self, name)

AttributeError: 'DataFrame' object has no attribute 'crs'

데이터를 읽었을때, 데이터에 좌표계가 존재하지 않기 때문에 crs(Coordinate Reference System)함수가 에러를 나타낸다. 하지만, 좌표계가 있는데 왜 이런 에러를 발생시킬까??

데이터에 저장되있는 좌표는 컴퓨터 기준에서 단순 숫자에 지나지 않는다. 즉, fire_dat데이터 프레임에 저장된 la변수에 저장된 값 37.552875는 그냥 숫자일 뿐 좌표가 아니다. 그러므로, 이 숫자가 좌표라는 것을 입력해줘야 한다.

화재출동 현황 데이터에 있는 변수 longitudela는 EPSG:4326 좌표계를 의미한다.. 그러므로 변수들 la(위도), longitude(경도)를 좌표계로 지정해 준다.

fire_dat = gpd.GeoDataFrame(fire_dat, geometry = gpd.points_from_xy(fire_dat['longitude'], fire_dat['la']), crs='EPSG:4326')
fire_dat.crs
<Geographic 2D CRS: EPSG:4326>
Name: WGS 84
Axis Info [ellipsoidal]:
- Lat[north]: Geodetic latitude (degree)
- Lon[east]: Geodetic longitude (degree)
Area of Use:
- name: World.
- bounds: (-180.0, -90.0, 180.0, 90.0)
Datum: World Geodetic System 1984 ensemble
- Ellipsoid: WGS 84
- Prime Meridian: Greenwich

geopandas 패키지에 GeoDataFrame함수를 이용하여 geometry 정보를 입력하는 방법이다. 위 데이터에서는 위도는 la, 경도는 longitude으로 입력하고, crs함수를 이용해 확인하였다.

이제 위경도를 입력한 데이터에 전국 법정동 좌표를 가져와 데이터를 합쳐서 그림을 표현하자.

전국 법정동 좌표 정보 데이터#

예시로 사용하기위한 서울특별시 법정동 좌표 데이터는 shp파일로 저장되어 있다.

  • shp 파일 : csv, xlsx과 다르게 위치 정보(geometry)를 포함하고 있는 파일로, 확장자가 dbf, prj, shp, shx 4가지 파일이 모두 있어야 사용 가능하다. (prj는 없어도 가능, 이유는 prj파일이 좌표계 정보를 가지고 있는 파일이기 때문이다.)

seoul_map = gpd.read_file("https://uos-bigdata.github.io/lab_data/docs/assets/data_lab_fire/seoul_shp.shp")

seoul_map 데이터의 정보는 다음과 같다.

seoul_map.info()
seoul_map
<class 'geopandas.geodataframe.GeoDataFrame'>
RangeIndex: 467 entries, 0 to 466
Data columns (total 8 columns):
 #   Column      Non-Null Count  Dtype   
---  ------      --------------  -----   
 0   fid         467 non-null    float64 
 1   bjd_code    467 non-null    float64 
 2   shape_area  467 non-null    float64 
 3   sido        467 non-null    object  
 4   si          467 non-null    object  
 5   sgg         467 non-null    object  
 6   bjd         467 non-null    object  
 7   geometry    467 non-null    geometry
dtypes: float64(3), geometry(1), object(4)
memory usage: 29.3+ KB
fid bjd_code shape_area sido si sgg bjd geometry
0 1.0 1.111010e+09 7.926179e+05 서울특별시 종로구 종로구 청운동 POLYGON ((953700.022 1954605.065, 953693.871 1...
1 2.0 1.111010e+09 9.882172e+04 서울특별시 종로구 종로구 신교동 POLYGON ((953233.465 1953996.984, 953235.183 1...
2 3.0 1.111010e+09 7.327165e+04 서울특별시 종로구 종로구 궁정동 POLYGON ((953560.228 1954257.466, 953561.190 1...
3 4.0 1.111010e+09 5.940365e+04 서울특별시 종로구 종로구 효자동 POLYGON ((953519.843 1953890.785, 953518.489 1...
4 5.0 1.111010e+09 5.939929e+04 서울특별시 종로구 종로구 창성동 POLYGON ((953516.123 1953734.362, 953516.526 1...
... ... ... ... ... ... ... ... ...
462 463.0 1.174011e+09 4.690167e+06 서울특별시 강동구 강동구 암사동 POLYGON ((968514.203 1950677.234, 968505.336 1...
463 464.0 1.174011e+09 3.057341e+06 서울특별시 강동구 강동구 천호동 POLYGON ((968336.280 1950222.697, 968337.437 1...
464 465.0 1.174011e+09 1.956188e+06 서울특별시 강동구 강동구 성내동 POLYGON ((967686.073 1948534.011, 967685.029 1...
465 466.0 1.174010e+09 1.611551e+06 서울특별시 강동구 강동구 길동 POLYGON ((969147.362 1949613.535, 969164.491 1...
466 467.0 1.174011e+09 2.457828e+06 서울특별시 강동구 강동구 둔촌동 POLYGON ((969669.593 1948748.489, 969656.716 1...

467 rows × 8 columns

지도에 화재 사고 표시#

이제, 화재출동 현황데이터를 법정동 지도 위에 나타내보자. 우선 서울 전체가 아닌 은평구에 대해서만 추출하여 표현해보자.

법정동 지도 데이터 및 화재출동 현황자료에서 은평구만 뽑는다.

tmp_ind = fire_dat["sigungu_nm"] == "은평구"
jg_fire_dat = fire_dat.loc[tmp_ind, :]

jg_fire_dat.crs
<Geographic 2D CRS: EPSG:4326>
Name: WGS 84
Axis Info [ellipsoidal]:
- Lat[north]: Geodetic latitude (degree)
- Lon[east]: Geodetic longitude (degree)
Area of Use:
- name: World.
- bounds: (-180.0, -90.0, 180.0, 90.0)
Datum: World Geodetic System 1984 ensemble
- Ellipsoid: WGS 84
- Prime Meridian: Greenwich
tmp_ind = seoul_map["sgg"] == "은평구"
jg_map_dat = seoul_map.loc[tmp_ind, :]

jg_map_dat.crs
<Projected CRS: EPSG:5179>
Name: Korea 2000 / Unified CS
Axis Info [cartesian]:
- X[north]: Northing (metre)
- Y[east]: Easting (metre)
Area of Use:
- name: Republic of Korea (South Korea) - onshore and offshore.
- bounds: (122.71, 28.6, 134.28, 40.27)
Coordinate Operation:
- name: Korea Unified Belt
- method: Transverse Mercator
Datum: Geocentric datum of Korea
- Ellipsoid: GRS 1980
- Prime Meridian: Greenwich

하지만, 두 데이터의 좌표계는 서로 다름을 확인 할 수 있다. 이는 맨 처음 설명했듯이, 위치는 같은 곳을 의미하지만, 다른 지도에서의 좌표를 의미한다. 그러므로 이 둘의 좌표계를 맞춘다.

이를 위해 jg_fire_dat의 좌표계를 “EPSG:5179”좌표계로 맞춘다.

jg_fire_dat = jg_fire_dat.to_crs(5179)

jg_fire_dat.crs
<Projected CRS: EPSG:5179>
Name: Korea 2000 / Unified CS
Axis Info [cartesian]:
- X[north]: Northing (metre)
- Y[east]: Easting (metre)
Area of Use:
- name: Republic of Korea (South Korea) - onshore and offshore.
- bounds: (122.71, 28.6, 134.28, 40.27)
Coordinate Operation:
- name: Korea Unified Belt
- method: Transverse Mercator
Datum: Geocentric datum of Korea
- Ellipsoid: GRS 1980
- Prime Meridian: Greenwich

좌표가 “EPSG:5179”로 맞춰진것을 알 수 있다. 따라서, 이를 그림으로 그리면 다음과 같다.

fig, ax = plt.subplots(figsize = (7, 7))
jg_map_dat.plot(ax = ax, linewidth = 1, edgecolor = 'black', facecolor = "white")
jg_fire_dat.plot(ax = ax, color = 'red', markersize = 5)
<Axes: >
../../_images/715f97020e08fa02359ee501e8da93d9263e49af26ad4ce00023291b119559b5.png

즉, 위 그림은 서울특별시 은평구에 법정동 단위로 2017년부터 2021년까지 화재가 발생한 위치를 나타낸 지도이다. 이를 2021년에 대해 서울 전역으로 살펴보자.

fire_dat = fire_dat.to_crs(5179)

tmp_ind = fire_dat["fire_ocrn_yr"] == 2021
sub_fire_dat = fire_dat.loc[tmp_ind, :]

fig, ax = plt.subplots(figsize = (7, 7))
seoul_map.plot(ax = ax, linewidth = 1, edgecolor = 'black', facecolor = "white")
sub_fire_dat.plot(ax = ax, color = 'red', markersize = 2)
<Axes: >
../../_images/b6171d191ac39eb5eb51bd581b7ccdb07cc827943229d4569bb063404c508c41.png

이 중, 여름과 겨울을 구분하여 점으로 표현하면 다음 그래프로 나타난다.

sum_ind = sub_fire_dat["season_se_nm"] == "여름"
sum_fire_dat = sub_fire_dat.loc[sum_ind, :]

win_ind = sub_fire_dat["season_se_nm"] == "겨울"
win_fire_dat = sub_fire_dat.loc[win_ind, :]

fig, ax = plt.subplots(figsize = (7, 7))
seoul_map.plot(ax = ax, linewidth = 1, edgecolor = 'black', facecolor = "white")
win_fire_dat.plot(ax = ax, color = 'blue', markersize = 2)
sum_fire_dat.plot(ax = ax, color = 'red', markersize = 2)
<Axes: >
../../_images/ac73abe83a8186da82561f2a12dfde5adf3857907ea8c5156c69e2faf21a1305.png

다음으로, 점 그래프가 아닌 그 값에 따른 값을 지도에 채우는 것을 그려보자. 이번 예제는 2021년에 총 출동횟수를 각 법정동별로 나타내보자. 출동한 연도를 나타낸 변수는 fire_ocrn_yr이다.

year_ind = fire_dat['fire_ocrn_yr'] == 2021
year_dat = fire_dat.loc[year_ind, :]
year_dat["count"] = 1
C:\Users\hgkang\AppData\Roaming\Python\Python310\site-packages\geopandas\geodataframe.py:1538: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  super().__setitem__(key, value)

groupby함수와 agg함수, sum함수를 이용하여 2021년에 법정동에 대해 총 출동횟수를 계산한다.

count_dat = year_dat.groupby(["sido_nm", 'sigungu_nm', "emd_nm"])["count"].agg('sum')
count_dat = count_dat.reset_index()
count_dat
sido_nm sigungu_nm emd_nm count
0 서울특별시 강남구 개포동 30
1 서울특별시 강남구 논현동 78
2 서울특별시 강남구 대치동 39
3 서울특별시 강남구 도곡동 18
4 서울특별시 강남구 삼성동 42
... ... ... ... ...
389 서울특별시 중랑구 면목동 81
390 서울특별시 중랑구 묵동 23
391 서울특별시 중랑구 상봉동 30
392 서울특별시 중랑구 신내동 18
393 서울특별시 중랑구 중화동 30

394 rows × 4 columns

merge함수를 이용하여 두 데이터(법정동 지도 데이터, 법정동별 출동건수 데이터)를 merge하는데, 기준이 되는 변수는 각각 sido, sgg, bjd, sido_nm, sigungu_nm, emd_nm이다.

merge_dat = pd.merge(seoul_map, count_dat, left_on = ["sido", "sgg", "bjd"], right_on = ["sido_nm", "sigungu_nm", "emd_nm"])

마지막으로 출동횟수에 대한 변수에 대해 그래프를 생성한다.

fig, ax = plt.subplots(figsize = (6, 6))
ax = merge_dat.plot(column = 'count', cmap = 'Greens', ax = ax, legend = True)

plt.show()
../../_images/94c4eacb894ab06580c78294a115ffcb9c83a6a8a22a73adaa4bf5b47b602aa5.png