Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Tutorial 1 (Introductory): Visualizing Timeseries

Authors
Affiliations
University of Oxford
University of Toronto / University of Cambridge

Now that you’ve learned the fundamentals of neuroimaging and the connectomics workflow—including concepts such as fMRI timeseries, brain parcellation, and functional connectivity (FC)—it’s time to put those ideas into practice using LEMON dataset. In this notebook, we’ll learn how to load, explore, subset, and visualize brain timeseries data using common Python libraries.

Working with Timeseries Data

In this notebook, we will:

  1. Load a CSV file from the ts/ folder inside our project directory

  2. Inspect the data

  3. Get the Schaefer atlas region names with nilearn

  4. Subset one subject

  5. Subset one brain region

  6. Plot that region’s activity over time

Below, we’ll be importing some python packages that are required for this script. Common packages are

  • numpy and pandas to deal with numbers and dataframes (i.e., tables)

  • matplotlib for data visualization

  • pathlib for managing your file paths/directories

  • nilearn for anything to do with neuroimaging data

Feel free to run the code below to “import” these packages, which is a way to let the code knows that we’ll be using these specific packages.

from pathlib import Path

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

from nilearn.datasets import fetch_atlas_schaefer_2018

1. Set Up the File Path

First, we will define the directory where this notebook is using the function or command “cwd” from the “Path” package that we have imported from above from pathlib import Path

cwd = current working directory

We’ll be calling this current working directory as your project directory.

Then, where your code is, create a folder called “ts”. Within this folder “ts”, we’ll store our timeseries file in Schaefer parcellation (schaefer100_timeseries_long.csv).

With this, try to understand what the code below is doing!

PROJECT_DIR = Path("/Users/shemrock/Library/CloudStorage/OneDrive-Nexus365/Clematis/guide/Conn_comp/competition2026/lemon2026") # assign the main directory containing your data to the PROJECT_DIR variable
# PROJECT_DIR = Path.cwd() # OR assign your directory of this code to `PROJECT_DIR` variable
TS_DIR = PROJECT_DIR / "ts" # a variable indicating where your timeseries data is (i.e., in the "ts" folder)
CSV_FILE = TS_DIR / "schaefer100_timeseries_long.csv" # a variable specifying the exact location of your timeseries file

print("Project directory:", PROJECT_DIR) # using Python in-built "print" function to print your project directory
print("Timeseries file:", CSV_FILE) # using Python in-built "print" function to print the location of your csv timeseries file
Project directory: /Users/shemrock/Library/CloudStorage/OneDrive-Nexus365/Clematis/guide/Conn_comp/competition2026/lemon2026
Timeseries file: /Users/shemrock/Library/CloudStorage/OneDrive-Nexus365/Clematis/guide/Conn_comp/competition2026/lemon2026/ts/schaefer100_timeseries_long.csv

2. Load the CSV File

Next, we need to convert (or import) the csv file into a “dataframe” format.

A DataFrame is like a spreadsheet: rows and columns are organized in a table. Dataframes make it easy for us to manipulate large datasets with many rows and columns.

We use pandas.read_csv() to import the data into a DataFrame (i.e., read_csv function from the pandas package)

If you have imported the packages and indicated your file paths correctly, you should be able to run the code below.

timeseries_df = pd.read_csv(CSV_FILE) # import our csv file into a dataframe

print("Shape of dataframe:", timeseries_df.shape) # using `shape` (from pandas) to see the dimensions/size of the dataframe + print the results
timeseries_df.head() # see the first few rows of the dataframe using `head` from pandas
Shape of dataframe: (143440, 101)
Loading...

3. Understand the Structure of the Dataframe

Now that we have imported the timeseries data from the csv into the dataframe format (stored in the timeseries_df variable), let’s try to make sense of our dataframe or the timeseries data!

Dimension

First, let’s check the shape or the dimension of the dataframe using .shape from pandas.

print("Shape of dataframe:", timeseries_df.shape) # using `shape` (from pandas) to see the dimensions/size of the dataframe + print the results
Shape of dataframe: (143440, 101)

This returns: (143440, 101). Recall that a dataframe’s shape is reported as: (number_of_rows, number_of_columns). Therefore, our dataset contains 143,440 rows and 101 columns

The 101 columns consist of:

  • 1 column containing the subject ID (subject)

  • 100 columns containing brain activity values from the Schaefer100 atlas regions

The 143,440 rows contain data from all subjects combined.

Since this dataset contains 220 subjects, each with 652 fMRI time points, we have: 220 subjects × 652 time points = 143,440 rows

In other words, this CSV file stores the complete timeseries dataset for every participant in a single table.

Previewing Your Dataframe

Timeseries data from neuroimaging is often very large. However, we can use head from pandas to see the first few rows and some columns from our dataframe.

timeseries_df.head() # see the first few rows of the dataframe using `head` from pandas
Loading...

Using head(), we can see from the above that the first column contains the subject ID, while the remaining columns contain activity values from Schaefer100 brain regions (for example, 7Networks_LH_Vis_1, 7Networks_LH_Vis_2, and so on).

Each row represents a single fMRI time point from one subject:

  • each row = one fMRI time point

  • first column = subject identifier

  • remaining columns = brain activity values for each Schaefer100 brain region

  • consecutive rows for the same subject form that subject’s fMRI timeseries (we’ll be visualizing this timeseries later!)

Remember, all subjects are stored together in the same dataframe.

To gain some more intuition to this dataset, let’s do two things:

  • get a list of column names and subject names

  • extract the data just for a single subject and explore their timeseries before moving on to analyses involving the full dataset.

Each row represents a single fMRI time point from one subject:

  • each row = one fMRI time point

  • first column = subject identifier

  • remaining columns = brain activity values for each Schaefer100 brain region

  • consecutive rows for the same subject form that subject’s fMRI timeseries (we’ll be visualizing this timeseries later!)

Remember, all subjects are stored together in the same dataframe.

To gain some more intuition to this dataset, let’s do two things:

  • get a list of column names and subject names

  • extract the data just for a single subject and explore their timeseries before moving on to analyses involving the full dataset.

List of Brain Regions (Columns) and Subject Ids (Rows)

Columns (brain regions)

Recall that each column corresponds to either:

  • the subject identifier (subject)

  • a Schaefer100 brain region

The code below converts the dataframe’s column names into a Python list and prints them.

A list is one of Python’s basic data structures used to store multiple items in a specific order. You can think of a list like a numbered collection, where each item has a position and can be accessed individually.

For example, a list can store many brain region names, allowing us to access, inspect, or loop through them one at a time.

For the code below, all you need to know is that we are applying .tolist() to the dataframe to create a new list containing all their column names.

# Get a list containing all column names in the dataframe
column_names = timeseries_df.columns.tolist() # uses .tolist

# Print the list
print(column_names)
['subject', '7Networks_LH_Vis_1', '7Networks_LH_Vis_2', '7Networks_LH_Vis_3', '7Networks_LH_Vis_4', '7Networks_LH_Vis_5', '7Networks_LH_Vis_6', '7Networks_LH_Vis_7', '7Networks_LH_Vis_8', '7Networks_LH_Vis_9', '7Networks_LH_SomMot_1', '7Networks_LH_SomMot_2', '7Networks_LH_SomMot_3', '7Networks_LH_SomMot_4', '7Networks_LH_SomMot_5', '7Networks_LH_SomMot_6', '7Networks_LH_DorsAttn_Post_1', '7Networks_LH_DorsAttn_Post_2', '7Networks_LH_DorsAttn_Post_3', '7Networks_LH_DorsAttn_Post_4', '7Networks_LH_DorsAttn_Post_5', '7Networks_LH_DorsAttn_Post_6', '7Networks_LH_DorsAttn_PrCv_1', '7Networks_LH_DorsAttn_FEF_1', '7Networks_LH_SalVentAttn_ParOper_1', '7Networks_LH_SalVentAttn_FrOperIns_1', '7Networks_LH_SalVentAttn_FrOperIns_2', '7Networks_LH_SalVentAttn_PFCl_1', '7Networks_LH_SalVentAttn_Med_1', '7Networks_LH_SalVentAttn_Med_2', '7Networks_LH_SalVentAttn_Med_3', '7Networks_LH_Limbic_OFC_1', '7Networks_LH_Limbic_TempPole_1', '7Networks_LH_Limbic_TempPole_2', '7Networks_LH_Cont_Par_1', '7Networks_LH_Cont_PFCl_1', '7Networks_LH_Cont_pCun_1', '7Networks_LH_Cont_Cing_1', '7Networks_LH_Default_Temp_1', '7Networks_LH_Default_Temp_2', '7Networks_LH_Default_Par_1', '7Networks_LH_Default_Par_2', '7Networks_LH_Default_PFC_1', '7Networks_LH_Default_PFC_2', '7Networks_LH_Default_PFC_3', '7Networks_LH_Default_PFC_4', '7Networks_LH_Default_PFC_5', '7Networks_LH_Default_PFC_6', '7Networks_LH_Default_PFC_7', '7Networks_LH_Default_pCunPCC_1', '7Networks_LH_Default_pCunPCC_2', '7Networks_RH_Vis_1', '7Networks_RH_Vis_2', '7Networks_RH_Vis_3', '7Networks_RH_Vis_4', '7Networks_RH_Vis_5', '7Networks_RH_Vis_6', '7Networks_RH_Vis_7', '7Networks_RH_Vis_8', '7Networks_RH_SomMot_1', '7Networks_RH_SomMot_2', '7Networks_RH_SomMot_3', '7Networks_RH_SomMot_4', '7Networks_RH_SomMot_5', '7Networks_RH_SomMot_6', '7Networks_RH_SomMot_7', '7Networks_RH_SomMot_8', '7Networks_RH_DorsAttn_Post_1', '7Networks_RH_DorsAttn_Post_2', '7Networks_RH_DorsAttn_Post_3', '7Networks_RH_DorsAttn_Post_4', '7Networks_RH_DorsAttn_Post_5', '7Networks_RH_DorsAttn_PrCv_1', '7Networks_RH_DorsAttn_FEF_1', '7Networks_RH_SalVentAttn_TempOccPar_1', '7Networks_RH_SalVentAttn_TempOccPar_2', '7Networks_RH_SalVentAttn_FrOperIns_1', '7Networks_RH_SalVentAttn_Med_1', '7Networks_RH_SalVentAttn_Med_2', '7Networks_RH_Limbic_OFC_1', '7Networks_RH_Limbic_TempPole_1', '7Networks_RH_Cont_Par_1', '7Networks_RH_Cont_Par_2', '7Networks_RH_Cont_PFCl_1', '7Networks_RH_Cont_PFCl_2', '7Networks_RH_Cont_PFCl_3', '7Networks_RH_Cont_PFCl_4', '7Networks_RH_Cont_Cing_1', '7Networks_RH_Cont_PFCmp_1', '7Networks_RH_Cont_pCun_1', '7Networks_RH_Default_Par_1', '7Networks_RH_Default_Temp_1', '7Networks_RH_Default_Temp_2', '7Networks_RH_Default_Temp_3', '7Networks_RH_Default_PFCv_1', '7Networks_RH_Default_PFCv_2', '7Networks_RH_Default_PFCdPFCm_1', '7Networks_RH_Default_PFCdPFCm_2', '7Networks_RH_Default_PFCdPFCm_3', '7Networks_RH_Default_pCunPCC_1', '7Networks_RH_Default_pCunPCC_2']

Obtaining list of unique subject IDs:

Let’s try to obtain a list of unique subject IDs.

The results from the print(column_names) show that the first column is the ‘subject’ column. (Remember, list stores values in a specific order).

The dataframe contains many rows for each subject because every subject has multiple fMRI time points.

For example, subject sub-010002 may appear 652 times because that participant has 652 time points.

If we simply print the subject column, we would see many repeated subject IDs. The output shown after printing the subject column shows that there are 143440 rows (Length: 143440).

# Subset the subject column of the timeseries dataframe
subject_column = timeseries_df["subject"]

print(subject_column) # display subject_column
0         sub-010002
1         sub-010002
2         sub-010002
3         sub-010002
4         sub-010002
             ...    
143435    sub-010321
143436    sub-010321
143437    sub-010321
143438    sub-010321
143439    sub-010321
Name: subject, Length: 143440, dtype: str

So the next step is to find unique or distinct subject IDs that appear in subject_column.

In pandas, we can use .unique() to find distinct values in a single column, which is what we need to find distinct subject IDs that appear in subject_column.

# Get the unique values that appear in that column
subject_ids = subject_column.unique().tolist()

subject_ids
['sub-010002', 'sub-010003', 'sub-010004', 'sub-010005', 'sub-010006', 'sub-010007', 'sub-010010', 'sub-010012', 'sub-010015', 'sub-010016', 'sub-010017', 'sub-010019', 'sub-010020', 'sub-010021', 'sub-010022', 'sub-010023', 'sub-010024', 'sub-010026', 'sub-010027', 'sub-010028', 'sub-010029', 'sub-010030', 'sub-010031', 'sub-010032', 'sub-010033', 'sub-010034', 'sub-010035', 'sub-010036', 'sub-010037', 'sub-010038', 'sub-010039', 'sub-010040', 'sub-010041', 'sub-010042', 'sub-010043', 'sub-010044', 'sub-010045', 'sub-010046', 'sub-010048', 'sub-010050', 'sub-010051', 'sub-010052', 'sub-010053', 'sub-010056', 'sub-010059', 'sub-010060', 'sub-010061', 'sub-010062', 'sub-010063', 'sub-010064', 'sub-010065', 'sub-010066', 'sub-010067', 'sub-010068', 'sub-010069', 'sub-010070', 'sub-010071', 'sub-010072', 'sub-010073', 'sub-010074', 'sub-010075', 'sub-010076', 'sub-010077', 'sub-010078', 'sub-010079', 'sub-010080', 'sub-010081', 'sub-010083', 'sub-010084', 'sub-010085', 'sub-010086', 'sub-010087', 'sub-010088', 'sub-010089', 'sub-010090', 'sub-010091', 'sub-010092', 'sub-010093', 'sub-010094', 'sub-010100', 'sub-010104', 'sub-010110', 'sub-010126', 'sub-010134', 'sub-010136', 'sub-010137', 'sub-010138', 'sub-010141', 'sub-010142', 'sub-010146', 'sub-010148', 'sub-010150', 'sub-010152', 'sub-010155', 'sub-010157', 'sub-010162', 'sub-010163', 'sub-010164', 'sub-010165', 'sub-010166', 'sub-010168', 'sub-010169', 'sub-010170', 'sub-010176', 'sub-010183', 'sub-010191', 'sub-010192', 'sub-010193', 'sub-010194', 'sub-010195', 'sub-010196', 'sub-010197', 'sub-010199', 'sub-010200', 'sub-010201', 'sub-010202', 'sub-010203', 'sub-010204', 'sub-010207', 'sub-010210', 'sub-010213', 'sub-010214', 'sub-010215', 'sub-010216', 'sub-010218', 'sub-010219', 'sub-010220', 'sub-010222', 'sub-010223', 'sub-010224', 'sub-010225', 'sub-010226', 'sub-010228', 'sub-010229', 'sub-010231', 'sub-010232', 'sub-010233', 'sub-010234', 'sub-010235', 'sub-010236', 'sub-010237', 'sub-010238', 'sub-010239', 'sub-010240', 'sub-010241', 'sub-010242', 'sub-010243', 'sub-010244', 'sub-010245', 'sub-010246', 'sub-010247', 'sub-010250', 'sub-010252', 'sub-010253', 'sub-010254', 'sub-010255', 'sub-010256', 'sub-010257', 'sub-010260', 'sub-010261', 'sub-010262', 'sub-010263', 'sub-010264', 'sub-010265', 'sub-010266', 'sub-010267', 'sub-010268', 'sub-010269', 'sub-010270', 'sub-010271', 'sub-010272', 'sub-010273', 'sub-010274', 'sub-010275', 'sub-010276', 'sub-010277', 'sub-010278', 'sub-010279', 'sub-010280', 'sub-010281', 'sub-010282', 'sub-010283', 'sub-010284', 'sub-010285', 'sub-010286', 'sub-010287', 'sub-010288', 'sub-010289', 'sub-010290', 'sub-010291', 'sub-010292', 'sub-010293', 'sub-010294', 'sub-010295', 'sub-010296', 'sub-010297', 'sub-010298', 'sub-010299', 'sub-010300', 'sub-010301', 'sub-010302', 'sub-010303', 'sub-010304', 'sub-010305', 'sub-010306', 'sub-010307', 'sub-010308', 'sub-010309', 'sub-010310', 'sub-010311', 'sub-010312', 'sub-010313', 'sub-010314', 'sub-010315', 'sub-010316', 'sub-010317', 'sub-010318', 'sub-010319', 'sub-010320', 'sub-010321']
# You can display the number of items in the list by using `len`
len(subject_ids) # there're 220 subject ids! 
220

4. Subset the Dataframe for One Subject

Right now, our dataframe contains the timeseries data for all 220 subjects. While this is ideal for large-scale analyses, it is often easier to understand the structure of the data by first examining a single participant.

Let’s extract all time points belonging to a single participant: sub-010003.

Because each row corresponds to one time point, the resulting dataframe will contain many rows (for example, ~652 rows) rather than a single row.

subject_id = "sub-010003" # choose the subject we want to extract

# Keep only the rows belonging to the selected subject and store them in a new dataframe `sub_df`
sub_df = timeseries_df[timeseries_df["subject"] == subject_id].copy()

# Display the shape/dimension of the sub_df
print("Shape:", sub_df.shape) # 652 rows/timepoints x 101 columns (as expected)

# Display few rows/columns
sub_df.head()
Shape: (652, 101)
Loading...
# Notice that the row number starts from 652 onwards.
# We'll reset row numbers so the first time point starts at index 0
sub_df = sub_df.reset_index(drop=True)

# Let's display the dataframe again
sub_df.head()
Loading...

5. Extract a Single Brain Region Timeseries of a Subject

Now let’s focus on one brain region:

7Networks_LH_Vis_1

This region belongs to the left hemisphere visual network.

By selecting this column from our subject dataframe, we obtain a single timeseries showing how activity in this region changes over time.

region_name = "7Networks_LH_Vis_1" # choose a region

# Extract the column (defined by `region_name`) from sub_df and assign to new variable `region_ts`
region_ts = sub_df[region_name] # subset

# Print shape of region_ts
print(region_ts.shape) # (652, ) --> 652 values, as expected (because each scan = 652 timepoints)

# Preview region_ts
region_ts.head()
(652,)
0 0.304409 1 0.291162 2 0.287490 3 0.242856 4 0.217101 Name: 7Networks_LH_Vis_1, dtype: float64

6. Visualize the Timeseries

We can now visualize how activity changes over time in this brain region.

The x-axis represents fMRI time points.

The y-axis represents the activity value recorded for this brain region at each time point.

We’ll be using matplotlib to plot our data.

# Create a new figure (plotting canvas)
# figsize=(12, 4) means 12 inches wide and 4 inches tall
plt.figure(figsize=(12, 4))

# Plot the timeseries values as a line graph
# The x-axis will automatically use the row number (0, 1, 2, ...)
# The y-axis will show the activity values for this brain region
plt.plot(region_ts)

# Add a title to the plot
# The f-string inserts the region name and subject ID into the title
plt.title(f"{region_name} timeseries for {subject_id}")

# Label the x-axis
# Each point corresponds to one fMRI time point
plt.xlabel("Time point")

# Label the y-axis
# These values represent brain activity in the selected region
plt.ylabel("Activity")

# Adjust spacing so labels and titles fit neatly inside the figure
plt.tight_layout()

# Display the completed plot
plt.show()
<Figure size 1200x400 with 1 Axes>

Understanding the plot

The plot() function draws a line connecting the activity values recorded at each time point.

In this figure:

  • the x-axis represents time points in the fMRI scan

  • the y-axis represents activity in the selected brain region

  • each point on the line corresponds to one row in the subject’s timeseries dataframe

Notice how the signal rises and falls over time. These fluctuations are the fMRI signal measured from this brain region. Later, we will compare these fluctuations across different brain regions to estimate functional connectivity.