Coverage for tasks/nart.py : 47%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#!/usr/bin/env python
3"""
4camcops_server/tasks/nart.py
6===============================================================================
8 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com).
10 This file is part of CamCOPS.
12 CamCOPS is free software: you can redistribute it and/or modify
13 it under the terms of the GNU General Public License as published by
14 the Free Software Foundation, either version 3 of the License, or
15 (at your option) any later version.
17 CamCOPS is distributed in the hope that it will be useful,
18 but WITHOUT ANY WARRANTY; without even the implied warranty of
19 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 GNU General Public License for more details.
22 You should have received a copy of the GNU General Public License
23 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
25===============================================================================
27"""
29import math
30from typing import Any, Dict, List, Optional, Tuple, Type
32from sqlalchemy.ext.declarative import DeclarativeMeta
33from sqlalchemy.sql.sqltypes import Boolean, Float
35from camcops_server.cc_modules.cc_constants import CssClass
36from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo
37from camcops_server.cc_modules.cc_html import answer, td, tr_qa
38from camcops_server.cc_modules.cc_request import CamcopsRequest
39from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup
40from camcops_server.cc_modules.cc_sqla_coltypes import (
41 BIT_CHECKER,
42 CamcopsColumn,
43)
44from camcops_server.cc_modules.cc_summaryelement import SummaryElement
45from camcops_server.cc_modules.cc_task import (
46 Task,
47 TaskHasClinicianMixin,
48 TaskHasPatientMixin,
49)
52WORDLIST = [ # Value is true/1 for CORRECT, false/0 for INCORRECT
53 "chord",
54 "ache",
55 "depot",
56 "aisle",
57 "bouquet",
58 "psalm",
59 "capon",
60 "deny", # NB reserved word in SQL (auto-handled)
61 "nausea",
62 "debt",
63 "courteous",
64 "rarefy",
65 "equivocal",
66 "naive", # accent required
67 "catacomb",
68 "gaoled",
69 "thyme",
70 "heir",
71 "radix",
72 "assignate",
73 "hiatus",
74 "subtle",
75 "procreate",
76 "gist",
77 "gouge",
78 "superfluous",
79 "simile",
80 "banal",
81 "quadruped",
82 "cellist",
83 "facade", # accent required
84 "zealot",
85 "drachm",
86 "aeon",
87 "placebo",
88 "abstemious",
89 "detente", # accent required
90 "idyll",
91 "puerperal",
92 "aver",
93 "gauche",
94 "topiary",
95 "leviathan",
96 "beatify",
97 "prelate",
98 "sidereal",
99 "demesne",
100 "syncope",
101 "labile",
102 "campanile"
103]
104ACCENTED_WORDLIST = list(WORDLIST)
105# noinspection PyUnresolvedReferences
106ACCENTED_WORDLIST[ACCENTED_WORDLIST.index("naive")] = "naïve"
107ACCENTED_WORDLIST[ACCENTED_WORDLIST.index("facade")] = "façade"
108ACCENTED_WORDLIST[ACCENTED_WORDLIST.index("detente")] = "détente"
111# =============================================================================
112# NART
113# =============================================================================
115class NartMetaclass(DeclarativeMeta):
116 # noinspection PyInitNewSignature
117 def __init__(cls: Type['Nart'],
118 name: str,
119 bases: Tuple[Type, ...],
120 classdict: Dict[str, Any]) -> None:
121 for w in WORDLIST:
122 setattr(
123 cls,
124 w,
125 CamcopsColumn(
126 w, Boolean,
127 permitted_value_checker=BIT_CHECKER,
128 comment=f"Pronounced {w} correctly (0 no, 1 yes)"
129 )
130 )
131 super().__init__(name, bases, classdict)
134class Nart(TaskHasPatientMixin, TaskHasClinicianMixin, Task,
135 metaclass=NartMetaclass):
136 """
137 Server implementation of the NART task.
138 """
139 __tablename__ = "nart"
140 shortname = "NART"
142 @staticmethod
143 def longname(req: "CamcopsRequest") -> str:
144 _ = req.gettext
145 return _("National Adult Reading Test")
147 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
148 if not self.is_complete():
149 return CTV_INCOMPLETE
150 return [CtvInfo(
151 content=(
152 "NART predicted WAIS FSIQ {n_fsiq}, WAIS VIQ {n_viq}, "
153 "WAIS PIQ {n_piq}, WAIS-R FSIQ {nw_fsiq}, "
154 "WAIS-IV FSIQ {b_fsiq}, WAIS-IV GAI {b_gai}, "
155 "WAIS-IV VCI {b_vci}, WAIS-IV PRI {b_pri}, "
156 "WAIS_IV WMI {b_wmi}, WAIS-IV PSI {b_psi}".format(
157 n_fsiq=self.nelson_full_scale_iq(),
158 n_viq=self.nelson_verbal_iq(),
159 n_piq=self.nelson_performance_iq(),
160 nw_fsiq=self.nelson_willison_full_scale_iq(),
161 b_fsiq=self.bright_full_scale_iq(),
162 b_gai=self.bright_general_ability(),
163 b_vci=self.bright_verbal_comprehension(),
164 b_pri=self.bright_perceptual_reasoning(),
165 b_wmi=self.bright_working_memory(),
166 b_psi=self.bright_perceptual_speed(),
167 )
168 )
169 )]
171 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
172 return self.standard_task_summary_fields() + [
173 SummaryElement(
174 name="nelson_full_scale_iq",
175 coltype=Float(),
176 value=self.nelson_full_scale_iq(),
177 comment="Predicted WAIS full-scale IQ (Nelson 1982)"),
178 SummaryElement(
179 name="nelson_verbal_iq",
180 coltype=Float(),
181 value=self.nelson_verbal_iq(),
182 comment="Predicted WAIS verbal IQ (Nelson 1982)"),
183 SummaryElement(
184 name="nelson_performance_iq",
185 coltype=Float(),
186 value=self.nelson_performance_iq(),
187 comment="Predicted WAIS performance IQ (Nelson 1982"),
188 SummaryElement(
189 name="nelson_willison_full_scale_iq",
190 coltype=Float(),
191 value=self.nelson_willison_full_scale_iq(),
192 comment="Predicted WAIS-R full-scale IQ (Nelson & Willison 1991"), # noqa
193 SummaryElement(
194 name="bright_full_scale_iq",
195 coltype=Float(),
196 value=self.bright_full_scale_iq(),
197 comment="Predicted WAIS-IV full-scale IQ (Bright 2016)"),
198 SummaryElement(
199 name="bright_general_ability",
200 coltype=Float(),
201 value=self.bright_general_ability(),
202 comment="Predicted WAIS-IV General Ability Index (Bright 2016)"), # noqa
203 SummaryElement(
204 name="bright_verbal_comprehension",
205 coltype=Float(),
206 value=self.bright_verbal_comprehension(),
207 comment="Predicted WAIS-IV Verbal Comprehension Index (Bright 2016)"), # noqa
208 SummaryElement(
209 name="bright_perceptual_reasoning",
210 coltype=Float(),
211 value=self.bright_perceptual_reasoning(),
212 comment="Predicted WAIS-IV Perceptual Reasoning Index (Bright 2016)"), # noqa
213 SummaryElement(
214 name="bright_working_memory",
215 coltype=Float(),
216 value=self.bright_working_memory(),
217 comment="Predicted WAIS-IV Working Memory Index (Bright 2016)"), # noqa
218 SummaryElement(
219 name="bright_perceptual_speed",
220 coltype=Float(),
221 value=self.bright_perceptual_speed(),
222 comment="Predicted WAIS-IV Perceptual Speed Index (Bright 2016)"), # noqa
223 ]
225 def is_complete(self) -> bool:
226 return (
227 self.all_fields_not_none(WORDLIST) and
228 self.field_contents_valid()
229 )
231 def n_errors(self) -> int:
232 e = 0
233 for w in WORDLIST:
234 if getattr(self, w) is not None and not getattr(self, w):
235 e += 1
236 return e
238 def get_task_html(self, req: CamcopsRequest) -> str:
239 # Table rows for individual words
240 q_a = ""
241 nwords = len(WORDLIST)
242 ncolumns = 3
243 nrows = int(math.ceil(float(nwords)/float(ncolumns)))
244 column = 0
245 row = 0
246 # x: word index (shown in top-to-bottom then left-to-right sequence)
247 for unused_loopvar in range(nwords):
248 x = (column * nrows) + row
249 if column == 0: # first column
250 q_a += "<tr>"
251 q_a += td(ACCENTED_WORDLIST[x])
252 q_a += td(answer(getattr(self, WORDLIST[x])))
253 if column == (ncolumns - 1): # last column
254 q_a += "</tr>"
255 row += 1
256 column = (column + 1) % ncolumns
258 # Annotations
259 nelson = "; Nelson 1982 <sup>[1]</sup>"
260 nelson_willison = "; Nelson & Willison 1991 <sup>[2]</sup>"
261 bright = "; Bright 2016 <sup>[3]</sup>"
263 # HTML
264 h = """
265 <div class="{CssClass.SUMMARY}">
266 <table class="{CssClass.SUMMARY}">
267 {tr_is_complete}
268 {tr_total_errors}
270 {nelson_full_scale_iq}
271 {nelson_verbal_iq}
272 {nelson_performance_iq}
273 {nelson_willison_full_scale_iq}
275 {bright_full_scale_iq}
276 {bright_general_ability}
277 {bright_verbal_comprehension}
278 {bright_perceptual_reasoning}
279 {bright_working_memory}
280 {bright_perceptual_speed}
281 </table>
282 </div>
283 <div class="{CssClass.EXPLANATION}">
284 Estimates premorbid IQ by pronunciation of irregular words.
285 </div>
286 <table class="{CssClass.TASKDETAIL}">
287 <tr>
288 <th width="16%">Word</th><th width="16%">Correct?</th>
289 <th width="16%">Word</th><th width="16%">Correct?</th>
290 <th width="16%">Word</th><th width="16%">Correct?</th>
291 </tr>
292 {q_a}
293 </table>
294 <div class="{CssClass.FOOTNOTES}">
295 [1] Nelson HE (1982), <i>National Adult Reading Test (NART):
296 For the Assessment of Premorbid Intelligence in Patients
297 with Dementia: Test Manual</i>, NFER-Nelson, Windsor, UK.
298 [2] Nelson HE, Wilson J (1991)
299 <i>National Adult Reading Test (NART)</i>,
300 NFER-Nelson, Windsor, UK; see [3].
301 [3] Bright P et al (2016). The National Adult Reading Test:
302 restandardisation against the Wechsler Adult Intelligence
303 Scale—Fourth edition.
304 <a href="https://www.ncbi.nlm.nih.gov/pubmed/27624393">PMID
305 27624393</a>.
306 </div>
307 <div class="{CssClass.COPYRIGHT}">
308 NART: Copyright © Hazel E. Nelson. Used with permission.
309 </div>
310 """.format(
311 CssClass=CssClass,
312 tr_is_complete=self.get_is_complete_tr(req),
313 tr_total_errors=tr_qa("Total errors", self.n_errors()),
314 nelson_full_scale_iq=tr_qa(
315 "Predicted WAIS full-scale IQ = 127.7 – 0.826 × errors" + nelson, # noqa
316 self.nelson_full_scale_iq()
317 ),
318 nelson_verbal_iq=tr_qa(
319 "Predicted WAIS verbal IQ = 129.0 – 0.919 × errors" + nelson,
320 self.nelson_verbal_iq()
321 ),
322 nelson_performance_iq=tr_qa(
323 "Predicted WAIS performance IQ = 123.5 – 0.645 × errors" +
324 nelson,
325 self.nelson_performance_iq()
326 ),
327 nelson_willison_full_scale_iq=tr_qa(
328 "Predicted WAIS-R full-scale IQ "
329 "= 130.6 – 1.24 × errors" + nelson_willison,
330 self.nelson_willison_full_scale_iq()
331 ),
332 bright_full_scale_iq=tr_qa(
333 "Predicted WAIS-IV full-scale IQ "
334 "= 126.41 – 0.9775 × errors" + bright,
335 self.bright_full_scale_iq()
336 ),
337 bright_general_ability=tr_qa(
338 "Predicted WAIS-IV General Ability Index "
339 "= 126.5 – 0.9656 × errors" + bright,
340 self.bright_general_ability()
341 ),
342 bright_verbal_comprehension=tr_qa(
343 "Predicted WAIS-IV Verbal Comprehension Index "
344 "= 126.81 – 1.0745 × errors" + bright,
345 self.bright_verbal_comprehension()
346 ),
347 bright_perceptual_reasoning=tr_qa(
348 "Predicted WAIS-IV Perceptual Reasoning Index "
349 "= 120.18 – 0.6242 × errors" + bright,
350 self.bright_perceptual_reasoning()
351 ),
352 bright_working_memory=tr_qa(
353 "Predicted WAIS-IV Working Memory Index "
354 "= 120.53 – 0.7901 × errors" + bright,
355 self.bright_working_memory()
356 ),
357 bright_perceptual_speed=tr_qa(
358 "Predicted WAIS-IV Perceptual Speed Index "
359 "= 114.53 – 0.5285 × errors" + bright,
360 self.bright_perceptual_speed()
361 ),
362 q_a=q_a,
363 )
364 return h
366 def predict(self, intercept: float, slope: float) -> Optional[float]:
367 if not self.is_complete():
368 return None
369 return intercept + slope * self.n_errors()
371 def nelson_full_scale_iq(self) -> Optional[float]:
372 return self.predict(intercept=127.7, slope=-0.826)
374 def nelson_verbal_iq(self) -> Optional[float]:
375 return self.predict(intercept=129.0, slope=-0.919)
377 def nelson_performance_iq(self) -> Optional[float]:
378 return self.predict(intercept=123.5, slope=-0.645)
380 def nelson_willison_full_scale_iq(self) -> Optional[float]:
381 return self.predict(intercept=130.6, slope=-1.24)
383 def bright_full_scale_iq(self) -> Optional[float]:
384 return self.predict(intercept=126.41, slope=-0.9775)
386 def bright_general_ability(self) -> Optional[float]:
387 return self.predict(intercept=126.5, slope=-0.9656)
389 def bright_verbal_comprehension(self) -> Optional[float]:
390 return self.predict(intercept=126.81, slope=-1.0745)
392 def bright_perceptual_reasoning(self) -> Optional[float]:
393 return self.predict(intercept=120.18, slope=-0.6242)
395 def bright_working_memory(self) -> Optional[float]:
396 return self.predict(intercept=120.53, slope=-0.7901)
398 def bright_perceptual_speed(self) -> Optional[float]:
399 return self.predict(intercept=114.53, slope=-0.5285)
401 def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:
402 codes = [SnomedExpression(req.snomed(SnomedLookup.NART_PROCEDURE_ASSESSMENT))] # noqa
403 if self.is_complete():
404 codes.append(SnomedExpression(
405 req.snomed(SnomedLookup.NART_SCALE),
406 {
407 # Best value debatable:
408 req.snomed(SnomedLookup.NART_SCORE): self.nelson_full_scale_iq(), # noqa
409 }
410 ))
411 return codes