String Ops
Strings are where bioinformatics data is messiest. Gene symbols, sample IDs, chromosome
labels: all text, all inconsistent across files. stringr in R and the .str namespace in
Polars cover the same operations. The one that saves the most time is the last: cleaning
sample names so a join stops silently failing.
Every code block runs in a container in the companion repository, and the two languages agree on the results.
Basics: length, extract, detect
Section titled “Basics: length, extract, detect”Measure a string, pull a number out of it, and test for a pattern. Here, the chromosome
number is extracted from labels like chr17 by removing the chr prefix.
library(stringr)
annotation |> mutate( gene_len = str_length(gene), chrom_num = as.integer(str_remove(chromosome, "chr")), is_chr17 = str_detect(chromosome, "chr17") ) gene pathway chromosome gene_len chrom_num is_chr171 TP53 Apoptosis chr17 4 17 TRUE2 EGFR Growth signaling chr7 4 7 FALSE3 MYC Growth signaling chr8 3 8 FALSE4 BRCA1 DNA repair chr17 5 17 TRUE5 GAPDH Housekeeping chr12 5 12 FALSE6 ACTB Housekeeping chr7 4 7 FALSE7 KRAS Growth signaling chr12 4 12 FALSEannotation.with_columns( pl.col("gene").str.len_chars().alias("gene_len"), pl.col("chromosome").str.replace("chr", "").cast(pl.Int64).alias("chrom_num"), pl.col("chromosome").str.contains("chr17").alias("is_chr17"),)shape: (7, 6)┌───────┬──────────────────┬────────────┬──────────┬───────────┬──────────┐│ gene ┆ pathway ┆ chromosome ┆ gene_len ┆ chrom_num ┆ is_chr17 ││ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ││ str ┆ str ┆ str ┆ u32 ┆ i64 ┆ bool │╞═══════╪══════════════════╪════════════╪══════════╪═══════════╪══════════╡│ TP53 ┆ Apoptosis ┆ chr17 ┆ 4 ┆ 17 ┆ true ││ EGFR ┆ Growth signaling ┆ chr7 ┆ 4 ┆ 7 ┆ false ││ MYC ┆ Growth signaling ┆ chr8 ┆ 3 ┆ 8 ┆ false ││ BRCA1 ┆ DNA repair ┆ chr17 ┆ 5 ┆ 17 ┆ true ││ GAPDH ┆ Housekeeping ┆ chr12 ┆ 5 ┆ 12 ┆ false ││ ACTB ┆ Housekeeping ┆ chr7 ┆ 4 ┆ 7 ┆ false ││ KRAS ┆ Growth signaling ┆ chr12 ┆ 4 ┆ 12 ┆ false │└───────┴──────────────────┴────────────┴──────────┴───────────┴──────────┘Cleaning messy sample names
Section titled “Cleaning messy sample names”This is the one that bites everyone. The same sample arrives labelled s01, S_02,
S-03 , s04: mixed case, a hyphen where another file used an underscore, a stray
leading or trailing space. None of these match S01 on a join, and nothing warns you.
The fix is three steps: strip whitespace, upcase, remove the separators.
messy <- read.csv("../fixtures/messy_samples.csv")messy |> mutate(sample_id = str_replace_all(str_to_upper(str_trim(sample_label)), "[-_ ]", "")) sample_label rin sample_id1 s01 9.1 S012 S_02 8.7 S023 S-03 9.4 S034 s04 8.2 S045 S05 9.0 S056 S 06 7.9 S067 s-07 8.8 S078 S08 9.3 S08messy = pl.read_csv("../fixtures/messy_samples.csv")messy.with_columns( pl.col("sample_label") .str.strip_chars() .str.to_uppercase() .str.replace_all("[-_ ]", "") .alias("sample_id"))shape: (8, 3)┌──────────────┬─────┬───────────┐│ sample_label ┆ rin ┆ sample_id ││ --- ┆ --- ┆ --- ││ str ┆ f64 ┆ str │╞══════════════╪═════╪═══════════╡│ s01 ┆ 9.1 ┆ S01 ││ S_02 ┆ 8.7 ┆ S02 ││ S-03 ┆ 9.4 ┆ S03 ││ s04 ┆ 8.2 ┆ S04 ││ S05 ┆ 9.0 ┆ S05 ││ S 06 ┆ 7.9 ┆ S06 ││ s-07 ┆ 8.8 ┆ S07 ││ S08 ┆ 9.3 ┆ S08 │└──────────────┴─────┴───────────┘Why it matters: the join
Section titled “Why it matters: the join”The payoff is concrete. Join the sample labels to a per-sample summary before cleaning,
and only the one already-canonical label (S05) matches. After cleaning, all eight do.
raw_match <- inner_join(messy, sample_totals, by = c("sample_label" = "sample_id"))clean_match <- inner_join(messy, sample_totals, by = "sample_id")raw labels matched: 1 of 8cleaned labels matched: 8 of 8raw_match = messy.join(sample_totals, left_on="sample_label", right_on="sample_id", how="inner")clean_match = messy.join(sample_totals, on="sample_id", how="inner")raw labels matched: 1 of 8cleaned labels matched: 8 of 8Seven samples silently dropped, with no error, until you clean the key. When a join returns fewer rows than you expect, the labels are the first thing to check.