Curso intensivo · Análisis de EEG

MATLAB para potenciales evocados

Seis sesiones para pasar de no haber escrito nunca una línea de código a correr un pipeline de ERP completo en EEGLAB y ERPLAB, sobre datos reales.

Duración
6 sesiones · 2 h
Software
MATLAB · EEGLAB · ERPLAB 13
Datos
ERP CORE — N400
Requisito previo
Ninguno
componente latente 1 ensayo promedio de 10 promedio de 40

Los tres paneles comparten la escala. El componente latente —la línea punteada— es idéntico en los tres: una onda positiva de 8 µV centrada en 400 ms, enterrada bajo ruido con desviación estándar de 25 µV. Lo único que cambia es cuántos ensayos entraron al promedio. Esta figura la vas a generar tú mismo en la segunda sesión.

Sesión 045 min · sin código

Antes de empezar

Instalar es la mitad del problema en este mundo. Hazlo con calma el día anterior, no diez minutos antes de la primera sesión.

Cómo está armada la página

Cada sesión termina con un ejercicio en tres pasos —uno resuelto, uno con huecos y uno solo—, con un recuadro que dice cómo saber si quedó bien y la solución completa a un clic. Los ejercicios que sí conviene hacer son los de los tres pasos: leer código no enseña a escribirlo.

Los bloques marcados Notas para quien dicta son para quien enseña. Si estás siguiendo el curso por tu cuenta, ábrelos al terminar cada día: ahí están los malentendidos que hay que buscarse.

  • MATLAB con la Signal Processing Toolbox
    La mayoría de las universidades tienen licencia de campus; consúltala con tu unidad de informática. Si no alcanzas a activarla, MATLAB Online sirve para todo lo que hay aquí. ERPLAB exige la Signal Processing Toolbox. Las de Statistics and Machine Learning y Parallel Computing sólo hacen falta si más adelante haces decoding (MVPA). Los desarrolladores de ERPLAB desaconsejan MATLAB 2025a en adelante; si puedes elegir versión, elige una anterior.
  • EEGLAB desde el sitio del SCCN
    Descárgalo de eeglab.org, no como ZIP de GitHub: a ese le faltan plugins. Descomprímelo en una carpeta sin espacios ni tildes en la ruta — es la causa número uno de errores inexplicables en máquinas hispanohablantes.
  • ERPLAB 13.00 como extensión de EEGLAB
    Lo más limpio es abrir EEGLAB y usar File → Manage EEGLAB extensions. La alternativa es descomprimir el release dentro de eeglab/plugins/ y reiniciar. ERPLAB 13 trae dos caras: ERPLAB Studio (interfaz nueva) y ERPLAB Classic (el menú clásico dentro de EEGLAB). El curso usa Classic, porque es el que se traduce a script.
  • Los datos: ERP CORE, paradigma N400
    40 participantes, siete componentes, con los scripts de procesamiento publicados (erpinfo.org/erp-core, CC BY-SA 4.0). Baja sólo el experimento N400 — son varios GB si bajas todo. Es el mismo set con el que trabaja el libro de Luck, así que cualquier duda tiene respuesta escrita en alguna parte.
  • Verificar que todo se ve desde MATLAB
    Abre MATLAB, escribe eeglab y presiona Enter. Debería abrirse la ventana principal y aparecer un menú ERPLAB. Después corre las tres líneas de abajo: si alguna devuelve vacío, algo no quedó en el path.
which eeglab                 % ¿ve MATLAB a EEGLAB?
which pop_basicfilter        % ¿ve a ERPLAB?
ver('signal')                % ¿está la Signal Processing Toolbox?
El path

MATLAB sólo puede llamar funciones que estén en su path. Ejecutar eeglab agrega EEGLAB y sus plugins automáticamente, y por eso todo script de este curso empieza llamando a eeglab, aunque después trabajemos sólo con comandos. Si un día te aparece Undefined function 'pop_epochbin', casi siempre es que olvidaste esa línea.

Notas para quien dicta

Esta sesión conviene hacerla en vivo aunque parezca trivial: el 80% de los problemas de la semana se originan aquí, y todos son invisibles hasta el día 3.

MinBloqueQué hace el grupo
0–10Qué vamos a hacer en la semanaMirar la figura de portada y responder: ¿por qué el primer panel no sirve?
10–30Instalación asistidaInstalar en su propia máquina, con la lista de chequeo abierta
30–40Descarga de datosBajar sólo N400; mientras baja, recorrer la estructura de carpetas de ERP CORE
40–45VerificaciónCorrer las tres líneas de chequeo y mostrar la pantalla

Qué vigilar

  • Rutas con tildes, ñ o espacios (C:\Users\María José\Mis documentos\). Es el problema más común en máquinas hispanohablantes y produce errores que no mencionan la ruta por ninguna parte.
  • Quien descargó EEGLAB como ZIP de GitHub va a llegar al día 5 sin ERPLAB funcionando. Pídeles que muestren la salida de which pop_basicfilter, no que digan que «ya está».
  • Dos copias de EEGLAB en el path (una vieja en Descargas) producen fallos intermitentes: which -all eeglab los delata.

Si alguien no alcanza a instalar

MATLAB Online corre todo el curso salvo la parte más pesada del día 6. Es preferible a que pase la primera sesión mirando la pantalla del compañero.

Día 12 h · MATLAB puro

Una señal es un vector

Al terminar vas a poder construir un eje de tiempo, graficar una señal, recortar una ventana entre dos latencias y explicar por qué un EEG es una matriz.

Empieza escribiendo

Abre MATLAB. La ventana grande del centro es la Command Window: lo que escribes ahí se ejecuta al presionar Enter. Prueba esto, línea por línea:

8 + 5
v = [2 8 3 9]
mean(v)
v(2)
v(2:3)
ans = 13 v = 2 8 3 9 ans = 5.5000 ans = 8 ans = 8 3

Tres cosas ya quedaron claras sin que nadie las anunciara. Cuando no asignas el resultado a nada, MATLAB lo guarda en una variable llamada ans. Los corchetes [ ] arman un vector: cuatro números en fila. Y v(2) devolvió 8, el segundo elemento — en MATLAB los índices empiezan en 1, no en 0. Si vienes de Python o de R, esta es la primera y más frecuente fuente de errores por un puesto.

El punto y coma al final de una línea silencia la salida. Con vectores de 500 mil números lo vas a agradecer:

ruido = randn(1, 500000);    % sin salida en pantalla
size(ruido)                  % 1 fila, 500000 columnas

De índices a segundos

Un registro de EEG es una lista de voltajes tomados a intervalos regulares. Si el equipo muestrea a 500 Hz, hay una muestra cada 2 ms. El vector de tiempo se construye con el operador dos puntos, que significa «desde : paso : hasta»:

srate = 500;                      % frecuencia de muestreo, en Hz
t = (0:499) / srate;    % un segundo de tiempo, 500 muestras
numel(t)                          % 500 muestras
t(end)                            % 0.998 s — la última

x = sin(2*pi*10*t);               % un seno de 10 Hz
plot(t, x)
xlabel('Tiempo (s)'), ylabel('Amplitud (\muV)')

El truco que vas a usar todos los días: en vez de contar índices a mano, deja que MATLAB los busque por vos con una comparación. t >= 0.2 devuelve un vector de verdaderos y falsos, y ese vector sirve como índice.

vent = t >= 0.2 & t < 0.4;   % una ventana de 200 ms
sum(vent)                     % 100 muestras caen dentro
mean(x(vent))                 % amplitud media en esa ventana

Eso es, literalmente, lo que hace ERPLAB cuando le pides «amplitud media entre 300 y 500 ms». No hay magia debajo.

Dos dimensiones: canales por tiempo

Un electrodo da un vector. Treinta electrodos dan una matriz: filas para los canales, columnas para el tiempo. Esa es exactamente la forma de EEG.data en EEGLAB, y saber leerla es la mitad del curso.

datos = zeros(4, numel(t));       % 4 canales × 500 muestras
for ch = 1:4
    datos(ch,:) = ch * sin(2*pi*10*t);
end

size(datos)          % 4   500
size(datos, 1)       % 4   → número de canales
datos(3, :)          % todo el canal 3
datos(:, 100)        % los 4 canales en la muestra 100
mean(datos, 2)       % promedio en el tiempo, un valor por canal

El segundo argumento de mean es la dimensión sobre la que promedias: 1 recorre las filas (promedia canales entre sí), 2 recorre las columnas (promedia el tiempo dentro de cada canal). Equivocarse de dimensión produce resultados que parecen razonables, así que compruébalo siempre con size.

El punto que cambia todo

MATLAB nació como lenguaje de álgebra lineal, así que * significa producto matricial. Para multiplicar elemento por elemento — que es lo que casi siempre quieres con señales — se antepone un punto:

a = [1 2 3];
b = [4 5 6];
a .* b        % 4  10  18   ← elemento a elemento
a * b'        % 32          ← producto punto (b' es b transpuesto)
a * b         % ERROR: Inner matrix dimensions must agree

Lo mismo vale para ./ y .^. Cuando veas el error Matrix dimensions must agree, el 90% de las veces falta un punto.

Corrección de línea base, a mano

Restar la línea base es sólo eso: calcular el promedio del período previo al estímulo y restarlo. Escríbelo una vez a mano y nunca más vas a tratar la opción de ERPLAB como una caja negra.

t   = (-100:399) / srate;   % época típica de ERP; enteros / srate: t(101) es 0 exacto
sig = 8 * exp(-((t - 0.400).^2) / (2*0.080^2));   % un pico en 400 ms

base = t < 0;                     % el período pre-estímulo
sig_corr = sig - mean(sig(base)); % restar su media

mean(sig(base))        % antes: distinto de cero
mean(sig_corr(base))   % después: cero (o 1e-16, que es cero)

Del Command Window al script

Todo lo anterior se escribió línea por línea y se perdió al cerrar. Un análisis no se hace así: se escribe en un archivo que se puede volver a correr, corregir y mandar por correo. Ese archivo es un script, un texto con extensión .m.

  1. Pestaña Home, botón New → Script.
  2. Guárdalo como dia1.m en tu carpeta del curso — sin tildes ni espacios en la ruta, igual que con EEGLAB.
  3. Escribe, guarda, y presiona Run (o F5): MATLAB ejecuta el archivo completo, de arriba abajo, como si hubieras tecleado cada línea.
% dia1.m — vectores, tiempo y ventanas de medición
clear                        % borra las variables que quedaron de antes
clc                          % limpia la pantalla

srate = 500;
t = (0:499) / srate;
x = sin(2*pi*10*t);
plot(t, x)

Ese clear de la segunda línea evita el error más difícil de ver de todos: una variable vieja con el mismo nombre sobrevive de la corrida anterior, el script la usa sin quejarse, y los resultados salen mal sin ningún mensaje. Si un script sólo funciona cuando lo corres dos veces, ahí está el problema.

Un atajo que vas a usar toda la semana: dos signos de porcentaje %% parten el archivo en secciones. Con el cursor dentro de una, Ctrl+Enter (Cmd+Enter en Mac) ejecuta sólo esa sección. Así pruebas el bloque que estás escribiendo sin volver a correr los cinco anteriores.

Ejercicio 1Dos ondas y una ventana

Un electrodo real recoge, entre otras cosas, ritmo alfa (unos 10 Hz, decenas de µV) y el zumbido de la red eléctrica (50 Hz en Chile, no 60). Vas a fabricar esa mezcla y a medirla.

Paso 1 · resuelto

Dos segundos a 250 Hz, sólo el alfa. Corre esto y compara con la salida:

clear; clc;
srate = 250;
t     = (0:499) / srate;   % dos segundos
alfa  = 20 * sin(2*pi*10*t);         % 10 Hz, 20 µV de amplitud

numel(t)
std(alfa)
ans = 500 ans = 14.1563

Fíjate en que la desviación estándar de un seno no es su amplitud: es la amplitud dividida por raíz de dos. 20/√2 = 14,14. Ese número lo vas a poder anticipar sin correr nada.

Paso 2 · completa los huecos

Ahora suma el ruido de red y recorta una ventana. Reemplaza cada ____ y corre:

red   = 3 * sin(2*pi*____*t);    % 50 Hz, 3 µV
senal = alfa + ____;

plot(t, senal)
xlabel('Tiempo (s)'), ylabel('Amplitud (\muV)')

vent = t >= 0.5 & t < ____;      % de 0,5 a 1,5 segundos
sum(vent)                        % ¿cuántas muestras deberían caer adentro?
Paso 3 · por tu cuenta

Calcula la desviación estándar de la señal completa y la de la ventana, usando indexado lógico. Escribe la respuesta como comentario dentro del script: ¿por qué son casi iguales, si la ventana es sólo la mitad del registro?

Cómo saber si quedó bien

sum(vent) devuelve 250. Las dos desviaciones estándar dan ≈ 14,3 y difieren entre sí en menos de una décima.

Si sum(vent) te dio 251, usaste <= donde va <. Si la desviación te dio ≈ 20 o ≈ 3, sumaste sólo una de las dos ondas. Si te dio un número enorme, revisa el paréntesis del seno.

Solución y respuesta
clear; clc;
srate = 250;
t     = (0:499) / srate;

alfa  = 20 * sin(2*pi*10*t);
red   = 3  * sin(2*pi*50*t);
senal = alfa + red;

plot(t, senal)
xlabel('Tiempo (s)'), ylabel('Amplitud (\muV)')

vent = t >= 0.5 & t < 1.5;

fprintf('muestras en la ventana: %d\n', sum(vent));
fprintf('sd completa: %.4f\n', std(senal));
fprintf('sd ventana : %.4f\n', std(senal(vent)));

% Son casi iguales porque esta señal no cambia de carácter con el tiempo:
% cada segundo se parece a cualquier otro. Un ERP no es así — el componente
% ocurre en un momento y no en los demás — y por eso ahí la ventana sí importa.
muestras en la ventana: 250 sd completa: 14.3147 sd ventana : 14.3290

Que las dos ondas se sumen sin estorbarse tampoco es casualidad: 14,31 es √(14,14² + 2,12²). Las varianzas de dos señales de frecuencias distintas se suman; las amplitudes, no. Esa es la misma aritmética que hace funcionar el promediado del día siguiente.

Cierre del día 1

Antes de mirar la respuesta. Tienes datos, una matriz de 4 canales × 500 muestras. Anota qué devuelve cada una de estas tres líneas — cuántos números, y qué significa cada uno:

mean(datos)
mean(datos, 2)
mean(datos(:))
Respuesta

mean(datos) devuelve 500 números: sin decirle la dimensión, MATLAB promedia siempre hacia abajo, por columnas. Aquí eso significa promediar los 4 canales entre sí en cada instante — casi nunca es lo que quieres, y nunca da error.

mean(datos, 2) devuelve 4 números, uno por canal: el promedio en el tiempo de cada electrodo.

mean(datos(:)) devuelve uno. Los dos puntos entre paréntesis estiran la matriz entera en una sola columna.

La lección: mean sin dimensión explícita es una apuesta sobre la forma de tus datos. Escribe siempre el segundo argumento.

  • ConceptosVector y matriz · índice desde 1 · indexado lógico
  • ReconstruyeLa corrección de línea base en tres líneas, sin mirar
  • Error típicoPromediar sobre la dimensión equivocada: no da error, da un resultado plausible
  • Queda abiertoSi un ensayo suelto es puro ruido, ¿de dónde sale la onda limpia de los papers?
Notas para quien dicta
MinBloqueQué hace el grupo
0–20Command WindowTeclear en vivo; que cada quien rompa algo a propósito y lea el mensaje
20–45Tiempo, senos, indexado lógicoConstruir t y graficar; nadie avanza sin ver la figura
45–60Matrices y dimensionessize después de cada operación, en voz alta
60–75El punto y la línea baseEscribir la corrección de línea base a mano
75–90Del Command Window al scriptCrear dia1.m, guardarlo, correrlo con F5
90–115Ejercicio 1Pasos 1 y 2 juntos; el 3 solos
115–120CierreLa pregunta de las tres líneas, a mano alzada antes de correrla

Qué vigilar

  • Quien pregunta «¿y esto para qué sirve?» en el minuto 20 todavía no vio la conexión con los datos. Muéstrale ahí mismo un EEG.data real y su size: son los mismos corchetes.
  • Si alguien escribe t = 0:2 esperando dos segundos, está confundiendo el índice con el tiempo. Es la confusión que después reaparece como milisegundos contra muestras en el día 3: vale la pena desarmarla ahora.
  • Respuesta que delata un malentendido en el cierre: «mean(datos) da cuatro números». Quien la dé está imaginando que MATLAB adivina que las filas son canales.

Para discutir, si sobra tiempo

El indexado lógico t >= 0.3 & t <= 0.5 es literalmente la ventana de medición de un paper. ¿Qué implica que elegirla sea una línea de código tan barata de cambiar?

Día 22 h · MATLAB puro

Por qué se promedia

Al terminar vas a haber demostrado con tus propios números la ley de la raíz de N, y de paso vas a saber escribir bucles, condicionales y funciones.

Primero los números

Simulemos el problema entero. Un componente positivo de 8 µV en 400 ms, ruido con desviación estándar de 25 µV — proporciones realistas para un P3 de un solo ensayo — y 40 ensayos.

rng(42);                    % semilla: los resultados se pueden repetir

srate  = 500;
t      = (-100:399) / srate;         % enteros / srate: t(101) es 0 exacto
ntimes = numel(t);

senal = 8 * exp(-((t - 0.400).^2) / (2*0.080^2));
ruido_sd = 25;
ntrials  = 40;

epocas = zeros(ntrials, ntimes);     % preasignar: filas = ensayos
for k = 1:ntrials
    epocas(k,:) = senal + ruido_sd * randn(1, ntimes);
end

vent = t >= 0.300 & t <= 0.500;      % ventana de medición
base = t < 0;                        % pre-estímulo

fprintf('%-6s %8s %8s %8s\n', 'n', 'pico', 'ruido', 'SNR');
for n = [1 2 10 40]
    p = mean(epocas(1:n,:), 1);      % promedio de los primeros n ensayos
    amp = mean(p(vent));             % amplitud media en la ventana
    ruido = std(p(base));            % cuánto tiembla la línea base
    fprintf('%-6d %8.2f %8.2f %8.2f\n', n, amp, ruido, amp/ruido);
end
n pico ruido SNR 1 5.42 28.33 0.19 2 5.82 18.81 0.31 10 7.94 8.17 0.97 40 6.12 3.83 1.60

Mira la tercera columna. El ruido del período basal cae de 28 a 3,8 al pasar de 1 a 40 ensayos: no dividido por 40, sino por algo cercano a la raíz de 40, que es 6,3. Compruébalo:

ruido_sd / sqrt(40)         % 3.95 — lo que predice la teoría

prom40 = mean(epocas, 1);   % MATLAB no deja indexar el resultado de una
std(prom40(base))           % función, así que van dos pasos:  3.83

Los mismos números, ahora con letras

En la tabla hay tres cantidades y conviene ponerles nombre antes de escribir nada: el ruido de un ensayo suelto, que aquí vale 25 µV; la cantidad de ensayos que entraron al promedio; y el ruido que queda después de promediar, que fue 3,83.

σ_promedio = σ_ensayo / √n = 25 / √40 = 3,95 µV

Ahora agrega la amplitud del componente, que el promedio no cambia porque es idéntica en todos los ensayos: 6,30 µV medidos entre 300 y 500 ms. Divide una cosa por la otra y tienes la razón señal-ruido del promedio:

SNR(n) = A / σ_promedio = (A / σ_ensayo) · √n
SímboloQué esEn la corrida de arriba
AAmplitud del componente en la ventana de medición6,30 µV
σ_ensayoRuido de un ensayo, medido como sd de la línea base25 µV
nEnsayos promediados1, 2, 10, 40
σ_promedioRuido que sobrevive al promedio28,3 → 3,83

La forma de la fórmula es todo lo que hay que recordar: la n entra bajo una raíz, y entra multiplicando. Nada de esto es una propiedad del EEG —es aritmética de promedios— pero de ahí sale la consecuencia práctica que gobierna cualquier diseño de experimento:

duplicar la calidad de la señal cuesta cuadruplicar los ensayos. Pasar de 40 a 80 ensayos mejora un 41%; pasar de 40 a 160 mejora al doble. Es la razón por la que los paradigmas de ERP son tan largos y tan aburridos, y la razón por la que una condición con pocos ensayos válidos no es comparable con una que los tiene todos.

Ojo con los números exactos

Tu tabla no va a coincidir dígito por dígito con la de arriba, porque el generador de números aleatorios cambia entre versiones y entre MATLAB y Octave. Lo que sí se va a repetir es la columna del ruido: siempre ≈ 25/√n.

Las tres estructuras que necesitas

Ya usaste un for dos veces. Estas son las otras dos piezas.

% Condicional
n_malos = sum(max(abs(epocas), [], 2) > 100);   % ensayos que pasan de ±100 µV
if n_malos / ntrials > 0.25
    warning('Se rechazaría más del 25%% de los ensayos');
else
    fprintf('Rechazo: %.1f%%\n', 100 * n_malos / ntrials);
end

% Función anónima: una fórmula guardada en una variable
razon_sn = @(w) mean(w(vent)) / std(w(base));
razon_sn(mean(epocas(1:10,:), 1))
No le pongas a una variable el nombre de una función

Esa fórmula se podría haber llamado snr, y ahí empieza el problema: snr ya es una función de MATLAB, igual que log, mean, max o i. Una variable con ese nombre la tapa en silencio y desde esa línea en adelante la función original deja de existir para tu sesión. Si algo que funcionaba deja de funcionar sin motivo, which log te dice cuál de los dos está ganando y clear log devuelve las cosas a su lugar.

Cuando la operación no cabe en una línea, se escribe una función de verdad. En MATLAB una función puede ir en su propio archivo .m — con el mismo nombre que la función — o al final de un script, nunca al medio ni al principio. Esa restricción no existe en Octave, y es un error que sólo aparece cuando entregas el script a otra persona.

function p = promediar_n(epocas, n)
% PROMEDIAR_N  Promedia los primeros n ensayos de una matriz de épocas.
%   epocas : matriz ensayos × tiempo
%   n      : cuántos ensayos incluir
    if n > size(epocas, 1)
        error('Pediste %d ensayos y sólo hay %d.', n, size(epocas,1));
    end
    p = mean(epocas(1:n, :), 1);
end

Las líneas de comentario que van justo debajo de function son lo que aparece cuando alguien escribe help promediar_n. Escríbelas siempre: es la única documentación que vas a leer en seis meses.

Ejercicio 2¿Cuántos ensayos necesito?

Es la pregunta que hay que responder antes de programar un experimento, y se responde con este mismo script. Guárdalo como ej2_snr.m.

Paso 1 · resuelto

Simula 200 ensayos y calcula el SNR del promedio para cada n:

clear; clc; rng(42);

srate = 500;
t     = (-100:399) / srate;         % enteros / srate: t(101) es 0 exacto
senal = 8 * exp(-((t - 0.400).^2) / (2*0.080^2));
vent  = t >= 0.300 & t <= 0.500;
base  = t < 0;
nmax  = 200;

epocas = senal + 25 * randn(nmax, numel(t));   % 200 ensayos de una vez

curva = zeros(1, nmax);
for n = 1:nmax
    p = mean(epocas(1:n,:), 1);
    curva(n) = mean(p(vent)) / std(p(base));
end

plot(1:nmax, curva)
xlabel('Ensayos promediados'), ylabel('SNR')

La línea del randn merece un segundo: senal es una fila de 500 números y randn(200, 500) es una matriz. MATLAB copia la fila en las 200 filas de la matriz sin que se lo pidas. Es cómodo y también es peligroso: si te equivocas de orientación, no se queja, simplemente suma en la dirección que no era.

Paso 2 · completa los huecos

Encuentra el primer n que llega a un SNR de 2, e imprímelo:

k = find(curva > ____, 1);      % el 1 significa «sólo el primero»

if isempty(k)
    fprintf('No llega a SNR 2 con %d ensayos\n', ____);
else
    fprintf('Primer n con SNR > 2: %d\n', k);
end
Paso 3 · por tu cuenta

Envuelve todo en un bucle sobre tres niveles de ruido —15, 25 y 40 µV— y superpón las tres curvas en una figura con hold on, más una línea horizontal en 2. Anota los tres valores de k. Después, sin volver a correr nada, predice cuántos ensayos harían falta con un ruido de 50 µV y comprueba tu predicción cambiando un número.

Cómo saber si quedó bien

La fórmula del recuadro anterior predice 23 ensayos para un ruido de 15 µV, 63 para 25 µV y 161 para 40 µV. Tu simulación va a dar algo cercano pero no idéntico: con semillas distintas los valores caen alrededor de 16–28, 47–89 y 123–200. Con 40 µV incluso puede no llegar nunca dentro de los 200 ensayos, y ese isempty(k) es justamente para eso.

Si tus tres números son casi iguales entre sí, el bucle no está usando el ruido que cambia. Si el SNR baja en vez de subir, promediaste sobre la dimensión equivocada.

Solución y comentario
clear; clc; rng(42);

srate = 500;
t     = (-100:399) / srate;         % enteros / srate: t(101) es 0 exacto
senal = 8 * exp(-((t - 0.400).^2) / (2*0.080^2));
vent  = t >= 0.300 & t <= 0.500;
base  = t < 0;
nmax  = 200;

figure; hold on
for sd = [15 25 40]
    epocas = senal + sd * randn(nmax, numel(t));

    curva = zeros(1, nmax);
    for n = 1:nmax
        p = mean(epocas(1:n,:), 1);
        curva(n) = mean(p(vent)) / std(p(base));
    end

    plot(1:nmax, curva, 'LineWidth', 1.2)

    k = find(curva > 2, 1);
    if isempty(k)
        fprintf('sd = %2d uV  ->  no llega a SNR 2 en %d ensayos\n', sd, nmax);
    else
        fprintf('sd = %2d uV  ->  primer n con SNR > 2: %d\n', sd, k);
    end
end

plot([1 nmax], [2 2], '--')      % la meta
legend('15 \muV', '25 \muV', '40 \muV', 'SNR = 2')
xlabel('Ensayos promediados'), ylabel('SNR')

Dos cosas que se ven en la figura y no en los números. La primera: las curvas no suben suave, tiemblan, porque cada punto usa una realización distinta del ruido — el «primer n que cruza 2» es en parte suerte, y por eso siempre queda un poco por debajo del valor que predice la fórmula.

La segunda: las tres curvas tienen la misma forma estirada. Duplicar el ruido no duplica los ensayos necesarios, los cuadruplica. Un experimento con electrodos malos no cuesta el doble; cuesta cuatro veces más.

Cierre del día 2

Antes de mirar la respuesta. Un colega tiene 40 ensayos por condición y un efecto que no alcanza a ser significativo. Puede hacer dos cosas: correr 40 ensayos más, o cambiar de electrodos y bajar el ruido de 25 a 18 µV. ¿Cuál de las dos mejora más el SNR?

Respuesta

Pasar de 40 a 80 ensayos multiplica el SNR por √2 = 1,41. Bajar el ruido de 25 a 18 lo multiplica por 25/18 = 1,39. Empatan, y el segundo camino además no le agrega media hora de tarea a cada participante.

La asimetría es toda de la raíz: el ruido entra directo, los ensayos entran a la mitad de fuerza. Por eso una preparación cuidadosa del gel rinde más que una hora extra de registro, aunque la segunda se sienta más productiva.

  • ConceptosPromediado coherente · ruido no correlacionado · razón señal-ruido
  • ReconstruyeSNR(n) = (A/σ)·√n, a partir de la tabla de la simulación
  • Error típicoLeer «cuatro veces más ensayos» como «cuatro veces mejor»
  • Queda abiertoEl promedio supone que el componente es idéntico en cada ensayo. ¿Y si su latencia varía?
Notas para quien dicta
MinBloqueQué hace el grupo
0–10Repaso activoReconstruir la corrección de línea base sin mirar el día 1
10–35Simulación de 40 ensayosEscribirla completa; que cada quien vea su propia tabla
35–50De la tabla a la fórmulaComparar 3,83 con 25/√40 antes de que aparezca la letra σ
50–70Bucle, condicional, funciónEscribir promediar_n.m y probarla con un n imposible
70–110Ejercicio 2Pasos 1 y 2 guiados; paso 3 en parejas
110–120CierreLa pregunta de los 40 ensayos, votando a mano alzada primero

Qué vigilar

  • El malentendido central del día: creer que promediar «limpia» cada ensayo. No los toca; lo único que hace es que el ruido, al no estar alineado con el estímulo, se cancele consigo mismo. Si alguien dice «el promedio filtra el ruido», vale la pena detenerse: filtrar y promediar son operaciones distintas y el día 4 depende de que la diferencia esté clara.
  • Quien vote «correr 40 ensayos más» en el cierre está pensando la relación como lineal. Es exactamente la intuición que la raíz corrige.
  • Ojo con quienes copian la tabla del material y no la generan: los números no van a coincidir dígito por dígito y eso es parte de lo que hay que entender.

Para discutir, si sobra tiempo

Si el componente aparece en 380 ms en un ensayo y en 430 en el siguiente, el promedio no reconstruye la onda de un ensayo: la ensancha y le baja el pico. ¿Qué consecuencias tiene eso para comparar amplitudes entre grupos con distinta variabilidad de latencia? Es una puerta directa a la literatura sobre medidas de área y jitter.

Día 32 h · EEGLAB

El objeto EEG

Al terminar vas a poder abrir cualquier dataset de EEGLAB, saber qué contiene sin adivinar, y contar cuántas veces apareció cada código de estímulo.

Un dato con nombres

Hasta aquí los datos eran números sueltos. Un registro real trae además la frecuencia de muestreo, los nombres de los electrodos, los eventos, la historia de lo que se le hizo. Todo eso viaja junto en una estructura: una variable con campos nombrados, a los que se llega con un punto.

Vamos a fabricar uno de juguete, con la misma forma que el de verdad. Todo lo que sigue en esta sesión funciona sobre él, así que puedes practicar aunque los datos todavía se estén descargando:

clear; clc;

EEG = struct();
EEG.setname  = 'juguete';
EEG.srate    = 500;
EEG.data     = randn(4, 5000);      % 4 canales, 10 segundos de ruido
EEG.pnts     = size(EEG.data, 2);
EEG.chanlocs = struct('labels', {'Fz','Cz','Pz','Oz'});
EEG.event    = struct('type',    {211, 201, 221, 201, 211}, ...
                      'latency', {500, 900, 1800, 2200, 3500});

fprintf('%s: %d canales, %.1f s a %g Hz\n', ...
    EEG.setname, size(EEG.data,1), EEG.pnts/EEG.srate, EEG.srate);

numel(EEG.chanlocs)     % cuántos electrodos
numel(EEG.event)        % cuántos eventos
isfield(EEG, 'EVENTLIST')   % 0: este dataset todavía no pasó por ERPLAB
juguete: 4 canales, 10.0 s a 500 Hz ans = 4 ans = 5

Los tres puntos ... continúan una instrucción en la línea siguiente. Los vas a usar mucho: las llamadas a ERPLAB son largas.

Y ojo con lo que hizo esa llave en struct('labels', {'Fz','Cz','Pz','Oz'}). No creó un campo con cuatro nombres adentro: creó cuatro estructuras, una por electrodo, cada una con su propio labels. Ésa es exactamente la forma que tienen chanlocs y event en un dataset real, y es la razón de la sintaxis rara que viene en un momento.

Los campos que importan

Cuando cargas un .set, EEGLAB deja en el workspace una estructura llamada EEG con decenas de campos. Estos son los que vas a tocar:

CampoQué esForma
EEG.dataLos voltajes, en µVcanales × tiempo (continuo)
canales × tiempo × épocas
EEG.srateFrecuencia de muestreo en Hzescalar
EEG.pntsMuestras por época (o totales)escalar
EEG.trialsNúmero de épocas; 1 si es continuoescalar
EEG.timesEje de tiempo en ms1 × pnts
EEG.chanlocsUn elemento por electrodo, con .labelsstruct array
EEG.eventUn elemento por evento, con .type y .latencystruct array
EEG.EVENTLISTLo que agrega ERPLAB: bins y flagsstruct
Segundos, milisegundos, muestras

EEG.times está en milisegundos, pero EEG.event(k).latency está en muestras, contando desde el inicio del registro. Convertir de una a otra es dividir o multiplicar por EEG.srate, y confundirlas produce ventanas de medición desplazadas que nadie nota hasta la revisión por pares.

Recorrer un struct array

Los campos que tienen un elemento por canal o por evento son arreglos de estructuras. Para sacarles una columna completa hay dos sintaxis, y conviene aprenderlas juntas: corchetes cuando el contenido es numérico, llaves cuando es texto.

etiquetas = {EEG.chanlocs.labels};        % celda con todos los nombres
iPz = find(strcmp(etiquetas, 'Pz'));      % ¿en qué posición está Pz?
fprintf('Pz es el canal %d\n', iPz);

codigos = [EEG.event.type];               % vector con todos los códigos
for c = unique(codigos)
    fprintf('  código %3d: %4d veces\n', c, sum(codigos == c));
end
Pz es el canal 3 código 201: 2 veces código 211: 2 veces código 221: 1 veces

Ese pequeño bucle es lo primero que hay que correr con datos nuevos. Si el paradigma dice que hubo 120 targets y el conteo dice 118, quieres saberlo antes de promediar, no después.

Cuidado con type mixto

En algunos formatos EEG.event.type viene como texto ('S111') y no como número. Entonces [EEG.event.type] concatena caracteres y devuelve un disparate. Compruébalo con class(EEG.event(1).type): si es char, usa {EEG.event.type} y strcmp.

Tres unidades para la misma cosa

Una latencia se puede expresar en muestras, en segundos o en milisegundos, y el objeto EEG usa las tres a la vez. Practica la conversión ahora, con números chicos y a mano, porque equivocarse aquí no produce ningún error: produce una ventana de medición corrida que igual entrega un resultado.

EEG.event(3).latency                        % está en MUESTRAS
EEG.event(3).latency / EEG.srate            % → segundos
1000 * EEG.event(3).latency / EEG.srate     % → milisegundos

% Al revés: dentro de una época que empieza en -200 ms,
% ¿qué muestra corresponde a 300 ms?
round((300 - (-200)) / 1000 * EEG.srate) + 1
ans = 1800 ans = 3.6000 ans = 3600 ans = 251

El + 1 del final es el mismo de siempre: la primera muestra de la época es la número 1, no la 0. Y el round no es cosmético: sin él queda un índice con decimales y MATLAB contesta Subscript indices must be positive integers.

En la práctica casi nunca vas a hacer esta cuenta. Sobre un dataset cargado de verdad, EEG.times ya trae el eje en milisegundos y una comparación lógica —la misma del día 1— resuelve el problema sin aritmética:

vent = EEG.times >= 300 & EEG.times <= 500;   % en ms, no en segundos

Si escribes 0.3 en vez de 300, vent queda vacío, el promedio da NaN y no aparece ningún mensaje de error. Ése es el precio de que las tres unidades convivan.

Rutas, celdas y nombres de archivo

Los sujetos viven en carpetas. Armar rutas pegando texto con [ ] funciona pero se rompe entre sistemas operativos; fullfile pone el separador correcto solo.

raiz = '~/Documents/ERP_CORE/N400';
sujetos = {'sub-001','sub-002','sub-010'};

for k = 1:numel(sujetos)
    s = sujetos{k};                                  % llaves: sacar de una celda
    archivo = fullfile(raiz, s, [s '_N400.set']);
    fprintf('%s\n', archivo);
end

sprintf('sub-%03d', 7)     % 'sub-007' — ceros a la izquierda

La GUI escribe tu script

Este es el atajo más útil de EEGLAB y casi nadie lo menciona el primer día. Haz una operación con los menús — cargar un archivo, filtrar, lo que sea — y después escribe:

eegh

EEGLAB imprime en la consola el comando exacto, con todos sus argumentos, que equivale a lo que acabas de hacer con el mouse. Copias, pegas en el editor, y ya tienes media línea de pipeline escrita sin memorizar nada. El flujo de trabajo de todo el curso es ese: hacerlo una vez apuntando, pedirle a eegh la traducción, y quedarte con el script.

[ALLEEG, EEG, CURRENTSET, ALLCOM] = eeglab;   % arranca y limpia el workspace

EEG = pop_loadset('filename', 'sub-001_N400.set', ...
                  'filepath', '/ruta/a/N400/sub-001/');
[ALLEEG, EEG, CURRENTSET] = eeg_store(ALLEEG, EEG);
eeglab redraw                                  % refresca la ventana de EEGLAB

ALLEEG es la lista de todos los datasets abiertos y CURRENTSET el índice del activo. Trabajando por script casi siempre basta con EEG; los otros dos importan cuando quieres ver el resultado en la interfaz.

Ejercicio 3Un inventario de tus datos

Vas a escribir la función que corres siempre que llegan datos nuevos, antes de tocar nada. Puedes desarrollarla entera sobre el EEG de juguete.

Paso 1 · resuelto

La cabecera: qué es y de qué tamaño.

fprintf('\n--- %s ---\n', EEG.setname);
fprintf('canales   : %d\n', size(EEG.data, 1));
fprintf('muestreo  : %g Hz\n', EEG.srate);
fprintf('duración  : %.1f minutos\n', EEG.pnts / EEG.srate / 60);
--- juguete --- canales : 4 muestreo : 500 Hz duración : 0.2 minutos
Paso 2 · completa los huecos

El conteo de códigos, escrito para que funcione con números y con texto. La idea es pasar todo a texto primero y no volver a preguntarse por el tipo:

tipos = {EEG.event.type};              % llaves: funciona en los dos casos

if isnumeric(EEG.event(1).type)
    tipos = cellfun(@num2str, tipos, 'UniformOutput', false);
end

unicos = ____(tipos);                  % los códigos distintos, sin repetir

for k = 1:numel(unicos)
    n = sum(____(tipos, unicos{k}));   % ¿cuántas veces aparece este código?
    fprintf('   %-6s %4d veces\n', unicos{k}, n);
end

Pista para el segundo hueco: comparar texto con == no sirve; la función que compara cadenas ya apareció en esta sesión buscando el canal Pz.

Paso 3 · por tu cuenta

Junta los dos pasos en una función inventario(EEG), en su propio archivo inventario.m, y agrégale una línea que distinga si el dataset está continuo o epocado (pista: EEG.trials). Pruébala con el juguete, después con un sujeto real de ERP CORE, y compara los conteos con los códigos que declara el paradigma.

Cómo saber si quedó bien

Sobre el juguete: 4 canales, 5 eventos, 3 códigos distintos, con 201 y 211 dos veces cada uno y 221 una vez.

La prueba que de verdad importa es ésta: escribe EEG.event(1).type = 'S211'; y vuelve a correrla. Si se cae o si te dice que hay 5 códigos distintos, el paso 2 no está haciendo lo que crees. Una función que sólo funciona con el formato que te tocó hoy no sirve para el archivo del mes que viene.

Solución
function inventario(EEG)
% INVENTARIO  Resumen de un dataset de EEGLAB, antes de tocar nada.
%   inventario(EEG) imprime forma, duración, estado y conteo de eventos.

    fprintf('\n--- %s ---\n', EEG.setname);
    fprintf('canales   : %d\n', size(EEG.data, 1));
    fprintf('muestreo  : %g Hz\n', EEG.srate);

    if isfield(EEG, 'trials') && EEG.trials > 1
        fprintf('estado    : epocado, %d épocas de %d muestras\n', ...
            EEG.trials, EEG.pnts);
    else
        fprintf('estado    : continuo, %.1f minutos\n', EEG.pnts / EEG.srate / 60);
    end

    if ~isfield(EEG, 'event') || isempty(EEG.event)
        fprintf('sin eventos\n');
        return
    end

    tipos = {EEG.event.type};
    if isnumeric(EEG.event(1).type)
        tipos = cellfun(@num2str, tipos, 'UniformOutput', false);
    end

    unicos = unique(tipos);
    fprintf('eventos   : %d en total, %d códigos distintos\n', ...
        numel(tipos), numel(unicos));
    for k = 1:numel(unicos)
        fprintf('   %-6s %4d veces\n', unicos{k}, sum(strcmp(tipos, unicos{k})));
    end
end
--- juguete --- canales : 4 muestreo : 500 Hz estado : continuo, 0.2 minutos eventos : 5 en total, 3 códigos distintos 201 2 veces 211 2 veces 221 1 veces

El truco de todo el archivo es la tercera línea desde el final del bloque de eventos: convertir los números a texto una vez y trabajar siempre con texto. La alternativa —dos ramas completas, una numérica y otra de cadenas— es el doble de código y el doble de lugares donde equivocarse.

return sale de la función antes de tiempo. Es la forma limpia de manejar el caso raro sin anidar todo lo demás dentro de un else.

Cierre del día 3

Antes de correr nada. Un archivo trae los códigos como texto. Alguien escribe las tres líneas de siempre. Anota qué contiene codigos y qué número imprime la última línea:

EEG.event(1).type = 'S211';
EEG.event(2).type = 'S201';

codigos = [EEG.event.type];
sum(codigos == 211)
Respuesta

codigos no es un vector de códigos: es la cadena 'S211S201...', todos los textos pegados uno detrás de otro. Los corchetes concatenan, y concatenar texto con texto da texto.

La última línea imprime 0. No da error: MATLAB compara cada carácter con el número 211 usando su código ASCII, ninguno coincide, y la suma da cero. Un cero que significa «este código no existe en los datos» cuando en realidad hay decenas.

Ése es el error más caro de la semana, porque se ve exactamente igual que un paradigma bien contado. La defensa es la de siempre: class(EEG.event(1).type) antes de creerle a nada.

  • ConceptosEstructura · arreglo de estructuras · celda
  • ReconstruyeLas dos formas de sacar una columna: [ ] para números, { } para texto
  • Error típicoMezclar muestras, segundos y milisegundos sin que nada dé error
  • Queda abiertoEl conteo cuadra con el paradigma, pero ¿cuántos de esos ensayos son utilizables?
Notas para quien dicta
MinBloqueQué hace el grupo
0–10Repaso activo¿Por qué 4× ensayos = 2× SNR? Que lo diga alguien en voz alta
10–30EEG de jugueteConstruirlo campo por campo y mirar whos EEG
30–45Los campos que importanAbrir un .set real y buscar los mismos campos
45–60Struct arrays y unidadesConteo de códigos y conversión de latencias
60–75eeghCargar un archivo con el menú y pedirle a EEGLAB el comando
75–110Ejercicio 3Pasos 1 y 2 juntos; el 3 solos, con datos reales si ya los tienen
110–120CierreLa predicción del sum(codigos == 211), antes de correrla

Qué vigilar

  • La sintaxis {EEG.chanlocs.labels} parece arbitraria y no lo es: si el grupo no entiende que chanlocs son 33 estructuras y no una, van a copiar la línea sin poder adaptarla. Vale la pena escribir EEG.chanlocs(1) y EEG.chanlocs(2) por separado en la pantalla.
  • Respuesta que delata un malentendido: «codigos == 211 daría error». La mayoría espera un error donde hay un cero silencioso, y ésa es la distinción que hay que instalar hoy.
  • eegh es el momento en que la interfaz deja de ser una alternativa al script y pasa a ser la manera de escribirlo. Si alguien sigue prefiriendo los menús después de este bloque, muéstrale el día 6 en pantalla: cinco sujetos, un archivo.

Para discutir, si sobra tiempo

Los conteos del archivo no cuadran con el paradigma publicado: faltan dos ensayos. ¿Qué explicaciones son compatibles con eso, y cuál se puede descartar mirando los datos en vez de suponiendo?

Día 42 h · MATLAB + ERPLAB

Filtrar sin romper

Al terminar vas a saber qué hace un filtro por dentro, vas a haber visto un filtro fabricar un componente que no existía, y vas a saber qué parámetros usar.

Ocho números

Toma esta lista y reemplaza cada valor por el promedio de sí mismo y sus dos vecinos:

x = [2 8 3 9 4 10 5 11];

y = zeros(1, numel(x));
for k = 2:numel(x)-1
    y(k) = (x(k-1) + x(k) + x(k+1)) / 3;
end
y
y = 0 4.33 6.67 5.33 7.67 6.33 8.67 0

Los saltos de la serie original —de 2 a 8, de 3 a 9— se aplanaron: la variación rápida se fue, la tendencia lenta quedó. Eso ya es un filtro paso bajo. Todo lo demás es esa misma operación hecha con pesos mejor elegidos.

Ahora los nombres. Los tres pesos [1/3 1/3 1/3] son el núcleo del filtro. Deslizarlos por la señal, multiplicando y sumando en cada posición, se llama convolución. Y el bucle que acabas de escribir, puesto en símbolos, es esto:

y[k] = b₁·x[k−1] + b₂·x[k] + b₃·x[k+1]
SímboloQué esEn el ejemplo
x[k]La señal de entrada en la muestra k2, 8, 3, 9, …
bLos pesos del núcleo1/3, 1/3, 1/3
y[k]La salida en la muestra k4,33, 6,67, …

Con más pesos la lista se hace larga, así que se escribe con una suma:

y[k] = Σⱼ bⱼ · x[k−j]

Eso es un filtro digital, entero. Butterworth, FIR, paso alto, paso bajo: todos son esta misma suma, y lo único que los distingue es cuántos pesos hay y cuánto valen. MATLAB la calcula en una línea:

conv(x, [1 1 1]/3, 'same')
conv(x, [0.25 0.5 0.25], 'same')        % el centro pesa el doble
ans = 3.3333 4.3333 6.6667 5.3333 7.6667 6.3333 8.6667 5.3333 ans = 3.0000 5.2500 5.7500 6.2500 6.7500 7.2500 7.7500 6.7500

Compara con la salida del bucle: en el medio son idénticas, pero en los extremos el bucle dejó ceros y conv devolvió 3,33 y 5,33. Ninguno de los dos tiene razón, porque ahí no hay datos: el bucle se negó a inventarlos y conv supuso que la señal seguía con ceros.

Todo filtro tiene ese problema en los bordes, y en un registro real los bordes son los primeros milisegundos del archivo, los últimos, y cada corte entre bloques. Guárdalo: reaparece más abajo como 'Boundary','boundary'.

Qué frecuencias sobreviven

Un filtro se describe por cuánto deja pasar de cada frecuencia. Esa curva se obtiene transformando el núcleo, y es todo lo que hay detrás de la palabra «respuesta en frecuencia»:

srate = 500;  L = 9;
H = abs(fft(ones(1,L)/L, 512));
f = (0:511) * srate / 512;

for fq = [2 30 50]
    [~, i] = min(abs(f - fq));
    fprintf('ganancia en %5.1f Hz: %.3f\n', f(i), H(i));
end
ganancia en 2.0 Hz: 0.998 ganancia en 30.3 Hz: 0.582 ganancia en 49.8 Hz: 0.115

Una media móvil de 9 puntos a 500 Hz deja pasar casi intacto lo que ocurre a 2 Hz, se come casi la mitad de lo que ocurre a 30 Hz y deja pasar apenas un 11% de los 50 Hz de la red eléctrica. Y no tiene ningún parámetro salvo su largo. Los filtros «de verdad» (Butterworth, FIR) sólo agregan control sobre la frecuencia de corte —dónde empieza a atenuar— y sobre el orden —cuán abrupta es la caída, que se mide en dB por octava.

El filtro que inventa un componente

Aquí está el motivo por el que este día existe. Un paso alto de primer orden cabe en cinco líneas; escribámoslo y pasémosle una onda que es puramente positiva:

srate = 500;
t = (-100:500) / srate;            % enteros / srate: sin residuos de punto flotante
onda = 8 * exp(-((t - 0.400).^2) / (2*0.080^2));

for fc = [0.1 0.5 2 5]
    y = pasoalto1(onda, fc, srate);
    fprintf('fc = %4.1f Hz  ->  pico %5.2f uV,  posterior %6.2f uV\n', ...
        fc, max(y), min(y(t > 0.6)));
end
fprintf('original      ->  pico %5.2f uV,  posterior %6.2f uV\n', ...
    max(onda), min(onda(t > 0.6)));

function y = pasoalto1(x, fc, srate)
    a = 1 / (1 + 2*pi*fc/srate);
    y = zeros(size(x));
    for n = 2:numel(x)
        y(n) = a * (y(n-1) + x(n) - x(n-1));
    end
end
fc = 0.1 Hz -> pico 7.52 uV, posterior -0.83 uV fc = 0.5 Hz -> pico 6.04 uV, posterior -2.40 uV fc = 2.0 Hz -> pico 3.39 uV, posterior -2.13 uV fc = 5.0 Hz -> pico 1.73 uV, posterior -0.73 uV original -> pico 8.00 uV, posterior 0.00 uV

La señal de entrada no baja de cero en ningún momento. La señal filtrada a 2 Hz tiene un valle de −2,13 µV después del pico, y el pico mismo perdió más de la mitad de su amplitud. Grafica onda y y superpuestas y míralo: ese valle no está en los datos, lo fabricó el filtro. Si tu hipótesis es sobre un componente negativo tardío, un paso alto agresivo te lo regala.

Por eso la recomendación de ERPLAB es conservadora: paso alto en 0,1 Hz o menos, paso bajo en 20 Hz o más, con pendiente de 12 dB/octava, salvo que sepas exactamente lo que estás haciendo. Y por eso, en un paper de ERP, los parámetros del filtro son parte de los métodos, no un detalle técnico.

Cómo se hace en la práctica

Dos reglas de orden que vienen de la misma física: el paso alto necesita ver tramos largos para estimar la deriva, así que va sobre el EEG continuo, antes de epocar; el paso bajo sólo suaviza, así que puede ir al final, sobre el ERP ya promediado, donde no afecta la detección de artefactos.

% Paso alto sobre el EEG continuo, canales 1 a 33
EEG = pop_basicfilter(EEG, 1:33, 'Filter', 'highpass', 'Design', 'butter', ...
    'Cutoff', 0.1, 'Order', 2, 'RemoveDC', 'on', 'Boundary', 'boundary');

% Paso bajo, mucho después, sobre el ERP promediado
ERP = pop_filterp(ERP, 1:33, 'Filter', 'lowpass', 'Design', 'butter', ...
    'Cutoff', 20, 'Order', 2);

El argumento 'Boundary','boundary' le dice al filtro que no cruce los cortes del registro: si hubo una pausa entre bloques, filtrar a través de ella produce un transitorio enorme que después se ve como artefacto.

Referencia: aritmética entre canales

El voltaje sólo existe como diferencia entre dos puntos, así que «rereferenciar» es simplemente restar. ERPLAB lo hace con fórmulas escritas en texto plano, una por canal:

% Referencia al promedio de mastoides, y un canal bipolar de EOG
EEG = pop_eegchanoperator(EEG, { ...
    'ch1 = ch1 - (ch30 + ch31)/2 label Fp1', ...
    'ch2 = ch2 - (ch30 + ch31)/2 label Fp2', ...
    'nch34 = ch32 - ch33 label VEOG-bipolar' }, ...
    'ErrorMsg', 'popup', 'KeepChLoc', 'on', 'Warning', 'on');

ch reemplaza un canal existente y nch crea uno nuevo. Para un montaje de 32 canales no vas a escribir 32 líneas a mano: el Reference Assistant de la interfaz las genera y las guarda en un .txt que después le pasas a la función como ruta.

Ejercicio 4Documenta tu propia distorsión

La pregunta del ejercicio es cuánta amplitud te cobra cada filtro. Empieza por la onda limpia, donde no hay ruido que confunda la medición y el número se puede comprobar exactamente. Usa la función pasoalto1 del bloque anterior, copiada al final del script.

Paso 1 · resuelto
clear; clc;
srate = 500;
t     = (-100:399) / srate;         % enteros / srate: t(101) es 0 exacto
onda  = 8 * exp(-((t - 0.400).^2) / (2*0.080^2));
vent  = t >= 0.300 & t <= 0.500;

y01 = pasoalto1(onda, 0.1, srate);

a0 = mean(onda(vent));
a1 = mean(y01(vent));
fprintf('sin filtrar   : %.3f uV\n', a0);
fprintf('paso alto 0,1 : %.3f uV  (%.1f%% menos)\n', a1, 100*(1 - a1/a0));
sin filtrar : 6.300 uV paso alto 0,1 : 5.817 uV (7.7% menos)

Un 7,7% de amplitud perdida con el filtro más suave que se recomienda. No es cero, y nunca lo es.

Paso 2 · completa los huecos

Repite con un paso alto de 1 Hz, que sigue pareciendo inofensivo:

y10 = pasoalto1(onda, ____, srate);

a2 = mean(y10(____));
fprintf('paso alto 1,0 : %.3f uV  (%.1f%% menos)\n', a2, 100*(1 - ____/a0));

plot(t, onda, t, y01, t, y10)
legend('sin filtrar', '0,1 Hz', '1,0 Hz')
xlabel('Tiempo (s)'), ylabel('Amplitud (\muV)')
Paso 3 · por tu cuenta

Ahora con datos: toma el promedio de 40 ensayos del día 2 —señal más ruido— y repite las tres mediciones. Agrega al gráfico una línea horizontal en cero y mira qué pasa después del pico. Escribe, en dos frases, cómo lo reportarías en la sección de métodos de un paper: qué filtro, qué corte, qué orden, y qué le hace a la amplitud que estás midiendo.

Cómo saber si quedó bien

Con la onda limpia los tres números son exactos: 6,300 µV sin filtrar, 5,817 con paso alto en 0,1 Hz (−7,7%) y 3,043 con paso alto en 1 Hz (−51,7%). Si te dan valores distintos, revisa que pasoalto1 esté copiada tal cual y que la ventana use >= y <= (son 101 muestras, no 100).

Con el promedio ruidoso las cifras se mueven unos pocos puntos porcentuales en cada corrida —el ruido también entra a la ventana de medición— pero el orden se mantiene: el filtro de 1 Hz se come alrededor de la mitad del componente.

Solución y las dos frases
clear; clc; rng(42);
srate = 500;
t     = (-100:399) / srate;         % enteros / srate: t(101) es 0 exacto
onda  = 8 * exp(-((t - 0.400).^2) / (2*0.080^2));
vent  = t >= 0.300 & t <= 0.500;

prom = mean(onda + 25 * randn(40, numel(t)), 1);   % el promedio del día 2

for entrada = {onda, prom}
    x  = entrada{1};
    a0 = mean(x(vent));
    fprintf('\nsin filtrar   : %6.3f uV\n', a0);
    for fc = [0.1 1.0]
        y = pasoalto1(x, fc, srate);
        a = mean(y(vent));
        fprintf('paso alto %3.1f : %6.3f uV  (%.1f%% menos)\n', fc, a, 100*(1 - a/a0));
    end
end

figure; hold on
plot(t, prom)
plot(t, pasoalto1(prom, 0.1, srate))
plot(t, pasoalto1(prom, 1.0, srate))
plot([t(1) t(end)], [0 0], 'k')
legend('sin filtrar', '0,1 Hz', '1,0 Hz')
xlabel('Tiempo (s)'), ylabel('Amplitud (\muV)')

function y = pasoalto1(x, fc, srate)
    a = 1 / (1 + 2*pi*fc/srate);
    y = zeros(size(x));
    for n = 2:numel(x)
        y(n) = a * (y(n-1) + x(n) - x(n-1));
    end
end

Dos frases posibles para los métodos: «El EEG continuo se filtró con un paso alto Butterworth no causal de segundo orden, corte a 0,1 Hz (12 dB/octava). Sobre una simulación del componente medido, ese filtro reduce la amplitud media entre 300 y 500 ms en un 7,6%, y un corte de 1 Hz la habría reducido a la mitad.»

Eso segundo casi nunca se reporta y es lo que permite comparar amplitudes entre estudios. Un efecto de 2 µV filtrado a 0,1 y uno de 2 µV filtrado a 1 Hz no son el mismo efecto.

Una diferencia que el ejercicio esconde

pasoalto1 recorre la señal hacia adelante, así que además de bajar la amplitud corre el componente en el tiempo. Los filtros de ERPLAB pasan hacia adelante y hacia atrás (no causales, con filtfilt), lo que cancela ese corrimiento pero no la pérdida de amplitud ni el valle fabricado. El ejercicio exagera un problema y muestra otro tal cual es.

Cierre del día 4

Antes de mirar la respuesta. Tu hipótesis es sobre una negatividad tardía, alrededor de 600 ms. Los registros tienen deriva y alguien sugiere subir el paso alto de 0,1 a 0,5 Hz «para limpiar». Predice dos cosas: qué le pasa a la amplitud de tu componente, y qué le pasa a la probabilidad de que encuentres el efecto que buscas.

Respuesta

La amplitud del componente baja: la tabla del bloque anterior da 6,04 µV en el pico contra 8 sin filtrar. Eso solo haría el efecto más difícil de encontrar.

Pero el filtro también fabrica un valle de −2,4 µV justo después del pico, en el territorio donde vive tu componente tardío. Si tu ventana de medición cae ahí, el efecto puede crecer. Y si crece, no tienes forma de distinguir en los datos filtrados qué parte es fisiología y qué parte es el filtro.

Por eso el corte se elige antes de mirar los resultados y se reporta. Un filtro que se ajusta hasta que el efecto aparece es un procedimiento que garantiza encontrar algo.

  • ConceptosNúcleo · convolución · frecuencia de corte y orden
  • Reconstruyey[k] = Σ bⱼ·x[k−j], partiendo de la media móvil de tres puntos
  • Error típicoTratar el filtro como limpieza neutra en vez de como una transformación que hay que reportar
  • Queda abiertoSi el filtro distorsiona, ¿por qué no trabajar sin filtrar? ¿Qué se pierde?
Notas para quien dicta
MinBloqueQué hace el grupo
0–15Ocho númerosHacer la media móvil a mano en papel antes de escribir el bucle
15–35De los números al formalismoEscribir la suma con símbolos a partir del bucle propio
35–50Respuesta en frecuenciaCorrer el bloque de ganancias y explicar qué mide cada número
50–75El filtro que inventa un componenteCorrer la demo y graficar entrada y salida superpuestas
75–95Filtrado y referencia en ERPLABTraducir la demo a pop_basicfilter
95–115Ejercicio 4Pasos 1 y 2 guiados; el 3 queda de tarea si falta tiempo
115–120CierreLa predicción del revisor, por escrito antes de discutirla

Qué vigilar

  • El objetivo del día no es que sepan elegir parámetros, es que dejen de leer «filtrado 0,1–30 Hz» como una frase decorativa en los métodos. La demo del valle fabricado es el momento del curso donde eso ocurre: vale la pena correrla en vivo y en silencio.
  • Respuesta que delata un malentendido: «el filtro elimina el ruido». Un filtro no distingue ruido de señal, sólo frecuencias; si el componente y el artefacto comparten banda, se van juntos.
  • La distinción causal / no causal aparece escondida en la solución del ejercicio. Si el grupo tiene fluidez matemática, conviene sacarla a la pizarra: explica por qué el filtro de ERPLAB no corre los componentes en el tiempo y el del ejercicio sí.

Para discutir, si sobra tiempo

La recomendación conservadora (0,1 Hz) supone que la deriva lenta es ruido y el componente no está en esa banda. ¿Para qué componentes es falso ese supuesto, y qué haría alguien que estudia potenciales lentos?

Día 52 h · EEGLAB + ERPLAB

El pipeline completo

Al terminar vas a haber convertido un registro continuo de un sujeto en un archivo .erp, pasando por las cinco etapas, y vas a saber qué hace cada una.

Las cinco etapas

ERPLAB impone un orden. No es arbitrario: cada paso necesita lo que dejó el anterior.

EtapaFunciónQué deja
1. EventListpop_creabasiceventlistUna tabla ordenada de eventos dentro del dataset
2. Asignar binspop_binlisterCada evento etiquetado con la condición a la que pertenece
3. Epocarpop_epochbinDatos de 2D a 3D: canales × tiempo × épocas
4. Artefactospop_artmwppthÉpocas marcadas con una bandera, no borradas
5. Promediarpop_averagerUn objeto ERP, uno por bin

Los códigos del paradigma N400

En este experimento cada ensayo tiene tres eventos: aparece una palabra prime, después una palabra target que puede estar semánticamente relacionada con ella o no, y el participante responde si lo están. Los códigos de ERP CORE tienen tres dígitos con un significado cada uno.

CódigoQué marcaLos dígitos
111 · 112Prime de un par relacionado1 = prime · 1 = relacionado · lista 1 o 2
121 · 122Prime de un par no relacionado1 = prime · 2 = no relacionado · lista 1 o 2
211 · 212Target relacionado2 = target · 1 = relacionado · lista 1 o 2
221 · 222Target no relacionado2 = target · 2 = no relacionado · lista 1 o 2
201Respuesta correcta
202Respuesta incorrecta

El tercer dígito distingue dos listas de palabras que se contrabalancean entre participantes, así que para el análisis se juntan: lo que separa las condiciones son los dos primeros.

El archivo que define tus condiciones

El paso conceptualmente más importante es el segundo, y se controla con un archivo de texto que escribes tú: el bin descriptor file. Cada bloque declara una condición: número de bin, una etiqueta y una secuencia de códigos. El punto marca el evento al que se alinea el tiempo cero.

bin 1
Prime de par relacionado
.{111;112}

bin 2
Prime de par no relacionado
.{121;122}

bin 3
Target relacionado, respuesta correcta
.{211;212}{201}

bin 4
Target no relacionado, respuesta correcta
.{221;222}{201}

El bin 3 se lee así: «alinea el tiempo cero en un 211 o en un 212, y acepta la época sólo si el evento siguiente es un 201». El punto y coma dentro de las llaves significa «cualquiera de estos» —es como se juntan las dos listas de palabras— y el bloque que va después del punto exige un contexto posterior. También se pueden poner llaves antes del punto para exigir un contexto previo: con esa gramática, «rara precedida de frecuente y seguida de respuesta correcta» es una sola línea.

Los primes van en sus propios bins aunque el efecto N400 no esté ahí. Sirven de control: si el prime relacionado y el no relacionado difieren, algo está mal, porque hasta ese momento las dos condiciones son idénticas para el participante.

Verifica igual

Estos son los códigos que usa ERP CORE, pero el conteo que hiciste el día 3 sigue siendo obligatorio: te dice si el archivo que bajaste los trae como números o como texto ('S211'), y si el número de ensayos por condición cuadra con el paradigma. Un BDF que no encuentra nada da el error No bins found, y la causa es casi siempre esa.

El pipeline, línea por línea

[ALLEEG, EEG, CURRENTSET, ALLCOM] = eeglab;

raiz = '/ruta/a/ERP_CORE/N400';
s    = 'sub-001';
carp = fullfile(raiz, s);

% --- cargar
EEG = pop_loadset('filename', [s '_N400.set'], 'filepath', carp);

% --- paso alto sobre el continuo
EEG = pop_basicfilter(EEG, 1:33, 'Filter', 'highpass', 'Design', 'butter', ...
    'Cutoff', 0.1, 'Order', 2, 'RemoveDC', 'on', 'Boundary', 'boundary');

% --- 1. EventList
EEG = pop_creabasiceventlist(EEG, 'AlphanumericCleaning', 'on', ...
    'BoundaryNumeric', {-99}, 'BoundaryString', {'boundary'});

% --- 2. asignar a bins
EEG = pop_binlister(EEG, 'BDF', fullfile(raiz, 'bins_n400.txt'), ...
    'IndexEL', 1, 'SendEL2', 'EEG', 'Voutput', 'EEG');

% --- 3. epocar: -200 a 800 ms, línea base = todo el pre-estímulo
EEG = pop_epochbin(EEG, [-200.0 800.0], 'pre');

% --- 4. detectar artefactos (ventana móvil, pico a pico)
EEG = pop_artmwppth(EEG, 'Channel', 1:33, 'Threshold', 100, ...
    'Twindow', [-200 798], 'Windowsize', 200, 'Windowstep', 50, ...
    'Flag', 1, 'Review', 'off');

% --- 5. promediar sólo las épocas buenas
ERP = pop_averager(EEG, 'Criterion', 'good', 'SEM', 'on', 'Compute', 'ERP');

ERP = pop_savemyerp(ERP, 'erpname', [s '_N400'], ...
    'filename', [s '_N400.erp'], 'filepath', carp, 'Warning', 'off');

Dos detalles que valen los cinco minutos que toma entenderlos.

El tercer argumento de pop_epochbin, 'pre', dice que la línea base es todo el período anterior al estímulo. Es el valor por defecto sensato; se puede dar un rango explícito como [-200 0].

Y pop_artmwppth no borra nada. Desliza una ventana de 200 ms por la época, mide la diferencia entre el máximo y el mínimo dentro de esa ventana, y si supera los 100 µV levanta una bandera. Las épocas siguen ahí; es pop_averager con 'Criterion','good' quien decide ignorarlas. Que la marca sea reversible es la razón por la que se puede volver a mirar un rechazo dudoso sin reprocesar todo.

pop_summary_AR_eeg_detection(EEG, '');   % % de rechazo, por bin y total
pop_eegplot(EEG, 1, 1, 1);               % mirar las épocas marcadas
pop_ploterps(ERP, [3 4], 1:33);          % los dos bins de target, todos los canales

iCPz = find(strcmp({ERP.chanlocs.labels}, 'CPz'));   % el sitio donde vive el N400
pop_ploterps(ERP, [3 4], iCPz);

El N400 se mide en CPz —canal 14 en el montaje de ERP CORE, pero búscalo por nombre en vez de confiar en el número— y la ventana estándar es 300 a 500 ms. En el bin 4 (target no relacionado) la onda debería ser más negativa que en el bin 3 en ese tramo. Los bins 1 y 2 deberían quedar prácticamente encima uno del otro.

Mirar los datos no es opcional

Un porcentaje de rechazo sobre 25% en algún bin significa que algo pasó: un electrodo suelto, un umbral mal puesto, un sujeto que parpadeó en cada ensayo. Y si el rechazo es muy distinto entre condiciones, la comparación queda contaminada aunque el promedio se vea impecable. pop_eegplot es lento y aburrido y hay que usarlo igual.

Ejercicio 5Un sujeto, de principio a fin

Es el ejercicio central de la semana: un registro continuo entra, un .erp sale, y tú puedes explicar cada paso del camino.

Paso 1 · resuelto

Antes que nada, el archivo de bins. Escríbelo desde MATLAB en vez de a mano: así queda dentro del script, y el script es el método.

raiz = '/ruta/a/ERP_CORE/N400';
bdf  = fullfile(raiz, 'bins_n400.txt');

fid = fopen(bdf, 'w');
fprintf(fid, 'bin 1\nPrime de par relacionado\n.{111;112}\n\n');
fprintf(fid, 'bin 2\nPrime de par no relacionado\n.{121;122}\n\n');
fprintf(fid, 'bin 3\nTarget relacionado, respuesta correcta\n.{211;212}{201}\n\n');
fprintf(fid, 'bin 4\nTarget no relacionado, respuesta correcta\n.{221;222}{201}\n');
fclose(fid);

type(bdf)          % míralo antes de usarlo

fopen con 'w' crea el archivo (y borra el que hubiera), fprintf con un fid como primer argumento escribe en él en vez de en pantalla, y fclose lo cierra. Si te saltas el fclose, el archivo puede quedar vacío: MATLAB todavía no alcanzó a escribirlo en el disco.

Paso 2 · completa los huecos

Las tres primeras etapas sobre el sujeto cargado y filtrado:

size(EEG.data)      % antes de epocar: DOS números

EEG = pop_creabasiceventlist(EEG, 'AlphanumericCleaning', 'on', ...
    'BoundaryNumeric', {-99}, 'BoundaryString', {'boundary'});

EEG = pop_binlister(EEG, 'BDF', ____, ...
    'IndexEL', 1, 'SendEL2', 'EEG', 'Voutput', 'EEG');

EEG = pop_epochbin(EEG, [-200.0 ____], 'pre');

size(EEG.data)      % después: TRES números. ¿Qué es cada uno?
EEG.trials
Paso 3 · por tu cuenta

Completa las etapas 4 y 5, guarda el .erp y revisa. Antes de promediar, corre tu inventario del día 3 sobre el dataset: los seis códigos tienen que estar y en la cantidad que declara el paradigma. Después anota el porcentaje de rechazo por bin, grafica los bins 3 y 4 en CPz, y repite el pipeline completo con el umbral de artefactos en 50 y en 200 µV. Anota cuántas épocas sobreviven en cada caso y cuánto cambia la diferencia entre condiciones.

Cómo saber si quedó bien
  • size(EEG.data) pasa de dos números a tres. El tercero es el total de épocas y tiene que ser parecido a la suma de los ensayos de los cuatro bins que contaste con inventario.
  • El rechazo por bin queda bajo el 25% y —esto importa más que el número— es parecido entre el bin 3 y el bin 4.
  • En CPz, entre 300 y 500 ms, el bin 4 va por debajo del bin 3. Los bins 1 y 2 quedan prácticamente encima uno del otro: hasta ese momento del ensayo las dos condiciones son idénticas para el participante, así que cualquier diferencia grande ahí es una señal de que algo se asignó mal.
  • Si aparece No bins found, no toques el BDF todavía: primero lista los códigos reales. Casi siempre vienen como texto ('S211').
Solución, y qué mirar en cada salida
function pipeline_un_sujeto(raiz, s, umbral)
% PIPELINE_UN_SUJETO  De .set continuo a .erp, un participante.
%   raiz   : carpeta del experimento
%   s      : 'sub-001'
%   umbral : criterio de artefactos en µV (100 por defecto)

    if nargin < 3, umbral = 100; end
    carp   = fullfile(raiz, s);
    salida = fullfile(raiz, 'derivados');          % todo lo generado, junto
    if ~exist(salida, 'dir'), mkdir(salida); end

    EEG = pop_loadset('filename', [s '_N400.set'], 'filepath', carp);

    EEG = pop_basicfilter(EEG, 1:33, 'Filter', 'highpass', 'Design', 'butter', ...
        'Cutoff', 0.1, 'Order', 2, 'RemoveDC', 'on', 'Boundary', 'boundary');

    EEG = pop_creabasiceventlist(EEG, 'AlphanumericCleaning', 'on', ...
        'BoundaryNumeric', {-99}, 'BoundaryString', {'boundary'});

    EEG = pop_binlister(EEG, 'BDF', fullfile(raiz, 'bins_n400.txt'), ...
        'IndexEL', 1, 'SendEL2', 'EEG', 'Voutput', 'EEG');

    EEG = pop_epochbin(EEG, [-200.0 800.0], 'pre');

    EEG = pop_artmwppth(EEG, 'Channel', 1:33, 'Threshold', umbral, ...
        'Twindow', [-200 798], 'Windowsize', 200, 'Windowstep', 50, ...
        'Flag', 1, 'Review', 'off');

    pop_summary_AR_eeg_detection(EEG, '');       % queda en la consola

    ERP = pop_averager(EEG, 'Criterion', 'good', 'SEM', 'on', 'Compute', 'ERP');
    ERP = pop_savemyerp(ERP, 'erpname', sprintf('%s_N400_%d', s, umbral), ...
        'filename', sprintf('%s_N400_%d.erp', s, umbral), ...
        'filepath', salida, 'Warning', 'off');
end

Dos cambios respecto del bloque de más arriba, los dos pensando en el día 6: el .erp queda en una carpeta común derivados en vez de dentro de la del sujeto, y el nombre del archivo lleva el umbral, para que las tres versiones puedan convivir sin pisarse.

Poner el umbral como argumento —en vez de escribirlo dentro— es lo que convierte «repite todo con 50 y con 200» en tres líneas. Es también la razón por la que este archivo sirve tal cual para el día 6.

Al comparar los tres umbrales vas a ver el compromiso completo: con 50 µV sobreviven pocas épocas y el promedio queda ruidoso aunque cada época sea impecable; con 200 entran casi todas, incluidas algunas con parpadeos que aportan más varianza que señal. Lo que decide no es cuál da el efecto más grande, sino cuál rechaza de forma pareja entre condiciones.

Cierre del día 5

Antes de correrlo. Alguien reordena el pipeline y llama a pop_epochbin justo después de cargar, sin haber pasado por las dos primeras etapas. Predice: ¿qué mensaje aparece, y por qué ése y no otro?

Respuesta

EVENTLIST structure is not attached. La clave está en el nombre de la función: pop_epochbin no corta épocas alrededor de eventos, corta épocas por bin, y los bins no existen hasta que el binlister los asignó. La etapa 3 depende de la 2, que depende de la 1.

Es un buen mensaje de error: dice exactamente lo que falta. La mayoría no lo son, y por eso conviene aprender a leer estos cinco pasos como una cadena de dependencias y no como una receta.

  • ConceptosEventList · bin · época · marca reversible de artefacto
  • ReconstruyeLas cinco etapas en orden, y qué deja cada una para la siguiente
  • Error típicoCreer que la detección de artefactos borra épocas; sólo levanta banderas
  • Queda abierto¿Qué hace un rechazo del 30% en una condición y del 8% en la otra, aunque el promedio se vea perfecto?
Notas para quien dicta
MinBloqueQué hace el grupo
0–10Repaso activoReconstruir de memoria qué hace un filtro paso alto agresivo
10–25Las cinco etapasDibujarlas en papel, con qué entra y qué sale de cada una
25–45Códigos y bin descriptor fileLeer el bin 3 en voz alta: «alinea en 211 o 212 y exige un 201 después»
45–80El pipeline línea por líneaCorrerlo sobre un sujeto, deteniéndose en cada etapa a mirar size
80–105Ejercicio 5Paso 3, con la comparación de umbrales si alcanzan
105–120Revisar y cerrarpop_eegplot sobre épocas marcadas; la predicción del orden

Qué vigilar

  • Esta sesión se puede caer entera por un problema de datos: reserva diez minutos al principio para que todos tengan un .set que carga. Quien no lo tenga puede trabajar con un compañero, pero no en silencio mirando.
  • El bin descriptor file es el único momento del curso en que se escribe algo que no es MATLAB. Conviene decirlo explícitamente: es una gramática aparte, la de ERPLAB, y su sintaxis del punto y las llaves no tiene nada que ver con el resto de la semana.
  • Respuesta que delata un malentendido: «los bins 1 y 2 son de control, así que se pueden omitir». Sirven de comprobación de que el experimento y el código hacen lo que se cree; sin ellos, un error de asignación es indistinguible de un efecto.
  • Si alguien anuncia que su rechazo fue del 0%, casi siempre es que la detección corrió sobre el dataset equivocado o el umbral quedó altísimo.

Para discutir, si sobra tiempo

El umbral de 100 µV es una convención, no un hallazgo. ¿Qué habría que saber de una muestra —edad, patología, montaje— para justificar moverlo, y cómo se reporta esa decisión sin que parezca ajustada al resultado?

Día 62 h · ERPLAB

Automatizar y medir

Al terminar vas a tener un script que procesa N sujetos sin supervisión y entrega un archivo de valores listo para R o JASP.

El bucle sobre sujetos

Todo lo del día 5 se convierte en una función de un argumento, y el script se reduce a recorrer una lista. La estructura que sigue tiene tres piezas que valen más que el resto: un try/catch para que un sujeto roto no detenga las tres horas de procesamiento, un registro de lo que pasó, y una lista explícita de sujetos en vez de dir('*').

[ALLEEG, EEG, CURRENTSET, ALLCOM] = eeglab;

raiz     = '/ruta/a/ERP_CORE/N400';
sujetos  = {'sub-001','sub-002','sub-003','sub-004','sub-005'};
registro = {};                      % no lo llames log: log() es el logaritmo

for k = 1:numel(sujetos)
    s = sujetos{k};
    fprintf('\n===== %s (%d de %d) =====\n', s, k, numel(sujetos));
    try
        pipeline_un_sujeto(raiz, s);          % la función del día 5
        registro{end+1} = sprintf('%s  OK', s);
    catch err
        registro{end+1} = sprintf('%s  FALLO: %s', s, err.message);
        fprintf(2, 'Falló %s: %s\n', s, err.message);
    end
end

fid = fopen(fullfile(raiz, 'log_procesamiento.txt'), 'w');
fprintf(fid, '%s\n', registro{:});
fclose(fid);

Tres detalles de esas veinte líneas. registro{end+1} = ... agrega un elemento al final de una celda sin saber cuántos van a ser. El 2 de fprintf(2, ...) manda el texto al canal de errores, que MATLAB imprime en rojo: al volver de almorzar, los fallos se ven de lejos. Y err.message es el mensaje que habría aparecido si el error hubiera detenido todo — catch no lo esconde, lo guarda.

Una lista escrita a mano parece un retroceso frente a leer la carpeta automáticamente. No lo es: cuando excluyas un sujeto por calidad de datos, la exclusión queda escrita en el script, con fecha en el control de versiones, en vez de ser una carpeta que alguien movió.

Ondas de diferencia

La resta entre condiciones se hace con la misma gramática de texto que los canales, pero sobre bins. Se ejecuta sobre el objeto ERP, sujeto por sujeto, después de promediar:

ERP = pop_binoperator(ERP, { ...
    'bin5 = bin4 - bin3 label Target no relacionado menos relacionado' });

La diferencia elimina toda la actividad que es idéntica en las dos condiciones y deja sólo lo que las separa. Un aviso de la documentación que conviene tener presente: al crear bins nuevos por operación, ERPLAB pierde las métricas de calidad de datos asociadas, porque no puede estimarlas para una onda derivada.

Gran promedio

pop_gaverager no recibe los ERP: recibe la ruta de un archivo de texto con una ruta .erp por línea. Ese archivo se genera en el mismo script, que es como lo hacen los scripts publicados de ERP CORE.

salida = fullfile(raiz, 'derivados');       % donde quedaron los .erp

lista = fullfile(salida, 'lista_erpsets.txt');
fid = fopen(lista, 'w');
for k = 1:numel(sujetos)
    fprintf(fid, '%s\n', fullfile(salida, sprintf('%s_N400_100.erp', sujetos{k})));
end
fclose(fid);

GA = pop_gaverager(lista, 'ExcludeNullBin', 'on', 'SEM', 'on');
GA = pop_savemyerp(GA, 'erpname', 'GA_N400', ...
    'filename', 'GA_N400.erp', 'filepath', salida);

Qué número medir

Antes de exportar hay que decidir qué se exporta, y la opción intuitiva —la altura del pico— es la peor de las disponibles. Compruébalo con la simulación del día 2: la misma señal de siempre, 40 ensayos, y dos niveles de ruido.

rng(7);
srate = 500;
t     = (-100:399) / srate;         % enteros / srate: t(101) es 0 exacto
senal = 8 * exp(-((t - 0.400).^2) / (2*0.080^2));
vent  = t >= 0.300 & t <= 0.500;

for sd = [25 50]
    p = mean(senal + sd*randn(40, numel(t)), 1);
    fprintf('ruido %2d uV -> pico %5.2f   media %5.2f\n', sd, max(p(vent)), mean(p(vent)));
end
fprintf('verdad       -> pico %5.2f   media %5.2f\n', ...
    max(senal(vent)), mean(senal(vent)));

Tus números van a bailar de corrida en corrida. Promediando dos mil corridas queda así:

Ruido por ensayoPico medidoAmplitud media
25 µV16,86,29
50 µV26,56,34
El valor verdadero8,06,30

El pico medido duplica y triplica el verdadero, y crece cuando crece el ruido. La amplitud media no se mueve. La razón es que buscar un máximo es elegir el punto más alto de la ventana: el ruido que empuja hacia arriba se queda, el que empuja hacia abajo se descarta. Promediar la ventana, en cambio, deja que se cancelen.

La consecuencia es directa y es la razón por la que ERP CORE mide amplitud media: el pico depende de cuánto ruido quedó, y cuánto ruido quedó depende de cuántos ensayos sobrevivieron a la detección de artefactos. Comparar picos entre dos condiciones con distinto número de ensayos aceptados es, en parte, comparar sus niveles de ruido. Con datos reales —filtrados en paso bajo, con ruido menos abrupto que el de esta simulación— la exageración es menor, pero nunca es cero y nunca es simétrica.

Un pico no es un componente

Hay una segunda razón, más profunda, para desconfiar del pico. Lo que la onda muestra en 400 ms es la suma de todo lo que esté ocurriendo en ese instante, y los componentes se solapan en el tiempo. El máximo del trazado no marca «el momento del N400»: marca dónde la suma de varios procesos alcanzó su punto más alto. La latencia del pico y la latencia del proceso son dos cosas distintas, y confundirlas es una de las tentaciones clásicas de esta literatura.

Sacar los números

El último paso del análisis en MATLAB es también el más corto: extraer una medida por sujeto, bin y canal, y escribirla a un archivo que abra cualquier programa estadístico.

% ALLERP es la lista de ERPsets que hay en memoria; se llena con pop_loaderp.
% Ventana 300-500 ms; bins 3, 4 y 5; el canal, buscado por nombre.
iCPz = find(strcmp({ALLERP(1).chanlocs.labels}, 'CPz'));

[ALLERP, Amp, Lat] = pop_geterpvalues(ALLERP, [300 500], [3 4 5], iCPz, ...
    'Measure',    'meanbl', ...      % amplitud media, con línea base
    'Baseline',   'pre', ...
    'Erpsets',    1:numel(sujetos), ...
    'Filename',   fullfile(raiz, 'amplitudes_n400.txt'), ...
    'Foutput',    'erpset', ...      % formato largo: una fila por medición
    'Fracreplace','NaN', ...
    'Resolution', 3, ...
    'SendtoWorkspace', 'on', ...
    'Warning',    'off');

Los primeros cuatro argumentos son la ventana en ms, los bins, los canales, y el resto son opciones. Los valores de arriba son los que recomienda ERP CORE para el N400: amplitud media entre 300 y 500 ms en CPz. 'Measure' acepta también 'peaklatbl' (latencia del pico local) y 'areap' (área positiva), entre otras. 'Foutput','erpset' entrega formato largo —una fila por sujeto × bin × canal—, que es el que quiere R; el formato ancho pone todas las medidas de un sujeto en una línea, que es el que quieren las tablas dinámicas.

Una medida por hipótesis

La ventana de medición, los canales y el tipo de medida se eligen antes de mirar los resultados, idealmente tomándolos de la literatura previa (los papers de ERP CORE proponen ventanas para cada componente). Elegir la ventana donde el efecto se ve mejor es exactamente lo que produce hallazgos que no se replican.

Lo que evita que te odies en seis meses

  • Los datos crudos son de sólo lectura. Cada etapa escribe un archivo nuevo con un sufijo (_elist, _be, _ar). Nunca sobrescribes lo que llegó del equipo.
  • El script es el método. Si el pipeline está en un .m versionado, la sección de métodos se escribe leyéndolo, y un revisor puede pedirlo.
  • Semilla fija con rng(42) en cualquier cosa que use aleatoriedad, incluida ICA.
  • Reporta la calidad de los datos, no sólo el número de ensayos rechazados. ERPLAB calcula el error estándar de medición (SME), que estima directamente cuánto ruido queda en la medida que vas a analizar.
  • Guarda el log. Un archivo de texto con qué sujeto se procesó, cuándo, con qué parámetros y con qué porcentaje de rechazo vale más que la memoria.
Ejercicio finalCinco sujetos, un archivo de números

Empieza agregando una línea a tu pipeline del día 5 —la onda de diferencia, justo después de pop_averager— y vuelve a correr los cinco sujetos. Que la forma de cambiar el análisis sea «editar el script y volver a correrlo» es la mitad de lo que enseña este día.

ERP = pop_binoperator(ERP, { ...
    'bin5 = bin4 - bin3 label Target no relacionado menos relacionado' });
Paso 1 · resuelto

Con los cinco .erp en la carpeta derivados, cárgalos todos a memoria. ALLERP es la lista que van a pedir las funciones de medición:

[ALLEEG, EEG, CURRENTSET, ALLCOM] = eeglab;

raiz    = '/ruta/a/ERP_CORE/N400';
salida  = fullfile(raiz, 'derivados');
sujetos = {'sub-001','sub-002','sub-003','sub-004','sub-005'};

archivos = cell(1, numel(sujetos));
for k = 1:numel(sujetos)
    archivos{k} = sprintf('%s_N400_100.erp', sujetos{k});
end

[ERP, ALLERP] = pop_loaderp('filename', archivos, 'filepath', salida);

numel(ALLERP)              % ¿cargaron los cinco?
ALLERP(1).bindescr         % las etiquetas de los bins del primero
Paso 2 · completa los huecos

La medición. Busca el canal por nombre, nunca por número:

iCPz = find(strcmp({ALLERP(1).chanlocs.labels}, '____'));

[ALLERP, Amp, Lat] = pop_geterpvalues(ALLERP, [____ ____], [3 4 5], iCPz, ...
    'Measure',    'meanbl', ...
    'Baseline',   'pre', ...
    'Erpsets',    1:numel(sujetos), ...
    'Filename',   fullfile(salida, 'amplitudes_n400.txt'), ...
    'Foutput',    'erpset', ...
    'Fracreplace','NaN', ...
    'Resolution', 3, ...
    'SendtoWorkspace', 'on', ...
    'Warning',    'off');

size(Amp)                  % ¿cuántas mediciones salieron, y de qué forma?
Paso 3 · por tu cuenta

Calcula el gran promedio con pop_gaverager, grafica los bins 3, 4 y 5 en CPz, y abre el archivo exportado en R o JASP para correr una prueba t pareada entre el bin 3 y el bin 4. Después escribe el párrafo de métodos completo: filtros con sus cortes y órdenes, ventana de epocado, criterio de artefactos, sujetos incluidos y excluidos, ventana de medición y canal. Si alguno de esos datos no se puede leer en tu script, al script le falta algo.

Cómo saber si quedó bien
  • El archivo exportado tiene una fila por sujeto × bin × canal: con 5 sujetos, 3 bins y 1 canal, quince mediciones.
  • La comprobación que de verdad cierra el círculo: para cada sujeto, el valor del bin 5 tiene que ser el del bin 4 menos el del bin 3, hasta el último decimal. Si no cuadra, la onda de diferencia se calculó sobre otra cosa.
  • La diferencia media entre condiciones es negativa —el bin 4 más negativo que el 3— y del orden de unos pocos microvoltios. Cinco sujetos no alcanzan para un resultado publicable; alcanzan de sobra para comprobar que el pipeline funciona.
  • Si iCPz queda vacío, el montaje usa otra nomenclatura: imprime {ALLERP(1).chanlocs.labels} completo y elige mirando.
Solución y el párrafo de métodos
[ALLEEG, EEG, CURRENTSET, ALLCOM] = eeglab;

raiz     = '/ruta/a/ERP_CORE/N400';
salida   = fullfile(raiz, 'derivados');
sujetos  = {'sub-001','sub-002','sub-003','sub-004','sub-005'};
registro = {};

% --- 1. procesar cada sujeto, sin que uno malo detenga el lote
for k = 1:numel(sujetos)
    s = sujetos{k};
    fprintf('\n===== %s (%d de %d) =====\n', s, k, numel(sujetos));
    try
        pipeline_un_sujeto(raiz, s, 100);
        registro{end+1} = sprintf('%s  OK', s);
    catch err
        registro{end+1} = sprintf('%s  FALLO: %s', s, err.message);
        fprintf(2, 'Falló %s: %s\n', s, err.message);
    end
end

fid = fopen(fullfile(salida, 'log_procesamiento.txt'), 'w');
fprintf(fid, '%s\n', registro{:});
fclose(fid);

% --- 2. cargar los .erp
archivos = cell(1, numel(sujetos));
for k = 1:numel(sujetos)
    archivos{k} = sprintf('%s_N400_100.erp', sujetos{k});
end
[ERP, ALLERP] = pop_loaderp('filename', archivos, 'filepath', salida);

% --- 3. gran promedio, desde una lista en texto
lista = fullfile(salida, 'lista_erpsets.txt');
fid = fopen(lista, 'w');
for k = 1:numel(archivos)
    fprintf(fid, '%s\n', fullfile(salida, archivos{k}));
end
fclose(fid);

GA = pop_gaverager(lista, 'ExcludeNullBin', 'on', 'SEM', 'on');
GA = pop_savemyerp(GA, 'erpname', 'GA_N400', ...
    'filename', 'GA_N400.erp', 'filepath', salida);

% --- 4. medir y exportar
iCPz = find(strcmp({ALLERP(1).chanlocs.labels}, 'CPz'));
pop_ploterps(GA, [3 4 5], iCPz);

[ALLERP, Amp, Lat] = pop_geterpvalues(ALLERP, [300 500], [3 4 5], iCPz, ...
    'Measure', 'meanbl', 'Baseline', 'pre', ...
    'Erpsets', 1:numel(sujetos), ...
    'Filename', fullfile(salida, 'amplitudes_n400.txt'), ...
    'Foutput', 'erpset', 'Fracreplace', 'NaN', 'Resolution', 3, ...
    'SendtoWorkspace', 'on', 'Warning', 'off');

Un párrafo de métodos que se puede escribir leyendo ese archivo:

«Se analizaron cinco participantes del conjunto abierto ERP CORE (paradigma N400). El EEG continuo se filtró con un paso alto Butterworth de segundo orden con corte en 0,1 Hz (12 dB/octava), sin cruzar los límites de bloque. Los ensayos se segmentaron entre −200 y 800 ms respecto del target, con corrección de línea base sobre todo el período pre-estímulo. Se marcaron como artefacto las épocas con una diferencia pico a pico superior a 100 µV en cualquier canal, usando una ventana móvil de 200 ms con pasos de 50 ms; el promedio incluyó sólo las épocas no marcadas. Se promedió por separado el target relacionado y el no relacionado, ambos con respuesta correcta, y se calculó la onda de diferencia (no relacionado − relacionado). El N400 se cuantificó como amplitud media entre 300 y 500 ms en CPz.»

Cada dato de ese párrafo sale de una línea concreta del script, y ninguno sale de la memoria. Si mañana cambias el umbral a 75 µV, el párrafo queda mal en un solo lugar y sabes cuál.

Cierre del día 6

Antes de mirar la respuesta. Terminas el análisis y la tabla de rechazo dice: bin 3, 8% de épocas descartadas; bin 4, 31%. El efecto entre condiciones sale grande y significativo. ¿Puedes reportarlo tal cual? ¿Qué explicación alternativa hay que descartar primero?

Respuesta

No, todavía no. Con 31% de rechazo el bin 4 se promedió con muchos menos ensayos que el bin 3, así que su onda es más ruidosa. Si mides amplitud media, ese ruido extra no sesga la amplitud pero sí infla la varianza; si mides pico, la sesga directamente hacia arriba, como viste más arriba.

Y hay algo peor que el ruido: un rechazo tan desparejo indica que las dos condiciones se diferencian en algo que no es el N400 —más parpadeos, más movimiento, más tiempo en pantalla— y ese algo pudo haber entrado también a las épocas que sí sobrevivieron.

Lo primero que hay que descartar es lo mecánico: ¿el bin 4 tiene realmente más artefactos, o su definición en el BDF está capturando eventos que no corresponden? Después vienen las explicaciones interesantes. Reportar el porcentaje de rechazo por condición, y no sólo el total, es lo que permite que otro se haga esta misma pregunta.

  • ConceptosOnda de diferencia · gran promedio · amplitud media contra pico
  • ReconstruyePor qué el pico medido crece con el ruido y la amplitud media no
  • Error típicoElegir la ventana de medición después de ver dónde se ve mejor el efecto
  • Queda abierto¿Cuánto ruido queda en la medida que vas a analizar? Ahí empieza el SME
Notas para quien dicta
MinBloqueQué hace el grupo
0–10Repaso activoNombrar las cinco etapas del día 5 sin mirar
10–35El bucle sobre sujetosConvertir el script del día 5 en función y llamarlo en bucle
35–50Diferencias y gran promedioAgregar el bin 5 y volver a correr los cinco
50–70Qué número medirCorrer la simulación de pico contra media y comparar tablas
70–95Ejercicio finalMedir, exportar, abrir el archivo en R o JASP
95–110El párrafo de métodosEscribirlo leyendo el propio script, en silencio
110–120Chequeo finalLos cinco fragmentos de la sección siguiente, en parejas

Qué vigilar

  • El bloque del pico contra la amplitud media es el resultado más contraintuitivo de la semana y conviene dejar que lo descubran corriendo el código: casi todos esperan que el pico medido sea 8. Ver 16 y 26 hace el argumento solo.
  • Respuesta que delata un malentendido en el cierre: «hay que subir el umbral hasta que los dos bins rechacen parecido». El umbral se fija antes y por criterios de calidad de señal; ajustarlo mirando el balance entre condiciones es la misma trampa que elegir la ventana donde se ve el efecto.
  • Si alguien termina temprano: pedirle que cambie un solo parámetro del script, vuelva a correr los cinco sujetos y compare. Sentir que un análisis completo cuesta dos minutos es el argumento definitivo a favor de trabajar por script.

Para discutir, si sobra tiempo

Todo el pipeline de la semana toma decisiones —corte del filtro, umbral, ventana, canal— que podrían haber sido otras y que casi nunca se reportan como decisiones. ¿Cuáles de ellas cambiarían el resultado lo suficiente como para justificar reportar el análisis con varias combinaciones, y dónde está el límite con la pesca de resultados?

Al cierre60–90 min · en parejas

Chequeo final

Cinco fragmentos con un error plantado y dos decisiones de análisis. Ninguno da un mensaje de error obvio: todos producen un resultado que se ve razonable. Es la parte difícil del oficio.

Diagnóstico

Para cada fragmento, responde tres cosas antes de abrir la respuesta: qué está mal, qué resultado produce, y con qué línea de código lo comprobarías en treinta segundos.

1 · El promedio que no promedia lo que crees

% epocas viene de otro script, con forma tiempo × ensayos
size(epocas)                  % 500   40

promedio = mean(epocas, 1);
plot(promedio)
Respuesta

Con esa forma, la dimensión 1 es el tiempo. mean(epocas, 1) no promedia los 40 ensayos: promedia los 500 instantes de cada ensayo y devuelve 40 números, uno por ensayo. El gráfico sale igual, con 40 puntos, y parece una curva.

Lo correcto aquí es mean(epocas, 2), y el resultado queda como columna, así que probablemente necesites transponerlo. La comprobación de treinta segundos es size(promedio): si no tiene 500 elementos, no es un promedio de ensayos.

Convención útil para no volver a caer: en EEGLAB y ERPLAB el tiempo siempre va en la segunda dimensión. Si tus datos llegan al revés, transpónlos apenas entran y no vuelvas a pensar en eso.

2 · La ventana vacía

vent = ERP.times >= 0.3 & ERP.times <= 0.5;
amp  = mean(ERP.bindata(iCPz, vent, 4));
fprintf('N400: %.2f uV\n', amp);
Respuesta

ERP.times está en milisegundos, así que ninguna muestra cumple «entre 0,3 y 0,5». vent queda lleno de falsos, la selección sale vacía y mean de un vector vacío devuelve NaN. La línea imprime N400: NaN uV sin quejarse.

Comprobación: sum(vent). Si da 0, el problema son las unidades. Si da un número enorme —el largo entero de la época— probablemente escribiste | donde va &.

El caso peligroso no es éste, es el primo cercano: escribir [30 500] en vez de [300 500]. Ahí no hay NaN, hay un número perfectamente creíble medido en la ventana equivocada.

3 · Un sujeto que son siete

sujetos = 'sub-001';

for k = 1:numel(sujetos)
    s = sujetos(k);
    fprintf('procesando %s\n', s);
    pipeline_un_sujeto(raiz, s);
end
Respuesta

'sub-001' no es un nombre: es un vector de siete caracteres. numel devuelve 7 y el bucle da siete vueltas, procesando 's', 'u', 'b', '-'… Cada llamada falla con un error de archivo no encontrado, y si hay un try/catch alrededor, el log queda con siete líneas de fallo y nadie mira por qué.

La forma correcta es una celda, aunque tenga un solo elemento: sujetos = {'sub-001'}; y s = sujetos{k}; con llaves.

Comprobación: numel(sujetos) antes del bucle. Si el número de vueltas no es el número de sujetos, ahí está.

4 · El filtro en el lugar equivocado

EEG = pop_epochbin(EEG, [-200.0 800.0], 'pre');

EEG = pop_basicfilter(EEG, 1:33, 'Filter', 'highpass', 'Design', 'butter', ...
    'Cutoff', 0.1, 'Order', 2, 'RemoveDC', 'on');
Respuesta

El paso alto corre después de epocar, sobre trozos de un segundo. Un filtro con corte en 0,1 Hz necesita ver varios segundos para distinguir una deriva lenta de la señal; en una época de un segundo no tiene con qué, y lo que produce son transitorios gigantes en los bordes de cada época —justo donde está la línea base y justo donde termina la ventana de medición.

Es el mismo problema de los bordes que viste con conv, pero ahora ocurre 1.200 veces, una por época.

El orden correcto es el del día 5: paso alto sobre el continuo, antes de epocar; paso bajo al final, sobre el ERP promediado. Comprobación: graficar una época filtrada y mirar los extremos.

5 · La ventana que encontró el efecto

dif = ERP.bindata(iCPz, :, 5);        % la onda de diferencia

[~, i] = max(abs(dif));               % ¿dónde es más grande?
vent   = i-25 : i+25;                 % 100 ms alrededor

amp = mean(dif(vent));
fprintf('efecto: %.2f uV\n', amp);
Respuesta

Aquí no hay ningún error de programación: el código hace exactamente lo que dice. El problema es que elige la ventana de medición mirando los datos, y centrada en el máximo. Con datos que fueran ruido puro, este procedimiento también encontraría un efecto, y de un tamaño respetable.

La ventana se fija antes, desde la literatura o desde un conjunto de datos independiente. Si de verdad no se sabe dónde mirar, existen procedimientos que corrigen por haber mirado en todas partes —pruebas de permutación sobre el trazado completo, por ejemplo— y lo que no existe es la versión gratis.

De regalo, dos errores de programación escondidos: i es la unidad imaginaria de MATLAB y quedó tapada, y si el máximo cae cerca de un borde, i-25 puede ser negativo y reventar el índice.

Dos decisiones

Primera. Un colega mide amplitud de pico y encuentra que su grupo clínico tiene picos más chicos que los controles. En el mismo trabajo reporta que en el grupo clínico rechazó bastantes más ensayos por artefactos. ¿En qué dirección empuja ese desbalance a su resultado?

Respuesta

Más ensayos rechazados significa menos ensayos promediados, y eso significa más ruido residual. Como viste en el día 6, el ruido residual infla el pico medido. El grupo clínico debería, sólo por eso, mostrar picos más grandes.

Su efecto va en contra del sesgo, no a favor: el desbalance lo está perjudicando, y el efecto real probablemente sea algo mayor que el que reporta. Eso no salva el análisis —seguiría siendo mejor medir amplitud media— pero cambia por completo lo que hay que escribir en la discusión.

Razonar la dirección del sesgo, y no sólo su existencia, es lo que separa una objeción útil de una objeción decorativa.

Segunda. Un revisor pide aplicar un paso bajo de 30 Hz al ERP promediado «para que las figuras se vean más limpias». Tú mides amplitud media entre 300 y 500 ms. ¿Cuánto puede cambiar tu resultado?

Respuesta

Casi nada, y por una razón que ya conoces: promediar 200 ms de trazado ya es, en sí mismo, un filtro paso bajo. Lo que el filtro de 30 Hz saca son frecuencias que tu ventana estaba cancelando de todos modos.

La respuesta sería distinta si midieras amplitud de pico o latencia de pico. Ahí el suavizado cambia dónde está el máximo y cuánto vale, y aplicarlo o no aplicarlo puede mover el resultado.

Conclusión práctica: qué tan sensible es tu análisis al procesamiento depende de qué mides. Con amplitud media el filtro es cosmético; con medidas de pico es parte del método.

Lo que deberías poder hacer sin mirar

Si la semana funcionó, estas cinco cosas se pueden hacer con MATLAB abierto y la página cerrada. Si alguna se atasca, ya sabes a qué día volver.

  1. Escribir, de memoria, la corrección de línea base de una época: tres líneas.
  2. Explicar, con la fórmula, por qué cuadruplicar los ensayos duplica el SNR — y decir cuántos ensayos harían falta para duplicarlo de nuevo.
  3. Nombrar las cinco etapas del pipeline de ERPLAB en orden y decir qué se rompe si intercambias dos.
  4. Dar un valor de un parámetro de filtrado que pueda fabricar un componente que no existe, y explicar por dónde aparece.
  5. Escribir el párrafo de métodos de tu propio análisis leyendo únicamente tu script.
Y después de esto

Lo que queda afuera de la semana y sigue en el mismo camino: ICA para corregir parpadeos en vez de descartar la época; el error estándar de medición (SME) como reporte de calidad; análisis en el trazado completo con pruebas de permutación en vez de una ventana fija; y decoding (MVPA) sobre los mismos datos. Los cuatro se apoyan en exactamente lo que ya sabes: una matriz de canales por tiempo, un promedio, y un script que se puede volver a correr.

ReferenciaConsulta rápida

Cuando algo falle

Los errores de MATLAB son crípticos pero repetitivos. Casi todos los que vas a ver esta semana están en esta tabla.

Lo que diceLo que pasó
Undefined function 'pop_epochbin' EEGLAB o ERPLAB no están en el path. Corre eeglab antes que nada; si persiste, ERPLAB no quedó en eeglab/plugins/.
Index exceeds matrix dimensions Pediste un elemento que no existe. Típicamente indexaste desde 0, o pediste v(n+1) en la última vuelta de un bucle. Revisa con size.
Matrix dimensions must agree Falta un punto: * en vez de .*. O estás mezclando un vector fila con uno columna — size lo delata, ' lo arregla.
Subscript indices must be positive integers Un índice quedó en 0, negativo o decimal. Suele venir de convertir milisegundos a muestras sin redondear: usa round.
No bins found tras BINLISTER Los códigos del BDF no coinciden con los del EventList. Lista los códigos reales con el bucle del día 3 y compáralos uno por uno.
EVENTLIST structure is not attached Te saltaste pop_creabasiceventlist, o lo corriste sobre otro dataset. El orden de las cinco etapas no es negociable.
La figura sale vacía o en blanco Falta hold on entre dos plot, o la variable que graficaste quedó llena de NaN. Comprueba con any(isnan(x)).
El resultado es NaN y no hay ningún error Casi siempre una ventana vacía: comparaste milisegundos con segundos. sum(vent) lo confirma en un segundo. Si no es eso, hay un canal con NaN: any(isnan(x)).
El bucle da más vueltas que sujetos La lista es texto y no una celda: numel('sub-001') es 7. Usa {'sub-001'} y saca los elementos con llaves.
MATLAB se congela al graficar Le pediste 33 canales por diez minutos a 500 Hz. Grafica un canal, o un tramo: plot(EEG.times(1:5000), EEG.data(13,1:5000)).
Errores que aparecen y desaparecen Hay tildes, eñes o espacios en la ruta de los archivos, o dos versiones de EEGLAB en el path. which -all pop_averager muestra si hay duplicados.
El resultado cambia entre corridas Algo usa aleatoriedad sin semilla — casi siempre ICA. Pon rng(42) al inicio del script.

Y el reflejo general: cuando algo no cuadra, escribe el nombre de la variable sin punto y coma y mírala. size, class, min, max y whos resuelven más problemas que cualquier búsqueda en internet.

ReferenciaPara tener al lado

Chuleta

Todo lo que aparece en la semana, en un solo lugar.

MATLAB base

ComandoPara qué
a : b : cVector desde a hasta c en pasos de b
linspace(a,b,n)n valores repartidos entre a y b
zeros(m,n) · numel · sizePreasignar; contar elementos; dimensiones
v(end) · v(2:5) · v(v>0)Último; rango; indexado lógico
mean(M,2) · std · max(M,[],2)Estadísticos, con la dimensión explícita
.* ./ .^Operar elemento a elemento
find · unique · strcmpBuscar posiciones; valores distintos; comparar texto
fprintf · sprintf · dispImprimir con formato; formar texto; mostrar
fullfile · dir · exist · mkdirRutas portables; listar carpeta; ¿existe?; crearla
fopen · fprintf(fid,…) · fclose · typeEscribir un archivo de texto desde el script; mirarlo sin salir de MATLAB
isempty · isnumeric · isfield · cellfunPreguntar por lo que llegó antes de usarlo
rng(42) · randnSemilla; ruido gaussiano
try / catch · error · warningQue un fallo no detenga el lote
whos · class · which -allQué hay en memoria; de qué tipo; de dónde sale

EEGLAB y ERPLAB

FunciónPara qué
eeglab · eeghArrancar; ver el comando de lo que hiciste con el mouse
pop_loadset · pop_savesetAbrir y guardar datasets .set
pop_chaneditCargar coordenadas de electrodos
pop_basicfilter · pop_filterpFiltrar el EEG · filtrar el ERP
pop_eegchanoperatorRereferenciar, crear canales bipolares
pop_creabasiceventlistEtapa 1: construir el EventList
pop_binlisterEtapa 2: aplicar el bin descriptor file
pop_epochbinEtapa 3: epocar por bin, con línea base
pop_artmwppth · pop_artstepEtapa 4: ventana móvil pico a pico · escalón
pop_summary_AR_eeg_detectionPorcentaje de rechazo por bin
pop_averager · pop_savemyerpEtapa 5: promediar y guardar el .erp
pop_binoperatorOndas de diferencia entre bins
pop_loaderpCargar varios .erp y llenar ALLERP
pop_gaveragerGran promedio, desde una lista en .txt
pop_geterpvaluesMedir amplitudes y latencias, exportar
pop_ploterps · pop_eegplotVer los ERP · revisar las épocas marcadas