Cách trích xuất dữ liệu Apple Health bằng Python

Ở bài này, chúng ta sẽ tìm cách trích xuất dữ liệu của Apple Health (bao gồm số bước chạy, quãng đường di chuyển hay thời gian ngủ). Dữ liệu của Apple Health được lưu dưới định dạng .xml nên việc lọc dữ liệu sẽ vất vả hơn so với .csv. Tuy nhiên, với Google là người bạn thân thiết thì chúng ta hoàn toàn có thể xử lý được.

How to extract Apple Health data in Python.

Xuất file .xml

Để lấy được file gốc chứa tất cả các thông tin của bạn, hãy vào

Health -> Nhấn vào biểu tượng Profile ở góc trên bên phải -> Export Health Data

Sau khi giải nén file .zip, chúng ta sẽ có được một folder chứa 2 file là export.xml và export_cda.xml. File export_cda.xml là dữ liệu theo chuẩn của Clinical Document Architecture (CDA) phổ biến tại Mỹ. CDA hay được sử dụng tại các phòng khám và đây là nơi nó chứa dữ liệu về lịch sử khám bệnh của bạn. Ở đây, chúng ta quan tâm đến file export chứa các dữ liệu về quãng đường di chuyển, giờ giấc sinh hoạt của các bạn thôi.

Để tiến hành các thao tác lấy dữ liệu, các bạn có thể dùng các texteditor phổ biến như VSC, Atom hay Jupyter Notebook. Xài Jupyter thì dễ chịu hơn vì không phải chạy từ đầu đến cuối sau mỗi lần sửa code.

Mình không phải chuyên gia về XML, nhưng mà sau bài này và tra wiki thì cũng hình dung ra được đại khái. Hình dung đơn giản thì XML sẽ có dạng cây như này.

Product
├───Name
└───Details
    └───Description
    └───Price
<Product>
    <Name>Widget</Name>
    <Details>
        <Description>
            This Widget is the highest quality widget. 
        </Description>
        <Price>5.50</Price>
    </Details>
</Product>

Nhìn qua thì khá giống một cái dictionary gồm các cặp key:value nhưng một value ở đây có thể chứa thêm nhiều dictionary nữa. Chúng ta sẽ dùng một số thư viện để phân tách (parse) dữ liệu từ file xml này sang dạng phù hợp để xử lý trong Pandas.

Tạo DataFrame từ dữ liệu Health

Đầu tiên là tải các thư viện sẽ dùng | Load libraries

import numpy as np
import pandas as pd
import xmltodict #essential to parse from xml to Python dict
import datetime
import matplotlib.pyplot as plt

Trong này thì thư viện xmltodict sẽ giúp ta xử lý công đoạn phân tách và chuyển dạng dữ liệu đã nói ở trên. Cú pháp như sau:

#parse xml. to pandas df.
input_path = '.../export.xml'
with open(input_path, 'r') as xml_file:
    input_read = xmltodict.parse(xml_file.read())
records = input_read['HealthData']['Record']
df = pd.DataFrame(records)

#format dates to pandas datetime format 
format = '%Y-%m-%d %H:%M:%S %z'
df['@creationDate'] = pd.to_datetime(df['@creationDate'],format=format)
df['@startDate'] = pd.to_datetime(df['@startDate'],format=format)
df['@endDate'] = pd.to_datetime(df['@endDate'],format=format)

Chỗ input_path là đường dẫn đến file export.xml mà các bạn vừa thu được ban nãy. File input_read sau khi xuất ra là một cái dictionary, chúng ta lưu các thông tin liên quan đến ‘HealthData’ và ‘Record’ vào biến records và cuối cùng dùng Pandas để biến nó thành một cái DataFrame. Sau khi chuyển sang DataFrame rồi thì mình convert chúng sang dạng Datetime của Pandas. Chúng ta có thể in thử cái DataFrame này ra xem nó gồm những cái gì.


,@type,@sourceName,@sourceVersion,@unit,@creationDate,@startDate,@endDate,@value,@device,MetadataEntry
0,HKQuantityTypeIdentifierHeight,Sức khỏe,12.2,cm,2019-05-01 16:16:47 +0900,2019-05-01 16:16:47 +0900,2019-05-01 16:16:47 +0900,179,NaN,NaN
1,HKQuantityTypeIdentifierBodyMass,Sức khỏe,12.2,kg,2019-05-01 16:16:47 +0900,2019-05-01 16:16:47 +0900,2019-05-01 16:16:47 +0900,80,NaN,NaN
2,HKQuantityTypeIdentifierStepCount,iPhone,12.1,count,2019-04-26 14:40:21 +0900,2019-04-26 14:28:07 +0900,2019-04-26 14:36:51 +0900,196,"<, name:iPhone, manufac...",NaN
3,HKQuantityTypeIdentifierStepCount,iPhone,12.1,count,2019-04-26 14:53:44 +0900,2019-04-26 14:39:17 +0900,2019-04-26 14:39:19 +0900,2,"<, name:iPhone, manufac...",NaN
4,HKQuantityTypeIdentifierStepCount,iPhone,12.1,count,2019-04-26 15:29:13 +0900,2019-04-26 14:50:32 +0900,2019-04-26 14:59:15 +0900,149,"<, name:iPhone, manufac...",NaN
...,...,...,...,...,...,...,...,...,...,...
74694,HKQuantityTypeIdentifierWalkingAsymmetryPercen...,iPhone,14.4,%,2021-03-06 13:30:24 +0900,2021-03-06 13:08:37 +0900,2021-03-06 13:08:58 +0900,0,"<, name:iPhone, manufac...","{'@key': 'HKMetadataKeyDevicePlacementSide', '..."
74695,HKQuantityTypeIdentifierWalkingAsymmetryPercen...,iPhone,14.4,%,2021-03-06 13:50:28 +0900,2021-03-06 13:38:52 +0900,2021-03-06 13:41:28 +0900,0.01,"<, name:iPhone, manufac...","{'@key': 'HKMetadataKeyDevicePlacementSide', '..."
74696,HKQuantityTypeIdentifierWalkingAsymmetryPercen...,iPhone,14.4,%,2021-03-06 15:53:18 +0900,2021-03-06 15:43:18 +0900,2021-03-06 15:44:02 +0900,0,"<, name:iPhone, manufac...","{'@key': 'HKMetadataKeyDevicePlacementSide', '..."
74697,HKQuantityTypeIdentifierWalkingAsymmetryPercen...,iPhone,14.4,%,2021-03-06 15:53:18 +0900,2021-03-06 15:46:17 +0900,2021-03-06 15:46:38 +0900,0,"<, name:iPhone, manufac...","{'@key': 'HKMetadataKeyDevicePlacementSide', '..."
74698,HKQuantityTypeIdentifierWalkingAsymmetryPercen...,iPhone,14.4,%,2021-03-06 15:53:18 +0900,2021-03-06 15:47:19 +0900,2021-03-06 15:48:11 +0900,0,"<, name:iPhone, manufac...","{'@key': 'HKMetadataKeyDevicePlacementSide', '..."

Ở đây các bạn thấy rằng, @type là biến. Ví dụ, ở dòng thứ 3, ta có ‘HKQuantityTypeIdentifierStepCount’ là biến “số bước chân”, giá trị của nó tương ứng ở cột @value (ở đây là 196 bước), đơn vị (@unit) đơn giản là “count”, số bước chân này được khởi tạo ở cột @creationDate (2019-04-26 14:53:44), bắt đầu record từ (@startDate) 2019-04-26 14:28:07 +0900 đến (@endDate) 2019-04-26 14:36:51 +0900, tức là khoảng 8 phút. Có rất nhiều loại biến tuỳ vào dữ liệu mà Apple Health ghi lại. Chúng ta có thể gọi chúng ra bằng lệnh sau:

print(df['@type'].unique())
['HKQuantityTypeIdentifierHeight' 'HKQuantityTypeIdentifierBodyMass'
 'HKQuantityTypeIdentifierStepCount'
 'HKQuantityTypeIdentifierDistanceWalkingRunning
 'HKQuantityTypeIdentifierFlightsClimbed'
 'HKQuantityTypeIdentifierHeadphoneAudioExposure'
 'HKQuantityTypeIdentifierWalkingDoubleSupportPercentage'
 'HKQuantityTypeIdentifierWalkingSpeed'
 'HKQuantityTypeIdentifierWalkingStepLength'
 'HKQuantityTypeIdentifierWalkingAsymmetryPercentage'
 'HKCategoryTypeIdentifierSleepAnalysis']

Như vậy có thể thấy Health ghi lại những gì. Số bước chân, quãng đường, số bậc, tốc độ di chuyển, giấc ngủ, … Chúng ta hoàn toàn có thể lấy được hết những dữ liệu này.

Trích xuất dữ liệu

Sau khi đã tạo được DataFrame chứa tất cả dữ liệu về Health rồi thì việc lọc các dữ liệu bạn cần cũng khá đơn giản. Trước tiên, để tiện cho việc tái sử dụng, mình sẽ tạo một list chứa các loại biến trong cột @type ở trên. Như vậy, khi bạn cần lấy ra dữ liệu nào thì chỉ cần gọi element trong cái list đó ra là được.

variables = []
for x in df['@type']:
    if x not in variables:
        variables.append(x)
print(variables)

Ví dụ bây giờ chúng ta sẽ lấy dữ liệu về “số bước chân” (step counts). Số bước chân là biến ‘HKQuantityTypeIdentifierStepCount’, nằm ở vị trí thứ 2 trong list variables. (thứ tự trong list tính từ 0 nhé)

x = 2
#call variable
data = df[df['@type'] == variables[x]]
#convert to numeric 
data.loc[:, '@value'] = pd.to_numeric(data.loc[:, '@value'])
#take sum by startdate
data_bystart = data.groupby('@startDate').sum().reset_index()
#set startdate to be datetime index
df_bystart = data_bystart.set_index(['@startDate'])
df_bystart.index = pd.to_datetime(df_bystart.index)
print(df_bystart)
                           @value
@startDate                       
2019-04-26 14:28:07+09:00     196
2019-04-26 14:39:17+09:00       2
2019-04-26 14:50:32+09:00     149
2019-04-26 15:06:51+09:00     811
2019-04-26 15:17:58+09:00     889
...                           ...
2021-03-06 13:12:12+09:00      40
2021-03-06 13:35:29+09:00     357
2021-03-06 13:46:13+09:00      23
2021-03-06 14:35:54+09:00      11
2021-03-06 15:42:47+09:00     338

Giờ bạn đã lấy được tất cả dữ liệu liên quan đến step counts rồi, những mà 1 ngày sẽ có rất nhiều entries. Giờ ta sẽ bắt đầu gộp tất cả những entries cùng 1 ngày lại với nhau.

#sum all data of the same day
df_bystart_daysum = df_bystart['@value'].resample('D').sum()
df1 = pd.DataFrame(df_bystart_daysum).reset_index()
df1_copy = df1.rename(columns={'@startDate':'@startDate','@value':variables[x]})
print(df1_copy)
                   @startDate  HKQuantityTypeIdentifierStepCount
0   2019-04-26 00:00:00+09:00                              10342
1   2019-04-27 00:00:00+09:00                              12270
2   2019-04-28 00:00:00+09:00                              20801
3   2019-04-29 00:00:00+09:00                              10927
4   2019-04-30 00:00:00+09:00                               7079
..                        ...                                ...
676 2021-03-02 00:00:00+09:00                              12623
677 2021-03-03 00:00:00+09:00                               8459
678 2021-03-04 00:00:00+09:00                              11410
679 2021-03-05 00:00:00+09:00                              11070
680 2021-03-06 00:00:00+09:00                               6561

Hoặc nhóm tất cả các ngày trong cùng một tháng bằng cách thay chữ ‘D’ trong phần resample ở code trên bằng ‘M’

df_bystart_monthsum = df_bystart['@value'].resample('M').sum()
df2 = pd.DataFrame(df_bystart_monthsum).reset_index()
df2_copy = df2.rename(columns={'@startDate':'@startDate','@value':variables[x]})
print(df2_copy)
                  @startDate  HKQuantityTypeIdentifierStepCount
0  2019-04-30 00:00:00+09:00                              61419
1  2019-05-31 00:00:00+09:00                             341379
2  2019-06-30 00:00:00+09:00                             275759
3  2019-07-31 00:00:00+09:00                             405166
4  2019-08-31 00:00:00+09:00                             440526
5  2019-09-30 00:00:00+09:00                             264301
6  2019-10-31 00:00:00+09:00                             311649
7  2019-11-30 00:00:00+09:00                             269716
8  2019-12-31 00:00:00+09:00                             314266
9  2020-01-31 00:00:00+09:00                             351780
10 2020-02-29 00:00:00+09:00                             255069
11 2020-03-31 00:00:00+09:00                             313234
12 2020-04-30 00:00:00+09:00                             159965
13 2020-05-31 00:00:00+09:00                             150959
14 2020-06-30 00:00:00+09:00                             236338
15 2020-07-31 00:00:00+09:00                             247335
16 2020-08-31 00:00:00+09:00                             267572
17 2020-09-30 00:00:00+09:00                             261147
18 2020-10-31 00:00:00+09:00                             209530
19 2020-11-30 00:00:00+09:00                             264328
20 2020-12-31 00:00:00+09:00                             343939
21 2021-01-31 00:00:00+09:00                             312814
22 2021-02-28 00:00:00+09:00                             324354
23 2021-03-31 00:00:00+09:00                              67445

Sau khi có được dữ liệu này rồi, chúng ta có thể xem mô tả nhanh cuả nó bằng cách gõ describe

print(df1_copy.describe())
       HKQuantityTypeIdentifierStepCount
count                          24.000000
mean                       268749.583333
std                         91231.577283
min                         61419.000000
25%                        244585.750000
50%                        268644.000000
75%                        316788.000000
max                        440526.000000

Trung bình mỗi tháng có 24 ngày có di chuyển, trung bình khoảng hơn 268,000 bước (cộng trừ 90,000 bước). Ngày bước ít nhất là 61,000, ngày nhiều nhất là 440,000. Con số này tương đương với bao nhiêu km thì cũng không rõ. Các bạn thay x bằng các con số khác thì sẽ ra các biến khác, ví dụ nếu muốn xem quãng đường di chuyển thì thay x = 3.

Plotting

Ở mục này, mình sẽ lấy dữ liệu quãng đường di chuyển, nhóm chúng lại theo tháng và tạo 2 cột, 1 cột tổng và 1 cột trung bình.

#filter distance in df
dist = df[df['@type'] == 'HKQuantityTypeIdentifierDistanceWalkingRunning']
dist.loc[:, '@value'] = pd.to_numeric(dist.loc[:, '@value'])
dist_data = dist.groupby('@startDate').sum().reset_index()
#set startDate as Datetime Index (so we can use resample)
dist_data = dist_data.set_index(['@startDate'])
dist_data.index = pd.to_datetime(dist_data.index)
#resample data by month and take sum
dist_data_bymonth = dist_data['@value'].resample('M').sum()
df3 = pd.DataFrame(dist_data_bymonth)
#create new column to store mean value
df3['@avgdays_walk'] = dist_data['@value'].resample('M').mean()
df4 = df3.rename(columns={'@startDate':'date','@value':'total_dist','@avg_walk':'avg_dist'})
df4
	date	                        total_dist	avg_dist
0	2019-04-30 00:00:00+09:00	42.219122	0.219891
1	2019-05-31 00:00:00+09:00	231.402861	0.201045
2	2019-06-30 00:00:00+09:00	187.477085	0.151803
3	2019-07-31 00:00:00+09:00	269.015497	0.177568
4	2019-08-31 00:00:00+09:00	294.213523	0.190924
5	2019-09-30 00:00:00+09:00	182.792288	0.190806
6	2019-10-31 00:00:00+09:00	228.439375	0.261672
7	2019-11-30 00:00:00+09:00	197.082577	0.226532
8	2019-12-31 00:00:00+09:00	214.001283	0.162245
9	2020-01-31 00:00:00+09:00	232.283888	0.158772
10	2020-02-29 00:00:00+09:00	164.443494	0.120032
11	2020-03-31 00:00:00+09:00	211.279689	0.130420
12	2020-04-30 00:00:00+09:00	108.975800	0.124260
13	2020-05-31 00:00:00+09:00	107.790604	0.146256
14	2020-06-30 00:00:00+09:00	169.292904	0.167783
15	2020-07-31 00:00:00+09:00	166.349716	0.148130
16	2020-08-31 00:00:00+09:00	181.402526	0.153341
17	2020-09-30 00:00:00+09:00	176.855602	0.165286
18	2020-10-31 00:00:00+09:00	139.094619	0.114012
19	2020-11-30 00:00:00+09:00	179.516308	0.152910
20	2020-12-31 00:00:00+09:00	234.404877	0.163348
21	2021-01-31 00:00:00+09:00	216.535164	0.169832
22	2021-02-28 00:00:00+09:00	230.273059	0.220357
23	2021-03-31 00:00:00+09:00	48.575416	0.269863

Sau đó plot hai dữ liệu này lên cùng 1 chart. Mình sẽ plot data tổng ở dạng cột (bar) và data về số bước trung bình ở dạng line.

#declare plot data 
fig, ax1 = plt.subplots()
x = df4['date']
y1 = df4['total_dist']
y2 = df4['avg_dist']
#plot elements 
plt.xticks(rotation=60)
ax2 = ax1.twinx() #mirror subplots 
ax1.bar(x, y1, color='orange', width = 20)
ax2.plot(x, y2, color='blue', marker='o')
ax1.set_xlabel('Time')
ax1.set_ylabel('Total distance (km)', color='orange')
ax2.set_ylabel('Daily average (km)', color='blue')
plt.title('Walking distance record')
fig.tight_layout()
plt.show()

Nhìn sơ qua biểu đồ thì thấy, có một số tháng tổng số quãng đường di chuyển rất cao nhưng con số trung bình lại rất thấp (như tháng 04/2020 chẳng hạn), đó là do đi không đều, có hôm quá nhiều, có những hôm lại chẳng đi mấy. Ngoài ra, nếu có thêm nhiều dữ kiện hơn, các bạn có thể làm các thí nghiệm nhỏ rất đơn giản. Ví dụ, nếu có số liệu quãng đường di chuyển và thời gian ngủ, các bạn có thể chứng minh xem là liệu có mối quan hệ gì giữa số quãng đường di chuyển và thời gian ngủ không. Liệu việc di chuyển nhiều trong ngày có giúp bạn ngủ ngon và lâu hơn không chẳng hạn. Giả thuyết này khá thú vị, và với dữ liệu của Apple Health mà các bạn có được, việc test nó là hoàn toàn khả thi 🙂

Như vậy, thông qua việc tự mình trích xuất dữ liệu Apple Health, chúng ta hoàn toàn có thể tự mình vẽ được các biểu đồ, cũng như xem đầy đủ thông tin về sức khoẻ được ghi lại chẳng kém gì app chính chủ.

Tham khảo:

Guido Carisaghi, Analyze Your iOS Health Data With Python. URL: https://betterprogramming.pub/analyze-your-icloud-health-data-with-pandas-dd5e963e902f

Để lại bình luận

Điền thông tin vào ô dưới đây hoặc nhấn vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Đăng xuất /  Thay đổi )

Google photo

Bạn đang bình luận bằng tài khoản Google Đăng xuất /  Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Đăng xuất /  Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Đăng xuất /  Thay đổi )

Connecting to %s

Trang web này sử dụng Akismet để lọc thư rác. Tìm hiểu cách xử lý bình luận của bạn.