ケィオスの時系列解析メモランダム

時系列解析、生体情報解析などをやわらかく語ります

【Rで生体信号解析1】EDFファイルの読み込み

EDF(European Data Format) は、脳波 (EEG)、心電図(ECG)などの生体信号データを保存するための標準的なファイル形式です。1980年代末に提案され、今でも睡眠ポリグラフや臨床脳波、研究用 EEG などで広く使われています。
 今回は、計測されたデータをパソコンに取り込まないことには分析を始められませんので、R を使って EDF ファイルを読み込む方法を解説します。

1. EDFファイルの構造

 まずは、心の準備として、EDFファイルにはどのような情報が含まれているのかを確認しておきましょう。ざっくりいうと、EDFファイルは次の三層構造になっています。

  1. 固定長ヘッダ(基本情報)

    • 患者情報(ID、性別・生年月日の簡略コードなど)
    • 記録条件(Recording ID)
    • 記録開始日時(Start date/time)
    • 1レコードの長さ(sec)
    • レコード数(=観測時間 / レコード長)
    • チャンネル数(脳波チャンネル+EOG、EMG、ECGなど)
  2. シグナルごとのヘッダ(各チャネルのメタ情報) チャンネルごとに、次のような情報が並びます。

    • ラベル(Fp1C3-A2ECGEMG chin など)
    • トランスデューサ(電極、圧センサ、サーミスタなど)
    • 単位(uVmVHzarb. unit など)
    • 物理的最小値・最大値(例:-200 µV ~ 200 µV)
    • デジタル最小値・最大値(例:-32768 ~ 32767)
    • 1レコードあたりのサンプル数(→ サンプリング周波数に対応)
    • フィルタ条件やノッチ (HP: 0.3 HzLP: 70 HzNotch: 50 Hz などが書かれることもある)
  3. データ本体(サンプル列)

    • 各レコードごとに、全チャネル分のサンプルが順番に並んでいます。
    • もともとは 整数値(デジタル値)で保存されており、 物理値 = (digital - digital_min) * (physical_range / digital_range) + physical_min のような線形変換で、µV などの物理量に変換します。
    • EDF+ では、注釈専用チャネルに「スリープステージ」「アーティファクト」「イベント」などの文字情報が書き込まれていることもあります。

2. R で EDF を読み込む:基本スクリプト

 ここでは、R の edfReader パッケージを使って、EDFヘッダ情報とシグナル(波形)を読み込む例を示します。

2.1 パッケージのインストールと読み込み

 edfReader パッケージを使ったことがなく、まだインストールしていないのであれば、初回だけinstall.packages("edfReader") を実行する必要があります。良く分からない人もまずは、以下のRスクリプトを実行してください。

# 必要なパッケージ名
pkg <- "edfReader"

# インストールされていなければ自動でインストール
if (!requireNamespace(pkg, quietly = TRUE)) {
  install.packages(pkg)
}

# ライブラリを読み込む
library(edfReader)

2.2 EDFヘッダを読む

 データの概要を確認するために、EDFファイルのヘッダ情報を読み込みます。ここでは例として、Windows環境を想定し、読み込みたいEDFファイルがあるフォルダ 脳波 へのパスを D:\Document\研究\脳波、読み込むEDF ファイル名を DATA_001.edf とします。

# フォルダへのパス
DIR <- r"(D:\Document\研究\脳波)"

# ファイルパス(作業ディレクトリにある場合)
edf_file <- "DATA_001.edf"

# 1. ヘッダ情報の読み込み
setwd(DIR)
hdr <- readEdfHeader(edf_file)

# 簡単な確認
hdr$patientID      # 患者IDなど
hdr$recordingID    # 記録条件(機器名やモンタージュ情報など)
hdr$startTime      # 記録開始時刻
hdr$nSignals       # チャンネル数
hdr$sHeaders      # シグナルヘッダ(各チャネルのメタ情報)

hdr の中で特に重要なのは

  • hdr$startTime / hdr$startDate:記録開始日時
  • hdr$nSignals:チャネル数
  • hdr$sHeaders各チャネルの詳細情報(ラベル、単位、サンプリング周波数など)

です。
 私の手元のデータの例では、EDFヘッダを読み込むと次のような情報が得られます。

> hdr$patientID      # 患者IDなど
NULL
> hdr$recordingID    # 記録条件(機器名やモンタージュ情報など)
NULL
> hdr$startTime      # 記録開始時刻
[1] "2020-12-03 23:39:15 JST"
> hdr$nSignals       # チャンネル数
[1] 2
> hdr$sHeaders      # シグナルヘッダ(各チャネルのメタ情報)
Signal labels : EEG, ECG  

 この例では、患者ID(patientID記録条件(recordingIDNULL になっています。これは測定機器や保存設定によっては、これらの項目が記録されない(あるいは匿名化されている)場合があるためです。
 一方で、記録開始時刻(startTime

2020-12-03 23:39:15 JST

のように正しく取得されています。これは、後で波形の横軸を「実際の記録時刻」で表示する際に重要になります。また、

hdr$nSignals = 2

より、この EDF データには 2つのチャネル(シグナル) が含まれていることがわかります。さらに、hdr$sHeaders の出力に

Signal labels : EEG, ECG

とあるように、今回のデータには EEG(脳波)ECG(心電図) の 2チャネルが含まれていることが確認できます。sHeaders には、このほかにも各チャネルの単位(µV など)やトランスデューサ情報、サンプル数、物理レンジなど、波形解析に必要となるメタ情報がすべて保存されています。

2.3 チャネル情報(ラベル・サンプリング周波数など)の確認

 時系列データを読み込む前に、チャネル情報を確認します。

# シグナルヘッダ(各チャネルのメタ情報)が data.frame で入っています
sinfo <- hdr$sHeaders

# 先頭数行を確認
head(sinfo)

# 代表的な列
sinfo$label         # チャネル名(Fp1、Fp2、C3-A2、ECG、EOG、EMGなど)
sinfo$transducer    # トランスデューサの種類(電極・センサ等)
sinfo$physicalDim   # 単位(uV、mVなど)
sinfo$samplesPerRecord  # 1レコードあたりのサンプル数
hdr$recordDuration      # 1レコードの長さ(秒)

# サンプリング周波数(Hz)を計算
fs <- sinfo$samplesPerRecord / hdr$recordDuration
data.frame(label = sinfo$label, fs = fs)

ここで注目するポイント:

  • チャネル名 (label)

    • 国際10–20法に沿ったラベルか、独自のラベルか。
    • 参照(C3-A2 のように差分記録か、C3 単独か)も確認します。
  • 単位 (physicalDim)

    • µV(uV)なのか mV なのか。後でスペクトル解析などをする際に、スケールが合っているか確認します。
  • サンプリング周波数 fs

    • 解析したい周波数帯(α波 8–13 Hz、β波 13–30 Hz など)に対して十分か。
    • チャンネルごとに fs が異なる可能性もあるので注意します(EDFではチャネルごとに samplesPerRecord が違ってもよい仕様)。

3. 波形データの読み込みとプロット

 ヘッダが読めたら、次は実際のサンプル値(波形データ)を読みます。

3.1 波形データの読み込み

 以下のスクリプトでは

  • チャネルのメタ情報を確認し

  • サンプリング周波数を計算し

  • 実際の波形データを読み込み

  • EEG・ECG のチャネルを取り出す

といった一連の処理を行っています。あくまで参考例であり、実際の計測データの構成に応じて、必要に応じて命令を修正してください。

############################################################
# 1. シグナルヘッダ(チャネル情報)の取得
############################################################
# EDFヘッダの中には、チャネルごとのメタ情報が data.frame として保存されている
sinfo <- hdr$sHeaders
# チャネル情報の先頭数行を確認(どんな列があるか把握する)
head(sinfo)
# 代表的な列を確認:
sinfo$label             # チャネル名(例:Fp1、Fp2、C3-A2、EOG、EMG など)
sinfo$transducer        # トランスデューサ(電極の種類・センサ種別)
sinfo$physicalDim       # 単位(uV、mV、Hz など)
sinfo$samplesPerRecord  # 1レコードに含まれるサンプル数(チャネルごとに異なる場合あり)
hdr$recordDuration      # 1レコードの長さ(秒単位)
# サンプリング周波数(Hz)を計算
#   EDFはサンプリング周波数を明示的に持たないため、
#   「1レコードあたりのサンプル数 / レコード長」で求める
fs <- sinfo$samplesPerRecord / hdr$recordDuration
# チャネルごとのサンプリング周波数を一覧化する
data.frame(label = sinfo$label, sampling_rate_Hz = fs)
############################################################
# 2. シグナルデータの読み込み
############################################################
# 全チャネルの波形データ(数値ベクトル)を読み込む
signals <- readEdfSignals(hdr)
# 読み込んだデータは「チャネル名」を名前に持つリストとして格納されている
names(signals)   # 利用可能なチャネル名を確認
############################################################
# 3. EEG と ECG のチャネルを取り出す例
############################################################
# 例として、最初のチャネル(通常はEEG)を ch1 とする
ch1_name <- sinfo$label[1]
ch1 <- signals[[ch1_name]]
# 2番目のチャネル(例: ECG)がある場合は ch2 として取り出す
ch2_name <- sinfo$label[2]
ch2 <- signals[[ch2_name]]
############################################################
# 4. 各チャネルのデータ内容を確認
############################################################
# ch1 のサンプル数と先頭部分を確認
length(ch1)   # サンプル数
head(ch1)     # 最初の数点を表示
# ch2 のサンプル数と先頭部分を確認
length(ch2)
head(ch2)

 readEdfSignals() のオプションによっては、すでに 物理量(µVなど)に変換された numeric ベクトルとして得られます。その場合は、そのままプロットやフィルタ処理に使えます。

3.2 データの最初と最後の5分間をプロット

 念のため読み込んでデータをプロットするRスクリプトの例を示しておきます.

## ==== EEG (ch1) の情報を取り出す ===================================
sig1   <- ch1$signal          # EEG 波形ベクトル
fs1    <- ch1$sRate           # サンプリング周波数 [Hz]
n_all1 <- length(sig1)        # 全サンプル数
# 記録開始・終了時刻(POSIXctへ変換)
start_time1 <- as.POSIXct(ch1$startTime)
end_time1   <- start_time1 + (n_all1 - 1) / fs1
# 5分 = 300秒
dur_sec <- 5 * 60
n_5min1 <- fs1 * dur_sec

if (n_all1 < 2 * n_5min1) {
  stop("EEG(ch1) のデータ長が不足しているため、最初と最後の5分を両方取り出せません。")
}

## EEG: 最初の5分
idx1_first    <- 1:n_5min1
elapsed1_first <- (idx1_first - 1) / fs1
t1_first      <- start_time1 + elapsed1_first
sig1_first    <- sig1[idx1_first]
## EEG: 最後の5分
idx1_last    <- (n_all1 - n_5min1 + 1):n_all1
elapsed1_last <- (idx1_last - 1) / fs1
t1_last      <- start_time1 + elapsed1_last
sig1_last    <- sig1[idx1_last]

## ==== ECG (ch2) の情報を取り出す ===================================
sig2   <- ch2$signal          # ECG 波形ベクトル
fs2    <- ch2$sRate           # サンプリング周波数 [Hz]
n_all2 <- length(sig2)        # 全サンプル数
start_time2 <- as.POSIXct(ch2$startTime)
end_time2   <- start_time2 + (n_all2 - 1) / fs2
n_5min2 <- fs2 * dur_sec

if (n_all2 < 2 * n_5min2) {
  stop("ECG(ch2) のデータ長が不足しているため、最初と最後の5分を両方取り出せません。")
}

## ECG: 最初の5分
idx2_first     <- 1:n_5min2
elapsed2_first <- (idx2_first - 1) / fs2
t2_first       <- start_time2 + elapsed2_first
sig2_first     <- sig2[idx2_first]
## ECG: 最後の5分
idx2_last     <- (n_all2 - n_5min2 + 1):n_all2
elapsed2_last <- (idx2_last - 1) / fs2
t2_last       <- start_time2 + elapsed2_last
sig2_last     <- sig2[idx2_last]

## ==== プロット(2×2) =============================================
par(mfrow = c(2, 2), mar = c(4, 4, 3, 2))
## (1) 上段左:EEG 最初の5分
plot(
  t1_first, sig1_first,
  type = "l",
  xlab = "Time",
  ylab = "EEG [uV]",
  main = sprintf("EEG: First 5 min (Start: %s)",
                 format(start_time1, "%Y-%m-%d %H:%M:%S")),
  col  = 2,
  xaxs = "i",
  xaxt = "n"
)
axis.POSIXct(1, t1_first, format = "%H:%M")
## (2) 上段右:EEG 最後の5分
plot(
  t1_last, sig1_last,
  type = "l",
  xlab = "Time",
  ylab = "EEG [uV]",
  main = sprintf("EEG: Last 5 min (End: %s)",
                 format(end_time1, "%Y-%m-%d %H:%M:%S")),
  col  = 2,
  xaxs = "i",
  xaxt = "n"
)
axis.POSIXct(1, t1_last, format = "%H:%M")
## (3) 下段左:ECG 最初の5分
plot(
  t2_first, sig2_first,
  type = "l",
  xlab = "Time",
  ylab = "ECG [uV]",
  main = sprintf("ECG: First 5 min (Start: %s)",
                 format(start_time2, "%Y-%m-%d %H:%M:%S")),
  col  = 2,
  xaxs = "i",
  xaxt = "n"
)
axis.POSIXct(1, t2_first, format = "%H:%M")
## (4) 下段右:ECG 最後の5分
plot(
  t2_last, sig2_last,
  type = "l",
  xlab = "Time",
  ylab = "ECG [uV]",
  main = sprintf("ECG: Last 5 min (End: %s)",
                 format(end_time2, "%Y-%m-%d %H:%M:%S")),
  col  = 2,
  xaxs = "i",
  xaxt = "n"
)
axis.POSIXct(1, t2_last, format = "%H:%M")

Rスクリプトの実行例

4. 読み込んだ情報で「まず見るべきポイント」

 実際に EDF を R に読み込んだとき、最初にチェックしておくと良い項目を整理しておきます。

(1) 記録のメタデータ

hdr$patientID
hdr$recordingID
hdr$startDate
hdr$startTime
hdr$duration      # 記録全体の長さ(秒)
  • 誰のデータか(匿名化されているか)
  • いつ・どの条件で計測されたか(薬剤投与、睡眠、安静閉眼など)
  • 記録の長さ(30秒×nレコード → 睡眠全体、など)

(2) チャネル構成とサンプリング

sinfo$label
fs <- sinfo$samplesPerRecord / hdr$recordDuration
data.frame(label = sinfo$label, fs = fs)
  • EEG チャネル(Fp1、Fp2、C3、C4、O1、O2 など)が揃っているか
  • EOG / EMG / ECG / 呼吸 / SpO2 といった補助チャネルの有無
  • チャネルごとのサンプリング周波数が妥当か(例:EEG 256 Hz、ECG 1000 Hz など)

(3) 単位とレンジ

sinfo$physicalDim
sinfo$physicalMin
sinfo$physicalMax
sinfo$digitalMin
sinfo$digitalMax
  • µVレンジが狭すぎないか(クリッピングしていないか)
  • デジタルレンジ(-32768 ~ 32767 など)に対して物理レンジが設定されているか
  • 後で自分で物理量に変換する必要があるかどうかを確認

(4) 注釈チャネル(EDF+ の場合)

 labelAnnotationsEDF Annotations などの名前がついたチャネルがあれば、そこに

  • スリープステージ(N1、N2、N3、REM など)
  • 発作、けいれん、アーティファクトなどのイベント
  • 刺激提示のタイミング(音・光刺激)

などの情報が入っている可能性があります。edfReader には注釈を扱うための関数もあるので、必要に応じて利用してください。

5. まとめ

 今回は、EDF ファイルの構造を理解しながら、R を使ってヘッダ情報と実際の波形データ(EEG・ECG)を読み込む方法を紹介しました。EDF 形式は、生体信号の国際的な標準であり、医療・研究の幅広い場面で利用されています。その分、ファイルサイズが非常に大きくなることも多く、数百 MB〜数 GB になることも珍しくありません。
 そのため、読み込みに時間がかかることがあります。古いPCやメモリの少ない環境では、処理が重く感じられるかもしれません。特に睡眠ポリグラフ(PSG)や長時間脳波記録になると、扱うデータ量は膨大になります。
 また、生体信号データは長時間かつ多チャンネルで記録されるため、Excel などの表計算ソフトではほとんど歯が立ちません。行数の制限にもすぐ到達してしまい、データの一部しか読み込めない、固まってしまう、といった問題が発生します。
 したがって、EDF のような大規模データを扱うには、R や Python のようなプログラミング環境を使いこなすことが必須です。これらのツールを使うことで、数百万〜数千万点のデータを効率よく処理し、フィルタリングやスペクトル解析、イベント検出、可視化などの高度な解析が可能になります。
 今回紹介したスクリプトは基本的な読み込み手順の一例ですが、実際の研究では、前処理・ノイズ除去・イベント解析・機械学習など、多くの応用が可能です。ぜひ少しずつコードに慣れ、自分のデータに合わせた解析フローを構築してみてください。

※ もし記事の中で「ここ違うよ」という点や気になるところがあれば、気軽に指摘していただけると助かります。質問や「このテーマも取り上げてほしい」といったリクエストも大歓迎です。必ず対応するとは約束できませんが、できるだけ今後の記事で扱いたいと思います。それと、下のはてなブログランキングはあまり信用できる指標ではなさそうですが (私のブログを読んでいる人は、実際とても少ないです)、押してもらえるとシンプルに励みになります。気が向いたときにポチッとしていただけたら嬉しいです。

■ おまけ(全体を実行するRスクリプト

 いちいち、上の説明を読みながらRで実行するのが面倒な人は、以下のスクリプトを参考にしてください。

############################################################
# EDF ファイル読み込み・プロット・記録情報確認 一括スクリプト
############################################################

#############################
# 0. パッケージのインストール&読み込み
#############################

pkg <- "edfReader"

# インストールされていなければ自動インストール
if (!requireNamespace(pkg, quietly = TRUE)) {
  install.packages(pkg)
}

# ライブラリ読み込み
library(edfReader)


#############################
# 1. 読み込む EDF ファイルの指定
#############################

# ★自分の環境に合わせてここを書き換えてください
DIR      <- r"(D:\Document\研究)"  # EDFファイルが入っているフォルダ
edf_file <- "DATA_001.edf"              # 読み込む EDF ファイル名

setwd(DIR)


#############################
# 2. EDF ヘッダ情報の読み込み
#############################

hdr <- readEdfHeader(edf_file)

# 基本情報の確認(必要なら print()/str() で詳細を確認)
patientID   <- hdr$patientID      # 患者IDなど(匿名化されていると NULL のことも多い)
recordingID <- hdr$recordingID    # 記録条件(機器名・モンタージュ情報など)
startTime   <- hdr$startTime      # 記録開始時刻(POSIXlt)
nSignals    <- hdr$nSignals       # チャネル数
sinfo       <- hdr$sHeaders       # 各チャネルのメタ情報(data.frame)

# チャネルが 2 未満ならここで終了
if (nSignals < 2) {
  stop("このスクリプトは少なくとも 2 チャネル(EEG と ECG)を想定しています。")
}


#############################
# 3. チャネル情報とサンプリング周波数の確認
#############################

# シグナルヘッダの先頭をざっと確認
# print(head(sinfo))

# サンプリング周波数(Hz)を計算
# EDF はサンプリング周波数を直接持たないため、
# 「1レコードあたりのサンプル数 / レコード長」で求める
fs_vec <- sinfo$samplesPerRecord / hdr$recordDuration

# チャネルごとのサンプリング周波数一覧
fs_table <- data.frame(
  label = sinfo$label,
  sampling_rate_Hz = fs_vec,
  physicalDim = sinfo$physicalDim,
  physicalMin = sinfo$physicalMin,
  physicalMax = sinfo$physicalMax,
  digitalMin  = sinfo$digitalMin,
  digitalMax  = sinfo$digitalMax
)

# (あとでまとめて表示するので、ここでは計算だけ)


#############################
# 4. 波形データの読み込み(全チャネル)
#############################

signals <- readEdfSignals(hdr)

# 利用可能なチャネル名を確認(必要に応じて)
# print(names(signals))

# ここでは「1番目を EEG(ch1)、2番目を ECG(ch2)」と仮定
ch1_name <- sinfo$label[1]
ch2_name <- sinfo$label[2]

ch1 <- signals[[ch1_name]]  # EEG
ch2 <- signals[[ch2_name]]  # ECG


#############################
# 5. EEG / ECG の最初・最後5分を取り出してプロット
#############################

## ==== EEG (ch1) の情報 ====================================

sig1   <- ch1$signal          # EEG 波形ベクトル
fs1    <- ch1$sRate           # サンプリング周波数 [Hz]
n_all1 <- length(sig1)        # 全サンプル数

# 記録開始・終了時刻(POSIXctへ変換)
start_time1 <- as.POSIXct(ch1$startTime)
end_time1   <- start_time1 + (n_all1 - 1) / fs1

# 5分 = 300秒
dur_sec <- 5 * 60
n_5min1 <- fs1 * dur_sec

if (n_all1 < 2 * n_5min1) {
  stop("EEG(ch1) のデータ長が不足しているため、最初と最後の5分を両方取り出せません。")
}

# EEG: 最初の5分
idx1_first      <- 1:n_5min1
elapsed1_first  <- (idx1_first - 1) / fs1
t1_first        <- start_time1 + elapsed1_first
sig1_first      <- sig1[idx1_first]

# EEG: 最後の5分
idx1_last       <- (n_all1 - n_5min1 + 1):n_all1
elapsed1_last   <- (idx1_last - 1) / fs1
t1_last         <- start_time1 + elapsed1_last
sig1_last       <- sig1[idx1_last]


## ==== ECG (ch2) の情報 ====================================

sig2   <- ch2$signal          # ECG 波形ベクトル
fs2    <- ch2$sRate           # サンプリング周波数 [Hz]
n_all2 <- length(sig2)        # 全サンプル数

start_time2 <- as.POSIXct(ch2$startTime)
end_time2   <- start_time2 + (n_all2 - 1) / fs2

n_5min2 <- fs2 * dur_sec

if (n_all2 < 2 * n_5min2) {
  stop("ECG(ch2) のデータ長が不足しているため、最初と最後の5分を両方取り出せません。")
}

# ECG: 最初の5分
idx2_first     <- 1:n_5min2
elapsed2_first <- (idx2_first - 1) / fs2
t2_first       <- start_time2 + elapsed2_first
sig2_first     <- sig2[idx2_first]

# ECG: 最後の5分
idx2_last      <- (n_all2 - n_5min2 + 1):n_all2
elapsed2_last  <- (idx2_last - 1) / fs2
t2_last        <- start_time2 + elapsed2_last
sig2_last      <- sig2[idx2_last]


## ==== プロット(2×2:EEG/ECG × 最初/最後5分) ==============

par(mfrow = c(2, 2), mar = c(4, 4, 3, 2))

# (1) 上段左:EEG 最初の5分
plot(
  t1_first, sig1_first,
  type = "l",
  xlab = "Time",
  ylab = paste0(ch1_name, " [", ch1$range, "]"),
  main = sprintf("EEG: First 5 min (Start: %s)",
                 format(start_time1, "%Y-%m-%d %H:%M:%S")),
  col  = 2,
  xaxs = "i",
  xaxt = "n"
)
axis.POSIXct(1, t1_first, format = "%H:%M")

# (2) 上段右:EEG 最後の5分
plot(
  t1_last, sig1_last,
  type = "l",
  xlab = "Time",
  ylab = paste0(ch1_name, " [", ch1$range, "]"),
  main = sprintf("EEG: Last 5 min (End: %s)",
                 format(end_time1, "%Y-%m-%d %H:%M:%S")),
  col  = 2,
  xaxs = "i",
  xaxt = "n"
)
axis.POSIXct(1, t1_last, format = "%H:%M")

# (3) 下段左:ECG 最初の5分
plot(
  t2_first, sig2_first,
  type = "l",
  xlab = "Time",
  ylab = paste0(ch2_name, " [", ch2$range, "]"),
  main = sprintf("ECG: First 5 min (Start: %s)",
                 format(start_time2, "%Y-%m-%d %H:%M:%S")),
  col  = 2,
  xaxs = "i",
  xaxt = "n"
)
axis.POSIXct(1, t2_first, format = "%H:%M")

# (4) 下段右:ECG 最後の5分
plot(
  t2_last, sig2_last,
  type = "l",
  xlab = "Time",
  ylab = paste0(ch2_name, " [", ch2$range, "]"),
  main = sprintf("ECG: Last 5 min (End: %s)",
                 format(end_time2, "%Y-%m-%d %H:%M:%S")),
  col  = 2,
  xaxs = "i",
  xaxt = "n"
)
axis.POSIXct(1, t2_last, format = "%H:%M")


#############################
# 6. 記録情報を整理
#############################
summary_text <- paste0(
  "\n==============================\n",
  "  EDF 記録メタデータ\n",
  "==============================\n",
  "ファイル名         : ", edf_file, "\n",
  "患者 ID           : ", ifelse(is.null(patientID), "NULL", patientID), "\n",
  "記録条件          : ", ifelse(is.null(recordingID), "NULL", recordingID), "\n",
  "記録開始時刻      : ", format(as.POSIXct(startTime), "%Y-%m-%d %H:%M:%S %Z"), "\n",
  "チャネル数        : ", nSignals, "\n",
  "記録全体の長さ(秒): ",
      if (!is.null(hdr$duration)) hdr$duration else "(duration 情報なし)",
  "\n\n",

  "==============================\n",
  "  チャネル情報とサンプリング周波数\n",
  "==============================\n\n",
  paste(capture.output(print(fs_table)), collapse = "\n"), "\n"
)

# 表示
cat(summary_text)