@nahisaho/satori 0.22.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,274 @@
1
+ ---
2
+ name: scientific-geospatial-analysis
3
+ description: |
4
+ 地理空間データ解析スキル。GeoPandas ベクターデータ処理・
5
+ Rasterio ラスター解析・Folium/Kepler.gl インタラクティブ地図・
6
+ 空間自己相関 (Moran's I)・クリギング補間・CRS 変換。
7
+ ---
8
+
9
+ # Scientific Geospatial Analysis
10
+
11
+ 地理空間データの前処理・空間統計・インタラクティブ地図可視化
12
+ パイプラインを提供する。
13
+
14
+ ## When to Use
15
+
16
+ - GeoPandas でベクターデータ (Shapefile/GeoJSON) を処理するとき
17
+ - ラスターデータ (GeoTIFF) を読み込み解析するとき
18
+ - 空間自己相関 (Moran's I / LISA) を検定するとき
19
+ - クリギング (Kriging) で空間補間するとき
20
+ - Folium/Kepler.gl でインタラクティブ地図を作成するとき
21
+ - CRS (座標参照系) 変換・空間結合をするとき
22
+
23
+ > **Note**: 環境特化 GIS (SoilGrids/WorldClim) は `scientific-environmental-geodata` を参照。
24
+
25
+ ---
26
+
27
+ ## Quick Start
28
+
29
+ ## 1. GeoPandas ベクターデータ処理
30
+
31
+ ```python
32
+ import numpy as np
33
+ import pandas as pd
34
+
35
+
36
+ def load_and_process_geodata(filepath, target_crs="EPSG:4326"):
37
+ """
38
+ GeoPandas ベクター/ポイントデータ読み込み・CRS 変換。
39
+
40
+ Parameters:
41
+ filepath: str — Shapefile / GeoJSON / GPKG パス
42
+ target_crs: str — 変換先座標系
43
+ """
44
+ import geopandas as gpd
45
+
46
+ gdf = gpd.read_file(filepath)
47
+ original_crs = gdf.crs
48
+
49
+ if gdf.crs != target_crs:
50
+ gdf = gdf.to_crs(target_crs)
51
+
52
+ # 基本統計
53
+ bounds = gdf.total_bounds # [minx, miny, maxx, maxy]
54
+ geom_types = gdf.geometry.geom_type.value_counts().to_dict()
55
+
56
+ print(f"GeoData: {len(gdf)} features, CRS: {original_crs} → {target_crs}")
57
+ print(f" Bounds: [{bounds[0]:.4f}, {bounds[1]:.4f}] "
58
+ f"to [{bounds[2]:.4f}, {bounds[3]:.4f}]")
59
+ print(f" Geometry types: {geom_types}")
60
+ return gdf
61
+
62
+
63
+ def spatial_join(gdf_left, gdf_right, how="inner", predicate="intersects"):
64
+ """
65
+ 空間結合 (Spatial Join)。
66
+
67
+ Parameters:
68
+ gdf_left: GeoDataFrame — 左テーブル
69
+ gdf_right: GeoDataFrame — 右テーブル
70
+ how: str — "inner" / "left" / "right"
71
+ predicate: str — "intersects" / "within" / "contains"
72
+ """
73
+ import geopandas as gpd
74
+
75
+ if gdf_left.crs != gdf_right.crs:
76
+ gdf_right = gdf_right.to_crs(gdf_left.crs)
77
+
78
+ joined = gpd.sjoin(gdf_left, gdf_right, how=how, predicate=predicate)
79
+
80
+ print(f"Spatial Join ({predicate}, {how}): "
81
+ f"{len(gdf_left)} × {len(gdf_right)} → {len(joined)}")
82
+ return joined
83
+ ```
84
+
85
+ ## 2. 空間自己相関 (Moran's I / LISA)
86
+
87
+ ```python
88
+ def spatial_autocorrelation(gdf, value_col, weight_type="queen"):
89
+ """
90
+ 空間自己相関検定 — Global Moran's I + LISA。
91
+
92
+ Parameters:
93
+ gdf: GeoDataFrame — ジオメトリ + 属性データ
94
+ value_col: str — 解析対象カラム
95
+ weight_type: str — "queen" / "rook" / "knn"
96
+ """
97
+ from libpysal.weights import Queen, Rook, KNN
98
+ from esda.moran import Moran, Moran_Local
99
+ import matplotlib.pyplot as plt
100
+
101
+ # 空間重み行列
102
+ if weight_type == "queen":
103
+ w = Queen.from_dataframe(gdf)
104
+ elif weight_type == "rook":
105
+ w = Rook.from_dataframe(gdf)
106
+ elif weight_type == "knn":
107
+ w = KNN.from_dataframe(gdf, k=5)
108
+
109
+ w.transform = "r"
110
+ y = gdf[value_col].values
111
+
112
+ # Global Moran's I
113
+ moran_global = Moran(y, w)
114
+
115
+ # LISA (Local Indicators of Spatial Association)
116
+ moran_local = Moran_Local(y, w)
117
+
118
+ gdf = gdf.copy()
119
+ gdf["lisa_cluster"] = moran_local.q # 1=HH, 2=LH, 3=LL, 4=HL
120
+ gdf["lisa_significant"] = moran_local.p_sim < 0.05
121
+ gdf["local_moran_i"] = moran_local.Is
122
+
123
+ # 可視化
124
+ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 7))
125
+
126
+ gdf.plot(column=value_col, ax=ax1, legend=True,
127
+ cmap="RdYlBu_r", edgecolor="gray", linewidth=0.3)
128
+ ax1.set_title(f"{value_col} (Moran's I={moran_global.I:.4f}, "
129
+ f"p={moran_global.p_sim:.4f})")
130
+
131
+ cluster_labels = {1: "High-High", 2: "Low-High",
132
+ 3: "Low-Low", 4: "High-Low", 0: "Not Significant"}
133
+ sig_gdf = gdf[gdf["lisa_significant"]]
134
+ if len(sig_gdf) > 0:
135
+ sig_gdf.plot(column="lisa_cluster", ax=ax2,
136
+ categorical=True, legend=True,
137
+ edgecolor="gray", linewidth=0.3)
138
+ ax2.set_title("LISA Clusters (p < 0.05)")
139
+
140
+ plt.tight_layout()
141
+ path = "spatial_autocorrelation.png"
142
+ plt.savefig(path, dpi=150, bbox_inches="tight")
143
+ plt.close()
144
+
145
+ print(f"Moran's I = {moran_global.I:.4f}, p = {moran_global.p_sim:.4f}")
146
+ print(f"LISA: {gdf['lisa_significant'].sum()} significant clusters")
147
+ return {"moran_i": moran_global.I, "p_value": moran_global.p_sim,
148
+ "gdf": gdf, "fig": path}
149
+ ```
150
+
151
+ ## 3. クリギング空間補間
152
+
153
+ ```python
154
+ def kriging_interpolation(points_df, x_col, y_col, value_col,
155
+ grid_resolution=100,
156
+ variogram_model="spherical"):
157
+ """
158
+ Ordinary Kriging 空間補間。
159
+
160
+ Parameters:
161
+ points_df: pd.DataFrame — 観測点データ
162
+ x_col, y_col: str — 座標カラム
163
+ value_col: str — 補間対象カラム
164
+ grid_resolution: int — グリッド解像度
165
+ variogram_model: str — "spherical" / "exponential" / "gaussian"
166
+ """
167
+ from pykrige.ok import OrdinaryKriging
168
+ import matplotlib.pyplot as plt
169
+
170
+ x = points_df[x_col].values
171
+ y = points_df[y_col].values
172
+ z = points_df[value_col].values
173
+
174
+ ok = OrdinaryKriging(
175
+ x, y, z,
176
+ variogram_model=variogram_model,
177
+ verbose=False, enable_plotting=False)
178
+
179
+ grid_x = np.linspace(x.min(), x.max(), grid_resolution)
180
+ grid_y = np.linspace(y.min(), y.max(), grid_resolution)
181
+ z_pred, ss_pred = ok.execute("grid", grid_x, grid_y)
182
+
183
+ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))
184
+
185
+ im1 = ax1.imshow(z_pred, origin="lower",
186
+ extent=[x.min(), x.max(), y.min(), y.max()],
187
+ cmap="viridis")
188
+ ax1.scatter(x, y, c="red", s=10, edgecolors="black", linewidths=0.5)
189
+ ax1.set_title(f"Kriging Prediction ({variogram_model})")
190
+ plt.colorbar(im1, ax=ax1)
191
+
192
+ im2 = ax2.imshow(ss_pred, origin="lower",
193
+ extent=[x.min(), x.max(), y.min(), y.max()],
194
+ cmap="Reds")
195
+ ax2.set_title("Kriging Variance (Uncertainty)")
196
+ plt.colorbar(im2, ax=ax2)
197
+
198
+ plt.tight_layout()
199
+ path = "kriging_result.png"
200
+ plt.savefig(path, dpi=150, bbox_inches="tight")
201
+ plt.close()
202
+
203
+ print(f"Kriging ({variogram_model}): {grid_resolution}×{grid_resolution} grid, "
204
+ f"{len(x)} observation points")
205
+ return {"z_pred": z_pred, "variance": ss_pred,
206
+ "grid_x": grid_x, "grid_y": grid_y, "fig": path}
207
+ ```
208
+
209
+ ## 4. Folium インタラクティブ地図
210
+
211
+ ```python
212
+ def interactive_map(gdf, value_col=None, popup_cols=None,
213
+ tiles="CartoDB positron",
214
+ output="interactive_map.html"):
215
+ """
216
+ Folium インタラクティブ地図。
217
+
218
+ Parameters:
219
+ gdf: GeoDataFrame — 地理空間データ
220
+ value_col: str | None — Choropleth カラム
221
+ popup_cols: list[str] | None — ポップアップ表示カラム
222
+ tiles: str — タイル名
223
+ output: str — 出力 HTML
224
+ """
225
+ import folium
226
+
227
+ center = [gdf.geometry.centroid.y.mean(),
228
+ gdf.geometry.centroid.x.mean()]
229
+ m = folium.Map(location=center, zoom_start=8, tiles=tiles)
230
+
231
+ if value_col and gdf.geometry.geom_type.iloc[0] in ["Polygon", "MultiPolygon"]:
232
+ folium.Choropleth(
233
+ geo_data=gdf.__geo_interface__,
234
+ data=gdf, columns=[gdf.index.name or "index", value_col],
235
+ key_on="feature.id",
236
+ fill_color="YlOrRd",
237
+ legend_name=value_col
238
+ ).add_to(m)
239
+ else:
240
+ for _, row in gdf.iterrows():
241
+ popup_text = ""
242
+ if popup_cols:
243
+ popup_text = "<br>".join(
244
+ [f"<b>{c}</b>: {row[c]}" for c in popup_cols])
245
+ folium.CircleMarker(
246
+ location=[row.geometry.centroid.y, row.geometry.centroid.x],
247
+ radius=5, popup=popup_text,
248
+ color="blue", fill=True
249
+ ).add_to(m)
250
+
251
+ m.save(output)
252
+ print(f"Interactive map → {output} ({len(gdf)} features)")
253
+ return output
254
+ ```
255
+
256
+ ---
257
+
258
+ ## パイプライン統合
259
+
260
+ ```
261
+ environmental-geodata → geospatial-analysis → advanced-visualization
262
+ (環境データ取得) (空間解析) (高度可視化)
263
+ │ │ ↓
264
+ epidemiology ───────────────┘ interactive-dashboard
265
+ (空間疫学) (ダッシュボード)
266
+ ```
267
+
268
+ ## パイプライン出力
269
+
270
+ | ファイル | 説明 | 次スキル |
271
+ |---------|------|---------|
272
+ | `spatial_autocorrelation.png` | Moran's I + LISA | → reporting |
273
+ | `kriging_result.png` | クリギング補間 | → visualization |
274
+ | `interactive_map.html` | Folium 地図 | → dashboard |
@@ -0,0 +1,346 @@
1
+ ---
2
+ name: scientific-interactive-dashboard
3
+ description: |
4
+ インタラクティブダッシュボードスキル。
5
+ Streamlit / Dash / Panel / Voilà による
6
+ 科学データダッシュボード構築・リアルタイムパラメータ探索 UI ・
7
+ ウィジェット連動・データアップロード・解析パイプライン UI 化。
8
+ ---
9
+
10
+ # Scientific Interactive Dashboard
11
+
12
+ 科学データのインタラクティブダッシュボードを構築し、
13
+ パラメータ探索・結果共有・リアルタイム解析を実現する。
14
+
15
+ ## When to Use
16
+
17
+ - Streamlit で迅速にデータ探索ダッシュボードを構築するとき
18
+ - Dash でカスタマイズ性の高い解析 UI を作成するとき
19
+ - Panel / Voilà で Jupyter ノートブックをダッシュボード化するとき
20
+ - パラメータスライダー + リアルタイム更新の UI を実装するとき
21
+ - 複数人で解析結果を共有するとき
22
+ - 非プログラマーに解析ツールを提供するとき
23
+
24
+ ---
25
+
26
+ ## Quick Start
27
+
28
+ ## 1. Streamlit 科学データダッシュボード
29
+
30
+ ```python
31
+ def generate_streamlit_dashboard(output_path="dashboard_app.py"):
32
+ """
33
+ Streamlit ダッシュボードテンプレート生成。
34
+
35
+ Parameters:
36
+ output_path: str — 出力 Python ファイル
37
+ """
38
+ code = '''
39
+ import streamlit as st
40
+ import pandas as pd
41
+ import numpy as np
42
+ import plotly.express as px
43
+
44
+
45
+ st.set_page_config(page_title="Scientific Data Dashboard",
46
+ layout="wide", page_icon="🔬")
47
+
48
+ st.title("🔬 Scientific Data Dashboard")
49
+
50
+ # --- サイドバー: データアップロード & パラメータ ---
51
+ st.sidebar.header("Settings")
52
+
53
+ uploaded_file = st.sidebar.file_uploader(
54
+ "Upload CSV / Excel", type=["csv", "xlsx"])
55
+
56
+ if uploaded_file is not None:
57
+ if uploaded_file.name.endswith(".csv"):
58
+ df = pd.read_csv(uploaded_file)
59
+ else:
60
+ df = pd.read_excel(uploaded_file)
61
+ else:
62
+ # デモデータ
63
+ np.random.seed(42)
64
+ n = 500
65
+ df = pd.DataFrame({
66
+ "x": np.random.randn(n),
67
+ "y": np.random.randn(n),
68
+ "z": np.random.randn(n),
69
+ "category": np.random.choice(["A", "B", "C"], n),
70
+ "value": np.random.exponential(2, n)
71
+ })
72
+ st.sidebar.info("Demo data loaded (upload your own CSV)")
73
+
74
+ # --- データ概要 ---
75
+ col1, col2, col3 = st.columns(3)
76
+ col1.metric("Rows", len(df))
77
+ col2.metric("Columns", len(df.columns))
78
+ col3.metric("Missing", int(df.isnull().sum().sum()))
79
+
80
+ # --- タブ ---
81
+ tab1, tab2, tab3, tab4 = st.tabs(
82
+ ["📊 Explorer", "📈 Distribution", "🔗 Correlation", "📋 Data"])
83
+
84
+ numeric_cols = df.select_dtypes(include=[np.number]).columns.tolist()
85
+ cat_cols = df.select_dtypes(exclude=[np.number]).columns.tolist()
86
+
87
+ with tab1:
88
+ st.subheader("Interactive Explorer")
89
+ c1, c2 = st.columns(2)
90
+ x_col = c1.selectbox("X axis", numeric_cols, index=0)
91
+ y_col = c2.selectbox("Y axis", numeric_cols,
92
+ index=min(1, len(numeric_cols)-1))
93
+ color_col = st.selectbox("Color", [None] + cat_cols + numeric_cols)
94
+
95
+ fig = px.scatter(df, x=x_col, y=y_col, color=color_col,
96
+ opacity=0.7, title=f"{x_col} vs {y_col}")
97
+ st.plotly_chart(fig, use_container_width=True)
98
+
99
+ with tab2:
100
+ st.subheader("Distribution Analysis")
101
+ dist_col = st.selectbox("Column", numeric_cols, key="dist")
102
+ n_bins = st.slider("Bins", 10, 100, 30)
103
+ fig2 = px.histogram(df, x=dist_col, nbins=n_bins,
104
+ marginal="box", title=f"Distribution: {dist_col}")
105
+ st.plotly_chart(fig2, use_container_width=True)
106
+
107
+ with tab3:
108
+ st.subheader("Correlation Matrix")
109
+ corr = df[numeric_cols].corr()
110
+ fig3 = px.imshow(corr, text_auto=".2f", color_continuous_scale="RdBu_r",
111
+ title="Correlation Heatmap")
112
+ st.plotly_chart(fig3, use_container_width=True)
113
+
114
+ with tab4:
115
+ st.subheader("Raw Data")
116
+ st.dataframe(df, use_container_width=True)
117
+ csv = df.to_csv(index=False)
118
+ st.download_button("Download CSV", csv, "data.csv", "text/csv")
119
+ '''
120
+
121
+ with open(output_path, "w") as f:
122
+ f.write(code)
123
+
124
+ print(f"Streamlit dashboard → {output_path}")
125
+ print(f" Run: streamlit run {output_path}")
126
+ return output_path
127
+ ```
128
+
129
+ ## 2. Dash コールバックダッシュボード
130
+
131
+ ```python
132
+ def generate_dash_dashboard(output_path="dash_app.py"):
133
+ """
134
+ Dash ダッシュボードテンプレート生成。
135
+
136
+ Parameters:
137
+ output_path: str — 出力 Python ファイル
138
+ """
139
+ code = '''
140
+ from dash import Dash, html, dcc, Input, Output, dash_table
141
+ import pandas as pd
142
+ import numpy as np
143
+ import plotly.express as px
144
+
145
+ app = Dash(__name__)
146
+
147
+ # デモデータ
148
+ np.random.seed(42)
149
+ n = 500
150
+ df = pd.DataFrame({
151
+ "x": np.random.randn(n),
152
+ "y": np.random.randn(n),
153
+ "z": np.random.randn(n),
154
+ "group": np.random.choice(["Control", "Treatment A", "Treatment B"], n),
155
+ "response": np.random.exponential(2, n)
156
+ })
157
+
158
+ numeric_cols = df.select_dtypes(include=[np.number]).columns.tolist()
159
+
160
+ app.layout = html.Div([
161
+ html.H1("Scientific Data Dashboard", style={"textAlign": "center"}),
162
+
163
+ html.Div([
164
+ html.Div([
165
+ html.Label("X Axis"),
166
+ dcc.Dropdown(id="x-col", options=numeric_cols,
167
+ value=numeric_cols[0])
168
+ ], style={"width": "30%", "display": "inline-block"}),
169
+ html.Div([
170
+ html.Label("Y Axis"),
171
+ dcc.Dropdown(id="y-col", options=numeric_cols,
172
+ value=numeric_cols[1])
173
+ ], style={"width": "30%", "display": "inline-block"}),
174
+ html.Div([
175
+ html.Label("Color"),
176
+ dcc.Dropdown(id="color-col",
177
+ options=df.columns.tolist(),
178
+ value="group")
179
+ ], style={"width": "30%", "display": "inline-block"}),
180
+ ], style={"padding": "20px"}),
181
+
182
+ html.Div([
183
+ html.Div([dcc.Graph(id="scatter-plot")],
184
+ style={"width": "50%", "display": "inline-block"}),
185
+ html.Div([dcc.Graph(id="histogram")],
186
+ style={"width": "50%", "display": "inline-block"}),
187
+ ]),
188
+
189
+ html.Div([
190
+ html.H3("Summary Statistics"),
191
+ dash_table.DataTable(
192
+ id="summary-table",
193
+ columns=[{"name": c, "id": c}
194
+ for c in ["stat"] + numeric_cols],
195
+ style_table={"overflowX": "auto"})
196
+ ], style={"padding": "20px"})
197
+ ])
198
+
199
+ @app.callback(
200
+ [Output("scatter-plot", "figure"),
201
+ Output("histogram", "figure"),
202
+ Output("summary-table", "data")],
203
+ [Input("x-col", "value"),
204
+ Input("y-col", "value"),
205
+ Input("color-col", "value")]
206
+ )
207
+ def update_plots(x_col, y_col, color_col):
208
+ fig1 = px.scatter(df, x=x_col, y=y_col, color=color_col,
209
+ opacity=0.7, title=f"{x_col} vs {y_col}")
210
+ fig2 = px.histogram(df, x=x_col, color=color_col,
211
+ marginal="box", barmode="overlay", opacity=0.7)
212
+ stats = df[numeric_cols].describe().reset_index()
213
+ stats.columns = ["stat"] + numeric_cols
214
+ return fig1, fig2, stats.to_dict("records")
215
+
216
+ if __name__ == "__main__":
217
+ app.run(debug=True, port=8050)
218
+ '''
219
+
220
+ with open(output_path, "w") as f:
221
+ f.write(code)
222
+
223
+ print(f"Dash dashboard → {output_path}")
224
+ print(f" Run: python {output_path}")
225
+ return output_path
226
+ ```
227
+
228
+ ## 3. Panel ダッシュボード
229
+
230
+ ```python
231
+ def generate_panel_dashboard(output_path="panel_app.py"):
232
+ """
233
+ Panel ダッシュボードテンプレート生成。
234
+
235
+ Parameters:
236
+ output_path: str — 出力 Python ファイル
237
+ """
238
+ code = '''
239
+ import panel as pn
240
+ import pandas as pd
241
+ import numpy as np
242
+ import plotly.express as px
243
+
244
+ pn.extension("plotly")
245
+
246
+ # デモデータ
247
+ np.random.seed(42)
248
+ n = 500
249
+ df = pd.DataFrame({
250
+ "x": np.random.randn(n),
251
+ "y": np.random.randn(n),
252
+ "z": np.random.randn(n),
253
+ "group": np.random.choice(["A", "B", "C"], n),
254
+ "value": np.random.exponential(2, n)
255
+ })
256
+
257
+ numeric_cols = df.select_dtypes(include=[np.number]).columns.tolist()
258
+
259
+ # ウィジェット
260
+ x_select = pn.widgets.Select(name="X Axis", options=numeric_cols, value="x")
261
+ y_select = pn.widgets.Select(name="Y Axis", options=numeric_cols, value="y")
262
+ n_bins = pn.widgets.IntSlider(name="Histogram Bins", start=10, end=100, value=30)
263
+
264
+
265
+ @pn.depends(x_select, y_select)
266
+ def scatter_plot(x_col, y_col):
267
+ fig = px.scatter(df, x=x_col, y=y_col, color="group",
268
+ opacity=0.7, title=f"{x_col} vs {y_col}")
269
+ return fig
270
+
271
+
272
+ @pn.depends(x_select, n_bins)
273
+ def hist_plot(x_col, bins):
274
+ fig = px.histogram(df, x=x_col, nbins=bins, color="group",
275
+ barmode="overlay", opacity=0.7)
276
+ return fig
277
+
278
+
279
+ dashboard = pn.template.FastListTemplate(
280
+ title="Scientific Data Dashboard",
281
+ sidebar=[x_select, y_select, n_bins],
282
+ main=[
283
+ pn.Row(pn.pane.Plotly(scatter_plot, sizing_mode="stretch_width"),
284
+ pn.pane.Plotly(hist_plot, sizing_mode="stretch_width")),
285
+ pn.pane.DataFrame(df.describe().T, sizing_mode="stretch_width")
286
+ ]
287
+ )
288
+
289
+ dashboard.servable()
290
+ '''
291
+
292
+ with open(output_path, "w") as f:
293
+ f.write(code)
294
+
295
+ print(f"Panel dashboard → {output_path}")
296
+ print(f" Run: panel serve {output_path}")
297
+ return output_path
298
+ ```
299
+
300
+ ## 4. ダッシュボード比較ガイド
301
+
302
+ ```python
303
+ def compare_dashboard_frameworks():
304
+ """
305
+ Streamlit / Dash / Panel / Voilà 比較表を出力。
306
+ """
307
+ comparison = pd.DataFrame({
308
+ "Framework": ["Streamlit", "Dash", "Panel", "Voilà"],
309
+ "Ease_of_Use": ["★★★★★", "★★★☆☆", "★★★★☆", "★★★★★"],
310
+ "Customization": ["★★★☆☆", "★★★★★", "★★★★☆", "★★☆☆☆"],
311
+ "Interactivity": ["★★★★☆", "★★★★★", "★★★★★", "★★★☆☆"],
312
+ "Performance": ["★★★☆☆", "★★★★★", "★★★★☆", "★★★☆☆"],
313
+ "Deployment": ["Streamlit Cloud", "Heroku/AWS", "Any ASGI", "Binder/Hub"],
314
+ "Best_For": [
315
+ "Rapid prototyping, data exploration",
316
+ "Production apps, complex callbacks",
317
+ "Jupyter integration, scientific viz",
318
+ "Notebook → dashboard conversion"
319
+ ]
320
+ })
321
+
322
+ print("=== Dashboard Framework Comparison ===")
323
+ print(comparison.to_string(index=False))
324
+ return comparison
325
+ ```
326
+
327
+ ---
328
+
329
+ ## パイプライン統合
330
+
331
+ ```
332
+ advanced-visualization → interactive-dashboard → presentation-design
333
+ (高度可視化) (ダッシュボード) (プレゼン)
334
+ │ │ ↓
335
+ missing-data-analysis ────────┘ scientific-schematics
336
+ (欠損値解析) (図式デザイン)
337
+ ```
338
+
339
+ ## パイプライン出力
340
+
341
+ | ファイル | 説明 | 次スキル |
342
+ |---------|------|---------|
343
+ | `dashboard_app.py` | Streamlit ダッシュボード | → deployment |
344
+ | `dash_app.py` | Dash ダッシュボード | → deployment |
345
+ | `panel_app.py` | Panel ダッシュボード | → deployment |
346
+ | `framework_comparison.csv` | フレームワーク比較 | → 選択指針 |