Korean National Assembly Database
열린국회정보 Open API의 여러 endpoint를 결합하여 구축한 대한민국 국회 법안 생애주기 마스터 데이터베이스. 17대부터 22대까지의 발의, 심사, 표결, 공포 전 과정을 추적합니다. 2026-09-23까지 발의된 법안을 포함합니다.
데이터 구조와 수집 파이프라인
열린국회정보 Open API의 목록, 법안별 상세, 제안자·소위·대안 endpoint를 결합하여 법안 단위(bill-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, plenary_decided, promulgated, law_reflected
재의요구 vetoed, veto_dt, revote_rslt, first_plenary_rslt
표결 vote_yes, vote_no, vote_abstain
파생 days_to_proc, days_to_committee
Cross-Assembly Overview
17대 국회(2004)부터 22대(2024-)까지 약 20년간 발의된 법안 현황을 조망합니다. 발의 건수는 꾸준히 증가하는 반면, 가결률은 지속적으로 하락하는 추세를 보입니다.
22대 국회 심층 분석
22대 국회의 21,581건 법안을 위원회별, 처리 상태별, 발의자 유형별로 살펴봅니다. 현재 진행 중인 회기이므로 주기적으로 갱신됩니다.
입법 타임라인
22대 국회 개원(2024.5) 이후 월별 법률안 발의 추이와, 발의자 유형에 따른 처리 소요일 분포를 보여줍니다.
입법 퍼널
법률안이 발의부터 공포까지 각 단계를 얼마나 통과하는지 보여줍니다.
각 단계의 건수는 그 단계에 도달했거나 이후 단계까지 간 법안 수입니다.
위원장 대안처럼 앞 단계를 건너뛴 법안도 이후 단계에 도달했다면 앞 단계에 포함됩니다.
본회의 의결은 본회의에서 원안가결, 수정가결 또는 부결된 법안(plenary_decided)이고,
공포는 공포일이 기록된 법률안(promulgated)입니다. kna stats funnel과 같은 정의입니다.
표결 패턴
본회의 기명 표결이 이루어진 1,847건의 법안에 대한 투표 현황입니다. 찬성률 80% 미만 법안을 '쟁점 법안'으로 표시하여 여야 갈등 법안을 식별합니다.
의원 발의 순위
22대 국회에서 법률안을 가장 많이 대표발의한 의원 20인입니다. 공동 대표발의도 각 의원의 건수에 포함합니다. 막대 색상은 해당 의원의 협의 가결률(enacted rate)을 나타냅니다.
데이터 구축 현황
대수별 마스터 데이터 구축 수준을 정리합니다. 17-22대 모두 열린국회정보 API를 결합한 Full Master이며, 공포 건수·소관위 처리·처리 소요일 등 법안 생애주기 전체를 포함합니다. 표결 데이터(건별 찬반)는 20-22대는 API에서, 17-19대는 본회의 회의록 부록의 명단에서 구축했습니다.
| 대수 | 총 법안 | 수준 | 표결 데이터 | 표결 건수 | 소관위 처리 | 공포 (법률안) | 공동발의 법안 | 처리 소요일 |
|---|---|---|---|---|---|---|---|---|
| 17대 | 8,368 | Full Master | O | 2,167 | O | 1,913 | 5,728 | O |
| 18대 | 14,762 | Full Master | O | 2,539 | O | 2,353 | 11,191 | O |
| 19대 | 18,735 | Full Master | O | 3,095 | O | 2,793 | 15,444 | O |
| 20대 | 24,996 | Full Master | O | 3,475 | O | 3,195 | 21,594 | O |
| 21대 | 26,707 | Full Master | O | 3,266 | O | 2,959 | 23,655 | O |
| 22대 | 21,581 | Full Master | O | 1,847 | O | 1,555 | 19,651 | O |
- 22대 의원별 표결은 2026년 9월 17일까지 수집했습니다. 의원별 표결 API가 빠뜨린 2026년 재직 의원의 표결 4,406행은 의안정보시스템의 표결 페이지에서 보완했습니다. 22대 표결 1,847건 중 1건만 의원별 찬성·반대·기권 수가 공식 집계와 다르며, 그 표결은 공식 페이지에서도 명단과 집계가 다릅니다.
- 대안반영폐기 법안을 흡수한 대안에 연결하는 비율은 17대가 57.5%로, 18-22대의 99.3% 이상보다 낮습니다. 17대의 숫자형 의안 ID 대안은 공식 API가 흡수 법안 목록을 거의 돌려주지 않기 때문입니다.
- 의원이 발의한 결의안 등 법률안 외 의안 2,032건은 제안자 목록이 없어 공동발의 edge가 없습니다.
- 제안이유 텍스트는 의안정보시스템에서 수집한 텍스트와 공식 API의 제안이유 및 주요내용을 합친 것입니다. 텍스트가 있는 가장 최근 법안은 2026-09-23에 발의되었습니다. API가 빈 텍스트를 돌려주는 법률안은 텍스트가 없습니다.
이 데이터로 답할 수 있는 연구 질문들
아래는 본 데이터베이스를 활용하여 탐구할 수 있는 연구 질문입니다. 각 질문에 필요한 변수, 방법론, 그리고 데이터 가용 수준을 함께 표기합니다.
입법 인플레이션과 중복 발의
법안 발의 건수의 급증은 실질적 의제 다양성 확대인가, 아니면 동일 법률에 대한 중복 발의(credit claiming)의 증가인가?
bill_nm, ppsl_dt, rst_proposer, committee_nm조세특례제한법 일부개정법률안만 849건이 발의되었습니다.
21,581건 중 고유 법안명은 3,641종뿐입니다.
여소야대와 입법 효율
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(시기)공동발의 네트워크와 입법 성과
교차정당 공동발의 관계가 실제 법안 통과율을 높이는가? 네트워크상 중심성이 높은 의원의 법안이 더 성공적인가?
cosponsorship_edges의 bill_id, member_id, role, party + 마스터의 passed, enactedcosponsorship_edges. 17-22대 의원발의 법안 97,263건, edge 1,379,763개.정책 의제 공간의 구조와 변화
한국 국회의 정책 의제 공간은 어떤 구조이며, 정당 간/대수 간 어떻게 변화하는가? 여야가 다른 주제의 법안을 발의하는가?
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]
# 공식 공동발의 테이블 (17-22대, 1 row = 1 bill x 발의자)
edges = pd.read_parquet("data/processed/cosponsorship_edges.parquet")
edges22 = edges[edges["age"] == 22]
print(edges22["role"].value_counts()) # 대표발의 / 공동발의 / 찬성
# 법안 결과를 붙여 발의자-법안 네트워크 구성
edge_df = edges22.merge(master[["bill_id", "passed", "enacted"]], on="bill_id")
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에 마스터 테이블 67개 변수의 상세 설명,
DATA_AVAILABILITY.md에 대수별 데이터 가용성 및 제약사항,
CORRECTIONS.md에 정정 이력이 정리되어 있습니다.