1.2 Reproducible bioinformatics: project layout, environments and git¶
Theme: Tools & technology
Over the next four days you will run two workflows, use four Python packages, make dozens of plots and write a gene dossier. By Friday you need to be able to say exactly which data, code and software versions produced each figure. The easiest way to manage that is to set up a few habits on day one.
This module covers three of them: a standard project layout, software environments with pinned versions, and version control with git and GitHub. At the end you fork the course repository and make your first commit to your own dossier folder. You will commit to it every day.
The ideas here draw on Vince Buffalo’s Bioinformatics Data Skills [1] and Wilson et al.'s “Good enough practices in scientific computing” [2]. Both are worth reading in full.
Learning objectives¶
At the end of this module you will be able to:
Organise a bioinformatics project so that raw data are protected and results can be regenerated.
Explain why software environments and version pinning matter, and manage a project environment with pixi.
Explain the git concepts repository, commit, branch, remote, fork and pull request.
Fork the course repository, and commit and push your own work to it.
Setup¶
We only need tools that are already in Colab and Codespaces (the shell, curl, git and pandas);
the environments section installs pixi if it is missing.
All files are written to a course-work/ folder next to this notebook.
import hashlib
import os
from pathlib import Path
import pandas as pd
# Stay put if this cell is run twice
WORKDIR = Path.cwd() if Path.cwd().name == "course-work" else Path("course-work").resolve()
WORKDIR.mkdir(exist_ok=True)
os.chdir(WORKDIR)
print("Working in", WORKDIR)Working in /content/course-work
Why reproducibility is worth the effort¶
A result is reproducible if someone else, with your data and code, gets the same answer. That “someone” is usually you, three months later, when a reviewer asks you to change one parameter. Common ways this goes wrong:
You cannot tell which of
results_final.tsvandresults_final_v2_USE_THIS.tsvwent into the figure.A raw data file was edited by hand (or overwritten), and the original is gone.
A package update changed a default, and the same code now gives a different answer.
The analysis ran on a laptop that no longer exists.
None of the fixes needs special skills. Both of the sources above make the same point: aim for practices that are “good enough” and that you will actually keep up, not perfect ones.
Project layout¶
Principles¶
One project, one folder, one git repository. Everything needed to understand the project lives under one directory.
Raw data are read-only. Put downloaded or sequenced data in
data/raw/, never edit it, and record where each file came from, when, and its checksum. Make the files read-only so you cannot overwrite them by accident.Results can be thrown away. Everything in
results/is produced by code from the raw data. If you could not deleteresults/and rebuild it, something is missing from your code.Code is separate from data and results. Reusable code goes in
scripts/(orworkflow/for a Snakemake workflow). Exploratory notebooks go innotebooks/, numbered in the order they run.Write things down. A
README.mdat the top says what the project is and how to run it. Keep a dated log of what you did and why; Buffalo suggests a README in each data directory describing where the files came from.Use relative paths (
data/raw/samples.tsv, not/Users/sanjay/Desktop/...) so the project works on another computer.Name files for computers and people. No spaces, lower case, dates as
YYYY-MM-DDso they sort correctly, zero-padded numbers (01-,02-, ...10-).Prefer plain text (TSV, CSV, FASTA, YAML) to spreadsheets for anything a program reads.
A layout for your gene dossier¶
my-dossier/
├── README.md # what this is, how to reproduce it
├── pixi.toml, pixi.lock # the software you used (Module 1.2, below)
├── data/
│ ├── README.md # where each raw file came from, when, checksums
│ └── raw/ # read-only
├── notebooks/ # 01-busia-de.ipynb, 02-anoexpress.ipynb, ...
├── scripts/ # reusable functions and scripts
├── results/ # generated; safe to delete
└── docs/ # dossier text, figures for your talkRNA-Seq-Pop and AmpSeeker use the standard Snakemake layout (config/, workflow/,
resources/, results/), which follows the same ideas. You will see it in Module 1.4.
Practical: set up a project and protect the raw data¶
We create the layout, then download a small real file: the ENA run table for the Busia RNA-seq project (PRJNA748581). It lists every sequencing run with its FASTQ checksums.
%%bash
mkdir -p my-project/{data/raw,notebooks,scripts,results,docs}
cd my-project
chmod u+w data/raw/* 2>/dev/null || true # only matters if you rerun this cell
# Download the run table for the Busia RNA-seq project from ENA
curl -sS -o data/raw/PRJNA748581_runs.tsv \
"https://www.ebi.ac.uk/ena/portal/api/filereport?accession=PRJNA748581&result=read_run&fields=run_accession,sample_alias,read_count,fastq_md5,fastq_ftp&format=tsv"
find . | sort.
./data
./data/raw
./data/raw/PRJNA748581_runs.tsv
./docs
./notebooks
./results
./scripts
Now record where the file came from, with its checksum, and make it read-only.
raw = Path("my-project/data/raw/PRJNA748581_runs.tsv")
md5 = hashlib.md5(raw.read_bytes()).hexdigest()
readme = Path("my-project/data/README.md")
readme.write_text(
"# Raw data\n\n"
"| File | Source | Downloaded | MD5 |\n"
"|---|---|---|---|\n"
f"| raw/{raw.name} | ENA filereport API for PRJNA748581 | "
f"{pd.Timestamp.today():%Y-%m-%d} | {md5} |\n"
)
raw.chmod(0o444) # read-only for everyone
print(readme.read_text())# Raw data
| File | Source | Downloaded | MD5 |
|---|---|---|---|
| raw/PRJNA748581_runs.tsv | ENA filereport API for PRJNA748581 | 2026-09-26 | d29eac755e75c8f911158d13e6b349a9 |
%%bash
cd my-project
# Try to overwrite the raw file. This should fail.
if [ "$(id -u)" = "0" ]; then
echo "You are the root user (as in Colab). Root ignores file permissions, so this"
echo "protection only works for normal users, e.g. in Codespaces, on a server or a laptop."
else
(echo "oops" > data/raw/PRJNA748581_runs.tsv) 2>/dev/null || echo "Good: the raw file is protected."
fiGood: the raw file is protected.
The run table itself shows why checksums matter. ENA gives an MD5 for every FASTQ file, so after downloading ~113 GB of reads you can check that nothing was corrupted on the way.
runs = pd.read_csv("my-project/data/raw/PRJNA748581_runs.tsv", sep="\t")
print(len(runs), "runs")
runs[["run_accession", "sample_alias", "read_count", "fastq_md5"]].sort_values("sample_alias")16 runs
Software environments¶
Why environments?¶
Your results depend on your data, your code and the exact versions of all the software the code uses. Packages change defaults, fix bugs and drop functions. Two tools may also need incompatible versions of the same library. An environment is an isolated set of packages for one project, and an environment file is a recipe to rebuild it anywhere.
Tools¶
| Tool | What it manages | Notes |
|---|---|---|
| pixi (recommended) | Conda packages (Python, R, and command-line tools such as kallisto and samtools) plus PyPI packages, per project | The environment lives inside the project folder, is described by pixi.toml, and is locked automatically in pixi.lock. Also runs project commands (“tasks”) |
| conda / mamba | The same conda packages, in named environments shared across projects | The older, still very common route. Snakemake uses it to build per-rule environments (see below) |
| pip + venv / uv | Python packages from PyPI only | What we use in Colab: %pip install package==x.y.z |
| Containers (Docker, Apptainer) | A whole operating system image | The most portable option; common on HPC and with Nextflow |
Conda packages come from community channels: conda-forge for general software and bioconda for bioinformatics tools [3]. pixi and conda use the same packages; they differ in how they manage environments.
Levels of pinning¶
Unpinned (
pandas): you get whatever is newest today. Not reproducible.Pinned (
pandas=2.2.2): the packages you asked for are fixed, but their dependencies can still change.Locked (
pixi.lock,conda-lock,uv.lock): every package, including dependencies, is fixed for each platform.
Pinning is a trade-off: pinned environments are reproducible but get old. Update on purpose, rerun your analysis, and commit the new files.
Why we recommend pixi¶
One environment per project, in the project. No need to remember which named conda environment goes with which folder;
pixi runalways uses the right one.Locked by default. Every
pixi addupdatespixi.lock, so a collaborator (or you, next year) gets exactly the same versions withpixi install.Conda and PyPI together. Many of our tools (
malariagen_data,anoexpress,anoprimer) are only on PyPI; pixi resolves them together with conda packages instead of layering pip on top.Tasks. Common commands are written down in
pixi.toml, sopixi run buildmeans the same thing for everyone.
The everyday commands:
pixi init my-dossier # create my-dossier/pixi.toml
cd my-dossier
pixi add python=3.11 pandas=2.2.2 # conda-forge packages
pixi add --pypi malariagen_data==15.9.0 # a PyPI-only package
pixi task add plots "python scripts/make_plots.py"
pixi run plots # run a task inside the environment
pixi shell # or start a shell in the environment
pixi install # recreate the environment from pixi.lockCommit pixi.toml and pixi.lock; never commit the .pixi/ folder (pixi adds it to .gitignore for you).
Example: this course’s own environment¶
The course repository has a pixi.toml at its root. It builds the course book and runs the
notebooks outside Colab. Notice the comments: malariagen_data needs exactly pandas 2.2.2 and
numpy 2.0, so those are pinned on the conda side too. Constraints like these are the kind of
thing that silently breaks environments that are not locked.
# Environment for building the course book and running the course notebooks locally.
# Colab users do not need this: each notebook pins its own packages with %pip.
# Usage: pixi install # create the environment from pixi.lock
# pixi run start # live preview of the book
# pixi run build # build the HTML book
# pixi run lab # open the notebooks in JupyterLab
[workspace]
name = "anopheles-omics-training"
description = "Anopheles Omics: from gene expression to field assays"
channels = ["conda-forge"]
platforms = ["linux-64", "osx-64", "osx-arm64"]
[tasks]
build = "jupyter book build --html"
start = "jupyter book start"
lab = "jupyter lab"
[dependencies]
python = "3.11.*"
jupyter-book = ">=2.1,<3"
jupyterlab = ">=4,<5"
pandas = "==2.2.2" # malariagen_data 15.9.0 requires exactly this
plotly = ">=5.24,<6" # anoexpress needs plotly 5
numpy = "2.0.*" # malariagen_data needs numpy >=2.0.2,<2.1
packaging = ">=24,<26" # snakemake 9.27 needs packaging <26
graphviz = "*" # the `dot` program, for Snakemake DAGs in module 1.3
pip = "*"
# Not on conda-forge, so installed from PyPI
[pypi-dependencies]
malariagen_data = "==15.9.0"
anoexpress = "==0.3.3"
anoprimer = "==2.0.8"
jupyterquiz = "==2.9.6.4"
snakemake = "==9.27.0" # module 1.3After cloning the repository, pixi install recreates the environment from pixi.lock,
pixi run lab opens the notebooks, and pixi run build builds the book.
pixi.toml and environment.yml¶
You will still see conda environment.yml files everywhere, including inside Snakemake
workflows. The two formats describe the same thing:
pixi.toml + pixi.lock | environment.yml | |
|---|---|---|
| Tool | pixi | conda / mamba |
| Where the environment lives | .pixi/ inside the project | A named environment in your conda folder |
| Lock file | Always, for every listed platform | Only with extra tools (conda-lock) or conda env export |
| PyPI packages | Solved together with conda packages | A pip: section installed afterwards |
| Tasks | Yes | No |
For comparison, a conda environment.yml:
name: my-dossier
channels:
- conda-forge
- bioconda
dependencies:
- python=3.11
- pandas=2.2.2
- samtools=1.21
- pip
- pip:
- malariagen_data==15.9.0Why workflows still use conda¶
Snakemake can give each rule its own environment, defined by a small environment.yml file
next to the rule, and build them for you with --use-conda (or
--software-deployment-method conda). RNA-Seq-Pop and AmpSeeker do this, because their tools
(kallisto, freebayes, R packages) would conflict in one environment. So in the Codespace you use
pixi to install Snakemake itself, and Snakemake uses conda to build the per-rule
environments. You will see this in Module 1.3.
Every Colab notebook in this course starts with pinned %pip install lines for the same
reasons: Colab has no pixi or conda by default.
Practical: a pixi project¶
This installs pixi (a single program) if it is not already available, creates a pixi project
inside my-project, adds two packages and a task, and runs it. Adding packages downloads
Python and pandas, so it takes a minute or two.
%%bash
if ! command -v pixi > /dev/null; then
# Install a pinned pixi version into course-work/.pixi-home (no changes to your shell setup)
curl -fsSL https://pixi.sh/install.sh | \
PIXI_VERSION=v0.73.0 PIXI_HOME="$PWD/.pixi-home" PIXI_NO_PATH_UPDATE=1 bash > /dev/null
fi
export PATH="$PWD/.pixi-home/bin:$PATH"
pixi --versionpixi 0.73.0
%%bash
export PATH="$PWD/.pixi-home/bin:$PATH"
cd my-project
pixi init . > /dev/null 2>&1
pixi add "python=3.11" "pandas=2.2.2" > /dev/null 2>&1
pixi task add versions 'python -c "import sys, pandas; print(sys.version.split()[0], pandas.__version__)"' > /dev/null 2>&1
cat pixi.toml
echo "---- pixi.lock has $(wc -l < pixi.lock) lines"
pixi run versions[workspace]
channels = ["conda-forge"]
name = "my-project"
platforms = ["osx-arm64"]
version = "0.1.0"
[tasks]
versions = 'python -c "import sys, pandas; print(sys.version.split()[0], pandas.__version__)"'
[dependencies]
python = "3.11.*"
pandas = "2.2.2.*"
---- pixi.lock has 479 lines
✨ Pixi task (versions): python -c "import sys, pandas; print(sys.version.split()[0], pandas.__version__)"
3.11.16 2.2.2
pixi run versions used the project’s own Python and pandas, not the ones in this notebook.
For comparison, here is the notebook’s own environment, recorded the pip way:
# Save a record of every Python package in the notebook's environment
%pip freeze > my-project/results/pip-freeze.txt
print("\n".join(Path("my-project/results/pip-freeze.txt").read_text().splitlines()[:10]))Note: you may need to restart the kernel to use updated packages.
annotated-types==0.8.0
anyio==4.15.1
appnope==1.0.0
argon2-cffi==25.1.0
argon2-cffi-bindings==26.1.0
argparse-dataclass==2.0.0
arrow==1.4.0
asttokens==3.0.2
async-lru==2.3.0
attrs==26.1.0
Version control with git and GitHub¶
Why?¶
Git records a history of your project. Every saved version (a commit) has a message saying
what changed and why, and you can compare or return to any earlier version. That removes the
need for analysis_v2_final.py. GitHub hosts git repositories online, so your work is backed up,
shareable, and can be linked to a paper [4].
Concepts¶
| Term | Meaning |
|---|---|
| Repository (repo) | A project folder whose history git tracks |
| Commit | A saved snapshot of the tracked files, with a message, author and date |
| Staging area | Where you collect the changes that go into the next commit (git add) |
| Branch | A separate line of commits, so you can try something without touching main |
| Merge | Bringing the commits from one branch into another |
| Remote | A copy of the repo somewhere else, usually GitHub (origin) |
| Clone | Download a repo, with its history, to your computer |
| Push / pull | Send your commits to a remote / fetch and merge commits from it |
| Fork | Your own copy of someone else’s repo on GitHub |
| Pull request (PR) | A request to merge commits from your fork or branch into another repo, with review and discussion |
A history with one branch might look like this:
And this is how the course repository, your fork and your codespace relate:
What not to commit¶
Large data. GitHub rejects files over 100 MB, and git is slow with big binary files. Keep raw data in
data/raw/but list it in.gitignore; the data README says how to get it back. Share large data through ENA, SRA or Zenodo instead (Module 5.2).Secrets: passwords, API keys and access tokens. Once pushed, assume they are public.
Anything that can be regenerated quickly, such as most of
results/. Small final tables and figures for your dossier are fine to commit.
Practical 1: git in a practice repository¶
This runs in Colab, Codespaces or on your own computer. We turn my-project into a git
repository and make two commits. The git config lines set your name for this repository only.
%%bash
cd my-project
git init -q -b main
git config user.name "Course Participant"
git config user.email "participant@example.com"
cat > README.md <<'EOF'
# Busia practice project
Practice project for the malaria vector software course.
EOF
git status --short?? .gitattributes
?? .gitignore
?? README.md
?? data/
?? pixi.lock
?? pixi.toml
?? results/
%%bash
cd my-project
git add README.md data/README.md
git commit -q -m "Add README and data provenance notes"
echo "Data: ENA PRJNA748581 run table (see data/README.md)." >> README.md
git diffdiff --git a/README.md b/README.md
index 531257a..143416e 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,3 @@
# Busia practice project
Practice project for the malaria vector software course.
+Data: ENA PRJNA748581 run table (see data/README.md).
%%bash
cd my-project
git commit -q -am "Describe the data in the README"
git log --onelinec4063fb Describe the data in the README
e4b214b Add README and data provenance notes
Practical 2: fork the course repo and start your dossier¶
This part uses your own GitHub account, so you do it in the browser and a terminal rather than in this notebook. If you do not have a GitHub account, create one at https://github.com first.
1. Fork. Go to https://https://github.com/<your-username>/anopheles-omics-training.
2. Open a terminal on your fork. We recommend GitHub Codespaces, which you also use for
RNA-Seq-Pop in Module 1.4. On your fork, click Code → Codespaces → Create codespace on main.
Codespaces is already logged in to your GitHub account, so git push works without a password.
3. Make your dossier folder and commit it. In the codespace terminal:
git remote -v # origin should point to YOUR fork
mkdir -p my-dossier
cp assessment/gene-dossier-template.md my-dossier/dossier.md
printf "# Gene dossier\n\nMy work for the malaria vector software course.\n" > my-dossier/README.md
git status
git add my-dossier
git commit -m "Start my gene dossier"
git pushRefresh your fork on GitHub: the my-dossier/ folder should be there.
Other options.
github.dev: on your fork, press
.to open a browser editor. You can create and edit files and commit from the Source Control panel. There is no terminal, but it is enough for editing your dossier.Colab: you can
!git cloneyour fork into Colab to read it, but pushing needs a personal access token. Never type a token into a notebook cell that you save or share. For pushing, use Codespaces or github.dev.
4. Keep it going. At the end of each day, commit your dossier notes and figures with a
message that says what you did (e.g. Day 2: AnoExpress results for CYP6P3). When the course
repo is updated, use Sync fork on GitHub and then git pull in your codespace.
Exercises¶
Exercise 1¶
Make git ignore data/raw/ and results/ in my-project, then commit the .gitignore file.
(pixi init has already created a .gitignore containing .pixi/, so add to it rather than
replacing it.) Use git status before and after to check it worked.
Show answer
%%bash
cd my-project
git status --short # data/raw/ and results/ are listed as untracked
printf "data/raw/\nresults/\n" >> .gitignore # >> appends; > would overwrite
git status --short # data/ now only shows data/README.md (already committed), results/ is gone
git add .gitignore .gitattributes pixi.toml pixi.lock
git commit -m "Add pixi environment and ignore raw data and results"Exercise 2¶
Using pixi, set up a project that uses Python 3.11, pandas, kallisto (from bioconda) and the
anoexpress package (from PyPI), with every package pinned, and a task called kallisto-version
that prints kallisto’s version. Why must bioconda be added as a channel? And why might you
need to pin pandas to 2.2.2 and numpy to 2.0?
Show answer
In a terminal (or a %%bash cell, with pixi on the PATH as above):
pixi init -c conda-forge -c bioconda busia-expression
cd busia-expression
pixi add python=3.11 pandas=2.2.2 numpy=2.0 kallisto=0.52.0
pixi add --pypi anoexpress==0.3.3
pixi task add kallisto-version "kallisto version"
pixi run kallisto-versionThe resulting pixi.toml:
[workspace]
channels = ["conda-forge", "bioconda"]
name = "busia-expression"
platforms = ["linux-64"] # whichever platform you created it on; add more with `pixi workspace platform add`
[tasks]
kallisto-version = "kallisto version"
[dependencies]
python = "3.11.*"
pandas = "2.2.2.*"
numpy = "2.0.*"
kallisto = "0.52.0.*"
[pypi-dependencies]
anoexpress = "==0.3.3"kallisto is a bioinformatics command-line tool distributed through bioconda, not PyPI or conda-forge. anoexpress depends on malariagen_data, which requires exactly pandas 2.2.2 and numpy 2.0.x. If the conda side installs a newer pandas or numpy, pixi cannot install the PyPI package and tells you why; pinning them on the conda side fixes it. (Check the latest versions on prefix.dev and PyPI before you use them.)
Exercise 3¶
In your own words, what is the difference between (a) a fork and a clone, and (b) a commit and a push?
Show answer
(a) A fork is a copy of a repository made on GitHub, under your account. A clone is a copy downloaded to a computer (or codespace). You usually fork once, then clone your fork.
(b) A commit saves a snapshot in your local repository only. A push sends your commits to the remote (GitHub). Until you push, nobody else can see your commits, and they are not backed up.
Quiz¶
Check your understanding. The quiz runs in Colab and in the course book.
%pip install -q jupyterquiz==2.9.6.4
from jupyterquiz import display_quiz
display_quiz("https://raw.githubusercontent.com/sanjaynagi/anopheles-omics-training/main/quizzes/1-2-reproducible-practice.json")Summary¶
Keep each project in one folder with a standard layout. Raw data are read-only and documented; results can be deleted and rebuilt from code.
Environments isolate the software for each project. We recommend pixi:
pixi.tomlsays what you asked for,pixi.lockfixes every version, and tasks record how to run things. Workflows still use conda for their per-rule environments.Git records the history of your project; GitHub backs it up and lets you share it. Commit small, meaningful changes with clear messages, and never commit large data or secrets.
You have forked the course repo and started
my-dossier/. Commit to it every day.
Well done!¶
Next, in Module 1.3, you see how workflow managers apply the same ideas to whole analyses.
References¶
Buffalo V (2015). Bioinformatics Data Skills: Reproducible and Robust Research with Open Source Tools. O’Reilly Media. ISBN 978-1-4493-6737-4. Chapters 1–2 (project organisation) and 5 (git).
Wilson G et al. (2017). Good enough practices in scientific computing. PLoS Computational Biology. Wilson et al. (2017)
Grüning B et al. (2018). Bioconda: sustainable and comprehensive software distribution for the life sciences. Nature Methods. undefined et al. (2018)
Perez-Riverol Y et al. (2016). Ten simple rules for taking advantage of Git and GitHub. PLoS Computational Biology. Perez-Riverol et al. (2016)
Noble WS (2009). A quick guide to organizing computational biology projects. PLoS Computational Biology. Noble (2009)
pixi documentation: https://pixi.sh
- Wilson, G., Bryan, J., Cranston, K., Kitzes, J., Nederbragt, L., & Teal, T. K. (2017). Good enough practices in scientific computing. PLOS Computational Biology, 13(6), e1005510. 10.1371/journal.pcbi.1005510
- Grüning, B., Dale, R., Sjödin, A., Chapman, B. A., Rowe, J., Tomkins-Tinch, C. H., Valieris, R., & Köster, J. (2018). Bioconda: sustainable and comprehensive software distribution for the life sciences. Nature Methods, 15(7), 475–476. 10.1038/s41592-018-0046-7
- Perez-Riverol, Y., Gatto, L., Wang, R., Sachsenberg, T., Uszkoreit, J., Leprevost, F. da V., Fufezan, C., Ternent, T., Eglen, S. J., Katz, D. S., Pollard, T. J., Konovalov, A., Flight, R. M., Blin, K., & Vizcaíno, J. A. (2016). Ten Simple Rules for Taking Advantage of Git and GitHub. PLOS Computational Biology, 12(7), e1004947. 10.1371/journal.pcbi.1004947
- Noble, W. S. (2009). A Quick Guide to Organizing Computational Biology Projects. PLoS Computational Biology, 5(7), e1000424. 10.1371/journal.pcbi.1000424