💡대전광역시 버스 관련 데이터 시각화 및 분석
이번에 사용할 데이터는 대전광역시의 버스와 관련한 정보이다. 이 데이터를 활용하여 대전광역시의 버스 정류장 현황을 지도에 표시하고, 버스 이용이 많이 발생하는 자치구를 확인하려고 한다. 그리고 도로 안전을 위해 버스전용차로 단속카메라의 보완이 필요한 위치를 분석해보고자 한다.
활용한 데이터
대전광역시_버스정류장 현황
https://www.data.go.kr/data/15110461/fileData.do
대전광역시_시내버스 기반정보
https://www.data.go.kr/data/15081730/fileData.do
대전광역시_자치구별 인구이동 현황
https://www.data.go.kr/data/15062511/fileData.do
한국교통안전공단_대전광역시 최다 이용 정류장
https://www.data.go.kr/data/15074183/fileData.do
대전광역시_버스전용차로단속카메라위치정보
https://www.data.go.kr/data/15061885/fileData.do
라이브러리
Folium
pip install streamlit_folium folium
Folium은 Leaflet.js 라이브러리를 기반으로 하는 파이썬 라이브러리이다.
Python에서 지도를 생성하고, 여러 가지 마커, 다각형, 선 등을 추가하여 웹 상에서 보여줄 수 있다.
Matplotlib
pip install matplotlib
Matplotlib은 Python에서 데이터 시각화를 위해 사용하는 라이브러리이다.
선 그래프, 산점도, 히스토그램, 막대그래프 등 다양한 종류의 그래프를 생성할 수 있다.
geopy
pip install geopy
Geopy는 지리적 데이터를 다루기 위한 파이썬 라이브러리이다.
주소, 위도 및 경도 등과 같은 지리적 정보를 처리하고, 주어진 위치의 거리 및 방위를 계산하는 등의 기능을 제공한다.
Seaborn
pip install seaborn
Seaborn은 Matplotlib을 기반으로 한 파이썬 데이터 시각화 라이브러리이다.
Matplotlib보다 직관적인 인터페이스를 가지고 있어 보다 효과로 그래픽을 생성할 수 있다.
csv 파일 인코딩
먼저 CSV 파일의 인코딩 형식을 확인하였다. 주로 한글을 포함한 데이터는 UTF-8 또는 CP949(윈도우용)로 인코딩 되어 있는데, 이번에 데이터를 사용하려고 하니 인코딩 오류가 발생했다.
UTF-8로 데이터를 받아야 한글 깨짐 오류 없이 그래프에 나타낼 수 있었기 때문에 인코딩 형식을 변경하여 데이터를 불러왔다.
Matplotlib figure 객체 에러
Matplotlib의 전역 figure 객체를 사용하는 것이 스레드 안전하지 않다는 경고가 발생했다. 이를 해결하기 위해 코드에서 Matplotlib의 figure 객체를 명시적으로 전달하였다.
hangjeongdong_대전광역시.geojson
https://github.com/raqoon886/Local_HangJeongDong/blob/master/hangjeongdong_대전광역시.geojson
위 GeoJSON 파일은 대전시의 행정 구역에 대한 지리 정보를 담고 있다. 이를 사용하여 버스 정류장 현황을 지도에 표시하고, 자치구별 인구 이동 현황을 시각화할 수 있다.
📌시각화 및 분석 스크립트
import streamlit as st
from streamlit_folium import folium_static
import folium
from folium.plugins import MarkerCluster
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import matplotlib.font_manager as fm
plt.rc("font", family = "Malgun Gothic")
sns.set(font="Malgun Gothic", rc={"axes.unicode_minus":False}, style='white')
# 데이터 불러오기
bus_stops_data = pd.read_csv("대전광역시_버스정류장 현황_20221215.csv", encoding="utf-8")
top_bus_stops_data = pd.read_csv("한국교통안전공단_대전광역시 최다 이용 정류장_20221231.csv", encoding="utf-8")
population_movement_data = pd.read_csv("대전광역시_자치구별 인구이동 현황_20211231.csv", encoding="utf-8")
bus_lane_camera_data = pd.read_csv("대전광역시_버스전용차로단속카메라위치정보_20230328.csv", encoding="utf-8")
# 최다 이용 정류장 좌표 정의
most_used_bus_stops = {
"복합터미널(8001097)": {"위도": 36.349038, "경도": 127.437193},
"은하수네거리(8002453)": {"위도": 36.350414, "경도": 127.378035},
"복합터미널(8001096)": {"위도": 36.349013, "경도": 127.435667},
"유성온천역7번출구(8002412)": {"위도": 36.354377, "경도": 127.342016},
"대전역(8001418)": {"위도": 36.332539, "경도": 127.43213},
"대전역/역전시장(8001412)": {"위도": 36.329527, "경도": 127.43368},
"충남대학교(8002721)": {"위도": 36.36156, "경도": 127.344114},
"으능정이거리(8002437)": {"위도": 36.329101, "경도": 127.427446},
"반석역(6800944)": {"위도": 36.39154528, "경도": 127.3147684},
"갤러리아타임월드(8001078)": {"위도": 36.352716, "경도": 127.37907}
}
# Streamlit 앱 제목
st.title('대전광역시 버스 정류장 위치')
# 지도 생성 및 설정
m = folium.Map(location=[36.350411, 127.384548], zoom_start=12)
marker_cluster = MarkerCluster().add_to(m)
# 버스 정류장 마커 추가
for idx, row in bus_stops_data.iterrows():
if row['정류장명'] in most_used_bus_stops:
most_used_info = most_used_bus_stops[row['정류장명']]
folium.Marker(
location=[most_used_info['위도'], most_used_info['경도']],
popup=row['정류장명'],
icon=folium.Icon(color='red', icon='info-sign')
).add_to(m)
else:
folium.Marker(
location=[row['위도'], row['경도']],
popup=row['정류장명']
).add_to(marker_cluster)
# 지도 출력
st.subheader('버스 정류장 분포')
folium_static(m)
# 최다 이용 정류장 분석
st.subheader('최다 이용 정류장 분석')
# 최다 이용 정류장 선택
selected_bus_stop = st.selectbox('최다 이용 정류장을 선택하세요.', top_bus_stops_data['정류장명'])
# 전체 최다 이용 정류장 데이터프레임 출력
styled_most_used_bus_stops_data = top_bus_stops_data.copy()
styled_most_used_bus_stops_data['선택'] = styled_most_used_bus_stops_data['정류장명'].apply(lambda x: "📌" if x == selected_bus_stop else '')
st.write(styled_most_used_bus_stops_data)
# 선택한 정류장 위치 확인
if selected_bus_stop in most_used_bus_stops:
most_used_info = most_used_bus_stops[selected_bus_stop]
selected_stop_location = pd.DataFrame({'위도': [most_used_info['위도']], '경도': [most_used_info['경도']]})
else:
selected_stop_location = bus_stops_data.loc[bus_stops_data['정류장명'] == selected_bus_stop, ['위도', '경도']]
# 선택한 정류장이 데이터에 있는지 확인
if not selected_stop_location.empty:
selected_stop_location = selected_stop_location.iloc[0]
# 선택한 정류장을 강조하여 지도에 출력
m = folium.Map(location=[selected_stop_location['위도'], selected_stop_location['경도']], zoom_start=15)
# 선택한 정류장을 빨간 마커로 추가
folium.Marker(
location=[selected_stop_location['위도'], selected_stop_location['경도']],
popup=selected_bus_stop,
icon=folium.Icon(color='red', icon='star')
).add_to(m)
# 나머지 최다 이용 정류장들을 초록 마커로 개별적으로 추가
for idx, row in top_bus_stops_data.iterrows():
if row['정류장명'] != selected_bus_stop and row['정류장명'] in most_used_bus_stops:
most_used_info = most_used_bus_stops[row['정류장명']]
folium.Marker(
location=[most_used_info['위도'], most_used_info['경도']],
popup=row['정류장명'],
icon=folium.Icon(color='gray', icon='star')
).add_to(m)
# 나머지 정류장들을 클러스터로 그룹화하여 추가
other_stops = bus_stops_data[~bus_stops_data['정류장명'].isin(most_used_bus_stops.keys())]
marker_cluster = MarkerCluster().add_to(m)
for idx, row in other_stops.iterrows():
folium.Marker(
location=[row['위도'], row['경도']],
popup=row['정류장명']
).add_to(marker_cluster)
# 지도 출력
st.subheader('선택한 최다 이용 정류장 위치')
folium_static(m)
else:
st.error(f"Selected bus stop '{selected_bus_stop}' not found in the dataset.")
# 선택된 행의 배경색 지정
highlight_color = 'lightcoral'
# 선택된 행에 배경색을 적용하는 함수
def highlight_row(row):
if row['정류장명'] == selected_bus_stop:
return [f'background-color: {highlight_color}' for _ in row]
else:
return ['' for _ in row]
# 선택된 행에 배경색을 적용한 데이터프레임 생성
styled_selected_bus_stop_data = top_bus_stops_data[top_bus_stops_data['정류장명'] == selected_bus_stop].style.apply(highlight_row, axis=1)
# 스타일이 적용된 표 출력
st.write(styled_selected_bus_stop_data)
# 지도 생성
m = folium.Map(location=[36.350411, 127.384548], zoom_start=12)
marker_cluster = MarkerCluster().add_to(m)
# 최다 이용 정류장을 빨간 마커로 표시
for idx, row in top_bus_stops_data.iterrows():
if row['정류장명'] in most_used_bus_stops:
most_used_info = most_used_bus_stops[row['정류장명']]
folium.Marker(
location=[most_used_info['위도'], most_used_info['경도']],
popup=row['정류장명'],
icon=folium.Icon(color='red', icon='star')
).add_to(m)
# 버스전용차로단속기 위치를 녹색 마커로 표시
for idx, row in bus_lane_camera_data.iterrows():
folium.Marker(
location=[row['Y좌표'], row['X좌표']],
popup=row['버스전용차로단속기점'],
icon=folium.Icon(color='green', icon='camera')
).add_to(m)
# 버스정류장 위치를 클러스터로 그룹화하여 표시
for idx, row in bus_stops_data.iterrows():
folium.Marker(
location=[row['위도'], row['경도']],
popup=row['정류장명']
).add_to(marker_cluster)
# 지도 출력
st.subheader('버스전용차로단속기 위치, 최다 이용 정류장 및 버스정류장 현황')
st.markdown("녹색 마커: 버스전용차로단속기 위치, 빨간 마커: 최다 이용 정류장, 클러스터: 버스정류장 위치")
folium_static(m)
# 자치구별 인구 이동 데이터 그래프로 출력
st.subheader('자치구별 인구 이동 현황')
# 대전광역시 자치구별 인구 이동 현황 데이터에서 필요한 열만 추출
population_movement_data = population_movement_data[['행정구역(시군구)별', '총전입(명)', '총전출(명)', '순이동(명)']]
# 행정구역(시군구)별을 인덱스로 설정
population_movement_data.set_index('행정구역(시군구)별', inplace=True)
# 그래프 스타일 설정
plt.rc("font", family="Malgun Gothic")
sns.set(font="Malgun Gothic", rc={"axes.unicode_minus": False}, style='whitegrid')
# 그래프 크기 설정
plt.figure(figsize=(10, 6))
# 막대 그래프 그리기
ax = population_movement_data.plot(kind='bar', stacked=False)
# 그래프 위에 값 표시
for p in ax.patches:
ax.annotate(str(round(p.get_height(), 2)), (p.get_x() * 1.005, p.get_height() * 1.005))
# 그래프 제목과 축 라벨 설정
plt.title('대전광역시 자치구별 인구 이동 현황')
plt.xlabel('자치구')
plt.ylabel('인구 수')
# 범례 표시
plt.legend(loc='upper right')
# 그래프 출력
st.pyplot(plt)
# 선 그래프 데이터 설정
x = population_movement_data.index
y1 = population_movement_data['총전입(명)']
y2 = population_movement_data['총전출(명)']
y3 = population_movement_data['순이동(명)']
# 선 그래프 그리기
plt.figure(figsize=(10, 7))
plt.plot(x, y1, marker='o', label='총전입(명)', color='skyblue')
plt.plot(x, y2, marker='s', label='총전출(명)', color='salmon')
plt.plot(x, y3, marker='^', label='순이동(명)', color='lightgreen')
# 그래프 위에 값 표시
for i, txt in enumerate(y1):
plt.annotate(str(round(txt, 2)), (x[i], y1[i]), textcoords="offset points", xytext=(0,10), ha='center')
for i, txt in enumerate(y2):
plt.annotate(str(round(txt, 2)), (x[i], y2[i]), textcoords="offset points", xytext=(0,10), ha='center')
for i, txt in enumerate(y3):
plt.annotate(str(round(txt, 2)), (x[i], y3[i]), textcoords="offset points", xytext=(0,10), ha='center')
# 그래프 제목과 축 라벨 설정
plt.title('대전광역시 자치구별 인구 이동 현황')
plt.xlabel('자치구')
plt.ylabel('인구 이동 수')
# x축 라벨 회전
plt.xticks(rotation=45)
# 범례 표시
plt.legend()
# 그래프 출력
st.pyplot(plt)
먼저 필요한 라이브러리를 가져오고, 분석에 사용할 CSV 파일을 읽어왔다. 이때, 데이터의 인코딩을 UTF-8로 가져와 한글 데이터를 올바르게 읽을 수 있도록 했다. 최다 이용 정류장은 좌표를 따로 정의해 줘야 했기 때문에 시내버스 기반정보를 바탕으로 좌표를 가져왔다.
Streamlit 앱의 제목을 설정하고, Folium을 사용하여 지도를 생성하여 버스 정류장 데이터를 지도에 표시하는 방식으로 웹 애플리케이션을 만들었다.
버스 정류장 분포
버스 정류장 분포를 클러스터링과 마커로 시각화하여 지도에 나타냈다.
최다 이용 정류장 분석
최다 이용 정류장 데이터 프레임을 보여주고, 사용자가 선택한 최다 이용 정류장을 데이터 프레임과 지도에 표시하였다.
선택한 최다 이용 정류장 위치
사용자가 선택한 최다 이용 정류장의 위치를 빨간색으로 강조하여 지도에 표시하였다. 이때 사용자가 선택하지 않은 최다 이용 정류장은 회색으로 표시하였다.
버스전용차로단속기 위치, 최다 이용 정류장 및 버스정류장 현황
3개의 데이터를 한 번에 가져와 버스전용차로단속기 위치를 녹색 마커로 표시하고, 최다 이용 정류장을 빨간 마커로, 나머지 정류장을 클러스터로 그룹화하여 지도에 표시하였다.
자치구별 인구 이동 현황
자치구별 인구 이동 현황을 막대그래프와 선 그래프로 표시하였다.
이외에도, 불법 주정차 데이터, 주차장 주차면 데이터, 불법 주정차 단속 카메라 데이터 등을 활용하여 특정 시간대에 불법 주정차가 발생할 가능성이 높은 위치를 예측하거나, 주차장 시설의 부족한 지역 또는 불법 주정차가 잦은 구간 등 불법 주정차 문제가 심각한 지역을 식별하는 데에도 활용할 수 있다.
참고 자료
[실습3] 공공데이터 앱 제작 (1) 페이지 구성 및 그래프 출력, 엘리스코딩, 2024.02.09.