dltmdgus commited on
Commit
2e37891
·
verified ·
1 Parent(s): 416c0ee

Upload 15 files

Browse files
.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
- title: KOPIS
3
- emoji: 🚀
4
- colorFrom: red
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
- # Welcome to Streamlit!
 
 
 
 
 
 
 
 
 
15
 
16
- Edit `/src/streamlit_app.py` to customize this app to your heart's desire. :heart:
17
 
18
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
19
- forums](https://discuss.streamlit.io).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- altair
2
  pandas
3
- streamlit
 
 
 
 
 
 
 
 
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