Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: pages

# main 에 반영되면 정적 대시보드(_site/)를 만들어 GitHub Pages 로 배포한다.
# 최초 1회: Settings → Pages → Build and deployment → Source: "GitHub Actions"
# 선택: Settings → Secrets → Actions 에 LAW_OC(법제처 OC) 를 넣으면 조례 목록을 자동 수집한다.
on:
push:
branches: [main]
workflow_dispatch:
schedule:
- cron: "0 18 * * 0" # 매주 월요일 03:00 KST 데이터 갱신

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: pages
cancel-in-progress: true

jobs:
build:
runs-on: ubuntu-latest
env:
LAW_OC: ${{ secrets.LAW_OC }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: pip
- run: pip install -e .
- name: Refresh ordinance snapshots (법제처, optional)
if: ${{ env.LAW_OC != '' }}
run: python scripts/refresh_law_snapshots.py
- run: python site/build.py
- uses: actions/upload-pages-artifact@v3
with:
path: _site

deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v4
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,4 @@ Thumbs.db
.idea/
.vscode/
*.swp
_site/
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

---

**대시보드**: https://causalinferencelab.github.io/policy-effect-analytics-agent/ (GitHub Pages, `main` 반영 시 자동 배포)

## 왜 하나요?

1. 공공·사회 문제를 **데이터로 정의**하고, 그 해법(정책)이 **실제로 어떤 효과를 냈는지 추정**합니다.
Expand Down Expand Up @@ -75,9 +77,12 @@ cp -r cases/_template cases/group3-youth-rent # 폴더명: <조>-<주제>, 소
| 6 | 리포트·플랫폼 | `report.md`, Streamlit 반영 |
| 7 | 발표·회고·공개 정리 | 최종 PR 머지, 릴리스 태그 |

## 이슈 → 데이터 → 효과
## 이슈 → 주제 → 정책 전체 → 효과

소셜 반응(뉴스 제목·SNS 글) 한 건에서 출발해 정책을 찾고, 공공데이터를 추천하고, 효과 분석까지 잇습니다.
소셜 반응(뉴스 제목·SNS 글)은 **어느 주제를 볼지까지만** 정합니다. 화제가 된 정책 하나만 골라 분석하면
결과를 보고 사례를 고르는 셈이 되기 때문입니다(출발 키트 05). 주제가 정해지면 그 주제의 정책을
전부 모으고(법제처 조례·고시), 세 관문(언제·누가·무엇을)을 통과한 것만 분석합니다.
주제 목록: `catalog/topics.yaml` · 정적 대시보드: `python site/build.py` → `_site/`

1. **정책 식별**: `catalog/policies.yaml`(정책 × 데이터셋 카탈로그)에서 키워드로 찾습니다. LLM은 후보 중에서 고르는 보조 역할만 합니다.
2. **데이터셋 추천**: 카탈로그에 검증해 둔 데이터셋과 공공데이터포털 실시간 검색 결과를 보여줍니다.
Expand Down
Binary file modified cases/t3-land-permit-2025/figures/event_study.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 17 additions & 17 deletions cases/t3-land-permit-2025/flow_log.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@
"question": "2025년 3월 24일 강남·서초·송파·용산구 전체 아파트를 토지거래허가구역으로 지정한 뒤, 서울의 나머지 21개 자치구와 비교해 이 4개 구의 아파트 매매 거래건수와 ㎡당 가격이 달라졌는가?\n",
"status": "warn",
"verdict": "conditional",
"started_at": "2026-09-26T07:07:15+00:00",
"ended_at": "2026-09-26T07:07:21+00:00",
"started_at": "2026-09-26T07:28:46+00:00",
"ended_at": "2026-09-26T07:28:52+00:00",
"steps": [
{
"index": 1,
"step": "define_problem",
"title": "① 문제 정의",
"status": "ok",
"started_at": "2026-09-26T07:07:15+00:00",
"ended_at": "2026-09-26T07:07:15+00:00",
"duration_s": 0.011,
"started_at": "2026-09-26T07:28:46+00:00",
"ended_at": "2026-09-26T07:28:46+00:00",
"duration_s": 0.012,
"message": "분석계획 검증 완료, 사전 등록(git 커밋) 확인: t3-land-permit-2025",
"artifacts": {
"plan": "plan.yaml",
Expand All @@ -27,8 +27,8 @@
"step": "collect",
"title": "② 데이터 수집",
"status": "warn",
"started_at": "2026-09-26T07:07:15+00:00",
"ended_at": "2026-09-26T07:07:15+00:00",
"started_at": "2026-09-26T07:28:46+00:00",
"ended_at": "2026-09-26T07:28:46+00:00",
"duration_s": 0.0,
"message": "기존 스냅샷 재사용 (새로 받으려면 파일 삭제 후 재실행) / ⚠️ 합성 데이터 — 결과는 실제 정책 효과가 아님",
"artifacts": {
Expand All @@ -47,8 +47,8 @@
"step": "structure_metrics",
"title": "③ 지표 구조화·품질 점검",
"status": "ok",
"started_at": "2026-09-26T07:07:15+00:00",
"ended_at": "2026-09-26T07:07:15+00:00",
"started_at": "2026-09-26T07:28:46+00:00",
"ended_at": "2026-09-26T07:28:46+00:00",
"duration_s": 0.004,
"message": "12개 점검 모두 통과",
"artifacts": {
Expand All @@ -61,9 +61,9 @@
"step": "estimate",
"title": "④ 효과 추정",
"status": "warn",
"started_at": "2026-09-26T07:07:15+00:00",
"ended_at": "2026-09-26T07:07:20+00:00",
"duration_s": 4.896,
"started_at": "2026-09-26T07:28:46+00:00",
"ended_at": "2026-09-26T07:28:51+00:00",
"duration_s": 5.026,
"message": "event_study: -0.403 [-0.494, -0.323] → identified",
"artifacts": {
"log_trades": {
Expand All @@ -89,8 +89,8 @@
"step": "guard",
"title": "⑤ 과잉해석 가드",
"status": "ok",
"started_at": "2026-09-26T07:07:20+00:00",
"ended_at": "2026-09-26T07:07:20+00:00",
"started_at": "2026-09-26T07:28:51+00:00",
"ended_at": "2026-09-26T07:28:51+00:00",
"duration_s": 0.0,
"message": "판정 identified, 서술=template",
"artifacts": {
Expand All @@ -104,9 +104,9 @@
"step": "report",
"title": "⑥ 리포트·재현 기록",
"status": "ok",
"started_at": "2026-09-26T07:07:20+00:00",
"ended_at": "2026-09-26T07:07:21+00:00",
"duration_s": 0.31,
"started_at": "2026-09-26T07:28:51+00:00",
"ended_at": "2026-09-26T07:28:52+00:00",
"duration_s": 0.334,
"message": "리포트 2개 그림 포함 작성",
"artifacts": {
"report": "report.md",
Expand Down
6 changes: 3 additions & 3 deletions cases/t3-land-permit-2025/run_manifest.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"case_id": "t3-land-permit-2025",
"started_at": "2026-09-26T07:07:15+00:00",
"finished_at": "2026-09-26T07:07:20+00:00",
"git_sha": "0e9570f1f745fa79b42366e581270e928bb1a41d",
"started_at": "2026-09-26T07:28:46+00:00",
"finished_at": "2026-09-26T07:28:52+00:00",
"git_sha": "3ccf0b76a74f39efc81fdcd9537e96c99efece83",
"git_dirty": true,
"plan_sha256": "8f7e7b2d06338470404f27bdcac6a6acf9784785e5851a6729ca6673b27a1ff2",
"data_sha256": "c35f969dc154aa5732b6e05759185eb3cf246f9a3f9641c5182ab89214a43aa6",
Expand Down
98 changes: 98 additions & 0 deletions catalog/topics.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# 주제(topic) 카탈로그 — 소셜 신호는 "어느 주제를 볼지"까지만 정합니다.
#
# 출발 키트 05 "주제 고르는 법"의 B 방식:
# 소셜 신호 → 주제 지정 → 그 주제의 정책을 전부 모으기(법령·조례·고시) → 3개 관문 → 계획 먼저 → 효과 추정
# 소셜 신호는 분석 대상을 고르는 데 쓰지 않고, ① 주제 좁히기 ② 선반영(미리 반응) 점검 ③ 관심도 자체를 결과로 — 에만 씁니다.
#
# collect: 정책 목록을 모으는 방법
# law_ordinance : 법제처 자치법규 API로 조례 제정일·지자체를 자동 수집 (query 사용)
# notice : 부처·지자체 고시문에서 지정·해제 이력을 수집 (에이전트 과제)
# curated : 카탈로그에 적은 정책만 (전국 단위 등)

version: 1
topics:
- id: housing-regulation
name: 부동산 거래 규제
question: 토지거래허가구역·규제지역 지정과 해제는 거래와 가격을 바꿨나?
keywords: [부동산, 집값, 아파트, 토지거래허가, 토허제, 토허구역, 토허, 규제지역, 투기과열, 조정대상, 강남3구, 용산, 잠삼대청, 풍선효과, 실거래가, 거래량]
policies: [T3]
collect:
method: notice
note: "국토부·서울시 고시에서 지정·해제 이력 전체(2017~)를 수집 → 지역×시점 처치표. 정리된 표가 없어 에이전트에게 맡기기 좋은 일"
events:
- {date: 2020-06-23, what: "잠실·삼성·대치·청담 토지거래허가구역 지정"}
- {date: 2025-02-12, what: "잠실·삼성·대치·청담 지정 해제 (발표·시행 월 기준)"}
- {date: 2025-03-24, what: "강남·서초·송파·용산 전역 지정", source: "https://www.ajunews.com/view/20250324064239711"}
- {date: 2025-10-20, what: "서울 전역·경기 12곳 지정 (10·15 대책)", source: "https://www.korea.kr/news/policyNewsView.do?newsId=148950973"}
gates:
when: {status: pass, note: "고시·보도자료로 날짜 확인"}
who: {status: pass, note: "지정 구 vs 미지정 구"}
what: {status: key, note: "국토부 실거래가 API — 키 발급 전이라 지금은 시뮬레이션"}

- id: local-currency
name: 지역화폐 (지역사랑상품권)
question: 지역화폐를 일찍 도입한 지자체의 지역 소비·소상공인 매출이 달라졌나?
keywords: [지역화폐, 지역사랑상품권, 지역상품권, 상품권, 지역경제, 골목상권, 소상공인]
policies: []
collect:
method: law_ordinance
query: [지역사랑상품권, 지역화폐]
note: "법제처 자치법규 API로 지자체별 조례·시행일자 자동 수집 (무료, 신청 즉시 승인). 발행 지자체가 2018년 66곳 → 2019년 172곳으로 늘어 시차 도입 비교가 가능. 검색 결과는 최신 개정 기준이라 '제정일'은 연혁으로 확인"
gates:
when: {status: pass, note: "조례 제정일을 법제처 API로 자동 수집"}
who: {status: pass, note: "일찍 도입 vs 늦게 도입 지자체 (시차 도입)"}
what: {status: key, note: "조폐공사 결제정보 API·상권 매출 — 키 필요"}
pitfalls: ["옆 지자체 소비를 끌어온다는 지적 → 인접 지자체 대조군 오염", "조례 제정일과 실제 발행일이 다름", "이름이 같아도 할인율·규모가 다름"]

- id: traffic-safety
name: 교통안전
question: 속도 하향·처벌 강화 같은 교통안전 정책은 사고를 줄였나?
keywords: [교통사고, 교통안전, 안전속도, "5030", 제한속도, 민식이법, 스쿨존, 어린이보호구역, 윤창호법, 음주운전, 보행자]
policies: [T4, T5, T6]
collect:
method: curated
note: "지자체별 조기 시행일(5030)은 보도자료·고시로 추가 수집"
gates:
when: {status: pass, note: "법 시행일 확정"}
who: {status: check, note: "5030은 조기 도입 지자체로 구분 가능, 민식이법·윤창호법은 전국 동시(관문 2 약함)"}
what: {status: key, note: "TAAS·도로교통공단 API — 키 필요"}

- id: air-quality
name: 대기질
question: 운행제한·계절관리제는 미세먼지와 이산화질소를 줄였나?
keywords: [미세먼지, 초미세먼지, 대기질, 공기, 녹색교통, 5등급, 운행제한, 계절관리제, NO2, 이산화질소, PM2.5]
policies: [T1, T8]
collect:
method: curated
note: "시행일만 보고 시행(오염도와 무관) → 비교가 깔끔"
gates:
when: {status: pass, note: "2019.12 시작, 매년 12~3월"}
who: {status: check, note: "녹색교통: 사대문 안 측정소 소수(합성통제 필요). 계절관리제: 수도권 위주 → 비수도권 비교 가능한지 확인"}
what: {status: pass, note: "에어코리아 확정자료 파일 (API는 하루 500회 제한)"}

- id: retail-hours
name: 대형마트 의무휴업
question: 의무휴업일을 평일로 바꾼 자치구의 골목상권 매출이 달라졌나?
keywords: [대형마트, 의무휴업, 휴업일, 전통시장, 골목상권, 상권, 매출]
policies: [T2]
collect:
method: notice
note: "자치구별 전환 고시일 수집 (시차 도입)"
gates:
when: {status: check, note: "전환 자치구 목록·날짜를 고시로 구축해야 함"}
who: {status: pass, note: "전환 vs 미전환 자치구"}
what: {status: key, note: "서울 상권분석서비스 추정매출 — 키 필요"}

- id: birth-marriage
name: 출산·혼인 지원
question: 지자체 출산·혼인장려금은 출생과 혼인을 늘렸나?
keywords: [출산, 출생, 저출산, 출산장려금, 출산지원금, 혼인, 결혼장려금, 혼인장려금, 인구]
policies: [T7]
collect:
method: law_ordinance
query: [출산장려금, 혼인장려금]
note: "법제처 자치법규 API로 조례 제정일 수집 → 처치 데이터셋 구축이 과제의 절반"
gates:
when: {status: pass, note: "조례 제정일 자동 수집"}
who: {status: pass, note: "도입 vs 미도입 시군구"}
what: {status: key, note: "KOSIS 시군구 출생·혼인 — 키 필요"}
79 changes: 79 additions & 0 deletions core/adapters/law.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""법제처 국가법령정보 공동활용 API — 자치법규(조례) 검색 어댑터.

- 인증: open.law.go.kr 에서 신청(무료, 즉시 승인)한 OC 값을 LAW_OC 환경변수에 넣는다.
- 용도: 주제(예: 지역사랑상품권)의 조례를 지자체별로 모아 '어느 지역이 언제부터'를 자동으로 만든다
(출발 키트 05 관문 1).
- 주의: 검색 결과의 날짜는 **현행(최신 개정) 기준**이다. 처음 제정된 날은 연혁 조회로 확인해야 한다.
그래서 이 어댑터가 만든 표는 '후보 처치표'이고, 제정일 확정은 사람이 검증한다.
"""

from __future__ import annotations

import time
import xml.etree.ElementTree as ET

import pandas as pd

from .base import BaseAdapter, SourceMeta

SEARCH_URL = "https://www.law.go.kr/DRF/lawSearch.do"
KEEP = [
"자치법규ID",
"자치법규명",
"지자체기관명",
"제개정구분명",
"공포일자",
"시행일자",
"자치법규종류",
]


def parse_ordinance_xml(xml: bytes | str) -> tuple[list[dict], int]:
root = ET.fromstring(xml)
total = int(root.findtext("totalCnt") or 0)
rows = [{c.tag: (c.text or "").strip() for c in item} for item in root.iter("law")]
return rows, total


class LawOrdinanceAdapter(BaseAdapter):
meta = SourceMeta(
name="법제처 자치법규 검색",
provider="법제처 국가법령정보 공동활용",
license="other", # 법령·자치법규 본문은 저작권 보호 대상이 아님(저작권법 제7조). 메타데이터만 사용
url="https://open.law.go.kr",
)
api_key_env = "LAW_OC"

def fetch(self, queries: list[str], per_page: int = 100, max_pages: int = 20) -> pd.DataFrame:
import requests

rows: list[dict] = []
for q in queries:
page, total = 1, None
while page <= max_pages and (total is None or (page - 1) * per_page < total):
r = requests.get(
SEARCH_URL,
params={
"OC": self.api_key(),
"target": "ordin",
"type": "XML",
"query": q,
"display": per_page,
"page": page,
},
timeout=30,
)
r.raise_for_status()
got, total = parse_ordinance_xml(r.content)
rows += [{**g, "query": q} for g in got]
page += 1
time.sleep(0.3)
return tidy(pd.DataFrame(rows))


def tidy(df: pd.DataFrame) -> pd.DataFrame:
if df.empty:
return pd.DataFrame(columns=[*KEEP, "query", "year"])
df = df[[c for c in [*KEEP, "query"] if c in df.columns]].drop_duplicates("자치법규ID")
date = pd.to_datetime(df["시행일자"], format="%Y%m%d", errors="coerce")
return df.assign(year=date.dt.year).sort_values("시행일자").reset_index(drop=True)
2 changes: 1 addition & 1 deletion core/agent/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ def report(s: FlowState):
figs["Raw trends"] = "figures/raw_trends.png"
if "coefs" in r.extra:
note = (
"per-period CIs cluster-robust, reference only: few treated units"
"per-period CIs: reference only (few treated units)"
if "few_treated_clusters" in r.triggers
else "cluster-robust"
)
Expand Down
13 changes: 10 additions & 3 deletions core/discovery/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
"""소셜 반응 → 정책 식별 → 데이터셋 추천 → 분석계획 연결 (Flow ① 앞단).
"""소셜 반응 → 주제 → 주제 안의 정책 전체 → 데이터셋 → 분석계획 (Flow ① 앞단).

원칙(출발 키트 B): 소셜 신호는 '어느 주제를 볼지'까지만 정한다. 화제가 된 정책 하나만
골라 분석하면 결과를 보고 사례를 고르는 셈이 되어 효과가 과대 추정된다.

from core.discovery import discover
r = discover("토허제 확대하고 강남 집값 잡혔나요?")
Expand All @@ -11,15 +14,19 @@
- 분석 방법은 LLM이 아니라 카탈로그의 design(규칙 기반 판별 결과)으로 정한다.
"""

from .catalog import Dataset, Policy, load_catalog
from .match import Match, match_issue
from .catalog import Dataset, Policy, Topic, load_catalog, load_topics
from .match import Match, TopicMatch, match_issue, match_topic
from .pipeline import DiscoveryResult, discover
from .search import SearchHit, search_datago

__all__ = [
"Dataset",
"Policy",
"load_catalog",
"Topic",
"load_topics",
"TopicMatch",
"match_topic",
"Match",
"match_issue",
"SearchHit",
Expand Down
Loading
Loading