Core Verbs
Most data wrangling is a handful of verbs applied over and over: keep some rows, keep some columns, sort, and compute new columns. dplyr in R and Polars in Python express the same four ideas with different words. This page runs both on one tidy bioinformatics table, so you can read a verb in whichever language you know and see its twin in the other.
Every code block runs in a container in the companion repository. The tables shown are the real output of those runs, and the two languages agree on every number.
The verbs at a glance
Section titled “The verbs at a glance”Polars borrowed some dplyr names, so two of the four verbs are the same word in both. The other two differ.
| Operation | dplyr (R) | Polars (Python) |
|---|---|---|
| keep rows | filter |
filter |
| keep columns | select |
select |
| sort rows | arrange |
sort |
| add a column | mutate |
with_columns |
A shared name is not a shared syntax. dplyr reads a column as if it were a variable in
scope (filter(expr, condition == "treated")), while Polars builds an expression on an
explicit column object (expr.filter(pl.col("condition") == "treated")), and combines
conditions with & and | rather than commas. That pl.col(...) expression style is the
real difference between the two, more than the verb names.
The data
Section titled “The data”expression.csv is a long counts table: one row per sample and gene. Eight samples, four
control and four treated, measured across six genes.
library(dplyr)
expr <- read.csv("../data/expression.csv")head(expr) sample_id condition gene counts1 S01 control TP53 3932 S01 control EGFR 2943 S01 control MYC 2474 S01 control BRCA1 1955 S01 control GAPDH 39396 S01 control ACTB 3117import polars as pl
expr = pl.read_csv("../data/expression.csv")expr.head()shape: (5, 4)┌───────────┬───────────┬───────┬────────┐│ sample_id ┆ condition ┆ gene ┆ counts ││ --- ┆ --- ┆ --- ┆ --- ││ str ┆ str ┆ str ┆ i64 │╞═══════════╪═══════════╪═══════╪════════╡│ S01 ┆ control ┆ TP53 ┆ 393 ││ S01 ┆ control ┆ EGFR ┆ 294 ││ S01 ┆ control ┆ MYC ┆ 247 ││ S01 ┆ control ┆ BRCA1 ┆ 195 ││ S01 ┆ control ┆ GAPDH ┆ 3939 │└───────────┴───────────┴───────┴────────┘filter: keep the rows you want
Section titled “filter: keep the rows you want”Subset rows by a condition. Here, only the treated samples. dplyr names the column
directly; Polars wraps it in pl.col.
treated <- filter(expr, condition == "treated")head(treated) sample_id condition gene counts1 S05 treated TP53 6192 S05 treated EGFR 7573 S05 treated MYC 8104 S05 treated BRCA1 2045 S05 treated GAPDH 43766 S05 treated ACTB 3292treated = expr.filter(pl.col("condition") == "treated")treated.head()shape: (5, 4)┌───────────┬───────────┬───────┬────────┐│ sample_id ┆ condition ┆ gene ┆ counts ││ --- ┆ --- ┆ --- ┆ --- ││ str ┆ str ┆ str ┆ i64 │╞═══════════╪═══════════╪═══════╪════════╡│ S05 ┆ treated ┆ TP53 ┆ 619 ││ S05 ┆ treated ┆ EGFR ┆ 757 ││ S05 ┆ treated ┆ MYC ┆ 810 ││ S05 ┆ treated ┆ BRCA1 ┆ 204 ││ S05 ┆ treated ┆ GAPDH ┆ 4376 │└───────────┴───────────┴───────┴────────┘select: keep the columns you want
Section titled “select: keep the columns you want”Pick a subset of columns by name.
select(expr, sample_id, gene, counts) sample_id gene counts1 S01 TP53 3932 S01 EGFR 2943 S01 MYC 2474 S01 BRCA1 1955 S01 GAPDH 39396 S01 ACTB 3117expr.select("sample_id", "gene", "counts")shape: (5, 3)┌───────────┬───────┬────────┐│ sample_id ┆ gene ┆ counts ││ --- ┆ --- ┆ --- ││ str ┆ str ┆ i64 │╞═══════════╪═══════╪════════╡│ S01 ┆ TP53 ┆ 393 ││ S01 ┆ EGFR ┆ 294 ││ S01 ┆ MYC ┆ 247 ││ S01 ┆ BRCA1 ┆ 195 ││ S01 ┆ GAPDH ┆ 3939 │└───────────┴───────┴────────┘arrange / sort: order the rows
Section titled “arrange / sort: order the rows”Sort by a column, highest first. The verb is arrange(desc(...)) in dplyr and
sort(..., descending=True) in Polars. The housekeeping genes ACTB and GAPDH top the list,
as expected.
arrange(expr, desc(counts)) sample_id condition gene counts1 S03 control ACTB 43982 S05 treated GAPDH 43763 S02 control ACTB 43314 S08 treated GAPDH 43125 S06 treated ACTB 41996 S03 control GAPDH 4118expr.sort("counts", descending=True)shape: (5, 4)┌───────────┬───────────┬───────┬────────┐│ sample_id ┆ condition ┆ gene ┆ counts ││ --- ┆ --- ┆ --- ┆ --- ││ str ┆ str ┆ str ┆ i64 │╞═══════════╪═══════════╪═══════╪════════╡│ S03 ┆ control ┆ ACTB ┆ 4398 ││ S05 ┆ treated ┆ GAPDH ┆ 4376 ││ S02 ┆ control ┆ ACTB ┆ 4331 ││ S08 ┆ treated ┆ GAPDH ┆ 4312 ││ S06 ┆ treated ┆ ACTB ┆ 4199 │└───────────┴───────────┴───────┴────────┘mutate / with_columns: add a derived column
Section titled “mutate / with_columns: add a derived column”Compute a new column from existing ones. Raw counts are usually log-transformed before
analysis, so here we add log2(counts + 1). dplyr calls it mutate; Polars calls it
with_columns and needs an explicit .alias for the new name.
expr <- mutate(expr, log2_expr = log2(counts + 1))head(expr) sample_id condition gene counts log2_expr1 S01 control TP53 393 8.6220522 S01 control EGFR 294 8.2045713 S01 control MYC 247 7.9541964 S01 control BRCA1 195 7.6147105 S01 control GAPDH 3939 11.9439806 S01 control ACTB 3117 11.606405expr = expr.with_columns((pl.col("counts") + 1).log(2).alias("log2_expr"))expr.head()shape: (5, 5)┌───────────┬───────────┬───────┬────────┬───────────┐│ sample_id ┆ condition ┆ gene ┆ counts ┆ log2_expr ││ --- ┆ --- ┆ --- ┆ --- ┆ --- ││ str ┆ str ┆ str ┆ i64 ┆ f64 │╞═══════════╪═══════════╪═══════╪════════╪═══════════╡│ S01 ┆ control ┆ TP53 ┆ 393 ┆ 8.622052 ││ S01 ┆ control ┆ EGFR ┆ 294 ┆ 8.204571 ││ S01 ┆ control ┆ MYC ┆ 247 ┆ 7.954196 ││ S01 ┆ control ┆ BRCA1 ┆ 195 ┆ 7.61471 ││ S01 ┆ control ┆ GAPDH ┆ 3939 ┆ 11.94398 │└───────────┴───────────┴───────┴────────┴───────────┘Chaining the verbs
Section titled “Chaining the verbs”The verbs compose. dplyr threads a table through the native pipe |>; Polars chains
methods on the frame. Both read the same way: take the treated MYC rows, keep three
columns, sort by expression.
expr |> filter(gene == "MYC", condition == "treated") |> select(sample_id, counts, log2_expr) |> arrange(desc(log2_expr)) sample_id counts log2_expr1 S07 854 9.7397812 S08 834 9.7056323 S05 810 9.6635584 S06 531 9.055282( expr.filter((pl.col("gene") == "MYC") & (pl.col("condition") == "treated")) .select("sample_id", "counts", "log2_expr") .sort("log2_expr", descending=True))shape: (4, 3)┌───────────┬────────┬───────────┐│ sample_id ┆ counts ┆ log2_expr ││ --- ┆ --- ┆ --- ││ str ┆ i64 ┆ f64 │╞═══════════╪════════╪═══════════╡│ S07 ┆ 854 ┆ 9.739781 ││ S08 ┆ 834 ┆ 9.705632 ││ S05 ┆ 810 ┆ 9.663558 ││ S06 ┆ 531 ┆ 9.055282 │└───────────┴────────┴───────────┘Same rows, same order, same values. Across the full table the two agree exactly: 24 treated rows, 76872 total counts, and a mean log2 expression of 9.541 for MYC in the treated group. That agreement is checked on every run by the harness parity gate, so this page cannot drift from its code.