-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathWTAR-curve-modelling.Rmd
executable file
·1381 lines (1202 loc) · 67.9 KB
/
WTAR-curve-modelling.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
---
title: "Wechsler Test of Adult Reading in Parkinson’s: a stable yet imperfect measure of premorbid cognitive function"
author:
- name: Kyla-Louise Horne, PhD
affiliation: '1'
corresponding: yes
address: 66 Stewart St, Christchurch 8011, New Zealand
email: kyla.horne@nzbri.org
- name: Reza Shoorangiz, PhD
affiliation: '1'
- name: Daniel J. Myall, PhD
affiliation: '1'
- name: Toni L. Pitcher, PhD
affiliation: '1,2'
- name: Tim J. Anderson, FRACP, MD
affiliation: '1,2,3'
- name: John C. Dalrymple-Alford, PhD
affiliation: '1,2,4'
- name: Michael R. MacAskill, PhD
affiliation: '1,2'
shorttitle: WTAR premorbid measure in Parkinson's
output:
papaja::apa6_pdf:
latex_engine: xelatex
papaja::apa6_word:
link-citations: yes
csl: format/bmj.csl
bibliography: format/wtar-references.bib
appendix: WTAR-supplementary.Rmd
linenumbers: yes
mask: no
draft: no
documentclass: apa6
classoption: man
affiliation:
- id: '1'
institution: New Zealand Brain Research Institute, 66 Stewart St,
Christchurch, New Zealand
- id: '2'
institution: Department of Medicine, University of Otago, Christchurch;
Christchurch, New Zealand
- id: '3'
institution: Department of Neurology, Christchurch Hospital, Christchurch, New
Zealand
- id: '4'
institution: School of Psychology, Speech, and Hearing, University of
Canterbury, Christchurch, New Zealand
abstract: Purpose. To assess long-term trajectories of Wechsler Test of Adult
Reading (WTAR) scores in people with Parkinson’s as a function of age. The
WTAR has been recommended as a measure of premorbid cognitive function in
English speakers with Parkinson’s disease and as a reference against which to
assess current cognitive status. For this, however, it needs to be shown that
WTAR scores remain stable despite the substantial cognitive deterioration that
can occur in Parkinson’s. Methods. From 252 Parkinson's and 57 Control
participants who had completed at least two WTARs, we analyzed scores over
time using latent class trajectory modeling. This allows for individual
participants to be classified into data-driven clusters, depending on the
shape of their longitudinal trajectory. Results. WTAR scores were quite stable
within both Controls and Parkinson's participants, even for those who
progressed to dementia. This validates it as a research tool for comparing
premorbid function at a group level. In both Parkinson's and Controls, and
regardless of current cognitive status, the distribution of scores was,
however, higher than expected from the population norms, making it an
unreliable benchmark against which to detect cognitive decline at an
individual level. Conclusion. The WTAR is stable in Parkinson's even when
participants decline from normal cognitive function to early dementia.
Nonetheless, its apparent over-estimation of premorbid IQ and the
impracticality of implementing analogous tests in many other languages makes
it poorly-suited for detecting current cognitive impairment in individuals
with Parkinson's.
keywords: Parkinson disease, cognitive impairment, dementia, neuropsychology,
premorbid function
wordcount: 'Abstract: 239 (limit 250), main body: 3870 (limit 4000)'
editor_options:
chunk_output_type: console
---
```{r setup, include=FALSE}
## NB notice the chunk_output_type: console setting in the YAML header above.
## This was part of addressing an issue where one of the linear models would
## converge in R, "but kept failing during knitting in Rmarkdown. The problem
## seemed to be caused by the parallel package and how it generates random
## numbers" also we now "create/destroy a new parallel pool within the
## for-loop"- problem and solution identified by Reza.
knitr::opts_chunk$set(
echo = FALSE,
message = FALSE,
warning = FALSE)
options(knitr.kable.NA = '')
```
```{r define-variables}
################################################################################
### 1. NZBRI users can save the time taken to re-import live data online
### every time this document is generated, by setting the IMPORT_LIVE_DATA
### variable to be FALSE. This will instead import static values from a
### locally-cached .csv data file.
### 2. That anonymised cached version of the dataset is distributed publicly
### with these files, so that external (non-NZBRI) users can also generate
### and alter the document, without needing the live data import functions
### provided by the in-house `chchpd` package.
### 3. You should usually choose not to re-compute the latent class models on
### each run, as this is very time-consuming (hours to days). Instead use
### the cached model objects to save re-computing them each time, by setting
### RUN_NEW_CUBIC_MODELS and RUN_NEW_LINEAR_MODELS to FALSE.
################################################################################
IMPORT_LIVE_DATA = FALSE
RUN_NEW_CUBIC_MODELS = FALSE
RUN_NEW_LINEAR_MODELS = FALSE
################################################################################
# define the maximum number of classes to run models to:
max_classes_cubic = 5
max_classes_linear = 5
# define colour scheme values for normal, MCI, and dementia:
cog_colours <- c('#339900', '#FF9326', '#D92121')
# colour schemes for two categories:
blue_orange = c('#599BC2', '#E08F6B')
blue_orange_sat = c('#0072B2', '#D55E00')
blue_brown = c('#599BC2', '#CD8969')
blue_brown_sat = c('#0072B2', '#B85628')
border_colour = '#8E2226' # JMD reddish
fig_1_width = 8.5; fig_1_height = 20.0
fig_2_width = 17.5; fig_2_height = 21.5
DPI = 600 # set resolution of exported PNGs
```
```{r import-packages}
# An NZBRI-only in-house package for importing the latest live data:
if (IMPORT_LIVE_DATA) {
library(chchpd) # initial install via devtools::install_github('nzbri/chchpd')
}
library(readr) # to read/write locally cached .csv data files
library(dplyr) # for data manipulation
library(tidyr) # for fill & pivot_
library(lubridate) # for date intervals
library(magrittr) # for the %<>% operator
library(ggplot2) # for visualisation
library(scales) # improved labelling in ggplot
library(ggnewscale) # allow multiple colour scales in ggplot
library(directlabels)# to label lines on plots
library(cowplot) # combine multiple separate plots in one figure
library(lcmm) # latent class models
library(parallel) # for parallel computation
library(huxtable) # tables that work in Word output
library(knitr) # for kable pretty tables
library(kableExtra) # extra kable options
library(papaja) # for apa_table
library(stringr) # to manipulate apa_table output
```
```{r font-issues}
# Need this to get around a bug after an update in macOS Monterey in not being
# able to find fonts:
library(showtext)
# Using my favourite font will limit portability of this script:
#font_add('GillSans', '/System/Library/Fonts/Supplemental/GillSans.ttc')
# So instead go online for a font that everyone can access, regardless of
# platform (Lato is not as beautiful as Gill Sans but an okay-ish substitute).
font_add_google(name = 'Lato', family = 'Lato', regular.wt = 400, bold.wt = 700)
# Note can choose a lighter regular weight for this font if desired (300 or
# even 100, see https://fonts.google.com/specimen/Lato
showtext_auto()
# needed to avoid microscopic text in PNG output:
showtext_opts(dpi = DPI)
# see https://community.rstudio.com/t/font-gets-really-small-when-saving-to-png-using-ggsave-and-showtext/147029/7
```
```{r import-data}
# evaluated only if the chchpd package has been imported, to
# access live data sources:
if (IMPORT_LIVE_DATA) { # put this here for when not knitting
participants = chchpd::import_participants(anon_id = FALSE) %>%
rename(group = participant_group)
sessions = chchpd::import_sessions() %>%
# the PDD follow-up sessions were mostly not coded as part of the
# Progression study, so gather sessions from both:
filter(study %in% c('Follow-up', 'PDD Followup')) %>%
# but some sessions *were* entered in both studies, so remove any duplicates:
group_by(session_id) %>%
filter(row_number() == 1) %>%
# this person's session seems to have a fictitious WTAR score (none should
# have been gathered on that session, no original can be found,
# and the value changes so markedly from the two previous ones that it
# results in a separate class just to fit this one person). Can
# drop this filter step once the value is removed from the database:
filter(session_id != '310PDS_2020-03-04')
np = chchpd::import_neuropsyc()
updrs = chchpd::import_motor_scores()
}
```
```{r collate-dataset}
if (IMPORT_LIVE_DATA) {
dat = right_join(participants, sessions, by = 'subject_id') %>%
left_join(np, by = 'session_id') %>%
left_join(updrs, by = 'session_id')
dat = dat %>%
filter(np_group %in% c('Control', 'PD'), # exclude atypical cases
!is.na(cognitive_status), # remove not-yet classified sessions
!is.na(WTAR)) %>%
mutate(group = factor(group, levels = c('Control', 'PD'))) %>%
mutate(cognitive_status = factor(cognitive_status,
levels = c('N', 'MCI', 'D'),
labels = c('Normal', 'MCI', 'Dementia'))) %>%
group_by(subject_id) %>%
arrange(years_from_baseline) %>%
mutate(assessment_num = row_number()) %>%
mutate(n_sessions = max(assessment_num)) %>%
filter(n_sessions > 1) %>% # exclude cases without follow-up
mutate(ever_demented = 'Dementia' %in% cognitive_status) %>%
# to make line colours match the destination point rather than the origin
# point, need to use a diagnosis variable that leads the current diagnosis
# for each person by one step:
mutate(cognitive_status_lead = lead(cognitive_status)) %>%
# this makes the last value for each person NA, which can cause issues.
# fill() replaces them with the latest previous diagnosis:
fill(cognitive_status_lead) %>%
mutate(sampling_interval =
years_from_baseline - lag(years_from_baseline)) %>%
# should be much the same but here to be explicit:
mutate(wtar_interval =
interval(start = lag(session_date), end = session_date)/years(1)) %>%
ungroup()
# lcmm insists on access to a numeric subject variable:
dat = dat %>%
mutate(subject_id = factor(subject_id)) %>%
mutate(subject_id_num = as.integer(subject_id))
# for lcmm we standardise the age to avoid problems with polynomial effects,
# as per Proust-Lima et al. (2017). Note that we are using the grand mean age at
# assessment, rather than averaging across some landmark age for each subject.
# We use the same standardisation for the linear models, so we can compare them.
dat$age_std = (dat$age - mean(dat$age))/10
dat = dat %>%
arrange(subject_id_num, age) %>%
select(subject_id_num, group, sex, ethnicity, symptom_onset_age,
diagnosis_age, education, age, age_std, wtar_interval, group,
cognitive_status, cognitive_status_lead, ever_demented, WTAR, MoCA,
global_z, attention_domain, executive_domain, visuo_domain,
learning_memory_domain, language_domain, H_Y, Part_III)
# store a cached version of the dataset for optional access on subsequent runs
# of generating this document:
dat %>% write_csv(file = 'data/dat_wtar.csv')
}
# Regardless of whether IMPORT_LIVE_DATA has been set to TRUE, we import from
# the cached dataset to ensure consistency:
dat = read_csv(file = 'data/dat_wtar.csv') %>%
mutate(cognitive_status =
factor(cognitive_status,
levels = c('Normal', 'MCI', 'Dementia')),
cognitive_status_lead =
factor(cognitive_status_lead,
levels = c('Normal', 'MCI', 'Dementia')),
cognitive_group =
case_when(group == 'Control' ~ 'Control',
cognitive_status == 'Normal' ~ 'PDN',
cognitive_status == 'MCI' ~ 'PD-MCI',
cognitive_status == 'Dementia' ~ 'PDD'),
cognitive_group =
factor(cognitive_group,
levels = c('Control', 'PDN', 'PD-MCI', 'PDD'))) %>%
# labels to avoid abbreviations in Figure 1:
mutate(panel_label_1 =
factor(cognitive_group,
labels = c('Control', 'PD unimpaired', 'PD-MCI', 'PD dementia'))) %>%
# divide subjects into three panels for Figure 2:
mutate(panel_label_2 =
case_when(group == 'Control' ~ 'Control',
ever_demented == FALSE ~ 'PD non-dementia',
TRUE ~ 'PD dementia'),
panel_label_2 =
factor(panel_label_2,
levels = c('Control', 'PD non-dementia', 'PD dementia'))) %>%
# do this to avoid an issue with lcmm not correctly detecting numeric
# column types in a tibble:
as.data.frame()
```
# Introduction
The prevalence of Parkinson's is
rising,[@myall2017_ParkinsonOldestOld; @pitcher2018_ParkinsonDiseaseEthnicities]
and hence the burden of dementia associated with the disease will also continue
to grow. There is therefore value in detecting the transitional stage of mild
cognitive impairment in Parkinson's, in particular because it may provide a
therapeutic window for future treatments, prior to the irreversible pathological
damage associated with dementia. To provide for consistent delineation of this
transitional state, the International Parkinson and Movement Disorder
Society (MDS) published guidelines in 2012 for the diagnosis of mild cognitive
impairment in Parkinson's (PD-MCI).[@litvan2012_DiagnosticCriteriaMild]
When the diagnosis is based on a comprehensive battery of multiple individual
neuropsychological tests (rather than on a single scale of global cognitive
ability), the MDS guidelines propose that significant impairment may be
determined in three ways. Firstly, current performance on a test can be shown to
be below appropriate norms (which may be established either relative to
standardized norms for that test, or relative to a local matched control group).
This approach has the disadvantage that the cause of poor current performance
cannot be distinguished between that due to a recent decline (such as the onset
of MCI or dementia), or a long-standing poor level of cognitive ability. To
unambiguously detect recent impairment in cognitive status therefore requires
some way of demonstrating a _decline_ in function within an individual, rather
than simply poor current performance. To achieve this, the guidelines suggest
seeking evidence of either a decline in performance on serial testing, or, in
the absence of such prior testing, evidence of a decline from an individual's
_estimated_ premorbid level of function. For pragmatic reasons, however, methods
based upon current norms still tend to predominate in research
studies.[@aarsland2021_ParkinsonDiseaseassociatedCognitive] In practice, one
seldom has reference to prior formal serial testing. Even in a well-resourced
research setting, serial testing remains somewhat ambiguous, as the first
available test session may itself be contaminated by early disease-related
cognitive impairment. This would produce a falsely low baseline and thereby
impede the ability to detect cognitive decline.
A reliable method of estimating premorbid cognitive function is therefore
appealing. Establishing a person's premorbid ability addresses the shortcomings
of both the current-norms approach (by showing whether current poor performance
is indeed a decline from past status), and the issue with the timing of serial
testing (which will usually begin only after disease diagnosis). The Wechsler
Test of Adult Reading (WTAR)[@psychologicalcorporation2001_WTARWechslerTest] was
suggested in the MDS guidelines as being suitable for this purpose (along with
the National Adult Reading Test,
NART).[@strauss2006_CompendiumNeuropsychologicalTests] Both tests are based on
the ability to correctly pronounce phonetically-irregular English words, such as
'porpoise' or 'hyperbole'. Unlike many neuropsychological functions, this
ability has been shown to be preserved in the face of both normal ageing and a
broad range of brain
insults.[@psychologicalcorporation2001_WTARWechslerTest; @strauss2006_CompendiumNeuropsychologicalTests]
When the WTAR was initially normed, it was tested in Parkinson's as well as a
number of other neurological or psychiatric disorders, such as Huntington's,
schizophrenia, and traumatic brain
injury.[@psychologicalcorporation2001_WTARWechslerTest] Only in Alzheimer's
disease were scores shown to be significantly lower than in matched
controls.[@psychologicalcorporation2001_WTARWechslerTest; @strauss2006_CompendiumNeuropsychologicalTests]
Additionally in the Alzheimer's group, WTAR scores were lower in those with
lower overall current cognitive
status,[@strauss2006_CompendiumNeuropsychologicalTests] further indicating that
it is not a stable measure of premorbid function in that condition. Those
results were, however, published in 2001, well before the formal MDS guidelines
on diagnosing PD-MCI[@litvan2012_DiagnosticCriteriaMild] and Parkinson's
dementia[@dubois2007_DiagnosticProceduresParkinson] were promulgated. It is
therefore unknown to what extent the findings from that sample are valid in the
face of the significant cognitive deterioration that can occur in Parkinson's,
as it could not have been formally diagnosed at the time. Moreover, their
Parkinson's sub-sample was also comprised of only 10 people: given the marked
heterogeneity of function in this disorder, this restricts the validity of
generalizing from those findings. We know of only one study in which the WTAR
has actually been used as a criterion against which to establish mild cognitive
impairment in Parkinson's.[@marras2013_MeasuringMildCognitive] The proportion
classified as PD-MCI using WTAR-estimated premorbid functioning was strikingly
higher than when using the more conventional method of comparing current
performance against norms (79% vs 33%). This naturally leads to questions about
whether the
WTAR is in fact a valid measure to be used for cognitive classification in
Parkinson's. In particular, if using the WTAR leads to most people with
Parkinson's being classified as having MCI, does that result in a category that
no longer has any prognostic or discriminative utility?
Performance on the WTAR is known to be impacted negatively by Alzheimer’s
dementia, but as noted the initial claim of stability in Parkinson’s was based
on very limited evidence.[@psychologicalcorporation2001_WTARWechslerTest] We
therefore sought to examine WTAR scores longitudinally in a large and
cognitively formally-characterized cohort that spanned the range from normal
function through to mild cognitive impairment and dementia. We included only
those participants who had completed a minimum of two WTARs, so that the
trajectory of the measure within individuals over time could be examined. To do
this, we used latent class trajectory analysis[@proust-lima2017estimation-of-e].
This allows for detecting heterogeneous patterns of change over time in a
data-driven fashion, rather than assigning individuals to _a priori_ sub-groups.
That is, we combined both Control and Parkinson's participants in a single pool.
The modeling process examines individual subjects and assigns them into clusters
depending on similarities in their longitudinal trajectories. If the presence of
either Parkinson's or cognitive impairment affects longitudinal WTAR scores,
this should be reflected by differential membership across groups in the
resulting data-driven trajectory clusters. For example, Parkinson’s participants
with normal cognition might cluster together with most of the Controls in a
“flat” trajectory class, while those showing cognitive impairment might cluster
in one or more trajectory classes showing decline over time. Conversely, if the
WTAR is truly stable in people with Parkinson’s, we would expect them to cluster
together with controls in relatively flat trajectory classes, regardless of
their current level of cognitive function.In particular, we sought to examine
whether WTAR performance declines in the dementia due to Parkinson's, as it does
in Alzheimer's. If so, this would impact its usefulness in estimating premorbid
function in the way proposed in the MDS guidelines for cognitive diagnosis.
# Methods
## Participants
```{r descriptives}
descriptives = dat %>%
group_by(subject_id_num) %>%
mutate(session_num = row_number(),
n_sessions = max(session_num)) %>%
slice_head() %>% # one row per subject
ungroup()
n_group = descriptives %>%
group_by(group) %>%
summarise(n = n(),
min_sessions = min(n_sessions),
mean_sessions = mean(n_sessions),
sd_sessions = sd(n_sessions),
max_sessions = max(n_sessions))
n_wtar = descriptives %>%
summarise(n = n(),
min_sessions = min(n_sessions),
mean_sessions = mean(n_sessions),
sd_sessions = sd(n_sessions),
max_sessions = max(n_sessions),
total_sessions = sum(n_sessions))
n_ethnicity = descriptives %>%
group_by(group, ethnicity) %>%
summarise(n = n()) %>%
# remove some (currently 3) missing values, which otherwise cause problems:
drop_na()
n_pd_with_ethnicity =
with(n_ethnicity, sum(n[group == 'PD']))
n_pd_pakeha =
with(n_ethnicity, n[group == 'PD' & ethnicity == 'New Zealand European'])
n_pd_maori =
with(n_ethnicity, n[group == 'PD' & ethnicity == 'Maori'])
n_pd_samoan =
with(n_ethnicity, n[group == 'PD' & ethnicity == 'Samoan'])
n_pd_indian =
with(n_ethnicity, n[group == 'PD' & ethnicity == 'Indian'])
n_pd_other =
with(n_ethnicity, n[group == 'PD' & ethnicity == 'Other'])
n_control_with_ethnicity =
with(n_ethnicity, sum(n[group == 'Control']))
n_control_pakeha =
with(n_ethnicity, n[group == 'Control' & ethnicity == 'New Zealand European'])
n_control_other =
with(n_ethnicity, n[group == 'Control' & ethnicity == 'Other'])
WTAR_by_status = dat %>%
group_by(cognitive_group) %>%
summarise(wtar_mean = mean(WTAR),
wtar_median = median(WTAR),
wtar_sd = sd(WTAR),
wtar_n = n())
durations = dat %>% # 1155 rows
filter(group == 'PD') %>% # 902 rows
group_by(subject_id_num) %>%
arrange(age) %>%
mutate(disease_duration = age - diagnosis_age) %>%
summarise(dur_at_onset = first(disease_duration), # 252 rows
dur_at_end = last(disease_duration),
dur_of_followup = dur_at_end - dur_at_onset) %>%
summarise(mean_at_onset =
format(round(mean(dur_at_onset), digits = 1), digits = 2, nsmall = 1), # 1 row
min_at_onset = round(min(dur_at_onset)),
max_at_onset = round(max(dur_at_onset)),
mean_followup =
format(round(mean(dur_of_followup), digits = 1), digits = 2, nsmall = 1),
min_followup = round(min(dur_of_followup)),
max_followup = round(max(dur_of_followup)))
```
The New Zealand Parkinson's Progression Programme (NZP^3^) is a longitudinal
study of a convenience prevalence sample of idiopathic Parkinson's
participants,[@macaskill2022_NewZealandParkinsona] largely recruited from the
specialist Movement Disorders Clinic at the New Zealand Brain Research Institute
(NZBRI). Ethical approval was granted by the Southern Health and Disability
Ethics Committee of the New Zealand Ministry of Health. The ongoing recruitment
commenced in 2007, and includes patients ranging from the recently-diagnosed to
those with advanced disease. Inclusion and exclusion criteria have been
described previously.[@wood2016_DifferentPDMCICriteria] For this analysis, we
selected data from the `r n_group$n[n_group$group =='PD']` Parkinson's and
`r n_group$n[n_group$group == 'Control']` Control participants who had completed
at least two WTARs (range 2 – `r n_wtar$max_sessions`, mean =
`r round(n_wtar$mean_sessions, digits = 1)`, total number of measures =
`r n_wtar$total_sessions`). The period between successive WTAR assessments was
multi-modal, mostly clustering around intervals of one and two years, due to
varying follow-up periods over the course of the wider study (see Supplementary
Material). Of the Parkinson's participants, the mean duration since diagnosis
at the first WTAR assessment was `r durations$mean_at_onset` years (range
`r durations$min_at_onset` – `r durations$max_at_onset`) and the mean duration
of follow-up was a further `r durations$mean_followup` years (range
`r durations$min_followup` – `r durations$max_followup`). All participants were
classified as having normal cognition, mild cognitive impairment, or dementia
via a comprehensive Level II neuropsychological
battery,[@dalrymple-alford2011_CharacterizingMildCognitive;@wood2016_DifferentPDMCICriteria]
administered in accordance with MDS
guidelines.[@dubois2007_DiagnosticProceduresParkinson;@litvan2012_DiagnosticCriteriaMild]
A PD-MCI classification was based on at least two test scores falling $\geq$ 1.5
standard deviations below norms in at least one cognitive domain, without
significant impairment in activities of daily living. For PD dementia, a
significant impairment ($\geq$ 2.0 standard deviations below normative data) in
at least two cognitive domains was required, as well as evidence of significant
impairment in activities of daily
living.[@dalrymple-alford2011_CharacterizingMildCognitive;@wood2016_DifferentPDMCICriteria]
The WTAR itself was not used in the cognitive diagnostic classification
procedures. Characteristics of the sample are shown in Table \@ref(tab:table-1).
All participants were speakers of New Zealand English, and were assessed against
the US norms of the WTAR, corrected for age, sex, and years of education. The
provided ethnicity classifications are not applicable in a New Zealand context
and all participants were assessed against norms for "Whites". Of the
`r n_pd_with_ethnicity` Parkinson's participants with a recorded ethnicity,
`r n_pd_pakeha`
(`r scales::label_percent(accuracy = 0.1)(n_pd_pakeha/n_pd_with_ethnicity)`)
identified as Pākehā (New Zealand European), `r n_pd_maori` as Māori,
`r n_pd_samoan` as Samoan, `r n_pd_indian` as Indian, and `r n_pd_other` were
'Other'. Of the Controls, `r n_control_pakeha`
(`r scales::label_percent(accuracy = 0.1)(n_control_pakeha/n_control_with_ethnicity)`)
identified as Pākehā and `r n_control_other` was 'Other'.
## Latent class trajectory modeling
We fitted latent class trajectory models using the _hlme_ function from the
_lcmm_ package [@proust-lima2017estimation-of-e] (version
`r packageVersion('lcmm')`), running in the R statistical environment
[@R-Core-Team_R_2021] (version `r R.version$major`.`r R.version$minor`), and
used the _tidyverse_ constellation of packages [@wickham2019_WelcomeTidyverse]
for data manipulation and vizualisation. The dependent variable was
WTAR-estimated premorbid IQ, initially modeled within each subject as a
polynomial (cubic) function of age, to allow for non-linear trajectories. The
resulting trajectories were close to straight lines (see Supplementary Material)
so we simplified the models to be a linear function of age. The models were not
informed by any other variables (such as the diagnostic categories of PD-MCI or
dementia). We used age rather than disease duration as the time metric to allow
for direct comparison against any aging effect in controls.
A latent class model is fitted by first specifying an _a priori_ number of
classes (i.e. clusters) into which the participants can be divided. For example,
if just one cluster is specified, then the trajectory that is produced will
simply describe the average change in score over time for all participants,
disregarding any possible sub-groups. If two classes are specified, then the
model will determine two trajectories that optimally separate the total sample
into two clusters, on the basis of differing performance over time. Multiple
independent models are fitted, with increasing numbers of classes. Those models
are then compared to see which produces the best description of the data. The
classes are termed 'latent' as they are not known _a priori_, but instead arise
from the data. The resulting classes can, however, then be compared to known
groupings (such as Parkinson's vs Controls). This allows us to examine to what
extent performance over time might be driven by known factors. For example, if
Control and Parkinson's participants perform differently over time, they should
fall unequally into the various latent classes (for models with more than one
class).
We fitted five separate models, with the specified number of latent classes
ranging from 1 to 5. The _gridsearch_ function of the _lcmm_ package was used
to run 500 departures from random initial values for each multi-class model,
using the one-class model from which to generate the starting values. The
maximum number of iterations within the _hlme_ function was set at 1000.
Parameters corresponding to the best log-likelihood were used as initial values
for the final estimation of the parameters[@proust-lima2017estimation-of-e]. The
optimal model of the five candidates was selected on the basis of it converging
and having the lowest BIC (Bayesian information criterion). That is, this
allowed us to determine whether longitudinal performance on the WTAR was best
described by the participants falling into either 1, 2, 3, 4, or 5 underlying
trajectory classes.
When reporting values from the chosen model, we formed the interval in brackets
following the maximum likelihood estimate (MLE) by subtracting and adding 1.96
times the standard error given by _hlme_ to the MLE.
```{r table-1}
table_df = dat %>%
group_by(subject_id_num) %>%
arrange(age) %>%
slice_tail() %>% # last observation per subject
group_by(group) %>%
summarise(n = n(),
Male = scales::percent_format(accuracy = 0.1)(sum(sex == 'Male')/n),
Age = paste0(sprintf('%.1f', mean(age)), ' (', sprintf('%.1f',sd(age)), ')'),
Education = paste0(sprintf('%.1f', mean(education)), ' (', sprintf('%.1f', sd(education)), ')'),
WTAR = paste0(sprintf('%.1f', mean(WTAR)), ' (', sprintf('%.1f', sd(WTAR)), ')'),
Normal = scales::percent_format(accuracy = 0.1)(sum(cognitive_status == 'Normal')/n),
MCI = scales::percent_format(accuracy = 0.1)(sum(cognitive_status == 'MCI')/n),
Dementia = scales::percent_format(accuracy = 0.1)(sum(cognitive_status == 'Dementia')/n)) %>%
mutate(n = as.character(n)) %>%
pivot_longer(n:Dementia) %>%
pivot_wider(names_from = 'group')
table_df %>%
mutate(name = case_when(name == 'Age' ~ 'Age (SD)',
name == 'Education' ~ 'Years of education (SD)',
name == 'WTAR' ~ 'WTAR (SD)',
name == 'Normal' ~ 'Normal cognition',
TRUE ~ name)) %>%
rename(' ' = name,
"Parkinson's" = PD) %>%
apa_table(caption = "Demographics of the Parkinson's and Control groups, and cognitive status as at their latest assessment.", align = 'lrr')
```
## Reproducibility
The code and anonymised dataset extracts sufficient to reproduce the analyses
and generate this manuscript are publicly available at
[github.com/nzbri/wtar-trajectory](https://github.com/nzbri/wtar-trajectory).
There are a number of decisions that can affect the outcome of a latent class
modeling analysis and for reproducibility in the Supplementary Material we
therefore report our performance against the 16-item checklist "Guidelines for
Reporting on Latent Trajectory Studies".[@van_de_schoot_grolts-checklist_2017]
# Results
```{r wtar-modeling-cubic, include=FALSE}
# prepare list to hold models with 1 through max_classes_cubic classes:
cubic_models = list()
if (RUN_NEW_CUBIC_MODELS) {
# currently have to coerce data to a dataframe (done above), as tibbles lead
# to a bug with the subject id column not being accepted by lcmm as numeric.
# use the 1-class model to set initial start values:
cubic_models[[1]] <-
lcmm::hlme(WTAR ~ poly(age_std, degree = 3, raw = TRUE),
random = ~1 + poly(age_std, degree = 3, raw = TRUE),
subject = 'subject_id_num', ng = 1, data = dat)
saveRDS(cubic_models[[1]], 'models/cubic_WTAR_1.RDS') # cache to disk
# for each higher-class model, run with 5000 random starts based on the
# initial one-class model, then run the selected one up to 1000 times to
# (hopeful) convergence:
for (class_num in 2:max_classes_cubic) {
print(class_num)
# make use of parallel computation to speed up working through the large
# number of iterations to be used for each model. Needs to be done within
# the loop to work around an issue when knitting in R Markdown with the
# generation of random numbers in the parallel package:
num_cores = parallel::detectCores()
cl = parallel::makeCluster(num_cores)
# need to explicitly pass some variables to the namespace of each process,
# or the gridsearch and hlme functions below will trip up by not "knowing"
# some of their parameters:
parallel::clusterExport(cl, c('class_num', 'cubic_models'))
cubic_models[[class_num]] <-
lcmm::gridsearch(rep = 500, maxiter = 100, minit = cubic_models[[1]], cl = cl,
hlme(WTAR ~ poly(age_std, degree = 3, raw = TRUE),
random = ~1 + poly(age_std, degree = 3, raw = TRUE),
ng = class_num, nwg = TRUE, maxiter = 1000,
mixture = ~ poly(age_std, degree = 3, raw = TRUE),
subject = 'subject_id_num', data = dat))
parallel::stopCluster(cl)
saveRDS(cubic_models[[class_num]], paste0('models/cubic_WTAR_',
class_num, '.RDS'))
}
} else { # use previously-computed cubic models:
for (class_num in 1:max_classes_cubic) {
cubic_models[[class_num]] = readRDS(paste0('models/cubic_WTAR_',
class_num, '.RDS'))
}
}
cubic_WTAR_model_table =
summarytable(cubic_models[[1]], cubic_models[[2]], cubic_models[[3]],
cubic_models[[4]], cubic_models[[5]],
which = c('G', 'loglik', 'conv', 'BIC', 'entropy',
'%class')) %>%
as_tibble() %>%
mutate(conv = factor(conv, levels = c(1, 2, 3, 4, 5),
labels = c('yes', 'no', '', 'optimisation problem',
'optimisation problem'))) %>%
rename(converged = conv)
# manually select model, on the basis of one that converged & had lowest BIC:
chosen_cubic_classes = 1
chosen_cubic_model = cubic_models[[chosen_cubic_classes]]
# create a vector of ages to predict against, centred on the mean across all
# sessions, and in decades rather than years to limit polynomial instabilities):
mean_age = mean(dat$age)
new_data_cubic =
data.frame(age = seq(from = 43, to = 89, by = 0.5)) %>%
mutate(age_std = (age - mean_age)/10)
# create a dataframe that contains predictions for all candidate models
# (will be used in the supplementary material).
predictions = list()
for (i in 1:max_classes_cubic) {
prediction = lcmm::predictY(cubic_models[[i]], newdata = new_data_cubic,
var.time = 'age_std', draws = TRUE)
predictions[[i]] =
as.data.frame(prediction$pred) %>% # extract just the predicted values
cbind(new_data_cubic) # and add in the corresponding age values
predictions[[i]]$ng = i # and label with the number of classes
}
# the first one needs to be modified so that it can act as the template to
# row bind to the others:
predictions[[1]] = predictions[[1]] %>% # add a suffix to the variable names
rename('Ypred_class1' = 'Ypred',
'lower.Ypred_class1' = 'lower.Ypred',
'upper.Ypred_class1' = 'upper.Ypred') %>%
select(ng, age, age_std, everything()) # set order for the resulting dataframe
cubic_predictions = bind_rows(predictions[1:5]) %>%
# reshape to allow ggplotting:
pivot_longer(cols = Ypred_class1:last_col(),
names_to = c('line', 'class'), names_sep = '_',
values_to = 'predicted_WTAR') %>%
pivot_wider(names_from = line, values_from = predicted_WTAR) %>%
filter(!is.na(Ypred)) %>%
arrange(ng, class, age)
```
```{r wtar-modeling-linear, include=FALSE}
# prepare list to hold models with 1 through max_classes_linear classes:
linear_models = list()
if (RUN_NEW_LINEAR_MODELS) {
# currently have to coerce data to a dataframe (done above), as tibbles lead
# to a bug with the subject id column not being accepted by lcmm as numeric.
# use the 1-class model to set initial start values:
linear_models[[1]] <-
lcmm::hlme(WTAR ~ age_std,
random = ~1 + age_std,
subject = 'subject_id_num', ng = 1, data = dat)
saveRDS(linear_models[[1]], 'models/linear_WTAR_1.RDS') # cache to disk
# for each higher-class model, run with 5000 random starts based on the
# initial one-class model, then run the selected one up to 1000 times to
# (hopeful) convergence:
for (class_num in 2:max_classes_linear) {
print(class_num)
# make use of parallel computation to speed up working through the large
# number of iterations to be used for each model. Needs to be done within
# the loop to work around an issue when knitting in R Markdown with the
# generation of random numbers in the parallel package:
num_cores = parallel::detectCores()
cl = parallel::makeCluster(num_cores)
# need to explicitly pass some variables to the namespace of each process,
# or the gridsearch and hlme functions below will trip up by not "knowing"
# some of their parameters:
parallel::clusterExport(cl, c('class_num', 'linear_models'))
linear_models[[class_num]] <-
lcmm::gridsearch(rep = 500, maxiter = 100, minit = linear_models[[1]],
cl = cl,
hlme(WTAR ~ age_std,
random = ~1 + age_std,
ng = class_num, nwg = TRUE,
mixture = ~ age_std, maxiter = 1000,
subject = 'subject_id_num', data = dat))
parallel::stopCluster(cl)
saveRDS(linear_models[[class_num]], paste0('models/linear_WTAR_',
class_num, '.RDS'))
}
} else { # use previously-computed linear models:
for (class_num in 1:max_classes_linear) {
linear_models[[class_num]] = readRDS(paste0('models/linear_WTAR_',
class_num, '.RDS'))
}
}
linear_WTAR_model_table =
summarytable(linear_models[[1]], linear_models[[2]], linear_models[[3]],
linear_models[[4]], linear_models[[5]],
which = c('G', 'loglik', 'conv', 'BIC', 'entropy',
'%class')) %>%
as_tibble() %>%
mutate(conv = factor(conv, levels = c(1, 2, 3, 4, 5),
labels = c('yes', 'no', '', 'optimisation problem',
'optimisation problem'))) %>%
rename(converged = conv)
# manually select model, on the basis of one that converged & had lowest BIC:
chosen_linear_classes = 2
chosen_linear_model = linear_models[[chosen_linear_classes]]
coeffs = coef(chosen_linear_model)
# create a vector of ages to predict against, centred on the mean across all
# sessions, and in decades rather than years to be consistent with the cubic
# modeling where we need to limit instabilities):
mean_age = mean(dat$age)
new_data_linear =
data.frame(age = seq(from = 43, to = 89, by = 0.5)) %>%
mutate(age_std = (age - mean_age)/10)
# create a dataframe that contains predictions for all candidate models
predictions = list()
for (i in 1:max_classes_linear) {
prediction = lcmm::predictY(linear_models[[i]], newdata = new_data_linear,
var.time = 'age_std', draws = TRUE)
predictions[[i]] =
as.data.frame(prediction$pred) %>% # extract just the predicted values
cbind(new_data_linear) # and add in the corresponding age values
predictions[[i]]$ng = i # and label with the number of classes
}
# the first one needs to be modified so that it can act as the template to
# row bind to the others:
predictions[[1]] = predictions[[1]] %>% # add a suffix to the variable names
rename('Ypred_class1' = 'Ypred',
'lower.Ypred_class1' = 'lower.Ypred',
'upper.Ypred_class1' = 'upper.Ypred') %>%
select(ng, age, age_std, everything()) # set order for the resulting dataframe
linear_predictions = bind_rows(predictions[1:5]) %>%
# reshape to allow ggplotting:
pivot_longer(cols = Ypred_class1:last_col(),
names_to = c('line', 'class'), names_sep = '_',
values_to = 'predicted_WTAR') %>%
pivot_wider(names_from = line, values_from = predicted_WTAR) %>%
filter(!is.na(Ypred)) %>%
arrange(ng, class, age)
chosen_linear_predictions = linear_predictions %>%
filter(ng == chosen_linear_classes) %>%
mutate(class =
factor(class,
levels = c('class2', 'class1'),
labels = c('High performers', 'Typical'))) %>%
rename(class_for_lines = class)
# link subjects to their assigned classes:
dat_with_classes = dat %>%
left_join(chosen_linear_model$pprob, by = 'subject_id_num') %>%
mutate(class =
factor(class,
levels = c('2', '1'),
labels = c('High performers', 'Typical'))) %>%
rowwise() %>%
mutate(class_probability = max(c(prob1, prob2)))
# get a dataframe with just one row per subject
individuals = dat_with_classes %>%
group_by(subject_id_num) %>% arrange(age) %>%
slice_tail()
class_proportions = individuals %>%
group_by(class) %>%
tally() %>%
mutate(proportion = scales::percent_format(accuracy = 1)(n/sum(n)))
class_proportions_by_group = individuals %>%
group_by(group, class) %>%
tally() %>% group_by(group) %>%
mutate(proportion = scales::percent_format(accuracy = 1)(n/sum(n)))
```
```{r percent-above-100}
above_100 = dat %>%
mutate(above_100 = WTAR >= 100) |>
group_by(group, above_100) %>%
tally() %>%
pivot_wider(names_from = above_100,
values_from = n, names_prefix = 'above_') %>%
mutate(proportion =
scales::percent_format(accuracy = 0.1)(above_TRUE/(above_FALSE + above_TRUE)))
```
```{r chi-square}
class_n_by_group = individuals %>%
group_by(group, class) %>%
tally() %>%
pivot_wider(names_from = group, values_from = n)
chi = chisq.test(class_n_by_group[, -1])
chi_n_observed = chi$observed
chi_n_expected = chi$expected
chi_p_value = chi$p.value
chi_statistic = chi$statistic
chi_df = chi$parameter
```
```{r formatted-text-results, include=FALSE}
# extract coefficients from model summary:
summary = data.frame(summary(chosen_linear_model))
typical_slope = summary$coef[row.names(summary) == 'age_std class1']
typical_slope_se = summary$Se[row.names(summary) == 'age_std class1']
typical_intcpt = summary$coef[row.names(summary) == 'intercept class1']
typical_intcpt_se = summary$Se[row.names(summary) == 'intercept class1']
high_slope = summary$coef[row.names(summary) == 'age_std class2']
high_slope_se = summary$Se[row.names(summary) == 'age_std class2']
high_intcpt = summary$coef[row.names(summary) == 'intercept class2']
high_intcpt_se = summary$Se[row.names(summary) == 'intercept class2']
# construct strings for the coefficients and intervals:
typical_slope_str = sprintf('%.1f', typical_slope)
high_slope_str = sprintf('%.1f', high_slope)
typical_intcpt_str = sprintf('%.1f', typical_intcpt)
high_intcpt_str = sprintf('%.1f', high_intcpt)
typical_slope_intrvl =
paste0('[', sprintf('%.1f', typical_slope - typical_slope_se * 1.96), ', ',
sprintf('%.1f', typical_slope + typical_slope_se * 1.96), ']')
high_slope_intrvl =
paste0('[', sprintf('%.1f', high_slope - high_slope_se * 1.96), ', ',
sprintf('%.1f', high_slope + high_slope_se * 1.96), ']')
typical_intcpt_intrvl =
paste0('[', sprintf('%.1f', typical_intcpt - typical_intcpt_se * 1.96), ', ',
sprintf('%.1f', typical_intcpt + typical_intcpt_se * 1.96), ']')
high_intcpt_intrvl =
paste0('[', sprintf('%.1f', high_intcpt - high_intcpt_se * 1.96), ', ',
sprintf('%.1f', high_intcpt + high_intcpt_se * 1.96), ']')
```
## Distribution of scores
The distributions of all WTAR-estimated IQ scores for the Parkinson's and
Control participants are shown in Figure \@ref(fig:figure-1-modified-histogram).
The mean of the latest score for participants in each of the groups is given in
Table \@ref(tab:table-1), with both being well above the expected population
norm mean IQ score of 100. The population distribution of IQ should be
symmetrical about 100, yet in our sample,
`r above_100$proportion[above_100$group == 'PD']` of Parkinson's WTAR scores
were greater than or equal to 100, as were
`r above_100$proportion[above_100$group == 'Control']` of Control scores.
## Longitudinal trajectories
The raw data showed that WTAR scores within individuals were relatively constant
over time (Figure \@ref(fig:figure-2-trajectories)A). This was evident even in
participants whose overall cognitive performance declined substantially, as they
progressed to PD-MCI or dementia (Figure \@ref(fig:figure-2-trajectories)B). We
modeled WTAR scores as a linear function of age, with repeated measures within
individuals, and fitted candidate models that ranged from having one to five
latent trajectory classes. We selected the two-class model on the basis of it
having the lowest BIC value. Full details on the modeling process are provided
in the Supplementary Material.
Individuals were assigned to the class for which they had the highest posterior
classification probability. The mean assigned probability was