Gray wolf dataset

This section will focus on analyzing SNP data from North American gray wolves (Canis lupus). The original data consist of a few hundred wolf samples that were enriched for a custom capture array consisting of several thousands of regions across the wolf genome.

Our data consist of genic SNPs genotyped for three wolf ecotypes: (1) the British Columbia coastal, (2) the High Arctic, and (3) the Boreal Forest. These wolves have pretty striking ecological and behavioral differences that are in part driven by differences amongst their environments. We used these data to look at population genetic variation and signals of local adaptation amongst these ecotypes (Schweizer et al. 2015, Molecular Ecology).

Figure 5.1: Ranges and ecotypes of sampled gray wolf populations.

This data are also included in the project repo you previously cloned. Let's navigate to the wolf-snps folder now to minimize the paths we have to type when using the data. You should already be in the root project directory (~/congen-bioinformatics/), so you should just have to type the command:

cd data/wolf-snps/
Click to see command breakdown
Command line parameterDescription
cdThe Linux change directory command
data/wolf-snpsThe path to the directory you want to change to. In this case, this is a relative path. If you are not in the ~/congen-bioinformatics/ directory, you will likely get an error saying this path does not exist.

If you are somewhere else other than the root project directory, first navigate back to that folder and then change to the wolf-snps folder. Or, if you're familiar enough with the file tree, navigate straight to the data folder!

Tip - Remember the pwd command!

Lost somewhere in the file system? Not sure exactly where you are? Remember the print working directory command: pwd. This simply prints your current location in the file system and can help you get your bearings if you've been moving around a lot. This and ls are essential for navigating your file system.


VCF files and PLINK

I've downsampled the full data set to a subset of 35 individuals and a subset of genic SNPs. We will be working with these SNP data in a VCF file. VCF stands for variant call format and is a tab-delimited plain text file that stores genetic variants, such as SNPs, insertions, and deletions, found by comparing sequenced samples to a reference genome. Each site is described by its location, alleles, and quality scores, plus each individual's genotype, depth of coverage, and more, at that site. Note that sites can be both variable and non-variable.

The full VCF file format specifications can be found here: samtools VCF specs

VCF files also contain a header, denoted with the # character at the beginning of the line. The header can be a single line or thousands of lines and provides information on how the VCF file was generated, to which reference genome the sequence data was aligned, and definitions of what the various fields in each column of the data mean. Given all this information, VCF files can potentially be enormous in size and cumbersome to work with.

Luckily, there is software out there to help! PLINK is a widely used toolset for working with genotype data. It was originally developed for human genome-wide association studies, but it works with data from any diploid organism and can read VCF files directly. PLINK can be used to filter SNPs and individuals, assess data quality such as missingness, and calculate a variety of statistics like allele frequencies, Hardy-Weinberg equilibrium, and per-individual genotype counts. We'll be using PLINK 2, the current version of the program, which is run with the command plink2.

Unlike many of the Unix commands we've seen so far, PLINK doesn't print its results to the terminal to be piped (|) or redirected (>). Instead, it writes its results to files whose names start with a prefix you choose with the --out option, and the file extension is determined by the analysis. Every run also writes a .log file that records the command you ran and a summary of what PLINK did, which is handy for checking your work later. In general, the program is called as follows:

plink2 <input option> <input file> <analysis options> --out <output prefix>
Click to see command breakdown
Command line parameterDescription
plink2A program for working with genotype data, including variant calls in VCF format
<input option>The option specifying the format of the input file, e.g. --vcf for a VCF file
<input file>The path to the file you want to run on
<analysis options>The analyses to run, plus any options for filtering SNPs or samples
--outThe option to specify the prefix for all output files
<output prefix>The desired prefix for the output files. The file extension will be determined by the analysis.

Here are some more explicit examples of PLINK commands:

# Calculate allele frequencies for every SNP (writes input_freqs.afreq)
plink2 --vcf input.vcf --dog --freq --out input_freqs

# Report the amount of missing data per sample and per SNP (writes input_missing.smiss and input_missing.vmiss)
plink2 --vcf input.vcf --dog --missing --out input_missing

# Keep only the samples listed in a file and write them to a new VCF (writes input_subset.vcf)
plink2 --vcf input.vcf --dog --keep samples.txt --export vcf --out input_subset
Tip - Telling PLINK what organism you're working with

Because PLINK was built for human data, it assumes human chromosomes by default: 22 autosomes plus the sex chromosomes and mitochondria. Wolves have 38 pairs of autosomes, the same as domestic dogs, so we add the --dog option to every PLINK command. Before running PLINK on a new data set, it's a good idea to check how the chromosomes are named in the VCF file:

grep -v "#" Filtered_NAwolf_n35_variableSites_GenicRegions.recode.vcf | cut -f1 | sort | uniq -c

Here, cut -f1 keeps only the first tab-separated column of each line, which in a VCF file is the chromosome name. Then sort | uniq -c groups identical names together and counts how many SNPs are on each chromosome.

If the chromosomes are numbered (like 1 through 38, or chr1 through chr38), --dog will work. If they're scaffold or contig names instead, use the --allow-extra-chr option.


Calculating heterozygosity from a VCF file

For this section of the workshop, we're going to start with a simple task, calculating heterozygosity, for a single wolf ecotype. Then we're going to learn how to automate it over multiple populations through using loops and writing a script.

Before we do that, let's get some basic info about our called variants in the VCF file.

TASK 1: How many SNPs are there?

We know that our VCF file has a number of header lines (which don't count towards the actual number of SNPs) and then a separate line for each SNP. Because of this, if we just use wc -l we will count all lines of the file, which doesn't tell us the number of SNPs.

But by using grep with the -v flag, we can find all the lines of text that don't contain a character, string, or pattern. Then we can use our handy | to get our answer:

grep -v "^#" Filtered_NAwolf_n35_variableSites_GenicRegions.recode.vcf | wc -l
Click to see command breakdown
Command line parameterDescription
grepA Unix string and pattern searching command
-vBy default, grep prints all lines that contain the specified string/pattern. The -v option tells grep to print all the lines that DO NOT contain the specified string/pattern instead.
"#"The string or pattern you want to search for
Filtered_NAwolf_n35_variableSites_GenicRegions.recode.vcfThe path to the file you want to search
|The pipe character means the output from the command specified before it will be used as the input to the command specified after it
wcThe Linux word count command. Counts the number of lines, words, and characters in an input file.
-lTells wc to only count number of lines

This should output the number of lines in the VCF file that don't contain the "#" character:

19620

Since the only lines with "#" should be the informational header lines and all other lines represent one SNP, this means there are 19,620 SNPs represented in this VCF file.

TASK 2: Calculate heterozygosity for the coastal wolf samples.

We're going to use one of the functions of PLINK to count the homozygous and heterozygous genotypes (--sample-counts) of each individual within only the coastal wolf population. PLINK lets us specify which individuals to include in an analysis through the --keep option with a file of sample IDs.

plink2 --vcf Filtered_NAwolf_n35_variableSites_GenicRegions.recode.vcf --dog --keep pop_coastal.txt --sample-counts cols=hom,het --out Filtered_NAwolf_n35_variableSites_GenicRegions_coastal
Click to see command breakdown
Command line parameterDescription
plink2A program for working with genotype data, including variant calls in VCF format
--vcfThe option to specify that the input file is a VCF file
Filtered_NAwolf_n35_variableSites_GenicRegions.recode.vcf The path to the input file
--dogTells PLINK to use the dog chromosome set, which wolves share. Without this, PLINK assumes human chromosomes.
--keepThis option is used with a file that lists sample IDs. Only samples in that file will be used by the program for this run.
pop_coastal.txtThe path to the file with the sample IDs to keep.
--sample-countsThis option tells PLINK to count the genotypes of different types (homozygous, heterozygous, missing, etc.) for each sample.
cols=hom,hetA modifier for --sample-counts that sets which columns to report: here, the number of homozygous genotypes (hom) and heterozygous genotypes (het) for each sample. Without it, PLINK reports a longer default set of columns.
--outThe option to specify the prefix for all output files. The file extension will be determined by the analysis.
Filtered_NAwolf_n35_variableSites_GenicRegions_coastal The desired name of the output file. If it exists it will be overwritten.

Check your work - Read the log file

Every time PLINK runs, it writes a .log file. The log records the exact command you ran, how many samples and variants PLINK loaded, how many remained after filtering, and any warnings. Make a habit of reading it after every run:

cat Filtered_NAwolf_n35_variableSites_GenicRegions_coastal.log

Find the lines reporting how many samples were loaded and how many remained after --keep. Does the number remaining match the number of IDs in the pop file?

wc -l pop_coastal.txt

Try this: Make a copy of the pop file with a typo in one sample ID, then rerun PLINK with the copy. Does PLINK stop with an error? Does it tell you anything in the terminal or the log? If a problem like this doesn't cause an error, the log may be the only place you'd notice it.

sed '1s/.$/X/' pop_coastal.txt > pop_coastal_typo.txt
plink2 --vcf Filtered_NAwolf_n35_variableSites_GenicRegions.recode.vcf --dog --keep pop_coastal_typo.txt --sample-counts cols=hom,het --out typo_test

If we use ls to look at the output files, we can see that PLINK has provided a .log file, which contains our command and a summary of the run, as well as a .scount file that provides the per-individual genotype counts.

But does this file actually give us the heterozygosity?

head Filtered_NAwolf_n35_variableSites_GenicRegions_coastal.scount
Click to see command breakdown
Command line parameterDescription
headA Unix command that prints the first few lines of a file
Filtered_NAwolf_n35_variableSites_GenicRegions_coastal.scount The path to the input file

No, the output provides the number of homozygous genotypes and the number of heterozygous genotypes for each individual, below a header line that starts with #IID. But we can use this information with a handy one-liner in awk to calculate heterozygosity, the proportion of each individual's genotyped sites that are heterozygous:

awk 'NR > 1 {print $1, $2, $3, $3/($2+$3)}' Filtered_NAwolf_n35_variableSites_GenicRegions_coastal.scount > Filtered_NAwolf_n35_variableSites_GenicRegions_coastal.scount.calc
Click to see command breakdown
Command line parameterDescription
awkA Linux text processing language
'NR > 1 {print $1, $2, $3, $3/($2+$3)}' The user-coded program to run. NR is awk's built-in count of the current line number, so NR > 1 skips the header line. For every other line, this prints the sample ID ($1), the homozygous count ($2), the heterozygous count ($3), and the heterozygosity: the heterozygous count divided by the number of sites where this sample had a genotype call — homozygous plus heterozygous, so missing genotypes are excluded.
Filtered_NAwolf_n35_variableSites_GenicRegions_coastal.scount The path to the file on which to run the program
>The redirect character means the output from the command specified before it will be saved to the file specified after it
Filtered_NAwolf_n35_variableSites_GenicRegions_coastal.scount.calc The path to the desired output file. This will be created if it doesn't exist and WILL OVERWRITE files that do exist

One thing to be clear about before we go further: the denominator here is the number of SNPs in this file that the individual was genotyped at, not the number of bases in the wolf genome. Our VCF only contains sites that were variable somewhere in the full data set, so what we're calculating is the proportion of those SNPs at which an individual is heterozygous. That's a good number for comparing individuals and populations to each other here, because every sample is measured against the same set of sites. But it isn't genome-wide heterozygosity, which is reported per base pair and is a far smaller number. Calculating that would also require the invariant sites, plus a record of which positions had enough coverage to be called at all.


Check your work - Count rows in outputs

Each step of our analysis should keep one line per individual, so a quick line count can catch samples that were silently dropped along the way:

wc -l pop_coastal.txt Filtered_NAwolf_n35_variableSites_GenicRegions_coastal.scount Filtered_NAwolf_n35_variableSites_GenicRegions_coastal.scount.calc

The .scount file should have one more line than the pop file (for its header line), and the .scount.calc file should have the same number of lines as the pop file. If the numbers don't line up, something in the pipeline removed or added lines, and you'll want to find out what before using the results.

This kind of check matters because filtering steps can remove data without any error. For example, an earlier version of this tutorial used the awk pattern /RKW/ to select sample lines. Any wolf whose ID didn't contain "RKW" would have vanished from the results without a single warning. Only counting lines would have caught it.


Check your work - Check that the right samples came through

Counting lines tells you whether the number of samples is right, but not whether they're the right samples. Let's check that the sample IDs in our results exactly match the IDs in the pop file. First, save a sorted copy of each list of IDs:

sort pop_coastal.txt > pop_coastal_ids.txt
awk '{print $1}' Filtered_NAwolf_n35_variableSites_GenicRegions_coastal.scount.calc | sort > calc_coastal_ids.txt

Then compare the two lists with diff, which prints any lines that differ between two files:

diff pop_coastal_ids.txt calc_coastal_ids.txt
Click to see command breakdown
Command line parameterDescription
diffA Unix command that compares two files line by line and prints the lines that differ
pop_coastal_ids.txtThe first file to compare. Lines only in this file are printed with a < in front.
calc_coastal_ids.txtThe second file to compare. Lines only in this file are printed with a > in front.

If diff prints nothing, the lists are identical. If it does print something, those are samples that were dropped, added, or renamed along the way. Differences can also come from invisible characters, like trailing spaces or Windows-style line endings in a pop file, which is worth knowing because they can also make PLINK fail to match sample IDs.


Check your work - Recompute one number by hand

Software can have bugs, and more often, it can calculate something slightly different from what you assumed. One of the best ways to check a result is to recompute it yourself for a single sample. Pick one coastal wolf (the first ID in head -1 pop_coastal.txt works), and replace SAMPLE_ID below with that ID.

First, find which column of the VCF file holds that sample's genotypes. The line starting with #CHROM contains the column names, so we split it into one name per line and find the line number of our sample:

grep "^#CHROM" Filtered_NAwolf_n35_variableSites_GenicRegions.recode.vcf | tr '\t' '\n' | grep -n -x "SAMPLE_ID"
Click to see command breakdown
Command line parameterDescription
grep "^#CHROM"Finds the line that starts with #CHROM. The ^ means "at the start of the line," so this matches only the column-name line, not other header lines that contain the text elsewhere.
tr '\t' '\n'The Unix translate command. Replaces every tab with a newline, so each column name ends up on its own line.
grep -nThe -n option tells grep to print the line number of each match in front of it, followed by a :. Since each column name is now on its own line, the line number is the column number.
-xTells grep to match only lines that are exactly the search text, rather than lines that contain it. Without -x, searching for a sample like RKW1 would also match RKW10, RKW11, and so on.
"SAMPLE_ID"The sample ID to search for

The number before the : is the column number. Now pull out that column for every SNP, keep just the genotype (the first :-separated field), and count each genotype type, replacing COLUMN with that number:

grep -v "^#" Filtered_NAwolf_n35_variableSites_GenicRegions.recode.vcf | cut -f COLUMN | cut -d: -f1 | sort | uniq -c
Click to see command breakdown
Command line parameterDescription
grep -v "^#"Prints every line that does not start with #, which removes the header and keeps one line per SNP
cut -f COLUMNThe Unix cut command, which pulls out selected columns (fields) from each line. By default, cut splits lines at tabs, so -f followed by a number keeps just that tab-separated column: here, our sample's column.
cut -d: -f1Each sample's entry looks like 0/1:12:99, with the genotype first and other information (like read depth and quality) after it, separated by colons. The -d: option tells cut to split at : instead of tabs, and -f1 keeps only the first piece: the genotype.
sortSorts the genotypes so identical ones are next to each other
uniq -cCollapses identical adjacent lines into one and, with -c, counts how many times each appeared. uniq only merges lines that are next to each other, which is why we sort first.

You should see counts for genotypes like 0/0 and 1/1 (homozygous), 0/1 (heterozygous), and possibly ./. (missing). Use these counts to calculate heterozygosity yourself: the number of heterozygous genotypes divided by the number of homozygous plus heterozygous genotypes. Then compare your answer to PLINK's:

grep "SAMPLE_ID" Filtered_NAwolf_n35_variableSites_GenicRegions_coastal.scount.calc

Do they match? If not, why not? Look closely at the genotype codes you counted. Did you see any written with | instead of /, or any other codes you didn't expect?

Using this, we can also calculate the mean population heterozygosity using awk:

awk '{sum += $4} END {if (NR > 0) print sum / NR }' Filtered_NAwolf_n35_variableSites_GenicRegions_coastal.scount.calc
Click to see command breakdown
Command line parameterDescription
awkA Linux text processing language
'{sum += $4} END {if (NR > 0) print sum / NR }' The user-coded program to run. This adds up the heterozygosity values in column 4, then divides the total by the number of lines (NR) to get the mean.
Filtered_NAwolf_n35_variableSites_GenicRegions_coastal.scount.calc The path to the file on which to run the program

Check your work - Check that values are plausible

Before trusting a result, ask whether it's even possible. Heterozygosity is a proportion, so every value must be between 0 and 1. Sorting the results by heterozygosity (column 4) puts the extremes at the top and bottom, where problems are easiest to spot:

sort -k4,4n Filtered_NAwolf_n35_variableSites_GenicRegions_coastal.scount.calc
Click to see command breakdown
Command line parameterDescription
sortThe Unix command to sort lines of text
-k4,4Sort using only column 4
nSort numerically rather than alphabetically

Are any values outside 0 to 1? Is any individual far from the rest? An outlier isn't necessarily wrong, but it's worth investigating. For example, check whether that individual has many more missing genotypes than the others.

Try this: Sometimes code runs without errors and gives numbers that look reasonable, but it's calculating the wrong thing. Run this version of our awk command and look at the results:

awk 'NR > 1 {print $1, $2, $3, $2/($2+$3)}' Filtered_NAwolf_n35_variableSites_GenicRegions_coastal.scount

All the values are between 0 and 1, and nothing crashes. What did this command actually calculate? Compare these values with the heterozygosity values you calculated above. Would you have caught the mistake if you didn't already know roughly what to expect?

Great! So now we have calculated the per-individual heterozygosity and the population heterozygosity for the coastal populations. Let's figure out how to repeat this set of commands for any number of additional populations.

Introduction to variables in Unix

Instead of hard-coding our command to be specific to the coastal wolves, we can use shell variables. We indicate a shell variable like so:

wolf_pop=coastal
Click to see command breakdown
Command line parameterDescription
wolf_popThe desired variable name appears to the left of the equals sign
=The equals sign indicates variable assignment in programming languages
coastalThe value you wish to assign to the variable name

We can check that variable assignment was successful by printing the value with the echo command:

echo ${wolf_pop}
Click to see command breakdown
Command line parameterDescription
echoThe Unix print command. Values given as input will be printed to the screen.
${wolf_pop}The item to print to the screen. In this case, because wolf_pop is a defined variable, we must use the ${} symbols to let echo know to print its assigned value to the screen, rather than just the text "wolf_pop".

We can use variables in any command in the terminal. For instance, we can run the same command as before to count genotypes, but instead of typing "coastal" in the command, we can use our variable:

plink2 --vcf Filtered_NAwolf_n35_variableSites_GenicRegions.recode.vcf --dog --keep pop_${wolf_pop}.txt --sample-counts cols=hom,het --out Filtered_NAwolf_n35_variableSites_GenicRegions_${wolf_pop}
Click to see command breakdown
Command line parameterDescription
plink2A program for working with genotype data, including variant calls in VCF format
--vcfThe option to specify that the input file is a VCF file
Filtered_NAwolf_n35_variableSites_GenicRegions.recode.vcf The path to the input file
--dogTells PLINK to use the dog chromosome set, which wolves share. Without this, PLINK assumes human chromosomes.
--keepThis option is used with a file that lists sample IDs. Only samples in that file will be used by the program for this run.
pop_${wolf_pop}.txtThe path to the file with the sample IDs to keep. In this case, we're using our variable, wolf_pop, to indicate which existing population file to use.
--sample-countsThis option tells PLINK to count the genotypes of different types (homozygous, heterozygous, missing, etc.) for each sample.
cols=hom,hetA modifier for --sample-counts that sets which columns to report: here, the number of homozygous genotypes (hom) and heterozygous genotypes (het) for each sample. Without it, PLINK reports a longer default set of columns.
--outThe option to specify the prefix for all output files. The file extension will be determined by the analysis.
Filtered_NAwolf_n35_variableSites_GenicRegions_${wolf_pop} The desired name of the output file. If it exists it will be overwritten. In this case, we use the wolf_pop variable to create a file with the name of the current population.

This provides the same output as the command we ran earlier. So, we could assign a new value to the wolf_pop variable. Notice also how I have been consistent in how I named all of my pop files so that I can substitute an ecotype name but otherwise still have that same general file name structure.

wolf_pop=highArctic
Click to see command breakdown
Command line parameterDescription
wolf_popThe desired variable name appears to the left of the equals sign
=The equals sign indicates variable assignment in programming languages
highArcticThe value you wish to assign to the variable name

We can check that variable assignment was successful by printing the value with the echo command:

echo ${wolf_pop}
Click to see command breakdown
Command line parameterDescription
echoThe Unix print command. Values given as input will be printed to the screen.
${wolf_pop}The item to print to the screen. In this case, because wolf_pop is a defined variable, we must use the ${} symbols to let echo know to print its assigned value to the screen, rather than just the text "wolf_pop".

Now that we've reassigned the wolf_pop variable to the highArctic population, we can run the same series of commands as above, but PLINK will use the samples and provide output for this population.

First, run PLINK to count the homozygous and heterozygous genotypes:

plink2 --vcf Filtered_NAwolf_n35_variableSites_GenicRegions.recode.vcf --dog --keep pop_${wolf_pop}.txt --sample-counts cols=hom,het --out Filtered_NAwolf_n35_variableSites_GenicRegions_${wolf_pop}
Click to see command breakdown
Command line parameterDescription
plink2A program for working with genotype data, including variant calls in VCF format
--vcfThe option to specify that the input file is a VCF file
Filtered_NAwolf_n35_variableSites_GenicRegions.recode.vcf The path to the input file
--dogTells PLINK to use the dog chromosome set, which wolves share. Without this, PLINK assumes human chromosomes.
--keepThis option is used with a file that lists sample IDs. Only samples in that file will be used by the program for this run.
pop_${wolf_pop}.txtThe path to the file with the sample IDs to keep. In this case, we're using our variable, wolf_pop, to indicate which existing population file to use.
--sample-countsThis option tells PLINK to count the genotypes of different types (homozygous, heterozygous, missing, etc.) for each sample.
cols=hom,hetA modifier for --sample-counts that sets which columns to report: here, the number of homozygous genotypes (hom) and heterozygous genotypes (het) for each sample. Without it, PLINK reports a longer default set of columns.
--outThe option to specify the prefix for all output files. The file extension will be determined by the analysis.
Filtered_NAwolf_n35_variableSites_GenicRegions_${wolf_pop} The desired name of the output file. If it exists it will be overwritten. In this case, we use the wolf_pop variable to create a file with the name of the current population.

Then run awk to calculate heterozygosity for each individual in the highArctic sample. Again note that we use the wolf_pop variable in our input and output file names:

awk 'NR > 1 {print $1, $2, $3, $3/($2+$3)}' Filtered_NAwolf_n35_variableSites_GenicRegions_${wolf_pop}.scount > Filtered_NAwolf_n35_variableSites_GenicRegions_${wolf_pop}.scount.calc
Click to see command breakdown
Command line parameterDescription
awkA Linux text processing language
'NR > 1 {print $1, $2, $3, $3/($2+$3)}' The user-coded program to run
Filtered_NAwolf_n35_variableSites_GenicRegions_${wolf_pop}.scount The path to the file on which to run the program. Note that we use the wolf_pop variable to indicate the population file to run.
>The redirect character means the output from the command specified before it will be saved to the file specified after it
Filtered_NAwolf_n35_variableSites_GenicRegions_${wolf_pop}.scount.calc The path to the desired output file. This will be created if it doesn't exist and WILL OVERWRITE files that do exist. Note that we use the wolf_pop variable to save to a file name labeled with the correct population.

And then awk again with the wolf_pop variable to get mean population heterozygosity:

awk '{sum += $4} END {if (NR > 0) print sum / NR }' Filtered_NAwolf_n35_variableSites_GenicRegions_${wolf_pop}.scount.calc
Click to see command breakdown
Command line parameterDescription
awkA Linux text processing language
'{sum += $4} END {if (NR > 0) print sum / NR }' The user-coded program to run
Filtered_NAwolf_n35_variableSites_GenicRegions_${wolf_pop}.scount.calc The path to the file on which to run the program. Note that we use the wolf_pop variable to indicate the population file to run.
Introduction to scripting

Now, we're going to add some of these commands with variables to a script so that we can automate the series of commands to generate per-individual and per-population heterozygosity.

A script is simply a text file where we can write and save the code and commands we want to run. Scripts give us the flexibility to perform complex operations based on conditions and ranges of values. Since a script is a text file, we're going to create a new text file and copy some commands into it. We're going to use an in-terminal text editor, vim, to do this, although there are several other options.

vim het_analysis.sh
Click to see command breakdown
Command line parameterDescription
vimAn in-terminal text editor available on Linux.
het_analysis.shThe name of the text file to create or view (if it exists). The .sh extension indicates this is a shell script

You should now be in the vim text editor. Your screen should be cleared of commands and the cursor should be blinking on top, with tilde characters (~) on every other line. Importantly, vim has both a viewing mode and an editing mode. When you create a new file with vim, as we have here, you start out in viewing mode. We can switch to editing mode by pressing the a key. Once you do this, you'll see the text -- INSERT -- at the bottom of the screen, indicating we have switched to editing mode.

Now, we can copy and paste our commands for calculating heterozygosity into this file. Of course, there are other ways we could get the commands into the file (such as using echo and redirecting), but we'll stay simple today.

Copy the following commands:

wolf_pop=$1
plink2 --vcf Filtered_NAwolf_n35_variableSites_GenicRegions.recode.vcf --dog --keep pop_${wolf_pop}.txt --sample-counts cols=hom,het --out Filtered_NAwolf_n35_variableSites_GenicRegions_${wolf_pop}
awk 'NR > 1 {print $1, $2, $3, $3/($2+$3)}' Filtered_NAwolf_n35_variableSites_GenicRegions_${wolf_pop}.scount > Filtered_NAwolf_n35_variableSites_GenicRegions_${wolf_pop}.scount.calc
awk '{sum += $4} END {if (NR > 0) print sum / NR }' Filtered_NAwolf_n35_variableSites_GenicRegions_${wolf_pop}.scount.calc

The last three commands should be familiar to you already. They are the same commands we used above for the coastal population, but we've substituted the string "coastal" for the variable wolf_pop.

The first line, wolf_pop=$1, is our variable assignment. It tells the script to assign the variable wolf_pop the value of $1. Well what is $1? It has a dollar sign ($) preceding it, so we know it is a variable. But we haven't assigned anything to it yet.

Like other commands, your own scripts can take input options from the command line. By default, a shell script (like the one we are writing) will assign these options to internal variables: $1 for the first option, $2 for the second one, and so on. So, wolf_pop will be assigned whatever we type into the terminal when we run the script.

Let's exit out of vim to try and run our script. First, switch back to viewing mode by pressing the ESC key. You should no longer see the text -- INSERT -- at the bottom of the screen. When in viewing mode, any input we type is interpreted as internal vim commands, such as saving, exiting, etc. Let's tell vim to save the changes we've made to the file with the write command (w) and then quit (q) the text editor by typing:

:wq
Click to see command breakdown
Command line parameterDescription
:Tells vim to interpret the following text as commands
wThe vim write command that saves changes made to the file
qThe vim quit command to exit the text editor and return to the terminal

TASK 3: Make the script executable and try it

We will use the chmod command to change the permissions on the script so the shell knows that its contents should be interpreted as commands. This is known as making the file executable:

chmod a+x het_analysis.sh
Click to see command breakdown
Command line parameterDescription
chmodThe Unix change mode command used to change permissions on files
a+xThis tells chmod to give all users (a) permission to execute the file (+x)
het_analysis.shThe name of the file to change permissions

Now let's try to execute our file on the coastal population:

./het_analysis.sh coastal
Click to see command breakdown
Command line parameterDescription
./het_analysis.shThe name of the file to run. Since we have changed its permissions to be executable, its contents will be interpreted as shell commands. ./ also indicates to run this file as a shell script
coastalRemember, the script we wrote expects input from the command line to assign to the $1 variable. This will be that value

If we use our handy ls -lht command, we can see that the three newest files are of the coastal wolf samples, and that the script we wrote printed the mean population heterozygosity to the screen.

Check your work - Record versions and keep everything in scripts

Software changes over time. New versions can change default settings, rename options, or reorder output columns, which can change your results without any error. To make sure you (or anyone else) can reproduce your analysis later, record which version of each program you used:

plink2 --version

You can also add this line to the top of het_analysis.sh, so every run of the script records the version it used alongside its output.

A few other habits help here:

  • Keep your .log files. They record exactly what PLINK did on each run.
  • Run every step from a script. Avoid editing output files by hand. A script is a complete record of how you got from raw data to results, and you can rerun it after fixing a mistake.
  • Rerun the whole script when something changes. If you fix a pop file or change a command, rerun the script from the beginning instead of patching individual outputs. This keeps every result consistent with the current inputs.

Documenting our code with comments and print statements

It is also good practice to document what each step of our code does in human-readable language. This is called commenting our code. The way to comment code differs between programming languages, but a fairly common method (used in Python, R, and the shell scripting we're doing) is with the # symbol. For languages that use this symbol, any line starting with # will be interpreted as a comment rather than a command to be run.

We can also provide some helpful context to the output of our script. Rather than just printing out a number that may be confusing to someone else using the script (or even ourselves if we re-run this same script months or years later), we can use echo within the script to print some more information, like that the number printed represents mean population heterozygosity.

Feel free to open up your script and follow along as I comment my version and add contextual print statements:

vim het_analysis.sh
Click to see command breakdown
Command line parameterDescription
vimAn in-terminal text editor available on Linux.
het_analysis.shThe name of the text file to create or view (if it exists). The .sh extension indicates this is a shell script

TASK 4: Which population has the highest mean heterozygosity?

To find this out now, all we have to do is run our script on all three populations:

./het_analysis.sh coastal
./het_analysis.sh highArctic
./het_analysis.sh borealForest

This shows that the boreal forest population has the highest mean heterozygosity at 0.190026. Remember what that number is: the average proportion of our panel of SNPs at which an individual is heterozygous. It's the right number for ranking these three ecotypes against each other, but not comparable to heterozygosity values you'd read in a paper.

Introduction to for loops

If only there was a way to automate the analysis even further... Aha! We can use for loops to call the script multiple times with different input values, in this case our 3 population strings, for the wolf_pop variable. for loops in a shell script are generated in a similar way as in other programming languages:

for <variable> in <iterable>; do <code>; done
Click to see command breakdown
Command line parameterDescription
forThe start of the for loop
<variable>for loops iterate over a set of values. Whatever is named here as <variable> will be the variable name of the current value in the set.
inIndicates that the following will be the set of values to iterate over
<iterable>;A data type that contains multiple values
doA keyword that indicates a block of code within a for loop has started
<code>;The code to execute on each value in the set of values
doneA keyword that indicates the end of the for loop

For instance, we could do the following to loop over letters in the alphabet from A to F and print them out:

for letter in {A..F}; do echo $letter; done

Or the following to subtract 1 from each integer from 1 to 4 and print the resulting value to the screen:

for num in 1 2 3 4; do echo "The value of $num - 1 is " `expr $num - 1`; done

TASK 5: Use a for loop to calculate heterozygosity for the three wolf ecotypes

for pop in coastal highArctic borealForest; do ./het_analysis.sh $pop; done
Click to see command breakdown
Command line parameterDescription
forThe start of the for loop
popThis tells the for loop to assign the current value being executed to the $pop variable
inIndicates that the following will be the set of values to iterate over
coastal highArctic borealForestThe values over which to iterate
doA keyword that indicates a block of code within a for loop has started
./het_analysis.sh $pop;Run the het_analysis.sh script on the current value stored in $pop
doneA keyword that indicates the end of the for loop

This should produce the same output as when we ran the three commands manually above!

Hopefully you've become more familiar with how to use variables in the command-line and in scripts, how to write a script and edit it using vim, and how to use for loops to make your life easier!