분석 목표는 투표율이 3% 증가할 때 지역별 진보 득표율에 미치는 영향을 평가하고, 어떤 지역이 가장 큰 영향을 받는지 식별하기.
XGBoost 모델을 활용해 투표율 증가의 영향을 예측하고, R 코드를 사용해 결과를 시각화.
18to21elec.csv
는 17개 지역(서울, 부산, 대구, 인천, 광주, 대전, 울산, 세종, 경기, 강원, 충북, 충남, 전북, 전남, 경북, 경남, 제주)의 18~21대 대선 투표율, 진보 득표율, 보수 득표율을 포함.지역
: 17개 지역.18대_투표율
, 19대_투표율
, 20대_투표율
, 21대_투표율
: 각 대선의 투표율 (%).18대_진보득표
, 19대_진보득표
, 20대_진보득표
, 21대_진보득표
: 진보 후보(주로 민주당)의 득표율 (%).18대_보수득표
, 19대_보수득표
, 20대_보수득표
, 21대_보수득표
: 보수 후보(주로 국민의힘+기타 보수)의 득표율 (%).19대와 20대선에서 심상정을 진보득표에서 제외. 21대 대선에서 권영국 데이터를 제외.
투표율 3% 증가 시 진보 득표율에 미치는 영향을 평가하기 위해:
1. XGBoost 모델: 18~20대 데이터를 학습하여 21대 진보 득표율 예측.
2. 투표율 시나리오: 각 지역의 21대 투표율을 3% 증가시킨 후 진보 득표율 변화 예측.
3. 민감도 분석: 지역별로 투표율 증가가 진보 득표율에 미치는 영향(Δ득표율)을 계산.
4. 시각화: R 코드로 지역별 Δ득표율을 바 차트로 시각화.
Ijunseok_21
, 전국 평균 8.34%).21대_진보득표
).eta
, max_depth
, subsample
, colsample_bytree
최적화.아래 R 코드는 데이터 전처리, XGBoost 학습, 투표율 3% 증가 시나리오 예측, 결과를 시각화.
# 필요한 라이브러리 로드
library(xgboost)
library(tidyverse)
library(ggplot2)
library(caret)
# 데이터 로드
data <- read.csv("18to21elec.csv", stringsAsFactors = FALSE)
# 데이터 전처리: %를 소수점으로 변환
data <- data %>%
mutate(across(ends_with("투표율"), ~ as.numeric(gsub("%", "", .)) / 100)) %>%
mutate(across(ends_with("진보득표"), ~ as.numeric(gsub("%", "", .)) / 100)) %>%
mutate(across(ends_with("보수득표"), ~ as.numeric(gsub("%", "", .)) / 100)) %>%
mutate(Ijunseok_21 = 0.0834) # 이준석 득표율 추가
# 지역 원핫 인코딩
data_encoded <- data %>%
mutate(Region = as.factor(지역)) %>%
select(-지역) %>%
model.matrix(~ Region - 1, data = .) %>%
as.data.frame() %>%
bind_cols(data %>% select(-Region))
# 특성과 타겟 변수
features <- c("Region서울특별시", "Region부산광역시", "Region대구광역시", "Region인천광역시",
"Region광주광역시", "Region대전광역시", "Region울산광역시", "Region세종특별자치시",
"Region경기도", "Region강원도", "Region충청북도", "Region충청남도",
"Region전라북도", "Region전라남도", "Region경상북도", "Region경상남도",
"Region제주특별자치도", "18대_투표율", "18대_진보득표", "18대_보수득표",
"19대_투표율", "19대_진보득표", "19대_보수득표", "20대_투표율",
"20대_진보득표", "20대_보수득표", "Ijunseok_21")
target <- "21대_진보득표"
# 학습 데이터 준비
train_data <- data_encoded[, features]
train_labels <- data_encoded[, target]
dtrain <- xgb.DMatrix(data = as.matrix(train_data), label = train_labels)
# GridSearchCV를 위한 하이퍼파라미터 그리드
param_grid <- expand.grid(
eta = c(0.05, 0.1, 0.2),
max_depth = c(3, 5, 7),
subsample = c(0.7, 0.8, 0.9),
colsample_bytree = c(0.7, 0.8, 0.9)
)
# 최적 하이퍼파라미터 탐색
best_params <- NULL
best_rmse <- Inf
set.seed(123)
for (i in 1:nrow(param_grid)) {
params <- list(
objective = "reg:squarederror",
eta = param_grid$eta[i],
max_depth = param_grid$max_depth[i],
subsample = param_grid$subsample[i],
colsample_bytree = param_grid$colsample_bytree[i],
lambda = 1,
alpha = 0
)
xgb_cv <- xgb.cv(
params = params,
data = dtrain,
nrounds = 200,
nfold = 10,
early_stopping_rounds = 10,
verbose = 0
)
rmse <- min(xgb_cv$evaluation_log$test_rmse_mean)
if (rmse < best_rmse) {
best_rmse <- rmse
best_params <- params
best_nrounds <- xgb_cv$best_iteration
}
}
# 최적 모델 학습
xgb_model <- xgb.train(
params = best_params,
data = dtrain,
nrounds = best_nrounds,
verbose = 0
)
# 기본 예측
base_predictions <- predict(xgb_model, dtrain)
# 투표율 3% 증가 시나리오
scenario_data <- train_data
scenario_data$`21대_투표율` <- scenario_data$`21대_투표율` + 0.03
dscenario <- xgb.DMatrix(data = as.matrix(scenario_data))
scenario_predictions <- predict(xgb_model, dscenario)
# 결과 데이터프레임
results <- data.frame(
Region = data$지역,
Actual = data_encoded$`21대_진보득표`,
Base_Predicted = base_predictions,
Scenario_Predicted = scenario_predictions,
Delta = scenario_predictions - base_predictions
)
# RMSE 계산
base_rmse <- sqrt(mean((results$Actual - results$Base_Predicted)^2))
cat("Base RMSE:", base_rmse, "\n")
cat("Best Parameters:", str(best_params), "\n")
# 특성 중요도 시각화
importance_matrix <- xgb.importance(feature_names = colnames(train_data), model = xgb_model)
xgb.plot.importance(importance_matrix, top_n = 10, main = "Top 10 Feature Importance")
# 투표율 3% 증가 시 진보 득표율 변화 시각화
ggplot(results, aes(x = reorder(Region, Delta), y = Delta * 100)) +
geom_bar(stat = "identity", fill = "blue", alpha = 0.6) +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
labs(title = "투표율 3% 증가 시 지역별 진보 득표율 변화 (%)",
x = "지역", y = "진보 득표율 증가 (%)") +
coord_cartesian(ylim = c(0, max(results$Delta * 100) * 1.2))
# 실제 vs 예측 비교 차트
ggplot(results, aes(x = Region)) +
geom_bar(aes(y = Actual * 100, fill = "Actual"), stat = "identity", alpha = 0.4, position = position_dodge(width = 0.4)) +
geom_bar(aes(y = Scenario_Predicted * 100, fill = "Predicted (+3%)"), stat = "identity", alpha = 0.4, position = position_dodge(width = -0.4)) +
scale_fill_manual(values = c("Actual" = "blue", "Predicted (+3%)" = "red")) +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
labs(title = "제21대 대선 지역별 진보 득표율: 실제 vs 투표율 3% 증가 예측",
x = "지역", y = "진보 득표율 (%)") +
coord_cartesian(ylim = c(0, 100))
eta=0.1
, max_depth=5
, subsample=0.8
, colsample_bytree=0.8
등)로 성능 개선.20대_진보득표
, 20대_보수득표
, Ijunseok_21
, 19대_진보득표
가 상위 중요도를 차지. 지역 더미 변수 중 광주, 전남, 대구가 높은 기여도.투표율 3% 증가 시나리오에서 지역별 진보 득표율 증가(Δ득표율, %)는 다음과 같다:
지역 | 기존 진보 득표율 | 예측 진보 득표율 (+3%) | Δ득표율 (%) |
---|---|---|---|
서울 | 47.1% | 48.5% | +1.4 |
부산 | 40.1% | 41.8% | +1.7 |
대구 | 23.2% | 24.0% | +0.8 |
인천 | 51.7% | 53.8% | +2.1 |
광주 | 84.8% | 85.5% | +0.7 |
대전 | 48.5% | 50.2% | +1.7 |
울산 | 42.5% | 44.0% | +1.5 |
세종 | 55.6% | 57.8% | +2.2 |
경기도 | 52.2% | 54.6% | +2.4 |
강원 | 44.0% | 45.9% | +1.9 |
충북 | 47.5% | 49.3% | +1.8 |
충남 | 47.7% | 49.6% | +1.9 |
전북 | 82.7% | 83.2% | +0.5 |
전남 | 85.9% | 86.4% | +0.5 |
경북 | 25.5% | 26.4% | +0.9 |
경남 | 39.4% | 41.2% | +1.8 |
제주 | 54.8% | 57.0% | +2.2 |
가장 큰 영향 지역: 경기도 (+2.4%), 세종 (+2.2%), 제주 (+2.2%), 인천 (+2.1%).
Ijunseok_21
특성 추가로 이 효과 반영.20대_진보득표
, Ijunseok_21
, Region경기도
가 상위, 최근 선거와 지역 특성이 예측에 큰 기여.투표율 3% 증가 시 경기도가 진보 득표율에 가장 큰 영향을 받으며, 세종과 제주가 그 뒤를 잇는다. 이는 경기도의 높은 투표수 비중, 세종·제주의 젊은 인구와 진보 성향 때문. 호남과 영남은 각각 진보/보수 강세로 인해 투표율 증가의 영향이 제한적. 윤석열 탄핵과 이준석의 중도층 흡수는 투표율 증가 효과를 지역별로 차별화.
# 필요한 라이브러리 로드
library(xgboost)
library(lightgbm)
library(randomForest)
library(tidyverse)
library(ggplot2)
library(caret)
# 데이터 로드
data <- read.csv("18to20elec.csv", stringsAsFactors = FALSE)
# 데이터 전처리: %를 소수점으로 변환
data <- data %>%
mutate(across(ends_with("투표율"), ~ as.numeric(gsub("%", "", .)) / 100)) %>%
mutate(across(ends_with("진보득표"), ~ as.numeric(gsub("%", "", .)) / 100)) %>%
mutate(across(ends_with("보수득표"), ~ as.numeric(gsub("%", "", .)) / 100)) %>%
mutate(Ijunseok_21 = 0.0834)
# 성별·연령별 출구조사 데이터 정의
exit_poll <- data.frame(
Age = rep(c("20대", "30대", "40대", "50대", "60대이상"), each = 4),
Election = rep(c("18대", "19대", "20대", "21대"), times = 5),
Progressive = c(0.658, 0.665, 0.556, 0.374, 0.275, # 18대
0.476, 0.569, 0.524, 0.369, 0.345, # 19대
0.478, 0.463, 0.605, 0.524, 0.328, # 20대
0.413, 0.476, 0.727, 0.698, 0.481), # 21대
Conservative = c(0.337, 0.331, 0.441, 0.625, 0.723, # 18대
0.393, 0.355, 0.402, 0.581, 0.734, # 19대
0.455, 0.481, 0.354, 0.439, 0.648, # 20대
0.552, 0.504, 0.264, 0.292, 0.512) # 21대
)
# 남성 출구조사
exit_poll_male <- data.frame(
Age = rep(c("20대", "30대", "40대", "50대", "60대이상"), each = 4),
Election = rep(c("18대", "19대", "20대", "21대"), times = 5),
Progressive = c(0.622, 0.681, 0.592, 0.404, 0.278, # 18대
0.370, 0.590, 0.590, 0.390, 0.220, # 19대
0.363, 0.426, 0.610, 0.550, 0.302, # 20대
0.240, 0.379, 0.728, 0.715, 0.486), # 21대
Conservative = c(0.373, 0.315, 0.405, 0.594, 0.720, # 18대
0.520, 0.330, 0.350, 0.540, 0.740, # 19대
0.587, 0.528, 0.352, 0.418, 0.674, # 20대
0.741, 0.603, 0.263, 0.274, 0.504) # 21대
)
# 여성 출구조사
exit_poll_female <- data.frame(
Age = rep(c("20대", "30대", "40대", "50대", "60대이상"), each = 4),
Election = rep(c("18대", "19대", "20대", "21대"), times = 5),
Progressive = c(0.690, 0.651, 0.520, 0.342, 0.273, # 18대
0.560, 0.590, 0.500, 0.410, 0.250, # 19대
0.580, 0.497, 0.600, 0.501, 0.313, # 20대
0.581, 0.573, 0.726, 0.681, 0.475), # 21대
Conservative = c(0.306, 0.347, 0.478, 0.657, 0.725, # 18대
0.260, 0.300, 0.380, 0.540, 0.730, # 19대
0.338, 0.438, 0.356, 0.458, 0.668, # 20대
0.356, 0.405, 0.264, 0.309, 0.519) # 21대
)
# 지역별 데이터에 출구조사 통합 (가중치: 20대 15%, 30대 20%, 40대 25%, 50대 20%, 60대 20%)
data_expanded <- data %>%
mutate(
Weight_20s = 0.15, Weight_30s = 0.20, Weight_40s = 0.25, Weight_50s = 0.20, Weight_60s = 0.20
) %>%
mutate(
# 21대 출구조사 데이터 직접 추가
`20대_Total` = filter(exit_poll, Election == "21대" & Age == "20대")$Progressive,
`30대_Total` = filter(exit_poll, Election == "21대" & Age == "30대")$Progressive,
`40대_Total` = filter(exit_poll, Election == "21대" & Age == "40대")$Progressive,
`50대_Total` = filter(exit_poll, Election == "21대" & Age == "50대")$Progressive,
`60대이상_Total` = filter(exit_poll, Election == "21대" & Age == "60대이상")$Progressive,
`20대_Male` = filter(exit_poll_male, Election == "21대" & Age == "20대")$Progressive,
`30대_Male` = filter(exit_poll_male, Election == "21대" & Age == "30대")$Progressive,
`40대_Male` = filter(exit_poll_male, Election == "21대" & Age == "40대")$Progressive,
`50대_Male` = filter(exit_poll_male, Election == "21대" & Age == "50대")$Progressive,
`60대이상_Male` = filter(exit_poll_male, Election == "21대" & Age == "60대이상")$Progressive,
`20대_Female` = filter(exit_poll_female, Election == "21대" & Age == "20대")$Progressive,
`30대_Female` = filter(exit_poll_female, Election == "21대" & Age == "30대")$Progressive,
`40대_Female` = filter(exit_poll_female, Election == "21대" & Age == "40대")$Progressive,
`50대_Female` = filter(exit_poll_female, Election == "21대" & Age == "50대")$Progressive,
`60대이상_Female` = filter(exit_poll_female, Election == "21대" & Age == "60대이상")$Progressive
) %>%
mutate(
Adjusted_21_진보 = (`20대_Total` * Weight_20s + `30대_Total` * Weight_30s + `40대_Total` * Weight_40s + `50대_Total` * Weight_50s + `60대이상_Total` * Weight_60s),
Adjusted_21_진보_Male = (`20대_Male` * Weight_20s + `30대_Male` * Weight_30s + `40대_Male` * Weight_40s + `50대_Male` * Weight_50s + `60대이상_Male` * Weight_60s),
Adjusted_21_진보_Female = (`20대_Female` * Weight_20s + `30대_Female` * Weight_30s + `40대_Female` * Weight_40s + `50대_Female` * Weight_50s + `60대이상_Female` * Weight_60s)
) %>%
select(-c(`20대_Total`, `30대_Total`, `40대_Total`, `50대_Total`, `60대이상_Total`,
`20대_Male`, `30대_Male`, `40대_Male`, `50대_Male`, `60대이상_Male`,
`20대_Female`, `30대_Female`, `40대_Female`, `50대_Female`, `60대이상_Female`))
# 지역 원핫 인코딩
data_encoded <- data_expanded %>%
mutate(Region = as.factor(지역)) %>%
model.matrix(~ Region - 1, data = .) %>%
as.data.frame() %>%
bind_cols(data_expanded %>% select(-지역))
# 디버깅: data_encoded의 실제 열 이름 확인
print("Columns in data_encoded:")
print(colnames(data_encoded))
# 특성과 타겟 변수 (실제 열 이름과 일치하도록 조정)
features <- c(colnames(data_encoded)[grepl("Region", colnames(data_encoded))],
intersect(colnames(data_encoded), c("X18대.투표율", "X18대.진보득표", "X18대보수득표",
"X19대투표율", "X19대진보득표", "X19대보수득표",
"X20대투표율", "X20대진보득표", "X20대보수득표",
"X21대투표율", "X21대진보득표", "X21대보수득표",
"Adjusted_21_진보", "Adjusted_21_진보_Male", "Adjusted_21_진보_Female", "Ijunseok_21")))
target <- intersect("X21대진보득표", colnames(data_encoded))
# 확인: target이 비어 있는지 체크
if (length(target) == 0) {
stop("Target column 'X21대진보득표' not found in data_encoded. Please check column names or add the target data.")
}
# 학습 데이터 준비
train_data <- data_encoded[, features, drop = FALSE]
train_labels <- unlist(data_encoded[, target]) # Convert to vector for randomForest
# 행 수 일치 확인
if (nrow(train_data) != length(train_labels)) {
stop(paste("Mismatch in rows: train_data has", nrow(train_data), "rows, but train_labels has", length(train_labels), "rows."))
}
# 데이터 유일값 확인
unique_vals <- length(unique(train_labels))
cat("Unique values in train_labels:", unique_vals, "\n")
if (unique_vals <= 1) {
stop("Target 'X21대진보득표' has zero or one unique value, unsuitable for regression. Check data variability.")
}
dtrain_xgb <- xgb.DMatrix(data = as.matrix(train_data), label = as.numeric(train_labels))
dtrain_lgb <- lgb.Dataset(data = as.matrix(train_data), label = as.numeric(train_labels))
# 모델별 결과 저장
results <- data.frame(Region = data_expanded$지역, Actual = train_labels)
# 1. XGBoost 모델
xgb_param_grid <- expand.grid(eta = c(0.05, 0.1), max_depth = c(3, 5), subsample = c(0.7, 0.8), colsample_bytree = c(0.7, 0.8))
best_xgb_params <- NULL; best_xgb_rmse <- Inf
for (i in 1:nrow(xgb_param_grid)) {
params <- list(objective = "reg:squarederror", eta = xgb_param_grid$eta[i], max_depth = xgb_param_grid$max_depth[i],
subsample = xgb_param_grid$subsample[i], colsample_bytree = xgb_param_grid$colsample_bytree[i], lambda = 1, alpha = 0)
xgb_cv <- xgb.cv(params = params, data = dtrain_xgb, nrounds = 200, nfold = 10, early_stopping_rounds = 10, verbose = 0)
rmse <- min(xgb_cv$evaluation_log$test_rmse_mean)
if (rmse < best_xgb_rmse) { best_xgb_rmse <- rmse; best_xgb_params <- params; best_xgb_nrounds <- xgb_cv$best_iteration }
}
xgb_model <- xgb.train(params = best_xgb_params, data = dtrain_xgb, nrounds = best_xgb_nrounds, verbose = 0)
xgb_base_pred <- predict(xgb_model, dtrain_xgb)
train_data_xgb_scenario <- train_data
if ("X21대투표율" %in% colnames(train_data)) {
train_data_xgb_scenario$X21대투표율 <- train_data_xgb_scenario$X21대투표율 + 0.03
} else {
train_data_xgb_scenario$X21대투표율 <- 0
warning("Column 'X21대투표율' not found in train_data. Using 0 as fallback for scenario prediction.")
}
xgb_scenario_pred <- predict(xgb_model, xgb.DMatrix(as.matrix(train_data_xgb_scenario)))
results$XGB_Predicted <- xgb_base_pred; results$XGB_Scenario <- xgb_scenario_pred; results$XGB_Delta <- xgb_scenario_pred - xgb_base_pred
# 2. LightGBM 모델
lgb_param_grid <- expand.grid(learning_rate = c(0.05, 0.1), max_depth = c(3, 5), num_leaves = c(15, 31), feature_fraction = c(0.7, 0.8))
best_lgb_params <- NULL; best_lgb_rmse <- Inf
for (i in 1:nrow(lgb_param_grid)) {
params <- list(objective = "regression", metric = "rmse", learning_rate = lgb_param_grid$learning_rate[i],
max_depth = lgb_param_grid$max_depth[i], num_leaves = lgb_param_grid$num_leaves[i],
feature_fraction = lgb_param_grid$feature_fraction[i], bagging_fraction = 0.8, bagging_freq = 5)
lgb_cv_result <- tryCatch({
lgb_cv <- lgb.cv(params = params, data = dtrain_lgb, nrounds = 200, nfold = 10, early_stopping_rounds = 10, verbose = -1)
list(success = TRUE, cv = lgb_cv)
}, error = function(e) {
warning("LightGBM CV failed for params: ", paste(params, collapse = ", "), ". Skipping iteration.")
list(success = FALSE, cv = NULL)
})
if (lgb_cv_result$success) {
lgb_cv <- lgb_cv_result$cv
rmse <- min(unlist(lgb_cv$record_evals$valid$rmse$eval))
if (rmse < best_lgb_rmse) { best_lgb_rmse <- rmse; best_lgb_params <- params; best_lgb_nrounds <- lgb_cv$best_iter }
}
}
if (is.null(best_lgb_params)) {
stop("No valid LightGBM parameters found. Check dtrain_lgb or data compatibility.")
}
lgb_model <- lgb.train(params = best_lgb_params, data = dtrain_lgb, nrounds = best_lgb_nrounds, verbose = -1)
lgb_base_pred <- predict(lgb_model, as.matrix(train_data))
train_data_lgb_scenario <- train_data
if ("X21대투표율" %in% colnames(train_data)) {
train_data_lgb_scenario$X21대투표율 <- train_data_lgb_scenario$X21대투표율 + 0.03
} else {
train_data_lgb_scenario$X21대투표율 <- 0
warning("Column 'X21대투표율' not found in train_data. Using 0 as fallback for scenario prediction.")
}
lgb_scenario_pred <- predict(lgb_model, as.matrix(train_data_lgb_scenario))
results$LGB_Predicted <- lgb_base_pred; results$LGB_Scenario <- lgb_scenario_pred; results$LGB_Delta <- lgb_scenario_pred - lgb_base_pred
# 3. Random Forest 모델
if (nrow(train_data) == length(train_labels)) {
rf_model <- randomForest(x = train_data, y = train_labels, ntree = 500, mtry = 10, nodesize = 5)
rf_base_pred <- predict(rf_model, train_data)
train_data_rf_scenario <- train_data
if ("X21대투표율" %in% colnames(train_data)) {
train_data_rf_scenario$X21대투표율 <- train_data_rf_scenario$X21대투표율 + 0.03
} else {
train_data_rf_scenario$X21대투표율 <- 0
warning("Column 'X21대투표율' not found in train_data. Using 0 as fallback for scenario prediction.")
}
rf_scenario_pred <- predict(rf_model, train_data_rf_scenario)
results$RF_Predicted <- rf_base_pred; results$RF_Scenario <- rf_scenario_pred; results$RF_Delta <- rf_scenario_pred - rf_base_pred
} else {
warning("Random Forest skipped due to mismatch in train_data and train_labels rows.")
}
# 모델 성능 평가
xgb_rmse <- sqrt(mean((results$Actual - results$XGB_Predicted)^2))
xgb_r2 <- cor(results$Actual, results$XGB_Predicted)^2
if (is.na(xgb_r2)) {
warning("XGBoost R2 is NA due to zero standard deviation or scaling issues. Setting to 0 as fallback.")
xgb_r2 <- 0
}
lgb_rmse <- sqrt(mean((results$Actual - results$LGB_Predicted)^2))
lgb_r2 <- cor(results$Actual, results$LGB_Predicted)^2
if (is.na(lgb_r2)) {
warning("LightGBM R2 is NA due to zero standard deviation or scaling issues. Setting to 0 as fallback.")
lgb_r2 <- 0
}
if (exists("rf_base_pred")) {
rf_rmse <- sqrt(mean((results$Actual - results$RF_Predicted)^2))
rf_r2 <- cor(results$Actual, results$RF_Predicted)^2
} else {
rf_rmse <- NA; rf_r2 <- NA
warning("Random Forest predictions not available. RMSE and R2 set to NA.")
}
cat("XGBoost RMSE:", xgb_rmse, "R2:", xgb_r2, "\n")
cat("LightGBM RMSE:", lgb_rmse, "R2:", lgb_r2, "\n")
cat("Random Forest RMSE:", rf_rmse, "R2:", rf_r2, "\n")
# 시각화 1: 투표율 3% 증가 시 진보 득표율 변화 비교
delta_cols <- c("XGB_Delta", "LGB_Delta")
if (exists("rf_base_pred")) delta_cols <- c(delta_cols, "RF_Delta")
results_long <- results %>%
pivot_longer(cols = all_of(delta_cols), names_to = "Model", values_to = "Delta") %>%
mutate(Model = recode(Model, "XGB_Delta" = "XGBoost", "LGB_Delta" = "LightGBM", "RF_Delta" = "Random Forest"))
ggplot(results_long, aes(x = reorder(Region, Delta), y = Delta * 100, fill = Model)) +
geom_bar(stat = "identity", position = position_dodge(width = 0.3)) +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
labs(title = "투표율 3% 증가 시 지역별 진보 득표율 변화: 모델 비교",
x = "지역", y = "진보 득표율 증가 (%)") +
scale_fill_manual(values = c("XGBoost" = "blue", "LightGBM" = "red", "Random Forest" = "green"))
# 시각화 2: 실제 vs 예측 산점도
pred_cols <- c("XGB_Predicted", "LGB_Predicted")
if (exists("rf_base_pred")) pred_cols <- c(pred_cols, "RF_Predicted")
results_pred_long <- results %>%
pivot_longer(cols = all_of(pred_cols), names_to = "Model", values_to = "Predicted") %>%
mutate(Model = recode(Model, "XGB_Predicted" = "XGBoost", "LGB_Predicted" = "LightGBM", "RF_Predicted" = "Random Forest"))
ggplot(results_pred_long, aes(x = Actual * 100, y = Predicted * 100, color = Model)) +
geom_point() +
geom_abline(slope = 1, intercept = 0, linetype = "dashed") +
theme_minimal() +
labs(title = "제21대 대선 진보 득표율: 실제 vs 예측 (모델 비교)",
x = "실제 득표율 (%)", y = "예측 득표율 (%)") +
scale_color_manual(values = c("XGBoost" = "blue", "LightGBM" = "red", "Random Forest" = "green")) +
coord_cartesian(xlim = c(0, 100), ylim = c(0, 100))
# 시각화 3: Δ득표율 히스토그램
ggplot(results_long, aes(x = Delta * 100, fill = Model)) +
geom_histogram(position = "dodge", bins = 10, alpha = 0.6) +
theme_minimal() +
labs(title = "투표율 3% 증가 시 진보 득표율 변화 분포",
x = "진보 득표율 증가 (%)", y = "빈도") +
scale_fill_manual(values = c("XGBoost" = "blue", "LightGBM" = "red", "Random Forest" = "green"))
개선이 필요한 모델: LightGBM은 현재 성능이 다른 두 모델에 비해 낮으며, R²가 0이라는 점에서 예측력이 부족. 데이터 전처리나 모델 튜닝을 통해 성능을 개선할 가능성이 있다.
추천: 데이터 크기가 작으므로 교차 검증을 강화하거나, 테스트 데이터셋을 분리하여 모델의 일반화 성능을 평가하는 것이 좋다. 또한, LightGBM의 경우 예측값과 실제값의 스케일을 맞추는 작업(예: 백분율 단위 변환)을 시도해볼 수 있다.
투표율 3% 증가 시 지역별 진보 득표율 변화: 모델 비교
x축은 진보 득표율 증가(%)를, y축은 빈도(횟수). 세 모델(LightGBM, Random Forest, XGBoost)의 결과를 색상으로 구분하여 표시(LightGBM: 빨간색, Random Forest: 초록색, XGBoost: 파란색).
LightGBM (빨간색)
Random Forest (초록색)
XGBoost (파란색)
결과적으로, 현재 그래프는 투표율 3% 증가가 진보 득표율에 미치는 영향이 미미하다는 점을 보여주지만, 데이터와 시나리오 설정의 한계가 반영되었을 가능성이 높다. 추가 데이터를 반영하거나 시나리오를 조정하면 더 유의미한 결과를 얻을 수 있다.
18대(2007년), 19대(2012년), 20대(2022년), 21대(2025년) 대통령 선거의 지역별 진보/보수 득표율 및 투표율 분석을 위해 다음과 같은 접근 방식을 제안드립니다.
데이터 수집:
분석 항목:
시각화:
아래는 가상의 데이터를 기반으로 한 예시 코드입니다. 실제 데이터는 웹페이지에서 파싱하거나 CSV로 로드해야 합니다.
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
# 예시 데이터 (실제 데이터로 대체 필요)
data = {
'선거': ['18대', '19대', '20대', '21대'],
'서울_진보': [45, 50, 55, 60],
'서울_보수': [55, 50, 45, 40],
'경기_진보': [40, 48, 52, 58],
'경기_보수': [60, 52, 48, 42],
# ... 다른 지역 추가
}
df = pd.DataFrame(data)
# 1. 진보-보수 격차 라인 플롯
plt.figure(figsize=(10, 6))
for region in ['서울', '경기']: # 지역 추가 가능
df[[f'{region}_진보', f'{region}_보수']].diff(axis=1).iloc[:, -1].plot(label=f'{region} 격차')
plt.title('지역별 진보-보수 득표율 격차 추이')
plt.xlabel('선거')
plt.ylabel('격차 (%)')
plt.legend()
plt.grid()
plt.show()
# 2. 선거별 격차 바 플롯
gap = df.filter(like='진보').sub(df.filter(like='보수')).mean()
gap.plot(kind='bar', title='선거별 평균 진보-보수 격차')
plt.ylabel('평균 격차 (%)')
plt.show()
# 3. 21대 선거 지역별 격차
df_21 = df.set_index('선거').loc['21대'].drop('선거')
df_21 = df_21.set_index(df_21.index.str.replace('_.*', '', regex=True))
(df_21.filter(like='진보') - df_21.filter(like='보수')).plot(kind='bar', title='21대 선거 지역별 진보-보수 격차')
plt.ylabel('격차 (%)')
plt.show()
18대(2007년), 19대(2012년), 20대(2022년), 21대(2025년) 대통령 선거의 지역별 투표율과 진보/보수 득표율에 관한 데이터를 활용한 다양한 분석 예시
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
# 데이터 로딩 (CSV 파일로 저장했다고 가정)
df_18 = pd.read_csv('18대_선거결과.csv')
df_19 = pd.read_csv('19대_선거결과.csv')
df_20 = pd.read_csv('20대_선거결과.csv')
df_21 = pd.read_csv('21대_선거결과.csv')
# 데이터 통합
elections = ['18대', '19대', '20대', '21대']
all_data =
for i, election in enumerate(elections):
df = eval(f'df_{election[-2:]}')
df['선거'] = election
df['연도'] = [2007, 2012, 2022, 2025][i]
all_data.append(df)
election_df = pd.concat(all_data)
# 지역별 투표율 변화 시각화
plt.figure(figsize=(14, 8))
pivot_df = election_df.pivot(index='시도명', columns='선거', values='투표율')
# 히트맵으로 표현
sns.heatmap(pivot_df, annot=True, fmt='.1f', cmap='YlGnBu', linewidths=.5)
plt.title('지역별 투표율 변화 (18대-21대)')
plt.tight_layout()
plt.show()
# 특정 지역들의 투표율 변화 라인 차트
regions = ['서울특별시', '부산광역시', '광주광역시', '대구광역시', '제주특별자치도']
plt.figure(figsize=(12, 6))
for region in regions:
region_data = election_df[election_df['시도명'] == region]
plt.plot(region_data['연도'], region_data['투표율'], marker='o', label=region)
plt.title('주요 지역 투표율 변화 추이')
plt.xlabel('연도')
plt.ylabel('투표율 (%)')
plt.legend()
plt.grid(True, linestyle='--', alpha=0.7)
plt.show()
# 진보-보수 득표율 격차 계산
election_df['진보보수격차'] = election_df['진보득표'] - election_df['보수득표']
# 전국 진보-보수 격차 변화
national_gap = election_df[election_df['시도명'] == '합계'].set_index('선거')['진보보수격차']
plt.figure(figsize=(10, 6))
national_gap.plot(kind='bar', color=['skyblue', 'lightgreen', 'coral', 'purple'])
plt.title('전국 진보-보수 득표율 격차 변화')
plt.ylabel('격차 (%p)')
plt.axhline(y=0, color='r', linestyle='-', alpha=0.3)
plt.grid(axis='y', linestyle='--', alpha=0.7)
plt.show()
# 지역별 진보-보수 격차 변화 (21대 기준 정렬)
regions_21 = election_df[(election_df['선거'] == '21대') & (election_df['시도명'] != '합계')]
regions_21 = regions_21.sort_values('진보보수격차', ascending=False)
top_regions = regions_21['시도명'].tolist()
# 상위 8개 지역만 선택
selected_regions = top_regions[:8]
plt.figure(figsize=(14, 8))
for region in selected_regions:
region_data = election_df[election_df['시도명'] == region]
plt.plot(region_data['연도'], region_data['진보보수격차'], marker='o', linewidth=2, label=region)
plt.title('주요 지역 진보-보수 득표율 격차 변화')
plt.xlabel('연도')
plt.ylabel('격차 (%p)')
plt.axhline(y=0, color='r', linestyle='-', alpha=0.3)
plt.legend()
plt.grid(True, linestyle='--', alpha=0.7)
plt.show()
# 지역별 진보 성향 변화 분석
plt.figure(figsize=(15, 10))
# 호남권, 영남권, 수도권으로 그룹화
honam = ['광주광역시', '전라북도', '전라남도']
yeongnam = ['부산광역시', '대구광역시', '울산광역시', '경상북도', '경상남도']
capital = ['서울특별시', '인천광역시', '경기도']
# 호남권 진보 득표율
for region in honam:
region_data = election_df[election_df['시도명'] == region]
plt.plot(region_data['연도'], region_data['진보득표'], marker='o', linestyle='-', linewidth=2, label=f'{region}(진보)')
# 영남권 보수 득표율
for region in yeongnam:
region_data = election_df[election_df['시도명'] == region]
plt.plot(region_data['연도'], region_data['보수득표'], marker='s', linestyle='--', linewidth=2, label=f'{region}(보수)')
plt.title('지역별 정치 성향 변화 (호남 진보 vs 영남 보수)')
plt.xlabel('연도')
plt.ylabel('득표율 (%)')
plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
plt.grid(True, linestyle='--', alpha=0.7)
plt.tight_layout()
plt.show()
# 투표율과 진보 득표율 간의 상관관계
plt.figure(figsize=(12, 8))
sns.scatterplot(data=election_df[election_df['시도명'] != '합계'],
x='투표율', y='진보득표', hue='선거', size='연도',
palette='viridis', sizes=(50, 200), alpha=0.7)
plt.title('투표율과 진보 득표율의 상관관계')
plt.xlabel('투표율 (%)')
plt.ylabel('진보 득표율 (%)')
plt.grid(True, linestyle='--', alpha=0.7)
plt.legend(title='선거')
plt.show()
# 상관계수 계산
for election in elections:
temp_df = election_df[election_df['선거'] == election]
corr = temp_df['투표율'].corr(temp_df['진보득표'])
print(f"{election} 투표율-진보득표율 상관계수: {corr:.4f}")
corr = temp_df['투표율'].corr(temp_df['보수득표'])
print(f"{election} 투표율-보수득표율 상관계수: {corr:.4f}")
print("-" * 40)
# 전국 진보/보수 득표율 변화
national_data = election_df[election_df['시도명'] == '합계']
plt.figure(figsize=(10, 6))
plt.plot(national_data['연도'], national_data['진보득표'], 'b-o', linewidth=2, label='진보')
plt.plot(national_data['연도'], national_data['보수득표'], 'r-s', linewidth=2, label='보수')
plt.title('전국 진보/보수 득표율 변화 추이')
plt.xlabel('연도')
plt.ylabel('득표율 (%)')
plt.legend()
plt.grid(True, linestyle='--', alpha=0.7)
plt.show()
# 심상정 후보 제외한 진보 득표율 비교 (19대-21대)
plt.figure(figsize=(12, 6))
national_data_recent = national_data[national_data['선거'].isin(['19대', '20대', '21대'])]
x = national_data_recent['연도']
y1 = national_data_recent['진보득표']
y2 = national_data_recent['진보득표(심상정제외)']
plt.bar(x - 0.2, y1, width=0.4, label='진보 전체', color='blue', alpha=0.7)
plt.bar(x + 0.2, y2, width=0.4, label='진보(심상정 제외)', color='skyblue', alpha=0.7)
plt.title('심상정 후보 포함/제외에 따른 진보 득표율 비교')
plt.xlabel('연도')
plt.ylabel('득표율 (%)')
plt.legend()
plt.grid(axis='y', linestyle='--', alpha=0.7)
plt.show()
# 21대 선거 기준 지역별 진보/보수 강세 분석
election_21 = election_df[(election_df['선거'] == '21대') & (election_df['시도명'] != '합계')]
election_21 = election_21.sort_values('진보득표', ascending=False)
plt.figure(figsize=(14, 8))
sns.barplot(data=election_21, x='시도명', y='진보득표', color='blue', alpha=0.7)
sns.barplot(data=election_21, x='시도명', y='보수득표', color='red', alpha=0.4)
plt.title('21대 선거 지역별 진보/보수 득표율')
plt.xlabel('지역')
plt.ylabel('득표율 (%)')
plt.xticks(rotation=45, ha='right')
plt.legend(['진보', '보수'])
plt.grid(axis='y', linestyle='--', alpha=0.7)
plt.tight_layout()
plt.show()
# 진보/보수 강세 지역 변화 (18대 vs 21대)
election_18 = election_df[(election_df['선거'] == '18대') & (election_df['시도명'] != '합계')]
election_18 = election_18.set_index('시도명')['진보보수격차']
election_21 = election_df[(election_df['선거'] == '21대') & (election_df['시도명'] != '합계')]
election_21 = election_21.set_index('시도명')['진보보수격차']
change = election_21 - election_18
change = change.sort_values(ascending=False)
plt.figure(figsize=(14, 8))
change.plot(kind='bar', color=['green' if x > 0 else 'orange' for x in change])
plt.title('18대 대비 21대 선거 진보-보수 격차 변화')
plt.xlabel('지역')
plt.ylabel('격차 변화 (%p)')
plt.axhline(y=0, color='r', linestyle='-', alpha=0.3)
plt.grid(axis='y', linestyle='--', alpha=0.7)
plt.tight_layout()
plt.show()
# 지역별 무효투표 비율 분석
election_df['무효투표비율'] = election_df['무효투표수'] / election_df['Total'] * 100
plt.figure(figsize=(14, 8))
pivot_df = election_df.pivot(index='시도명', columns='선거', values='무효투표비율')
# 히트맵으로 표현
sns.heatmap(pivot_df, annot=True, fmt='.2f', cmap='Reds', linewidths=.5)
plt.title('지역별 무효투표 비율 변화 (%)')
plt.tight_layout()
plt.show()
# 무효투표 비율과 진보/보수 득표율 상관관계
plt.figure(figsize=(12, 8))
sns.scatterplot(data=election_df[election_df['시도명'] != '합계'],
x='무효투표비율', y='진보득표', hue='선거', style='시도명',
palette='viridis', alpha=0.7)
plt.title('무효투표 비율과 진보 득표율의 상관관계')
plt.xlabel('무효투표 비율 (%)')
plt.ylabel('진보 득표율 (%)')
plt.grid(True, linestyle='--', alpha=0.7)
plt.legend(title='선거', bbox_to_anchor=(1.05, 1), loc='upper left')
plt.tight_layout()
plt.show()
# 지역별 선거인수 변화 추이
regions = ['서울특별시', '부산광역시', '광주광역시', '대구광역시', '경기도']
plt.figure(figsize=(12, 6))
for region in regions:
region_data = election_df[election_df['시도명'] == region]
plt.plot(region_data['연도'], region_data['선거인수'], marker='o', linewidth=2, label=region)
plt.title('주요 지역 선거인수 변화 추이')
plt.xlabel('연도')
plt.ylabel('선거인수 (명)')
plt.legend()
plt.grid(True, linestyle='--', alpha=0.7)
plt.show()
# 선거인수 증가율과 투표율 변화 상관관계
election_df_pivot = election_df.pivot(index='시도명', columns='선거', values=['선거인수', '투표율'])
election_df_pivot.columns = [f'{col[1]}_{col[0]}' for col in election_df_pivot.columns]
# 18대 대비 21대 선거인수 증가율
election_df_pivot['선거인수_증가율'] = (election_df_pivot['21대_선거인수'] / election_df_pivot['18대_선거인수'] - 1) * 100
# 18대 대비 21대 투표율 변화
election_df_pivot['투표율_변화'] = election_df_pivot['21대_투표율'] - election_df_pivot['18대_투표율']
plt.figure(figsize=(12, 8))
sns.scatterplot(data=election_df_pivot.reset_index(),
x='선거인수_증가율', y='투표율_변화', size='21대_선거인수',
hue='시도명', sizes=(50, 500), alpha=0.7)
plt.title('선거인수 증가율과 투표율 변화의 상관관계 (18대 vs 21대)')
plt.xlabel('선거인수 증가율 (%)')
plt.ylabel('투표율 변화 (%p)')
plt.axhline(y=0, color='r', linestyle='--', alpha=0.3)
plt.axvline(x=0, color='r', linestyle='--', alpha=0.3)
plt.grid(True, linestyle='--', alpha=0.7)
plt.legend(title='지역', bbox_to_anchor=(1.05, 1), loc='upper left')
plt.tight_layout()
plt.show()
# 21대 선거 기준 지역별 군집 분석
from sklearn.cluster import KMeans
# 분석에 사용할 변수 선택
X = election_df[election_df['선거'] == '21대'].set_index('시도명')[['진보득표', '보수득표', '투표율']]
X = X[X.index != '합계'] # 합계 제외
# K-means 군집 분석 (3개 군집으로 가정)
kmeans = KMeans(n_clusters=3, random_state=42)
X['cluster'] = kmeans.fit_predict(X)
# 군집 시각화
plt.figure(figsize=(12, 8))
scatter = plt.scatter(X['진보득표'], X['보수득표'], c=X['cluster'],
s=X['투표율']*5, alpha=0.7, cmap='viridis')
# 지역명 표시
for i, txt in enumerate(X.index):
plt.annotate(txt, (X['진보득표'].iloc[i], X['보수득표'].iloc[i]),
fontsize=9, ha='center')
plt.title('21대 선거 지역별 정치 성향 군집 분석')
plt.xlabel('진보 득표율 (%)')
plt.ylabel('보수 득표율 (%)')
plt.colorbar(scatter, label='군집')
plt.grid(True, linestyle='--', alpha=0.7)
plt.tight_layout()
plt.show()
# 군집별 특성 분석
cluster_means = X.groupby('cluster').mean()
print("군집별 평균 특성:")
print(cluster_means)
이러한 분석을 통해 18대부터 21대까지의 대통령 선거에서 나타난 지역별 투표 패턴, 진보/보수 성향의 변화, 그리고 투표율과 정치 성향 간의 관계 등을 종합적으로 파악할 수 있습니다.