Skip to content

Encoding Your Sample IDs

Building short, systematic codes for your samples, not just descriptive names, and why it matters more than it seems.

This is a different problem from file naming. The GDC Basics page on the GDA course site covers general file naming conventions on disk, no spaces, leading zeros, avoiding special characters. This page is about something upstream of that: the actual identifier you assign to each biological sample, the one that ends up in your map file, your file names, your plot legends, and every table you produce. Get this right and consistent file naming follows almost automatically. Get it wrong and no file naming convention will fully save you.

In many years of doing this, we've seen very few genuinely well-thought-out sample ID schemes. It's a persistent, common problem, not a rare mistake, and this page won't pretend to have one perfect answer. What follows are the ideas that consistently help.

Encode, Don't Describe

A descriptive sample name captures what the sample is, but usually inconsistently: spelled-out ordinals, inconsistent capitalization, concatenated words with no clear boundary between fields.

Bad:    first-control, second_control, WaterSampleSG1, WaterSampleTG1
Better: CTR1, CTR2, WS-SG1, WS-TG1

Both versions carry the same information. The second is short, consistent, and, critically, can be reliably split back into its component parts later (sample type, site, replicate number) whenever you need to. The first can't, not without writing custom parsing logic for every inconsistency by hand.

Build the Code from Fixed Components

A workable scheme is usually a small number of short, fixed-meaning components, joined by one consistent delimiter:

<SampleType>-<SiteCode><Number>

WS-SG1   → Water Sample, site SG, replicate 1
WS-TG1   → Water Sample, site TG, replicate 1
CTR1     → Control, replicate 1

Decide on your components (sample type, site, timepoint, replicate, whatever your actual design needs) and their abbreviations before sampling starts, write them down, and don't improvise exceptions partway through a project. A short lookup table (WS = Water Sample, SG = Site Grindel) costs you five minutes now and saves real confusion later, for you and for anyone else who works with the data.

Use a Hyphen, Not an Underscore, Inside Your Sample ID

Illumina (and most sequencer) filenames already use underscores to separate the fields the instrument adds: SampleID_S1_L001_R1_001.fastq.gz. If your sample ID also contains underscores, splitting that filename back apart to recover the ID becomes ambiguous, you can no longer tell which underscores belong to your ID and which were added by the machine. Using a hyphen inside your sample ID instead avoids the collision entirely: everything before the first underscore is unambiguously your sample ID, hyphens and all, no matter how complicated the rest of the filename gets. See Data Prep Output for what these raw filenames actually look like in practice.

Suggested Codes for Common Controls

Controls need the same short, consistent encoding as real samples, arguably more so, since they're compared across the whole project rather than analysed individually. A starting point, extending the same ControlType pattern used on After Import: Controls and First Diagnostics (which shows three of the categories below; the rest follow the same idea for controls that page doesn't cover):

Control Code → ControlType value
Extraction blank (no sample through extraction) EXBExtractionNegative
Library prep negative (blank through library prep) LPNLibraryPrepNegative
PCR no-template control (water instead of template) NTCPCRNegative
Mock community / positive control (known composition) MOCK or POSLibraryPrepPositive
Field blank (e.g. a filter with no water passed through, common in eDNA) FLBFieldNegative
Equipment or rinse blank EQBEquipmentNegative

The short code goes in the sample ID itself (NTC1, MOCK1, EXB2), the full descriptive label goes in the ControlType metadata column. That split is deliberate: the ID stays short and consistent with your other samples, while the metadata column carries the meaning a reader (or a script) actually needs. Adjust the specific codes to your own project's controls, not every project needs every category here, and some need categories not listed, but keep the same short-code-in-ID, full-label-in-metadata split regardless.

Zero-Pad Your Numbers

Once numbers are part of the code, they need leading zeros, or sorting breaks:

Without padding: WS-SG1, WS-SG10, WS-SG11, WS-SG2, WS-SG3
With padding:    WS-SG01, WS-SG02, WS-SG03, WS-SG10, WS-SG11

sort() in R, the shell, and most spreadsheet tools order text character by character, not numerically, so WS-SG10 sorts before WS-SG2 without padding. This isn't cosmetic, it affects sample order in every plot legend, table, and ordination you produce for the rest of the project. Pad to however many digits your largest number needs, and if a related project might grow past what you need today, pad a little further now rather than renaming everything later.

Remember: This Will Show Up in Every Plot

Whatever ID you choose ends up, unchanged, as the sample name in your phyloseq object, and from there it's what R uses by default for axis labels, legends, and facet titles in every plot you produce. A thoughtful ID scheme pays off twice: it's easier to work with in code, and it's easier to actually read on a figure.

This is a reason to go a step beyond the zero-padding above: keep IDs a similar length across different sample types too, not just consistent within one type. CTR1 sitting next to WS-SG01 on the same axis looks ragged, uneven lengths draw the eye to the mismatch rather than the data. CTR01 next to WS-SG01 lines up cleanly. On a plot with dozens of sample labels crammed onto an x-axis, that consistency is the difference between something scannable at a glance and something that needs squinting.

Avoid Purely Numeric IDs

If a sample ID is nothing but digits, "12", "105", R will often silently prepend an X when it becomes a column or row name, since R identifiers can't start with a digit. "12" quietly becomes "X12" somewhere in your pipeline, usually with no warning. This is exactly the kind of mismatch Data Import's sanity checks (sample_names(ps)) are meant to catch, check for it there, but it's easier to avoid at the source: give every ID a non-numeric leading character, which a SampleType prefix naturally provides anyway.

The Code Needs to Survive the Whole Pipeline

A sample ID that looks fine in your map file still needs to match, exactly, the ID used in your raw filenames, your demultiplexed files, and your final count table. See Data Prep Output's example of simplifying a long, redundant sequencer-generated filename down to something usable, that simplified name has to land on the same code your map file already uses, or the two won't join. Decide on the final code before sequencing, not after, so there's one ID per sample to keep consistent throughout, rather than a raw name, a map file code, and a "what I actually meant" version that all drift slightly apart.

There Isn't a Perfect Scheme

We don't have a single example to hold up as universally correct, because every project's real constraints differ: how many components actually need to be in the ID versus recorded as separate metadata columns instead, how many samples you'll realistically have, whether the code needs to mean something to someone at the bench or just needs to be internally consistent. What consistently helps, regardless of the specific scheme: decide on it before sampling starts, write it down, and keep it fixed.