beta3 commited on
Commit
a7719c0
·
verified ·
1 Parent(s): 09778c4

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +630 -38
src/streamlit_app.py CHANGED
@@ -1,40 +1,632 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
4
  import streamlit as st
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
-
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
-
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
 
 
 
1
  import streamlit as st
2
+ import pandas as pd
3
+ import numpy as np
4
+ import plotly.graph_objects as go
5
+ from plotly.subplots import make_subplots
6
+ import mne
7
+ from pathlib import Path
8
+ import zipfile
9
+ import os
10
+
11
+ st.set_page_config(
12
+ page_title="EEG Mental Arithmetic Explorer",
13
+ page_icon="🧠",
14
+ layout="wide",
15
+ initial_sidebar_state="expanded"
16
+ )
17
+
18
+ st.markdown("""
19
+ <style>
20
+ /* Main header styling */
21
+ .main-header {
22
+ font-size: 2.8rem;
23
+ font-weight: 700;
24
+ text-align: center;
25
+ color: #1e3a8a;
26
+ margin-bottom: 0.5rem;
27
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
28
+ }
29
+
30
+ .sub-header {
31
+ text-align: center;
32
+ color: #64748b;
33
+ font-size: 1.1rem;
34
+ margin-bottom: 2.5rem;
35
+ font-weight: 400;
36
+ }
37
+
38
+ /* Sidebar styling */
39
+ [data-testid="stSidebar"] {
40
+ background-color: #1e293b;
41
+ }
42
+
43
+ [data-testid="stSidebar"] [data-testid="stMarkdownContainer"] p {
44
+ color: #e2e8f0;
45
+ }
46
+
47
+ [data-testid="stSidebar"] h1,
48
+ [data-testid="stSidebar"] h2,
49
+ [data-testid="stSidebar"] h3 {
50
+ color: #f1f5f9;
51
+ }
52
+
53
+ /* Sidebar selectbox and radio buttons */
54
+ [data-testid="stSidebar"] .stSelectbox label,
55
+ [data-testid="stSidebar"] .stRadio label {
56
+ color: #f1f5f9 !important;
57
+ font-weight: 500;
58
+ }
59
+
60
+ /* Dropdown menu background */
61
+ [data-testid="stSidebar"] [data-baseweb="select"] > div {
62
+ background-color: #334155;
63
+ color: #f1f5f9;
64
+ }
65
+
66
+ /* Radio button text */
67
+ [data-testid="stSidebar"] [data-baseweb="radio"] label {
68
+ color: #e2e8f0;
69
+ }
70
+
71
+ /* Success and info boxes in sidebar */
72
+ [data-testid="stSidebar"] .stAlert {
73
+ background-color: #334155;
74
+ color: #e2e8f0;
75
+ }
76
+
77
+ /* Tabs styling */
78
+ .stTabs [data-baseweb="tab-list"] {
79
+ gap: 1rem;
80
+ background-color: #f1f5f9;
81
+ padding: 0.5rem;
82
+ border-radius: 0.5rem;
83
+ }
84
+
85
+ .stTabs [data-baseweb="tab"] {
86
+ padding: 0.75rem 1.5rem;
87
+ font-weight: 500;
88
+ border-radius: 0.375rem;
89
+ color: #334155;
90
+ }
91
+
92
+ .stTabs [data-baseweb="tab"][aria-selected="true"] {
93
+ background-color: #1e40af;
94
+ color: white;
95
+ }
96
+
97
+ /* Metric cards */
98
+ [data-testid="stMetricValue"] {
99
+ font-size: 1.75rem;
100
+ font-weight: 600;
101
+ color: #1e40af;
102
+ }
103
+
104
+ /* Info boxes */
105
+ .stAlert {
106
+ border-radius: 0.5rem;
107
+ }
108
+
109
+ /* Section headers */
110
+ h3 {
111
+ color: #1e40af;
112
+ font-weight: 600;
113
+ margin-top: 1.5rem;
114
+ margin-bottom: 1rem;
115
+ border-bottom: 2px solid #e2e8f0;
116
+ padding-bottom: 0.5rem;
117
+ }
118
+
119
+ /* Dataframe styling */
120
+ [data-testid="stDataFrame"] {
121
+ border-radius: 0.5rem;
122
+ }
123
+ </style>
124
+ """, unsafe_allow_html=True)
125
+
126
+
127
+ st.markdown('<p class="main-header">EEG Mental Arithmetic Explorer</p>', unsafe_allow_html=True)
128
+ st.markdown('<p class="sub-header">Cognitive Workload Assessment through Brain Activity Analysis</p>', unsafe_allow_html=True)
129
+
130
+ # Data paths - Root level structure
131
+ ZIP_FILE_PATH = "edf_files.zip"
132
+ EDF_EXTRACT_PATH = "edf_extracted"
133
+
134
+ # Uncompress EDF files if needed
135
+ @st.cache_resource
136
+ def extract_edf_files():
137
+ """Extract EDF files from ZIP if not already extracted"""
138
+ if not os.path.exists(EDF_EXTRACT_PATH):
139
+ if os.path.exists(ZIP_FILE_PATH):
140
+ with st.spinner("Extracting EDF files... This may take a moment."):
141
+ os.makedirs(EDF_EXTRACT_PATH, exist_ok=True)
142
+ with zipfile.ZipFile(ZIP_FILE_PATH, 'r') as zip_ref:
143
+ file_list = zip_ref.namelist()
144
+ for file in file_list:
145
+ if file.endswith('.edf') and not file.startswith('__MACOSX'):
146
+ # Extract to root of EDF_EXTRACT_PATH, removing any subdirectories
147
+ filename = os.path.basename(file)
148
+ target_path = os.path.join(EDF_EXTRACT_PATH, filename)
149
+ if not os.path.exists(target_path):
150
+ with zip_ref.open(file) as source, open(target_path, 'wb') as target:
151
+ target.write(source.read())
152
+ return True
153
+ else:
154
+ return False
155
+ return True
156
+
157
+ extraction_success = extract_edf_files()
158
+
159
+ if not extraction_success:
160
+ st.error(f"Could not find {ZIP_FILE_PATH}")
161
+ st.info("""
162
+ Expected structure:
163
+ ```
164
+ space/
165
+ ├── app.py
166
+ ├── requirements.txt
167
+ ├── README.md
168
+ └── edf_files.zip
169
+ ```
170
+ """)
171
+ st.stop()
172
+
173
+ def get_available_subjects():
174
+ """Get list of available subjects from EDF files"""
175
+ edf_files = list_available_files()
176
+ subjects = set()
177
+ for f in edf_files:
178
+ # Extract subject ID from filename (e.g., Subject01_1.edf -> Subject01)
179
+ name = f.stem
180
+ if '_' in name:
181
+ subject_id = name.split('_')[0]
182
+ subjects.add(subject_id)
183
+ return sorted(list(subjects))
184
+
185
+ def list_available_files():
186
+ """List available EDF files in extracted directory"""
187
+ if not os.path.exists(EDF_EXTRACT_PATH):
188
+ return []
189
+ # Get only .edf files directly in the extract path (no subdirectories)
190
+ edf_files = [f for f in Path(EDF_EXTRACT_PATH).glob("*.edf")]
191
+ return edf_files
192
+
193
+ @st.cache_resource
194
+ def load_edf_data(subject_id, suffix):
195
+ """Load EDF EEG data from extracted files"""
196
+ # Direct path in extracted directory
197
+ file_path = f"{EDF_EXTRACT_PATH}/{subject_id}{suffix}.edf"
198
+
199
+ if not os.path.exists(file_path):
200
+ # List available files for debugging
201
+ available_files = list(Path(EDF_EXTRACT_PATH).glob("*.edf"))
202
+ available_names = sorted([f.name for f in available_files])
203
+ raise FileNotFoundError(
204
+ f"Could not find: {subject_id}{suffix}.edf\n"
205
+ f"Available files ({len(available_names)}): {available_names[:10]}"
206
+ )
207
+
208
+ try:
209
+ # Load EDF with verbose to see any warnings
210
+ raw = mne.io.read_raw_edf(file_path, preload=True, verbose=True)
211
+
212
+ # Get data in Volts (MNE returns data in Volts by default)
213
+ data = raw.get_data() # Shape: (n_channels, n_samples)
214
+
215
+ # Convert to microvolts
216
+ data_uv = data * 1e6
217
+
218
+ channels = raw.ch_names
219
+ sfreq = raw.info['sfreq']
220
+ n_samples = data.shape[1]
221
+ time = np.arange(n_samples) / sfreq
222
+
223
+ # Create DataFrame with microvolts
224
+ df = pd.DataFrame(data_uv.T, columns=channels)
225
+ df.insert(0, 'time', time)
226
+
227
+ return df, sfreq, channels, file_path
228
+ except Exception as e:
229
+ raise Exception(f"Error loading EDF file {file_path}: {e}")
230
+
231
+ def list_available_files():
232
+ """List available EDF files in extracted directory"""
233
+ if not os.path.exists(EDF_EXTRACT_PATH):
234
+ return []
235
+ # Get only .edf files directly in the extract path (no subdirectories)
236
+ edf_files = [f for f in Path(EDF_EXTRACT_PATH).glob("*.edf")]
237
+ return edf_files
238
+
239
+
240
+ st.sidebar.header("Dataset Controls")
241
+
242
+ # Check available files
243
+ edf_files = list_available_files()
244
+
245
+ if not edf_files:
246
+ st.error("No EDF files found after extraction!")
247
+ st.info(f"Checked directory: {EDF_EXTRACT_PATH}")
248
+ st.stop()
249
+
250
+
251
+ unique_files = len(edf_files)
252
+ st.sidebar.success(f"Found {unique_files} EDF files")
253
+
254
+ subject_ids = get_available_subjects()
255
+
256
+ if not subject_ids:
257
+ st.error("No subject files found!")
258
+ st.stop()
259
+
260
+ selected_subject = st.sidebar.selectbox(
261
+ "Select Subject",
262
+ subject_ids,
263
+ index=0
264
+ )
265
+
266
+ recording_type = st.sidebar.radio(
267
+ "Recording Type",
268
+ ["Resting State (Baseline)", "Mental Arithmetic Task"],
269
+ index=0
270
+ )
271
+
272
+ suffix = "_1" if recording_type == "Resting State (Baseline)" else "_2"
273
+
274
+ st.sidebar.markdown("---")
275
+ st.sidebar.markdown("") # Espacio adicional
276
+ st.sidebar.markdown("### Subject Information")
277
+ st.sidebar.markdown(f"**ID:** {selected_subject}")
278
+ st.sidebar.markdown(f"**Recording:** {recording_type}")
279
+
280
+ st.sidebar.markdown("") # Espacio adicional
281
+ st.sidebar.markdown("---")
282
+ st.sidebar.markdown("### Data Source")
283
+ st.sidebar.info("Data loaded from EDF files")
284
+
285
+ # Main content
286
+ tab1, tab2, tab3, tab4 = st.tabs(["Signal Viewer", "Spectral Analysis", "Statistics", "About Dataset"])
287
+
288
+ # Load data
289
+ try:
290
+ with st.spinner(f"Loading {selected_subject}{suffix}..."):
291
+ df, sfreq, channels, file_path = load_edf_data(selected_subject, suffix)
292
+
293
+ data_loaded = True
294
+ st.sidebar.success(f"Loaded: {Path(file_path).name}")
295
+
296
+ except Exception as e:
297
+ st.error(f"Error loading data: {e}")
298
+ st.info(f"Attempting to load: {selected_subject}{suffix}")
299
+ data_loaded = False
300
+
301
+ if data_loaded:
302
+
303
+ # TAB 1: Signal Viewer
304
+ with tab1:
305
+ st.markdown("### EEG Signal Visualization")
306
+
307
+ col1, col2, col3 = st.columns([2, 2, 1])
308
+
309
+ with col1:
310
+ time_range = st.slider(
311
+ "Time Window (seconds)",
312
+ min_value=0.0,
313
+ max_value=float(df['time'].max()),
314
+ value=(0.0, min(10.0, float(df['time'].max()))),
315
+ step=0.5
316
+ )
317
+
318
+ with col2:
319
+ selected_channels = st.multiselect(
320
+ "Select Channels",
321
+ channels,
322
+ default=channels[:6] if len(channels) >= 6 else channels
323
+ )
324
+
325
+ with col3:
326
+ plot_style = st.selectbox(
327
+ "Plot Style",
328
+ ["Stacked", "Overlay"]
329
+ )
330
+
331
+ if selected_channels:
332
+ # Filter data by time range
333
+ mask = (df['time'] >= time_range[0]) & (df['time'] <= time_range[1])
334
+ df_plot = df[mask]
335
+
336
+ if plot_style == "Stacked":
337
+ # Create stacked subplots
338
+ fig = make_subplots(
339
+ rows=len(selected_channels),
340
+ cols=1,
341
+ shared_xaxes=True,
342
+ vertical_spacing=0.02,
343
+ subplot_titles=selected_channels
344
+ )
345
+
346
+ for idx, channel in enumerate(selected_channels, 1):
347
+ fig.add_trace(
348
+ go.Scatter(
349
+ x=df_plot['time'],
350
+ y=df_plot[channel],
351
+ mode='lines',
352
+ name=channel,
353
+ line=dict(width=1),
354
+ showlegend=False
355
+ ),
356
+ row=idx, col=1
357
+ )
358
+
359
+ fig.update_layout(
360
+ height=150 * len(selected_channels),
361
+ showlegend=False,
362
+ hovermode='x unified'
363
+ )
364
+ fig.update_xaxes(title_text="Time (s)", row=len(selected_channels), col=1)
365
+
366
+ else: # Overlay
367
+ fig = go.Figure()
368
+
369
+ for channel in selected_channels:
370
+ fig.add_trace(
371
+ go.Scatter(
372
+ x=df_plot['time'],
373
+ y=df_plot[channel],
374
+ mode='lines',
375
+ name=channel,
376
+ line=dict(width=1)
377
+ )
378
+ )
379
+
380
+ fig.update_layout(
381
+ height=600,
382
+ xaxis_title="Time (s)",
383
+ yaxis_title="Amplitude (μV)",
384
+ hovermode='x unified',
385
+ legend=dict(
386
+ orientation="v",
387
+ yanchor="top",
388
+ y=1,
389
+ xanchor="left",
390
+ x=1.01
391
+ )
392
+ )
393
+
394
+ st.plotly_chart(fig, use_container_width=True)
395
+
396
+ # Signal metrics
397
+ st.markdown("### Signal Metrics")
398
+ metric_cols = st.columns(4)
399
+
400
+ with metric_cols[0]:
401
+ st.metric("Channels", len(selected_channels))
402
+ with metric_cols[1]:
403
+ st.metric("Sampling Rate", f"{sfreq:.0f} Hz")
404
+ with metric_cols[2]:
405
+ st.metric("Duration", f"{df['time'].max():.2f} s")
406
+ with metric_cols[3]:
407
+ st.metric("Samples", len(df_plot))
408
+ else:
409
+ st.warning("Please select at least one channel to display")
410
+
411
+ # TAB 2: Spectral Analysis
412
+ with tab2:
413
+ st.markdown("### Power Spectral Density Analysis")
414
+
415
+ col1, col2 = st.columns([3, 1])
416
+
417
+ with col2:
418
+ channel_for_psd = st.selectbox(
419
+ "Select Channel for PSD",
420
+ channels,
421
+ index=0
422
+ )
423
+
424
+ freq_bands = st.checkbox("Show Frequency Bands", value=True)
425
+
426
+ # Compute PSD
427
+ from scipy import signal
428
+
429
+ channel_data = df[channel_for_psd].values
430
+ frequencies, psd = signal.welch(channel_data, fs=sfreq, nperseg=min(256, len(channel_data)))
431
+
432
+ # Plot PSD
433
+ fig = go.Figure()
434
+
435
+ fig.add_trace(go.Scatter(
436
+ x=frequencies,
437
+ y=10 * np.log10(psd),
438
+ mode='lines',
439
+ name='PSD',
440
+ line=dict(color='steelblue', width=2)
441
+ ))
442
+
443
+ # Add frequency bands if selected
444
+ if freq_bands:
445
+ bands = {
446
+ 'Delta': (0.5, 4, 'rgba(255, 0, 0, 0.1)'),
447
+ 'Theta': (4, 8, 'rgba(255, 165, 0, 0.1)'),
448
+ 'Alpha': (8, 13, 'rgba(255, 255, 0, 0.1)'),
449
+ 'Beta': (13, 30, 'rgba(0, 255, 0, 0.1)'),
450
+ 'Gamma': (30, 50, 'rgba(0, 0, 255, 0.1)')
451
+ }
452
+
453
+ # Add colored bands
454
+ for band_name, (low, high, color) in bands.items():
455
+ fig.add_vrect(
456
+ x0=low, x1=high,
457
+ fillcolor=color,
458
+ layer="below",
459
+ line_width=0
460
+ )
461
+
462
+ # Add annotations at the top of the plot
463
+ y_max = 10 * np.log10(psd).max()
464
+ annotations = []
465
+ for band_name, (low, high, color) in bands.items():
466
+ mid_freq = (low + high) / 2
467
+ annotations.append(
468
+ dict(
469
+ x=mid_freq,
470
+ y=y_max,
471
+ text=band_name,
472
+ showarrow=False,
473
+ font=dict(size=10, color='black'),
474
+ bgcolor='rgba(255, 255, 255, 0.8)',
475
+ borderpad=4
476
+ )
477
+ )
478
+
479
+ fig.update_layout(annotations=annotations)
480
+
481
+ fig.update_layout(
482
+ height=500,
483
+ xaxis_title="Frequency (Hz)",
484
+ yaxis_title="Power Spectral Density (dB/Hz)",
485
+ hovermode='x'
486
+ )
487
+
488
+ fig.update_xaxes(range=[0, 100])
489
+
490
+ st.plotly_chart(fig, use_container_width=True)
491
+
492
+ # Band power analysis
493
+ st.markdown("### Band Power Analysis")
494
+
495
+ bands_power = {
496
+ 'Delta': (0.5, 4),
497
+ 'Theta': (4, 8),
498
+ 'Alpha': (8, 13),
499
+ 'Beta': (13, 30),
500
+ 'Gamma': (30, 50)
501
+ }
502
+
503
+ band_powers = {}
504
+ for band_name, (low, high) in bands_power.items():
505
+ mask = (frequencies >= low) & (frequencies <= high)
506
+ # Use trapezoid instead of trapz (numpy 2.0+)
507
+ band_powers[band_name] = np.trapezoid(psd[mask], frequencies[mask])
508
+
509
+ # Plot band powers
510
+ fig_bands = go.Figure(data=[
511
+ go.Bar(
512
+ x=list(band_powers.keys()),
513
+ y=list(band_powers.values()),
514
+ marker_color=['#ff6b6b', '#ffa500', '#ffff00', '#90ee90', '#6495ed']
515
+ )
516
+ ])
517
+
518
+ fig_bands.update_layout(
519
+ height=400,
520
+ xaxis_title="Frequency Band",
521
+ yaxis_title="Absolute Power",
522
+ showlegend=False
523
+ )
524
+
525
+ st.plotly_chart(fig_bands, use_container_width=True)
526
+
527
+ # TAB 3: Statistics
528
+ with tab3:
529
+ st.markdown("### Statistical Analysis")
530
+
531
+ # Channel statistics table
532
+ stats_data = []
533
+ for channel in channels:
534
+ channel_series = df[channel]
535
+ mean_val = float(channel_series.mean())
536
+ std_val = float(channel_series.std())
537
+ min_val = float(channel_series.min())
538
+ max_val = float(channel_series.max())
539
+
540
+ stats_data.append({
541
+ 'Channel': channel,
542
+ 'Mean (μV)': mean_val,
543
+ 'Std (μV)': std_val,
544
+ 'Min (μV)': min_val,
545
+ 'Max (μV)': max_val,
546
+ 'Range (μV)': max_val - min_val
547
+ })
548
+
549
+ stats_df = pd.DataFrame(stats_data)
550
+
551
+ # Format numeric columns to 2 decimals
552
+ numeric_cols = ['Mean (μV)', 'Std (μV)', 'Min (μV)', 'Max (μV)', 'Range (μV)']
553
+ for col in numeric_cols:
554
+ stats_df[col] = stats_df[col].apply(lambda x: f"{x:.2f}")
555
+
556
+ st.dataframe(stats_df, height=400)
557
+
558
+ # Correlation heatmap
559
+ st.markdown("### Channel Correlation Matrix")
560
+
561
+ corr_matrix = df[channels].corr()
562
+
563
+ fig_corr = go.Figure(data=go.Heatmap(
564
+ z=corr_matrix.values,
565
+ x=channels,
566
+ y=channels,
567
+ colorscale='RdBu',
568
+ zmid=0,
569
+ text=corr_matrix.values,
570
+ texttemplate='%{text:.2f}',
571
+ textfont={"size": 8},
572
+ colorbar=dict(title="Correlation")
573
+ ))
574
+
575
+ fig_corr.update_layout(
576
+ height=750,
577
+ title="Channel Correlation Matrix"
578
+ )
579
+
580
+ st.plotly_chart(fig_corr, use_container_width=True)
581
+
582
+ # TAB 4: About
583
+ with tab4:
584
+ st.markdown("""
585
+ ### About This Dataset
586
+
587
+ This dataset contains EEG recordings from 36 healthy participants during resting state
588
+ and mental arithmetic task performance.
589
+
590
+ #### Key Features
591
+ - **Participants**: 36 healthy subjects
592
+ - **Recordings**: Paired (resting state + task)
593
+ - **Channels**: 23 EEG channels (International 10/20 system)
594
+ - **Duration**: 60 seconds per recording
595
+ - **Sampling Rate**: Approximately 500 Hz
596
+ - **Task**: Serial subtraction (4-digit minus 2-digit numbers)
597
+
598
+ #### Subject Groups
599
+ - **Good Performers** (24 subjects): Mean 21 operations in 4 minutes
600
+ - **Poor Performers** (12 subjects): Mean 7 operations in 4 minutes
601
+
602
+ #### Preprocessing
603
+ - High-pass filter at 30 Hz
604
+ - Notch filter at 50 Hz
605
+ - ICA artifact removal (eyes, muscles, cardiac)
606
+
607
+ #### Citation
608
+ ```
609
+ Zyma I, Tukaev S, Seleznov I, Kiyono K, Popov A, Chernykh M, Shpenkov O.
610
+ Electroencephalograms during Mental Arithmetic Task Performance.
611
+ Data. 2019; 4(1):14.
612
+ https://doi.org/10.3390/data4010014
613
+ ```
614
+
615
+ #### Resources
616
+ - [PhysioNet Dataset](https://physionet.org/content/eegmat/1.0.0/)
617
+ - [Original Paper](https://doi.org/10.3390/data4010014)
618
+ - [Hugging Face Dataset](https://huggingface.co/datasets/BrainSpectralAnalytics/eeg-mental-arithmetic)
619
+
620
+ #### Contact
621
+ Ivan Seleznov: ivan.seleznov1@gmail.com
622
+ """)
623
+
624
+ else:
625
+ st.warning("Unable to load data. Please check the selected subject and recording type.")
626
 
627
+ # Footer
628
+ st.markdown("---")
629
+ st.markdown(
630
+ '<p style="text-align: center; color: #94a3b8; font-size: 0.9rem;">Built with Streamlit | EEG Mental Arithmetic Dataset Explorer</p>',
631
+ unsafe_allow_html=True
632
+ )