@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.
- package/LICENCE +0 -0
- package/README.md +191 -0
- package/bin/satori.js +95 -0
- package/package.json +29 -0
- package/src/.github/skills/scientific-academic-writing/SKILL.md +361 -0
- package/src/.github/skills/scientific-academic-writing/assets/acs_article.md +199 -0
- package/src/.github/skills/scientific-academic-writing/assets/elsevier_article.md +244 -0
- package/src/.github/skills/scientific-academic-writing/assets/ieee_transactions.md +212 -0
- package/src/.github/skills/scientific-academic-writing/assets/imrad_standard.md +181 -0
- package/src/.github/skills/scientific-academic-writing/assets/nature_article.md +179 -0
- package/src/.github/skills/scientific-academic-writing/assets/qiita_technical_article.md +385 -0
- package/src/.github/skills/scientific-academic-writing/assets/science_research_article.md +169 -0
- package/src/.github/skills/scientific-bioinformatics/SKILL.md +220 -0
- package/src/.github/skills/scientific-biosignal-processing/SKILL.md +357 -0
- package/src/.github/skills/scientific-causal-inference/SKILL.md +347 -0
- package/src/.github/skills/scientific-cheminformatics/SKILL.md +196 -0
- package/src/.github/skills/scientific-data-preprocessing/SKILL.md +413 -0
- package/src/.github/skills/scientific-data-simulation/SKILL.md +244 -0
- package/src/.github/skills/scientific-doe/SKILL.md +360 -0
- package/src/.github/skills/scientific-eda-correlation/SKILL.md +141 -0
- package/src/.github/skills/scientific-feature-importance/SKILL.md +208 -0
- package/src/.github/skills/scientific-image-analysis/SKILL.md +310 -0
- package/src/.github/skills/scientific-materials-characterization/SKILL.md +368 -0
- package/src/.github/skills/scientific-meta-analysis/SKILL.md +352 -0
- package/src/.github/skills/scientific-metabolomics/SKILL.md +326 -0
- package/src/.github/skills/scientific-ml-classification/SKILL.md +265 -0
- package/src/.github/skills/scientific-ml-regression/SKILL.md +215 -0
- package/src/.github/skills/scientific-multi-omics/SKILL.md +303 -0
- package/src/.github/skills/scientific-network-analysis/SKILL.md +257 -0
- package/src/.github/skills/scientific-pca-tsne/SKILL.md +235 -0
- package/src/.github/skills/scientific-pipeline-scaffold/SKILL.md +331 -0
- package/src/.github/skills/scientific-process-optimization/SKILL.md +215 -0
- package/src/.github/skills/scientific-publication-figures/SKILL.md +208 -0
- package/src/.github/skills/scientific-sequence-analysis/SKILL.md +389 -0
- package/src/.github/skills/scientific-spectral-signal/SKILL.md +227 -0
- package/src/.github/skills/scientific-statistical-testing/SKILL.md +240 -0
- package/src/.github/skills/scientific-survival-clinical/SKILL.md +239 -0
- 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 バースト
|