Skip to content

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.

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.

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 counts
1 S01 control TP53 393
2 S01 control EGFR 294
3 S01 control MYC 247
4 S01 control BRCA1 195
5 S01 control GAPDH 3939
6 S01 control ACTB 3117

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 counts
1 S05 treated TP53 619
2 S05 treated EGFR 757
3 S05 treated MYC 810
4 S05 treated BRCA1 204
5 S05 treated GAPDH 4376
6 S05 treated ACTB 3292

Pick a subset of columns by name.

select(expr, sample_id, gene, counts)
sample_id gene counts
1 S01 TP53 393
2 S01 EGFR 294
3 S01 MYC 247
4 S01 BRCA1 195
5 S01 GAPDH 3939
6 S01 ACTB 3117

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 counts
1 S03 control ACTB 4398
2 S05 treated GAPDH 4376
3 S02 control ACTB 4331
4 S08 treated GAPDH 4312
5 S06 treated ACTB 4199
6 S03 control GAPDH 4118

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_expr
1 S01 control TP53 393 8.622052
2 S01 control EGFR 294 8.204571
3 S01 control MYC 247 7.954196
4 S01 control BRCA1 195 7.614710
5 S01 control GAPDH 3939 11.943980
6 S01 control ACTB 3117 11.606405

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_expr
1 S07 854 9.739781
2 S08 834 9.705632
3 S05 810 9.663558
4 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.