,
vignettes/intro.Rmd
intro.RmdThe package eeguana provides a framework for doing simple
pre-processing with specialized functions and manipulating EEG data with
dplyr verbs (e.g., eeg_mutate,
eeg_filter, eeg_summarize) extended to a new
class eeg_lst, and ggplot wrapper functions.
The new class is inspired by tidyverse principles but it’s not really
“tidy” (due to space considerations), it’s a list of (i) a wide table
that contains the signal amplitudes at every sample point of the EEG,
(ii) an events table with information about markers (or triggers),
blinks and other exported information, and (iii) a long table with
experimental information, such as participant (recording),
conditions, etc.
While it’s possible to transform the eeg_lst to a
data.frame, data.table or a
tibble (with as.data.frame(),
as.data.table() and as_tibble()), the
motivation for manipulating the data in the eeg_lst format
has to do with size considerations. In this case, the original file was
113 MB, converting it to a long format entails a lot of repetition and
generates an object of 556 MB. While this will still work here, a long
format quickly becomes prohibitive in real settings with longer
recordings.
Dplyr-like verbs always return an eeg_lst object, which
allows us to use magrittr’s pipe, %>% (see
?`dplyr-eeguana`) or the relatively new R pipe
|>. In addition, eeg_/ch_
functions will also return an eeg_lst unless they have a
suffix _tbl that indicates they return a data frame. In
general, we will work with the eeg_lst, unless we want to
modify the channels information (with channels_tbl()) or
the events table containing markers, artifacts ans custom annotations
(with events_tbl).
Here, I exemplify the use of eeguana with raw EEG data exported in the format of BrainVision 1.0. The data belong to a simple experiment where a participant was presented 100 faces and 100 assorted images in random order. The task of the experiment was to mentally count the number of faces.
First we download the data:
# Run the following or just download the files from raw_faces folder in https://osf.io/tbwvz/
library(httr)
GET("https://osf.io/wv5hp//?action=download",
write_disk("./s1_faces.vhdr", overwrite = TRUE),
progress()
)
GET("https://osf.io/c8wea//?action=download",
write_disk("./s1_faces.vmrk", overwrite = TRUE),
progress()
)
GET("https://osf.io/uhsde//?action=download",
write_disk("./s1_faces.eeg", overwrite = TRUE),
progress()
)BrainVision 1.0 exports three files: s1_faces.vhdr,
s1_faces.vmrk, and s1_faces.eeg. The file
s1_faces.vhdr contains the metadata and links to the other
two files, s1_faces.vmrk contains the triggers and other
events in the samples, and s1_faces.eeg contains the
signals at every sample for every channel recorded.
We load the relevant packages first. Here, I use
tidytable which is a faster alternative to
dplyr with a very similar interface.
library(tidytable) # faster alternative to dplyr
library(ggplot2)
library(stringr)
library(eeguana)
set.seed(123) # ICA will always find the same componentsWe first need to read the data:
faces <- read_vhdr("s1_faces.vhdr")
#> # Reading file s1_faces.vhdr...
#> # Data from ./s1_faces.eeg was read.
#> # Data from 1 segment(s) and 34 channels was loaded.
#> # Object size in memory 113.4 MbThe function read_vhdr() creates a list with data frames
for the signal, events, segments information, and incorporates in its
attributes generic EEG information.
faces
#> # EEG data:
#>
#> # Signal table:
#> .id .sample Fp1 Fpz Fp2 F7
#> <int> <sample_int> <channel_dbl> <channel_dbl> <channel_dbl> <channel_dbl>
#> 1: 1 1 -17878.56 -8921.395 -14028.81 -284.2541
#> 2: 1 2 -17903.75 -8948.022 -14056.53 -303.9134
#> 3: 1 3 -17916.55 -8961.906 -14070.91 -307.1683
#> 4: 1 4 -17918.17 -8963.691 -14068.63 -299.8859
#> 5: 1 5 -17917.71 -8965.658 -14069.16 -294.7915
#> ---
#> 424484: 1 424484 -16094.71 -8234.730 -13505.21 609.9043
#> 424485: 1 424485 -16140.11 -8280.301 -13547.56 603.1918
#> 424486: 1 424486 -16145.24 -8275.078 -13528.22 541.3096
#> 424487: 1 424487 -16165.22 -8294.479 -13546.50 467.7130
#> 424488: 1 424488 -16167.39 -8305.199 -13561.12 576.3062
#> F3 Fz F4 F8 FC5
#> <channel_dbl> <channel_dbl> <channel_dbl> <channel_dbl> <channel_dbl>
#> 1: -1378.1467 -8077.128 1167.489 -1663.4490 -2315.080
#> 2: -1400.6560 -8101.660 1145.292 -1685.9034 -2338.859
#> 3: -1407.8466 -8114.331 1138.028 -1691.4203 -2343.475
#> 4: -1401.9985 -8116.114 1139.702 -1683.0713 -2333.746
#> 5: -1401.2996 -8119.442 1136.373 -1684.2664 -2326.648
#> ---
#> 424484: -236.9552 -7991.669 1977.146 -162.8618 -2290.511
#> 424485: -311.0485 -8045.019 1938.122 -183.0542 -2424.133
#> 424486: -291.0218 -8032.237 1965.358 -132.3712 -2291.431
#> 424487: -302.0740 -8052.393 1930.877 -180.4978 -2361.589
#> 424488: -319.2688 -8065.579 1927.474 -188.0929 -2368.595
#> FC1 FC2 FC6 M1 T7
#> <channel_dbl> <channel_dbl> <channel_dbl> <channel_dbl> <channel_dbl>
#> 1: -9040.689 -795.0549 -3665.146 0 -5156.998
#> 2: -9064.137 -816.1298 -3688.133 0 -5184.417
#> 3: -9071.438 -823.4675 -3690.046 0 -5186.146
#> 4: -9065.608 -817.9690 -3681.642 0 -5174.358
#> 5: -9061.397 -817.2882 -3679.563 0 -5170.146
#> ---
#> 424484: -9349.164 -318.8641 -2805.395 0 -3703.158
#> 424485: -9410.679 -364.1038 -2833.090 0 -3834.959
#> 424486: -9379.728 -342.3297 -2811.445 0 -3773.260
#> 424487: -9381.456 -358.2924 -2868.325 0 -3746.392
#> 424488: -9409.171 -363.8277 -2853.797 0 -3615.768
#> C3 Cz C4 T8 M2
#> <channel_dbl> <channel_dbl> <channel_dbl> <channel_dbl> <channel_dbl>
#> 1: -2466.320 -1568.4097 -4427.577 -10351.511 -1845.363
#> 2: -2490.411 -1591.3053 -4450.049 -10372.402 -1850.862
#> 3: -2495.726 -1597.7970 -4454.775 -10378.323 -1845.087
#> 4: -2487.358 -1591.5627 -4449.590 -10373.983 -1836.370
#> 5: -2481.878 -1588.5833 -4446.334 -10370.489 -1841.961
#> ---
#> 424484: -2301.288 -395.4219 -2952.459 -9502.499 -2128.771
#> 424485: -2372.439 -444.3945 -2994.426 -9465.224 -2179.546
#> 424486: -2312.064 -419.6414 -2975.410 -9389.198 -2153.340
#> 424487: -2304.304 -426.3353 -2986.242 -9369.264 -2141.644
#> 424488: -2358.720 -437.9209 -2984.145 -9521.864 -2130.978
#> CP5 CP1 CP2 CP6 P7
#> <channel_dbl> <channel_dbl> <channel_dbl> <channel_dbl> <channel_dbl>
#> 1: -8938.128 -4755.838 -1369.6688 -1729.708 -7513.455
#> 2: -8963.231 -4778.917 -1391.3324 -1750.029 -7535.836
#> 3: -8969.171 -4784.986 -1397.7506 -1757.532 -7541.739
#> 4: -8957.989 -4775.938 -1391.9026 -1754.553 -7530.282
#> 5: -8950.726 -4772.205 -1388.7207 -1755.197 -7524.361
#> ---
#> 424484: -8037.571 -4200.332 -677.7635 -1046.501 -5433.473
#> 424485: -8083.840 -4253.405 -721.7708 -1080.431 -5470.565
#> 424486: -8027.603 -4225.636 -702.5348 -1048.874 -5474.501
#> 424487: -8046.398 -4230.160 -709.1735 -1040.856 -5488.533
#> 424488: -8060.742 -4244.541 -712.0606 -1053.931 -5478.491
#> P3 Pz P4 P8 POz
#> <channel_dbl> <channel_dbl> <channel_dbl> <channel_dbl> <channel_dbl>
#> 1: -8512.474 -537.63159 -99.10343 -875.6397 -852.6708
#> 2: -8533.438 -557.97095 -117.14455 -892.3933 -871.1710
#> 3: -8537.741 -561.55725 -122.82701 -892.9816 -873.9114
#> 4: -8529.099 -552.78510 -117.75130 -886.6740 -866.0771
#> 5: -8523.562 -549.19885 -117.82441 -885.3681 -865.5070
#> ---
#> 424484: -8150.485 -34.94090 496.89807 -182.1897 -304.2810
#> 424485: -8197.932 -79.35287 458.84891 -229.4153 -345.6953
#> 424486: -8173.215 -60.83399 476.24600 -226.5095 -342.8815
#> 424487: -8182.244 -64.40166 471.66693 -247.6580 -343.4331
#> 424488: -8187.780 -69.53255 475.47369 -247.5108 -350.5685
#> O1 Oz O2 HEOG VEOG
#> <channel_dbl> <channel_dbl> <channel_dbl> <channel_dbl> <channel_dbl>
#> 1: -4117.080 -1950.149 -817.4540 -5006.420 -9116.806
#> 2: -4136.997 -1965.578 -825.3984 -5027.017 -9133.780
#> 3: -4138.725 -1965.137 -824.5893 -5037.775 -9138.781
#> 4: -4126.606 -1957.450 -823.7434 -5033.031 -9134.222
#> 5: -4127.543 -1967.270 -834.0415 -5031.210 -9139.996
#> ---
#> 424484: -2730.290 -1170.744 -67.8408 -3137.224 -8591.514
#> 424485: -2777.571 -1209.235 -104.1242 -3216.466 -8672.283
#> 424486: -2794.121 -1232.792 -135.4974 -3136.874 -8648.578
#> 424487: -2790.444 -1224.516 -152.9679 -3105.096 -8658.159
#> 424488: -2804.640 -1245.077 -186.8792 -3162.988 -8660.311
#>
#> # Events table:
#> .id .type .description .initial .final .channel
#> <int> <char> <char> <sample_int> <sample_int> <char>
#> 1: 1 New Segment 1 1 <NA>
#> 2: 1 Stimulus s111 10528 10528 <NA>
#> 3: 1 Stimulus s130 10742 10742 <NA>
#> 4: 1 Stimulus s71 11175 11175 <NA>
#> 5: 1 Stimulus s130 12806 12806 <NA>
#> ---
#> 401: 1 Stimulus s130 411197 411197 <NA>
#> 402: 1 Stimulus s71 411469 411469 <NA>
#> 403: 1 Stimulus s121 412943 412943 <NA>
#> 404: 1 Stimulus s122 412966 412966 <NA>
#> 405: 1 Stimulus s102 412989 412989 <NA>
#>
#> # Segments table:
#> .id .recording segment
#> <int> <char> <int>
#> 1: 1 s1_faces.vhdr 1
summary(faces)
#> # EEG data:
#> # Sampling rate: 512 Hz.
#> # Size in memory: 113.4 Mb.
#> # Total duration: 00:13:49.
#> # Summary of segments
#> .recording n_segments n_incomplete
#> <char> <int> <int>
#> 1: s1_faces.vhdr 1 0
#> # Summary of events
#> .type .description n
#> <char> <char> <int>
#> 1: New Segment 1
#> 2: Stimulus s102 1
#> 3: Stimulus s111 1
#> 4: Stimulus s121 1
#> 5: Stimulus s122 1
#> 6: Stimulus s130 200
#> 7: Stimulus s70 100
#> 8: Stimulus s71 100We see that there is no electrode positions in the object, but since
we know that the layout was a standard 10/20, we’ll add this layout to
the object, using the dataset layout_32_1020. This is
required to be able to create topographic plots.
channels_tbl(faces)
#> .channel number .reference resolution unit radius theta phi .x .y .z
#> <char> <char> <char> <num> <char> <num> <num> <num> <num> <num> <num>
#> 1: Fp1 Ch1 <NA> 1 microvolts NA NA NA NA NA NA
#> 2: Fpz Ch2 <NA> 1 microvolts NA NA NA NA NA NA
#> 3: Fp2 Ch3 <NA> 1 microvolts NA NA NA NA NA NA
#> 4: F7 Ch4 <NA> 1 microvolts NA NA NA NA NA NA
#> 5: F3 Ch5 <NA> 1 microvolts NA NA NA NA NA NA
#> 6: Fz Ch6 <NA> 1 microvolts NA NA NA NA NA NA
#> 7: F4 Ch7 <NA> 1 microvolts NA NA NA NA NA NA
#> 8: F8 Ch8 <NA> 1 microvolts NA NA NA NA NA NA
#> 9: FC5 Ch9 <NA> 1 microvolts NA NA NA NA NA NA
#> 10: FC1 Ch10 <NA> 1 microvolts NA NA NA NA NA NA
#> 11: FC2 Ch11 <NA> 1 microvolts NA NA NA NA NA NA
#> 12: FC6 Ch12 <NA> 1 microvolts NA NA NA NA NA NA
#> 13: M1 Ch13 <NA> 1 microvolts NA NA NA NA NA NA
#> 14: T7 Ch14 <NA> 1 microvolts NA NA NA NA NA NA
#> 15: C3 Ch15 <NA> 1 microvolts NA NA NA NA NA NA
#> 16: Cz Ch16 <NA> 1 microvolts NA NA NA NA NA NA
#> 17: C4 Ch17 <NA> 1 microvolts NA NA NA NA NA NA
#> 18: T8 Ch18 <NA> 1 microvolts NA NA NA NA NA NA
#> 19: M2 Ch19 <NA> 1 microvolts NA NA NA NA NA NA
#> 20: CP5 Ch20 <NA> 1 microvolts NA NA NA NA NA NA
#> 21: CP1 Ch21 <NA> 1 microvolts NA NA NA NA NA NA
#> 22: CP2 Ch22 <NA> 1 microvolts NA NA NA NA NA NA
#> 23: CP6 Ch23 <NA> 1 microvolts NA NA NA NA NA NA
#> 24: P7 Ch24 <NA> 1 microvolts NA NA NA NA NA NA
#> 25: P3 Ch25 <NA> 1 microvolts NA NA NA NA NA NA
#> 26: Pz Ch26 <NA> 1 microvolts NA NA NA NA NA NA
#> 27: P4 Ch27 <NA> 1 microvolts NA NA NA NA NA NA
#> 28: P8 Ch28 <NA> 1 microvolts NA NA NA NA NA NA
#> 29: POz Ch29 <NA> 1 microvolts NA NA NA NA NA NA
#> 30: O1 Ch30 <NA> 1 microvolts NA NA NA NA NA NA
#> 31: Oz Ch31 <NA> 1 microvolts NA NA NA NA NA NA
#> 32: O2 Ch32 <NA> 1 microvolts NA NA NA NA NA NA
#> 33: HEOG Ch33 <NA> 1 microvolts NA NA NA NA NA NA
#> 34: VEOG Ch34 <NA> 1 microvolts NA NA NA NA NA NA
#> .channel number .reference resolution unit radius theta phi .x .y .z
#> <char> <char> <char> <num> <char> <num> <num> <num> <num> <num> <num>
## In case the order of the electrodes is different, we do a left_join instead of replacing the table:
channels_tbl(faces) <- select(channels_tbl(faces), .channel) |>
left_join(layout_32_1020)The plots that eeguana produce are
ggplot objects that can be modified like regular
ggplot.
plot(faces) +
ggtitle("All the experiment")
#> # Downsampling from 512Hz to 256Hz.
#> # Object size in memory 56.7 Mb

Here the preprocessing is done with only one EEG dataset. If the EEG
data of different participants is available, one can repeat a similar
preprocessing pipeline for each dataset and bind the EEG datasets with
bind().
In this dataset, the signal from all electrodes is monopolar and referenced to the left mastoid (M1). We want the signal (excluding the EOG channels) to be referenced to linked (left and right) mastoids (M1 and M2).
faces <- eeg_rereference(faces, -VEOG, -HEOG, .ref = c("M1", "M2"))We apply a band pass filter of 0.1 to 30 Hz to all the channels except the EOG channels. We don’t segment yet, because discontinuities in the signal create artifacts on the edges.
faces_filt <- eeg_filt_band_pass(faces, -HEOG, -VEOG, .freq = c(.1, 30))
#> Setting up band-pass filter from 0.1 - 30 Hz
#> Width of the transition band at the low cut-off frequency is 0.1 Hz
#> Width of the transition band at the high cut-off frequency is 7.5 HzWe want to apply ICA to as much data as possible, but to “representative” data: to data of the experiment and not when the participants were moving or reading the instructions. For the same reason, we want to the ignore artifacts that we are sure are not representing brain activity, because of the extreme amplitudes in the signal.
We first cut a large segment that excludes data before and after the
experiment was ran using eeg_segment() (that is, only data
from the marker “s111” to “s121”), and then we mark differences of 200
microvolts between peaks with eeg_artif_minmax().
faces_ls <- eeg_segment(faces_filt,
.description == "s111",
.end = .description == "s121") |>
eeg_artif_minmax(-HEOG, -VEOG,
.threshold = 200,
.window = 200,
.unit = "ms")
#> # Total of 1 segments found.
#> # Object size in memory 107.5 Mb after segmentation.
#> # Number of intervals with artifacts: 118eeg_artif_minimax() only adds the artifacts in the
events table and doesn’t modify the signal. We can have a look at the
type of artifacts that were detected, by plotting the signal and the
events with annotate_events(). It would be a good idea to
just check a couple of seconds of the signal, for that we use the
dplyr-like function eeg_filter().
faces_ls |>
eeg_select(-HEOG, -VEOG)|>
eeg_filter(as_time(.sample, .unit = "s") |> between(0, 90)) |>
plot() +
annotate_events() +
theme(legend.position = "bottom")
#> # Downsampling from 512Hz to 256Hz.
#> # Object size in memory 5.9 Mb

Now we can run ICA, removing the EOG and reference electrodes, and ignoring the artifacts.
## By default, it will ignore artifacts
faces_ica <- faces_ls |>
eeg_ica(-HEOG, -VEOG, -M1, -M2, .method = adapt_fast_ICA, .ignore = .type == "artifact")
#> # ICA is being done using adapt_fast_ICA...
#> # 97% of the samples will be used.
#> # ICA took 1.09 minsWe can now check the different topographic plots of the ICAs with
plot_components(), and their correlation with the EOG
channels looking a the summary of the object.
faces_ica |> plot_components()
eeg_ica_summary_tbl(faces_ica)
#> EOG channels detected as: HEOG, VEOG
#> # Downsampling from 512Hz to 256Hz.
#> # Object size in memory 53.8 Mb
#> .recording EOG .ICA cor var
#> <char> <char> <char> <num> <num>
#> 1: s1_faces.vhdr VEOG ICA5 0.3110616894 3.757060e-01
#> 2: s1_faces.vhdr HEOG ICA5 0.0060600201 3.757060e-01
#> 3: s1_faces.vhdr VEOG ICA14 0.0594109202 7.014123e-02
#> 4: s1_faces.vhdr HEOG ICA14 0.0115211860 7.014123e-02
#> 5: s1_faces.vhdr VEOG ICA25 0.0191137136 5.700112e-02
#> 6: s1_faces.vhdr HEOG ICA25 0.0079409656 5.700112e-02
#> 7: s1_faces.vhdr VEOG ICA28 0.0215901453 5.238918e-02
#> 8: s1_faces.vhdr HEOG ICA28 -0.0026613292 5.238918e-02
#> 9: s1_faces.vhdr VEOG ICA13 -0.0733915975 4.883691e-02
#> 10: s1_faces.vhdr HEOG ICA13 0.0028042746 4.883691e-02
#> 11: s1_faces.vhdr VEOG ICA16 0.0189808192 3.920680e-02
#> 12: s1_faces.vhdr HEOG ICA16 -0.0052139432 3.920680e-02
#> 13: s1_faces.vhdr VEOG ICA27 -0.0146958686 3.894493e-02
#> 14: s1_faces.vhdr HEOG ICA27 -0.0005737728 3.894493e-02
#> 15: s1_faces.vhdr HEOG ICA26 -0.0127623329 3.356974e-02
#> 16: s1_faces.vhdr VEOG ICA26 -0.0103929696 3.356974e-02
#> 17: s1_faces.vhdr VEOG ICA21 -0.0110379588 3.209207e-02
#> 18: s1_faces.vhdr HEOG ICA21 0.0041385932 3.209207e-02
#> 19: s1_faces.vhdr VEOG ICA30 -0.0318988532 3.195685e-02
#> 20: s1_faces.vhdr HEOG ICA30 -0.0049492927 3.195685e-02
#> 21: s1_faces.vhdr HEOG ICA11 -0.0080689883 1.531692e-02
#> 22: s1_faces.vhdr VEOG ICA11 -0.0022034332 1.531692e-02
#> 23: s1_faces.vhdr VEOG ICA8 -0.0061078333 1.503484e-02
#> 24: s1_faces.vhdr HEOG ICA8 -0.0033160702 1.503484e-02
#> 25: s1_faces.vhdr VEOG ICA22 -0.0729992398 1.342674e-02
#> 26: s1_faces.vhdr HEOG ICA22 -0.0075857794 1.342674e-02
#> 27: s1_faces.vhdr VEOG ICA19 -0.0113037545 1.276725e-02
#> 28: s1_faces.vhdr HEOG ICA19 0.0040990970 1.276725e-02
#> 29: s1_faces.vhdr HEOG ICA29 -0.0098960226 1.268456e-02
#> 30: s1_faces.vhdr VEOG ICA29 0.0082034132 1.268456e-02
#> 31: s1_faces.vhdr VEOG ICA10 0.0029772146 1.075145e-02
#> 32: s1_faces.vhdr HEOG ICA10 -0.0012705350 1.075145e-02
#> 33: s1_faces.vhdr VEOG ICA23 -0.0139531104 1.074641e-02
#> 34: s1_faces.vhdr HEOG ICA23 0.0080386572 1.074641e-02
#> 35: s1_faces.vhdr VEOG ICA1 0.0590125612 9.976911e-03
#> 36: s1_faces.vhdr HEOG ICA1 -0.0556782278 9.976911e-03
#> 37: s1_faces.vhdr VEOG ICA2 -0.0126428949 7.926289e-03
#> 38: s1_faces.vhdr HEOG ICA2 -0.0078820112 7.926289e-03
#> 39: s1_faces.vhdr VEOG ICA24 -0.0121801971 4.817679e-03
#> 40: s1_faces.vhdr HEOG ICA24 -0.0016042248 4.817679e-03
#> 41: s1_faces.vhdr HEOG ICA7 -0.0020891334 3.213752e-03
#> 42: s1_faces.vhdr VEOG ICA7 -0.0003910153 3.213752e-03
#> 43: s1_faces.vhdr VEOG ICA12 0.0144307012 1.612964e-03
#> 44: s1_faces.vhdr HEOG ICA12 0.0128083915 1.612964e-03
#> 45: s1_faces.vhdr HEOG ICA17 -0.0091874914 1.242015e-03
#> 46: s1_faces.vhdr VEOG ICA17 0.0017426840 1.242015e-03
#> 47: s1_faces.vhdr VEOG ICA18 -0.0131764353 8.322457e-04
#> 48: s1_faces.vhdr HEOG ICA18 -0.0117696565 8.322457e-04
#> 49: s1_faces.vhdr HEOG ICA4 -0.0190202998 5.031454e-04
#> 50: s1_faces.vhdr VEOG ICA4 -0.0171506626 5.031454e-04
#> 51: s1_faces.vhdr VEOG ICA6 -0.0089105463 4.392834e-05
#> 52: s1_faces.vhdr HEOG ICA6 0.0049753303 4.392834e-05
#> 53: s1_faces.vhdr VEOG ICA20 -0.0062150226 1.952812e-06
#> 54: s1_faces.vhdr HEOG ICA20 -0.0011994425 1.952812e-06
#> 55: s1_faces.vhdr HEOG ICA9 -0.0019097967 -4.193406e-04
#> 56: s1_faces.vhdr VEOG ICA9 0.0015343447 -4.193406e-04
#> 57: s1_faces.vhdr VEOG ICA15 -0.0265727074 -1.012186e-03
#> 58: s1_faces.vhdr HEOG ICA15 -0.0041488999 -1.012186e-03
#> 59: s1_faces.vhdr VEOG ICA3 0.0866623859 -7.459674e-03
#> 60: s1_faces.vhdr HEOG ICA3 -0.0019740756 -7.459674e-03
#> .recording EOG .ICA cor var
#> <char> <char> <char> <num> <num>We’ll look closer at the ICA that are more likely to be related to eye movements, and we’ll compare with signals that look like blinks and saccades:
faces_ica <- faces_ica |>
eeg_artif_step(HEOG, VEOG,
.threshold = 30,
.window = 200,
.unit = "ms",
.freq = c(1, 10)) |>
eeg_artif_peak(VEOG, .threshold = 100, .freq = c(1, 10))
#> Setting up band-pass filter from 1 - 10 Hz
#> Width of the transition band at the low cut-off frequency is 1 Hz
#> Width of the transition band at the high cut-off frequency is 2.5 Hz
#> # Number of intervals with artifacts: 407
#> Setting up band-pass filter from 1 - 10 Hz
#> Width of the transition band at the low cut-off frequency is 1 Hz
#> Width of the transition band at the high cut-off frequency is 2.5 Hz
#> # Number of intervals with artifacts: 158In order to investigate the ICA, we are using an experimental
function not yet exported (that’s why we use the :::). This function
will probably converted into a shiny app. It’s possible to get the same
plots by using a combination of plot_topo,
eeg_ica_show, eeg_ica_summary_tbl, and some
ggplot and cowplot functions.
We’ll see how the components explaining the most variance and most correlated to the EOG channels behave:
#samples with blinks
s_peaks <- filter(events_tbl(faces_ica), str_starts(.description, "peak")) |>
pull(.initial)
eeguana:::plot_ica.eeg_ica_lst(faces_ica, samples = seq(s_peaks[1]-2000,s_peaks[1]+2000))
#> Warning in eeguana:::plot_ica.eeg_ica_lst(faces_ica, samples = seq(s_peaks[1] - : This is
#> an experimental function, and it might change or disappear in the future. (Or it might be
#> transformed into a shinyapp)
#> Using recording: s1_faces.vhdr
#> EOG channels detected as: HEOG, VEOG
#> Calculating the correlation of ICA components with filtered EOG channels...
#> Setting up band-pass filter from 0.1 - 30 Hz
#> Width of the transition band at the low cut-off frequency is 0.1 Hz
#> Width of the transition band at the high cut-off frequency is 7.5 Hz
#> EOG channels detected as: HEOG, VEOG
#> # Downsampling from 512Hz to 256Hz.
#> # Object size in memory 53.9 Mb
#> The following columns of signal_tbl are not channels (or ICA components): ICA5ICA14ICA25ICA28ICA13ICA16ICA27ICA26ICA21ICA30ICA11ICA8ICA22ICA19ICA29ICA10
#> * To build a channel use `channel_dbl()` function, e.g. channel_dbl(0) to populate the table with a channel containing 0 microvolts.
#> * To copy the structure of an existing channel one can do `new_ch = existing_channel * 0 + ...`

The clearest ones is ICA5, and so we’ll just remove that one.
faces_icaed <- faces_ica |>
eeg_ica_keep(-ICA5) |>
as_eeg_lst()Now we’ll segment the data to appropriate check if there are still artifacts.
events_tbl(faces_icaed) <- events_tbl(faces_icaed) |>
filter(!.type %in% "artifact")
faces_seg <- faces_icaed |>
eeg_select(-description, -type) |>
eeg_segment(.description %in% c("s70", "s71"), .lim = c(-.1, .5))
#> # Total of 200 segments found.
#> # Object size in memory 16.5 Mb after segmentation.
faces_seg_artif <- faces_seg |>
eeg_artif_minmax(-HEOG, -VEOG, .threshold = 100, .window = 150, .unit = "ms") |>
eeg_artif_step(-HEOG, -VEOG, .threshold = 50, .window = 200, .unit = "ms")
#> # Number of intervals with artifacts: 0
#> # Number of intervals with artifacts: 1
## extracts the ids of the segments with artifacts
bad <- filter(events_tbl(faces_seg_artif), .type == "artifact") |>
pull(.id) |>
unique()
## Show the segment with artifact and one before and after:
faces_seg_artif |>
eeg_filter(.id %in% c(bad-1, bad, bad+1)) |>
eeg_select(-VEOG, -HEOG) |>
plot() +
annotate_events() +
theme(legend.position = "bottom")

faces_seg <- faces_seg_artif |>
eeg_events_to_NA(.type == "artifact", .entire_seg = TRUE, .drop_events = TRUE)
summary(faces_seg)
#> # EEG data:
#> # Sampling rate: 512 Hz.
#> # Size in memory: 16.5 Mb.
#> # Total duration: 00:02:00.
#> # Summary of segments
#> .recording n_segments n_incomplete
#> <char> <int> <int>
#> 1: s1_faces.vhdr 200 1
#> # Summary of events
#> .type .description n
#> <char> <char> <int>
#> 1: Stimulus s70 100
#> 2: Stimulus s71 100Finally, we can baseline the segments:
faces_seg <- faces_seg |>
eeg_baseline()We edit the segmentation information and add more descriptive labels.
faces_seg <- faces_seg |>
eeg_mutate(
condition =
ifelse(description == "s70", "faces", "non-faces")
) |>
eeg_select(-type)
faces_seg
#> # EEG data:
#>
#> # Signal table:
#> .id .sample Fp1 Fpz Fp2 F7
#> <int> <sample_int> <channel_dbl> <channel_dbl> <channel_dbl> <channel_dbl>
#> 1: 1 -50 -0.2239737 2.926696 0.1088197 -0.1107286
#> 2: 1 -49 0.9975430 3.910457 0.7585352 0.7172289
#> 3: 1 -48 2.2477072 4.853125 1.5129425 1.7686192
#> 4: 1 -47 3.3361201 5.596977 2.2208750 2.9135916
#> 5: 1 -46 4.0950011 6.007830 2.7411833 4.0297893
#> ---
#> 61596: 200 253 -7.4272958 -4.030794 -3.3414518 2.5704919
#> 61597: 200 254 -7.0028675 -4.076854 -3.7139526 1.3456003
#> 61598: 200 255 -6.3948136 -4.124318 -4.0795149 0.2178476
#> 61599: 200 256 -5.7744059 -4.236721 -4.4670174 -0.8343465
#> 61600: 200 257 -5.3225204 -4.474952 -4.9176401 -1.8464510
#> F3 Fz F4 F8 FC5
#> <channel_dbl> <channel_dbl> <channel_dbl> <channel_dbl> <channel_dbl>
#> 1: 3.793132 4.228278 2.840110 -3.15472635 1.6113910
#> 2: 4.038747 4.270218 2.382479 -2.35472265 2.0405680
#> 3: 4.340948 4.421612 2.013301 -1.24644410 2.4513040
#> 4: 4.642705 4.591250 1.709526 0.05225002 2.8087541
#> 5: 4.899710 4.700407 1.452931 1.40978255 3.1051410
#> ---
#> 61596: 4.188729 -8.730353 -12.167556 -1.36556543 4.6994268
#> 61597: 3.356811 -9.453131 -11.842863 -2.17206941 3.4988400
#> 61598: 2.644884 -9.985577 -11.460650 -2.78238153 2.2688876
#> 61599: 1.996052 -10.366157 -11.047480 -3.20156158 1.0080235
#> 61600: 1.323714 -10.661645 -10.644149 -3.46204537 -0.3005228
#> FC1 FC2 FC6 M1 T7
#> <channel_dbl> <channel_dbl> <channel_dbl> <channel_dbl> <channel_dbl>
#> 1: 1.592771 -2.364200 -1.8391090 -2.030141 8.787014
#> 2: 1.860704 -2.861841 -1.1455898 -1.922150 9.114149
#> 3: 2.219617 -3.123105 -0.1976263 -1.785010 8.977623
#> 4: 2.615841 -3.167049 0.8983126 -1.654058 8.398226
#> 5: 3.005053 -3.015121 2.0401272 -1.545839 7.452935
#> ---
#> 61596: -3.708406 -19.005953 -12.4167239 4.310870 4.908478
#> 61597: -4.958880 -19.459708 -13.3369315 4.454409 4.680512
#> 61598: -6.309382 -19.836152 -14.2119912 4.482182 4.315460
#> 61599: -7.730844 -20.137723 -14.9925296 4.394789 3.730447
#> 61600: -9.204715 -20.380105 -15.6416067 4.203761 2.864558
#> C3 Cz C4 T8 M2
#> <channel_dbl> <channel_dbl> <channel_dbl> <channel_dbl> <channel_dbl>
#> 1: -2.0086889 -5.711339 -0.795271 3.910053 2.030141
#> 2: -2.1197829 -5.858017 -1.282006 5.477720 1.922150
#> 3: -2.2059909 -5.721134 -1.762303 7.069972 1.785010
#> 4: -2.2445165 -5.330143 -2.204568 8.401729 1.654058
#> 5: -2.1993214 -4.723109 -2.557211 9.193991 1.545839
#> ---
#> 61596: -0.5437559 -14.854063 -34.752050 -25.822482 -4.310870
#> 61597: -2.1197706 -16.258766 -35.506742 -27.019577 -4.454409
#> 61598: -3.8504649 -17.808367 -36.200622 -27.809796 -4.482182
#> 61599: -5.6655686 -19.413279 -36.808126 -28.198220 -4.394789
#> 61600: -7.5086186 -20.985826 -37.301315 -28.211914 -4.203761
#> CP5 CP1 CP2 CP6 P7
#> <channel_dbl> <channel_dbl> <channel_dbl> <channel_dbl> <channel_dbl>
#> 1: 2.1730493 -5.558311 -3.912675 2.0442154 -1.8486407
#> 2: 1.6874719 -5.681516 -4.454747 1.4513845 -2.6139953
#> 3: 1.0244766 -5.664134 -4.852158 0.8377522 -3.1638517
#> 4: 0.2760940 -5.506764 -5.089553 0.2248167 -3.4395628
#> 5: -0.4454964 -5.204630 -5.150715 -0.3509800 -3.4148548
#> ---
#> 61596: 3.4352795 -5.102525 -31.824672 -29.5344073 2.7192767
#> 61597: 2.4188361 -6.646814 -33.109751 -30.5440118 2.3539444
#> 61598: 1.1739532 -8.403855 -34.341884 -31.5585170 1.6063736
#> 61599: -0.2539754 -10.263130 -35.433438 -32.5685325 0.5699239
#> 61600: -1.8208137 -12.116925 -36.299732 -33.5345508 -0.6454750
#> P3 Pz P4 P8 POz
#> <channel_dbl> <channel_dbl> <channel_dbl> <channel_dbl> <channel_dbl>
#> 1: -2.6437673 -3.191924 -0.6720984 0.5889623 -1.233383
#> 2: -3.1920017 -3.790144 -1.4646696 -1.8056977 -2.397317
#> 3: -3.6552420 -4.342437 -2.1770814 -4.0661883 -3.500260
#> 4: -3.9730269 -4.813028 -2.7552677 -5.9136674 -4.440224
#> 5: -4.0892851 -5.160562 -3.1471226 -7.1129100 -5.124399
#> ---
#> 61596: 2.7365646 -12.488676 -27.5485924 -23.0238529 -5.742807
#> 61597: 1.6113645 -13.716290 -28.1890858 -22.9764754 -6.553364
#> 61598: 0.2901116 -14.977399 -28.6721202 -22.1172213 -7.394155
#> 61599: -1.1512356 -16.183334 -28.9837949 -20.4604781 -8.207270
#> 61600: -2.6374742 -17.250028 -29.1134647 -18.1089704 -8.943773
#> O1 Oz O2 HEOG VEOG
#> <channel_dbl> <channel_dbl> <channel_dbl> <channel_dbl> <channel_dbl>
#> 1: -3.5386535 -0.7969323 0.8721568 -8.172153 -4.070868
#> 2: -4.7377453 -2.2782486 -0.7106985 -15.399203 -10.286688
#> 3: -5.8496010 -3.7413040 -2.2901256 -17.091586 -4.218329
#> 4: -6.7658792 -5.0345596 -3.6833116 -22.001254 -2.728094
#> 5: -7.3928468 -6.0157379 -4.7178515 -14.368930 3.892999
#> ---
#> 61596: 2.5152642 -3.0701651 -5.1484950 40.897595 104.275391
#> 61597: 1.8963234 -3.5788828 -3.8985745 19.307751 67.568359
#> 61598: 1.0904936 -4.1790541 -2.9052267 24.585583 66.189453
#> 61599: 0.2077071 -4.8230480 -2.2435981 4.779675 43.698242
#> 61600: -0.6593847 -5.4755758 -1.9725447 -2.815540 36.453125
#>
#> # Events table:
#> .id .type .description .initial .final .channel
#> <int> <char> <char> <sample_int> <sample_int> <char>
#> 1: 1 Stimulus s71 1 1 <NA>
#> 2: 2 Stimulus s71 1 1 <NA>
#> 3: 3 Stimulus s71 1 1 <NA>
#> 4: 4 Stimulus s71 1 1 <NA>
#> 5: 5 Stimulus s71 1 1 <NA>
#> ---
#> 196: 196 Stimulus s71 1 1 <NA>
#> 197: 197 Stimulus s70 1 1 <NA>
#> 198: 198 Stimulus s71 1 1 <NA>
#> 199: 199 Stimulus s71 1 1 <NA>
#> 200: 200 Stimulus s71 1 1 <NA>
#>
#> # Segments table:
#> .id .recording segment description condition
#> <int> <char> <int> <char> <char>
#> 1: 1 s1_faces.vhdr 1 s71 non-faces
#> 2: 2 s1_faces.vhdr 2 s71 non-faces
#> 3: 3 s1_faces.vhdr 3 s71 non-faces
#> 4: 4 s1_faces.vhdr 4 s71 non-faces
#> 5: 5 s1_faces.vhdr 5 s71 non-faces
#> ---
#> 196: 196 s1_faces.vhdr 196 s71 non-faces
#> 197: 197 s1_faces.vhdr 197 s70 faces
#> 198: 198 s1_faces.vhdr 198 s71 non-faces
#> 199: 199 s1_faces.vhdr 199 s71 non-faces
#> 200: 200 s1_faces.vhdr 200 s71 non-facesWith some ggplot skills, we can create customized plots.
ggplot is overloaded to work on an eeg_lst by
first downsampling the signal when necessary, and converting it to a
long-format data frame that is feed into ggplot. This
object can then be customized. (Notice that the channels, or component
names are in a .key column and their amplitude in
.value column, and instead of samples there are now
.time in seconds).
## ggplot uses internally a table that looks like this:
faces_seg |> eeg_filter(.id %in% 1:3) |>
as.data.frame() |> head()
#> .time .id .key .value .recording segment description condition
#> 1 -0.09960938 1 Fp1 -0.2239737 s1_faces.vhdr 1 s71 non-faces
#> 2 -0.09765625 1 Fp1 0.9975430 s1_faces.vhdr 1 s71 non-faces
#> 3 -0.09570312 1 Fp1 2.2477072 s1_faces.vhdr 1 s71 non-faces
#> 4 -0.09375000 1 Fp1 3.3361201 s1_faces.vhdr 1 s71 non-faces
#> 5 -0.09179688 1 Fp1 4.0950011 s1_faces.vhdr 1 s71 non-faces
#> 6 -0.08984375 1 Fp1 4.4007441 s1_faces.vhdr 1 s71 non-faces
faces_seg |>
eeg_select(O1, O2, P7, P8) |>
ggplot(aes(x = .time, y = .value)) +
geom_line(alpha = .1, aes(group = .id, color = condition)) +
stat_summary(
fun = "mean", geom = "line", alpha = 1, size = 1.5,
aes(color = condition)
) +
facet_wrap(~.key) +
geom_vline(xintercept = 0, linetype = "dashed") +
geom_vline(xintercept = .17, linetype = "dotted") +
theme(legend.position = "bottom")
#> Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
#> ℹ Please use `linewidth` instead.
#> This warning is displayed once per session.
#> Call `lifecycle::last_lifecycle_warnings()` to see where this warning was generated.
We can see here the N170 component in the faces condition for the
average trial on a single participant. We can investigate the signal by
averaging the channels of the occipital and parietal lobes using
transmute(), and the special function,
chs_mean(), a wrapper for rowMeans(), which
takes as arguments the relevant channels and whether missing values
should be omitted from the calculations.
faces_seg |>
eeg_transmute(
Occipital = chs_mean(across(starts_with("O")), na.rm = TRUE), #O1, O2, Oz
Parietal = chs_mean(across(starts_with("O")), na.rm = TRUE) # P3, P4, P7, P8, Pz
) |>
ggplot(aes(x = .time, y = .value)) +
geom_line(alpha = .1, aes(group = .id, color = condition)) +
stat_summary(
fun = "mean", geom = "line", alpha = 1, size = 1,
aes(color = condition)
) +
facet_wrap(~.key) +
theme(legend.position = "bottom")
We can also calculate the ERPs and then plot them in their layout
(and we’ll add the same theme that plot uses for
eeg_lsts):
ERP_faces <- faces_seg |>
eeg_group_by(.sample, condition) |>
eeg_summarize(across_ch(mean, na.rm = TRUE))
ERP_plot <- ERP_faces |>
ggplot(aes(x = .time, y = .value)) +
geom_line(aes(color = condition)) +
facet_wrap(~.key) +
theme(legend.position = "bottom") +
ggtitle("ERPs for faces vs non-faces") +
theme_eeguana()
ERP_plot |> plot_in_layout()
#> Warning: `aes_()` was deprecated in ggplot2 3.0.0.
#> ℹ Please use tidy evaluation idioms with `aes()`
#> ℹ The deprecated feature was likely used in the eeguana package.
#> Please report the issue at <https://github.com/bnicenboim/eeguana/issues>.
#> This warning is displayed once per session.
#> Call `lifecycle::last_lifecycle_warnings()` to see where this warning was generated.
Another possibility is to create a topographic plot of the two
conditions, by first making segments that include only the interval
.1-.2 s after the onset of the stimuli, creating a table with
interpolated amplitudes and using the ggplot wrapper
plot_topo().
faces_seg |>
eeg_filter(between(as_time(.sample, .unit = "s"), .1, .2)) |>
eeg_group_by(condition) |>
eeg_summarize(across_ch(mean, na.rm = TRUE)) |>
plot_topo() +
annotate_head() +
geom_contour() +
annotate_electrodes(colour = "black") +
facet_grid(~condition)
For more specialized plots or analyses, it might be necessary to extract the data in long data frame format first. Even though, we can do this without transforming the object, here, we transform the data first and then we visualize independent t-tests at every electrode and time point.
df <- faces_seg |>
eeg_select(O1, O2, P7, P8) |>
as_tidytable() |> #or alternatively as_tibble() if one uses dplyr
# We can use regular dplyr/tidytable functions now
group_by(.key, .time) |>
summarize(
`t-value` = t.test(
.value[condition == "faces"],
.value[condition == "non-faces"]
)$statistic
)
df
#> # A tidytable: 1,232 × 3
#> # Groups: .key
#> .key .time `t-value`
#> <chr> <dbl> <dbl>
#> 1 O1 -0.0996 0.204
#> 2 O1 -0.0977 0.227
#> 3 O1 -0.0957 0.234
#> 4 O1 -0.0938 0.219
#> 5 O1 -0.0918 0.173
#> 6 O1 -0.0898 0.0880
#> 7 O1 -0.0879 -0.0415
#> 8 O1 -0.0859 -0.221
#> 9 O1 -0.0840 -0.448
#> 10 O1 -0.0820 -0.713
#> # ℹ 1,222 more rowsThen we just load the data frame into ggplot.
ggplot(df, aes(x = .time, y = `t-value`)) + geom_line() +
facet_wrap(~.key)
However, this can be also done in the without transforming the
eeg_lst object:
faces_seg_t <-
faces_seg |>
eeg_select(O1, O2, P7, P8) |>
eeg_group_by(.sample) |>
eeg_summarize(across_ch(list(t = ~t.test(
.[condition == "faces"],
.[condition == "non-faces"]
)$statistic)))
faces_seg_t |>
ggplot(aes(x = .time, y = .value)) +
geom_line(alpha = .1, aes(group = .id)) +
stat_summary(fun = "mean", geom = "line", alpha = 1, size = 1) +
facet_wrap(~.key) +
theme(legend.position = "bottom")