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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ cases/
<조-주제>/ 조별 케이스 (plan.yaml, fetch.py, estimate.py, report.md, figures/)
app/streamlit_app.py 케이스 브라우저 (API 키 없이 실행)
docs/ops/ 조별 운영 가이드, GitHub 온보딩, 모니터링
catalog/ 정책 × 데이터셋 카탈로그 (이슈 → 데이터 연결)
docs/strategy/ 문제 정의·전략 문서
scripts/ 운영 스크립트 (weekly_activity.py 등)
tests/ 테스트
Expand Down Expand Up @@ -74,6 +75,22 @@ cp -r cases/_template cases/group3-youth-rent # 폴더명: <조>-<주제>, 소
| 6 | 리포트·플랫폼 | `report.md`, Streamlit 반영 |
| 7 | 발표·회고·공개 정리 | 최종 PR 머지, 릴리스 태그 |

## 이슈 → 데이터 → 효과

소셜 반응(뉴스 제목·SNS 글) 한 건에서 출발해 정책을 찾고, 공공데이터를 추천하고, 효과 분석까지 잇습니다.

1. **정책 식별**: `catalog/policies.yaml`(정책 × 데이터셋 카탈로그)에서 키워드로 찾습니다. LLM은 후보 중에서 고르는 보조 역할만 합니다.
2. **데이터셋 추천**: 카탈로그에 검증해 둔 데이터셋과 공공데이터포털 실시간 검색 결과를 보여줍니다.
3. **분석 설계**: 카탈로그의 설계(이중차분·합성통제·단절 시계열)로 정합니다. LLM이 고르지 않습니다.
4. **효과 분석**: 사전 등록된 `plan.yaml`로 아래 Flow를 실행합니다.

```bash
make app # 사이드바 '이슈 → 데이터 → 효과'
python -c "from core.discovery import discover; r=discover('토허제 확대하고 집값 잡혔나'); print(r.top.name, r.next_step)"
```

샘플: [`cases/t3-land-permit-2025`](cases/t3-land-permit-2025/) (현재 시뮬레이션 데이터, API 키 발급 후 실데이터로 전환)

## Flow — 6단계 에이전트 흐름

`core.agent`가 케이스 하나를 아래 6단계로 실행하고 `cases/<케이스>/flow_log.json`에 단계별 결과를 남깁니다. 앱의 **Flow** 페이지(`make app` → 사이드바 Flow)에서 실행하거나 저장된 로그를 볼 수 있습니다.
Expand Down
158 changes: 158 additions & 0 deletions app/pages/0_Issue_to_Effect.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
"""이슈 → 데이터 → 효과: 소셜 반응 한 건을 정책·데이터셋·효과 분석까지 잇는 화면."""

from __future__ import annotations

import sys
from pathlib import Path

import streamlit as st

APP_DIR = Path(__file__).resolve().parents[1]
for p in (APP_DIR, APP_DIR.parent):
if str(p) not in sys.path:
sys.path.insert(0, str(p))

import flow_view as fv # noqa: E402

from core.discovery import discover # noqa: E402

EXAMPLES = { # 직접 작성한 예시 문장 (실제 게시글 인용 아님)
"부동산": "토허제 강남3구·용산까지 확대하고 나서 집값 진짜 잡힌 거 맞음? 옆 동네만 올랐다는 얘기도 있던데",
"교통안전": "민식이법 시행되고 스쿨존 어린이 사고가 줄었다는 게 사실인가요?",
"속도제한": "5030 속도 줄이고 나서 보행자 교통사고 줄었다는데 체감이 안 됨",
"대기질": "사대문 안 5등급 차 운행제한하고 도심 공기 좋아졌나?",
}

st.set_page_config(page_title="이슈 → 데이터 → 효과", layout="wide")
st.title("이슈 → 데이터 → 효과")
st.caption(
"소셜 반응(뉴스 제목·SNS 글)에서 정책을 찾고 → 공공데이터를 추천하고 → 효과를 분석합니다."
)

with st.sidebar:
st.markdown("**예시 입력** (직접 작성한 문장)")
for label, text in EXAMPLES.items():
if st.button(label, use_container_width=True):
st.session_state["issue"] = text
live = st.checkbox(
"공공데이터포털 실시간 검색",
value=False,
help="data.go.kr 검색 결과를 함께 보여줍니다(네트워크 필요).",
)
use_llm = st.toggle("LLM 보조", value=False, disabled=not fv.llm_available())

text = st.text_area(
"① 소셜 반응", key="issue", height=90, placeholder="예: 토허제 확대하고 강남 집값 잡혔나요?"
)
if not text.strip():
st.info("왼쪽 예시를 누르거나 뉴스 제목·SNS 글을 붙여 넣으세요.")
st.stop()

res = discover(text, live_search=live, use_llm=use_llm)
if not res.matches:
st.warning(res.next_step)
st.stop()

# ② 정책 식별 -------------------------------------------------------------------------
st.subheader("② 정책 식별")
idx = st.radio(
"후보",
range(len(res.matches)),
format_func=lambda i: (
f"{res.matches[i].policy.id} · {res.matches[i].policy.name} (점수 {res.matches[i].score:.0f})"
),
horizontal=True,
label_visibility="collapsed",
)
m = res.matches[idx]
p = m.policy
c = st.columns([3, 2])
with c[0]:
st.markdown(f"**{p.name}** \n{p.summary}")
st.caption(m.reason)
if p.sources:
st.markdown("출처: " + " · ".join(f"[{i + 1}]({u})" for i, u in enumerate(p.sources)))
with c[1]:
dates = [
f"발표 {p.announced}" if p.announced else "",
f"시행 {p.effective}" if p.effective else "",
]
st.markdown(f"**시점** {' / '.join(d for d in dates if d) or '확인 필요'}")
st.markdown(f"**분석 단위** {p.unit}")
st.markdown(
f"**설계** {p.design_ko} — "
+ (f":green[{p.support}]" if p.support == "지원" else f":orange[{p.support}]")
)
if p.pitfalls:
with st.expander(f"식별상 함정 {len(p.pitfalls)}개", expanded=True):
for x in p.pitfalls:
st.markdown(f"- {x}")

# ③ 데이터셋 추천 ---------------------------------------------------------------------
st.subheader("③ 추천 데이터셋")
access_ko = {"api_key": "API 키(자동승인)", "file": "파일", "manual": "수동 수집"}
st.dataframe(
[
{
"데이터셋": d.name,
"제공": d.provider,
"용도": {"outcome": "결과", "treatment": "처치", "covariate": "공변량"}[d.role],
"단위": d.granularity,
"접근": access_ko[d.access],
"라이선스": d.license,
"링크": d.url,
}
for d in p.datasets
],
column_config={"링크": st.column_config.LinkColumn()},
hide_index=True,
use_container_width=True,
)
if live and idx == 0:
st.caption(res.live_status)
if res.live_hits:
st.dataframe(
[
{
"공공데이터포털 검색": h.title,
"유형": h.kind,
"제공": h.provider,
"수정일": h.modified,
"링크": h.url,
}
for h in res.live_hits
],
column_config={"링크": st.column_config.LinkColumn()},
hide_index=True,
use_container_width=True,
)

# ④ 분석 ------------------------------------------------------------------------------
st.subheader("④ 효과 분석")
case = fv.ROOT / p.sample_case if p.sample_case else None
if case is None or not (case / "plan.yaml").exists():
st.info(
res.next_step
if idx == 0
else "이 정책은 아직 사전 등록된 분석계획이 없습니다. cases/_template 으로 시작하세요."
)
st.stop()

plan = fv.load_plan_dict(case)
st.markdown(
f"사전 등록된 분석계획: `{p.sample_case}/plan.yaml` \n> {plan.get('question', '').strip()}"
)
run = st.button("효과 분석 실행", type="primary")
log = None
if run:
with st.spinner("수집 → 품질 점검 → 추정(무작위화 추론) → 가드 → 리포트 ..."):
try:
log = fv.run_flow_for(case, allow_uncommitted=False, use_llm=use_llm)
except Exception as exc: # noqa: BLE001
st.exception(exc)
else:
log = fv.load_flow_log(case)
if log:
st.caption("저장된 실행 결과를 표시합니다. 다시 돌리려면 '효과 분석 실행'.")
if log:
fv.render_flow(st, case, log, plan)
32 changes: 32 additions & 0 deletions cases/t3-land-permit-2025/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# T3. 토지거래허가구역 확대 지정(2025.3.24) → 아파트 거래

> ⚠️ **현재 결과는 시뮬레이션 데이터입니다.** API 키 발급 전 파이프라인 시연용이며, 수치는 실제 정책 효과가 아닙니다.

**흐름**: 소셜 반응("토허제 확대하고 집값 잡혔나?") → `catalog/policies.yaml` T3 → 이 분석계획 → Flow ①~⑥

| 항목 | 내용 |
|---|---|
| 처치 | 서초·강남·송파·용산구 (2025.3.24 전역 지정, 첫 처치 월 2025-04) |
| 대조 | 서울 나머지 21개 자치구 |
| 기간 | 2024-01 ~ 2025-09 (2025-03 전환월 제외, 2025.10.20 서울 전역 지정 이전까지) |
| 결과 | 아파트 매매 거래건수(로그, 1차), ㎡당 가격 중위값(로그) |
| 추정 | 이벤트 스터디 + **무작위화 추론** (처치 구가 4개뿐이라 군집-강건 표준오차를 쓰지 않음) |

## 실행

```bash
make flow CASE=cases/t3-land-permit-2025 # 또는 앱의 '이슈 → 데이터 → 효과' 화면
```

## 실데이터로 전환

1. [공공데이터포털 15126468](https://www.data.go.kr/data/15126468/openapi.do)에서 활용신청(자동승인) → `.env`에 `DATA_GO_KR_API_KEY`
2. `plan.yaml`에서 `data_sources` 순서를 바꿔 `molit`을 첫 번째로, `synthetic_data: false` → 이 변경만 PR로 커밋
3. `rm data/panel.csv && make flow CASE=cases/t3-land-permit-2025`

`core/adapters/molit.py`는 공식 명세대로 작성했지만 아직 실제 호출로 검증하지 않았습니다. 첫 실행 때 응답 필드명을 확인하세요.

## 주의할 점
- 2025.2.12 잠실·삼성·대치·청담 해제 직후 거래가 급등했습니다. 이 선반영은 사전추세 검정과 가짜 시점 검정으로 확인합니다.
- 마포·성동 등 인접 구로 수요가 옮겨 가면 대조군이 오염되어 효과가 과대 추정됩니다.
- 거래 신고 기한이 30일이라 최근 월은 과소 집계됩니다.
Loading
Loading