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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/9bb6713d-137d-4147-ab01-0cc73e6cecba.png)

## 目次
- [環境準備](#環境準備)
- [Streamlitのマルチページ機能とは](#streamlitのマルチページ機能とは)
- [シナリオ：シンプルなデータ分析ダッシュボード](#シナリオシンプルなデータ分析ダッシュボード)
- [マルチページアプリケーションの構築](#マルチページアプリケーションの構築)
- [ページ間のデータ共有](#ページ間のデータ共有)
- [実装のポイントとベストプラクティス](#実装のポイントとベストプラクティス)
- [まとめ](#まとめ)

## 環境準備

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

```bash
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/8805689f-59d2-43aa-864d-831391760d75.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/fb95bc91-fa3c-4ced-9d65-b82c8ad3beb7.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/154c748e-80b3-4b60-89a5-9542bd448f81.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/767d8cca-ad0d-421b-b1e9-103d165dc461.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/120ea34f-b614-4dbc-9431-5539ecd77266.png)

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

### ディレクトリ構成

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

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

**重要なポイント**：
- `pages`フォルダ名は変更できません（Streamlitがこの名前を特別に認識します）
- ファイル名の先頭に数字やアイコン（例：`1_📊_`）を付けることで、メニューの表示順やアイコンを制御できます

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

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

```python
# 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）の作成

```python
# 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`フォルダ内に分析ページを作成します：

```python
# 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("「日付」または「売上」列がデータに存在しません。")
```

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

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

```python
# 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`があるディレクトリ）で以下のコマンドを実行します：

```bash
streamlit run Home.py
```

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

## ページ間のデータ共有

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

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

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

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

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

1. **ユーザー入力の保持**
   ```python
   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. **計算結果のキャッシュ**
   ```python
   # 重い計算の結果をキャッシュ
   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. 共通のユーティリティ関数とスタイルの一貫性

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

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

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

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

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

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

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

### 4. レスポンシブな設計

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

```python
# 画面幅に応じたレイアウト
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/b53c220a-45b3-488b-acb5-2dade204116d.png)

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

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

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

## 参考リンク

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

- [Streamlit公式ドキュメント - マルチページアプリ](https://docs.streamlit.io/develop/concepts/multipage-apps/overview)
- [Streamlit - st.session_state](https://docs.streamlit.io/develop/api-reference/caching-and-state/st.session_state)
- [Streamlit Community Cloud](https://streamlit.io/cloud) - 作成したアプリを無料でデプロイできるサービス

## 免責事項
本記事の作成にあたり、文章や図解の生成にClaude Sonnet, Feloを、ファクトチェックにChatGPTを活用しました。最終的な編集と確認は筆者が行っています。