Enhance app.py with type annotations, improved error handling, and detailed docstrings for functions. The Supabase client and data fetching methods have been updated for better clarity and functionality, ensuring compatibility with both v1 and v2 APIs.
Browse files
app.py
CHANGED
|
@@ -4,6 +4,8 @@
|
|
| 4 |
|
| 5 |
import io
|
| 6 |
import os
|
|
|
|
|
|
|
| 7 |
import pandas as pd
|
| 8 |
import numpy as np
|
| 9 |
import matplotlib.pyplot as plt
|
|
@@ -22,7 +24,7 @@ try:
|
|
| 22 |
except Exception:
|
| 23 |
# 旧API互換(v1 をお使いの場合は import supabase; supabase.create_client を利用)
|
| 24 |
create_client = None
|
| 25 |
-
import supabase as supabase_v1
|
| 26 |
|
| 27 |
plt.switch_backend("Agg") # サーバー実行向け
|
| 28 |
|
|
@@ -33,12 +35,25 @@ matplotlib.rcParams['font.family'] = ['DejaVu Sans', 'Hiragino Sans', 'Yu Gothic
|
|
| 33 |
|
| 34 |
# .env 読み込み
|
| 35 |
load_dotenv()
|
| 36 |
-
SUPABASE_URL = os.environ.get("SUPABASE_URL")
|
| 37 |
-
SUPABASE_KEY = os.environ.get("SUPABASE_KEY")
|
| 38 |
-
TABLE_NAME = "estimated_cause_mocdata" # ご指定のテーブル名
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
-
|
| 41 |
-
|
|
|
|
| 42 |
if not SUPABASE_URL or not SUPABASE_KEY:
|
| 43 |
raise RuntimeError("環境変数 SUPABASE_URL または SUPABASE_KEY が設定されていません。")
|
| 44 |
if create_client is not None:
|
|
@@ -46,16 +61,29 @@ def _get_supabase_client():
|
|
| 46 |
# v1 fallback
|
| 47 |
return supabase_v1.create_client(SUPABASE_URL, SUPABASE_KEY)
|
| 48 |
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
client = _get_supabase_client()
|
| 51 |
# v2 と v1 で返り値が異なるため分岐
|
| 52 |
try:
|
| 53 |
-
resp = client.table(TABLE_NAME).select("*").execute()
|
| 54 |
-
data = getattr(resp, "data", None) if hasattr(resp, "data") else None
|
| 55 |
if data is None:
|
| 56 |
-
# v1 の場合、resp が dict
|
| 57 |
if isinstance(resp, dict) and "data" in resp:
|
| 58 |
-
data = resp["data"]
|
| 59 |
if not data:
|
| 60 |
raise RuntimeError(f"Supabase テーブル '{TABLE_NAME}' からデータを取得できませんでした。")
|
| 61 |
df = pd.DataFrame(data)
|
|
@@ -63,9 +91,25 @@ def _fetch_supabase_df():
|
|
| 63 |
raise RuntimeError(f"Supabase テーブル '{TABLE_NAME}' にレコードがありません。")
|
| 64 |
return df
|
| 65 |
except Exception as e:
|
| 66 |
-
raise RuntimeError(f"Supabase 取得エラー: {e}")
|
|
|
|
| 67 |
|
| 68 |
-
def _boxplot_image(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
fig = plt.figure()
|
| 70 |
plt.boxplot([a, b], labels=["正常(0)", "悪化(1)"])
|
| 71 |
plt.title(f"Boxplot: {feature_name}")
|
|
@@ -77,28 +121,66 @@ def _boxplot_image(a, b, feature_name):
|
|
| 77 |
img = Image.open(buf) # PIL.Image.Image
|
| 78 |
return np.array(img) # Gallery は numpy 配列でもOK
|
| 79 |
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
# ---- データ取得 ----
|
| 82 |
try:
|
| 83 |
df = _fetch_supabase_df()
|
| 84 |
except Exception as e:
|
| 85 |
-
msg =
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
return (msg, None, None, None, None, [], None)
|
| 87 |
|
| 88 |
-
status_md = f"**テーブル:** `{TABLE_NAME}`\n\n**データ形状:** {df.shape[0]} 行 × {df.shape[1]} 列\n\n"
|
| 89 |
-
head_df = df.head()
|
| 90 |
|
| 91 |
# ---- 目的変数の作成(悪化=1, 正常=0)----
|
| 92 |
target_col = "CODcr(S)sin"
|
| 93 |
if target_col not in df.columns:
|
| 94 |
-
return (
|
|
|
|
|
|
|
|
|
|
| 95 |
|
| 96 |
df = df.copy()
|
| 97 |
# 数値化(もし文字列が混ざっていても NaN に落とす)
|
| 98 |
df[target_col] = pd.to_numeric(df[target_col], errors="coerce")
|
| 99 |
-
df["label"] = (df[target_col] > threshold).astype(int)
|
| 100 |
|
| 101 |
-
label_counts = df["label"].value_counts(dropna=False).rename_axis("label").to_frame("count")
|
| 102 |
status_md += (
|
| 103 |
f"**閾値:** {threshold}\n\n"
|
| 104 |
f"**目的変数の分布:**\n"
|
|
@@ -107,8 +189,8 @@ def analyze_from_supabase(threshold, top_k):
|
|
| 107 |
)
|
| 108 |
|
| 109 |
# ---- 説明変数の準備 ----
|
| 110 |
-
X = df.drop(columns=[target_col, "label"])
|
| 111 |
-
y = df["label"]
|
| 112 |
|
| 113 |
# 既知の小数表記ゆれ対策(あれば)
|
| 114 |
if "分散菌槽DO" in X.columns:
|
|
@@ -116,22 +198,22 @@ def analyze_from_supabase(threshold, top_k):
|
|
| 116 |
X["分散菌槽DO"] = pd.to_numeric(X["分散菌槽DO"], errors="coerce")
|
| 117 |
|
| 118 |
# ---- 相関 (point-biserial) ----
|
| 119 |
-
rows = []
|
| 120 |
for col in X.columns:
|
| 121 |
try:
|
| 122 |
col_num = pd.to_numeric(X[col], errors="coerce")
|
| 123 |
r, p = pointbiserialr(y, col_num)
|
| 124 |
-
rows.append((col, r, p))
|
| 125 |
except Exception:
|
| 126 |
-
rows.append((col,
|
| 127 |
-
corr_df = (
|
| 128 |
pd.DataFrame(rows, columns=["feature", "r_pb", "pval"])
|
| 129 |
.set_index("feature")
|
| 130 |
.sort_values(by="r_pb", key=lambda s: s.abs(), ascending=False)
|
| 131 |
)
|
| 132 |
|
| 133 |
# ---- t検定 ----
|
| 134 |
-
ttest_rows = []
|
| 135 |
for col in X.columns:
|
| 136 |
col_num = pd.to_numeric(X[col], errors="coerce")
|
| 137 |
a = col_num[y == 0].dropna()
|
|
@@ -142,22 +224,22 @@ def analyze_from_supabase(threshold, top_k):
|
|
| 142 |
ttest_rows.append(
|
| 143 |
{
|
| 144 |
"feature": col,
|
| 145 |
-
"mean_normal": a.mean(),
|
| 146 |
-
"mean_bad": b.mean(),
|
| 147 |
-
"pval": p,
|
| 148 |
-
"n_normal": len(a),
|
| 149 |
-
"n_bad": len(b),
|
| 150 |
}
|
| 151 |
)
|
| 152 |
except Exception:
|
| 153 |
pass
|
| 154 |
-
ttest_df = (
|
| 155 |
pd.DataFrame(ttest_rows).set_index("feature").sort_values(by="pval", ascending=True)
|
| 156 |
if ttest_rows else pd.DataFrame()
|
| 157 |
)
|
| 158 |
|
| 159 |
# ---- 箱ひげ図 (ギャラリー) ----
|
| 160 |
-
gallery_imgs = []
|
| 161 |
for col in X.columns:
|
| 162 |
col_num = pd.to_numeric(X[col], errors="coerce")
|
| 163 |
a_plot = col_num[y == 0].dropna()
|
|
@@ -171,10 +253,10 @@ def analyze_from_supabase(threshold, top_k):
|
|
| 171 |
X_num = X_num.loc[:, X_num.notna().sum() > 0] # すべてNaN列を落とす
|
| 172 |
|
| 173 |
if X_num.shape[1] == 0:
|
| 174 |
-
coef_df = pd.DataFrame(columns=["feature", "coef", "sign", "rank"]).set_index("feature")
|
| 175 |
status_md += "\n⚠️ 数値説明変数がありませんでした。係数は空です。"
|
| 176 |
else:
|
| 177 |
-
pipe = Pipeline(
|
| 178 |
steps=[
|
| 179 |
("imputer", SimpleImputer(strategy="median")),
|
| 180 |
("scaler", StandardScaler()),
|
|
@@ -199,7 +281,7 @@ def analyze_from_supabase(threshold, top_k):
|
|
| 199 |
.drop(columns=["abs_coef"])
|
| 200 |
)
|
| 201 |
coef_df["rank"] = np.arange(1, len(coef_df) + 1)
|
| 202 |
-
status_md += "\n\n**悪化原因の候補(上位{}項目)**:\n- ".format(top_k) + "\n- ".join(
|
| 203 |
[f"{f}: 係数={coef[f]:.3f} {('↑' if coef[f]>0 else '↓')}" for f in top_features]
|
| 204 |
)
|
| 205 |
except Exception as e:
|
|
@@ -210,6 +292,7 @@ def analyze_from_supabase(threshold, top_k):
|
|
| 210 |
|
| 211 |
return status_md, head_df, label_counts, corr_df, ttest_df, gallery_imgs, coef_df
|
| 212 |
|
|
|
|
| 213 |
# === Gradio UI ===
|
| 214 |
with gr.Blocks(title="水質データ 解析アプリ(Supabase版)") as demo:
|
| 215 |
gr.Markdown(
|
|
|
|
| 4 |
|
| 5 |
import io
|
| 6 |
import os
|
| 7 |
+
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
|
| 8 |
+
|
| 9 |
import pandas as pd
|
| 10 |
import numpy as np
|
| 11 |
import matplotlib.pyplot as plt
|
|
|
|
| 24 |
except Exception:
|
| 25 |
# 旧API互換(v1 をお使いの場合は import supabase; supabase.create_client を利用)
|
| 26 |
create_client = None
|
| 27 |
+
import supabase as supabase_v1 # type: ignore
|
| 28 |
|
| 29 |
plt.switch_backend("Agg") # サーバー実行向け
|
| 30 |
|
|
|
|
| 35 |
|
| 36 |
# .env 読み込み
|
| 37 |
load_dotenv()
|
| 38 |
+
SUPABASE_URL: Optional[str] = os.environ.get("SUPABASE_URL")
|
| 39 |
+
SUPABASE_KEY: Optional[str] = os.environ.get("SUPABASE_KEY")
|
| 40 |
+
TABLE_NAME: str = "estimated_cause_mocdata" # ご指定のテーブル名
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def _get_supabase_client() -> Any:
|
| 44 |
+
"""
|
| 45 |
+
Supabase クライアントを生成して返す(v2 を優先、なければ v1 にフォールバック)。
|
| 46 |
+
|
| 47 |
+
環境変数:
|
| 48 |
+
SUPABASE_URL (str): Supabase の URL
|
| 49 |
+
SUPABASE_KEY (str): Supabase の API キー
|
| 50 |
+
|
| 51 |
+
Raises:
|
| 52 |
+
RuntimeError: 必要な環境変数が未設定、またはクライアント生成に失敗した場合。
|
| 53 |
|
| 54 |
+
Returns:
|
| 55 |
+
Any: Supabase クライアントオブジェクト(v2 または v1)。
|
| 56 |
+
"""
|
| 57 |
if not SUPABASE_URL or not SUPABASE_KEY:
|
| 58 |
raise RuntimeError("環境変数 SUPABASE_URL または SUPABASE_KEY が設定されていません。")
|
| 59 |
if create_client is not None:
|
|
|
|
| 61 |
# v1 fallback
|
| 62 |
return supabase_v1.create_client(SUPABASE_URL, SUPABASE_KEY)
|
| 63 |
|
| 64 |
+
|
| 65 |
+
def _fetch_supabase_df() -> pd.DataFrame:
|
| 66 |
+
"""
|
| 67 |
+
Supabase の指定テーブルから全件取得し、pandas DataFrame に変換して返す。
|
| 68 |
+
|
| 69 |
+
テーブル:
|
| 70 |
+
TABLE_NAME: 既定では 'estimated_cause_mocdata'
|
| 71 |
+
|
| 72 |
+
Raises:
|
| 73 |
+
RuntimeError: 通信エラー、期待した形でデータが得られない、またはレコードが空の場合。
|
| 74 |
+
|
| 75 |
+
Returns:
|
| 76 |
+
pd.DataFrame: 取得したレコードの DataFrame。
|
| 77 |
+
"""
|
| 78 |
client = _get_supabase_client()
|
| 79 |
# v2 と v1 で返り値が異なるため分岐
|
| 80 |
try:
|
| 81 |
+
resp: Any = client.table(TABLE_NAME).select("*").execute()
|
| 82 |
+
data: Optional[List[Dict[str, Any]]] = getattr(resp, "data", None) if hasattr(resp, "data") else None
|
| 83 |
if data is None:
|
| 84 |
+
# v1 の場合、resp が dict のこともある
|
| 85 |
if isinstance(resp, dict) and "data" in resp:
|
| 86 |
+
data = resp["data"] # type: ignore[index]
|
| 87 |
if not data:
|
| 88 |
raise RuntimeError(f"Supabase テーブル '{TABLE_NAME}' からデータを取得できませんでした。")
|
| 89 |
df = pd.DataFrame(data)
|
|
|
|
| 91 |
raise RuntimeError(f"Supabase テーブル '{TABLE_NAME}' にレコードがありません。")
|
| 92 |
return df
|
| 93 |
except Exception as e:
|
| 94 |
+
raise RuntimeError(f"Supabase 取得エラー: {e}") from e
|
| 95 |
+
|
| 96 |
|
| 97 |
+
def _boxplot_image(
|
| 98 |
+
a: Union[pd.Series, Sequence[float], np.ndarray],
|
| 99 |
+
b: Union[pd.Series, Sequence[float], np.ndarray],
|
| 100 |
+
feature_name: str
|
| 101 |
+
) -> np.ndarray:
|
| 102 |
+
"""
|
| 103 |
+
2群(正常/悪化)の値から箱ひげ図を描画し、画像(ndarray)を返す。
|
| 104 |
+
|
| 105 |
+
Args:
|
| 106 |
+
a: 正常(0) 群の値(Series, list, ndarray など数値配列)
|
| 107 |
+
b: 悪化(1) 群の値(Series, list, ndarray など数値配列)
|
| 108 |
+
feature_name (str): グラフタイトルや y 軸ラベルに用いる特徴量名
|
| 109 |
+
|
| 110 |
+
Returns:
|
| 111 |
+
np.ndarray: 生成した箱ひげ図の画像配列(RGB)。
|
| 112 |
+
"""
|
| 113 |
fig = plt.figure()
|
| 114 |
plt.boxplot([a, b], labels=["正常(0)", "悪化(1)"])
|
| 115 |
plt.title(f"Boxplot: {feature_name}")
|
|
|
|
| 121 |
img = Image.open(buf) # PIL.Image.Image
|
| 122 |
return np.array(img) # Gallery は numpy 配列でもOK
|
| 123 |
|
| 124 |
+
|
| 125 |
+
# 解析結果の返却タプル型(status_md, head_df, label_counts, corr_df, ttest_df, gallery_imgs, coef_df)
|
| 126 |
+
AnalysisResult = Tuple[
|
| 127 |
+
str,
|
| 128 |
+
Optional[pd.DataFrame],
|
| 129 |
+
Optional[pd.DataFrame],
|
| 130 |
+
Optional[pd.DataFrame],
|
| 131 |
+
Optional[pd.DataFrame],
|
| 132 |
+
List[Tuple[np.ndarray, str]],
|
| 133 |
+
Optional[pd.DataFrame],
|
| 134 |
+
]
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def analyze_from_supabase(threshold: Union[int, float], top_k: Union[int, float]) -> AnalysisResult:
|
| 138 |
+
"""
|
| 139 |
+
Supabase から水質データを取得し、閾値によるラベリングを行った上で
|
| 140 |
+
相関(point-biserial)、t検定、箱ひげ図、ロジスティック回帰による特徴量重要度を算出する。
|
| 141 |
+
|
| 142 |
+
Args:
|
| 143 |
+
threshold (int | float): 目的変数列 'CODcr(S)sin' を悪化(1) と判定する閾値。
|
| 144 |
+
top_k (int | float): ロジスティック回帰の係数上位として表示する特徴量数。
|
| 145 |
+
|
| 146 |
+
Returns:
|
| 147 |
+
AnalysisResult: 以下の7要素タプル
|
| 148 |
+
- status_md (str): 解析の要約(マークダウン)
|
| 149 |
+
- head_df (pd.DataFrame | None): 先頭行のプレビュー
|
| 150 |
+
- label_counts (pd.DataFrame | None): 目的変数の分布
|
| 151 |
+
- corr_df (pd.DataFrame | None): point-biserial 相関表
|
| 152 |
+
- ttest_df (pd.DataFrame | None): t検定の結果表
|
| 153 |
+
- gallery_imgs (List[Tuple[np.ndarray, str]]): 箱ひげ図(画像配列, タイトル)のリスト
|
| 154 |
+
- coef_df (pd.DataFrame | None): ロジスティック回帰の係数ランキング
|
| 155 |
+
"""
|
| 156 |
# ---- データ取得 ----
|
| 157 |
try:
|
| 158 |
df = _fetch_supabase_df()
|
| 159 |
except Exception as e:
|
| 160 |
+
msg = (
|
| 161 |
+
f"❌ データ取得に失敗:{e}\n"
|
| 162 |
+
f"- .env に SUPABASE_URL / SUPABASE_KEY を設定してください\n"
|
| 163 |
+
f"- テーブル名: {TABLE_NAME}"
|
| 164 |
+
)
|
| 165 |
return (msg, None, None, None, None, [], None)
|
| 166 |
|
| 167 |
+
status_md: str = f"**テーブル:** `{TABLE_NAME}`\n\n**データ形状:** {df.shape[0]} 行 × {df.shape[1]} 列\n\n"
|
| 168 |
+
head_df: pd.DataFrame = df.head()
|
| 169 |
|
| 170 |
# ---- 目的変数の作成(悪化=1, 正常=0)----
|
| 171 |
target_col = "CODcr(S)sin"
|
| 172 |
if target_col not in df.columns:
|
| 173 |
+
return (
|
| 174 |
+
f"❌ 必須列 '{target_col}' が見つかりません。現在の列: {list(df.columns)}",
|
| 175 |
+
None, None, None, None, [], None
|
| 176 |
+
)
|
| 177 |
|
| 178 |
df = df.copy()
|
| 179 |
# 数値化(もし文字列が混ざっていても NaN に落とす)
|
| 180 |
df[target_col] = pd.to_numeric(df[target_col], errors="coerce")
|
| 181 |
+
df["label"] = (df[target_col] > float(threshold)).astype(int)
|
| 182 |
|
| 183 |
+
label_counts: pd.DataFrame = df["label"].value_counts(dropna=False).rename_axis("label").to_frame("count")
|
| 184 |
status_md += (
|
| 185 |
f"**閾値:** {threshold}\n\n"
|
| 186 |
f"**目的変数の分布:**\n"
|
|
|
|
| 189 |
)
|
| 190 |
|
| 191 |
# ---- 説明変数の準備 ----
|
| 192 |
+
X: pd.DataFrame = df.drop(columns=[target_col, "label"])
|
| 193 |
+
y: pd.Series = df["label"]
|
| 194 |
|
| 195 |
# 既知の小数表記ゆれ対策(あれば)
|
| 196 |
if "分散菌槽DO" in X.columns:
|
|
|
|
| 198 |
X["分散菌槽DO"] = pd.to_numeric(X["分散菌槽DO"], errors="coerce")
|
| 199 |
|
| 200 |
# ---- 相関 (point-biserial) ----
|
| 201 |
+
rows: List[Tuple[str, float, float]] = []
|
| 202 |
for col in X.columns:
|
| 203 |
try:
|
| 204 |
col_num = pd.to_numeric(X[col], errors="coerce")
|
| 205 |
r, p = pointbiserialr(y, col_num)
|
| 206 |
+
rows.append((col, float(r), float(p)))
|
| 207 |
except Exception:
|
| 208 |
+
rows.append((col, float("nan"), float("nan")))
|
| 209 |
+
corr_df: pd.DataFrame = (
|
| 210 |
pd.DataFrame(rows, columns=["feature", "r_pb", "pval"])
|
| 211 |
.set_index("feature")
|
| 212 |
.sort_values(by="r_pb", key=lambda s: s.abs(), ascending=False)
|
| 213 |
)
|
| 214 |
|
| 215 |
# ---- t検定 ----
|
| 216 |
+
ttest_rows: List[Dict[str, Union[str, float, int]]] = []
|
| 217 |
for col in X.columns:
|
| 218 |
col_num = pd.to_numeric(X[col], errors="coerce")
|
| 219 |
a = col_num[y == 0].dropna()
|
|
|
|
| 224 |
ttest_rows.append(
|
| 225 |
{
|
| 226 |
"feature": col,
|
| 227 |
+
"mean_normal": float(a.mean()),
|
| 228 |
+
"mean_bad": float(b.mean()),
|
| 229 |
+
"pval": float(p),
|
| 230 |
+
"n_normal": int(len(a)),
|
| 231 |
+
"n_bad": int(len(b)),
|
| 232 |
}
|
| 233 |
)
|
| 234 |
except Exception:
|
| 235 |
pass
|
| 236 |
+
ttest_df: pd.DataFrame = (
|
| 237 |
pd.DataFrame(ttest_rows).set_index("feature").sort_values(by="pval", ascending=True)
|
| 238 |
if ttest_rows else pd.DataFrame()
|
| 239 |
)
|
| 240 |
|
| 241 |
# ---- 箱ひげ図 (ギャラリー) ----
|
| 242 |
+
gallery_imgs: List[Tuple[np.ndarray, str]] = []
|
| 243 |
for col in X.columns:
|
| 244 |
col_num = pd.to_numeric(X[col], errors="coerce")
|
| 245 |
a_plot = col_num[y == 0].dropna()
|
|
|
|
| 253 |
X_num = X_num.loc[:, X_num.notna().sum() > 0] # すべてNaN列を落とす
|
| 254 |
|
| 255 |
if X_num.shape[1] == 0:
|
| 256 |
+
coef_df: pd.DataFrame = pd.DataFrame(columns=["feature", "coef", "sign", "rank"]).set_index("feature")
|
| 257 |
status_md += "\n⚠️ 数値説明変数がありませんでした。係数は空です。"
|
| 258 |
else:
|
| 259 |
+
pipe: Pipeline = Pipeline(
|
| 260 |
steps=[
|
| 261 |
("imputer", SimpleImputer(strategy="median")),
|
| 262 |
("scaler", StandardScaler()),
|
|
|
|
| 281 |
.drop(columns=["abs_coef"])
|
| 282 |
)
|
| 283 |
coef_df["rank"] = np.arange(1, len(coef_df) + 1)
|
| 284 |
+
status_md += "\n\n**悪化原因の候補(上位{}項目)**:\n- ".format(int(top_k)) + "\n- ".join(
|
| 285 |
[f"{f}: 係数={coef[f]:.3f} {('↑' if coef[f]>0 else '↓')}" for f in top_features]
|
| 286 |
)
|
| 287 |
except Exception as e:
|
|
|
|
| 292 |
|
| 293 |
return status_md, head_df, label_counts, corr_df, ttest_df, gallery_imgs, coef_df
|
| 294 |
|
| 295 |
+
|
| 296 |
# === Gradio UI ===
|
| 297 |
with gr.Blocks(title="水質データ 解析アプリ(Supabase版)") as demo:
|
| 298 |
gr.Markdown(
|