【Streamlit】売上分析ダッシュボードを作ってみた

プログラミング
この記事は約26分で読めます。

こんにちは、木川です。

データ分析の結果をチームに共有する際、「Excelファイルを添付したメール」や「静的なグラフ画像」では、相手が自由に探索できず不便ではありませんか?

ここでは、Pythonのライブラリ「Streamlit」を使って、誰でもブラウザで操作できるインタラクティブなデータ分析ダッシュボードを作成する方法を、実際に動くコード付きで解説します。

それではまいりましょう!

この記事で作るもの

売上データを可視化・分析できるダッシュボードを作成します。

主要な機能は以下です。

  • 売上トレンドの可視化 (折れ線グラフ、棒グラフなど)
  • 期間や商品カテゴリでのフィルタリング
  • 主要指標(KPI)の表示
  • CSVデータのアップロード機能
  • 分析結果のダウンロード

Streamlitとは

Streamlitは、Pythonを使用して簡単にインタラクティブなWebアプリケーションを作成できるオープンソースのフレームワークです。

特徴は以下です。

  • HTMLやJavaScriptの知識不要
  • Pythonコードだけでインタラクティブなアプリを作成
  • ローカルでもクラウドでも簡単にデプロイ可能
  • データ分析ライブラリ(pandas、matplotlib等)との相性抜群

環境構築

必要なライブラリのインストール

まずはお決まりの流れでPython開発環境を作成します。以下をインストールしましょう。

pip install streamlit pandas plotly openpyxl

バージョン確認

Streamlitのバージョンを確認します。ターミナルで以下のコマンドを実行し、streamlit version 1.X.Xが表示されればOKです。

streamlit --version

実装

実際に実装していきます。本記事では、後で差分を比較できるように、各セクションで各機能を追加する度に、新しいファイルを作成します。そのため、後半に進むにつれて、例にあげるコードが長くなっていきますが、その分コピー&ペーストでそのまま動くようになっています。

サンプルデータの準備

プロジェクトのルートディレクトリに「sample_data_generator.py」というサンプルファイルを作成し、以下のようなデータを作成します。

import pandas as pd
import numpy as np
from datetime import datetime, timedelta

# サンプルデータ生成
np.random.seed(42)
date_range = pd.date_range(start='2024-01-01', end='2024-12-31', freq='D')

data = {
    '日付': date_range,
    '売上': np.random.randint(50000, 200000, len(date_range)),
    '商品カテゴリ': np.random.choice(['電子機器', '家具', '食品', '衣類'], len(date_range)),
    '地域': np.random.choice(['東京', '大阪', '名古屋', '福岡'], len(date_range)),
    '顧客数': np.random.randint(10, 100, len(date_range))
}

df = pd.DataFrame(data)
df.to_csv('sales_data.csv', index=False, encoding='utf-8-sig')
print("サンプルデータを sales_data.csv として保存しました")

作成できたら、ターミナルでルートディレクトリに移動し、上記のファイルを実行しましょう。

python sample_data_generator.py

すると、以下のようなサンプルデータのCSVファイルが生成されます。

基本的なStreamlitアプリの作成

まずはシンプルな形から作成します。「app_basic.py」というファイルを作成し、以下のコードを書きます。

# app_basic.py
import streamlit as st
import pandas as pd

# ページ設定
st.set_page_config(
    page_title="売上分析ダッシュボード",
    page_icon="📊",
    layout="wide"
)

# タイトル
st.title("📊 売上分析ダッシュボード")

# データ読み込み
@st.cache_data  # キャッシュして高速化
def load_data():
    return pd.read_csv('sales_data.csv', parse_dates=['日付'])

df = load_data()

# データ表示
st.subheader("データプレビュー")
st.dataframe(df.head(10))

# 基本統計量
st.subheader("基本統計量")
st.write(df.describe())

この状態でアプリを実行してみます。

streamlit run app_basic.py

すると、以下のような画面が立ち上がります。

この実装のポイントは、以下です。

・@st.cache_dataデコレータ

この実装により、同じ入力なら再実行せずにキャッシュを使用するため、動作の高速化に寄与します。

インタラクティブ機能の追加

フィルタリング機能を追加して、ユーザーがデータを絞り込めるようにします。「app_interactive.py」というファイルを作成し、以下のコードを書きます。

# app_interactive.py
import streamlit as st
import pandas as pd
import plotly.express as px

st.set_page_config(page_title="売上分析ダッシュボード", page_icon="📊", layout="wide")

@st.cache_data
def load_data():
    df = pd.read_csv('sales_data.csv', parse_dates=['日付'])
    return df

df = load_data()

# サイドバーでフィルター
st.sidebar.header("🔍 フィルター設定")

# 日付範囲選択
date_range = st.sidebar.date_input(
    "期間を選択",
    value=(df['日付'].min(), df['日付'].max()),
    min_value=df['日付'].min(),
    max_value=df['日付'].max()
)

# カテゴリ選択
categories = st.sidebar.multiselect(
    "商品カテゴリ",
    options=df['商品カテゴリ'].unique(),
    default=df['商品カテゴリ'].unique()
)

# 地域選択
regions = st.sidebar.multiselect(
    "地域",
    options=df['地域'].unique(),
    default=df['地域'].unique()
)

# データフィルタリング
if len(date_range) == 2:
    mask = (
        (df['日付'] >= pd.Timestamp(date_range[0])) &
        (df['日付'] <= pd.Timestamp(date_range[1])) &
        (df['商品カテゴリ'].isin(categories)) &
        (df['地域'].isin(regions))
    )
    filtered_df = df[mask]
else:
    filtered_df = df

# メインエリア
st.title("📊 売上分析ダッシュボード")

# KPI表示
col1, col2, col3, col4 = st.columns(4)

with col1:
    st.metric("総売上", f"¥{filtered_df['売上'].sum():,.0f}")
with col2:
    st.metric("平均売上", f"¥{filtered_df['売上'].mean():,.0f}")
with col3:
    st.metric("総顧客数", f"{filtered_df['顧客数'].sum():,}")
with col4:
    st.metric("データ件数", f"{len(filtered_df):,}")

# グラフ表示
st.subheader("📈 売上トレンド")
daily_sales = filtered_df.groupby('日付')['売上'].sum().reset_index()
fig = px.line(daily_sales, x='日付', y='売上', title='日次売上推移')
st.plotly_chart(fig, use_container_width=True)

# カテゴリ別売上
st.subheader("🏷️ カテゴリ別売上")
category_sales = filtered_df.groupby('商品カテゴリ')['売上'].sum().reset_index()
fig2 = px.bar(category_sales, x='商品カテゴリ', y='売上', title='商品カテゴリ別売上')
st.plotly_chart(fig2, use_container_width=True)

同じようにアプリを実行します。

streamlit run app_interactive.py

すると、今回は以下のような画面になっています。

この実装のポイントは、以下です。

①サイドバーの活用

  • フィルターをサイドバーにまとめ、メインのレイアウトがすっきり
  • multiselectでセレクトボックスの入力欄
  • date_inputで日付選択

②レイアウトのカラム分割

  • st.columns()で横並びレイアウト
  • st.metric()でKPIを見やすく表示

完全版ダッシュボード

それでは、今回のメインとなる、完全版のダッシュボードを書いていきます。「app_complete.py」というファイルを作成し、以下のコードを書きます。

# app_complete.py
import streamlit as st
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
from io import BytesIO

# ページ設定
st.set_page_config(
    page_title="売上分析ダッシュボード",
    page_icon="📊",
    layout="wide",
    initial_sidebar_state="expanded"
)

# カスタムCSS
st.markdown("""
    <style>
    .main {
        padding: 0rem 1rem;
    }
    .stMetric {
        background-color: #f0f2f6;
        padding: 15px;
        border-radius: 10px;
    }
    </style>
    """, unsafe_allow_html=True)

# データ読み込み関数
@st.cache_data
def load_data(file):
    if file is not None:
        df = pd.read_csv(file, parse_dates=['日付'])
    else:
        df = pd.read_csv('sales_data.csv', parse_dates=['日付'])
    return df

# Excel変換関数
def to_excel(df):
    output = BytesIO()
    with pd.ExcelWriter(output, engine='openpyxl') as writer:
        df.to_excel(writer, index=False, sheet_name='分析結果')
    return output.getvalue()

# サイドバー
st.sidebar.title("⚙️ 設定")

# ファイルアップロード
uploaded_file = st.sidebar.file_uploader(
    "CSVファイルをアップロード",
    type=['csv'],
    help="日付,売上,商品カテゴリ,地域,顧客数の列が必要です"
)

# データ読み込み
df = load_data(uploaded_file)

# サイドバーフィルター
st.sidebar.header("🔍 フィルター")

# 日付範囲
min_date = df['日付'].min().date()
max_date = df['日付'].max().date()

date_range = st.sidebar.date_input(
    "期間",
    value=(min_date, max_date),
    min_value=min_date,
    max_value=max_date
)

# カテゴリ選択
all_categories = df['商品カテゴリ'].unique().tolist()
selected_categories = st.sidebar.multiselect(
    "商品カテゴリ",
    options=all_categories,
    default=all_categories
)

# 地域選択
all_regions = df['地域'].unique().tolist()
selected_regions = st.sidebar.multiselect(
    "地域",
    options=all_regions,
    default=all_regions
)

# 売上範囲フィルター
min_sales, max_sales = int(df['売上'].min()), int(df['売上'].max())
sales_range = st.sidebar.slider(
    "売上範囲",
    min_value=min_sales,
    max_value=max_sales,
    value=(min_sales, max_sales)
)

# データフィルタリング
if len(date_range) == 2:
    mask = (
        (df['日付'] >= pd.Timestamp(date_range[0])) &
        (df['日付'] <= pd.Timestamp(date_range[1])) &
        (df['商品カテゴリ'].isin(selected_categories)) &
        (df['地域'].isin(selected_regions)) &
        (df['売上'] >= sales_range[0]) &
        (df['売上'] <= sales_range[1])
    )
    filtered_df = df[mask].copy()
else:
    filtered_df = df.copy()

# メインエリア
st.title("📊 売上分析ダッシュボード")
st.markdown("---")

# KPIメトリクス
col1, col2, col3, col4, col5 = st.columns(5)

total_sales = filtered_df['売上'].sum()
avg_sales = filtered_df['売上'].mean()
total_customers = filtered_df['顧客数'].sum()
avg_customers = filtered_df['顧客数'].mean()
data_count = len(filtered_df)

with col1:
    st.metric(
        "総売上",
        f"¥{total_sales:,.0f}",
        delta=f"{(total_sales/df['売上'].sum()*100):.1f}% of total"
    )

with col2:
    st.metric(
        "平均売上",
        f"¥{avg_sales:,.0f}"
    )

with col3:
    st.metric(
        "総顧客数",
        f"{total_customers:,}"
    )

with col4:
    st.metric(
        "平均顧客数",
        f"{avg_customers:.1f}"
    )

with col5:
    st.metric(
        "データ件数",
        f"{data_count:,}"
    )

st.markdown("---")

# タブで情報を整理
tab1, tab2, tab3, tab4 = st.tabs(["📈 トレンド分析", "🏷️ カテゴリ分析", "🗺️ 地域分析", "📋 詳細データ"])

with tab1:
    st.subheader("売上トレンド")
    
    # 日次集計
    daily_sales = filtered_df.groupby('日付').agg({
        '売上': 'sum',
        '顧客数': 'sum'
    }).reset_index()
    
    # 折れ線グラフ
    fig1 = go.Figure()
    fig1.add_trace(go.Scatter(
        x=daily_sales['日付'],
        y=daily_sales['売上'],
        mode='lines',
        name='売上',
        line=dict(color='#1f77b4', width=2)
    ))
    fig1.update_layout(
        title='日次売上推移',
        xaxis_title='日付',
        yaxis_title='売上(円)',
        hovermode='x unified'
    )
    st.plotly_chart(fig1, use_container_width=True)
    
    # 移動平均
    window = st.slider("移動平均日数", 1, 30, 7)
    daily_sales['移動平均'] = daily_sales['売上'].rolling(window=window).mean()
    
    fig2 = px.line(daily_sales, x='日付', y=['売上', '移動平均'],
                   title=f'{window}日移動平均')
    st.plotly_chart(fig2, use_container_width=True)

with tab2:
    st.subheader("商品カテゴリ別分析")
    
    col1, col2 = st.columns(2)
    
    with col1:
        # カテゴリ別売上
        category_sales = filtered_df.groupby('商品カテゴリ')['売上'].sum().reset_index()
        fig3 = px.bar(
            category_sales,
            x='商品カテゴリ',
            y='売上',
            title='カテゴリ別売上',
            color='売上',
            color_continuous_scale='Blues'
        )
        st.plotly_chart(fig3, use_container_width=True)
    
    with col2:
        # 円グラフ
        fig4 = px.pie(
            category_sales,
            values='売上',
            names='商品カテゴリ',
            title='カテゴリ別売上構成比'
        )
        st.plotly_chart(fig4, use_container_width=True)
    
    # カテゴリ×地域のヒートマップ
    st.subheader("カテゴリ×地域 売上ヒートマップ")
    pivot_data = filtered_df.pivot_table(
        values='売上',
        index='商品カテゴリ',
        columns='地域',
        aggfunc='sum',
        fill_value=0
    )
    fig5 = px.imshow(
        pivot_data,
        labels=dict(x="地域", y="商品カテゴリ", color="売上"),
        x=pivot_data.columns,
        y=pivot_data.index,
        color_continuous_scale='RdYlGn',
        title='カテゴリ×地域の売上分布'
    )
    st.plotly_chart(fig5, use_container_width=True)

with tab3:
    st.subheader("地域別分析")
    
    col1, col2 = st.columns(2)
    
    with col1:
        # 地域別売上
        region_sales = filtered_df.groupby('地域')['売上'].sum().reset_index()
        fig6 = px.bar(
            region_sales,
            x='地域',
            y='売上',
            title='地域別売上',
            color='売上',
            color_continuous_scale='Greens'
        )
        st.plotly_chart(fig6, use_container_width=True)
    
    with col2:
        # 地域別顧客数
        region_customers = filtered_df.groupby('地域')['顧客数'].sum().reset_index()
        fig7 = px.bar(
            region_customers,
            x='地域',
            y='顧客数',
            title='地域別顧客数',
            color='顧客数',
            color_continuous_scale='Oranges'
        )
        st.plotly_chart(fig7, use_container_width=True)

with tab4:
    st.subheader("詳細データ")
    
    # 検索機能
    search_term = st.text_input("🔍 検索(商品カテゴリまたは地域)")
    if search_term:
        display_df = filtered_df[
            (filtered_df['商品カテゴリ'].str.contains(search_term, case=False)) |
            (filtered_df['地域'].str.contains(search_term, case=False))
        ]
    else:
        display_df = filtered_df
    
    # データ表示
    st.dataframe(
        display_df.sort_values('日付', ascending=False),
        use_container_width=True,
        height=400
    )
    
    # ダウンロードボタン
    col1, col2 = st.columns(2)
    
    with col1:
        csv = display_df.to_csv(index=False).encode('utf-8-sig')
        st.download_button(
            label="📥 CSVダウンロード",
            data=csv,
            file_name=f"filtered_data_{pd.Timestamp.now().strftime('%Y%m%d')}.csv",
            mime="text/csv"
        )
    
    with col2:
        excel_data = to_excel(display_df)
        st.download_button(
            label="📥 Excelダウンロード",
            data=excel_data,
            file_name=f"filtered_data_{pd.Timestamp.now().strftime('%Y%m%d')}.xlsx",
            mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
        )

# フッター
st.markdown("---")
st.markdown(
    """
    <div style='text-align: center'>
        <p>Powered by Streamlit | データ件数: {} | 最終更新: {}</p>
    </div>
    """.format(len(df), pd.Timestamp.now().strftime('%Y-%m-%d %H:%M:%S')),
    unsafe_allow_html=True
)

コチラも同様に、アプリを実行します。

streamlit run app_complete.py

すると、このようなアプリが立ち上がります。

タブ切り替え

今回の実装のポイントは以下です。

①タブによる切り替え(st.tabs)

  • 複数の分析結果を1ページにまとめられる
  • ユーザーが見たい情報に素早くアクセス可能

②ファイルアップロード(st.sidebar.file_uploader)

  • ユーザーが自分で用意したデータを使用して分析可能
  • 題に引数の「type」でファイル形式を指定

③ファイルダウンロード(df.to_csv)

  • 分析結果をエクスポート可能
  • utf-8-sigで文字化け防止

まとめ

本記事では、Streamlitを使ってデータ分析ダッシュボードを作成する方法を解説しました。普段プログラミングをする方であれば、「かなりコード量が少ないな」と感じたのではないでしょうか?

このように、Streamlitを使えば、ものの数時間で本格的なダッシュボードが作れます。ぜひ活用してみてください!

最後まで読んでいただき、ありがとうございました!

コメント

タイトルとURLをコピーしました