-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsearch.json
More file actions
1178 lines (1178 loc) · 383 KB
/
search.json
File metadata and controls
1178 lines (1178 loc) · 383 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
[
{
"objectID": "index.html",
"href": "index.html",
"title": "Welcome to DevStart",
"section": "",
"text": "DevStart is an online hands-on manual for anyone who is approaching developmental psychology and developmental cognitive neuroscience for the first time, from master’s students and PhDs to postdocs. \nAssuming no a priori knowledge, this website will guide you through your first steps as a developmental researcher. You’ll find many examples and guidelines on how to grasp the basic principles of developmental psychology research, design and set up a study with different research methods, analyse data, and start programming.\n\n\nDevStart is built on three guiding principles that reflect our approach to developmental research.\nWe’re committed to open source tools and aim to only use software that is freely available to everyone. Academia is already difficult enough without financial barriers - we want to reduce the impact of money on research and ensure that good science isn’t limited by budget constraints.\nOur resources are completely free to use because we believe knowledge should be accessible to all researchers regardless of their funding situation.\nMost importantly, we see ourselves as a knowledge hub rather than the authority on “best practices.” We don’t have all the answers, just hard-earned solutions from our own research journeys. What worked for us might need tweaking for you, and we welcome those contributions. After all, developmental research is as much about collaboration as it is about discovery.\n\n\n\nThere are many resources on the web to learn how to program, build experiments, and analyse data. However, they often assume basic skills in programming, and they don’t tackle the challenges that we encounter when testing babies and young children. This knowledge is often handed down from Professors and Postdocs to PhD students, or it is acquired through hours and hours of despair. \nWe tried to summarize all the struggles that we encountered during our PhDs and the solutions we came up with. With this website, we aim to offer you a solution to overcome these problems, and we invite anyone to help us and contribute to this open science framework!\n\n\n\nIf you’re an expert on a specific topic and you want to change something or add something, if you don’t agree with our pipeline, or you spot an error in our code, please let us know!\nThere are different ways you can contribute:\n\nGitHub: You can fork the repository, make your changes, and submit a pull request. This is the best way to contribute code or documentation updates.\nForum Discussions: We are starting a dedicated forum where you can ask questions, share ideas, and discuss topics related to developmental research. This is a great place to connect with other researchers and get feedback on your work. Please join the forum and start a discussion if you have any questions or suggestions. We will be happy to help you!\nEmail: If you prefer, you can email us with your suggestions or corrections. We welcome any feedback that can help improve DevStart. You can find us at: T.ghilardi@bbk.ac.uk , Francesco.Poli@mrc-cbu.cam.ac.uk, G.serino@bbk.ac.uk\n\n\n\n\n\nOur story began in 2018 at the Donders Institute in the Netherlands. Tommaso and Francesco were in the midst of their PhDs when Giulia joined them, seeking data for her master’s thesis. What started as a research collaboration quickly grew into a lasting partnership. We share a passion for developmental science and each bring our own approach to the table.\nTommaso is our programmer in love with R, Python and anything methodological. Francesco is our deep thinker, focused on theory development and creating computational models to explain behavioral patterns. Giulia brings creativity to our experiments, blending scientific rigor with visual design. Over the years, we’ve continued collaborating across institutions, finding that our different strengths complement each other.\nDevStart emerged from our research journeys - the challenges we faced and the solutions we discovered along the way\n\n \n \n \n Tommaso Ghilardi\n Postdoctoral researcherCentre for Brain and Cognitive Development\n \n \n \n \n \n \n Giulia Serino\n Postdoctoral researcherCentre for Brain and Cognitive Development\n \n \n \n \n \n \n Francesco Poli\n Postdoctoral researcherCambridge University\n \n \n\n\n\n\nWe tried our best to offer the best and most accurate solutions. However, do always check the code and if the outputs make sense. Please get in touch if you spot any errors.\nWe apologize in advance for our poor coding skills. Our scripts are not perfect, and they don’t mean to be. But, as Francesco always says, they work! And we hope they will support you during your PhD.",
"crumbs": [
"Welcome to DevStart"
]
},
{
"objectID": "index.html#core-principles",
"href": "index.html#core-principles",
"title": "Welcome to DevStart",
"section": "",
"text": "DevStart is built on three guiding principles that reflect our approach to developmental research.\nWe’re committed to open source tools and aim to only use software that is freely available to everyone. Academia is already difficult enough without financial barriers - we want to reduce the impact of money on research and ensure that good science isn’t limited by budget constraints.\nOur resources are completely free to use because we believe knowledge should be accessible to all researchers regardless of their funding situation.\nMost importantly, we see ourselves as a knowledge hub rather than the authority on “best practices.” We don’t have all the answers, just hard-earned solutions from our own research journeys. What worked for us might need tweaking for you, and we welcome those contributions. After all, developmental research is as much about collaboration as it is about discovery.",
"crumbs": [
"Welcome to DevStart"
]
},
{
"objectID": "index.html#why-did-we-create-it",
"href": "index.html#why-did-we-create-it",
"title": "Welcome to DevStart",
"section": "",
"text": "There are many resources on the web to learn how to program, build experiments, and analyse data. However, they often assume basic skills in programming, and they don’t tackle the challenges that we encounter when testing babies and young children. This knowledge is often handed down from Professors and Postdocs to PhD students, or it is acquired through hours and hours of despair. \nWe tried to summarize all the struggles that we encountered during our PhDs and the solutions we came up with. With this website, we aim to offer you a solution to overcome these problems, and we invite anyone to help us and contribute to this open science framework!",
"crumbs": [
"Welcome to DevStart"
]
},
{
"objectID": "index.html#how-to-contribute",
"href": "index.html#how-to-contribute",
"title": "Welcome to DevStart",
"section": "",
"text": "If you’re an expert on a specific topic and you want to change something or add something, if you don’t agree with our pipeline, or you spot an error in our code, please let us know!\nThere are different ways you can contribute:\n\nGitHub: You can fork the repository, make your changes, and submit a pull request. This is the best way to contribute code or documentation updates.\nForum Discussions: We are starting a dedicated forum where you can ask questions, share ideas, and discuss topics related to developmental research. This is a great place to connect with other researchers and get feedback on your work. Please join the forum and start a discussion if you have any questions or suggestions. We will be happy to help you!\nEmail: If you prefer, you can email us with your suggestions or corrections. We welcome any feedback that can help improve DevStart. You can find us at: T.ghilardi@bbk.ac.uk , Francesco.Poli@mrc-cbu.cam.ac.uk, G.serino@bbk.ac.uk",
"crumbs": [
"Welcome to DevStart"
]
},
{
"objectID": "index.html#who-are-we",
"href": "index.html#who-are-we",
"title": "Welcome to DevStart",
"section": "",
"text": "Our story began in 2018 at the Donders Institute in the Netherlands. Tommaso and Francesco were in the midst of their PhDs when Giulia joined them, seeking data for her master’s thesis. What started as a research collaboration quickly grew into a lasting partnership. We share a passion for developmental science and each bring our own approach to the table.\nTommaso is our programmer in love with R, Python and anything methodological. Francesco is our deep thinker, focused on theory development and creating computational models to explain behavioral patterns. Giulia brings creativity to our experiments, blending scientific rigor with visual design. Over the years, we’ve continued collaborating across institutions, finding that our different strengths complement each other.\nDevStart emerged from our research journeys - the challenges we faced and the solutions we discovered along the way\n\n \n \n \n Tommaso Ghilardi\n Postdoctoral researcherCentre for Brain and Cognitive Development\n \n \n \n \n \n \n Giulia Serino\n Postdoctoral researcherCentre for Brain and Cognitive Development\n \n \n \n \n \n \n Francesco Poli\n Postdoctoral researcherCambridge University",
"crumbs": [
"Welcome to DevStart"
]
},
{
"objectID": "index.html#warnings",
"href": "index.html#warnings",
"title": "Welcome to DevStart",
"section": "",
"text": "We tried our best to offer the best and most accurate solutions. However, do always check the code and if the outputs make sense. Please get in touch if you spot any errors.\nWe apologize in advance for our poor coding skills. Our scripts are not perfect, and they don’t mean to be. But, as Francesco always says, they work! And we hope they will support you during your PhD.",
"crumbs": [
"Welcome to DevStart"
]
},
{
"objectID": "CONTENT/Workshops/BCCCD2024.html",
"href": "CONTENT/Workshops/BCCCD2024.html",
"title": "BCCCD2024",
"section": "",
"text": "Hello hello!!! This page has been created to provide support and resources for the tutorial and we presented at BCCCD24."
},
{
"objectID": "CONTENT/Workshops/BCCCD2024.html#software",
"href": "CONTENT/Workshops/BCCCD2024.html#software",
"title": "BCCCD2024",
"section": "Software",
"text": "Software\nIn this tutorial, our primary tool will be Python!! We recommend installing it via Miniconda. Our interaction with Python will primarily be through the Spyder IDE. You’re free to use any IDE of your choice, but if you’d like to follow along more smoothly, we suggest checking out our guide on how to install both Miniconda and Spyder: Getting started with Python.\nBesides Python, we’ll also be utilizing Psychopy! Psychopy is an awesome set of packages and functions designed for conducting psychological experiments. It’s available as a standalone software or can be installed as a Python package.\nBased on our experience, we find it more advantageous to use Psychopy as a Python package due to its increased flexibility. We provide this anaconda environment to easily create a virtual environment with both python, psychopy and all the libraries that we will need. You can find details on how to install a conda environment on this page: Getting started with Psychopy"
},
{
"objectID": "CONTENT/Workshops/BCCCD2024.html#files",
"href": "CONTENT/Workshops/BCCCD2024.html#files",
"title": "BCCCD2024",
"section": "Files",
"text": "Files\nIn our workshop we will create a cool eye-tracking study, collect the data and analyze them. To make everything smoother we provide HERE the stimuli and the data that we will use for the workshop."
},
{
"objectID": "CONTENT/Stats/LinearModels.html",
"href": "CONTENT/Stats/LinearModels.html",
"title": "Linear Models",
"section": "",
"text": "Welcome to the first tutorial on data analysis!!!\nIn previous tutorials, we formulated our research hypotheses, designed an experimental task, collected data, and pre-processed it. Here, we’ll be working with a dataset containing (simulated) data from the same task.\nIn this tutorial, we will use a linear model to test one of our initial hypotheses, so we suggest to go check them out here if you haven’t done so already. We want to test whether infants look longer at a Complex stimulus compared to a Simple stimulus. Given that infants see repeated presentations of the same stimuli, it might also be worth checking whether this effect changes over time! It might either get stronger or disappear…\nWe’re going to begin with a very basic linear model, and in the upcoming tutorials, we’ll gradually increase both the complexity and accuracy of our approach.\nSo, if you notice something that doesn’t seem quite perfect at this stage, don’t worry! It’s all part of the plan. Our goal is to guide you step-by-step towards building the best model. Just remember, this process takes time!",
"crumbs": [
"Stats",
"Linear Models"
]
},
{
"objectID": "CONTENT/Stats/LinearModels.html#import-data",
"href": "CONTENT/Stats/LinearModels.html#import-data",
"title": "Linear Models",
"section": "Import data",
"text": "Import data\nYou can download the data that we will use in this tutorial from here:\n Dataset.csv \nOnce downloaded we need to import it in our R session. Here we read our csv and we print a small preview of it.\n\ndf = read.csv(\"resources/Stats/Dataset.csv\")\nhead(df)\n\n Id Event TrialN SaccadicRT LookingTime SES\n1 1 Simple 2 354.4634 1143.3041 high\n2 1 Simple 4 287.6544 1053.8191 high\n3 1 Simple 5 298.2421 1003.3585 high\n4 1 Simple 8 NA 853.2175 high\n5 1 Simple 9 NA 773.4392 high\n6 1 Simple 12 234.9623 742.8035 high\n\n\nYou can see that the data is really simple! We have 6 columns:\n\nId column that specifies the participant ID. You can see that each participant has multiple rows: that’s because they performed multiple trials of the same task.\nEvent specifies each trial’s condition. Trials either belong to the Simple or the Complex condition.\nTrialN indicates the sequential number of trials in the experiment.\nReactionTime how quickly participants reacted to the stimulus in each trial. We will ignore this variable for now.\nLookingTime is the variable of interest for today, the one that we want to model. It reports how long participants look at the stimulus on each trial.\nSES represents socioeconomic status collected via questionnaire (high, medium, or low) for each participant - we’ll also ignore this for now.\n\n\n\n\n\n\n\nLong format\n\n\n\nOne important piece of information that we need to keep in mind is that to run lm() (and most models!!) in R we need the data in a long format and not a wide format.\nIn long format, each row represents a single observation. Variables are organized in columns, with one column for the variable names and another for the values. This means that the column you want to model (in the example LookingTime) has 1 row for observation but the other columns usually have repeated entries ( e.g. Id , TrialN, Event).\nIn wide format, on the other hand, each row represents a subject or group, with multiple columns for different variables or time-points. While this can be visually appealing for humans, it’s not optimal for our linear modeling needs.\nHere a small comparison of the two formats:\n\n\nLong Format\n\n\n# A tibble: 8 × 4\n Id Event TrialN LookingTime\n <chr> <chr> <int> <dbl>\n1 1 Complex 1 1450\n2 1 Complex 2 1420\n3 1 Complex 3 1390\n4 1 Simple 1 1380\n5 1 Simple 2 1350\n6 1 Simple 3 1320\n7 2 Complex 1 1480\n8 2 Complex 2 1460\n\n\n\n\nWide Format\n\n\n# A tibble: 3 × 7\n Id Complex_Trial1 Complex_Trial2 Complex_Trial3 Simple_Trial1 Simple_Trial2\n <chr> <dbl> <dbl> <dbl> <dbl> <dbl>\n1 1 1450 1420 1390 1380 1350\n2 2 1480 1460 1410 1390 1370\n3 3 1520 1490 1450 1410 1380\n# ℹ 1 more variable: Simple_Trial3 <dbl>\n\n\n\n\n\nIf your data is currently in wide format, don’t worry! R provides tools like the tidyr package with functions such as pivot_longer() to easily convert your data from wide to long format.",
"crumbs": [
"Stats",
"Linear Models"
]
},
{
"objectID": "CONTENT/Stats/LinearModels.html#formula",
"href": "CONTENT/Stats/LinearModels.html#formula",
"title": "Linear Models",
"section": "Formula",
"text": "Formula\nTo run models in R we usually use formulas! Sounds complex doesn’t it?!? Well it is not, let me guide you through it.\nIn R, model formulas follow a specific structure. On the left side of the formula, we place our dependent variable - the outcome we collected and we’re interested in studying. In this case, it’s LookingTime. Next, we use the tilde symbol ~. This tilde tells R that we want to predict the variable on the left using the variables on the right side of the formula. On the right side, we list the independent variables (predictors) we believe may influence our dependent variable. Do you remember what our hypothesis was!?? What shall we put here?\nWell, first off, we said that LookingTime might differ across conditions (Simple vs Complex, contained in Event):\nwe can use the formula:\n\n\nLookingTime ~ Event. This basic structure allows us to examine a single predictor.\n\nWe also said that looking time might change over trials. To test whether also TrialN predicts LookingTime, can extend this model by adding another variable:\n\n\nLookingTime ~ Event + TrialN. This formulation tells the model to assess whether either TrialN and Event predicts LookingTime, treating them as independent predictors.\n\n\n\n\n\n\n\nNote\n\n\n\nAdding TrialN also means we are controlling for it while estimate the effect of Event. In other words, the Simple–Complex difference is not allowed to “pick up” changes that are really just due to being earlier vs later in the experiment. This is what people mean when they say the model controls for a certain variable.\n\n\nWe can also examine the interaction between these two variables in predicting LookingTime. This means that the effect of TrialN on LookingTime can be different for Simple vs Complex. So instead of assuming the two conditions follow the same trend over time, the interaction tests whether their trends are different. We use the following syntax:\n\n\nLookingTime ~ TrialN : Event. This instructs the model to evaluate whether the interaction between the two variables predicts LookingTime.\n\nIt’s important to note that using : only tests the interaction, not the individual effects of each variable. To include both main effects and their interaction, we can use the formula:\n\n\nLookingTime ~ TrialN + Event + TrialN:Event.\n\nR offers a shorthand for this complete model using the * operator. The formula:\n\n\nLookingTime ~ TrialN * Event is equivalent to the longer version above, testing both main effects and the interaction in a more concise format.\n\nThese formulas are for simple linear models. Different types of models add small and different pieces to this basic structure. We will see in the next tutorial how to handle these. Now that we have seen how to make a proper formula let’s use it in our model!!",
"crumbs": [
"Stats",
"Linear Models"
]
},
{
"objectID": "CONTENT/Stats/LinearModels.html#run-the-model",
"href": "CONTENT/Stats/LinearModels.html#run-the-model",
"title": "Linear Models",
"section": "Run the model",
"text": "Run the model\nRunning a linear model is extremely simple. We will use the function lm() and we will pass our dataframe df and the formula we just made together!!\nAfter fitting the model we extract the summary of it. This is how we will get all the information we need.\n\nmod_lm = lm(LookingTime ~ TrialN*Event, data = df)\nsummary(mod_lm)\n\n\nCall:\nlm(formula = LookingTime ~ TrialN * Event, data = df)\n\nResiduals:\n Min 1Q Median 3Q Max \n-609.15 -212.47 13.17 218.58 676.70 \n\nCoefficients:\n Estimate Std. Error t value Pr(>|t|) \n(Intercept) 1549.469 39.763 38.967 < 2e-16 ***\nTrialN -12.384 3.362 -3.683 0.000264 ***\nEventSimple -175.450 56.846 -3.086 0.002176 ** \nTrialN:EventSimple -8.661 4.725 -1.833 0.067605 . \n---\nSignif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1\n\nResidual standard error: 268.1 on 376 degrees of freedom\n (20 observations deleted due to missingness)\nMultiple R-squared: 0.2892, Adjusted R-squared: 0.2835 \nF-statistic: 50.99 on 3 and 376 DF, p-value: < 2.2e-16\n\n\nThat’s it!!! 🎉\nNow we can use the output of the model to understand whether the variables are predicting Looking Time. The magic number we’re looking for is the p-value, hiding in the last column of the Coefficients section. If the p-value is below 0.05, we’ve got ourselves an effect! If it’s above, sadly we don’t. AND YES, EVEN IF IT’S 0.051!!! Rules are rules in the p-value game!\nWhat can we spot here? First, our Intercept is significant (don’t worry, we’ll decode this mysterious value later!). Even more exciting, we’ve got a significant effect of TrialN (p = 0.0003) and of Event (p = 0.00218). However the interaction between TrialN and Event (p = 0.068) does not seem to be significant.\nThis is already pretty cool, right?!? But here’s the deal - when looking at model outputs, people often get hypnotized by p-values. However, there’s MUCH more to unpack in a model summary! The full story of our data is waiting to be discovered in the complete model output. Let’s dive in together!",
"crumbs": [
"Stats",
"Linear Models"
]
},
{
"objectID": "CONTENT/Stats/LinearModels.html#estimates",
"href": "CONTENT/Stats/LinearModels.html#estimates",
"title": "Linear Models",
"section": "Estimates",
"text": "Estimates\nThe Estimate section is probably one of the most important parts of our model summary. While the other columns (t-values and p-values) are just numbers that tell us whether the predictor fits, the estimates tell us HOW they fit!!\nLet’s go together through the most challenging information:\n(Intercept)\nThe intercept often confuses people who approach linear models for the first time. What exactly is it? 🤔\nThe (Intercept) represents the reference levels where all our predictors (TrialN and Event) are 0. While TrialN is easy to understand when it’s 0 (TrialN 0 in our case would be the first trial), you may be scratching your head thinking…how can Event be 0? It’s a categorical variable, it can’t be 0!!!! TrialN == 0…sure….but Event??\nYou are absolutely right! When a model encounters a categorical variable, it cleverly selects the first level of such variable as the reference level. If you take another look at our model summary, you can see that there’s information for the Simple Stimulus level of the Event variable but nothing about the Complex Stimulus level. This is because the Complex Stimulus level has been selected by the model as the reference level and is thus represented in the intercept value! 💡\nSo our intercept (1390.1) actually represents the predicted Looking Time when:\n\nWe’re on the first trial (TrialN = 0)\nWe’re in the Simple Stimulus condition\n\nThe Standard Error of the estimate (29.1) tells us the precision of the estimate.\n\n\n\n\n\n\nTip\n\n\n\nSince the intercept has a significant p-value, it means that the estimate for the Simple condition at trial 0 is actually significantly different from 0. In other words, our participants are definitely looking at something during the Simple condition in the first trial: their LookingTime isn’t zero! This might seem obvious (of course they’re looking!), but statistically confirming this baseline is actually meaningful.\n\n\nOk, all this explanation is great…but it’s much easier to visualize these Estimates!\n\n\n\n\n\n\n\n\nAs you can see, the intercept is pretty straightforward: it gives us the estimate when everything is set to 0, both for continuous and categorical variables. The intercept is the foundation of your model, where all the predictors are at their baseline value (in this case, Simple was cleverly selected as the reference or 0 level for the categorical variable).\nEvent\nAwesome! Now that we’ve got the intercept down, let’s take a look at the rest of the model output. We’ll skip over the TrialN variable for now and focus on what’s happening with the Event.\nAt first, the results for Event [Complex] might look like they’re giving us the value of looking time for the Complex Stimulus. Super easy, right?!?\nWell… not exactly! 🤔\nIn linear models, each coefficient shows the difference in relation to the intercept (the 0 or the reference level), not the exact value of the Complex condition.\nIt sounds a bit confusing, but let’s break it down. If we want to understand what is the estimate for a Complex event we need to take the Intercept (1549.47) –as we mentioned that is actually the Complex event – and then just simply add to it the Estimate for the Event [Simple] (-175.45). So the model is telling us that a Complex event should be 1549.47- -175.45= -350.90.\nSee? Not too bad! Let’s visualize it and make it even clearer!\n\n\n\n\n\n\n\n\nTrialN\nSo, interpreting the coefficients for categorical variables wasn’t too tricky, right? But what about continuous variables like TrialN?\nNo worries, it’s actually pretty straightforward! The coefficient for a continuous variable represents the slope of the line for that variable.\nIn simpler terms, it shows how much the outcome (in this case, LookingTime) changes for each unit increase in the continuous variable (TrialN). So, in our case the coefficient for TrialN is -7.5, this means that for each unit increase in TrialN, the LookingTime is expected to decrease by 7.5 units (assuming all other variables stay the same).\n\n\n\n\n\n\nImportant\n\n\n\nRemember!! This coefficient represents the effect of TrialN specifically when Event is at its reference level (Simple). In other words, this -7.5 decrease in LookingTime per trial applies specifically to the Simple condition!\n\n\nEven easier..let’s plot again!\n\n\n\n\n\n\n\n\nInteraction\nAlright, now we’re getting to the final step! Let’s talk about the interaction between TrialN and Event! Now, we’re not just dealing with a single factor or continuous variable, but looking at how they interact with each other. Don’t worry - if you understood the previous steps, this will be a breeze!\nWe’ll take it step by step and look at the interaction in our model parameters. The interaction term between TrialN and Event [Complex] tells us how the relationship between TrialN and LookingTime changes when we switch from the reference Event (Simple) to the Complex condition.\nTo put it simply:\n\nFor Complex events, LookingTime decreases by -12.38 units per trial (that’s our main TrialN coefficient)\nThe interaction coefficient (-8.66) tells us how this slope changes for Simple events\n\nSo for Simple events, the slope would be: -12.38+ -8.66 = -21.05 units per trial.\nWhat does this mean in real terms? If the interaction coefficient is positive (like in our example), it means participants’ looking time decreases more slowly during Complex trials compared to Simple trials – the estimate is slightly less negative.\n\n\n\n\n\n\nImportant\n\n\n\nIn this model the interaction effect is extremely small and not significant. This means that there is actually no difference in how the TrialN predicted Looking time in either Simple or Complex! Looking time decreases at basically the same rate regardless of which event type we’re looking at!!!\n\n\nWhile there is no significant difference, let’s plot the estimated effects for TrialN for both Complex and Simple conditions to visualize this relationship!\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nImportant\n\n\n\nDo you notice any difference in how the slopes between the two conditions? Probably not! If you examine the model summary, you’ll see the interaction effect is very small and not statistically significant.\nThis tells us that TrialN predicts Looking time similarly in both Simple and Complex conditions. In other words, looking time decreases at essentially the same rate regardless of which event type participants observed.\n\n\nI hope that by now you got a decent understanding on how to interpret what a linear model is telling you!! Different kinds of models will add small pieces of information here and there, but the main information will still be there. Thus, if you got here, you are a step closer to becoming a stats genius!!! 🧠📊\n\n\n\n\n\n\nImportant\n\n\n\nDon’t you dare leave the tutorial just yet! We just threw you into the pool so you could start swimming, but we skipped a couple of important steps.Two of them are below (centering predictors and checking the model’s assumptions) and more will follow in the next tutorials (accounting for the data structure with random effects, estimating contrasts, and more)",
"crumbs": [
"Stats",
"Linear Models"
]
},
{
"objectID": "CONTENT/Stats/LinearModels.html#run-the-model-1",
"href": "CONTENT/Stats/LinearModels.html#run-the-model-1",
"title": "Linear Models",
"section": "Run the model",
"text": "Run the model\nOk, this sounds fair right? Before running a model we can just standardize the values of our predictors (in our case TrialN). Yeah..but how to? Our favorite way is to use the standardize() function from the the easystats library.\n\n\n\n\n\n\nNote\n\n\n\nEasystats\nEasystats is a collection of R packages that includes tools dedicated to the post-processing of statistical models. It is made of all these packages: report, correlation, modelbased, bayestestR, effectsize, see, parameters, performance, insight, datawizard. We will extensively use all these package in our tutorials. The cool thing is that you can import all of them by just simply importing the collection Easystats with library(easystats).\nIn this tutorial here we will use the function from the packages datawizardand performance(in the Model checks sections).\n\ndatawizard allows to manipulate, clean, transform, and prepare your data\nperformance is a package to check model performance metrics.\n\n\n\nSo now we import easystats and we use the function standardize()\n\nlibrary(easystats)\ndf$StandardTrialN = standardize(df$TrialN)\nhead(df)\n\n Id Event TrialN SaccadicRT LookingTime SES StandardTrialN\n1 1 Simple 2 354.4634 1143.3041 high -1.4722432\n2 1 Simple 4 287.6544 1053.8191 high -1.1258330\n3 1 Simple 5 298.2421 1003.3585 high -0.9526279\n4 1 Simple 8 NA 853.2175 high -0.4330127\n5 1 Simple 9 NA 773.4392 high -0.2598076\n6 1 Simple 12 234.9623 742.8035 high 0.2598076\n\n\nWe have created like this a new column StandardTrialN with the standardized values of the TrialN and now we can run the model again\n\nmod_lm_standardized = lm(LookingTime ~ StandardTrialN*Event, data = df)\nsummary(mod_lm_standardized)\n\n\nCall:\nlm(formula = LookingTime ~ StandardTrialN * Event, data = df)\n\nResiduals:\n Min 1Q Median 3Q Max \n-609.15 -212.47 13.17 218.58 676.70 \n\nCoefficients:\n Estimate Std. Error t value Pr(>|t|) \n(Intercept) 1419.44 19.51 72.740 < 2e-16 ***\nStandardTrialN -71.50 19.41 -3.683 0.000264 ***\nEventSimple -266.39 27.53 -9.677 < 2e-16 ***\nStandardTrialN:EventSimple -50.01 27.28 -1.833 0.067605 . \n---\nSignif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1\n\nResidual standard error: 268.1 on 376 degrees of freedom\n (20 observations deleted due to missingness)\nMultiple R-squared: 0.2892, Adjusted R-squared: 0.2835 \nF-statistic: 50.99 on 3 and 376 DF, p-value: < 2.2e-16\n\n\nAs you can see the estimates are different!!! Because as we mentioned the reference level and the scale are different now. However do not panic, even if some values are different the relations are the same, we still have negative estimate for the time variable (here StandardTrialN and before TrialN) and positive for the Complex condition. Also the p-values are the same!\nWhat is important to keep in mind is that now 0 is the mean of our data and this makes the intercept much more interpretable! All the estimates are now in relation to the mean of the data. Let’s visualize this!\n\n\n\n\n\n\n\n\nAs you can see the plot is the same but now the data hits 0 at the mean of the data!!\nSo there you have it - standardizing your predictors makes your model both more interpretable and more robust, all while keeping the fundamental relationships in your data intact. It’s one of those simple steps that can make a big difference in your statistical journey! 🌟",
"crumbs": [
"Stats",
"Linear Models"
]
},
{
"objectID": "CONTENT/Stats/LinearModels.html#statistical-tests",
"href": "CONTENT/Stats/LinearModels.html#statistical-tests",
"title": "Linear Models",
"section": "Statistical tests",
"text": "Statistical tests\nYou’ve probably noticed that we’ve been relying on visual checks so far. In our view, this is often the best approach, as statistical tests for model assumptions can sometime be overly stringent. However, there may be situations where you need to provide statistical evidence to support your model assumptions. This often happens when a reviewer (let’s call them Reviewer 2, shall we?) insists on seeing numerical proof. Fortunately, easystats has got your back.\nHere are some examples of what you can use:\n\ncheck_normality(mod_lm)\n\nWarning: Non-normality of residuals detected (p = 0.001).\n\n\nTo check the normality of our residuals and:\n\ncheck_homogeneity(mod_lm)\n\nOK: There is not clear evidence for different variances across groups (Bartlett Test, p = 0.983).\n\n\nto check homoscedasticity/homogeneity of variance. Again you can find all the function in the performance package (part of the Easystats collection)",
"crumbs": [
"Stats",
"Linear Models"
]
},
{
"objectID": "CONTENT/Stats/GeneralisedModels.html",
"href": "CONTENT/Stats/GeneralisedModels.html",
"title": "Generalised mixed-effect models",
"section": "",
"text": "If you’ve been following our statistical journey, you’re already familiar with our adventures in linear models and the exciting world of linear mixed-effect models!!! Now it’s time to level UP our statistical toolkit with something even MORE POWERFUL: Generalised Linear Mixed-Effects Models (GLMMs)!!! 🎉\nIn this tutorial, we’ll tackle those variables that just WON’T behave normally! Our focus will be on Reaction Time. Remember the initial hypotheses for our study? (if not, you can find an in-depth description of the experimental paradigm in our intro tutorial) They were not only about Looking Time, but also saccadic reaction times! These reaction times indicate how quickly participants direct their gaze to the target stimuli when they appeared on screen.\nWe took faster/shorter saccadic reaction times as an index of learning. Based on this, we predicted that participants would learn to predict the location of both the Complex and the Simple stimulus, which should result in a significant decrease in saccadic reaction times over trials.",
"crumbs": [
"Stats",
"Generalised mixed-effect models"
]
},
{
"objectID": "CONTENT/Stats/GeneralisedModels.html#linear-model",
"href": "CONTENT/Stats/GeneralisedModels.html#linear-model",
"title": "Generalised mixed-effect models",
"section": "Linear model",
"text": "Linear model\nFollowing what we have learned in the previous tutorial we will fit a linear mixed effect model including a random intercept and random slope:\n\nmod_lm <- lmer(\n SaccadicRT ~ StandardTrialN * Event + (1 + StandardTrialN | Id),\n data = df\n)\n\nNow, before even looking at the summary, let’s check whether the model follows the model assumptions. For that, we can use the function check_model() as we explained in the tutorial on linear models:\n\ncheck_model(mod_lm)\n\n\n\n\n\n\n\nThis does not look too great. The linearity is not really flat… It seems U shaped. The homogeneity of variance as well! In addition we have influential observations (red outliers) and also the normality of residuals is not too great!!\nHowever, the plot I want you to focus on actually is the first one, the posterior predictive check. Do you note anything weird?? As you can see, the model believes there are values below 200ms, but that as we saw earlier is IMPOSSIBLE!! No participant can respond that quickly in our dataset!\nAll these indicators tell us that the model is fitting poorly!!! Our linear model is struggling to capture the true nature of reaction time data. The model is trying to force our non-normal data into a normal distribution, and it’s clearly not working!",
"crumbs": [
"Stats",
"Generalised mixed-effect models"
]
},
{
"objectID": "CONTENT/Stats/GeneralisedModels.html#finding-a-better-family",
"href": "CONTENT/Stats/GeneralisedModels.html#finding-a-better-family",
"title": "Generalised mixed-effect models",
"section": "Finding a better family",
"text": "Finding a better family\nSo what’s the solution to this mess? If normal distributions don’t work for our data, we need to find a distribution family that better matches reaction times! This is exactly where Generalized Linear Mixed Models (GLMMs) come to the rescue - they allow us to specify different distribution families beyond just Gaussian.\nBut which distribution should we use? The more you get familiarised with distributions, the more you will learn to recognise them. But since we’re not black belt of statistics yet, let’s use the Cullen and Frey graph instead! For this, we will use the fitdistrplus package:\n\nlibrary(fitdistrplus)\n\n# first we need to remove NAs:\nRTs = as.vector(na.omit(df$SaccadicRT))\n# then we can plot\ndescdist(RTs, discrete = FALSE, boot = 500)\n\n\n\n\n\n\n\nsummary statistics\n------\nmin: 189.959 max: 927.502 \nmedian: 308.5174 \nmean: 335.2607 \nestimated sd: 110.3912 \nestimated skewness: 1.694027 \nestimated kurtosis: 7.483638 \n\n\nThe Cullen and Frey graph reveals which distribution best matches our data by plotting skewness against kurtosis. Looking at our reaction time data (red dot), we can immediately see it falls far from the normal distribution (asterisk) and instead sits near the gamma distribution (dashed line). The yellow circles represent bootstrapped samples that help us assess uncertainty in our estimate. Since these bootstrapped points tend to closely follow the gamma distribution line, we have strong evidence to select gamma as our distribution family! This explains why our linear model (which assumes normality) performed so poorly and gives us a clear direction for building a more appropriate model.\n\n\n\n\n\n\nCaution\n\n\n\nBeware! this plot only shows 7 or 8 distributions, and many more exist! For example, do you know what a bernoulli or a binary distribution are? If not we advise you check out https://distribution-explorer.github.io/index.html",
"crumbs": [
"Stats",
"Generalised mixed-effect models"
]
},
{
"objectID": "CONTENT/Stats/GeneralisedModels.html#summary",
"href": "CONTENT/Stats/GeneralisedModels.html#summary",
"title": "Generalised mixed-effect models",
"section": "Summary",
"text": "Summary\nOnce we are confident that the assumptions are respected, we can interpret the results:\n\nsummary(mod_gamma)\n\nGeneralized linear mixed model fit by maximum likelihood (Laplace\n Approximation) [glmerMod]\n Family: Gamma ( log )\nFormula: SaccadicRT ~ StandardTrialN * Event + (1 + StandardTrialN | Id)\n Data: df\n\n AIC BIC logLik -2*log(L) df.resid \n 2610.4 2638.9 -1297.2 2594.4 253 \n\nScaled residuals: \n Min 1Q Median 3Q Max \n-1.8107 -0.5617 0.0282 0.5889 3.3005 \n\nRandom effects:\n Groups Name Variance Std.Dev. Corr\n Id (Intercept) 0.014666 0.12110 \n StandardTrialN 0.006314 0.07946 0.10\n Residual 0.014798 0.12165 \nNumber of obs: 261, groups: Id, 20\n\nFixed effects:\n Estimate Std. Error t value Pr(>|z|) \n(Intercept) 5.30372 0.07102 74.676 <2e-16 ***\nStandardTrialN -0.35867 0.03868 -9.273 <2e-16 ***\nEventSimple 0.31795 0.01452 21.901 <2e-16 ***\nStandardTrialN:EventSimple 0.18635 0.01442 12.926 <2e-16 ***\n---\nSignif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1\n\nCorrelation of Fixed Effects:\n (Intr) StndTN EvntSm\nStandrdTrlN -0.072 \nEventSimple -0.164 -0.176 \nStndrdTN:ES -0.101 -0.275 0.476\n\n\nOMG, these results are AMAZING!!! 🎉 Look at that interaction between trial number and Event - it’s significant! This is super important to note because when we have significant interactions, we need to focus on understanding THOSE rather than looking at the main effects in isolation.\nThe interaction tells us that the effect of trial number on reaction time DEPENDS on which event condition we’re in! To really grasp what this pattern means in practical terms, let’s visualize this interaction:\n\nCodeis_pred <- estimate_expectation(mod_gamma, include_random=F)\n\n# 2. Plot the interaction between StandardTrialN and Event\nggplot(is_pred, aes(x = StandardTrialN, \n y = Predicted, \n color = Event, \n group = Event)) +\n geom_line(size = 1.2) + \n geom_ribbon(aes(ymin = Predicted - SE, ymax = Predicted + SE, fill = Event),\n alpha = 0.2, color = \"transparent\") +\n labs(y = \"Reaction Time (Predicted)\", x = \"Scaled Trial #\") +\n scale_color_manual(values = c('Simple' = '#4A6274', 'Complex' = '#E2725A')) +\n scale_fill_manual(values = c('Simple' = '#4A6274', 'Complex' = '#E2725A'))+\n theme_classic(base_size = 16)\n\n\n\n\n\n\n\nReaction times decrease much more across trials for the Complex than for the Simple condition! However, we don’t have a complete picture yet. For example, we do not know whether the decrease in reaction times across trials is significant only for the Complex condition or also for the Simple condition. To find out, we can estimate the slopes as we did in the previous tutorial: Model Estimates\n\nestimate_slopes(mod_gamma, trend = 'StandardTrialN', by = 'Event')\n\nEstimated Marginal Effects\n\nEvent | Slope | SE | 95% CI | t(253) | p\n--------------------------------------------------------------\nComplex | -87.40 | 13.10 | [-113.19, -61.61] | -6.67 | < .001\nSimple | -51.16 | 13.85 | [ -78.43, -23.88] | -3.69 | < .001\n\nMarginal effects estimated for StandardTrialN\nType of slope was dY/dX\n\n\nNice !! This is confirming what we could already guess from the plot!!\nthat there is an change in the effect of the complex with the number of trials but there for the complex shape is\nAlthough we’ve been focusing on the main effects, remember that we also had random intercepts and slopes in our model! We can have a look at the random effects as well:\n\nCodeis_pred_random = estimate_expectation(mod_gamma, include_random =T)\n\nggplot(is_pred_random, aes(x= StandardTrialN, y= Predicted, color= Id, shape = Event))+\n geom_point(data = df, aes(y= SaccadicRT, color= Id), position= position_jitter(width=0.2))+\n geom_line()+\n geom_ribbon(aes(ymin=Predicted-SE, ymax=Predicted+SE, fill = Id),color= 'transparent', alpha=0.1)+\n labs(y='Reaction time', x='# trial')+\n theme_classic(base_size = 20)+\n theme(legend.position = 'none')+\n facet_wrap(~Event)\n\n\n\n\n\n\n\nThe variability is impressive! For example, looking at the Simple condition, we can see that Reaction times are getting slower over time for some infants, but they are getting faster for others! These participants are probably loosing interest in the Simple condition, because they learn across time that nothing interesting is going to appear on the screen after the cue! It’s also really interesting to see how, from the group level estimates, it seemed that the Simple effect was simply weaker than the Complex effect, while there’s something more going on: there seem to be more variability in the Simpe condition, with a subgroup of infants being especially uninterested in this specific condition only!\nAren’t generalised mixed-effect models pretty cool!?? We get to pick the distribution that better resembles the data and we get to model random effects, and these things really improve the fit of our model! We often get to have stronger main effects (which is very often the main thing we are interested in) and we get the insane bonus of looking at individual differences and unique patterns that we would otherwise completely miss!\nWe hope you’ll get to use GLMMs a lot in your research, and that they will help you find super cool results just like we did here!!!!",
"crumbs": [
"Stats",
"Generalised mixed-effect models"
]
},
{
"objectID": "CONTENT/GettingStarted/GettingStartedWithPython.html",
"href": "CONTENT/GettingStarted/GettingStartedWithPython.html",
"title": "Starting with Python",
"section": "",
"text": "Python is one of the most popular programming languages in general. In data science, it competes with Matlab and R for first place on the podium.\nIn our everyday we often use python to pre-process and analyze the data. In this tutorial we will explain our preferred way of installing python and managing its libraries. There are several ways to install python, this is the one we recommend for its simplicity and flexibility.",
"crumbs": [
"Getting started",
"Starting with Python"
]
},
{
"objectID": "CONTENT/GettingStarted/GettingStartedWithPython.html#miniconda",
"href": "CONTENT/GettingStarted/GettingStartedWithPython.html#miniconda",
"title": "Starting with Python",
"section": "Miniconda",
"text": "Miniconda\nMiniconda is our favorite way to install Python, and for good reason! While you’ve probably heard of Anaconda – that feature-packed GUI many folks use to manage Python, packages, and environments – Miniconda is its sleek, lightweight counterpart.\nWhat makes Miniconda special? It skips all the extra GUI elements and pre-installed packages that come with Anaconda. The result? A much lighter installation that doesn’t weigh down your system! With Miniconda, you get just the essentials: Conda, Python, and a few critical dependencies. This minimal setup gives you greater control over what gets installed, keeping everything lean and efficient. Plus, it includes the default Conda package manager and channels, so you still have full access to the official Conda repository for all your package needs.\n\nWindowsMac\n\n\nTo use Miniconda download the installer from Miniconda (remember to scroll down under Anaconda)\nThe installation process is similar to that of Anaconda. Once the installation is complete, you will find the Anaconda Prompt among your programs. This prompt serves as your interface for installing Python packages and creating environments. To verify everything’s working properly, simply type conda. You’ll be greeted with a comprehensive list of available conda commands ready for you to explore and use (we will see below how to do so).\n\n\n\n\n\n\n\nTo use Miniconda download the installer from Miniconda (remember to scroll down under Anaconda)\n\n\n\n\n\n\nCaution\n\n\n\n⚠️ Important\nPlease make sure that you select the correct version for your system!!! Select the version depending if you have Apple Silicon or an Intel processor.\n\n\nOnce the installation is complete, you’ll have the full power of Miniconda at your fingertips through your terminal! To verify everything’s working properly, simply open your terminal and type conda. You’ll be greeted with a comprehensive list of available conda commands ready for you to explore and use (we will see below how to do so).\n\n\n\nFlowchart",
"crumbs": [
"Getting started",
"Starting with Python"
]
},
{
"objectID": "CONTENT/GettingStarted/DevStartSetupGuide.html",
"href": "CONTENT/GettingStarted/DevStartSetupGuide.html",
"title": "DevStart Setup Guide",
"section": "",
"text": "Welcome to DevStart!!!\nThis is your starting point for an exciting journey with us! Together, we’ll explore everything from creating experiments and collecting data to processing and analyzing your results. In this section, we’ll prepare your computer with all the essential tools you’ll need for our tutorials and ensure you have the best setup for your future research endeavors!",
"crumbs": [
"DevStart Setup Guide"
]
},
{
"objectID": "CONTENT/GettingStarted/DevStartSetupGuide.html#getting-started",
"href": "CONTENT/GettingStarted/DevStartSetupGuide.html#getting-started",
"title": "DevStart Setup Guide",
"section": "Getting started",
"text": "Getting started\nIn DevStart, you’ll learn to create psychological studies using Python and PsychoPy, then preprocess and analyze your data with a powerful combination of Python and R.\nWe’ve chosen these tools because they’re completely free and open-source – which we absolutely love! However, we’ll be honest: they’re not always the most straightforward to install (especially Python and PsychoPy). That’s exactly why we’ve created detailed, step-by-step installation guides based on our own experience and best practices.\nPlease note: These aren’t the only ways to set up these tools, but they represent what we use and have seen works reliably.\nWe’ve prepared comprehensive guides for:\n\nGetting started with Python\nGetting started with Psychopy\nGetting started with R",
"crumbs": [
"DevStart Setup Guide"
]
},
{
"objectID": "CONTENT/GettingStarted/DevStartSetupGuide.html#however.",
"href": "CONTENT/GettingStarted/DevStartSetupGuide.html#however.",
"title": "DevStart Setup Guide",
"section": "HOWEVER….",
"text": "HOWEVER….\nIf you’re new to these tools, we strongly recommend following our interactive flowcharts below. Simply click the buttons to navigate to the most relevant sections of our tutorials based on your experience level and goals.\nThe flowcharts will guide you through the setup process in the right order for your specific situation.\n\nPython and Psychopy\nHere the flwochart to install Python and Psychopy.\n\n\n\n\n\n\nImportant\n\n\n\n\n\n In case the flowchart is not rendering properly on your screen, please download it here:\n \n FlowchartPython.pdf\n \n\n\n\n\n\n\n \n Responsive PDF with Clickable Links\n \n \n \n \n\n\n \n \n \n \n \n\n \n\n\n\n\nR and Rstudio\nHere the flowchart to install R and Rstudio (this is way more straightforward).\n\n\n\n\n\n\nImportant\n\n\n\n\n\n In case the flowchart is not rendering properly on your screen, please download it here:\n \n FlowchartR.pdf\n \n\n\n\n\n\n\n \n R Flowchart PDF Viewer",
"crumbs": [
"DevStart Setup Guide"
]
},
{
"objectID": "CONTENT/EyeTracking/PupilPreprocessing.html",
"href": "CONTENT/EyeTracking/PupilPreprocessing.html",
"title": "Pupil data pre-processing",
"section": "",
"text": "Welcome to your first step into the world of pupillometry! In this tutorial, we’ll walk you through how to preprocess pupil data using a handy R package called PupillometryR. This package makes it simple to clean and even analyze your pupil data with just a few lines of R code.\nTo keep things straightforward, we’ll be working with a simulated dataset that you can download right here:\nPupilData.zip\nDownload and unzip this folder. This dataset is based on the experimental design we introduced earlier in our eye-tracking series. If you’re not familiar with it or need a quick refresher, we recommend checking out the “Introduction to eye-tracking” guide before diving in.\nThis tutorial serves as a foundation for understanding how to preprocess pupil data. Once you’ve grasped the essentials, we encourage you to explore the full range of functions and features PupillometryR has to offer.",
"crumbs": [
"Eye-tracking",
"Pupil data pre-processing"
]
},
{
"objectID": "CONTENT/EyeTracking/PupilPreprocessing.html#read-the-data",
"href": "CONTENT/EyeTracking/PupilPreprocessing.html#read-the-data",
"title": "Pupil data pre-processing",
"section": "Read the data",
"text": "Read the data\nLet’s begin by importing the necessary libraries and loading the downloaded dataframe.\n\nlibrary(PupillometryR) # Library to process pupil signal\nlibrary(tidyverse) # Library to wrangle dataframes\nlibrary(patchwork)\n\nGreat! Now, let’s locate and load all our data files. We’ll use list.files() to identify all the .csv files in our folder. Make sure to update the file path to match the location where your data is stored.\n\ncsv_files = list.files(\n path = \"resources/Pupillometry/RAW\",\n pattern = \"\\\\.csv$\", # regex pattern to match .csv files\n full.names = TRUE # returns the full file paths\n)\n\ncsv_files is now a list containing all the .csv files we’ve identified. To better understand our dataset, let’s start by focusing on the first file, representing the first subject, and inspect its structure. This will give us a clear overview before we proceed further.\n\nRaw_data = read.csv(csv_files[1])\nhead(Raw_data) # database peak\n\n Unnamed..0 Subject time L_P R_P Event TrialN\n1 1 Subject_1 1.000000 3.187428 3.228510 Fixation 1\n2 2 Subject_1 4.333526 3.153315 3.193957 NA\n3 3 Subject_1 7.667052 3.102050 3.142032 NA\n4 4 Subject_1 11.000578 3.163670 3.204446 NA\n5 5 Subject_1 14.334104 3.152682 3.193316 NA\n6 6 Subject_1 17.667630 3.086508 3.126289 NA\n\n\nOur dataframe consists of several easily interpretable columns. time represents elapsed time in milliseconds, Subject identifies the participant, and Event indicates when and which stimuli were presented. TrialN tracks the trial number, while L_P and R_P measure pupil dilation for the left and right eyes, respectively, in millimeters.\nLet’s plot the data! Visualizing it first is always a crucial step as it provides an initial understanding of its structure and key patterns.\n\nggplot(Raw_data, aes(x = time, y = R_P)) +\n geom_line(aes(y = R_P, color = 'Pupil Right'), lwd = 1.2) +\n geom_line(aes(y = L_P, color = 'Pupil Left'), lwd = 1.2) +\n geom_vline(data = Raw_data |> dplyr::filter(Event!= \"\"), aes(xintercept = time, linetype = Event), lwd = 1.3) +\n \n theme_bw(base_size = 35) +\n ylim(1, 6) +\n labs(color= 'Signal', y='Pupil size')+\n scale_color_manual(\n values = c('Pupil Right' = '#4A6274', 'Pupil Left' = '#E2725A') ) +\n theme(\n legend.position = 'bottom' ) +\n guides(\n color = guide_legend(override.aes = list(lwd = 20)),\n linetype = guide_legend(override.aes = list(lwd = 1.2))\n )",
"crumbs": [
"Eye-tracking",
"Pupil data pre-processing"
]
},
{
"objectID": "CONTENT/EyeTracking/PupilPreprocessing.html#prepare-the-data",
"href": "CONTENT/EyeTracking/PupilPreprocessing.html#prepare-the-data",
"title": "Pupil data pre-processing",
"section": "Prepare the data",
"text": "Prepare the data\nNice!! Now we have some sense of our data!! And….you’ve probably noticed two things:\n\nSo many events! That’s intentional — it’s better to have too many triggers than miss something important. When we recorded the data, we saved all possible events to ensure nothing was overlooked. But don’t worry, for our pupil dilation analysis, we only care about two key events: Circle and Square (check the paradigm intro if you need a refresher on why this is)\nSingle-sample events! Like in most studies, events are marked at a single time point (when the stimulus is presented). But PupilometryR needs a different structure — it expects the event value to be repeated for every row while the event is happening.\n\nSo, how do we fix this? First, let’s isolate the rows in our dataframe where the events are Circle or Square. We start by creating a list of the events we care about, then use it to filter our dataframe and keep only the rows related to those events in a new dataframe called Events. In the process we will also create a new column called TrialN. In this column we will store the passing of time as trial. This will help us identify different trials of the same Event.\n\nEvents_to_keep = c('Circle','Square')\nEvents = Raw_data %>% filter(Event %in% Events_to_keep) %>% # filter data\n mutate(TrialN = seq(n()))\nhead(Events) # database peak\n\n Unnamed..0 Subject time L_P R_P Event TrialN\n1 221 Subject_1 734.3757 NA NA Circle 1\n2 2552 Subject_1 8504.8248 3.596057 3.642405 Square 2\n3 4883 Subject_1 16275.2739 3.543367 NA Circle 3\n4 7215 Subject_1 24049.0565 3.164419 3.205205 Circle 4\n5 9546 Subject_1 31819.5055 3.147494 3.188061 Square 5\n6 11877 Subject_1 39589.9546 3.343493 3.386587 Circle 6\n\n\nPerfect! Now onto the second point: we need to repeat the events we just selected for the entire duration we want to analyze. But what’s this duration? We want to cover the full cue presentation (2 seconds), plus an extra 0.1 seconds before the stimulus appears. Why? This pre-stimulus period will serve as our baseline, which we’ll use later in the analysis.\nSo, let’s define how much time to include before and after the stimulus. We’ll also set the framerate of our data (300Hz) and create a time vector that starts from the pre-stimulus period and continues in steps of 1/Hz, with a total length equal to Pre_stim + Post_stim.\n\n# Settings to cut events\nFs = 300 # framerate\nStep = 1000/Fs\n\nPre_stim = 100 # pre stimulus time (100ms)\nPost_stim = 2000 # post stimulus time (2000ms)\nPre_stim_samples = Pre_stim/Step # pre stimulus in samples\nPost_stim_samples = Post_stim/Step # post stimulus in samples\n\n# Time vector based on the event duration\nTime = seq(from = -Pre_stim, by=Step, length.out = Pre_stim_samples+Post_stim_samples) # time vector\n\nHere’s where the magic happens. We loop through each event listed in our Events dataframe. Each row in Events corresponds to a specific event (like a “Circle” or “Square” cue) that occurred for a specific subject during a specific trial.\nFor each event, we extract 2 key details:\n\nEvent (to know if it’s a Circle or Square cue)\nTrialN (to know which trial this event is part of)\n\nNext, we identify the rows of interest in our main dataframe. First, we locate the row where the time is closest to the onset of the event. Then, we select a range of rows that fall within the Pre_stim and Post_stim window around the event.\nFinally, we use these identified rows to add the event information. The Time, Event, and TrialN values are repeated across all the rows in this window, ensuring every row in the event window is properly labeled.\n\n# Loop for each event \nfor (trial in 1:nrow(Events)){\n\n # Extract the information\n Event = Events[trial,]$Event\n TrialN = Events[trial,]$TrialN\n \n # Event onset information\n Onset = Events[trial,]$time\n Onset_index = which.min(abs(Raw_data$time - Onset))\n \n # Find the rows to update based on pre post samples\n rows_to_update = seq(Onset_index - Pre_stim_samples,\n Onset_index + Post_stim_samples-1)\n \n # Repeat the values of interest for all the rows\n Raw_data[rows_to_update, 'time'] = Time\n Raw_data[rows_to_update, 'Event'] = Event\n Raw_data[rows_to_update, 'TrialN'] = TrialN\n}\n\nPerfect! We’ve successfully extended the event information backward and forward based on our Pre_stim and Post_stim windows. Now, it’s time to clean things up.\nSince we only care about the rows that are part of our trial of interest —and because the event information is now repeated for each row during its duration— we’ll remove all the rows that don’t belong to these event windows. This will leave us with a clean, focused dataset that only contains the data relevant to our analysis.\n\nTrial_data = Raw_data %>% \n filter(Event %in% Events_to_keep)\n\nFor all subjects\nGreat job making it this far! Fixing the data to make it usable in PupillometryR is definitely one of the trickiest parts. But… we’ve only done this for one subject so far—oops! 😅 No worries, though. Let’s automate this process by putting everything into a loop for each subject. In this loop, we’ll fix the event structure for each subject, store each subject’s processed dataframe in a list, and finally combine all these dataframes into one single dataframe for further analysis. Let’s make it happen!\n\n# Libraries and files --------------------------------------------------------------------\n\nlibrary(PupillometryR) # Library to process pupil signal\nlibrary(tidyverse) # Library to wrangle dataframes\nlibrary(patchwork) \n\ncsv_files = list.files(\n path = \"resources/Pupillometry/RAW\",\n pattern = \"\\\\.csv$\", # regex pattern to match .csv files\n full.names = TRUE # returns the full file paths\n)\n\n\n# Event settings --------------------------------------------------------------------\n\nFs = 300 # framerate\nStep = 1000/Fs\n\nPre_stim = 100 # pre stimulus time (100ms)\nPost_stim = 2000 # post stimulus time (2000ms)\nPre_stim_samples = Pre_stim/Step # pre stimulus in samples\nPost_stim_samples = Post_stim/Step # post stimulus in samples\n\n# Time vector based on the event duration\nTime = seq(from = -Pre_stim, by=Step, length.out = Pre_stim_samples+Post_stim_samples) # time vector\n\n# Event fixes --------------------------------------------------------------------\n\n# Events to keep\nEvents_to_keep = c('Circle','Square')\n\nList_of_subject_dataframes = list() # Empty list to be filled with dataframes\n\n# Loop for each subject\nfor (sub in 1:length(csv_files)) {\n \n Raw_data = read.csv(csv_files[sub]) # Raw data\n Events = Raw_data %>% filter(Event %in% Events_to_keep) %>% # filter data\n mutate(TrialN = seq(n()))\n \n # Loop for each event \n for (trial in 1:nrow(Events)){\n \n # Extract the information\n Event = Events[trial,]$Event\n TrialN = Events[trial,]$TrialN\n \n # Event onset information\n Onset = Events[trial,]$time\n Onset_index = which.min(abs(Raw_data$time - Onset))\n \n # Find the rows to update based on pre post samples\n rows_to_update = seq(Onset_index - Pre_stim_samples,\n Onset_index + Post_stim_samples-1)\n \n # Repeat the values of interest for all the rows\n Raw_data[rows_to_update, 'time'] = Time\n Raw_data[rows_to_update, 'Event'] = Event\n Raw_data[rows_to_update, 'TrialN'] = TrialN\n }\n \n \n # Filter only events of interest\n Trial_data = Raw_data %>% \n filter(Event %in% Events_to_keep)\n \n # Add daframe to list\n List_of_subject_dataframes[[sub]] = Trial_data\n}\n\n# Combine the list of dataframes into 1 dataframe\nTrial_data = bind_rows(List_of_subject_dataframes)\n\nNow we have our dataset all fixed and organized for each subject. Let’s take a look!\n\nCodeggplot(Trial_data, aes(x = time, y = R_P, group = TrialN)) +\n geom_line(aes(y = R_P, color = 'Pupil Right'), lwd = 1.2) +\n geom_line(aes(y = L_P, color = 'Pupil Left'), lwd = 1.2) +\n geom_vline(aes(xintercept = 0), linetype = 'dashed', color = 'black', lwd = 1.2) +\n facet_wrap(~Subject) +\n \n ylim(1, 6) +\n scale_color_manual(values = c('Pupil Right' = '#4A6274', 'Pupil Left' = '#E2725A')) +\n \n theme_bw(base_size = 35) +\n theme(\n legend.position = 'bottom', \n legend.title = element_blank()) +\n guides(color = guide_legend(override.aes = list(lwd = 20))) \n\n\n\n\n\n\n\nAs you can see, the data structure is now completely transformed. We’ve segmented the data into distinct time windows, with each segment starting at -0.1 seconds (-100 ms) and extending to 2 seconds (2000 ms). This new structure ensures consistency across all segments, making the data ready for further analysis.\nMake PupillometryR data\nOk, now it’s time to start working with PupillometryR! 🎉\nIn the previous steps, we changed our event structure, and you might be wondering — why all that effort? Well, it’s because PupillometryR needs the data in this specific format to do its magic. To get started, we’ll pass our dataframe to the make_pupillometryr_data() function. If you’re already thinking, “Oh no, not another weird object type that’s hard to work with!” — don’t worry! The good news is that the main object it creates is just a regular dataframe. That means we can still interact with like we’re used to. This makes the pre-processing steps much less frustrating. Let’s get started!\n\nPupilR_data = make_pupillometryr_data(data = Trial_data,\n subject = Subject,\n trial = TrialN,\n time = time,\n condition = Event)\n\nHere, we’re simply using the make_pupillometryr_data() function to pass in our data and specify which columns contain the key information. This tells PupillometryR where to find the crucial details, like subject IDs, events, and pupil measurements, so it knows how to structure and process the data properly.\n\n\n\n\n\n\nTip\n\n\n\nIf you have extra columns that you want to keep in your PupillometryR data during preprocessing, you can pass them as a list using the other = c(OtherColumn1, OtherColumn2) argument. This allows you to keep these additional columns alongside your main data throughout most of the preprocessing steps.\nBut here’s a heads-up — not all functions can keep these extra columns every time. For example, downsampling may not retain them since it reduces the number of rows, and it’s not always clear how to summarize extra columns. So, keep that in mind as you plan your analysis!\n\n\nPlot\nOne cool feature of the data created using make_pupillometryr_data() is that it comes with a simple, built-in plot function. This makes it super easy to visualize your data without needing to write tons of code. The plot function works by averaging the data over the group variable. So we can group over subject or condition. Here we use the group variable to focus on the condition and average over the subjects.\nIn this example, we’re plotting the L_P (left pupil) data, grouped by condition. The plot() function is actually just a ggplot2 wrapper, which means you can customize to a certain extent like any other ggplot. That’s why we can add elements to it, like theme_bw(), which gives the plot a cleaner, black-and-white look. Give it a go without adding anything and then learn to customize it!!\n\n\n\n\n\n\nTip\n\n\n\nPro tip! If you want more control over your plots, you can always use ggplot2. Remember, the Pupil data is just a regular dataframe, so you can plot it in any way you like!\n\n\n\nplot(PupilR_data, pupil = L_P, group = 'condition', geom = 'line') +\n theme_bw(base_size = 45) +\n theme(\n legend.position = 'bottom', \n legend.title = element_blank()) +\n guides(color = guide_legend(override.aes = list(lwd = 20))) \n\n\n\n\n\n\n\n\n\n\n\n\n\nNote\n\n\n\nIn this tutorial, we’ll use two methods to plot our data. We’ll use the PupillometryR plot to visualize the average pupil response by condition, and we’ll also use ggplot to manually plot our data. Both approaches are valid and offer unique benefits.\nThe PupillometryR plot provides a quick overview by automatically averaging pupil responses across condition levels, making it ideal for high-level trend visualization. On the other hand, ggplot gives you full control to visualize specific details or customize every aspect of the plot, allowing for deeper insights and flexibility.",
"crumbs": [
"Eye-tracking",
"Pupil data pre-processing"
]
},
{
"objectID": "CONTENT/EyeTracking/PupilPreprocessing.html#pre-processing",
"href": "CONTENT/EyeTracking/PupilPreprocessing.html#pre-processing",
"title": "Pupil data pre-processing",
"section": "Pre-processing",
"text": "Pre-processing\nNow that we have our pupillometry data in the required format we can actually start the pre-processing!!\nRegress\nThe first step is to regress L_P against R_P (and vice versa) using a simple linear regression. This corrects small inconsistencies in pupil data caused by noise. The regression is done separately for each participant, trial, and time point, ensuring smoother and more consistent pupil dilation measurements.\n\nRegressed_data = regress_data(data = PupilR_data,\n pupil1 = L_P,\n pupil2 = R_P)\n\nError in `mutate()`:\nℹ In argument: `pupil1newkk = .predict_right(L_P, R_P)`.\nℹ In group 1: `Subject = \"Subject_1\"`, `TrialN = 1`, `Event = \"Circle\"`.\nCaused by error in `lm.fit()`:\n! 0 (non-NA) cases\n\n\nPwa pwa pwaaaaa…!!🤦♂️ We got an error!\nWhat’s it saying? It’s telling us that one of the trials is completely full of NAs, and since there’s no data to work with, the function fails. This happens a lot when testing infants — they don’t always do what we expect, like watching the screen. Instead, they move around or look away.\nWe’ll deal with missing data properly later, but for now, we need a quick fix. What can we do? We can simply drop any trials where both pupils (L_P and R_P) are entirely NA. This way, we avoid errors and keep the analysis moving.\nSo let’s filter our data and then redo the last two steps (make PupilR_data and then regress data)\n\n# Filter the trial data\nTrial_data = Trial_data %>%\n group_by(Subject, TrialN) %>% # group by Subject and TrialN\n filter(!all(is.na(L_P) & is.na(R_P))) %>% # filter out if both R_P and L_P are all NA\n ungroup() # Remove grouping\n\n# Make pupilloemtryR data\nPupilR_data = make_pupillometryr_data(data = Trial_data,\n subject = Subject,\n trial = TrialN,\n time = time,\n condition = Event)\n# Regress data\nRegressed_data = regress_data(data = PupilR_data,\n pupil1 = L_P,\n pupil2 = R_P)\n\nAnd now everything worked!! Perfect!\nMean pupil\nAs the next steps we will average the two pupil signals. This will create a new variable called mean_pupil\n\nMean_data = calculate_mean_pupil_size(data = Regressed_data, \n pupil1 = L_P, \n pupil2 = R_P)\n\nplot(Mean_data, pupil = mean_pupil, group = 'condition', geom = 'line')+\n theme_bw(base_size = 45) +\n theme(\n legend.position = 'bottom', \n legend.title = element_blank()) +\n guides(color = guide_legend(override.aes = list(lwd = 20))) \n\n\n\n\n\n\n\nLowpass\nNow that we have a single pupil signal, we can move on to filtering it. The goal here is to remove fast noise and fluctuations that aren’t relevant to our analysis. Why? Because we know that pupil dilation is a slow physiological signal, and those rapid changes are likely just noise from blinks, eye movements, or measurement errors. By filtering out these fast fluctuations, we can focus on the meaningful changes in pupil dilation that matter for our analysis.\n\nFiltered_data = filter_data(data = Mean_data,\n pupil = mean_pupil,\n filter = 'median',\n degree = 11)\nplot(Filtered_data, pupil = mean_pupil, group = 'condition', geom = 'line')+\n theme_bw(base_size = 45) +\n theme(\n legend.position = 'bottom', \n legend.title = element_blank()) +\n guides(color = guide_legend(override.aes = list(lwd = 20))) \n\n\n\n\n\n\n\nThere are different ways to filter the data in PupillometryR we suggest you check the actual package website and make decision based on your data (filter_data). Here we use a median filter based on a 11 sample window.\nTrial Rejection\nNow that our data is smaller and smoother, it’s a good time to take a look at it. It doesn’t make sense to keep trials that are mostly missing values, nor does it make sense to keep participants with very few good trials.\nWhile you might already have info on trial counts and participant performance from other sources (like video coding), PupillometryR has a super handy function to check this directly. This way, you can quickly see how many valid trials each participant has and decide which ones to keep or drop.\n\nMissing_data = calculate_missing_data(Filtered_data,\n mean_pupil)\nhead(Missing_data, n=20)\n\n# A tibble: 20 × 3\n Subject TrialN Missing\n <chr> <dbl> <dbl>\n 1 Subject_1 2 0.0603\n 2 Subject_1 3 0.125 \n 3 Subject_1 4 0.0746\n 4 Subject_1 5 0.106 \n 5 Subject_1 6 0.0746\n 6 Subject_1 7 0.0635\n 7 Subject_1 8 0.0587\n 8 Subject_1 9 0.0619\n 9 Subject_1 10 0.130 \n10 Subject_2 1 0.143 \n11 Subject_2 2 0.121 \n12 Subject_2 3 0.0619\n13 Subject_2 4 0 \n14 Subject_2 5 0.132 \n15 Subject_2 6 0.137 \n16 Subject_2 7 0.0841\n17 Subject_2 8 0.0968\n18 Subject_2 9 0.0841\n19 Subject_2 10 0.0508\n20 Subject_3 1 0.0587\n\n\nThis gives us a new dataframe that shows the amount of missing data for each subject and each trial. While we could manually decide which trials and subjects to keep or remove, PupillometryR makes it easier with the clean_missing_data() function.\nThis function lets you set two % thresholds — one for trials and one for subjects. Here, we’ll set it to reject trials with more than 25% missing data (keep at least 75% of the data) and reject subjects with more than 25% missing data. This way, we ensure our analysis is based on clean, high-quality data.\n\nClean_data = clean_missing_data(Filtered_data,\n pupil = mean_pupil,\n trial_threshold = .75,\n subject_trial_threshold = .75)\n\nRemoving trials with a proportion missing > 0.75 \n ...removed 3 trials \n\n\nRemoving subjects with a proportion of missing trials > 0.75 \n ...removed 0 subjects \n\n\nSee?! PupillometryR shows us exactly how many trials and subjects are being excluded from our dataframe based on our thresholds. Cool!\n\n\n\n\n\n\nWarning\n\n\n\nNote that this function calculates the percentage of missing trials based only on the trials present in the dataframe. For example, if a participant only completed one trial (and watched it perfectly) before the session had to stop, the percentage would be calculated on that single trial, and the participant wouldn’t be rejected.\nIf you have more complex conditions for excluding participants (e.g., based on total expected trials or additional criteria), you’ll need to handle this manually to ensure subjects are dropped appropriately.\n\n\nFill the signal\nNow our data is clean, but… while the average signal for each condition looks smooth (as seen in our plots), the data for each individual participant is still noisy. We can still spot blinks and missing data in the signal.\nTo handle this, we’ll use interpolation to fill in the missing points. Interpolation “connects the dots” between gaps, creating a more continuous and cleaner signal. This step is crucial because large chunks of missing data can distort our analysis, and interpolation allows us to retain more usable data from each participant.\n\nggplot(Clean_data, aes(x = time, y = mean_pupil, group = TrialN, color= Event))+\n geom_line( lwd =1.2)+\n geom_vline(aes(xintercept = 0), linetype= 'dashed', color = 'black', lwd =1.2)+\n\n facet_wrap(~Subject)+\n ylim(1,6)+\n \n theme_bw(base_size = 45) +\n theme(\n legend.position = 'bottom', \n legend.title = element_blank()) +\n labs(y = 'Pupil Size')+\n guides(color = guide_legend(override.aes = list(lwd = 20))) \n\n\n\n\n\n\n\nSo to remove these missing values we can interpolate our data. Interpolating is easy with PupillometryR we can simply:\n\nInt_data = interpolate_data(data = Clean_data,\n pupil = mean_pupil,\n type = 'linear')\n\nggplot(Int_data, aes(x = time, y = mean_pupil, group = TrialN, color = Event))+\n geom_line(lwd =1.2)+\n geom_vline(aes(xintercept = 0), linetype= 'dashed', color = 'black', lwd =1.2)+\n\n facet_wrap(~Subject)+\n ylim(1,6)+\n \n theme_bw(base_size = 45) +\n theme(\n legend.position = 'bottom', \n legend.title = element_blank()) +\n labs(y = 'Pupil Size')+\n guides(color = guide_legend(override.aes = list(lwd = 20))) \n\n\n\n\n\n\n\nDone!! Well, you’ve probably noticed something strange… When there’s a blink, the pupil signal can rapidly decrease until it’s completely missing. Right now, this drop gets interpolated, and the result is a weird, unrealistic curve where the signal dips sharply and then smoothly recovers. This makes our data look horrible! 😩\nLet’s fix it!\nTo do this, we’ll use PupillometryR’s blink detection functions. There are two main ways to detect blinks:\n\nBased on size — detects pupil size.\nBased on velocity — detects rapid changes in pupil size (which happens during blinks).\n\nHere, we’ll use detection by velocity. We set a velocity threshold to detect when the pupil size changes too quickly. To ensure we capture the full blink, we use extend_forward and extend_back to expand the blink window, including the fast decrease in pupil size. The key idea is to make the entire blink period NA, not just the moment the pupil disappears. This prevents interpolation from creating unrealistic artifacts. When we interpolate, the process skips over the entire blink period, resulting in a cleaner, more natural signal.\n\nBlink_data = detect_blinks_by_velocity(\n Clean_data,\n mean_pupil,\n threshold = 0.1,\n extend_forward = 70,\n extend_back = 70)\n\nggplot(Blink_data, aes(x = time, y = mean_pupil, group = TrialN, color=Event))+\n geom_line(lwd =1.2)+\n geom_vline(aes(xintercept = 0), linetype= 'dashed', color = 'black', lwd =1.2)+\n\n facet_wrap(~Subject)+\n ylim(1,6)+\n \n theme_bw(base_size = 45) +\n theme(\n legend.position = 'bottom', \n legend.title = element_blank()) +\n labs(y = 'Pupil Size')+\n guides(color = guide_legend(override.aes = list(lwd = 20))) \n\n\n\n\n\n\n\nSee !! now the rapid shrinking disappeared and we can now interpolate\n\nInt_data = interpolate_data(data = Blink_data,\n pupil = mean_pupil,\n type = 'linear')\n\nggplot(Int_data, aes(x = time, y = mean_pupil, group = TrialN, color=Event))+\n geom_line(lwd =1.2)+\n geom_vline(aes(xintercept = 0), linetype= 'dashed', color = 'black', lwd =1.2)+\n\n facet_wrap(~Subject)+\n ylim(1,6)+\n \n theme_bw(base_size = 45) +\n theme(\n legend.position = 'bottom', \n legend.title = element_blank()) +\n labs(y = 'Pupil Size')+\n guides(color = guide_legend(override.aes = list(lwd = 20))) \n\n\n\n\n\n\n\nLook how beautiful our signal is now!! 😍 Good job!!!\n\n\n\n\n\n\nCaution\n\n\n\nYou won’t always run into blink issues like this. Downsampling and filtering usually handle rapid changes during earlier preprocessing steps. Whether this happens can depend on the tracker, sampling rate, or even the population you’re testing. In this simulated data, we exaggerated the blink effects on purpose to show you how to spot and fix them! Thus, blink detection may not always be necessary. The best approach is to check your data before deciding. And how do you check it? Plotting! Plotting your signal is the best way to see if blinks are causing rapid drops or if you’re just dealing with missing data. Let the data guide your decisions.\n\n\nDownsample\nAs mentioned before, Pupil dilation is a slow signal, so 20Hz is enough — no need for 300Hz. Downsampling reduces file size, speeds up processing, and even naturally smooths the signal, all while preserving the key information we need for analysis. To downsample to 20Hz, we’ll set the timebin size to 50 ms (since 1000/20 = 0.05 seconds = 50 ms) and calculate the median for each time bin.\n\nNewHz = 20\ntimebinSize = 1000/NewHz\n\nDownsampled_data = downsample_time_data(data = Int_data,\n pupil = mean_pupil,\n timebin_size = timebinSize,\n option = 'median')\nplot(Downsampled_data, pupil = mean_pupil, group = 'condition', geom = 'line') +\n theme_bw(base_size = 45) +\n theme(\n legend.position = 'bottom', \n legend.title = element_blank()) +\n guides(color = guide_legend(override.aes = list(lwd = 20))) \n\n\n\n\n\n\n\nBaseline Correction\nGood job getting this far!!! We’re now at the final step of our pre-processing: baseline correction.\nBaseline correction helps remove variability between trials and participants, like differences in baseline pupil size caused by individual differences, fatigue, or random fluctuations. By doing this, we can focus only on the variability caused by our paradigm. This step ensures that any changes we see in pupil size are truly driven by the experimental conditions, not irrelevant noise. To further adjust the data, we’ll subtract the calculated baseline, ensuring the values start at 0 instead of -100. Finally, to make the next steps of analysis easier, we’ll select only the columns of interest, dropping any irrelevant ones.\nLet’s get it done!\n\nBase_data = baseline_data(data = Downsampled_data,\n pupil = mean_pupil,\n start = -100,\n stop = 0)\n\n# Remove the baseline\nFinal_data = subset_data(Base_data, start = 0) %>% \n select(Subject, Event, TrialN, mean_pupil, time)\n\nLet’s plot it to see what baseline correction and removal are actually doing!! We will plot both the average signal using the plot function (with some addition information about color and theme) and using ggplot to plot the data for each subject separately.\n\nOne = plot(Final_data, pupil = mean_pupil, group = 'condition', geom = 'line')+\n theme_bw(base_size = 45) +\n theme(legend.position = 'none')\n\n\nTwo = ggplot(Final_data, aes(x = time, y = mean_pupil, group = TrialN, color = Event))+\n geom_line(lwd =1.2)+\n geom_vline(aes(xintercept = 0), linetype= 'dashed', color = 'black', lwd =1.2)+\n\n facet_wrap(~Subject)+\n\n theme_bw(base_size = 45) +\n theme(\n legend.position = 'bottom', \n legend.title = element_blank()) +\n labs(y = 'Pupil Size')+\n guides(color = guide_legend(override.aes = list(lwd = 20))) \n\n# Using patchwork to put the plot together\nOne / Two\n\n\n\n\n\n\n\nSave and analysis\nThis tutorial will not cover the analysis of pupil dilation. We’ll stop here since, after baseline correction, the data is ready to be explored and analyzed. From this point on, we’ll shift from pre-processing to analysis, so it’s a good idea to save the data as a simple .csv file for easy access and future use.\n\nwrite.csv(Final_data, \"resources/Pupillometry/Processed/Processed_PupilData.csv\")\n\nThere are multiple ways to analyze pupil data, and we’ll show you some of our favorite methods in a dedicated tutorial: Analyze Pupil Dilation.",
"crumbs": [
"Eye-tracking",
"Pupil data pre-processing"
]
},
{
"objectID": "CONTENT/EyeTracking/PupilPreprocessing.html#cite-pupillometryr",
"href": "CONTENT/EyeTracking/PupilPreprocessing.html#cite-pupillometryr",
"title": "Pupil data pre-processing",
"section": "Cite PupillometryR",
"text": "Cite PupillometryR\nIf you decide to use PupillometryR in your analysis, don’t forget to cite it! Proper citation acknowledges the authors’ work and supports the development of such valuable tools.\n\nCodecitation(\"PupillometryR\")\n\nTo cite PupillometryR in publications use:\n\n Forbes, S. H. (2020). PupillometryR: An R package for preparing and\n analysing pupillometry data. Journal of Open Source Software, 5(50),\n 2285. https://doi.org/10.21105/joss.02285\n\nA BibTeX entry for LaTeX users is\n\n @Article{,\n title = {PupillometryR: An R package for preparing and analysing pupillometry data},\n author = {Samuel H. Forbes},\n journal = {Journal of Open Source Software},\n year = {2020},\n volume = {5},\n number = {50},\n pages = {2285},\n doi = {10.21105/joss.02285},\n }",
"crumbs": [
"Eye-tracking",
"Pupil data pre-processing"
]
},
{
"objectID": "CONTENT/EyeTracking/PupilPreprocessing.html#all-code",
"href": "CONTENT/EyeTracking/PupilPreprocessing.html#all-code",
"title": "Pupil data pre-processing",
"section": "All code",
"text": "All code\nHere below we report the whole code we went trough this tutorial as an unique script to make it easier for you to copy and explore it in it’s entirety.\n\n# Libraries and files --------------------------------------------------------------------\n\nlibrary(PupillometryR) # Library to process pupil signal\nlibrary(tidyverse) # Library to wrangle dataframes\nlibrary(patchwork)\n\ncsv_files = list.files(\n path = \"/resources/Pupillometry/RAW\",\n pattern = \"\\\\.csv$\", # regex pattern to match .csv files\n full.names = TRUE # returns the full file paths\n)\n\n\n# Prepare data --------------------------------------------------------------------\n\n## Event settings --------------------------------------------------------------------\n\n# Settings to cut events\nFs = 300 # framerate\nStep = 1000/Fs\n\nPre_stim = 100 # pre stimulus time (100ms)\nPost_stim = 2000 # post stimulus time (2000ms)\nPre_stim_samples = Pre_stim/Step # pre stimulus in samples\nPost_stim_samples = Post_stim/Step # post stimulus in samples\n\n# Time vector based on the event duration\nTime = seq(from = -Pre_stim, by=Step, length.out = (Pre_stim+Post_stim)/Step) # time vector\n\n\n## Event fixes --------------------------------------------------------------------\n\nList_of_subject_dataframes = list() # Empty list to be filled with dataframes\n\n# Loop for each subject\nfor (sub in 1:length(csv_files)) {\n \n Raw_data = read.csv(csv_files[sub]) # Raw data\n Events = Raw_data %>% filter(Event %in% Events_to_keep) %>% # filter data\n mutate(TrialN = seq(n()))\n \n # Loop for each event \n for (trial in 1:nrow(Events)){\n \n # Extract the information\n Event = Events[trial,]$Event\n TrialN = Events[trial,]$TrialN\n \n # Event onset information\n Onset = Events[trial,]$time\n Onset_index = which.min(abs(Raw_data$time - Onset))\n \n # Find the rows to update based on pre post samples\n rows_to_update = seq(Onset_index - Pre_stim_samples,\n Onset_index + Post_stim_samples-1)\n \n # Repeat the values of interest for all the rows\n Raw_data[rows_to_update, 'time'] = Time\n Raw_data[rows_to_update, 'Event'] = Event\n Raw_data[rows_to_update, 'TrialN'] = TrialN\n }\n \n \n # Filter only events of interest\n Trial_data = Raw_data %>% \n filter(Event %in% Events_to_keep)\n \n # Add daframe to list\n List_of_subject_dataframes[[sub]] = Trial_data\n}\n\n# Combine the list of dataframes into 1 dataframe\nTrial_data = bind_rows(List_of_subject_dataframes)\n\n\n### Plot Raw Data -----------------------------------------------------------------\n\nggplot(Raw_data, aes(x = time, y = R_P)) +\n geom_line(aes(y = R_P, color = 'Pupil Right'), lwd = 1.2) +\n geom_line(aes(y = L_P, color = 'Pupil Left'), lwd = 1.2) +\n geom_vline(data = Raw_data |> dplyr::filter(!is.na(Event)), aes(xintercept = time, linetype = Event), lwd = 1.3) +\n \n theme_bw(base_size = 45) +\n ylim(1, 6) +\n labs(color= 'Signal', y='Pupil size')+\n scale_color_manual(\n values = c('Pupil Right' = '#4A6274', 'Pupil Left' = '#E2725A') ) +\n theme(\n legend.position = 'bottom' ) +\n guides(\n color = guide_legend(override.aes = list(lwd = 20)),\n linetype = guide_legend(override.aes = list(lwd = 1.2))\n )\n\n\n\n# Pre-processing -----------------------------------------------------------------\n\n## Filter Out Trials with all NA -----------------------------------------------------------------\n\nTrial_data = Trial_data %>%\n group_by(Subject, TrialN) %>%\n filter(!all(is.na(L_P) & is.na(R_P))) %>%\n ungroup()\n\n\n## Make PupillometryR Data -----------------------------------------------------------------\nPupilR_data = make_pupillometryr_data(data = Trial_data,\n subject = Subject,\n trial = TrialN,\n time = time,\n condition = Event)\n\n### Plot ------------------------------------------------------------------\nplot(PupilR_data, pupil = L_P, group = 'condition', geom = 'line') +\n theme_bw(base_size = 45) +\n theme(\n legend.position = 'bottom', \n legend.title = element_blank()) +\n guides(color = guide_legend(override.aes = list(lwd = 20))) \n\n\n## Regress Data -----------------------------------------------------------------\n\nRegressed_data = regress_data(data = PupilR_data,\n pupil1 = L_P,\n pupil2 = R_P)\n\n\n## Calculate Mean Pupil -----------------------------------------------------------------\n\nMean_data = calculate_mean_pupil_size(data = Regressed_data, \n pupil1 = L_P, \n pupil2 = R_P)\n\n### Plot --------------------------------------------------------------------\n\nplot(Mean_data, pupil = mean_pupil, group = 'condition', geom = 'line')+\n theme_bw(base_size = 45) +\n theme(\n legend.position = 'bottom', \n legend.title = element_blank()) +\n guides(color = guide_legend(override.aes = list(lwd = 20))) \n\n\n## Lowpass Filter -----------------------------------------------------------------\n\nfiltered_data = filter_data(data = Mean_data,\n pupil = mean_pupil,\n filter = 'median',\n degree = 11)\n\n### Plot --------------------------------------------------------------------\n\nplot(filtered_data, pupil = mean_pupil, group = 'condition', geom = 'line')+\n theme_bw(base_size = 45) +\n theme(\n legend.position = 'bottom', \n legend.title = element_blank()) +\n guides(color = guide_legend(override.aes = list(lwd = 20))) \n\n\n## Downsample -----------------------------------------------------------------\n\nNewHz = 20\n\ntimebinSize = 1 / NewHz\n\nDownsampled_data = downsample_time_data(data = filtered_data,\n pupil = mean_pupil,\n timebin_size = timebinSize,\n option = 'median')\n\n# Plot --------------------------------------------------------------------\n\nplot(Downsampled_data, pupil = mean_pupil, group = 'condition', geom = 'line') +\n theme_bw(base_size = 45) +\n theme(\n legend.position = 'bottom', \n legend.title = element_blank()) +\n guides(color = guide_legend(override.aes = list(lwd = 20))) \n\n\n## Calculate Missing Data -----------------------------------------------------------------\n\nMissing_data = calculate_missing_data(Downsampled_data, mean_pupil)\n\n\n## Clean Missing Data -----------------------------------------------------------------\n\nClean_data = clean_missing_data(Downsampled_data,\n pupil = mean_pupil,\n trial_threshold = .75,\n subject_trial_threshold = .75)\n\n### Plot --------------------------------------------------------------------\n\nggplot(Clean_data, aes(x = Time, y = mean_pupil, group = TrialN, color= Event))+\n geom_line( lwd =1.2)+\n geom_vline(aes(xintercept = 0), linetype= 'dashed', color = 'black', lwd =1.2)+\n \n facet_wrap(~Subject)+\n ylim(1,6)+\n \n theme_bw(base_size = 45) +\n theme(\n legend.position = 'bottom', \n legend.title = element_blank()) +\n guides(color = guide_legend(override.aes = list(lwd = 20))) \n\n\n## Detect Blinks -----------------------------------------------------------------\n\nBlink_data = detect_blinks_by_velocity(\n Clean_data,\n mean_pupil,\n threshold = 0.1,\n extend_forward = 50,\n extend_back = 50)\n\n### Plot --------------------------------------------------------------------\n\nggplot(Blink_data, aes(x = time, y = mean_pupil, group = TrialN, color=Event))+\n geom_line(lwd =1.2)+\n geom_vline(aes(xintercept = 0), linetype= 'dashed', color = 'black', lwd =1.2)+\n \n facet_wrap(~Subject)+\n ylim(1,6)+\n \n theme_bw(base_size = 45) +\n theme(\n legend.position = 'bottom', \n legend.title = element_blank()) +\n guides(color = guide_legend(override.aes = list(lwd = 20))) \n\n\n## Interpolate Data -----------------------------------------------------------------\n\nInt_data = interpolate_data(data = Clean_data,\n pupil = mean_pupil,\n type = 'linear')\n\n### Plot --------------------------------------------------------------------\n\nggplot(Int_data, aes(x = Time, y = mean_pupil, group = TrialN, color = Event))+\n geom_line(lwd =1.2)+\n geom_vline(aes(xintercept = 0), linetype= 'dashed', color = 'black', lwd =1.2)+\n\n facet_wrap(~Subject)+\n ylim(1,6)+\n \n theme_bw(base_size = 45) +\n theme(\n legend.position = 'bottom', \n legend.title = element_blank()) +\n guides(color = guide_legend(override.aes = list(lwd = 20)))\n\n\n# Baseline correction -----------------------------------------------------\n\nBase_data = baseline_data(data = Int_data,\n pupil = mean_pupil,\n start = -100,\n stop = 0)\n\n# Remove the baseline\nFinal_data = subset_data(Base_data, start = 0)\n\n\n### Final plot --------------------------------------------------------------\n\nOne = plot(Final_data, pupil = mean_pupil, group = 'condition')+\n theme_bw(base_size = 45) +\n theme(legend.position = 'none')\n\n\nTwo = ggplot(Final_data, aes(x = time, y = mean_pupil, group = TrialN, color = Event))+\n geom_line(lwd =1.2)+\n geom_vline(aes(xintercept = 0), linetype= 'dashed', color = 'black', lwd =1.2)+\n \n facet_wrap(~Subject)+\n \n theme_bw(base_size = 45) +\n theme(\n legend.position = 'bottom', \n legend.title = element_blank()) +\n guides(color = guide_legend(override.aes = list(lwd = 20))) \n\nOne / Two\n\n\n\n# Save data ---------------------------------------------------------------\n\nwrite.csv(Final_data, \"resources/Pupillometry/Processed/Peocessed_PupilData.csv\")",
"crumbs": [
"Eye-tracking",
"Pupil data pre-processing"
]
},
{
"objectID": "CONTENT/EyeTracking/Intro_eyetracking.html",
"href": "CONTENT/EyeTracking/Intro_eyetracking.html",
"title": "Introduction to eye-tracking",
"section": "",
"text": "Eye tracking is a great tool to study cognition. It is especially suitable for developmental studies, as infants and young children might have advanced cognitive abilities, but little chances to show them (they cannot talk!).\nAcross the following tutorials, we will go through you all you need to navigate the huge and often confusing eye-tracking world. First, we will introduce how an experimental design can (and should) be built. Then, we will explain how to implement the design in Python, connect it to an eye-tracker, and record eye-tracking data. Once the data is collected, we will focus on how to analyse the data, reducing the seemingly overwhelming amount of rows and columns in a few variables of interest (such as saccadic latency, looking time, or pupil dilation).",
"crumbs": [
"Eye-tracking"
]
},
{
"objectID": "CONTENT/EyeTracking/Intro_eyetracking.html#what-do-you-want-to-measure",
"href": "CONTENT/EyeTracking/Intro_eyetracking.html#what-do-you-want-to-measure",
"title": "Introduction to eye-tracking",
"section": "What do you want to measure?",
"text": "What do you want to measure?\nGreat, we have a theory (Curiosity is a drive to learn) and a specific hypothesis to test it (When stimuli offer a learning opportunity, infants should be more motivated to predict where they will appear). Now we need specific metrics which will allow us to test our hypotheses.\nFirst of all, we should have some measure that actually checks whether infants spend more time engaging with a complex stimulus vs a simple one. For this, we might use the looking time to the stimuli.\nIf this “background check” is passed, and infants actually engage more with the complex stimuli, we might then try to find a more direct answer to our hypothesis: Will they be more motivated to predict where the complex stimulus will appear? This hypothesis can be broken into two components: We have a learning component (predict where the stimulus will appear) and a motivational component (more motivated to do so). For these two components, we might need different measures.\nFor the learning component, we can rely on saccadic latency. Saccadic latency measures the speed at which infants look at the target stimulus once it is presented. When infants learn the location of a stimulus, they become faster and faster at predicting it. Sometimes, they might even look at its location before the stimulus appears! This would be a correct prediction, which is indicative of successful learning.\nFor the motivational component, we could use pupil size. Pupil size measures many different things, including arousal. If a stimulus is more engaging, arousal should increase. In this specific case, we might observe greater pupil size in trials with complex stimuli. Pupil size is a complicated measure to use, so for now we will not get into more details. Let’s just agree that pupil is great to understanding not only learning, but also motivation.\nThis example paradigm allows us to understand how to start from general theories, make testable hypotheses, and finally narrow down how these hypotheses can be tested empirically. In our case, given our hypotheses and what kind of measures might allow us to test them, eye-tracking seems to be perfect for us! It would allow us to simply collect gaze and pupil data, which underlie all the metrics we mentioned above. Remember, usually we start from the theory and hypotheses, and then we pick the method (for example eye-tracking, EEG, fNIRS, and so on) that would be the most helpful in testing our hypotheses, and not the other way around!\nLooking time\nWe saw how looking time could be a good measure of interest in a stimulus, but what exactly looking time measures depends on the experimental paradigm. Classic paradigms on infant research, such as Habituation and Violation of Expectations, rely on looking time as main measure.\nIn Habituation paradigms, infants are presented with the same stimulus (e.g. a cat) over and over again. Usually, the looking time to the stimulus decreases across repeated presentations. The number of repeated presentations can be fixed (e.g., 10 for all infants) or dependent on the infant looking (e.g., when looking time to the last 3 stimulus presentations combined has fallen by 50% compared to the first three stimulus presentations). Afterwards, a different stimulus is presented (e.g., a dog). If infants have no ability to discriminate cats from dogs, they will not notice the change and will look at the new stimulus the same amount, or even less. If, however, infants can discriminate cats from dogs, they will be more interested in the new stimulus, and will look longer at the dog. Habituation can be helpful to understand what kind of internal representations infants already have, and which ones still need to develop.\n\n\n\n\n\n\nWarning\n\n\n\nIn the example above, we can conclude that infants can distinguish dogs from cats only if we control for many alternative explanations and chose the stimuli carefully. In reality, all we know with habituation paradigms is whether infants can spot any difference between the images. If for example the dog image is bigger, or it has different colors, it might be that infants are reacting to changes in size or color rather than animal category! Always chose your stimuli carefully.\nCan you spot the problem here?😁\n\n\n\n\n\n\nAnother common paradigm using looking time is the Violation of Expectations paradigm. Here the underlying logic is a bit different from the Habituation paradigm: Infants might be presented with an expected event (a ball falling) or an unexpected event (a ball floating in mid air). If infants’ expectations are violated, they will be surprised, which will lead to an increase in looking time.\nYou can already see how changing the paradigm changes what looking time measures: Novelty detection in the first case, surprise in the second. However, more broadly, we (and infants) tend to look more at something if we care more about it. In more scientific terms, what we observe is the focus of our “overt” attention, and receives preferential processing. What exactly attracts our overt attention might then depend on the paradigm (a more novel item, a more surprising one, a scarier one even).\n\n\n\n\n\n\nImportant\n\n\n\nWith looking time, we can conclude that infants decided to allocate more attention at something, but not why! Was it because the stimulus newer? More surprising? Scarier? More soothing? Be careful about what kind of conclusions you draw when you observe a difference in looking times between different stimuli or conditions!\n\n\nSaccadic Latency\nAnother very popular eye-tracking measure is saccadic latency. It measures how quickly infants can direct their gaze onto a stimulus or event. For example, infants might be presented with the video of a person grabbing a mug and bringing it to their mouth (predictable event) or their ear (unpredictable event). The key event we are interested in is the moment the mug makes contact with the body of the person (either the mouth or the ear). If infants have learned that mugs are usually brought to the mouth, they will quickly direct their gaze (saccade) towards the person’s mouth, irrespective of the type of event (predictable vs unpredictable). If infants cannot make this prediction yet, they will wait to see where the mug goes before making a saccade. When saccadic latency is so fast that it occurs before the event (that is, the mug touching the body), the resulting look (also called fixation) is called anticipatory look.\nInfants (just like adults) are trying to predict what will happen next all the time. By looking at the speed of saccadic latencies, we can get an insight onto what their predictions are (where did they look?) and how strong the predictions are (how quickly did they look?).\n\n\n\n\n\n\nWarning\n\n\n\nWhile anticipatory looks give us a window on what infants prediction are, they are not always reliable. For example, a very cool study has found that infants make predictions when the outcome is probabilistic but not when it is deterministic. Keep this in mind when planning your next study!\n\n\nIn our new experimental paradigm about infant curiosity, stimuli are presented over and over in two locations (the simple stimulus on the left, the complex one on the right). Here, we expect infants to get faster and faster at looking at the stimuli (that is, saccadic latencies will get shorter and shorter). For example, at the beginning infants might look at the stimuli +500 milliseconds after they have been presented. As they learn, saccadic latency might reduce (for example, to +100 milliseconds) even to the point that they happen before the stimulus is presented (-200 milliseconds). While the last example is clearly an anticipatory look (they looked before the stimulus was even there!), here’s something that might come as a surprise: the second example (+100 milliseconds) is also an anticipatory look! This might seem counterintuitive at first—how can looking after something appears be anticipatory? But when we think more about it, here’s what’s happening: it takes around 200 milliseconds for adults to plan a saccade (and even more for infants!), so if a saccade happens at +100 milliseconds after stimulus presentation, the brain actually started planning it 200 milliseconds before that—which means at -100 milliseconds, before the stimulus even appeared. This one is also anticipatory!\n\n\n\n\n\n\nWarning\n\n\n\nSaccadic latencies are not a perfect measure of learning. Infants might be faster at looking at something just because they are more interested (pick interesting stimuli to keep them engaged!), and they might become slower due to boredom or fatigue (have some variation in the stimuli, so that they do not get as boring over time!).\n\n\nPupillometry\nMost eye-trackers do not only track the position of the eyes on the screen (gaze data) but also the size of the pupil. In our opinion, pupil size is the most fascinating, the coolest, and possibly the most misunderstood eye-tracking measure. Pupil size changes depending on the light (it gets smaller when light is more intense) to help us see better. However, it changes more subtly also depending on cognitive processes. When lighting conditions are kept stable, it is much easier to catch these subtler processes.\n\n\n\n\n\n\nImportant\n\n\n\nThe fact that changes in pupil size due to cognitive processes are subtler than overall changes due to light has led to the common misconception that pupil data are noisy and unreliable. However, pupil size is a measure with a high signal to noise ratio. This means that if we do everything right, we can get really good data with very little noise. In our experience, pupil size is usually more reliable than gaze data (looking time, saccadic latency, and so on), but it requires some additional thought both in building the experimental paradigm and in processing the data. But don’t worry, our tutorials will guide you through everything!\n\n\nSo, what does pupil size measure? Again, it depends on the task. Generally speaking, we have to make the distinction between tonic pupil size and phasic pupil dilation, because they measure different things. Tonic pupil size is simply the size of the pupil at any moment in time - even better if measured when nothing much is happening on the screen. Phasic pupil dilation is a sudden change in pupil size due to the presentation of a certain stimulus or event.\nGreater tonic pupil size has been associated with heightened arousal. Again, many things might impact arousal (how interesting, scary, difficult or uncertain a stimulus or situation is). In contrast, phasic or transient pupil responses to task-relevant unexpected events are more specifically linked to prediction-error processing and the subsequent updating of internal beliefs.\nHere an image that tries to show you the different components of the pupil dilation and how they are part of the pupil signal\n\n\n\n\nPupil signal and its components\n\n\n\n\n\n\n\n\n\nNote\n\n\n\nTonic and phasic pupil signals map onto the tonic and phasic firing modes of the locus coeruleus, which are thought to regulate sustained arousal and vigilance (tonic mode) and rapid, event-driven shifts in attention or learning (phasic mode) through noradrenergic transmission. So the neural correlates of pupillometry are surprisingly clear!\n\n\nEarlier on, we said pupil data are really good if we do everything right. One thing we have to do right is to think through how to create stimuli that keep the luminance constant across conditions, otherwise we might observe differences in pupil size due to changes in light rather than due to our experimental conditions. For this reason, in our curiosity paradigm we decided to have two cue stimuli (circle and square) with exactly the same area. The preceding fixation cross also has the same area! So we are good to go!\n\n\n\n\n\n\nSome practical recommendations for successful pupillometry\n\n\n\nControl stimulus properties: Present all stimuli of interest in the same location on the screen, as pupil size changes depending on screen location (it gets smaller away from the centre).\nAllow sufficient time: Even phasic changes in pupil size, which are the fast ones, are still relatively slow (1-3 seconds), so trials have to be a little slower to give the pupil its time to shine.\nInclude baseline periods: Often, a fixation cross has to precede the moment in which pupil dilation is measured, so that pupil size can return to baseline before the event you care about happens.\nWith all these things in mind, and by having a look at other examples, good luck with using this super cool measure!",
"crumbs": [
"Eye-tracking"
]
},
{
"objectID": "CONTENT/EyeTracking/FromFixationsToData.html",
"href": "CONTENT/EyeTracking/FromFixationsToData.html",
"title": "From fixations to measures",
"section": "",
"text": "In the previous two tutorials we collected some eye-tracking data and then we used I2MC to extract the fixations from that data. Let’s load the data we recorded and pre-processed in the previous tutorial. We will import some libraries and read the raw data and the output from I2MC.\nimport os\nfrom pathlib import Path\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport matplotlib.patches as patches\n\n#%% Settings\n\n# Screen resolution\nscreensize = (1920, 1080) \n\n#%% Settings and reading data\n\n# Define paths using pathlib\nbase_path = Path('resources/FromFixationToData/DATA')\n\n# The fixation data extracted from I2MC\nFixations = pd.read_csv(base_path / 'i2mc_output' / 'Adult1' / 'Adult1.csv')\n\n# The original RAW data\nRaw_data = pd.read_hdf(base_path / 'RAW' / 'Adult1.h5', key='gaze')\nWhat can we do with just the raw data and the fixations? Not much I am afraid. But we can use these fixations to extract some more meaningful indexes.\nIn this tutorial, we will look at how to extract two variables from our paradigm:\nSo what do these two measures have in common? pregnant pause for you to answer EXACTLY!!! They are both clearly related to the position of our stimuli. For this reason, it is important to define Areas Of Interest (AOIs) on the screen (for example, the locations of the targets). Defining AOIs will allow us to check, for each single fixation, whether it happened in an area that we are interested in.",
"crumbs": [
"Eye-tracking",
"From fixations to measures"
]
},
{
"objectID": "CONTENT/EyeTracking/FromFixationsToData.html#define-aois",
"href": "CONTENT/EyeTracking/FromFixationsToData.html#define-aois",
"title": "From fixations to measures",
"section": "Define AOIs",
"text": "Define AOIs\nLet’s define AOIs. We will define two squares around the target locations. To do this, we can simply pass two coordinates for each AOI: the lower left corner and the upper right corner of an imaginary square.\nAn important point to understand is that tobii and Psychopy use two different coordinate systems:\n\nPsychopy has its origin (0,0) in the centre of the window/screen by default.\nTobii reports data with its origin (0,0) in the lower left corner.\n\nThis inconsistency is not a problem per se, but we need to take it into account when defining the AOIs. Let’s try to define the AOIs:\n\n# Screen resolution\nscreensize = (1920, 1080)\n\n# Define variables related to AOIs and target position\ndimension_of_AOI = 600/2 # half-width of the square\nTarget_position = 500 # distance from center (pixels)\n\n# Create Areas of Interest (AOIs)\n# Format: [[bottom_left_x, bottom_left_y], [top_right_x, top_right_y]]\n\n# AOI 1: Left Target (Negative X)\n# Center is (-500, 0)\nAOI1 = [\n [-Target_position - dimension_of_AOI, -dimension_of_AOI], # Bottom-Left\n [-Target_position + dimension_of_AOI, dimension_of_AOI] # Top-Right\n] \n\n# AOI 2: Right Target (Positive X)\n# Center is (+500, 0)\nAOI2 = [\n [Target_position - dimension_of_AOI, -dimension_of_AOI], # Bottom-Left\n [Target_position + dimension_of_AOI, dimension_of_AOI] # Top-Right\n]\n\nAOIs = [AOI1, AOI2]\n\nNice!! This step is essential. We have created two AOIs. We will use them to define whether each fixation of our participant was within either of these two AOIs. Let’s get a better idea by just plotting these two AOIs and two random points (600, 500) and (1400,1000).\n\n\nCode\nimport matplotlib.pyplot as plt\nimport matplotlib.patches as patches\n\n# Create a figure\nfig, ax = plt.subplots(1, figsize=(8, 4.4))\n\n# Set the limits of the plot (Center origin!)\n# X: -960 to 960\n# Y: -540 to 540\nax.set_xlim(-screensize[0]/2, screensize[0]/2)\nax.set_ylim(-screensize[1]/2, screensize[1]/2)\n\n# Define the colors for the rectangles\ncolors = ['#46AEB9', '#C7D629']\n\n# Add axes lines to show the center (0,0) clearly\nax.axhline(0, color='gray', linestyle='--', alpha=0.5)\nax.axvline(0, color='gray', linestyle='--', alpha=0.5)\n\n# Create a rectangle for each area of interest and add it to the plot\nfor i, (bottom_left, top_right) in enumerate(AOIs):\n width = top_right[0] - bottom_left[0]\n height = top_right[1] - bottom_left[1]\n \n rectangle = patches.Rectangle(\n bottom_left, width, height, \n linewidth=2, edgecolor='k', facecolor=colors[i], alpha=0.6\n )\n ax.add_patch(rectangle)\n\n# Plot random points to test (e.g., one inside Left AOI, one inside Right AOI)\n# Note: These coordinates are now relative to center!\n# (-600, 0) is inside the Left AOI\n# (600, 0) is inside the Right AOI\nax.plot(-500, 0, marker='o', markersize=8, color='white', label='Left Target Center')\nax.plot(500, 0, marker='o', markersize=8, color='white', label='Right Target Center')\n\n# Show the plot\nplt.title(\"AOIs\")\nplt.show()",
"crumbs": [
"Eye-tracking",
"From fixations to measures"
]
},
{
"objectID": "CONTENT/EyeTracking/FromFixationsToData.html#points-in-aois",
"href": "CONTENT/EyeTracking/FromFixationsToData.html#points-in-aois",
"title": "From fixations to measures",
"section": "Points in AOIs",
"text": "Points in AOIs\nAs you can see from our plot, we have two defined Areas of Interest (AOIs). Visually, it is obvious which points would fall inside the boxes and which do not. But how do we get Python to determine this mathematically?\nThe logic is actually quite simple. We just need to check if the point’s coordinates lie within the boundaries of the rectangle:\n\nIs the point’s X between the rectangle’s Left and Right edges?\nIs the point’s Y between the rectangle’s Bottom and Top edges?\n\nHere is the basic logic for checking a single area:\n\n\n\n\n\nSo imagine we have a point: point and an area: area, we can check if the point falls inside the area by:\n\n# 1. Extract the rectangle boundaries\nbottom_left, top_right = area\nmin_x, min_y = bottom_left\nmax_x, max_y = top_right\n\n# 2. Extract the gaze point coordinates\nx, y = point\n\n# 3. Check if the point is inside boundaries\nis_inside = (min_x <= x <= max_x) and (min_y <= y <= max_y)\n\nPerfect, this will return True if the point falls inside the area and False if it falls outside.\n\nChecking Multiple AOIs\nSince we have two targets (Left and Right), simply knowing “True/False” isn’t enough. We need to know which specific AOI the participant was looking at.\nTo do this, we will build a function called find_area_for_point. Instead of returning a boolean, this function will return an index:\n\n0 if the point is in the first AOI (Left Target).\n1 if the point is in the second AOI (Right Target).\n-1 if the point is essentially “nowhere” (looking at the background).\n\nWe achieve this using Python’s enumerate function.\n\n\n\n\n\n\nMore details\n\n\n\n\n\nNormally, looping through a list (for area in areas) gives you the items one by one but forgets their position. enumerate solves this by giving us two things simultaneously:\n\nThe Index (i): The ID number of the item (0, 1, 2…).\nThe Item (area): The actual coordinates of the AOI.\n\nThis is perfect for us: we use the Item to do the math (checking if the point is inside), and if it is, we return the Index so we know which target was looked at.\n\n\n\n\n# We define a function that simply takes the a point and a list of areas.\n# This function checks in which area this point is and return the index\n# of the area. If the point is in no area it returns -1\ndef find_area_for_point(point, areas):\n \"\"\"\n Checks which AOI a point falls into.\n Returns the index of the AOI, or -1 if the point is outside all areas.\n \"\"\"\n for i, area in enumerate(areas):\n # Extract boundaries\n bottom_left, top_right = area\n min_x, min_y = bottom_left\n max_x, max_y = top_right\n \n # Extract point\n x, y = point\n \n # Check boundaries\n if (min_x <= x <= max_x) and (min_y <= y <= max_y):\n return i\n \n # If the loop finishes without returning, the point is outside all AOIs\n return -1\n\nNow we have a cool function to check whether a point falls into any of our AOIs. We can use this function to filter the fixations that are in the AOIs: These are the only ones we care about.",
"crumbs": [
"Eye-tracking",
"From fixations to measures"
]
},
{
"objectID": "CONTENT/EyeTracking/FromFixationsToData.html#first-fixation",
"href": "CONTENT/EyeTracking/FromFixationsToData.html#first-fixation",
"title": "From fixations to measures",
"section": "First fixation",
"text": "First fixation\nWe now have a list of all correct fixations, but often there are multiple fixations within a single trial (e.g., the participant looks at the target, looks away, then looks back).\nFor Saccadic Latency, we care only about the first time they looked at the target. That is their reaction time.\nHow do we isolate just that one specific row for every trial? We use the powerful groupby function in pandas.\ngroupby allows us to split our dataframe into small chunks (one chunk per Trial), find the minimum value in each chunk (the smallest Latency = the earliest fixation), and glue it all back together.\n\n# Group the data by 'Events' (Condition) and 'TrialN' (Trial Number)\n# Then find the minimum (.min) Latency in each group\nSaccadicLatency = Correct_Target_fixations.groupby(['Events', 'TrialN'])['Latency'].min().reset_index()\n\nMission Accomplished! You now have a clean dataframe containing exactly one latency value per trial.\nFinally, let’s see what our data looks like! We can use the seaborn library to create a beautiful scatterplot showing how saccadic latency changes across the trials.\n\nimport seaborn as sns\nimport matplotlib.pyplot as plt\n\nplt.figure(figsize=(8, 6))\n\n# Create the scatterplot\n# x = Trial Number\n# y = Latency (Reaction Time)\n# hue = Color points differently based on the Event (Simple vs Complex)\nax = sns.scatterplot(\n data=SaccadicLatency, \n x=\"TrialN\", \n y=\"Latency\", \n hue='Events', \n style='Events', # Different shapes for different events\n s=100 # Make the dots bigger\n)\n\n# Move the legend to a clean spot\nsns.move_legend(ax, \"upper left\", bbox_to_anchor=(1, 1))\n\nplt.title(\"Saccadic Latency across Trials\")\nplt.ylabel(\"Latency (ms)\")\nplt.xlabel(\"Trial Number\")\nplt.tight_layout()\n\n# Show the plot\nplt.show()",
"crumbs": [
"Eye-tracking",
"From fixations to measures"
]
},
{
"objectID": "CONTENT/EyeTracking/CreateAnEyetrackingExperiment.html",
"href": "CONTENT/EyeTracking/CreateAnEyetrackingExperiment.html",
"title": "Create an eye-tracking experiment",
"section": "",
"text": "This page will show you how to collect eye-tracking data in a simple PsychoPy paradigm. We will build upon the exact same paradigm we created in the Getting started with Psychopy tutorial. If you have not completed that tutorial yet, please go through it first, as we will be modifying that existing code.",
"crumbs": [
"Eye-tracking",
"Create an eye-tracking experiment"
]
},
{
"objectID": "CONTENT/EyeTracking/CreateAnEyetrackingExperiment.html#preparation",
"href": "CONTENT/EyeTracking/CreateAnEyetrackingExperiment.html#preparation",
"title": "Create an eye-tracking experiment",
"section": "Preparation",
"text": "Preparation\nLet’s begin importing the libraries that we will need for this example\n\nimport os\nfrom pathlib import Path\nfrom psychopy import core, event, visual,sound\nfrom DeToX import ETracker\n\nMost of these imports should look familiar from our previous tutorial—we need them to locate files, handle timing, and present our stimuli.\nThe new addition here is ETracker. This is DeToX’s main class and acts as your central hub for all eye-tracking operations. Throughout your experiment, you will interact with this single object to handle everything from calibration to data recording.\n\nWindow\nAs we have seen in previous tutorials, every PsychoPy experiment needs a window. This is the canvas where all your stimuli will appear and where participants will interact with your study.\nCrucially, the ETracker class requires this window object to function properly. It uses the window’s properties (like size and unit system) to correctly map eye-tracking coordinates to your screen.\nLet’s create one now:\n\n# Create the experiment window\nwin = visual.Window(\n size=[1920, 1080], # Window dimensions in pixels\n fullscr=True, # Expand to fill the entire screen\n units='pix' # Use pixels as the measurement unit\n)",
"crumbs": [
"Eye-tracking",
"Create an eye-tracking experiment"
]
},
{
"objectID": "CONTENT/EyeTracking/CreateAnEyetrackingExperiment.html#connect-to-the-eye-tracker",
"href": "CONTENT/EyeTracking/CreateAnEyetrackingExperiment.html#connect-to-the-eye-tracker",
"title": "Create an eye-tracking experiment",
"section": "Connect to the eye-tracker",
"text": "Connect to the eye-tracker\nWith our window prepared, the next step is to establish a connection to the hardware. Unlike standard SDKs that require you to manually search for devices and manage data streams, DeToX streamlines this process through a single main controller: the ETracker.\nTo initialize it, simply pass your PsychoPy window object. DeToX will automatically locate the first available Tobii tracker and configure the coordinate systems to match your screen settings.\n\nET_controller = ETracker(win)\n\n\n\n\n\n\n\nDon’t Have an Eye Tracker? No Problem!\n\n\n\nIf you’re following along without a Tobii eye tracker connected, you can still test everything using simulation mode. Just pass simulate=True when creating your ETracker:\n\nET_controller = ETracker(win, simulate=True)\n\nThis tells DeToX to collect data from your mouse position instead of an actual eye tracker—perfect for development, testing, or learning the workflow before you have hardware access 😉\n\n\nUpon execution, DeToX connects to the device and prints a confirmation summary. This is a quick way to verify that your tracker is detected and running at the correct frequency:\n┌────────────────── Eyetracker Info ──────────────────┐\n│Connected to the eyetracker: │\n│ - Model: Tobii Pro Fusion │\n│ - Current frequency: 250.0 Hz │\n│ - Current illumination mode: Default │\n│Other options: │\n│ - Possible frequencies: (30.0, 60.0, 120.0, 250.0) │\n│ - Possible illumination modes: ('Default',) │\n└─────────────────────────────────────────────────────┘\nNow that we are connected, we are ready to start recording!",
"crumbs": [
"Eye-tracking",
"Create an eye-tracking experiment"
]
},
{
"objectID": "CONTENT/EyeTracking/CreateAnEyetrackingExperiment.html#collect-data",
"href": "CONTENT/EyeTracking/CreateAnEyetrackingExperiment.html#collect-data",
"title": "Create an eye-tracking experiment",
"section": "Collect data",
"text": "Collect data\nGreat! You’re now connected to the eye-tracker (or simulating it). However, we’re not actually collecting any data yet—let’s fix that.\nTo begin data collection, simply call the start_recording method on your controller:\n\n# Start recording data\nET_controller.start_recording(filename=\"testing.h5\")\n\nThis sets everything in motion. The eye-tracker will now continuously collect data in the background while you run your experiment.\nWe use the HDF5 format (ending in .h5), which is a modern and efficient way to store scientific data. It keeps everything organized and fast, so you don’t have to worry about managing massive, messy text files.\nIf you don’t provide a filename, DeToX will automatically generate one for you based on the current time. For more details on the data structure, check the DeToX website.",
"crumbs": [
"Eye-tracking",
"Create an eye-tracking experiment"
]
},
{
"objectID": "CONTENT/EyeTracking/CreateAnEyetrackingExperiment.html#triggersevents",
"href": "CONTENT/EyeTracking/CreateAnEyetrackingExperiment.html#triggersevents",
"title": "Create an eye-tracking experiment",
"section": "Triggers/Events",
"text": "Triggers/Events\nWe have successfully started recording, and data is now being collected continuously in the background. Now you are free to present images, videos, sounds, or whatever your experimental design requires!\nHowever, presenting stimuli is only half the battle. While the eye tracker records where the participant is looking, it is crucial to mark when specific events happen (e.g., “Image appeared”, “Sound started”).\nWithout these markers (or “triggers”), your data will just be a long, unbroken stream of coordinates, making it impossible to determine what the participant was looking at at any given moment. This synchronization is essential for analysis.\n\nHow to Record an Event\nTo mark a specific moment in time, simply call the record_event function. DeToX automatically captures the precise system timestamp and merges it into your data file.\nYou should call this function right after your window flips. Since win.flip() is the moment the stimulus actually appears on the screen, recording the event right after ensures your timestamp is as accurate as possible.\n\n# Send event\nET_controller.record_event('Event number 1')",
"crumbs": [
"Eye-tracking",
"Create an eye-tracking experiment"
]
},
{
"objectID": "CONTENT/EyeTracking/CreateAnEyetrackingExperiment.html#save-data",
"href": "CONTENT/EyeTracking/CreateAnEyetrackingExperiment.html#save-data",
"title": "Create an eye-tracking experiment",
"section": "Save data",
"text": "Save data\nWhile the recording is active, your data (and events) are held in your computer’s short-term memory (RAM) for speed. To make this data permanent, it must be written to a file on your hard drive.\nThere are two ways to do this:\n\nThe Standard Way: Save at the End\nThe simplest approach is to save everything once your experiment finishes. When you are done, simply call:\n\n# Stop recording and save everything\nET_controller.stop_recording()\n\nThis function performs three critical tasks at once:\n\nStops the data stream from the eye tracker.\nSaves all data currently in memory to your file.\nSafely disconnects from the device.\n\nYou will see a confirmation message summarizing the session:\n┌────────────── Recording Complete ───────────────┐\n│Data collection lasted approximately 4.02 seconds│\n│Data has been saved to testing.h5 │\n└─────────────────────────────────────────────────┘\n\n\nThe “Safe” Way: Periodic Saving\nIf your experiment is short, saving at the end is perfectly fine. However, for longer studies, we highly recommend saving intermittently.\nIf your computer were to crash halfway through a long session, you would lose all the data currently sitting in memory. To prevent this, you can “flush” the data to the disk during quiet moments, such as an Inter-Stimulus Interval (ISI) or a break.\nSimply call this method whenever you want to secure the data collected so far:\n\n# Append current data to file and clear memory\nET_controller.save_data()\n\nThis takes whatever is in memory, appends it to your file, and clears the buffer to free up RAM.\n\n\n\n\n\n\nNote\n\n\n\nNote: Even if you use save_data() periodically, you must still call stop_recording() at the very end of your experiment to save the final chunk of data and disconnect from the eye-tracker.",
"crumbs": [
"Eye-tracking",
"Create an eye-tracking experiment"
]
},
{
"objectID": "CONTENT/EyeTracking/CreateAnEyetrackingExperiment.html#short-recap-of-the-paradigm",
"href": "CONTENT/EyeTracking/CreateAnEyetrackingExperiment.html#short-recap-of-the-paradigm",
"title": "Create an eye-tracking experiment",
"section": "Short Recap of the Paradigm",
"text": "Short Recap of the Paradigm\nWe’ll use the experimental design from Getting started with PsychoPy and add eye tracking to it. If you need a refresher on the paradigm, take a quick look at that tutorial.\nHere’s a brief summary: After a fixation cross, participants see either a circle or square. The circle predicts a complex shape that will appear on the right side of the screen, while the square predicts a simple shape will on the left.",
"crumbs": [
"Eye-tracking",
"Create an eye-tracking experiment"
]
},
{
"objectID": "CONTENT/EyeTracking/CreateAnEyetrackingExperiment.html#putting-it-all-together",
"href": "CONTENT/EyeTracking/CreateAnEyetrackingExperiment.html#putting-it-all-together",
"title": "Create an eye-tracking experiment",
"section": "Putting It All Together",
"text": "Putting It All Together\nLet’s build the complete experiment step by step.\n\nImport Libraries and load the Stimuli\nFirst, we need to import our libraries, create the window, initialize the eye tracker, and load our stimuli.\nAgain this part is identical to our previous PsychoPy tutorial:\n\nimport os\nfrom pathlib import Path\nfrom psychopy import core, event, visual,sound\nfrom DeToX import ETracker\n\n#%% Load and prepare stimuli\n\n# Setting the directory of our experiment\nos.chdir(r'<<< YOUR PATH >>>>')\n\n# Now create a Path object for the stimuli directory\nstimuli_dir = Path('EXP') / 'Stimuli'\n\n# Load images \nfixation = visual.ImageStim(win, image=str(stimuli_dir / 'fixation.png'), size=(200, 200))\ncircle = visual.ImageStim(win, image=str(stimuli_dir / 'circle.png'), size=(200, 200))\nsquare = visual.ImageStim(win, image=str(stimuli_dir / 'square.png'), size=(200, 200))\ncomplex = visual.ImageStim(win, image=str(stimuli_dir / 'complex.png'), size=(200, 200), pos=(250, 0))\nsimple = visual.ImageStim(win, image=str(stimuli_dir / 'simple.png'), size=(200, 200), pos=(-250, 0))\n\n# Load sound \npresentation_sound = sound.Sound(str(stimuli_dir / 'presentation.wav'))\n\n# List of stimuli\ncues = [circle, square] # put both cues in a list\ntargets = [complex, simple] # put both rewards in a list\n\n# Create a list of trials in which 0 means winning and 1 means losing\nTrials = [0, 1, 0, 0, 1, 0, 1, 1, 0, 1 ]\n\n\n\nStart recording\nNow we are ready to connect to the eye tracker and start collecting data. With DeToX, this is just two lines of code: one to initialize the connection and one to start the recording stream.\n\n#%% Record the data\n\n# Connect to the eye tracker\nET_controller = ETracker(win)\n\n# Start recording\n# DeToX will automatically create this file and start saving data to it\nET_controller.start_recording(filename=\"testing.h5\")\n\n\n\nPresent Our Stimuli\nThe eye tracking is running! Now we can loop through our trials and show the participant our stimuli.\nThe most critical step here is to mark exactly when a stimulus appears on the screen. We do this by sending an “event marker” to the data file. With DeToX, this is incredibly simple: immediately after win.flip() (which updates the screen), we call ET_controller.record_event('Label').\nYou will also notice the special trick we use during the Inter-Stimulus Interval (ISI) to save our data safely without disrupting the experiment timing.\n\n#%% Trials\nfor trial in Trials:\n\n ### 1. Present the Fixation\n fixation.draw()\n win.flip() # Stimulus appears\n ET_controller.record_event('Fixation') # Log event immediately\n \n core.wait(1)\n\n ### 2. Present the Cue\n cues[trial].draw()\n win.flip()\n \n # Log specific cue type\n if trial == 0:\n ET_controller.record_event('Circle')\n else:\n ET_controller.record_event('Square')\n \n core.wait(3)\n\n ### 3. Wait for Saccadic Latency\n win.flip()\n core.wait(0.75)\n\n ### 4. Present the Target\n targets[trial].draw()\n win.flip()\n \n if trial == 0:\n ET_controller.record_event('Complex')\n else:\n ET_controller.record_event('Simple')\n\n presentation_sound.play()\n core.wait(2)\n \n ### 5. ISI and Smart Saving\n win.flip()\n ET_controller.record_event('ISI')\n \n # Start a clock to measure our ISI duration\n clock = core.Clock() \n \n # --- SAVE DATA ---\n # Flush the data currently in memory to the disk\n ET_controller.save_data()\n \n # Wait for whatever time is left in the 1-second ISI\n # This ensures the ISI is exactly 1s, even if saving took 0.1s\n core.wait(1 - clock.getTime()) \n \n ### Check for escape key to exit\n keys = event.getKeys()\n if 'escape' in keys:\n ET_controller.stop_recording()\n win.close()\n core.quit()\n\n\nA Note on Smart Saving\nDid you catch the logic inside the ISI section?\nAs we mentioned in Save data, it is best to save your data intermittently to avoid loss if the computer crashes. The ISI is the perfect moment for this because the participant is just looking at a blank screen.\nIf you remember from our Getting Started with PsychoPy tutorial, we used a core.Clock() for the ISI instead of a simple core.wait(). This is exactly why!\n\nStart Clock: We start a timer immediately when the ISI begins.\nSave Data: We call ET_controller.save_data(). This might take 10ms or 50ms depending on your computer.\nWait for Remainder: We calculate 1 - clock.getTime().\n\nThis subtraction is the “cool” part. It ensures that the total ISI is exactly 1 second, automatically subtracting the time it took to save the data. If we just used core.wait(1) after saving, our ISI would be too long (1s + saving time).\n\n\n\n\n\n\nWarning\n\n\n\nCareful!!!\nIf saving the data takes more than 1 second, your ISI will also be longer. However, this should not be the case with typical studies where trials are not too long. Nonetheless, it’s always a good idea to keep an eye out.\n\n\n\n\n\nStop recording\nAlmost done! We’ve collected data, sent events, and saved everything. The final step is to stop data collection (otherwise Python will keep getting endless data from the eye tracker!). We simply unsubscribe from the eye tracker:\n\n# --- End Experiment ---\nET_controller.stop_recording() # Save remaining data and disconnect\nwin.close()\ncore.quit()\n\nNote that we also closed the Psychopy window, so that the stimulus presentation is also officially over. Well done!!! Now go and get your data!!! We’ll see you back when it’s time to analyze it.",
"crumbs": [
"Eye-tracking",
"Create an eye-tracking experiment"
]
},
{
"objectID": "CONTENT/ContentList.html",
"href": "CONTENT/ContentList.html",
"title": "Content listing",
"section": "",
"text": "Order By\n Default\n \n Title\n \n \n Date - Oldest\n \n \n Date - Newest\n \n \n Author\n \n \n \n \n \n \n \n\n\n\n\n\n\n\n\nStarting with Python\n\n\n\nSetup\n\nPython\n\n\n\n\n\n\n\n\n\n\nOct 30, 2023\n\n\n\n\n\n\n\n\n\n\n\nStarting with R\n\n\n\nSetup\n\nR\n\n\n\n\n\n\n\n\n\n\nNov 21, 2023\n\n\n\n\n\n\n\n\n\n\n\nStarting with PsychoPy\n\n\n\nSetup\n\nPython\n\n\n\n\n\n\n\n\n\n\nDec 27, 2023\n\n\n\n\n\n\n\n\n\n\n\nCreate your first paradigm\n\n\n\nPython\n\n\n\n\n\n\n\n\n\n\nFeb 16, 2024\n\n\n\n\n\n\n\n\n\n\n\nIntroduction to eye-tracking\n\n\n\nEye-tracking\n\nTheory\n\n\n\n\n\n\n\n\n\n\nMar 20, 2024\n\n\n\n\n\n\n\n\n\n\n\nCreate an eye-tracking experiment\n\n\n\nEye-tracking\n\nPython\n\n\n\n\n\n\n\n\n\n\nMay 20, 2024\n\n\n\n\n\n\n\n\n\n\n\nUsing I2MC for robust fixation extraction\n\n\n\nEye-tracking\n\nPython\n\n\n\n\n\n\n\n\n\n\nJul 19, 2024\n\n\n\n\n\n\n\n\n\n\n\nFrom fixations to measures\n\n\n\nEye-tracking\n\nPython\n\n\n\n\n\n\n\n\n\n\nSep 7, 2024\n\n\n\n\n\n\n\n\n\n\n\nCalibrating eye-tracking\n\n\n\nEye-tracking\n\nPython\n\n\n\n\n\n\n\n\n\n\nOct 19, 2024\n\n\n\n\n\n\n\n\n\n\n\nPupil data pre-processing\n\n\n\nPupillometry\n\nR\n\nPre-processing\n\n\n\n\n\n\n\n\n\n\nDec 18, 2024\n\n\n\n\n\n\n\n\n\n\n\nPupil Data Analysis\n\n\n\nStats\n\nR\n\nModelling\n\nAdditive models\n\n\n\n\n\n\n\n\n\n\nJan 2, 2025\n\n\n\n\n\n\n\n\n\n\n\nLinear Models\n\n\n\nStats\n\nR\n\nlinear models\n\n\n\n\n\n\n\n\n\n\nMay 3, 2025\n\n\n\n\n\n\n\n\n\n\n\nLinear mixed effect models\n\n\n\nR\n\nStats\n\nMixed effects models\n\n\n\n\n\n\n\n\n\n\nMar 3, 2026\n\n\n\n\n\n\n\n\n\n\n\nGeneralised mixed-effect models\n\n\n\nR\n\nStats\n\nMixed effects models\n\n\n\n\n\n\n\n\n\n\nApr 3, 2026\n\n\n\n\n\n\n\n\n\n\n\nModelEstimates\n\n\n\nStats\n\nR\n\nlinear models\n\n\n\n\n\n\n\nTommaso Ghilardi\n\n\nNov 3, 2026\n\n\n\n\n\n\n\n\n\n\n\nModel Estimates\n\n\n\nStats\n\nR\n\nlinear models\n\n\n\n\n\n\n\nTommaso Ghilardi\n\n\nNov 3, 2026\n\n\n\n\n\n\n\n\n\n\n\nDevStart Setup Guide\n\n\n\nSetup\n\nR\n\nPython\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nNo matching items\n Back to top"
},
{
"objectID": "CONTENT/EyeTracking/EyetrackingCalibration.html",
"href": "CONTENT/EyeTracking/EyetrackingCalibration.html",
"title": "Calibrating eye-tracking",
"section": "",
"text": "Before running an eye-tracking study, we usually need to do a calibration. What is a calibration, you ask?\nWell, an eye-tracker works by using illuminators to emit near-infrared light towards the eye. This light reflects off the cornea, creating a visible glint or reflection that the eye-tracker’s sensors can detect. By capturing this reflection, the eye-tracker can determine the position of the eye relative to a screen, allowing it to pinpoint exactly where the user is looking.\nBut why is this step necessary?\nEvery eye is unique. Calibration allows the software to build a custom mathematical model for each participant, accounting for individual differences in eye shape, size, and how the light reflects off their specific cornea. Without this step, the eye-tracker’s gaze estimation will lack precision and accuracy, potentially introducing significant offsets in your data.\n\n\n\n\n\n\n\nImage from the tobii website\n\n\nThis process is essential for data quality. Good eye-tracking data starts long before you present your first stimulus—it begins with proper setup and calibration. This is particularly critical when testing infants and children, where data quality can be challenging even under ideal conditions.\n\n\nYou have a couple of options to perform this crucial step:\n\nTobii Pro Eye Tracker Manager: This is a free software from Tobii. It is reliable and easy to use. The main problem is that the calibration is not really “infant-friendly”—it usually involves very small moving dots that might not capture a baby’s attention effectively.\nDeToX (via Tobii SDK): We can use DeToX to do just that! We created this library to wrap around PsychoPy and the Tobii SDK, allowing you to run flexible, infant-friendly calibrations (using fun images and sounds!) without the headache of coding it all from scratch.\n\nIn this tutorial, we will use DeToX to handle the heavy lifting. We’ll focus on two essential steps:\n\nPositioning your participant in the eye tracker’s optimal tracking zone.\nRunning the calibration procedure.\n\nGet these right, and everything else falls into place.\n\n\n\n\n\nEvery eye tracker has a track box—an invisible 3D zone in front of the screen where it can accurately detect eyes. Step outside this zone, and your tracking quality drops fast!\nWhile we try to seat everyone consistently, even small differences in posture or chair height can matter. The goal is simple but crucial: center the participant’s eyes on the screen and ensure their gaze is perpendicular to it.\nYou can see here two classic scenarios of infants in front of an eye-tracker. Whether they are in a high chair or on a parent’s lap, the important thing is that the head and gaze of the child are aligned with the screen.\n\n\n\n\n\nBut …how do we check it?\nWe could just eyeball it, but “it looks about right” isn’t exactly scientific rigor. The good news? DeToX makes checking the position as easy as calling a single method.\n\n\n\nFirst, let’s get our script running. We will follow the same steps from the previous tutorial to set up our window and connect to the tracker.\n\nfrom psychopy import visual, core\nfrom DeToX import ETracker\n\n# 1. Create the Window\n# This is where all our stimuli (and the calibration points) will appear\nwin = visual.Window(\n size=[1920, 1080], # Window dimensions in pixels\n fullscr=True, # Expand to fill the entire screen\n units='pix' # Use pixels as the measurement unit\n)\n\n# 2. Connect to the Eye Tracker\n# We create an ETracker object which automatically finds and connects to the device\nET_controller = ETracker(win)\n\nWhat is happening here?\n\nWe import the necessary librarires.\nWe create a standard PsychoPy window.\nWe initialize ET_controller. This one line does a lot of heavy lifting: it searches for connected Tobii devices, establishes communication, and links the tracker to your window.\n\nIf everything is working, you will see connection details printed in your console. Now, our ET_controller object is ready to take command!\n\n\n\nNow for the magic—visualizing where your participant’s eyes are in the track box:\n\nET_controller.show_status()\n\n\n\n\nWhen you call show_status(), an animated video of animals appears to keep infants engaged. Overlaid on the video are two colored circles—🔵 light blue and 🔴 pink —showing where the tracker detects your participant’s eyes in real-time. A black rectangle marks the track box boundaries where tracking works best.\nBelow the rectangle, a green bar with a moving black line indicates distance from the screen. The line should be centered on the bar (about 65 cm distance).\n\n\n\n\n\n\n\n\n\n\n\nSimulation Mode\n\n\n\nIf you’re running with simulate=True, the positioning interface uses your mouse instead of real eye tracker data. Move the mouse around to see the eye circles follow, and use the scroll wheel to simulate moving closer to or further from the screen.\n\n\nCenter both colored circles within the black rectangle. If they’re drifting toward the edges, ask the participant to lean in the needed direction, or adjust the eye tracker mount if possible.\nFor distance, watch the black line on the green bar. Too far left means too close (move back), too far right means too far (move forward). Aim for the center.\nPress SPACE when positioning looks good—you’re ready for calibration!\n\n\n\nBy default, show_status() displays an animated video of animals to keep infants engaged while you adjust their position. However, you can customize this behavior using the video_help parameter:\nvideo_help=True: Uses the built-in instructional video included with DeToX (default). This is the easiest option and works well for most studies.\nvideo_help=False: Disables the video entirely, showing only the eye position circles and track box. Useful if you prefer a minimal display or if video playback causes performance issues.\nvideo_help=visual.MovieStim(...): Uses your own custom video. You’ll need to pre-load and configure the MovieStim object yourself, including setting the appropriate size and position for your display layout.\n\n\n\n\nNow that your participant is perfectly positioned, it’s showtime.\nIn standard scripts, this is the part where you might have to write a complex loop to handle dot positions, keyboard inputs, and validation. In DeToX, it’s a single function call.\n\n\nHere is the command to start the full procedure:\n\nET_controller.calibrate(\n calibration_points=5,\n shuffle=True,\n audio=True,\n anim_type='zoom',\n visualization_style='circles'\n)\n\nLet’s break down what these arguments actually do:\n\ncalibration_points=5: This sets a standard 5-point pattern (4 corners + center). It is the industry standard and works well for most studies. You can bump this up to 9 for higher precision, or even pass a custom list of coordinates if you are feeling fancy.\nshuffle=True: Randomizes the order of the points. This is crucial for infants—it prevents them from predicting the next spot and getting bored.\naudio=True: Plays a fun, attention-getting sound along with the visual stimulus.\nanim_type='zoom': Makes the stimuli gently pulse in size to catch the eye. You can also use 'trill' for a rotating animation.\nvisualization_style='circles': Determines how the results are shown. 'circles' draws dots where the eye looked. 'lines' draws a line connecting the target to the gaze point (visualizing the error).\n\n\n\n\n\n\n\nNote\n\n\n\nA quick heads-up: These are actually the default settings. This means you could more simply call ET_controller.calibrate() and get the exact same behavior! We have shown all the settings here just so you can see what you are implicitly passing. For more information and details about all the possibilities, we suggest looking at the DeToX website.\n\n\n\n\n\nOnce you run that code, DeToX takes over the screen. Here is how the process flows:\n\n\nFirst, you will see a control panel. You (the experimenter) drive this process using the keyboard.\n┌──────────────────── Calibration Setup ─────────────────────┐\n│Mouse-Based Calibration Setup: │\n│ │\n│ - Press number keys (1-5) to select calibration points │\n│ - Move your mouse to the animated stimulus │\n│ - Press SPACE to collect samples at the selected point │\n│ - Press ENTER to finish collecting and see results │\n│ - Press ESCAPE to exit calibration │\n│ │\n│ Any key will start calibration immediately! │\n└────────────────────────────────────────────────────────────┘\n\n\n\nPress any key to enter the active mode. You will see a thin red border indicating that calibration is live.\n\nSelect a Point: Press a number key (e.g., 1). The animated stimulus will appear at that location.\nWait for the Gaze: Watch the participant. Wait for them to actually look at the pulsing animation.\nCapture: When you are confident they are fixating on the target, press SPACE.\n\nThe system pauses briefly (0.25s) to ensure stability, records the data, and then hides the point.\n\nRepeat: Do this for all points. You don’t need to follow a specific order—follow the infant’s attention!\n\n\n\n\nOnce you have collected data for all points (or as many as you could get), press ENTER. DeToX will compute the math and show you the results visually.\nYou will see the targets on the screen, along with marks showing where the participant actually looked.\n┌────────────── Calibration Results ───────────────┐\n│Review calibration results above. │\n│ │\n│ - Press ENTER to accept calibration │\n│ - Press Numbers → SPACE to retry some points │\n│ - Press ESCAPE to restart calibration │\n│ │\n└──────────────────────────────────────────────────┘\nAt this stage, you have three choices:\n\nAccept (ENTER): If the dots align nicely with the targets, you are good to go!\nStart Over (ESCAPE): If the data is a mess (the baby was crying, the tracker was bumped), discard it and restart.\nRetry Specific Points (Number Keys): This is the superpower of DeToX.\n\nOften, a calibration is perfect except for one point where the baby looked away.\nInstead of redoing the whole thing, just press the number for that specific point (it will highlight yellow).\nPress SPACE, and you can recollect data for just that point. This saves massive amounts of time and patience!\n\n\n\n\n\n\nCongratulations! You’ve successfully positioned your participant and completed the calibration procedure using DeToX. With accurate calibration in place, you’re now ready to present your experimental stimuli and collect high-quality eye tracking data.\nHere’s a video demonstrating the entire calibration workflow from start to finish:\n\n \n\n\n\n\n\n\n\n\nSave and Load calibration\n\n\n\n\n\nWhile we recommend performing calibration at the start of each session to ensure optimal accuracy, DeToX also allows you to save and load calibration data for convenience. In our opinion, this should only be used in special circumstances where there is a headrest and little to no chance of movement between sessions. However, if you need to save and reuse calibration data, here’s how:\n\n\nAfter completing a successful calibration, you can save the calibration data to a file:\n# Save with custom filename\nET_controller.save_calibration(filename=\"S01_calibration.dat\")\n┌────── Calibration Saved ─────┐\n│Calibration data saved to: │\n│S01_calibration.dat │\n└──────────────────────────────┘\nThe calibration data is saved as a binary file (.dat format) that can be reloaded in future sessions. If you don’t specify a filename, DeToX automatically generates a timestamped name like 2024-01-15_14-30-00_calibration.dat.\nyou can also choose to use a GUI file dialog to select the save location:\n# Save with GUI file dialog\nET.save_calibration(use_gui=True)\n\n\n\nTo reuse a previously saved calibration in a new session:\n# Load from specific file\nET.load_calibration(filename=\"S01_calibration.dat\")\n┌───── Calibration Loaded ──────┐\n│Calibration data loaded from: │\n│S01_calibration.dat │\n└───────────────────────────────┘\nor again, use a GUI file dialog to select the file:\n# Load with GUI file dialog\nET.load_calibration(use_gui=True)",
"crumbs": [
"Eye-tracking",
"Calibrating eye-tracking"
]
},
{
"objectID": "CONTENT/EyeTracking/EyetrackingCalibration.html#how-do-we-run-it",
"href": "CONTENT/EyeTracking/EyetrackingCalibration.html#how-do-we-run-it",
"title": "Calibrating eye-tracking",
"section": "",
"text": "You have a couple of options to perform this crucial step:\n\nTobii Pro Eye Tracker Manager: This is a free software from Tobii. It is reliable and easy to use. The main problem is that the calibration is not really “infant-friendly”—it usually involves very small moving dots that might not capture a baby’s attention effectively.\nDeToX (via Tobii SDK): We can use DeToX to do just that! We created this library to wrap around PsychoPy and the Tobii SDK, allowing you to run flexible, infant-friendly calibrations (using fun images and sounds!) without the headache of coding it all from scratch.\n\nIn this tutorial, we will use DeToX to handle the heavy lifting. We’ll focus on two essential steps:\n\nPositioning your participant in the eye tracker’s optimal tracking zone.\nRunning the calibration procedure.\n\nGet these right, and everything else falls into place.",
"crumbs": [
"Eye-tracking",
"Calibrating eye-tracking"
]
},
{
"objectID": "CONTENT/EyeTracking/EyetrackingCalibration.html#part-1-positioning-your-participant",
"href": "CONTENT/EyeTracking/EyetrackingCalibration.html#part-1-positioning-your-participant",
"title": "Calibrating eye-tracking",
"section": "",
"text": "Every eye tracker has a track box—an invisible 3D zone in front of the screen where it can accurately detect eyes. Step outside this zone, and your tracking quality drops fast!\nWhile we try to seat everyone consistently, even small differences in posture or chair height can matter. The goal is simple but crucial: center the participant’s eyes on the screen and ensure their gaze is perpendicular to it.\nYou can see here two classic scenarios of infants in front of an eye-tracker. Whether they are in a high chair or on a parent’s lap, the important thing is that the head and gaze of the child are aligned with the screen.\n\n\n\n\n\nBut …how do we check it?\nWe could just eyeball it, but “it looks about right” isn’t exactly scientific rigor. The good news? DeToX makes checking the position as easy as calling a single method.\n\n\n\nFirst, let’s get our script running. We will follow the same steps from the previous tutorial to set up our window and connect to the tracker.\n\nfrom psychopy import visual, core\nfrom DeToX import ETracker\n\n# 1. Create the Window\n# This is where all our stimuli (and the calibration points) will appear\nwin = visual.Window(\n size=[1920, 1080], # Window dimensions in pixels\n fullscr=True, # Expand to fill the entire screen\n units='pix' # Use pixels as the measurement unit\n)\n\n# 2. Connect to the Eye Tracker\n# We create an ETracker object which automatically finds and connects to the device\nET_controller = ETracker(win)\n\nWhat is happening here?\n\nWe import the necessary librarires.\nWe create a standard PsychoPy window.\nWe initialize ET_controller. This one line does a lot of heavy lifting: it searches for connected Tobii devices, establishes communication, and links the tracker to your window.\n\nIf everything is working, you will see connection details printed in your console. Now, our ET_controller object is ready to take command!\n\n\n\nNow for the magic—visualizing where your participant’s eyes are in the track box:\n\nET_controller.show_status()\n\n\n\n\nWhen you call show_status(), an animated video of animals appears to keep infants engaged. Overlaid on the video are two colored circles—🔵 light blue and 🔴 pink —showing where the tracker detects your participant’s eyes in real-time. A black rectangle marks the track box boundaries where tracking works best.\nBelow the rectangle, a green bar with a moving black line indicates distance from the screen. The line should be centered on the bar (about 65 cm distance).\n\n\n\n\n\n\n\n\n\n\n\nSimulation Mode\n\n\n\nIf you’re running with simulate=True, the positioning interface uses your mouse instead of real eye tracker data. Move the mouse around to see the eye circles follow, and use the scroll wheel to simulate moving closer to or further from the screen.\n\n\nCenter both colored circles within the black rectangle. If they’re drifting toward the edges, ask the participant to lean in the needed direction, or adjust the eye tracker mount if possible.\nFor distance, watch the black line on the green bar. Too far left means too close (move back), too far right means too far (move forward). Aim for the center.\nPress SPACE when positioning looks good—you’re ready for calibration!\n\n\n\nBy default, show_status() displays an animated video of animals to keep infants engaged while you adjust their position. However, you can customize this behavior using the video_help parameter:\nvideo_help=True: Uses the built-in instructional video included with DeToX (default). This is the easiest option and works well for most studies.\nvideo_help=False: Disables the video entirely, showing only the eye position circles and track box. Useful if you prefer a minimal display or if video playback causes performance issues.\nvideo_help=visual.MovieStim(...): Uses your own custom video. You’ll need to pre-load and configure the MovieStim object yourself, including setting the appropriate size and position for your display layout.",
"crumbs": [
"Eye-tracking",
"Calibrating eye-tracking"
]
},
{
"objectID": "CONTENT/EyeTracking/EyetrackingCalibration.html#part-2-running-the-calibration",
"href": "CONTENT/EyeTracking/EyetrackingCalibration.html#part-2-running-the-calibration",
"title": "Calibrating eye-tracking",
"section": "",
"text": "Now that your participant is perfectly positioned, it’s showtime.\nIn standard scripts, this is the part where you might have to write a complex loop to handle dot positions, keyboard inputs, and validation. In DeToX, it’s a single function call.\n\n\nHere is the command to start the full procedure:\n\nET_controller.calibrate(\n calibration_points=5,\n shuffle=True,\n audio=True,\n anim_type='zoom',\n visualization_style='circles'\n)\n\nLet’s break down what these arguments actually do:\n\ncalibration_points=5: This sets a standard 5-point pattern (4 corners + center). It is the industry standard and works well for most studies. You can bump this up to 9 for higher precision, or even pass a custom list of coordinates if you are feeling fancy.\nshuffle=True: Randomizes the order of the points. This is crucial for infants—it prevents them from predicting the next spot and getting bored.\naudio=True: Plays a fun, attention-getting sound along with the visual stimulus.\nanim_type='zoom': Makes the stimuli gently pulse in size to catch the eye. You can also use 'trill' for a rotating animation.\nvisualization_style='circles': Determines how the results are shown. 'circles' draws dots where the eye looked. 'lines' draws a line connecting the target to the gaze point (visualizing the error).\n\n\n\n\n\n\n\nNote\n\n\n\nA quick heads-up: These are actually the default settings. This means you could more simply call ET_controller.calibrate() and get the exact same behavior! We have shown all the settings here just so you can see what you are implicitly passing. For more information and details about all the possibilities, we suggest looking at the DeToX website.\n\n\n\n\n\nOnce you run that code, DeToX takes over the screen. Here is how the process flows:\n\n\nFirst, you will see a control panel. You (the experimenter) drive this process using the keyboard.\n┌──────────────────── Calibration Setup ─────────────────────┐\n│Mouse-Based Calibration Setup: │\n│ │\n│ - Press number keys (1-5) to select calibration points │\n│ - Move your mouse to the animated stimulus │\n│ - Press SPACE to collect samples at the selected point │\n│ - Press ENTER to finish collecting and see results │\n│ - Press ESCAPE to exit calibration │\n│ │\n│ Any key will start calibration immediately! │\n└────────────────────────────────────────────────────────────┘\n\n\n\nPress any key to enter the active mode. You will see a thin red border indicating that calibration is live.\n\nSelect a Point: Press a number key (e.g., 1). The animated stimulus will appear at that location.\nWait for the Gaze: Watch the participant. Wait for them to actually look at the pulsing animation.\nCapture: When you are confident they are fixating on the target, press SPACE.\n\nThe system pauses briefly (0.25s) to ensure stability, records the data, and then hides the point.\n\nRepeat: Do this for all points. You don’t need to follow a specific order—follow the infant’s attention!\n\n\n\n\nOnce you have collected data for all points (or as many as you could get), press ENTER. DeToX will compute the math and show you the results visually.\nYou will see the targets on the screen, along with marks showing where the participant actually looked.\n┌────────────── Calibration Results ───────────────┐\n│Review calibration results above. │\n│ │\n│ - Press ENTER to accept calibration │\n│ - Press Numbers → SPACE to retry some points │\n│ - Press ESCAPE to restart calibration │\n│ │\n└──────────────────────────────────────────────────┘\nAt this stage, you have three choices:\n\nAccept (ENTER): If the dots align nicely with the targets, you are good to go!\nStart Over (ESCAPE): If the data is a mess (the baby was crying, the tracker was bumped), discard it and restart.\nRetry Specific Points (Number Keys): This is the superpower of DeToX.\n\nOften, a calibration is perfect except for one point where the baby looked away.\nInstead of redoing the whole thing, just press the number for that specific point (it will highlight yellow).\nPress SPACE, and you can recollect data for just that point. This saves massive amounts of time and patience!\n\n\n\n\n\n\nCongratulations! You’ve successfully positioned your participant and completed the calibration procedure using DeToX. With accurate calibration in place, you’re now ready to present your experimental stimuli and collect high-quality eye tracking data.\nHere’s a video demonstrating the entire calibration workflow from start to finish:\n\n \n\n\n\n\n\n\n\n\nSave and Load calibration\n\n\n\n\n\nWhile we recommend performing calibration at the start of each session to ensure optimal accuracy, DeToX also allows you to save and load calibration data for convenience. In our opinion, this should only be used in special circumstances where there is a headrest and little to no chance of movement between sessions. However, if you need to save and reuse calibration data, here’s how:\n\n\nAfter completing a successful calibration, you can save the calibration data to a file:\n# Save with custom filename\nET_controller.save_calibration(filename=\"S01_calibration.dat\")\n┌────── Calibration Saved ─────┐\n│Calibration data saved to: │\n│S01_calibration.dat │\n└──────────────────────────────┘\nThe calibration data is saved as a binary file (.dat format) that can be reloaded in future sessions. If you don’t specify a filename, DeToX automatically generates a timestamped name like 2024-01-15_14-30-00_calibration.dat.\nyou can also choose to use a GUI file dialog to select the save location:\n# Save with GUI file dialog\nET.save_calibration(use_gui=True)\n\n\n\nTo reuse a previously saved calibration in a new session:\n# Load from specific file\nET.load_calibration(filename=\"S01_calibration.dat\")\n┌───── Calibration Loaded ──────┐\n│Calibration data loaded from: │\n│S01_calibration.dat │\n└───────────────────────────────┘\nor again, use a GUI file dialog to select the file:\n# Load with GUI file dialog\nET.load_calibration(use_gui=True)",
"crumbs": [
"Eye-tracking",
"Calibrating eye-tracking"
]
},
{
"objectID": "CONTENT/EyeTracking/I2MC_tutorial.html",
"href": "CONTENT/EyeTracking/I2MC_tutorial.html",
"title": "Using I2MC for robust fixation extraction",
"section": "",
"text": "When it comes to eye-tracking data, a fundamental role is played by fixations. A fixation indicates that a person’s eyes are looking at a particular point of interest for a given amount of time. More specifically, a fixation is a cluster of consecutive data points in an eye-tracking dataset for which a person’s gaze remains relatively still and focused on a particular area or object.\nTypically, eye-tracking programs come with their own fixation detection algorithms that give us a rough idea of what the person was looking at. But here’s the problem: these tools aren’t always very good when it comes to data from infants and children. Why? Because infants and children can be all over the place! They move their heads, put their hands (or even feet) in front of their faces, close their eyes, or just look away. All of this makes the data a big mess that’s hard to make sense of with regular fixation detection algorithms. Because the data is so messy, it is difficult to tell which data points are part of the same fixation or different fixations.\nBut don’t worry! We’ve got a solution: I2MC.\nI2MC stands for “Identification by Two-Means Clustering”, and it was designed specifically for this kind of problem. It’s designed to deal with all kinds of noise, and even periods of data loss. In this tutorial, we’ll show you how to use I2MC to find fixations. We won’t get into the nerdy stuff about how it works - this is all about the practical side. If you’re curious about the science, you can read the original article.\nNow that we’ve introduced I2MC, let’s get our hands dirty and see how to use it!",
"crumbs": [
"Eye-tracking",
"Using I2MC for robust fixation extraction"
]
},
{
"objectID": "CONTENT/EyeTracking/I2MC_tutorial.html#import-data",
"href": "CONTENT/EyeTracking/I2MC_tutorial.html#import-data",
"title": "Using I2MC for robust fixation extraction",
"section": "Import data",
"text": "Import data\nNow we will write a simple function to import our data. Because different eye trackers produce different file structures, this step acts as a “translator” to get everything into a standard format.\nFor this tutorial, you can either:\n\nUse our sample data: Download the dataset we collected with DeToX here.\nUse your own data: You will just need to adapt the column names in this function to match your specific file format.\n\nLet’s build the import function step by step.\n\n# Load data\nraw_df = pd.read_hdf(fname, key='gaze')\n\nOnce the data is loaded, we need to create a clean DataFrame containing only the information essential for analysis.\nThis is the most important part: we need to map your specific column names to a standard set of 5 columns (Time, Left X, Left Y, Right X, Right Y). If you are using a different eye tracker (e.g., Eyelink), this is a part of the code you will need to change!\n\n# Create empty dataframe\ndf = pd.DataFrame()\n \n# Extract required data\ndf['time'] = raw_df['TimeStamp']\ndf['L_X'] = raw_df['Left_X']\ndf['L_Y'] = raw_df['Left_Y']\ndf['R_X'] = raw_df['Right_X']\ndf['R_Y'] = raw_df['Right_Y']\n\nAfter extracting the raw coordinates, we perform some minimal pre-processing to remove artifacts.\nEye trackers can occasionally produce “spurious” data points where the gaze coordinates jump far outside the screen boundaries. We define a valid range (the monitor size plus a margin) and mark any samples outside this range as missing (NaN).\nAdditionally, we use the Validity codes provided by the eye tracker. DeToX standardizes these so that 1 indicates a valid sample and 0 indicates an invalid one. We simply reject any sample marked as invalid.\n\n# Sometimes we have weird peaks where one sample is (very) far outside the\n# monitor. Here, count as missing any data that is more than one monitor\n# distance outside the monitor.\n\n# Screen resolution\nres = [1920, 1080]\n\n# --- Left Eye Processing ---\n# 1. Check for coordinates far outside the monitor\nlMiss1 = (df['L_X'] < -res[0]) | (df['L_X'] > 2 * res[0])\nlMiss2 = (df['L_Y'] < -res[1]) | (df['L_Y'] > 2 * res[1])\n\n# 2. Check validity (0 = invalid in DeToX)\n# Combine criteria: Miss if out of bounds OR validity is 0\nlMiss = lMiss1 | lMiss2 | (raw_df['Left_Validity'] == 0)\n\n# 3. Replace invalid samples with NaN\ndf.loc[lMiss, 'L_X'] = np.nan\ndf.loc[lMiss, 'L_Y'] = np.nan\n\n# --- Right Eye Processing ---\nrMiss1 = (df['R_X'] < -res[0]) | (df['R_X'] > 2 * res[0])\nrMiss2 = (df['R_Y'] < -res[1]) | (df['R_Y'] > 2 * res[1])\nrMiss = rMiss1 | rMiss2 | (raw_df['Right_Validity'] == 0)\n\ndf.loc[rMiss, 'R_X'] = np.nan\ndf.loc[rMiss, 'R_Y'] = np.nan\n\nPerfect!!!\n\nEverything into a function\nWe have successfully loaded the data, extracted the relevant columns, and cleaned up the artifacts. To make this easy to use with I2MC (and to keep our main script clean), we will wrap all these steps into a single, reusable function.\n\ndef tobii_TX300(fname, res=[1920,1080]):\n \"\"\"\n Import and preprocess DeToX HDF5 data for I2MC.\n \"\"\"\n # 1. Load the raw data\n raw_df = pd.read_hdf(fname, key='gaze')\n \n # 2. Create the output DataFrame\n df = pd.DataFrame()\n df['time'] = raw_df['TimeStamp']\n \n # Map DeToX columns to I2MC expected names\n df['L_X'] = raw_df['Left_X']\n df['L_Y'] = raw_df['Left_Y']\n df['R_X'] = raw_df['Right_X']\n df['R_Y'] = raw_df['Right_Y']\n \n # 3. Clean Artifacts (Out of bounds)\n # Left Eye\n lMiss1 = (df['L_X'] < -res[0]) | (df['L_X'] > 2 * res[0])\n lMiss2 = (df['L_Y'] < -res[1]) | (df['L_Y'] > 2 * res[1])\n lMiss = lMiss1 | lMiss2 | (raw_df['Left_Validity'] == 0)\n \n df.loc[lMiss, 'L_X'] = np.nan\n df.loc[lMiss, 'L_Y'] = np.nan\n\n # Right Eye\n rMiss1 = (df['R_X'] < -res[0]) | (df['R_X'] > 2 * res[0])\n rMiss2 = (df['R_Y'] < -res[1]) | (df['R_Y'] > 2 * res[1])\n rMiss = rMiss1 | rMiss2 | (raw_df['Right_Validity'] == 0)\n \n df.loc[rMiss, 'R_X'] = np.nan\n df.loc[rMiss, 'R_Y'] = np.nan\n\n return df\n\n\n\nFind our data\nNice!! we have our import function that we will use to read our data. Now, let’s find our data! To do this, we will use the glob library, which is a handy tool for finding files in Python. Before that let’s set our working directory. The working directory is the folder where we have all our scripts and data. We can set it using the os library:\n\nimport os\nos.chdir(r'<<< YOUR PATH >>>>')\n\nThis is my directory, you will have something different, you need to change it to where your data are. Once you are done with that we can use glob to find our data files. In the code below, we are telling Python to look for files with a .h5 extension in a specific folder on our computer:\n\nfrom pathlib import Path\ndata_files = list(Path().glob('DATA/RAW/**/*.h5'))\n\n\nDATA\\\\RAW\\\\: This is the path where we want to start our search.\n**: This special symbol tells Python to search in all the subfolders (folders within folders) under our starting path.\n*.h5: We’re asking Python to look for files with names ending in “.h5”.\n\nSo, when we run this code, Python will find and give us a list of all the “.h5” files located in any subfolder within our specified path. This makes it really convenient to find and work with lots of files at once.\n\n\nDefine the output folder\nBefore processing the files, we need a dedicated place to save the results. We will create a folder called i2mc_output inside our existing DATA directory.\nUsing Python’s pathlib, we can define and create this directory safely in just two lines of code:\n\nfrom pathlib import Path\n\n# Define the output folder path\noutput_folder = Path('DATA') / 'i2mc_output' # define folder path\\name\n\n# Create the folder (will do nothing if it already exists)\noutput_folder.mkdir(parents=True, exist_ok=True)\n\nThe .mkdir() method is incredibly convenient here:\n\nparents=True ensures that if the parent folder (DATA) is missing, Python creates it for you automatically.\nexist_ok=True prevents the script from crashing if the folder already exists—perfect for when you need to run your analysis script multiple times.\n\n\n\nI2MC settings\nNow that we have our data and our import function, we are almost ready to run I2MC. But first, we need to configure the algorithm.\nThese settings act as instructions for I2MC. The defaults provided below usually work well for most eye-tracking setups, so you can often leave them as they are. However, it is critical that you verify the Necessary Variables—specifically the screen resolution (xres, yres) and sampling frequency (freq)—to match your specific eye tracker model.\nIf you are curious about the math behind these options, we recommend reading the original I2MC article.\nLet’s define these settings:\n\n# =============================================================================\n# NECESSARY VARIABLES\n# =============================================================================\n\nopt = {}\n\n# --- General variables ---\nopt['xres'] = 1920.0 # Max horizontal resolution in pixels\nopt['yres'] = 1080.0 # Max vertical resolution in pixels\nopt['missingx'] = np.nan # Missing value code (we used np.nan in our import function)\nopt['missingy'] = np.nan # Missing value code\nopt['freq'] = 300.0 # Sampling frequency (Hz). CHECK YOUR DEVICE! (e.g., 60, 120, 300)\n\n# --- Visual Angle Calculation ---\n# Used for calculating noise measures (RMS and BCEA). \n# If left empty, noise measures will be reported in pixels instead of degrees.\nopt['scrSz'] = [50.9, 28.6] # Screen size in cm (Width, Height)\nopt['disttoscreen'] = 65.0 # Viewing distance in cm\n\n# --- Output Options ---\ndo_plot_data = True # Save a plot of fixation detection for each trial?\n# Note: Figures work best for short trials (up to ~20 seconds)\n\n# =============================================================================\n# OPTIONAL VARIABLES (Algorithm Fine-Tuning)\n# =============================================================================\n# Only change these if you have a specific reason to deviate from defaults.\n\n# --- Interpolation Settings (Steffen) ---\nopt['windowtimeInterp'] = 0.1 # Max duration (s) of missing data to interpolate\nopt['edgeSampInterp'] = 2 # Samples required at edges for interpolation\nopt['maxdisp'] = opt['xres'] * 0.2 * np.sqrt(2) # Max displacement allowed\n\n# --- K-Means Clustering Settings ---\nopt['windowtime'] = 0.2 # Time window (s) for clustering (approx 1 saccade duration)\nopt['steptime'] = 0.02 # Window shift (s) per iteration\nopt['maxerrors'] = 100 # Max errors allowed before skipping file\nopt['downsamples'] = [2, 5, 10]\nopt['downsampFilter'] = False # Chebychev filter (False avoids ringing artifacts)\n\n# --- Fixation Determination Settings ---\nopt['cutoffstd'] = 2.0 # Std devs above mean weight for fixation cutoff\nopt['onoffsetThresh'] = 3.0 # MADs for refining fixation start/end points\nopt['maxMergeDist'] = 30.0 # Max pixels between fixations to merge them\nopt['maxMergeTime'] = 30.0 # Max ms between fixations to merge them\nopt['minFixDur'] = 40.0 # Min duration (ms) for a valid fixation\n\n\n\nRun I2MC\nNow we can finally run the algorithm on all our files!\nWe will use a loop to iterate through every file we found. For each file, we will:\n\nCreate a specific folder for that participant.\nImport the data using our tobii_TX300 function.\nRun I2MC to detect fixations.\nSave the results (CSV) and the visualization (PNG).\n\n\n#%% Run I2MC\nimport matplotlib.pyplot as plt\nimport I2MC\n\nfor file_idx, file in enumerate(data_files):\n print(f'Processing file {file_idx + 1} of {len(data_files)}: {file.name}')\n \n # 1. Setup Folders\n name = file.stem\n subj_folder = output_folder / name\n subj_folder.mkdir(exist_ok=True)\n \n # 2. Import Data (using the function we defined earlier)\n # Note: We pass the resolution from our options to ensure consistency\n data = tobii_TX300(file, res=[opt['xres'], opt['yres']])\n \n # 3. Run I2MC\n # Returns: fix (dict of results), data (interpolated data), par (final parameters)\n fix, _, _ = I2MC.I2MC(data, opt)\n \n # 4. Save Plot (Optional)\n if do_plot_data and fix:\n save_plot = subj_folder / f\"{name}.png\"\n \n # Generate plot\n f = I2MC.plot.data_and_fixations(\n data, \n fix, \n fix_as_line=True, \n res=[opt['xres'], opt['yres']]\n )\n \n # Save and close to free memory\n f.savefig(save_plot)\n plt.close(f)\n \n # 5. Save Data to CSV\n fix['participant'] = name\n fix_df = pd.DataFrame(fix)\n \n save_file = subj_folder / f\"{name}.csv\"\n fix_df.to_csv(save_file, index=False)",
"crumbs": [
"Eye-tracking",
"Using I2MC for robust fixation extraction"
]
},
{
"objectID": "CONTENT/EyeTracking/I2MC_tutorial.html#we-are-done",
"href": "CONTENT/EyeTracking/I2MC_tutorial.html#we-are-done",
"title": "Using I2MC for robust fixation extraction",
"section": "WE ARE DONE!!!!!",
"text": "WE ARE DONE!!!!!\nCongratulations! You have successfully processed your eye-tracking data. You should now see a new folder named i2mc_output containing a CSV file and a visualization plot for each of your participants.\nBut what exactly did we just generate?\nI2MC analyzes your raw gaze data and identifies fixations—periods where the eye is relatively still and processing information. The resulting data frame contains several key pieces of information:\nWhat I2MC Returns:\n\ncutoff: A number representing the cutoff used for fixation detection.\nstart: An array holding the indices where fixations start.\nend: An array holding the indices where fixations end.\nstartT: An array containing the times when fixations start.\nendT: An array containing the times when fixations end.\ndur: An array storing the durations of fixations.\nxpos: An array representing the median horizontal position for each fixation in the trial.\nypos: An array representing the median vertical position for each fixation in the trial.\nflankdataloss: A boolean value (1 or 0) indicating whether a fixation is flanked by data loss (1) or not (0).\nfracinterped: A fraction that tells us the amount of data loss or interpolated data in the fixation data.\n\nIn simple terms, I2MC helps us understand where and for how long a person’s gaze remains fixed during an eye-tracking experiment.\nThis is just the first step!!\nNow that we have our fixations, we’ll need to use them to extract the information we’re interested in. Typically, this involves using the raw data to understand what was happening at each specific time point and using the data from I2MC to determine where the participant was looking at that time. This will be covered in a new tutorial. For now, you’ve successfully completed the pre-processing of your eye-tracking data, extracting a robust estimation of participants’ fixations!!\n\n\n\n\n\n\nWarning\n\n\n\nPlease keep in mind that this tutorial uses a simplified approach for demonstration purposes. It assumes:\n\nEach participant has a single data file (one trial/block).\nThe data is relatively clean and continuous.\nFiles are not missing critical columns or metadata.\n\nIf your real-world data is more complex (e.g., multiple sessions per participant, corrupted files), you may need to add extra checks to your script.\nFor comprehensive documentation and advanced examples—including how to handle missing data and batch processing errors—we highly recommend checking out the official I2MC repository.\nIf you encounter issues running this script on your own data, don’t hesitate to reach out to us. Happy coding!",
"crumbs": [
"Eye-tracking",
"Using I2MC for robust fixation extraction"
]
},
{
"objectID": "CONTENT/EyeTracking/I2MC_tutorial.html#entire-script",
"href": "CONTENT/EyeTracking/I2MC_tutorial.html#entire-script",
"title": "Using I2MC for robust fixation extraction",
"section": "Entire script",
"text": "Entire script\nTo simplify things, here is the complete script we have developed together. This version incorporates all the changes we discussed, including the use of DeToX’s HDF5 data format and the updated import function.\n\nimport os\nfrom pathlib import Path\n\nimport I2MC\nimport pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\n\n\n# =============================================================================\n# 1. Function to Import Data (DeToX format)\n# =============================================================================\n\ndef tobii_TX300(fname, res=[1920, 1080]):\n \"\"\"\n Import and preprocess DeToX HDF5 data for I2MC.\n \"\"\"\n # 1. Load the raw data from HDF5\n # We read the 'gaze' key where DeToX stores samples\n raw_df = pd.read_hdf(fname, key='gaze')\n \n # 2. Create the output DataFrame expected by I2MC\n df = pd.DataFrame()\n df['time'] = raw_df['TimeStamp']\n \n # Map DeToX columns to I2MC expected names\n df['L_X'] = raw_df['Left_X']\n df['L_Y'] = raw_df['Left_Y']\n df['R_X'] = raw_df['Right_X']\n df['R_Y'] = raw_df['Right_Y']\n \n # 3. Clean Artifacts (Out of bounds)\n # Define valid screen area (monitor + margin)\n \n # --- Left Eye ---\n lMiss1 = (df['L_X'] < -res[0]) | (df['L_X'] > 2 * res[0])\n lMiss2 = (df['L_Y'] < -res[1]) | (df['L_Y'] > 2 * res[1])\n # Combine with DeToX validity flag (0 = invalid)\n lMiss = lMiss1 | lMiss2 | (raw_df['Left_Validity'] == 0)\n \n df.loc[lMiss, 'L_X'] = np.nan\n df.loc[lMiss, 'L_Y'] = np.nan\n\n # --- Right Eye ---\n rMiss1 = (df['R_X'] < -res[0]) | (df['R_X'] > 2 * res[0])\n rMiss2 = (df['R_Y'] < -res[1]) | (df['R_Y'] > 2 * res[1])\n rMiss = rMiss1 | rMiss2 | (raw_df['Right_Validity'] == 0)\n \n df.loc[rMiss, 'R_X'] = np.nan\n df.loc[rMiss, 'R_Y'] = np.nan\n\n return df\n\n\n# =============================================================================\n# 2. Preparation and Setup\n# =============================================================================\n\n# Setting the working directory\nos.chdir(r'<<< YOUR PATH >>>>')\n\n# Find the files (DeToX uses .h5 files)\ndata_files = list(Path().glob('DATA/RAW/**/*.h5'))\n\n# define the output folder\noutput_folder = Path('DATA') / 'i2mc_output'\n\n# Create the folder (will do nothing if it already exists)\noutput_folder.mkdir(parents=True, exist_ok=True)\n\n\n# =============================================================================\n# 3. I2MC Settings (NECESSARY VARIABLES)\n# =============================================================================\n\nopt = {}\n# General variables for eye-tracking data\nopt['xres'] = 1920.0 # Max horizontal resolution in pixels\nopt['yres'] = 1080.0 # Max vertical resolution in pixels\nopt['missingx'] = np.nan # Missing value code\nopt['missingy'] = np.nan # Missing value code\nopt['freq'] = 300.0 # Sampling frequency (Hz) - CHECK YOUR DEVICE!\n\n# Visual Angle Calculation\nopt['scrSz'] = [50.9, 28.6] # Screen size in cm\nopt['disttoscreen'] = 65.0 # Distance to screen in cm\n\n# Plotting\ndo_plot_data = True # Save visualization plots?\n\n\n# =============================================================================\n# 4. Optional Variables (Fine-Tuning)\n# =============================================================================\n\n# Interpolation\nopt['windowtimeInterp'] = 0.1\nopt['edgeSampInterp'] = 2\nopt['maxdisp'] = opt['xres'] * 0.2 * np.sqrt(2)\n\n# Clustering\nopt['windowtime'] = 0.2\nopt['steptime'] = 0.02\nopt['maxerrors'] = 100\nopt['downsamples'] = [2, 5, 10]\nopt['downsampFilter'] = False\n\n# Fixation Determination\nopt['cutoffstd'] = 2.0\nopt['onoffsetThresh'] = 3.0\nopt['maxMergeDist'] = 30.0\nopt['maxMergeTime'] = 30.0\nopt['minFixDur'] = 40.0\n\n\n# =============================================================================\n# 5. Run I2MC Loop\n# =============================================================================\n\nfor file_idx, file in enumerate(data_files):\n print(f'Processing file {file_idx + 1} of {len(data_files)}: {file.name}')\n\n # Extract name\n name = file.stem \n \n # Create subject folder\n subj_folder = output_folder / name\n subj_folder.mkdir(exist_ok=True)\n \n # Import data using our new function\n data = tobii_TX300(file, res=[opt['xres'], opt['yres']])\n\n # Run I2MC\n fix, _, _ = I2MC.I2MC(data, opt)\n\n # Save Plot\n if do_plot_data and fix:\n save_plot = subj_folder / f\"{name}.png\"\n f = I2MC.plot.data_and_fixations(\n data, fix, \n fix_as_line=True, \n res=[opt['xres'], opt['yres']]\n )\n f.savefig(save_plot)\n plt.close(f)\n\n # Save Data\n fix['participant'] = name\n fix_df = pd.DataFrame(fix)\n save_file = subj_folder / f\"{name}.csv\"\n fix_df.to_csv(save_file, index=False)",
"crumbs": [
"Eye-tracking",
"Using I2MC for robust fixation extraction"
]
},
{
"objectID": "CONTENT/EyeTracking/PupilDataAnalysis.html",
"href": "CONTENT/EyeTracking/PupilDataAnalysis.html",
"title": "Pupil Data Analysis",
"section": "",
"text": "If you have collected and pre-processed your pupil data, the long awaited moment arrived: It’s finally time to analyse your data and get your results!!!\nIn this tutorial we will do two types of analysis. The first one is more simple, and the second is more advanced. For some research questions, simple analyses are enough: they are intuitive and easy to understand. However, pupil data is actually very rich and complex, and more sophisticated analyses can sometimes help to really get the most out of your data and let them shine!\nBefore starting any type of analysis, let’s import the data and take a quick look at it.",
"crumbs": [
"Eye-tracking",
"Pupil Data Analysis"
]
},
{
"objectID": "CONTENT/EyeTracking/PupilDataAnalysis.html#import-data",
"href": "CONTENT/EyeTracking/PupilDataAnalysis.html#import-data",
"title": "Pupil Data Analysis",
"section": "Import data",
"text": "Import data\nThe data used in this tutorial comes from the pre-processing tutorial of pupil dilation. If you haven’t run that tutorial yet, it’s a good idea to check it out first to ensure your data is prepared and ready for analysis: Pre-processing pupil data. In case you did not save the result of the pre-processing you can download them from here :\n Preprocessed_PupilData.csv \nNow that you have the data, let’s import it along with the necessary libraries. We’ll also ensure that the Event and Subjects columns are properly set as factors (categorical for easier analysis. Here’s how:\n\nlibrary(tidyverse) # Data manipulation and visualization\nlibrary(easystats) # Easy statistical modeling\n\ndata = read.csv(\"resources/Pupillometry/Processed/Processed_PupilData.csv\")\n\n# Make sure Event and Subject are factors\ndata$Event = as.factor(data$Event)\ndata$Subject = as.factor(data$Subject)\n\nhead(data)\n\n X Subject Event TrialN mean_pupil time\n1 1 Subject_1 Square 2 0.04125765 0\n2 2 Subject_1 Square 2 0.10143081 50\n3 3 Subject_1 Square 2 0.12862906 100\n4 4 Subject_1 Square 2 0.16747935 150\n5 5 Subject_1 Square 2 0.21956792 200\n6 6 Subject_1 Square 2 0.27146304 250\n\n\nFor a detailed description of the data, you can have a look at the tutorial on preprocessing pupil data. The key variables to focus on here are the following:\n\nmean_pupil indicates what the pupil size was at every moment in time (every 50 milliseconds, 20Hz). This is our dependent variable.\ntime indicates the specific moment in time within each trial\nTrialN indicates the trial number\nEvent indicates whether the trial contained a circle (followed by a complex stimulus) or a square (not followed by a simple stimulus). This variable is not numerical, but categorical. We thus set it to factor with as.factor().\nSubject contains a different ID number for each subject. This is also a categorical variable.",
"crumbs": [
"Eye-tracking",
"Pupil Data Analysis"
]
},
{
"objectID": "CONTENT/EyeTracking/PupilDataAnalysis.html#comparing-means",
"href": "CONTENT/EyeTracking/PupilDataAnalysis.html#comparing-means",
"title": "Pupil Data Analysis",
"section": "Comparing means",
"text": "Comparing means\nIn many paradigms, you have two or more conditions and want to test whether your dependent variable (pupil size in this case!) is significantly different across conditions. In our example paradigm, we may want to test whether, on average, pupil size while looking at the cue predicting the complex stimulus (the circle) is greater than pupil size while looking at the cue predicting the simple stimulus (the square). If we think of pupil as an index of how interesting or arousing a stimulus will be, this would mean that even before the complex stimulus is presented, infants will dilate their pupil in anticipation of it! Pretty cool, uh?\nIf we want to test multiple groups, we can use a t-test, an ANOVA or… A linear model! Here, we’ll be using a special type of linear model, a mixed-effect model - which is infinitely better for many many reasons [add link].\nAdapt the data\nWe want to compare the means across conditions but… We don’t have means yet! We have a much richer dataset, that contains hundreds of datapoints with milliseconds precision. For this first simple analysis, we just want one average measure of pupil dilation for each trial instead. We can compute this using the tidyverse library (that is container of multiple packages) a powerful collection of packages for wrangling and visualuzating dataframes in R.\nHere, we group the data by Subject, Event, and TrialN, then summarize it within these groups by calculating the mean values.\n\naveraged_data = data %>%\n group_by(Subject, Event, TrialN) %>%\n summarise(mean_pupil = mean(mean_pupil, na.rm = TRUE))\n\nhead(averaged_data)\n\n# A tibble: 6 × 4\n# Groups: Subject, Event [2]\n Subject Event TrialN mean_pupil\n <fct> <fct> <int> <dbl>\n1 Subject_1 Circle 3 0.0397\n2 Subject_1 Circle 4 0.281 \n3 Subject_1 Circle 6 0.0559\n4 Subject_1 Circle 9 -0.0393\n5 Subject_1 Square 2 -0.139 \n6 Subject_1 Square 5 0.0420\n\n\n\n\n\n\n\n\nImportant\n\n\n\nIn this step, we used the average to calculate an index for each trial, meaning we averaged the pupil dilation over the trial duration. However, this is not the only option. Other approaches include extracting the peak value (max(mean_pupil, na.rm = TRUE)) or calculating the sum of the signal (sum(mean_pupil, na.rm = TRUE)), which can also represent the area under the curve (AUC) for the trial.\n\n\nLinear mixed-effects model\nWith a single value for each participant, condition, and trial (averaged across time points), we are now ready to proceed with our analysis. Even if the word “Linear mixed-effect model” might sound scary, the model is actually very simple. We take our experimental conditions (Event) and check whether they affect pupil size (mean_pupil). To account for individual differences in pupil response intensity, we include participants as a random intercept.\nLet’s give it a go!!!\n\nlibrary(lmerTest) # Mixed-effect models library\n\n# The actual model\nmodel_avg = lmer(mean_pupil ~ Event + (1|Subject), data = averaged_data)\n\nsummary(model_avg) # summary of the model\n\nLinear mixed model fit by REML. t-tests use Satterthwaite's method [\nlmerModLmerTest]\nFormula: mean_pupil ~ Event + (1 | Subject)\n Data: averaged_data\n\nREML criterion at convergence: 17.3\n\nScaled residuals: \n Min 1Q Median 3Q Max \n-1.84767 -0.66407 0.02253 0.65595 2.02017 \n\nRandom effects:\n Groups Name Variance Std.Dev.\n Subject (Intercept) 0.0008353 0.0289 \n Residual 0.0709333 0.2663 \nNumber of obs: 55, groups: Subject, 6\n\nFixed effects:\n Estimate Std. Error df t value Pr(>|t|) \n(Intercept) 0.10770 0.05263 17.80244 2.046 0.0558 . \nEventSquare -0.20781 0.07186 48.97131 -2.892 0.0057 **\n---\nSignif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1\n\nCorrelation of Fixed Effects:\n (Intr)\nEventSquare -0.695\n\n\n\n\n\n\n\n\nTip\n\n\n\nWe won’t dive into the detailed interpretation of the results here—this isn’t the right place for that. However, if you are not familiar with linear mixed-effects models you can check our introduction of Linear models and Linear mixed-effects models where we try to break everything down step by step!\n\n\nThe key takeaway here is that there’s a significatn difference between the Event. Specifically, the Square cue appears to result in smaller pupil dilation compared to the Circle event (which serves as the reference level for the intercept). COOL!\nLet’s visualize the effect!!\n\nCode# Create a data grid for Event and time\ndatagrid = get_datagrid(model_avg, by = c('Event'))\n\n# Compute model-based expected values for each level of Event\npred = estimate_expectation(datagrid)\n\n# 'pred' now contains predicted values and confidence intervals for each event condition.\n# We can visualize these predictions and overlay them on the observed data.\n\nggplot() +\n # Observed data (jittered points to show distribution)\n geom_jitter(data = averaged_data, aes(x=Event, y=mean_pupil, color=Event), width=0.1, alpha=0.5, size = 5) +\n \n # Model-based predictions: points for Predicted values\n geom_point(data=pred, aes(x=Event, y=Predicted, fill=Event), \n shape=21, size=10) +\n \n # Error bars for the confidence intervals\n geom_errorbar(data=pred, aes(x=Event, ymin=Predicted - SE, ymax=Predicted + SE, color=Event), \n width=0.2, lwd=1.5) +\n \n theme_bw(base_size = 45)+\n theme(legend.position = 'none') +\n labs(title=\"Predicted Means vs. Observed Data\",\n x=\"Condition\",\n y=\"Baseline-Corrected Pupil Size\")",
"crumbs": [
"Eye-tracking",
"Pupil Data Analysis"
]
},
{
"objectID": "CONTENT/EyeTracking/PupilDataAnalysis.html#analysing-the-time-course-of-pupil-size",
"href": "CONTENT/EyeTracking/PupilDataAnalysis.html#analysing-the-time-course-of-pupil-size",
"title": "Pupil Data Analysis",
"section": "Analysing the time course of pupil size",
"text": "Analysing the time course of pupil size\nAlthough we have seen how to compare mean values of pupil size, our original data was much richer. By taking averages, we made it simpler but we also lost precious information. Usually, it is better to keep the data as rich as possible, even if that might require more complex analyses. Here we’ll show you one example of a more complex analysis: generalised additive models. Fear not though!!! As usual, we will try to break it down in small digestible bites, and you might realise it’s not actually that complicated after all.\nThe key aspect here is that we will stop taking averages, and analyse the time course of pupil dilation instead. We will analyse how it changes over time with precision in the order of milliseconds!! This is exciting!!!\nThis is something that we cannot do with linear models. For example, in this case linear models would assume that, over the course of a trial, pupil size will only increase linearly over time. The model would be something like this:\n\nlinear_model = lmer(mean_pupil ~ Event * time + (1|Subject), data = data) \n\nNote that, compared to the previous model, we have made two changes: First, we have changed the data. While before we were using averages, now we use the richer data set; Second, we added time as a predictor. We are saying that mean_pupil might be changing linearly across time… But this is very silly!!! To understand how silly it is, let’s have a look at the data over time.\n\nCode# Let's first compute average pupil size at each time point by condition\ndata_avg_time = data %>%\n group_by(Event, time) %>%\n summarise(mean_pupil = mean(mean_pupil, na.rm=TRUE))\n\n# Now let's plot these averages over time\nggplot(data_avg_time, aes(x=time, y=mean_pupil, color=Event)) +\n geom_line(lwd=1.5) +\n \n theme_bw(base_size = 45)+\n theme(legend.position = 'bottom',\n legend.title = element_blank()) +\n guides(color = guide_legend(override.aes = list(lwd = 20))) +\n\n labs(x = \"time (ms)\",\n y = \"Baseline-Corrected Pupil Size\") \n\n\n\n\n\n\n\nHere’s the data averaged by condition at each time point. As you can clearly see, pupil dilation doesn’t follow a simple linear increase or decrease; the pattern is much more complex. Let’s see how poorly a simple linear model fits this intricate pattern!\n\nCode# Create a data grid for Event and time\ndatagrid = get_datagrid(linear_model, by = c('Event','time'))\n\n# Estimate expectation and uncertainty (Predicted and SE)\nEst = estimate_expectation(linear_model, datagrid)\n\n# Plot predictions with confidence intervals and the observed data\nggplot() +\n # Real data line\n geom_line(data = data_avg_time, aes(x=time, y=mean_pupil, color=Event), lwd=1.5) +\n \n # Predicted ribbons\n geom_ribbon(data = Est, aes(x=time, ymin = Predicted - SE, ymax = Predicted + SE,\n fill = Event), alpha = 0.2) +\n \n # Predicted lines\n geom_line(data = Est, aes(x=time, y=Predicted, color=Event), lwd=1.8,linetype = \"dashed\") +\n \n theme_bw(base_size = 45)+\n theme(legend.position = 'bottom',\n legend.title = element_blank()) +\n guides(color = guide_legend(override.aes = list(lwd = 20))) +\n labs(title = \"Linear Model Predictions vs. Data\",\n x = \"time (ms)\",\n y = \"Baseline-corrected Pupil Size\")\n\n\n\n\n\n\n\nThe estimates from our model don’t really resemble the actual data! To capture all those non-linear, smooth changes over time, we need a more sophisticated approach. Enter Generalized Additive Models (GAMs)—the perfect tool to save the day!\nGeneralized additive model\nHere, we will not get into all the details of generalized additive models (from now on, GAMs). We will just show one example of how they can be used to model pupil size. To do this, we have to abandon linear models and download a new package instead, mgcv (install.packages(\"mgcv\")). This package is similar to the one we used before for linear models but offers greater flexibility, particularly for modeling time-series data and capturing non-linear relationships.\nWhat are GAMs\nOk, cool! GAMs sound awesome… but you might still be wondering what they actually do. Let me show you an example with some figures—that always helps make things clearer!\n\nCodelibrary(patchwork)\n\n# Parameters\namp <- 1; freq <- 1; phase <- 0; rate <- 100; dur <- 2\ntime <- seq(0, dur, by = 1 / rate)\n\n# Sinusoidal wave with noise\nwave <- amp * sin(2 * pi * freq * time + phase) + rnorm(length(time), mean = 0, sd = 0.2)\n\n\n# Plot\none = ggplot()+\n geom_point(aes(y=wave, x= time),size=3)+\n theme_bw(base_size = 45)+\n labs(y='Data')\n\n\ntwo = ggplot()+\n geom_point(aes(y=wave, x= time),size=3)+\n geom_smooth(aes(y=wave, x= time), method = 'lm', color='black', lwd=1.5)+\n theme_bw(base_size = 45)+\n theme(\n axis.title.y = element_blank(),\n axis.text.y = element_blank()\n )\n\ntree= ggplot()+\n geom_point(aes(y=wave, x= time),size=3)+\n geom_smooth(aes(y=wave, x= time), method = 'gam', color='black', lwd=1.5)+\n theme_bw(base_size = 45)+\n theme(\n axis.title.y = element_blank(),\n axis.text.y = element_blank()\n )\n\none + two + tree\n\n\n\n\n\n\n\nWhen modeling data with many fluctuations, a simple linear model often falls short. In the left plot, we see the raw data with its complex, non-linear pattern. The middle plot illustrates a linear model’s attempt to capture these fluctuations, but it oversimplifies the relationships and fails to reflect the true data structure. Finally, the right plot showcases an additive model, which adapts to the data’s variability by following its fluctuations and accurately capturing the underlying pattern. This demonstrates the strength of additive models in modeling non-linear, smooth changes.\nWell….. This sounds like the same problem we have in our pupil data!!! Let’s go figure\n\n\n\n\n\n\nNote\n\n\n\nLinear models can be extended to capture fluctuations using polynomial terms, but this approach has limitations. Higher-order polynomials can overfit the data, capturing noise instead of meaningful patterns. Additive models, however, use smooth functions like splines to flexibly adapt to data fluctuations without the instability of polynomials, making them a more robust and interpretable choice.\n\n\nRun our GAM\nTo run a GAM, the syntax is relatively similar to what we used in the linear model section.\n\nlibrary(\"mgcv\")\n\n# Additive model\nadditive_model = bam(mean_pupil ~ Event\n + s(time, by=Event, k=20)\n + s(time, Subject, bs='fs', m=1),\n data=data)\n\nLet’s break the formula down:\n\nmean_pupil ~ Event: Here, I treat Condition as a main effect, just like we did before.\ns(time, by=Event, k=20): This is where the magic happens. By wrapping time in s(), we are telling the model: “Don’t assume that changes in pupil size over time are linear. Instead, estimate a smooth, wiggly function.” The argument by=Event means: “Do this separately for each condition, so that each condition gets its own smooth curve over time.” Finally, k=20 controls how wiggly the curve can be (technically, how many ‘knots’ or flexibility points the smoothing function is allowed to have). In practice, we are allowing the model to capture complex, non-linear patterns of pupil size changes over time for each condition.\ns(time, Subject, bs='fs', m=1): Here, we go one step further and acknowledge that each participant might have their own unique shape of the time course. By using bs='fs', I am specifying a ‘factor smooth’, which means: “For each subject, estimate their own smooth function over time.” Setting m=1 is a specific parameter choice that defines how we penalize wiggliness. Essentially, this term is allowing us to capture individual differences in how pupil size changes over time, over and above the general pattern captured by the main smooth. It’s something like the random effect we have seen before in the linear mixed-effect model.\n\nNow that we have run our first GAM, we can see how well it predicts the data!\n\nCode# Data grid\ndatagrid = get_datagrid(additive_model, length = 100, include_random = T)\n\n# Estimate expectation and uncertainty (Predicted and SE)\nEst = estimate_expectation(additive_model, datagrid, exclude=c(\"s(time,Subject)\"))\n\n\n# Plot predictions with confidence intervals and the observed data\nggplot() +\n # Real data line\n geom_line(data = data_avg_time, aes(x=time, y=mean_pupil, color=Event), size=1.5) +\n \n # Predicted ribbons\n geom_ribbon(data = Est, aes(x=time, ymin = CI_low, ymax = CI_high,\n fill = Event), alpha = 0.2) +\n \n # Predicted lines\n geom_line(data = Est, aes(x=time, y=Predicted, color=Event), size=1.8, linetype = \"dashed\") +\n \n theme_bw(base_size = 45)+\n theme(legend.position = 'bottom',\n legend.title = element_blank()) +\n guides(color = guide_legend(override.aes = list(lwd = 20))) +\n labs(title = \"Additive model Predictions vs. Data\",\n x = \"time (ms)\",\n y = \"Baseline-corrected Pupil Size\")\n\n\n\n\n\n\n\nThis looks so much better!!! The line fit so much better to the data!! We can also have a look at whether the effect of our experimental condition is significant:\n\nsummary(additive_model) \n\n\nFamily: gaussian \nLink function: identity \n\nFormula:\nmean_pupil ~ Event + s(time, by = Event, k = 20) + s(time, Subject, \n bs = \"fs\", m = 1)\n\nParametric coefficients:\n Estimate Std. Error t value Pr(>|t|) \n(Intercept) 0.10594 0.03649 2.903 0.00373 ** \nEventSquare -0.20353 0.01333 -15.267 < 2e-16 ***\n---\nSignif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1\n\nApproximate significance of smooth terms:\n edf Ref.df F p-value \ns(time):EventCircle 3.959 4.883 6.306 1.31e-05 ***\ns(time):EventSquare 1.512 1.748 5.269 0.0197 * \ns(time,Subject) 20.334 53.000 4.408 < 2e-16 ***\n---\nSignif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1\n\nR-sq.(adj) = 0.215 Deviance explained = 22.4%\nfREML = 594.43 Scale est. = 0.097128 n = 2200\n\n\nThe fixed effects (Parametric coefficients) show a strong negative effect for EventSquare, indicating that pupil size for Square is significantly lower than for Circle. This suggests that pupil size is greater when expecting a complex stimulus compared to a simple one.\nThe smooth terms indicate whether the non-linear relationships modeled by s() explain significant variance in the data. A significant smooth term confirms that the function captures meaningful, non-linear patterns beyond random noise or simpler terms. While fixed effects are typically more important for hypothesis testing, it’s crucial to ensure the model specification captures the data’s fluctuations accurately.\nYou did it!! You started from a simpler model and little by little you built a very complex Generalized Additive Model!! Amazing work!!!\n\n\n\n\n\n\nWarning\n\n\n\nThis is just a very basic tutorial!\nThere are additional checks and considerations to keep in mind when using additive models to model pupil dilation data. We plan to extend this tutorial over time to include more details.\nLuckily, there are researchers who have already explored and explained these steps thoroughly. This paper, in particular, has greatly informed our approach. It dives deeper into the use of GAMs (still with the mgcv package), reviewing techniques for fitting models, addressing auto-correlations, and ensuring the accuracy and robustness of your GAMs. We highly recommend reading this paper to deepen your understanding!",
"crumbs": [
"Eye-tracking",
"Pupil Data Analysis"
]
},
{
"objectID": "CONTENT/GettingStarted/CreateYourFirstParadigm.html",
"href": "CONTENT/GettingStarted/CreateYourFirstParadigm.html",
"title": "Create your first paradigm",
"section": "",
"text": "We will create a very simple and basic experiment that will be the stepping stone for some of the future tutorials. In the future tutorials we will show you how to extend and make this tutorial in a real experiment.\n\n\n\n\n\n\nStimuli!\n\n\n\nYou can download from here the stimuli that we will use in this example. They are very simple and basic stimuli:\n\na fixation cross\ntwo cues (a circle and a square)\ntwo target stimuli (one simple and one complex)\na sound of winning at an arcade game\na sound of losing at an arcade game\n\n\n\nIn this tutorial, we will create an experiment in which, after the fixation cross, one of the two cues is presented. The cues will indicate whether we will see a complex stimulus or a simple one afterwards and where this will appear. After the circle is presented as a cue, the complex stimulus will be presented on the right. After the square is presented as a cue, the simple stimulus will be presented on the left. Thus, if you follow the cued indication you will be able to predict the location of the next stimuli and whether it will be complex or simple. Here below you can find a graphic representation of the design:\n\n\n\nFirst things first, let’s import the relevant libraries and define the path to where our stimuli are. PsychoPy has a lot of different modules that allow us to interface with different types of stimuli and systems. For this tutorial we need the following:\n\n# Import some libraries from PsychoPy and others\nimport os\nfrom pathlib import Path\nfrom psychopy import core, event, visual,sound\n\n\n\n\nThe next step is to create the window. The window is what we will show the stimuli in; it is the canvas on which to draw objects. For now, we will create a small window of 960*540 pixels. In this way, we will able to see the stimuli and still interact with the rest of our PC interface. In a real experiment, we would probably set the window dimension to the entirety of the display (fullscr=True) and maybe on a secondary screen (screen = 1).\n\n# Winsize\nwinsize = (960, 540)\n\n# create a window\nwin = visual.Window(size = winsize, fullscr=False, units=\"pix\", pos =(0,30), screen=0)\n\nNow let’s import the stimuli that we will present in this tutorial. We have 5 stimuli:\n\na fixation cross that we will use to catch the attention of our participants\na circle that will be our cue that signals a rewarding trial\na square that will be our cue that signals a non-rewarding trial\na complex stimulus that will serve as interesting stimulus\na simple stimulus that will serve as uninteresting stimulus\n\nOn top of these visual stimuli, we will also import two sounds that will help us signal the type of trials. So:\n\na tada! winning sound\na papapaaa! losing sound\n\n\n\n\n\n\n\nTip\n\n\n\nWhen importing a visual stimulus we need to pass to the importing function in which window it will be displayed. In our case, we will pass all of them the “win” window that we just created.\n\n\n\n\n\n\n\n\nPATHS\n\n\n\nWhen working with file paths in Python, it’s important to remember that Windows and macOS/Linux use different conventions for their file paths:\n\nWindowsMac\n\n\nWindows file paths typically use backslashes (\\). However, in Python, a backslash is used as an escape character. To handle this, you have two options:\n\nUse double backslashes to avoid Python interpreting the backslash as an escape character:\n'C:\\\\Users\\\\tomma\\\\Desktop'\nAlternatively, use a raw string by prefixing the path with r, which tells Python to treat backslashes as literal characters:\nr'C:\\Users\\tomma\\Desktop'\n\n\n\nmacOS and Linux use forward slashes (/) for their file paths, which are also compatible with Python’s string handling. You can use the path directly. There’s no need for double slashes or raw strings in macOS/Linux paths.\n'/Users/tomma/Desktop'\n\n\n\n\n\nPython 3.4+ includes the pathlib module which provides a more intuitive, object-oriented approach to working with file paths that works across all operating systems. Once you create a Path object, the most powerful feature of pathlib is the ability to use the forward slash / operator to join paths together. This makes constructing file paths much more intuitive and readable:\n\nfrom pathlib import Path\n\n# Create a path object\nbase_dir = Path('C:/Users/tomma/Desktop') # Works on Windows too!\n\n# Join paths with the / operator\nfull_path = base_dir / 'project' / 'data.csv'\n\n# Convert to string if needed for other libraries\nstr_path = str(full_path)\n\nThe / operator works just like you’d expect when writing file paths: it simply joins path components together, automatically handling separators correctly for your operating system. This means:\n\nNo need to remember to use different separators on different systems (\\ on Windows vs. / on Mac)\nNo issues with escape characters in strings (the \\ in Windows paths normally needs escaping in Python strings)\n\nFor our tutorial, we’ll use pathlib for better cross-platform compatibility, but it’s good to understand how paths work!\n\n\n\n\n#%% Load and prepare stimuli\n\n# Setting the directory of our experiment\nos.chdir(r'C:\\Users\\tomma\\OneDrive - Birkbeck, University of London\\TomassoGhilardi\\PersonalProj\\BCCCD')\n\n# Now create a Path object for the stimuli directory\nstimuli_dir = Path('EXP') / 'Stimuli'\n\n# Load images \nfixation = visual.ImageStim(win, image=str(stimuli_dir / 'fixation.png'), size=(200, 200))\ncircle = visual.ImageStim(win, image=str(stimuli_dir / 'circle.png'), size=(200, 200))\nsquare = visual.ImageStim(win, image=str(stimuli_dir / 'square.png'), size=(200, 200))\ncomplex = visual.ImageStim(win, image=str(stimuli_dir / 'complex.png'), size=(200, 200), pos=(250, 0))\nsimple = visual.ImageStim(win, image=str(stimuli_dir / 'simple.png'), size=(200, 200), pos=(-250, 0))\n\n# Load sound \npresentation_sound = sound.Sound(str(stimuli_dir / 'presentation.wav'))\n\n# List of stimuli\ncues = [circle, square] # put both cues in a list\nrewards = [complex, simple] # put both rewards in a list\n\n# Create a list of trials in which 0 means winning and 1 means losing\nTrials = [0, 1, 0, 0, 1, 0, 1, 1, 0, 1 ]\n\nNote that in this simple experiment, we will present the complex stimulus always on the right and the simple stimulus always on the left that’s why when we import the two target stimuli we set their pos to (250,0) and (-250,0). The first value indicates the number of pixels on the x-axis and the second is the number of pixels on the y-axis.\n\n\n\nNow we want to show a stimulus in the center of our window. To do so, we will have to use the function “draw”. As the name suggests this function draws the stimulus that we want on the window that we have created.\nLet’s start with displaying the fixation cross in the center.\n\n# Draw the fixation\nfixation.draw()\n\nDo you see the fixation cross?????? Probably not!! This is because we have drawn the fixation cross but we have not refreshed the window. Psychopy allows you to draw as many stimuli as you want on a window but the changes are only shown when you “refresh” the window. To do so we need to use the “flip” function.\n\nwin.flip()\n\nPerfect!!!! The fixation cross is there. Before each flip, we need to draw our objects. Otherwise, we will only see the basic window with nothing in it. Let’s try!!! flip the window now.\n\n# Flipping the window (refreshing)\nwin.flip()\n\nThe fixation is gone again! Exactly as predicted. Flipping the window allows us to draw and show something new in each frame. This means that the speed limit of our presentation is the actual frame rate of our display. If we have a 60Hz display we can present an image 60 times in a second.\nSo if we want to present our fixation for an entire second we would have to draw and flip it 60 times (our display has a refresh rate of 60Hz)! Let’s try:\n\nfor _ in range(60):\n fixation.draw()\n win.flip()\nwin.flip() # we re-flip at the end to clean the window\n\nNow we have shown the fixation for 1 second and then it disappeared. Nice!! However, you probably have already figured out that what we have done was unnecessary. If we want to present a static stimulus for 1s we could have just drawn it, flipped the window, and then waited for 1s. But now you have an idea of how to show animated stimuli or even videos!!! AMAZING!!!.\nNow let’s try to show the fixation for 1s by just waiting.\n\nfixation.draw()\nwin.flip()\ncore.wait(1) # wait for 1 second\nwin.flip() # we re-flip at the end to clean the window\n\n\n\n\nWe have seen how to show a stimulus let’s now play the sound that we have imported. This is extremely simple, we can just play() it:\n\npresentation_sound.play()\ncore.wait(2)\n\nGreat now we have played our sond!!\n\n\n\n\n\n\nWarning\n\n\n\nWhen playing a sound the script will continue and will not wait for the sound to have finished playing. So if you play two sounds one after without waiting the two sounds will play overlapping. That’s why we have used core.wait(2) in this example, this tells PsychoPy to wait 2 seconds after starting to play the sound.\n\n\n\n\n\nNow let’s try to put everything we have learned in one place and present one rewarding and one non-rewarding trial:\n\nwe present the fixation for 1s\nwe present one of the two cues for 3s\nwe wait 750ms of blank screen\nwe present the reward or the non-reward depending on the cue for 2s.\n\nIn the end, we also close the window.\n\n###### 1st Trial ######\n\n### Present the fixation\nwin.flip() # we flip to clean the window\n\nfixation.draw()\nwin.flip()\ncore.wait(1) # wait for 1 second\n\n### Present the cue to the complex stimulus\ncircle.draw()\nwin.flip()\ncore.wait(3) # wait for 3 seconds\n\n### Present the complex stimulus \ncomplex.draw()\nwin.flip()\npresentation_sound.play()\ncore.wait(2) # wait for 1 second\nwin.flip() # we re-flip at the end to clean the window\n\n###### 2nd Trial ######\n\n### Present the fixation\nwin.flip() # we flip to clean the window\n\nfixation.draw()\nwin.flip()\ncore.wait(1) # wait for 1 second\n\n### Present the cue to the simple stimulus\nsquare.draw()\nwin.flip()\ncore.wait(3) # wait for 3 seconds\n\n### Present the simple stimulus\nsimple.draw()\nwin.flip()\npresentation_sound.play()\ncore.wait(2) # wait for 2 second\nwin.flip() # we re-flip at the end to clean the window\n\n\nwin.close() # let's close the window at the end of the trial\n\n\n\n\nWe’ve now completed a trial, but having trials run back-to-back is often not ideal. Typically, we insert a brief pause between trials—this is called the Inter-Stimulus Interval (ISI).One common way to implement an ISI is to use a simple wait function, for example:\n\ncore.wait(1)\n\nThis is straightforward, but in some cases you might want more control over the timing. For example, if you need to account for any slight delays during a trial, you can use a clock to measure elapsed time.\nTo implement this ISI, we’ll create a PsychoPy core.Clock(). Once initiated, this clock starts keeping track of time, letting us know how much time has elapsed. After kicking off the clock, we can do some other tasks, then wait for the remaining time to reach our 1-second ISI.\n\n### ISI\nremaining_time = 1 - clock.getTime()\nif remaining_time > 0:\n core.wait(remaining_time)\n\nPerfect—this method essentially waits for 1 second just like a simple core.wait(1). But here’s the cool part: since we’re using the clock, we can run other code in the meantime without stopping everything entirely. This flexibility will be super handy in future tutorials—you’ll see!\nLet’s add this at the end of our trials!!\n\n\n\nFantastic, we’ve nearly have our study! However, studies often don’t run to completion, especially when we’re working with infants and children. More often than not, we need to halt the study prematurely. This could be due to the participant becoming fatigued or distracted, or perhaps we need to tweak some settings.\nHow can we accomplish this? Of course, we could just shut down Python and let the experiment crash… but surely, there’s a more elegant solution… And indeed, there is! In fact, there are numerous methods to achieve this, and we’re going to demonstrate one of the most straightforward and flexible ones to you.\nWe can use the event.getKeys() function to ask Psychopy to report any key that has been pressed during our trial. In our case, we will check if the ESC key has been pressed and if it has, we will simply close the window and stop the study.\n\n### Check for closing experiment\nkeys = event.getKeys() # collect list of pressed keys\nif 'escape' in keys:\n win.close() # close window\n core.quit() # stop study\n\n\n\n\n\n\n\nImportant\n\n\n\nYou can add this check for closing the study at any point during the study. However, we recommend placing it at the end of each trial. This ensures that even if you stop the experiment, you will have a complete trial, making it easier to analyze data since you won’t have any incomplete sections of the study.\nAlso, you can use this same method to pause the study or interact with its progress in general.\n\n\n\n\n\nIn an experiment, we want more than 1 trial. Let’s then create an experiment with 10 trials. We just need to repeat what we have done above multiple times. However, we need to randomize the type of trials, otherwise, it would be too easy to learn. To do so, we will create a list of 0 and 1. where 0 would identify a rewarding trial and 1 would index a non-rewarding trial.\nTo properly utilize this list of 0 and 1, we will need to create other lists of our stimuli. This will make it easier to call the right stimuli depending on the trial. We can do so by:\n\n# Create list of trials in which 0 means winning and 1 means losing\nTrials = [0, 1, 0, 0, 1, 0, 1, 1, 0, 1 ]\n\n# List of stimuli\ncues = [circle, square] # put both cues in a list\ntargets = [complex, simple] # put both rewards in a list\n\nPerfect!! Now we can put all the pieces together and run our experiment.\n\n\n\n\n\n\nCaution\n\n\n\nIn this final script, we will change the dimension of the window we will use. Since in most of the experiments, we will want to use the entire screen to our disposal, we will set fullscr = True when defining the window. In addition, we will also change the position of the rewarding and non-rewarding stimulus since now the window is bigger.\nIf you are testing this script on your laptop and do not want to lose the ability to interact with it until the experiment is finished, keep the same window size and position as the previous lines of code.\n\n\n\n# Import some libraries from PsychoPy and others\nimport os\nfrom pathlib import Path\nfrom psychopy import core, event, visual, sound\n\n#%% Load and prepare stimuli\n\n# Setting the directory of our experiment\nos.chdir(r'<<< YOUR PATH >>>>')\n\n# Now create a Path object for the stimuli directory\nstimuli_dir = Path('EXP') / 'Stimuli'\n\n\n# Winsize\nwinsize = (1920, 1080)\n\n# create a window\nwin = visual.Window(size = winsize, fullscr=False, units=\"pix\", pos =(0,30), screen=1)\n\n\n# Load images \nfixation = visual.ImageStim(win, image=str(stimuli_dir / 'fixation.png'), size=(200, 200))\ncircle = visual.ImageStim(win, image=str(stimuli_dir / 'circle.png'), size=(200, 200))\nsquare = visual.ImageStim(win, image=str(stimuli_dir / 'square.png'), size=(200, 200))\ncomplex = visual.ImageStim(win, image=str(stimuli_dir / 'complex.png'), size=(200, 200), pos=(250, 0))\nsimple = visual.ImageStim(win, image=str(stimuli_dir / 'simple.png'), size=(200, 200), pos=(-250, 0))\n\n# Load sound \npresentation_sound = sound.Sound(str(stimuli_dir / 'presentation.wav'))\n\n# List of stimuli\ncues = [circle, square] # put both cues in a list\ntargets = [complex, simple] # put both rewards in a list\n\n# Create a list of trials in which 0 means winning and 1 means losing\nTrials = [0, 1, 0, 0, 1, 0, 1, 1, 0, 1 ]\n\n\n\n#%% Trials\n\nfor trial in Trials:\n\n ### Present the fixation\n win.flip() # we flip to clean the window\n\n fixation.draw()\n win.flip()\n core.wait(1) # wait for j,1 second\n\n\n ### Present the cue\n cues[trial].draw()\n win.flip()\n core.wait(3) # wait for 3 seconds\n\n\n ### Present the target stimulus\n targets[trial].draw()\n win.flip()\n presentation_sound.play()\n core.wait(2) # wait for 1 second\n win.flip() # we re-flip at the end to clean the window\n\n ### ISI\n clock = core.Clock() # start the clock\n core.wait(1 - clock.getTime()) # wait for remaining time\n \n ### Check for closing experiment\n keys = event.getKeys() # collect list of pressed keys\n if 'escape' in keys:\n win.close() # close window\n core.quit() # stop study\n \nwin.close()\ncore.quit()",
"crumbs": [
"Creating an experiment:"
]
},
{
"objectID": "CONTENT/GettingStarted/CreateYourFirstParadigm.html#preparation",
"href": "CONTENT/GettingStarted/CreateYourFirstParadigm.html#preparation",
"title": "Create your first paradigm",
"section": "",
"text": "First things first, let’s import the relevant libraries and define the path to where our stimuli are. PsychoPy has a lot of different modules that allow us to interface with different types of stimuli and systems. For this tutorial we need the following:\n\n# Import some libraries from PsychoPy and others\nimport os\nfrom pathlib import Path\nfrom psychopy import core, event, visual,sound",
"crumbs": [
"Creating an experiment:"
]
},
{
"objectID": "CONTENT/GettingStarted/CreateYourFirstParadigm.html#stimuli-1",
"href": "CONTENT/GettingStarted/CreateYourFirstParadigm.html#stimuli-1",
"title": "Create your first paradigm",
"section": "",
"text": "The next step is to create the window. The window is what we will show the stimuli in; it is the canvas on which to draw objects. For now, we will create a small window of 960*540 pixels. In this way, we will able to see the stimuli and still interact with the rest of our PC interface. In a real experiment, we would probably set the window dimension to the entirety of the display (fullscr=True) and maybe on a secondary screen (screen = 1).\n\n# Winsize\nwinsize = (960, 540)\n\n# create a window\nwin = visual.Window(size = winsize, fullscr=False, units=\"pix\", pos =(0,30), screen=0)\n\nNow let’s import the stimuli that we will present in this tutorial. We have 5 stimuli:\n\na fixation cross that we will use to catch the attention of our participants\na circle that will be our cue that signals a rewarding trial\na square that will be our cue that signals a non-rewarding trial\na complex stimulus that will serve as interesting stimulus\na simple stimulus that will serve as uninteresting stimulus\n\nOn top of these visual stimuli, we will also import two sounds that will help us signal the type of trials. So:\n\na tada! winning sound\na papapaaa! losing sound\n\n\n\n\n\n\n\nTip\n\n\n\nWhen importing a visual stimulus we need to pass to the importing function in which window it will be displayed. In our case, we will pass all of them the “win” window that we just created.\n\n\n\n\n\n\n\n\nPATHS\n\n\n\nWhen working with file paths in Python, it’s important to remember that Windows and macOS/Linux use different conventions for their file paths:\n\nWindowsMac\n\n\nWindows file paths typically use backslashes (\\). However, in Python, a backslash is used as an escape character. To handle this, you have two options:\n\nUse double backslashes to avoid Python interpreting the backslash as an escape character:\n'C:\\\\Users\\\\tomma\\\\Desktop'\nAlternatively, use a raw string by prefixing the path with r, which tells Python to treat backslashes as literal characters:\nr'C:\\Users\\tomma\\Desktop'\n\n\n\nmacOS and Linux use forward slashes (/) for their file paths, which are also compatible with Python’s string handling. You can use the path directly. There’s no need for double slashes or raw strings in macOS/Linux paths.\n'/Users/tomma/Desktop'\n\n\n\n\n\nPython 3.4+ includes the pathlib module which provides a more intuitive, object-oriented approach to working with file paths that works across all operating systems. Once you create a Path object, the most powerful feature of pathlib is the ability to use the forward slash / operator to join paths together. This makes constructing file paths much more intuitive and readable:\n\nfrom pathlib import Path\n\n# Create a path object\nbase_dir = Path('C:/Users/tomma/Desktop') # Works on Windows too!\n\n# Join paths with the / operator\nfull_path = base_dir / 'project' / 'data.csv'\n\n# Convert to string if needed for other libraries\nstr_path = str(full_path)\n\nThe / operator works just like you’d expect when writing file paths: it simply joins path components together, automatically handling separators correctly for your operating system. This means:\n\nNo need to remember to use different separators on different systems (\\ on Windows vs. / on Mac)\nNo issues with escape characters in strings (the \\ in Windows paths normally needs escaping in Python strings)\n\nFor our tutorial, we’ll use pathlib for better cross-platform compatibility, but it’s good to understand how paths work!\n\n\n\n\n#%% Load and prepare stimuli\n\n# Setting the directory of our experiment\nos.chdir(r'C:\\Users\\tomma\\OneDrive - Birkbeck, University of London\\TomassoGhilardi\\PersonalProj\\BCCCD')\n\n# Now create a Path object for the stimuli directory\nstimuli_dir = Path('EXP') / 'Stimuli'\n\n# Load images \nfixation = visual.ImageStim(win, image=str(stimuli_dir / 'fixation.png'), size=(200, 200))\ncircle = visual.ImageStim(win, image=str(stimuli_dir / 'circle.png'), size=(200, 200))\nsquare = visual.ImageStim(win, image=str(stimuli_dir / 'square.png'), size=(200, 200))\ncomplex = visual.ImageStim(win, image=str(stimuli_dir / 'complex.png'), size=(200, 200), pos=(250, 0))\nsimple = visual.ImageStim(win, image=str(stimuli_dir / 'simple.png'), size=(200, 200), pos=(-250, 0))\n\n# Load sound \npresentation_sound = sound.Sound(str(stimuli_dir / 'presentation.wav'))\n\n# List of stimuli\ncues = [circle, square] # put both cues in a list\nrewards = [complex, simple] # put both rewards in a list\n\n# Create a list of trials in which 0 means winning and 1 means losing\nTrials = [0, 1, 0, 0, 1, 0, 1, 1, 0, 1 ]\n\nNote that in this simple experiment, we will present the complex stimulus always on the right and the simple stimulus always on the left that’s why when we import the two target stimuli we set their pos to (250,0) and (-250,0). The first value indicates the number of pixels on the x-axis and the second is the number of pixels on the y-axis.",
"crumbs": [
"Creating an experiment:"
]
},
{
"objectID": "CONTENT/GettingStarted/CreateYourFirstParadigm.html#show-a-visual-stimulus",
"href": "CONTENT/GettingStarted/CreateYourFirstParadigm.html#show-a-visual-stimulus",
"title": "Create your first paradigm",
"section": "",
"text": "Now we want to show a stimulus in the center of our window. To do so, we will have to use the function “draw”. As the name suggests this function draws the stimulus that we want on the window that we have created.\nLet’s start with displaying the fixation cross in the center.\n\n# Draw the fixation\nfixation.draw()\n\nDo you see the fixation cross?????? Probably not!! This is because we have drawn the fixation cross but we have not refreshed the window. Psychopy allows you to draw as many stimuli as you want on a window but the changes are only shown when you “refresh” the window. To do so we need to use the “flip” function.\n\nwin.flip()\n\nPerfect!!!! The fixation cross is there. Before each flip, we need to draw our objects. Otherwise, we will only see the basic window with nothing in it. Let’s try!!! flip the window now.\n\n# Flipping the window (refreshing)\nwin.flip()\n\nThe fixation is gone again! Exactly as predicted. Flipping the window allows us to draw and show something new in each frame. This means that the speed limit of our presentation is the actual frame rate of our display. If we have a 60Hz display we can present an image 60 times in a second.\nSo if we want to present our fixation for an entire second we would have to draw and flip it 60 times (our display has a refresh rate of 60Hz)! Let’s try:\n\nfor _ in range(60):\n fixation.draw()\n win.flip()\nwin.flip() # we re-flip at the end to clean the window\n\nNow we have shown the fixation for 1 second and then it disappeared. Nice!! However, you probably have already figured out that what we have done was unnecessary. If we want to present a static stimulus for 1s we could have just drawn it, flipped the window, and then waited for 1s. But now you have an idea of how to show animated stimuli or even videos!!! AMAZING!!!.\nNow let’s try to show the fixation for 1s by just waiting.\n\nfixation.draw()\nwin.flip()\ncore.wait(1) # wait for 1 second\nwin.flip() # we re-flip at the end to clean the window",
"crumbs": [
"Creating an experiment:"
]
},
{
"objectID": "CONTENT/GettingStarted/CreateYourFirstParadigm.html#play-a-sound",
"href": "CONTENT/GettingStarted/CreateYourFirstParadigm.html#play-a-sound",
"title": "Create your first paradigm",
"section": "",
"text": "We have seen how to show a stimulus let’s now play the sound that we have imported. This is extremely simple, we can just play() it:\n\npresentation_sound.play()\ncore.wait(2)\n\nGreat now we have played our sond!!\n\n\n\n\n\n\nWarning\n\n\n\nWhen playing a sound the script will continue and will not wait for the sound to have finished playing. So if you play two sounds one after without waiting the two sounds will play overlapping. That’s why we have used core.wait(2) in this example, this tells PsychoPy to wait 2 seconds after starting to play the sound.",
"crumbs": [
"Creating an experiment:"
]
},
{
"objectID": "CONTENT/GettingStarted/CreateYourFirstParadigm.html#create-a-trial",
"href": "CONTENT/GettingStarted/CreateYourFirstParadigm.html#create-a-trial",
"title": "Create your first paradigm",
"section": "",
"text": "Now let’s try to put everything we have learned in one place and present one rewarding and one non-rewarding trial:\n\nwe present the fixation for 1s\nwe present one of the two cues for 3s\nwe wait 750ms of blank screen\nwe present the reward or the non-reward depending on the cue for 2s.\n\nIn the end, we also close the window.\n\n###### 1st Trial ######\n\n### Present the fixation\nwin.flip() # we flip to clean the window\n\nfixation.draw()\nwin.flip()\ncore.wait(1) # wait for 1 second\n\n### Present the cue to the complex stimulus\ncircle.draw()\nwin.flip()\ncore.wait(3) # wait for 3 seconds\n\n### Present the complex stimulus \ncomplex.draw()\nwin.flip()\npresentation_sound.play()\ncore.wait(2) # wait for 1 second\nwin.flip() # we re-flip at the end to clean the window\n\n###### 2nd Trial ######\n\n### Present the fixation\nwin.flip() # we flip to clean the window\n\nfixation.draw()\nwin.flip()\ncore.wait(1) # wait for 1 second\n\n### Present the cue to the simple stimulus\nsquare.draw()\nwin.flip()\ncore.wait(3) # wait for 3 seconds\n\n### Present the simple stimulus\nsimple.draw()\nwin.flip()\npresentation_sound.play()\ncore.wait(2) # wait for 2 second\nwin.flip() # we re-flip at the end to clean the window\n\n\nwin.close() # let's close the window at the end of the trial",
"crumbs": [
"Creating an experiment:"
]
},
{
"objectID": "CONTENT/GettingStarted/CreateYourFirstParadigm.html#isi",
"href": "CONTENT/GettingStarted/CreateYourFirstParadigm.html#isi",
"title": "Create your first paradigm",
"section": "",
"text": "We’ve now completed a trial, but having trials run back-to-back is often not ideal. Typically, we insert a brief pause between trials—this is called the Inter-Stimulus Interval (ISI).One common way to implement an ISI is to use a simple wait function, for example:\n\ncore.wait(1)\n\nThis is straightforward, but in some cases you might want more control over the timing. For example, if you need to account for any slight delays during a trial, you can use a clock to measure elapsed time.\nTo implement this ISI, we’ll create a PsychoPy core.Clock(). Once initiated, this clock starts keeping track of time, letting us know how much time has elapsed. After kicking off the clock, we can do some other tasks, then wait for the remaining time to reach our 1-second ISI.\n\n### ISI\nremaining_time = 1 - clock.getTime()\nif remaining_time > 0:\n core.wait(remaining_time)\n\nPerfect—this method essentially waits for 1 second just like a simple core.wait(1). But here’s the cool part: since we’re using the clock, we can run other code in the meantime without stopping everything entirely. This flexibility will be super handy in future tutorials—you’ll see!\nLet’s add this at the end of our trials!!",
"crumbs": [
"Creating an experiment:"
]
},
{
"objectID": "CONTENT/GettingStarted/CreateYourFirstParadigm.html#stop-the-experiment",
"href": "CONTENT/GettingStarted/CreateYourFirstParadigm.html#stop-the-experiment",
"title": "Create your first paradigm",
"section": "",
"text": "Fantastic, we’ve nearly have our study! However, studies often don’t run to completion, especially when we’re working with infants and children. More often than not, we need to halt the study prematurely. This could be due to the participant becoming fatigued or distracted, or perhaps we need to tweak some settings.\nHow can we accomplish this? Of course, we could just shut down Python and let the experiment crash… but surely, there’s a more elegant solution… And indeed, there is! In fact, there are numerous methods to achieve this, and we’re going to demonstrate one of the most straightforward and flexible ones to you.\nWe can use the event.getKeys() function to ask Psychopy to report any key that has been pressed during our trial. In our case, we will check if the ESC key has been pressed and if it has, we will simply close the window and stop the study.\n\n### Check for closing experiment\nkeys = event.getKeys() # collect list of pressed keys\nif 'escape' in keys:\n win.close() # close window\n core.quit() # stop study\n\n\n\n\n\n\n\nImportant\n\n\n\nYou can add this check for closing the study at any point during the study. However, we recommend placing it at the end of each trial. This ensures that even if you stop the experiment, you will have a complete trial, making it easier to analyze data since you won’t have any incomplete sections of the study.\nAlso, you can use this same method to pause the study or interact with its progress in general.",
"crumbs": [
"Creating an experiment:"
]
},
{
"objectID": "CONTENT/GettingStarted/CreateYourFirstParadigm.html#create-an-entire-experiment",
"href": "CONTENT/GettingStarted/CreateYourFirstParadigm.html#create-an-entire-experiment",
"title": "Create your first paradigm",
"section": "",
"text": "In an experiment, we want more than 1 trial. Let’s then create an experiment with 10 trials. We just need to repeat what we have done above multiple times. However, we need to randomize the type of trials, otherwise, it would be too easy to learn. To do so, we will create a list of 0 and 1. where 0 would identify a rewarding trial and 1 would index a non-rewarding trial.\nTo properly utilize this list of 0 and 1, we will need to create other lists of our stimuli. This will make it easier to call the right stimuli depending on the trial. We can do so by:\n\n# Create list of trials in which 0 means winning and 1 means losing\nTrials = [0, 1, 0, 0, 1, 0, 1, 1, 0, 1 ]\n\n# List of stimuli\ncues = [circle, square] # put both cues in a list\ntargets = [complex, simple] # put both rewards in a list\n\nPerfect!! Now we can put all the pieces together and run our experiment.\n\n\n\n\n\n\nCaution\n\n\n\nIn this final script, we will change the dimension of the window we will use. Since in most of the experiments, we will want to use the entire screen to our disposal, we will set fullscr = True when defining the window. In addition, we will also change the position of the rewarding and non-rewarding stimulus since now the window is bigger.\nIf you are testing this script on your laptop and do not want to lose the ability to interact with it until the experiment is finished, keep the same window size and position as the previous lines of code.\n\n\n\n# Import some libraries from PsychoPy and others\nimport os\nfrom pathlib import Path\nfrom psychopy import core, event, visual, sound\n\n#%% Load and prepare stimuli\n\n# Setting the directory of our experiment\nos.chdir(r'<<< YOUR PATH >>>>')\n\n# Now create a Path object for the stimuli directory\nstimuli_dir = Path('EXP') / 'Stimuli'\n\n\n# Winsize\nwinsize = (1920, 1080)\n\n# create a window\nwin = visual.Window(size = winsize, fullscr=False, units=\"pix\", pos =(0,30), screen=1)\n\n\n# Load images \nfixation = visual.ImageStim(win, image=str(stimuli_dir / 'fixation.png'), size=(200, 200))\ncircle = visual.ImageStim(win, image=str(stimuli_dir / 'circle.png'), size=(200, 200))\nsquare = visual.ImageStim(win, image=str(stimuli_dir / 'square.png'), size=(200, 200))\ncomplex = visual.ImageStim(win, image=str(stimuli_dir / 'complex.png'), size=(200, 200), pos=(250, 0))\nsimple = visual.ImageStim(win, image=str(stimuli_dir / 'simple.png'), size=(200, 200), pos=(-250, 0))\n\n# Load sound \npresentation_sound = sound.Sound(str(stimuli_dir / 'presentation.wav'))\n\n# List of stimuli\ncues = [circle, square] # put both cues in a list\ntargets = [complex, simple] # put both rewards in a list\n\n# Create a list of trials in which 0 means winning and 1 means losing\nTrials = [0, 1, 0, 0, 1, 0, 1, 1, 0, 1 ]\n\n\n\n#%% Trials\n\nfor trial in Trials:\n\n ### Present the fixation\n win.flip() # we flip to clean the window\n\n fixation.draw()\n win.flip()\n core.wait(1) # wait for j,1 second\n\n\n ### Present the cue\n cues[trial].draw()\n win.flip()\n core.wait(3) # wait for 3 seconds\n\n\n ### Present the target stimulus\n targets[trial].draw()\n win.flip()\n presentation_sound.play()\n core.wait(2) # wait for 1 second\n win.flip() # we re-flip at the end to clean the window\n\n ### ISI\n clock = core.Clock() # start the clock\n core.wait(1 - clock.getTime()) # wait for remaining time\n \n ### Check for closing experiment\n keys = event.getKeys() # collect list of pressed keys\n if 'escape' in keys:\n win.close() # close window\n core.quit() # stop study\n \nwin.close()\ncore.quit()",
"crumbs": [
"Creating an experiment:"
]
},
{
"objectID": "CONTENT/GettingStarted/GettingStartedWithPsychopy.html",
"href": "CONTENT/GettingStarted/GettingStartedWithPsychopy.html",
"title": "Starting with PsychoPy",
"section": "",
"text": "PsychoPy is an open-source software package written in the Python programming language primarily for use in neuroscience and experimental psychology research. It’s one of our favorite ways to create experiments and we will use it through our tutorials.\nSo, let’s start and install PsychoPy!!!",
"crumbs": [
"Getting started",
"Starting with PsychoPy"
]
},
{
"objectID": "CONTENT/GettingStarted/GettingStartedWithPsychopy.html#miniconda",
"href": "CONTENT/GettingStarted/GettingStartedWithPsychopy.html#miniconda",
"title": "Starting with PsychoPy",
"section": "Miniconda",
"text": "Miniconda\nWe recommend installing PsychoPy in a dedicated virtual environment where you can create and run your studies. To ensure optimal performance and avoid compatibility issues, keep this environment as clean as possible. Ideally, install PsychoPy and only the additional libraries required for your study.\n\nWindowsMac\n\n\nOpen your anaconda terminal and create a new environment:\nconda create -n psychopy python=3.10\nConda will ask you to confirm with a y and will create a new environment called psychopy.\nConda will also tell you that if you want to interact and install packages in the newly created environment, you should activate it. Let’s do it!!! Type:\nconda activate psychpy\nYou’ll see that the parenthesis at the command line will change from (base) to (psychopy). This means we are in!!! Now we can install any package and those packages will be contained in this new environment.\nAs mentioned before, we will use PsychoPy to create our experimental design!! So let’s install it (and also let’s install DeToX, our package to run eye-tracking studies). Type:\npip install psychopy dvst-detox ipykernel\nConda will think and download stuff for a while!! Do not worry if you see a lot of lines popping up! Give conda time and confirm if prompted. After a while, hopefully you will see that everything went well and now you have a nice psychopy environment with PsychoPy in it!!\n\n\nTo create a new environment open your terminal and type:\nconda create -n psychopy python=3.10\nConda will ask you to confirm with a y and will create a new environment called psychopy.\nConda will also tell you that if you want to interact and install packages in the newly created environment, you should activate it. Let’s do it!!! Type:\nconda activate psychpy\nYou’ll see that the parenthesis at the command line will change from (base) to (psychopy). This means we are in!!! Now we can install any package and those packages will be contained in this new environment.\nAs mentioned before, we will use PsychoPy to create our experimental design!! So let’s install it (and also let’s install DeToX, our package to run eye-tracking studies). Type:\npip install psychopy dvst-detox ipykernel\nConda will think and download stuff for a while!! Do not worry if you see a lot of lines popping up! Give conda time and confirm if prompted. After a while, hopefully you will see that everything went well and now you have a nice psychopy environment with PsychoPy in it!!\n\n\n\nFlowchart",
"crumbs": [
"Getting started",
"Starting with PsychoPy"
]
},
{
"objectID": "CONTENT/GettingStarted/GettingStartedWithPsychopy.html#psychopy-standalone",
"href": "CONTENT/GettingStarted/GettingStartedWithPsychopy.html#psychopy-standalone",
"title": "Starting with PsychoPy",
"section": "Psychopy Standalone",
"text": "Psychopy Standalone\nInstalling PsychoPy as a library sometimes can be tricky as it depends a lot on which OS you are on, which hardware that OS has, and a lot of dependencies. While we prefer to install PsychoPy as a library and use it through Spyder, it is not always possible.\nHowever, there is a simple solution. PsychoPy offers a standalone version. This version comes packaged with everything that is needed to run your experiments and includes the PsychoPy GUI.\nIt is usually easier and less error-prone, but it offers less flexibility in some aspects. However, for this study it will be plenty enough! So in case you had any problem installing PsychoPy in a separate environment, we suggest you install it as a standalone.\nYou can find it herse. Please download and install it following your operating system guidelines!\nWell done!! Now you should have the PsychoPy icon on your machine!!\nFYI! It’s important to note that PsychoPy offers two main ways to create experiments:\n\nThe Builder: Ideal for those who prefer a graphical, point-and-click interface. \nThe Coder: Designed for users who prefer to program their experiments from scratch. \n\nIn our tutorials, we will focus on coding experiments directly. If you’re using the PsychoPy standalone installer, you’ll need to follow along using the Coder interface:\n\n\n\n\n\nInstall DeToX PsychoPy doesn’t include DeToX by default, so we’ll need to install it separately. The easiest way is through PsychoPy’s built-in package manager:\n\nOpen PsychoPy\nSwitch to Coder View (the interface with the code editor)\nOpen the Tools menu\nSelect “Plugins/package manager…”\nClick on the “Packages” tab at the top\nClick the “Open PIP terminal” button\nIn the terminal, type: install dvst-detox\nPress Enter and wait for the installation to complete\n\nThat’s it! You now have both PsychoPy and DeToX installed and ready to use for your eye-tracking studies..\nFlowchart",
"crumbs": [
"Getting started",
"Starting with PsychoPy"
]
},
{
"objectID": "CONTENT/GettingStarted/GettingStartedWithR.html",
"href": "CONTENT/GettingStarted/GettingStartedWithR.html",
"title": "Starting with R",
"section": "",
"text": "R is THE BEST programming language for statistics. We are ready to fight you on this! We’ll use R extensively throughout the website, especially for statistical modeling, data preprocessing, creating beautiful visualizations, and running complex analyses like mixed-effects models. Whether you’re analyzing reaction times, eye-tracking data, or running sophisticated statistical tests, R will be your best friend. So… on this page, we’ll guide you through installing R and Positron (one of its best IDEs), plus all the essential libraries you’ll need for our tutorials. Don’t worry – these steps are straightforward and easy to follow.\n\n\n\n\n\n\nTip\n\n\n\nWe strongly recommend using our interactive flowchart to guide yourself through the tutorials! It will help you follow the setup steps in the right order based on your experience level. Flowchart\n\n\nInstall R\n Let’s start..:\n\nGo to this page (CRAN stands for Comprehensive R Archive Network – it’s the official repository for R)\nDownload R for your operating system\nRun the installer and follow the on-screen instructions (just keep clicking “Next” – the defaults are perfectly fine)\n\nWell… that really was easy! R is now installed on your computer, but we’re not quite done yet.\nFlowchart\nPositron\n\n\n\n\n\n\nNote\n\n\n\nIf you have already follow the getting started with Python documentation you shoudl already have positron installed and you can skip this step!\n\n\nWhile the R installation includes a basic IDE (think of it as a very plain text editor), we definitely want to use something more refined. While Rstudio has been our favorite IDE for a while we now prefer Positron. Positron is a IDE ( based on a fork of Vs Code) that allows to run and interact efficiently with both R and Python and has become our IDE of choice lately.\nInstalling Positron is straightforward:\n\nGo to the Positron website\nFollow the instructions and download Positron for your operating system\nRun the installer and follow the on-screen instructions\n\nOnce installed, you can launch Positron. Here it is in all its glory:\n\n\n\n\nThe first time you launch Positron, you’ll see a “Start Session” button in the top-right corner. (If it’s not your first time, you’ll instead see a button showing which environment you’re currently using.)\nClick it—and voilà!—you’ll get a list of all your R interpreters and Python environments. Pretty neat, right?\nJust pick the R interpreter you want to work in (you probably have just one), and the console at the bottom will instantly switch to that setup. From that moment on, any code you run in Positron will use the exact R version and packages installed in it.\nThat’s it—now any code you run will execute within the environment you selected. Well done!\nFlowchart\nInstalling R packages\nNow that we have both R and Positron, we need some essential libraries (called “packages” in R). I’ll describe only a few of the main ones here, but more will be installed and covered throughout the tutorials. Core Packages:\n\ntidyverse: This is a collection of packages, so when you install and import it, you’re not just getting one library but an entire suite of them. It includes ggplot2 (for stunning visualizations), dplyr (for data manipulation), readr (for reading data), and many more. Perfect for data wrangling and creating beautiful plots. This is probably the most important package ecosystem in R. Explore all the sub-packages to learn more!\neasystats: A user-friendly collection that makes statistical analysis much simpler and more intuitive. It includes packages for model fitting, reporting, and interpretation. Explore all the sub-packages to learn more!\nlme4: The go-to package for fitting linear and generalized linear mixed-effects models. Essential for analyzing complex experimental data where you have repeated measures or hierarchical data structures (which is pretty much every psychology experiment).\npupillometryR: Specialized package for preprocessing and analyzing pupillometry data. A lifesaver if you’re working with eye-tracking studies.\n\nInstalling the packages is extremely easy. In the R console we just activated paste:\n\ninstall.packages(c(\"tidyverse\", \"easystats\", \"lme4\", \"lmerTest\", \"patchwork\", \"PupillometryR\", \"fitdistrplus\"))\n\nFollow the instructions if any and you are done!!!!\n\n\n\n\n\n\nAdditional Packages\n\n\n\nWe may use additional packages in specific tutorials, but don’t worry – we’ll always mention which ones you need and provide clear installation instructions when the time comes!\n\n\nFlowchart\nTest your installation\n🎉 Congratulations! You should now have R and Positron fully set up with all essential packages!\nBut let’s make sure everything is working perfectly before we dive into the tutorials. Want to test whether all packages installed correctly and you’re ready to go? We’ve created a test script that checks whether all is ok.\nDownload our test script and run it in Positron. If everything is working properly, you’ll see detailed progress updates, sample results, and a nice completion message with a celebration plot at the end. If there are any issues, the script will try to tell you what is wrong.\n📥 Download Test Script\nHow to use the script:\n\nDownload the file above\nOpen Positron\nOpen the downloaded TestInstallationR.R file\nSelect all the code and run it (or press Ctrl+A then Ctrl/Cmd+Enter)\nWatch the magic happen!\n\nIf you see the celebration plot at the end, you’re officially ready to tackle all our R tutorials!\n\n\n\n\n Back to top",
"crumbs": [
"Getting started",
"Starting with R"
]
},
{
"objectID": "CONTENT/Stats/LinearMixedModels.html",
"href": "CONTENT/Stats/LinearMixedModels.html",
"title": "Linear mixed effect models",
"section": "",
"text": "Welcome to this introduction to Linear Mixed-effects Models (LMM)!! In this tutorial, we will use R to run some simple LMMs, and we will try to understand together how to leverage these models for our analysis.\n LMMs are amazing tools that have saved our asses countless times during our PhDs and Postdocs. They'll probably continue to be our trusty companions forever.\nThis tutorial introduces the statistical concept of hierarchical modeling, often called mixed-effects modeling. This approach shines when dealing with nested data: situations where observations are grouped, like multiple classes within a school or multiple measurements for each individual.\nThis might not mean a lot to you: why bother with understanding the structure of the data? Can’t we just run our simple linear models we’ve already learned about? Well well well, let us introduce you to the Simpson’s Paradox.\nImagine we’re looking at how years of experience impact salary at a university:\nCodelibrary(easystats)\nlibrary(tidyverse)\nlibrary(patchwork)\nset.seed(1234)\ndata <- simulate_simpson(n = 10, groups = 5, r = 0.5,difference = 1.5) %>% \n mutate(V2= (V2 +abs(min(V2)))*10000) %>% \n rename(Department = Group)\n\n# Lookup vector: map old values to new ones\nlookup <- c(G_1 = \"Informatics\", G_2 = \"English\", \n G_3 = \"Sociology\", G_4 = \"Biology\", G_5 = \"Statistics\")\n\n# Replace values using the lookup vector\ndata$Department <- lookup[as.character(data$Department)]\n\n\none = ggplot(data, aes(x = V1, y = V2)) +\n geom_point()+\n geom_smooth(method = 'lm')+\n labs(y='Salary', x='Year of experience', title = \"A. Linear model\")+\n theme_bw(base_size = 20)\n\ntwo = ggplot(data, aes(x = V1, y = V2)) +\n geom_point(aes(color = Department)) +\n geom_smooth(aes(color = Department), method = \"lm\", alpha = 0.3) +\n geom_smooth(method = \"lm\", alpha = 0.3)+\n labs(y='Salary', x='Year of experience', title = \"B. Linear model acounting for grouping structure\")+\n theme_bw(base_size = 20)+\n theme(legend.position = 'bottom')\n\n(one / two)\nTake a look at the first plot. Whoa, wait a minute: this says the more years of experience you have, the less you get paid! What kind of backwards world is this? Before you march into HR demanding answers, let’s look a little closer.\nNow, check out the second plot. Once we consider the departments (Informatics, English, Sociology, Biology, and Statistics) a different story emerges. Each department shows a positive trend between experience and salary. In other words, more experience does mean higher pay, as it should!\nSo what happened? Well, Simpson’s Paradox happened! By ignoring meaningful structure in the data (lumping all departments together), we might completely miss the real relationship hiding in plain sight. This is why hierarchical modeling is so powerful: it allows us to correctly analyze data with nested structures and uncover the real patterns within them.",
"crumbs": [
"Stats",
"Linear mixed effect models"
]
},
{
"objectID": "CONTENT/Stats/LinearMixedModels.html#settings-and-data",
"href": "CONTENT/Stats/LinearMixedModels.html#settings-and-data",
"title": "Linear mixed effect models",
"section": "Settings and data",
"text": "Settings and data\nIn this section, we’ll set up our working environment by loading the necessary libraries and importing the dataset. You’ll likely already have this dataset available if you completed the previous linear models tutorial. If not, don’t worry—you can easily download it using the link below:\n\n\nlibrary(lme4)\nlibrary(lmerTest)\n\nlibrary(tidyverse)\nlibrary(easystats)\n\nThe lme4 package is the go-to library for running Linear Mixed Models (LMM) in R. To make your life easier, there’s also the lmerTest package, which enhances lme4 by allowing you to extract p-values and providing extra functions to better understand your models. In my opinion, you should always use lmerTest alongside lme4—it just makes everything smoother!\nTo run our Linear Mixed Effects Model, these are the key packages we’ll use. On top of that, the tidyverse suite will help with data wrangling and visualization, while easystats will let us easily extract and summarize the important details from our models. Let’s get started!\nRead the data\n\nCodedf = read.csv(\"resources/Stats/Dataset.csv\")\ndf$Id = factor(df$Id) # make sure subject_id is a factor\ndf$StandardTrialN = standardize(df$TrialN) # create standardize trial column\n\n\nAfter importing the data, we’ve ensured that Id is treated as a factor rather than a numerical column and that we have a standardized column of TrialN.",
"crumbs": [
"Stats",
"Linear mixed effect models"
]
},
{
"objectID": "CONTENT/Stats/LinearMixedModels.html#linear-model",
"href": "CONTENT/Stats/LinearMixedModels.html#linear-model",
"title": "Linear mixed effect models",
"section": "Linear Model",
"text": "Linear Model\nWhile we have already seen how to run a linear model, we will rerun it here as a comparison to the next steps. In case something is not clear about this lm(), please go back to the previous tutorial on linear models.\n\nmod_lm = lm(LookingTime ~ StandardTrialN*Event, data = df)\nsummary(mod_lm)\n\n\nCall:\nlm(formula = LookingTime ~ StandardTrialN * Event, data = df)\n\nResiduals:\n Min 1Q Median 3Q Max \n-609.15 -212.47 13.17 218.58 676.70 \n\nCoefficients:\n Estimate Std. Error t value Pr(>|t|) \n(Intercept) 1419.44 19.51 72.740 < 2e-16 ***\nStandardTrialN -71.50 19.41 -3.683 0.000264 ***\nEventSimple -266.39 27.53 -9.677 < 2e-16 ***\nStandardTrialN:EventSimple -50.01 27.28 -1.833 0.067605 . \n---\nSignif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1\n\nResidual standard error: 268.1 on 376 degrees of freedom\n (20 observations deleted due to missingness)\nMultiple R-squared: 0.2892, Adjusted R-squared: 0.2835 \nF-statistic: 50.99 on 3 and 376 DF, p-value: < 2.2e-16\n\n\nWe won’t delve into the details of the model results in this tutorial, as we have already covered it in the previous one. However, we want to point out one thing about the data we run it on!!\n\nCodeprint_html(data_codebook(df))\n\n\n\n\n\n\ndf (400 rows and 7 variables, 7 shown)\n\n\nID\nName\nType\nMissings\nValues\nN\n\n\n\n\n1\nId\ncategorical\n0 (0.0%)\n1\n20 (5.0%)\n\n\n\n\n\n\n2\n20 (5.0%)\n\n\n\n\n\n\n3\n20 (5.0%)\n\n\n\n\n\n\n4\n20 (5.0%)\n\n\n\n\n\n\n5\n20 (5.0%)\n\n\n\n\n\n\n6\n20 (5.0%)\n\n\n\n\n\n\n7\n20 (5.0%)\n\n\n\n\n\n\n8\n20 (5.0%)\n\n\n\n\n\n\n9\n20 (5.0%)\n\n\n\n\n\n\n10\n20 (5.0%)\n\n\n\n\n\n\n(...)\n\n\n\n2\nEvent\ncharacter\n0 (0.0%)\nComplex\n202 (50.5%)\n\n\n\n\n\n\nSimple\n198 (49.5%)\n\n\n3\nTrialN\ninteger\n0 (0.0%)\n[1, 20]\n400\n\n\n4\nSaccadicRT\nnumeric\n139 (34.8%)\n[189.96, 927.5]\n261\n\n\n5\nLookingTime\nnumeric\n20 (5.0%)\n[400, 1980]\n380\n\n\n6\nSES\ncharacter\n0 (0.0%)\nhigh\n120 (30.0%)\n\n\n\n\n\n\nlow\n160 (40.0%)\n\n\n\n\n\n\nmedium\n120 (30.0%)\n\n\n7\nStandardTrialN\nnumeric\n0 (0.0%)\n[-1.65, 1.65]\n400\n\n\n\n\n\n\nWait a minute! Look at our data - we have an Id column! 👀 This column tells us which participant each trial belongs to. As each subject experienced all trial conditions, we have multiple data points per person. This is similar to the departments in the previous example… it’s a grouping variable\nWait..but then we should have taken it into consideration!!!\nInstead, there was nothing about Id in our lm()…there is nothing in the formula about Id….\nYes, we did not account for this grouping structure…let’s fix that!! But how do we do so? Well, at this point it’s obvious…with Mixed effects models!! Let’s dive in..",
"crumbs": [
"Stats",
"Linear mixed effect models"
]
},
{
"objectID": "CONTENT/Stats/LinearMixedModels.html#mixed-effects",
"href": "CONTENT/Stats/LinearMixedModels.html#mixed-effects",
"title": "Linear mixed effect models",
"section": "Mixed Effects",
"text": "Mixed Effects\nRandom Intercept\nAlright, let’s start with Random Intercepts! What are they? Well, the name gives it away—they’re just intercepts…but with a twist! 🤔\nIf you recall your knowledge of linear models, you’ll remember that each model has one intercept—the point where the model crosses the y-axis (when x=0).\nBut what makes random intercepts special? They allow the model to have different intercepts for each grouping variable—in this case, the Ids. This means we’re letting the model assume that each subject may have a slightly different baseline performance.\nHere’s the idea:\n\nOne person might naturally be a bit better.\nSomeone else could be slightly worse.\nAnd me? Well, let’s just say I’m starting from rock bottom.\n\nHowever, even though we’re starting from different baselines, the rate of improvement over trials can still be consistent across subjects.\nThis approach helps us capture variation in the starting performance, acknowledging that people are inherently different but might still follow a similar overall pattern of improvement. It’s a simple yet powerful way to model individual differences!\nNow, let’s look at how to include this in our mixed model.\nModel\nTo run a linear mixed-effects model, we’ll use the lmer function from the lme4 package. It functions very similarly to the lm function we used before: you pass a formula and a dataset, but with one important addition: specifying the random intercept.\nThe formula is nearly the same as a standard linear model, but we include (1|subject_id) to tell the model that each subject should have its own unique intercept. This accounts for variations in baseline performance across individuals.\n\n\n\n\n\n\nCaution\n\n\n\nWhen specifying random intercepts (like (1|Group)), your grouping variables must be factors! If a grouping variable is numeric, R will wrongly treat it as a continuous variable rather than discrete categories. Character variables are automatically fine, but numeric grouping variables must be explicitly converted using factor().\n\n\n\nCodemod_rintercept =lmer(LookingTime ~ StandardTrialN * Event+ (1|Id ), data= df, na.action = na.exclude)\nsummary(mod_rintercept)\n\nLinear mixed model fit by REML. t-tests use Satterthwaite's method [\nlmerModLmerTest]\nFormula: LookingTime ~ StandardTrialN * Event + (1 | Id)\n Data: df\n\nREML criterion at convergence: 4774.7\n\nScaled residuals: \n Min 1Q Median 3Q Max \n-3.7640 -0.5509 0.0426 0.5462 2.7304 \n\nRandom effects:\n Groups Name Variance Std.Dev.\n Id (Intercept) 59981 244.9 \n Residual 14531 120.5 \nNumber of obs: 380, groups: Id, 20\n\nFixed effects:\n Estimate Std. Error df t value Pr(>|t|) \n(Intercept) 1418.273 55.468 19.493 25.569 < 2e-16 ***\nStandardTrialN -51.123 8.781 357.118 -5.822 1.29e-08 ***\nEventSimple -261.851 12.480 357.153 -20.981 < 2e-16 ***\nStandardTrialN:EventSimple -85.062 12.404 357.212 -6.858 3.09e-11 ***\n---\nSignif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1\n\nCorrelation of Fixed Effects:\n (Intr) StndTN EvntSm\nStandrdTrlN 0.005 \nEventSimple -0.113 -0.020 \nStndrdTN:ES -0.003 -0.715 -0.008\n\n\nWow! Now the model is showing us something new compared to the simple linear model. We observe an interaction between Event and StandardTrialN. By letting the intercept vary for each subject, the model is able to capture nuances in the data that a standard linear model might miss.\nTo understand this interaction, let’s plot what the random intercept is doing.\n\nCodei_pred = estimate_expectation(mod_rintercept, include_random=T)\n\nggplot(i_pred, aes(x= StandardTrialN, y= Predicted, color= Id, shape = Event))+\n geom_point(data = df, aes(y= LookingTime, color= Id), position= position_jitter(width=0.2))+\n geom_line()+\n geom_ribbon(aes(ymin=Predicted-SE, ymax=Predicted+SE, fill = Id),color= 'transparent', alpha=0.1)+\n labs(y='Looking time', x='# trial')+\n theme_modern(base_size = 20)+\n theme(legend.position = 'none')+\n facet_wrap(~Event)\n\n\n\n\n\n\n\nAs you can see here, each color represents a different subject, and we’ve divided the plot into two panels - one for each type of event - to make visualization simpler.\nYou can clearly see that our model is capturing the different intercepts that subjects have!! Cool isn’t it??.\nNow, you might be thinking, “This looks interesting, but my plot is going to be a mess with all these individual estimates!” Well, don’t worry! While what we’ve plotted is how the data is modeled by our mixed-effects model, the random effects are actually used to make more accurate estimates—but the model still returns an overall estimate.\nThink of it like this: the random effects allow the model to account for individual differences between subjects. But instead of just showing all the individual estimates in the plot, the model takes these individual estimates for each subject and returns the average of these estimates to give you a cleaner, more generalizable result.\nwe can plot the actual estimate of the model:\n\nCodei_pred = estimate_expectation(mod_rintercept, include_random =F)\n\nggplot(i_pred, aes(x= StandardTrialN, y= Predicted))+\n geom_point(data = df, aes(y= LookingTime, color= Id, shape = Event), position= position_jitter(width=0.2))+\n geom_line(aes(group= Event),color= 'blue', lwd=1.4)+\n geom_ribbon(aes(ymin=Predicted-SE, ymax=Predicted+SE, group= Event),color= 'transparent', alpha=0.1)+\n labs(y='Looking time', x='# trial')+\n theme_bw(base_size = 20)+\n theme(legend.position = 'none')+\n facet_wrap(~Event)\n\n\n\n\n\n\n\nSlope\nCoool!!!!!!! So far, we’ve modeled a different intercept for each subject, which lets each subject have their own baseline level of performance. But here’s the catch: our model assumes that everyone improves over the trials in exactly the same way, with the same slope. That doesn’t sound quite right, does it? We know that some people may get better faster than others, or their learning might follow a different pattern.\nModel\nThis is where we can model random slopes to capture these individual differences in learning rates. By adding (0 + StandardTrialN | Id), we’re telling the model that while the intercept (starting point) is the same for everyone, the rate at which each subject improves (the slope) can vary.\nThis way, we’re allowing each subject to have their own slope in addition to their own intercept, making the model more flexible and reflective of real-world variations in learning!\n\n\n\n\n\n\nCaution\n\n\n\nAny variable used as a random slope (before the |) must also be included as a fixed effect in your model. The fixed effect estimates the overall effect, while the random slope captures how that effect varies across groups. Without the fixed effect, you’re modeling deviations from zero instead of from an average, which rarely makes theoretical sense.\n\n\n\nCodemod_rslope =lmer(LookingTime ~ StandardTrialN * Event+ (0 + StandardTrialN | Id ), data= df)\nsummary(mod_rslope)\n\nLinear mixed model fit by REML. t-tests use Satterthwaite's method [\nlmerModLmerTest]\nFormula: LookingTime ~ StandardTrialN * Event + (0 + StandardTrialN | Id)\n Data: df\n\nREML criterion at convergence: 5272\n\nScaled residuals: \n Min 1Q Median 3Q Max \n-1.84592 -0.78577 -0.03054 0.80683 2.04769 \n\nRandom effects:\n Groups Name Variance Std.Dev.\n Id StandardTrialN 8279 90.99 \n Residual 63840 252.67 \nNumber of obs: 380, groups: Id, 20\n\nFixed effects:\n Estimate Std. Error df t value Pr(>|t|) \n(Intercept) 1424.42 18.47 359.45 77.131 <2e-16 ***\nStandardTrialN -72.82 27.52 31.83 -2.646 0.0126 * \nEventSimple -277.27 26.16 361.58 -10.601 <2e-16 ***\nStandardTrialN:EventSimple -47.07 26.31 368.33 -1.789 0.0744 . \n---\nSignif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1\n\nCorrelation of Fixed Effects:\n (Intr) StndTN EvntSm\nStandrdTrlN 0.019 \nEventSimple -0.711 -0.010 \nStndrdTN:ES -0.016 -0.485 -0.017\n\n\nThe results aren’t too different from the intercept-only model, but let’s take a closer look at what we’ve actually modeled.\n\nCodes_pred = estimate_expectation(mod_rslope, include_random =T)\n\nggplot(s_pred, aes(x= StandardTrialN, y= Predicted, color= Id, shape = Event))+\n geom_point(data = df, aes(y= LookingTime, color= Id), position= position_jitter(width=0.2))+\n geom_line()+\n geom_ribbon(aes(ymin=Predicted-SE, ymax=Predicted+SE, fill = Id),color= 'transparent', alpha=0.1)+\n labs(y='Looking time', x='# trial')+\n theme_modern(base_size = 20)+\n theme(legend.position = 'none')+\n facet_wrap(~Event)\n\n\n\n\n\n\n\nAs you can see here we have an unique intercept ( all the lines pass trough the same point where x = 0) but the slope of each line is different. And…..I don’t have to tell you that what we are looking at dosen’t really make sense.\nIntercept + Slope\nThat plot does look nuts, and it’s a clear signal that something is off. Why? Because by modeling only the random slopes while keeping the intercepts fixed, we’re essentially forcing all subjects to start from the same baseline. That’s clearly unrealistic for most real-world data.\nIn real life, the intercept and slope often go hand-in-hand for each subject.\nModel\nTo make the model more realistic, we can model both the random intercept and the random slope together. We simply modify the random effects part of the formula to (trial_number | subject_id).\nNow, we are telling the model to estimate both a random intercept (baseline performance) and a random slope (rate of improvement). This captures the full variability in how each subject learns over time!\n\nCodemod_rinterraction = lmer(LookingTime ~ StandardTrialN * Event+ (1 + StandardTrialN | Id ), data= df)\nsummary(mod_rinterraction)\n\nLinear mixed model fit by REML. t-tests use Satterthwaite's method [\nlmerModLmerTest]\nFormula: LookingTime ~ StandardTrialN * Event + (1 + StandardTrialN | Id)\n Data: df\n\nREML criterion at convergence: 4320.1\n\nScaled residuals: \n Min 1Q Median 3Q Max \n-2.40197 -0.62005 0.02697 0.68028 2.66893 \n\nRandom effects:\n Groups Name Variance Std.Dev. Corr\n Id (Intercept) 60185 245.33 \n StandardTrialN 10981 104.79 0.41\n Residual 3290 57.36 \nNumber of obs: 380, groups: Id, 20\n\nFixed effects:\n Estimate Std. Error df t value Pr(>|t|) \n(Intercept) 1425.068 55.019 19.115 25.901 2.38e-16 ***\nStandardTrialN -51.595 23.816 19.656 -2.166 0.0428 * \nEventSimple -276.278 6.007 338.381 -45.991 < 2e-16 ***\nStandardTrialN:EventSimple -83.322 6.100 339.010 -13.659 < 2e-16 ***\n---\nSignif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1\n\nCorrelation of Fixed Effects:\n (Intr) StndTN EvntSm\nStandrdTrlN 0.407 \nEventSimple -0.055 -0.001 \nStndrdTN:ES -0.001 -0.130 -0.026\n\n\nNow, let’s visualize how the model is modeling the data:\n\nCodeis_pred = estimate_expectation(mod_rinterraction, include_random =T)\n\nggplot(is_pred, aes(x= StandardTrialN, y= Predicted, color= Id, shape = Event))+\n geom_point(data = df, aes(y= LookingTime, color= Id), position= position_jitter(width=0.2))+\n geom_line()+\n geom_ribbon(aes(ymin=Predicted-SE, ymax=Predicted+SE, fill = Id),color= 'transparent', alpha=0.1)+\n labs(y='Looking time', x='# trial')+\n theme_modern(base_size = 20)+\n theme(legend.position = 'none')+\n facet_wrap(~Event)\n\n\n\n\n\n\n\nIf you look closely, you’ll see each subject has their own intercept and slope now. That means we’re actually modeling the real differences between people—exactly what we wanted!",
"crumbs": [
"Stats",
"Linear mixed effect models"
]
},
{
"objectID": "CONTENT/Stats/LinearMixedModels.html#summary-of-mixed-models",
"href": "CONTENT/Stats/LinearMixedModels.html#summary-of-mixed-models",
"title": "Linear mixed effect models",
"section": "Summary of mixed models",
"text": "Summary of mixed models\nNow that we’ve seen how to run mixed-effects models, it’s time to focus on interpreting the summary output. While we’ve been building models, we haven’t delved into what the summary actually tells us or which parts of it deserve our attention. Let’s fix that!\nTo start, we’ll use our final model and inspect its summary. This will give us a chance to break it down step by step and understand the key information it provides. Here’s how to check the summary:\n\nsummary(mod_rinterraction)\n\nLinear mixed model fit by REML. t-tests use Satterthwaite's method [\nlmerModLmerTest]\nFormula: LookingTime ~ StandardTrialN * Event + (1 + StandardTrialN | Id)\n Data: df\n\nREML criterion at convergence: 4320.1\n\nScaled residuals: \n Min 1Q Median 3Q Max \n-2.40197 -0.62005 0.02697 0.68028 2.66893 \n\nRandom effects:\n Groups Name Variance Std.Dev. Corr\n Id (Intercept) 60185 245.33 \n StandardTrialN 10981 104.79 0.41\n Residual 3290 57.36 \nNumber of obs: 380, groups: Id, 20\n\nFixed effects:\n Estimate Std. Error df t value Pr(>|t|) \n(Intercept) 1425.068 55.019 19.115 25.901 2.38e-16 ***\nStandardTrialN -51.595 23.816 19.656 -2.166 0.0428 * \nEventSimple -276.278 6.007 338.381 -45.991 < 2e-16 ***\nStandardTrialN:EventSimple -83.322 6.100 339.010 -13.659 < 2e-16 ***\n---\nSignif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1\n\nCorrelation of Fixed Effects:\n (Intr) StndTN EvntSm\nStandrdTrlN 0.407 \nEventSimple -0.055 -0.001 \nStndrdTN:ES -0.001 -0.130 -0.026\n\n\nThe Random effects section in the model summary shows how variability is accounted for by the random effects. The Groups column indicates the grouping factor (e.g., subject), while the Name column lists the random effects (e.g., intercept and slope). The Variance column represents the variability for each random effect—higher values indicate greater variation in how the effect behaves across groups. The Std.Dev. column is simply the standard deviation of the variance, showing the spread in the same units as the data.\nThe Corr column reflects the correlation between random effects, telling us whether different aspects of the data (e.g., intercepts and slopes) tend to move together. A negative correlation would suggest that higher intercepts (starting points) are associated with smaller slopes (slower learning rates), while a positive correlation would suggest the opposite.\nThe Residual section shows the unexplained variability after accounting for the fixed and random effects.\nThe key takeaway here is that random effects capture the variability in the data that can’t be explained by the fixed effects alone. If the variance for a random effect is low, it suggests the random effect isn’t adding much to the model and may be unnecessary. On the other hand, high variance indicates that the random effect is important for capturing group-level differences and improving the model’s accuracy.",
"crumbs": [
"Stats",
"Linear mixed effect models"
]
},
{
"objectID": "CONTENT/Stats/LinearMixedModels.html#model-comparison",
"href": "CONTENT/Stats/LinearMixedModels.html#model-comparison",
"title": "Linear mixed effect models",
"section": "Model comparison",
"text": "Model comparison\nThere are many indices to compare model performance. One of the best indices is the Akaike Information Criterion (AIC). AIC gives a relative measure of how well a model fits the data, while penalizing the number of parameters in the model. Lower AIC values indicate better models, as they balance goodness-of-fit with model complexity.\n\n\n\n\n\n\nWhy AIC?\n\n\n\nAIC is great because it balances how well your model fits the data with how many predictors you’re using. If you just keep adding fixed or random effects, the fit will likely improve. Without any additional penalty, you’d end up overfitting: your model would fit the current data perfectly but fail on new data. AIC solves this problem by penalising models with more effects!\nThe AIC score by itself doesn’t mean much. You usually get wacky values like 24’783. However, it is super useful as a relative measure, for comparing different models against each other. When you do compare models, lower AIC values indicate better models.\n\n\nYou can compare the AIC of different models using the following:\n\ncompare_performance(mod_rintercept, mod_rinterraction, metrics='AIC')\n\n# Comparison of Model Performance Indices\n\nName | Model | AIC (weights)\n----------------------------------------------------\nmod_rintercept | lmerModLmerTest | 4815.8 (<.001)\nmod_rinterraction | lmerModLmerTest | 4364.8 (>.999)\n\n\nAs you can see, the best model based on AIC is the one with both intercept and slope. This is a good way to check if and which random effect structure is necessary for our model.\n\n\n\n\n\n\nWarning\n\n\n\nNever decide if your random effect structure is good by just looking at p-values! P-values are not necessarily related to how well the model fits your data. Always use model comparison and fit indices like AIC to guide your decision.",
"crumbs": [
"Stats",
"Linear mixed effect models"
]
},
{
"objectID": "CONTENT/Stats/LinearMixedModels.html#formulary",
"href": "CONTENT/Stats/LinearMixedModels.html#formulary",
"title": "Linear mixed effect models",
"section": "Formulary",
"text": "Formulary\nIn this tutorial, we introduced linear mixed-effects models. However, these models can be far more versatile and complex than what we’ve just explored. The lme4 package allows you to specify various models to suit diverse research scenarios. While we won’t dive into every possibility, here’s a handy reference for the different random effects structures you can specify\n\n\nFormula\nDescription\n\n\n\n(1|s)\nRandom intercepts for unique level of the factor s.\n\n\n(1|s) + (1|i)\nRandom intercepts for each unique level of s and for each unique level of i.\n\n\n(1|s/i)\nRandom intercepts for factor s and i, where the random effects for i are nested in s. This expands to (1|s) + (1|s:i), i.e., a random intercept for each level of s, and each unique combination of the levels of s and i. Nested random effects are used in so-called multilevel models. For example, s might refer to schools, and i to classrooms within those schools.\n\n\n(a|s)\nRandom intercepts and random slopes for a, for each level of s. Correlations between the intercept and slope effects are also estimated. (Identical to (a*b|s).)\n\n\n(a*b|s)\nRandom intercepts and slopes for a, b, and the a:b interaction, for each level of s. Correlations between all the random effects are estimated.\n\n\n(0+a|s)\nRandom slopes for a for each level of s, but no random intercepts.\n\n\n(a||s)\nRandom intercepts and random slopes for a, for each level of s, but no correlations between the random effects (i.e., they are set to 0). This expands to: (0+a|s) + (1|s).",
"crumbs": [
"Stats",
"Linear mixed effect models"
]
},
{
"objectID": "CONTENT/Stats/ModelEstimates.html",
"href": "CONTENT/Stats/ModelEstimates.html",
"title": "Model Estimates",
"section": "",
"text": "Welcome back, stats adventurer! 👋 If you are here, it means you have already gone through our tutorial on: Linear models, Linear mixed effect models. Well done!! You are nearly there, my young stats padawan.\nYou might think that once you’ve fit the model, you’re basically done: check the summary, look for stars, write the results. But there’s a catch: If we just look at summary(), we will know whether effects exist (yes or no), but we still won’t be able to describe them in detail.\nFor example, if we find a significant interaction, we still don’t know the pattern that produced it. This is exactly what happened in our previous tutorial on mixed-effect models. Our hypothesis was simple: looking time differs across conditions (Simple vs Complex) but this difference might change over trials.\nEven if we find a significant interaction between Condition (Simple vs Complex) and Trial Number, we still don’t know what exactly is happening: is looking time significantly decreasing over trials in the Simple condition? Or is it increasing in the Complex condition? Or is looking time changing in both condition, but at different rates? Let’s formulate our questions a bit more clearly:\nLuckily for us, there is a solution to this: we can further describe what the main and interaction effects look like, and we can even confidently say - backed by statistics - which differences are significant.\nThat’s the goal of this next tutorial. We will show how to visualise and test the specific effects you care about using model-based estimated means, contrasts, and slopes, so you can make clear statements about where and how conditions differ!",
"crumbs": [
"Stats",
"Model Estimates"
]
},
{
"objectID": "CONTENT/Stats/ModelEstimates.html#what-are-predictions-anyway",
"href": "CONTENT/Stats/ModelEstimates.html#what-are-predictions-anyway",
"title": "ModelEstimates",
"section": "What Are Predictions, Anyway?",
"text": "What Are Predictions, Anyway?\nNow we can extract the predictions from our model. We want to see what our model predicts the values of Looking time would be for each row of our dataframe. Here is where the Easystats package (specifically the modelbased sub-package) comes to the rescue with the function estimate_expectation().\n\nPred = estimate_expectation(mod)\nhead(Pred)\n\nModel-based Predictions\n\nStandardTrialN | Event | SES | Id | Predicted | SE | 95% CI | Residuals\n----------------------------------------------------------------------------------------\n-1.47 | Simple | high | 1 | 1146.35 | 97.20 | [955.22, 1337.49] | -3.05\n-1.13 | Simple | high | 1 | 1055.49 | 95.05 | [868.58, 1242.40] | -1.67\n-0.95 | Simple | high | 1 | 1010.06 | 94.92 | [823.39, 1196.73] | -6.70\n-0.43 | Simple | high | 1 | 873.77 | 98.36 | [680.34, 1067.20] | -20.55\n-0.26 | Simple | high | 1 | 828.34 | 100.70 | [630.30, 1026.38] | -54.90\n0.26 | Simple | high | 1 | 692.05 | 110.79 | [474.18, 909.92] | 50.76\n\nVariable predicted: LookingTime\n\n\nLook!! The function gave us a new dataframe with all the levels of Event and of StandardTrialN and ID that we had in our dataframe. OK, OK, I will agree….this looks so similar to the original dataframe we had…so why even bother???? Can’t we just use the original dataframe?? Well because the predictions represent what our model thinks the data should look like based on the patterns it discovered, after accounting for random effects and noise!\nLet’s visualize the difference between the raw data and the predictions\n\n\n\n\n\n\n\n\nAs you can see, the raw data and the predicted data are very similar but not the same!! The predictions represent our model’s “best guess” at what the looking times should be based on the fixed effects (StandardTrialN, Event and SES) and random effects (individual participant differences). This smooths out some of the noise in the original data and gives us a simpler picture of the underlying patterns!\nThis is cool indeed… but how can this help me?? Let’s remember our question about Events….",
"crumbs": [
"Stats",
"ModelEstimates"
]
},
{
"objectID": "CONTENT/Stats/ModelEstimates.html#predictions-for-reference-values",
"href": "CONTENT/Stats/ModelEstimates.html#predictions-for-reference-values",
"title": "ModelEstimates",
"section": "Predictions for reference values",
"text": "Predictions for reference values\nThese predictions are fascinating, but how do they help us answer our original question about Event types? Let’s pull ourselves back to our main goal - understanding the difference in looking times between Complex and Simple events.\nWith our prediction tools in hand, we can now directly extract the specific information we need without getting lost in coefficient calculations or being overwhelmed by the full dataset. The beauty of predictions is that they transform complex statistical relationships into concrete, interpretable values that directly address our research questions.\nThis is where the powerful by argument comes into play. It allows us to create focused predictions for specific variables of interest, making our analysis more targeted and interpretable.\nFactors\n\nestimate_expectation(mod, by = 'Event')\n\nModel-based Predictions\n\nEvent | Predicted | SE | CI\n-------------------------------------------------\nComplex | 1397.49 | 105.33 | [1190.35, 1604.63]\nSimple | 1114.50 | 105.31 | [ 907.40, 1321.60]\n\nVariable predicted: LookingTime\nPredictors modulated: Event\nPredictors controlled: StandardTrialN (0.005), SES (high)\n\n\nThis gives us predictions for each level of the Event and SES factor while:\n\nSetting StandardTrialN to its mean value\nSES is set to “high”. This is because the function can’t meaningfully “average” a categorical variable like SES, so it uses its reference level (“high” in our case) for making predictions.\n\nThis is already better!!! LOOK we have 1 value for each Event level!! Amazing\n\n\n\n\n\n\nTip\n\n\n\nIn these examples (and the next ones), we explored a very simple case. We just passed the predictors we were interested in, and the function gave us all the levels of that predictor. However, we can also specify exactly which level we want instead of having the function automatically return all the ones it thinks are relevant. There are different ways to do this - the simplest is:\n\nestimate_expectation(mod, by = c(\"Event == 'Simple'\", \"SES\"))\n\nModel-based Predictions\n\nSES | Predicted | SE | CI\n------------------------------------------------\nhigh | 1397.49 | 105.33 | [1190.35, 1604.63]\nlow | 1463.44 | 91.23 | [1284.05, 1642.84]\nmedium | 1401.34 | 105.31 | [1194.25, 1608.44]\n\nVariable predicted: LookingTime\nPredictors modulated: Event == 'Simple', SES\nPredictors controlled: StandardTrialN (0.005), Event (Complex)\n\n\nRemember to learn more at the modelbased website !!\n\n\nContinuous\nWhat we have done for the Event can also be done for a continuous variable!! When we pass a continuous variable to the by argument, the function automatically extracts a range of values across that predictor. We can control how many values to sample using the length parameter. For example, setting length=3 will give us predictions at just three points along the range of our continuous variable, while a larger value would provide a more detailed sampling.\n\nestimate_expectation(mod, by = 'StandardTrialN', length=3)\n\nModel-based Predictions\n\nStandardTrialN | Predicted | SE | CI\n--------------------------------------------------------\n-1.64 | 1495.37 | 99.36 | [1299.97, 1690.77]\n0.00 | 1397.79 | 105.24 | [1190.84, 1604.74]\n1.64 | 1300.21 | 153.04 | [ 999.26, 1601.15]\n\nVariable predicted: LookingTime\nPredictors modulated: StandardTrialN\nPredictors controlled: Event (Complex), SES (high)\n\nestimate_expectation(mod, by = 'StandardTrialN', length=6)\n\nModel-based Predictions\n\nStandardTrialN | Predicted | SE | CI\n--------------------------------------------------------\n-1.64 | 1495.37 | 99.36 | [1299.97, 1690.77]\n-0.99 | 1456.34 | 94.95 | [1269.61, 1643.07]\n-0.33 | 1417.30 | 99.71 | [1221.22, 1613.39]\n0.33 | 1378.27 | 112.48 | [1157.07, 1599.47]\n0.99 | 1339.24 | 130.94 | [1081.75, 1596.73]\n1.64 | 1300.21 | 153.04 | [ 999.26, 1601.15]\n\nVariable predicted: LookingTime\nPredictors modulated: StandardTrialN\nPredictors controlled: Event (Complex), SES (high)\n\n\nFactor x Continuous\nWant to see how factors and continuous variables interact? Simply include both in the by argument:\n\nestimate_expectation(mod, by = c('StandardTrialN','Event'), length=6)\n\nModel-based Predictions\n\nStandardTrialN | Event | Predicted | SE | CI\n------------------------------------------------------------------\n-1.64 | Complex | 1495.37 | 99.36 | [1299.97, 1690.77]\n-0.99 | Complex | 1456.34 | 94.95 | [1269.61, 1643.07]\n-0.33 | Complex | 1417.30 | 99.71 | [1221.22, 1613.39]\n0.33 | Complex | 1378.27 | 112.48 | [1157.07, 1599.47]\n0.99 | Complex | 1339.24 | 130.94 | [1081.75, 1596.73]\n1.64 | Complex | 1300.21 | 153.04 | [ 999.26, 1601.15]\n-1.64 | Simple | 1353.21 | 99.18 | [1158.18, 1548.24]\n-0.99 | Simple | 1258.02 | 94.90 | [1071.41, 1444.63]\n-0.33 | Simple | 1162.83 | 99.70 | [ 966.76, 1358.89]\n0.33 | Simple | 1067.63 | 112.44 | [ 846.53, 1288.74]\n0.99 | Simple | 972.44 | 130.80 | [ 715.22, 1229.66]\n1.64 | Simple | 877.25 | 152.78 | [ 576.80, 1177.69]\n\nVariable predicted: LookingTime\nPredictors modulated: StandardTrialN, Event\nPredictors controlled: SES (high)\n\n\nThis creates predictions for all combinations of Event levels and StandardTrialN values - perfect for visualizing interaction effects!\n\n\n\n\n\n\nTip\n\n\n\nIn this tutorial, we focused on the estimate_expectation() function. However, modelbased offers additional functions that return similar things but with slightly different defaults and behaviors.\nFor example, if we run estimate_relation(mod), instead of returning predictions based on the values in our original dataframe, it will automatically return predictions on a reference grid of all predictors. This is equivalent to running estimate_expectation(mod, by = c('StandardTrialN','Event')) that we saw before.\nThese functions accomplish very similar tasks but may be more convenient depending on what you’re trying to do. The different function names reflect their slightly different default behaviors, saving you time when you need different types of predictions. Don’t be intimidated by these differences - explore them further at the modelbased website to learn them better.\n\n\nPlot\nAll these predictions allow for simple direct plotting. These are ggplot objects that can be customized by adding other arguments (like we are doing here where we are changing the theme ). Note that there’s a limit to how well these automatic plots handle complexity - with too many predictors, you might need to create custom plots instead.\n\nEst_plot = estimate_expectation(mod, by = c('StandardTrialN','Event'), length=10)\nEst_plot\n\nModel-based Predictions\n\nStandardTrialN | Event | Predicted | SE | CI\n------------------------------------------------------------------\n-1.64 | Complex | 1495.37 | 99.36 | [1299.97, 1690.77]\n-1.28 | Complex | 1473.72 | 95.80 | [1285.33, 1662.11]\n-0.91 | Complex | 1452.01 | 95.03 | [1265.13, 1638.89]\n-0.55 | Complex | 1430.30 | 97.14 | [1239.27, 1621.33]\n-0.18 | Complex | 1408.64 | 101.93 | [1208.19, 1609.09]\n0.18 | Complex | 1386.93 | 109.07 | [1172.44, 1601.43]\n0.55 | Complex | 1365.28 | 118.11 | [1133.02, 1597.54]\n0.91 | Complex | 1343.57 | 128.68 | [1090.52, 1596.62]\n1.28 | Complex | 1321.86 | 140.43 | [1045.71, 1598.00]\n1.64 | Complex | 1300.21 | 153.04 | [ 999.26, 1601.15]\n-1.64 | Simple | 1353.21 | 99.18 | [1158.18, 1548.24]\n-1.28 | Simple | 1300.41 | 95.69 | [1112.23, 1488.59]\n-0.91 | Simple | 1247.46 | 94.98 | [1060.67, 1434.25]\n-0.55 | Simple | 1194.51 | 97.12 | [1003.51, 1385.51]\n-0.18 | Simple | 1141.70 | 101.92 | [ 941.28, 1342.13]\n0.18 | Simple | 1088.76 | 109.04 | [ 874.33, 1303.18]\n0.55 | Simple | 1035.95 | 118.04 | [ 803.83, 1268.07]\n0.91 | Simple | 983.00 | 128.55 | [ 730.20, 1235.80]\n1.28 | Simple | 930.05 | 140.24 | [ 654.28, 1205.83]\n1.64 | Simple | 877.25 | 152.78 | [ 576.80, 1177.69]\n\nVariable predicted: LookingTime\nPredictors modulated: StandardTrialN, Event\nPredictors controlled: SES (high)\n\nplot(Est_plot )+\n theme_classic(base_size = 20)+\n theme(legend.position = 'bottom')\n\n\n\n\n\n\n\nIn this plot, we can see the predicted values for looking time for the two Events changing over the StandardTrialN. The lines represent our model’s best estimates, while the shaded areas show the confidence intervals around these estimates.\n\n\n\n\n\n\nImportant\n\n\n\nIn this plot the predictions are extracted with the SES (socioeconomic status) being held at its high level! In a model with multiple predictors, predictions are always made with the other predictors fixed at specific values (typically their mean or reference level). Always be aware of what values other variables are being held at when interpreting your plots.",
"crumbs": [
"Stats",
"ModelEstimates"
]
},
{
"objectID": "CONTENT/Stats/ModelEstimates.html#plot-1",
"href": "CONTENT/Stats/ModelEstimates.html#plot-1",
"title": "ModelEstimates",
"section": "Plot",
"text": "Plot\nThe estimate_means() function also comes with its own built-in plotting capability, automatically generating a clean ggplot visualization of your estimated means.\n\nplot(Est_means)+\n theme_classic(base_size = 20)+\n theme(legend.position = 'bottom')\n\n\n\n\n\n\n\nThis creates a simple yet informative ggplot showing our estimated means with confidence intervals. Of course, we can also use the dataframe that the function returns to create more customized and refined plots if needed.",
"crumbs": [
"Stats",
"ModelEstimates"
]
},
{
"objectID": "CONTENT/Stats/ModelEstimates.html#continuous-1",
"href": "CONTENT/Stats/ModelEstimates.html#continuous-1",
"title": "ModelEstimates",
"section": "Continuous",
"text": "Continuous\nAs we have seen before for predictions, we can also pass a continuous predictor to our by= argument. In this case, estimate_means() gives us the averaged values across the range of the continuous predictor.\n\nestimate_means(mod, by = 'StandardTrialN', length=6)\n\nEstimated Marginal Means\n\nStandardTrialN | Mean | SE | 95% CI | t(364)\n--------------------------------------------------------------\n-1.64 | 1433.66 | 54.56 | [1326.37, 1540.95] | 26.28\n-0.99 | 1373.39 | 52.30 | [1270.54, 1476.24] | 26.26\n-0.33 | 1313.12 | 55.03 | [1204.91, 1421.33] | 23.86\n0.33 | 1252.85 | 62.08 | [1130.77, 1374.93] | 20.18\n0.99 | 1192.58 | 72.21 | [1050.59, 1334.58] | 16.52\n1.64 | 1132.32 | 84.31 | [ 966.53, 1298.10] | 13.43\n\nVariable predicted: LookingTime\nPredictors modulated: StandardTrialN\nPredictors averaged: Event, SES, Id",
"crumbs": [
"Stats",
"ModelEstimates"
]
},
{
"objectID": "CONTENT/Stats/ModelEstimates.html#plot-2",
"href": "CONTENT/Stats/ModelEstimates.html#plot-2",
"title": "ModelEstimates",
"section": "Plot",
"text": "Plot\nestimate_contrast() has a plotting function as well! However, it’s slightly more complex as the plot function requires both our estimated contrasts and means. If we pass both, the plot will show which level is different from the other using lighthouse plots - visualizations that highlight significant differences between groups.\n\nplot(Contrast, Est_means)+\n theme_classic(base_size = 20)+\n theme(legend.position = 'bottom')",
"crumbs": [
"Stats",
"ModelEstimates"
]
},
{
"objectID": "CONTENT/Stats/ModelEstimates.html#factor-x-continuous-1",
"href": "CONTENT/Stats/ModelEstimates.html#factor-x-continuous-1",
"title": "ModelEstimates",
"section": "Factor x Continuous",
"text": "Factor x Continuous\nWhile estimate_contrast() is usually very useful for checking differences between levels of a categorical variable, it can also be used to estimate contrasts for continuous variables. However, where in our opinion the function really shines is when examining interactions between categorical and continuous predictors like we have in our model!!!\n\nContrastbyTrial = estimate_contrasts(mod, contrast = 'Event', by = 'StandardTrialN', p_adjust = 'hochberg')\nContrastbyTrial\n\nMarginal Contrasts Analysis\n\nLevel1 | Level2 | StandardTrialN | Difference | SE | 95% CI | t(364) | p\n---------------------------------------------------------------------------------------------\nSimple | Complex | -1.64 | -138.52 | 11.91 | [-161.95, -115.09] | -11.63 | < .001\nSimple | Complex | -1.28 | -169.04 | 10.05 | [-188.79, -149.28] | -16.83 | < .001\nSimple | Complex | -0.91 | -199.63 | 8.36 | [-216.08, -183.19] | -23.88 | < .001\nSimple | Complex | -0.55 | -230.23 | 7.00 | [-244.00, -216.46] | -32.88 | < .001\nSimple | Complex | -0.18 | -260.74 | 6.19 | [-272.92, -248.57] | -42.10 | < .001\nSimple | Complex | 0.18 | -291.34 | 6.15 | [-303.43, -279.25] | -47.40 | < .001\nSimple | Complex | 0.55 | -321.85 | 6.88 | [-335.38, -308.33] | -46.78 | < .001\nSimple | Complex | 0.91 | -352.45 | 8.19 | [-368.55, -336.35] | -43.05 | < .001\nSimple | Complex | 1.28 | -383.05 | 9.84 | [-402.41, -363.69] | -38.91 | < .001\nSimple | Complex | 1.64 | -413.56 | 11.70 | [-436.56, -390.56] | -35.36 | < .001\n\nVariable predicted: LookingTime\nPredictors contrasted: Event\nPredictors averaged: SES, Id\np-value adjustment method: Hochberg (1988)\n\n\n\n\n\n\n\n\nImportant\n\n\n\nWhile we’ve been using contrast to specify one predictor and by for another, estimate_contrasts() is more flexible than we’ve shown. The contrast = argument can accept multiple predictors, calculating contrasts between all combinations of their levels. You can also mix and match with multiple predictors in contrast = while still using the by = argument to see how these combined contrasts change across levels of another variable. While this generates many comparisons at once (which isn’t always desirable), it can be valuable for exploring complex interaction patterns in your data.\nThe word is your contrast!\n\n\nThis gives us the contrast between the levels of Event for a set of values of StandardTrialN. So we can check whether the difference between Complex and Simple actually changes over the course of trials!! I’ll plot it to make it simpler to understand:\n\n\n\n\n\n\n\n\nThis plot shows that the two levels are always significantly different from each other (the confidence intervals never touch the dashed zero line) and that the difference is always positive - looking time for Complex is consistently higher than for Simple across all trial numbers. SUUPER COOL EH!!! I agree!!",
"crumbs": [
"Stats",
"ModelEstimates"
]
},
{
"objectID": "CONTENT/Stats/ModelEstimates.html#contrast-of-slopes",
"href": "CONTENT/Stats/ModelEstimates.html#contrast-of-slopes",
"title": "ModelEstimates",
"section": "Contrast of slopes",
"text": "Contrast of slopes\nOK now we know the difference between levels of Event and also how this difference may change over time… the last step? We could check whether the slopes of StandardTrialN is different between the two conditions!\nTo do so, we can again use the estimate_contrast() function but inverting the arguments we used last time. So StandardTrialN moves to the contrast argument and Event goes to the by argument. Super easy:\n\nestimate_contrasts(mod, contrast = 'StandardTrialN', by = 'Event')\n\nMarginal Contrasts Analysis\n\nLevel1 | Level2 | Difference | SE | 95% CI | t(364) | p\n-------------------------------------------------------------------------\nSimple | Complex | -83.60 | 6.15 | [-95.70, -71.50] | -13.59 | < .001\n\nVariable predicted: LookingTime\nPredictors contrasted: StandardTrialN\nPredictors averaged: StandardTrialN (0.005), SES, Id\np-values are uncorrected.\n\n\nThis shows us that the effect of StandardTrialN is actually different between the two levels. This is something we already knew from the model summary (remember the significant interaction term?), but this approach gives us the precise difference between the two slopes while averaging over all other possible effects. This becomes particularly valuable when working with more complex models that have multiple predictors and interactions.",
"crumbs": [
"Stats",
"ModelEstimates"
]
},
{
"objectID": "CONTENT/Stats/ModelEstimates.html#factor-x-continuous-2",
"href": "CONTENT/Stats/ModelEstimates.html#factor-x-continuous-2",
"title": "ModelEstimates",
"section": "Factor x Continuous",
"text": "Factor x Continuous\nSimilar to the estimate_contrast function, estimate_slopes really shines when exploring interactions between continuous and categorical variables.\nRemember! Our model indicated an interaction between StandardTrialN and Event, which means the rate of change (slope) should differ between the two Event types. However, from just knowing there’s an interaction, we can’t determine exactly what these slopes are doing. One condition might show a steeper decline than the other, one might be flat while the other decreases, or they might even go in opposite directions. The model just tells us they’re different, not how they’re different (or at least it is not so simple)\nTo visualize exactly how these slopes differ, we can include Event with the by argument:\n\nSlopes = estimate_slopes(mod, trend = 'StandardTrialN', by = 'Event')\nSlopes\n\nEstimated Marginal Effects\n\nEvent | Slope | SE | 95% CI | t(364) | p\n---------------------------------------------------------------\nComplex | -49.79 | 25.03 | [ -99.02, -0.57] | -1.99 | 0.047\nSimple | -133.39 | 25.01 | [-182.57, -84.21] | -5.33 | < .001\n\nMarginal effects estimated for StandardTrialN\nType of slope was dY/dX\n\n\nNow we get the average effect for both Events. So we see that both of the lines significantly decrease!\nWe can also plot it with:\n\nplot(Slopes)+\n geom_hline(yintercept = 0, linetype = 'dashed')+\n theme_classic(base_size = 20)+\n theme(legend.position = 'bottom')\n\n\n\n\n\n\n\nHere we see that the slopes for both Event types are negative (below zero) and statistically significant (confidence intervals don’t cross the dashed zero line). This confirms that looking time reliably decreases as trials progress, regardless of whether it’s a Complex or Simple event.",
"crumbs": [
"Stats",
"ModelEstimates"
]
},
{
"objectID": "CONTENT/Stats/ModelEstimates.html#datagrid",
"href": "CONTENT/Stats/ModelEstimates.html#datagrid",
"title": "ModelEstimates",
"section": "Datagrid",
"text": "Datagrid\nThe first step in custom plotting is creating the right reference grid for your predictions. We’ll use the get_datagrid() function, which takes your model as its first argument and the by = parameter to specify which predictors you want in your visualization.\nLet’s extract a reference grid for our StandardTrialN * Event interaction:\n\nget_datagrid(mod, by = c('Event', 'StandardTrialN'))\n\nVisualisation Grid\n\nEvent | StandardTrialN | SES | Id\n------------------------------------\nComplex | -1.64 | high | 0\nComplex | -1.28 | high | 0\nComplex | -0.91 | high | 0\nComplex | -0.55 | high | 0\nComplex | -0.18 | high | 0\nComplex | 0.18 | high | 0\nComplex | 0.55 | high | 0\nComplex | 0.91 | high | 0\nComplex | 1.28 | high | 0\nComplex | 1.64 | high | 0\nSimple | -1.64 | high | 0\nSimple | -1.28 | high | 0\nSimple | -0.91 | high | 0\nSimple | -0.55 | high | 0\nSimple | -0.18 | high | 0\nSimple | 0.18 | high | 0\nSimple | 0.55 | high | 0\nSimple | 0.91 | high | 0\nSimple | 1.28 | high | 0\nSimple | 1.64 | high | 0\n\nMaintained constant: SES\n\n\nLooking at the output, you’ll see we get both levels of Event (Complex and Simple) and a range of values for StandardTrialN (10 values by default spanning the range of our data). Notice that the Id column is set to 0. This is how the function indicates that random effects aren’t included—we’re focusing on the fixed effects that represent the “average response” across all subjects.\n\n\n\n\n\n\nCaution\n\n\n\nDifferent models declare non-level for the random effects in different ways, but datagrid will adapt to the specific model you are running. Just do not be scared if the values are not 0.\n\n\nThe datagrid is where we can really flex our customization muscles. Instead of accepting the default values, we can request specific levels of a factor, custom ranges for continuous variables, or statistical landmarks of continuous variables.\nFor example, instead of the default range of values, we could request 'StandardTrialN = [quartiles]' which would give us the lower-hinge, median, and upper-hinge of the continuous variable. Cool, right? There are many functions and statistical aspects we can extract - check the documentation of get_datagrid() for the full range of possibilities.\nLet’s create a more sophisticated datagrid to demonstrate:\n\nGrid = get_datagrid(mod, by = c('Event', 'StandardTrialN = [fivenum]'))\nhead(as.data.frame(Grid), n=20 )\n\n Event StandardTrialN SES Id\n1 Complex -1.645 high 0\n2 Complex -0.953 high 0\n3 Complex 0.087 high 0\n4 Complex 0.953 high 0\n5 Complex 1.645 high 0\n6 Simple -1.645 high 0\n7 Simple -0.953 high 0\n8 Simple 0.087 high 0\n9 Simple 0.953 high 0\n10 Simple 1.645 high 0\n\n\nWhat we’ve done here is request both levels of Event, the five-number summary for StandardTrialN (minimum, lower-hinge, median, upper-hinge, maximum), and all different subjects by setting include_random = TRUE. Why would we want such a complex grid? Well, first things first to make it complex…… and also we want to see what is happening for each subject!! Let’s now use this grid……",
"crumbs": [
"Stats",
"ModelEstimates"
]
},
{
"objectID": "CONTENT/Stats/ModelEstimates.html#prediction-on-grid",
"href": "CONTENT/Stats/ModelEstimates.html#prediction-on-grid",
"title": "ModelEstimates",
"section": "Prediction on grid",
"text": "Prediction on grid\nNow that we have our grid, we can pass it to the get_predicted() function. This will give us all the predictions on the grid!!\n\nget_predicted(mod, Grid, ci = T)\n\nPredicted values:\n\n [1] 1495.3716 1454.3213 1392.6274 1341.2553 1300.2050 1353.2138 1253.1015 1102.6437 977.3587 877.2464\n\nNOTE: Confidence intervals, if available, are stored as attributes and can be accessed using `as.data.frame()` on this output.\n\n\nAs you notice, this gives us a vector of predictions, but we also need the confidence intervals to make a proper plot. As the message mentions, those are ‘hidden’ and need to be accessed using as.data.frame().\n\nPred = as.data.frame(get_predicted(mod, Grid, ci = T))\nhead(Pred)\n\n Predicted SE CI_low CI_high\n1 1495.372 99.36313 -Inf Inf\n2 1454.321 94.97684 -Inf Inf\n3 1392.627 106.99807 -Inf Inf\n4 1341.255 129.88136 -Inf Inf\n5 1300.205 153.03811 -Inf Inf\n6 1353.214 99.17637 -Inf Inf\n\n\nNow we can merge the two to have a final dataframe that has all the information:\n\ndb = bind_cols(Grid, Pred)\nhead(db)\n\nVisualisation Grid\n\nEvent | StandardTrialN | SES | Id | Predicted | SE | CI_low | CI_high\n----------------------------------------------------------------------------\nComplex | -1.64 | high | 0 | 1495.37 | 99.36 | -Inf | Inf\nComplex | -0.95 | high | 0 | 1454.32 | 94.98 | -Inf | Inf\nComplex | 0.09 | high | 0 | 1392.63 | 107.00 | -Inf | Inf\nComplex | 0.95 | high | 0 | 1341.26 | 129.88 | -Inf | Inf\nComplex | 1.64 | high | 0 | 1300.21 | 153.04 | -Inf | Inf\nSimple | -1.64 | high | 0 | 1353.21 | 99.18 | -Inf | Inf\n\nMaintained constant: SES",
"crumbs": [
"Stats",
"ModelEstimates"
]
},
{
"objectID": "CONTENT/Stats/ModelEstimates.html#plot-3",
"href": "CONTENT/Stats/ModelEstimates.html#plot-3",
"title": "ModelEstimates",
"section": "Plot",
"text": "Plot\nNow we have our dataframe we can do whatever we want with it. Let’s plot it:\n\nggplot(db, aes(x = StandardTrialN, y = Predicted, color = Event, fill = Event ))+\n geom_ribbon(aes(ymin = Predicted-SE, ymax = Predicted+SE), alpha = .4)+\n geom_line(lwd = 1.2 )+\n theme_classic(base_size = 20)+\n theme(legend.position = 'bottom')\n\n\n\n\n\n\n\nWell, you did it! You’ve survived the wild jungle of model-based estimation! By breaking down the prediction process into customizable steps, you’ve gained complete control over how to extract and visualize the patterns in your eye-tracking data. Whether you use the convenient built-in functions or create custom plots from scratch, you now have the tools to answer sophisticated research questions and present your findings beautifully. Happy modeling!",
"crumbs": [
"Stats",
"ModelEstimates"
]
},
{
"objectID": "CONTENT/Workshops/GAP_2024.html",
"href": "CONTENT/Workshops/GAP_2024.html",
"title": "Bridging the Technological Gap Workshop (GAP)",
"section": "",
"text": "Hello hello!!! This page has been created to provide support and resources for the tutorial that will take place during the Bridging the Technological Gap Workshop."
},
{
"objectID": "CONTENT/Workshops/GAP_2024.html#python",
"href": "CONTENT/Workshops/GAP_2024.html#python",
"title": "Bridging the Technological Gap Workshop (GAP)",
"section": "Python",
"text": "Python\nIn this tutorial, our primary tool will be Python!! There are lots of ways to install python. We recommend installing it via Miniconda. However, for this workshop, the suggested way to install Python is using Anaconda.\nYou might ask….Then which installation should I follow? Well, it doesn’t really matter! Miniconda is a minimal installation of Anaconda. It lacks the GUI, but has all the main features. So follow whichever one you like more!\nOnce you have it installed, we need a few more things. For the Gaze Tracking & Pupillometry Workshop (the part we will be hosting) we will need some specific libraries and files. We have tried our best to make everything as simple as possible:\n\nLibraries\nWe will be working with a conda environment (a self-contained directory that contains a specific collection of Python packages and dependencies, allowing you to manage different project requirements separately). To create this environment and install all the necessary libraries, all you need is this file:\n Psychopy.yml \nOnce you have downloaded the file, simply open the anaconda/miniconda terminal and type conda env create -f, then simply drag and drop the downloaded file onto the terminal. This will copy the filename with its absolute path. In my case it looked something like this:\n\n\n\n\n\nNow you will be asked to confirm a few things (by pressing Y) and after a while of downloading and installing you will have your new workshop environment called Psychopy!\nNow you should see a shortcut in your start menu called Spyder(psychopy), just click on it to open spyder in our newly created environment. If you don’t see it, just reopen the anaconda/miniconda terminal, activate your new environment by typing conda activate psychopy and then just type spyder.\n\n\nFiles\nWe also need some files if you want to run the examples with us. Here you can download the zip files with everything you need:\n Files \nOnce downloaded, simply extract the file by unzipping it. For our workshop we will work together in a folder that should look like this:\n\n\n\n\n\nIf you have a similar folder… you are ready to go!!!!"
},
{
"objectID": "CONTENT/Workshops/GAP_2024.html#videos",
"href": "CONTENT/Workshops/GAP_2024.html#videos",
"title": "Bridging the Technological Gap Workshop (GAP)",
"section": "Videos",
"text": "Videos\nWe received several questions about working with videos and PsychoPy while doing eye-tracking. It can be quite tricky, but here are some tips:\n\nMake sure you’re using the right codec.\nIf you need to change the codec of the video, you can re-encode it using a tool like\nHandbrake (remember to set the constant framerate in the video option)\n\nBelow, you’ll find a code example that adapts our Create an eye-tracking experiment tutorial to work with a video file. The main differences are:\n\nWe’re showing a video after the fixation.\nWe’re saving triggers to our eye-tracking data and also saving the frame index at each sample (as a continuous number column).\n\n\nimport os\nimport glob\nimport pandas as pd\nimport numpy as np\n\n# Import some libraries from PsychoPy\nfrom psychopy import core, event, visual, prefs\nprefs.hardware['audioLib'] = ['PTB']\nfrom psychopy import sound\n\nimport tobii_research as tr\n\n\n#%% Functions\n\n# This will be called every time there is new gaze data\ndef gaze_data_callback(gaze_data):\n global trigger\n global gaze_data_buffer\n global winsize\n global frame_indx\n \n # Extract the data we are interested in\n t = gaze_data.system_time_stamp / 1000.0\n lx = gaze_data.left_eye.gaze_point.position_on_display_area[0] * winsize[0]\n ly = winsize[1] - gaze_data.left_eye.gaze_point.position_on_display_area[1] * winsize[1]\n lp = gaze_data.left_eye.pupil.diameter\n lv = gaze_data.left_eye.gaze_point.validity\n rx = gaze_data.right_eye.gaze_point.position_on_display_area[0] * winsize[0]\n ry = winsize[1] - gaze_data.right_eye.gaze_point.position_on_display_area[1] * winsize[1]\n rp = gaze_data.right_eye.pupil.diameter\n rv = gaze_data.right_eye.gaze_point.validity\n \n # Add gaze data to the buffer \n gaze_data_buffer.append((t,lx,ly,lp,lv,rx,ry,rp,rv,trigger, frame_indx))\n trigger = ''\n \ndef write_buffer_to_file(buffer, output_path):\n\n # Make a copy of the buffer and clear it\n buffer_copy = buffer[:]\n buffer.clear()\n \n # Define column names\n columns = ['time', 'L_X', 'L_Y', 'L_P', 'L_V', \n 'R_X', 'R_Y', 'R_P', 'R_V', 'Event', 'FrameIndex']\n\n # Convert buffer to DataFrame\n out = pd.DataFrame(buffer_copy, columns=columns)\n \n # Check if the file exists\n file_exists = not os.path.isfile(output_path)\n \n # Write the DataFrame to an HDF5 file\n out.to_csv(output_path, mode='a', index =False, header = file_exists)\n \n \n \n#%% Load and prepare stimuli\n\nos.chdir(r'C:\\Users\\tomma\\Desktop\\EyeTracking\\Files')\n\n# Winsize\nwinsize = (960, 540)\n\n# create a window\nwin = visual.Window(size = winsize,fullscr=False, units=\"pix\", screen=0)\n\n\n# Load images and video\nfixation = visual.ImageStim(win, image='EXP\\\\Stimuli\\\\fixation.png', size = (200, 200))\nVideo = visual.MovieStim(win, filename='EXP\\\\Stimuli\\\\Video60.mp4', loop=False, size=[600,380],volume =0.4, autoStart=True) \n\n\n# Define the trigger and frame index variable to pass to the gaze_data_callback\ntrigger = ''\nframe_indx = np.nan\n\n\n\n#%% Record the data\n\n# Find all connected eye trackers\nfound_eyetrackers = tr.find_all_eyetrackers()\n\n# We will just use the first one\nEyetracker = found_eyetrackers[0]\n\n#Start recording\nEyetracker.subscribe_to(tr.EYETRACKER_GAZE_DATA, gaze_data_callback)\n\n# Crate empty list to append data\ngaze_data_buffer = []\n\nTrials_number = 10\nfor trial in range(Trials_number):\n\n ### Present the fixation\n win.flip() # we flip to clean the window\n\n \n fixation.draw()\n win.flip()\n trigger = 'Fixation'\n core.wait(1) # wait for 1 second\n\n Video.play()\n trigger = 'Video'\n while not Video.isFinished:\n\n # Draw the video frame\n Video.draw()\n\n # Flip the window and add index to teh frame_indx\n win.flip()\n \n # add which frame was just shown to the eyetracking data\n frame_indx = Video.frameIndex\n \n Video.stop()\n win.flip()\n\n\n ### ISI\n win.flip() # we re-flip at the end to clean the window\n clock = core.Clock()\n write_buffer_to_file(gaze_data_buffer, 'DATA\\\\RAW\\\\Test.csv')\n while clock.getTime() < 1:\n pass\n \n ### Check for closing experiment\n keys = event.getKeys() # collect list of pressed keys\n if 'escape' in keys:\n win.close() # close window\n Eyetracker.unsubscribe_from(tr.EYETRACKER_GAZE_DATA, gaze_data_callback) # unsubscribe eyetracking\n core.quit() # stop study\n \nwin.close() # close window\nEyetracker.unsubscribe_from(tr.EYETRACKER_GAZE_DATA, gaze_data_callback) # unsubscribe eyetracking\ncore.quit() # stop study"
},
{
"objectID": "CONTENT/Workshops/GAP_2024.html#calibration",
"href": "CONTENT/Workshops/GAP_2024.html#calibration",
"title": "Bridging the Technological Gap Workshop (GAP)",
"section": "Calibration",
"text": "Calibration\nWe received a question about the calibration. How to change the focus time that the eye-tracking uses to record samples for each calibration point. Luckily, the function from the Psychopy_tobii_infant repository allows for an additional argument that specifies how long we want the focus time (default = 0.5s). Thus, you can simply change it by running it with a different value.\nHere below we changed the example of Calibrating eye-tracking by increasing the focus_time to 2s. You can increase or decrease it based on your needs!!\n\nimport os\nfrom psychopy import visual, sound\n\n# import Psychopy tobii infant\nos.chdir(r\"C:\\Users\\tomma\\Desktop\\EyeTracking\\Files\\Calibration\")\nfrom psychopy_tobii_infant import TobiiInfantController\n\n\n#%% window and stimuli\nwinsize = [1920, 1080]\nwin = visual.Window(winsize, fullscr=True, allowGUI=False,screen = 1, color = \"#a6a6a6\", unit='pix')\n\n# visual stimuli\nCALISTIMS = glob.glob(\"CalibrationStim\\\\*.png\")\n\n# video\nVideoGrabber = visual.MovieStim(win, \"CalibrationStim\\\\Attentiongrabber.mp4\", loop=True, size=[800,450],volume =0.4, unit = 'pix') \n\n# sound\nSound = sound.Sound(directory + \"CalibrationStim\\\\audio.wav\")\n\n\n#%% Center face - screen\n\n# set video playing\nVideoGrabber.setAutoDraw(True)\nVideoGrabber.play()\n\n# show the relative position of the subject to the eyetracker\nEyeTracker.show_status()\n\n# stop the attention grabber\nVideoGrabber.setAutoDraw(False)\nVideoGrabber.stop()\n\n\n#%% Calibration\n\n# define calibration points\nCALINORMP = [(-0.4, 0.4), (-0.4, -0.4), (0.0, 0.0), (0.4, 0.4), (0.4, -0.4)]\nCALIPOINTS = [(x * winsize[0], y * winsize[1]) for x, y in CALINORMP]\n\nsuccess = controller.run_calibration(CALIPOINTS, CALISTIMS, audio = Sound, focus_time=2)\nwin.flip()"
},
{
"objectID": "CONTENT/Stats/ModelContrasts.html",
"href": "CONTENT/Stats/ModelContrasts.html",
"title": "ModelEstimates",
"section": "",
"text": "Welcome back, stats adventurer! 👋 If you are here, it means you have already gone through our tutorial on: Linear models, Linear mixed effect models and Generalized linear models. Well done!! You are nearly there, my young stats padawan.\nIn this tutorial, we’re going to take our analysis skills to the next level. One of the coolest and most important aspects of linear models (in my opinion!) isn’t just the p-values you get for each predictor. Don’t get me wrong—those are important and tell you where there is an effect. But after knowing where the effect is, the exciting part is trying to understand what our effect actually is. In other words, how can it be described?\nBe ready!! We’ll explore how to extract and interpret predictions from your model, before learning how to transform these predictions into meaningful statistical insights that tell the complete story of your eyetracking data."
},
{
"objectID": "CONTENT/Stats/ModelContrasts.html#what-are-predictions-anyway",
"href": "CONTENT/Stats/ModelContrasts.html#what-are-predictions-anyway",
"title": "ModelEstimates",
"section": "What Are Predictions, Anyway?",
"text": "What Are Predictions, Anyway?\nNow we can extract the predictions from our model. We want to see what our model predicts the values of Looking time would be for each row of our dataframe. Here is where the Easystats package (specifically the modelbased sub-package) comes to the rescue with the function estimate_expectation().\n\nPred = estimate_expectation(mod)\nhead(Pred)\n\nModel-based Predictions\n\nStandardTrialN | Event | SES | Id | Predicted | SE | 95% CI | Residuals\n----------------------------------------------------------------------------------------\n-1.47 | Simple | high | 1 | 1146.35 | 97.20 | [955.22, 1337.49] | -3.05\n-1.13 | Simple | high | 1 | 1055.49 | 95.05 | [868.58, 1242.40] | -1.67\n-0.95 | Simple | high | 1 | 1010.06 | 94.92 | [823.39, 1196.73] | -6.70\n-0.43 | Simple | high | 1 | 873.77 | 98.36 | [680.34, 1067.20] | -20.55\n-0.26 | Simple | high | 1 | 828.34 | 100.70 | [630.30, 1026.38] | -54.90\n0.26 | Simple | high | 1 | 692.05 | 110.79 | [474.18, 909.92] | 50.76\n\nVariable predicted: LookingTime\n\n\nLook!! The function gave us a new dataframe with all the levels of Event and of StandardTrialN and ID that we had in our dataframe. OK, OK, I will agree….this looks so similar to the original dataframe we had…so why even bother???? Can’t we just use the original dataframe?? Well because the predictions represent what our model thinks the data should look like based on the patterns it discovered, after accounting for random effects and noise!\nLet’s visualize the difference between the raw data and the predictions\n\n\n\n\n\n\n\n\nAs you can see, the raw data and the predicted data are very similar but not the same!! The predictions represent our model’s “best guess” at what the looking times should be based on the fixed effects (StandardTrialN, Event and SES) and random effects (individual participant differences). This smooths out some of the noise in the original data and gives us a simpler picture of the underlying patterns!\nThis is cool indeed… but how can this help me?? Let’s remember our question about Events…."
},
{
"objectID": "CONTENT/Stats/ModelContrasts.html#predictions-for-reference-values",
"href": "CONTENT/Stats/ModelContrasts.html#predictions-for-reference-values",