SEM（構造方程式モデリング）をStreamlitで可視化

# **user**:

初学者向けにVSCodeを使ってStreamlitアプリを作るガイドを書いてください。そのアプリは、次のような動作をします。

・ユーザにcsvファイルまたはエクセルファイルの入力を促します。
・エクセルファイルの場合は、どのシートを見たいかユーザに確認します。
・入力された表形式データから、ユーザは、目的変数を表す列を複数、説明変数を表す列を複数、選択します。
・SEM(構造方程式モデリング) を適用し、因果分析します。

# **assistant**:

以下は、**初学者向けに VSCode を使って Streamlit アプリを作るガイド**です。このガイドでは、**CSV または Excel ファイルのアップロード・読み込みから、SEM（構造方程式モデリング）による因果分析まで**を丁寧に説明します。

---

# 🎓 初心者向け Streamlit アプリ作成ガイド：SEM による因果分析

## 1. 事前準備

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

まず、Python 環境を整えましょう。以下のコマンドをターミナルで実行してください：

```bash
pip install streamlit pandas openpyxl semopy
```

> `semopy` は SEM（構造方程式モデリング）を扱うライブラリです。

---

## 2. VSCodeでプロジェクトを作成

1. 任意のフォルダに移動し、新しいプロジェクト用のディレクトリを作成します。
2. VSCode でそのフォルダを開きます。
3. `app.py` というファイルを作成します。

---

## 3. アプリコードの作成

`app.py` に以下のコードを貼り付けてください。

```python
import streamlit as st
import pandas as pd
from semopy import Model, semplot
import io

# タイトル
st.title("📊 SEMによる因果分析アプリ")

# ファイルアップロード
uploaded_file = st.file_uploader("CSV または Excel ファイルをアップロードしてください", type=["csv", "xlsx"])

df = None

# ファイル読み込み処理
if uploaded_file is not None:
    if uploaded_file.name.endswith(".csv"):
        df = pd.read_csv(uploaded_file)
    elif uploaded_file.name.endswith(".xlsx"):
        xls = pd.ExcelFile(uploaded_file)
        sheet_name = st.selectbox("シートを選択してください", xls.sheet_names)
        df = pd.read_excel(xls, sheet_name=sheet_name)

if df is not None:
    st.write("データプレビュー：", df.head())

    # 列選択
    target_columns = st.multiselect("目的変数（従属変数）を選択してください", df.columns)
    predictor_columns = st.multiselect("説明変数（独立変数）を選択してください", df.columns)

    if target_columns and predictor_columns:
        # SEMモデル文字列の作成（単純な回帰ベース）
        model_desc = "\n".join(
            f"{target} ~ {' + '.join(predictor_columns)}" for target in target_columns
        )
        st.text("モデル定義:")
        st.code(model_desc)

        try:
            model = Model(model_desc)
            model.fit(df)

            # 結果表示
            st.subheader("📈 モデル結果")
            st.dataframe(model.inspect())

            # パス図の表示（必要に応じて）
            fig = semplot(model)
            st.pyplot(fig)

        except Exception as e:
            st.error(f"モデルの構築中にエラーが発生しました: {e}")
```

---

## 4. アプリの起動

ターミナルで以下のコマンドを実行：

```bash
streamlit run app.py
```

ブラウザが開いてアプリが表示されます 🎉

---

## 5. より高度なカスタマイズ（発展）

- 変数の標準化オプションの追加
- モデルの適合度指標（RMSEA, CFIなど）の表示
- ユーザーによるモデル構造の編集

---

## まとめ

このアプリでは以下のようなスキルを学べます：

- VSCode での Python/Streamlit 開発
- CSV・Excel データの読み込み
- GUI でのカラム選択
- SEM モデルの構築と可視化

---

必要であれば、この内容を Markdown やブログ記事形式に整えて提供することもできます！続きを希望しますか？

# **user**:

ありがとうございます。次のエラーが生じましたので、解決した全コードを書いてください。

```
モデルの構築中にエラーが発生しました: semplot() missing 1 required positional argument: 'filename'
```

# **assistant**:

ありがとうございます！そのエラーは、`semopy.semplots.semplot()` 関数が **保存先のファイル名を必須引数として要求する**ために発生しています。

しかし、Streamlit 上で可視化したい場合は、`graphviz` を直接返す方法の方が自然です。

### ✅ 解決策：`semopy.semplot()` の代わりに `model.inspect()` に加えて `model.plot_graph()` を使う

`semopy` の最新版では `model.plot_graph()` を使うことで、`graphviz` の `Dot` オブジェクトを得られ、それを `st.graphviz_chart()` に渡せば Streamlit でパス図を描画できます。

---

## ✅ 修正済みの全コード（`app.py`）

```python
import streamlit as st
import pandas as pd
from semopy import Model
import io

# タイトル
st.title("📊 SEMによる因果分析アプリ")

# ファイルアップロード
uploaded_file = st.file_uploader("CSV または Excel ファイルをアップロードしてください", type=["csv", "xlsx"])

df = None

# ファイル読み込み処理
if uploaded_file is not None:
    if uploaded_file.name.endswith(".csv"):
        df = pd.read_csv(uploaded_file)
    elif uploaded_file.name.endswith(".xlsx"):
        xls = pd.ExcelFile(uploaded_file)
        sheet_name = st.selectbox("シートを選択してください", xls.sheet_names)
        df = pd.read_excel(xls, sheet_name=sheet_name)

if df is not None:
    st.write("データプレビュー：", df.head())

    # 列選択
    target_columns = st.multiselect("目的変数（従属変数）を選択してください", df.columns)
    predictor_columns = st.multiselect("説明変数（独立変数）を選択してください", df.columns)

    if target_columns and predictor_columns:
        # SEMモデル文字列の作成（単純な回帰ベース）
        model_desc = "\n".join(
            f"{target} ~ {' + '.join(predictor_columns)}" for target in target_columns
        )
        st.text("モデル定義:")
        st.code(model_desc)

        try:
            model = Model(model_desc)
            model.fit(df)

            # 結果表示
            st.subheader("📈 モデル結果")
            st.dataframe(model.inspect())

            # パス図の表示
            st.subheader("🧭 パス図（因果構造）")
            dot = model.plot_graph()
            st.graphviz_chart(dot.source)

        except Exception as e:
            st.error(f"モデルの構築中にエラーが発生しました: {e}")
```

---

### 🔧 補足：Graphviz のエラーが出た場合

以下のライブラリもインストールしておく必要があります：

```bash
pip install graphviz
```

---

これで、Streamlit アプリ上に SEM モデルのパス図も正しく表示されるようになります。  
もし他にも改善したいポイントや機能追加の希望があれば、お知らせください！

# **user**:

ありがとうございます。次のエラーが生じましたので、解決した全コードを書いてください。

```
モデルの構築中にエラーが発生しました: 'Model' object has no attribute 'plot_graph'
```

# **assistant**:

ありがとうございます！ご指摘のエラー：

> `'Model' object has no attribute 'plot_graph'`

は、`semopy` のバージョンによって `plot_graph()` メソッドが存在しないことが原因です（`semopy` はまだ開発途上のライブラリのため、バージョンによって API がかなり異なります）。

---

## ✅ 解決策

代わりに、`semopy.semplot()` を使って **一時ファイルにパス図を保存し、それを Streamlit に読み込ませて表示**するようにします。

---

## ✅ 修正版の全コード（`app.py`）

```python
import streamlit as st
import pandas as pd
from semopy import Model, semplot
import io
import tempfile
from PIL import Image

# タイトル
st.title("📊 SEMによる因果分析アプリ")

# ファイルアップロード
uploaded_file = st.file_uploader("CSV または Excel ファイルをアップロードしてください", type=["csv", "xlsx"])

df = None

# ファイル読み込み処理
if uploaded_file is not None:
    if uploaded_file.name.endswith(".csv"):
        df = pd.read_csv(uploaded_file)
    elif uploaded_file.name.endswith(".xlsx"):
        xls = pd.ExcelFile(uploaded_file)
        sheet_name = st.selectbox("シートを選択してください", xls.sheet_names)
        df = pd.read_excel(xls, sheet_name=sheet_name)

if df is not None:
    st.write("データプレビュー：", df.head())

    # 列選択
    target_columns = st.multiselect("目的変数（従属変数）を選択してください", df.columns)
    predictor_columns = st.multiselect("説明変数（独立変数）を選択してください", df.columns)

    if target_columns and predictor_columns:
        # SEMモデル文字列の作成（単純な回帰ベース）
        model_desc = "\n".join(
            f"{target} ~ {' + '.join(predictor_columns)}" for target in target_columns
        )
        st.text("モデル定義:")
        st.code(model_desc)

        try:
            model = Model(model_desc)
            model.fit(df)

            # 結果表示
            st.subheader("📈 モデル結果")
            st.dataframe(model.inspect())

            # パス図の表示（画像として保存して読み込む）
            st.subheader("🧭 パス図（因果構造）")
            with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmpfile:
                semplot(model, tmpfile.name)
                image = Image.open(tmpfile.name)
                st.image(image, caption="SEM パス図")

        except Exception as e:
            st.error(f"モデルの構築中にエラーが発生しました: {e}")
```

---

## 🔧 補足：必要な追加ライブラリ

もし以下が未インストールであれば、入れてください：

```bash
pip install pillow graphviz
```

---

これで、古い `semopy` バージョンでも **SEM の結果とパス図を Streamlit 上で表示**できるようになります。

ご希望であれば、適合度指標（RMSEAやCFIなど）も追加できます。必要ですか？

# **user**:

ありがとうございます。次のエラーが生じましたので、解決した全コードを書いてください。

```
モデルの構築中にエラーが発生しました: failed to execute PosixPath('dot'), make sure the Graphviz executables are on your systems' PATH
```

# **assistant**:

こちらこそ、詳細ありがとうございます！  
そのエラーは、**Graphviz の本体（実行ファイル群）がシステムにインストールされていない**か、**PATH に通っていない**ために発生します。

---

## ✅ 原因

`semopy` → `graphviz` を内部的に使ってパス図を描画していますが、その際に `dot` という Graphviz の実行ファイルが使われます。これは **Python ライブラリだけでなく、Graphviz 本体もインストール**しておく必要があります。

---

## ✅ 解決手順

### ▼ ① Graphviz 本体のインストール

#### macOS の場合：
```bash
brew install graphviz
```

#### Windows の場合：

1. 以下の公式サイトから Graphviz をインストール  
   👉 [https://graphviz.org/download/](https://graphviz.org/download/)
2. インストール後、「環境変数 PATH に Graphviz の `bin` ディレクトリを追加」します  
   例: `C:\Program Files\Graphviz\bin`

#### Linux の場合：
```bash
sudo apt-get install graphviz
```

---

### ▼ ② Python ライブラリのインストール（すでに入っている場合はスキップ）

```bash
pip install graphviz pillow
```

---

### ▼ ③ 修正不要！前回のコードがそのまま動くようになります

Graphviz の実行ファイルがインストールされて PATH に通っていれば、**前回お渡しした `app.py` はそのまま動作**します。

---

## 🔁 確認用のコードスニペット（再掲）

```python
# semopy + semplot によるパス図の描画
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmpfile:
    semplot(model, tmpfile.name)
    image = Image.open(tmpfile.name)
    st.image(image, caption="SEM パス図")
```

---

## ✅ 補足：PATH に通っているか確認する方法（Windows）

1. `cmd` を開く
2. 次のコマンドを入力：
   ```cmd
   where dot
   ```

出力に `dot.exe` のパスが表示されれば OK です。

---

これでエラーは解決するはずです！  
パス図のスタイル調整や、モデル適合度指標の追加などもご希望であれば対応しますよ〜。

# **user**:

networkx で代用した修正を行なってください。

# **assistant**:

了解です！Graphviz のインストールなしで SEM の構造を描画したいということですね。  
その場合は `networkx` と `matplotlib` を使って、**簡易的なパス図**を自前で描画する方法に切り替えられます。

---

## ✅ 方法の概要

- `model_desc` の構造をパースして、`networkx` の有向グラフを作成
- Streamlit 上に `matplotlib` で描いたネットワーク図を表示

---

## ✅ 修正済みの全コード（Graphviz 不要）

```python
import streamlit as st
import pandas as pd
from semopy import Model
import io
import matplotlib.pyplot as plt
import networkx as nx
import tempfile

# タイトル
st.title("📊 SEMによる因果分析アプリ（Graphviz 不要版）")

# ファイルアップロード
uploaded_file = st.file_uploader("CSV または Excel ファイルをアップロードしてください", type=["csv", "xlsx"])

df = None

# ファイル読み込み処理
if uploaded_file is not None:
    if uploaded_file.name.endswith(".csv"):
        df = pd.read_csv(uploaded_file)
    elif uploaded_file.name.endswith(".xlsx"):
        xls = pd.ExcelFile(uploaded_file)
        sheet_name = st.selectbox("シートを選択してください", xls.sheet_names)
        df = pd.read_excel(xls, sheet_name=sheet_name)

if df is not None:
    st.write("データプレビュー：", df.head())

    # 列選択
    target_columns = st.multiselect("目的変数（従属変数）を選択してください", df.columns)
    predictor_columns = st.multiselect("説明変数（独立変数）を選択してください", df.columns)

    if target_columns and predictor_columns:
        # SEMモデル文字列の作成（単純な回帰ベース）
        model_desc = "\n".join(
            f"{target} ~ {' + '.join(predictor_columns)}" for target in target_columns
        )
        st.text("モデル定義:")
        st.code(model_desc)

        try:
            model = Model(model_desc)
            model.fit(df)

            # 結果表示
            st.subheader("📈 モデル結果")
            st.dataframe(model.inspect())

            # 簡易パス図（networkx で可視化）
            st.subheader("🧭 簡易パス図（networkx）")
            G = nx.DiGraph()
            for target in target_columns:
                for predictor in predictor_columns:
                    G.add_edge(predictor, target)

            fig, ax = plt.subplots(figsize=(6, 4))
            pos = nx.spring_layout(G)
            nx.draw(G, pos, with_labels=True, node_size=1500, node_color="lightblue", arrows=True, ax=ax)
            st.pyplot(fig)

        except Exception as e:
            st.error(f"モデルの構築中にエラーが発生しました: {e}")
```

---

## ✅ 必要な追加パッケージ（まだの場合）

```bash
pip install networkx matplotlib
```

---

## 📌 特徴

- `networkx` により Graphviz なしでも SEM の構造が視覚化可能
- ノードの位置は自動配置（`spring_layout`）ですが、見やすく整理されます
- パス係数や標準化効果なども後で追加可能です（ご希望あれば）

---

必要であれば「相関を線の太さに反映」「方向性ありの因果係数表示」などにも対応できます！お気軽にどうぞ。

# **user**:

ありがとうございます。モデル結果の、lval, op, rval, Estimate, Std.Err, z-value, p-value について説明していただけますか？

# **assistant**:

もちろんです！`semopy` を使った SEM（構造方程式モデリング）では、`model.inspect()` で以下のようなテーブルが得られます。これは、**推定されたモデル内の各パラメータに関する統計的な情報**を示しています。

---

## 🔍 各列の意味

| 列名         | 説明 |
|--------------|------|
| **lval**     | **左辺の変数名**（左辺値：left value）<br>式でいうと `Y ~ X` の `Y` にあたります（従属変数、目的変数）。 |
| **op**       | **演算子**（operation）<br>例えば `~`（回帰）や `~~`（分散・共分散）など。 |
| **rval**     | **右辺の変数名**（右辺値：right value）<br>式でいうと `Y ~ X` の `X` にあたります（独立変数、説明変数）。 |
| **Estimate** | **推定された係数（パス係数）**<br>目的変数が右辺の変数に対してどれだけ影響を受けるかの推定値。 |
| **Std.Err**  | **標準誤差（standard error）**<br>Estimate の精度の指標。小さいほど精度が高い。 |
| **z-value**  | **z 統計量**（Estimate ÷ Std.Err）<br>係数が 0 と異なるかどうかを検定するための統計量。 |
| **p-value**  | **p 値**<br>統計的有意性の指標。通常は 0.05 以下であれば「有意に0ではない」と解釈される。 |

---

## 🧠 例（実際の行）

| lval | op | rval | Estimate | Std.Err | z-value | p-value |
|------|----|------|----------|---------|---------|---------|
| `Y`  | `~`| `X1` |  0.80    |  0.10   |   8.00  |  0.0000 |

**解釈**：
- `Y = 0.8 * X1 + 誤差` という関係が推定された。
- この係数（0.8）は z=8.0 で非常に有意（p < 0.001）なので、`X1` は `Y` に有意な影響を与えていると考えられる。

---

## ✅ よくある op（演算子）の種類

| 演算子 | 意味                             |
|--------|----------------------------------|
| `~`    | 回帰（従属変数 ~ 独立変数）      |
| `~~`   | 分散・共分散                     |
| `~1`   | 定数項（切片）                   |
| `=~`   | 潜在変数の測定モデル（因子分析）※ |

※ 今回は使用していないが、潜在変数を扱う場合には `=~` が使われます。

---

## 👀 もし知りたいことがあれば：

- 適合度指標（RMSEA, CFI など）の出し方
- `Estimate` を標準化した結果（β係数）の取得
- 潜在変数を含めたモデルの記述方法（`=~` の使い方）

などもサポートできます！必要に応じて教えてくださいね。

# **user**:

ありがとうございます。現状、次のようなコードになっています。このコードをベースに、次の改変を加えた全コードを書いてください。

・有意な因果関係のみを取り出して因果構造を描画する。

```python
import streamlit as st
import pandas as pd
from semopy import Model
import io
import matplotlib.pyplot as plt
import networkx as nx
import tempfile

# タイトル
st.title("📊 SEMによる因果分析アプリ")

# ファイルアップロード
uploaded_file = st.file_uploader(
    "CSV または Excel ファイルをアップロードしてください", type=["csv", "xlsx"]
)

df = None

# ファイル読み込み処理
if uploaded_file is not None:
    if uploaded_file.name.endswith(".csv"):
        df = pd.read_csv(uploaded_file)
    elif uploaded_file.name.endswith(".xlsx"):
        xls = pd.ExcelFile(uploaded_file)
        sheet_name = st.selectbox("シートを選択してください", xls.sheet_names)
        df = pd.read_excel(xls, sheet_name=sheet_name)

if df is not None:
    st.write("データプレビュー：", df.head())

    # 列選択
    target_columns = st.multiselect(
        "目的変数（従属変数）を選択してください", df.columns
    )
    predictor_columns = st.multiselect(
        "説明変数（独立変数）を選択してください", df.columns
    )

    if target_columns and predictor_columns:
        # SEMモデル文字列の作成（単純な回帰ベース）
        model_descs = []
        vals = list(set(predictor_columns + target_columns))
        for y in vals:
            xs = [x for x in vals]
            xs.remove(y)
            model_descs.append(f"{y} ~ {' + '.join(xs)}")
        model_desc = "\n".join(model_descs)
        st.text("モデル定義:")
        st.code(model_desc)

        try:
            model = Model(model_desc)
            model.fit(df)

            # 結果表示
            st.subheader("📈 モデル結果")
            st.dataframe(model.inspect())

            # 簡易パス図（networkx で可視化）
            st.subheader("🧭 簡易パス図（networkx）")
            G = nx.DiGraph()
            for target in target_columns:
                for predictor in predictor_columns:
                    G.add_edge(predictor, target)

            fig, ax = plt.subplots(figsize=(6, 4))
            pos = nx.spring_layout(G)
            nx.draw(
                G,
                pos,
                with_labels=True,
                node_size=1500,
                node_color="lightblue",
                arrows=True,
                ax=ax,
            )
            st.pyplot(fig)

        except Exception as e:
            st.error(f"モデルの構築中にエラーが発生しました: {e}")
```

# **assistant**:

ありがとうございます！  
ご要望通り、**推定結果の p 値に基づいて有意な因果関係（通常 p < 0.05）** のみを抽出して、`networkx` で描画するようにコードを改良しました。

---

## ✅ 改変ポイント

- `model.inspect()` の結果から **p 値が有意（p < 0.05）な `~`（回帰）関係**を抽出
- その関係のみを使ってグラフ描画

---

## 🔧 改良済みの全コード

```python
import streamlit as st
import pandas as pd
from semopy import Model
import matplotlib.pyplot as plt
import networkx as nx

# タイトル
st.title("📊 SEMによる因果分析アプリ")

# ファイルアップロード
uploaded_file = st.file_uploader(
    "CSV または Excel ファイルをアップロードしてください", type=["csv", "xlsx"]
)

df = None

# ファイル読み込み処理
if uploaded_file is not None:
    if uploaded_file.name.endswith(".csv"):
        df = pd.read_csv(uploaded_file)
    elif uploaded_file.name.endswith(".xlsx"):
        xls = pd.ExcelFile(uploaded_file)
        sheet_name = st.selectbox("シートを選択してください", xls.sheet_names)
        df = pd.read_excel(xls, sheet_name=sheet_name)

if df is not None:
    st.write("データプレビュー：", df.head())

    # 列選択
    target_columns = st.multiselect("目的変数（従属変数）を選択してください", df.columns)
    predictor_columns = st.multiselect("説明変数（独立変数）を選択してください", df.columns)

    if target_columns and predictor_columns:
        # SEMモデル文字列の作成（完全結合モデル）
        model_descs = []
        vals = list(set(predictor_columns + target_columns))
        for y in vals:
            xs = [x for x in vals if x != y]
            if xs:
                model_descs.append(f"{y} ~ {' + '.join(xs)}")
        model_desc = "\n".join(model_descs)
        st.text("モデル定義:")
        st.code(model_desc)

        try:
            # モデル作成とフィッティング
            model = Model(model_desc)
            model.fit(df)

            # 結果表示
            st.subheader("📈 モデル結果")
            results_df = model.inspect()
            st.dataframe(results_df)

            # 有意な因果関係の抽出
            significant_paths = results_df[
                (results_df["op"] == "~") & (results_df["p-value"] < 0.05)
            ]

            st.subheader("🔍 有意な因果関係（p < 0.05）")
            st.dataframe(significant_paths)

            # 有意な関係のみでグラフ構築
            st.subheader("🧭 有意な因果構造（networkx）")
            G = nx.DiGraph()
            for _, row in significant_paths.iterrows():
                G.add_edge(row["rval"], row["lval"], weight=row["Estimate"])

            fig, ax = plt.subplots(figsize=(6, 4))
            pos = nx.spring_layout(G)
            nx.draw(
                G,
                pos,
                with_labels=True,
                node_size=1500,
                node_color="lightblue",
                arrows=True,
                ax=ax,
            )
            edge_labels = nx.get_edge_attributes(G, "weight")
            edge_labels = {k: f"{v:.2f}" for k, v in edge_labels.items()}
            nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels, ax=ax)

            st.pyplot(fig)

        except Exception as e:
            st.error(f"モデルの構築中にエラーが発生しました: {e}")
```

---

## ✅ このバージョンの特長

- **p値で有意なもののみ**を因果グラフに反映
- **係数（Estimate）をラベルに表示**
- SEM 結果と可視化をしっかりリンク

---

他にも次のような拡張が可能です：

- 標準化係数で描画（`model.inspect(std_est=True)`）
- スライダーで p 値のしきい値を変更
- 因果グラフの保存ボタン追加

必要があれば遠慮なくどうぞ！

# **user**:

ありがとうございます。上記コードをベースとしながら、下記コードを参考に、ネットワークをインタラクティブに可視化できるように改変してください。

```python
if "model" in st.session_state and "selected_cols" in st.session_state:
    model = st.session_state["model"]
    selected_cols = st.session_state["selected_cols"]
    data = st.session_state["data"]
    target_cols = st.session_state["target_col"]
    method = st.session_state["method"]

    if method == "VARLiNGAM":
        params["mat"] = st.slider("隣接行列", 1, 10, 1)

    if hasattr(model, "adjacency_matrix_"):
        adj_matrix = model.adjacency_matrix_
    elif hasattr(model, "adjacency_matrices_"):
        adj_matrix = model.adjacency_matrices_[params["mat"] - 1]
    elif method == "BootstrapLiNGAM":
        model = model_.bootstrap(X, n_sampling=100)
        adj_matrix = model.get_directed_adjacency_matrix()
    else:
        print("hogehogeHOGE")
        adj_matrix = None

    if adj_matrix is not None:
        st.success(f"{method} による因果構造の推定が完了しました！")

        st.subheader("因果行列（推定された重み）")
        weight_df = pd.DataFrame(adj_matrix, columns=selected_cols, index=selected_cols)
        st.dataframe(weight_df.round(3))

        output = BytesIO()
        with pd.ExcelWriter(output, engine="xlsxwriter") as writer:
            weight_df.to_excel(writer, sheet_name="CausalMatrix")
        output.seek(0)

        st.download_button(
            label="因果行列をExcelファイルとしてダウンロード",
            data=output,
            file_name="causal_matrix.xlsx",
            mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        )

        st.subheader("因果グラフの可視化")

        view_mode = st.radio(
            "可視化モードを選択してください",
            ["目的変数に到達可能な全てのパス", "全てのパス"],
        )

        T = st.slider("閾値T（-T〜+Tの範囲の辺を削除）", 0.0, 1.0, 0.3, 0.01)

        G = nx.DiGraph()
        for i in range(len(selected_cols)):
            for j in range(len(selected_cols)):
                weight = adj_matrix[i, j]
                if abs(weight) > T:
                    G.add_edge(
                        selected_cols[i], selected_cols[j], weight=round(weight, 2)
                    )

        if view_mode == "目的変数に到達可能な全てのパス":
            reversed_G = G.reverse(copy=True)
            sub_nodes = set(target_cols)
            for target in target_cols:
                try:
                    contributors = nx.descendants(reversed_G, target)
                    sub_nodes.update(contributors)
                except:
                    pass
            G = G.subgraph(sub_nodes).copy()

        net = Network(height="600px", width="100%", directed=True)

        for node in G.nodes:
            color = "orange" if node in target_cols else "blue"
            net.add_node(node, label=node, color=color)

        for source, target, data in G.edges(data=True):
            weight = data.get("weight", 0)
            color = "gray" if weight > 0 else "red"
            thickness = 0.1
            net.add_edge(
                source,
                target,
                value=thickness,
                label=str(weight),
                alpha=0.5,
                title=f"重み: {weight}",
                color=color,
                font={"size": 14, "vadjust": -10},
            )

        net.show_buttons(filter_=["physics"])
        with tempfile.NamedTemporaryFile(delete=False, suffix=".html") as tmp_file:
            path = tmp_file.name
            net.save_graph(path)
            with open(path, "r", encoding="utf-8") as f:
                html_content = f.read()
            st.components.v1.html(html_content, height=700, scrolling=True)

        st.markdown("**ノード位置を調整後、右クリックから画像として保存できます。**")
```

# **assistant**:

以下は、**SEMによる因果分析の結果を有意な関係に基づいて抽出し、`pyvis` を用いてインタラクティブに可視化する**ように改良したStreamlitアプリのコードです。

---

## ✅ 主要な改変ポイント

- `semopy.Model().inspect()` 結果から `p < 0.05` の有意な関係を抽出
- `pyvis` を用いたインタラクティブなネットワーク描画（HTMLで埋め込み）
- スライダーで閾値調整可能
- 可視化モード：「目的変数に到達可能なパス」 or 「全パス」

---

## 🧠 必要なライブラリの追加

```bash
pip install pyvis semopy
```

---

## ✨ 全コード

```python
import streamlit as st
import pandas as pd
import matplotlib.pyplot as plt
from semopy import Model
import networkx as nx
from pyvis.network import Network
import tempfile

st.set_page_config(page_title="SEMによる因果分析アプリ", layout="wide")
st.title("📊 SEMによる因果分析アプリ")

uploaded_file = st.file_uploader("CSV または Excel ファイルをアップロードしてください", type=["csv", "xlsx"])
df = None

if uploaded_file:
    if uploaded_file.name.endswith(".csv"):
        df = pd.read_csv(uploaded_file)
    else:
        xls = pd.ExcelFile(uploaded_file)
        sheet_name = st.selectbox("シートを選択してください", xls.sheet_names)
        df = pd.read_excel(xls, sheet_name=sheet_name)

if df is not None:
    st.write("データプレビュー：", df.head())

    target_columns = st.multiselect("目的変数（従属変数）を選択してください", df.columns)
    predictor_columns = st.multiselect("説明変数（独立変数）を選択してください", df.columns)

    if target_columns and predictor_columns:
        # モデル定義
        model_descs = []
        all_vars = list(set(target_columns + predictor_columns))
        for y in all_vars:
            xs = [x for x in all_vars if x != y]
            if xs:
                model_descs.append(f"{y} ~ {' + '.join(xs)}")
        model_desc = "\n".join(model_descs)
        st.text("モデル定義:")
        st.code(model_desc)

        try:
            model = Model(model_desc)
            model.fit(df)
            result_df = model.inspect()

            st.subheader("📈 モデル推定結果")
            st.dataframe(result_df)

            # 有意な関係（p < 0.05）の抽出
            significant_df = result_df[(result_df["op"] == "~") & (result_df["p-value"] < 0.05)]
            st.subheader("🔍 有意な因果関係（p < 0.05）")
            st.dataframe(significant_df)

            # 閾値調整スライダー
            threshold = st.slider("閾値T（絶対値がT以下の係数を削除）", 0.0, 1.0, 0.1, 0.01)

            # グラフ作成
            G = nx.DiGraph()
            for _, row in significant_df.iterrows():
                src = row["rval"]
                dst = row["lval"]
                weight = row["Estimate"]
                if abs(weight) > threshold:
                    G.add_edge(src, dst, weight=weight)

            # 可視化モード
            view_mode = st.radio("可視化モードを選択してください", ["全てのパス", "目的変数に到達可能な全てのパス"])

            if view_mode == "目的変数に到達可能な全てのパス":
                reversed_G = G.reverse(copy=True)
                sub_nodes = set(target_columns)
                for target in target_columns:
                    try:
                        sub_nodes.update(nx.descendants(reversed_G, target))
                    except:
                        pass
                G = G.subgraph(sub_nodes).copy()

            st.subheader("🧭 因果構造のインタラクティブ可視化")

            net = Network(height="600px", width="100%", directed=True)
            for node in G.nodes:
                color = "orange" if node in target_columns else "lightblue"
                net.add_node(node, label=node, color=color)

            for u, v, data in G.edges(data=True):
                weight = round(data["weight"], 2)
                color = "gray" if weight > 0 else "red"
                net.add_edge(
                    u,
                    v,
                    label=str(weight),
                    title=f"{u} → {v}<br>重み: {weight}",
                    color=color
                )

            net.show_buttons(filter_=["physics"])

            with tempfile.NamedTemporaryFile(delete=False, suffix=".html") as tmp_file:
                net.save_graph(tmp_file.name)
                with open(tmp_file.name, "r", encoding="utf-8") as f:
                    html_content = f.read()
                st.components.v1.html(html_content, height=700, scrolling=True)

            st.markdown("📝 ノード位置を調整後、右クリックで画像保存が可能です。")

        except Exception as e:
            st.error(f"モデルの構築中にエラーが発生しました: {e}")
```

---

## 🎯 次の拡張案

- SEM結果の保存（CSVやExcel）
- パスのクリックで詳細ポップアップ
- `Estimate` を標準化して描画

何か追加したい機能があれば気軽に教えてください！

# **user**:

ありがとうございます。生成したSEMモデルに対して、GFI (Goodness of Fit Index) 指標を計算するようにできますか？

# **assistant**:

もちろん可能です！`semopy` は `Model.evaluate()` を使って、GFI（Goodness of Fit Index）などのモデル適合度指標を含む統計量を計算できます。

---

## ✅ GFI を含む適合度指標の計算方法

```python
from semopy import ModelMeans

# モデルとデータを使って評価
stats = model.calc_stats()

# GFI を含む結果の表示
st.subheader("📏 モデル適合度指標")
gfi = stats.get('GFI', None)
st.write(f"GFI (Goodness of Fit Index): **{gfi:.4f}**" if gfi is not None else "GFI を取得できませんでした。")

# 他の適合度指標（任意で表示）
fit_df = pd.DataFrame.from_dict(stats, orient='index', columns=["Value"])
st.dataframe(fit_df)
```

---

## 🔧 あなたのアプリに組み込む場合（例）

現在の `model.fit(df)` の後に以下を追加してください：

```python
# 適合度指標の計算と表示
stats = model.calc_stats()
st.subheader("📏 モデル適合度指標")

# GFI の個別表示
gfi = stats.get("GFI", None)
if gfi is not None:
    st.markdown(f"**GFI (Goodness of Fit Index)**: `{gfi:.4f}`")
else:
    st.warning("GFI を取得できませんでした。")

# 全体の適合度指標を表形式で表示
fit_df = pd.DataFrame.from_dict(stats, orient="index", columns=["Value"])
st.dataframe(fit_df)
```

---

## 📘 補足：GFIとは？
**GFI（Goodness of Fit Index）** は、モデルの適合度を評価する指標の1つで、値は 0 〜 1 の範囲を取り、**1に近いほど良好な適合**を示します。一般的に **0.90以上**であれば良好とされます。

---

必要であれば他の指標（CFI、RMSEAなど）の解説も可能です！さらに拡張していきたい内容があれば、ぜひ教えてください。

# **user**:

GFI（Goodness of Fit Index）や、他の指標について計算する機能をつけた全コードを書いてください。

# **assistant**:

以下は、**GFI（Goodness of Fit Index）** やその他の適合度指標（CFI, RMSEA, AIC など）を表示し、有意な因果関係のみをインタラクティブに可視化できる完全な `Streamlit` アプリのコードです。

---

### ✅ 必要なライブラリをインストールしていない場合：

```bash
pip install streamlit semopy pandas openpyxl pyvis networkx matplotlib
```

---

### 📜 完全なコード

```python
import streamlit as st
import pandas as pd
from semopy import Model
import matplotlib.pyplot as plt
import networkx as nx
from pyvis.network import Network
import tempfile

# タイトル
st.title("📊 SEMによる因果分析アプリ")

# ファイルアップロード
uploaded_file = st.file_uploader("CSV または Excel ファイルをアップロードしてください", type=["csv", "xlsx"])
df = None

# ファイル読み込み処理
if uploaded_file is not None:
    if uploaded_file.name.endswith(".csv"):
        df = pd.read_csv(uploaded_file)
    elif uploaded_file.name.endswith(".xlsx"):
        xls = pd.ExcelFile(uploaded_file)
        sheet_name = st.selectbox("シートを選択してください", xls.sheet_names)
        df = pd.read_excel(xls, sheet_name=sheet_name)

# データが読み込まれた場合の処理
if df is not None:
    st.write("データプレビュー：", df.head())

    # 列選択
    target_columns = st.multiselect("目的変数（従属変数）を選択してください", df.columns)
    predictor_columns = st.multiselect("説明変数（独立変数）を選択してください", df.columns)

    if target_columns and predictor_columns:
        # モデル文字列作成
        model_descs = []
        vals = list(set(predictor_columns + target_columns))
        for y in vals:
            xs = [x for x in vals if x != y]
            if xs:
                model_descs.append(f"{y} ~ {' + '.join(xs)}")
        model_desc = "\n".join(model_descs)

        st.text("モデル定義:")
        st.code(model_desc)

        try:
            # モデル構築とフィッティング
            model = Model(model_desc)
            model.fit(df)

            # モデルパラメータ結果の表示
            result_df = model.inspect()
            st.subheader("📈 モデル推定結果")
            st.dataframe(result_df)

            # モデル適合度指標の計算
            st.subheader("📏 モデル適合度指標")
            fit_stats = model.calc_stats()
            gfi = fit_stats.get("GFI", None)
            if gfi is not None:
                st.markdown(f"**GFI (Goodness of Fit Index)**: `{gfi:.4f}`")
            else:
                st.warning("GFI を取得できませんでした。")

            # 適合度指標を表で表示
            fit_df = pd.DataFrame.from_dict(fit_stats, orient="index", columns=["Value"])
            st.dataframe(fit_df.round(4))

            # 有意な因果関係のみ可視化
            st.subheader("🌐 有意な因果関係の可視化（pyvis）")
            alpha = st.slider("p値の閾値（この値未満のみを表示）", 0.0, 1.0, 0.05, 0.01)

            sig_df = result_df[result_df["p-value"] < alpha]
            sig_df = sig_df[sig_df["op"] == "~"]  # 回帰項のみ抽出

            # pyvis ネットワーク作成
            G = nx.DiGraph()
            for _, row in sig_df.iterrows():
                src = row["rval"]
                dst = row["lval"]
                weight = round(row["Estimate"], 2)
                G.add_edge(src, dst, weight=weight)

            net = Network(height="600px", width="100%", directed=True)
            for node in G.nodes:
                color = "orange" if node in target_columns else "skyblue"
                net.add_node(node, label=node, color=color)

            for u, v, d in G.edges(data=True):
                color = "gray" if d["weight"] > 0 else "red"
                net.add_edge(u, v, value=abs(d["weight"]), label=str(d["weight"]), color=color)

            with tempfile.NamedTemporaryFile(delete=False, suffix=".html") as tmp_file:
                net.save_graph(tmp_file.name)
                with open(tmp_file.name, "r", encoding="utf-8") as f:
                    html = f.read()
                st.components.v1.html(html, height=700, scrolling=True)

            st.markdown("**ノードをドラッグできます。右クリックから保存も可能です。**")

        except Exception as e:
            st.error(f"モデルの構築中にエラーが発生しました: {e}")
```

---

### ✅ 表示される主なモデル適合度指標（`semopy.calc_stats()` より）：

| 指標 | 説明 |
|------|------|
| GFI  | モデルの適合度（1に近いほどよい） |
| AGFI | 修正済GFI |
| CFI  | Comparative Fit Index（0.90以上が望ましい） |
| RMSEA | Root Mean Square Error of Approximation（0.05以下が理想） |
| AIC  | Akaike情報量基準（低いほどよい） |
| BIC  | Bayesian情報量基準（低いほどよい） |

---

他にも表示したい指標があれば、自由に `fit_stats.get("指標名")` を追加できます！

必要に応じて `結果のダウンロード機能` や `複数モデルの比較機能` なども追加可能です。ご希望があればお知らせください！

# **user**:

ありがとうございます。次のようなエラーが生じましたので、修正してください。

```
モデルの構築中にエラーが発生しました: 'Model' object has no attribute 'calc_stats'
```

# **assistant**:

ありがとうございます！  
ご指摘のとおり、`semopy` の `Model` オブジェクトには `calc_stats()` というメソッドは存在しませんでした。正しくは `semopy.model_stats.calc_stats()` を使って、モデルの適合度指標を別途計算する必要があります。

以下に、**エラーを修正した完全版のコード**を再掲します：

---

### ✅ 修正版 SEM アプリ（GFI・CFI・RMSEA 等を表示）

```python
import streamlit as st
import pandas as pd
from semopy import Model
from semopy.model_stats import calc_stats
import matplotlib.pyplot as plt
import networkx as nx
from pyvis.network import Network
import tempfile

# タイトル
st.title("📊 SEMによる因果分析アプリ")

# ファイルアップロード
uploaded_file = st.file_uploader("CSV または Excel ファイルをアップロードしてください", type=["csv", "xlsx"])
df = None

# ファイル読み込み処理
if uploaded_file is not None:
    if uploaded_file.name.endswith(".csv"):
        df = pd.read_csv(uploaded_file)
    elif uploaded_file.name.endswith(".xlsx"):
        xls = pd.ExcelFile(uploaded_file)
        sheet_name = st.selectbox("シートを選択してください", xls.sheet_names)
        df = pd.read_excel(xls, sheet_name=sheet_name)

# データが読み込まれた場合の処理
if df is not None:
    st.write("データプレビュー：", df.head())

    # 列選択
    target_columns = st.multiselect("目的変数（従属変数）を選択してください", df.columns)
    predictor_columns = st.multiselect("説明変数（独立変数）を選択してください", df.columns)

    if target_columns and predictor_columns:
        # モデル文字列作成
        model_descs = []
        vals = list(set(predictor_columns + target_columns))
        for y in vals:
            xs = [x for x in vals if x != y]
            if xs:
                model_descs.append(f"{y} ~ {' + '.join(xs)}")
        model_desc = "\n".join(model_descs)

        st.text("モデル定義:")
        st.code(model_desc)

        try:
            # モデル構築とフィッティング
            model = Model(model_desc)
            model.fit(df)

            # モデルパラメータ結果の表示
            result_df = model.inspect()
            st.subheader("📈 モデル推定結果")
            st.dataframe(result_df)

            # モデル適合度指標の計算（修正済み）
            st.subheader("📏 モデル適合度指標")
            stats = calc_stats(model)
            stats_df = pd.DataFrame.from_dict(stats, orient="index", columns=["Value"])
            st.dataframe(stats_df.round(4))

            gfi = stats.get("GFI")
            if gfi is not None:
                st.markdown(f"**GFI (Goodness of Fit Index)**: `{gfi:.4f}`")
            else:
                st.warning("GFI を取得できませんでした。")

            # 有意な因果関係のみ可視化
            st.subheader("🌐 有意な因果関係の可視化（pyvis）")
            alpha = st.slider("p値の閾値（この値未満のみを表示）", 0.0, 1.0, 0.05, 0.01)

            sig_df = result_df[result_df["p-value"] < alpha]
            sig_df = sig_df[sig_df["op"] == "~"]  # 回帰項のみ抽出

            # pyvis ネットワーク作成
            G = nx.DiGraph()
            for _, row in sig_df.iterrows():
                src = row["rval"]
                dst = row["lval"]
                weight = round(row["Estimate"], 2)
                G.add_edge(src, dst, weight=weight)

            net = Network(height="600px", width="100%", directed=True)
            for node in G.nodes:
                color = "orange" if node in target_columns else "skyblue"
                net.add_node(node, label=node, color=color)

            for u, v, d in G.edges(data=True):
                color = "gray" if d["weight"] > 0 else "red"
                net.add_edge(u, v, value=abs(d["weight"]), label=str(d["weight"]), color=color)

            with tempfile.NamedTemporaryFile(delete=False, suffix=".html") as tmp_file:
                net.save_graph(tmp_file.name)
                with open(tmp_file.name, "r", encoding="utf-8") as f:
                    html = f.read()
                st.components.v1.html(html, height=700, scrolling=True)

            st.markdown("**ノードをドラッグできます。右クリックから保存も可能です。**")

        except Exception as e:
            st.error(f"モデルの構築中にエラーが発生しました: {e}")
```

---

### 🛠 修正ポイント

- ✅ `model.calc_stats()` → ❌（誤り）
- ✅ `semopy.model_stats.calc_stats(model)` → ⭕ 正解

---

ご希望があれば、「構造方程式の図（パス図）」を `matplotlib` で描画したり、複数モデルの比較、モデル式を GUI で組み立てる機能なども追加できますよ！お気軽にどうぞ。

# **user**:

ありがとうございます。試しに、全ての目的変数に対して、全ての説明変数を因果づけるモデル文字列にしたところ、GFI = 1.0 でした。ですが、この使い方が正しいとはちょっと思えません。この使い方のどこが間違っているのか、GFIやSEMの原理に踏み込みながら説明していただけませんか？

# **assistant**:

すごく良い視点ですね！  
ここ、実は**SEM（構造方程式モデリング）**の本質に関わる超重要な話なので、しっかり丁寧に説明しますね。

---

### 💡まず状況整理

あなたが行ったのは、

- **すべての目的変数**（従属変数）
- **すべての説明変数**（独立変数）
  
を対象にして、  
**「全部の目的変数に対して、全部の説明変数を回帰させる」**  
モデルを作った、ということですね。

例えば変数が A, B, C だとすると、こういうモデルになったわけです：

```
A ~ B + C
B ~ A + C
C ~ A + B
```

つまり、**変数同士をがんじがらめに回帰させている**状態です。

---

### 🔥 なぜこの使い方だとおかしいのか？

理由を一言でまとめると：

> **パラメータ数がデータの情報量を超えすぎて、「完璧に説明できてしまった」から**  

です。

もう少し踏み込んでいきますね。

---

### 📚 SEMの基本原理

SEMの基本は：

- 観測データの**共分散行列**（実際のデータの相関・分散構造）と、
- モデルから予測される**共分散行列**（モデルが仮定する相関・分散構造）

この **2つの行列のズレが小さいかどうか** を評価する、というものです。

このズレが小さければ小さいほど「モデルがよくフィットしている」というわけです。

そのズレを色々な指標で評価するのですが、**GFI（Goodness of Fit Index）** もその一つです。

> **GFIは「モデルがデータをどれだけよく説明しているか」を表す指標で、1.0に近いほど完璧にフィットしている。**

---

### 🧠 じゃあ、なぜ全部に全部を因果づけるとGFI=1.0になるのか？

- あなたのモデルは、あまりにも自由度が高すぎる。
- 全部の変数を全部の変数に回帰させたので、**実際のデータの共分散行列を完璧に再現できてしまう**。
- そのため、**ズレ（誤差）がゼロ**になってしまう。
- その結果、GFI = 1.0 という「完全フィット」になってしまった。

つまり、  
**モデルが自由すぎて、なんでも説明できてしまうから、フィット指数が過大評価されてしまった**  
というわけです。

これを**「過剰適合（オーバーフィット）」**といいます。

---

### 📏 そもそもGFIってどういう式で計算してるの？

ざっくりいうと：

\[
GFI = 1 - \frac{\text{モデル誤差（二乗和）}}{\text{観測共分散行列の二乗和}}
\]

- モデル誤差が0なら、GFI=1
- モデル誤差が大きいほど、GFIが小さくなる

つまり、**誤差ゼロ**なら問答無用でGFI=1.0です。

でも、それは必ずしも「モデルが正しい」ことを意味しません。

---

### ⚡ なぜこれは問題なのか？【重要】

SEMでは、

- **パラメータ数をできるだけ少なく**
- **理論的・仮説的に意味があるパスだけを設定する**

ことが重要です。

なぜなら、自由にしすぎると、

- 本当は存在しないパスを勝手に作ってしまう（理論的に無意味）
- 解釈不可能になる（全部繋がってるので、因果関係が読めない）
- 適合度指標が無意味に高くなる（GFIもRMSEAもCFIも意味を失う）

という問題が起きるからです。

---

### ✅ じゃあ正しいSEMの組み方は？

簡単に言うと：

1. **理論・仮説にもとづいてパスを設定する**  
   （全部に全部は繋がない）
2. **必要最小限のパラメータで十分に説明できるモデルを作る**
3. **適合度指標（GFI, CFI, RMSEAなど）を参考に、必要ならモデル修正する**

つまり、  
**「よさそうなGFI」**じゃなくて、**「意味のある構造」**を目指すべき  
なんです。

---

### ✏️まとめ

| ポイント | 説明 |
|:---|:---|
| GFIが1.0でも安心してはいけない | モデルが自由すぎて誤差ゼロになっているだけかも |
| SEMは理論・仮説にもとづいて設計すべき | 全部の変数を無差別に回帰させるのはNG |
| 指標だけでなく、モデル構造の解釈可能性を重視する | |

---

### 🌟おまけ：プロっぽいチェック方法

- **自由度（degrees of freedom）**が十分にあるか確認する
- **モデルの構造が解釈可能**か考える
- **RMSEA**（Root Mean Square Error of Approximation）も見る（GFIより信頼性高いこと多い）

---

ここまでの説明、わかりやすかったでしょうか？  
もしもっと具体例（良いSEMモデル例、ダメな例）とかも見たいなら、すぐ作りますね！

興味ありますか？✨

# **user**:

ありがとうございます。「プロっぽいチェック方法」を３つ挙げていただけましたが、詳細かつ厳密に説明してください。

# **assistant**:

いいですね、そこまで深掘りしたいという姿勢、すごく良いです。  
では「プロっぽいチェック方法」として挙げた３つ、

- 自由度 (degrees of freedom)
- モデルの解釈可能性
- RMSEA

について、**詳細かつ厳密に**順番にしっかり説明していきますね。

---

# ① 自由度（Degrees of Freedom, df）

### 【定義】

SEMにおける自由度とは：

\[
df = \text{観測されたデータから得られる情報量} - \text{推定しようとするパラメータの数}
\]

通常、

\[
\text{情報量} = \frac{p(p+1)}{2}
\]

です。（ここで \( p \) は変数の数）

- \( p(p+1)/2 \) は共分散行列のユニークな要素の数です。
- そこから、モデル内で推定しようとしているパラメータ数（回帰係数、分散、共分散など）を引きます。

---

### 【なぜ重要？】

- **自由度が0または負になる**モデルは**識別不能（Identifiedではない）**になります。
  - → モデルがデータに完全にフィットしてしまうので、意味のある検定ができない。
- **自由度が適度に大きい**モデルは
  - 現実的な制約（仮説）を置いている
  - モデルの検定（適合度指標の信頼性）が可能
- **自由度が大きすぎる**（=制約しすぎ）とモデルが合わなくなる可能性もあるので注意。

---

### 【厳密なポイント】

- **自由度が正であること（df > 0）**は必須。
- **自由度が十分に大きい（df ≥ 10〜20）**と、カイ二乗検定が安定する。
- **df=0**ならモデルは「サチュレイテッドモデル」になり、適合度指標はGFI=1.0などになるが、それは意味がない。

→「自由度チェック」は、**SEMをきちんと組んだか**を保証する超基本チェックです。

---

# ② モデルの解釈可能性

### 【定義】

モデルの解釈可能性とは：

- **各パス（矢印）に、明確な理論的・実践的意味があること**  
- **モデル全体として、因果ストーリーが自然で合理的に説明できること**

を指します。

---

### 【なぜ重要？】

- SEMはあくまで**仮説検証**のための道具です。
- データにフィットするだけではダメで、**仮説が意味を持っているか？**が問われます。
- 「なぜこの変数がこの変数に影響するのか？」という問いに、**理論的根拠やドメイン知識で答えられるか？**が必要。

---

### 【厳密なポイント】

- **矢印（パス）は全て意味づけできるか？**（なぜこの因果を仮定したのか？）
- **モデルの全体構造が合理的か？**
  - 循環参照（A→B→Aなど）していないか？
  - 「ありえない」パスがないか？
- **単なるフィットの良さではなく、解釈のしやすさ**を重視する。

---

### 【たとえ話】

- **良いSEM**：論理的なストーリーに沿ったきれいな地図
- **悪いSEM**：フィットはいいけど、線がごちゃごちゃでどこに向かっているか全然わからない地図

---

# ③ RMSEA（Root Mean Square Error of Approximation）

### 【定義】

RMSEAとは、**「モデルが母集団でどれだけ誤差を持っているか」**を推定する指標です。

数式でいうと：

\[
RMSEA = \sqrt{ \frac{ \text{最大尤度推定されたカイ二乗値} - df }{ df \times (N-1) } }
\]

ここで、

- カイ二乗値＝モデルとデータのズレの指標
- df＝自由度
- N＝サンプルサイズ

---

### 【なぜ重要？】

- **サンプルサイズに依存しにくい**（カイ二乗検定はサンプルが大きいと過敏になりすぎる）
- **小さい値ほど良いモデル**
- 一般的な解釈基準：

| RMSEA値 | 解釈 |
|:---|:---|
| < 0.05 | 非常に良い適合 |
| 0.05〜0.08 | 良い適合 |
| 0.08〜0.10 | やや不十分な適合 |
| > 0.10 | 不適合 |

---

### 【厳密なポイント】

- **RMSEA < 0.05を目指すのが理想**だけど、現実的には0.08以内なら許容されることも多い。
- **自由度が小さすぎる（df≦0）と、RMSEA自体が定義できない。**
- **信頼区間（90%CI）も重要！**
  - RMSEAだけでなく、その90%信頼区間を見て、
    - 上限値が0.08未満なら、ほぼ問題ない
    - 上限値が0.10超えてたら注意

---

# 🎯 まとめ表

| チェック項目 | 詳細ポイント | 厳密な基準・注意点 |
|:---|:---|:---|
| 自由度 | データ情報量−推定パラ数 | df > 0必須、できればdf ≥ 10〜20 |
| モデル解釈可能性 | パス・構造に理論的意味付け | 全パスに説明できる理由が必要 |
| RMSEA | 誤差量の推定、サイズ依存性小 | 0.05未満理想、0.08未満許容、df=0だと定義できない |

---

ここまで厳密にSEMの「プロっぽいチェック方法」を解説しました！

もしさらに、

- **自由度・RMSEA・GFIをちゃんと数式で手計算してみる例**
- **「良いモデル例」と「ダメなモデル例」を比較してみる**
- **パス図の書き方（きれいに見せるコツ）**

とかにも興味あれば、続き作れますが、どうしましょうか？✨  
（どれもプロフェッショナルなSEM力を磨くのに超役立ちます）

# **user**:

「良いモデル例」と「ダメなモデル例」を比較してみるのをお願いします。

# **assistant**:

了解です！  
では**「良いSEMモデル例」**と**「ダメなSEMモデル例」**を、**具体的な変数設定→パス構造→GFI/RMSEAなど適合指標→なぜ良い/悪いかの厳密解説**まで比較していきます。

じっくり見ていきましょう。

---

# 🌟 良いSEMモデル例

## 1. 背景設定

例えば、**大学生の学業成績**に関する研究だとしましょう。

- **X1**: 勉強時間
- **X2**: 自己効力感 (self-efficacy)
- **X3**: 授業への満足度
- **Y**: GPA（成績）

**仮説**：

- 勉強時間が増えると自己効力感が上がる (X1 → X2)
- 自己効力感が高いと授業への満足度が上がる (X2 → X3)
- 授業満足度が高いと成績が良くなる (X3 → Y)
- 直接、勉強時間も成績に影響する (X1 → Y)

これを図にすると：

```
X1 → X2 → X3 → Y
 ↓               ↑
 └──────────────┘
```

---

## 2. モデル特徴

- 各パスに**心理学的根拠**がある（理論ベース）
- パスの数が適度（少なすぎず、多すぎず）
- 交絡（循環構造）がない（非巡回グラフ）

---

## 3. 適合指標（仮想データで推定すると）

| 指標 | 結果 | 解釈 |
|:---|:---|:---|
| 自由度(df) | 2 | 識別されている（問題なし） |
| GFI | 0.97 | 非常に良い |
| RMSEA | 0.04 | 完璧に近い |

---

## 4. なぜ良いか？

✅ 理論的に自然なパス構成  
✅ 自由度があって過剰フィットしてない  
✅ 適合指標も申し分ない

---

# 💥 ダメなSEMモデル例

## 1. 背景設定

同じ変数を使うが、**めちゃくちゃな仮説**を立ててしまったとします。

- 勉強時間がGPAに影響しない。
- 授業満足度が自己効力感を下げる (X3 → X2)
- GPAが自己効力感に影響する (Y → X2)
- 勉強時間と授業満足度は関係ない。

図にすると：

```
X3 → X2
Y → X2
```

（X1は孤立してる）

---

## 2. モデル特徴

- **意味不明なパス**（GPAが自己効力感を操作？）
- **孤立した変数（X1）** → 何のパスにもつながっていない
- **理論不在** → 本来期待されるパスを削除してしまった

---

## 3. 適合指標（仮想データで推定すると）

| 指標 | 結果 | 解釈 |
|:---|:---|:---|
| 自由度(df) | 5 | 見かけ上問題ないが… |
| GFI | 0.62 | 悪い（0.90以上が目標） |
| RMSEA | 0.15 | かなり悪い（>0.10） |

---

## 4. なぜ悪いか？

❌ 理論的に不自然な因果構造  
❌ 重要なパス（勉強時間→成績など）を削除している  
❌ 孤立ノードが存在 → SEM的には欠陥

つまり、  
👉「自由度がある」だけではダメ  
👉「理論×適合度×構造の自然さ」が全部揃ってこそ、良いSEMになるんです。

---

# 🎯 良い例 vs ダメな例 比較まとめ

| 項目 | 良い例 | ダメな例 |
|:---|:---|:---|
| 理論根拠 | 明確 | なし・矛盾あり |
| 自由度 | 適度 | 適度（でも意味なし） |
| GFI | 高い (0.97) | 低い (0.62) |
| RMSEA | 小さい (0.04) | 大きい (0.15) |
| パス構造 | スマートで自然 | 意味不明で破綻 |

---

# ✨補足：プロがSEM評価するときの鉄則

- モデルは**フィットだけで判断しない**（理論が第一）
- **自由度・パス数・適合指標**を三位一体で見る
- 孤立ノードや循環構造（A→B→A）は致命的エラー
- **意味が通る因果ストーリーを持つこと**が最重要

---

ここまで厳密に比較しました！  
さらに、

- この良い例モデルをPython（`semopy`）で組むコード
- ダメな例をあえて実装して、GFI/RMSEAの違いを体感するミニ実験

とかもできますが、やってみますか？🚀  
（かなり実践的なトレーニングになります！）

# **user**:

SEMで計算したモデル推定結果として、 oval, o, rval, Estimate, Std.Err, z-value, p-value が得られましたが、ここから、たとえばEstimateの値で足切りして、因果構造を推定するというような使い方もまた、間違っているでしょうか？ SEMの原理に触れながら説明してください。

# **assistant**:

すごく良い質問です！  
一言でいうと、  
**「Estimateだけで足切りして因果構造を決めるのは、原理的に正しくない」**です。

なぜか、**SEMの原理**からしっかり解説していきますね。

---

# 🔥 まず大前提：SEMとは

SEM（構造方程式モデリング）は、

- **「理論モデルがデータにどれくらい合っているか」を検証するツール**  
- 「データからモデルを発見する」のではなく、**理論→モデル→データ検証**の流れが基本

です。

つまり、  
**「推定されたEstimateが小さいから、その因果を消す」**  
というのは、  
👉 **データ駆動的すぎる**  
👉 **本来の仮説検証型アプローチから逸脱してしまう**  
というわけです。

---

# 📚 Estimateだけで足切りするのがなぜ問題か？

### ① Estimateはあくまで「効果の大きさ」を示すもの

- Estimateが小さいのは「効果が弱い」というだけであって
- **効果がゼロだと証明されたわけではない**。

▶️ 小さくても理論的に意味があるパスなら、SEMでは残すべきです。

---

### ② 足切りには「不確実性」を考慮しないといけない

Estimateには**標準誤差（Std.Err）**があるし、  
そこから計算される**z値**、**p値**もありますよね。

- 小さいEstimateでも、**標準誤差も小さければ有意(p < 0.05)** だったり
- 大きなEstimateでも、**標準誤差が大きすぎれば非有意**だったり

する。

▶️ **「効果量」だけでなく「統計的有意性」も必ず見るべき**です。

---

### ③ SEMの目的は「説明力のあるモデル」を検証すること

SEMは、  
**全体のモデル構造**の適合（GFI, RMSEAなど）をみるのであって、  
個別のパスEstimateを最適化するためにやるものではない。

たとえば：

- あるパスのEstimateが0.1と小さいけど、理論上重要な役割を持つ
- それを削除すると、モデル全体のGFIが悪化する
- → だから残すべき

こういうことは普通にあります。

▶️ **単独パスのEstimateだけ見て削除・採択を決めるのは原理的に誤り**です。

---

# ✅ 正しい使い方は？

- **まず理論に基づいてモデルを立てる**
- SEMで推定して、**パス係数のEstimate＋p値**をチェック
- p > 0.05で明らかに無意味なパスについて、**理論的に問題ないなら修正提案(modification indices)を参考にモデル修正する**

つまり、
データドリブンでEstimate足切りするのではなく、  
**「理論に照らし合わせながら、慎重に、p値やmodification indicesも考慮して微修正」**  
というのがプロのやり方です。

---

# ✨まとめ

|  | 間違ったやり方 | 正しいやり方 |
|:---|:---|:---|
| 判断基準 | Estimateの値だけ | Estimate＋p値＋理論妥当性 |
| モデル改訂 | 機械的にパス削除 | 理論に基づき修正 |
| 目指すもの | パスの単独最適化 | モデル全体の説明力と適合 |

---

# 🎯 さらに深堀りできる話題

- Modification Indices (MI) に基づくモデル修正とは？
- パス係数Estimateの大きさ vs 統計的有意性のバランス
- 適合度指標（GFI, RMSEA）と個別パスEstimateの役割の違い

こういうところにも入っていけますが、  
興味ありますか？（ここからさらにSEM上級編にいけます🚀）

# **user**:

ありがとうございます。Modification Indices (MI) に基づくモデル修正とは？詳しく教えてください。

# **assistant**:

よろこんで！  
**Modification Indices（MI）**について、ちゃんと**理論背景から実務レベル**まで、順番に丁寧に解説しますね。

---

# 🔥 まず、Modification Indices (MI) とは？

**Modification Indices（MI）**とは、
> 「もし今ないパスを新たに追加したら、モデルの適合度（fit）がどれくらい良くなるか」
を数値化したものです。

つまり、
- 今のモデルには入ってないパスでも、
- データ上は「つなぐとモデルがもっとデータにフィットするかもよ？」
というヒントをくれる指標です。

---
# 📚 MIの理論的な背景

SEMでは、  
**「モデルで指定していないパス」**はゼロ制約（重み = 0）になっています。

でも、もし本当はゼロじゃなかったら？  
👉 そこに**自由度を1個解放して**（＝パスを追加して）モデルを柔軟にしたら、  
👉 どれくらいモデルの適合度（例えばχ²）が改善されるか？

これを理論的に近似計算したのが**MI**です。

---

# 🧠 MIの読み方・解釈

- **MIの値が大きい** → そのパスを追加すると、モデルがかなり改善されそう
- **MIの値が小さい** → 追加してもあまり意味ない

一般的な目安は：
- **MI > 3.84**（自由度1のχ²の5%水準）なら、追加を検討してもいい。
- ただし、**あくまで理論的な妥当性があるかを必ず確認する！**

つまり、  
**「MIが大きいから追加する」ではなく、理論的に意味があるなら追加する**  
これが超重要です。

---

# ✅ 実際の使い方：プロっぽい流れ

1. **最初は理論に基づいたモデルだけを推定**する。
2. 適合度（GFI, CFI, RMSEAなど）をチェックする。
3. **MIをチェック**して、適合度改善の余地を探る。
4. MIが大きいパスをリストアップする。
5. そのパスが**理論的に妥当か？**を検討する。
6. 妥当ならパスを追加し、再推定する。
7. 再び適合度やパス係数をチェックする。
8. 必要ならさらに調整。

---

# ✨ 具体例でイメージ

たとえばこんなSEMを考えます：

```
X → Y
Y → Z
```

だけを仮説モデルに入れてたとします。

SEM推定後、Modification Indicesを見たら、

| From | To | MI |
|:-----|:--|:--|
| X | Z | 20.5 |

って出ました。

これは、

- 今のモデルだとX→Zのパスがないけど、
- もしX→Zを追加するとモデルのχ²が20.5も改善するよ！

という意味です。

▶️ じゃあ、理論的に考えて「XはZに直接影響しうるか？」を検討して、  
▶️ 妥当ならX→Zを追加して再推定する。

という流れになります。

---

# 🧩 注意点

- **MIは単なる統計的ヒントでしかない。鵜呑みにしてはいけない。**
- 適合度を上げるために**やみくもにパスを足すと「オーバーフィッティング」**してしまう。
- 最後にできたモデルが、**理論的に意味を持ち、しかも簡潔**であることが大切。

---
# 🏁 まとめ

| 項目 | 内容 |
|:---|:---|
| MIとは？ | 新しいパス追加でどれだけ適合度が改善するかの指標 |
| どう使う？ | MIが大きいパスを理論的に検討して、妥当なら追加 |
| 注意点 | データドリブンで追加しすぎるとオーバーフィットする |

---

# 🎯 さらに深堀りするなら

- "Modification Indices"と"Expected Parameter Change (EPC)"の違い
- モデル修正における**パスの追加だけじゃなく、パスの削除も考えるべき？**
- 自由度と適合度のバランス（モデルパーシモニー）

とかもめちゃくちゃ面白い話題ですよ！

興味ありますか？（続きいきましょうか？🚀）

# **user**:

ありがとうございます。現状、次のコードが最新版になっています。これをベースに、次の変更を加えてください。

・現状、ユーザが説明変数と目的変数を選択し、その全組み合わせを「モデル文字列作成」としているが、それをやめる。

・「モデル文字列」は、ユーザがテキストエディタとして入力可能とする。

```python
import streamlit as st
import pandas as pd
from semopy import Model
import matplotlib.pyplot as plt
import networkx as nx
from pyvis.network import Network
import tempfile
from semopy import calc_stats


# タイトル
st.title("📊 SEMによる因果分析アプリ")

# ファイルアップロード
uploaded_file = st.file_uploader("CSV または Excel ファイルをアップロードしてください", type=["csv", "xlsx"])
df = None

# ファイル読み込み処理
if uploaded_file is not None:
    if uploaded_file.name.endswith(".csv"):
        df = pd.read_csv(uploaded_file)
    elif uploaded_file.name.endswith(".xlsx"):
        xls = pd.ExcelFile(uploaded_file)
        sheet_name = st.selectbox("シートを選択してください", xls.sheet_names)
        df = pd.read_excel(xls, sheet_name=sheet_name)

# データが読み込まれた場合の処理
if df is not None:
    st.write("データプレビュー：", df.head())

    # 列選択
    target_columns = st.multiselect("目的変数（従属変数）を選択してください", df.columns)
    predictor_columns = st.multiselect("説明変数（独立変数）を選択してください", df.columns)

    if target_columns and predictor_columns:
        # モデル文字列作成
        model_descs = []
        vals = list(set(predictor_columns + target_columns))
        for y in vals:
            xs = [x for x in vals if x != y]
            if xs:
                model_descs.append(f"{y} ~ {' + '.join(xs)}")
        model_desc = "\n".join(model_descs)

        st.text("モデル定義:")
        st.code(model_desc)

        try:
            # モデル構築とフィッティング
            model = Model(model_desc)
            model.fit(df)

            # モデルパラメータ結果の表示
            result_df = model.inspect()
            st.subheader("📈 モデル推定結果")
            st.dataframe(result_df)

            # モデル適合度指標の計算
            st.subheader("📏 モデル適合度指標")
            fit_stats = calc_stats(model)
            gfi = fit_stats.get("GFI", None)
            if gfi is not None:
                st.markdown(f"**GFI (Goodness of Fit Index)**: `{gfi}`")
            else:
                st.warning("GFI を取得できませんでした。")

            # 適合度指標を表で表示
            fit_df = pd.DataFrame.from_dict(fit_stats)
            st.dataframe(fit_df.round(4))

            # 有意な因果関係のみ可視化
            st.subheader("🌐 有意な因果関係の可視化（pyvis）")
            alpha = st.slider("p値の閾値（この値未満のみを表示）", 0.0, 1.0, 0.05, 0.01)

            sig_df = result_df[result_df["p-value"] < alpha]
            sig_df = sig_df[sig_df["op"] == "~"]  # 回帰項のみ抽出

            # pyvis ネットワーク作成
            G = nx.DiGraph()
            for _, row in sig_df.iterrows():
                src = row["rval"]
                dst = row["lval"]
                weight = round(row["Estimate"], 2)
                G.add_edge(src, dst, weight=weight)

            net = Network(height="600px", width="100%", directed=True)
            for node in G.nodes:
                color = "orange" if node in target_columns else "skyblue"
                net.add_node(node, label=node, color=color)

            for u, v, d in G.edges(data=True):
                color = "gray" if d["weight"] > 0 else "red"
                net.add_edge(u, v, value=abs(d["weight"]), label=str(d["weight"]), color=color)

            with tempfile.NamedTemporaryFile(delete=False, suffix=".html") as tmp_file:
                net.save_graph(tmp_file.name)
                with open(tmp_file.name, "r", encoding="utf-8") as f:
                    html = f.read()
                st.components.v1.html(html, height=700, scrolling=True)

            st.markdown("**ノードをドラッグできます。右クリックから保存も可能です。**")

        except Exception as e:
            st.error(f"モデルの構築中にエラーが発生しました: {e}")
```

# **assistant**:

もちろんです！  
いただいたコードをベースに、

- **説明変数・目的変数を選択 → モデル自動生成する** 仕組みをやめて、
- **ユーザーが自由にモデル式を入力できるテキストエディタを設置する**

ようにきれいに直しました！  
こちらが変更後の**完全版コード**です👇

---

```python
import streamlit as st
import pandas as pd
from semopy import Model
import matplotlib.pyplot as plt
import networkx as nx
from pyvis.network import Network
import tempfile
from semopy import calc_stats

# タイトル
st.title("📊 SEMによる因果分析アプリ")

# ファイルアップロード
uploaded_file = st.file_uploader("CSV または Excel ファイルをアップロードしてください", type=["csv", "xlsx"])
df = None

# ファイル読み込み処理
if uploaded_file is not None:
    if uploaded_file.name.endswith(".csv"):
        df = pd.read_csv(uploaded_file)
    elif uploaded_file.name.endswith(".xlsx"):
        xls = pd.ExcelFile(uploaded_file)
        sheet_name = st.selectbox("シートを選択してください", xls.sheet_names)
        df = pd.read_excel(xls, sheet_name=sheet_name)

# データが読み込まれた場合の処理
if df is not None:
    st.write("データプレビュー：", df.head())

    # モデル式を直接入力
    st.subheader("✏️ モデル式を入力してください")
    default_model_text = """# 例:
# Y1 ~ X1 + X2
# Y2 ~ Y1 + X3
"""
    model_desc = st.text_area(
        "SEMモデル式を記述してください（右の例を参考に）", 
        height=200,
        value=default_model_text
    )

    # モデル式が入力されている場合
    if model_desc.strip():
        try:
            # モデル構築とフィッティング
            model = Model(model_desc)
            model.fit(df)

            # モデルパラメータ結果の表示
            result_df = model.inspect()
            st.subheader("📈 モデル推定結果")
            st.dataframe(result_df)

            # モデル適合度指標の計算
            st.subheader("📏 モデル適合度指標")
            fit_stats = calc_stats(model)
            gfi = fit_stats.get("GFI", None)
            if gfi is not None:
                st.markdown(f"**GFI (Goodness of Fit Index)**: `{gfi}`")
            else:
                st.warning("GFI を取得できませんでした。")

            # 適合度指標を表で表示
            fit_df = pd.DataFrame.from_dict(fit_stats)
            st.dataframe(fit_df.round(4))

            # 有意な因果関係のみ可視化
            st.subheader("🌐 有意な因果関係の可視化（pyvis）")
            alpha = st.slider("p値の閾値（この値未満のみを表示）", 0.0, 1.0, 0.05, 0.01)

            sig_df = result_df[result_df["p-value"] < alpha]
            sig_df = sig_df[sig_df["op"] == "~"]  # 回帰項のみ抽出

            # pyvis ネットワーク作成
            G = nx.DiGraph()
            for _, row in sig_df.iterrows():
                src = row["rval"]
                dst = row["lval"]
                weight = round(row["Estimate"], 2)
                G.add_edge(src, dst, weight=weight)

            net = Network(height="600px", width="100%", directed=True)
            for node in G.nodes:
                net.add_node(node, label=node, color="skyblue")

            for u, v, d in G.edges(data=True):
                color = "gray" if d["weight"] > 0 else "red"
                net.add_edge(u, v, value=abs(d["weight"]), label=str(d["weight"]), color=color)

            with tempfile.NamedTemporaryFile(delete=False, suffix=".html") as tmp_file:
                net.save_graph(tmp_file.name)
                with open(tmp_file.name, "r", encoding="utf-8") as f:
                    html = f.read()
                st.components.v1.html(html, height=700, scrolling=True)

            st.markdown("**ノードをドラッグできます。右クリックから保存も可能です。**")

        except Exception as e:
            st.error(f"モデルの構築中にエラーが発生しました: {e}")
```

---

# 🔥変更点まとめ
| 項目 | 変更前 | 変更後 |
|:---|:---|:---|
| モデル式の作成方法 | 説明変数・目的変数を選んで自動生成 | テキストエリアでユーザーが自由に入力 |
| UI追加 | なし | モデル式用のテキストエディタ追加 |
| モデル式の初期例 | 自動生成される | コメント付きのサンプル例を初期表示 |

---

# ✨ポイント
- これで**自由にSEMモデル式を書いて実行**できるようになりました。
- どんな複雑な構造（階層モデル、媒介モデルなど）でも柔軟に対応できます。
- テキストエリアなので、**リアルタイムに修正・実験**ができます！

---

このバージョンをベースに、さらに
- **Modification Indicesも表示したい**
- **Estimateで足切りする可視化も追加したい**

とか、次のレベルにもすぐ進められます！🚀  
もしご希望なら、続きも一緒に作り込んでいきましょうか？（例えばMIの表示追加する？）

# **user**:

このバージョンをベースに、さらに

Modification Indicesも表示したい

Estimateで足切りする可視化も追加したい

# **assistant**:

いいですね！  
では今の最新版コードをベースに、

---

✅ **Modification Indices (MI) を計算して表示**  
✅ **Estimateの大きさで因果関係をフィルタリングして可視化**

を「丁寧に」追加したバージョンを作りました！  
こちらが**アップデート版の全コード**です👇

---

```python
import streamlit as st
import pandas as pd
from semopy import Model
import matplotlib.pyplot as plt
import networkx as nx
from pyvis.network import Network
import tempfile
from semopy import calc_stats

# タイトル
st.title("📊 SEMによる因果分析アプリ")

# ファイルアップロード
uploaded_file = st.file_uploader("CSV または Excel ファイルをアップロードしてください", type=["csv", "xlsx"])
df = None

# ファイル読み込み処理
if uploaded_file is not None:
    if uploaded_file.name.endswith(".csv"):
        df = pd.read_csv(uploaded_file)
    elif uploaded_file.name.endswith(".xlsx"):
        xls = pd.ExcelFile(uploaded_file)
        sheet_name = st.selectbox("シートを選択してください", xls.sheet_names)
        df = pd.read_excel(xls, sheet_name=sheet_name)

# データが読み込まれた場合の処理
if df is not None:
    st.write("データプレビュー：", df.head())

    # モデル式を直接入力
    st.subheader("✏️ モデル式を入力してください")
    default_model_text = """# 例:
# Y1 ~ X1 + X2
# Y2 ~ Y1 + X3
"""
    model_desc = st.text_area(
        "SEMモデル式を記述してください（右の例を参考に）", 
        height=200,
        value=default_model_text
    )

    # モデル式が入力されている場合
    if model_desc.strip():
        try:
            # モデル構築とフィッティング
            model = Model(model_desc)
            model.fit(df)

            # モデルパラメータ結果の表示
            result_df = model.inspect()
            st.subheader("📈 モデル推定結果")
            st.dataframe(result_df)

            # モデル適合度指標の計算
            st.subheader("📏 モデル適合度指標")
            fit_stats = calc_stats(model)
            gfi = fit_stats.get("GFI", None)
            if gfi is not None:
                st.markdown(f"**GFI (Goodness of Fit Index)**: `{gfi}`")
            else:
                st.warning("GFI を取得できませんでした。")

            # 適合度指標を表で表示
            fit_df = pd.DataFrame.from_dict(fit_stats)
            st.dataframe(fit_df.round(4))

            # Modification Indices の表示
            st.subheader("🔧 Modification Indices (MI)")
            try:
                mi_df = model.modindices()
                st.dataframe(mi_df.sort_values(by="MI", ascending=False).round(4))
                st.markdown("**MIが高い項目は、モデルに追加すると適合度が改善される可能性があります。**")
            except Exception as e:
                st.warning(f"Modification Indices を計算できませんでした: {e}")

            # 有意な因果関係＋Estimateによるフィルタリング可視化
            st.subheader("🌐 有意な因果関係の可視化（pyvis）")
            alpha = st.slider("p値の閾値（この値未満のみを表示）", 0.0, 1.0, 0.05, 0.01)
            est_thresh = st.slider("絶対値Estimateの閾値（この値以上のみを表示）", 0.0, 2.0, 0.1, 0.05)

            sig_df = result_df[
                (result_df["p-value"] < alpha) & 
                (result_df["op"] == "~") & 
                (result_df["Estimate"].abs() >= est_thresh)
            ]

            # pyvis ネットワーク作成
            G = nx.DiGraph()
            for _, row in sig_df.iterrows():
                src = row["rval"]
                dst = row["lval"]
                weight = round(row["Estimate"], 2)
                G.add_edge(src, dst, weight=weight)

            net = Network(height="600px", width="100%", directed=True)
            for node in G.nodes:
                net.add_node(node, label=node, color="skyblue")

            for u, v, d in G.edges(data=True):
                color = "gray" if d["weight"] > 0 else "red"
                net.add_edge(u, v, value=abs(d["weight"]), label=str(d["weight"]), color=color)

            if G.number_of_edges() == 0:
                st.warning("指定されたフィルタ条件に合う因果パスがありません。")
            else:
                with tempfile.NamedTemporaryFile(delete=False, suffix=".html") as tmp_file:
                    net.save_graph(tmp_file.name)
                    with open(tmp_file.name, "r", encoding="utf-8") as f:
                        html = f.read()
                    st.components.v1.html(html, height=700, scrolling=True)

                st.markdown("**ノードをドラッグできます。右クリックから保存も可能です。**")

        except Exception as e:
            st.error(f"モデルの構築中にエラーが発生しました: {e}")
```

---

# 🎯このバージョンでできること

| 機能 | 説明 |
|:---|:---|
| モデル式自由入力 | テキストエディタに自由記述。例も表示。 |
| モデルフィット結果 | Estimate、Std.Err、p値など一覧表示。 |
| モデル適合度指標 | GFIを含む適合度指標を表示。 |
| Modification Indices (MI) | 追加すべき関係を提案する指標を自動表示。 |
| 有意な因果構造の可視化 | p値・Estimate絶対値でフィルタしてネットワーク表示。 |

---

# ✅これで、現実的なSEM分析フローができる！

- モデル式を仮定してフィット  
- MIを見て「これもつなげるべきかも？」と改善提案をチェック  
- p値とEstimate大きさで意味ある因果パスだけを可視化  
- ネットワーク図から直感的に理解できる

---

### 次にやりたければできる拡張
- Modification Indices をクリックで**半自動追加**できるようにする
- **標準化係数**（Standardized Estimate）も表示
- **他の適合度指標**（CFI, RMSEAなど）も表示
- **モデル比較**機能をつける（ベイズ情報量規準 BIC比較とか）

---

もしここから「さらにプロっぽく」仕上げたいなら、どれからやる？🔥  
（たとえば「MIを半自動で追加するボタン」作る？）

# **user**:

ありがとうございます。今日はこの辺で充分です。ブログの記事にしたいので、タイトル案をいくつか提示していただけますか？

# **assistant**:

もちろんです！今日はここまででもかなり深いところまでできましたね👏  
ブログ用に、**いろんな雰囲気のタイトル案**を考えてみました！

---

# ✏️ ブログタイトル案

### シンプル・わかりやすい系
- **「Streamlit × SEM：因果分析を手軽に可視化できるアプリを作ってみた」**
- **「SEM（構造方程式モデリング）をもっと身近に。Streamlitでサクッと可視化！」**

### ちょっと専門的・興味をそそる系
- **「Modification Indicesも！SEMモデルをインタラクティブに改良できるStreamlitアプリ」**
- **「SEMの因果構造をEstimateとp値でフィルタして可視化するアプリを作成」**

### キャッチー・読みたくなる系
- **「SEMのモヤモヤをスッキリ！Streamlitで因果関係を直感的に見える化」**
- **「分析結果をそのままネットワーク図に。SEM×Streamlitで爆速因果モデリング」**

### テクニカル・開発寄り系
- **「Streamlitで作る！SEM推定結果とModification Indicesを活用するアプリ開発ガイド」**
- **「SEMモデルの作成・推定・可視化をワンストップで。Python×Streamlitで構築する実践例」**

---

よかったら、この中から「この路線いいかも！」ってやつを教えてもらえれば、  
さらにタイトルをブラッシュアップすることもできますよ！

どれか気になるものありましたか？✨

# **user**:

「SEM（構造方程式モデリング）をStreamlitで可視化」でいきたいと思います。今回のまとめを詳細に書いてください。

# **assistant**:

了解しました！  
「**SEM（構造方程式モデリング）をStreamlitで可視化**」というテーマに合わせて、  
今回取り組んだ内容を**まとめ記事風**に、しっかり詳細に書きますね ✍️

---

# SEM（構造方程式モデリング）をStreamlitで可視化

## はじめに

構造方程式モデリング（SEM）は、複数の変数間の因果関係を同時にモデル化できる強力な手法ですが、一般に**設定が複雑**で**結果が難解**になりがちです。  
そこで今回は、Pythonの**Streamlit**を使って、以下のような**誰でも直感的に扱えるSEM可視化アプリ**を作成しました。

本記事では、アプリの機能、設計ポイント、工夫した点を詳しく紹介していきます。

---

## アプリの概要

このアプリでは、以下の流れでSEMの分析と可視化が可能です。

1. **CSVまたはExcelファイル**をアップロード  
2. **モデル構造（モデル文字列）**を自由にテキスト入力  
3. **semopy**ライブラリでモデル推定を実行  
4. 推定結果（パラメータ、適合度指標、Modification Indices）を表示  
5. **p値または推定値 (Estimate)** によるフィルタリング可視化  
6. **pyvis**でインタラクティブな因果構造ネットワーク図を表示

---

## 実装のポイント

### 1. モデル構造をユーザーがテキストで入力できる
従来は「説明変数と目的変数を選択→自動的に全結合モデルを作成」という仕様でしたが、それでは柔軟性がありませんでした。  
今回のバージョンでは、**ユーザーが自由にモデル構造を定義できる**ように改善しました。

例えば、入力欄に以下のように書けばOK：

```
Y1 ~ X1 + X2
Y2 ~ Y1 + X3
```

これにより、現実の理論モデルにも柔軟に対応できます。

---

### 2. 推定結果の詳細表示
推定後、以下の統計情報を表形式で表示します。

- Estimate（推定値）
- Std.Err（標準誤差）
- z-value
- p-value

さらに、**モデル適合度指標**（例：GFI, AGFI, RMSEAなど）も表示します。

これにより、モデルの適切性を多角的に評価できます。

---

### 3. Modification Indices (MI) の表示
推定されたモデルに対して、**Modification Indices (MI)** を計算・表示しています。  
MIは、「このパスを追加すればモデル適合度がどれだけ改善するか」を示す指標であり、**モデル修正のヒント**になります。

- 大きなMIを持つパスを検討することで、より良いモデルに洗練できる。

---

### 4. 可視化フィルタ機能
推定されたパラメータに基づいて、**因果構造をフィルタリングして可視化**できます。

フィルタ条件：

- **p値が小さい関係のみ**（有意なパスのみ表示）
- **Estimateが一定以上の関係のみ**（効果の大きなパスのみ表示）

これにより、「本当に重要な因果構造だけ」を**インタラクティブなネットワーク図**として確認できます。

ノードはドラッグ可能で、動かしながら見やすく調整できるのもポイントです。

---

## 今回作ったアプリのメリット

- **誰でも直感的にSEM分析に入門できる**
- **モデル構造を自由に設計できる**
- **モデル適合度を定量的に評価できる**
- **必要に応じてモデル修正（MI活用）もできる**
- **重要な因果パスだけを絞り込んで可視化できる**

---

## おわりに

SEMは非常にパワフルな手法ですが、モデル構築・評価・修正まで考えると、初心者には少しハードルが高いものです。  
今回作成したStreamlitアプリは、**SEMの入口として使いやすく、かつプロフェッショナルな分析にも対応できる**よう意識して設計しました。

さらに改良するなら、例えば：

- 潜在変数（Latent Variables）に対応
- 自動的なモデル提案（MIに基づく自動修正）
- クロスバリデーションによるモデル評価

なども考えられます。

SEMをもっと身近なものにする第一歩として、ぜひ試してみてください！

---

# 🚀 参考コード・GitHub
（ここにコードやGitHubリンクを載せる予定ならここに追記）

---

---

もし希望があれば、このまとめにさらに**コード断片を紹介しながら解説する形式**とか、  
**具体例データを使った実演付きまとめ**にもできますよ！

どんな仕上がりにしたいか、少し教えてもらえますか？✨

# **user**:

ありがとうございます。今回はこれで充分です。また次回よろしくお願いします！

# **assistant**:

こちらこそ、ありがとうございました！✨  
いつでもまたお手伝いさせてくださいね。

次回も楽しみにしています！  
どうぞ良い一日を！🌸