<?xml version="1.0" encoding="utf-8"?> 
<rss version="2.0">

<channel>

<title>Блог об аналитике, визуализации данных, data science и BI, заметки с тегом: apple health</title>
<link>http://test.leftjoin.ru/tags/apple-health/</link>
<description></description>
<generator>E2 (v3365; Aegea)</generator>

<item>
<title>Экспорт исторических данных Apple Health в Google Sheets</title>
<guid isPermaLink="false">99</guid>
<link>http://test.leftjoin.ru/all/apple-health-export/</link>
<comments>http://test.leftjoin.ru/all/apple-health-export/</comments>
<description>
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/pic.jpg" width="659" height="395" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Для устройств на базе iOS и watchOS существует приложение Health, которое ежедневно записывает все данные о здоровье носителя и синхронизирует их со сторонними приложениями. Все эти данные в любой момент можно получить прямо из приложения в виде XML-документа. Сегодня мы выгрузим исторические данные о здоровье из приложения Apple Health, обработаем их и отправим в Google Sheets для анализа и визуализации в будущем.&lt;/p&gt;
&lt;h2&gt;Экспорт архива из приложения&lt;/h2&gt;
&lt;p&gt;Зайдите в приложение Health на iPhone. Нажмите на аватарку своего профиля в верхнем правом углу — откроется меню приложения.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/i@2x.jpg" width="371" height="648" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Внизу нажмите на кнопку «Экспортировать медданные». Через некоторое время откроется меню экспорта — отправьте архив себе на компьютер любым способом, можно по AirDrop или даже по почте в письме самому себе. Из архива нужен только один файл — «экспорт.xml». Достаньте его и положите в папку с ноутбуком jupyter.&lt;/p&gt;
&lt;h2&gt;Парсер XML в DataFrame&lt;/h2&gt;
&lt;p&gt;При помощи библиотеки XML составляем дерево на основе документа из Health. Собирать в словарь будем следующие атрибуты: тип, единица измерения, дата создания, дата начала, дата конца, значение. Проходим по всему дереву и отправляем полученные значения атрибутов в records_dict.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;from xml.etree import ElementTree
import pandas as pd
import datetime

tree = ElementTree.parse('экспорт.xml')
root = tree.getroot()
records = root.findall('Record')

records_dict = {
    'type':[],
    'unit':[],
    'creationDate':[],
    'startDate':[],
    'endDate':[],
    'value':[]
}

for record in records:
    for attribute in records_dict.keys():
        attribute_value = record.get(attribute)
        records_dict[attribute].append(attribute_value)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;События записаны в нечитабельном виде — для перевода составим специальный словарь с нужными типами, где ключ — старое название, а значение — новое. Мы возьмём только 11 событий: минуты осознанности, дистанция на велосипеде, дистанция заплыва, дистанция ходьбы и бега, пройдено пролётов, пульс, пульс в покое, шаги, активная энергия, энергия покоя и средний пульс при ходьбе.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;types_dict = {
    'HKCategoryTypeIdentifierMindfulSession': 'Mindful Session',
    'HKQuantityTypeIdentifierDistanceCycling': 'Cycling Distance',
    'HKQuantityTypeIdentifierDistanceSwimming': 'Swimming Distance',
    'HKQuantityTypeIdentifierDistanceWalkingRunning': 'Walking + Running Distance',
    'HKQuantityTypeIdentifierFlightsClimbed': 'Flights Climbed',
    'HKQuantityTypeIdentifierHeartRate': 'Heart Rate',
    'HKQuantityTypeIdentifierRestingHeartRate': 'Resting Heart Rate',
    'HKQuantityTypeIdentifierStepCount': 'Steps',
    'HKQuantityTypeIdentifierActiveEnergyBurned': 'Active Calories',
    'HKQuantityTypeIdentifierBasalEnergyBurned': 'Resting Calories',
    'HKQuantityTypeIdentifierWalkingHeartRateAverage': 'Walking Heart Rate Average'
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Для минут осознанности в поле значения записей нет — мы сами посчитаем позже это поле как разницу даты окончания и начала события. Разница будет представлена как timedelta, поэтому напишем функцию перевода timedelta в минуты:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;def td_to_m(td):
    seconds = td.seconds + td.days * 24 * 60 * 60
    return seconds // 60&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Из словаря создаём DataFrame и задаём названия колонок. Оставляем только те 11 событий, которые есть в словаре types_dict и приводим все колонки к нужным типам данных:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;df = pd.DataFrame(records_dict)
df.columns = ['type', 'unit', 'date', 'start', 'end', 'value']
df = df[df['type'].isin(types_dict.keys())]
df['value'] = df['value'].astype(float)
df['date'] = df['date'].astype('datetime64')
df['date'] = df['date'].dt.date
df['start'] = df['start'].astype('datetime64')
df['end'] = df['end'].astype('datetime64')
df['unit'] = df['unit'].astype(str)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Данные Health при экспорте никак не группируются — мы сделаем это самостоятельно. DataFrame можно поделить на три: в первом будут события, у которых единица измерения «количество в минуту» — для таких событий нужно искать среднее значение. В другой группе будут минуты осознанности — считаем число минут в каждой записи и суммируем. В последней группе находятся все остальные записи, связанные с количественными событиями — шаги, дистанция ходьбы и бега и так далее. Их тоже суммируем.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;df_1 = df[df['unit'] == 'count/min']
df_1 = df_1.groupby(by=['date', 'type', 'unit'], as_index=False).agg({'start':'min',
                                                                      'end':'max',
                                                                      'value':'mean'})

df_2 = df[df['type'] == 'HKCategoryTypeIdentifierMindfulSession']
df_2['value'] = df_2['end'] - df_2['start']
df_2['value'] = df_2['value'].map(td_to_m)
df_2 = df_2.groupby(by=['date', 'type', 'unit'], as_index=False).agg({'start':'min',
                                                                     'end':'max',
                                                                     'value':'sum'})
df_3 = df[(df['unit'] != 'count/min') &amp;amp; (df['type'] != 'HKCategoryTypeIdentifierMindfulSession')]
df_3 = df_3.groupby(by=['date', 'type', 'unit'], as_index=False).agg({'start':'min',
                                                                      'end':'max',
                                                                      'value':'sum'})
df = pd.concat([df_1, df_2, df_3])&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Дату создания записи переводим в строковый тип. Все наименования типов событий заменяем согласно словарю types_dict. В переменную dates записываем все уникальные даты.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;df['date'] = df['date'].astype(str)
df['type'] = df['type'].apply(lambda x: types_dict[x])
dates = df['date'].unique()&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;В результате нужен словарь с колонкой даты и отдельной колонкой под каждое из 11 событий:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;result = {
    'date': [],
    'Steps': [],
    'Walking + Running Distance': [],
    'Swimming Distance': [],
    'Cycling Distance': [],
    'Resting Calories': [],
    'Active Calories': [],
    'Flights Climbed': [],
    'Heart Rate': [],
    'Resting Heart Rate': [],
    'Walking Heart Rate Average': [],
    'Mindful Session': []
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Проходим по каждой дате и получаем кусок DataFrame за эту дату. Добавляем её в словарь и проходим по каждому ключу, пробуя добавить значение:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;for date in dates:
    part = df[df['date'] == date]
    result['date'].append(date)
    for key in result.keys():
        if key == 'date':
            continue
        else:
            field = 'value'
        try:
            result[key].append(part[part['type'] == key][field].values[0])
        except IndexError:
            result[key].append(None)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Из полученного словаря создаём DataFrame, округляем всё до двух знаков после запятой и сортируем по дате:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;result_df = pd.DataFrame(result)
result_df = result_df.round(2)
result_df = result_df.sort_values(by='date')&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;В результате получается такая таблица с историческими данными по 11 событиям:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/2-24.png" width="996" height="530" alt="" /&gt;
&lt;/div&gt;
&lt;h2&gt;Экспорт DataFrame в Google Sheets&lt;/h2&gt;
&lt;p class="note"&gt;Для экспорта в Google Docs необходим сервисный аккаунт и json-файл с ключом. О том, как его получить, мы писали в материале &lt;a href="http://test.leftjoin.ru/all/get-data-from-vk/" class="nu"&gt;«&lt;u&gt;Собираем данные по рекламным кампаниям ВКонтакте&lt;/u&gt;»&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Создайте новый документ в Google Sheets. Весь DataFrame можно вставить одним действием при помощи методов библиотеки gspread. Импортируйте её, а также укажите идентификатор документа и json-файл с ключом. В методе get_worksheet указывается порядковый номер листа в файле начиная с нуля.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;import pandas as pd
import gspread
from gspread_dataframe import set_with_dataframe
gc = gspread.service_account(filename='serviceAccount.json')
sh = gc.open_by_key('1osKA63LQkUC0FC0eIZ63jEJwn1TeIkUvqCV6ur')
worksheet = sh.get_worksheet(0)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;В итоге в Google Spreadsheets появится такая таблица:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/--2021-03-03--15.31.48.png" width="1375" height="761" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;А в следующем материале посмотрим, как наладить ежедневный экспорт данных Здоровья в эту таблицу при помощи шорткатов и Google AppScript!&lt;/p&gt;
</description>
<pubDate>Wed, 03 Mar 2021 16:57:03 +0300</pubDate>
</item>


</channel>
</rss>