[備忘録] Streamlitで複数画面構成を実現するページ分割機能の実装方法 #Python - Qiita

こんにちは!今回は、Streamlitで複数画面構成のアプリケーションを作成する方法について解説します。単一ファイルでシンプルなWebアプリが作れるStreamlitですが、機能が増えてくるとページを分けたくなりますよね。この記事では、Streamlit公式のマルチページ機能を使って、効率的に複数画面のアプリを構築する方法を学びましょう。

image.png

目次

環境準備

まずは必要なライブラリをインストールしましょう。

pip install streamlit
pip install pandas numpy matplotlib  # データ処理・可視化用

Streamlitのマルチページ機能とは

Streamlit 1.10.0以降では、公式に複数ページをサポートする機能が追加されました。この機能を使うことで、アプリケーションを複数のPythonファイルに分割し、構造化された複数画面のアプリを開発できます。

マルチページアプリケーションの主な特徴:

  1. ファイル構造に基づく画面分割: メインファイルとpagesフォルダ内のファイルが自動的に複数ページとして認識されます
  2. 自動生成されるサイドバーナビゲーション: ファイル名に基づいたナビゲーションが自動的に生成されます
  3. セッション状態を利用したページ間のデータ共有: st.session_stateでページ間でデータを共有できます

シナリオ:シンプルなデータ分析ダッシュボード

この記事では、以下の3ページから構成される「シンプルなデータ分析ダッシュボード」を例として実装します:

  1. ホームページ: アプリケーションの概要とデータのアップロード
  2. データ分析ページ: 基本的な統計情報と分析結果の表示
  3. データ可視化ページ: グラフやチャートによるデータの可視化

image.png

image.png

データ可視化ページにはタブがあり、タブを切り替えて表示内容を変更できます。

image.png

image.png

image.png

マルチページアプリケーションの構築

ディレクトリ構成

マルチページアプリケーションを作成するには、以下のようなディレクトリ構成を作成します:

データ分析ダッシュボード/
├── Home.py           # メインページ(ホーム)
├── utils.py          # 共通ユーティリティを格納
└── pages/            # 追加ページを格納するフォルダ(必須名称)
    ├── 1_📊_データ分析.py
    └── 2_📈_データ可視化.py

重要なポイント

  • pagesフォルダ名は変更できません(Streamlitがこの名前を特別に認識します)
  • ファイル名の先頭に数字やアイコン(例:1_📊_)を付けることで、メニューの表示順やアイコンを制御できます

共通ユーティリティの作成

まず、共通のユーティリティ関数をutils.pyに記述します:

# utils.py
import pandas as pd
import numpy as np

def create_sample_data():
    """サンプルデータ(売上データ)を生成する関数"""
    dates = pd.date_range(start="2023-01-01", end="2023-12-31", freq="D")
    sales = np.random.randint(100, 5000, size=len(dates))
    categories = np.random.choice(["食品", "衣料", "電化製品", "雑貨"], size=len(dates))
    
    data = pd.DataFrame({
        "日付": dates,
        "売上": sales,
        "カテゴリ": categories
    })
    return data

メインアプリケーション(Home.py)の作成

# Home.py
import streamlit as st
import pandas as pd
from utils import create_sample_data

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

st.title("データ分析ダッシュボード")
st.sidebar.success("上のメニューからページを選択してください")

# セッションステートの初期化(初回のみサンプルデータを作成)
if "data" not in st.session_state:
    st.session_state.data = create_sample_data()

# ホームページのコンテンツ
st.header("ホームページ")
st.write("このアプリケーションでは、売上データの分析と可視化を行います。")

# データアップロード機能
st.subheader("データのアップロード")
st.write("CSVファイルをアップロードするか、サンプルデータを使用してください。")

uploaded_file = st.file_uploader("CSVファイルを選択", type=["csv"])

if uploaded_file is not None:
    try:
        data = pd.read_csv(uploaded_file)
        st.success("ファイルのアップロードに成功しました!")
        st.session_state.data = data  # セッションステートにデータを保存
        st.dataframe(data.head())
    except Exception as e:
        st.error(f"エラーが発生しました: {e}")
else:
    st.info("ファイルがアップロードされていないため、サンプルデータを使用します。")
    st.dataframe(st.session_state.data.head())

# アプリケーションの使用方法
st.markdown("""
## 使用方法

1. CSVファイルをアップロードするか、サンプルデータを使用します。
2. サイドバーのメニューから各ページにアクセスできます:
   - **データ分析**: 基本統計情報とカテゴリ別分析
   - **データ可視化**: グラフによるデータの可視化
   
## データ形式

アップロードするCSVファイルは以下の列を含むことを推奨します:
- `日付`: 日付データ (YYYY-MM-DD形式)
- `売上`: 数値データ
- `カテゴリ`: カテゴリ/区分データ

上記の列がない場合も、アプリは動作しますが一部の機能が利用できない場合があります。
""")

データ分析ページの作成

次に、pagesフォルダ内に分析ページを作成します:

# pages/1_📊_データ分析.py
import streamlit as st
import pandas as pd
import numpy as np

# ページ設定
st.set_page_config(
    page_title="データ分析",
    page_icon="📊",
)

st.title("データ分析ダッシュボード")
st.header("データ分析")

# セッションステートからデータの取得
if "data" not in st.session_state:
    st.error("データがありません。ホームページでデータをアップロードしてください。")
    st.stop()  # これ以降の処理を中止

data = st.session_state.data

# 日付データの処理(存在する場合)
if "日付" in data.columns:
    # 日付型に変換
    try:
        data["日付"] = pd.to_datetime(data["日付"])
    except:
        st.warning("日付の変換に失敗しました。日付形式を確認してください。")

# 基本統計情報
st.subheader("基本統計情報")
# 数値データのみを対象に統計情報を表示
numeric_cols = data.select_dtypes(include=[np.number]).columns.tolist()
if numeric_cols:
    st.write(data[numeric_cols].describe())
else:
    st.warning("数値データが見つかりません。")

# カテゴリ別分析
st.subheader("カテゴリ別分析")
if "カテゴリ" in data.columns and "売上" in data.columns:
    # カテゴリごとの集計
    category_summary = data.groupby("カテゴリ")["売上"].agg(["sum", "mean", "count"])
    category_summary.columns = ["合計売上", "平均売上", "データ数"]
    category_summary = category_summary.sort_values("合計売上", ascending=False)
    
    st.write(category_summary)
    
    # カテゴリごとの売上合計をバーチャートで表示
    st.subheader("カテゴリ別売上合計")
    st.bar_chart(category_summary["合計売上"])
else:
    st.warning("「カテゴリ」または「売上」列がデータに存在しません。")

# 時系列分析
st.subheader("時系列分析")
if "日付" in data.columns and "売上" in data.columns:
    # 日付でグループ化するレベルを選択
    time_level = st.selectbox(
        "時間単位を選択",
        options=["日別", "週別", "月別", "四半期別", "年別"],
        index=2  # デフォルトは月別
    )
    
    # 選択した時間単位でグループ化
    if time_level == "日別":
        data["時間単位"] = data["日付"].dt.date
    elif time_level == "週別":
        data["時間単位"] = data["日付"].dt.to_period("W").dt.start_time.dt.date
    elif time_level == "月別":
        data["時間単位"] = data["日付"].dt.to_period("M").dt.start_time.dt.date
    elif time_level == "四半期別":
        data["時間単位"] = data["日付"].dt.to_period("Q").dt.start_time.dt.date
    else:  # 年別
        data["時間単位"] = data["日付"].dt.year
    
    # グループ化して集計
    time_summary = data.groupby("時間単位")["売上"].sum().reset_index()
    time_summary = time_summary.sort_values("時間単位")
    
    # 時系列チャートの表示
    st.line_chart(time_summary.set_index("時間単位"))
    
    # 詳細データの表示
    with st.expander("詳細データを表示"):
        st.dataframe(time_summary)
else:
    st.warning("「日付」または「売上」列がデータに存在しません。")

データ可視化ページの作成

続いて、データ可視化ページを作成します:

# pages/2_📈_データ可視化.py
import streamlit as st
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl

# 日本語フォント設定
plt.rcParams['font.family'] = 'sans-serif'
plt.rcParams['font.sans-serif'] = ['MS Gothic', 'Yu Gothic', 'Meiryo', 'sans-serif']
plt.rcParams['axes.unicode_minus'] = False

# ページ設定
st.set_page_config(
    page_title="データ可視化",
    page_icon="📈",
    layout="wide"
)

st.title("データ分析ダッシュボード")
st.header("データ可視化")

# セッションステートからデータの取得
if "data" not in st.session_state:
    st.error("データがありません。ホームページでデータをアップロードしてください。")
    st.stop()

data = st.session_state.data

# 日付データの処理(存在する場合)
if "日付" in data.columns:
    try:
        data["日付"] = pd.to_datetime(data["日付"])
        data[""] = data["日付"].dt.strftime("%Y-%m")
    except:
        st.warning("日付の変換に失敗しました。")

# タブで可視化を分類
tab1, tab2, tab3 = st.tabs(["カテゴリ分析", "時系列分析", "データ分布"])

# タブ1: カテゴリ分析
with tab1:
    st.subheader("カテゴリ別売上割合")
    
    if "カテゴリ" in data.columns and "売上" in data.columns:
        # カテゴリ別集計
        category_sales = data.groupby("カテゴリ")["売上"].sum()
        
        # 円グラフの表示
        fig, ax = plt.subplots(figsize=(10, 6))
        ax.pie(
            category_sales, 
            labels=category_sales.index, 
            autopct='%1.1f%%',
            startangle=90,
            shadow=True,
        )
        ax.axis('equal')  # 円を真円に
        plt.title("カテゴリ別売上割合")
        st.pyplot(fig)
        
        # データ表示
        st.dataframe(category_sales.reset_index().rename(
            columns={"index": "カテゴリ", "売上": "合計売上"}
        ))
    else:
        st.warning("「カテゴリ」または「売上」列がデータに存在しません。")

# タブ2: 時系列分析
with tab2:
    st.subheader("月別売上トレンド")
    
    if "日付" in data.columns and "売上" in data.columns:
        # 月別集計
        monthly_data = data.groupby("")["売上"].sum().reset_index()
        
        # グラフ表示
        fig, ax = plt.subplots(figsize=(12, 6))
        ax.bar(monthly_data[""], monthly_data["売上"])
        plt.xticks(rotation=45)
        plt.title("月別売上推移")
        plt.tight_layout()
        st.pyplot(fig)
        
        # カテゴリ別月次推移(存在する場合)
        if "カテゴリ" in data.columns:
            st.subheader("カテゴリ別月次売上推移")
            
            # カテゴリと月でグループ化
            category_monthly = data.groupby(["", "カテゴリ"])["売上"].sum().reset_index()
            
            # グラフ表示
            fig, ax = plt.subplots(figsize=(12, 8))
            
            # カテゴリごとに異なる色で線グラフを描画
            for category in category_monthly["カテゴリ"].unique():
                category_data = category_monthly[category_monthly["カテゴリ"] == category]
                ax.plot(category_data[""], category_data["売上"], marker='o', label=category)
            
            ax.legend()
            plt.xticks(rotation=45)
            plt.title("カテゴリ別月次売上推移")
            plt.tight_layout()
            st.pyplot(fig)
    else:
        st.warning("「日付」または「売上」列がデータに存在しません。")

# タブ3: データ分布
with tab3:
    st.subheader("売上データの分布")
    
    if "売上" in data.columns:
        # ヒストグラム
        fig, ax = plt.subplots(figsize=(10, 6))
        ax.hist(data["売上"], bins=20, alpha=0.7, density=True)

        # 平均線を追加
        mean_value = data["売上"].mean()
        ax.axvline(mean_value, color='red', linestyle='--', linewidth=1, label=f'平均: {mean_value:.1f}')
        plt.title("売上データのヒストグラム")
        ax.legend()
        st.pyplot(fig)
        
        # 箱ひげ図(カテゴリ別)
        if "カテゴリ" in data.columns:
            st.subheader("カテゴリ別売上分布")
            fig, ax = plt.subplots(figsize=(10, 6))
            
            # カテゴリごとに箱ひげ図を作成
            category_list = data["カテゴリ"].unique()
            category_data = [data[data["カテゴリ"] == category]["売上"] for category in category_list]
            ax.boxplot(category_data, tick_labels=category_list)
            
            plt.title("カテゴリ別売上の箱ひげ図")
            plt.xticks(rotation=45)
            plt.tight_layout()
            st.pyplot(fig)
    else:
        st.warning("「売上」列がデータに存在しません。")

アプリケーションの実行

アプリケーションを実行するには、メインディレクトリ(Home.pyがあるディレクトリ)で以下のコマンドを実行します:

すると、ブラウザが自動的に開き、アプリケーションが表示されます。サイドバーには自動的にページ一覧が表示され、ユーザーは各ページを切り替えることができます。

ページ間のデータ共有

Streamlitでは、st.session_stateを使用してページ間でデータを共有します。上記の実装でも、アップロードされたデータや分析結果をページ間で共有するために使用しています。

セッションステートの基本的な使い方

# データの保存
st.session_state.データ名 = データ値

# データの取得
if "データ名" in st.session_state:
    data = st.session_state.データ名
else:
    # データが存在しない場合の処理
    st.error("データが見つかりません")

セッションステートの活用例

  1. ユーザー入力の保持

    if "user_settings" not in st.session_state:
        st.session_state.user_settings = {"theme": "light", "language": "ja"}
    
    # 設定を変更
    selected_theme = st.selectbox("テーマを選択", ["light", "dark"], 
                                  index=0 if st.session_state.user_settings["theme"] == "light" else 1)
    
    # 変更を保存
    if selected_theme != st.session_state.user_settings["theme"]:
        st.session_state.user_settings["theme"] = selected_theme
    
  2. 計算結果のキャッシュ

    # 重い計算の結果をキャッシュ
    if "analysis_results" not in st.session_state:
        # 初回のみ計算を実行
        st.session_state.analysis_results = perform_heavy_analysis(data)
        
    # キャッシュした結果を使用
    results = st.session_state.analysis_results
    

実装のポイントとベストプラクティス

1. わかりやすいファイル名とページ構成

マルチページアプリケーションでは、ファイル名がそのままメニュー項目になります。以下のような命名規則がおすすめです:

1_🏠_ホーム.py
2_📊_データ分析.py
3_📈_可視化.py
  • 数字プレフィックス: 順序を制御
  • 絵文字: 視覚的にわかりやすく
  • 簡潔な名前: 機能を表す

2. 共通のユーティリティ関数とスタイルの一貫性

共通のユーティリティ関数は別ファイルに切り出し、各ページで再利用します。また、ページタイトルやレイアウトなども一貫性を持たせましょう。

# 全ページで同じ設定を使用
st.set_page_config(
    page_title="アプリ名 - ページ名",
    page_icon="アイコン",
    layout="wide"  # 一貫したレイアウト
)

# 一貫したタイトル構造
st.title("アプリケーション名")
st.header("ページ名")

3. エラー処理とユーザーガイダンス

データが存在しない場合や形式が正しくない場合のエラー処理を適切に行いましょう。

# データの存在確認
if "data" not in st.session_state:
    st.error("データがありません。ホームページでデータをアップロードしてください。")
    st.stop()  # これ以降の処理を中止

# データの形式確認
if "必要な列" not in st.session_state.data.columns:
    st.warning("「必要な列」がデータに存在しません。一部の機能が利用できない場合があります。")

4. レスポンシブな設計

さまざまな画面サイズに対応するため、レスポンシブな設計を心がけましょう。

# 画面幅に応じたレイアウト
if st.session_state.get("screen_width", "wide") == "wide":
    col1, col2 = st.columns([2, 1])  # 大画面では2:1の比率
else:
    col1, col2 = st.columns([1, 1])  # 小画面では1:1の比率

まとめ

マインドマップ (39).png

この記事では、Streamlitの公式マルチページ機能を使って複数画面構成のアプリケーションを実装する方法を学びました。実際のデータ分析ダッシュボードの例を通じて、以下のポイントを解説しました:

  1. 適切なフォルダ構成とファイル名の付け方
  2. ページ間でのデータ共有方法
  3. 一貫性のあるユーザーインターフェース設計
  4. エラー処理とユーザーガイダンス

Streamlitの公式マルチページ機能を使うことで、メンテナンスしやすく拡張性の高いWebアプリケーションを開発できます。ぜひ、この記事で紹介した方法を活用して、あなた自身のアイデアを形にしてみてください!

参考リンク

最新の実装方法や注意事項は公式情報を確認してください。



フラッグシティパートナーズ海外不動産投資セミナー 【DMM FX】入金

Source link