Ch 02. 서울시 교통 데이터 분석
이 챕터에서는 서울 지하철 승하차 데이터를 처음부터 끝까지 분석합니다. 데이터를 가져와 정제하고, 질문을 던지고, 시각화로 답을 찾는 과정을 모두 경험합니다.
분석은 항상 질문에서 출발합니다. 오늘의 질문은 이렇습니다. "서울 지하철에서 가장 붐비는 역은 어디이고, 언제 사람이 가장 많을까?"
데이터 가져오기
서울 열린데이터 광장(data.seoul.go.kr) 또는 공공데이터 포털(data.go.kr)에서 '서울시 지하철 시간대별 이용객수'를 검색합니다. '서울교통공사 연도별 역별 일별 시간대별 승하차인원'이라는 이름으로 제공됩니다.
파일을 내려받아 data/subway.csv 경로에 저장합니다.
library(tidyverse)
# 파일을 읽습니다. EUC-KR 인코딩인 경우가 많습니다
df_raw <- read_csv(
"data/subway.csv",
locale = locale(encoding = "EUC-KR")
)
glimpse(df_raw)
실제 데이터를 바로 사용할 수 없는 경우를 위해 같은 구조의 예제 데이터를 만들어 실습할 수 있습니다.
library(tidyverse)
# 실습용 예제 데이터를 생성합니다 (실제 데이터 구조와 동일)
set.seed(42)
stations <- c(
"강남", "홍대입구", "신촌", "잠실", "구로디지털단지",
"사당", "신림", "건대입구", "왕십리", "선릉"
)
lines <- c("2호선", "2호선", "2호선", "2호선", "2호선",
"2호선", "2호선", "2호선", "2호선", "2호선")
hours <- 0:23
df_raw <- expand_grid(
date = seq(as.Date("2023-01-01"), as.Date("2023-01-07"), by = "day"),
station = stations,
hour = hours
) |>
mutate(
line = lines[match(station, stations)],
get_on = as.integer(runif(n(), 500, 15000)),
get_off = as.integer(runif(n(), 500, 15000))
)
glimpse(df_raw)
전처리
컬럼 이름 정리
공공데이터는 컬럼 이름이 길고 한자나 특수문자가 섞여 있을 때가 많습니다. 짧고 영문인 이름으로 바꾸면 코드가 깔끔해집니다.
# 예제 데이터는 이미 정리되어 있지만, 실제 데이터라면 다음처럼 변환합니다
df <- df_raw |>
rename(
# 실제 데이터 컬럼명에 맞게 조정하세요
# station = `역명`,
# line = `호선`,
# hour = `시간대`,
# get_on = `승차총승객수`,
# get_off = `하차총승객수`
)
# 예제 데이터는 그대로 사용합니다
df <- df_raw
데이터 타입 변환
# 날짜 컬럼이 문자열로 읽혔다면 Date 타입으로 변환합니다
df <- df |>
mutate(
date = as.Date(date),
hour = as.integer(hour)
)
# 타입 확인
glimpse(df)
결측치 확인과 처리
# 결측치 현황 파악
colSums(is.na(df))
# 승하차 인원이 NA인 행 제거
df <- df |>
filter(!is.na(get_on), !is.na(get_off))
# 음수나 비현실적으로 큰 값 제거
df <- df |>
filter(get_on >= 0, get_off >= 0)
cat("전처리 후 행 수:", nrow(df), "\n")
파생 변수 생성
# 총 이용객 수와 요일 컬럼을 추가합니다
df <- df |>
mutate(
total = get_on + get_off,
weekday = weekdays(date),
is_weekend = weekday %in% c("토요일", "일요일")
)
head(df)
탐색적 데이터 분석
전처리가 끝나면 데이터에 질문을 던지기 시작합니다.
역별 총 이용객 순위
# 전체 기간 동안 역별 총 이용객 수를 집계합니다
station_total <- df |>
group_by(station, line) |>
summarise(
total_passengers = sum(total),
.groups = "drop"
) |>
arrange(desc(total_passengers))
print(station_total)
시간대별 승하차 패턴
# 시간대별 평균 승차·하차 인원을 계산합니다
hourly_avg <- df |>
group_by(hour) |>
summarise(
avg_on = mean(get_on),
avg_off = mean(get_off),
.groups = "drop"
)
print(hourly_avg)
평일·주말 비교
# 평일과 주말의 이용 패턴 차이를 확인합니다
weekday_compare <- df |>
group_by(is_weekend, hour) |>
summarise(
avg_total = mean(total),
.groups = "drop"
) |>
mutate(day_type = if_else(is_weekend, "주말", "평일"))
print(weekday_compare)
ggplot2 시각화
그래프 1: 역별 이용객 순위 (막대 그래프)
library(ggplot2)
station_total |>
slice_max(total_passengers, n = 10) |>
mutate(station = fct_reorder(station, total_passengers)) |>
ggplot(aes(x = station, y = total_passengers / 10000, fill = line)) +
geom_col() +
coord_flip() +
labs(
title = "서울 지하철 이용객 상위 10개 역",
x = "역명",
y = "총 이용객 (만 명)",
fill = "노선"
) +
theme_minimal(base_family = "AppleGothic") +
theme(
plot.title = element_text(face = "bold", size = 14),
legend.position = "bottom"
)
그래프 2: 시간대별 승하차 패턴 (선 그래프)
hourly_avg |>
pivot_longer(
cols = c(avg_on, avg_off),
names_to = "type",
values_to = "passengers"
) |>
mutate(
type = case_when(
type == "avg_on" ~ "승차",
type == "avg_off" ~ "하차"
)
) |>
ggplot(aes(x = hour, y = passengers, color = type, group = type)) +
geom_line(linewidth = 1.2) +
geom_point(size = 2) +
scale_x_continuous(breaks = seq(0, 23, 2)) +
labs(
title = "시간대별 평균 승하차 인원",
x = "시간대",
y = "평균 인원 (명)",
color = "구분"
) +
theme_minimal(base_family = "AppleGothic") +
theme(plot.title = element_text(face = "bold", size = 14))
그래프 3: 평일 vs 주말 비교 (패싯 그래프)
weekday_compare |>
ggplot(aes(x = hour, y = avg_total, color = day_type, group = day_type)) +
geom_line(linewidth = 1.2) +
geom_point(size = 1.5) +
facet_wrap(~ day_type, ncol = 1) +
scale_x_continuous(breaks = seq(0, 23, 2)) +
labs(
title = "평일·주말 시간대별 이용객 비교",
x = "시간대",
y = "평균 이용객 (명)",
color = "구분"
) +
theme_minimal(base_family = "AppleGothic") +
theme(
plot.title = element_text(face = "bold", size = 14),
legend.position = "none"
)
그래프 4: 히트맵으로 보는 역-시간대 패턴
# 상위 5개 역의 시간대별 이용객 히트맵
top5_stations <- station_total |>
slice_max(total_passengers, n = 5) |>
pull(station)
df |>
filter(station %in% top5_stations) |>
group_by(station, hour) |>
summarise(avg_total = mean(total), .groups = "drop") |>
ggplot(aes(x = hour, y = station, fill = avg_total)) +
geom_tile(color = "white") +
scale_fill_gradient(low = "#fff7bc", high = "#d73027", name = "평균\n이용객") +
scale_x_continuous(breaks = seq(0, 23, 2)) +
labs(
title = "주요 역별 시간대 이용객 히트맵",
x = "시간대",
y = "역명"
) +
theme_minimal(base_family = "AppleGothic") +
theme(plot.title = element_text(face = "bold", size = 14))
결과 저장
분석 결과와 그래프를 파일로 저장합니다.
# 집계 결과를 CSV로 저장합니다
write_csv(station_total, "output/station_total.csv")
write_csv(hourly_avg, "output/hourly_avg.csv")
# 그래프를 PNG로 저장합니다
ggsave(
filename = "output/station_rank.png",
plot = last_plot(),
width = 8,
height = 5,
dpi = 150
)
cat("저장 완료.\n")
지하철 데이터 분석을 통해 전처리부터 시각화까지 전체 흐름을 경험했습니다. 같은 패턴은 어떤 데이터에도 적용할 수 있습니다. 다음 챕터에서는 인구 통계 데이터로 비슷한 사이클을 다시 한번 밟아보겠습니다.