[ad_1]
Quer compartilhar seu conteúdo em R-bloggers? clique aqui se você tiver um blog, ou aqui se não tiver.
Expandir para o código EKG
library(hrbrthemes) library(elementalist) # remotes::install_github("teunbrand/elementalist") library(ggplot2) read_csv( file = "~/Data/apple_health_export/electrocardiograms/ecg_2020-09-24.csv", # this is extracted below skip = 12, col_names = "µV" ) %>% mutate( idx = 1:n() ) -> ekg ggplot() + geom_line_theme( data = ekg %>% tail(3000) %>% head(2500), aes(idx, µV), size = 0.125, color = "#cb181d" ) + labs(x = NULL, y = NULL) + theme_ipsum_inter(grid="") + theme( panel.background = element_rect(color = NA, fill = "#141414"), plot.background = element_rect(color = NA, fill = "#141414") ) + theme( axis.text.x = element_blank(), axis.text.y = element_blank(), elementalist.geom_line= element_line_glow() )
Os proprietários do Apple Watch têm a capacidade de exportar seus dados rastreados e fazer o que quiserem com eles. Como é Dia dos Namorados, achei que seria divertido mostrar duas maneiras de ler dados de frequência cardíaca dessas exportações.
Por que dois maneiras? Bem, eu tenho um Apple Watch intermitente desde o dispositivo de primeira geração, e quando a Apple diz que você pode exportar todo seus dados, eles significam todo. O apple_health_export.zip
o arquivo é gerado acessando o aplicativo iOS “Health”, tocando em seu avatar no canto superior esquerdo, rolando para baixo e tocando no botão de exportação:
(NOTA: sugiro salvá-lo e baixá-lo do iCloud em vez de usar o AirDrop local para o seu sistema.)
Este arquivo compactado tem um tamanho enganadoramente ~ 58 MB. Abri-lo resulta em uma árvore de diretório de quase 3 GB de espaço em disco consumido O_o. Essa árvore tem a seguinte estrutura:
fs::dir_tree("~/Data/apple_health_export", recurse = 1) ## ~/Data/apple_health_export ## ├── electrocardiograms ## │ └── ecg_2020-09-24.csv # 122 KB ## ├── export.xml # 882 MB ## ├── export_cda.xml # 950 MB ## └── workout-routes # 81 MB ## ├── ... ## ├── route_2021-01-28_5.21pm.gpx ## ├── route_2021-01-31_4.28pm.gpx ## ├── route_2021-02-02_1.26pm.gpx ## ├── route_2021-02-04_3.52pm.gpx ## ├── route_2021-02-06_2.24pm.gpx ## └── route_2021-02-10_4.54pm.gpx
Os dados de frequência cardíaca estão em pouco menos de 1 GB export.xml
e é misturado com todos os outros pontos de dados que a Apple registra. Eles se parecem com isto:
Observe que os registros mais recentes desse tipo não são tags vazias.
Embora lidar com arquivos gigabyte + XML não sejam tão insustentáveis como costumavam ser em R, construir uma árvore XML analisada na memória para todos esses registros ocupará uma quantidade não insignificante de RAM (veremos quanto abaixo) . Já que quero começar a brincar com esses dados com mais frequência, decidi tentar duas abordagens: uma que processa o XML em “blocos” de streaming e outra que o faz da maneira que você provavelmente está acostumado (se você tiver o azar de ter trabalhar com XML regularmente).
Transmissão
Beats
Começaremos com a abordagem de streaming, o que significa usar o venerável pacote {XML}, que tem xmlEventParse()
que é um orientado por eventos ou o analisador de estilo SAX (API Simples para XML) que processa XML sem construir a árvore, mas identifica tokens no fluxo de caracteres e os passa para manipuladores que podem entendê-los no contexto. Já que estamos indo para a velha escola, também usaremos {data.table} para obter um conjunto de dados organizado para trabalhar.
Encontraremos registros de frequência cardíaca e armazenaremos os dados deles em uma lista, portanto, precisaremos abrir espaço para eles e usar atribuições de valores com base indexada para evitar fazer milhares de cópias com append()
. Para descobrir de quanto espaço precisaremos, vou “trapacear” um pouco e usar o ripgrep para contar quantos HKQuantityTypeIdentifierHeartRate
registros existem e usam esse resultado para reservar espaço na lista:
library(XML) library(data.table) nlThere are just under 790K records buried in that file. The
xmlEventParse()
function has ahandlers
parameter which takes a list named functions for various events. The event we care about is the one where we start processing an XML element, which is unsurprisingly calledstartElement
. In it, we’ll only processHKQuantityTypeIdentifierHeartRate
records and further only care about data since 2019:invisible(xmlEventParse( file = "~/Data/apple_health_export/export.xml", handlers = list( # process at element start startElement = function(name, attrs) { # only care about the heart rate recs if ((name == "Record") && (attrs["type"] == "HKQuantityTypeIdentifierHeartRate")) { # only care about records >= the year 2019 if (substr(attrs["endDate"], 1, 4) >= 2019) { # if we find them, add them to the list (note theAt this point we have a list of all those records and have taken the R session memory from 131 MiB to 629 MiB (so, we’re eating about ~500 MiB of RAM with that call), and it took around 34 painful seconds to process the XML file.
Now, we’ll use {data.table} to tidy it up:
recordsThat took around 4.5 seconds, and when the R garbage collector kicks in we’re now consuming ~695 MiB, so not much more than the previous step.
So, ~38s for the ingestion & conversion, and a maximum of ~695 MiB in play at any time during the R session. Let’s see how the new/modern way (i.e. {xml2}) compares.
Modern
A menos que eu tenha perdido algo na página de índice {xml2}, não há processador de streaming equivalente, então temos que ler todo o documento na RAM ativa:
biblioteca (xml2) registros da biblioteca (tidyverse)Esta operação leva 15,7s e a sessão R agora consome ~ 5,8 GiB de RAM. Isso é um “G”, como em gigabyte.
Agora, encontraremos todos os registros de nosso interesse (como acima). Faremos isso por meio de um modesto seletor XPath:
xml_find_all (registros, xpath = ".//Record[ @type="HKQuantityTypeIdentifierHeartRate" and (starts-with(@endDate, '2019') or starts-with(@endDate, '2020') or starts-with(@endDate, '2021')) ]") -> registrosEssa operação demorou cerca de 6,5s e ainda estamos consumindo cerca de 6,23 GiB de RAM.
Agora, vamos arrumar isso:
tibble( ts = records %>% xml_attr("endDate") %>% as.POSIXct(format = "%Y-%m-%d %H:%M:%S %z"), rate = records %>% xml_attr("value") %>% as.integer() ) -> records records ## # A tibble: 734,530 x 2 ## ts rate #### 1 2019-02-12 15:19:54 69 ## 2 2019-02-12 15:26:11 90 ## 3 2019-02-12 15:31:33 92 ## 4 2019-02-12 15:34:24 89 ## 5 2019-02-12 15:57:33 120 ## 6 2019-02-12 15:44:09 80 ## 7 2019-02-12 16:03:24 110 ## 8 2019-02-12 16:13:08 118 ## 9 2019-02-12 16:08:10 100 ## 10 2019-02-12 16:15:04 95 ## # … with 734,520 more rows Isso levou cerca de 10,4s e, depois que a coleta de lixo acontece, estamos de volta a um consumo muito mais razoável de ~ 890 MiB de RAM após um fluxo de trabalho máximo de 6 GiB, levando um total de ~ 32,6 segundos.
FIM
Se / quando a memória estiver apertada, é bom ter algumas alternativas além de “pegar uma caixa maior”, e esta é uma abordagem (existem outras) para realizar este tipo de cirurgia XML em R.
Fique seguro / forte, pessoal.
Relacionado
[ad_2]