Repository: rmarkello/abagen (python)

Issue:
Include RNAseq data fetchers + loaders?
## The issue

For two of the AHBA donors RNA sequencing was run for a number of tissue samples. These data are, generally, higher quality than the microarray data that are used throughout most of ``abagen``. It might be nice to include an automated way to fetch and load this data for people who want to do...things...with it. Maybe, eventually, we will think about including some potential things you can do with RNA sequencing data. For now, we consider adding ways to get it and load it easily.

## Proposed solution

Adding the following function to `abagen.datasets.fetchers`:

```python
def fetch_rnaseq(data_dir=None, donors=None, resume=True, verbose=1):
    """
    Downloads RNA-sequencing data from the Allen Human Brain Atlas

    Parameters
    ----------
    data_dir : str, optional
        Directory where data should be downloaded and unpacked. Default:
        current directory
    donors : list, optional
        List of donors to download; can be either donor number or UID. Can also
        specify 'all' to download all available donors. Default: 9861
    resume : bool, optional
        Whether to resume download of a partly-downloaded file. Default: True
    verbose : int, optional
        Verbosity level (0 means no message). Default: 1

    Returns
    -------
    data : dict
        Dictionary with keys ['counts', 'tpm', 'ontology', 'genes',
        'annotation'], where corresponding values are lists of filepaths to
        downloaded CSV files.

    References
    ----------
    Hawrylycz, M. J., Lein, E. S., Guillozet-Bongaarts, A. L., Shen, E. H., Ng,
    L., Miller, J. A., ... & Abajian, C. (2012). An anatomically comprehensive
    atlas of the adult human brain transcriptome. Nature, 489(7416), 391.
    """
```

and the following functions to `abagen.io`:

```python
def read_tpm():
```

```python
def read_counts():

```

```python
def read_genes():
```

should be a good start! The current `abagen.io.read_annotation` and `abagen.io.read_ontology` work for the ontology and annotation files shipped with the RNA sequencing data, already, too.

Pull request description:
[ENH] Adds fetchers / loaders for RNAseq data
Closes #98 

Adds new functions for fetching and loading RNAseq data. Modifies other related functions / updates some doc-strings as necessary (e.g., `io.read_annotation` doc-string).

The changes to `abagen.get_expression_data()` (i.e., the call to `fetch_rnaseq`) should never actually be trigged, currently, until #137 is addressed (we explicitly check the provided `probe_selection` method against `probes_.SELECTION_METHODS`, and since 'rnaseq' isn't a valid option it will error out before hitting the fetcher).

Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index e38c490..716b8af 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -89,7 +89,7 @@ jobs:
       - persist_to_workspace:
           root: /tmp
           paths:
-              - src/data/allenbrain
+              - src/data/microarray
   style_check:
     executor: conda_exec
     steps:
diff --git a/abagen/__init__.py b/abagen/__init__.py
index 43f21fe..abc1a20 100644
--- a/abagen/__init__.py
+++ b/abagen/__init__.py
@@ -2,7 +2,7 @@ __all__ = [
     '__version__', '__doc__',
     'io', 'mouse', 'get_expression_data', 'keep_stable_genes',
     'normalize_expression', 'remove_distance', 'fetch_desikan_killiany',
-    'fetch_gene_group', 'fetch_microarray', 'fetch_raw_mri'
+    'fetch_gene_group', 'fetch_microarray', 'fetch_raw_mri', 'fetch_rnaseq'
 ]
 
 from ._version import get_versions
@@ -15,4 +15,4 @@ from . import io, mouse
 from .allen import get_expression_data
 from .correct import keep_stable_genes, normalize_expression, remove_distance
 from .datasets import (fetch_desikan_killiany, fetch_gene_group,
-                       fetch_microarray, fetch_raw_mri)
+                       fetch_microarray, fetch_raw_mri, fetch_rnaseq)
diff --git a/abagen/allen.py b/abagen/allen.py
index 46bbe69..337ccad 100644
--- a/abagen/allen.py
+++ b/abagen/allen.py
@@ -153,7 +153,9 @@ def get_expression_data(atlas, atlas_info=None, *,
     donors : list, optional
         List of donors to use as sources of expression data. Can be either
         donor numbers or UID. If not specified will use all available donors.
-        Default: 'all'
+        Note that donors '9861' and '10021' have samples from both left + right
+        hemispheres; all other donors have samples from the left hemisphere
+        only. Default: 'all'
     data_dir : str, optional
         Directory where expression data should be downloaded (if it does not
         already exist) / loaded. If not specified will use the current
@@ -172,12 +174,12 @@ def get_expression_data(atlas, atlas_info=None, *,
         Microarray expression for `R` regions in `atlas` for `G` genes,
         aggregated across donors, where the index corresponds to the unique
         integer IDs of `atlas` and the columns are gene names. If
-        ``return_donors`` is set to ``True`` then this is a list of (R, G)
-        dataframes, one for each donor.
+        ``return_donors=True`` then this is a list of (R, G) dataframes, one
+        for each donor.
     counts : (R, D) pandas.DataFrame
         Number of samples assigned to each of `R` regions in `atlas` for each
         of `D` donors (if multiple donors were specified); only returned if
-        ``return_counts`` is set to ``True``.
+        ``return_counts=True``.
 
     Notes
     -----
@@ -285,6 +287,9 @@ def get_expression_data(atlas, atlas_info=None, *,
         raise ValueError('Cannot use diff_stability for probe_selection with '
                          'only one donor. Please specify a different probe_'
                          'selection method or use more donors.')
+    elif probe_selection == 'rnaseq':  # fetch RNAseq if we're gonna need it
+        datasets.fetch_rnaseq(data_dir=data_dir, donors=donors,
+                              verbose=verbose)
 
     # get some info on labels in `atlas_img`
     all_labels = utils.get_unique_labels(atlas)
diff --git a/abagen/datasets/__init__.py b/abagen/datasets/__init__.py
index 071e0b7..22c762d 100644
--- a/abagen/datasets/__init__.py
+++ b/abagen/datasets/__init__.py
@@ -5,9 +5,9 @@ dataset
 
 __all__ = [
     'fetch_microarray', 'fetch_raw_mri', 'fetch_desikan_killiany',
-    'fetch_gene_group', 'WELL_KNOWN_IDS', '_get_dataset_dir'
+    'fetch_gene_group', 'fetch_rnaseq', 'WELL_KNOWN_IDS', '_get_dataset_dir'
 ]
 
 from .fetchers import (fetch_microarray, fetch_raw_mri, fetch_desikan_killiany,
-                       fetch_gene_group, WELL_KNOWN_IDS)
+                       fetch_gene_group, fetch_rnaseq, WELL_KNOWN_IDS)
 from .utils import _get_dataset_dir
diff --git a/abagen/datasets/fetchers.py b/abagen/datasets/fetchers.py
index 208d616..3b027e4 100644
--- a/abagen/datasets/fetchers.py
+++ b/abagen/datasets/fetchers.py
@@ -12,7 +12,6 @@ import pandas as pd
 
 from .. import io
 from .utils import _get_dataset_dir, _fetch_files
-
 WELL_KNOWN_IDS = Recoder(
     (('9861', 'H0351.2001', '178238387', '157722636', '157722638'),
      ('10021', 'H0351.2002', '178238373', '157723301', '157723303'),
@@ -20,7 +19,7 @@ WELL_KNOWN_IDS = Recoder(
      ('15496', 'H0351.1015', '178238266', '162021642', '162021644'),
      ('14380', 'H0351.1012', '178238316', '157721937', '157721939'),
      ('15697', 'H0351.1016', '178236545', '157682966', '157682968')),
-    fields=('subj', 'uid', 'url', 't1w', 't2w',)
+    fields=('subj', 'uid', 'url', 't1w', 't2w')
 )
 
 VALID_DONORS = sorted(WELL_KNOWN_IDS.value_set('subj')
@@ -68,7 +67,7 @@ def fetch_microarray(data_dir=None, donors=None, resume=True, verbose=1,
 
     url = "https://human.brain-map.org/api/v2/well_known_file_download/{}"
 
-    dataset_name = 'allenbrain'
+    dataset_name = 'microarray'
     data_dir = _get_dataset_dir(dataset_name, data_dir=data_dir,
                                 verbose=verbose)
 
@@ -76,24 +75,11 @@ def fetch_microarray(data_dir=None, donors=None, resume=True, verbose=1,
                  'Ontology.csv', 'PACall.csv',
                  'Probes.csv', 'SampleAnnot.csv')
     n_files = len(sub_files)
+    donors = check_donors(donors)
 
     if n_proc < 0:
         n_proc = mp.cpu_count() + n_proc + 1
 
-    if donors is None:
-        donors = ['12876']
-    elif donors == 'all':
-        donors = list(WELL_KNOWN_IDS.value_set('subj'))
-    elif isinstance(donors, str):
-        donors = [donors]
-
-    for n, sub_id in enumerate(donors):
-        if sub_id not in VALID_DONORS:
-            raise ValueError('Invalid subject id: {0}. Subjects must in: {1}.'
-                             .format(sub_id, VALID_DONORS))
-        donors[n] = WELL_KNOWN_IDS[sub_id]  # convert to ID system
-    donors = sorted(set(donors), key=lambda x: donors.index(x))
-
     files = [
         [(os.path.join('normalized_microarray_donor{}'.format(sub), fname),
             url.format(WELL_KNOWN_IDS.url[sub]),
@@ -132,6 +118,75 @@ def fetch_microarray(data_dir=None, donors=None, resume=True, verbose=1,
     )
 
 
+def fetch_rnaseq(data_dir=None, donors=None, resume=True, verbose=1):
+    """
+    Downloads RNA-sequencing data from the Allen Human Brain Atlas
+
+    Parameters
+    ----------
+    data_dir : str, optional
+        Directory where data should be downloaded and unpacked. Default:
+        current directory
+    donors : list, optional
+        List of donors to download; can be either donor number or UID. Can also
+        specify 'all' to download all available donors (two). Default: 9861
+    resume : bool, optional
+        Whether to resume download of a partly-downloaded file. Default: True
+    verbose : int, optional
+        Verbosity level (0 means no message). Default: 1
+
+    Returns
+    -------
+    data : dict
+        Dictionary with keys ['counts', 'tpm', 'ontology', 'genes',
+        'annotation'], where corresponding values are lists of filepaths to
+        downloaded CSV files.
+
+    References
+    ----------
+    Hawrylycz, M. J., Lein, E. S., Guillozet-Bongaarts, A. L., Shen, E. H., Ng,
+    L., Miller, J. A., ... & Abajian, C. (2012). An anatomically comprehensive
+    atlas of the adult human brain transcriptome. Nature, 489(7416), 391.
+    """
+
+    url = "https://human.brain-map.org/api/v2/well_known_file_download/{}"
+    well_known_ids = {
+        '9861': '278447594',
+        '10021': '278448166'
+    }
+
+    dataset_name = 'rnaseq'
+    data_dir = _get_dataset_dir(dataset_name, data_dir=data_dir,
+                                verbose=verbose)
+
+    sub_files = ('Contents.txt', 'Genes.csv', 'Ontology.csv',
+                 'RNAseqCounts.csv', 'RNAseqTPM.csv', 'SampleAnnot.csv')
+    n_files = len(sub_files)
+    valid = ['9861', '10021', 'H0351.2001', 'H0351.2002']
+    donors = check_donors(donors, default=valid[0], valid=valid)
+
+    files = [
+        [(os.path.join('rnaseq_donor{}'.format(sub), fname),
+            url.format(well_known_ids[sub]),
+            dict(uncompress=True,
+                 move=os.path.join('rnaseq_donor{}'.format(sub),
+                                   'donor{}.zip'.format(sub))))
+         for fname in sub_files]
+        for sub in donors
+    ]
+
+    files = [l for f in files for l in f]
+    files = _fetch_files(data_dir, files, resume=resume, verbose=verbose)
+
+    return dict(
+        genes=files[1::n_files],
+        ontology=files[2::n_files],
+        counts=files[3::n_files],
+        tpm=files[4::n_files],
+        annotation=files[5::n_files]
+    )
+
+
 def fetch_raw_mri(data_dir=None, donors=None, resume=True, verbose=1):
     """
     Downloads the "raw" Allen Human Brain Atlas T1w/T2w MRI images
@@ -158,31 +213,18 @@ def fetch_raw_mri(data_dir=None, donors=None, resume=True, verbose=1):
 
     url = "https://human.brain-map.org/api/v2/well_known_file_download/{}"
 
-    dataset_name = 'allenbrain'
+    dataset_name = 'mri'
     data_dir = _get_dataset_dir(dataset_name, data_dir=data_dir,
                                 verbose=verbose)
 
     sub_files = dict(t1w='T1.nii.gz', t2w='T2.nii.gz')
     n_files = len(sub_files)
-
-    if donors is None:
-        donors = ['12876']
-    elif donors == 'all':
-        donors = list(WELL_KNOWN_IDS.value_set('subj'))
-    elif isinstance(donors, str):
-        donors = [donors]
-
-    for n, sub_id in enumerate(donors):
-        if sub_id not in VALID_DONORS:
-            raise ValueError('Invalid subject id: {0}. Subjects must in: {1}.'
-                             .format(sub_id, VALID_DONORS))
-        donors[n] = WELL_KNOWN_IDS[sub_id]  # convert to ID system
-    donors = sorted(set(donors), key=lambda x: donors.index(x))
+    donors = check_donors(donors)
 
     files = [
-        (os.path.join('normalized_microarray_donor{}'.format(sub), fname),
+        (os.path.join('mri_donor{}'.format(sub), fname),
          url.format(getattr(WELL_KNOWN_IDS, img)[sub]),
-         dict(move=os.path.join('normalized_microarray_donor{}'.format(sub),
+         dict(move=os.path.join('mri_donor{}'.format(sub),
                                 fname)))
         for sub in donors
         for img, fname in sub_files.items()
@@ -196,6 +238,45 @@ def fetch_raw_mri(data_dir=None, donors=None, resume=True, verbose=1):
     )
 
 
+def check_donors(donors, default='12876', valid=VALID_DONORS):
+    """
+    Checks that provided `donors` are valid
+
+    Parameters
+    ----------
+    donors : list of str
+        List of donors to download; can be either donor number or UID. Can also
+        specify 'all' to download all available donors. If 'None' is provided
+        then `default` will be used.
+    default : str, optional
+        Default donor to use if `donors` is None. Default: '12876'
+    valid : list of str, optional
+        List of valid donnor numbers and UIDs. Default: :obj:`VALID_DONORS`
+
+    Returns
+    -------
+    donors : list of str
+        Donor subject IDs
+    """
+
+    if donors is None:
+        donors = [default]
+    elif donors == 'all':
+        donors = valid
+    elif isinstance(donors, str):
+        donors = [donors]
+
+    donors = list(donors)
+    for n, sub_id in enumerate(donors):
+        if sub_id not in valid:
+            raise ValueError('Invalid subject id: {0}. Subjects must in: {1}.'
+                             .format(sub_id, valid))
+        donors[n] = WELL_KNOWN_IDS[sub_id]  # convert to ID system
+    donors = sorted(set(donors), key=lambda x: int(x))
+
+    return donors
+
+
 def fetch_desikan_killiany(*args, **kwargs):
     """
     Fetches Desikan-Killiany atlas shipped with `abagen`
@@ -213,6 +294,7 @@ def fetch_desikan_killiany(*args, **kwargs):
     for subdividing the human cerebral cortex on MRI scans into gyral based
     regions of interest. Neuroimage, 31(3), 968-980.
     """
+
     # grab resource filenames
     image = resource_filename('abagen', 'data/atlas-desikankilliany.nii.gz')
     info = resource_filename('abagen', 'data/atlas-desikankilliany.csv')
diff --git a/abagen/io.py b/abagen/io.py
index 204cf6c..22b797e 100644
--- a/abagen/io.py
+++ b/abagen/io.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 """
-Functions for loading the various files associated with the AHBA microarray
-dataset.
+Functions for loading the various files associated with the AHBA microarray and
+RNAseq datasets.
 
 This also contains functionality for optionally converting the downloaded CSV
 files to parquet format, which provides much faster I/O access / quicker load
@@ -266,11 +266,26 @@ def read_annotation(fname, copy=False):
     annotation : (S, 13) pandas.DataFrame
         Dataframe containing structural information on `S` samples. The row
         index is the unique sample ID (integer, beginning with 1) which can be
-        used to match data to the information obtained with
-        :func:`read_microarray` and :func:`read_pacall`. Columns include
-        'structure_id', 'slab_num', 'well_id', 'slab_type',
-        'structure_acronym', 'structure_name', 'polygon_id', 'mri_voxel_x',
-        'mri_voxel_y', 'mri_voxel_z', 'mni_x', 'mni_y', 'mni_z'.
+        used to match data to the information obtained with e.g.,
+        :func:`read_microarray`.
+
+    Notes
+    -----
+    If the provided annotation file is from microarray expression data
+    (obtained by, e.g., `abagen.fetch_microarray()`), then the returned
+    DataFrame will have the following columns: 'structure_id', 'slab_num',
+    'well_id', 'slab_type', 'structure_acronym', 'structure_name',
+    'polygon_id', 'mri_voxel_x', 'mri_voxel_y', 'mri_voxel_z', 'mni_x',
+    'mni_y', 'mni_z'.
+
+    If the provided annotation file is from RNAseq data (obtained by, e.g.,
+    `abagen.fetch_rnaseq()`), then the returned DataFrame will have the
+    following columns: 'RNAseq_sample_name', 'replicate_sample', 'sample_name',
+    'well_id', 'microarray_run_id', 'ontology_color', 'main_structure',
+    'sub_structure', 'ontology_structure_id', 'ontology_structure_acronym',
+    'hemisphere', 'brain', 'million_clusters', 'clip_percentage',
+    'RIN_RNA_squality', 'rnaseq_run_id', 'A.Pct', 'C.Pct', 'G.Pct', 'T.Pct',
+    'N.Pct'
     """
 
     try:
@@ -283,3 +298,122 @@ def read_annotation(fname, copy=False):
         data = fname.copy() if copy else fname
 
     return data
+
+
+def read_tpm(fname, copy=False):
+    """
+    Loads RNAseqTPM.csv file found at `fname`
+
+    RNAseq TPM files contain TPM values for all the tissue samples taken from a
+    single donor across all genes. TPM values are scaled fragment (read) counts
+    derived using RSEM.
+
+    Parameters
+    ----------
+    fname : str
+        Path to RNAseqTPM.csv file
+    copy : bool, optional
+        Whether to return a copy if `fname` is a pre-loaded pandas.Dataframe.
+        Default: False
+
+    Returns
+    -------
+    tpm : (G, S) pandas.DataFrame
+        Dataframe containing RNAseq TPM expression data, where `G` is genes
+        and `S` is samples. The row index is the unique gene symbol assigned
+        during processing, which can be used to match data to the information
+        obtained with :func:`read_genes`. The column index is the unique
+        sample ID (integer, beginning at 0) which can be used to match data to
+        the information obtained with :func:`read_annotation`.
+    """
+
+    try:
+        data = pd.read_csv(fname, header=None, index_col=0)
+        data.index.name = 'gene_symbol'
+        data.columns = pd.Series(range(len(data.columns)), name='sample_id')
+    except ValueError:
+        if not isinstance(fname, pd.DataFrame):
+            raise TypeError('Provided fname must be filepath to RNAseqTPM'
+                            '.csv file from Allen Human Brain Atlas.')
+        data = fname.copy() if copy else fname
+
+    return data
+
+
+def read_counts(fname, copy=False):
+    """
+    Loads RNAseqCounts.csv file found at `fname`
+
+    RNAseq count files contain fragment counts for all the tissue samples taken
+    from a single donor across all genes. Fragment counts can be fractional, as
+    ambiguous reads are distributed between relevant transcripts. For present /
+    absent calling, a value of zero indicates no transcript was seen.
+
+    Parameters
+    ----------
+    fname : str
+        Path to RNAseqCounts.csv file
+    copy : bool, optional
+        Whether to return a copy if `fname` is a pre-loaded pandas.Dataframe.
+        Default: False
+
+    Returns
+    -------
+    tpm : (G, S) pandas.DataFrame
+        Dataframe containing RNAseq count expression data, where `G` is genes
+        and `S` is samples. The row index is the unique gene symbol assigned
+        during processing, which can be used to match data to the information
+        obtained with :func:`read_genes`. The column index is the unique
+        sample ID (integer, beginning at 0) which can be used to match data to
+        the information obtained with :func:`read_annotation`.
+    """
+
+    try:
+        data = pd.read_csv(fname, header=None, index_col=0)
+        data.index.name = 'gene_symbol'
+        data.columns = pd.Series(range(len(data.columns)), name='sample_id')
+    except ValueError:
+        if not isinstance(fname, pd.DataFrame):
+            raise TypeError('Provided fname must be filepath to RNAseqCounts'
+                            '.csv file from Allen Human Brain Atlas.')
+        data = fname.copy() if copy else fname
+
+    return data
+
+
+def read_genes(fname, copy=False):
+    """
+    Loads Genes.csv file found at `fname`
+
+    Genes files contain metadata on all genes used in the RNAseq AHBA data.
+    These files should be the same for every donor.
+
+    Parameters
+    ----------
+    fname : str
+        Path to Genes.csv file
+    copy : bool, optional
+        Whether to return a copy if `fname` is a pre-loaded pandas.Dataframe.
+        Default: False
+
+    Returns
+    -------
+    genes : (G, 11) pandas.DataFrame
+        Dataframe containing information for `G` unique genes. The row index
+        is the unique gene symbol which can be used to match metadata to
+        information obtained with :func:`read_tpm` and :func:`read_counts`.
+        Columns include 'gene_id', 'entrez_id', 'chromosome', 'strand',
+        'number_of_transcripts', 'median_transcriptome_length',
+        'median_genome_length', 'median_number_of_exons', 'median_gene_start',
+        and 'median_gene_end'
+    """
+
+    try:
+        data = pd.read_csv(fname, index_col=0)
+    except ValueError:
+        if not isinstance(fname, pd.DataFrame):
+            raise TypeError('Provided fname must be filepath to Annotation'
+                            '.csv file from Allen Human Brain Atlas.')
+        data = fname.copy() if copy else fname
+
+    return data
diff --git a/docs/api.rst b/docs/api.rst
index 6d0d82e..8c70107 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -63,6 +63,8 @@ Reference API
    abagen.io.read_pacall
    abagen.io.read_probes
 
+.. _ref_correct:
+
 :mod:`abagen.correct` - Post-processing corrections
 ---------------------------------------------------
 .. automodule:: abagen.correct
@@ -79,38 +81,7 @@ Reference API
    abagen.keep_stable_genes
    abagen.normalize_expression
 
-:mod:`abagen.probes` - Operations on microarry probes
------------------------------------------------------
-.. automodule:: abagen.probes
-   :no-members:
-   :no-inherited-members:
-
-.. currentmodule:: abagen.probes
-
-.. autosummary::
-   :template: function.rst
-   :toctree: generated/
-
-   abagen.probes.reannotate_probes
-   abagen.probes.filter_probes
-   abagen.probes.collapse_probes
-
-:mod:`abagen.samples` - Operations on microarray samples
---------------------------------------------------------
-.. automodule:: abagen.samples
-   :no-members:
-   :no-inherited-members:
-
-.. currentmodule:: abagen.samples
-
-.. autosummary::
-   :template: function.rst
-   :toctree: generated/
-
-   abagen.samples.update_mni_coords
-   abagen.samples.drop_mismatch_samples
-   abagen.samples.label_samples
-   abagen.samples.mirror_samples
+.. _ref_utils:
 
 :mod:`abagen.utils` - Utility functions
 ---------------------------------------


Test patch:
diff --git a/abagen/tests/conftest.py b/abagen/tests/conftest.py
index ba35d48..20733d0 100644
--- a/abagen/tests/conftest.py
+++ b/abagen/tests/conftest.py
@@ -6,7 +6,9 @@ Fixtures for all abagen tests
 import os
 import pytest
 
-from abagen.datasets import fetch_desikan_killiany, fetch_microarray
+from abagen.datasets import (fetch_desikan_killiany,
+                             fetch_microarray,
+                             fetch_rnaseq)
 
 
 @pytest.fixture(scope='session')
@@ -23,6 +25,11 @@ def testfiles(datadir):
                             n_proc=2)
 
 
+@pytest.fixture(scope='session')
+def rnafiles(datadir):
+    return fetch_rnaseq(data_dir=datadir, donors=['9861'])
+
+
 @pytest.fixture(scope='session')
 def atlas():
     return fetch_desikan_killiany()
diff --git a/abagen/tests/datasets/test_fetchers.py b/abagen/tests/datasets/test_fetchers.py
index 3a9d16a..9501f52 100644
--- a/abagen/tests/datasets/test_fetchers.py
+++ b/abagen/tests/datasets/test_fetchers.py
@@ -52,6 +52,23 @@ def test_fetch_raw_mri():
         fetchers.fetch_raw_mri(donors=['notadonor'])
 
 
+def test_fetch_rnaseq():
+    f1 = fetchers.fetch_rnaseq(donors=['9861'])
+    f2 = fetchers.fetch_rnaseq(donors='9861')
+    f3 = fetchers.fetch_rnaseq(donors='H0351.2001')
+    f4 = fetchers.fetch_rnaseq(donors=None)
+
+    assert f1 == f2 == f3 == f4
+    for k in ['genes', 'ontology', 'counts', 'tpm', 'annotation']:
+        assert len(f1.get(k)) == 1
+
+    with pytest.raises(ValueError):
+        fetchers.fetch_rnaseq(donors='notadonor')
+
+    with pytest.raises(ValueError):
+        fetchers.fetch_rnaseq(donors=['9861', 'notadonor'])
+
+
 @pytest.mark.parametrize('group, expected', [
     ('brain', 2413),
     ('neuron', 2530),
diff --git a/abagen/tests/test_io.py b/abagen/tests/test_io.py
index 8b48c15..aeb4b9c 100644
--- a/abagen/tests/test_io.py
+++ b/abagen/tests/test_io.py
@@ -27,6 +27,19 @@ probe_cols = [
     'probe_name', 'gene_id', 'gene_symbol', 'gene_name', 'entrez_id',
     'chromosome'
 ]
+gene_cols = [
+    'gene_id', 'entrez_id', 'chromosome', 'strand', 'number_of_transcripts',
+    'median_transcriptome_length', 'median_genome_length',
+    'median_number_of_exons', 'median_gene_start', 'median_gene_end'
+]
+rna_annot_cols = [
+    'RNAseq_sample_name', 'replicate_sample', 'sample_name', 'well_id',
+    'microarray_run_id', 'ontology_color', 'main_structure',
+    'sub_structure', 'ontology_structure_id', 'ontology_structure_acronym',
+    'hemisphere', 'brain', 'million_clusters', 'clip_percentage',
+    'RIN_RNA_quality', 'rnaseq_run_id', 'A.Pct', 'C.Pct', 'G.Pct', 'T.Pct',
+    'N.Pct'
+]
 
 
 @pytest.mark.parametrize('key, has_parq, columns', [
@@ -71,3 +84,45 @@ def test_readfiles(testfiles, key, has_parq, columns):
 
         with pytest.raises(FileNotFoundError):
             func('notafile')
+
+
+@pytest.mark.parametrize('key, columns', [
+    ('genes', gene_cols),
+    ('ontology', ont_cols),
+    ('counts', None),
+    ('tpm', None),
+    ('annotation', rna_annot_cols)
+])
+def test_readrnaseq(rnafiles, key, columns):
+    for fn in rnafiles.get(key):
+        func = getattr(io, 'read_{}'.format(key))
+
+        # check file exists
+        assert op.exists(fn)
+
+        # check loading from filepath
+        data = func(fn)
+        assert isinstance(data, pd.DataFrame)
+
+        # check loading from dataframe (should return same object)
+        data2 = func(data)
+        assert id(data) == id(data2)
+
+        # check that copy parameter works as expected
+        data3 = func(data, copy=True)
+        assert isinstance(data3, pd.DataFrame)
+        assert id(data) != id(data3)
+
+        # confirm columns are as expected
+        if columns is not None:
+            assert np.all(columns == data.columns)
+
+        # confirm errors
+        with pytest.raises(TypeError):
+            func(1)
+
+        with pytest.raises(TypeError):
+            func([1, 2, 3])
+
+        with pytest.raises(FileNotFoundError):
+            func('notafile')