WGS84 좌표 기반 데이터 집계 및 시각화

공공 데이터를 다루다 보면, WGS84 좌표계 즉, 위경도 좌표 정보가 포함되어 있는 경우가 종종 있다. 특정 위치를 기준으로 근거리 내에 관심 있는 관측치가 얼마나 존재하는지 또는 어디에 분포하는지 등을 확인하고 싶을때가 있다. 사각 범위 혹은 반경 범위 내에 위치하고 있는 데이터들을 집계하고 시각화해보겠다.


필요한 라이브러리 불러오기

해당 작업을 진행하기 위해서는 몇 가지 라이브러리들이 필요하다. geopy.distance의 great_circle 메서드는 좌표 간 거리를 km 단위로 변환해준다. pandas는 테이블 데이터 분석에 최적화된 라이브러리이며, folium은 지도 기반 데이터 시각화가 필요한 경우에 유용한 라이브러리이다.

from geopy.distance import great_circle
import pandas as pd
import folium


CountByWGS84 클래스 정의

관심 지역을 기준으로 근거리에 있는 데이터를 집계하고 플로팅하기 위해서 CountByWGS84 클래스를 정의한다. 최초 입력값으로 데이터 프레임, 위도, 경도, 기준 거리로 초기화한다. 기준 거리(dist)는 미입력 시 기본값을 1km로 설정해두었다.

class CountByWGS84:

    def __init__(self, df, lat, lon, dist=1):
        """
        df: 데이터 프레임
        lat: 중심 위도
        lon: 중심 경도
        dist: 기준 거리(km)
        """
        self.df = df
        self.lat = lat
        self.lon = lon
        self.dist = dist


사각 범위 내 데이터 필터링 메서드

중심 위도, 중심 경도를 기준으로 기준 거리 만큼 더하고 빼는 간단한 연산을 통해 상하좌우 경계점의 위도, 경도를 구한다. 이 때, 위도는 기준 거리에 0.01을 곱하고, 경도는 기준 거리에 0.015를 곱하는 연산이 존재한다. 통상 위도 사이의 거리에 0.01을 곱하면 약 1km가 되고, 경도의 경우에는 0.015를 곱하면 약 1km가 된다고 알려져 있다.self.points에는 사각 범위 내 데이터 플로팅 시 사각형의 경계 마커를 추가할 때 활용할 좌표값들을 리스트로 지정한다. 다음으로는 데이터 세트에서 상하좌우 경계점을 기준으로 사각 범위 영역 안에 포함되는 관측치들을 필터링하고 그 값을 반환하도록 한다.

    def filter_by_rectangle(self):
        """
        사각 범위 내 데이터 필터링
        """
        lat_min = self.lat - 0.01 * self.dist
        lat_max = self.lat + 0.01 * self.dist

        lon_min = self.lon - 0.015 * self.dist
        lon_max = self.lon + 0.015 * self.dist

        self.points = [[lat_min, lon_min], [lat_max, lon_max]]

        result = self.df.loc[
            (self.df['lat'] > lat_min) &
            (self.df['lat'] < lat_max) &
            (self.df['lon'] > lon_min) &
            (self.df['lon'] < lon_max)
        ]
        result.index = range(len(result))

        return result


반경 범위 내 데이터 필터링 메서드

데이터 세트 내 모든 관측치들과 중심 좌표 사이의 거리를 구하고 기준 거리와 비교하는 것은 꽤 비효율적이다. 위에서 정의한 사각 범위 내 데이터 필터링 메서드를 일차적으로 이용하여 필터링하고, 필터링된 결과 tmp에 한하여 좌표 사이의 거리를 구해 기준 거리와 비교하는 메서드를 정의한다. 연산 범위가 좁혀지므로 모든 거리를 구하는 경우 보다 훨씬 효율적이다.

    def filter_by_radius(self):
        """
        반경 범위 내 데이터 필터링
        """
        # 사각 범위 내 데이터 필터링
        tmp = self.filter_by_rectangle()

        # 기준 좌표 포인트
        center = (self.lat, self.lon)

        result = pd.DataFrame()
        for index, row in tmp.iterrows():
            # 개별 좌표 포인트
            point = (row['lat'], row['lon'])
            d = great_circle(center, point).kilometers
            if d <= self.dist:
                result = pd.concat([result, tmp.iloc[index, :].to_frame().T])

        result.index = range(len(result))

        return result


사각 범위 내 데이터 시각화 메서드

사각 범위 내 데이터를 필터링한 결과를 바탕으로 지도 상에 시각화하는 함수를 정의한다.

    def plot_by_rectangle(self, df):
        """
        사각 범위 내 데이터 플로팅
        """

        m = folium.Map(location=[self.lat, self.lon], zoom_start=14)

        for idx, row in df.iterrows():

            lat_ = row['lat']
            lon_ = row['lon']

            folium.Marker(location=[lat_, lon_],
                          radius=15,
                          tooltip=row['지점명']).add_to(m)

        folium.Rectangle(bounds=self.points,
                         color='#ff7800',
                         fill=True,
                         fill_color='#ffff00',
                         fill_opacity=0.2).add_to(m)

        return m


반경 범위 내 데이터 시각화 메서드

반경 범위 내 데이터를 필터링한 결과를 바탕으로 지도 상에 시각화하는 함수를 정의한다.

    def plot_by_radius(self, df):
        """
        반경 범위 내 데이터 플로팅
        """

        m = folium.Map(location=[self.lat, self.lon], zoom_start=14)

        for idx, row in df.iterrows():

            lat_ = row['lat']
            lon_ = row['lon']

            folium.Marker(location=[lat_, lon_],
                          radius=15,
                          tooltip=row['지점명']).add_to(m)

        folium.Circle(radius=dist * 1000,
                      location=[lat, lon],
                      color="#ff7800",
                      fill_color='#ffff00',
                      fill_opacity=0.2
                      ).add_to(m)

        return m


위경도 정보가 포함된 데이터 불러오기

WGS84 좌표계 즉, 위경도 좌표 정보가 포함된 데이터를 불러온다. 여기에서는 공공데이터포털에서 얻을 수 있는 소상공인 상가업소 데이터 중 서울 지역 데이터를 샘플로 설정하였다. 해당 데이터를 불러와 상호명이 스타벅스인 경우만 필터링하였다. 위에서 정의한 클래스에서 위도, 경도 값은 데이터 프레임 내에서 각각 lat, lon이라는 컬럼 명으로 간주하고 있으므로, 샘플 데이터의 컬럼명을 동일하게 재지정해준다.

# Sample Dataset
df = pd.read_csv("./sample/sample.csv", sep="|")
df = df.loc[df['상호명'].str.contains("스타벅스", na=False)]
df = df.rename(columns={'경도': 'lon', '위도': 'lat'})
df.index = range(len(df))


파라미터 설정

관심 지역의 위도, 경도 값과 기준 거리를 설정한다. 여기에서는 신논현역 부근의 좌표를 예시로 인스턴스를 생성하도록 한다.

# 파라미터 설정
lat = 37.50229485705552
lon = 127.02449138906029
dist = 1


인스턴스 생성

위에서 정의한 파라미터 값들을 이용해 아래와 같이 cbw 인스턴스를 생성한다.

# 반경 집계 인스턴스 생성
cbw = CountByWGS84(df, lat, lon, dist)


필터링 결과 확인

사각 범위 내 데이터 필터링 메서드와 반경 범위 내 데이터 필터링 메서드를 실행하면 다음과 같은 결과를 얻을 수 있다. 지정한 위치를 기준으로 상하좌우 1km에 해당하는 사각 범위 내 스타벅스 매장과 반경 1km 범위 내 스타벅스 매장이 각각 구해진다.

# 사각 범위 내 데이터 필터링
result_rectangle = cbw.filter_by_rectangle()

# 반경 범위 내 데이터 필터링
result_radius = cbw.filter_by_radius()

print(f"""
{"="*50}
중심 위도: {cbw.lat}
중심 경도: {cbw.lon}
기준 거리: {cbw.dist} km
사각 범위 내 데이터 필터링 결과: {len(result_rectangle):,} 건
반경 범위 내 데이터 필터링 결과: {len(result_radius):,}{"="*50}
""")
==================================================
중심 위도: 37.50229485705552
중심 경도: 127.02449138906029
기준 거리: 1 km
사각 범위 내 데이터 필터링 결과: 36 건
반경 범위 내 데이터 필터링 결과: 18 건
==================================================


플로팅 결과 확인

사각 범위 내 데이터 시각화 결과

plot_1 = cbw.plot_by_rectangle(result_rectangle)
plot_1


반경 범위 내 데이터 시각화 결과

plot_2 = cbw.plot_by_radius(result_radius)
plot_2


전체 소스 코드

from geopy.distance import great_circle
import pandas as pd
import folium

class CountByWGS84:

    def __init__(self, df, lat, lon, dist=1):
        """
        df: 데이터 프레임
        lat: 중심 위도
        lon: 중심 경도
        dist: 기준 거리(km)
        """
        self.df = df
        self.lat = lat
        self.lon = lon
        self.dist = dist

    def filter_by_rectangle(self):
        """
        사각 범위 내 데이터 필터링
        """
        lat_min = self.lat - 0.01 * self.dist
        lat_max = self.lat + 0.01 * self.dist

        lon_min = self.lon - 0.015 * self.dist
        lon_max = self.lon + 0.015 * self.dist

        self.points = [[lat_min, lon_min], [lat_max, lon_max]]

        result = self.df.loc[
            (self.df['lat'] > lat_min) &
            (self.df['lat'] < lat_max) &
            (self.df['lon'] > lon_min) &
            (self.df['lon'] < lon_max)
        ]
        result.index = range(len(result))

        return result

    def filter_by_radius(self):
        """
        반경 범위 내 데이터 필터링
        """
        # 사각 범위 내 데이터 필터링
        tmp = self.filter_by_rectangle()

        # 기준 좌표 포인트
        center = (self.lat, self.lon)

        result = pd.DataFrame()
        for index, row in tmp.iterrows():
            # 개별 좌표 포인트
            point = (row['lat'], row['lon'])
            d = great_circle(center, point).kilometers
            if d <= self.dist:
                result = pd.concat([result, tmp.iloc[index, :].to_frame().T])

        result.index = range(len(result))

        return result

    def plot_by_rectangle(self, df):
        """
        사각 범위 내 데이터 플로팅
        """

        m = folium.Map(location=[self.lat, self.lon], zoom_start=14)

        for idx, row in df.iterrows():

            lat_ = row['lat']
            lon_ = row['lon']

            folium.Marker(location=[lat_, lon_],
                          radius=15,
                          tooltip=row['지점명']).add_to(m)

        folium.Rectangle(bounds=self.points,
                         color='#ff7800',
                         fill=True,
                         fill_color='#ffff00',
                         fill_opacity=0.2).add_to(m)

        return m

    def plot_by_radius(self, df):
        """
        반경 범위 내 데이터 플로팅
        """

        m = folium.Map(location=[self.lat, self.lon], zoom_start=14)

        for idx, row in df.iterrows():

            lat_ = row['lat']
            lon_ = row['lon']

            folium.Marker(location=[lat_, lon_],
                          radius=15,
                          tooltip=row['지점명']).add_to(m)

        folium.Circle(radius=dist * 1000,
                      location=[lat, lon],
                      color="#ff7800",
                      fill_color='#ffff00',
                      fill_opacity=0.2
                      ).add_to(m)

        return m

카테고리:

업데이트:

댓글남기기