데이터 대시보드는 여러 데이터 소스에서 수집된 정보를 시각적으로 표현하여 중요한 지표를 한눈에 파악할 수 있게 해주는 도구입니다. 이 가이드에서는 Python과 웹 기술을 활용하여 효과적인 대시보드를 구축하는 방법을 알아보겠습니다.
1. 대시보드 기획 및 설계
효과적인 대시보드를 만들기 위해서는, 먼저 목적과 사용자 요구사항을 명확히 정의해야 합니다.
1.1 대시보드 목적 정의
대시보드는 다음과 같은 목적으로 설계될 수 있습니다:
- 분석용 대시보드: 데이터 분석가가 심층적인 분석을 위해 사용
- 전략용 대시보드: 경영진이 의사결정을 위해 참고하는 KPI(핵심성과지표) 중심
- 운영용 대시보드: 일상적인 업무 모니터링 및 이상 징후 감지
1.2 핵심 지표(KPI) 선정
# 대시보드에 표시할 KPI 정의 예시
kpi_metrics = {
'sales': {
'daily_revenue': '일일 매출',
'monthly_growth': '월간 성장률',
'conversion_rate': '전환율',
'average_order_value': '평균 주문 금액'
},
'user': {
'active_users': '활성 사용자 수',
'new_users': '신규 사용자 수',
'retention_rate': '사용자 유지율',
'churn_rate': '이탈률'
},
'product': {
'top_selling': '최다 판매 제품',
'inventory_level': '재고 수준',
'return_rate': '반품률'
}
}
1.3 대시보드 레이아웃 설계
대시보드 레이아웃은 사용자의 정보 흐름과 시선 이동을 고려하여 설계해야 합니다:
- Z-패턴: 왼쪽 상단에서 오른쪽 상단, 왼쪽 하단, 오른쪽 하단으로 시선이 이동하는 패턴
- F-패턴: 웹사이트 방문자의 시선이 왼쪽 상단에서 시작하여 F자 형태로 움직이는 패턴
- 중요도 기반: 가장 중요한 정보를 왼쪽 상단에 배치하고 덜 중요한 정보를 오른쪽 하단으로 배치
# 간단한 대시보드 레이아웃 계획 예시
dashboard_layout = {
'row1': [
{'width': 6, 'height': 4, 'title': '일일 매출 추이', 'chart_type': 'line'},
{'width': 6, 'height': 4, 'title': '활성 사용자 현황', 'chart_type': 'bar'}
],
'row2': [
{'width': 4, 'height': 3, 'title': '전환율', 'chart_type': 'gauge'},
{'width': 4, 'height': 3, 'title': '신규 사용자', 'chart_type': 'counter'},
{'width': 4, 'height': 3, 'title': '평균 세션 시간', 'chart_type': 'counter'}
],
'row3': [
{'width': 12, 'height': 5, 'title': '제품별 매출 분포', 'chart_type': 'pie'}
]
}
2. 대시보드 개발 도구 선택
2.1 Python 기반 대시보드 라이브러리
Dash by Plotly
대표적인 Python 웹 대시보드 프레임워크입니다.
pip install dash dash-bootstrap-components pandas plotly
Streamlit
빠르게 데이터 애플리케이션을 구축할 수 있는 도구입니다.
pip install streamlit pandas matplotlib plotly
Panel
대화형 시각화 애플리케이션을 만들기 위한 고급 툴킷입니다.
pip install panel holoviews hvplot
2.2 도구 비교 및 선택 기준
도구 장점 단점 적합한 사용 사례
Dash | - 세밀한 커스터마이징<br>- 반응형 레이아웃<br>- 풍부한 컴포넌트 | - 상대적으로 높은 학습 곡선<br>- 복잡한 코드 구조 | 기업용 대시보드<br>복잡한 인터랙션 |
Streamlit | - 매우 간단한 API<br>- 빠른 개발 속도<br>- 간편한 배포 | - 커스터마이징 제한<br>- 복잡한 앱에 부적합 | 빠른 프로토타입<br>데이터 분석 리포트 |
Panel | - 다양한 시각화 라이브러리와 호환<br>- 유연한 레이아웃<br>- Jupyter 통합 | - 문서화 부족<br>- 소규모 커뮤니티 | 데이터 과학자용<br>탐색적 시각화 |
3. Dash를 활용한 대시보드 구현
3.1 기본 구조 설정
# app.py
import dash
from dash import dcc, html
import dash_bootstrap_components as dbc
import plotly.express as px
import pandas as pd
from datetime import datetime, timedelta
import sqlite3
# 애플리케이션 초기화
app = dash.Dash(__name__,
external_stylesheets=[dbc.themes.BOOTSTRAP],
meta_tags=[{'name': 'viewport', 'content': 'width=device-width, initial-scale=1'}])
# 앱 타이틀 설정
app.title = '비즈니스 성과 대시보드'
# 레이아웃 정의
app.layout = dbc.Container([
dbc.Row([
dbc.Col(html.H1("비즈니스 성과 대시보드", className='text-center mb-4'), width=12)
]),
# 필터 및 컨트롤 섹션
dbc.Row([
dbc.Col([
html.Label("날짜 범위 선택:"),
dcc.DatePickerRange(
id='date-range',
start_date=(datetime.now() - timedelta(days=30)).date(),
end_date=datetime.now().date(),
display_format='YYYY-MM-DD'
)
], width=6),
dbc.Col([
html.Label("제품 카테고리:"),
dcc.Dropdown(
id='category-filter',
options=[
{'label': '전체', 'value': 'all'},
{'label': '전자제품', 'value': 'electronics'},
{'label': '의류', 'value': 'clothing'},
{'label': '식품', 'value': 'food'}
],
value='all'
)
], width=6)
], className='mb-4'),
# KPI 카드 섹션
dbc.Row([
dbc.Col(dbc.Card([
dbc.CardBody([
html.H5("총 매출", className="card-title"),
html.H3(id="total-sales", children="₩0", className="card-text text-success")
])
]), width=3),
dbc.Col(dbc.Card([
dbc.CardBody([
html.H5("주문 수", className="card-title"),
html.H3(id="order-count", children="0", className="card-text")
])
]), width=3),
dbc.Col(dbc.Card([
dbc.CardBody([
html.H5("평균 주문금액", className="card-title"),
html.H3(id="avg-order", children="₩0", className="card-text")
])
]), width=3),
dbc.Col(dbc.Card([
dbc.CardBody([
html.H5("전환율", className="card-title"),
html.H3(id="conversion-rate", children="0%", className="card-text")
])
]), width=3)
], className='mb-4'),
# 차트 섹션
dbc.Row([
dbc.Col([
dbc.Card([
dbc.CardHeader("일일 매출 추이"),
dbc.CardBody(dcc.Graph(id="sales-trend"))
])
], width=6),
dbc.Col([
dbc.Card([
dbc.CardHeader("카테고리별 매출 분포"),
dbc.CardBody(dcc.Graph(id="category-distribution"))
])
], width=6)
], className='mb-4'),
dbc.Row([
dbc.Col([
dbc.Card([
dbc.CardHeader("상위 판매 제품"),
dbc.CardBody(dcc.Graph(id="top-products"))
])
], width=12)
])
], fluid=True)
# 서버 정의
server = app.server
# 메인 실행 부분
if __name__ == '__main__':
app.run_server(debug=True)
3.2 데이터 연동
# SQLite 데이터베이스에서 데이터 가져오기
def get_sales_data(start_date, end_date, category='all'):
conn = sqlite3.connect('sales_data.db')
# 쿼리 조건 설정
query = """
SELECT
s.order_id,
s.order_date,
s.product_id,
p.product_name,
p.category,
s.quantity,
s.price,
s.quantity * s.price as total_amount
FROM
sales s
JOIN
products p ON s.product_id = p.product_id
WHERE
s.order_date BETWEEN ? AND ?
"""
params = [start_date, end_date]
if category != 'all':
query += " AND p.category = ?"
params.append(category)
# 데이터 읽기
df = pd.read_sql_query(query, conn, params=params)
conn.close()
# 날짜 형식 변환
df['order_date'] = pd.to_datetime(df['order_date'])
return df
3.3 콜백 함수 정의
# 콜백 함수: 필터 변경 시 차트 업데이트
@app.callback(
[
dash.dependencies.Output('total-sales', 'children'),
dash.dependencies.Output('order-count', 'children'),
dash.dependencies.Output('avg-order', 'children'),
dash.dependencies.Output('conversion-rate', 'children'),
dash.dependencies.Output('sales-trend', 'figure'),
dash.dependencies.Output('category-distribution', 'figure'),
dash.dependencies.Output('top-products', 'figure')
],
[
dash.dependencies.Input('date-range', 'start_date'),
dash.dependencies.Input('date-range', 'end_date'),
dash.dependencies.Input('category-filter', 'value')
]
)
def update_dashboard(start_date, end_date, category):
# 데이터 가져오기
df = get_sales_data(start_date, end_date, category)
if df.empty:
return "₩0", "0", "₩0", "0%", {}, {}, {}
# KPI 계산
total_sales = df['total_amount'].sum()
order_count = df['order_id'].nunique()
avg_order = total_sales / order_count if order_count > 0 else 0
# 전환율 계산 (예시: 방문자 수 데이터가 있다고 가정)
visitors = 12000 # 실제로는 데이터베이스에서 가져와야 함
conversion_rate = (order_count / visitors) * 100 if visitors > 0 else 0
# 일일 매출 추이 차트
daily_sales = df.groupby(df['order_date'].dt.date)['total_amount'].sum().reset_index()
sales_trend_fig = px.line(
daily_sales,
x='order_date',
y='total_amount',
title='일일 매출 추이',
labels={'order_date': '날짜', 'total_amount': '매출액'},
template='plotly_white'
)
# 카테고리별 매출 분포 차트
category_sales = df.groupby('category')['total_amount'].sum().reset_index()
category_fig = px.pie(
category_sales,
values='total_amount',
names='category',
title='카테고리별 매출 분포',
template='plotly_white'
)
# 상위 판매 제품 차트
product_sales = df.groupby('product_name')['total_amount'].sum().reset_index()
top_products = product_sales.sort_values('total_amount', ascending=False).head(10)
top_products_fig = px.bar(
top_products,
x='product_name',
y='total_amount',
title='상위 10개 판매 제품',
labels={'product_name': '제품명', 'total_amount': '매출액'},
template='plotly_white'
)
return (
f"₩{total_sales:,.0f}",
f"{order_count:,}",
f"₩{avg_order:,.0f}",
f"{conversion_rate:.1f}%",
sales_trend_fig,
category_fig,
top_products_fig
)
4. Streamlit을 활용한 대시보드 구현
4.1 기본 구조 설정
# app.py
import streamlit as st
import pandas as pd
import matplotlib.pyplot as plt
import plotly.express as px
import plotly.graph_objects as go
import sqlite3
from datetime import datetime, timedelta
# 페이지 설정
st.set_page_config(
page_title="비즈니스 성과 대시보드",
page_icon="📊",
layout="wide",
initial_sidebar_state="expanded"
)
# 페이지 제목
st.title("비즈니스 성과 대시보드")
# 사이드바 필터
st.sidebar.header("필터")
# 날짜 범위 선택
start_date = st.sidebar.date_input(
"시작일",
datetime.now() - timedelta(days=30)
)
end_date = st.sidebar.date_input(
"종료일",
datetime.now()
)
# 카테고리 필터
categories = ["전체", "전자제품", "의류", "식품"]
selected_category = st.sidebar.selectbox("제품 카테고리", categories)
# 데이터 로드 함수
@st.cache_data
def load_data(start_date, end_date, category):
conn = sqlite3.connect('sales_data.db')
# 쿼리 조건 설정
query = """
SELECT
s.order_id,
s.order_date,
s.product_id,
p.product_name,
p.category,
s.quantity,
s.price,
s.quantity * s.price as total_amount
FROM
sales s
JOIN
products p ON s.product_id = p.product_id
WHERE
s.order_date BETWEEN ? AND ?
"""
params = [start_date, end_date]
if category != "전체":
query += " AND p.category = ?"
params.append(category)
# 데이터 읽기
df = pd.read_sql_query(query, conn, params=params)
conn.close()
# 날짜 형식 변환
df['order_date'] = pd.to_datetime(df['order_date'])
return df
# 데이터 로드
df = load_data(start_date, end_date, selected_category)
# KPI 지표 행
col1, col2, col3, col4 = st.columns(4)
total_sales = df['total_amount'].sum()
order_count = df['order_id'].nunique()
avg_order = total_sales / order_count if order_count > 0 else 0
visitors = 12000 # 실제로는 DB에서 가져와야 함
conversion_rate = (order_count / visitors) * 100 if visitors > 0 else 0
with col1:
st.metric("총 매출", f"₩{total_sales:,.0f}")
with col2:
st.metric("주문 수", f"{order_count:,}")
with col3:
st.metric("평균 주문금액", f"₩{avg_order:,.0f}")
with col4:
st.metric("전환율", f"{conversion_rate:.1f}%")
# 차트 행 1
st.subheader("매출 분석")
col1, col2 = st.columns(2)
with col1:
# 일일 매출 추이 차트
daily_sales = df.groupby(df['order_date'].dt.date)['total_amount'].sum().reset_index()
fig = px.line(
daily_sales,
x='order_date',
y='total_amount',
title='일일 매출 추이',
labels={'order_date': '날짜', 'total_amount': '매출액'}
)
st.plotly_chart(fig, use_container_width=True)
with col2:
# 카테고리별 매출 분포 차트
category_sales = df.groupby('category')['total_amount'].sum().reset_index()
fig = px.pie(
category_sales,
values='total_amount',
names='category',
title='카테고리별 매출 분포'
)
st.plotly_chart(fig, use_container_width=True)
# 차트 행 2
st.subheader("제품 분석")
# 상위 판매 제품 차트
product_sales = df.groupby('product_name')['total_amount'].sum().reset_index()
top_products = product_sales.sort_values('total_amount', ascending=False).head(10)
fig = px.bar(
top_products,
x='product_name',
y='total_amount',
title='상위 10개 판매 제품',
labels={'product_name': '제품명', 'total_amount': '매출액'}
)
st.plotly_chart(fig, use_container_width=True)
# 원시 데이터 표시 (접을 수 있는 섹션)
with st.expander("원시 데이터 보기"):
st.dataframe(df)
5. 대시보드 기능 확장
5.1 인터랙티브 요소 추가
# Dash에 드릴다운 기능 추가
@app.callback(
dash.dependencies.Output('product-detail-modal', 'is_open'),
[dash.dependencies.Input('top-products', 'clickData')],
[dash.dependencies.State('product-detail-modal', 'is_open')]
)
def toggle_modal(clickData, is_open):
if clickData:
return not is_open
return is_open
@app.callback(
[dash.dependencies.Output('product-detail-title', 'children'),
dash.dependencies.Output('product-detail-content', 'children')],
[dash.dependencies.Input('top-products', 'clickData')],
)
def update_modal_content(clickData):
if not clickData:
return "제품 상세", "제품을 선택하세요"
product_name = clickData['points'][0]['x']
# 제품 상세 정보 조회
conn = sqlite3.connect('sales_data.db')
product_query = """
SELECT * FROM products WHERE product_name = ?
"""
product_df = pd.read_sql_query(product_query, conn, params=[product_name])
sales_query = """
SELECT
s.order_date,
s.quantity,
s.price,
s.quantity * s.price as total_amount
FROM
sales s
JOIN
products p ON s.product_id = p.product_id
WHERE
p.product_name = ?
ORDER BY
s.order_date DESC
LIMIT 10
"""
sales_df = pd.read_sql_query(sales_query, conn, params=[product_name])
conn.close()
# 제품 상세 정보 표시
if product_df.empty:
return f"{product_name} 상세", "제품 정보를 찾을 수 없습니다."
product_info = product_df.iloc[0]
content = html.Div([
html.H5("제품 정보"),
html.P(f"카테고리: {product_info['category']}"),
html.P(f"가격: ₩{product_info['price']:,.0f}"),
html.P(f"재고: {product_info['stock']} 개"),
html.H5("최근 판매 내역"),
dash_table.DataTable(
data=sales_df.to_dict('records'),
columns=[{'name': i, 'id': i} for i in sales_df.columns],
style_table={'overflowX': 'auto'},
style_cell={'textAlign': 'left'},
style_header={
'backgroundColor': 'rgb(230, 230, 230)',
'fontWeight': 'bold'
}
)
])
return f"{product_name} 상세", content
5.2 대시보드 성능 최적화
# 대용량 데이터 처리를 위한 최적화 기법
# 1. 데이터 캐싱
@st.cache_data(ttl=3600) # 1시간 동안 캐시 유지
def load_large_data(start_date, end_date, category):
# 데이터 로드 코드...
return df
# 2. 데이터 다운샘플링
def downsample_time_series(df, freq='D'):
"""시계열 데이터를 지정된 주기로 다운샘플링"""
return df.resample(freq, on='timestamp').mean()
# 3. 집계 쿼리 사용
def optimized_query(start_date, end_date, category):
query = """
SELECT
date(s.order_date) as order_day,
sum(s.quantity * s.price) as daily_sales
FROM
sales s
JOIN
products p ON s.product_id = p.product_id
WHERE
s.order_date BETWEEN ? AND ?
"""
if category != "전체":
query += " AND p.category = ?"
params = [start_date, end_date, category]
else:
params = [start_date, end_date]
query += " GROUP BY date(s.order_date)"
# 실행...
return result_df
# 4. 페이징 처리
@app.callback(
dash.dependencies.Output('paginated-table', 'children'),
[dash.dependencies.Input('page', 'value')]
)
def update_table(page):
page_size = 20
offset = (page - 1) * page_size
query = f"""
SELECT * FROM large_table
LIMIT {page_size} OFFSET {offset}
"""
# 쿼리 실행...
return table
5.3 대시보드 배포 방법
Dash 애플리케이션 배포
# requirements.txt 생성
"""
dash==2.10.2
dash-bootstrap-components==1.4.1
pandas==2.0.3
plotly==5.15.0
gunicorn==21.2.0
"""
# Procfile (Heroku 배포용)
"""
web: gunicorn app:server
"""
# Docker 배포 (Dockerfile)
"""
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8050
CMD ["gunicorn", "--bind", "0.0.0.0:8050", "app:server"]
"""
Streamlit 애플리케이션 배포
# requirements.txt 생성
"""
streamlit==1.24.0
pandas==2.0.3
plotly==5.15.0
matplotlib==3.7.2
"""
# Streamlit Cloud에 배포
# 1. GitHub 저장소에 코드 푸시
# 2. https://streamlit.io/cloud 접속
# 3. 저장소 연결 및 앱 배포
import pandas as pd
import numpy as np
from prophet import Prophet
from datetime import datetime, timedelta
def generate_sales_forecast(historical_df, forecast_days=30):
"""Prophet 모델을 사용하여 매출 예측"""
# 데이터 준비
forecast_df = historical_df[['order_date', 'total_amount']].copy()
forecast_df.columns = ['ds', 'y']
# Prophet 모델 학습
model = Prophet(
daily_seasonality=False,
weekly_seasonality=True,
yearly_seasonality=True,
seasonality_mode='multiplicative'
)
model.fit(forecast_df)
# 예측 기간 설정
future = model.make_future_dataframe(periods=forecast_days)
# 예측 수행
forecast = model.predict(future)
return forecast
def create_anomaly_detection_system(df, threshold=2.0):
"""Z-점수 기반 이상치 감지 시스템"""
# 이동 평균 및 표준편차 계산
window = 7 # 7일 기준
df['rolling_mean'] = df['total_amount'].rolling(window=window).mean()
df['rolling_std'] = df['total_amount'].rolling(window=window).std()
# Z-점수 계산
df['z_score'] = np.abs((df['total_amount'] - df['rolling_mean']) / df['rolling_std'])
# 이상치 플래그 설정
df['is_anomaly'] = df['z_score'] > threshold
return df
# 알림 이메일 전송 함수
def send_alert_email(anomaly_date, metric_name, actual_value, expected_value):
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
# 이메일 설정
sender_email = "alerts@company.com"
receiver_email = "admin@company.com"
password = "your_password"
# 이메일 내용 생성
subject = f"대시보드 알림: {metric_name} 이상치 감지"
body = f"""
안녕하세요,
{anomaly_date}에 {metric_name}에서 이상치가 감지되었습니다.
실제값: {actual_value:.2f}
예상값: {expected_value:.2f}
편차: {(actual_value - expected_value) / expected_value * 100:.2f}%
대
'오픈소스를 위한 기초 상식' 카테고리의 다른 글
LangChain 기초 학습 가이드 (0) | 2025.03.30 |
---|---|
OpenAI API 기초 학습 가이드 (0) | 2025.03.29 |
자동 리포트 생성 시스템 학습 자료 (0) | 2025.03.27 |
데이터 수집-분석-저장 파이프라인 구축 가이드 (0) | 2025.03.26 |
SQLite 기반 자동화 시스템 구축 가이드 (0) | 2025.03.25 |