Korean National Assembly Database
열린국회정보 Open API 8종을 결합하여 구축한 대한민국 국회 법안 생애주기 마스터 데이터베이스. 17대부터 22대까지의 발의, 심사, 표결, 공포 전 과정을 추적합니다.
데이터 구조와 수집 파이프라인
8개의 열린국회정보 Open API를 결합하여 법안 단위(bill-level) 마스터 테이블과 회의 단위(meeting-level) 위성 테이블을 구축합니다.
Open API
발의자 ppsr_kind, rst_proposer, rst_mona_cd, publ_mona_cd
타임스탬프 ppsl_dt → committee_dt → cmt_proc_dt → law_proc_dt → rgs_rsln_dt → prom_dt
결과 status, passed, enacted, proc_rslt
표결 vote_yes, vote_no, vote_abstain
파생 days_to_proc, days_to_committee
Cross-Assembly Overview
17대 국회(2004)부터 22대(2024-)까지 약 20년간 발의된 법안 현황을 조망합니다. 발의 건수는 꾸준히 증가하는 반면, 가결률은 지속적으로 하락하는 추세를 보입니다.
22대 국회 심층 분석
22대 국회의 17,205건 법안을 위원회별, 처리 상태별, 발의자 유형별로 살펴봅니다. 현재 진행 중인 회기이므로 주기적으로 갱신됩니다.
입법 타임라인
22대 국회 개원(2024.5) 이후 월별 법률안 발의 추이와, 발의자 유형에 따른 처리 소요일 분포를 보여줍니다.
입법 퍼널
법률안이 발의부터 공포까지 각 단계를 얼마나 통과하는지 보여줍니다. 대안반영(위원회 대안에 흡수)된 법안은 별도 경로를 거치므로, 단계별 수치가 단순 감소하지 않을 수 있습니다.
표결 패턴
본회의 기명 표결이 이루어진 1,236건의 법안에 대한 투표 현황입니다. 찬성률 80% 미만 법안을 '쟁점 법안'으로 표시하여 여야 갈등 법안을 식별합니다.
의원 발의 순위
22대 국회에서 법률안을 가장 많이 대표발의한 의원 20인입니다. 막대 색상은 해당 의원의 협의 가결률(enacted rate)을 나타냅니다.
데이터 구축 현황
대수별 마스터 데이터 구축 수준을 정리합니다. 17-22대 모두 열린국회정보 API를 결합한 Full Master이며, 공포 건수·소관위 처리·처리 소요일 등 법안 생애주기 전체를 포함합니다. 표결 데이터(건별 찬반)는 20-22대에서 API로 제공됩니다.
| 대수 | 총 법안 | 수준 | 표결 데이터 | 표결 건수 | 소관위 처리 | 공포 건수 | 공동발의자 | 처리 소요일 |
|---|---|---|---|---|---|---|---|---|
| 17대 | 8,369 | Full Master | - | - | O | 1,912 | O | O |
| 18대 | 14,762 | Full Master | - | - | O | 2,353 | O | O |
| 19대 | 18,735 | Full Master | - | - | O | 2,793 | O | O |
| 20대 | 24,996 | Full Master | O | 3,435 | O | 3,195 | O | O |
| 21대 | 26,711 | Full Master | O | 3,220 | O | 2,959 | O | O |
| 22대 | 17,205 | Full Master | O | 1,236 | O | 1,060 | O | O |
이 데이터로 답할 수 있는 연구 질문들
아래는 본 데이터베이스를 활용하여 탐구할 수 있는 연구 질문입니다. 각 질문에 필요한 변수, 방법론, 그리고 데이터 가용 수준을 함께 표기합니다.
입법 인플레이션과 중복 발의
법안 발의 건수의 급증은 실질적 의제 다양성 확대인가, 아니면 동일 법률에 대한 중복 발의(credit claiming)의 증가인가?
bill_nm, ppsl_dt, rst_proposer, committee_nm조세특례제한법 일부개정법률안만 661건이 발의되었습니다.
17,205건 중 고유 법안명은 2,984종뿐입니다.
여소야대와 입법 효율
Divided government(여소야대)는 법안 통과율과 처리 소요기간에 어떤 영향을 미치는가? 어느 입법 단계에서 그 효과가 가장 두드러지는가?
passed, enacted, days_to_proc, ppsr_kind + 외부 여소야대 코딩위원회 병목과 법안의 죽음
법안은 입법 과정의 어느 단계에서 "죽는가"? 위원회별로 병목 패턴이 다른가? 위원회 위원장의 정당이 심사 속도에 영향을 미치는가?
ppsl_dt ~ prom_dt (전 lifecycle 타임스탬프), committee_nm, status법사위 Veto Player 가설
법제사법위원회가 야당 법안의 사실상 거부권자(veto player)로 기능하는가? 법사위 체계/자구 심사가 정치적 필터링 기제인가?
law_submit_dt, law_proc_dt, law_proc_rslt + 발의자 여야 코딩법안 통과 예측과 제도적 변수의 중요도
법안 특성(발의 주체, 위원회, 공동발의 규모, 시기 등)으로 통과 여부를 예측할 수 있는가? 어떤 변수가 가장 예측력이 높은가?
passed/enacted (Y), ppsr_kind, committee_nm, proposer_text(공동발의자 수 파싱), ppsl_dt(시기)공동발의 네트워크와 입법 성과
교차정당 공동발의 관계가 실제 법안 통과율을 높이는가? 네트워크상 중심성이 높은 의원의 법안이 더 성공적인가?
rst_mona_cd, publ_mona_cd(파싱 → edge list), passed, enactedpubl_mona_cd 필드를 파싱하면 의원 간 공동발의 edge list를 즉시 생성 가능.
22대 기준 421명의 대표발의자, 13,894개의 고유 공동발의 조합이 존재합니다.
정책 의제 공간의 구조와 변화
한국 국회의 정책 의제 공간은 어떤 구조이며, 정당 간/대수 간 어떻게 변화하는가? 여야가 다른 주제의 법안을 발의하는가?
bill_nm (법안명 텍스트), ppsr_kind, rst_mona_cd + 의원 정당 정보Getting Started
아래 코드 예시를 통해 데이터를 로드하고 기본적인 분석을 시작할 수 있습니다. Python + pandas 환경을 전제합니다.
import pandas as pd
# 22대 Full Master (전 생애주기 타임스탬프 포함)
master = pd.read_parquet("data/processed/master_bills_22.parquet")
print(f"22대: {len(master):,} bills, {len(master.columns)} variables")
# 위원회 회의 기록 (1:N)
meetings = pd.read_parquet("data/processed/committee_meetings_22.parquet")
# 17-21대 Full Master
for age in range(17, 22):
df = pd.read_parquet(f"data/processed/master_bills_{age}.parquet")
print(f"{age}대: {len(df):,} bills, {len(df.columns)} variables")
# 법률안만 필터링
laws = master[master["bill_kind"] == "법률안"]
# 의원 발의만
member_bills = laws[laws["ppsr_kind"] == "의원"]
# 특정 위원회
health = laws[laws["committee_nm"] == "보건복지위원회"]
# 처리 완료된 법안만
processed = laws[laws["status"] != "계류중"]
# 가결된 법안만
enacted = laws[laws["enacted"] == 1]
# publ_mona_cd를 파싱하여 edge list 생성
edges = []
for _, row in member_bills.iterrows():
if pd.notna(row["publ_mona_cd"]) and pd.notna(row["rst_mona_cd"]):
co_sponsors = row["publ_mona_cd"].split(",")
for cs in co_sponsors:
edges.append({
"bill_id": row["bill_id"],
"from": row["rst_mona_cd"],
"to": cs.strip(),
"passed": row["passed"],
})
edge_df = pd.DataFrame(edges)
print(f"Edges: {len(edge_df):,}") # 수십만 개의 공동발의 관계
# 17-22대 통합
frames = []
for age in range(17, 23):
suffix = "" if age == 22 else "_lite"
path = f"data/processed/master_bills_{age}{suffix}.parquet"
df = pd.read_parquet(path)
frames.append(df)
all_bills = pd.concat(frames, ignore_index=True)
print(f"Total: {len(all_bills):,} bills across {all_bills['age'].nunique()} assemblies")
# 대수별 가결률 추이
trend = all_bills.groupby("age").agg(
total=("bill_id", "count"),
enacted=("enacted", "sum"),
).assign(rate=lambda x: x["enacted"] / x["total"] * 100)
print(trend)
import sqlite3
conn = sqlite3.connect("data/processed/master_bills_22.sqlite")
# 위원회별 통과율
query = """
SELECT committee_nm,
COUNT(*) as total,
SUM(enacted) as enacted,
ROUND(SUM(enacted) * 100.0 / COUNT(*), 1) as rate
FROM bills
WHERE bill_kind = '법률안' AND committee_nm IS NOT NULL
GROUP BY committee_nm
HAVING total >= 50
ORDER BY rate DESC
"""
pd.read_sql(query, conn)
R Implementation
R(tidyverse + arrow) 환경에서의 동일한 분석 코드입니다.
library(arrow)
library(dplyr)
library(tidyr)
library(ggplot2)
# 22대 Full Master
master <- read_parquet("data/processed/master_bills_22.parquet")
cat(sprintf("22대: %s bills, %d variables\n", format(nrow(master), big.mark=","), ncol(master)))
# 위원회 회의 기록
meetings <- read_parquet("data/processed/committee_meetings_22.parquet")
# 17-22대 통합
all_bills <- bind_rows(
lapply(17:22, function(age) read_parquet(sprintf("data/processed/master_bills_%d.parquet", age)))
)
cat(sprintf("Total: %s bills\n", format(nrow(all_bills), big.mark=",")))
library(tidyplots) # 또는 ggplot2
trend <- all_bills %>%
group_by(age) %>%
summarise(
total = n(),
enacted = sum(enacted),
passed = sum(passed),
.groups = "drop"
) %>%
mutate(
enact_rate = enacted / total * 100,
pass_rate = passed / total * 100
)
# tidyplots 방식
trend %>%
pivot_longer(cols = c(enact_rate, pass_rate),
names_to = "measure", values_to = "rate") %>%
tidyplot(x = factor(age), y = rate, color = measure) %>%
add_data_points(size = 3) %>%
add_line() %>%
adjust_colors(c("#D55E00", "#0072B2")) %>%
adjust_labels(x = "국회 대수", y = "가결률 (%)") %>%
remove_title() %>%
adjust_size(width = 150, height = 90) %>%
save_plot("output/passage_trend.pdf")
cmt_stats <- master %>%
filter(bill_kind == "법률안", !is.na(committee_nm)) %>%
group_by(committee_nm) %>%
summarise(
total = n(),
enacted = sum(enacted),
passed = sum(passed),
avg_days = mean(days_to_proc, na.rm = TRUE),
.groups = "drop"
) %>%
filter(total >= 50) %>%
mutate(enact_rate = enacted / total * 100) %>%
arrange(desc(total))
# 가결률 수평 막대
cmt_stats %>%
tidyplot(x = enact_rate, y = reorder(committee_nm, enact_rate)) %>%
add_barstack_absolute() %>%
add_reference_lines(x = median(cmt_stats$enact_rate), linetype = "dashed") %>%
adjust_colors("#57068C") %>%
adjust_labels(x = "가결률 (%)", y = "") %>%
remove_title() %>%
adjust_size(width = 150, height = 120) %>%
save_plot("output/committee_rates.pdf")
library(survival)
library(fixest)
# 처리까지의 생존 데이터 구성
surv_data <- master %>%
filter(bill_kind == "법률안", ppsr_kind %in% c("의원", "정부", "위원장")) %>%
mutate(
# 관측 종료: 처리일 또는 현재 날짜
event = as.integer(!is.na(proc_dt)),
duration = as.numeric(
difftime(coalesce(proc_dt, Sys.Date()), ppsl_dt, units = "days")
)
) %>%
filter(duration >= 0)
# Kaplan-Meier by proposer type
km <- survfit(Surv(duration, event) ~ ppsr_kind, data = surv_data)
plot(km, col = c("#E69F00", "#56B4E9", "#009E73"),
xlab = "Days", ylab = "Survival probability",
lwd = 2, mark.time = FALSE)
legend("topright", levels(factor(surv_data$ppsr_kind)),
col = c("#E69F00", "#56B4E9", "#009E73"), lwd = 2)
# Cox PH model
cox <- coxph(Surv(duration, event) ~ ppsr_kind + committee_nm, data = surv_data)
summary(cox)
library(fixest)
# 공동발의자 수 파싱
reg_data <- master %>%
filter(bill_kind == "법률안", ppsr_kind == "의원") %>%
mutate(
n_cosponsors = stringr::str_extract(proposer_text, "\\d+") %>% as.integer(),
month = format(ppsl_dt, "%Y-%m")
) %>%
filter(!is.na(n_cosponsors))
# Linear probability model with committee FE
m1 <- feols(enacted ~ n_cosponsors | committee_nm, data = reg_data)
m2 <- feols(passed ~ n_cosponsors | committee_nm, data = reg_data)
etable(m1, m2, se = "hetero",
headers = c("Enacted", "Passed (broad)"),
notes = "Committee FE included. Robust SE.")
CODEBOOK.md에 54개 변수의 상세 설명,
DATA_AVAILABILITY.md에 대수별 데이터 가용성 및 제약사항,
MASTER_DATA_PLAN.md에 확장 로드맵이 정리되어 있습니다.