import subprocess
import sys
# 必要なライブラリをリストにまとめる
= [
required_libraries 'json',
'pandas',
'matplotlib',
'pytz',
'ipywidgets',
'numpy',
'seaborn',
'ipyfilechooser',
'plotly'
]
# ライブラリのインストール関数
def install_and_import(library):
try:
__import__(library)
except ImportError:
"-m", "pip", "install", library])
subprocess.check_call([sys.executable, __import__(library)
# 各ライブラリのインストールとインポート
for library in required_libraries:
install_and_import(library)
# インポート文
import json
import pandas as pd
import os
import matplotlib.pyplot as plt
'font.family'] = 'Yu Mincho', #'Hiragino Kaku Gothic ProN', #'Meiryo', #'Noto Sans CJK JP'
plt.rcParams[import matplotlib.dates as mdates
import pytz
import ipywidgets as widgets
import numpy as np
import seaborn as sns
import plotly.graph_objects as go
import plotly.express as px
import plotly.io as pio
from ipywidgets import DatePicker, Button, HBox
from matplotlib.lines import Line2D
from pytz import timezone
from IPython.display import display, clear_output
from ipyfilechooser import FileChooser
from datetime import datetime
from datetime import timedelta
print("All libraries are installed and imported successfully.")
Google Fitに集約した睡眠データをローカル環境で可視化するJupyter Notebookを作成しました
1. 要旨
Google Fitに集約された睡眠データをダウンロードし、ローカル環境で分析をしたいと思いJupyter Notebook(.ipynb
ファイル)を作成しました。 このノートブックでは、事前にダウンロードした睡眠データ(.json
ファイル)を用いて、四半期ごとあるいは任意の期間のダブルプロットアクトグラム(+睡眠ステージ), 四半期ごとの睡眠時間及び質の推移, 四半期ごとの曜日別睡眠時間及び睡眠の質, 任意の日付の睡眠ステージの推移を可視化することが出来ます。 このノートブックの新規性として、複数デバイスによる観測に対応していること, 一般的な睡眠分析アプリには見られない「ダブルプロットのアクトグラム」を採用し、なおかつ色分けにより睡眠ステージが分かるようになっているという2つが挙げられます。特にダブルプロットアクトグラムを採用することで、従来の睡眠記録の可視化手法では難しかった、昼寝を含む全ての睡眠を直感的に理解しやすい形で表現することができます。
2. 背景
筆者はガジェットオタクであり、両腕に付けていたスマートウォッチや、睡眠マットなど、様々な機器で睡眠記録を(半ば自動的に)記録しGoogle Fitに集約していました。
しかし、Google Fitの睡眠可視化機能は複数のデバイスからの記録を想定しておらず、またクラウドに保存しているせいかレスポンスも非常に悪く使いにくさを感じています。
そのため、睡眠分析に興味はあったもののそのデータを活用するという気が起きなかったのですが、Pokémon Sleepに触発され、もっとしっかり自分の睡眠を分析したいという気持ちが芽生えたためこのプロジェクトを立ち上げました。
2-1. 動機
睡眠データを記録するデバイス・アプリは数多くあります。例えば、Sleep as AndroidやPokémon Sleepなど、スマートフォンをセンサーにするタイプのものは手軽な反面、手動で睡眠記録の測定を行う必要があったり(寝落ちして忘れることも)、バッテリー消費が激しいためスマートフォンを充電し続ける必要があります。
スマートウォッチ・スマートバンドを持っている場合、たいていのものは睡眠記録を自動で付けてくれるため更に手軽(寝落ちして忘れる心配もない)ですが、途中でバッテリーが切れてしまったり、睡眠に本腰をいれてないのか、精度が低かったり分析アプリのデザインがイマイチだったりします1。
その点、Pokémon Sleepで言うところのPokémon GO Plus +
や、Withingsの睡眠マット(Withings Sleep WSM02-ALL-JP
)は、精度が良くアプリも見やすくて良いですが、睡眠のためのデバイスを買うぞという覚悟(と投資)が必要です。
…と、ここまで長々と筆者の睡眠デバイスの経歴を語ってしまいましたが、これらは基本的に独自の手法・基準でデータ分析を行い可視化します。
筆者のように、複数のデバイスを使っている場合、それぞれのデバイスとセットで提供されるアプリを見比べることになるのですが、アプリが違うのでデザイン(測定項目・グラフなど)は全く統一されておらず、比較は困難です。
また、睡眠分析アプリは昼寝に対応しているものが少なく、可視化の際に昼寝が表示されない・あるいは昼寝を可視化に含めたせいでグラフのX軸が広くなってしまい見にくくなることを不満にも思っていました。
幸い、ほとんどのアプリは睡眠記録をGoogle Fitに自動でアップロードしてくれるので、Google Fitを使えば昼寝の件はともかく複数デバイス問題は解決する…ように思えたのですが、前述の通りGoogle Fitは複数のデバイスで同時に睡眠を記録することは想定しておらず、記録自体は保存してくれるのですが、睡眠記録を確認(可視化)する際に、デバイスAの結果を採用する日もあればデバイスBの記録を採用する日もあるといった感じです。というか、表示できればまだ良い方で、大抵の場合(複数デバイスのせいで)エラーが出るのかタイムアウトしてしまい、結果が見れないことのほうが多いです。
このような背景から、だったら自分で(複数デバイスで記録した)睡眠データを可視化・分析するツールを作ろうと思い立ちました。
2-2. 技術的背景
2024年5月現在、筆者の睡眠データは図 1のように記録・可視化され、集約されています。使用デバイス・アプリこそ違えど、Google Fitにデータを集約している方はこのようなワークフローでデータをエクスポートしているはずです。
肝となるのは、各デバイスで計測した睡眠データを各アプリがGoogle Fit APIに基づいてJSON形式に加工しているという点です。これによりデバイスが違っても同じフォーマットでデータを取り扱うことができます。
2-2-1. Google Fit APIについて
各アプリ・デバイスで計測した生データは、(Google Fitと連携させた場合)Google Fit APIを使ってJSON形式でアップロードされます。
データには睡眠ステージ, その睡眠ステージの開始時刻, 終了時刻が含まれています。睡眠ステージの判断は大元である各デバイス・アプリに任せられているようです。
サンプルJSONデータ.json
{
"Data Source": "raw:com.google.sleep.segment:com.hoge",
"Data Points": [
{"fitValue":[{"value":{"intVal":1}}],"originDataSourceId":"","endTimeNanos":1620291780000000000,"dataTypeName":"com.google.sleep.segment","startTimeNanos":1620285780000000000,"modifiedTimeMillis":1624604115227,"rawTimestampNanos":0},
{"fitValue":[{"value":{"intVal":3}}],"originDataSourceId":"","endTimeNanos":1620338280000000000,"dataTypeName":"com.google.sleep.segment","startTimeNanos":1620313260000000000,"modifiedTimeMillis":1624604115227,"rawTimestampNanos":0},
{"fitValue":[{"value":{"intVal":5}}],"originDataSourceId":"","endTimeNanos":1620379200000000000,"dataTypeName":"com.google.sleep.segment","startTimeNanos":1620367020000000000,"modifiedTimeMillis":1624604115227,"rawTimestampNanos":0},
{"fitValue":[{"value":{"intVal":6}}],"originDataSourceId":"","endTimeNanos":1620435480000000000,"dataTypeName":"com.google.sleep.segment","startTimeNanos":1620412740000000000,"modifiedTimeMillis":1624604117511,"rawTimestampNanos":0},
{"fitValue":[{"value":{"intVal":1}}],"originDataSourceId":"","endTimeNanos":1620450300000000000,"dataTypeName":"com.google.sleep.segment","startTimeNanos":1620443520000000000,"modifiedTimeMillis":1624604117511,"rawTimestampNanos":0},
]
}
睡眠ステージの内訳は以下の通りです(Google for Developers 2023)。
睡眠ステージのタイプ | 値 |
---|---|
覚醒(睡眠サイクル中) | 1 |
睡眠 | 2 |
ベッド外 | 3 |
浅い睡眠 | 4 |
深い睡眠 | 5 |
レム睡眠 | 6 |
懸念事項として、Google Fit APIが終了しHealth Connect API(?)へと移行されること(Google for Developers 2023)が挙げられます。今後、睡眠データはGoogle FitではなくHealth Connectに集約されることとなるでしょう。データのフォーマットが変わってしまうと、このノートブックの前提が崩れてしまうので、詳細が分かり次第対応していきたいと思います。
2-2-2. Python及びJupyter Notebookの採用理由
何を使って睡眠データの可視化・分析ツールを作るかは少し悩みました。R言語は、必要最低限の読み書きができるものの、ChatGPTをはじめとする生成AIの支援を受けにくいこと, 自分以外の人が使う際に配布・再現しにくいことがネックだったため、Pythonを採用しました。
Pythonは print("Hello World!")
くらいしか知らないためChatGPTにあれこれ聞き、最終的にPythonというかJupyter Notebookを採用することになりました。
データの読み込み, データ属性の入力, 日付やデータ期間の選択といった作業を、スクリプトを書き換えるのではなくユーザーが直感的に分かりやすくボタン操作などで行える点(図 2)が、筆者の持つPython並びにJupyter Notebookへの苦手意識を上回ったからです。
2-3. 可視化手法
前述の通り、既存の睡眠分析・可視化アプリには、昼寝に対応しているものが少なく、可視化の際に昼寝が表示されない・あるいは昼寝を可視化に含めたせいでグラフのX軸が広くなってしまい見にくくなる(図 3)という問題がありました。
図中、上段はWithings Sleepの睡眠可視化,下段はXiaomi MiBand8のサードパーティアプリNotify
他にも、例えばPokémon SleepやSleep as Androidの睡眠周期の可視化手法には、軸が24時間以下なのでイレギュラーな睡眠に弱く、その範囲を超えてしまうと見切れてしまうという問題があります(図 4)。
図中、左はSleep as Android, 右はPokémon Sleep
こうした問題(図 3, 図 4)を解決する可視化手法が、ダブルプロットされたアクトグラム(+睡眠ステージも色で表示した筆者オリジナルのもの)です(図 5)。
.ipynb
ファイル)を参照筆者は生物学を専攻していたこともあり、このアクトグラムというものを概日リズム(サーカディアンリズム; circadian rhythm)の説明の中で知りました。
概日リズムというのは要するに体内時計のことで、恒常環境下(例えばずっと真っ暗闇な状態)でも約24時間の周期を示し, (光)同調性を持ち(光によってリセットされ), 温度補償性を持っているものとされています(Carl Hirschie Johnson & Foster 2003)。概日リズム自体はそれ以前より何となく知られていたものではありましたが、概日リズムの基礎となったのはモデル生物であるショウジョウバエを使った研究で、そのルーツは時間生物学(chronobiology)の創立者の一人であるコリン・ピッテンドリー(Colin Pittendrigh)であると言えるでしょう(Tataroglu & Emery 2014)。
概日リズムやそれを司る時計遺伝子2に関しては興味があればググっていただくとして、ここでは概日リズムの可視化に用いられる手法の1つであるアクトグラムについて解説していきます。
アクトグラムは1926年にメイナード ジョンソン(Maynard Johnson)によって導入されたと言われています(Refinetti et al. 2007)。彼の発明したアクトグラムという可視化手法は活動のリズムが視覚的に分かりやすいという点で優れています(図 6)。 しかし、彼の発明したアクトグラムは24時間(1日区切り)であったため、24時間を超えるリズムに気付きにくいという欠点がありました。学術的な意味合いはともかく、筆者が実現したかった昼寝や3交代制など不規則な睡眠を見切れることなく表示するには24時間の軸では足りません。
これを解決するのが、24時間のアクトグラムを2つ並べたDouble-plotted actogramで、(少なくとも筆者の調べた限り)1960年にコリン・ピッテンドリー(Colin Pittendrigh)が”Circadian Rhythms and the Circadian Organization of Living Systems”という論文(Pittendrigh 1960)で用いたものが初出だと思います(図 7)。
ということで、今回はダブルプロットのアクトグラムを筆者なりに改良した可視化手法で睡眠の周期を分析することにしました(図 5)。
3. 材料と方法
このJupyter Notebook(.ipynb
ファイル)は、Google Fitからダウンロードした睡眠データ(.json
), VS Code及びPython, Jupyterの拡張機能を必要とします。Google Colabでは動作しません。また、それ以外の環境では検証していません。
VS Codeで実行することで、
- 睡眠データの分析は全てローカル環境で実行される
- 大容量のデータも扱える
- データが外部にアップロードされることがない というメリットが生まれます。
このJupyter Notebook(.ipynb
ファイル)はモジュール式となっているため、個別の可視化手法を必要に応じて実行することが可能です。
今のところ、四半期ごとのアクトグラム(図 8), 任意の期間のアクトグラム及びミッドスリープタイム(図 9), 四半期ごとの睡眠時間及び睡眠の質の推移の可視化(図 10 (a))及び四半期の範囲で曜日別の睡眠時間及び睡眠の質の可視化(図 10 (b)), 任意の日付の睡眠記録(睡眠ステージのサイクル)の可視化(図 11)が行えます。
3-1. 必要なもの
前述の通り、このJupyter Notebook(.ipynb
ファイル)は以下のものを必要とします。
- GitHubからダウンロードしたJupyter Notebook(
.ipynb
ファイル) - Google Fitからダウンロードした睡眠データ(
.json
ファイル) - VS Code(Google Colabでは実行できません)
- Python拡張機能
- Jupyter拡張機能
3-2. 実行方法
Google データ エクスポートにアクセスしGoogle Fitのデータをダウンロードした後(データのエクスポートをリクエストしてからダウンロード可能になるまで、数時間~数日かかります)、Jupyter Notebook(.ipynb
ファイル)を、1セルずつ順番に実行してください(Step by Stepでファイルの読み込みや期間の設定をするため 「すべてを実行」で一括して行うことはできません。)
3-2-1. Google Fit からデータをダウンロードする方法
Google データ エクスポートにアクセスします。
追加するデータの選択
にて選択をすべて解除
します(余計なデータが多すぎるため)。Fit
(Google Fit)のみを選択します。一番下までスクロールし、
次のステップ
をクリックします。ファイル形式、エクスポート回数、エクスポート先の選択をします。
エクスポート先
はダウンロードリンクをメールで送信
を選択します頻度
は1回エクスポート
を選択しますファイル形式
は.zip
を選択しますファイルサイズ
は50GB
を選択します(念の為)- 上記を確認した後、
エクスポートを作成
をクリックします
データのエクスポートが処理され、ダウンロードリンクがメールで送られていくるまで数時間から数日かかります。気長に待ちます(ページを閉じても大丈夫です)。
- 進捗はGoogle データ エクスポートにて確認できます
- メールはデータのエクスポートをリクエストしたGoogleアカウント宛(gmail)に届きます
ダウンロードリンクが書かれたメールを開き、ダウンロードページに跳びます(
ファイルをダウンロード
をクリックします)。- メールの件名は
Google データをダウンロードできるようになりました
でした
- メールの件名は
Googleアカウントのパスワードを入力後、データをダウンロードします。
- 筆者の場合、エラーが含まれていました(が、どうしようもないので続行します)
以上で、Google Fitデータのダウンロードは終了です。
3-2-2. データ分析の流れ
モジュールのインポート
データの取り込み
(手動の睡眠記録がある場合)データセットの選択
(必要であれば)データをCSVファイルとしてエクスポート
アクトグラムを用いた四半期ごとの睡眠記録の可視化
四半期ごとの統計
任意の日付の睡眠記録の可視化
3-2-2-1. モジュールのインポート
以下のセルを実行し、(もしそのモジュールがない場合、自動で)必要なモジュールをインポートします。
import subprocess
import sys
# 必要なライブラリをリストにまとめる
= [
required_libraries 'json',
'pandas',
'matplotlib',
'pytz',
'ipywidgets',
'numpy',
'seaborn',
'ipyfilechooser',
'plotly'
]
# ライブラリのインストール関数
def install_and_import(library):
try:
__import__(library)
except ImportError:
"-m", "pip", "install", library])
subprocess.check_call([sys.executable, __import__(library)
# 各ライブラリのインストールとインポート
for library in required_libraries:
install_and_import(library)
# インポート文
import json
import pandas as pd
import os
import matplotlib.pyplot as plt
'font.family'] = 'Yu Mincho', #'Hiragino Kaku Gothic ProN', #'Meiryo', #'Noto Sans CJK JP'
plt.rcParams[import matplotlib.dates as mdates
import pytz
import ipywidgets as widgets
import numpy as np
import seaborn as sns
import plotly.graph_objects as go
import plotly.express as px
import plotly.io as pio
from ipywidgets import DatePicker, Button, HBox
from matplotlib.lines import Line2D
from pytz import timezone
from IPython.display import display, clear_output
from ipyfilechooser import FileChooser
from datetime import datetime
from datetime import timedelta
print("All libraries are installed and imported successfully.")
3-2-2-2. データの取り込み
以下のセルを実行し、
- データの読み込み
- データの変換 を行います。
VS Codeで実行している場合、睡眠データの分析は全てローカル環境で実行されます。 また、データが外部にアップロードされることはありませんのでご安心ください。
睡眠データの読み込み
# ファイルアップロードウィジェットの作成
= widgets.FileUpload(
uploader ='.json', # JSONファイルのみを許可
accept=True, # 複数のファイルをアップロード可能
multiple='Upload JSON files'
description
)
= False
file_loading_flag
# アップロードされたデータを処理する関数
def process_uploaded_files(change):
global file_loading_flag
# 処理中メッセージを表示
with output:
clear_output()print("ファイルの処理中です。次のセルには進まないでください。")
# 出力を即座にフラッシュ
sys.stdout.flush()
try:
for file_info in change['new']:
print(f"Processing {file_info['name']}")
# 出力を即座にフラッシュ
sys.stdout.flush() = file_info['content']
content = json.loads(content.tobytes().decode('utf-8'))
import_data = load_and_process_sleep_data(import_data, 'Type of Sleep')
df print("ファイルの読み込みが完了しました。次のセルに進んでください")
= True
file_loading_flag
with output:
if file_loading_flag is True:
clear_output()print("ファイルの読み込みが完了しました。次のセルに進んでください。")
# 出力を即座にフラッシュ
sys.stdout.flush()
except Exception as e:
with output:
clear_output()print(f"エラーが発生しました: {e}")
# 出力を即座にフラッシュ
sys.stdout.flush()
# JSONデータをDataFrameに変換するための関数
def load_and_process_sleep_data(import_data, type_value):
= import_data['Data Source']
data_source = import_data['Data Points']
data_points = pd.DataFrame([{
df 'data_source': data_source,
'start_time_ns': dp['startTimeNanos'],
'end_time_ns': dp['endTimeNanos'],
'sleep_state': dp['fitValue'][0]['value']['intVal'],
'modified_time_ms': dp['modifiedTimeMillis'],
'Type': type_value
for dp in data_points])
} 'start_time'] = pd.to_datetime(df['start_time_ns'], unit='ns')
df['end_time'] = pd.to_datetime(df['end_time_ns'], unit='ns')
df[return df
# 出力ウィジェットの作成
= widgets.Output()
output
# 初期メッセージの表示
with output:
print("ファイルの処理が完了するまで、次のセルには進まないでください")
# アップロードイベントに関数をバインド
='value')
uploader.observe(process_uploaded_files, names
# ウィジェットの表示
display(uploader) display(output)
この際に、ファイルの読み込みが完了しました。次のセルに進んでください
と表示されるまでは次のセルに進まないでください。
睡眠データのフォーマット関数など
def parse_datetime_with_format(dt_series):
= dt_series[dt_series.astype(str).str.contains(r"\.\d+")]
dt_series_with_ms = dt_series[~dt_series.astype(str).str.contains(r"\.\d+")]
dt_series_without_ms = pd.to_datetime(dt_series_with_ms, format='%Y-%m-%d %H:%M:%S.%f', errors='coerce')
parsed_with_ms = pd.to_datetime(dt_series_without_ms, format='%Y-%m-%d %H:%M:%S', errors='coerce')
parsed_without_ms return pd.concat([parsed_with_ms, parsed_without_ms]).sort_index()
# アップロードされたファイル名とデータの取得
= uploader.value uploaded_files
3-2-2-3. 手動で記録されたデータセットの指定
睡眠記録は、スマートウォッチや睡眠マットを使っている場合は自動で記録されますが、そうではない場合はスマートフォンのアプリ等を使って、手動で睡眠記録を開始・終了しているはずです。
手動で睡眠記録を開始・終了することで、布団(ベッド)に入った時刻が確実に分かるため、睡眠潜時(就寝してから入眠するまでにかかる時間)が算出できるため、手動・自動を区別しています。
以下のスクリプトでは、チェックボックスにチェックを入れることで、(手動で記録されたものがある場合)そのデータが手動か自動かを識別します。
手動睡眠記録の識別
# 説明文を表示
= widgets.Label('もし手動で睡眠記録を開始/停止したデータセットがあれば該当するものにチェックを入れてください。')
description_label
display(description_label)
# 空のデータフレームを初期化
= pd.DataFrame()
combined_data
# ファイルと対応するチェックボックスを表示
= []
checkboxes for file_details in uploader.value:
= widgets.Checkbox(
cb =False,
value='',
description=False
disabled
)= widgets.Label(file_details['name'])
label = widgets.HBox([cb, label])
box
checkboxes.append(cb)
display(box)
# プロセスボタンを作成
= widgets.Button(description="決定(データを処理)")
process_button
# ボタンのイベントハンドラー
def on_button_clicked(b):
=True)
clear_output(waitglobal combined_data
for cb, file_details in zip(checkboxes, uploader.value):
= file_details['name']
filename = file_details['content']
content = json.loads(content.tobytes().decode('utf-8'))
sleep_data
# チェックボックスの値に応じてデータタイプを設定
= 'Manual' if cb.value else 'Auto'
type_column_value = load_and_process_sleep_data(sleep_data, type_column_value)
data = pd.concat([combined_data, data], ignore_index=True)
combined_data
# データ処理後の状態を表示
print("Data processing complete. Dataframe contains:", combined_data.shape[0], "rows.")
process_button.on_click(on_button_clicked) display(process_button)
3-2-2-4. データをCSVとしてエクスポート
もし、他のツール(例えばChatGPTなど)で分析を行ないたい場合、Google Fitからダウンロードしたファイル(.json
形式)をそのまま用いるよりも、CSVファイルの方が便利です。
CSVファイルとしてデータをエクスポートしたい場合は、以下のセルを実行してください。Select
ボタンを押して保存するフォルダを指定し、再度Select
ボタンを押し、その下の✔Save
ボタンを押すことで、保存が行えます。 なお、このセルは任意(オプション)のため、実行しなくても問題ありません。
CSVとしてエクスポート
# CSVを保存する関数
# ユーザーのデスクトップパスを取得
= os.path.join(os.environ['USERPROFILE'], 'Desktop')
desktop_path
# CSVを保存する関数
def save_csv(sleep_data, path):
=False)
sleep_data.to_csv(path, indexreturn f'CSVファイルを {path}に保存しました。'
# 「Save」ボタンの動作を定義
def on_save_button_clicked(b):
with output:
clear_output()if not fc.selected:
print("CSVファイルの保存先を選択してください")
else:
# ここでDataFrameを保存
= save_csv(sleep_data, fc.selected) # dfは保存したいDataFrameの変数名
result print(result)
# ファイル選択ダイアログを設定
= FileChooser(desktop_path)
fc = 'sleep_data.csv'
fc.default_filename = True
fc.use_dir_icons
# 「Save」ボタンの作成
= widgets.Button(
save_button ='Save',
description='',
button_style='Click to save the CSV file',
tooltip='check'
icon
)
save_button.on_click(on_save_button_clicked)
# 出力エリアを設定
= widgets.Output()
output
# ウィジェットを表示
display(fc, save_button, output)
CSVファイルの構造は以下の通りです。
data_source
: インポートしたデータセットの名前です(基本的にはGoogle Fitから取得されたデバイスの名前です)Mid_sleep_time
: 算出されたミッドスリープタイム
Type
: 基本的にはAuto
かManual
のどちらかが入りますAuto
: 自動で睡眠記録が開始・停止していることを意味しますManual
: 手動で睡眠記録が開始・停止していることを意味しますOther
: 算出されたミッドスリープタイムであることを意味します
in_bed_time
: 手動で睡眠記録を開始している場合、その開始時刻が入りますstart_time
: 各睡眠ステージが開始された時刻で、協定世界時(UTC)となっています(日本標準時ではないことに注意してください)end_time
: 各睡眠ステージが終了した時刻で、協定世界時(UTC)となっています(日本標準時ではないことに注意してください)sleep_state
: [Google Fit](https://developers.google.com/fit/scenarios/read-sleep-data?hl=ja#sleep_stage_valuesで定められた睡眠ステージの値(1~6)と、算出したミッドスリープタイム(10)が入りますsession_id
: 1回の睡眠ごとに割り振られたIDです(end_time
から次のstart_time
までの間が2時間以上離れている場合、別の睡眠とみなしています)
睡眠ステージは以下の通りです。
睡眠ステージのタイプ | 値 |
---|---|
覚醒(睡眠サイクル中) | 1 |
睡眠 | 2 |
ベッド外 | 3 |
浅い睡眠 | 4 |
深い睡眠 | 5 |
レム睡眠 | 6 |
ミッドスリープタイム | 10 |
(おそらくですが)睡眠ステージ2はデータの信頼性が低く使われていない傾向にあります ミッドスリープタイムはオリジナル(Google Fitのデータ)にはない項目です
3-2-2-5. アクトグラムを用いた四半期ごとの睡眠記録の可視化
以下のセルを実行することで、アクトグラムの可視化を行えます。
- JSONファイルに含まれていたデータ期間に応じて、四半期(3ヶ月)ごとにアクトグラムがプロットされます
- 1年につき4枚グラフが出るので、含まれているデータ期間が長い場合は、全部のグラフが出力されるまで時間がかかります
- 任意の期間のアクトグラムを表示することも可能です
アクトグラムの表示を準備するセル
# 日またぎを処理する関数
def adjust_end_time(start, end):
if end < start:
+= 1440 # 翌日にまたがる場合は24時間分(分)を加算
end return end
def convert_to_jst_if_needed(column):
# タイムゾーン情報を確認し、必要に応じて変換を行う
if column.dt.tz is None:
# タイムゾーン情報がない場合、UTCとして解釈し、JSTに変換
return pd.to_datetime(column, utc=True).dt.tz_convert('Asia/Tokyo')
elif str(column.dt.tz) == 'UTC':
# タイムゾーンがUTCであれば、JSTに変換
return column.dt.tz_convert('Asia/Tokyo')
elif str(column.dt.tz) != 'Asia/Tokyo':
# タイムゾーンがJSTでない他のタイムゾーンであれば、JSTに変換
return column.dt.tz_convert('Asia/Tokyo')
else:
# 既にJSTであればそのまま返す
return column
def convert_sleep_data_to_jst(sleep_data):
# sleep_dataをコピーしてタイムゾーンを変換
= sleep_data.copy()
jst_sleep_data 'start_time'] = convert_to_jst_if_needed(jst_sleep_data['start_time'])
jst_sleep_data['end_time'] = convert_to_jst_if_needed(jst_sleep_data['end_time'])
jst_sleep_data[
# 明示的に datetime64[ns, Asia/Tokyo] にキャスト
'start_time'] = jst_sleep_data['start_time'].astype('datetime64[ns, Asia/Tokyo]')
jst_sleep_data['end_time'] = jst_sleep_data['end_time'].astype('datetime64[ns, Asia/Tokyo]')
jst_sleep_data[
return jst_sleep_data
def plot_actogram(sleep_data, start_date, end_date):
# タイムゾーン変換後のデータを取得
= convert_sleep_data_to_jst(sleep_data)
jst_sleep_data
# 指定された期間でデータをフィルタリング
= jst_sleep_data[
filtered_data 'start_time'] >= pd.Timestamp(start_date).tz_localize('Asia/Tokyo')) &
(jst_sleep_data['end_time'] <= pd.Timestamp(end_date).tz_localize('Asia/Tokyo'))
(jst_sleep_data[
].copy()
if filtered_data.empty:
print(f"No data available to plot between {start_date} and {end_date}.")
return
# 日またぎを考慮した時間の計算
'start_minutes'] = filtered_data['start_time'].apply(lambda dt: dt.hour * 60 + dt.minute)
filtered_data['end_minutes'] = filtered_data.apply(
filtered_data[lambda row: adjust_end_time(row['start_minutes'], row['start_minutes'] + (row['end_time'] - row['start_time']).seconds // 60), axis=1)
= {1: '#e0ffff', 2: '#b3e5fc', 3: '#ff5252', 4: '#03a9f4', 5: '#303f9f', 6: '#ab47bc', 10: 'yellow'}
color_map 'color'] = filtered_data['sleep_state'].map(color_map)
filtered_data[
= max(1, (pd.Timestamp(end_date) - pd.Timestamp(start_date)).days + 1)
num_days = plt.subplots(figsize=(20, num_days * 0.4))
fig, ax for _, row in filtered_data.iterrows():
= (row['start_time'] - pd.Timestamp(start_date).tz_localize('Asia/Tokyo')).days
day_of_week 'start_minutes'], row['end_minutes']], [day_of_week, day_of_week], color=row['color'], alpha=0.7)
ax.plot([row['start_minutes'] + 1440, row['end_minutes'] + 1440], [day_of_week + 1, day_of_week + 1], color=row['color'], alpha=0.7)
ax.plot([row[
0, 2880)
ax.set_xlim(0, num_days)
ax.set_ylim(range(num_days))
ax.set_yticks('Asia/Tokyo') + pd.Timedelta(days=x)).strftime('%Y-%m-%d') for x in range(num_days)])
ax.set_yticklabels([(pd.Timestamp(start_date).tz_localize('Time')
ax.set_xlabel('Days from Start Date')
ax.set_ylabel(f'Actogram from {start_date} to {end_date}')
plt.title(True)
plt.grid(=[i * 60 for i in range(49)], labels=[f'{(i % 24):02d}:00' if i % 2 == 0 else '' for i in range(49)], rotation=45)
plt.xticks(ticks
plt.show()
# データセットの範囲確認
= convert_sleep_data_to_jst(sleep_data)
jst_sleep_data = jst_sleep_data['start_time'].min().strftime('%Y-%m-%d')
start_date = jst_sleep_data['start_time'].max().strftime('%Y-%m-%d')
end_date print(f"This dataset contains data from {start_date} to {end_date}.")
上記のセルを実行すると、 > This dataset contains data from YYYY-MM-DD
to YYYY-MM-DD
と表示されます。ここに表示される期間がデータセットに含まれていた期間です。
全てのデータを可視化する場合は、以下のセルを実行してください(このセルを飛ばし、任意の期間だけを可視化することも可能です)。
全ての期間を対象に四半期ごとにアクトグラムをプロット
# 四半期毎にデータをプロット
= jst_sleep_data['start_time'].dt.year.min()
start_year = jst_sleep_data['start_time'].dt.year.max()
end_year = jst_sleep_data['start_time'].max()
last_date
for year in range(start_year, end_year + 1):
for quarter in range(1, 5):
= 3 * quarter - 2
start_month = 3 * quarter
end_month = pd.Timestamp(year=year, month=start_month, day=1).tz_localize('Asia/Tokyo')
quarter_start_date = pd.Timestamp(year=year, month=end_month, day=1).tz_localize('Asia/Tokyo') + pd.DateOffset(months=1) - pd.DateOffset(days=1)
quarter_end_date
if quarter_start_date > last_date:
break # この四半期の開始日がデータセットの最後の日を超えている場合はスキップ
if quarter_end_date > last_date:
= last_date # 四半期の終了日がデータセットの最後の日を超えている場合は調整
quarter_end_date
'%Y-%m-%d'), quarter_end_date.strftime('%Y-%m-%d')) plot_actogram(jst_sleep_data, quarter_start_date.strftime(
任意の期間を選択し、可視化したい場合は以下のセルを実行してください。
- 以下のセルを実行することで、任意の期間に絞ったアクトグラムをブラウザ上に表示できます
- データの期間は四半期(3ヶ月)を推奨しています
- 四半期より長い期間を選択した場合、Y軸の文字が潰れてしまいます
- このセルでは、ミッドスリープタイムも併せて表示しています
- このセルを実行しなくても問題はありません
任意の期間に絞ったアクトグラムのプロット
# 任意の期間のアクトグラム
def plot_interactive_actogram(sleep_data, start_date, end_date):
# タイムゾーン変換後のデータを取得
= convert_sleep_data_to_jst(sleep_data)
jst_sleep_data
# 指定された期間でデータをフィルタリング
= jst_sleep_data[
filtered_data 'start_time'] >= pd.Timestamp(start_date).tz_localize('Asia/Tokyo')) &
(jst_sleep_data['end_time'] <= pd.Timestamp(end_date).tz_localize('Asia/Tokyo'))
(jst_sleep_data[
].copy()
if filtered_data.empty:
print(f"No data available to plot between {start_date} and {end_date}.")
return
# 日またぎを考慮した時間の計算
'start_minutes'] = filtered_data['start_time'].apply(lambda dt: dt.hour * 60 + dt.minute)
filtered_data['end_minutes'] = filtered_data.apply(
filtered_data[lambda row: adjust_end_time(row['start_minutes'], row['start_minutes'] + (row['end_time'] - row['start_time']).seconds // 60), axis=1)
= {
color_map 1: '#e0ffff', # 覚醒(睡眠サイクル中)
2: '#b3e5fc', # 睡眠
3: '#ff5252', # ベッド外
4: '#03a9f4', # 浅い睡眠
5: '#303f9f', # 深い睡眠
6: '#ab47bc', # レム睡眠
10: 'black' # ミッドスリープタイム(色を強調)
}= {
sleep_stage_labels 1: '覚醒(睡眠サイクル中)',
2: '睡眠',
3: 'ベッド外',
4: '浅い睡眠',
5: '深い睡眠',
6: 'レム睡眠',
10: 'ミッドスリープタイム'
}'color'] = filtered_data['sleep_state'].map(color_map)
filtered_data[
= max(1, (pd.Timestamp(end_date) - pd.Timestamp(start_date)).days + 1)
num_days = go.Figure()
fig
for sleep_state, color in color_map.items():
= filtered_data[filtered_data['sleep_state'] == sleep_state]
sleep_state_data if not sleep_state_data.empty:
for _, row in sleep_state_data.iterrows():
= (row['start_time'] - pd.Timestamp(start_date).tz_localize('Asia/Tokyo')).days
day_of_week = 7.5 if sleep_state == 10 else 5 # ミッドスリープタイムの場合は線の太さを15に設定
line_width = 0 if sleep_state == 10 else 0 # ミッドスリープタイムの場合はy座標をさらにオフセット
y_offset = 1 if sleep_state == 10 else 0.5 # ミッドスリープタイム以外は透明度を0.3に設定
opacity
fig.add_trace(go.Scatter(=[row['start_minutes'], row['end_minutes']],
x=[day_of_week + y_offset, day_of_week + y_offset],
y='lines',
mode=dict(color=row['color'], width=line_width),
line=sleep_stage_labels[sleep_state],
name=f"{row['start_time'].strftime('%Y-%m-%d %H:%M')} to {row['end_time'].strftime('%Y-%m-%d %H:%M')}",
text='text',
hoverinfo=opacity
opacity
))
fig.add_trace(go.Scatter(=[row['start_minutes'] + 1440, row['end_minutes'] + 1440],
x=[day_of_week + 1 + y_offset, day_of_week + 1 + y_offset],
y='lines',
mode=dict(color=row['color'], width=line_width),
line=sleep_stage_labels[sleep_state],
name=f"{row['start_time'].strftime('%Y-%m-%d %H:%M')} to {row['end_time'].strftime('%Y-%m-%d %H:%M')}",
text='text',
hoverinfo=opacity
opacity
))
# 凡例を統合
= set()
unique_labels lambda trace: trace.update(showlegend=False) if trace.name in unique_labels else unique_labels.add(trace.name))
fig.for_each_trace(
fig.update_layout(=f'Interactive Actogram from {start_date} to {end_date}',
title='Time',
xaxis_title='Days from Start Date',
yaxis_title=dict(
xaxis='array',
tickmode=[i * 60 for i in range(49)],
tickvals=[f'{(i % 24):02d}:00' if i % 2 == 0 else '' for i in range(49)],
ticktextrange=[0, 2880]
),=dict(
yaxis=list(range(num_days)),
tickvals=[(pd.Timestamp(start_date).tz_localize('Asia/Tokyo') + pd.Timedelta(days=x)).strftime('%Y-%m-%d') for x in range(num_days)],
ticktextrange=[0, num_days],
=dict(size=10) # Y軸ラベルの文字サイズを小さく
tickfont
),='closest',
hovermode=dict(
legend='constant'
itemsizing
)
)
file='sleep_data_plot.html', auto_open=True)
pio.write_html(fig,
# ウィジェットの作成
= widgets.DatePicker(
start_date_picker ='Start Date',
description=False
disabled
)= widgets.DatePicker(
end_date_picker ='End Date',
description=False
disabled
)= widgets.Button(
interactive_button ='Plot Interactive Actogram',
description='info',
button_style='Click to plot the interactive actogram',
tooltip='line-chart'
icon
)= widgets.Label(
notice_label ='データ範囲は四半期(3ヶ月)程度にしてください。それ以上の期間を指定すると文字が潰れて読めなくなります。また、グラフの作成には少し時間がかかります。'
value
)
# ボタンがクリックされたときの動作
def on_button_clicked(b):
= start_date_picker.value
start_date = end_date_picker.value
end_date if start_date is not None and end_date is not None:
plot_interactive_actogram(sleep_data, start_date, end_date)else:
print("Please select both start and end dates.")
interactive_button.on_click(on_button_clicked)
# ウィジェットの表示
display(notice_label, start_date_picker, end_date_picker, interactive_button)
3-2-2-6. 睡眠の統計分析
以下のセルでは、
- 四半期ごとの睡眠時間及び睡眠の質の推移の可視化(折れ線グラフ)
- 1枚
- 四半期の範囲で曜日別の睡眠時間及び睡眠の質の可視化(箱ひげ図)
- 1年につき4枚 を行います。
含まれているデータ期間が長い場合は、全部のグラフが出力されるまで時間がかかります。 そのため、実行せず次のセルに進んでも構いません。
睡眠の統計
def calculate_sleep_quality(sleep_data):
# 各セッションの睡眠時間を計算
= sleep_data.groupby('session_id').agg(
session_start_end =('start_time', 'min'),
start_time=('end_time', 'max')
end_time
).reset_index()'sleep_duration_total'] = (session_start_end['end_time'] - session_start_end['start_time']).dt.total_seconds() / 3600
session_start_end[
# 深い睡眠の割合を計算
'sleep_duration'] = (sleep_data['end_time'] - sleep_data['start_time']).dt.total_seconds() / 3600
sleep_data[= sleep_data[sleep_data['sleep_state'] == 5] # 深い睡眠
deep_sleep_data = deep_sleep_data.groupby('session_id')['sleep_duration'].sum().reset_index()
deep_sleep_duration
# 列名を変更
={'sleep_duration': 'sleep_duration_deep'}, inplace=True)
deep_sleep_duration.rename(columns
= pd.merge(session_start_end, deep_sleep_duration, on='session_id', how='left')
sleep_quality 'sleep_quality'] = sleep_quality['sleep_duration_deep'].fillna(0) / sleep_quality['sleep_duration_total']
sleep_quality[
return sleep_quality[['session_id', 'sleep_duration_total', 'sleep_quality']]
def calculate_quarterly_sleep_stats(sleep_data):
= calculate_sleep_quality(sleep_data)
sleep_quality = pd.merge(sleep_data, sleep_quality, on='session_id')
sleep_data
'quarter'] = sleep_data['start_time'].dt.to_period('Q')
sleep_data[= sleep_data.groupby('quarter').agg(
quarterly_stats =('sleep_duration_total', 'mean'),
avg_sleep_time=('sleep_quality', 'mean')
avg_sleep_quality
).reset_index()
return quarterly_stats
def plot_quarterly_sleep_stats(quarterly_stats):
= plt.subplots(figsize=(18, 6))
fig, ax1
'Quarter')
ax1.set_xlabel('Average Sleep Time (hours)', color='tab:blue')
ax1.set_ylabel('quarter'].astype(str), quarterly_stats['avg_sleep_time'], color='tab:blue', marker='o', label='Avg Sleep Time')
ax1.plot(quarterly_stats[='y', labelcolor='tab:blue')
ax1.tick_params(axis
= ax1.twinx()
ax2 'Average Sleep Quality', color='tab:orange')
ax2.set_ylabel('quarter'].astype(str), quarterly_stats['avg_sleep_quality'], color='tab:orange', marker='o', linestyle='--', label='Avg Sleep Quality')
ax2.plot(quarterly_stats[='y', labelcolor='tab:orange')
ax2.tick_params(axis
fig.tight_layout()='upper left', bbox_to_anchor=(0.1, 0.9))
fig.legend(loc'Quarterly Average Sleep Time and Quality')
plt.title(
plt.show()
def calculate_weekly_sleep_stats(sleep_data):
= calculate_sleep_quality(sleep_data)
sleep_quality = pd.merge(sleep_data, sleep_quality, on='session_id')
sleep_data
'quarter'] = sleep_data['start_time'].dt.to_period('Q')
sleep_data['weekday'] = sleep_data['start_time'].dt.day_name()
sleep_data[= sleep_data.groupby(['quarter', 'weekday']).agg(
weekly_stats =('sleep_duration_total', 'mean'),
avg_sleep_time=('sleep_quality', 'mean')
avg_sleep_quality
).reset_index()
return weekly_stats
def plot_weekly_sleep_stats_boxplot(sleep_data_jst):
= calculate_sleep_quality(sleep_data_jst)
sleep_quality = pd.merge(sleep_data_jst, sleep_quality, on='session_id')
sleep_data
'quarter'] = sleep_data['start_time'].dt.to_period('Q')
sleep_data['weekday'] = sleep_data['start_time'].dt.day_name()
sleep_data[= ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
weekdays = ['#3498db', '#3498db', '#3498db', '#3498db', '#3498db', '#e74c3c', '#e74c3c'] # 平日は青、土日は赤
weekday_colors = dict(zip(weekdays, weekday_colors))
weekday_palette
for quarter in sleep_data['quarter'].unique():
= sleep_data[sleep_data['quarter'] == quarter]
quarter_data
= plt.subplots(2, 1, figsize=(12, 12))
fig, (ax1, ax2)
# 睡眠時間の箱ひげ図
='weekday', y='sleep_duration_total', data=quarter_data, order=weekdays, palette=weekday_palette, ax=ax1, hue='weekday', dodge=False)
sns.boxplot(xf'Weekly Sleep Duration for {quarter}')
ax1.set_title('Weekday')
ax1.set_xlabel('Sleep Duration (hours)')
ax1.set_ylabel(=False) # レジェンドを非表示にする
ax1.legend([],[], frameon
# 睡眠の質の箱ひげ図
='weekday', y='sleep_quality', data=quarter_data, order=weekdays, palette=weekday_palette, ax=ax2, hue='weekday', dodge=False)
sns.boxplot(xf'Weekly Sleep Quality for {quarter}')
ax2.set_title('Weekday')
ax2.set_xlabel('Sleep Quality')
ax2.set_ylabel(=False) # レジェンドを非表示にする
ax2.legend([],[], frameon
plt.tight_layout()
plt.show()
# データの準備
= convert_sleep_data_to_jst(sleep_data)
sleep_data_jst
# 四半期ごとの統計を計算
= calculate_quarterly_sleep_stats(sleep_data_jst)
quarterly_stats
# 結果をプロット
plot_quarterly_sleep_stats(quarterly_stats)
# 曜日別の統計を計算
= calculate_weekly_sleep_stats(sleep_data_jst)
weekly_stats
# 結果を箱ひげ図でプロット
plot_weekly_sleep_stats_boxplot(sleep_data_jst)
3-2-2-7. 任意の日付の睡眠記録の可視化
以下のセルを実行することで、ユーザーが指定した日付の睡眠セッション分析し、睡眠ステージの推移をグラフに表示することができます。
任意の日付の睡眠記録の可視化
def convert_to_jst_if_needed(column):
# タイムゾーン情報を確認し、必要に応じて変換を行う
if column.dt.tz is None:
# タイムゾーン情報がない場合、UTCとして解釈し、JSTに変換
return pd.to_datetime(column, utc=True).dt.tz_convert('Asia/Tokyo')
elif str(column.dt.tz) == 'UTC':
# タイムゾーンがUTCであれば、JSTに変換
return column.dt.tz_convert('Asia/Tokyo')
elif str(column.dt.tz) != 'Asia/Tokyo':
# タイムゾーンがJSTでない他のタイムゾーンであれば、JSTに変換
return column.dt.tz_convert('Asia/Tokyo')
else:
# 既にJSTであればそのまま返す
return column
def convert_sleep_data_to_jst(sleep_data):
# sleep_dataをコピーしてタイムゾーンを変換
= sleep_data.copy()
sleep_data_jst 'start_time'] = convert_to_jst_if_needed(sleep_data_jst['start_time'])
sleep_data_jst['end_time'] = convert_to_jst_if_needed(sleep_data_jst['end_time'])
sleep_data_jst[if 'in_bed_time' in sleep_data.columns:
'in_bed_time'] = convert_to_jst_if_needed(sleep_data_jst['in_bed_time'])
sleep_data_jst[else:
'in_bed_time'] = None
sleep_data_jst[
# 明示的に datetime64[ns, Asia/Tokyo] にキャスト
'start_time'] = sleep_data_jst['start_time'].astype('datetime64[ns, Asia/Tokyo]')
sleep_data_jst['end_time'] = sleep_data_jst['end_time'].astype('datetime64[ns, Asia/Tokyo]')
sleep_data_jst['in_bed_time'] = sleep_data_jst['in_bed_time'].astype('datetime64[ns, Asia/Tokyo]')
sleep_data_jst[
return sleep_data_jst
def create_session_data(sleep_data_jst):
# 各セッションの最終 'end_time' を取得して日付に変換
= sleep_data_jst.groupby('session_id')['end_time'].max().dt.date
session_dates = session_dates.reset_index()
session_dates ={'end_time': 'session_date'}, inplace=True)
session_dates.rename(columns
# 睡眠時間と睡眠潜時の計算
= sleep_data_jst.groupby('session_id').agg(
sleep_times =('end_time', lambda x: (x.max() - x.min()).total_seconds() / 3600),
sleep_time=('start_time', 'min')
start_time
)=True)
sleep_times.reset_index(inplace
# 睡眠潜時の計算
= sleep_data_jst.groupby('session_id').apply(
sleep_latency lambda group: calculate_sleep_latency(group[['in_bed_time', 'start_time', 'Type', 'sleep_state']]),
=False # 追加: グループ化列を適用操作から除外
include_groups='sleep_latency')
).reset_index(name
# 結合して全データを含むデータフレームを作成
= pd.merge(session_dates, sleep_times[['session_id', 'sleep_time']], on='session_id')
full_session_data = pd.merge(full_session_data, sleep_latency, on='session_id')
full_session_data
return full_session_data
def calculate_sleep_latency(group):
= group.sort_values(by='start_time')
group = group[(group['Type'] == 'Auto') & (group['sleep_state'] >= 4)]
auto_sleep_times if not auto_sleep_times.empty:
= auto_sleep_times['start_time'].iloc[0]
auto_sleep_time if pd.notna(group['in_bed_time'].iloc[0]) and group['in_bed_time'].iloc[0] <= group['start_time'].iloc[0]:
return (auto_sleep_time - group['in_bed_time'].iloc[0]).total_seconds() / 60
return np.nan
= convert_sleep_data_to_jst(sleep_data)
sleep_data_jst = create_session_data(sleep_data_jst)
full_session_data
# 日付選択ウィジェット
= DatePicker(description='Select Date', disabled=False)
date_picker
def on_prev_clicked(b):
= date_picker.value - pd.Timedelta(days=1) if date_picker.value else None
date_picker.value
def on_next_clicked(b):
= date_picker.value + pd.Timedelta(days=1) if date_picker.value else None
date_picker.value
= Button(description="Previous Day")
button_prev = Button(description="Next Day")
button_next
button_prev.on_click(on_prev_clicked)
button_next.on_click(on_next_clicked)
display(HBox([button_prev, button_next]))
display(date_picker)
# タイムゾーンを確認して適切に日付を表示する関数
def set_plot_title(ax, session_id, sleep_data_jst):
= pytz.timezone('Asia/Tokyo')
jst
= sleep_data_jst[sleep_data_jst['session_id'] == session_id]
session_data if not session_data.empty:
if session_data['start_time'].dt.tz:
= session_data['start_time'].min().astimezone(jst)
start_time_jst = session_data['end_time'].max().astimezone(jst)
end_time_jst else:
= session_data['start_time'].min().replace(tzinfo=pytz.utc)
start_time_utc = session_data['end_time'].max().replace(tzinfo=pytz.utc)
end_time_utc = start_time_utc.astimezone(jst)
start_time_jst = end_time_utc.astimezone(jst)
end_time_jst
= f"Sleep Session from {start_time_jst.strftime('%Y-%m-%d %H:%M')} to {end_time_jst.strftime('%Y-%m-%d %H:%M')}"
title
ax.set_title(title)else:
"No data available for this session")
ax.set_title(
# キャプションを追加する関数
def add_caption(ax, session_id, full_session_data):
= full_session_data[full_session_data['session_id'] == session_id].iloc[0]
record
# キャプションの初期部分
= f"睡眠時間: {record['sleep_time']:.2f} 時間\n"
caption
# sleep_latencyがNaNやマイナスでない場合のみ追加
if pd.notna(record['sleep_latency']) and record['sleep_latency'] >= 0:
+= f"睡眠潜時(布団に入ってから寝付くまでの時間): {record['sleep_latency']:.2f} 分"
caption
0.01, 0.95, caption, transform=ax.transAxes, fontsize=12, verticalalignment='top')
ax.text(
# 睡眠データをプロットする関数
def plot_sleep_data(session_id, sleep_data_jst, full_session_data):
= full_session_data[full_session_data['session_id'] == session_id]
session_info = sleep_data_jst[sleep_data_jst['session_id'] == session_id]
session_data
if not session_data.empty:
# 日時データのタイムゾーンを確認し、日本時間に設定
if session_data['start_time'].dt.tz is None:
'start_time'] = session_data['start_time'].dt.tz_localize('UTC').dt.tz_convert('Asia/Tokyo')
session_data[if session_data['end_time'].dt.tz is None:
'end_time'] = session_data['end_time'].dt.tz_localize('UTC').dt.tz_convert('Asia/Tokyo')
session_data[
= plt.subplots(figsize=(20, 7))
fig, ax = {3: 5, 1: 4, 4: 3, 6: 2, 5: 1, 10: 6}
stage_height = {1: '#e0ffff', 3: '#ff5252', 4: '#03a9f4', 5: '#303f9f', 6: '#ab47bc', 10:'yellow'}
stage_colors = session_data['data_source'].unique()
data_sources = len(data_sources)
source_count = 1 / source_count if source_count > 0 else 1
alpha_value
# プロットの時間を日本時間に合わせて設定
for index, row in session_data.iterrows():
= mdates.date2num(row['start_time'])
start_pos = mdates.date2num(row['end_time']) - start_pos
duration =start_pos, height=stage_height[row['sleep_state']], width=duration,
ax.bar(x=stage_colors.get(row['sleep_state'], '#FFFFFF'), edgecolor='black',
color='edge', alpha=alpha_value)
align
=timezone('Asia/Tokyo'))
ax.xaxis_date(tz=1))
ax.xaxis.set_major_locator(mdates.HourLocator(interval'%H:%M', tz=timezone('Asia/Tokyo')))
ax.xaxis.set_major_formatter(mdates.DateFormatter(0, 6)
ax.set_ylim(1, 2, 3, 4, 5, 6])
ax.set_yticks(['Deep Sleep', 'REM', 'Light Sleep', 'Awake', 'Out-of-bed', 'Mid Sleep Time'])
ax.set_yticklabels(['Time of Day')
ax.set_xlabel(
set_plot_title(ax, session_id, sleep_data_jst)
add_caption(ax, session_id, session_info)
plt.tight_layout()
plt.show()else:
print("No sleep data available for this session.")
# 日付変更時のイベントハンドラ
def on_date_change(change):
if change['new'] is not None:
= pd.to_datetime(change['new']).date()
selected_date = next((sid for sid, date in full_session_data.set_index('session_id')['session_date'].items() if date == selected_date), None)
session_id if session_id is not None:
plot_sleep_data(session_id, sleep_data_jst, full_session_data)else:
print("No sessions found for this date.")
='value') date_picker.observe(on_date_change, names
4. 課題と展望
複数デバイスからのデータを元に可視化や統計を行う際、現状ではそれぞれのデータを等価であると仮定していますが、実際には精度の低いデータセット(デバイス)が存在しています。
データの読み込みはユーザーに任せているので、そもそも精度の低いデータは読み込まなければいいだけの話なのですが、可視化してみて初めて精度の低さに気付くということもあるかと思います。せっかくモジュール式の仕組みを採用しているのだから、最初の可視化で得た気付き(このデバイスはダメそうといった判断)をフィードバックできればいいなぁと考えています。
もう1つ大きな課題として、特定の日の睡眠サイクルを可視化する際に、昼寝が可視化できないというのがあります。アクトグラムを使った睡眠リズムの可視化の際には昼寝がちゃんと可視化されているのに、その詳細は分からないというのは超ベリーバッドです。なんとかしたいです。
今後の展望としては、上記の課題の他に、英語と日本以外のタイムゾーンへの対応, 統計及びその可視化の見直し, ミッドスリープタイムの(アクトグラム以外の)可視化などを考えています。 また、睡眠に関する論文を漁り、何を可視化しどのように分析をすべきかをちゃんと勉強したいです。
4-1. 課題
- 統計や可視化の際に、対象とするデータセットを選べない
- チェックボックス等で使用するデータセットを選べるようにする
- 特定の日付の睡眠サイクルを可視化する際に、昼寝が可視化されない
- メインの睡眠に昼寝を統合してしまうとスケールが狂うので、現在の日付単位でのセレクト(
前日
・翌日
)から、睡眠単位のセレクト(前の睡眠
・次の睡眠
)へと切り替える
- メインの睡眠に昼寝を統合してしまうとスケールが狂うので、現在の日付単位でのセレクト(
- 特定の日付の睡眠サイクルを可視化する際に、睡眠データが存在しない日も選択画面(カレンダー)に表示されてしまう
- 睡眠データが存在しない日をカレンダーに表示しない・もしくは選べないようにする
- 任意の期間のアクトグラムを表示する際に、ミッドスリープタイムが1回の睡眠で2回以上表示されることがある
- 原因を詳しく調査し、ミッドスリープタイムの算出方法がおかしいのか、表示方法がおかしいのか、確かめ改善する
- ユーザーのタイムゾーンが日本時間であると仮定し分析や可視化を行っており、ユーザーがタイムゾーンを選べない(日本時間を強制している)
- タイムゾーンの変換スクリプトを見直し、決め打ちではなくユーザーが選べるようにする
- なんなら、現在、タイムゾーンの変換時をそれぞれの可視化関数で行っているのでこれをどうにかしたい
- タイムゾーンの変換も様々な書き方で行っているため、置換が効かない
- タイムゾーンの変換スクリプトを見直し、決め打ちではなくユーザーが選べるようにする
4-2. 展望
- 英語への対応
- 任意のタイムゾーンを選べるようにする
- 統計方法の見直し
- 可視化手法の見直し
- ミッドスリープタイムの可視化
- Health Connect APIへの対応
5. 感想
- 疲れました。
- PythonもJupyter Notebookも初心者かつ苦手意識がある中でなんとか世に公開できるものが出来たのは、ChatGPT-4(及び途中からGPT-4o)とClaude 3 Opusのおかげです。メインはGPT-4oでしたが、スクリプトのエラーが解決しない時はGPT-4に切り替えて、それでも解決しない時はClaude 3 Opusを使いました。Claude 3 Opusの方が解決力が高いようで、今のところClaude 3 Opusで解決できずにChatGPT-4系が解決できたという問題はありませんでした。
- ChatGPT-4とやりとりしているうちに、どんな関数があれば良くて、引数は多分こんな感じで戻り値に何が欲しくてといった仕様が見えてきたので、それをまるっと伝えるといい感じに書いてくれたのが良かったです。
- とりあえず、最初に要望を伝え企画書・仕様書を作らせ、それを元に雛形を作ってもらい、自分で必要な関数を考え、それを書いてもらうという流れでやればいいという発見を得たのが収穫でした。
- 恥ずかしながら、Git(及びGitHub)に対する知識がなく、バージョン管理がめちゃくちゃになってしまいました。ちゃんと勉強したいと思いました。
- 4月25日にこのプロジェクトを立ち上げ、Jupyter Notebookを完成させるまでに73時間, この記事を書いたり(論文漁ったり)GitHubに公開する準備をしたりするのに20時間くらいかかりました。4月から正真正銘のニートになったので、時間はいくらでもあったとは言え、ちょっとかけすぎたと感じています。あと右手の小指とその筋がとても痛くなりました。多分腱鞘炎です。
- 抗うつ薬が切れた後は起きていたくないので、18時くらいに就寝をして7時くらいに起きるという限界生活をしているのですが、それを図 8や図 9で晒すことになってしまい読者の方にドン引かれてないか心配です。そもそも読者がいるのか怪しいですが。
- 疲れました。
6. 付録
以下に、Jupyter Notebook(.ipynb)ファイルを貼ります。
7. おわりに
この記事及びJupyter Notebookが誰かのお役に立てれば幸いです。