[備忘録] StreamlitでAgGridを使った高機能テーブルの実装方法

Streamlitで高機能テーブルを実装したい方へ📊 AgGridを使うとExcel風の編集機能やフィルター、ソート機能などが簡単に実装できます。初心者向けに基本から行の追加・削除までをまとめてみました。🙏

## はじめに
Streamlitでデータ分析アプリを作る際、標準のテーブル表示では機能が不足しがちです。特にExcelのような編集機能やフィルタリング、ソート機能などが欲しい場合には「AgGrid」というライブラリが非常に便利です。この備忘録では、AgGridの基本的な使い方から、行の追加・削除などの操作を含めた実装方法をまとめています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/71177860-c167-4a38-be11-9925a5601cec.png)

## インストール方法

```bash
pip install streamlit-aggrid
```

## 基本的な使い方

AgGridを使ったExcel風の編集可能なグリッドを実装する完全なコード例を以下に示します。このコードには行の追加・削除といった基本的な操作も含まれています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/5d140f73-8e6b-4f45-aefa-fabecf73ee67.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/9391e6a7-1f6d-48f1-a753-0e7a4c86f02e.png)

```python
import streamlit as st
import pandas as pd
from st_aggrid import AgGrid, GridOptionsBuilder

# --------------------------------
# 1) セッションの初期化
# --------------------------------
if 'data' not in st.session_state:
    st.session_state.data = pd.DataFrame({
        "名前": ["佐藤", "鈴木", "田中"],
        "業種": ["Technology", "Finance", "Healthcare"],
        "評価": ["Hot", "Warm", "Cold"]
    })

st.title("🧮 Excel風 入力グリッド（AgGrid）")

# --------------------------------
# 2) GridOptions の設定
# --------------------------------
gb = GridOptionsBuilder.from_dataframe(st.session_state.data)
gb.configure_default_column(
    editable=True,
    sorteable=True,
    filterable=True,
    resizable=True
)

# 行選択（複数行選択）を有効化
gb.configure_selection('multiple')
grid_options = gb.build()

# --------------------------------
# 3) AgGridの表示
# --------------------------------
grid_response = AgGrid(
    st.session_state.data,
    gridOptions=grid_options,
    update_mode="MODEL_CHANGED",
    fit_columns_on_grid_load=True,
    allow_unsafe_jscode=True
)

# AgGrid から返されるデータをセッションに反映
if grid_response['data'] is not None:
    st.session_state.data = grid_response['data']

# 選択された行をDataFrameとして取得
selected_rows_df = grid_response['selected_rows']  # ここが DataFrame 形式の場合の想定

# --------------------------------
# 4) ボタンによる行操作
# --------------------------------
col1, col2, col3 = st.columns(3)

# ------ (A) 行の追加 ------
with col1:
    if st.button("➕ 行を追加"):
        new_row = pd.DataFrame([{"名前": "", "業種": "", "評価": ""}])
        st.session_state.data = pd.concat([st.session_state.data, new_row], ignore_index=True)
        st.rerun()

# ------ (B) 選択行の削除 ------
with col2:
    if st.button("🗑️ 選択行を削除"):
        # ★ DataFrame判定で空かどうかを確認
        if selected_rows_df is None or selected_rows_df.empty:
            st.warning("削除する行が選択されていません")
        else:
            current_data = st.session_state.data.copy()
            # 選択された行を 1 行ずつループして削除
            # （DataFrameなので .iterrows() で回す）
            for idx, row in selected_rows_df.iterrows():
                # "名前","業種","評価" が一致する行を削除する例
                mask = (
                    (current_data["名前"] == row["名前"]) &
                    (current_data["業種"] == row["業種"]) &
                    (current_data["評価"] == row["評価"])
                )
                current_data = current_data[~mask]
            # 削除結果をセッションへ反映
            st.session_state.data = current_data.reset_index(drop=True)
            st.rerun()

# ------ (C) 保存ボタン ------
with col3:
    if st.button("✅ 保存"):
        st.success("データが保存されました")
        st.write(st.session_state.data)

# ------ CSVエクスポート ------
csv_data = st.session_state.data.to_csv(index=False).encode("utf-8-sig")
st.download_button(
    "📥 CSVエクスポート",
    csv_data,
    "aggrid_data.csv",
    "text/csv",
    key='download-csv'
)
```

## コードの詳細解説

### 1. セッションの初期化

Streamlitはステートレスなフレームワークなので、ページがリロードされるたびに変数は初期化されます。そのため、データを保持するには`st.session_state`を使います。

```python
if 'data' not in st.session_state:
    st.session_state.data = pd.DataFrame({
        "名前": ["佐藤", "鈴木", "田中"],
        "業種": ["Technology", "Finance", "Healthcare"],
        "評価": ["Hot", "Warm", "Cold"]
    })
```

### 2. GridOptionsの設定

AgGridの挙動はGridOptionsで細かく設定できます。

```python
gb = GridOptionsBuilder.from_dataframe(st.session_state.data)
gb.configure_default_column(
    editable=True,    # 編集可能にする
    sorteable=True,   # ソート可能にする
    filterable=True,  # フィルタリング可能にする
    resizable=True    # 列幅のリサイズを可能にする
)

# 行選択（複数行選択）を有効化
gb.configure_selection('multiple')
grid_options = gb.build()
```

- `editable=True`: セルを直接編集できるようにします
- `sorteable=True`: 列ヘッダーをクリックしてソートできるようにします
- `filterable=True`: 列ヘッダーのメニューからフィルタリングできるようにします
- `resizable=True`: 列の幅を変更できるようにします
- `configure_selection('multiple')`: 複数行の選択を可能にします

### 3. AgGridの表示

```python
grid_response = AgGrid(
    st.session_state.data,
    gridOptions=grid_options,
    update_mode="MODEL_CHANGED",
    fit_columns_on_grid_load=True,
    allow_unsafe_jscode=True
)
```

- `update_mode="MODEL_CHANGED"`: データモデルが変更されたときに更新します
- `fit_columns_on_grid_load=True`: 初期表示時に列幅を自動調整します
- `allow_unsafe_jscode=True`: JavaScript関数の実行を許可します（カスタムレンダラーなどに必要）

グリッド操作後のデータを取得して、セッションに保存します：

```python
if grid_response['data'] is not None:
    st.session_state.data = grid_response['data']

# 選択された行をDataFrameとして取得
selected_rows_df = grid_response['selected_rows']
```

### 4. ボタンによる行操作

#### (A) 行の追加

```python
if st.button("➕ 行を追加"):
    new_row = pd.DataFrame([{"名前": "", "業種": "", "評価": ""}])
    st.session_state.data = pd.concat([st.session_state.data, new_row], ignore_index=True)
    st.rerun()
```

空の行を作成し、現在のデータフレームに結合してから、ページを再読み込みします。

#### (B) 選択行の削除

```python
if st.button("🗑️ 選択行を削除"):
    # DataFrame判定で空かどうかを確認
    if selected_rows_df is None or selected_rows_df.empty:
        st.warning("削除する行が選択されていません")
    else:
        current_data = st.session_state.data.copy()
        # 選択された行を1行ずつループして削除
        for idx, row in selected_rows_df.iterrows():
            mask = (
                (current_data["名前"] == row["名前"]) &
                (current_data["業種"] == row["業種"]) &
                (current_data["評価"] == row["評価"])
            )
            current_data = current_data[~mask]
        # 削除結果をセッションへ反映
        st.session_state.data = current_data.reset_index(drop=True)
        st.rerun()
```

選択された行の特定は、複数の列の値が一致する行を探すことで行っています。Pandasの条件式で一致する行を特定し、逆マスク（`~mask`）で一致しない行だけを残します。

#### (C) 保存とエクスポート

```python
if st.button("✅ 保存"):
    st.success("データが保存されました")
    st.write(st.session_state.data)

# CSVエクスポート
csv_data = st.session_state.data.to_csv(index=False).encode("utf-8-sig")
st.download_button(
    "📥 CSVエクスポート",
    csv_data,
    "aggrid_data.csv",
    "text/csv",
    key='download-csv'
)
```

保存ボタンは現在の状態を表示するだけですが、実際のアプリケーションではデータベースやCSVファイルなどに保存する処理を追加します。

CSVエクスポート機能は、データフレームをCSV形式に変換してダウンロードボタンを表示します。

## 応用例

### カスタム列設定

特定の列だけに異なる設定を適用したい場合：

```python
# 特定の列設定
gb.configure_column("名前", editable=False)  # 名前列は編集不可
gb.configure_column("評価", 
                   editable=True,
                   cellEditor='agSelectCellEditor',
                   cellEditorParams={
                       'values': ['Hot', 'Warm', 'Cold']
                   })  # 評価列はドロップダウンリストから選択
```

### 条件付き書式

値に応じてセルの色を変更したい場合：

```python
from st_aggrid import JsCode

# 条件付き書式
cellsytle_jscode = JsCode("""
function(params) {
    if (params.value == 'Cold') {
        return {
            'color': 'white',
            'backgroundColor': 'blue'
        }
    }
    if (params.value == 'Hot') {
        return {
            'color': 'white',
            'backgroundColor': 'red'
        }
    }
    return {
        'color': 'black',
        'backgroundColor': 'yellow'
    }
};
""")
gb.configure_column('評価', cellStyle=cellsytle_jscode)
```

### ページネーション

大量のデータを扱う場合にはページネーションが便利です：

```python
# ページネーション設定
gb.configure_grid_options(pagination=True, paginationAutoPageSize=True)
# または固定ページサイズ
# gb.configure_grid_options(pagination=True, paginationPageSize=10)
```

## 学習メモ・注意点

1. **Streamlitのステートレス性**:
   - Streamlitはステートレスなので、ボタンをクリックするたびにスクリプト全体が再実行されます
   - データを保持するには必ず`st.session_state`を使いましょう

2. **型の扱い**:
   - Pandas DataFrameを条件式で直接評価すると「The truth value of a DataFrame is ambiguous」エラーになります
   - 正しくは`if df is None or df.empty:`のように明示的にチェックします

3. **行の削除**:
   - 複数の列を使って行を一意に特定することが重要です
   - インデックスが予想と異なる場合があるので、値ベースでの比較が安全です

4. **パフォーマンス**:
   - 大きなデータセットを扱う場合は、ページネーションを有効にしましょう
   - フィルタリングやソートはクライアント側で行われるので、非常に大きなデータセットでは注意が必要です

## 参考リンク

- [streamlit-aggrid GitHub](https://github.com/PablocFonseca/streamlit-aggrid)
- [AG Grid 公式ドキュメント](https://www.ag-grid.com/documentation-main/documentation.php)
- [Streamlit Session State](https://docs.streamlit.io/library/api-reference/session-state)
- [GridOptionsBuilder](https://streamlit-aggrid.readthedocs.io/en/docs/GridOptionsBuilder.html)


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