BIQL Tutorial

Open In Colab

This tutorial demonstrates how to use the BIDS Query Language (BIQL) to query neuroimaging datasets. The examples below are automatically executed and updated whenever the documentation is built.

Want to run this interactively? Click the “Open in Colab” button above to run and modify the code yourself!

Welcome to the BIQL (BIDS Query Language) tutorial! This guide will walk you through using BIQL to query BIDS neuroimaging datasets. We’ll start with basic queries and progressively explore more advanced features.

What is BIQL?

BIQL is a SQL-like query language designed specifically for querying Brain Imaging Data Structure (BIDS) datasets. It allows you to:

  • Search for specific files based on BIDS entities (subject, session, task, etc.)
  • Filter data using metadata from JSON sidecars
  • Access participant information from participants.tsv
  • Perform aggregations and grouping operations
  • Export results in various formats

Prerequisites

First, let’s set up our environment and get the example data:

import tempfile
from pathlib import Path
import sys

# Install BIQL if running in Colab
if 'google.colab' in sys.modules:
    !pip install git+https://github.com/astewartau/biql.git > /dev/null 2>&1
from biql import create_query_engine

# Set up paths - use a temporary directory that works in different environments
bids_examples_dir = Path(tempfile.gettempdir()) / "bids-examples"

# Clone bids-examples if it doesn't exist
if not bids_examples_dir.exists():
    !git clone https://github.com/bids-standard/bids-examples.git {bids_examples_dir} > /dev/null 2>&1

Part 1: Basic Queries

Let’s start with the synthetic dataset from bids-examples. This is a simple dataset that’s perfect for learning BIQL basics.

dataset_path = bids_examples_dir / "synthetic"
q = create_query_engine(dataset_path)
q.dataset_stats()
{'total_files': 60,
 'total_subjects': 5,
 'files_by_datatype': {'anat': 10, 'func': 30, 'beh': 5},
 'subjects': ['01', '02', '03', '04', '05'],
 'datatypes': ['anat', 'beh', 'func']}

Simple Entity Queries

The most basic BIQL queries filter files by BIDS entities. You can query by any BIDS entity that appears in your filenames:

q.run_query("sub=01", format="dataframe").head(5)
filepath relative_path filename sub ses suffix datatype extension metadata participants task run
0 /tmp/bids-examples/synthetic/sub-01/ses-02/ana... sub-01/ses-02/anat/sub-01_ses-02_T1w.nii sub-01_ses-02_T1w.nii 01 02 T1w anat .nii {} age=34; sex=F NaN NaN
1 /tmp/bids-examples/synthetic/sub-01/ses-02/fun... sub-01/ses-02/func/sub-01_ses-02_task-nback_ru... sub-01_ses-02_task-nback_run-02_bold.nii 01 02 bold func .nii {} age=34; sex=F nback 02
2 /tmp/bids-examples/synthetic/sub-01/ses-02/fun... sub-01/ses-02/func/sub-01_ses-02_task-nback_ru... sub-01_ses-02_task-nback_run-01_bold.nii 01 02 bold func .nii {} age=34; sex=F nback 01
3 /tmp/bids-examples/synthetic/sub-01/ses-02/fun... sub-01/ses-02/func/sub-01_ses-02_task-rest_bol... sub-01_ses-02_task-rest_bold.nii 01 02 bold func .nii {} age=34; sex=F rest NaN
4 /tmp/bids-examples/synthetic/sub-01/ses-01/ana... sub-01/ses-01/anat/sub-01_ses-01_T1w.nii sub-01_ses-01_T1w.nii 01 01 T1w anat .nii {} age=34; sex=F NaN NaN
results = q.run_query("datatype=func")
len(results)  # Number of functional files
30
q.run_query("SELECT DISTINCT task WHERE datatype=func", format="dataframe")
task
0 nback
1 rest

Combining Conditions

You can combine multiple conditions using AND, OR, and NOT operators:

q.run_query("datatype=anat AND suffix=T1w", format="dataframe").head(5)
filepath relative_path filename sub ses suffix datatype extension metadata participants
0 /tmp/bids-examples/synthetic/sub-01/ses-02/ana... sub-01/ses-02/anat/sub-01_ses-02_T1w.nii sub-01_ses-02_T1w.nii 01 02 T1w anat .nii {} age=34; sex=F
1 /tmp/bids-examples/synthetic/sub-01/ses-01/ana... sub-01/ses-01/anat/sub-01_ses-01_T1w.nii sub-01_ses-01_T1w.nii 01 01 T1w anat .nii {} age=34; sex=F
2 /tmp/bids-examples/synthetic/sub-04/ses-02/ana... sub-04/ses-02/anat/sub-04_ses-02_T1w.nii sub-04_ses-02_T1w.nii 04 02 T1w anat .nii {} age=21; sex=F
3 /tmp/bids-examples/synthetic/sub-04/ses-01/ana... sub-04/ses-01/anat/sub-04_ses-01_T1w.nii sub-04_ses-01_T1w.nii 04 01 T1w anat .nii {} age=21; sex=F
4 /tmp/bids-examples/synthetic/sub-05/ses-02/ana... sub-05/ses-02/anat/sub-05_ses-02_T1w.nii sub-05_ses-02_T1w.nii 05 02 T1w anat .nii {} age=42; sex=M
q.run_query("task=nback OR task=rest", format="dataframe").head(5)
filepath relative_path filename sub ses task run suffix datatype extension metadata participants
0 /tmp/bids-examples/synthetic/sub-01/ses-02/fun... sub-01/ses-02/func/sub-01_ses-02_task-nback_ru... sub-01_ses-02_task-nback_run-02_bold.nii 01 02 nback 02 bold func .nii {} age=34; sex=F
1 /tmp/bids-examples/synthetic/sub-01/ses-02/fun... sub-01/ses-02/func/sub-01_ses-02_task-nback_ru... sub-01_ses-02_task-nback_run-01_bold.nii 01 02 nback 01 bold func .nii {} age=34; sex=F
2 /tmp/bids-examples/synthetic/sub-01/ses-02/fun... sub-01/ses-02/func/sub-01_ses-02_task-rest_bol... sub-01_ses-02_task-rest_bold.nii 01 02 rest NaN bold func .nii {} age=34; sex=F
3 /tmp/bids-examples/synthetic/sub-01/ses-01/fun... sub-01/ses-01/func/sub-01_ses-01_task-nback_ru... sub-01_ses-01_task-nback_run-02_bold.nii 01 01 nback 02 bold func .nii {} age=34; sex=F
4 /tmp/bids-examples/synthetic/sub-01/ses-01/fun... sub-01/ses-01/func/sub-01_ses-01_task-rest_bol... sub-01_ses-01_task-rest_bold.nii 01 01 rest NaN bold func .nii {} age=34; sex=F

Using WHERE Clause

For more SQL-like queries, you can use the WHERE clause:

q.run_query("WHERE sub=01 AND datatype=func", format="dataframe")
filepath relative_path filename sub ses task run suffix datatype extension metadata participants
0 /tmp/bids-examples/synthetic/sub-01/ses-02/fun... sub-01/ses-02/func/sub-01_ses-02_task-nback_ru... sub-01_ses-02_task-nback_run-02_bold.nii 01 02 nback 02 bold func .nii {} age=34; sex=F
1 /tmp/bids-examples/synthetic/sub-01/ses-02/fun... sub-01/ses-02/func/sub-01_ses-02_task-nback_ru... sub-01_ses-02_task-nback_run-01_bold.nii 01 02 nback 01 bold func .nii {} age=34; sex=F
2 /tmp/bids-examples/synthetic/sub-01/ses-02/fun... sub-01/ses-02/func/sub-01_ses-02_task-rest_bol... sub-01_ses-02_task-rest_bold.nii 01 02 rest NaN bold func .nii {} age=34; sex=F
3 /tmp/bids-examples/synthetic/sub-01/ses-01/fun... sub-01/ses-01/func/sub-01_ses-01_task-nback_ru... sub-01_ses-01_task-nback_run-02_bold.nii 01 01 nback 02 bold func .nii {} age=34; sex=F
4 /tmp/bids-examples/synthetic/sub-01/ses-01/fun... sub-01/ses-01/func/sub-01_ses-01_task-rest_bol... sub-01_ses-01_task-rest_bold.nii 01 01 rest NaN bold func .nii {} age=34; sex=F
5 /tmp/bids-examples/synthetic/sub-01/ses-01/fun... sub-01/ses-01/func/sub-01_ses-01_task-nback_ru... sub-01_ses-01_task-nback_run-01_bold.nii 01 01 nback 01 bold func .nii {} age=34; sex=F

Part 2: SELECT Clause and Field Selection

By default, BIQL returns all available fields. Use SELECT to choose specific fields:

q.run_query(
    "SELECT sub, task, run, filename WHERE datatype=func",
    format="dataframe"
).head(5)
sub task run filename
0 01 nback 02 sub-01_ses-02_task-nback_run-02_bold.nii
1 01 nback 01 sub-01_ses-02_task-nback_run-01_bold.nii
2 01 rest None sub-01_ses-02_task-rest_bold.nii
3 01 nback 02 sub-01_ses-01_task-nback_run-02_bold.nii
4 01 rest None sub-01_ses-01_task-rest_bold.nii
q.run_query(
    "SELECT sub, relative_path WHERE suffix=T1w",
    format="dataframe"
)
sub relative_path
0 01 sub-01/ses-02/anat/sub-01_ses-02_T1w.nii
1 01 sub-01/ses-01/anat/sub-01_ses-01_T1w.nii
2 04 sub-04/ses-02/anat/sub-04_ses-02_T1w.nii
3 04 sub-04/ses-01/anat/sub-04_ses-01_T1w.nii
4 05 sub-05/ses-02/anat/sub-05_ses-02_T1w.nii
5 05 sub-05/ses-01/anat/sub-05_ses-01_T1w.nii
6 02 sub-02/ses-02/anat/sub-02_ses-02_T1w.nii
7 02 sub-02/ses-01/anat/sub-02_ses-01_T1w.nii
8 03 sub-03/ses-02/anat/sub-03_ses-02_T1w.nii
9 03 sub-03/ses-01/anat/sub-03_ses-01_T1w.nii

Part 3: Pattern Matching

BIQL supports wildcards and regular expressions for flexible matching:

results = q.run_query("suffix=*bold*")
len(results)  # Count of files with 'bold' in suffix
30
q.run_query(
    "SELECT DISTINCT task WHERE task~=\".*back*\"",
    format="dataframe"
)
task
0 nback

Part 4: Ranges and Lists

BIQL supports convenient syntax for matching multiple values and ranges:

List Matching with IN

Use IN to match any value from a list:

# Find files for specific subjects
q.run_query(
    "SELECT sub, task, filename WHERE sub IN ['01', '02', '03'] AND datatype=func",
    format="dataframe"
).head()
sub task filename
0 01 nback sub-01_ses-02_task-nback_run-02_bold.nii
1 01 nback sub-01_ses-02_task-nback_run-01_bold.nii
2 01 rest sub-01_ses-02_task-rest_bold.nii
3 01 nback sub-01_ses-01_task-nback_run-02_bold.nii
4 01 rest sub-01_ses-01_task-rest_bold.nii
# Find specific tasks
q.run_query(
    "SELECT DISTINCT sub WHERE task IN ['nback', 'rest']",
    format="dataframe"
)
sub
0 01
1 04
2 05
3 02
4 03
# Combining lists and other conditions  
q.run_query(
    "SELECT sub, COUNT(*) as file_count "
    "WHERE sub IN ['01', '02'] AND task IN ['nback', 'rest'] "
    "GROUP BY sub",
    format="dataframe"
)
sub file_count
0 01 6
1 02 6

Range Matching

Use [start:end] syntax for numeric ranges (inclusive):

# Find runs 1 and 2 (inclusive range)
q.run_query(
    "SELECT sub, task, run WHERE run=[1:2] AND datatype=func",
    format="dataframe"
)
sub task run
0 01 nback 02
1 01 nback 01
2 01 nback 02
3 01 nback 01
4 04 nback 02
5 04 nback 01
6 04 nback 02
7 04 nback 01
8 05 nback 02
9 05 nback 01
10 05 nback 02
11 05 nback 01
12 02 nback 02
13 02 nback 01
14 02 nback 02
15 02 nback 01
16 03 nback 01
17 03 nback 02
18 03 nback 02
19 03 nback 01

Part 5: Grouping and Aggregation

BIQL supports SQL-like grouping and aggregation functions:

q.run_query("SELECT sub, COUNT(*) GROUP BY sub", format="dataframe")
sub count
0 01 12
1 04 12
2 05 12
3 02 12
4 03 12
q.run_query(
    "SELECT sub, datatype, COUNT(*) GROUP BY sub, datatype",
    format="json"
)
[{'sub': '01', 'datatype': 'anat', 'count': 2},
 {'sub': '01', 'datatype': 'func', 'count': 6},
 {'sub': '04', 'datatype': 'anat', 'count': 2},
 {'sub': '04', 'datatype': 'func', 'count': 6},
 {'sub': '05', 'datatype': 'anat', 'count': 2},
 {'sub': '05', 'datatype': 'func', 'count': 6},
 {'sub': '02', 'datatype': 'anat', 'count': 2},
 {'sub': '02', 'datatype': 'func', 'count': 6},
 {'sub': '03', 'datatype': 'anat', 'count': 2},
 {'sub': '03', 'datatype': 'func', 'count': 6},
 {'sub': '01', 'datatype': None, 'count': 3},
 {'sub': '01', 'datatype': 'beh', 'count': 1},
 {'sub': '04', 'datatype': None, 'count': 3},
 {'sub': '04', 'datatype': 'beh', 'count': 1},
 {'sub': '05', 'datatype': None, 'count': 3},
 {'sub': '05', 'datatype': 'beh', 'count': 1},
 {'sub': '02', 'datatype': None, 'count': 3},
 {'sub': '02', 'datatype': 'beh', 'count': 1},
 {'sub': '03', 'datatype': None, 'count': 3},
 {'sub': '03', 'datatype': 'beh', 'count': 1}]
# Compare DISTINCT vs non-DISTINCT - get all task names (including duplicates)
q.run_query(
    "SELECT sub, (task) as all_task_names, (DISTINCT task) as unique_tasks "
    "WHERE sub='01' "
    "GROUP BY sub",
    format="json"
)
[{'sub': '01',
  'all_task_names': ['nback',
   'nback',
   'nback',
   'nback',
   'rest',
   'rest',
   'stroop',
   None,
   None,
   None,
   None,
   None],
  'unique_tasks': ['nback', 'rest', 'stroop']}]
# Get unique tasks per subject
q.run_query(
    "SELECT sub, (DISTINCT task) as unique_tasks, COUNT(*) as total_files "
    "WHERE sub IN ['01', '02', '03'] "
    "GROUP BY sub",
    format="json"
)
[{'sub': '01', 'unique_tasks': ['nback', 'rest', 'stroop'], 'total_files': 12},
 {'sub': '02', 'unique_tasks': ['nback', 'rest', 'stroop'], 'total_files': 12},
 {'sub': '03', 'unique_tasks': ['nback', 'rest', 'stroop'], 'total_files': 12}]

Array Aggregation with DISTINCT

BIQL supports collecting values into arrays using the (field) syntax:

  • (DISTINCT field) returns unique non-null values
  • (field) returns all values including duplicates

Part 6: Working with Metadata

BIQL can query JSON sidecar metadata using the metadata. namespace. Let’s explore a more complex dataset to see this in action:

# Switch to a dataset with more metadata  
ds2_path = bids_examples_dir / "ds000117"
q2 = create_query_engine(ds2_path) if (bids_examples_dir / "ds000117").exists() else q

# Show what metadata fields are available
q2.run_query(
    "SELECT DISTINCT task WHERE datatype=func",
    format="dataframe"
)
task
0 facerecognition
# For the synthetic dataset, we can still demonstrate basic grouping by task
q.run_query(
    "SELECT task, COUNT(*) as file_count, "
    "COUNT(DISTINCT sub) as subjects "
    "GROUP BY task",
    format="dataframe"
)
task file_count subjects
0 None 25 5
1 nback 20 5
2 rest 10 5
3 stroop 5 5

Part 7: Participant Information

Access participant demographics using the participants. namespace:

q.run_query(
    "SELECT DISTINCT sub, participants.age, participants.sex",
    format="dataframe"
)
sub participants.age participants.sex
0 01 34 F
1 04 21 F
2 05 42 M
3 02 38 M
4 03 22 M
q.run_query(
    "SELECT sub, task, participants.age WHERE participants.age > 25",
    format="dataframe"
)
sub task participants.age
0 01 None 34
1 01 nback 34
2 01 nback 34
3 01 rest 34
4 01 None 34
5 01 nback 34
6 01 rest 34
7 01 nback 34
8 05 None 42
9 05 rest 42
10 05 nback 42
11 05 nback 42
12 05 None 42
13 05 nback 42
14 05 rest 42
15 05 nback 42
16 02 None 38
17 02 rest 38
18 02 nback 38
19 02 nback 38
20 02 None 38
21 02 nback 38
22 02 nback 38
23 02 rest 38
24 01 None 34
25 01 None 34
26 01 None 34
27 01 stroop 34
28 05 None 42
29 05 None 42
30 05 None 42
31 05 stroop 42
32 02 None 38
33 02 None 38
34 02 None 38
35 02 stroop 38

Part 8: Advanced Queries

Let’s combine multiple features for more complex queries:

q.run_query("""
    SELECT sub, ses, task, COUNT(*) as n_runs
    WHERE datatype=func AND task != rest
    GROUP BY sub, ses, task
    HAVING COUNT(*) > 1
    ORDER BY sub, task
""", format="json")
[{'sub': '01', 'ses': '02', 'task': 'nback', 'n_runs': 2},
 {'sub': '01', 'ses': '01', 'task': 'nback', 'n_runs': 2},
 {'sub': '02', 'ses': '02', 'task': 'nback', 'n_runs': 2},
 {'sub': '02', 'ses': '01', 'task': 'nback', 'n_runs': 2},
 {'sub': '03', 'ses': '02', 'task': 'nback', 'n_runs': 2},
 {'sub': '03', 'ses': '01', 'task': 'nback', 'n_runs': 2},
 {'sub': '04', 'ses': '02', 'task': 'nback', 'n_runs': 2},
 {'sub': '04', 'ses': '01', 'task': 'nback', 'n_runs': 2},
 {'sub': '05', 'ses': '02', 'task': 'nback', 'n_runs': 2},
 {'sub': '05', 'ses': '01', 'task': 'nback', 'n_runs': 2}]
q.run_query("""
    SELECT sub, task,
           (filename WHERE suffix='bold') as imaging_files,
           (filename WHERE run='01') as run01_files,
           (filename WHERE run='02') as run02_files
    WHERE datatype=func
    GROUP BY sub, task
""", format="table")  # Using table format since arrays don't display well in dataframes
'| imaging_files   | run01_files     | run02_files     | sub | task  |\n| --------------- | --------------- | --------------- | --- | ----- |\n| [...4 items...] | [...2 items...] | [...2 items...] | 01  | nback |\n| [...2 items...] | [...0 items...] | [...0 items...] | 01  | rest  |\n| [...4 items...] | [...2 items...] | [...2 items...] | 04  | nback |\n| [...2 items...] | [...0 items...] | [...0 items...] | 04  | rest  |\n| [...2 items...] | [...0 items...] | [...0 items...] | 05  | rest  |\n| [...4 items...] | [...2 items...] | [...2 items...] | 05  | nback |\n| [...2 items...] | [...0 items...] | [...0 items...] | 02  | rest  |\n| [...4 items...] | [...2 items...] | [...2 items...] | 02  | nback |\n| [...4 items...] | [...2 items...] | [...2 items...] | 03  | nback |\n| [...2 items...] | [...0 items...] | [...0 items...] | 03  | rest  |'

Part 9: Output Formats

BIQL supports multiple output formats for different use cases:

sample_query = "SELECT sub, task, run WHERE datatype=func AND sub=01"

print(q.run_query(sample_query, format="table"))
| run | sub | task  |
| --- | --- | ----- |
| 02  | 01  | nback |
| 01  | 01  | nback |
|     | 01  | rest  |
| 02  | 01  | nback |
|     | 01  | rest  |
| 01  | 01  | nback |
print(q.run_query(sample_query, format="csv"))
run,sub,task
02,01,nback
01,01,nback
,01,rest
02,01,nback
,01,rest
01,01,nback
results_json = q.run_query(sample_query, format="json")
results_json[:2]  # Show first 2 entries
[{'sub': '01', 'task': 'nback', 'run': '02'},
 {'sub': '01', 'task': 'nback', 'run': '01'}]
print(q.run_query("WHERE sub=01 AND suffix=T1w", format="paths"))
/tmp/bids-examples/synthetic/sub-01/ses-01/anat/sub-01_ses-01_T1w.nii
/tmp/bids-examples/synthetic/sub-01/ses-02/anat/sub-01_ses-02_T1w.nii
q.run_query(sample_query, format="dataframe")
sub task run
0 01 nback 02
1 01 nback 01
2 01 rest None
3 01 nback 02
4 01 rest None
5 01 nback 01

Part 10: Real-World Examples

Let’s look at some practical queries you might use in neuroimaging research:

q.run_query("""
    SELECT sub, 
           COUNT(*) as total_files,
           COUNT(DISTINCT datatype) as datatypes,
           (DISTINCT datatype) as available_data
    GROUP BY sub
""", format="json")
[{'sub': '01',
  'total_files': 12,
  'datatypes': 3,
  'available_data': ['anat', 'beh', 'func']},
 {'sub': '04',
  'total_files': 12,
  'datatypes': 3,
  'available_data': ['anat', 'beh', 'func']},
 {'sub': '05',
  'total_files': 12,
  'datatypes': 3,
  'available_data': ['anat', 'beh', 'func']},
 {'sub': '02',
  'total_files': 12,
  'datatypes': 3,
  'available_data': ['anat', 'beh', 'func']},
 {'sub': '03',
  'total_files': 12,
  'datatypes': 3,
  'available_data': ['anat', 'beh', 'func']}]
q.run_query("""
    SELECT sub, ses,
           COUNT(*) as files_per_session,
           (DISTINCT task) as tasks_in_session
    GROUP BY sub, ses
""", format="json")
[{'sub': '01',
  'ses': '02',
  'files_per_session': 5,
  'tasks_in_session': ['nback', 'rest']},
 {'sub': '01',
  'ses': '01',
  'files_per_session': 6,
  'tasks_in_session': ['nback', 'rest', 'stroop']},
 {'sub': '04',
  'ses': '02',
  'files_per_session': 5,
  'tasks_in_session': ['nback', 'rest']},
 {'sub': '04',
  'ses': '01',
  'files_per_session': 6,
  'tasks_in_session': ['nback', 'rest', 'stroop']},
 {'sub': '05',
  'ses': '02',
  'files_per_session': 5,
  'tasks_in_session': ['nback', 'rest']},
 {'sub': '05',
  'ses': '01',
  'files_per_session': 6,
  'tasks_in_session': ['nback', 'rest', 'stroop']},
 {'sub': '02',
  'ses': '02',
  'files_per_session': 5,
  'tasks_in_session': ['nback', 'rest']},
 {'sub': '02',
  'ses': '01',
  'files_per_session': 6,
  'tasks_in_session': ['nback', 'rest', 'stroop']},
 {'sub': '03',
  'ses': '02',
  'files_per_session': 5,
  'tasks_in_session': ['nback', 'rest']},
 {'sub': '03',
  'ses': '01',
  'files_per_session': 6,
  'tasks_in_session': ['nback', 'rest', 'stroop']},
 {'sub': '01', 'ses': None, 'files_per_session': 1, 'tasks_in_session': []},
 {'sub': '04', 'ses': None, 'files_per_session': 1, 'tasks_in_session': []},
 {'sub': '05', 'ses': None, 'files_per_session': 1, 'tasks_in_session': []},
 {'sub': '02', 'ses': None, 'files_per_session': 1, 'tasks_in_session': []},
 {'sub': '03', 'ses': None, 'files_per_session': 1, 'tasks_in_session': []}]
q.run_query("""
    SELECT sub,
           COUNT(DISTINCT task) as unique_tasks,
           (DISTINCT task) as completed_tasks,
           COUNT(*) as total_functional_files
    WHERE datatype=func
    GROUP BY sub
    HAVING COUNT(DISTINCT task) > 1  # Subjects with multiple tasks
""", format="json")
[{'sub': '01',
  'unique_tasks': 2,
  'completed_tasks': ['nback', 'rest'],
  'total_functional_files': 6},
 {'sub': '04',
  'unique_tasks': 2,
  'completed_tasks': ['nback', 'rest'],
  'total_functional_files': 6},
 {'sub': '05',
  'unique_tasks': 2,
  'completed_tasks': ['nback', 'rest'],
  'total_functional_files': 6},
 {'sub': '02',
  'unique_tasks': 2,
  'completed_tasks': ['nback', 'rest'],
  'total_functional_files': 6},
 {'sub': '03',
  'unique_tasks': 2,
  'completed_tasks': ['nback', 'rest'],
  'total_functional_files': 6}]