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