@nahisaho/satori 0.1.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.
Files changed (38) hide show
  1. package/LICENCE +0 -0
  2. package/README.md +191 -0
  3. package/bin/satori.js +95 -0
  4. package/package.json +29 -0
  5. package/src/.github/skills/scientific-academic-writing/SKILL.md +361 -0
  6. package/src/.github/skills/scientific-academic-writing/assets/acs_article.md +199 -0
  7. package/src/.github/skills/scientific-academic-writing/assets/elsevier_article.md +244 -0
  8. package/src/.github/skills/scientific-academic-writing/assets/ieee_transactions.md +212 -0
  9. package/src/.github/skills/scientific-academic-writing/assets/imrad_standard.md +181 -0
  10. package/src/.github/skills/scientific-academic-writing/assets/nature_article.md +179 -0
  11. package/src/.github/skills/scientific-academic-writing/assets/qiita_technical_article.md +385 -0
  12. package/src/.github/skills/scientific-academic-writing/assets/science_research_article.md +169 -0
  13. package/src/.github/skills/scientific-bioinformatics/SKILL.md +220 -0
  14. package/src/.github/skills/scientific-biosignal-processing/SKILL.md +357 -0
  15. package/src/.github/skills/scientific-causal-inference/SKILL.md +347 -0
  16. package/src/.github/skills/scientific-cheminformatics/SKILL.md +196 -0
  17. package/src/.github/skills/scientific-data-preprocessing/SKILL.md +413 -0
  18. package/src/.github/skills/scientific-data-simulation/SKILL.md +244 -0
  19. package/src/.github/skills/scientific-doe/SKILL.md +360 -0
  20. package/src/.github/skills/scientific-eda-correlation/SKILL.md +141 -0
  21. package/src/.github/skills/scientific-feature-importance/SKILL.md +208 -0
  22. package/src/.github/skills/scientific-image-analysis/SKILL.md +310 -0
  23. package/src/.github/skills/scientific-materials-characterization/SKILL.md +368 -0
  24. package/src/.github/skills/scientific-meta-analysis/SKILL.md +352 -0
  25. package/src/.github/skills/scientific-metabolomics/SKILL.md +326 -0
  26. package/src/.github/skills/scientific-ml-classification/SKILL.md +265 -0
  27. package/src/.github/skills/scientific-ml-regression/SKILL.md +215 -0
  28. package/src/.github/skills/scientific-multi-omics/SKILL.md +303 -0
  29. package/src/.github/skills/scientific-network-analysis/SKILL.md +257 -0
  30. package/src/.github/skills/scientific-pca-tsne/SKILL.md +235 -0
  31. package/src/.github/skills/scientific-pipeline-scaffold/SKILL.md +331 -0
  32. package/src/.github/skills/scientific-process-optimization/SKILL.md +215 -0
  33. package/src/.github/skills/scientific-publication-figures/SKILL.md +208 -0
  34. package/src/.github/skills/scientific-sequence-analysis/SKILL.md +389 -0
  35. package/src/.github/skills/scientific-spectral-signal/SKILL.md +227 -0
  36. package/src/.github/skills/scientific-statistical-testing/SKILL.md +240 -0
  37. package/src/.github/skills/scientific-survival-clinical/SKILL.md +239 -0
  38. package/src/.github/skills/scientific-time-series/SKILL.md +291 -0
@@ -0,0 +1,220 @@
1
+ ---
2
+ name: scientific-bioinformatics
3
+ description: |
4
+ バイオインフォマティクス解析パイプラインのスキル。scRNA-seq(Scanpy)、ゲノム配列解析
5
+ (BioPython)、PPI ネットワーク解析(NetworkX)、メタボロミクスの前処理を行う際に使用。
6
+ Scientific Skills Exp-01, 04, 07, 09 で確立したパターン。
7
+ ---
8
+
9
+ # Scientific Bioinformatics Pipelines
10
+
11
+ 生命科学データの解析パイプラインスキル。scRNA-seq、ゲノム配列解析、PPI ネットワーク、
12
+ メタボロミクスの 4 種のワークフローを統合的に提供する。
13
+
14
+ ## When to Use
15
+
16
+ - scRNA-seq データの QC・前処理・クラスタリング・DEG 解析
17
+ - ゲノム/タンパク質配列のアラインメント・系統解析・コドン使用解析
18
+ - タンパク質相互作用ネットワークの構築・中心性解析・コミュニティ検出
19
+ - メタボロミクスの前処理(補完・変換・スケーリング)・PLS-DA
20
+
21
+ ## Quick Start
22
+
23
+ ## 1. scRNA-seq パイプライン(Exp-01)
24
+
25
+ ### Scanpy 標準ワークフロー
26
+
27
+ ```python
28
+ import scanpy as sc
29
+
30
+ def scrnaseq_pipeline(adata, min_genes=200, max_genes=5000,
31
+ max_pct_mito=20, n_hvg=2000, resolution=0.5):
32
+ """
33
+ Scanpy の標準 scRNA-seq 解析パイプライン。
34
+ QC → 正規化 → HVG → PCA → Neighbors → Leiden → UMAP
35
+ """
36
+ # QC フィルタリング
37
+ sc.pp.filter_cells(adata, min_genes=min_genes)
38
+ sc.pp.filter_genes(adata, min_cells=3)
39
+
40
+ adata.var["mt"] = adata.var_names.str.startswith("MT-")
41
+ sc.pp.calculate_qc_metrics(adata, qc_vars=["mt"],
42
+ percent_top=None, inplace=True)
43
+ adata = adata[adata.obs["pct_counts_mt"] < max_pct_mito].copy()
44
+ adata = adata[adata.obs["n_genes_by_counts"] < max_genes].copy()
45
+
46
+ # 正規化 & Log 変換
47
+ sc.pp.normalize_total(adata, target_sum=1e4)
48
+ sc.pp.log1p(adata)
49
+
50
+ # HVG 選択
51
+ sc.pp.highly_variable_genes(adata, n_top_genes=n_hvg)
52
+ adata.raw = adata
53
+ adata = adata[:, adata.var["highly_variable"]].copy()
54
+
55
+ # PCA → Neighbors → UMAP → Leiden
56
+ sc.pp.scale(adata, max_value=10)
57
+ sc.tl.pca(adata, n_comps=50)
58
+ sc.pp.neighbors(adata, n_pcs=30)
59
+ sc.tl.umap(adata)
60
+ sc.tl.leiden(adata, resolution=resolution)
61
+
62
+ return adata
63
+ ```
64
+
65
+ ### 細胞タイプアノテーション
66
+
67
+ ```python
68
+ CELL_MARKERS = {
69
+ "CD4+ T": ["IL7R", "CD4"],
70
+ "CD8+ T": ["CD8A", "CD8B"],
71
+ "B cell": ["MS4A1", "CD79A"],
72
+ "NK": ["GNLY", "NKG7"],
73
+ "Monocyte": ["CD14", "LYZ"],
74
+ "DC": ["FCER1A", "CST3"],
75
+ "Platelet": ["PPBP"],
76
+ }
77
+
78
+ def annotate_clusters(adata, marker_dict=None):
79
+ """マーカー遺伝子ベースのクラスタアノテーション。"""
80
+ if marker_dict is None:
81
+ marker_dict = CELL_MARKERS
82
+ # マーカーごとの平均発現量でクラスタに細胞型を割り当て
83
+ cluster_annotations = {}
84
+ for cluster in adata.obs["leiden"].unique():
85
+ mask = adata.obs["leiden"] == cluster
86
+ best_score = -1
87
+ best_type = "Unknown"
88
+ for cell_type, markers in marker_dict.items():
89
+ available = [m for m in markers if m in adata.raw.var_names]
90
+ if available:
91
+ score = adata.raw[mask][:, available].X.mean()
92
+ if score > best_score:
93
+ best_score = score
94
+ best_type = cell_type
95
+ cluster_annotations[cluster] = best_type
96
+ adata.obs["cell_type"] = adata.obs["leiden"].map(cluster_annotations)
97
+ return adata
98
+ ```
99
+
100
+ ## 2. ゲノム配列解析パイプライン(Exp-09)
101
+
102
+ ```python
103
+ def compute_sequence_statistics(sequence):
104
+ """DNA 配列の基本統計量を算出する。"""
105
+ from collections import Counter
106
+ seq_str = str(sequence).upper()
107
+ counts = Counter(seq_str)
108
+ length = len(seq_str)
109
+ gc_content = (counts.get("G", 0) + counts.get("C", 0)) / length * 100
110
+ return {
111
+ "Length": length,
112
+ "GC_Content": gc_content,
113
+ "A": counts.get("A", 0), "T": counts.get("T", 0),
114
+ "G": counts.get("G", 0), "C": counts.get("C", 0),
115
+ }
116
+
117
+ def codon_usage_rscu(coding_sequence):
118
+ """RSCU(Relative Synonymous Codon Usage)を算出する。"""
119
+ from collections import Counter
120
+ codons = [coding_sequence[i:i+3] for i in range(0, len(coding_sequence)-2, 3)]
121
+ codon_counts = Counter(codons)
122
+ # RSCU = observed / expected_if_uniform
123
+ # 同義コドン群ごとに計算
124
+ return codon_counts # 簡略版、完全版は Exp-09 参照
125
+ ```
126
+
127
+ ## 3. PPI ネットワーク解析(Exp-04)
128
+
129
+ ```python
130
+ import networkx as nx
131
+
132
+ def build_ppi_network(interactions_df, source_col, target_col,
133
+ weight_col=None):
134
+ """PPI ネットワークを構築する。"""
135
+ G = nx.Graph()
136
+ for _, row in interactions_df.iterrows():
137
+ if weight_col:
138
+ G.add_edge(row[source_col], row[target_col],
139
+ weight=row[weight_col])
140
+ else:
141
+ G.add_edge(row[source_col], row[target_col])
142
+ return G
143
+
144
+ def centrality_analysis(G):
145
+ """4 種の中心性指標を一括計算する。"""
146
+ centralities = pd.DataFrame({
147
+ "Degree": dict(G.degree()),
148
+ "Betweenness": nx.betweenness_centrality(G),
149
+ "Closeness": nx.closeness_centrality(G),
150
+ "Eigenvector": nx.eigenvector_centrality(G, max_iter=1000),
151
+ })
152
+ centralities.to_csv("results/centrality_measures.csv")
153
+ return centralities
154
+
155
+ def community_detection(G, method="louvain"):
156
+ """コミュニティ検出。"""
157
+ if method == "louvain":
158
+ import community as community_louvain
159
+ partition = community_louvain.best_partition(G)
160
+ nx.set_node_attributes(G, partition, "community")
161
+ return partition
162
+ elif method == "greedy":
163
+ from networkx.algorithms.community import greedy_modularity_communities
164
+ communities = list(greedy_modularity_communities(G))
165
+ partition = {}
166
+ for i, comm in enumerate(communities):
167
+ for node in comm:
168
+ partition[node] = i
169
+ return partition
170
+ ```
171
+
172
+ ## 4. メタボロミクス前処理(Exp-07)
173
+
174
+ ```python
175
+ from sklearn.impute import KNNImputer
176
+
177
+ def metabolomics_preprocessing(df, metabolite_cols, group_col=None):
178
+ """
179
+ メタボロミクスの標準前処理パイプライン。
180
+ KNN 補完 → Log2 変換 → Pareto スケーリング
181
+ """
182
+ X = df[metabolite_cols].values.copy()
183
+
184
+ # KNN 補完
185
+ imputer = KNNImputer(n_neighbors=5)
186
+ X_imputed = imputer.fit_transform(X)
187
+
188
+ # Log2 変換(ゼロ値に小さな正の値を加算)
189
+ X_log = np.log2(X_imputed + 1)
190
+
191
+ # Pareto スケーリング
192
+ means = X_log.mean(axis=0)
193
+ stds = X_log.std(axis=0)
194
+ X_pareto = (X_log - means) / np.sqrt(stds + 1e-10)
195
+
196
+ result_df = pd.DataFrame(X_pareto, columns=metabolite_cols, index=df.index)
197
+ if group_col:
198
+ result_df[group_col] = df[group_col].values
199
+
200
+ return result_df
201
+ ```
202
+
203
+ ## References
204
+
205
+ ### Output Files
206
+
207
+ | ファイル | 形式 |
208
+ |---|---|
209
+ | `results/centrality_measures.csv` | CSV |
210
+ | `results/community_pathway_mapping.csv` | CSV |
211
+ | `results/codon_usage.csv` | CSV |
212
+ | `figures/umap_clusters.png` | PNG |
213
+ | `figures/network_visualization.png` | PNG |
214
+
215
+ #### 参照実験
216
+
217
+ - **Exp-01**: scRNA-seq(Scanpy + PyDESeq2 + KEGG)
218
+ - **Exp-04**: PPI ネットワーク(NetworkX + Louvain)
219
+ - **Exp-07**: メタボロミクス(KNN + PLS-DA + VIP)
220
+ - **Exp-09**: ゲノム配列(BioPython + 系統樹 + RSCU)
@@ -0,0 +1,357 @@
1
+ ---
2
+ name: scientific-biosignal-processing
3
+ description: |
4
+ 生体信号処理スキル。ECG(R波検出・HRV時間/周波数ドメイン・Poincaréプロット)、
5
+ EEG(マルチチャネル・バンドパワーδ/θ/α/β/γ・スペクトログラム・ERP)、
6
+ EMG(バースト検出・包絡線)、呼吸信号(RSA)の解析パイプライン。
7
+ Scientific Skills Exp-08 で確立したパターン。
8
+ ---
9
+
10
+ # Scientific Biosignal Processing
11
+
12
+ 心電図(ECG)、脳波(EEG)、筋電図(EMG)、呼吸信号などの生体信号を対象とした
13
+ 前処理・特徴量抽出・可視化パイプライン。spectral-signal スキルが分光データ
14
+ (ラマン・IR・UV-Vis)を対象とするのに対し、本スキルは時間軸上の生理学データに特化する。
15
+
16
+ ## When to Use
17
+
18
+ - ECG の R 波検出・HRV 解析が必要なとき
19
+ - EEG のバンドパワー抽出・ERP 解析が必要なとき
20
+ - EMG のバースト検出・onset 解析が必要なとき
21
+ - 生体信号のマルチチャネルデータを扱うとき
22
+
23
+ ---
24
+
25
+ ## Quick Start
26
+
27
+ ## 1. ECG 解析パイプライン
28
+
29
+ ### 1.1 PQRST 合成(テストデータ用)
30
+
31
+ ```python
32
+ import numpy as np
33
+ from scipy.signal import butter, filtfilt, find_peaks
34
+
35
+ def synthesize_ecg(duration_s=10, fs=500, heart_rate_bpm=72, noise_level=0.05):
36
+ """テスト用合成 ECG 信号を生成する。"""
37
+ t = np.arange(0, duration_s, 1/fs)
38
+ beat_interval = 60 / heart_rate_bpm
39
+ ecg = np.zeros_like(t)
40
+
41
+ beat_times = np.arange(0, duration_s, beat_interval)
42
+ for bt in beat_times:
43
+ # P波
44
+ ecg += 0.15 * np.exp(-((t - bt + 0.16)**2) / (2 * 0.01**2))
45
+ # QRS群
46
+ ecg += -0.10 * np.exp(-((t - bt + 0.04)**2) / (2 * 0.005**2))
47
+ ecg += 1.00 * np.exp(-((t - bt)**2) / (2 * 0.008**2))
48
+ ecg += -0.15 * np.exp(-((t - bt - 0.04)**2) / (2 * 0.005**2))
49
+ # T波
50
+ ecg += 0.30 * np.exp(-((t - bt - 0.20)**2) / (2 * 0.03**2))
51
+
52
+ ecg += noise_level * np.random.randn(len(t))
53
+ return t, ecg
54
+ ```
55
+
56
+ ### 1.2 R 波検出
57
+
58
+ ```python
59
+ def detect_r_peaks(ecg, fs, min_distance_ms=300):
60
+ """ECG からR波を検出し、RR間隔を算出する。"""
61
+ min_distance = int(min_distance_ms * fs / 1000)
62
+ height_threshold = np.mean(ecg) + 0.6 * np.std(ecg)
63
+
64
+ r_peaks, properties = find_peaks(ecg, height=height_threshold,
65
+ distance=min_distance)
66
+ rr_intervals_ms = np.diff(r_peaks) / fs * 1000 # ms
67
+
68
+ return r_peaks, rr_intervals_ms
69
+ ```
70
+
71
+ ### 1.3 HRV 時間ドメイン解析
72
+
73
+ ```python
74
+ def hrv_time_domain(rr_intervals_ms):
75
+ """
76
+ HRV 時間ドメイン指標を算出する。
77
+
78
+ Returns:
79
+ dict with keys: mean_rr, sdnn, rmssd, pnn50, mean_hr
80
+ """
81
+ rr = np.array(rr_intervals_ms)
82
+ diff_rr = np.diff(rr)
83
+
84
+ return {
85
+ "mean_rr_ms": np.mean(rr),
86
+ "sdnn_ms": np.std(rr, ddof=1),
87
+ "rmssd_ms": np.sqrt(np.mean(diff_rr**2)),
88
+ "pnn50_pct": np.sum(np.abs(diff_rr) > 50) / len(diff_rr) * 100,
89
+ "mean_hr_bpm": 60000 / np.mean(rr),
90
+ "cv_rr": np.std(rr, ddof=1) / np.mean(rr) * 100,
91
+ }
92
+ ```
93
+
94
+ ### 1.4 HRV 周波数ドメイン解析
95
+
96
+ ```python
97
+ from scipy.signal import welch
98
+
99
+ def hrv_frequency_domain(rr_intervals_ms, fs_interp=4):
100
+ """
101
+ HRV 周波数ドメイン指標を算出する。
102
+
103
+ バンド定義:
104
+ VLF: 0.003 - 0.04 Hz
105
+ LF: 0.04 - 0.15 Hz
106
+ HF: 0.15 - 0.40 Hz
107
+
108
+ Returns:
109
+ dict with vlf_power, lf_power, hf_power, lf_hf_ratio, total_power
110
+ """
111
+ from scipy.interpolate import interp1d
112
+
113
+ rr_s = np.array(rr_intervals_ms) / 1000
114
+ cum_time = np.cumsum(rr_s)
115
+ cum_time -= cum_time[0]
116
+
117
+ # 等間隔リサンプリング
118
+ t_interp = np.arange(0, cum_time[-1], 1/fs_interp)
119
+ f_interp = interp1d(cum_time, rr_s, kind="cubic", fill_value="extrapolate")
120
+ rr_resampled = f_interp(t_interp)
121
+ rr_resampled -= np.mean(rr_resampled)
122
+
123
+ # Welch PSD
124
+ freqs, psd = welch(rr_resampled, fs=fs_interp, nperseg=min(256, len(rr_resampled)))
125
+
126
+ # バンドごとのパワー
127
+ vlf_mask = (freqs >= 0.003) & (freqs < 0.04)
128
+ lf_mask = (freqs >= 0.04) & (freqs < 0.15)
129
+ hf_mask = (freqs >= 0.15) & (freqs < 0.40)
130
+
131
+ vlf = np.trapz(psd[vlf_mask], freqs[vlf_mask])
132
+ lf = np.trapz(psd[lf_mask], freqs[lf_mask])
133
+ hf = np.trapz(psd[hf_mask], freqs[hf_mask])
134
+
135
+ return {
136
+ "vlf_power_ms2": vlf * 1e6,
137
+ "lf_power_ms2": lf * 1e6,
138
+ "hf_power_ms2": hf * 1e6,
139
+ "lf_hf_ratio": lf / (hf + 1e-10),
140
+ "total_power_ms2": (vlf + lf + hf) * 1e6,
141
+ }
142
+ ```
143
+
144
+ ### 1.5 Poincaré プロット
145
+
146
+ ```python
147
+ import matplotlib.pyplot as plt
148
+
149
+ def poincare_plot(rr_intervals_ms, figsize=(8, 8)):
150
+ """
151
+ Poincaré プロット (RR_n vs RR_n+1) を描画する。
152
+ SD1: 短期変動(副交感指標)
153
+ SD2: 長期変動(交感+副交感)
154
+ """
155
+ rr = np.array(rr_intervals_ms)
156
+ rr_n = rr[:-1]
157
+ rr_n1 = rr[1:]
158
+
159
+ # SD1, SD2
160
+ diff = rr_n1 - rr_n
161
+ sd1 = np.std(diff / np.sqrt(2), ddof=1)
162
+ sd2 = np.std((rr_n + rr_n1) / np.sqrt(2), ddof=1)
163
+
164
+ fig, ax = plt.subplots(figsize=figsize)
165
+ ax.scatter(rr_n, rr_n1, c="steelblue", alpha=0.5, s=15)
166
+ ax.plot([rr.min(), rr.max()], [rr.min(), rr.max()], "k--", alpha=0.3)
167
+
168
+ # 楕円
169
+ from matplotlib.patches import Ellipse
170
+ center = (np.mean(rr_n), np.mean(rr_n1))
171
+ ellipse = Ellipse(center, width=2*sd2, height=2*sd1, angle=45,
172
+ fill=False, color="red", linewidth=2)
173
+ ax.add_patch(ellipse)
174
+
175
+ ax.set_xlabel("RR_n (ms)")
176
+ ax.set_ylabel("RR_{n+1} (ms)")
177
+ ax.set_title(f"Poincaré Plot (SD1={sd1:.1f}, SD2={sd2:.1f})",
178
+ fontweight="bold")
179
+ ax.set_aspect("equal")
180
+ plt.tight_layout()
181
+ plt.savefig("figures/poincare_plot.png", dpi=300, bbox_inches="tight")
182
+ plt.close()
183
+
184
+ return {"sd1_ms": sd1, "sd2_ms": sd2, "sd1_sd2_ratio": sd1/sd2}
185
+ ```
186
+
187
+ ---
188
+
189
+ ## 2. EEG 解析パイプライン
190
+
191
+ ### 2.1 バンドパワー抽出
192
+
193
+ ```python
194
+ EEG_BANDS = {
195
+ "delta": (0.5, 4),
196
+ "theta": (4, 8),
197
+ "alpha": (8, 13),
198
+ "beta": (13, 30),
199
+ "gamma": (30, 50),
200
+ }
201
+
202
+ def extract_band_power(eeg, fs, bands=None):
203
+ """
204
+ EEG 信号から各周波数バンドの相対パワーを抽出する。
205
+ """
206
+ if bands is None:
207
+ bands = EEG_BANDS
208
+
209
+ freqs, psd = welch(eeg, fs=fs, nperseg=min(fs*2, len(eeg)))
210
+ total_power = np.trapz(psd, freqs)
211
+
212
+ band_powers = {}
213
+ for band_name, (low, high) in bands.items():
214
+ mask = (freqs >= low) & (freqs < high)
215
+ power = np.trapz(psd[mask], freqs[mask])
216
+ band_powers[f"{band_name}_abs"] = power
217
+ band_powers[f"{band_name}_rel"] = power / total_power * 100
218
+
219
+ return band_powers
220
+ ```
221
+
222
+ ### 2.2 スペクトログラム
223
+
224
+ ```python
225
+ from scipy.signal import spectrogram
226
+
227
+ def plot_spectrogram(eeg, fs, channel_name="EEG", max_freq=50, figsize=(12, 4)):
228
+ """EEG のスペクトログラム(時間-周波数表現)を描画する。"""
229
+ nperseg = min(fs * 2, len(eeg))
230
+ f, t, Sxx = spectrogram(eeg, fs=fs, nperseg=nperseg,
231
+ noverlap=nperseg//2)
232
+
233
+ freq_mask = f <= max_freq
234
+
235
+ fig, ax = plt.subplots(figsize=figsize)
236
+ im = ax.pcolormesh(t, f[freq_mask], 10 * np.log10(Sxx[freq_mask] + 1e-10),
237
+ shading="gouraud", cmap="viridis")
238
+ ax.set_ylabel("Frequency (Hz)")
239
+ ax.set_xlabel("Time (s)")
240
+ ax.set_title(f"Spectrogram — {channel_name}", fontweight="bold")
241
+ plt.colorbar(im, ax=ax, label="Power (dB)")
242
+ plt.tight_layout()
243
+ plt.savefig(f"figures/spectrogram_{channel_name}.png", dpi=300,
244
+ bbox_inches="tight")
245
+ plt.close()
246
+ ```
247
+
248
+ ### 2.3 ERP (Event-Related Potential)
249
+
250
+ ```python
251
+ def compute_erp(eeg, fs, event_times_s, window_s=(-0.2, 0.8)):
252
+ """
253
+ イベント関連電位(ERP)を計算する。
254
+
255
+ Parameters:
256
+ eeg: 1D EEG signal
257
+ fs: サンプリング周波数
258
+ event_times_s: イベント時刻のリスト (秒)
259
+ window_s: (pre, post) 窓 (秒)
260
+
261
+ Returns:
262
+ erp_mean: 平均 ERP, erp_sem: 標準誤差, t_axis: 時間軸
263
+ """
264
+ pre_samples = int(abs(window_s[0]) * fs)
265
+ post_samples = int(window_s[1] * fs)
266
+ total = pre_samples + post_samples
267
+
268
+ epochs = []
269
+ for evt in event_times_s:
270
+ idx = int(evt * fs)
271
+ start = idx - pre_samples
272
+ end = idx + post_samples
273
+ if start >= 0 and end <= len(eeg):
274
+ epoch = eeg[start:end]
275
+ epoch = epoch - np.mean(epoch[:pre_samples]) # baseline correction
276
+ epochs.append(epoch)
277
+
278
+ epochs = np.array(epochs)
279
+ t_axis = np.linspace(window_s[0], window_s[1], total)
280
+ erp_mean = np.mean(epochs, axis=0)
281
+ erp_sem = np.std(epochs, axis=0) / np.sqrt(len(epochs))
282
+
283
+ return erp_mean, erp_sem, t_axis
284
+
285
+
286
+ def detect_erp_components(erp, t_axis, components=None):
287
+ """ERP コンポーネント(N100, P300 など)のピークを検出する。"""
288
+ if components is None:
289
+ components = {
290
+ "N100": {"window": (0.08, 0.15), "polarity": "negative"},
291
+ "P200": {"window": (0.15, 0.25), "polarity": "positive"},
292
+ "P300": {"window": (0.25, 0.50), "polarity": "positive"},
293
+ }
294
+
295
+ results = {}
296
+ for name, spec in components.items():
297
+ mask = (t_axis >= spec["window"][0]) & (t_axis <= spec["window"][1])
298
+ segment = erp[mask]
299
+ t_seg = t_axis[mask]
300
+ if spec["polarity"] == "negative":
301
+ idx = np.argmin(segment)
302
+ else:
303
+ idx = np.argmax(segment)
304
+ results[name] = {
305
+ "latency_ms": t_seg[idx] * 1000,
306
+ "amplitude_uV": segment[idx],
307
+ }
308
+ return results
309
+ ```
310
+
311
+ ---
312
+
313
+ ## 3. EMG 解析
314
+
315
+ ```python
316
+ def emg_envelope(emg, fs, cutoff=10):
317
+ """EMG の包絡線(Hilbert 変換 + ローパスフィルタ)を算出する。"""
318
+ from scipy.signal import hilbert
319
+ analytic = hilbert(emg)
320
+ envelope = np.abs(analytic)
321
+
322
+ b, a = butter(4, cutoff / (fs / 2), btype="low")
323
+ envelope_smooth = filtfilt(b, a, envelope)
324
+ return envelope_smooth
325
+
326
+
327
+ def detect_muscle_bursts(envelope, fs, threshold_factor=2.0,
328
+ min_duration_ms=50):
329
+ """EMG バースト(筋活動開始)を検出する。"""
330
+ threshold = np.mean(envelope) + threshold_factor * np.std(envelope)
331
+ active = envelope > threshold
332
+ min_samples = int(min_duration_ms * fs / 1000)
333
+
334
+ bursts = []
335
+ in_burst = False
336
+ start = 0
337
+ for i, val in enumerate(active):
338
+ if val and not in_burst:
339
+ start = i
340
+ in_burst = True
341
+ elif not val and in_burst:
342
+ duration = i - start
343
+ if duration >= min_samples:
344
+ bursts.append({
345
+ "onset_s": start / fs,
346
+ "offset_s": i / fs,
347
+ "duration_ms": duration / fs * 1000,
348
+ "peak_amplitude": np.max(envelope[start:i]),
349
+ })
350
+ in_burst = False
351
+
352
+ return bursts
353
+ ```
354
+
355
+ ## References
356
+
357
+ - **Exp-08**: ECG R波検出・HRV・Poincaré、EEG バンドパワー・スペクトログラム・ERP、EMG バースト