Upload 15 files
Browse files- .gitattributes +3 -0
- README.md +30 -16
- app.py +27 -0
- data/공연시설DB.xlsx +3 -0
- data/내한공연DB.xlsx +3 -0
- data/최종.xlsx +3 -0
- packages.txt +1 -0
- pages/1_📊_빅데이터_분석_페이지.py +47 -0
- pages/2_🔁_기존_내한_재추천_페이지.py +54 -0
- pages/3_🆕_신규_공연장_추천_페이지.py +119 -0
- pages/3_🎨_시각화.py +18 -0
- pages/4_🧠_신규벡터추천.py +23 -0
- requirements.txt +9 -2
- utils.py +15 -0
- utils/__init__.py +1 -0
- utils/recommend_utils.py +36 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
data/공연시설DB.xlsx filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
data/내한공연DB.xlsx filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
data/최종.xlsx filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -1,19 +1,33 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
colorTo: red
|
| 6 |
-
sdk: docker
|
| 7 |
-
app_port: 8501
|
| 8 |
-
tags:
|
| 9 |
-
- streamlit
|
| 10 |
-
pinned: false
|
| 11 |
-
short_description: Streamlit template space
|
| 12 |
-
---
|
| 13 |
|
| 14 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
-
|
| 17 |
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🎭 공연장 추천 시스템 (KOPIS 기반 Big Data Recommender)
|
| 2 |
+
|
| 3 |
+
본 프로젝트는 공연예술통합전산망(KOPIS) 데이터를 활용하여
|
| 4 |
+
**공연 벡터 → 공연장 벡터** 기반의 추천 시스템을 구현한 Streamlit 웹 애플리케이션입니다.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
+
## 📦 기능 요약
|
| 7 |
+
|
| 8 |
+
| 기능 | 설명 |
|
| 9 |
+
|------|------|
|
| 10 |
+
| 📍 공연 검색 | 공연ID 또는 공연명을 입력해 상세 정보 조회 |
|
| 11 |
+
| 🔎 유사도 기반 추천 | 기존 공연과 유사한 벡터를 가진 공연장 추천 |
|
| 12 |
+
| 🎨 시각화 | 공연벡터 클러스터링 (PCA 기반 시각화) |
|
| 13 |
+
| 🧠 신규 벡터 추천 | 직접 입력한 벡터로 Top-N 공연장 추천 |
|
| 14 |
+
|
| 15 |
+
---
|
| 16 |
|
| 17 |
+
## 🗂️ 폴더 구조
|
| 18 |
|
| 19 |
+
```bash
|
| 20 |
+
kopis-recommender/
|
| 21 |
+
├── app.py # Streamlit 메인 엔트리 포인트
|
| 22 |
+
├── utils.py # 공통 데이터 로딩 및 전처리 함수
|
| 23 |
+
├── pages/ # 개별 기능 페이지
|
| 24 |
+
│ ├── 1_📍_공연검색.py
|
| 25 |
+
│ ├── 2_🔎_유사도기반추천.py
|
| 26 |
+
│ ├── 3_🎨_시각화.py
|
| 27 |
+
│ └── 4_🧠_신규벡터추천.py
|
| 28 |
+
├── data/ # 공연 관련 데이터 엑셀 파일
|
| 29 |
+
│ ├── 최종.xlsx
|
| 30 |
+
│ ├── 공연시설DB.xlsx
|
| 31 |
+
│ └── 내한공연DB.xlsx
|
| 32 |
+
├── requirements.txt # 라이브러리 의존성
|
| 33 |
+
└── README.md
|
app.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
|
| 3 |
+
# 페이지 설정
|
| 4 |
+
st.set_page_config(
|
| 5 |
+
page_title="공연장 추천 시스템 🎭",
|
| 6 |
+
page_icon="🎭",
|
| 7 |
+
layout="wide",
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
# 메인 화면
|
| 11 |
+
st.title("🎭 공연장 추천 시스템")
|
| 12 |
+
st.markdown("""
|
| 13 |
+
이 웹앱은 **공연벡터 및 공연장벡터 기반 추천 시스템**으로,
|
| 14 |
+
사용자가 선택하거나 입력한 공연의 특성을 바탕으로 **가장 어울리는 공연장**을 추천합니다.
|
| 15 |
+
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
+
### 📌 기능 안내
|
| 19 |
+
- **📍 공연 검색**: 공연ID나 공연명을 입력해 상세 정보를 조회할 수 있습니다.
|
| 20 |
+
- **🔎 유사도 기반 추천**: 공연과 유사한 벡터를 가진 공연장의 Top-N을 추천합니다.
|
| 21 |
+
- **🎨 벡터 시각화**: PCA 기반 공연 클러스터링 시각화를 제공합니다.
|
| 22 |
+
- **🧠 신규 벡터 기반 추천**: 직접 입력한 공연벡터로 가장 유사한 공연장을 예측합니다.
|
| 23 |
+
|
| 24 |
+
---
|
| 25 |
+
|
| 26 |
+
### 🗂️ 좌측 메뉴에서 기능을 선택하세요!
|
| 27 |
+
""")
|
data/공연시설DB.xlsx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c8ec7eb0e70a755559ad37f13f5ce3b3773e86e6b4ba3b1563f333caab3f5a2a
|
| 3 |
+
size 567917
|
data/내한공연DB.xlsx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:bdb2168b327dd0f61eb29ba7c222e19fcb97a12486e2b20ca4f67cfe694d3b15
|
| 3 |
+
size 412235
|
data/최종.xlsx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:63b7e9e0d00ec14f6d622d677124569047bcd426af4132c895e9f942892c8512
|
| 3 |
+
size 189635
|
packages.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
chromium-chromedriver
|
pages/1_📊_빅데이터_분석_페이지.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import numpy as np
|
| 4 |
+
from sklearn.decomposition import PCA
|
| 5 |
+
from sklearn.cluster import KMeans
|
| 6 |
+
import matplotlib.pyplot as plt
|
| 7 |
+
import seaborn as sns
|
| 8 |
+
|
| 9 |
+
st.title("📊 내한 공연 적합성 분석 및 클러스터링")
|
| 10 |
+
|
| 11 |
+
# 데이터 불러오기
|
| 12 |
+
df = pd.read_excel("data/최종.xlsx")
|
| 13 |
+
df = df.dropna(subset=["공연벡터"])
|
| 14 |
+
df["공연벡터"] = df["공연벡터"].apply(eval)
|
| 15 |
+
|
| 16 |
+
# 적합성 통계 시각화
|
| 17 |
+
st.subheader("✅ 적합성 분석 결과")
|
| 18 |
+
st.bar_chart(df["적합성"].value_counts())
|
| 19 |
+
|
| 20 |
+
st.write("📌 적합 공연 수:", (df["적합성"] == "적합").sum())
|
| 21 |
+
st.write("📌 부적합 공연 수:", (df["적합성"] == "부적합").sum())
|
| 22 |
+
|
| 23 |
+
# 클러스터링
|
| 24 |
+
st.subheader("🎨 KMeans 클러스터링 분석")
|
| 25 |
+
|
| 26 |
+
X = np.vstack(df["공연벡터"])
|
| 27 |
+
pca = PCA(n_components=2)
|
| 28 |
+
X_pca = pca.fit_transform(X)
|
| 29 |
+
|
| 30 |
+
k = st.slider("클러스터 수 선택", 2, 10, 4)
|
| 31 |
+
kmeans = KMeans(n_clusters=k, random_state=42)
|
| 32 |
+
clusters = kmeans.fit_predict(X)
|
| 33 |
+
|
| 34 |
+
df["클러스터"] = clusters
|
| 35 |
+
|
| 36 |
+
fig, ax = plt.subplots(figsize=(8,6))
|
| 37 |
+
sns.scatterplot(x=X_pca[:, 0], y=X_pca[:, 1], hue=clusters, palette="tab10", s=80, ax=ax)
|
| 38 |
+
plt.title("PCA 기반 공연 클러스터링")
|
| 39 |
+
plt.xlabel("PC1")
|
| 40 |
+
plt.ylabel("PC2")
|
| 41 |
+
plt.legend(title="클러스터", bbox_to_anchor=(1.05, 1), loc='upper left')
|
| 42 |
+
plt.grid(True)
|
| 43 |
+
st.pyplot(fig)
|
| 44 |
+
|
| 45 |
+
if st.checkbox("📋 클러스터별 공연 보기"):
|
| 46 |
+
cluster_id = st.selectbox("🔍 클러스터 선택", sorted(df["클러스터"].unique()))
|
| 47 |
+
st.dataframe(df[df["클러스터"] == cluster_id][["공연명", "공연시설명(fcltynm)", "적합성", "클러스터"]])
|
pages/2_🔁_기존_내한_재추천_페이지.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import numpy as np
|
| 4 |
+
from utils.recommend_utils import compute_capacity_similarity
|
| 5 |
+
from sklearn.metrics.pairwise import cosine_similarity
|
| 6 |
+
# 페이지 상단에 추가
|
| 7 |
+
import matplotlib.pyplot as plt
|
| 8 |
+
import matplotlib.font_manager as fm
|
| 9 |
+
|
| 10 |
+
plt.rcParams['font.family'] = 'Malgun Gothic' # Windows
|
| 11 |
+
# plt.rcParams['font.family'] = 'AppleGothic' # macOS
|
| 12 |
+
# plt.rcParams['font.family'] = 'NanumGothic' # Linux 설치 필요
|
| 13 |
+
plt.rcParams['axes.unicode_minus'] = False
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
st.title("🔁 기존 내한 공연 재추천")
|
| 17 |
+
|
| 18 |
+
# 데이터 불러오기
|
| 19 |
+
df = pd.read_excel("data/최종.xlsx")
|
| 20 |
+
df = df[df["공연장벡터"].notna()]
|
| 21 |
+
df["공연벡터"] = df["공연벡터"].apply(eval)
|
| 22 |
+
df["공연장벡터"] = df["공연장벡터"].apply(eval)
|
| 23 |
+
|
| 24 |
+
venue_df = pd.read_excel("data/공연시설DB.xlsx")
|
| 25 |
+
df = df.merge(venue_df[["공연시설ID", "객석 수", "시설특성", "레스토랑", "카페", "편의점",
|
| 26 |
+
"장애시설-경사로", "장애시설-엘리베이터", "주소"]],
|
| 27 |
+
on="공연시설ID", how="left")
|
| 28 |
+
|
| 29 |
+
# 추천 함수 (간단히 포함)
|
| 30 |
+
def recommend_alternative_venues(perf_row, df, weights=[0.5,0.3,0.2], alpha=0.7, top_k=5):
|
| 31 |
+
perf_vec = np.array(perf_row["공연벡터"]) * np.array(weights)
|
| 32 |
+
perf_capacity = perf_row["객석 수"]
|
| 33 |
+
|
| 34 |
+
candidates = df[(df["적합성"] == "적합") & (df["공연시설ID"] != perf_row["공연시설ID"])]
|
| 35 |
+
candidates["공연장벡터"] = candidates["공연장벡터"].apply(lambda x: np.array(x) * np.array(weights))
|
| 36 |
+
candidates["유사도"] = candidates["공연장벡터"].apply(lambda v: cosine_similarity([perf_vec], [v])[0][0])
|
| 37 |
+
candidates["객석수유사도"] = candidates["객석 수"].apply(lambda c: compute_capacity_similarity(perf_capacity, c))
|
| 38 |
+
candidates["종합유사도"] = alpha * candidates["유사도"] + (1 - alpha) * candidates["객석수유사도"]
|
| 39 |
+
|
| 40 |
+
return candidates.sort_values("종합유사도", ascending=False).head(top_k)
|
| 41 |
+
|
| 42 |
+
# UI
|
| 43 |
+
target_title = st.selectbox("🎫 공연명을 선택하세요", df["공연명"].unique())
|
| 44 |
+
if target_title:
|
| 45 |
+
perf_row = df[df["공연명"] == target_title].iloc[0]
|
| 46 |
+
if perf_row["적합성"] == "적합":
|
| 47 |
+
st.info("✅ 이 공연은 이미 적합한 공연장에서 진행되었습니다.")
|
| 48 |
+
else:
|
| 49 |
+
st.warning("⚠️ 부적합 공연입니다. 대체 공연장을 추천합니다.")
|
| 50 |
+
results = recommend_alternative_venues(perf_row, df)
|
| 51 |
+
st.dataframe(results[[
|
| 52 |
+
"공연시설명(fcltynm)", "공연시설ID", "유사도", "객석수유사도", "종합유사도",
|
| 53 |
+
"객석 수", "레스토랑", "카페", "편의점", "장애시설-경사로", "장애시설-엘리베이터", "주소"
|
| 54 |
+
]])
|
pages/3_🆕_신규_공연장_추천_페이지.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import numpy as np
|
| 4 |
+
import time
|
| 5 |
+
import re
|
| 6 |
+
from urllib.parse import quote
|
| 7 |
+
from selenium import webdriver
|
| 8 |
+
from selenium.webdriver.common.by import By
|
| 9 |
+
from selenium.webdriver.chrome.options import Options
|
| 10 |
+
from utils.recommend_utils import recommend_venues
|
| 11 |
+
|
| 12 |
+
# 🎭 장르 점수 맵
|
| 13 |
+
genre_score_map = {
|
| 14 |
+
"연극": 0.5,
|
| 15 |
+
"무용(서양/한국무용)": 0.6,
|
| 16 |
+
"대중무용": 0.7,
|
| 17 |
+
"서양음악(클래식)": 0.6,
|
| 18 |
+
"한국음악(국악)": 0.5,
|
| 19 |
+
"대중음악": 0.85,
|
| 20 |
+
"복합": 0.4,
|
| 21 |
+
"서커스/마술": 0.3,
|
| 22 |
+
"뮤지컬": 0.7
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
# 🔍 뉴스 검색량 수집 함수
|
| 26 |
+
def get_news_search_count(keyword):
|
| 27 |
+
options = Options()
|
| 28 |
+
options.add_argument("--headless") # 창 안 띄움
|
| 29 |
+
options.add_argument("--disable-gpu")
|
| 30 |
+
options.add_argument("--no-sandbox")
|
| 31 |
+
options.add_argument("--disable-dev-shm-usage")
|
| 32 |
+
|
| 33 |
+
driver = webdriver.Chrome(options=options)
|
| 34 |
+
|
| 35 |
+
url = f"https://search.naver.com/search.naver?where=news&query={keyword}"
|
| 36 |
+
driver.get(url)
|
| 37 |
+
|
| 38 |
+
# 예시: 검색 결과 수 추출
|
| 39 |
+
from selenium.webdriver.common.by import By
|
| 40 |
+
import re
|
| 41 |
+
try:
|
| 42 |
+
element = driver.find_element(By.CLASS_NAME, "title_desc")
|
| 43 |
+
text = element.text
|
| 44 |
+
match = re.search(r'[\d,]+건', text)
|
| 45 |
+
if match:
|
| 46 |
+
count = int(match.group().replace(',', '').replace('건', ''))
|
| 47 |
+
else:
|
| 48 |
+
count = 0
|
| 49 |
+
except:
|
| 50 |
+
count = 0
|
| 51 |
+
driver.quit()
|
| 52 |
+
return count
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
# 💸 티켓가 추출 함수
|
| 56 |
+
def extract_first_ticket_price(text):
|
| 57 |
+
match = re.search(r'(\d[\d,]*)', str(text))
|
| 58 |
+
return int(match.group(1).replace(",", "")) if match else None
|
| 59 |
+
|
| 60 |
+
# 📐 공연 벡터 생성 함수
|
| 61 |
+
def create_perf_vector(title, cast, genre, price):
|
| 62 |
+
query = f"{title} {cast}" if cast else title
|
| 63 |
+
count = get_news_count_by_scroll(query)
|
| 64 |
+
st.info(f"🔍 '{query}' 검색 결과 뉴스 기사 수: {count}")
|
| 65 |
+
|
| 66 |
+
genre_score = genre_score_map.get(genre, 0.5)
|
| 67 |
+
price_value = extract_first_ticket_price(price)
|
| 68 |
+
price_norm = price_value / 200000 if price_value else 0
|
| 69 |
+
search_norm = count / 500 if count < 500 else 1.0 # 정규화 클립
|
| 70 |
+
|
| 71 |
+
return [round(price_norm, 3), round(genre_score, 2), round(search_norm, 3)]
|
| 72 |
+
|
| 73 |
+
# 🚀 Streamlit 앱 실행 함수
|
| 74 |
+
def render():
|
| 75 |
+
st.title("🆕 신규 내한 공연 정보 입력 → 공연장 추천")
|
| 76 |
+
|
| 77 |
+
st.subheader("1️⃣ 공연 정보 입력")
|
| 78 |
+
title = st.text_input("공연 제목")
|
| 79 |
+
cast = st.text_input("출연진 (첫 명만 입력해도 됨)")
|
| 80 |
+
genre = st.selectbox("장르 선택", list(genre_score_map.keys()))
|
| 81 |
+
price = st.text_input("대표 티켓가격 (예: 99,000원 또는 숫자만 입력)")
|
| 82 |
+
|
| 83 |
+
st.subheader("2️⃣ 유사도 가중치 설정")
|
| 84 |
+
w1 = st.slider("티켓가 가중치", 0.0, 1.0, 0.5)
|
| 85 |
+
w2 = st.slider("장르 가중치", 0.0, 1.0, 0.3)
|
| 86 |
+
w3 = st.slider("검색량 가중치", 0.0, 1.0, 0.2)
|
| 87 |
+
alpha = st.slider("🎯 종합유사도에서 벡터 유사도 비중 (α)", 0.0, 1.0, 0.7)
|
| 88 |
+
|
| 89 |
+
if st.button("🚀 벡터 생성 및 추천 실행"):
|
| 90 |
+
if not title or not genre or not price:
|
| 91 |
+
st.error("공연 제목, 장르, 가격은 필수 입력입니다.")
|
| 92 |
+
return
|
| 93 |
+
|
| 94 |
+
perf_vector = create_perf_vector(title, cast, genre, price)
|
| 95 |
+
st.success(f"🎯 생성된 공연 벡터: {perf_vector}")
|
| 96 |
+
|
| 97 |
+
# 데이터 로드 및 전처리
|
| 98 |
+
df = pd.read_excel("data/최종.xlsx")
|
| 99 |
+
venue_df = pd.read_excel("data/공연시설DB.xlsx")
|
| 100 |
+
df = df[df["공연장벡터"].notna()].copy()
|
| 101 |
+
df["공연장벡터"] = df["공연장벡터"].apply(eval)
|
| 102 |
+
|
| 103 |
+
# 객석 수 등 공연장 정보 병합
|
| 104 |
+
df = df.merge(venue_df, on="공연시설ID", how="left")
|
| 105 |
+
|
| 106 |
+
# 추천 수행
|
| 107 |
+
results = recommend_venues(perf_vector, df, weights=[w1, w2, w3], alpha=alpha)
|
| 108 |
+
|
| 109 |
+
# 결과 출력
|
| 110 |
+
st.subheader("✅ 추천 공연장 리스트 (유사도 기반 상위)")
|
| 111 |
+
st.dataframe(results[[
|
| 112 |
+
"공연시설명", "공연시설ID", "유사도", "객석수유사도", "종합유사도", "객석 수",
|
| 113 |
+
"레스토랑", "카페", "편의점",
|
| 114 |
+
"장애시설-주차장", "장애시설-화장실", "장애시설-경사로", "장애시설-엘리베이터"
|
| 115 |
+
]].head(10))
|
| 116 |
+
|
| 117 |
+
# 🟢 이 모듈이 직접 실행될 때만 앱 실행
|
| 118 |
+
if __name__ == "__main__":
|
| 119 |
+
render()
|
pages/3_🎨_시각화.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
import seaborn as sns
|
| 5 |
+
from sklearn.decomposition import PCA
|
| 6 |
+
from utils import load_data
|
| 7 |
+
|
| 8 |
+
st.title("🎨 공연벡터 시각화 (PCA)")
|
| 9 |
+
|
| 10 |
+
df = load_data()
|
| 11 |
+
X = np.stack(df["공연벡터"].values)
|
| 12 |
+
pca = PCA(n_components=2)
|
| 13 |
+
X_2d = pca.fit_transform(X)
|
| 14 |
+
|
| 15 |
+
plt.figure(figsize=(8,6))
|
| 16 |
+
sns.scatterplot(x=X_2d[:,0], y=X_2d[:,1])
|
| 17 |
+
plt.title("PCA 시각화")
|
| 18 |
+
st.pyplot(plt)
|
pages/4_🧠_신규벡터추천.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
from sklearn.metrics.pairwise import cosine_similarity
|
| 4 |
+
from utils import load_data
|
| 5 |
+
|
| 6 |
+
st.title("🧠 신규 공연벡터 → 공연장 추천")
|
| 7 |
+
|
| 8 |
+
df = load_data()
|
| 9 |
+
|
| 10 |
+
vec_input = st.text_input("공연벡터 입력 (예: [0.2, 0.8, 0.0])")
|
| 11 |
+
|
| 12 |
+
if vec_input:
|
| 13 |
+
try:
|
| 14 |
+
vec = np.array([eval(vec_input)])
|
| 15 |
+
mat = np.stack(df["공연벡터"].values)
|
| 16 |
+
sims = cosine_similarity(vec, mat)[0]
|
| 17 |
+
top_k = sims.argsort()[-5:][::-1]
|
| 18 |
+
|
| 19 |
+
for i in top_k:
|
| 20 |
+
r = df.iloc[i]
|
| 21 |
+
st.markdown(f"🎵 **{r['공연명']}** → **{r['공연시설명(fcltynm)']}** (유사도: {sims[i]:.3f})")
|
| 22 |
+
except:
|
| 23 |
+
st.error("올바른 벡터 형식을 입력해주세요.")
|
requirements.txt
CHANGED
|
@@ -1,3 +1,10 @@
|
|
| 1 |
-
|
| 2 |
pandas
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
streamlit>=1.18.0
|
| 2 |
pandas
|
| 3 |
+
numpy
|
| 4 |
+
scikit-learn
|
| 5 |
+
openpyxl
|
| 6 |
+
seaborn
|
| 7 |
+
matplotlib
|
| 8 |
+
streamlit-folium
|
| 9 |
+
folium
|
| 10 |
+
selenium
|
utils.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import numpy as np
|
| 3 |
+
import streamlit as st
|
| 4 |
+
|
| 5 |
+
@st.cache_data
|
| 6 |
+
def load_data():
|
| 7 |
+
final = pd.read_excel("data/최종.xlsx")
|
| 8 |
+
final["공연벡터"] = final["공연벡터"].apply(eval)
|
| 9 |
+
final["공연장벡터"] = final["공연장벡터"].apply(eval)
|
| 10 |
+
facility = pd.read_excel("data/공연시설DB.xlsx")
|
| 11 |
+
concert = pd.read_excel("data/내한공연DB.xlsx")
|
| 12 |
+
|
| 13 |
+
df = pd.merge(final, facility, on="공연시설ID", how="left")
|
| 14 |
+
df = pd.merge(df, concert, on="공연ID(mt20Id)", how="left")
|
| 15 |
+
return df
|
utils/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
utils/recommend_utils.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
from sklearn.metrics.pairwise import cosine_similarity
|
| 3 |
+
|
| 4 |
+
# 객석 수 유사도 함수
|
| 5 |
+
def compute_capacity_similarity(cap1, cap2):
|
| 6 |
+
try:
|
| 7 |
+
if cap1 <= 0 or cap2 <= 0:
|
| 8 |
+
return 0.0
|
| 9 |
+
return min(cap1, cap2) / max(cap1, cap2)
|
| 10 |
+
except:
|
| 11 |
+
return 0.0
|
| 12 |
+
|
| 13 |
+
# 공연장 추천 함수
|
| 14 |
+
def recommend_venues(perf_vector, df, weights=[0.5, 0.3, 0.2], alpha=0.7):
|
| 15 |
+
"""
|
| 16 |
+
perf_vector: [티켓가, 장르점수, 검색량] 형태의 신규 공연 벡터
|
| 17 |
+
df: 공연장 데이터프레임 (공연장벡터 + 객석 수 포함)
|
| 18 |
+
weights: 각 성분별 가중치
|
| 19 |
+
alpha: 종합 유사도 계산 시 벡터 유사도 비중
|
| 20 |
+
"""
|
| 21 |
+
perf_vec = np.array(perf_vector) * np.array(weights)
|
| 22 |
+
|
| 23 |
+
# 공연장 벡터 유사도 계산
|
| 24 |
+
df["공연장벡터"] = df["공연장벡터"].apply(lambda x: np.array(x) * np.array(weights))
|
| 25 |
+
df["유사도"] = df["공연장벡터"].apply(lambda v: cosine_similarity([perf_vec], [v])[0][0])
|
| 26 |
+
|
| 27 |
+
# 객석 수 유사도 계산
|
| 28 |
+
target_capacity = perf_vector[0] * 200000 # 역정규화된 객석 수 기준 (티켓가 기준과 맞춰짐)
|
| 29 |
+
df["객석수유사도"] = df["객석 수"].apply(lambda c: compute_capacity_similarity(target_capacity, c))
|
| 30 |
+
|
| 31 |
+
# 종합 유사도
|
| 32 |
+
df["종합유사도"] = alpha * df["유사도"] + (1 - alpha) * df["객석수유사도"]
|
| 33 |
+
|
| 34 |
+
# 정렬 후 반환
|
| 35 |
+
result = df.sort_values("종합유사도", ascending=False).reset_index(drop=True)
|
| 36 |
+
return result
|