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.
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 ToolboxLa 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 SCCNDescá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 EEGLABLo 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 N40040 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 MATLABAbre MATLAB, escribe
eeglaby 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?
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.
| Min | Bloque | Qué hace el grupo |
|---|---|---|
| 0–10 | Qué vamos a hacer en la semana | Mirar la figura de portada y responder: ¿por qué el primer panel no sirve? |
| 10–30 | Instalación asistida | Instalar en su propia máquina, con la lista de chequeo abierta |
| 30–40 | Descarga de datos | Bajar sólo N400; mientras baja, recorrer la estructura de carpetas de ERP CORE |
| 40–45 | Verificación | Correr 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 eeglablos 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.
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)
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.
- Pestaña Home, botón New → Script.
- Guárdalo como
dia1.men tu carpeta del curso — sin tildes ni espacios en la ruta, igual que con EEGLAB. - 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.
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.
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)
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.
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?
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?
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.
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
| Min | Bloque | Qué hace el grupo |
|---|---|---|
| 0–20 | Command Window | Teclear en vivo; que cada quien rompa algo a propósito y lea el mensaje |
| 20–45 | Tiempo, senos, indexado lógico | Construir t y graficar; nadie avanza sin ver la figura |
| 45–60 | Matrices y dimensiones | size después de cada operación, en voz alta |
| 60–75 | El punto y la línea base | Escribir la corrección de línea base a mano |
| 75–90 | Del Command Window al script | Crear dia1.m, guardarlo, correrlo con F5 |
| 90–115 | Ejercicio 1 | Pasos 1 y 2 juntos; el 3 solos |
| 115–120 | Cierre | La 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.datareal y susize: son los mismos corchetes. - Si alguien escribe
t = 0:2esperando 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?
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
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.
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:
| Símbolo | Qué es | En la corrida de arriba |
|---|---|---|
| A | Amplitud del componente en la ventana de medición | 6,30 µV |
| σ_ensayo | Ruido de un ensayo, medido como sd de la línea base | 25 µV |
| n | Ensayos promediados | 1, 2, 10, 40 |
| σ_promedio | Ruido que sobrevive al promedio | 28,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.
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))
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.
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.
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.
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
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.
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
| Min | Bloque | Qué hace el grupo |
|---|---|---|
| 0–10 | Repaso activo | Reconstruir la corrección de línea base sin mirar el día 1 |
| 10–35 | Simulación de 40 ensayos | Escribirla completa; que cada quien vea su propia tabla |
| 35–50 | De la tabla a la fórmula | Comparar 3,83 con 25/√40 antes de que aparezca la letra σ |
| 50–70 | Bucle, condicional, función | Escribir promediar_n.m y probarla con un n imposible |
| 70–110 | Ejercicio 2 | Pasos 1 y 2 guiados; paso 3 en parejas |
| 110–120 | Cierre | La 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.
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
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:
| Campo | Qué es | Forma |
|---|---|---|
EEG.data | Los voltajes, en µV | canales × tiempo (continuo) canales × tiempo × épocas |
EEG.srate | Frecuencia de muestreo en Hz | escalar |
EEG.pnts | Muestras por época (o totales) | escalar |
EEG.trials | Número de épocas; 1 si es continuo | escalar |
EEG.times | Eje de tiempo en ms | 1 × pnts |
EEG.chanlocs | Un elemento por electrodo, con .labels | struct array |
EEG.event | Un elemento por evento, con .type y .latency | struct array |
EEG.EVENTLIST | Lo que agrega ERPLAB: bins y flags | struct |
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
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.
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
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.
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.
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);
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.
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.
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
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
| Min | Bloque | Qué hace el grupo |
|---|---|---|
| 0–10 | Repaso activo | ¿Por qué 4× ensayos = 2× SNR? Que lo diga alguien en voz alta |
| 10–30 | EEG de juguete | Construirlo campo por campo y mirar whos EEG |
| 30–45 | Los campos que importan | Abrir un .set real y buscar los mismos campos |
| 45–60 | Struct arrays y unidades | Conteo de códigos y conversión de latencias |
| 60–75 | eegh | Cargar un archivo con el menú y pedirle a EEGLAB el comando |
| 75–110 | Ejercicio 3 | Pasos 1 y 2 juntos; el 3 solos, con datos reales si ya los tienen |
| 110–120 | Cierre | La 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 quechanlocsson 33 estructuras y no una, van a copiar la línea sin poder adaptarla. Vale la pena escribirEEG.chanlocs(1)yEEG.chanlocs(2)por separado en la pantalla. - Respuesta que delata un malentendido: «
codigos == 211daría error». La mayoría espera un error donde hay un cero silencioso, y ésa es la distinción que hay que instalar hoy. eeghes 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?
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
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:
| Símbolo | Qué es | En el ejemplo |
|---|---|---|
| x[k] | La señal de entrada en la muestra k | 2, 8, 3, 9, … |
| b | Los pesos del núcleo | 1/3, 1/3, 1/3 |
| y[k] | La salida en la muestra k | 4,33, 6,67, … |
Con más pesos la lista se hace larga, así que se escribe con una suma:
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
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
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
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.
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.
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));
Un 7,7% de amplitud perdida con el filtro más suave que se recomienda. No es cero, y nunca lo es.
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)')
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.
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.
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
| Min | Bloque | Qué hace el grupo |
|---|---|---|
| 0–15 | Ocho números | Hacer la media móvil a mano en papel antes de escribir el bucle |
| 15–35 | De los números al formalismo | Escribir la suma con símbolos a partir del bucle propio |
| 35–50 | Respuesta en frecuencia | Correr el bloque de ganancias y explicar qué mide cada número |
| 50–75 | El filtro que inventa un componente | Correr la demo y graficar entrada y salida superpuestas |
| 75–95 | Filtrado y referencia en ERPLAB | Traducir la demo a pop_basicfilter |
| 95–115 | Ejercicio 4 | Pasos 1 y 2 guiados; el 3 queda de tarea si falta tiempo |
| 115–120 | Cierre | La 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?
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.
| Etapa | Función | Qué deja |
|---|---|---|
| 1. EventList | pop_creabasiceventlist | Una tabla ordenada de eventos dentro del dataset |
| 2. Asignar bins | pop_binlister | Cada evento etiquetado con la condición a la que pertenece |
| 3. Epocar | pop_epochbin | Datos de 2D a 3D: canales × tiempo × épocas |
| 4. Artefactos | pop_artmwppth | Épocas marcadas con una bandera, no borradas |
| 5. Promediar | pop_averager | Un 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ódigo | Qué marca | Los dígitos |
|---|---|---|
| 111 · 112 | Prime de un par relacionado | 1 = prime · 1 = relacionado · lista 1 o 2 |
| 121 · 122 | Prime de un par no relacionado | 1 = prime · 2 = no relacionado · lista 1 o 2 |
| 211 · 212 | Target relacionado | 2 = target · 1 = relacionado · lista 1 o 2 |
| 221 · 222 | Target no relacionado | 2 = target · 2 = no relacionado · lista 1 o 2 |
| 201 | Respuesta correcta | — |
| 202 | Respuesta 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.
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.
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.
Es el ejercicio central de la semana: un registro continuo entra, un
.erp sale, y tú puedes explicar cada paso del camino.
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.
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
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.
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 coninventario.- 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
| Min | Bloque | Qué hace el grupo |
|---|---|---|
| 0–10 | Repaso activo | Reconstruir de memoria qué hace un filtro paso alto agresivo |
| 10–25 | Las cinco etapas | Dibujarlas en papel, con qué entra y qué sale de cada una |
| 25–45 | Códigos y bin descriptor file | Leer el bin 3 en voz alta: «alinea en 211 o 212 y exige un 201 después» |
| 45–80 | El pipeline línea por línea | Correrlo sobre un sujeto, deteniéndose en cada etapa a mirar size |
| 80–105 | Ejercicio 5 | Paso 3, con la comparación de umbrales si alcanzan |
| 105–120 | Revisar y cerrar | pop_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
.setque 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?
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 ensayo | Pico medido | Amplitud media |
|---|---|---|
| 25 µV | 16,8 | 6,29 |
| 50 µV | 26,5 | 6,34 |
| El valor verdadero | 8,0 | 6,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.
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.
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
.mversionado, 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.
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' });
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
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?
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.
- 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
iCPzqueda 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
| Min | Bloque | Qué hace el grupo |
|---|---|---|
| 0–10 | Repaso activo | Nombrar las cinco etapas del día 5 sin mirar |
| 10–35 | El bucle sobre sujetos | Convertir el script del día 5 en función y llamarlo en bucle |
| 35–50 | Diferencias y gran promedio | Agregar el bin 5 y volver a correr los cinco |
| 50–70 | Qué número medir | Correr la simulación de pico contra media y comparar tablas |
| 70–95 | Ejercicio final | Medir, exportar, abrir el archivo en R o JASP |
| 95–110 | El párrafo de métodos | Escribirlo leyendo el propio script, en silencio |
| 110–120 | Chequeo final | Los 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?
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.
- Escribir, de memoria, la corrección de línea base de una época: tres líneas.
- 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.
- Nombrar las cinco etapas del pipeline de ERPLAB en orden y decir qué se rompe si intercambias dos.
- Dar un valor de un parámetro de filtrado que pueda fabricar un componente que no existe, y explicar por dónde aparece.
- Escribir el párrafo de métodos de tu propio análisis leyendo únicamente tu script.
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.
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 dice | Lo 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.
Chuleta
Todo lo que aparece en la semana, en un solo lugar.
MATLAB base
| Comando | Para qué |
|---|---|
a : b : c | Vector desde a hasta c en pasos de b |
linspace(a,b,n) | n valores repartidos entre a y b |
zeros(m,n) · numel · size | Preasignar; 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 · strcmp | Buscar posiciones; valores distintos; comparar texto |
fprintf · sprintf · disp | Imprimir con formato; formar texto; mostrar |
fullfile · dir · exist · mkdir | Rutas portables; listar carpeta; ¿existe?; crearla |
fopen · fprintf(fid,…) · fclose · type | Escribir un archivo de texto desde el script; mirarlo sin salir de MATLAB |
isempty · isnumeric · isfield · cellfun | Preguntar por lo que llegó antes de usarlo |
rng(42) · randn | Semilla; ruido gaussiano |
try / catch · error · warning | Que un fallo no detenga el lote |
whos · class · which -all | Qué hay en memoria; de qué tipo; de dónde sale |
EEGLAB y ERPLAB
| Función | Para qué |
|---|---|
eeglab · eegh | Arrancar; ver el comando de lo que hiciste con el mouse |
pop_loadset · pop_saveset | Abrir y guardar datasets .set |
pop_chanedit | Cargar coordenadas de electrodos |
pop_basicfilter · pop_filterp | Filtrar el EEG · filtrar el ERP |
pop_eegchanoperator | Rereferenciar, crear canales bipolares |
pop_creabasiceventlist | Etapa 1: construir el EventList |
pop_binlister | Etapa 2: aplicar el bin descriptor file |
pop_epochbin | Etapa 3: epocar por bin, con línea base |
pop_artmwppth · pop_artstep | Etapa 4: ventana móvil pico a pico · escalón |
pop_summary_AR_eeg_detection | Porcentaje de rechazo por bin |
pop_averager · pop_savemyerp | Etapa 5: promediar y guardar el .erp |
pop_binoperator | Ondas de diferencia entre bins |
pop_loaderp | Cargar varios .erp y llenar ALLERP |
pop_gaverager | Gran promedio, desde una lista en .txt |
pop_geterpvalues | Medir amplitudes y latencias, exportar |
pop_ploterps · pop_eegplot | Ver los ERP · revisar las épocas marcadas |