Polars vs Tidyverse

polars side by side with the tidyverse

Create the data

Polars

import polars as pl

adsl = pl.DataFrame({
    "USUBJID": ["U01", "U02", "U03", "U04"],
    "ARM":     ["DRUG A", "DRUG B", "DRUG A", "DRUG B"],
    "AGE":     [55, 62, 48, 70],
    "SEX":     ["M", "F", "M", "F"],
})

advs = pl.DataFrame({
    "USUBJID": ["U01","U01","U02","U02","U03","U03","U04","U04"],
    "ARM":     ["DRUG A","DRUG A","DRUG B","DRUG B","DRUG A","DRUG A","DRUG B","DRUG B"],
    "AGE":     [55, 55, 62, 62, 48, 48, 70, 70],
    "SEX":     ["M","M","F","F","M","M","F","F"],
    "PARAM":   ["Systolic BP","Diastolic BP","Systolic BP","Diastolic BP","Systolic BP","Diastolic BP","Systolic BP","Diastolic BP"],
    "PARAMCD": ["SYSBP","DIABP","SYSBP","DIABP","SYSBP","DIABP","SYSBP","DIABP"],
    "AVAL":    [120, 80, 130, 85, 110, 70, 140, 90],
    "AVISIT":  ["Visit 1","Visit 1","Visit 1","Visit 1","Visit 2","Visit 2","Visit 2","Visit 2"],
    "AVISITN": [1, 1, 1, 1, 2, 2, 2, 2],
    "DTC":     ["2024-01-15","2024-01-15","2024-02-20","2024-02-20","2024-03-10","2024-03-10","2024-04-05","2024-04-05"],
})
adsl
shape: (4, 4)
USUBJIDARMAGESEX
strstri64str
"U01""DRUG A"55"M"
"U02""DRUG B"62"F"
"U03""DRUG A"48"M"
"U04""DRUG B"70"F"

tidyverse

library(dplyr)
library(tidyr)
library(stringr)
library(lubridate)
library(readr)

adsl <- tibble::tibble(
  USUBJID = c("U01", "U02", "U03", "U04"),
  ARM     = c("DRUG A", "DRUG B", "DRUG A", "DRUG B"),
  AGE     = c(55, 62, 48, 70),
  SEX     = c("M", "F", "M", "F")
)

advs <- tibble::tibble(
  USUBJID = c("U01","U01","U02","U02","U03","U03","U04","U04"),
  ARM     = c("DRUG A","DRUG A","DRUG B","DRUG B","DRUG A","DRUG A","DRUG B","DRUG B"),
  AGE     = c(55, 55, 62, 62, 48, 48, 70, 70),
  SEX     = c("M","M","F","F","M","M","F","F"),
  PARAM   = c("Systolic BP","Diastolic BP","Systolic BP","Diastolic BP","Systolic BP","Diastolic BP","Systolic BP","Diastolic BP"),
  PARAMCD = c("SYSBP","DIABP","SYSBP","DIABP","SYSBP","DIABP","SYSBP","DIABP"),
  AVAL    = c(120, 80, 130, 85, 110, 70, 140, 90),
  AVISIT  = c("Visit 1","Visit 1","Visit 1","Visit 1","Visit 2","Visit 2","Visit 2","Visit 2"),
  AVISITN = c(1, 1, 1, 1, 2, 2, 2, 2),
  DTC     = c("2024-01-15","2024-01-15","2024-02-20","2024-02-20","2024-03-10","2024-03-10","2024-04-05","2024-04-05")
)
adsl
# A tibble: 4 × 4
  USUBJID ARM      AGE SEX  
  <chr>   <chr>  <dbl> <chr>
1 U01     DRUG A    55 M    
2 U02     DRUG B    62 F    
3 U03     DRUG A    48 M    
4 U04     DRUG B    70 F    

Verbs

Select columns

Polars

adsl.select("USUBJID", "ARM", "AGE")
shape: (4, 3)
USUBJIDARMAGE
strstri64
"U01""DRUG A"55
"U02""DRUG B"62
"U03""DRUG A"48
"U04""DRUG B"70

tidyverse

adsl |> select(USUBJID, ARM, AGE)
# A tibble: 4 × 3
  USUBJID ARM      AGE
  <chr>   <chr>  <dbl>
1 U01     DRUG A    55
2 U02     DRUG B    62
3 U03     DRUG A    48
4 U04     DRUG B    70

Drop columns

Polars

adsl.drop("SEX")
shape: (4, 3)
USUBJIDARMAGE
strstri64
"U01""DRUG A"55
"U02""DRUG B"62
"U03""DRUG A"48
"U04""DRUG B"70

tidyverse

adsl |> select(-SEX)
# A tibble: 4 × 3
  USUBJID ARM      AGE
  <chr>   <chr>  <dbl>
1 U01     DRUG A    55
2 U02     DRUG B    62
3 U03     DRUG A    48
4 U04     DRUG B    70

Drop by rule, not by name

R’s where(is.numeric) and starts_with() have direct counterparts: cs.numeric() and cs.starts_with(), composed with | and &.

Polars

import polars.selectors as cs

adsl.select(cs.numeric() | cs.starts_with("U"))
shape: (4, 2)
USUBJIDAGE
stri64
"U01"55
"U02"62
"U03"48
"U04"70

tidyverse

adsl |> select(where(is.numeric) | starts_with("U"))
# A tibble: 4 × 2
    AGE USUBJID
  <dbl> <chr>  
1    55 U01    
2    62 U02    
3    48 U03    
4    70 U04    

Rename columns

The dictionary is keyed old-to-new, the opposite of rename()’s new = old.

Polars

adsl.rename({"AGE": "AGE_N"})
shape: (4, 4)
USUBJIDARMAGE_NSEX
strstri64str
"U01""DRUG A"55"M"
"U02""DRUG B"62"F"
"U03""DRUG A"48"M"
"U04""DRUG B"70"F"

tidyverse

adsl |> rename(AGE_N = AGE)
# A tibble: 4 × 4
  USUBJID ARM    AGE_N SEX  
  <chr>   <chr>  <dbl> <chr>
1 U01     DRUG A    55 M    
2 U02     DRUG B    62 F    
3 U03     DRUG A    48 M    
4 U04     DRUG B    70 F    

Filter rows

Conditions are expressions joined with &, |, ~; is_in is %in%. Note that polars has no unquoted column names - every reference goes through pl.col.

Polars

advs.filter(
    (pl.col("ARM") == "DRUG A") & (pl.col("AVISIT").is_in(["Visit 1", "Visit 2"]))
)
shape: (4, 10)
USUBJIDARMAGESEXPARAMPARAMCDAVALAVISITAVISITNDTC
strstri64strstrstri64stri64str
"U01""DRUG A"55"M""Systolic BP""SYSBP"120"Visit 1"1"2024-01-15"
"U01""DRUG A"55"M""Diastolic BP""DIABP"80"Visit 1"1"2024-01-15"
"U03""DRUG A"48"M""Systolic BP""SYSBP"110"Visit 2"2"2024-03-10"
"U03""DRUG A"48"M""Diastolic BP""DIABP"70"Visit 2"2"2024-03-10"

tidyverse

advs |> filter(ARM == "DRUG A" & AVISIT %in% c("Visit 1", "Visit 2"))
# A tibble: 4 × 10
  USUBJID ARM      AGE SEX   PARAM        PARAMCD  AVAL AVISIT  AVISITN DTC     
  <chr>   <chr>  <dbl> <chr> <chr>        <chr>   <dbl> <chr>     <dbl> <chr>   
1 U01     DRUG A    55 M     Systolic BP  SYSBP     120 Visit 1       1 2024-01…
2 U01     DRUG A    55 M     Diastolic BP DIABP      80 Visit 1       1 2024-01…
3 U03     DRUG A    48 M     Systolic BP  SYSBP     110 Visit 2       2 2024-03…
4 U03     DRUG A    48 M     Diastolic BP DIABP      70 Visit 2       2 2024-03…

Add or replace columns

Polars

(
    advs
    .with_columns(doubled=pl.col("AVAL") * 2)
    .with_columns(squared=pl.col("doubled") ** 2)
    .select("USUBJID", "AVISIT", "AVAL", "doubled", "squared")
)
shape: (8, 5)
USUBJIDAVISITAVALdoubledsquared
strstri64i64i64
"U01""Visit 1"12024057600
"U01""Visit 1"8016025600
"U02""Visit 1"13026067600
"U02""Visit 1"8517028900
"U03""Visit 2"11022048400
"U03""Visit 2"7014019600
"U04""Visit 2"14028078400
"U04""Visit 2"9018032400

tidyverse

advs |>
  mutate(
    doubled = AVAL * 2,
    squared = doubled^2
  ) |>
  select(USUBJID, AVISIT, AVAL, doubled, squared)
# A tibble: 8 × 5
  USUBJID AVISIT   AVAL doubled squared
  <chr>   <chr>   <dbl>   <dbl>   <dbl>
1 U01     Visit 1   120     240   57600
2 U01     Visit 1    80     160   25600
3 U02     Visit 1   130     260   67600
4 U02     Visit 1    85     170   28900
5 U03     Visit 2   110     220   48400
6 U03     Visit 2    70     140   19600
7 U04     Visit 2   140     280   78400
8 U04     Visit 2    90     180   32400

Conditional values (case_when)

Polars

advs.with_columns(
    response=pl.when(pl.col("AVAL") > 120)
    .then(pl.lit("HIGH"))
    .otherwise(pl.lit("NORMAL"))
).select("USUBJID", "AVAL", "response")
shape: (8, 3)
USUBJIDAVALresponse
stri64str
"U01"120"NORMAL"
"U01"80"NORMAL"
"U02"130"HIGH"
"U02"85"NORMAL"
"U03"110"NORMAL"
"U03"70"NORMAL"
"U04"140"HIGH"
"U04"90"NORMAL"

tidyverse

advs |>
  mutate(response = case_when(AVAL > 120 ~ "HIGH", TRUE ~ "NORMAL")) |>
  select(USUBJID, AVAL, response)
# A tibble: 8 × 3
  USUBJID  AVAL response
  <chr>   <dbl> <chr>   
1 U01       120 NORMAL  
2 U01        80 NORMAL  
3 U02       130 HIGH    
4 U02        85 NORMAL  
5 U03       110 NORMAL  
6 U03        70 NORMAL  
7 U04       140 HIGH    
8 U04        90 NORMAL  

Sort rows

The default placement of missing values differs: arrange() sorts NA last, sort() puts nulls first unless nulls_last=True.

Polars

adsl.sort("AGE", descending=True, nulls_last=True)
shape: (4, 4)
USUBJIDARMAGESEX
strstri64str
"U04""DRUG B"70"F"
"U02""DRUG B"62"F"
"U01""DRUG A"55"M"
"U03""DRUG A"48"M"

tidyverse

adsl |> arrange(desc(AGE))
# A tibble: 4 × 4
  USUBJID ARM      AGE SEX  
  <chr>   <chr>  <dbl> <chr>
1 U04     DRUG B    70 F    
2 U02     DRUG B    62 F    
3 U01     DRUG A    55 M    
4 U03     DRUG A    48 M    

Slice by row position

Polars

adsl.head(3)
shape: (3, 4)
USUBJIDARMAGESEX
strstri64str
"U01""DRUG A"55"M"
"U02""DRUG B"62"F"
"U03""DRUG A"48"M"
adsl.tail(2)
shape: (2, 4)
USUBJIDARMAGESEX
strstri64str
"U03""DRUG A"48"M"
"U04""DRUG B"70"F"
adsl.slice(1, 2)
shape: (2, 4)
USUBJIDARMAGESEX
strstri64str
"U02""DRUG B"62"F"
"U03""DRUG A"48"M"

tidyverse

adsl |> slice_head(n = 3)
# A tibble: 3 × 4
  USUBJID ARM      AGE SEX  
  <chr>   <chr>  <dbl> <chr>
1 U01     DRUG A    55 M    
2 U02     DRUG B    62 F    
3 U03     DRUG A    48 M    
adsl |> slice_tail(n = 2)
# A tibble: 2 × 4
  USUBJID ARM      AGE SEX  
  <chr>   <chr>  <dbl> <chr>
1 U03     DRUG A    48 M    
2 U04     DRUG B    70 F    
adsl |> slice(2:3)
# A tibble: 2 × 4
  USUBJID ARM      AGE SEX  
  <chr>   <chr>  <dbl> <chr>
1 U02     DRUG B    62 F    
2 U03     DRUG A    48 M    

Distinct

Polars

advs.select("USUBJID", "ARM").unique().sort("USUBJID")
shape: (4, 2)
USUBJIDARM
strstr
"U01""DRUG A"
"U02""DRUG B"
"U03""DRUG A"
"U04""DRUG B"

tidyverse

advs |> distinct(USUBJID, ARM) |> arrange(USUBJID)
# A tibble: 4 × 2
  USUBJID ARM   
  <chr>   <chr> 
1 U01     DRUG A
2 U02     DRUG B
3 U03     DRUG A
4 U04     DRUG B

Type conversion

Polars

adsl.with_columns(id=pl.col("USUBJID").str.extract(r"(\d+)").cast(pl.Int32)).select("USUBJID", "id")
shape: (4, 2)
USUBJIDid
stri32
"U01"1
"U02"2
"U03"3
"U04"4

tidyverse

adsl |> mutate(id = as.integer(str_extract(USUBJID, "\\d+"))) |> select(USUBJID, id)
# A tibble: 4 × 2
  USUBJID    id
  <chr>   <int>
1 U01         1
2 U02         2
3 U03         3
4 U04         4

Batch rename (prefix / suffix)

Polars

adsl.select(pl.col("AGE").name.prefix("BASE_"))
shape: (4, 1)
BASE_AGE
i64
55
62
48
70
adsl.select(pl.all().name.suffix("_raw"))
shape: (4, 4)
USUBJID_rawARM_rawAGE_rawSEX_raw
strstri64str
"U01""DRUG A"55"M"
"U02""DRUG B"62"F"
"U03""DRUG A"48"M"
"U04""DRUG B"70"F"

tidyverse

adsl |> select(AGE) |> rename_with(~ str_c("BASE_", .x))
# A tibble: 4 × 1
  BASE_AGE
     <dbl>
1       55
2       62
3       48
4       70
adsl |> rename_with(~ str_c(.x, "_raw"))
# A tibble: 4 × 4
  USUBJID_raw ARM_raw AGE_raw SEX_raw
  <chr>       <chr>     <dbl> <chr>  
1 U01         DRUG A       55 M      
2 U02         DRUG B       62 F      
3 U03         DRUG A       48 M      
4 U04         DRUG B       70 F      

Missing values

Check for missing values

Polars

advs.null_count()
shape: (1, 10)
USUBJIDARMAGESEXPARAMPARAMCDAVALAVISITAVISITNDTC
u32u32u32u32u32u32u32u32u32u32
0000000000

tidyverse

advs |> summarise(across(everything(), ~ sum(is.na(.))))
# A tibble: 1 × 10
  USUBJID   ARM   AGE   SEX PARAM PARAMCD  AVAL AVISIT AVISITN   DTC
    <int> <int> <int> <int> <int>   <int> <int>  <int>   <int> <int>
1       0     0     0     0     0       0     0      0       0     0

Drop rows with a missing value

Polars

miss = advs.with_columns(AVAL=pl.when(pl.col("USUBJID") == "U02").then(None).otherwise(pl.col("AVAL")))
miss.drop_nulls("AVAL").select("USUBJID", "AVAL")
shape: (6, 2)
USUBJIDAVAL
stri64
"U01"120
"U01"80
"U03"110
"U03"70
"U04"140
"U04"90

tidyverse

miss <- advs |> mutate(AVAL = ifelse(USUBJID == "U02", NA, AVAL))
miss |> drop_na(AVAL) |> select(USUBJID, AVAL)
# A tibble: 6 × 2
  USUBJID  AVAL
  <chr>   <dbl>
1 U01       120
2 U01        80
3 U03       110
4 U03        70
5 U04       140
6 U04        90

Fill with a constant

Polars

miss.with_columns(filled=pl.col("AVAL").fill_null(0)).select("USUBJID", "AVAL", "filled")
shape: (8, 3)
USUBJIDAVALfilled
stri64i64
"U01"120120
"U01"8080
"U02"null0
"U02"null0
"U03"110110
"U03"7070
"U04"140140
"U04"9090

tidyverse

miss |>
  mutate(filled = replace_na(AVAL, 0)) |>
  select(USUBJID, AVAL, filled)
# A tibble: 8 × 3
  USUBJID  AVAL filled
  <chr>   <dbl>  <dbl>
1 U01       120    120
2 U01        80     80
3 U02        NA      0
4 U02        NA      0
5 U03       110    110
6 U03        70     70
7 U04       140    140
8 U04        90     90

Carry the last value forward

Polars

miss.with_columns(
    filled=pl.col("AVAL").fill_null(strategy="forward")
).select("USUBJID", "AVISIT", "AVAL", "filled")
shape: (8, 4)
USUBJIDAVISITAVALfilled
strstri64i64
"U01""Visit 1"120120
"U01""Visit 1"8080
"U02""Visit 1"null80
"U02""Visit 1"null80
"U03""Visit 2"110110
"U03""Visit 2"7070
"U04""Visit 2"140140
"U04""Visit 2"9090

tidyverse

miss |>
  fill(AVAL, .direction = "down") |>
  select(USUBJID, AVISIT, AVAL)
# A tibble: 8 × 3
  USUBJID AVISIT   AVAL
  <chr>   <chr>   <dbl>
1 U01     Visit 1   120
2 U01     Visit 1    80
3 U02     Visit 1    80
4 U02     Visit 1    80
5 U03     Visit 2   110
6 U03     Visit 2    70
7 U04     Visit 2   140
8 U04     Visit 2    90

Fill within a group

Polars

miss.with_columns(
    filled=pl.col("AVAL").fill_null(strategy="forward").over("USUBJID")
).select("USUBJID", "AVISIT", "AVAL", "filled")
shape: (8, 4)
USUBJIDAVISITAVALfilled
strstri64i64
"U01""Visit 1"120120
"U01""Visit 1"8080
"U02""Visit 1"nullnull
"U02""Visit 1"nullnull
"U03""Visit 2"110110
"U03""Visit 2"7070
"U04""Visit 2"140140
"U04""Visit 2"9090

tidyverse

miss |>
  group_by(USUBJID) |>
  fill(AVAL, .direction = "down") |>
  ungroup() |>
  select(USUBJID, AVISIT, AVAL)
# A tibble: 8 × 3
  USUBJID AVISIT   AVAL
  <chr>   <chr>   <dbl>
1 U01     Visit 1   120
2 U01     Visit 1    80
3 U02     Visit 1    NA
4 U02     Visit 1    NA
5 U03     Visit 2   110
6 U03     Visit 2    70
7 U04     Visit 2   140
8 U04     Visit 2    90

Coalesce

pl.coalesce takes columns, coalesce() takes vectors.

Polars

co = pl.DataFrame({"a": [1, None, 3], "b": [10, 20, None]})
co.with_columns(c=pl.coalesce("a", "b"))
shape: (3, 3)
abc
i64i64i64
1101
null2020
3null3

tidyverse

co <- tibble(a = c(1, NA, 3), b = c(10, 20, NA))
co |> mutate(c = coalesce(a, b))
# A tibble: 3 × 3
      a     b     c
  <dbl> <dbl> <dbl>
1     1    10     1
2    NA    20    20
3     3    NA     3

Delete empty rows

Polars

blank = pl.DataFrame({"USUBJID": [None], "AVAL": [None]})
pl.concat([miss.select("USUBJID", "AVAL"), blank], how="vertical").drop_nulls()
shape: (6, 2)
USUBJIDAVAL
stri64
"U01"120
"U01"80
"U03"110
"U03"70
"U04"140
"U04"90

tidyverse

blank <- tibble(USUBJID = NA_character_, AVAL = NA_real_)
bind_rows(miss |> select(USUBJID, AVAL), blank) |> drop_na()
# A tibble: 6 × 2
  USUBJID  AVAL
  <chr>   <dbl>
1 U01       120
2 U01        80
3 U03       110
4 U03        70
5 U04       140
6 U04        90

Group and window

Summarise by group

Polars

advs.group_by("ARM").agg(
    n=pl.len(),
    mean_age=pl.col("AGE").mean(),
    mean_aval=pl.col("AVAL").mean(),
)
shape: (2, 4)
ARMnmean_agemean_aval
stru32f64f64
"DRUG B"466.0111.25
"DRUG A"451.595.0

tidyverse

advs |>
  group_by(ARM) |>
  summarise(
    n = n(),
    mean_age = mean(AGE),
    mean_aval = mean(AVAL)
  )
# A tibble: 2 × 4
  ARM        n mean_age mean_aval
  <chr>  <int>    <dbl>     <dbl>
1 DRUG A     4     51.5       95 
2 DRUG B     4     66        111.

Common statistics

Polars

advs.select(
    mean=pl.col("AVAL").mean(),
    median=pl.col("AVAL").median(),
    sd=pl.col("AVAL").std(),
    p75=pl.col("AVAL").quantile(0.75),
    total=pl.col("AVAL").sum(),
)
shape: (1, 5)
meanmediansdp75total
f64f64f64f64i64
103.125100.025.486341120.0825

tidyverse

advs |>
  summarise(
    mean = mean(AVAL),
    median = median(AVAL),
    sd = sd(AVAL),
    p75 = quantile(AVAL, 0.75),
    total = sum(AVAL)
  )
# A tibble: 1 × 5
   mean median    sd   p75 total
  <dbl>  <dbl> <dbl> <dbl> <dbl>
1  103.    100  25.5  122.   825

Summarise many columns at once (across)

Polars

advs.group_by("ARM").agg(
    pl.col("AGE", "AVAL").mean().name.prefix("mean_"),
    pl.col("AGE", "AVAL").std().name.prefix("sd_"),
)
shape: (2, 5)
ARMmean_AGEmean_AVALsd_AGEsd_AVAL
strf64f64f64f64
"DRUG B"66.0111.254.61880227.801379
"DRUG A"51.595.04.04145223.804761

tidyverse

advs |>
  group_by(ARM) |>
  summarise(across(c(AGE, AVAL), list(mean = mean, sd = sd)))
# A tibble: 2 × 5
  ARM    AGE_mean AGE_sd AVAL_mean AVAL_sd
  <chr>     <dbl>  <dbl>     <dbl>   <dbl>
1 DRUG A     51.5   4.04       95     23.8
2 DRUG B     66     4.62      111.    27.8

Count rows per group

len() is n().

Polars

advs.group_by("ARM").len()
shape: (2, 2)
ARMlen
stru32
"DRUG A"4
"DRUG B"4

tidyverse

advs |> count(ARM)
# A tibble: 2 × 2
  ARM        n
  <chr>  <int>
1 DRUG A     4
2 DRUG B     4

Window value (recycle an aggregate)

Polars

advs.with_columns(
    subj_mean=pl.col("AVAL").mean().over("USUBJID")
).select("USUBJID", "AVISIT", "AVAL", "subj_mean")
shape: (8, 4)
USUBJIDAVISITAVALsubj_mean
strstri64f64
"U01""Visit 1"120100.0
"U01""Visit 1"80100.0
"U02""Visit 1"130107.5
"U02""Visit 1"85107.5
"U03""Visit 2"11090.0
"U03""Visit 2"7090.0
"U04""Visit 2"140115.0
"U04""Visit 2"90115.0

tidyverse

advs |>
  group_by(USUBJID) |>
  mutate(subj_mean = mean(AVAL)) |>
  ungroup() |>
  select(USUBJID, AVISIT, AVAL, subj_mean)
# A tibble: 8 × 4
  USUBJID AVISIT   AVAL subj_mean
  <chr>   <chr>   <dbl>     <dbl>
1 U01     Visit 1   120      100 
2 U01     Visit 1    80      100 
3 U02     Visit 1   130      108.
4 U02     Visit 1    85      108.
5 U03     Visit 2   110       90 
6 U03     Visit 2    70       90 
7 U04     Visit 2   140      115 
8 U04     Visit 2    90      115 

Running total within a group

Polars

advs.with_columns(
    cum_aval=pl.col("AVAL").cum_sum().over("USUBJID")
).select("USUBJID", "AVISIT", "AVAL", "cum_aval")
shape: (8, 4)
USUBJIDAVISITAVALcum_aval
strstri64i64
"U01""Visit 1"120120
"U01""Visit 1"80200
"U02""Visit 1"130130
"U02""Visit 1"85215
"U03""Visit 2"110110
"U03""Visit 2"70180
"U04""Visit 2"140140
"U04""Visit 2"90230

tidyverse

advs |>
  group_by(USUBJID) |>
  mutate(cum_aval = cumsum(AVAL)) |>
  ungroup() |>
  select(USUBJID, AVISIT, AVAL, cum_aval)
# A tibble: 8 × 4
  USUBJID AVISIT   AVAL cum_aval
  <chr>   <chr>   <dbl>    <dbl>
1 U01     Visit 1   120      120
2 U01     Visit 1    80      200
3 U02     Visit 1   130      130
4 U02     Visit 1    85      215
5 U03     Visit 2   110      110
6 U03     Visit 2    70      180
7 U04     Visit 2   140      140
8 U04     Visit 2    90      230

Lag and lead

Polars

advs.with_columns(
    prev=pl.col("AVAL").shift(1).over("USUBJID"),
    lead_val=pl.col("AVAL").shift(-1).over("USUBJID"),
).select("USUBJID", "AVISIT", "AVAL", "prev", "lead_val")
shape: (8, 5)
USUBJIDAVISITAVALprevlead_val
strstri64i64i64
"U01""Visit 1"120null80
"U01""Visit 1"80120null
"U02""Visit 1"130null85
"U02""Visit 1"85130null
"U03""Visit 2"110null70
"U03""Visit 2"70110null
"U04""Visit 2"140null90
"U04""Visit 2"90140null

tidyverse

advs |>
  group_by(USUBJID) |>
  mutate(
    prev = lag(AVAL),
    lead_val = lead(AVAL)
  ) |>
  ungroup() |>
  select(USUBJID, AVISIT, AVAL, prev, lead_val)
# A tibble: 8 × 5
  USUBJID AVISIT   AVAL  prev lead_val
  <chr>   <chr>   <dbl> <dbl>    <dbl>
1 U01     Visit 1   120    NA       80
2 U01     Visit 1    80   120       NA
3 U02     Visit 1   130    NA       85
4 U02     Visit 1    85   130       NA
5 U03     Visit 2   110    NA       70
6 U03     Visit 2    70   110       NA
7 U04     Visit 2   140    NA       90
8 U04     Visit 2    90   140       NA

First and last row of each group

Polars

(
    advs
    .with_columns(
        rn=pl.col("AVAL").rank(method="ordinal").over("USUBJID", "AVISITN"),
        is_first=pl.col("AVAL").rank(method="ordinal").over("USUBJID", "AVISITN") == 1,
        is_last=pl.col("AVAL").rank(method="ordinal").over("USUBJID", "AVISITN") == pl.len().over("USUBJID", "AVISITN"),
    )
    .select("USUBJID", "AVISIT", "rn", "is_first", "is_last")
)
shape: (8, 5)
USUBJIDAVISITrnis_firstis_last
strstru32boolbool
"U01""Visit 1"2falsetrue
"U01""Visit 1"1truefalse
"U02""Visit 1"2falsetrue
"U02""Visit 1"1truefalse
"U03""Visit 2"2falsetrue
"U03""Visit 2"1truefalse
"U04""Visit 2"2falsetrue
"U04""Visit 2"1truefalse

tidyverse

advs |>
  group_by(USUBJID, AVISITN) |>
  mutate(
    rn = row_number(),
    is_first = row_number() == 1,
    is_last = row_number() == n()
  ) |>
  ungroup() |>
  select(USUBJID, AVISIT, rn, is_first, is_last)
# A tibble: 8 × 5
  USUBJID AVISIT     rn is_first is_last
  <chr>   <chr>   <int> <lgl>    <lgl>  
1 U01     Visit 1     1 TRUE     FALSE  
2 U01     Visit 1     2 FALSE    TRUE   
3 U02     Visit 1     1 TRUE     FALSE  
4 U02     Visit 1     2 FALSE    TRUE   
5 U03     Visit 2     1 TRUE     FALSE  
6 U03     Visit 2     2 FALSE    TRUE   
7 U04     Visit 2     1 TRUE     FALSE  
8 U04     Visit 2     2 FALSE    TRUE   

Row-wise sum of several columns

Polars

advs.with_columns(total=pl.sum_horizontal("AGE", "AVAL")).select("USUBJID", "AGE", "AVAL", "total")
shape: (8, 4)
USUBJIDAGEAVALtotal
stri64i64i64
"U01"55120175
"U01"5580135
"U02"62130192
"U02"6285147
"U03"48110158
"U03"4870118
"U04"70140210
"U04"7090160

tidyverse

advs |>
  mutate(total = AGE + AVAL) |>
  select(USUBJID, AGE, AVAL, total)
# A tibble: 8 × 4
  USUBJID   AGE  AVAL total
  <chr>   <dbl> <dbl> <dbl>
1 U01        55   120   175
2 U01        55    80   135
3 U02        62   130   192
4 U02        62    85   147
5 U03        48   110   158
6 U03        48    70   118
7 U04        70   140   210
8 U04        70    90   160

Join and reshape

Left join

Polars

arms = pl.DataFrame({"ARM": ["DRUG A", "DRUG B"], "ARMCD": ["A", "B"]})
adsl.join(arms, on="ARM", how="left")
shape: (4, 5)
USUBJIDARMAGESEXARMCD
strstri64strstr
"U01""DRUG A"55"M""A"
"U02""DRUG B"62"F""B"
"U03""DRUG A"48"M""A"
"U04""DRUG B"70"F""B"

tidyverse

arms <- tibble(ARM = c("DRUG A", "DRUG B"), ARMCD = c("A", "B"))
adsl |> left_join(arms, by = "ARM")
# A tibble: 4 × 5
  USUBJID ARM      AGE SEX   ARMCD
  <chr>   <chr>  <dbl> <chr> <chr>
1 U01     DRUG A    55 M     A    
2 U02     DRUG B    62 F     B    
3 U03     DRUG A    48 M     A    
4 U04     DRUG B    70 F     B    

Set operations

Polars

a = pl.DataFrame({"USUBJID": ["U01", "U02", "U03"]})
b = pl.DataFrame({"USUBJID": ["U02", "U03", "U04"]})
a.filter(pl.col("USUBJID").is_in(b["USUBJID"].to_list()))
shape: (2, 1)
USUBJID
str
"U02"
"U03"
a.join(b, on="USUBJID", how="anti")
shape: (1, 1)
USUBJID
str
"U01"

tidyverse

a <- tibble(USUBJID = c("U01", "U02", "U03"))
b <- tibble(USUBJID = c("U02", "U03", "U04"))
a |> filter(USUBJID %in% b$USUBJID)
# A tibble: 2 × 1
  USUBJID
  <chr>  
1 U02    
2 U03    
a |> anti_join(b, by = "USUBJID")
# A tibble: 1 × 1
  USUBJID
  <chr>  
1 U01    

Pivot long to wide

Polars

wide = advs.pivot(index="USUBJID", on="PARAMCD", values="AVAL")
wide
shape: (4, 3)
USUBJIDSYSBPDIABP
stri64i64
"U01"12080
"U02"13085
"U03"11070
"U04"14090

tidyverse

wide <- advs |> pivot_wider(names_from = PARAMCD, values_from = AVAL)
wide
# A tibble: 8 × 10
  USUBJID ARM      AGE SEX   PARAM        AVISIT  AVISITN DTC        SYSBP DIABP
  <chr>   <chr>  <dbl> <chr> <chr>        <chr>     <dbl> <chr>      <dbl> <dbl>
1 U01     DRUG A    55 M     Systolic BP  Visit 1       1 2024-01-15   120    NA
2 U01     DRUG A    55 M     Diastolic BP Visit 1       1 2024-01-15    NA    80
3 U02     DRUG B    62 F     Systolic BP  Visit 1       1 2024-02-20   130    NA
4 U02     DRUG B    62 F     Diastolic BP Visit 1       1 2024-02-20    NA    85
5 U03     DRUG A    48 M     Systolic BP  Visit 2       2 2024-03-10   110    NA
6 U03     DRUG A    48 M     Diastolic BP Visit 2       2 2024-03-10    NA    70
7 U04     DRUG B    70 F     Systolic BP  Visit 2       2 2024-04-05   140    NA
8 U04     DRUG B    70 F     Diastolic BP Visit 2       2 2024-04-05    NA    90

Pivot wide to long

Polars

wide.unpivot(index="USUBJID", on=["SYSBP","DIABP"], variable_name="PARAMCD", value_name="AVAL")
shape: (8, 3)
USUBJIDPARAMCDAVAL
strstri64
"U01""SYSBP"120
"U02""SYSBP"130
"U03""SYSBP"110
"U04""SYSBP"140
"U01""DIABP"80
"U02""DIABP"85
"U03""DIABP"70
"U04""DIABP"90

tidyverse

wide |> pivot_longer(c(SYSBP, DIABP), names_to = "PARAMCD", values_to = "AVAL")
# A tibble: 16 × 10
   USUBJID ARM      AGE SEX   PARAM        AVISIT  AVISITN DTC     PARAMCD  AVAL
   <chr>   <chr>  <dbl> <chr> <chr>        <chr>     <dbl> <chr>   <chr>   <dbl>
 1 U01     DRUG A    55 M     Systolic BP  Visit 1       1 2024-0… SYSBP     120
 2 U01     DRUG A    55 M     Systolic BP  Visit 1       1 2024-0… DIABP      NA
 3 U01     DRUG A    55 M     Diastolic BP Visit 1       1 2024-0… SYSBP      NA
 4 U01     DRUG A    55 M     Diastolic BP Visit 1       1 2024-0… DIABP      80
 5 U02     DRUG B    62 F     Systolic BP  Visit 1       1 2024-0… SYSBP     130
 6 U02     DRUG B    62 F     Systolic BP  Visit 1       1 2024-0… DIABP      NA
 7 U02     DRUG B    62 F     Diastolic BP Visit 1       1 2024-0… SYSBP      NA
 8 U02     DRUG B    62 F     Diastolic BP Visit 1       1 2024-0… DIABP      85
 9 U03     DRUG A    48 M     Systolic BP  Visit 2       2 2024-0… SYSBP     110
10 U03     DRUG A    48 M     Systolic BP  Visit 2       2 2024-0… DIABP      NA
11 U03     DRUG A    48 M     Diastolic BP Visit 2       2 2024-0… SYSBP      NA
12 U03     DRUG A    48 M     Diastolic BP Visit 2       2 2024-0… DIABP      70
13 U04     DRUG B    70 F     Systolic BP  Visit 2       2 2024-0… SYSBP     140
14 U04     DRUG B    70 F     Systolic BP  Visit 2       2 2024-0… DIABP      NA
15 U04     DRUG B    70 F     Diastolic BP Visit 2       2 2024-0… SYSBP      NA
16 U04     DRUG B    70 F     Diastolic BP Visit 2       2 2024-0… DIABP      90

Stack frames vertically

concat([...], how="vertical") is bind_rows.

Polars

top = advs.head(2)
bot = advs.tail(2)
pl.concat([top, bot], how="vertical")
shape: (4, 10)
USUBJIDARMAGESEXPARAMPARAMCDAVALAVISITAVISITNDTC
strstri64strstrstri64stri64str
"U01""DRUG A"55"M""Systolic BP""SYSBP"120"Visit 1"1"2024-01-15"
"U01""DRUG A"55"M""Diastolic BP""DIABP"80"Visit 1"1"2024-01-15"
"U04""DRUG B"70"F""Systolic BP""SYSBP"140"Visit 2"2"2024-04-05"
"U04""DRUG B"70"F""Diastolic BP""DIABP"90"Visit 2"2"2024-04-05"

tidyverse

top <- advs |> head(2)
bot <- advs |> tail(2)
bind_rows(top, bot)
# A tibble: 4 × 10
  USUBJID ARM      AGE SEX   PARAM        PARAMCD  AVAL AVISIT  AVISITN DTC     
  <chr>   <chr>  <dbl> <chr> <chr>        <chr>   <dbl> <chr>     <dbl> <chr>   
1 U01     DRUG A    55 M     Systolic BP  SYSBP     120 Visit 1       1 2024-01…
2 U01     DRUG A    55 M     Diastolic BP DIABP      80 Visit 1       1 2024-01…
3 U04     DRUG B    70 F     Systolic BP  SYSBP     140 Visit 2       2 2024-04…
4 U04     DRUG B    70 F     Diastolic BP DIABP      90 Visit 2       2 2024-04…

Stack frames side by side

Polars

left = adsl.select("USUBJID", "ARM")
right = adsl.select("AGE", "SEX")
pl.concat([left, right], how="horizontal_extend")
shape: (4, 4)
USUBJIDARMAGESEX
strstri64str
"U01""DRUG A"55"M"
"U02""DRUG B"62"F"
"U03""DRUG A"48"M"
"U04""DRUG B"70"F"

tidyverse

left <- adsl |> select(USUBJID, ARM)
right <- adsl |> select(AGE, SEX)
bind_cols(left, right)
# A tibble: 4 × 4
  USUBJID ARM      AGE SEX  
  <chr>   <chr>  <dbl> <chr>
1 U01     DRUG A    55 M    
2 U02     DRUG B    62 F    
3 U03     DRUG A    48 M    
4 U04     DRUG B    70 F    

Strings

Change case

Polars

advs.select(
    upper=pl.col("ARM").str.to_uppercase(),
    lower=pl.col("PARAMCD").str.to_lowercase(),
    title=pl.col("PARAM").str.to_titlecase(),
)
shape: (8, 3)
upperlowertitle
strstrstr
"DRUG A""sysbp""Systolic Bp"
"DRUG A""diabp""Diastolic Bp"
"DRUG B""sysbp""Systolic Bp"
"DRUG B""diabp""Diastolic Bp"
"DRUG A""sysbp""Systolic Bp"
"DRUG A""diabp""Diastolic Bp"
"DRUG B""sysbp""Systolic Bp"
"DRUG B""diabp""Diastolic Bp"

tidyverse

advs |> transmute(
  upper = str_to_upper(ARM),
  lower = str_to_lower(PARAMCD),
  title = str_to_title(PARAM)
)
# A tibble: 8 × 3
  upper  lower title       
  <chr>  <chr> <chr>       
1 DRUG A sysbp Systolic Bp 
2 DRUG A diabp Diastolic Bp
3 DRUG B sysbp Systolic Bp 
4 DRUG B diabp Diastolic Bp
5 DRUG A sysbp Systolic Bp 
6 DRUG A diabp Diastolic Bp
7 DRUG B sysbp Systolic Bp 
8 DRUG B diabp Diastolic Bp

Trim and measure length

Polars

advs.select(
    n=pl.col("PARAM").str.len_chars(),
    trimmed=pl.col("PARAM").str.strip_chars(),
)
shape: (8, 2)
ntrimmed
u32str
11"Systolic BP"
12"Diastolic BP"
11"Systolic BP"
12"Diastolic BP"
11"Systolic BP"
12"Diastolic BP"
11"Systolic BP"
12"Diastolic BP"

tidyverse

advs |> transmute(
  n = str_length(PARAM),
  trimmed = str_trim(PARAM)
)
# A tibble: 8 × 2
      n trimmed     
  <int> <chr>       
1    11 Systolic BP 
2    12 Diastolic BP
3    11 Systolic BP 
4    12 Diastolic BP
5    11 Systolic BP 
6    12 Diastolic BP
7    11 Systolic BP 
8    12 Diastolic BP

Test for a pattern

Polars

advs.filter(pl.col("PARAMCD").str.contains(r"^S"))
shape: (4, 10)
USUBJIDARMAGESEXPARAMPARAMCDAVALAVISITAVISITNDTC
strstri64strstrstri64stri64str
"U01""DRUG A"55"M""Systolic BP""SYSBP"120"Visit 1"1"2024-01-15"
"U02""DRUG B"62"F""Systolic BP""SYSBP"130"Visit 1"1"2024-02-20"
"U03""DRUG A"48"M""Systolic BP""SYSBP"110"Visit 2"2"2024-03-10"
"U04""DRUG B"70"F""Systolic BP""SYSBP"140"Visit 2"2"2024-04-05"

tidyverse

advs |> filter(str_detect(PARAMCD, "^S"))
# A tibble: 4 × 10
  USUBJID ARM      AGE SEX   PARAM       PARAMCD  AVAL AVISIT  AVISITN DTC      
  <chr>   <chr>  <dbl> <chr> <chr>       <chr>   <dbl> <chr>     <dbl> <chr>    
1 U01     DRUG A    55 M     Systolic BP SYSBP     120 Visit 1       1 2024-01-…
2 U02     DRUG B    62 F     Systolic BP SYSBP     130 Visit 1       1 2024-02-…
3 U03     DRUG A    48 M     Systolic BP SYSBP     110 Visit 2       2 2024-03-…
4 U04     DRUG B    70 F     Systolic BP SYSBP     140 Visit 2       2 2024-04-…

Extract the first match

Polars

advs.with_columns(num=pl.col("USUBJID").str.extract(r"(\d+)")).select("USUBJID", "num")
shape: (8, 2)
USUBJIDnum
strstr
"U01""01"
"U01""01"
"U02""02"
"U02""02"
"U03""03"
"U03""03"
"U04""04"
"U04""04"

tidyverse

advs |> mutate(num = str_extract(USUBJID, "\\d+")) |> select(USUBJID, num)
# A tibble: 8 × 2
  USUBJID num  
  <chr>   <chr>
1 U01     01   
2 U01     01   
3 U02     02   
4 U02     02   
5 U03     03   
6 U03     03   
7 U04     04   
8 U04     04   

Replace a pattern

Polars

advs.with_columns(arm=pl.col("ARM").str.replace(r"DRUG ", "")).select("ARM", "arm")
shape: (8, 2)
ARMarm
strstr
"DRUG A""A"
"DRUG A""A"
"DRUG B""B"
"DRUG B""B"
"DRUG A""A"
"DRUG A""A"
"DRUG B""B"
"DRUG B""B"

tidyverse

advs |> mutate(arm = str_remove(ARM, "DRUG ")) |> select(ARM, arm)
# A tibble: 8 × 2
  ARM    arm  
  <chr>  <chr>
1 DRUG A A    
2 DRUG A A    
3 DRUG B B    
4 DRUG B B    
5 DRUG A A    
6 DRUG A A    
7 DRUG B B    
8 DRUG B B    

Split and merge

Polars

advs.select(
    parts=pl.col("PARAM").str.split(" "),
    merged=pl.col("USUBJID") + pl.lit("-") + pl.col("AVISIT"),
)
shape: (8, 2)
partsmerged
list[str]str
["Systolic", "BP"]"U01-Visit 1"
["Diastolic", "BP"]"U01-Visit 1"
["Systolic", "BP"]"U02-Visit 1"
["Diastolic", "BP"]"U02-Visit 1"
["Systolic", "BP"]"U03-Visit 2"
["Diastolic", "BP"]"U03-Visit 2"
["Systolic", "BP"]"U04-Visit 2"
["Diastolic", "BP"]"U04-Visit 2"

tidyverse

advs |> transmute(
  parts = str_split(PARAM, " "),
  merged = str_c(USUBJID, "-", AVISIT)
)
# A tibble: 8 × 2
  parts     merged     
  <list>    <chr>      
1 <chr [2]> U01-Visit 1
2 <chr [2]> U01-Visit 1
3 <chr [2]> U02-Visit 1
4 <chr [2]> U02-Visit 1
5 <chr [2]> U03-Visit 2
6 <chr [2]> U03-Visit 2
7 <chr [2]> U04-Visit 2
8 <chr [2]> U04-Visit 2

Slice and pad

Polars

advs.select(
    sliced=pl.col("PARAMCD").str.slice(0, 3),
    padded=pl.col("USUBJID").str.zfill(6),
)
shape: (8, 2)
slicedpadded
strstr
"SYS""000U01"
"DIA""000U01"
"SYS""000U02"
"DIA""000U02"
"SYS""000U03"
"DIA""000U03"
"SYS""000U04"
"DIA""000U04"

tidyverse

advs |> transmute(
  sliced = str_sub(PARAMCD, 1, 3),
  padded = str_pad(USUBJID, width = 6, side = "left", pad = "0")
)
# A tibble: 8 × 2
  sliced padded
  <chr>  <chr> 
1 SYS    000U01
2 DIA    000U01
3 SYS    000U02
4 DIA    000U02
5 SYS    000U03
6 DIA    000U03
7 SYS    000U04
8 DIA    000U04

Dates

Parse a date, datetime or time

Polars

advs.with_columns(
    date=pl.col("DTC").str.to_date(),
).select("USUBJID", "date")
shape: (8, 2)
USUBJIDdate
strdate
"U01"2024-01-15
"U01"2024-01-15
"U02"2024-02-20
"U02"2024-02-20
"U03"2024-03-10
"U03"2024-03-10
"U04"2024-04-05
"U04"2024-04-05

tidyverse

advs |> mutate(date = ymd(DTC)) |> select(USUBJID, date)
# A tibble: 8 × 2
  USUBJID date      
  <chr>   <date>    
1 U01     2024-01-15
2 U01     2024-01-15
3 U02     2024-02-20
4 U02     2024-02-20
5 U03     2024-03-10
6 U03     2024-03-10
7 U04     2024-04-05
8 U04     2024-04-05

Pull out parts

Polars

advs.with_columns(
    d=pl.col("DTC").str.to_date(),
).select(
    year=pl.col("d").dt.year(),
    month=pl.col("d").dt.month(),
    day=pl.col("d").dt.day(),
    wday=pl.col("d").dt.weekday(),
)
shape: (8, 4)
yearmonthdaywday
i32i8i8i8
20241151
20241151
20242202
20242202
20243107
20243107
2024455
2024455

tidyverse

advs |>
  mutate(d = ymd(DTC)) |>
  transmute(
    year = year(d),
    month = month(d),
    day = mday(d),
    wday = wday(d)
  )
# A tibble: 8 × 4
   year month   day  wday
  <dbl> <dbl> <int> <dbl>
1  2024     1    15     2
2  2024     1    15     2
3  2024     2    20     3
4  2024     2    20     3
5  2024     3    10     1
6  2024     3    10     1
7  2024     4     5     6
8  2024     4     5     6

Format a date as text

Polars

advs.with_columns(
    txt=pl.col("DTC").str.to_date().dt.strftime("%d %b %Y")
).select("DTC", "txt")
shape: (8, 2)
DTCtxt
strstr
"2024-01-15""15 Jan 2024"
"2024-01-15""15 Jan 2024"
"2024-02-20""20 Feb 2024"
"2024-02-20""20 Feb 2024"
"2024-03-10""10 Mar 2024"
"2024-03-10""10 Mar 2024"
"2024-04-05""05 Apr 2024"
"2024-04-05""05 Apr 2024"

tidyverse

advs |> mutate(txt = format(ymd(DTC), "%d %b %Y")) |> select(DTC, txt)
# A tibble: 8 × 2
  DTC        txt        
  <chr>      <chr>      
1 2024-01-15 15 Jan 2024
2 2024-01-15 15 Jan 2024
3 2024-02-20 20 Feb 2024
4 2024-02-20 20 Feb 2024
5 2024-03-10 10 Mar 2024
6 2024-03-10 10 Mar 2024
7 2024-04-05 05 Apr 2024
8 2024-04-05 05 Apr 2024

Truncate to a period

Polars

advs.with_columns(
    month_start=pl.col("DTC").str.to_date().dt.truncate("1mo"),
).select("DTC", "month_start")
shape: (8, 2)
DTCmonth_start
strdate
"2024-01-15"2024-01-01
"2024-01-15"2024-01-01
"2024-02-20"2024-02-01
"2024-02-20"2024-02-01
"2024-03-10"2024-03-01
"2024-03-10"2024-03-01
"2024-04-05"2024-04-01
"2024-04-05"2024-04-01

tidyverse

advs |> mutate(month_start = floor_date(ymd(DTC), "month")) |> select(DTC, month_start)
# A tibble: 8 × 2
  DTC        month_start
  <chr>      <date>     
1 2024-01-15 2024-01-01 
2 2024-01-15 2024-01-01 
3 2024-02-20 2024-02-01 
4 2024-02-20 2024-02-01 
5 2024-03-10 2024-03-01 
6 2024-03-10 2024-03-01 
7 2024-04-05 2024-04-01 
8 2024-04-05 2024-04-01 

Month start and end

Polars

(
    advs
    .with_columns(d=pl.col("DTC").str.to_date())
    .with_columns(
        ms=pl.col("d").dt.month_start(),
        me=pl.col("d").dt.month_end(),
    )
    .select("DTC", "ms", "me")
)
shape: (8, 3)
DTCmsme
strdatedate
"2024-01-15"2024-01-012024-01-31
"2024-01-15"2024-01-012024-01-31
"2024-02-20"2024-02-012024-02-29
"2024-02-20"2024-02-012024-02-29
"2024-03-10"2024-03-012024-03-31
"2024-03-10"2024-03-012024-03-31
"2024-04-05"2024-04-012024-04-30
"2024-04-05"2024-04-012024-04-30

tidyverse

advs |>
  mutate(
    d = ymd(DTC),
    ms = floor_date(d, "month"),
    me = ceiling_date(d, "month") - days(1)
  ) |>
  select(DTC, ms, me)
# A tibble: 8 × 3
  DTC        ms         me        
  <chr>      <date>     <date>    
1 2024-01-15 2024-01-01 2024-01-31
2 2024-01-15 2024-01-01 2024-01-31
3 2024-02-20 2024-02-01 2024-02-29
4 2024-02-20 2024-02-01 2024-02-29
5 2024-03-10 2024-03-01 2024-03-31
6 2024-03-10 2024-03-01 2024-03-31
7 2024-04-05 2024-04-01 2024-04-30
8 2024-04-05 2024-04-01 2024-04-30

Add or subtract a period

Polars

jan = pl.DataFrame({"d": ["2024-01-31"]}).with_columns(d=pl.col("d").str.to_date())
jan.with_columns(
    next_month=pl.col("d").dt.offset_by("1mo"),
    prev_month=pl.col("d").dt.offset_by("-1mo"),
)
shape: (1, 3)
dnext_monthprev_month
datedatedate
2024-01-312024-02-292023-12-31

tidyverse

jan <- tibble(d = ymd("2024-01-31"))
jan |>
  mutate(
    next_month = d %m+% months(1),
    prev_month = d %m-% months(1)
  )
# A tibble: 1 × 3
  d          next_month prev_month
  <date>     <date>     <date>    
1 2024-01-31 2024-02-29 2023-12-31

Difference between dates

Polars

jan = pl.DataFrame({"d1": ["2024-01-01"], "d2": ["2024-04-01"]}).with_columns(
    pl.col("d1", "d2").str.to_date()
)
jan.with_columns(days=pl.col("d2") - pl.col("d1"))
shape: (1, 3)
d1d2days
datedateduration[μs]
2024-01-012024-04-0191d

tidyverse

jan <- tibble(d1 = ymd("2024-01-01"), d2 = ymd("2024-04-01"))
jan |> mutate(days = d2 - d1)
# A tibble: 1 × 3
  d1         d2         days   
  <date>     <date>     <drtn> 
1 2024-01-01 2024-04-01 91 days

Generate a sequence of dates

Polars

pl.DataFrame({"d": pl.date_range(pl.date(2024, 1, 1), pl.date(2024, 1, 5), "1d", eager=True)})
shape: (5, 1)
d
date
2024-01-01
2024-01-02
2024-01-03
2024-01-04
2024-01-05

tidyverse

tibble(d = seq(ymd("2024-01-01"), ymd("2024-01-05"), by = "day"))
# A tibble: 5 × 1
  d         
  <date>    
1 2024-01-01
2 2024-01-02
3 2024-01-03
4 2024-01-04
5 2024-01-05

Input / output

Read and write CSV

Polars

# write
advs.write_csv("advs.csv")
# read
advs = pl.read_csv("advs.csv", try_parse_dates=True)

tidyverse

# write
advs |> write_csv("advs.csv")
# read
advs <- read_csv("advs.csv")

Read and write Parquet

Polars

advs.write_parquet("advs.parquet")
advs = pl.read_parquet("advs.parquet")

tidyverse

advs |> arrow::write_parquet("advs.parquet")
advs <- arrow::read_parquet("advs.parquet")

Sources