Coverage for tasks/cardinal_expectationdetection.py: 31%
458 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 14:23 +0100
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 14:23 +0100
1"""
2camcops_server/tasks/cardinal_expectationdetection.py
4===============================================================================
6 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
9 This file is part of CamCOPS.
11 CamCOPS is free software: you can redistribute it and/or modify
12 it under the terms of the GNU General Public License as published by
13 the Free Software Foundation, either version 3 of the License, or
14 (at your option) any later version.
16 CamCOPS is distributed in the hope that it will be useful,
17 but WITHOUT ANY WARRANTY; without even the implied warranty of
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 GNU General Public License for more details.
21 You should have received a copy of the GNU General Public License
22 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
24===============================================================================
26"""
28import logging
29from typing import Any, Dict, List, Optional, Sequence, Tuple, Type
31from cardinal_pythonlib.logs import BraceStyleAdapter
32from matplotlib.axes import Axes
33import numpy
34from pendulum import DateTime as Pendulum
35import scipy.stats # http://docs.scipy.org/doc/scipy/reference/stats.html
36from sqlalchemy.orm import Mapped, mapped_column
37from sqlalchemy.sql.schema import Column, ForeignKey
38from sqlalchemy.sql.sqltypes import Float, Integer
40from camcops_server.cc_modules.cc_constants import (
41 CssClass,
42 MatplotlibConstants,
43 PlotDefaults,
44)
45from camcops_server.cc_modules.cc_db import (
46 ancillary_relationship,
47 GenericTabletRecordMixin,
48 TaskDescendant,
49)
50from camcops_server.cc_modules.cc_html import (
51 answer,
52 div,
53 get_yes_no_none,
54 identity,
55 italic,
56 td,
57 tr,
58 tr_qa,
59)
60from camcops_server.cc_modules.cc_request import CamcopsRequest
61from camcops_server.cc_modules.cc_sqla_coltypes import (
62 PendulumDateTimeAsIsoTextColType,
63)
64from camcops_server.cc_modules.cc_sqlalchemy import Base
65from camcops_server.cc_modules.cc_summaryelement import (
66 ExtraSummaryTable,
67 SummaryElement,
68)
69from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin
71log = BraceStyleAdapter(logging.getLogger(__name__))
74CONVERT_0_P_TO = 0.001 # for Z-transformed ROC plot
75CONVERT_1_P_TO = 0.999 # for Z-transformed ROC plot
77NRATINGS = 5 # numbered 0-4 in the database
78# -- to match DETECTION_OPTIONS.length in the original task
79N_CUES = 8 # to match magic number in original task
81ERROR_RATING_OUT_OF_RANGE = f"""
82 <div class="{CssClass.ERROR}">Can't draw figure: rating out of range</div>
83"""
84WARNING_INSUFFICIENT_DATA = f"""
85 <div class="{CssClass.WARNING}">Insufficient data</div>
86"""
87WARNING_RATING_MISSING = f"""
88 <div class="{CssClass.WARNING}">One or more ratings are missing</div>
89"""
90PLAIN_ROC_TITLE = "ROC"
91Z_ROC_TITLE = (
92 f"ROC in Z coordinates (0/1 first mapped to "
93 f"{CONVERT_0_P_TO}/{CONVERT_1_P_TO})"
94)
96AUDITORY = 0
97VISUAL = 1
100def a(x: Any) -> str:
101 """Answer formatting for this task."""
102 return answer(x, formatter_answer=identity, default="")
105# =============================================================================
106# Cardinal_ExpectationDetection
107# =============================================================================
110class ExpDetTrial(GenericTabletRecordMixin, TaskDescendant, Base):
111 __tablename__ = "cardinal_expdet_trials"
113 cardinal_expdet_id: Mapped[int] = mapped_column(
114 comment="FK to cardinal_expdet",
115 )
116 trial: Mapped[int] = mapped_column(comment="Trial number (0-based)")
118 # Config determines these (via an autogeneration process):
119 block: Mapped[Optional[int]] = mapped_column(
120 comment="Block number (0-based)"
121 )
122 group_num: Mapped[Optional[int]] = mapped_column(
123 comment="Group number (0-based)"
124 )
125 cue: Mapped[Optional[int]] = mapped_column(comment="Cue number (0-based)")
126 raw_cue_number: Mapped[Optional[int]] = mapped_column(
127 comment="Raw cue number (following counterbalancing) (0-based)",
128 )
129 target_modality: Mapped[Optional[int]] = mapped_column(
130 comment="Target modality (0 auditory, 1 visual)",
131 )
132 target_number: Mapped[Optional[int]] = mapped_column(
133 comment="Target number (0-based)"
134 )
135 target_present: Mapped[Optional[int]] = mapped_column(
136 comment="Target present? (0 no, 1 yes)"
137 )
138 iti_length_s: Mapped[Optional[float]] = mapped_column(
139 comment="Intertrial interval (s)"
140 )
142 # Task determines these (on the fly):
143 pause_given_before_trial: Mapped[Optional[int]] = mapped_column(
144 comment="Pause given before trial? (0 no, 1 yes)",
145 )
146 pause_start_time: Mapped[Optional[Pendulum]] = mapped_column(
147 PendulumDateTimeAsIsoTextColType,
148 comment="Pause start time (ISO-8601)",
149 )
150 pause_end_time: Mapped[Optional[Pendulum]] = mapped_column(
151 PendulumDateTimeAsIsoTextColType,
152 comment="Pause end time (ISO-8601)",
153 )
154 trial_start_time: Mapped[Optional[Pendulum]] = mapped_column(
155 PendulumDateTimeAsIsoTextColType,
156 comment="Trial start time (ISO-8601)",
157 )
158 cue_start_time: Mapped[Optional[Pendulum]] = mapped_column(
159 PendulumDateTimeAsIsoTextColType,
160 comment="Cue start time (ISO-8601)",
161 )
162 target_start_time: Mapped[Optional[Pendulum]] = mapped_column(
163 PendulumDateTimeAsIsoTextColType,
164 comment="Target start time (ISO-8601)",
165 )
166 detection_start_time: Mapped[Optional[Pendulum]] = mapped_column(
167 PendulumDateTimeAsIsoTextColType,
168 comment="Detection response start time (ISO-8601)",
169 )
170 iti_start_time: Mapped[Optional[Pendulum]] = mapped_column(
171 PendulumDateTimeAsIsoTextColType,
172 comment="Intertrial interval start time (ISO-8601)",
173 )
174 iti_end_time: Mapped[Optional[Pendulum]] = mapped_column(
175 PendulumDateTimeAsIsoTextColType,
176 comment="Intertrial interval end time (ISO-8601)",
177 )
178 trial_end_time: Mapped[Optional[Pendulum]] = mapped_column(
179 PendulumDateTimeAsIsoTextColType,
180 comment="Trial end time (ISO-8601)",
181 )
183 # Subject decides these:
184 responded: Mapped[Optional[int]] = mapped_column(
185 comment="Responded? (0 no, 1 yes)"
186 )
187 response_time: Mapped[Optional[Pendulum]] = mapped_column(
188 PendulumDateTimeAsIsoTextColType,
189 comment="Response time (ISO-8601)",
190 )
191 response_latency_ms: Mapped[Optional[int]] = mapped_column(
192 comment="Response latency (ms)"
193 )
194 rating: Mapped[Optional[int]] = mapped_column(
195 comment="Rating (0 definitely not - 4 definitely)"
196 )
197 correct: Mapped[Optional[int]] = mapped_column(
198 comment="Correct side of the middle rating? (0 no, 1 yes)",
199 )
200 points: Mapped[Optional[int]] = mapped_column(
201 comment="Points earned this trial"
202 )
203 cumulative_points: Mapped[Optional[int]] = mapped_column(
204 comment="Cumulative points earned"
205 )
207 @classmethod
208 def get_html_table_header(cls) -> str:
209 return f"""
210 <table class="{CssClass.EXTRADETAIL}">
211 <tr>
212 <th>Trial# (0-based)</th>
213 <th>Block# (0-based)</th>
214 <th>Group# (0-based)</th>
215 <th>Cue</th>
216 <th>Raw cue</th>
217 <th>Target modality</th>
218 <th>Target#</th>
219 <th>Target present?</th>
220 <th>ITI (s)</th>
221 <th>Pause before trial?</th>
222 </tr>
223 <tr class="{CssClass.EXTRADETAIL2}">
224 <th>...</th>
225 <th>Pause start@</th>
226 <th>Pause end@</th>
227 <th>Trial start@</th>
228 <th>Cue@</th>
229 <th>Target@</th>
230 <th>Detection start@</th>
231 <th>ITI start@</th>
232 <th>ITI end@</th>
233 <th>Trial end@</th>
234 </tr>
235 <tr class="{CssClass.EXTRADETAIL2}">
236 <th>...</th>
237 <th>Responded?</th>
238 <th>Responded@</th>
239 <th>Response latency (ms)</th>
240 <th>Rating</th>
241 <th>Correct?</th>
242 <th>Points</th>
243 <th>Cumulative points</th>
244 </tr>
245 """
247 # ratings: 0, 1 absent -- 2 don't know -- 3, 4 present
248 def judged_present(self) -> Optional[bool]:
249 if not self.responded:
250 return None
251 elif self.rating >= 3:
252 return True
253 else:
254 return False
256 def judged_absent(self) -> Optional[bool]:
257 if not self.responded:
258 return None
259 elif self.rating <= 1:
260 return True
261 else:
262 return False
264 def didnt_know(self) -> Optional[bool]:
265 if not self.responded:
266 return None
267 return self.rating == 2
269 def get_html_table_row(self) -> str:
270 return (
271 tr(
272 a(self.trial),
273 a(self.block),
274 a(self.group_num),
275 a(self.cue),
276 a(self.raw_cue_number),
277 a(self.target_modality),
278 a(self.target_number),
279 a(self.target_present),
280 a(self.iti_length_s),
281 a(self.pause_given_before_trial),
282 )
283 + tr(
284 "...",
285 a(self.pause_start_time),
286 a(self.pause_end_time),
287 a(self.trial_start_time),
288 a(self.cue_start_time),
289 a(self.target_start_time),
290 a(self.detection_start_time),
291 a(self.iti_start_time),
292 a(self.iti_end_time),
293 a(self.trial_end_time),
294 tr_class=CssClass.EXTRADETAIL2,
295 )
296 + tr(
297 "...",
298 a(self.responded),
299 a(self.response_time),
300 a(self.response_latency_ms),
301 a(self.rating),
302 a(self.correct),
303 a(self.points),
304 a(self.cumulative_points),
305 tr_class=CssClass.EXTRADETAIL2,
306 )
307 )
309 # -------------------------------------------------------------------------
310 # TaskDescendant overrides
311 # -------------------------------------------------------------------------
313 @classmethod
314 def task_ancestor_class(cls) -> Optional[Type["Task"]]:
315 return CardinalExpectationDetection
317 def task_ancestor(self) -> Optional["CardinalExpectationDetection"]:
318 return CardinalExpectationDetection.get_linked( # type: ignore[return-value] # noqa: E501
319 self.cardinal_expdet_id, self
320 )
323class ExpDetTrialGroupSpec(GenericTabletRecordMixin, TaskDescendant, Base):
324 __tablename__ = "cardinal_expdet_trialgroupspec"
326 cardinal_expdet_id: Mapped[int] = mapped_column(
327 comment="FK to cardinal_expdet",
328 )
329 group_num: Mapped[int] = mapped_column(comment="Group number (0-based)")
331 # Group spec
332 cue: Mapped[Optional[int]] = mapped_column(comment="Cue number (0-based)")
333 target_modality: Mapped[Optional[int]] = mapped_column(
334 comment="Target modality (0 auditory, 1 visual)",
335 )
336 target_number: Mapped[Optional[int]] = mapped_column(
337 comment="Target number (0-based)"
338 )
339 n_target: Mapped[Optional[int]] = mapped_column(
340 comment="Number of trials with target present"
341 )
342 n_no_target: Mapped[Optional[int]] = mapped_column(
343 comment="Number of trials with target absent"
344 )
346 DP = 3
348 @classmethod
349 def get_html_table_header(cls) -> str:
350 return f"""
351 <table class="{CssClass.EXTRADETAIL}">
352 <tr>
353 <th>Group# (0-based)</th>
354 <th>Cue (0-based)</th>
355 <th>Target modality (0 auditory, 1 visual)</th>
356 <th>Target# (0-based)</th>
357 <th># target trials</th>
358 <th># no-target trials</th>
359 </tr>
360 """
362 def get_html_table_row(self) -> str:
363 return tr(
364 a(self.group_num),
365 a(self.cue),
366 a(self.target_modality),
367 a(self.target_number),
368 a(self.n_target),
369 a(self.n_no_target),
370 )
372 # -------------------------------------------------------------------------
373 # TaskDescendant overrides
374 # -------------------------------------------------------------------------
376 @classmethod
377 def task_ancestor_class(cls) -> Optional[Type["Task"]]:
378 return CardinalExpectationDetection
380 def task_ancestor(self) -> Optional["CardinalExpectationDetection"]:
381 return CardinalExpectationDetection.get_linked( # type: ignore[return-value] # noqa: E501
382 self.cardinal_expdet_id, self
383 )
386class CardinalExpectationDetection(TaskHasPatientMixin, Task): # type: ignore[misc] # noqa: E501
387 """
388 Server implementation of the Cardinal_ExpDet task.
389 """
391 __tablename__ = "cardinal_expdet"
392 shortname = "Cardinal_ExpDet"
393 use_landscape_for_pdf = True
395 # Config
396 num_blocks: Mapped[Optional[int]] = mapped_column(
397 comment="Number of blocks"
398 )
399 stimulus_counterbalancing: Mapped[Optional[int]] = mapped_column(
400 comment="Stimulus counterbalancing condition",
401 )
402 is_detection_response_on_right: Mapped[Optional[int]] = mapped_column(
403 comment='Is the "detection" response on the right? (0 no, 1 yes)',
404 )
405 pause_every_n_trials: Mapped[Optional[int]] = mapped_column(
406 comment="Pause every n trials"
407 )
408 # ... cue
409 cue_duration_s: Mapped[Optional[float]] = mapped_column(
410 comment="Cue duration (s)"
411 )
412 visual_cue_intensity: Mapped[Optional[float]] = mapped_column(
413 comment="Visual cue intensity (0.0-1.0)"
414 )
415 auditory_cue_intensity: Mapped[Optional[float]] = mapped_column(
416 comment="Auditory cue intensity (0.0-1.0)",
417 )
418 # ... ISI
419 isi_duration_s: Mapped[Optional[float]] = mapped_column(
420 comment="Interstimulus interval (s)"
421 )
422 # .. target
423 visual_target_duration_s: Mapped[Optional[float]] = mapped_column(
424 comment="Visual target duration (s)"
425 )
426 visual_background_intensity: Mapped[Optional[float]] = mapped_column(
427 comment="Visual background intensity (0.0-1.0)",
428 )
429 visual_target_0_intensity: Mapped[Optional[float]] = mapped_column(
430 comment="Visual target 0 intensity (0.0-1.0)",
431 )
432 visual_target_1_intensity: Mapped[Optional[float]] = mapped_column(
433 comment="Visual target 1 intensity (0.0-1.0)",
434 )
435 auditory_background_intensity: Mapped[Optional[float]] = mapped_column(
436 comment="Auditory background intensity (0.0-1.0)",
437 )
438 auditory_target_0_intensity: Mapped[Optional[float]] = mapped_column(
439 comment="Auditory target 0 intensity (0.0-1.0)",
440 )
441 auditory_target_1_intensity: Mapped[Optional[float]] = mapped_column(
442 comment="Auditory target 1 intensity (0.0-1.0)",
443 )
444 # ... ITI
445 iti_min_s: Mapped[Optional[float]] = mapped_column(
446 comment="Intertrial interval minimum (s)"
447 )
448 iti_max_s: Mapped[Optional[float]] = mapped_column(
449 comment="Intertrial interval maximum (s)"
450 )
452 # Results
453 aborted: Mapped[Optional[int]] = mapped_column(
454 comment="Was the task aborted? (0 no, 1 yes)"
455 )
456 finished: Mapped[Optional[int]] = mapped_column(
457 comment="Was the task finished? (0 no, 1 yes)"
458 )
459 last_trial_completed: Mapped[Optional[int]] = mapped_column(
460 comment="Number of last trial completed",
461 )
463 # Relationships
464 trials = ancillary_relationship( # type: ignore[assignment]
465 parent_class_name="CardinalExpectationDetection",
466 ancillary_class_name="ExpDetTrial",
467 ancillary_fk_to_parent_attr_name="cardinal_expdet_id",
468 ancillary_order_by_attr_name="trial",
469 ) # type: List[ExpDetTrial]
470 groupspecs = ancillary_relationship( # type: ignore[assignment]
471 parent_class_name="CardinalExpectationDetection",
472 ancillary_class_name="ExpDetTrialGroupSpec",
473 ancillary_fk_to_parent_attr_name="cardinal_expdet_id",
474 ancillary_order_by_attr_name="group_num",
475 ) # type: List[ExpDetTrialGroupSpec]
477 @staticmethod
478 def longname(req: "CamcopsRequest") -> str:
479 _ = req.gettext
480 return _("Cardinal RN – Expectation–Detection task")
482 def is_complete(self) -> bool:
483 return bool(self.finished)
485 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
486 return self.standard_task_summary_fields() + [
487 SummaryElement(
488 name="final_score",
489 coltype=Integer(),
490 value=self.get_final_score(),
491 ),
492 SummaryElement(
493 name="overall_p_detect_present",
494 coltype=Float(),
495 value=self.get_overall_p_detect_present(),
496 ),
497 SummaryElement(
498 name="overall_p_detect_absent",
499 coltype=Float(),
500 value=self.get_overall_p_detect_absent(),
501 ),
502 SummaryElement(
503 name="overall_c", coltype=Float(), value=self.get_overall_c()
504 ),
505 SummaryElement(
506 name="overall_d", coltype=Float(), value=self.get_overall_d()
507 ),
508 ]
510 def get_final_score(self) -> Optional[int]:
511 trialarray = self.trials
512 if not trialarray:
513 return None
514 return trialarray[-1].cumulative_points
516 def get_group_html(self) -> str:
517 grouparray = self.groupspecs
518 html = ExpDetTrialGroupSpec.get_html_table_header()
519 for g in grouparray:
520 html += g.get_html_table_row()
521 html += """</table>"""
522 return html
524 @staticmethod
525 def get_c_dprime(
526 h: Optional[float],
527 fa: Optional[float],
528 two_alternative_forced_choice: bool = False,
529 ) -> Tuple[Optional[float], Optional[float]]:
530 if h is None or fa is None:
531 return None, None
532 # In this task, we're only presenting a single alternative.
533 if fa == 0:
534 fa = CONVERT_0_P_TO
535 if fa == 1:
536 fa = CONVERT_1_P_TO
537 if h == 0:
538 h = CONVERT_0_P_TO
539 if h == 1:
540 h = CONVERT_1_P_TO
541 z_fa = scipy.stats.norm.ppf(fa)
542 z_h = scipy.stats.norm.ppf(h)
543 if two_alternative_forced_choice:
544 dprime = (1.0 / numpy.sqrt(2)) * (z_h - z_fa)
545 else:
546 dprime = z_h - z_fa
547 c = -0.5 * (z_h + z_fa)
548 # c is zero when FA rate and miss rate (1 - H) are equal
549 # c is negative when FA > miss
550 # c is positive when miss > FA
551 return c, dprime
553 @staticmethod
554 def get_sdt_values(
555 count_stimulus: Sequence[int], count_nostimulus: Sequence[int]
556 ) -> Dict:
557 # Probabilities and cumulative probabilities
558 sum_count_stimulus = numpy.sum(count_stimulus)
559 sum_count_nostimulus = numpy.sum(count_nostimulus)
560 if sum_count_stimulus == 0 or sum_count_nostimulus == 0:
561 fa = []
562 h = []
563 z_fa = []
564 z_h = []
565 else:
566 p_stimulus = count_stimulus / sum_count_stimulus
567 p_nostimulus = count_nostimulus / sum_count_nostimulus
568 # ... may produce a RuntimeWarning in case of division by zero
569 cump_stimulus = numpy.cumsum(p_stimulus) # hit rates
570 cump_nostimulus = numpy.cumsum(p_nostimulus) # false alarm rates
571 # We're interested in all pairs except the last:
572 fa = cump_stimulus[:-1]
573 h = cump_nostimulus[:-1]
574 # WHICH WAY ROUND YOU ASSIGN THESE DETERMINES THE ROC'S APPEARANCE.
575 # However, it's arbitrary, in the sense that the left/right
576 # assignment of the ratings is arbitrary. To make the ROC look
577 # conventional (top left), assign this way round, so that "fa"
578 # starts low and grows, and "h" starts high and falls. Hmm...
579 fa[fa == 0] = CONVERT_0_P_TO
580 fa[fa == 1] = CONVERT_1_P_TO
581 h[h == 0] = CONVERT_0_P_TO
582 h[h == 1] = CONVERT_1_P_TO
583 z_fa = scipy.stats.norm.ppf(fa)
584 z_h = scipy.stats.norm.ppf(h)
586 # log.debug("p_stimulus: " + str(p_stimulus))
587 # log.debug("p_nostimulus: " + str(p_nostimulus))
588 # log.debug("cump_stimulus: " + str(cump_stimulus))
589 # log.debug("cump_nostimulus: " + str(cump_nostimulus))
591 # log.debug("h: " + str(h))
592 # log.debug("fa: " + str(fa))
593 # log.debug("z_h: " + str(z_h))
594 # log.debug("z_fa: " + str(z_fa))
595 return {"fa": fa, "h": h, "z_fa": z_fa, "z_h": z_h}
597 def plot_roc(
598 self,
599 req: CamcopsRequest,
600 ax: Axes,
601 count_stimulus: Sequence[int],
602 count_nostimulus: Sequence[int],
603 show_x_label: bool,
604 show_y_label: bool,
605 plainroc: bool,
606 subtitle: str,
607 ) -> None:
608 extraspace = 0.05
609 sdtval = self.get_sdt_values(count_stimulus, count_nostimulus)
611 # Calculate d' for all pairs but the last
612 if plainroc:
613 x = sdtval["fa"]
614 y = sdtval["h"]
615 xlabel = "FA"
616 ylabel = "H"
617 ax.set_xlim(0 - extraspace, 1 + extraspace)
618 ax.set_ylim(0 - extraspace, 1 + extraspace)
619 else:
620 x = sdtval["z_fa"]
621 y = sdtval["z_h"]
622 xlabel = "Z(FA)"
623 ylabel = "Z(H)"
624 # Plot
625 ax.plot(
626 x,
627 y,
628 marker=MatplotlibConstants.MARKER_PLUS,
629 color=MatplotlibConstants.COLOUR_BLUE,
630 linestyle=MatplotlibConstants.LINESTYLE_SOLID,
631 )
632 ax.set_xlabel(xlabel if show_x_label else "", fontdict=req.fontdict)
633 ax.set_ylabel(ylabel if show_y_label else "", fontdict=req.fontdict)
634 ax.set_title(subtitle, fontdict=req.fontdict)
635 req.set_figure_font_sizes(ax)
637 @staticmethod
638 def get_roc_info(
639 trialarray: List[ExpDetTrial],
640 blocks: List[int],
641 groups: Optional[List[int]],
642 ) -> Dict:
643 # Collect counts (Macmillan & Creelman p61)
644 total_n = 0
645 count_stimulus = numpy.zeros(NRATINGS)
646 count_nostimulus = numpy.zeros(NRATINGS)
647 rating_missing = False
648 rating_out_of_range = False
649 for t in trialarray:
650 if t.rating is None:
651 rating_missing = True
652 continue
653 if t.rating < 0 or t.rating >= NRATINGS:
654 rating_out_of_range = True
655 break
656 if groups and t.group_num not in groups:
657 continue
658 if blocks and t.block not in blocks:
659 continue
660 total_n += 1
661 if t.target_present:
662 count_stimulus[t.rating] += 1
663 else:
664 count_nostimulus[t.rating] += 1
665 return {
666 "total_n": total_n,
667 "count_stimulus": count_stimulus,
668 "count_nostimulus": count_nostimulus,
669 "rating_missing": rating_missing,
670 "rating_out_of_range": rating_out_of_range,
671 }
673 def get_roc_figure_by_group(
674 self,
675 req: CamcopsRequest,
676 trialarray: List[ExpDetTrial],
677 grouparray: List[ExpDetTrialGroupSpec],
678 plainroc: bool,
679 ) -> str:
680 if not trialarray or not grouparray:
681 return WARNING_INSUFFICIENT_DATA
682 figsize = (
683 PlotDefaults.FULLWIDTH_PLOT_WIDTH * 2,
684 PlotDefaults.FULLWIDTH_PLOT_WIDTH,
685 )
686 html = ""
687 fig = req.create_figure(figsize=figsize)
688 warned = False
689 for groupnum in range(len(grouparray)):
690 ax = fig.add_subplot(2, 4, groupnum + 1)
691 # ... rows, cols, plotnum (in reading order from 1)
692 rocinfo = self.get_roc_info(trialarray, [], [groupnum])
693 if rocinfo["rating_out_of_range"]:
694 return ERROR_RATING_OUT_OF_RANGE
695 if rocinfo["rating_missing"] and not warned:
696 html += WARNING_RATING_MISSING
697 warned = True
698 show_x_label = groupnum > 3
699 show_y_label = groupnum % 4 == 0
700 subtitle = f"Group {groupnum} (n = {rocinfo['total_n']})"
701 self.plot_roc(
702 req,
703 ax,
704 rocinfo["count_stimulus"],
705 rocinfo["count_nostimulus"],
706 show_x_label,
707 show_y_label,
708 plainroc,
709 subtitle,
710 )
711 title = PLAIN_ROC_TITLE if plainroc else Z_ROC_TITLE
712 fontprops = req.fontprops
713 fontprops.set_weight("bold")
714 fig.suptitle(title, fontproperties=fontprops)
715 html += req.get_html_from_pyplot_figure(fig)
716 return html
718 def get_roc_figure_firsthalf_lasthalf(
719 self,
720 req: CamcopsRequest,
721 trialarray: List[ExpDetTrial],
722 plainroc: bool,
723 ) -> str:
724 if not trialarray or not self.num_blocks:
725 return WARNING_INSUFFICIENT_DATA
726 figsize = (
727 PlotDefaults.FULLWIDTH_PLOT_WIDTH,
728 PlotDefaults.FULLWIDTH_PLOT_WIDTH / 2,
729 )
730 html = ""
731 fig = req.create_figure(figsize=figsize)
732 warned = False
733 for half in range(2):
734 ax = fig.add_subplot(1, 2, half + 1)
735 # ... rows, cols, plotnum (in reading order from 1)
736 blocks = list(
737 range(
738 half * self.num_blocks // 2, self.num_blocks // (2 - half)
739 )
740 )
741 rocinfo = self.get_roc_info(trialarray, blocks, None)
742 if rocinfo["rating_out_of_range"]:
743 return ERROR_RATING_OUT_OF_RANGE
744 if rocinfo["rating_missing"] and not warned:
745 html += WARNING_RATING_MISSING
746 warned = True
747 show_x_label = True
748 show_y_label = half == 0
749 subtitle = "First half" if half == 0 else "Second half"
750 self.plot_roc(
751 req,
752 ax,
753 rocinfo["count_stimulus"],
754 rocinfo["count_nostimulus"],
755 show_x_label,
756 show_y_label,
757 plainroc,
758 subtitle,
759 )
760 title = PLAIN_ROC_TITLE if plainroc else Z_ROC_TITLE
761 fontprops = req.fontprops
762 fontprops.set_weight("bold")
763 fig.suptitle(title, fontproperties=fontprops)
764 html += req.get_html_from_pyplot_figure(fig)
765 return html
767 def get_trial_html(self) -> str:
768 trialarray = self.trials
769 html = ExpDetTrial.get_html_table_header()
770 for t in trialarray:
771 html += t.get_html_table_row()
772 html += """</table>"""
773 return html
775 def get_task_html(self, req: CamcopsRequest) -> str:
776 grouparray = self.groupspecs
777 trialarray = self.trials
778 # THIS IS A NON-EDITABLE TASK, so we *ignore* the problem
779 # of matching to no-longer-current records.
780 # (See PhotoSequence.py for a task that does it properly.)
782 # Provide HTML
783 # HTML
784 h = f"""
785 <div class="{CssClass.SUMMARY}">
786 <table class="{CssClass.SUMMARY}">
787 {self.get_is_complete_tr(req)}
788 </table>
789 </div>
790 <div class="{CssClass.EXPLANATION}">
791 Putative assay of propensity to hallucinations.
792 </div>
793 <table class="{CssClass.TASKCONFIG}">
794 <tr>
795 <th width="50%">Configuration variable</th>
796 <th width="50%">Value</th>
797 </tr>
798 """
799 h += tr_qa("Number of blocks", self.num_blocks)
800 h += tr_qa("Stimulus counterbalancing", self.stimulus_counterbalancing)
801 h += tr_qa(
802 "“Detection” response on right?",
803 self.is_detection_response_on_right,
804 )
805 h += tr_qa(
806 "Pause every <i>n</i> trials (0 = no pauses)",
807 self.pause_every_n_trials,
808 )
809 h += tr_qa("Cue duration (s)", self.cue_duration_s)
810 h += tr_qa("Visual cue intensity (0–1)", self.visual_cue_intensity)
811 h += tr_qa("Auditory cue intensity (0–1)", self.auditory_cue_intensity)
812 h += tr_qa("ISI duration (s)", self.isi_duration_s)
813 h += tr_qa("Visual target duration (s)", self.visual_target_duration_s)
814 h += tr_qa(
815 "Visual background intensity", self.visual_background_intensity
816 )
817 h += tr_qa(
818 "Visual target 0 (circle) intensity",
819 self.visual_target_0_intensity,
820 )
821 h += tr_qa(
822 "Visual target 1 (“sun”) intensity", self.visual_target_1_intensity
823 )
824 h += tr_qa(
825 "Auditory background intensity", self.auditory_background_intensity
826 )
827 h += tr_qa(
828 "Auditory target 0 (tone) intensity",
829 self.auditory_target_0_intensity,
830 )
831 h += tr_qa(
832 "Auditory target 1 (“moon”) intensity",
833 self.auditory_target_1_intensity,
834 )
835 h += tr_qa("ITI minimum (s)", self.iti_min_s)
836 h += tr_qa("ITI maximum (s)", self.iti_max_s)
837 h += f"""
838 </table>
839 <table class="{CssClass.TASKDETAIL}">
840 <tr><th width="50%">Measure</th><th width="50%">Value</th></tr>
841 """
842 h += tr_qa("Aborted?", get_yes_no_none(req, self.aborted))
843 h += tr_qa("Finished?", get_yes_no_none(req, self.finished))
844 h += tr_qa("Last trial completed", self.last_trial_completed)
845 h += (
846 """
847 </table>
848 <div>
849 Trial group specifications (one block is a full set of
850 all these trials):
851 </div>
852 """
853 + self.get_group_html()
854 + """
855 <div>
856 Detection probabilities by block and group (c > 0 when
857 miss rate > false alarm rate; c < 0 when false alarm
858 rate > miss rate):
859 </div>
860 """
861 + self.get_html_correct_by_group_and_block(trialarray)
862 + "<div>Detection probabilities by block:</div>"
863 + self.get_html_correct_by_block(trialarray)
864 + "<div>Detection probabilities by group:</div>"
865 + self.get_html_correct_by_group(trialarray)
866 + """
867 <div>
868 Detection probabilities by half and high/low association
869 probability:
870 </div>
871 """
872 + self.get_html_correct_by_half_and_probability(
873 trialarray, grouparray
874 )
875 + """
876 <div>
877 Detection probabilities by block and high/low association
878 probability:
879 </div>
880 """
881 + self.get_html_correct_by_block_and_probability(
882 trialarray, grouparray
883 )
884 + """
885 <div>
886 Receiver operating characteristic (ROC) curves by group:
887 </div>
888 """
889 + self.get_roc_figure_by_group(req, trialarray, grouparray, True)
890 + self.get_roc_figure_by_group(req, trialarray, grouparray, False)
891 + "<div>First-half/last-half ROCs:</div>"
892 + self.get_roc_figure_firsthalf_lasthalf(req, trialarray, True)
893 + "<div>Trial-by-trial results:</div>"
894 + self.get_trial_html()
895 )
896 return h
898 def get_html_correct_by_group_and_block(
899 self, trialarray: List[ExpDetTrial]
900 ) -> str:
901 if not trialarray:
902 return div(italic("No trials"))
903 html = f"""
904 <table class="{CssClass.EXTRADETAIL}">
905 <tr>
906 <th>Block</th>
907 """
908 for g in range(N_CUES):
909 # Have spaces around | to allow web browsers to word-wrap
910 html += f"""
911 <th>Group {g} P(detected | present)</th>
912 <th>Group {g} P(detected | absent)</th>
913 <th>Group {g} c</th>
914 <th>Group {g} d'</th>
915 """
916 html += """
917 </th>
918 </tr>
919 """
920 for b in range(self.num_blocks):
921 html += "<tr>" + td(str(b))
922 for g in range(N_CUES):
923 (
924 p_detected_given_present,
925 p_detected_given_absent,
926 c,
927 dprime,
928 n_trials,
929 ) = self.get_p_detected(trialarray, [b], [g])
930 html += td(a(p_detected_given_present))
931 html += td(a(p_detected_given_absent))
932 html += td(a(c))
933 html += td(a(dprime))
934 html += "</tr>\n"
935 html += """
936 </table>
937 """
938 return html
940 def get_html_correct_by_half_and_probability(
941 self,
942 trialarray: List[ExpDetTrial],
943 grouparray: List[ExpDetTrialGroupSpec],
944 ) -> str:
945 if (not trialarray) or (not grouparray):
946 return div(italic("No trials or no groups"))
947 n_target_highprob = max([x.n_target for x in grouparray])
948 n_target_lowprob = min([x.n_target for x in grouparray])
949 groups_highprob = [
950 x.group_num for x in grouparray if x.n_target == n_target_highprob
951 ]
952 groups_lowprob = [
953 x.group_num for x in grouparray if x.n_target == n_target_lowprob
954 ]
955 html = f"""
956 <div><i>
957 High probability groups (cues):
958 {", ".join([str(x) for x in groups_highprob])}.\n
959 Low probability groups (cues):
960 {", ".join([str(x) for x in groups_lowprob])}.\n
961 </i></div>
962 <table class="{CssClass.EXTRADETAIL}">
963 <tr>
964 <th>Half (0 first, 1 second)</th>
965 <th>Target probability given stimulus (0 low, 1 high)</th>
966 <th>P(detected | present)</th>
967 <th>P(detected | absent)</th>
968 <th>c</th>
969 <th>d'</th>
970 </tr>
971 """
972 for half in (0, 1):
973 for prob in (0, 1):
974 blocks = list(
975 range(
976 half * self.num_blocks // 2,
977 self.num_blocks // (2 - half),
978 )
979 )
980 groups = groups_lowprob if prob == 0 else groups_highprob
981 (
982 p_detected_given_present,
983 p_detected_given_absent,
984 c,
985 dprime,
986 n_trials,
987 ) = self.get_p_detected(trialarray, blocks, groups)
988 html += tr(
989 half,
990 a(prob),
991 a(p_detected_given_present),
992 a(p_detected_given_absent),
993 a(c),
994 a(dprime),
995 )
996 html += """
997 </table>
998 """
999 return html
1001 def get_html_correct_by_block_and_probability(
1002 self,
1003 trialarray: List[ExpDetTrial],
1004 grouparray: List[ExpDetTrialGroupSpec],
1005 ) -> str:
1006 if (not trialarray) or (not grouparray):
1007 return div(italic("No trials or no groups"))
1008 n_target_highprob = max([x.n_target for x in grouparray])
1009 n_target_lowprob = min([x.n_target for x in grouparray])
1010 groups_highprob = [
1011 x.group_num for x in grouparray if x.n_target == n_target_highprob
1012 ]
1013 groups_lowprob = [
1014 x.group_num for x in grouparray if x.n_target == n_target_lowprob
1015 ]
1016 html = f"""
1017 <div><i>
1018 High probability groups (cues):
1019 {", ".join([str(x) for x in groups_highprob])}.\n
1020 Low probability groups (cues):
1021 {", ".join([str(x) for x in groups_lowprob])}.\n
1022 </i></div>
1023 <table class="{CssClass.EXTRADETAIL}">
1024 <tr>
1025 <th>Block (0-based)</th>
1026 <th>Target probability given stimulus (0 low, 1 high)</th>
1027 <th>P(detected | present)</th>
1028 <th>P(detected | absent)</th>
1029 <th>c</th>
1030 <th>d'</th>
1031 </tr>
1032 """
1033 for b in range(self.num_blocks):
1034 for prob in (0, 1):
1035 groups = groups_lowprob if prob == 0 else groups_highprob
1036 (
1037 p_detected_given_present,
1038 p_detected_given_absent,
1039 c,
1040 dprime,
1041 n_trials,
1042 ) = self.get_p_detected(trialarray, [b], groups)
1043 html += tr(
1044 b,
1045 prob,
1046 a(p_detected_given_present),
1047 a(p_detected_given_absent),
1048 a(c),
1049 a(dprime),
1050 )
1051 html += """
1052 </table>
1053 """
1054 return html
1056 def get_html_correct_by_group(self, trialarray: List[ExpDetTrial]) -> str:
1057 if not trialarray:
1058 return div(italic("No trials"))
1059 html = f"""
1060 <table class="{CssClass.EXTRADETAIL}">
1061 <tr>
1062 <th>Group</th>
1063 <th>P(detected | present)</th>
1064 <th>P(detected | absent)</th>
1065 <th>c</th>
1066 <th>d'</th>
1067 </tr>
1068 """
1069 for g in range(N_CUES):
1070 (
1071 p_detected_given_present,
1072 p_detected_given_absent,
1073 c,
1074 dprime,
1075 n_trials,
1076 ) = self.get_p_detected(trialarray, None, [g])
1077 html += tr(
1078 g,
1079 a(p_detected_given_present),
1080 a(p_detected_given_absent),
1081 a(c),
1082 a(dprime),
1083 )
1084 html += """
1085 </table>
1086 """
1087 return html
1089 def get_html_correct_by_block(self, trialarray: List[ExpDetTrial]) -> str:
1090 if not trialarray:
1091 return div(italic("No trials"))
1092 html = f"""
1093 <table class="{CssClass.EXTRADETAIL}">
1094 <tr>
1095 <th>Block</th>
1096 <th>P(detected | present)</th>
1097 <th>P(detected | absent)</th>
1098 <th>c</th>
1099 <th>d'</th>
1100 </tr>
1101 """
1102 for b in range(self.num_blocks):
1103 (
1104 p_detected_given_present,
1105 p_detected_given_absent,
1106 c,
1107 dprime,
1108 n_trials,
1109 ) = self.get_p_detected(trialarray, [b], None)
1110 html += tr(
1111 b,
1112 a(p_detected_given_present),
1113 a(p_detected_given_absent),
1114 a(c),
1115 a(dprime),
1116 )
1117 html += """
1118 </table>
1119 """
1120 return html
1122 def get_p_detected(
1123 self,
1124 trialarray: List[ExpDetTrial],
1125 blocks: Optional[List[int]],
1126 groups: Optional[List[int]],
1127 ) -> Tuple[
1128 Optional[float], Optional[float], Optional[float], Optional[float], int
1129 ]:
1130 n_present = 0
1131 n_absent = 0
1132 n_detected_given_present = 0
1133 n_detected_given_absent = 0
1134 n_trials = 0
1135 for t in trialarray:
1136 if (
1137 not t.responded
1138 or (blocks is not None and t.block not in blocks)
1139 or (groups is not None and t.group_num not in groups)
1140 ):
1141 continue
1142 if t.target_present:
1143 n_present += 1
1144 if t.judged_present():
1145 n_detected_given_present += 1
1146 else:
1147 n_absent += 1
1148 if t.judged_present():
1149 n_detected_given_absent += 1
1150 n_trials += 1
1151 p_detected_given_present = (
1152 (float(n_detected_given_present) / float(n_present))
1153 if n_present > 0
1154 else None
1155 )
1156 p_detected_given_absent = (
1157 (float(n_detected_given_absent) / float(n_absent))
1158 if n_absent > 0
1159 else None
1160 )
1161 (c, dprime) = self.get_c_dprime(
1162 p_detected_given_present, p_detected_given_absent
1163 )
1164 # hits: p_detected_given_present
1165 # false alarms: p_detected_given_absent
1166 return (
1167 p_detected_given_present,
1168 p_detected_given_absent,
1169 c,
1170 dprime,
1171 n_trials,
1172 )
1174 def get_extra_summary_tables(
1175 self, req: CamcopsRequest
1176 ) -> List[ExtraSummaryTable]:
1177 grouparray = self.groupspecs
1178 trialarray = self.trials
1179 trialarray_auditory = [
1180 x for x in trialarray if x.target_modality == AUDITORY
1181 ]
1182 blockprob_values = [] # type: List[Dict[str, Any]]
1183 halfprob_values = [] # type: List[Dict[str, Any]]
1185 if grouparray and trialarray:
1186 n_target_highprob = max([x.n_target for x in grouparray])
1187 n_target_lowprob = min([x.n_target for x in grouparray])
1188 groups_highprob = [
1189 x.group_num
1190 for x in grouparray
1191 if x.n_target == n_target_highprob
1192 ]
1193 groups_lowprob = [
1194 x.group_num
1195 for x in grouparray
1196 if x.n_target == n_target_lowprob
1197 ]
1198 for block in range(self.num_blocks):
1199 for target_probability_low_high in (0, 1):
1200 groups = (
1201 groups_lowprob
1202 if target_probability_low_high == 0
1203 else groups_highprob
1204 )
1205 (
1206 p_detected_given_present,
1207 p_detected_given_absent,
1208 c,
1209 dprime,
1210 n_trials,
1211 ) = self.get_p_detected(trialarray, [block], groups)
1212 (
1213 auditory_p_detected_given_present,
1214 auditory_p_detected_given_absent,
1215 auditory_c,
1216 auditory_dprime,
1217 auditory_n_trials,
1218 ) = self.get_p_detected(
1219 trialarray_auditory, [block], groups
1220 )
1221 blockprob_values.append(
1222 dict(
1223 cardinal_expdet_pk=self._pk, # tablename_pk
1224 n_blocks_overall=self.num_blocks,
1225 block=block,
1226 target_probability_low_high=target_probability_low_high, # noqa
1227 n_trials=n_trials,
1228 p_detect_present=p_detected_given_present,
1229 p_detect_absent=p_detected_given_absent,
1230 c=c,
1231 d=dprime,
1232 auditory_n_trials=auditory_n_trials,
1233 auditory_p_detect_present=auditory_p_detected_given_present, # noqa
1234 auditory_p_detect_absent=auditory_p_detected_given_absent, # noqa
1235 auditory_c=auditory_c,
1236 auditory_d=auditory_dprime,
1237 )
1238 )
1240 # Now another one...
1241 for half in range(2):
1242 blocks = list(
1243 range(
1244 half * self.num_blocks // 2,
1245 self.num_blocks // (2 - half),
1246 )
1247 )
1248 for target_probability_low_high in (0, 1):
1249 groups = (
1250 groups_lowprob
1251 if target_probability_low_high == 0
1252 else groups_highprob
1253 )
1254 (
1255 p_detected_given_present,
1256 p_detected_given_absent,
1257 c,
1258 dprime,
1259 n_trials,
1260 ) = self.get_p_detected(trialarray, blocks, groups)
1261 (
1262 auditory_p_detected_given_present,
1263 auditory_p_detected_given_absent,
1264 auditory_c,
1265 auditory_dprime,
1266 auditory_n_trials,
1267 ) = self.get_p_detected(
1268 trialarray_auditory, blocks, groups
1269 )
1270 halfprob_values.append(
1271 dict(
1272 cardinal_expdet_pk=self._pk, # tablename_pk
1273 half=half,
1274 target_probability_low_high=target_probability_low_high, # noqa
1275 n_trials=n_trials,
1276 p_detect_present=p_detected_given_present,
1277 p_detect_absent=p_detected_given_absent,
1278 c=c,
1279 d=dprime,
1280 auditory_n_trials=auditory_n_trials,
1281 auditory_p_detect_present=auditory_p_detected_given_present, # noqa
1282 auditory_p_detect_absent=auditory_p_detected_given_absent, # noqa
1283 auditory_c=auditory_c,
1284 auditory_d=auditory_dprime,
1285 )
1286 )
1288 blockprob_table = ExtraSummaryTable(
1289 tablename="cardinal_expdet_blockprobs",
1290 xmlname="blockprobs",
1291 columns=self.get_blockprob_columns(),
1292 rows=blockprob_values,
1293 task=self,
1294 )
1295 halfprob_table = ExtraSummaryTable(
1296 tablename="cardinal_expdet_halfprobs",
1297 xmlname="halfprobs",
1298 columns=self.get_halfprob_columns(),
1299 rows=halfprob_values,
1300 task=self,
1301 )
1302 return [blockprob_table, halfprob_table]
1304 @staticmethod
1305 def get_blockprob_columns() -> List[Column]:
1306 # Must be a function, not a constant, because as SQLAlchemy builds the
1307 # tables, it assigns the Table object to each Column. Therefore, a
1308 # constant list works for the first request, but fails on subsequent
1309 # requests with e.g. "sqlalchemy.exc.ArgumentError: Column object 'id'
1310 # already assigned to Table 'cardinal_expdet_blockprobs'"
1311 return [
1312 Column(
1313 "id",
1314 Integer,
1315 primary_key=True,
1316 autoincrement=True,
1317 comment="Arbitrary PK",
1318 ),
1319 Column(
1320 "cardinal_expdet_pk",
1321 Integer,
1322 ForeignKey("cardinal_expdet._pk"),
1323 nullable=False,
1324 comment="FK to the source table's _pk field",
1325 ),
1326 Column(
1327 "n_blocks_overall",
1328 Integer,
1329 comment="Number of blocks (OVERALL)",
1330 ),
1331 Column("block", Integer, comment="Block number"),
1332 Column(
1333 "target_probability_low_high",
1334 Integer,
1335 comment="Target probability given stimulus " "(0 low, 1 high)",
1336 ),
1337 Column(
1338 "n_trials",
1339 Integer,
1340 comment="Number of trials in this condition",
1341 ),
1342 Column("p_detect_present", Float, comment="P(detect | present)"),
1343 Column("p_detect_absent", Float, comment="P(detect | absent)"),
1344 Column(
1345 "c",
1346 Float,
1347 comment="c (bias; c > 0 when miss rate > false alarm rate; "
1348 "c < 0 when false alarm rate > miss rate)",
1349 ),
1350 Column("d", Float, comment="d' (discriminability)"),
1351 Column(
1352 "auditory_n_trials",
1353 Integer,
1354 comment="Number of auditory trials in this condition",
1355 ),
1356 Column(
1357 "auditory_p_detect_present",
1358 Float,
1359 comment="AUDITORY P(detect | present)",
1360 ),
1361 Column(
1362 "auditory_p_detect_absent",
1363 Float,
1364 comment="AUDITORY P(detect | absent)",
1365 ),
1366 Column("auditory_c", Float, comment="AUDITORY c"),
1367 Column("auditory_d", Float, comment="AUDITORY d'"),
1368 ]
1370 @staticmethod
1371 def get_halfprob_columns() -> List[Column]:
1372 return [
1373 Column(
1374 "id",
1375 Integer,
1376 primary_key=True,
1377 autoincrement=True,
1378 comment="Arbitrary PK",
1379 ),
1380 Column(
1381 "cardinal_expdet_pk",
1382 Integer,
1383 ForeignKey("cardinal_expdet._pk"),
1384 nullable=False,
1385 comment="FK to the source table's _pk field",
1386 ),
1387 Column("half", Integer, comment="Half number"),
1388 Column(
1389 "target_probability_low_high",
1390 Integer,
1391 comment="Target probability given stimulus " "(0 low, 1 high)",
1392 ),
1393 Column(
1394 "n_trials",
1395 Integer,
1396 comment="Number of trials in this condition",
1397 ),
1398 Column("p_detect_present", Float, comment="P(detect | present)"),
1399 Column("p_detect_absent", Float, comment="P(detect | absent)"),
1400 Column(
1401 "c",
1402 Float,
1403 comment="c (bias; c > 0 when miss rate > false alarm rate; "
1404 "c < 0 when false alarm rate > miss rate)",
1405 ),
1406 Column("d", Float, comment="d' (discriminability)"),
1407 Column(
1408 "auditory_n_trials",
1409 Integer,
1410 comment="Number of auditory trials in this condition",
1411 ),
1412 Column(
1413 "auditory_p_detect_present",
1414 Float,
1415 comment="AUDITORY P(detect | present)",
1416 ),
1417 Column(
1418 "auditory_p_detect_absent",
1419 Float,
1420 comment="AUDITORY P(detect | absent)",
1421 ),
1422 Column("auditory_c", Float, comment="AUDITORY c"),
1423 Column("auditory_d", Float, comment="AUDITORY d'"),
1424 ]
1426 def get_overall_p_detect_present(self) -> Optional[float]:
1427 trialarray = self.trials
1428 (
1429 p_detected_given_present,
1430 p_detected_given_absent,
1431 c,
1432 dprime,
1433 n_trials,
1434 ) = self.get_p_detected(trialarray, None, None)
1435 return p_detected_given_present
1437 def get_overall_p_detect_absent(self) -> Optional[float]:
1438 trialarray = self.trials
1439 (
1440 p_detected_given_present,
1441 p_detected_given_absent,
1442 c,
1443 dprime,
1444 n_trials,
1445 ) = self.get_p_detected(trialarray, None, None)
1446 return p_detected_given_absent
1448 def get_overall_c(self) -> Optional[float]:
1449 trialarray = self.trials
1450 (
1451 p_detected_given_present,
1452 p_detected_given_absent,
1453 c,
1454 dprime,
1455 n_trials,
1456 ) = self.get_p_detected(trialarray, None, None)
1457 return c
1459 def get_overall_d(self) -> Optional[float]:
1460 trialarray = self.trials
1461 (
1462 p_detected_given_present,
1463 p_detected_given_absent,
1464 c,
1465 dprime,
1466 n_trials,
1467 ) = self.get_p_detected(trialarray, None, None)
1468 return dprime