MarketSync / modules /visualization.py
hyeonjoo's picture
Initial project commit with LFS
9b1e3db
# modules/visualization.py
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib import font_manager
import numpy as np
import streamlit as st
import config
from modules.profile_utils import get_chat_profile_dict
logger = config.get_logger(__name__)
def set_korean_font():
"""
์‹œ์Šคํ…œ์— ์„ค์น˜๋œ ํ•œ๊ธ€ ํฐํŠธ๋ฅผ ์ฐพ์•„ Matplotlib์— ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
"""
font_list = ['Malgun Gothic', 'AppleGothic', 'NanumGothic']
found_font = False
for font_name in font_list:
if any(font.name == font_name for font in font_manager.fontManager.ttflist):
plt.rc('font', family=font_name)
logger.info(f"โœ… ํ•œ๊ธ€ ํฐํŠธ '{font_name}'์„(๋ฅผ) ์ฐพ์•„ ๊ทธ๋ž˜ํ”„์— ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.")
found_font = True
break
if not found_font:
logger.warning("โš ๏ธ ๊ฒฝ๊ณ : Malgun Gothic, AppleGothic, NanumGothic ํฐํŠธ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
plt.rcParams['axes.unicode_minus'] = False
def display_merchant_profile(profile_data: dict):
set_korean_font()
"""
๋ถ„์„๋œ ๊ฐ€๋งน์  ํ”„๋กœํ•„ ์ „์ฒด๋ฅผ Streamlit ํ™”๋ฉด์— ์‹œ๊ฐํ™”ํ•ฉ๋‹ˆ๋‹ค.
"""
if not profile_data or "store_profile" not in profile_data:
st.error("๋ถ„์„ํ•  ๊ฐ€๋งน์  ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
return
store_data = profile_data["store_profile"]
store_name = store_data.get('๊ฐ€๋งน์ ๋ช…', '์„ ํƒ ๋งค์žฅ')
st.info(f"**'{store_name}'**์˜ ์ƒ์„ธ ๋ถ„์„ ๊ฒฐ๊ณผ์ž…๋‹ˆ๋‹ค.")
tab1, tab2, tab3, tab4 = st.tabs([
"๐Ÿ“‹ ๊ธฐ๋ณธ ์ •๋ณด",
"๐Ÿง‘โ€๐Ÿคโ€๐Ÿง‘ ์ฃผ์š” ๊ณ ๊ฐ์ธต (์„ฑ๋ณ„/์—ฐ๋ น๋Œ€)",
"๐Ÿšถ ์ฃผ์š” ๊ณ ๊ฐ ์œ ํ˜• (์ƒ๊ถŒ)",
"๐Ÿ” ๊ณ ๊ฐ ์ถฉ์„ฑ๋„ (์‹ ๊ทœ/์žฌ๋ฐฉ๋ฌธ)"
])
with tab1:
render_basic_info_table(store_data)
with tab2:
st.subheader("๐Ÿง‘โ€๐Ÿคโ€๐Ÿง‘ ์ฃผ์š” ๊ณ ๊ฐ์ธต ๋ถ„ํฌ (์„ฑ๋ณ„/์—ฐ๋ น๋Œ€)")
fig2 = plot_customer_distribution(store_data)
st.pyplot(fig2)
with tab3:
st.subheader("๐Ÿšถ ์ฃผ์š” ๊ณ ๊ฐ ์œ ํ˜• (์ƒ๊ถŒ)")
fig3 = plot_customer_type_pie(store_data)
st.pyplot(fig3)
with tab4:
st.subheader("๐Ÿ” ์‹ ๊ทœ vs ์žฌ๋ฐฉ๋ฌธ ๊ณ ๊ฐ ๋น„์œจ")
fig4 = plot_loyalty_donut(store_data)
st.pyplot(fig4)
def get_main_customer_segment(store_data):
"""์ฃผ์š” ๊ณ ๊ฐ์ธต(์„ฑ๋ณ„/์—ฐ๋ น๋Œ€) ํ…์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค."""
segments = {
'๋‚จ์„ฑ 20๋Œ€ ์ดํ•˜': store_data.get('๋‚จ์„ฑ20๋Œ€์ดํ•˜๋น„์œจ', 0),
'๋‚จ์„ฑ 30๋Œ€': store_data.get('๋‚จ์„ฑ30๋Œ€๋น„์œจ', 0),
'๋‚จ์„ฑ 40๋Œ€': store_data.get('๋‚จ์„ฑ40๋Œ€๋น„์œจ', 0),
'๋‚จ์„ฑ 50๋Œ€ ์ด์ƒ': store_data.get('๋‚จ์„ฑ50๋Œ€๋น„์œจ', 0) + store_data.get('๋‚จ์„ฑ60๋Œ€์ด์ƒ๋น„์œจ', 0),
'์—ฌ์„ฑ 20๋Œ€ ์ดํ•˜': store_data.get('์—ฌ์„ฑ20๋Œ€์ดํ•˜๋น„์œจ', 0),
'์—ฌ์„ฑ 30๋Œ€': store_data.get('์—ฌ์„ฑ30๋Œ€๋น„์œจ', 0),
'์—ฌ์„ฑ 40๋Œ€': store_data.get('์—ฌ์„ฑ40๋Œ€๋น„์œจ', 0),
'์—ฌ์„ฑ 50๋Œ€ ์ด์ƒ': store_data.get('์—ฌ์„ฑ50๋Œ€๋น„์œจ', 0) + store_data.get('์—ฌ์„ฑ60๋Œ€์ด์ƒ๋น„์œจ', 0)
}
if not any(segments.values()):
return None
max_segment = max(segments, key=segments.get)
max_value = segments[max_segment]
if max_value == 0:
return None
return f"'{max_segment}({max_value:.1f}%)'"
def render_basic_info_table(store_data):
"""(Tab 1) ๊ธฐ๋ณธ ์ •๋ณด ์š”์•ฝ ํ‘œ์™€ ํ…์ŠคํŠธ๋ฅผ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค."""
summary_data = get_chat_profile_dict(store_data)
st.subheader("๐Ÿ“‹ ๊ฐ€๋งน์  ๊ธฐ๋ณธ ์ •๋ณด")
summary_df = pd.DataFrame(summary_data.items(), columns=["ํ•ญ๋ชฉ", "๋‚ด์šฉ"])
summary_df = summary_df[summary_df['ํ•ญ๋ชฉ'] != '์ž๋™์ถ”์ถœํŠน์ง•']
summary_df = summary_df.astype(str)
st.table(summary_df.set_index('ํ•ญ๋ชฉ'))
st.subheader("๐Ÿ“Œ ๋ถ„์„ ์š”์•ฝ")
st.write(f"โœ… **{summary_data.get('๊ฐ€๋งน์ ๋ช…', 'N/A')}**์€(๋Š”) '{summary_data.get('์ƒ๊ถŒ', 'N/A')}' ์ƒ๊ถŒ์˜ '{summary_data.get('์—…์ข…', 'N/A')}' ์—…์ข… ๊ฐ€๋งน์ ์ž…๋‹ˆ๋‹ค.")
st.write(f"๐Ÿ“ˆ ๋งค์ถœ ์ˆ˜์ค€์€ **{summary_data.get('๋งค์ถœ ์ˆ˜์ค€', 'N/A')}**์ด๋ฉฐ, ๋™์ผ ์ƒ๊ถŒ ๋‚ด ๋งค์ถœ ์ˆœ์œ„๋Š” **{summary_data.get('๋™์ผ ์ƒ๊ถŒ ๋Œ€๋น„ ๋งค์ถœ ์ˆœ์œ„', 'N/A')}**์ž…๋‹ˆ๋‹ค.")
st.write(f"๐Ÿ’ฐ ๋ฐฉ๋ฌธ ๊ณ ๊ฐ์ˆ˜๋Š” **{summary_data.get('๋ฐฉ๋ฌธ ๊ณ ๊ฐ์ˆ˜ ์ˆ˜์ค€', 'N/A')}** ์ˆ˜์ค€์ด๋ฉฐ, ๊ฐ๋‹จ๊ฐ€๋Š” **{summary_data.get('๊ฐ๋‹จ๊ฐ€ ์ˆ˜์ค€', 'N/A')}** ์ˆ˜์ค€์ž…๋‹ˆ๋‹ค.")
main_customer = get_main_customer_segment(store_data)
if main_customer:
st.write(f"๐Ÿ‘ฅ ์ฃผ์š” ๊ณ ๊ฐ์ธต์€ **{main_customer}**์ด(๊ฐ€) ๊ฐ€์žฅ ๋งŽ์Šต๋‹ˆ๋‹ค.")
def plot_customer_distribution(store_data):
"""(Tab 2) ๊ณ ๊ฐ ํŠน์„ฑ ๋ถ„ํฌ (์„ฑ๋ณ„/์—ฐ๋ น๋Œ€)๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ๋ง‰๋Œ€ ๊ทธ๋ž˜ํ”„๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค."""
labels = ['20๋Œ€ ์ดํ•˜', '30๋Œ€', '40๋Œ€', '50๋Œ€ ์ด์ƒ']
male_percents = [
store_data.get('๋‚จ์„ฑ20๋Œ€์ดํ•˜๋น„์œจ', 0), store_data.get('๋‚จ์„ฑ30๋Œ€๋น„์œจ', 0),
store_data.get('๋‚จ์„ฑ40๋Œ€๋น„์œจ', 0),
store_data.get('๋‚จ์„ฑ50๋Œ€๋น„์œจ', 0) + store_data.get('๋‚จ์„ฑ60๋Œ€์ด์ƒ๋น„์œจ', 0)
]
female_percents = [
store_data.get('์—ฌ์„ฑ20๋Œ€์ดํ•˜๋น„์œจ', 0), store_data.get('์—ฌ์„ฑ30๋Œ€๋น„์œจ', 0),
store_data.get('์—ฌ์„ฑ40๋Œ€๋น„์œจ', 0),
store_data.get('์—ฌ์„ฑ50๋Œ€๋น„์œจ', 0) + store_data.get('์—ฌ์„ฑ60๋Œ€์ด์ƒ๋น„์œจ', 0)
]
x = np.arange(len(labels))
width = 0.35
fig, ax = plt.subplots(figsize=(10, 6))
rects1 = ax.bar(x - width/2, male_percents, width, label='๋‚จ์„ฑ', color='cornflowerblue')
rects2 = ax.bar(x + width/2, female_percents, width, label='์—ฌ์„ฑ', color='salmon')
ax.set_ylabel('๊ณ ๊ฐ ๋น„์œจ (%)')
ax.set_title('์ฃผ์š” ๊ณ ๊ฐ์ธต ๋ถ„ํฌ (์„ฑ๋ณ„/์—ฐ๋ น๋Œ€)', fontsize=16)
ax.set_xticks(x)
ax.set_xticklabels(labels, fontsize=12)
ax.legend()
ax.grid(axis='y', linestyle='--', alpha=0.7)
ax.bar_label(rects1, padding=3, fmt='%.1f')
ax.bar_label(rects2, padding=3, fmt='%.1f')
fig.tight_layout()
return fig
def plot_customer_type_pie(store_data):
"""(Tab 3) ์ฃผ์š” ๊ณ ๊ฐ ์œ ํ˜• (๊ฑฐ์ฃผ์ž, ์ง์žฅ์ธ, ์œ ๋™์ธ๊ตฌ)์„ ํŒŒ์ด ์ฐจํŠธ๋กœ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค."""
customer_data = {
'์œ ๋™์ธ๊ตฌ': store_data.get("์œ ๋™์ธ๊ตฌ์ด์šฉ๋น„์œจ", 0),
'๊ฑฐ์ฃผ์ž': store_data.get("๊ฑฐ์ฃผ์ž์ด์šฉ๋น„์œจ", 0),
'์ง์žฅ์ธ': store_data.get("์ง์žฅ์ธ์ด์šฉ๋น„์œจ", 0)
}
filtered_data = {label: (size or 0) for label, size in customer_data.items()}
filtered_data = {label: size for label, size in filtered_data.items() if size > 0}
sizes = list(filtered_data.values())
labels = list(filtered_data.keys())
if not sizes or sum(sizes) == 0:
fig, ax = plt.subplots(figsize=(6, 6))
ax.text(0.5, 0.5, "๋ฐ์ดํ„ฐ ์—†์Œ", horizontalalignment='center', verticalalignment='center', transform=ax.transAxes)
ax.set_title("์ฃผ์š” ๊ณ ๊ฐ ์œ ํ˜•", fontsize=13)
return fig
pie_labels = [f"{label} ({size:.1f}%)" for label, size in zip(labels, sizes)]
fig, ax = plt.subplots(figsize=(6, 6))
wedges, texts, autotexts = ax.pie(
sizes,
labels=pie_labels,
autopct='%1.1f%%',
startangle=90,
pctdistance=0.8
)
plt.setp(autotexts, size=9, weight="bold", color="white")
ax.set_title("์ฃผ์š” ๊ณ ๊ฐ ์œ ํ˜•", fontsize=13)
ax.axis('equal')
return fig
def plot_loyalty_donut(store_data):
"""(Tab 4) ์‹ ๊ทœ vs ์žฌ๋ฐฉ๋ฌธ ๊ณ ๊ฐ ๋น„์œจ์„ ๋„๋„› ์ฐจํŠธ๋กœ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค."""
visit_ratio = {
'์‹ ๊ทœ ๊ณ ๊ฐ': store_data.get('์‹ ๊ทœ๊ณ ๊ฐ๋น„์œจ') or 0,
'์žฌ์ด์šฉ ๊ณ ๊ฐ': store_data.get('์žฌ์ด์šฉ๊ณ ๊ฐ๋น„์œจ') or 0
}
sizes = list(visit_ratio.values())
labels = list(visit_ratio.keys())
if not sizes or sum(sizes) == 0:
fig, ax = plt.subplots(figsize=(5, 5))
ax.text(0.5, 0.5, "๋ฐ์ดํ„ฐ ์—†์Œ", horizontalalignment='center', verticalalignment='center', transform=ax.transAxes)
ax.set_title("์‹ ๊ทœ vs ์žฌ๋ฐฉ๋ฌธ ๊ณ ๊ฐ ๋น„์œจ")
return fig
fig, ax = plt.subplots(figsize=(5, 5))
wedges, texts, autotexts = ax.pie(
sizes,
labels=labels,
autopct='%1.1f%%',
startangle=90,
pctdistance=0.85,
colors=['lightcoral', 'skyblue']
)
centre_circle = plt.Circle((0, 0), 0.70, fc='white')
ax.add_artist(centre_circle)
plt.setp(autotexts, size=10, weight="bold")
ax.set_title("์‹ ๊ทœ vs ์žฌ๋ฐฉ๋ฌธ ๊ณ ๊ฐ ๋น„์œจ", fontsize=14)
ax.axis('equal')
return fig