ginipick commited on
Commit
2834e16
Β·
verified Β·
1 Parent(s): eec8200

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +856 -0
app.py ADDED
@@ -0,0 +1,856 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ AI 기반 μƒκΆŒ 뢄석 μ‹œμŠ€ν…œ - κ°•ν™” 버전
3
+ Dataset: https://huggingface.co/datasets/ginipick/market
4
+ """
5
+ import gradio as gr
6
+ import pandas as pd
7
+ import numpy as np
8
+ from typing import Dict, List, Tuple
9
+ import json
10
+ from datasets import load_dataset
11
+ import plotly.express as px
12
+ import plotly.graph_objects as go
13
+ from plotly.subplots import make_subplots
14
+ import folium
15
+ from folium.plugins import HeatMap, MarkerCluster
16
+ import requests
17
+ from collections import Counter
18
+ import re
19
+ import os
20
+ import time
21
+
22
+ # ============================================================================
23
+ # 데이터 λ‘œλ” 클래슀
24
+ # ============================================================================
25
+
26
+ class MarketDataLoader:
27
+ """ν—ˆκΉ…νŽ˜μ΄μŠ€ μƒκΆŒ 데이터 λ‘œλ”"""
28
+
29
+ REGIONS = {
30
+ 'μ„œμšΈ': 'μ„œμšΈ_202506', 'κ²½κΈ°': 'κ²½κΈ°_202506', 'λΆ€μ‚°': 'λΆ€μ‚°_202506',
31
+ 'λŒ€κ΅¬': 'λŒ€κ΅¬_202506', '인천': '인천_202506', 'κ΄‘μ£Ό': 'κ΄‘μ£Ό_202506',
32
+ 'λŒ€μ „': 'λŒ€μ „_202506', 'μšΈμ‚°': 'μšΈμ‚°_202506', 'μ„Έμ’…': 'μ„Έμ’…_202506',
33
+ '경남': '경남_202506', '경뢁': '경뢁_202506', '전남': '전남_202506',
34
+ '전뢁': '전뢁_202506', '좩남': '좩남_202506', '좩뢁': '좩뢁_202506',
35
+ '강원': '강원_202506', '제주': '제주_202506'
36
+ }
37
+
38
+ # μ—…μ’… λΆ„λ₯˜ λ§€ν•‘
39
+ CATEGORY_MAPPING = {
40
+ 'G2': 'μ†Œλ§€μ—…',
41
+ 'I1': 'μˆ™λ°•μ—…',
42
+ 'I2': 'μŒμ‹μ μ—…',
43
+ 'L1': '뢀동산업',
44
+ 'M1': 'μ „λ¬Έ/κ³Όν•™/기술',
45
+ 'N1': '사업지원/μž„λŒ€',
46
+ 'P1': 'κ΅μœ‘μ„œλΉ„μŠ€',
47
+ 'Q1': '보건의료',
48
+ 'R1': '예술/슀포츠/μ—¬κ°€',
49
+ 'S2': '수리/κ°œμΈμ„œλΉ„μŠ€'
50
+ }
51
+
52
+ @staticmethod
53
+ def load_region_data(region: str, sample_size: int = 30000) -> pd.DataFrame:
54
+ """지역별 데이터 λ‘œλ“œ"""
55
+ try:
56
+ file_name = f"μ†Œμƒκ³΅μΈμ‹œμž₯μ§„ν₯곡단_상가(μƒκΆŒ)정보_{MarketDataLoader.REGIONS[region]}.csv"
57
+ dataset = load_dataset("ginipick/market", data_files=file_name, split="train")
58
+ df = dataset.to_pandas()
59
+
60
+ if len(df) > sample_size:
61
+ df = df.sample(n=sample_size, random_state=42)
62
+
63
+ return df
64
+ except Exception as e:
65
+ print(f"데이터 λ‘œλ“œ μ‹€νŒ¨: {str(e)}")
66
+ return pd.DataFrame()
67
+
68
+ @staticmethod
69
+ def load_multiple_regions(regions: List[str], sample_per_region: int = 30000) -> pd.DataFrame:
70
+ """μ—¬λŸ¬ μ§€μ—­ 데이터 λ‘œλ“œ"""
71
+ dfs = []
72
+ for region in regions:
73
+ df = MarketDataLoader.load_region_data(region, sample_per_region)
74
+ if not df.empty:
75
+ dfs.append(df)
76
+
77
+ if dfs:
78
+ return pd.concat(dfs, ignore_index=True)
79
+ return pd.DataFrame()
80
+
81
+
82
+ # ============================================================================
83
+ # μƒκΆŒ 뢄석 클래슀
84
+ # ============================================================================
85
+
86
+ class MarketAnalyzer:
87
+ """μƒκΆŒ 데이터 뢄석 μ—”μ§„"""
88
+
89
+ def __init__(self, df: pd.DataFrame):
90
+ self.df = df
91
+ self.prepare_data()
92
+
93
+ def prepare_data(self):
94
+ """데이터 μ „μ²˜λ¦¬"""
95
+ if '경도' in self.df.columns:
96
+ self.df['경도'] = pd.to_numeric(self.df['경도'], errors='coerce')
97
+ if 'μœ„λ„' in self.df.columns:
98
+ self.df['μœ„λ„'] = pd.to_numeric(self.df['μœ„λ„'], errors='coerce')
99
+ self.df = self.df.dropna(subset=['경도', 'μœ„λ„'])
100
+
101
+ # μΈ΅ 정보 μ •μ œ
102
+ if '측정보' in self.df.columns:
103
+ self.df['측정보_숫자'] = self.df['측정보'].apply(self._parse_floor)
104
+
105
+ def _parse_floor(self, floor_str):
106
+ """μΈ΅ 정보λ₯Ό 숫자둜 λ³€ν™˜"""
107
+ if pd.isna(floor_str):
108
+ return None
109
+ floor_str = str(floor_str)
110
+ if 'μ§€ν•˜' in floor_str or 'B' in floor_str:
111
+ match = re.search(r'\d+', floor_str)
112
+ return -int(match.group()) if match else -1
113
+ elif '1μΈ΅' in floor_str or floor_str == '1':
114
+ return 1
115
+ else:
116
+ match = re.search(r'\d+', floor_str)
117
+ return int(match.group()) if match else None
118
+
119
+ def get_comprehensive_insights(self) -> List[Dict]:
120
+ """포괄적인 μΈμ‚¬μ΄νŠΈ 생성"""
121
+ insights = []
122
+
123
+ # 1. 업쒅별 점포 수 (μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜)
124
+ insights.append(self._create_top_categories_chart())
125
+
126
+ # 2. λŒ€λΆ„λ₯˜λ³„ 뢄포 (파이 차트)
127
+ insights.append(self._create_major_category_pie())
128
+
129
+ # 3. 측별 뢄포 상세 뢄석
130
+ insights.append(self._create_floor_analysis())
131
+
132
+ # 4. 지역별 μ—…μ’… λ‹€μ–‘μ„± μ§€μˆ˜
133
+ insights.append(self._create_diversity_index())
134
+
135
+ # 5. ν”„λžœμ°¨μ΄μ¦ˆ vs κ°œμΈμ‚¬μ—…μž 뢄석
136
+ insights.append(self._create_franchise_analysis())
137
+
138
+ # 6. 업쒅별 μΈ΅ μ„ ν˜Έλ„
139
+ insights.append(self._create_floor_preference())
140
+
141
+ # 7. μ‹œκ΅°κ΅¬λ³„ μƒκΆŒ 밀집도 TOP 20
142
+ insights.append(self._create_district_density())
143
+
144
+ # 8. μ—…μ’… 상관관계 (같은 지역에 자주 λ‚˜νƒ€λ‚˜λŠ” μ—…μ’…)
145
+ insights.append(self._create_category_correlation())
146
+
147
+ # 9. μ†ŒλΆ„λ₯˜ νŠΈλ Œλ“œ (μƒμœ„ 20개)
148
+ insights.append(self._create_subcategory_trends())
149
+
150
+ # 10. 지역별 νŠΉν™” μ—…μ’…
151
+ insights.append(self._create_regional_specialization())
152
+
153
+ return insights
154
+
155
+ def _create_top_categories_chart(self) -> Dict:
156
+ """업쒅별 점포 수 차트"""
157
+ if 'μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…' not in self.df.columns:
158
+ return None
159
+
160
+ top_categories = self.df['μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…'].value_counts().head(15)
161
+ fig = px.bar(
162
+ x=top_categories.values,
163
+ y=top_categories.index,
164
+ orientation='h',
165
+ labels={'x': '점포 수', 'y': 'μ—…μ’…'},
166
+ title='πŸ† μƒμœ„ μ—…μ’… TOP 15',
167
+ color=top_categories.values,
168
+ color_continuous_scale='blues'
169
+ )
170
+ fig.update_layout(showlegend=False, height=500)
171
+ return {'type': 'plot', 'data': fig, 'title': '업쒅별 점포 수 뢄석'}
172
+
173
+ def _create_major_category_pie(self) -> Dict:
174
+ """λŒ€λΆ„λ₯˜λ³„ 뢄포"""
175
+ if 'μƒκΆŒμ—…μ’…λŒ€λΆ„λ₯˜μ½”λ“œ' not in self.df.columns:
176
+ return None
177
+
178
+ major_counts = self.df['μƒκΆŒμ—…μ’…λŒ€λΆ„λ₯˜μ½”λ“œ'].value_counts()
179
+ labels = [MarketDataLoader.CATEGORY_MAPPING.get(code, code) for code in major_counts.index]
180
+
181
+ fig = px.pie(
182
+ values=major_counts.values,
183
+ names=labels,
184
+ title='πŸ“Š μ—…μ’… λŒ€λΆ„λ₯˜ 뢄포',
185
+ hole=0.4,
186
+ color_discrete_sequence=px.colors.qualitative.Set3
187
+ )
188
+ fig.update_traces(textposition='inside', textinfo='percent+label')
189
+ return {'type': 'plot', 'data': fig, 'title': 'λŒ€λΆ„λ₯˜λ³„ μƒκΆŒ ꡬ성'}
190
+
191
+ def _create_floor_analysis(self) -> Dict:
192
+ """측별 뢄포 상세 뢄석"""
193
+ if '측정보_숫자' not in self.df.columns:
194
+ return None
195
+
196
+ floor_data = self.df['측정보_숫자'].dropna()
197
+ floor_counts = floor_data.value_counts().sort_index()
198
+
199
+ # μ§€ν•˜, 1μΈ΅, 2μΈ΅ μ΄μƒμœΌλ‘œ κ·Έλ£Ήν™”
200
+ underground = floor_counts[floor_counts.index < 0].sum()
201
+ first_floor = floor_counts.get(1, 0)
202
+ upper_floors = floor_counts[floor_counts.index > 1].sum()
203
+
204
+ fig = go.Figure(data=[
205
+ go.Bar(
206
+ x=['μ§€ν•˜', '1μΈ΅', '2μΈ΅ 이상'],
207
+ y=[underground, first_floor, upper_floors],
208
+ text=[f'{underground:,}<br>({underground/len(floor_data)*100:.1f}%)',
209
+ f'{first_floor:,}<br>({first_floor/len(floor_data)*100:.1f}%)',
210
+ f'{upper_floors:,}<br>({upper_floors/len(floor_data)*100:.1f}%)'],
211
+ textposition='auto',
212
+ marker_color=['#e74c3c', '#3498db', '#95a5a6']
213
+ )
214
+ ])
215
+ fig.update_layout(
216
+ title='🏒 측별 점포 뢄포 (μ§€ν•˜ vs 1μΈ΅ vs 상측)',
217
+ xaxis_title='μΈ΅ ꡬ뢄',
218
+ yaxis_title='점포 수',
219
+ height=400
220
+ )
221
+ return {'type': 'plot', 'data': fig, 'title': '측별 μž…μ§€ 뢄석'}
222
+
223
+ def _create_diversity_index(self) -> Dict:
224
+ """지역별 μ—…μ’… λ‹€μ–‘μ„± μ§€μˆ˜"""
225
+ if 'μ‹œκ΅°κ΅¬λͺ…' not in self.df.columns or 'μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…' not in self.df.columns:
226
+ return None
227
+
228
+ # 각 μ‹œκ΅°κ΅¬λ³„ μ—…μ’… λ‹€μ–‘μ„± 계산 (μ—…μ’… 수 / 전체 점포 수)
229
+ diversity_data = []
230
+ for district in self.df['μ‹œκ΅°κ΅¬λͺ…'].unique()[:20]: # μƒμœ„ 20개
231
+ district_df = self.df[self.df['μ‹œκ΅°κ΅¬λͺ…'] == district]
232
+ num_categories = district_df['μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…'].nunique()
233
+ total_stores = len(district_df)
234
+ diversity_score = (num_categories / total_stores) * 100
235
+ diversity_data.append({
236
+ 'μ‹œκ΅°κ΅¬': district,
237
+ 'λ‹€μ–‘μ„± μ§€μˆ˜': diversity_score,
238
+ 'μ—…μ’… 수': num_categories,
239
+ '총 점포': total_stores
240
+ })
241
+
242
+ diversity_df = pd.DataFrame(diversity_data).sort_values('λ‹€μ–‘μ„± μ§€μˆ˜', ascending=False).head(15)
243
+
244
+ fig = px.bar(
245
+ diversity_df,
246
+ x='λ‹€μ–‘μ„± μ§€μˆ˜',
247
+ y='μ‹œκ΅°κ΅¬',
248
+ orientation='h',
249
+ title='🌈 지역별 μ—…μ’… λ‹€μ–‘μ„± μ§€μˆ˜ (μƒμœ„ 15개)',
250
+ labels={'λ‹€μ–‘μ„± μ§€μˆ˜': 'λ‹€μ–‘μ„± μ§€μˆ˜ (%)', 'μ‹œκ΅°κ΅¬': 'μ§€μ—­'},
251
+ color='λ‹€μ–‘μ„± μ§€μˆ˜',
252
+ color_continuous_scale='viridis',
253
+ hover_data=['μ—…μ’… 수', '총 점포']
254
+ )
255
+ fig.update_layout(height=500)
256
+ return {'type': 'plot', 'data': fig, 'title': 'μ—…μ’… λ‹€μ–‘μ„± 뢄석'}
257
+
258
+ def _create_franchise_analysis(self) -> Dict:
259
+ """ν”„λžœμ°¨μ΄μ¦ˆ vs κ°œμΈμ‚¬μ—…μž 뢄석"""
260
+ if 'μƒν˜Έλͺ…' not in self.df.columns:
261
+ return None
262
+
263
+ # μ£Όμš” ν”„λžœμ°¨μ΄μ¦ˆ ν‚€μ›Œλ“œ
264
+ franchise_keywords = [
265
+ 'CU', 'GS25', 'μ„ΈλΈμΌλ ˆλΈ', '이마트24', 'λ―Έλ‹ˆμŠ€ν†±',
266
+ 'μŠ€νƒ€λ²…μŠ€', 'νˆ¬μΈν”Œλ ˆμ΄μŠ€', '이디야', 'νƒμ•€νƒμŠ€', 'μ»€ν”ΌλΉˆ',
267
+ 'λ§₯λ„λ‚ λ“œ', '버거킹', '둯데리아', 'KFC', 'λ§˜μŠ€ν„°μΉ˜',
268
+ 'BBQ', 'ꡐ촌', 'κ΅½λ„€', 'bhc', 'λ„€λ„€μΉ˜ν‚¨'
269
+ ]
270
+
271
+ franchise_counts = {}
272
+ for keyword in franchise_keywords:
273
+ count = self.df['μƒν˜Έλͺ…'].str.contains(keyword, case=False, na=False).sum()
274
+ if count > 0:
275
+ franchise_counts[keyword] = count
276
+
277
+ total_franchise = sum(franchise_counts.values())
278
+ total_stores = len(self.df)
279
+ individual_stores = total_stores - total_franchise
280
+
281
+ # 파이 차트
282
+ fig = make_subplots(
283
+ rows=1, cols=2,
284
+ specs=[[{'type': 'pie'}, {'type': 'bar'}]],
285
+ subplot_titles=('전체 λΉ„μœ¨', 'ν”„λžœμ°¨μ΄μ¦ˆλ³„ 점포 수')
286
+ )
287
+
288
+ # 전체 λΉ„μœ¨
289
+ fig.add_trace(
290
+ go.Pie(
291
+ labels=['κ°œμΈμ‚¬μ—…μž', 'ν”„λžœμ°¨μ΄μ¦ˆ'],
292
+ values=[individual_stores, total_franchise],
293
+ hole=0.3,
294
+ marker_colors=['#3498db', '#e74c3c']
295
+ ),
296
+ row=1, col=1
297
+ )
298
+
299
+ # ν”„λžœμ°¨μ΄μ¦ˆλ³„
300
+ top_franchises = dict(sorted(franchise_counts.items(), key=lambda x: x[1], reverse=True)[:10])
301
+ fig.add_trace(
302
+ go.Bar(
303
+ x=list(top_franchises.keys()),
304
+ y=list(top_franchises.values()),
305
+ marker_color='#e74c3c'
306
+ ),
307
+ row=1, col=2
308
+ )
309
+
310
+ fig.update_layout(
311
+ title_text='πŸͺ ν”„λžœμ°¨μ΄μ¦ˆ vs κ°œμΈμ‚¬μ—…μž 뢄석',
312
+ showlegend=True,
313
+ height=400
314
+ )
315
+ return {'type': 'plot', 'data': fig, 'title': 'ν”„λžœμ°¨μ΄μ¦ˆ 점유율'}
316
+
317
+ def _create_floor_preference(self) -> Dict:
318
+ """업쒅별 μΈ΅ μ„ ν˜Έλ„"""
319
+ if 'μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…' not in self.df.columns or '측정보_숫자' not in self.df.columns:
320
+ return None
321
+
322
+ # μƒμœ„ 10개 μ—…μ’…μ˜ 측별 뢄포
323
+ top_categories = self.df['μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…'].value_counts().head(10).index
324
+
325
+ floor_pref_data = []
326
+ for category in top_categories:
327
+ cat_df = self.df[self.df['μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…'] == category]
328
+ underground = (cat_df['측정보_숫자'] < 0).sum()
329
+ first_floor = (cat_df['측정보_숫자'] == 1).sum()
330
+ upper_floors = (cat_df['측정보_숫자'] > 1).sum()
331
+
332
+ total = underground + first_floor + upper_floors
333
+ if total > 0:
334
+ floor_pref_data.append({
335
+ 'μ—…μ’…': category,
336
+ 'μ§€ν•˜': (underground / total) * 100,
337
+ '1μΈ΅': (first_floor / total) * 100,
338
+ '2μΈ΅ 이상': (upper_floors / total) * 100
339
+ })
340
+
341
+ pref_df = pd.DataFrame(floor_pref_data)
342
+
343
+ fig = go.Figure(data=[
344
+ go.Bar(name='μ§€ν•˜', x=pref_df['μ—…μ’…'], y=pref_df['μ§€ν•˜'], marker_color='#e74c3c'),
345
+ go.Bar(name='1μΈ΅', x=pref_df['μ—…μ’…'], y=pref_df['1μΈ΅'], marker_color='#3498db'),
346
+ go.Bar(name='2μΈ΅ 이상', x=pref_df['μ—…μ’…'], y=pref_df['2μΈ΅ 이상'], marker_color='#95a5a6')
347
+ ])
348
+
349
+ fig.update_layout(
350
+ title='🎯 업쒅별 μΈ΅ μ„ ν˜Έλ„ (μƒμœ„ 10개)',
351
+ xaxis_title='μ—…μ’…',
352
+ yaxis_title='λΉ„μœ¨ (%)',
353
+ barmode='stack',
354
+ height=500,
355
+ xaxis_tickangle=-45
356
+ )
357
+ return {'type': 'plot', 'data': fig, 'title': '측별 μž…μ§€ μ„ ν˜Έλ„'}
358
+
359
+ def _create_district_density(self) -> Dict:
360
+ """μ‹œκ΅°κ΅¬λ³„ μƒκΆŒ 밀집도"""
361
+ if 'μ‹œκ΅°κ΅¬λͺ…' not in self.df.columns:
362
+ return None
363
+
364
+ district_counts = self.df['μ‹œκ΅°κ΅¬λͺ…'].value_counts().head(20)
365
+
366
+ fig = px.bar(
367
+ x=district_counts.values,
368
+ y=district_counts.index,
369
+ orientation='h',
370
+ title='πŸŒ† μ‹œκ΅°κ΅¬λ³„ μƒκΆŒ 밀집도 TOP 20',
371
+ labels={'x': '점포 수', 'y': 'μ‹œκ΅°κ΅¬'},
372
+ color=district_counts.values,
373
+ color_continuous_scale='reds'
374
+ )
375
+ fig.update_layout(showlegend=False, height=600)
376
+ return {'type': 'plot', 'data': fig, 'title': '지역별 밀집도'}
377
+
378
+ def _create_category_correlation(self) -> Dict:
379
+ """μ—…μ’… 상관관계 (같은 동에 자주 λ‚˜νƒ€λ‚˜λŠ” μ—…μ’…)"""
380
+ if '행정동λͺ…' not in self.df.columns or 'μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…' not in self.df.columns:
381
+ return None
382
+
383
+ # μƒμœ„ 10개 μ—…μ’…λ§Œ 뢄석
384
+ top_categories = self.df['μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…'].value_counts().head(10).index.tolist()
385
+
386
+ # 각 λ™λ³„λ‘œ μ—…μ’… 카운트
387
+ correlation_matrix = []
388
+ for cat1 in top_categories:
389
+ row = []
390
+ for cat2 in top_categories:
391
+ # 두 업쒅이 같은 동에 μžˆλŠ” 경우의 수
392
+ cat1_dongs = set(self.df[self.df['μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…'] == cat1]['행정동λͺ…'].unique())
393
+ cat2_dongs = set(self.df[self.df['μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…'] == cat2]['행정동λͺ…'].unique())
394
+ intersection = len(cat1_dongs & cat2_dongs)
395
+ union = len(cat1_dongs | cat2_dongs)
396
+ similarity = (intersection / union * 100) if union > 0 else 0
397
+ row.append(similarity)
398
+ correlation_matrix.append(row)
399
+
400
+ fig = go.Figure(data=go.Heatmap(
401
+ z=correlation_matrix,
402
+ x=top_categories,
403
+ y=top_categories,
404
+ colorscale='Blues',
405
+ text=np.round(correlation_matrix, 1),
406
+ texttemplate='%{text}',
407
+ textfont={"size": 10}
408
+ ))
409
+
410
+ fig.update_layout(
411
+ title='πŸ”— μ—…μ’… 상관관계 맀트릭슀 (같은 μ§€μ—­ λ™μ‹œ μΆœν˜„μœ¨)',
412
+ xaxis_title='μ—…μ’…',
413
+ yaxis_title='μ—…μ’…',
414
+ height=600,
415
+ xaxis_tickangle=-45
416
+ )
417
+ return {'type': 'plot', 'data': fig, 'title': 'μ—…μ’… 곡쑴 뢄석'}
418
+
419
+ def _create_subcategory_trends(self) -> Dict:
420
+ """μ†ŒλΆ„λ₯˜ νŠΈλ Œλ“œ"""
421
+ if 'μƒκΆŒμ—…μ’…μ†ŒλΆ„λ₯˜λͺ…' not in self.df.columns:
422
+ return None
423
+
424
+ subcat_counts = self.df['μƒκΆŒμ—…μ’…μ†ŒλΆ„λ₯˜λͺ…'].value_counts().head(20)
425
+
426
+ fig = px.treemap(
427
+ names=subcat_counts.index,
428
+ parents=[''] * len(subcat_counts),
429
+ values=subcat_counts.values,
430
+ title='πŸ” μ†ŒλΆ„λ₯˜ μ—…μ’… νŠΈλ Œλ“œ TOP 20',
431
+ color=subcat_counts.values,
432
+ color_continuous_scale='greens'
433
+ )
434
+ fig.update_layout(height=600)
435
+ return {'type': 'plot', 'data': fig, 'title': 'μ„ΈλΆ€ μ—…μ’… 뢄석'}
436
+
437
+ def _create_regional_specialization(self) -> Dict:
438
+ """지역별 νŠΉν™” μ—…μ’…"""
439
+ if 'μ‹œλ„λͺ…' not in self.df.columns or 'μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…' not in self.df.columns:
440
+ return None
441
+
442
+ # 각 μ‹œλ„λ³„ μƒμœ„ 3개 μ—…μ’…
443
+ specialization_data = []
444
+ for region in self.df['μ‹œλ„λͺ…'].unique():
445
+ region_df = self.df[self.df['μ‹œλ„λͺ…'] == region]
446
+ top_categories = region_df['μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…'].value_counts().head(3)
447
+ for category, count in top_categories.items():
448
+ specialization_data.append({
449
+ 'μ§€μ—­': region,
450
+ 'νŠΉν™”μ—…μ’…': category,
451
+ '점포수': count
452
+ })
453
+
454
+ spec_df = pd.DataFrame(specialization_data)
455
+
456
+ fig = px.sunburst(
457
+ spec_df,
458
+ path=['μ§€μ—­', 'νŠΉν™”μ—…μ’…'],
459
+ values='점포수',
460
+ title='🎯 지역별 νŠΉν™” μ—…μ’… (각 μ§€μ—­ TOP 3)',
461
+ color='점포수',
462
+ color_continuous_scale='oranges'
463
+ )
464
+ fig.update_layout(height=700)
465
+ return {'type': 'plot', 'data': fig, 'title': 'μ§€μ—­ νŠΉν™” 뢄석'}
466
+
467
+ def create_density_map(self, sample_size: int = 1000) -> str:
468
+ """점포 밀집도 지도 생성"""
469
+ df_sample = self.df.sample(n=min(sample_size, len(self.df)), random_state=42)
470
+
471
+ center_lat = df_sample['μœ„λ„'].mean()
472
+ center_lon = df_sample['경도'].mean()
473
+
474
+ m = folium.Map(location=[center_lat, center_lon], zoom_start=11, tiles='OpenStreetMap')
475
+
476
+ # 히트맡
477
+ heat_data = [[row['μœ„λ„'], row['경도']] for _, row in df_sample.iterrows()]
478
+ HeatMap(heat_data, radius=15, blur=25, max_zoom=13).add_to(m)
479
+
480
+ return m._repr_html_()
481
+
482
+ def analyze_for_llm(self) -> Dict:
483
+ """LLM μ»¨ν…μŠ€νŠΈμš© 뢄석 데이터"""
484
+ context = {
485
+ '총_점포_수': len(self.df),
486
+ 'μ§€μ—­_수': self.df['μ‹œλ„λͺ…'].nunique() if 'μ‹œλ„λͺ…' in self.df.columns else 0,
487
+ 'μ—…μ’…_수': self.df['μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…'].nunique() if 'μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…' in self.df.columns else 0,
488
+ }
489
+
490
+ if 'μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…' in self.df.columns:
491
+ context['μƒμœ„_μ—…μ’…_5'] = self.df['μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…'].value_counts().head(5).to_dict()
492
+
493
+ if '측정보_숫자' in self.df.columns:
494
+ first_floor_ratio = (self.df['측정보_숫자'] == 1).sum() / len(self.df) * 100
495
+ context['1μΈ΅_λΉ„μœ¨'] = f"{first_floor_ratio:.1f}%"
496
+
497
+ return context
498
+
499
+
500
+ # ============================================================================
501
+ # LLM 쿼리 ν”„λ‘œμ„Έμ„œ
502
+ # ============================================================================
503
+
504
+ class LLMQueryProcessor:
505
+ """Fireworks AI 기반 μžμ—°μ–΄ 처리"""
506
+
507
+ def __init__(self, api_key: str = None):
508
+ # ν™˜κ²½λ³€μˆ˜μ—μ„œ API ν‚€ κ°€μ Έμ˜€κΈ°
509
+ self.api_key = api_key or os.getenv("FIREWORKS_API_KEY")
510
+ self.base_url = "https://api.fireworks.ai/inference/v1/chat/completions"
511
+
512
+ if not self.api_key:
513
+ raise ValueError("❌ FIREWORKS_API_KEY ν™˜κ²½λ³€μˆ˜λ₯Ό μ„€μ •ν•˜κ±°λ‚˜ API ν‚€λ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”!")
514
+
515
+ def process_query(self, query: str, data_context: Dict, chat_history: List = None, max_retries: int = 3) -> str:
516
+ """μžμ—°μ–΄ 쿼리 처리 (μž¬μ‹œλ„ 둜직 포함)"""
517
+ system_prompt = f"""당신은 ν•œκ΅­ μƒκΆŒ 데이터 뢄석 μ „λ¬Έκ°€μž…λ‹ˆλ‹€.
518
+
519
+ πŸ“Š **ν˜„μž¬ 뢄석 데이터**
520
+ {json.dumps(data_context, ensure_ascii=False, indent=2)}
521
+
522
+ ꡬ체적인 μˆ«μžμ™€ λΉ„μœ¨λ‘œ μ •λŸ‰μ  뢄석을 μ œκ³΅ν•˜μ„Έμš”.
523
+ μ°½μ—…, 투자, 경쟁 뢄석 κ΄€μ μ—μ„œ μ‹€μš©μ  μΈμ‚¬μ΄νŠΈλ₯Ό μ œκ³΅ν•˜μ„Έμš”."""
524
+
525
+ messages = [{"role": "system", "content": system_prompt}]
526
+ if chat_history:
527
+ messages.extend(chat_history[-6:])
528
+ messages.append({"role": "user", "content": query})
529
+
530
+ payload = {
531
+ "model": "accounts/fireworks/models/qwen3-235b-a22b-instruct-2507",
532
+ "max_tokens": 2000,
533
+ "temperature": 0.7,
534
+ "messages": messages
535
+ }
536
+
537
+ headers = {
538
+ "Authorization": f"Bearer {self.api_key}",
539
+ "Content-Type": "application/json"
540
+ }
541
+
542
+ # μž¬μ‹œλ„ 둜직
543
+ for attempt in range(max_retries):
544
+ try:
545
+ # νƒ€μž„μ•„μ›ƒμ„ 60초둜 증가
546
+ response = requests.post(
547
+ self.base_url,
548
+ headers=headers,
549
+ json=payload,
550
+ timeout=60
551
+ )
552
+
553
+ if response.status_code == 200:
554
+ return response.json()['choices'][0]['message']['content']
555
+ elif response.status_code == 429:
556
+ # Rate limit - μž¬μ‹œλ„
557
+ wait_time = (attempt + 1) * 2
558
+ time.sleep(wait_time)
559
+ continue
560
+ else:
561
+ return f"⚠️ API 였λ₯˜: {response.status_code} - {response.text[:200]}"
562
+
563
+ except requests.exceptions.Timeout:
564
+ if attempt < max_retries - 1:
565
+ time.sleep(2)
566
+ continue
567
+ else:
568
+ return "⚠️ API 응닡 μ‹œκ°„ 초과. μž μ‹œ ν›„ λ‹€μ‹œ μ‹œλ„ν•΄μ£Όμ„Έμš”."
569
+
570
+ except requests.exceptions.ConnectionError:
571
+ if attempt < max_retries - 1:
572
+ time.sleep(2)
573
+ continue
574
+ else:
575
+ return "⚠️ λ„€νŠΈμ›Œν¬ μ—°κ²° 였λ₯˜. 인터넷 연결을 ν™•μΈν•΄μ£Όμ„Έμš”."
576
+
577
+ except Exception as e:
578
+ if attempt < max_retries - 1:
579
+ time.sleep(1)
580
+ continue
581
+ else:
582
+ return f"❌ 였λ₯˜: {str(e)}"
583
+
584
+ return "⚠️ μ΅œλŒ€ μž¬μ‹œλ„ 횟수 초과. μž μ‹œ ν›„ λ‹€μ‹œ μ‹œλ„ν•΄μ£Όμ„Έμš”."
585
+
586
+
587
+ # ============================================================================
588
+ # μ „μ—­ μƒνƒœ
589
+ # ============================================================================
590
+
591
+ class AppState:
592
+ def __init__(self):
593
+ self.analyzer = None
594
+ self.llm_processor = None
595
+ self.chat_history = []
596
+
597
+ app_state = AppState()
598
+
599
+
600
+ # ============================================================================
601
+ # Gradio μΈν„°νŽ˜μ΄μŠ€ ν•¨μˆ˜
602
+ # ============================================================================
603
+
604
+ def load_data(regions):
605
+ """데이터 λ‘œλ“œ"""
606
+ if not regions:
607
+ return "❌ μ΅œμ†Œ 1개 지역을 μ„ νƒν•΄μ£Όμ„Έμš”!", None, None, None
608
+
609
+ try:
610
+ df = MarketDataLoader.load_multiple_regions(regions, sample_per_region=30000)
611
+ if df.empty:
612
+ return "❌ 데이터 λ‘œλ“œ μ‹€νŒ¨!", None, None, None
613
+
614
+ app_state.analyzer = MarketAnalyzer(df)
615
+
616
+ # κΈ°λ³Έ 톡계
617
+ stats = f"""
618
+ βœ… **데이터 λ‘œλ“œ μ™„λ£Œ!**
619
+
620
+ πŸ“Š **톡계**
621
+ - 총 점포: {len(df):,}개
622
+ - 뢄석 μ§€μ—­: {', '.join(regions)}
623
+ - μ—…μ’… 수: {df['μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…'].nunique()}개
624
+ - λŒ€λΆ„λ₯˜: {df['μƒκΆŒμ—…μ’…λŒ€λΆ„λ₯˜λͺ…'].nunique()}개
625
+ """
626
+
627
+ return stats, gr.update(visible=True), gr.update(visible=True), gr.update(visible=True)
628
+ except Exception as e:
629
+ return f"❌ 였λ₯˜: {str(e)}", None, None, None
630
+
631
+
632
+ def generate_insights():
633
+ """μΈμ‚¬μ΄νŠΈ 생성"""
634
+ if app_state.analyzer is None:
635
+ return [None] * 11
636
+
637
+ insights = app_state.analyzer.get_comprehensive_insights()
638
+ map_html = app_state.analyzer.create_density_map(sample_size=2000)
639
+
640
+ result = [map_html]
641
+ for insight in insights:
642
+ if insight and insight['type'] == 'plot':
643
+ result.append(insight['data'])
644
+ else:
645
+ result.append(None)
646
+
647
+ # λΆ€μ‘±ν•œ μ°¨νŠΈλŠ” None으둜 μ±„μš°κΈ°
648
+ while len(result) < 11:
649
+ result.append(None)
650
+
651
+ return result[:11]
652
+
653
+
654
+ def chat_respond(message, history):
655
+ """챗봇 응닡"""
656
+ if app_state.analyzer is None:
657
+ return history + [[message, "❌ λ¨Όμ € 데이터λ₯Ό λ‘œλ“œν•΄μ£Όμ„Έμš”!"]]
658
+
659
+ data_context = app_state.analyzer.analyze_for_llm()
660
+
661
+ # LLM ν”„λ‘œμ„Έμ„œ μ΄ˆκΈ°ν™” (ν™˜κ²½λ³€μˆ˜μ—μ„œ API ν‚€ μžλ™ λ‘œλ“œ)
662
+ try:
663
+ if app_state.llm_processor is None:
664
+ app_state.llm_processor = LLMQueryProcessor()
665
+
666
+ chat_hist = []
667
+ for user_msg, bot_msg in history:
668
+ chat_hist.append({"role": "user", "content": user_msg})
669
+ chat_hist.append({"role": "assistant", "content": bot_msg})
670
+
671
+ response = app_state.llm_processor.process_query(message, data_context, chat_hist)
672
+
673
+ except ValueError as e:
674
+ # API ν‚€κ°€ μ—†λŠ” 경우 κΈ°λ³Έ 톡계 제곡
675
+ response = f"""πŸ“Š **κΈ°λ³Έ 데이터 뢄석 κ²°κ³Ό**
676
+
677
+ **전체 ν˜„ν™©**
678
+ - 총 점포 수: {data_context['총_점포_수']:,}개
679
+ - μ—…μ’… μ’…λ₯˜: {data_context['μ—…μ’…_수']}개
680
+ - 1μΈ΅ λΉ„μœ¨: {data_context.get('1μΈ΅_λΉ„μœ¨', 'N/A')}
681
+
682
+ ⚠️ **AI 뢄석 μ‚¬μš© 방법**
683
+ ν™˜κ²½λ³€μˆ˜λ₯Ό μ„€μ •ν•˜μ„Έμš”:
684
+ ```bash
685
+ export FIREWORKS_API_KEY="your_api_key_here"
686
+ ```
687
+
688
+ λ˜λŠ” Hugging Face Spaceμ—μ„œλŠ” Settings > Variables μ—μ„œ μ„€μ •ν•˜μ„Έμš”."""
689
+
690
+ history.append([message, response])
691
+ return history
692
+
693
+
694
+ # ============================================================================
695
+ # Gradio UI
696
+ # ============================================================================
697
+
698
+ with gr.Blocks(title="AI μƒκΆŒ 뢄석 μ‹œμŠ€ν…œ Pro", theme=gr.themes.Soft()) as demo:
699
+ gr.Markdown("""
700
+ # πŸͺ AI μƒκΆŒ 뢄석 μ‹œμŠ€ν…œ Pro
701
+ *μ „κ΅­ 상가(μƒκΆŒ) 데이터 기반 μ‹€μ‹œκ°„ 뢄석 | Powered by Fireworks AI*
702
+
703
+ **✨ 10κ°€μ§€ 심측 뢄석 제곡**: μ—…μ’… νŠΈλ Œλ“œ, 경쟁 강도, μž…μ§€ 뢄석, ν”„λžœμ°¨μ΄μ¦ˆ λΉ„μœ¨, μ§€μ—­ νŠΉν™”, 측별 μ„ ν˜Έλ„ λ“±
704
+ """)
705
+
706
+ with gr.Row():
707
+ with gr.Column(scale=1):
708
+ gr.Markdown("### βš™οΈ μ„€μ •")
709
+
710
+ # ν™˜κ²½λ³€μˆ˜ μƒνƒœ ν‘œμ‹œ
711
+ api_status = "βœ… API ν‚€ 섀정됨" if os.getenv("FIREWORKS_API_KEY") else "⚠️ API ν‚€ λ―Έμ„€μ • (κΈ°λ³Έ ν†΅κ³„λ§Œ 제곡)"
712
+ gr.Markdown(f"**πŸ”‘ API μƒνƒœ**: {api_status}")
713
+
714
+ region_select = gr.CheckboxGroup(
715
+ choices=list(MarketDataLoader.REGIONS.keys()),
716
+ value=['μ„œμšΈ'],
717
+ label="πŸ“ 뢄석 μ§€μ—­ 선택 (μ΅œλŒ€ 5개 ꢌμž₯)"
718
+ )
719
+
720
+ load_btn = gr.Button("πŸ“Š 데이터 λ‘œλ“œ", variant="primary", size="lg")
721
+
722
+ status_box = gr.Markdown("πŸ‘ˆ 지역을 μ„ νƒν•˜κ³  데이터λ₯Ό λ‘œλ“œν•˜μ„Έμš”!")
723
+
724
+ with gr.Column(scale=3):
725
+ with gr.Tabs() as tabs:
726
+ with gr.Tab("πŸ“Š μΈμ‚¬μ΄νŠΈ λŒ€μ‹œλ³΄λ“œ", id=0) as tab1:
727
+ insights_content = gr.Column(visible=False)
728
+
729
+ with insights_content:
730
+ gr.Markdown("### πŸ—ΊοΈ 점포 밀집도 히트맡")
731
+ map_output = gr.HTML()
732
+
733
+ gr.Markdown("---")
734
+ gr.Markdown("### πŸ“ˆ 10κ°€μ§€ 심측 μƒκΆŒ μΈμ‚¬μ΄νŠΈ")
735
+
736
+ with gr.Row():
737
+ chart1 = gr.Plot(label="업쒅별 점포 수")
738
+ chart2 = gr.Plot(label="λŒ€λΆ„λ₯˜ 뢄포")
739
+
740
+ with gr.Row():
741
+ chart3 = gr.Plot(label="측별 뢄포")
742
+ chart4 = gr.Plot(label="μ—…μ’… λ‹€μ–‘μ„±")
743
+
744
+ with gr.Row():
745
+ chart5 = gr.Plot(label="ν”„λžœμ°¨μ΄μ¦ˆ 뢄석")
746
+ chart6 = gr.Plot(label="μΈ΅ μ„ ν˜Έλ„")
747
+
748
+ with gr.Row():
749
+ chart7 = gr.Plot(label="μ§€μ—­ 밀집도")
750
+ chart8 = gr.Plot(label="μ—…μ’… 상관관계")
751
+
752
+ with gr.Row():
753
+ chart9 = gr.Plot(label="μ†ŒλΆ„λ₯˜ νŠΈλ Œλ“œ")
754
+ chart10 = gr.Plot(label="μ§€μ—­ νŠΉν™”")
755
+
756
+ with gr.Tab("πŸ€– AI 뢄석 챗봇", id=1) as tab2:
757
+ chat_content = gr.Column(visible=False)
758
+
759
+ with chat_content:
760
+ gr.Markdown("""
761
+ ### πŸ’‘ μƒ˜ν”Œ 질문
762
+ κ°•λ‚¨μ—μ„œ 카페 μ°½μ—…? | μΉ˜ν‚¨μ§‘ 포화 μ§€μ—­? | 1측이 μœ λ¦¬ν•œ μ—…μ’…? | ν”„λžœμ°¨μ΄μ¦ˆ 점유율?
763
+ """)
764
+
765
+ chatbot = gr.Chatbot(height=400, label="AI μƒκΆŒ 뢄석 μ–΄μ‹œμŠ€ν„΄νŠΈ")
766
+
767
+ with gr.Row():
768
+ msg_input = gr.Textbox(
769
+ placeholder="무엇이든 λ¬Όμ–΄λ³΄μ„Έμš”! (예: κ°•λ‚¨μ—μ„œ 카페 μ°½μ—…ν•˜λ €λ©΄?)",
770
+ show_label=False,
771
+ scale=4
772
+ )
773
+ submit_btn = gr.Button("전솑", variant="primary", scale=1)
774
+
775
+ # μƒ˜ν”Œ λ²„νŠΌλ“€
776
+ with gr.Row():
777
+ sample_btn1 = gr.Button("κ°•λ‚¨μ—μ„œ 카페 μ°½μ—…?", size="sm")
778
+ sample_btn2 = gr.Button("μΉ˜ν‚¨μ§‘ 포화 μ§€μ—­?", size="sm")
779
+ sample_btn3 = gr.Button("1측이 μœ λ¦¬ν•œ μ—…μ’…?", size="sm")
780
+ sample_btn4 = gr.Button("ν”„λžœμ°¨μ΄μ¦ˆ 점유율?", size="sm")
781
+
782
+ # 이벀트 ν•Έλ“€λŸ¬
783
+ load_btn.click(
784
+ fn=load_data,
785
+ inputs=[region_select],
786
+ outputs=[status_box, insights_content, chat_content, tab1]
787
+ ).then(
788
+ fn=generate_insights,
789
+ outputs=[map_output, chart1, chart2, chart3, chart4, chart5, chart6, chart7, chart8, chart9, chart10]
790
+ )
791
+
792
+ # 챗봇 이벀트 (API ν‚€ νŒŒλΌλ―Έν„° 제거)
793
+ submit_btn.click(
794
+ fn=chat_respond,
795
+ inputs=[msg_input, chatbot],
796
+ outputs=[chatbot]
797
+ ).then(
798
+ fn=lambda: "",
799
+ outputs=[msg_input]
800
+ )
801
+
802
+ msg_input.submit(
803
+ fn=chat_respond,
804
+ inputs=[msg_input, chatbot],
805
+ outputs=[chatbot]
806
+ ).then(
807
+ fn=lambda: "",
808
+ outputs=[msg_input]
809
+ )
810
+
811
+ # μƒ˜ν”Œ λ²„νŠΌ 이벀트
812
+ for btn, text in [
813
+ (sample_btn1, "κ°•λ‚¨μ—μ„œ 카페 μ°½μ—…?"),
814
+ (sample_btn2, "μΉ˜ν‚¨μ§‘ 포화 μ§€μ—­?"),
815
+ (sample_btn3, "1측이 μœ λ¦¬ν•œ μ—…μ’…?"),
816
+ (sample_btn4, "ν”„λžœμ°¨μ΄μ¦ˆ 점유율?")
817
+ ]:
818
+ btn.click(
819
+ fn=lambda t=text, h=chatbot: chat_respond(t, h.value or []),
820
+ outputs=[chatbot]
821
+ )
822
+
823
+ gr.Markdown("""
824
+ ---
825
+ ### πŸ“– μ‚¬μš© κ°€μ΄λ“œ
826
+ 1. μ§€μ—­ 선택 β†’ 2. 데이터 λ‘œλ“œ β†’ 3. 10κ°€μ§€ μΈμ‚¬μ΄νŠΈ 확인 λ˜λŠ” AIμ—κ²Œ 질문
827
+
828
+ ### πŸ”‘ AI 챗봇 ν™œμ„±ν™” 방법
829
+ ν™˜κ²½λ³€μˆ˜ μ„€μ •:
830
+ ```bash
831
+ export FIREWORKS_API_KEY="your_api_key_here"
832
+ ```
833
+
834
+ Hugging Face Spaceμ—μ„œλŠ”:
835
+ 1. Settings 메뉴 클릭
836
+ 2. Variables νƒ­ 선택
837
+ 3. New variable μΆ”κ°€: `FIREWORKS_API_KEY`
838
+
839
+ ### πŸ“Š μ œκ³΅λ˜λŠ” 10κ°€μ§€ 뢄석
840
+ 1. **업쒅별 점포 수**: κ°€μž₯ λ§Žμ€ μ—…μ’… TOP 15
841
+ 2. **λŒ€λΆ„λ₯˜ 뢄포**: μ†Œλ§€/μŒμ‹/μ„œλΉ„μŠ€ λ“± λŒ€λΆ„λ₯˜ λΉ„μœ¨
842
+ 3. **측별 뢄포**: μ§€ν•˜/1μΈ΅/상측 μž…μ§€ 뢄석
843
+ 4. **μ—…μ’… λ‹€μ–‘μ„±**: 지역별 μ—…μ’… λ‹€μ–‘μ„± μ§€μˆ˜
844
+ 5. **ν”„λžœμ°¨μ΄μ¦ˆ 뢄석**: 개인 vs ν”„λžœμ°¨μ΄μ¦ˆ λΉ„μœ¨
845
+ 6. **μΈ΅ μ„ ν˜Έλ„**: 업쒅별 μ„ ν˜Έ 측수
846
+ 7. **μ§€μ—­ 밀집도**: 점포 수 μƒμœ„ μ§€μ—­
847
+ 8. **μ—…μ’… 상관관계**: 같이 λ‚˜νƒ€λ‚˜λŠ” μ—…μ’… νŒ¨ν„΄
848
+ 9. **μ†ŒλΆ„λ₯˜ νŠΈλ Œλ“œ**: μ„ΈλΆ€ μ—…μ’… 뢄포
849
+ 10. **μ§€μ—­ νŠΉν™”**: 각 μ§€μ—­μ˜ νŠΉν™” μ—…μ’…
850
+
851
+ πŸ’‘ **Tip**: API ν‚€ 없이도 10κ°€μ§€ μ‹œκ°ν™” 뢄석과 κΈ°λ³Έ 톡계λ₯Ό 확인할 수 μžˆμŠ΅λ‹ˆλ‹€!
852
+ """)
853
+
854
+ # μ‹€ν–‰
855
+ if __name__ == "__main__":
856
+ demo.launch(server_name="0.0.0.0", server_port=7860, share=False)