Brain Observatory: What are those cells doing?#

This sandbox will help you analyze a dataset from the Allen Institute’s Brain Observatory.#

These datasets contain calcium imaging data for various different cell types in the visual cortex of the mouse. It’s likely that these cell types have different roles in the visual system – your mission is to figure out what these roles are. You will choose a visual area, a cell type, and look at their responses to natural stimuli.

By the end of this lesson, you will be able to:#

  1. Examine a 2-photon imaging dataset for a particular cell type, in a specific visual area.

  2. Use imshow to visualize two-dimensional images in Python.

  3. Create plots of raw calcium imaging data.

Additional information on this dataset, and how it was collected, can be found here as well as in the SDK documentation.

Step 1. Importing toolboxes#

First, we’ll import the necessary toolboxes to run this code. The first chunk of “import” lines will bring in some standard toolboxes that we need. For example, “numpy” is a toolbox that has functions to work with large arrays. The second chunk of import lines brings in some toolboxes that the Allen Brain Observatory has already packaged, to help users analyze its data.

Task: In addition to the modules already imported below, import numpy, pandas, and matplotlib.pyplot as their conventional names.
# Install allensdk if it isn't already there
try:
    import allensdk
    print("allensdk is already installed.")
except ImportError:
    print("allensdk not found, installing now...")
    !pip install allensdk
allensdk is already installed.
# Allen specific toolboxes
import allensdk.brain_observatory.stimulus_info as stim_info
from allensdk.core.brain_observatory_cache import BrainObservatoryCache

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
/opt/miniconda3/envs/jb_py311/lib/python3.11/site-packages/allensdk/core/brain_observatory_nwb_data_set.py:43: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
  from pkg_resources import parse_version

Similar to how we created an instance of the CellTypesCache, here we’ll create an instance of the “BrainObservatoryCache.” The datahub already has a manifest file available in the directory you see below. This directory also has all of the data we need.

Note: You need to run this notebook in the Docker container that includes allen-brain-observatory (often denoted by ‘w/ allen’). Otherwise you will get a permissions error when you attempt to run the cell below.

# We will create an instance of the Brain Observatory Cache as an object, "boc."
boc = BrainObservatoryCache(manifest_file='manifest.json')

Step 2. Get a list of all possible transgenic mouse lines and brain areas, and choose which to work with.#

Next, we’ll ask that “boc” structure to tell us what all of the possible Cre lines and brain areas are that we can analyze. You’ll need to use these exact names when you’re trying to pull a specific one from the dataset.

# We'll save the list of cre lines as a variable, 'cre-lines'.
cre_lines = boc.get_all_cre_lines()
print("all cre lines: " + str(cre_lines))

# We'll save the list of possible structures as a variable, 'brain_areas'.
brain_areas = boc.get_all_targeted_structures()
print("all brain regions: " + str(brain_areas))
all cre lines: ['Cux2-CreERT2', 'Emx1-IRES-Cre', 'Fezf2-CreER', 'Nr5a1-Cre', 'Ntsr1-Cre_GN220', 'Pvalb-IRES-Cre', 'Rbp4-Cre_KL100', 'Rorb-IRES2-Cre', 'Scnn1a-Tg3-Cre', 'Slc17a7-IRES2-Cre', 'Sst-IRES-Cre', 'Tlx3-Cre_PL56', 'Vip-IRES-Cre']
all brain regions: ['VISal', 'VISam', 'VISl', 'VISp', 'VISpm', 'VISrl']

Task: Choose a visual area and Cre line from the lists above to examine in the rest of the notebook. Refer back to our lecture slides or the Brain Observatory landing page to learn more about these different visual areas. Primary cortex (VISp) is surrounded by several other brain regions, which have unknown functions.

You can find more info about the Cre-lines here here.

  1. Create a new block of Markdown text below this one which briefly describes your visual area and Cre line. Remember that markdown is the other type of block that Jupyter supports. You can change your block to Markdown using the dropdown in the menu above. Check out this cheatsheet for a few handy elements of Markdown syntax.

  2. Assign your visual area and cre line to visual_area and cre_line, respectively.

visual_area = 'VISp'
cre_line = 'Rorb-IRES2-Cre'

Step 3. Extract an experiment session.#

Task: Create a dataframe of experiments.
  1. Use the get_experiment_containers() method of our boc object to get a list of experiments that are available. This method takes the arguments targeted_structures= as well as cre_lines= , which both require a list. Assign the outcome of this method to experiments.

  2. Create a dataframe out of exps and assign it to exps_df.

  3. There isn’t an experiment for every combination of Cre lines and visual areas above. Add an if statement that will print a statement for you if the dataframe you created is empty (Hint: use empty attribute).

  4. Show the head of your dataframe.

exps = boc.get_experiment_containers(targeted_structures=[visual_area],cre_lines =[cre_line])
exps_df = pd.DataFrame(exps)
exps_df.head()
id imaging_depth targeted_structure cre_line reporter_line donor_name specimen_name tags failed
0 511510675 275 VISp Rorb-IRES2-Cre Ai93(TITL-GCaMP6f) 228786 Rorb-IRES2-Cre;Camk2a-tTA;Ai93-228786 [] False
1 586351979 275 VISp Rorb-IRES2-Cre Ai93(TITL-GCaMP6f) 304756 Rorb-IRES2-Cre;Camk2a-tTA;Ai93-304756 [] False
2 590168381 275 VISp Rorb-IRES2-Cre Ai93(TITL-GCaMP6f) 305467 Rorb-IRES2-Cre;Camk2a-tTA;Ai93-305467 [] False
3 511510989 275 VISp Rorb-IRES2-Cre Ai93(TITL-GCaMP6f) 222431 Rorb-IRES2-Cre;Camk2a-tTA;Ai93-222431 [] False
4 512124562 275 VISp Rorb-IRES2-Cre Ai93(TITL-GCaMP6f) 234831 Rorb-IRES2-Cre;Camk2a-tTA;Ai93-234831 [] False

Once you’ve successfully found a combination that works, you should have the head of your dataframe above. Let’s look into one of these experiment containers, most of which have three different sessions for different types of visual stimuli.

Task:
  1. Pick an experiment from the table above. Copy the id in the “id” column and assign it to experiment_container_id.

  2. Use the get_ophys_experiments() method on our boc object, providing it with the arguments experiment_container_ids= and stimuli=[‘natural_scenes’]. This will restrict our search to experiments with our ID and where natural stimuli were shown. Assign this to expt_cont.

  3. Look at the expt_cont object. What kind of object is this?

experiment_container_id = 512124562

expt_cont = boc.get_ophys_experiments(experiment_container_ids=[experiment_container_id],stimuli=['natural_scenes'])
type(expt_cont)
expt_cont
[{'id': 512124564,
  'imaging_depth': 275,
  'targeted_structure': 'VISp',
  'cre_line': 'Rorb-IRES2-Cre',
  'reporter_line': 'Ai93(TITL-GCaMP6f)',
  'acquisition_age_days': 80,
  'experiment_container_id': 512124562,
  'session_type': 'three_session_B',
  'donor_name': '234831',
  'specimen_name': 'Rorb-IRES2-Cre;Camk2a-tTA;Ai93-234831',
  'fail_eye_tracking': False}]

Now, let’s get the id for this experiment and extract the data using the get_ophys_experiment_data method.

Task:
  1. Programmatically, assign the ‘id’ of your expt_cont object to session_id.

  2. Use the get_ophys_experiment_data() method, simply giving it your session_id as an argument, and assign the output of this to data.

  3. What kind of object is data?

session_id = expt_cont[0]['id']
data = boc.get_ophys_experiment_data(session_id)
data
<allensdk.core.brain_observatory_nwb_data_set.BrainObservatoryNwbDataSet at 0x1093bfd10>

Step 4. Download & inspect the natural scenes imaging session#

First, we’ll look at the session where the mouse viewed natural scenes.

Let’s take a quick look at the data you just acquired. We’ll take a maximum projection of the data, so that we can actually see the cells. If we just looked at one snapshot of the raw imaging data, the cells would look dim. A “maximum projection image” shows us the maximum brightness for each pixel, across the entire experiment.

Below, we are using the get_max_projection() method on our data, and then using the imshow() method in order to see our projection.

Note: The weird text for the ylabel is called “TeX” markup, in order to get the greek symbol mu (\(\mu\)). See documentation here.

# Get the maximum projection (a numpy array) of our data
max_projection = data.get_max_projection()

# Create a new figure
fig = plt.figure(figsize=(6,6))

# Use imshow to visualize our numpy array
plt.imshow(max_projection, cmap='gray')

# Add labels for microns; weird syntax below is to get the micro sign
plt.ylabel(r'$\mu$m',fontsize=14)
plt.xlabel(r'$\mu$m',fontsize=14)
plt.show()
../_images/1739ad63246f855700470861fcba3b6ea44f7ad901bdd63a168d7aeeb7f889da.png

Step 5. Look at the calcium transients of your cells#

Now we’ll plot the data of each of our cells (from the field of view above) across time. Each line shows the change in fluorescence over baseline (\(\Delta\))F/F) of the individual cells. When there are sharp increases, that’s when the cells are responding.

Task:
  1. Create a for loop that will plot, in succession, the responses of the first ten cells of your dataset, contained in dff. Use ‘ts’ (the timestamps of the data) for the x axis.

  2. Add informative labels to your plot. What is the x axis? What’s the y axis?

  3. Use plt.xlim() to zoom in on an interesting part of the data.

  4. Inspect the data here – how fast are these changes in fluorescence happening?

  5. Challenge: How could you write this loop so that your traces wouldn’t be overlapping?

# Use the `get_dff_traces()` method to return both timestamps (ts, in seconds) as well as the deltaF/F (dff) 
ts, dff = data.get_dff_traces()

# Set up a figure
fig = plt.figure(figsize=(20,10))
offset = 0

# Your plotting script below
for neuron in range(10):
    plt.plot(ts,dff[neuron]+offset)
    plt.xlim([50,100])
    offset = offset + 5

plt.show()
../_images/561a6912cac36c569c4de3956770b02d91acc6de3645426584d1ae283d4f2706.png

Step 6. Look at the response of your cells to natural scenes#

Hmm, there are some responses above, but it’s tough to see what’s going on with just the raw traces. Let’s instead see how these cells actually responded to different types of images. To do so, we’ll need to use the get_cell_specimens() method on our boc, giving it the name of the experiment container ID to look for. The dataframe that this creates will have a lot more information about what the cells in our experiment prefer.

# Get the cell specimens information for this session
cell_specimens = boc.get_cell_specimens(experiment_container_ids=[experiment_container_id])
cell_specimens_df = pd.DataFrame(cell_specimens)
cell_specimens_df.head()
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
Cell In[11], line 2
      1 # Get the cell specimens information for this session
----> 2 cell_specimens = boc.get_cell_specimens(experiment_container_ids=[experiment_container_id])
      3 cell_specimens_df = pd.DataFrame(cell_specimens)
      4 cell_specimens_df.head()

File /opt/miniconda3/envs/jb_py311/lib/python3.11/site-packages/allensdk/core/brain_observatory_cache.py:489, in BrainObservatoryCache.get_cell_specimens(self, file_name, ids, experiment_container_ids, include_failed, simple, filters)
    439 """Return cell specimens that have certain properies.
    440 
    441 Parameters
   (...)    484 list of dictionaries
    485 """
    487 file_name = self.get_cache_path(file_name, self.CELL_SPECIMENS_KEY)
--> 489 cell_specimens = self.api.get_cell_metrics(
    490     path=file_name,
    491     strategy="lazy",
    492     pre=lambda x: [y for y in x],
    493     **Cache.cache_json(),
    494 )
    496 cell_specimens = self.api.filter_cell_specimens(
    497     cell_specimens,
    498     ids=ids,
   (...)    501     filters=filters,
    502 )
    504 # drop the thumbnail columns

File /opt/miniconda3/envs/jb_py311/lib/python3.11/site-packages/allensdk/api/warehouse_cache/cache.py:661, in cacheable.<locals>.decor.<locals>.w(*args, **kwargs)
    658 if decor.post and not 'post in kwargs':
    659     kwargs['post'] = decor.post
--> 661 result = Cache.cacher(func,
    662                       *args,
    663                       **kwargs)
    664 return result

File /opt/miniconda3/envs/jb_py311/lib/python3.11/site-packages/allensdk/api/warehouse_cache/cache.py:384, in Cache.cacher(fn, *args, **kwargs)
    382 if writer:
    383     data = fn(*args, **kwargs)
--> 384     data = pre(data)
    385     writer(path, data)
    386 else:

File /opt/miniconda3/envs/jb_py311/lib/python3.11/site-packages/allensdk/core/brain_observatory_cache.py:492, in BrainObservatoryCache.get_cell_specimens.<locals>.<lambda>(x)
    439 """Return cell specimens that have certain properies.
    440 
    441 Parameters
   (...)    484 list of dictionaries
    485 """
    487 file_name = self.get_cache_path(file_name, self.CELL_SPECIMENS_KEY)
    489 cell_specimens = self.api.get_cell_metrics(
    490     path=file_name,
    491     strategy="lazy",
--> 492     pre=lambda x: [y for y in x],
    493     **Cache.cache_json(),
    494 )
    496 cell_specimens = self.api.filter_cell_specimens(
    497     cell_specimens,
    498     ids=ids,
   (...)    501     filters=filters,
    502 )
    504 # drop the thumbnail columns

File /opt/miniconda3/envs/jb_py311/lib/python3.11/site-packages/allensdk/core/brain_observatory_cache.py:492, in <listcomp>(.0)
    439 """Return cell specimens that have certain properies.
    440 
    441 Parameters
   (...)    484 list of dictionaries
    485 """
    487 file_name = self.get_cache_path(file_name, self.CELL_SPECIMENS_KEY)
    489 cell_specimens = self.api.get_cell_metrics(
    490     path=file_name,
    491     strategy="lazy",
--> 492     pre=lambda x: [y for y in x],
    493     **Cache.cache_json(),
    494 )
    496 cell_specimens = self.api.filter_cell_specimens(
    497     cell_specimens,
    498     ids=ids,
   (...)    501     filters=filters,
    502 )
    504 # drop the thumbnail columns

File /opt/miniconda3/envs/jb_py311/lib/python3.11/site-packages/allensdk/api/queries/rma_pager.py:58, in RmaPager.pager(fn, *args, **kwargs)
     56 while result_count == num_rows:
     57     kwargs['start_row'] = start_row
---> 58     data = fn(*args, **kwargs)
     60     start_row = start_row + num_rows
     61     result_count = len(data)

File /opt/miniconda3/envs/jb_py311/lib/python3.11/site-packages/allensdk/api/queries/brain_observatory_api.py:359, in BrainObservatoryApi.get_cell_metrics(self, cell_specimen_ids, *args, **kwargs)
    345 """Get cell metrics by id
    346 
    347 Parameters
   (...)    354 dict : cell metric metadata
    355 """
    357 order = kwargs.pop("order", ["'cell_specimen_id'"])
--> 359 data = self.template_query(
    360     "brain_observatory_queries",
    361     "cell_metric",
    362     cell_specimen_ids=cell_specimen_ids,
    363     order=order,
    364     *args,
    365     **kwargs,
    366 )
    368 return data

File /opt/miniconda3/envs/jb_py311/lib/python3.11/site-packages/allensdk/api/queries/rma_template.py:132, in RmaTemplate.template_query(self, template_name, entry_name, **kwargs)
    128     query_args['order'] = template['order']
    130 query_args.update(kwargs)
--> 132 data = self.model_query(**query_args)
    134 return data

File /opt/miniconda3/envs/jb_py311/lib/python3.11/site-packages/allensdk/api/queries/rma_api.py:257, in RmaApi.model_query(self, *args, **kwargs)
    217 def model_query(self, *args, **kwargs):
    218     '''Construct and execute a model stage of an RMA query string.
    219 
    220     Parameters
   (...)    255     response, including the normalized query.
    256     '''
--> 257     return self.json_msg_query(
    258         self.build_query_url(
    259             self.model_stage(*args, **kwargs)))

File /opt/miniconda3/envs/jb_py311/lib/python3.11/site-packages/allensdk/api/api.py:164, in Api.json_msg_query(self, url, dataframe)
    147 def json_msg_query(self, url, dataframe=False):
    148     ''' Common case where the url is fully constructed
    149         and the response data is stored in the 'msg' field.
    150 
   (...)    161         returned data; type depends on dataframe option
    162     '''
--> 164     data = self.do_query(lambda *a, **k: url,
    165                          self.read_data)
    167     if dataframe is True:
    168         warnings.warn("dataframe argument is deprecated", DeprecationWarning)

File /opt/miniconda3/envs/jb_py311/lib/python3.11/site-packages/allensdk/api/api.py:204, in Api.do_query(self, url_builder_fn, json_traversal_fn, *args, **kwargs)
    200 api_url = url_builder_fn(*args, **kwargs)
    202 post = kwargs.get('post', False)
--> 204 json_parsed_data = self.retrieve_parsed_json_over_http(api_url, post)
    206 return json_traversal_fn(json_parsed_data)

File /opt/miniconda3/envs/jb_py311/lib/python3.11/site-packages/allensdk/api/api.py:369, in Api.retrieve_parsed_json_over_http(self, url, post)
    366 self._log.info("Downloading URL: %s", url)
    368 if post is False:
--> 369     data = json_utilities.read_url_get(
    370         requests.utils.quote(url,
    371                              ';/?:@&=+$,'))
    372 else:
    373     data = json_utilities.read_url_post(url)

File /opt/miniconda3/envs/jb_py311/lib/python3.11/site-packages/allensdk/core/json_utilities.py:115, in read_url_get(url)
     98 def read_url_get(url):
     99     """Transform a JSON contained in a file into an equivalent
    100     nested python dict.
    101 
   (...)    113     the output will be of the corresponding type.
    114     """
--> 115     response = urllib_request.urlopen(url)
    116     json_string = response.read().decode("utf-8")
    118     return json.loads(json_string)

File /opt/miniconda3/envs/jb_py311/lib/python3.11/urllib/request.py:216, in urlopen(url, data, timeout, cafile, capath, cadefault, context)
    214 else:
    215     opener = _opener
--> 216 return opener.open(url, data, timeout)

File /opt/miniconda3/envs/jb_py311/lib/python3.11/urllib/request.py:519, in OpenerDirector.open(self, fullurl, data, timeout)
    516     req = meth(req)
    518 sys.audit('urllib.Request', req.full_url, req.data, req.headers, req.get_method())
--> 519 response = self._open(req, data)
    521 # post-process response
    522 meth_name = protocol+"_response"

File /opt/miniconda3/envs/jb_py311/lib/python3.11/urllib/request.py:536, in OpenerDirector._open(self, req, data)
    533     return result
    535 protocol = req.type
--> 536 result = self._call_chain(self.handle_open, protocol, protocol +
    537                           '_open', req)
    538 if result:
    539     return result

File /opt/miniconda3/envs/jb_py311/lib/python3.11/urllib/request.py:496, in OpenerDirector._call_chain(self, chain, kind, meth_name, *args)
    494 for handler in handlers:
    495     func = getattr(handler, meth_name)
--> 496     result = func(*args)
    497     if result is not None:
    498         return result

File /opt/miniconda3/envs/jb_py311/lib/python3.11/urllib/request.py:1377, in HTTPHandler.http_open(self, req)
   1376 def http_open(self, req):
-> 1377     return self.do_open(http.client.HTTPConnection, req)

File /opt/miniconda3/envs/jb_py311/lib/python3.11/urllib/request.py:1352, in AbstractHTTPHandler.do_open(self, http_class, req, **http_conn_args)
   1350     except OSError as err: # timeout error
   1351         raise URLError(err)
-> 1352     r = h.getresponse()
   1353 except:
   1354     h.close()

File /opt/miniconda3/envs/jb_py311/lib/python3.11/http/client.py:1395, in HTTPConnection.getresponse(self)
   1393 try:
   1394     try:
-> 1395         response.begin()
   1396     except ConnectionError:
   1397         self.close()

File /opt/miniconda3/envs/jb_py311/lib/python3.11/http/client.py:325, in HTTPResponse.begin(self)
    323 # read until we get a non-100 response
    324 while True:
--> 325     version, status, reason = self._read_status()
    326     if status != CONTINUE:
    327         break

File /opt/miniconda3/envs/jb_py311/lib/python3.11/http/client.py:286, in HTTPResponse._read_status(self)
    285 def _read_status(self):
--> 286     line = str(self.fp.readline(_MAXLINE + 1), "iso-8859-1")
    287     if len(line) > _MAXLINE:
    288         raise LineTooLong("status line")

File /opt/miniconda3/envs/jb_py311/lib/python3.11/socket.py:718, in SocketIO.readinto(self, b)
    716 while True:
    717     try:
--> 718         return self._sock.recv_into(b)
    719     except timeout:
    720         self._timeout_occurred = True

KeyboardInterrupt: 

Let’s create a histogram of preferred images in our dataset.

Task:
  1. Create a subset of this dataframe called ‘sig_cells’ where the column ‘p_ns’ is less than 0.05. In other words, we only want cells that significantly preferred a specific image.

  2. The preferred image ID is in the ‘pref_image_ns’ column of this dataframe. Assign this to ‘pref_images’.

  3. Create a histogram of preferred images with 118 bins (there are 118 different images). There are multiple ways to do this!

sig_cells = cell_specimens_df[cell_specimens_df['p_ns']<0.05]
pref_images = sig_cells['pref_image_ns']

fig = plt.figure(figsize=(20,10))
pref_images.value_counts().plot(kind='bar')
plt.xlabel('image')
plt.ylabel('number of cells')
plt.show()
../_images/c3bfeda0466c67e5d82c1249c39c9eef9cb5ddaafdfc36af724bf2632803f4a0.png

Wait, but what is that image? In order to actually see what this stimulus is, first, we’ll organize the stimulus table. This tells us which stimulus was played on each trial. This data set has 118 different scenes, and each scene is presented 50 times. Images of the scenes can be found here.

Task:
  1. Assign your top image to image_id. You can do this programmatically, or simply hard code it.

  2. The ‘natural_scene_template’ that we create below is a numpy array, where the first dimension is the image id, and the second and third are the values of the image. You want to plot natural_scene_template[image_id,:,:] Use the imshow() method to show the image, just as we did with our maximum projection above.

natural_scene_table = data.get_stimulus_table('natural_scenes')
natural_scene_template = data.get_stimulus_template('natural_scenes')
sceneIDs = np.unique(natural_scene_table.frame)


# Choose your image id
image_id = 111

plt.imshow(natural_scene_template[image_id,:,:])
plt.show()

# Plot this natural scene
../_images/6cc5dd6649db70e080a92aab0b3554fdc9205e73337fc992af4b62af0e501ccf.png

Step 7. Examine the direction selectivity of your cell#

Sometimes, the function of a cell is not particularly clear from natural stimuli. Those stimuli have a lot of information in them, and it might be hard to tell what a cell is actually responding to. Instead, we can use simple drifting gratings to look at one straightforward property of a cell: does it respond to specific directions of movement?

We can use the columns that look at the direction selectivity index (DSI) in order to determine whether our cells are direction selective (typically considered having a DSI > 0.5). Take another look at the cell_specimens_df we created above. How would you analyze the data here to see if your cells were direction selective? How would you go back and compare these data to that of other cell types?