Working with Temporal Data
Healthcare Applications

Alan Calvitti PhD
Statistician, VA San Diego Healthcare System
acalvitti@ucsd.edu

#WolframTechConf

Aims of Talk

• Mathematica (+Google) central to NoSQL workflow 
• Agile exploratory code on small samples, then scale up
• Functional query style flexible, maintainable, reusable
• Nested Associations more key processing 
• Statistics + Graphics, easy to integrate for communication, publication

2 Projects Represented (Sci + Engr)

Quick study (HHS/Agency Healthcare Research and Quality)

• Aims quantify EHR activity, clinical workflow, cognitive effort (NASA TLX), associations between these, via time-motion study
• Distributed team VASD, UCSD, SLC, Tx A&M, Pitt (R, Python)

• Cross-sectional study 2 sites (VASD, UCSD), 2 EHRs (CPRS, Epic), ~230 visits, primary & specialist physicians
• Data Layers mouse, keystrokes, screen, audio+video, eye-tracking, Epic logs (outside visit), interview transcripts (natural language)

Clinical Event Browser (VHA Human Factors Engr)

• Aims explore temporal visualization patterns; prototype a chronological display of clinical data
• CEB prototype is static, read-only, import data via web/REST 
• Usability interviews physicians suggested interactive features

Quick Study

Data pipeline and scientific workflow

•  Data collection, coding and analysis grouping (figures: J Biomed Inform)

{}

Activity data synchronized to exam room visit timeline

{}

Discarded layers: Eyetrack, Kinect ... (noise, calibration)

Top trace (chromogram): human coded eye-gaze, Bottom Kinect computed head yaw angle

Semi-structured medical data - difficult to group without informatics resources

quick[visitCPRSstuff] = quick[data]["CprsVisitInfo","CprsVisitInfo",All,KeyDrop["CPT CODES"] /*GroupBy[Head]/*Key[String]/* Map[ToLowerCase/*stringSplit["|"]/*Map[stringSplit["("]/*First/*StringTrim]]]

Data Management

Import from Google Drive

•  Source data typically CSV, Gsheets...

Indexing data hierarchies via nested Associations

?keyTrie

quick[index] =Dataset["~/Google Drive/VASD/Projects/Quick/Data"][fileIndex/*AssociationMap[Reverse]][keyTrie]

...superseded by [[experimental]] v11 FileSystemMap

(Key) Serialized File Census pre-import

quick[master] [1;;6]

?keyFlatten

?associationFlatten

quick[index]["VisitData",preLeafMap[KeyDrop["meta"]/* Keys]/* associationFlatten][All,GroupBy[quick[master][All,"Site"]]/* {"UCSD","VASD"},Replace[_Missing-> 0]/*Length]

Typical import & prep methods heterogeneous data

?importTable

?keyPop

?queryAt

dataA: study-wide spreadsheet data

quick[dataA] = quick[index][
queryAt["Master",importTable/*quick[masterPrep]]][
queryAt["Surveys", leafMap[importTable]]][
queryAt["Surveys","Survey"/*quick[prepTLX]]][
queryAt["Codebook", leafMap[importTable/*Map[colorHandler["*color*"]]]]][
queryAt["Demographics", leafMap[importTable]]][
queryAt["CprsVisitInfo", leafMap[importTable]]][
queryAt["Summaries","VASD",Query[ KeyDrop["Lin"],leafMap[importTable]]]][
queryAt["Summaries","SiteCombined",leafMap[importTable]]];//Timing

dataB: individual visit files

quick[dataB] = quick[dataA][
queryAt["VisitData","HumanCodedNonVerbal",preLeafMap[quick[visitDataProtocolImport]/* quick[syncMoraeVisitInterval]]]][
queryAt["VisitData","Vocalization","HumanSegmented",preLeafMap[quick[visitDataProtocolImport]/* quick[syncMoraeVisitInterval]]/*Transpose/*KeySort]][
queryAt["VisitData","EHR","HumanCodedMouseActivity",preLeafMap[quick[visitDataProtocolImport]/* quick[syncMoraeVisitPoint]] ]][queryAt["VisitData","EHR","HumanCodedMouseActivity","VASD","Flat",preLeafMap[Replace[_Missing:> ""]] /* preLeafMap[Query[{"TASK"-> quick[cprsTaskParserDecode]}]] ]];//Timing

Sync to Visit computer activity

• Typical file portion, note time keys, nested associations

quick[data]["VisitData","EHR","HumanCodedMouseActivity","VASD","Flat","Coder1",3,All,{"TASK"-> ifr}][10;;16]//nd

Basic Analysis

Graphics: demographics nested keyGroupBy

?keyGroupBy

quick[data]["Demographics","Patient",quick[visits]/* keyGroupBy[quick[patient2siteAndSpecGroup]]][All,Values/* GroupBy["AgeDecade"],Query[All,Replace[#Gender,{"Male"->Red,"Female"->Blue}]&]/* Sort/* partition[UpTo[5]]/*mrc][KeyMap[ReplaceAll[quick[icons]]/* ifr],KeySort]

Aggregation Merge (vs JoinAcross)

• doctor specialty census

quick[master][keyGroupBy[quick[patient2doc]]/* Keys][GroupBy[quick[doc2siteAndSpec]]][keyGroupBy[First]][KeyMap[quick[icons]], KeyMap[Last]/* KeySort,<|"Tally"-> Length,"PhysID"-> Identity|>]//nmd

Aggregation Visit Length x Patient Status x Billing Codes

{quick[dataCPT],quick[statusColor],quick[master][All,"VisitLength"/* unitConvert["Minutes"]/* qMag]}//mnmi  //Query[keyGroupBy[quick[patient2doc]]/* Map[Values/*Map[cptStatusGrReplacer2]/* graphics[ImageSize->300,PlotRange->{{0,65},{-1,1}},Frame->tfff]/* ifr]] //Query[keyGroupBy[quick[doc2siteAndSpecGroup]]]//nmd // KeyMap[ ReplaceAll[quick[icons]]]   //Row[ {#,{"Established"-> Black,"New"->Green},quick[observedCPTs][Keys/* AssociationMap[cptHTMLColorReplace]]},"   "]&

Graphics+Numerics: layout

quick[master][GroupBy["PhysID"], Keys/* Map[quick[visitInfo2]]/* Map[First]/*{histogram[{0,65,5},PlotRange->{{0,65},{0,6}},ImageSize->100]/*ifr,quantileStats[qt4]/*nnc}][keyGroupBy[Query[{quick[doc2site],quick[doc2specialtyGroup]}]]][KeyMap[ReplaceAll[quick[icons]]]][{1},1;;4]//nmd

(here showing different types of orders entered versus NASA effort survey scores)

Dataset[quick[ordersEffortGr]][{"D05","D18"}][All,fr]//nn // row["  "]

Screen changes “poster”

• Putting it all together - chromograms

{quick[nonverbalChromo],quick[ehrChromo],quick[tlxGr] }//Map[n]//KeyIntersection// Merge[Identity] // Query[keyGroupBy[quick@patient2doc],Replace[{a_,b_,c_}:> Row[{Panel@Column[{a,b},Spacings->-1],c}]]]//Query[All,KeySortBy[quick[tlxNumeric]] /*  KeyMap[quick[visitInfo3B] /* Column /* Panel]] // Query[keyGroupBy[quick@doc2siteAndSpecGroup],fr]

Temporal Analysis

Time interval processing keySubKeyMap

Distribute keys to nested sub-keys (look up in quick[master] metadata)

?keySubKeyMap

quick[syncMoraeVisitInterval] = Map[KeyMap[timeQuantity]]/*  keySubKeyMap[List/*Replace[{v_,t_}:>IntervalIntersection[Interval[t-quick[master][v,"MoraeVisitStartTime"]]//qMag,Interval[{0,quick[master][v,"VisitLength"]//qMag}]]]/* Replace[Interval[{t0_,t1_}]:> Map[quantity["Seconds"],{t0,t1}]]
]/* Map[keyExcept[_Interval]];

quick[data]["VisitData","HumanCodedNonVerbal","Coder1",All,kifr, {"GAZE"-> Replace[n@quick[data]["Codebook","Visit","NonVerbal","Gazes",All,"Description"]]}][{1},1;;6]//nmd

Graphical summary: associationHorizontalRectangles

?associationHorizontalRectangles

quick[nonverbalSumm] = quick[data]["VisitData","HumanCodedNonVerbal","Coder1",All,GroupBy["GAZE"],Keys/* intervalLength];

quick[nonverbalSumm]  [Values/* Merge[Total/* unitConvert["Hours"]/* qMag]][KeyMap[Replace@n@quick[data]["Codebook","Visit","NonVerbal","Gazes",All,"HTMLColor"]]/* associationHorizontalRectangles ]//n//graphics[ImageSize->800,Frame->tfff]

Graphics Legends from metadata (in Google Gsheets)

quick[cprsTab2color] = quick[data]["Codebook","Visit","Cprs","Tabs",Values/*Association,{"Description","HTMLColor"}/* Apply[Rule]]//n

quick[data]["Codebook","Visit","NonVerbal","Gazes",All,"HTMLColor"][keyDeleteMissing/* KeyMap[Replace @n@quick[data]["Codebook","Visit","NonVerbal","Gazes",All,"Description"]]]

• Visit-aggregated bar-charts

quick[nonverbalSumm] [All, KeyMap[Replace @n@quick[data]["Codebook","Visit","NonVerbal","Gazes",All,"HTMLColor"]],unitConvert["Minutes"]/* Round/* qMag][1;;3,Sort/* Reverse/* associationHorizontalRectangles/* graphics[ImageSize->200,Frame->tfff,AspectRatio->1/20,PlotRange->{{0,65},All}]]//n

Graphics Partition/Layout to study groups

quick[nonverbalSummGr ] = quick[nonverbalSumm]  [keyGroupBy[quick[patient2doc]]/* Map[Values/* Query[{Merge[Total/* unitConvert["Minutes"]/* qMag],Length}]/* Apply[Divide]]][SortBy[Total], KeyMap[Replace @n@quick[data]["Codebook","Visit","NonVerbal","Gazes",All,"HTMLColor"]]/* Sort/* Reverse/* associationHorizontalRectangles/* graphics[ImageSize->200,Frame->tfff,AspectRatio->1/20,PlotRange->{{0,40},All}]];

quick[nonverbalSummGr ][keyGroupBy[quick[doc2siteAndSpecGroup]]][KeyMap[ReplaceAll[quick[icons]]],All,ifr]//nmd

associationOuter

Scatterplot matrix

Temporal

• Compute EHR/Tab Timing conditional on doctors’ Gaze-to-Computer

Clickstream are point events, while Gazes are interval events:

1. Take discrete derivative of EHR mouse clickstream to convert to intervals b/w clicks (these include long pauses when doctors are talking w/ patients or physical exam)
2. For each visit compute (Association) Outer product of Interval intersection between (1) and nonverbal gaze events - thus preserving values associated w/ time keys

?keyPush

?keyPop

?associationOuter

quick[gazeEHRintervals] = quick[data]["VisitData","HumanCodedNonVerbal","Coder1",All,GroupBy["GAZE"]/* "K",Keys/* Apply[Interval]];

1. discrete derivative: point event series --> interval event series

quick[EHRdTab]= quick[data]["VisitData","EHR","HumanCodedMouseActivity","VASD","Flat","Coder1",All,keyPush["Time"]/*Values/* blockMap[Query[<|"dt"-> Query[All,"Time"],"tab"-> Query[First,"TASK"]/* "Tab"|>],2,1]/* keyPop["dt"]];

quick[EHRdTab] [1;;3,(1;;4)/* ifr]

2. merge visit files using outer product // associationOuter - Keys are intersected, Values are trivially Listed

quick[cprsTabSummGr]= quick[cprsTabGazeTimingSumm][keyGroupBy[quick[patient2doc]]/* Map[Values/* Query[{Merge[Total/* unitConvert["Minutes"]],Length}]/* Apply[Divide]/* Round /* qMag]/* Map[KeyMap[quick[cprsTab2color]]/* Sort/* Reverse/* associationHorizontalRectangles/* graphics[ImageSize->200,PlotRange->{{0,40},All},Frame-> False]],"Gaze/EHR" ];

legends

Alternative

quick[JBIfigure3B] = quick[comboGr][KeySortBy[quick[gazePhysicianMedian][All,KeyDrop["VisitLength"]/* Total]]][ keyGroupBy[quick@doc2siteAndSpecGroup]]//n // Map[n/* Column/*fr] // KeySort/* KeyMap[stringRiffle[" "]]/* KeyValueMap[List/* column[Baseline]] //Append[ quick[gazeEHRlegend3]]//row[" ",ImageSize->10000]

Clinical Event Browser

Multi-scale patient trajectory (NEJM 2014)

Difficult - requires layout engine

[Wils2014] Wilson et al. “Actionable diagnosis of neuroleptospirosis by next generation sequencing” NEJM  Jun 4 2014.

Import["~/Google Drive/VASD/AghaLab/Members/Alan/2016-HFE-InfoViz/Content/nejmoa_2014_Figure1.png"]// Show[#,ImageSize->500]&

Clinical Event Browser prototype

• Typical import & prep routine

ceb[data] = ceb[data0][
queryAt["Icons",leafMap[Image[#,ImageSize->{30,30}]&]]][queryAt["Patient Banner",keyPop["Element"]/* Map[Values/* First]/* queryAt["Photo", Replace[_:> Image[ceb[danPhoto],ImageSize->80]]]]][
queryAt["Vital Signs",preLeafMap[Query[{"Date"-> dateObjectHandlerDMY,"Time"-> ToString/* timeObjectHandlerHHMM}]]/* GroupBy[Query[{"Date","Time"}]/* Values]/* KeyMap[Apply[Append]]]][
queryAt["Medications",preLeafMap[Query@{"Expiration"-> dateObjectHandlerDMY,"Last Filled"-> dateObjectHandlerDMY}]/* GroupBy["Last Filled"]]][
queryAt["Active Problem List",preLeafMap[Query@{"Date"-> dateObjectHandlerDMY,"Date due"-> dateObjectHandlerDMYNow }]/* GroupBy["Date due"]]][
queryAt["Appointments",preLeafMap[Query@{"Date"-> dateObjectHandlerDMY,"Time"-> ToString/* timeObjectHandlerHHMM}]/* GroupBy[Query[{"Date","Time"}]/* Values]/* KeyMap[Apply[Append]]]][
queryAt["Labs",preLeafMap[DeleteMissing]/* DeleteCases[<||>]/* GroupBy["LabTestIndex"]]][
queryAt["Labs",map[Query[{First/* only[{"Date"-> stringSplit[" "]/* Replace[{{d_,t_}:> Append[ dateObjectHandlerDMY[d], timeObjectHandlerHHMM[t]],{d_}:>  dateObjectHandlerDMY[d]}],"SPECIMEN"-> Identity}]/* Values,Query[All,only@{"Test"-> Identity,"Result/Status"-> ToExpression,"Flag"-> Identity,"Reference Range"-> stringSplit["-"]/* ToExpression}]}],-3]]][
queryAt["Labs",Values/* GroupBy[First]/* keyGroupBy[First]/* Query[All,Values,Query[Association,Apply[Rule]]/* KeyMap[Last]]]][
queryAt["Notes",KeyMap[DateObject]/* preLeafMap[KeyMap[ceb[data]["Icons"]]/* Map[stringSplit["///"]/* Map[StringTrim]]]]][
queryAt["Allergies",preLeafMap[Query@{"Date"->dateObjectHandlerDMY}]/* GroupBy["Date"]]
];

ceb[cron][All,KeyValueMap[List/* Replace[{
{"Active Problem List",detail_}:> ceb[data]["Icons"]["Reminders"]->  Query[ceb[activeProblemList] ][detail],
{"Vital Signs",detail_}:> ceb[data]["Icons"]["Vital Signs"]->  Query[First,ceb[formatVitalsRow]][detail],
{"Appointments",detail_}:> ceb[data]["Icons"]["Appointments"]->  Query[First,"description"/* style[Blue,Bold]][detail],
{"Medications",detail_}:> ceb[formatMedication][detail],
{"Labs",detail_}:> (detail// Map[Keys]//Flatten//Map[ceb[data]["Icons","LabIcons"]]//Row),
{event_,detail_}:> Table[ceb[data]["Icons"][event],{detail}]}]]/*Flatten/* Column][yearMonthAlign] // Column[{ceb[banner],#},Alignment->Center]&

• Zoom in on labs only // dev interactive version  w/ user controls to filter

ceb[data]["Labs"][All,Merge[Apply@Join]/* Query[KeyMap[stringSplit[" "]/* Replace[{sp_,rest___}:> {ceb[data]["Icons","LabIcons"][sp],rest}]/* Column]]/* Query[All,column[Alignment-> Right]/* Panel,normalizeNumericalLab]/* n/*Row/* fr][KeySortBy[First]/* Reverse][KeyMap@dateTimeFormat2]//nnc

Desired WL features for data

• Key versions of functions for Values

KeyGroupBy; KeyMerge; KeyMapIndexed; KeyBlockMap ...

• RowAssociation factor out keys from each row to a header

RowAssociation[{"t0","t1"}->{"x","y","z"}][{{0,1}->{x1,y1,z1},{2,5}-> {x2,y2,z2},...}]

• KeyIntervalIntersection

KeyIntervalIntersection

• Open and semi-open Intervals, IntervalComplement

Interval[{1,Except[2]}]   (*  [1,2)  *);   intervalComplement

• Polynomial quantity representation vs explicit Quantity

2*Unit["Hours"] + 30*Unit["Minutes"]

http://mathematica.stackexchange.com/questions/114865/is-quantity-necessary

• Remote Touchpad controller to browse deep Dataset (open/close)

Conclusion

• Mathematica + Google Drive + Apps = NoSQL workflow 
• Agile exploratory code on small samples, then scale up
	+ Clinical Event Browser 6-month prototype + usability study 
• Functional query pipelines + Operator forms
• Nested Associations more key processing, timestamp keys
• Statistics + Graphics, easy to integrate for communication, publication