Split a 10xscATAC bam file by cluster

I want to split the PBMC scATAC bam from 10x by cluster id. So, I can then make a bigwig for each cluster to visualize in IGV.

The first thing I did was googling to see if anyone has written such a tool (Do not reinvent the wheels!). People have done that because I saw figures from the scATAC papers. I just could not find it. Maybe I need to refine my googling skills.

I decided to write one myself. The following is my journey for this small task.

download the 5k pbmc scATAC data from https://support.10xgenomics.com/single-cell-atac/datasets/1.0.1/atac_v1_pbmc_5k

split the cell barcodes by cluster id

cd analysis/clustering/graphclust
head clusters.csv
Barcode,Cluster
AAACGAAAGCGCAATG-1,1
AAACGAAAGGGTATCG-1,4
AAACGAAAGTAACATG-1,8
AAACGAAAGTTACACC-1,1
AAACGAACAGAGATGC-1,4
AAACGAACATGCTATG-1,5
AAACGAAGTGCATCAT-1,3
AAACGAAGTGGACGAT-1,3
AAACGAAGTGGCCTCA-1,7

# there are ^M characters at the end of the line if you do cat -A you will see it.
# change it to unix
dos2unix clusters.csv
awk -F"," 'NR>1{print $1 >> "cluster_"$2".csv"}' clusters.csv
wc -l *csv
   330 cluster_10.csv
   322 cluster_11.csv
   258 cluster_12.csv
   191 cluster_13.csv
   608 cluster_1.csv
   563 cluster_2.csv
   559 cluster_3.csv
   532 cluster_4.csv
   483 cluster_5.csv
   425 cluster_6.csv
   366 cluster_7.csv
   360 cluster_8.csv
   338 cluster_9.csv
  5336 clusters.csv

use bamtools

time bamtools filter -tag CB:Z:AAACTGCAGAGCAGCT-1 -in atac_v1_pbmc_5k_possorted_bam.bam -out AAACTGCAGAGCAGCT-1.bam

This takes a little over 1 hour for one barcode! And there is no easy way to specify a group of barcodes.

use the linux tricks

inspired partly by this post https://www.biostars.org/p/263346/

wc -l clusters.csv
5336 clusters.csv

and let’s see how fast each regular expression takes for awk

time samtools view atac_v1_pbmc_5k_possorted_bam.bam | awk -v tag="CB:Z:AAACTGCAGAGCAGCT-1" 'index($0,tag)>0' >> AAACTGCAGAGCAGCT-1.sam

real    27m14.332s
user    48m36.883s
sys     4m37.908s

It is not too bad, but if we loops over the clusters.csv files for 5335 times,

samtools view -H atac_v1_pbmc_5k_possorted_bam.bam > header.txt

cat clusters.csv \
| sed '1d' \
| while IFS=',' read -r barcode cluster
    do samtools view atac_v1_pbmc_5k_possorted_bam.bam |  awk -v tag="CB:Z:$barcode" 'index($0,tag)>0' >> "$cluster.sam"
    done

## then cat the header with the sam.

it will take ~30min * 5335 = ~100 days to finish.

we can do better to parallize by GNU parallel

## not tested...
cat clusters.csv \
| sed '1d' \
| parallel --colsep ',' -j 40 'samtools view atac_v1_pbmc_5k_possorted_bam.bam |awk -v tag="CB:Z:{1}" 'index(\$0,tag)>0' >> {2}.sam'

Again, using 40 cores may reduce our time to 10040 = 5 days.

use pysam

Let’s only loop over the sam file once

import pysam
import csv

cluster_dict = {}
with open('clusters.csv') as csv_file:
    csv_reader = csv.reader(csv_file, delimiter=',')
    #skip header
    header = next(csv_reader)
    for row in csv_reader:
        cluster_dict[row[0]] = row[1]

clusters = set(x for x in cluster_dict.values())


fin = pysam.AlignmentFile("atac_v1_pbmc_5k_possorted_bam.bam", "rb")

# open the number of bam files as the same number of clusters, and map the out file handler to the cluster id, write to a bam with wb
fouts_dict = {}
for cluster in clusters:
    fout = pysam.AlignmentFile("cluster" + cluster + ".bam", "wb", template = fin)
    fouts_dict[cluster] = fout

for read in fin:
    tags = read.tags
    CB_list = [ x for x in tags if x[0] == "CB"]
    if CB_list:
        cell_barcode = CB_list[0][1]
    # the bam files may contain reads not in the final clustered barcodes
    # will be None if the barcode is not in the clusters.csv file
    else: 
        continue
    cluster_id = cluster_dict.get(cell_barcode)
    if cluster_id:
        fouts_dict[cluster_id].write(read)

## do not forget to close the files
fin.close()
for fout in fouts_dict.values():
    fout.close()

real 172m58.758s user 172m10.678s sys 0m46.071s

Note, some read record in the bam file do not have CB but only CR.

from https://support.10xgenomics.com/single-cell-atac/software/pipelines/latest/output/bam

Tag Description
CB Chromium cellular barcode sequence that is error-corrected and confirmed against a list of known-good barcode sequences.
CR Chromium cellular barcode sequence as reported by the sequencer.

How many of those reads with CR?

# every read has a CR tag
samtools view atac_v1_pbmc_5k_possorted_bam.bam| grep -v "CR" | wc -l
0 

# not every read has a CB tag.
samtools view atac_v1_pbmc_5k_possorted_bam.bam| grep -v "CB" | wc -l
10647804

use pybam

https://www.biostars.org/p/186732/

https://github.com/JohnLonginotto/pybam

Note that pybam is python2.x

source activate py27
cd ~/apps
git clone https://github.com/JohnLonginotto/pybam

inside python:

import pybam
import csv

cluster_dict = {}
with open('clusters.csv') as csv_file:
    csv_reader = csv.reader(csv_file, delimiter=',')
    #skip header
    header = next(csv_reader)
    for row in csv_reader:
        cluster_dict[row[0]] = row[1]

clusters = set(x for x in cluster_dict.values())


# open the number of bam files as the same number of clusters, and map the out file handler to the cluster id

header = pybam.read('atac_v1_pbmc_5k_possorted_bam.bam').file_header
fouts_dict = {}
for cluster in clusters:
    fout = open("cluster" + cluster + ".sam", "w")
    fout.write(header)
    fouts_dict[cluster] = fout

for read in pybam.read('possorted_bam.bam'):
        ## not always the same position in the list for the CB tag
        ## there could be no CB tag for a certian read as well
        ## it will return empty list
        CB_list = [ x for x in read.sam_tags_list if x[0] == "CB"]
        if CB_list:
            cell_barcode = CB_list[0][2]
            cluster_id = cluster_dict.get(cell_barcode)
            if cluster_id:
                fouts_dict[cluster_id].write(read.sam + '\n')
        
## do not forget to close the files
for fout in fouts_dict.values():
    fout.close()

real 1262m30.849s user 1240m11.906s sys 22m9.325s

Did not find how to write to a bam file, so I have to write to a sam file. I asked on github issues but no responses. The author is not actively maintaining the library anymore.

use hts-nim

https://github.com/brentp/hts-nim-tools/issues/5

Thanks Brent for providing the code.

htslib need to be in $LD_LIBRARY_PATH:

wget https://github.com/samtools/htslib/releases/download/1.6/htslib-1.6.tar.bz2
tar xjf htslib-1.6.tar.bz2
cd htslib-1.6
./configure ~/bin/

make

# add this to .bashrc and source ~/.bashrc
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/n/home02/mtang/apps/htslib-1.6

install nim and hts-nim

wget https://raw.githubusercontent.com/brentp/hts-nim/master/scripts/simple-install.sh

chmod u+x simple-install.sh
./simple-install.sh

# add nim to PATH

git clone https://github.com/brentp/hts-nim
cd hts-nim
nimble install -y
import hts
import os
import strutils
import tables
var ibam:Bam

# lookup from cb -> cluster
var clusterTbl = initTable[string,string]()
# lookup from cluster -> bam
var tbl = initTable[string, Bam]()

for x in paramStr(1).lines:
  var toks = x.strip().split(",")
  clusterTbl[toks[0]] = toks[1]

if not open(ibam, paramStr(2)):
   quit "couldn't open bam"

for aln in ibam:
  var cb = tag[string](aln, "CB").get
  if cb.isNullOrEmpty: continue
  if cb notin clusterTbl: continue
  var cluster = clusterTbl[cb]
  if cluster notin tbl:
    var obam: Bam
    if not open(obam, "out-cluster-" & cluster & ".bam", mode="w"):
      quit "couldn't open bam for writing"
    obam.write_header(ibam.hdr)
    tbl[cluster] = obam
  tbl[cluster].write(aln)

for k, bam in tbl:
  bam.close()
ibam.close()

compile

save it to split_scATAC_bam.nim and compile:

nim compile -d:release scATAC_split_scATAC_bam.nim
split_scATAC_bam clusters.csv atac_v1_pbmc_5k_possorted_bam.bam

real 105m17.140s user 102m17.214s sys 2m58.312s

it is 172105 ~1.6 times faster in hts-nim than in pysam.

speed up

  • parallize by chromosome
  • pysam parallization
  • hts-nim from Brent:

you can add threads=2 (or 3) to the open calls to get a bit more speed on de/compressing the bam which will be the most CPU time

I tested using threads = 3 for the same bam file, it took

real 92m11.205s user 100m11.622s sys 6m3.067s

one saved another 105-92 = 13 mins using multi-thread hts-nim.

  • C htslib, I expect the speed will be similar to hts-nim since hts-nim is a wrapper around it.

Lessons learned

I had a bug in my pysam code and it pulls out some reads without the CB tag. Thanks Brent for catching it. I spent some time to debug and could not find it.

Lessons that I have learned:

  • How to make sure the output of the software is correct is very difficult. unit testing is important.
  • It is good to have someone else with more programming experience to look at the code for you. You are so used to the code that you write and can not find the “obvious” problem.
  • Do not use libraries that are not well maintained. The pybam author is not maintaining the library now and it is written in python2.x. I am writing all my python code in python3.x

I have put the python and nim code at scATACutils. The pysam code and hts-nim code generate exactly the same results.

Related

Next
Previous
comments powered by Disqus