iPhoneのHealthデータをエクスポートしてゴニョゴニョしてみる

最近機械学習の流れで、やっぱり自分でこねくり回せるデータの方が色々覚えるかなということで、自分が持っている加工しがいのあるデータを使ってみようかと思い立った週末の午後。と言っても、2010年から取り続けているWithings(現Nokia Health)の体重関連データ、あとは歩数の記録なんかぐらいしか見当たらない。WithingsとiPhoneのHealthデータは基本的に連携していたので、良い感じでデータが残っているものの、なぜか知らぬ間に連携が解除されていた期間があり、そこら辺を補完する意味で、まずはiPhone Healthのデータを解析してみることにしたという顛末記です。

iPhoneのHealthアプリのデータをエクスポートする方法

iOSのバージョンによって多少メニューが異なるもののだいたいこんな感じ。

1. Healthアプリの人影アイコンをタップ

f:id:deutschina:20180520214536j:plain

2. 一番下にあるExport Health Dataというメニューを選択

f:id:deutschina:20180520214753j:plain

3. 時間かかるけど良い?という確認を無視して、そのままExportをする

f:id:deutschina:20180520214850j:plain

f:id:deutschina:20180520214900j:plain

4. エクスポートが終わったら、メールに添付するなりしてファイルを入手する

f:id:deutschina:20180520214933j:plain

自分の場合は、メールに添付して送信。ZIPファイルの中に2つのXMLファイル(export.xml, export_cds.xml)が入っていたので、ざっくりと中身を確認してみる。まずはサイズの小さそうなexport_cda.xmlから。

import xml.etree.ElementTree as ET

with open('tmp_files/export_cda.xml', 'rb') as fd:
    root = ET.parse(fd).getroot()

どんな情報がありそうかざっくり確認。

>>> root.attrib
{'{http://www.w3.org/2001/XMLSchema-instance}schemaLocation': 'urn:hl7-org:v3 ../../../CDA%20R2/cda-schemas-and-samples/infrastructure/cda/CDA.xsd'}

>>>root.tag
'{urn:hl7-org:v3}ClinicalDocument'

>>>for child in root:
>>>    print(child.attrib)
{'code': 'US'}
{'root': '2.16.840.1.113883.1.3', 'extension': 'POCD_HD000040'}
{'root': '2.16.840.1.113883.10.20.22.1.2'}
{'extension': 'Health Export CDA', 'root': '1.1.1.1.1.1.1.1.1'}
{'codeSystem': '2.16.840.1.113883.6.1', 'codeSystemName': 'LOINC', 'code': '34109-9', 'displayName': 'Note'}
{}
{'value': '20180518144502+0900'}
{'code': 'N', 'codeSystem': '2.16.840.1.113883.5.25'}
{}
{}

Clinical documentという時点で、必要としている情報ではなさそう。にしては、ファイルサイズがでかいのが気になるけど、これは必要となったときに戻ってくると言うことにしましょう。ということで、ターゲットはもう1つのexport.xmlにすることに。

export.xmlの中身を確認してみる

同じように、export.xmlの中身を確認してみます。

with open('tmp_files/export.xml', 'rb') as fd:
    root = ET.parse(fd).getroot()

同じくルートを取ってからの中身の確認。先ほどの手順と同じなので詳細は省きますが、recordというノードの下に欲しそうなデータが転がっている事が判明。

>>> root[2].attrib
{'creationDate': '2016-08-14 12:43:21 +0900',
 'endDate': '2011-01-14 08:38:33 +0900',
 'sourceName': 'Health Mate',
 'sourceVersion': '2150100',
 'startDate': '2011-01-14 08:38:33 +0900',
 'type': 'HKQuantityTypeIdentifierBodyMassIndex',
 'unit': 'count',
 'value': '31.2459'}

ふむ。では、こんな感じで情報を溜めておけば、

>>> records = []
>>> for child in root:
>>>    if child.tag == 'Record':
>>>        records.append(child.attrib)

中身はこうなる。

>>> records[:5]
[{'creationDate': '2016-08-14 12:43:21 +0900',
  'endDate': '2011-01-14 08:38:33 +0900',
  'sourceName': 'Health Mate',
  'sourceVersion': '2150100',
  'startDate': '2011-01-14 08:38:33 +0900',
  'type': 'HKQuantityTypeIdentifierBodyMassIndex',
  'unit': 'count',
  'value': '31.2459'},
 {'creationDate': '2016-08-14 12:43:21 +0900',
  'endDate': '2010-08-26 08:17:40 +0900',
  'sourceName': 'Health Mate',
  'sourceVersion': '2150100',
  'startDate': '2010-08-26 08:17:40 +0900',
  'type': 'HKQuantityTypeIdentifierBodyMassIndex',
  'unit': 'count',
  'value': '30.7669'},
 {'creationDate': '2016-08-14 12:43:21 +0900',
  'endDate': '2010-10-21 08:09:23 +0900',
  'sourceName': 'Health Mate',
  'sourceVersion': '2150100',
  'startDate': '2010-10-21 08:09:23 +0900',
  'type': 'HKQuantityTypeIdentifierBodyMassIndex',
  'unit': 'count',
  'value': '31.1798'},
 {'creationDate': '2016-08-14 12:43:21 +0900',
  'endDate': '2010-12-10 09:23:06 +0900',
  'sourceName': 'Health Mate',
  'sourceVersion': '2150100',
  'startDate': '2010-12-10 09:23:06 +0900',
  'type': 'HKQuantityTypeIdentifierBodyMassIndex',
  'unit': 'count',
  'value': '31.1798'},
 {'creationDate': '2016-08-14 12:43:21 +0900',
  'endDate': '2010-03-18 07:24:23 +0900',
  'sourceName': 'Health Mate',
  'sourceVersion': '2150100',
  'startDate': '2010-03-18 07:24:23 +0900',
  'type': 'HKQuantityTypeIdentifierBodyMassIndex',
  'unit': 'count',
  'value': '31.5762'}]

これを基本として、あとでNokia Health側の情報と突き合わせることを考えて、XMLCSVに変換する事にしました。本当はJSONに変換してゴニョゴニョする予定だったけど、諸事情により中止。この顛末もそのうち記事にするかも。

最終的にCSV変換用に書いたソースがこちら。


Import XML file from Apple Health then convert to ...

長期間のデータが入っているので、アプリのバージョンが変わると、使われている属性(attributes)の顔ぶれが変わっているという問題の対応が必要でした。簡単に言うとアタマのエントリからkeys()を取るだけではダメだった。あとは、数字が入っているはずのvalueという項目になぜか文字列が入っているエントリがあり、最初は項目を変えて残す方向にしていたけど、あまり重要そうではなかったので無視するように変更したあたりがポイントかと。

これで、CSV化が完成したので、今度はこのファイルの中身をゴニョゴニョする感じで行く予定です。

(つづくかも)