충청북도 읍·면·동 단위 지역소멸 대응을 위한 AI 기반 정책 시뮬레이션 웹 애플리케이션입니다.
AI 예측 / SHAP / What-if 계산은 src/의 AI 예측·SHAP·What-if 서비스 레이어를 통해 실행합니다.
UI는 UI_Ref/*/DESIGN.md + code.html/screen.png 목업(상단 탭 네비게이션 + 좌측 "분석 필터" 사이드바)을
Streamlit 네이티브 멀티페이지(st.navigation(position="top"))로 재현했습니다. 목업에 있던 로그인 버튼,
가상의 "소멸위험지수", "일자리 창출 지원금" 같은 항목은 실제 AI 서비스 레이어에 없는 값이라 넣지 않았습니다 —
화면에 보이는 모든 수치는 src/가 계산한 실제 값입니다.
지역(시·군/읍·면·동) 선택은 모든 페이지에 공통으로 뜨는 좌측 사이드바("분석 필터")에서 하며, 선택 상태는 페이지를 이동해도 유지됩니다.
| 탭 | URL | 내용 | 구현 파일 |
|---|---|---|---|
| 지역 선택 (기본 화면) | /region-select |
충북 전체 146개 읍·면·동을 AI 예측 순이동률 기준 색상(choropleth)으로 표시하는 개요 지도, 현재 선택 지역 안내 + 다른 3개 탭으로 이동하는 카드형 버튼, 시·군별 예측 요약표 | views/region_select.py |
| 현황 분석 | /status |
선택한 읍·면·동을 강조 표시하는 지도, 인구구조 카드(총인구/청년비율/고령화율/인구증감률), GIS 접근성 카드(의료·응급의료·교육·교통 평균접근거리), 생활권 인프라 상세 데이터(펼치기) | views/status_overview.py |
| AI 예측 | /ai-prediction |
다음 해 순이동률 예측값(색상·굵은 글씨 강조), Local SHAP 상위 5개 모델 예측 기여도, "AI 분석 결과 해석" 박스, 지역별 최신 데이터 기준 Global SHAP(펼치기) | views/ai_prediction.py |
| 시뮬레이션 | /simulation |
What-if 정책 변수(4종) + 개선율(10/20/30%) 선택, 시나리오별 예측 변화 차트, 기존/조건 변경 후 모델 예측 카드, 시나리오 비교표, "모델 기반 시뮬레이션 분석" 해석 박스 | views/whatif_simulation.py |
새 화면을 추가하거나 기존 화면을 고칠 때 건드릴 파일이 명확히 나뉘어 있습니다.
화면 하나만 고치고 싶으면 views/ 안의 해당 파일만 보면 됩니다.
MVP/
├── app.py # 진입점 — 페이지 정의(st.navigation) + 공통 헤더/CSS/사이드바 호출만 담당
├── state.py # 세션 상태(선택된 시·군/읍·면·동) 초기화·조회 헬퍼
├── ai_module.py # src.* AI 서비스 레이어 어댑터 (모델/데이터 캐싱 + 함수 매핑)
├── run.sh # macOS libomp 폴백 실행 스크립트
├── scripts/
│ └── ai_health_check.py # Streamlit 없이 AI 서비스 레이어 전체 검증
├── icon.jpg # 브라우저 탭 파비콘 (app.py의 page_icon)
├── requirements.txt # pip 의존성 목록
├── packages.txt # Streamlit Cloud(Linux)용 apt 패키지 (libgomp1 — XGBoost용 OpenMP)
├── .gitignore # .venv, __pycache__, .DS_Store, secrets.toml 등 제외
│
├── .streamlit/
│ └── config.toml # 라이트 테마 고정(primaryColor #0055aa 등), 사용통계 수집 끔
│
├── views/ # 화면(페이지)별 콘텐츠 — 탭 하나 = 파일 하나
│ ├── region_select.py # "지역 선택" 탭
│ ├── status_overview.py # "현황 분석" 탭
│ ├── ai_prediction.py # "AI 예측" 탭
│ └── whatif_simulation.py # "시뮬레이션" 탭
│
├── components/ # 여러 화면에서 재사용하는 UI 조각 (수정 시 모든 화면에 반영됨)
│ ├── sidebar.py # 좌측 "분석 필터" 패널 (시·군/읍·면·동 선택 + 인구감소지역 배지)
│ ├── metric_card.py # 지표 카드(render_metric_card/row), 부호 강조 큰 숫자(render_signed_value)
│ ├── charts.py # Plotly 차트 (SHAP 바 차트, What-if 비교 차트) + PLOTLY_CONFIG
│ ├── map_view.py # Folium 지도 (단일 지역 강조 지도 / 전체 개요 choropleth 지도)
│ └── insight_box.py # "해석"/"AI 분석 결과 해석" 박스 (해시태그 칩 포함)
│
├── assets/
│ └── styles.css # 전역 CSS 한 곳에 모음 (UI_Ref/*/DESIGN.md 색상 토큰 기반)
│
├── src/ # AI 예측·SHAP·What-if 서비스 레이어
│ ├── data_loader.py # 모델 번들 / 지역 데이터 로딩, build_latest_region_data()
│ ├── region_service.py # 시군·읍면동 조회, 지역 현황(get_region_summary)
│ ├── inference.py # AI 예측 (predict_region)
│ ├── service.py # 지역 현황 + 예측 통합 (get_region_result)
│ ├── shap_service.py # Global/Local SHAP
│ ├── whatif_service.py # What-if 시나리오 계산
│ └── scenario_service.py # 대표 지역 시연용 시나리오
│
├── models/
│ └── final_model.pkl # 학습된 모델 번들 (model/features/target 딕셔너리)
│
├── data/
│ ├── chungbuk_dong.geojson # 읍면동 행정경계 (지역코드=adm_cd2로 AI 데이터와 매칭)
│ ├── dong_data.py # 지도용 GeoJSON 헬퍼 + 인구감소지역 목록(DEPOPULATION_AREAS)
│ └── 충북_최종학습데이터_E세트.csv # AI 서비스 레이어 학습/조회용 실데이터
│
└── UI_Ref/ # 화면별 디자인 시스템 참고 자료 (실제 코드에는 포함되지 않음)
├── main/ DESIGN.md + code.html + screen.png
├── ai예측 및 요인분석/ DESIGN.md + code.html + screen.png
├── what-if 정책 시뮬레이션/ DESIGN.md + code.html + screen.png
└── 지역 현황 상세 분석 리포트/ DESIGN.md + code.html + screen.png
app.py
st.Page로 4개 페이지를 정의하고st.navigation(pages, position="top")으로 상단 탭바를 만든다.- 페이지 전환은 진짜 Streamlit 멀티페이지 라우팅이라 클릭 시 URL이 바뀌고,
st.page_link를 쓰면 같은 탭 안에서 이동한다(raw<a>HTML은 Streamlit이 보안상target="_blank"를 강제로 붙여 새 탭이 열리므로 쓰지 않는다). page_icon은icon.png가 있으면 그것을, 없으면icon.jpg를, 둘 다 없으면 Material 아이콘을 파비콘으로 쓴다.assets/styles.css를 읽어<style>태그로 주입하고, 상단 헤더 배너 → 사이드바 렌더링 →pg.run()(선택된 페이지 실행) → 푸터 순서로 그린다.
state.py
SIGU_KEY/DONG_KEY세션 상태 키를 정의한다.init_region_state(): 세션에 값이 없거나 유효하지 않으면 기본값(제천시/첫 읍면동)으로 채운다.get_selected_region(): 현재 선택된{sigu, dong, region_code}를 반환한다. 모든views/*.py가 이 함수 하나로 선택 상태를 읽는다.
ai_module.py
src/함수들을 그대로 호출하는 어댑터. 모델 번들 + 최신 지역 데이터는st.cache_resource로 프로세스당 1회만 로드한다.- 제공 함수:
get_sigungu_list,get_eupmyeondong_list,get_region_code,get_region_result,get_local_shap,get_global_shap,run_what_if_scenarios,summarize_what_if_scenarios,run_representative_scenarios,get_all_predictions(전체 146개 지역 일괄 예측 — 지역 선택 페이지의 개요 지도용). get_global_shap()는 학습 전체 데이터가 아니라build_latest_region_data()로 만든 지역별 최신 데이터 146개 행을 기준으로 Global SHAP 요약을 계산한다. 화면에는 양/음이 상쇄되지 않도록mean_abs_shap을 표시한다.
components/sidebar.py
- 상단에 "충북 전체 보기" 페이지 이동 링크, 시·군/읍·면·동
selectbox2개, 인구감소지역 여부 배지를 렌더링한다. - 배지에는
st.caption(..., help=...)으로 Streamlit 네이티브 툴팁(물음표 아이콘)을 달아, 판정 기준(행정안전부 2021년 10월 지정 충북 6곳)을 hover로 볼 수 있다.
components/metric_card.py
render_metric_card/render_metric_row: 흰 배경 카드에 라벨+값+단위를 표시하는 기본 지표 카드.render_signed_value: 순이동률처럼 부호가 의미를 갖는 값을 초록(#00C853, 양수)/빨강(#FF5252, 음수)으로 크고 굵게 표시하고, 별도 부호를 갖는 델타 배지(예: 변화량)를 독립적으로 색칠할 수 있다.
components/charts.py
make_shap_chart: SHAP 값을 수평 막대 차트로 표시(양수=파랑, 음수=빨강, 모서리 둥글게, 격자선 최소화).make_whatif_comparison_chart: What-if 시나리오 Before/After 막대 차트._padded_range()로 Y축 범위를 데이터 min/max 기준으로 여유 있게 잡아 변화량이 잘려 보이지 않게 한다.PLOTLY_CONFIG = {"displayModeBar": False}: 모든st.plotly_chart호출에 공통으로 넘겨서 툴바를 숨긴다. (st.plotly_chart(..., width="stretch")처럼 존재하지 않는 키워드를 넘기면 "keyword arguments deprecated" 경고가 뜨므로, 반드시use_container_width=True, config=PLOTLY_CONFIG형태로 호출한다.)
components/map_view.py
create_region_map: 선택된 읍·면·동 폴리곤을 빨간색으로 강조하고 마커를 찍는 지도.fit_bounds로 읍면동 크기에 맞춰 자동 줌.create_overview_map: 충북 전체 GeoJSON을 순회하며get_all_predictions()값 기준으로 파랑(순유입)~빨강(순유출) 그라데이션을 칠하는 choropleth 지도 + 범례.
components/insight_box.py
info_box: 좌측 파란 보더의 단순 해석 박스.ai_insight_box: 제목 + 본문 + 해시태그 칩으로 구성된 강조 박스. 태그는 실제 SHAP 피처명/개선율처럼 계산된 값만 넣는다(임의로 지어낸 정책 태그 금지).
data/dong_data.py
- GeoJSON을 읽어 지역코드(
adm_cd2) 기준으로 지도 좌표/경계/색상을 계산한다. 예전에는 153개 읍면동 좌표를 손으로 입력한 테이블을 썼는데, 실제 AI 학습데이터의 읍면동 표기와 어긋나는 항목이 있어 GeoJSON에서 직접 파생하는 방식으로 바꿨다. DEPOPULATION_AREAS: 인구감소지역 6곳(제천시·보은군·옥천군·영동군·괴산군·단양군) 하드코딩 목록 — 행정안전부가 2021년 10월 전국 최초로 지정한 89개 인구감소지역 중 충청북도 해당 지역과 일치한다. AI 예측값과는 무관한 고정 값이라, 이후 행정안전부가 지정 목록을 갱신했다면 반영되어 있지 않을 수 있다.SIGUNGU_TO_SGGNM: AI 데이터의 "청주시"(통합 표기)를 GeoJSON의 4개 구(상당·서원·흥덕·청원)로 매핑.
src/ (AI 예측·SHAP·What-if 서비스 레이어)
- 모델/데이터 로딩, 지역 조회, 다음 해 순이동률 예측, SHAP 모델 예측 기여도, 모델 기반 What-if 민감도 분석, 대표 시나리오 실행을 담당한다.
사용자가 사이드바에서 시·군/읍·면·동 선택
│ (components/sidebar.py → st.session_state)
▼
state.get_selected_region() ──── region_code 확정 (src.region_service.get_region_code)
│
▼
ai_module.py (캐시된 모델 번들 + 최신 지역 데이터)
│
├─ get_region_result() → src.service → 인구/GIS/예측값 + 기준/예측연도
├─ get_local_shap() → src.shap_service → SHAP 상위 5개 모델 예측 기여도
├─ run_what_if_scenarios() → src.whatif_service → 10/20/30% 시나리오
├─ summarize_what_if_scenarios() → src.whatif_service → 방향성/단조성 요약
└─ get_all_predictions() → 146개 지역 일괄 예측 (지역 선택 개요 지도용)
│
▼
views/*.py 가 components/*.py(카드·차트·지도·해석박스)로 화면에 렌더링
# 1. 패키지 설치
pip install -r requirements.txt
# 2. 앱 실행
streamlit run app.pyStreamlit 없이 모델/데이터 로딩, 146개 지역 예측, Local SHAP, What-if, 대표 시나리오, 주요 regression 값을 한 번에 검증할 수 있습니다.
python scripts/ai_health_check.py정상 종료 시 마지막에 FINAL: PASS가 출력됩니다.
get_region_result()의 prediction은 다음 정보를 포함합니다.
{
"target": "타깃_다음해_순이동률",
"value": -2.1026148796081543,
"base_year": 2024,
"prediction_year": 2025,
}base_year: 해당 지역의 최신 데이터 기준연도prediction_year:base_year + 1
summarize_what_if_scenarios()는 이미 계산된 10/20/30% What-if 결과를 받아 모델 반응 패턴을 요약합니다.
{
"direction_consistent": True,
"monotonic": True,
"all_positive": True,
"all_negative": False,
"best_rate": 0.3,
"best_change": 0.32829549908638,
"interpretation": "개선율 증가에 따라 모델 예측값이 일관되게 증가하는 패턴입니다.",
}이 summary는 정책 효과 검증이 아니라 모델 기반 What-if 결과의 방향성과 단조성을 해석하기 위한 보조 정보입니다.
XGBoost는 실행에 libomp.dylib(OpenMP)이 필요합니다. Homebrew가 있다면:
brew install libompHomebrew가 없다면, scikit-learn을 추가 설치한 뒤 동봉된 run.sh로 실행하세요
(scikit-learn 휠에 번들된 libomp 사본 경로를 자동으로 찾아 폴백으로 지정합니다).
pip install scikit-learn
./run.sh- https://share.streamlit.io → New app
- Repository:
khuda-data/KHUBA_OB_Project, Branch:main, Main file path:app.py - Deploy 클릭 →
https://xxxx.streamlit.app링크 생성
packages.txt(libgomp1)가 Linux 배포 환경에서 XGBoost가 필요로 하는 OpenMP 라이브러리를 자동으로 설치해 준다.
배포 직후 첫 로딩(모델 로딩 + SHAP 계산 준비)은 로컬보다 조금 걸릴 수 있다.
| 하고 싶은 일 | 고칠 파일 |
|---|---|
| 카드에 새 지표 추가 | 해당 views/*.py에서 render_metric_row([...]) 항목 추가 |
| 새 탭(페이지) 추가 | views/에 파일 추가 → app.py의 pages 리스트에 st.Page(...) 한 줄 추가 |
| 사이드바 필터 항목 추가 | components/sidebar.py |
| 색상/폰트/여백 등 디자인 변경 | assets/styles.css |
| AI 서비스 레이어 함수 새로 연동 | ai_module.py에 어댑터 함수 추가 (src/ 원본은 건드리지 않음) |
| 배지/버튼에 hover 설명 추가 | 위젯의 help= 파라미터 사용 (st.caption, st.button 등이 지원 — native title 속성보다 안정적으로 표시됨) |
- 순이동률: 특정 지역에서 전입자 수와 전출자 수의 차이를 인구 대비 비율로 나타낸 지표. 양수(+)면 순유입(인구 증가 방향), 음수(-)면 순유출(인구 감소 방향)을 의미한다. 앱 안에서는 "AI 예측", "시뮬레이션", "지역 선택" 탭의 설명 문구 옆 물음표 아이콘에 이 설명이 달려 있다.
- SHAP: 모델 예측에 대한 변수별 기여도를 나타내는 지표. 원인이 아니다 — "모델 예측 기여도"로만 해석한다.
- What-if: 특정 입력 변수를 바꿨을 때 같은 학습 모델의 예측값이 어떻게 달라지는지 확인하는 모델 기반 민감도 분석. 실제 정책 시행의 인과효과를 보장하지 않는다.
- 인구감소지역: 행정안전부가 지정한 목록 기준(코드 상 고정값). 자세한 기준은 위 "전체 파일 구조" 섹션의
data/dong_data.py설명 참고.
- Python, Streamlit, Folium, Plotly, Pandas, NumPy
- XGBoost, SHAP, joblib (AI 서비스 레이어)
KHUDA OB 심화 프로젝트
ai_module.py는 AI 서비스 레이어(src/)를 그대로 호출하는 얇은 어댑터이며, Stub이 아니다.src/는 AI 예측·SHAP·What-if 서비스 레이어이며, Streamlit UI는ai_module.py를 통해 이 함수들을 호출한다.- 지도 표시 지역코드(GeoJSON
adm_cd2)와 AI 데이터의지역코드는 1:1로 매칭된다(146/153개 읍면동에 AI 데이터 존재). - 모든 What-if 해석 문구는 인과관계를 직접 단정하지 않는다 ("모델 기반 민감도 분석"으로 표현).
- SHAP은 **"모델 예측 기여도"**로 표현하며, 원인으로 해석하지 않는다.
UI_Ref/는 디자인 참고용 목업 자료일 뿐, 앱 실행에는 필요하지 않다.