├── .vscode └── settings.json ├── .github ├── CONTRIBUTING.md └── CODE-OF-CONDUCT.md └── README.md /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "markdown.extension.toc.levels": "2..6", 3 | "markdown.extension.toc.omittedFromToc": { 4 | "README.md": [ 5 | "## Contents" 6 | ] 7 | }, 8 | } -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to r-base-shortcuts 2 | 3 | :+1::tada: First off, thanks for taking your time to contribute! :tada::+1: 4 | 5 | You could contribute to this project by: 6 | 7 | 1. Filing a report or request in an issue. 8 | 2. Suggesting a change via a pull request. 9 | 10 | ## Pull requests 11 | 12 | To suggest a change via pull requests, please: 13 | 14 | 1. Fork the repository into your GitHub account. 15 | 2. Clone the forked repository to local machine, make the changes. 16 | 3. Commit and push the changes to GitHub. Create a pull request. 17 | 18 | ## Table of contents 19 | 20 | To contribute new shortcuts or edit the existing headings, install the 21 | VSCode extension [Markdown All in One](https://marketplace.visualstudio.com/items?itemName=yzhang.markdown-all-in-one) first. 22 | It helps update the table of contents automatically. 23 | 24 | ## R code style 25 | 26 | All R code should be formatted using [styler](https://styler.r-lib.org/). 27 | -------------------------------------------------------------------------------- /.github/CODE-OF-CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who 4 | contribute through reporting issues, posting feature requests, updating documentation, 5 | submitting pull requests or patches, and other activities. 6 | 7 | We are committed to making participation in this project a harassment-free experience for 8 | everyone, regardless of level of experience, gender, gender identity and expression, 9 | sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. 10 | 11 | Examples of unacceptable behavior by participants include the use of sexual language or 12 | imagery, derogatory comments or personal attacks, trolling, public or private harassment, 13 | insults, or other unprofessional conduct. 14 | 15 | Project maintainers have the right and responsibility to remove, edit, or reject comments, 16 | commits, code, wiki edits, issues, and other contributions that are not aligned to this 17 | Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed 18 | from the project team. 19 | 20 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by 21 | opening an issue or contacting one or more of the project maintainers. 22 | 23 | This Code of Conduct is adapted from the Contributor Covenant 24 | (https://contributor-covenant.org), version 1.0.0, available at 25 | https://contributor-covenant.org/version/1/0/0/. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # r-base-shortcuts 2 | 3 | A collection of lesser-known but powerful base R idioms and shortcuts 4 | for writing concise and fast base R code, useful for beginner level to 5 | intermediate level R developers. 6 | 7 | Please help me improve and extend this list. 8 | See [contributing guide](.github/CONTRIBUTING.md) 9 | and [code of conduct](.github/CODE-OF-CONDUCT.md). 10 | 11 | > Why? 12 | > 13 | > From 2012 to 2022, I answered thousands of R questions in the 14 | > online community [Capital of Statistics](https://d.cosx.org/). 15 | > These recipes are observed and digested from the recurring patterns 16 | > I learned from the frequently asked questions with less common answers. 17 | 18 | ## Contents 19 | 20 | - [Object creation](#object-creation) 21 | - [Create sequences with `seq_len()` and `seq_along()`](#create-sequences-with-seq_len-and-seq_along) 22 | - [Create an empty list of a given length](#create-an-empty-list-of-a-given-length) 23 | - [Create and assigning S3 classes in one step](#create-and-assigning-s3-classes-in-one-step) 24 | - [Assign names to vector elements or data frame columns at creation](#assign-names-to-vector-elements-or-data-frame-columns-at-creation) 25 | - [Use `I()` to include objects as is in data frames](#use-i-to-include-objects-as-is-in-data-frames) 26 | - [Generate factors using `gl()`](#generate-factors-using-gl) 27 | - [Object transformation](#object-transformation) 28 | - [Use `[` and `[[` as functions in apply calls](#use--and--as-functions-in-apply-calls) 29 | - [Sum all components in a list](#sum-all-components-in-a-list) 30 | - [Bind multiple data frames in a list](#bind-multiple-data-frames-in-a-list) 31 | - [Use `modifyList()` to update a list](#use-modifylist-to-update-a-list) 32 | - [Run-length encoding](#run-length-encoding) 33 | - [Conditions](#conditions) 34 | - [Use `inherits()` for class checking](#use-inherits-for-class-checking) 35 | - [Replace multiple `ifelse()` with `cut()`](#replace-multiple-ifelse-with-cut) 36 | - [Simplify recoding categorical values with `factor()`](#simplify-recoding-categorical-values-with-factor) 37 | - [Save the number of `if` conditions with upcasting](#save-the-number-of-if-conditions-with-upcasting) 38 | - [Use `findInterval()` for many breakpoints](#use-findinterval-for-many-breakpoints) 39 | - [Vectorization](#vectorization) 40 | - [Use `match()` for fast lookups](#use-match-for-fast-lookups) 41 | - [Use `mapply()` for element-wise operations on multiple lists](#use-mapply-for-element-wise-operations-on-multiple-lists) 42 | - [Simplify element-wise min and max operations with `pmin()` and `pmax()`](#simplify-element-wise-min-and-max-operations-with-pmin-and-pmax) 43 | - [Apply a function to all combinations of parameters](#apply-a-function-to-all-combinations-of-parameters) 44 | - [Generate all possible combinations of given characters](#generate-all-possible-combinations-of-given-characters) 45 | - [Vectorize a function with `Vectorize()`](#vectorize-a-function-with-vectorize) 46 | - [Pairwise computations using `outer()`](#pairwise-computations-using-outer) 47 | - [Functions](#functions) 48 | - [Specify formal argument lists with `alist()`](#specify-formal-argument-lists-with-alist) 49 | - [Use internal functions without `:::`](#use-internal-functions-without-) 50 | - [Side-effects](#side-effects) 51 | - [Return invisibly with `invisible()` for side-effect functions](#return-invisibly-with-invisible-for-side-effect-functions) 52 | - [Use `on.exit()` for cleanup](#use-onexit-for-cleanup) 53 | - [Numerical computations](#numerical-computations) 54 | - [Create step functions with `stepfun()`](#create-step-functions-with-stepfun) 55 | - [Further reading](#further-reading) 56 | 57 | ## Object creation 58 | 59 | ### Create sequences with `seq_len()` and `seq_along()` 60 | 61 | `seq_len()` and `seq_along()` are safer than `1:length(x)` or `1:nrow(x)` 62 | because they avoid the unexpected result when `x` is of length `0`: 63 | 64 | ```r 65 | # Safe version of 1:length(x) 66 | seq_len(length(x)) 67 | # Safe version of 1:length(x) 68 | seq_along(x) 69 | ``` 70 | 71 | ### Create an empty list of a given length 72 | 73 | Use the `vector()` function to create an empty list of a specific length: 74 | 75 | ```r 76 | x <- vector("list", length) 77 | ``` 78 | 79 | ### Create and assigning S3 classes in one step 80 | 81 | Avoid creating an object and assigning its class separately. 82 | Instead, use the `structure()` function to do both at once: 83 | 84 | ```r 85 | x <- structure(list(), class = "my_class") 86 | ``` 87 | 88 | Instead of: 89 | 90 | ```r 91 | x <- list() 92 | class(x) <- "my_class" 93 | ``` 94 | 95 | This makes the code more concise when returning an object of a specific class. 96 | 97 | ### Assign names to vector elements or data frame columns at creation 98 | 99 | The `setNames()` function allows you to assign names to vector elements or 100 | data frame columns during creation: 101 | 102 | ```r 103 | x <- setNames(1:3, c("one", "two", "three")) 104 | x <- setNames(data.frame(...), c("names", "of", "columns")) 105 | ``` 106 | 107 | ### Use `I()` to include objects as is in data frames 108 | 109 | The `I()` function allows you to include objects as is when creating data frames: 110 | 111 | ```r 112 | df <- data.frame(x = I(list(1:10, letters))) 113 | df$x 114 | #> [[1]] 115 | #> [1] 1 2 3 4 5 6 7 8 9 10 116 | #> 117 | #> [[2]] 118 | #> [1] "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l" "m" 119 | #> [14] "n" "o" "p" "q" "r" "s" "t" "u" "v" "w" "x" "y" "z" 120 | ``` 121 | 122 | This creates a data frame with one column `x` that is a list of vectors. 123 | 124 | ### Generate factors using `gl()` 125 | 126 | Create a vector with specific levels with `gl()` by specifying the levels 127 | and the number of repetitions: 128 | 129 | ```r 130 | gl(n = 2, k = 5, labels = c("Low", "High")) 131 | #> [1] Low Low Low Low Low High High High High High 132 | #> Levels: Low High 133 | ``` 134 | 135 | The `gl()` function is particularly useful when setting up experiments 136 | or simulations that involve categorical variables. 137 | 138 | ## Object transformation 139 | 140 | ### Use `[` and `[[` as functions in apply calls 141 | 142 | When you need to extract the same element from each item in a list or 143 | list-like object, you can leverage `[` and `[[` as functions 144 | (they actually are!) within `lapply()` and `sapply()` calls. 145 | 146 | Consider a list of named vectors: 147 | 148 | ```r 149 | lst <- list( 150 | item1 = c(a = 1, b = 2, c = 3), 151 | item2 = c(a = 4, b = 5, c = 6), 152 | item3 = c(a = 7, b = 8, c = 9) 153 | ) 154 | 155 | # Extract named element "a" using `[[` 156 | element_a <- sapply(lst, `[[`, "a") 157 | 158 | lst <- list( 159 | item1 = c(1, 2, 3), 160 | item2 = c(4, 5, 6), 161 | item3 = c(7, 8, 9) 162 | ) 163 | 164 | # Extract first element using `[` 165 | first_element <- sapply(lst, `[`, 1) 166 | ``` 167 | 168 | ### Sum all components in a list 169 | 170 | Use the `Reduce()` function with the infix function `+` to sum up all components 171 | in a list: 172 | 173 | ```r 174 | x <- Reduce("+", list) 175 | ``` 176 | 177 | ### Bind multiple data frames in a list 178 | 179 | The `do.call()` function with the `rbind` argument allows you to bind 180 | multiple data frames in a list into one data frame: 181 | 182 | ```r 183 | df_combined <- do.call("rbind", list_of_dfs) 184 | ``` 185 | 186 | Alternatively, more performant solutions for such operations are offered in 187 | `data.table::rbindlist()` and `dplyr::bind_rows()`. See 188 | [this article](https://rpubs.com/jimhester/rbind) for details. 189 | 190 | ### Use `modifyList()` to update a list 191 | 192 | The `modifyList()` function allows you to easily update values in a list 193 | without a verbose syntax: 194 | 195 | ```r 196 | old_list <- list(a = 1, b = 2, c = 3) 197 | new_vals <- list(a = 10, c = 30) 198 | new_list <- modifyList(defaults, new_vals) 199 | ``` 200 | 201 | This can be very useful for maintaining and updating a set of 202 | configuration parameters. 203 | 204 | ### Run-length encoding 205 | 206 | Run-length encoding is a simple form of data compression in which sequences 207 | of the same element are replaced by a single instance of the element followed 208 | by the number of times it appears in the sequence. 209 | 210 | Suppose you have a vector with many repeating elements: 211 | 212 | ```r 213 | x <- c(1, 1, 1, 2, 2, 3, 3, 3, 3, 2, 2, 2, 1, 1) 214 | ``` 215 | 216 | You can use `rle()` to compress this vector and decompress the result back 217 | into the original vector with `inverse.rle()`: 218 | 219 | ```r 220 | x <- c(1, 1, 1, 2, 2, 3, 3, 3, 3, 2, 2, 2, 1, 1) 221 | 222 | (y <- rle(x)) 223 | #> Run Length Encoding 224 | #> lengths: int [1:5] 3 2 4 3 2 225 | #> values : num [1:5] 1 2 3 2 1 226 | 227 | inverse.rle(y) 228 | #> [1] 1 1 1 2 2 3 3 3 3 2 2 2 1 1 229 | ``` 230 | 231 | ## Conditions 232 | 233 | ### Use `inherits()` for class checking 234 | 235 | Instead of using the `class()` function in conjunction with `==`, `!=`, 236 | or `%in%` operators to check if an object belongs to a certain class, 237 | use the `inherits()` function. 238 | 239 | ```r 240 | if (inherits(x, "class")) 241 | ``` 242 | 243 | This will return `TRUE` if "class" is one of the classes from which `x` inherits. 244 | This replaces the following more verbose forms: 245 | 246 | ```r 247 | if (class(x) == "class") 248 | ``` 249 | 250 | or 251 | 252 | ```r 253 | if (class(x) %in% c("class1", "class2")) 254 | ``` 255 | 256 | It is also more reliable because it checks for class inheritance, 257 | not just the first class name (R supports multiple classes for S3 and S4 objects). 258 | 259 | ### Replace multiple `ifelse()` with `cut()` 260 | 261 | For a series of range-based conditions, use `cut()` instead of chaining 262 | multiple `if-else` conditions or `ifelse()` calls: 263 | 264 | ```r 265 | categories <- cut( 266 | x, 267 | breaks = c(-Inf, 0, 10, Inf), 268 | labels = c("negative", "small", "large") 269 | ) 270 | ``` 271 | 272 | This assigns each element in `x` to the category that corresponds to the 273 | range it falls in. 274 | 275 | ### Simplify recoding categorical values with `factor()` 276 | 277 | When dealing with categorical variables, you might need to replace or 278 | recode certain levels. This can be achieved using chained `ifelse()` statements, 279 | but a more efficient and readable approach is to use the `factor()` function: 280 | 281 | ```r 282 | x <- c("M", "F", "F", NA) 283 | 284 | factor( 285 | x, 286 | levels = c("F", "M", NA), 287 | labels = c("Female", "Male", "Missing"), 288 | exclude = NULL # Include missing values in the levels 289 | ) 290 | ``` 291 | 292 | ### Save the number of `if` conditions with upcasting 293 | 294 | Sometimes, the number of conditions checked in multiple `if` statements 295 | can be reduced by cleverly using the fact that in R, 296 | `TRUE` is upcasted to `1` and `FALSE` to `0` in numeric contexts. 297 | This can be useful for selecting an index based on a set of conditions: 298 | 299 | ```r 300 | i <- (width >= 960) + (width >= 1140) + 1 301 | p <- p + facet_wrap(vars(class), ncol = c(1, 2, 4)[i]) 302 | ``` 303 | 304 | This does the same thing as the following code, but in a much more concise way: 305 | 306 | ```r 307 | if (width >= 1140) p <- p + facet_wrap(vars(class), ncol = 4) 308 | if (width >= 960 & width < 1140) p <- p + facet_wrap(vars(class), ncol = 2) 309 | if (width < 960) p <- p + facet_wrap(vars(class), ncol = 1) 310 | ``` 311 | 312 | This works because the condition checks in the parentheses result in a 313 | `TRUE` or `FALSE`, and when they are added together, they are 314 | upcasted to `1` or `0`. 315 | 316 | ### Use `findInterval()` for many breakpoints 317 | 318 | If you want to assign a variable to many different groups or intervals, 319 | instead of using a series of `if` statements, you can use the 320 | `findInterval()` function. Using the same example above: 321 | 322 | ```r 323 | breakpoints <- c(960, 1140) 324 | ncols <- c(1, 2, 4) 325 | i <- findInterval(width, breakpoints) + 1 326 | p <- p + facet_wrap(vars(class), ncol = ncols[i]) 327 | ``` 328 | 329 | The `findInterval()` function finds which interval each number in a 330 | given vector falls into and returns a vector of interval indices. 331 | It's a faster alternative when there are many breakpoints. 332 | 333 | ## Vectorization 334 | 335 | ### Use `match()` for fast lookups 336 | 337 | The `match()` function can be faster than `which()` for looking up 338 | values in a vector: 339 | 340 | ```r 341 | index <- match(value, my_vector) 342 | ``` 343 | 344 | This code sets `index` to the index of `value` in `my_vector`. 345 | 346 | ### Use `mapply()` for element-wise operations on multiple lists 347 | 348 | `mapply()` applies a function over a set of lists in an element-wise fashion: 349 | 350 | ```r 351 | mapply(sum, list1, list2, list3) 352 | ``` 353 | 354 | ### Simplify element-wise min and max operations with `pmin()` and `pmax()` 355 | 356 | When comparing two or more vectors on an element-wise basis and get the 357 | minimum or maximum of each set of elements, use `pmin()` and `pmax()`. 358 | 359 | ```r 360 | vec1 <- c(1, 5, 3, 9, 5) 361 | vec2 <- c(4, 2, 8, 1, 7) 362 | 363 | # Instead of using sapply() or a loop: 364 | sapply(1:length(vec1), function(i) min(vec1[i], vec2[i])) 365 | sapply(1:length(vec1), function(i) max(vec1[i], vec2[i])) 366 | 367 | # Use pmin() and pmax() for a more concise and efficient solution: 368 | pmin(vec1, vec2) 369 | pmax(vec1, vec2) 370 | ``` 371 | 372 | `pmin()` and `pmax()` perform these operations much more efficiently than 373 | alternatives such as applying `min()` and `max()` in a loop or using `sapply()`. 374 | This can lead to a noticeable performance improvement when working with large vectors. 375 | 376 | ### Apply a function to all combinations of parameters 377 | 378 | Sometimes we need to run a function on every combination of a set of 379 | parameter values, for example, in grid search. We can use the combination of 380 | `expand.grid()`, `mapply()`, and `do.call()` + `rbind()` to accomplish this. 381 | 382 | Suppose we have a simple function that takes two parameters, `a` and `b`: 383 | 384 | ```r 385 | f <- function(a, b) { 386 | result <- a * b 387 | data.frame(a = a, b = b, result = result) 388 | } 389 | ``` 390 | 391 | Create a grid of `a` and `b` parameter values to evaluate: 392 | 393 | ```r 394 | params <- expand.grid(a = 1:3, b = 4:6) 395 | ``` 396 | 397 | We use `mapply()` to apply `f` to each row of our parameter grid. 398 | We will use `SIMPLIFY = FALSE` to keep the results as a list of data frames: 399 | 400 | ```r 401 | lst <- mapply(f, a = params$a, b = params$b, SIMPLIFY = FALSE) 402 | ``` 403 | 404 | Finally, we bind all the result data frames together into one final data frame: 405 | 406 | ```r 407 | do.call(rbind, lst) 408 | ``` 409 | 410 | ### Generate all possible combinations of given characters 411 | 412 | To generate all possible combinations of a given set of characters, 413 | `expand.grid()` and `do.call()` with `paste0()` can help. 414 | The following snippet produces all possible three-digit character 415 | strings consisting of both letters (lowercase) and numbers: 416 | 417 | ```r 418 | x <- c(letters, 0:9) 419 | do.call(paste0, expand.grid(x, x, x)) 420 | ``` 421 | 422 | Here, `expand.grid()` generates a data frame where each row is a unique 423 | combination of three elements from `x`. Then, `do.call(paste0, ...)` 424 | concatenates each combination together into a string. 425 | 426 | ### Vectorize a function with `Vectorize()` 427 | 428 | If a function is not natively vectorized (it has arguments that only take 429 | one value at a time), you can use `Vectorize()` to create a new function 430 | that accepts vector inputs: 431 | 432 | ```r 433 | f <- function(x) x^2 434 | lower <- c(1, 2, 3) 435 | upper <- c(4, 5, 6) 436 | 437 | integrate_vec <- Vectorize(integrate, vectorize.args = c("lower", "upper")) 438 | 439 | result <- integrate_vec(f, lower, upper) 440 | unlist(result["value", ]) 441 | ``` 442 | 443 | The `Vectorize()` function works internally by leveraging the `mapply()` 444 | function, which applies a function over two or more vectors or lists. 445 | 446 | ### Pairwise computations using `outer()` 447 | 448 | The `outer()` function is useful for applying a function to every pair of 449 | elements from two vectors. This can be particularly useful for U-statistics 450 | and other situations requiring pairwise computations. 451 | 452 | Consider two vectors of numeric values for which we wish to compute a 453 | custom function for each pair: 454 | 455 | ```r 456 | x <- rnorm(5) 457 | y <- rnorm(5) 458 | 459 | outer(x, y, FUN = function(x, y) x + x^2 - y) 460 | ``` 461 | 462 | ## Functions 463 | 464 | ### Specify formal argument lists with `alist()` 465 | 466 | The `alist()` function can create lists where some elements are intentionally 467 | left blank (or are "missing"), which can be helpful when we want to specify 468 | formal arguments of a function, especially in conjunction with `formals()`. 469 | 470 | Consider this scenario. Suppose we are writing a function that wraps another 471 | function, and we want our wrapper function to have the same formal arguments 472 | as the original function, even if it does not use all of them. 473 | Here is how we can use `alist()` to achieve that: 474 | 475 | ```r 476 | original_function <- function(a, b, c = 3, d = "something") a + b 477 | 478 | wrapper_function <- function(...) { 479 | # Use the formals of the original function 480 | arguments <- match.call(expand.dots = FALSE)$... 481 | 482 | # Update the formals using `alist()` 483 | formals(wrapper_function) <- alist(a = , b = , c = 3, d = "something") 484 | 485 | # Call the original function 486 | do.call(original_function, arguments) 487 | } 488 | ``` 489 | 490 | Now, `wrapper_function()` has the same formal arguments as 491 | `original_function()`, and any arguments passed to `wrapper_function()` 492 | are forwarded to `original_function()`. This way, even if `wrapper_function()` 493 | does not use all the arguments, it can still accept them, and code that uses 494 | `wrapper_function()` can be more consistent with code that uses 495 | `original_function()`. 496 | 497 | The `alist()` function is used here to create a list of formals where 498 | some elements are missing, which represents the fact that some arguments 499 | are required and have no default values. This would not be possible 500 | with `list()`, which cannot create lists with missing elements. 501 | 502 | ### Use internal functions without `:::` 503 | 504 | To use internal functions from packages without using `:::`, you can use 505 | 506 | ```r 507 | f <- utils::getFromNamespace("f", ns = "package") 508 | f(...) 509 | ``` 510 | 511 | ## Side-effects 512 | 513 | ### Return invisibly with `invisible()` for side-effect functions 514 | 515 | R functions always return a value. However, some functions are primarily 516 | designed for their side effects. To suppress the automatic printing 517 | of the returned value, use `invisible()`. 518 | 519 | ```r 520 | f <- function(x) { 521 | print(x^2) 522 | invisible(x) 523 | } 524 | ``` 525 | 526 | The value of `x` can be used later when the result is assigned to a variable 527 | or piped into the next function. 528 | 529 | ### Use `on.exit()` for cleanup 530 | 531 | `on.exit()` is a useful function for cleaning up side effects, such as 532 | deleting temporary files or closing opened connections, even if a function 533 | exits early due to an error: 534 | 535 | ```r 536 | f <- function() { 537 | temp_file <- tempfile() 538 | on.exit(unlink(temp_file)) 539 | 540 | # Do stuff with temp_file 541 | } 542 | 543 | f <- function(file) { 544 | con <- file(file, "r") 545 | on.exit(close(con)) 546 | readLines(con) 547 | } 548 | ``` 549 | 550 | This function creates a temporary file and then ensures it gets deleted 551 | when the function exits, regardless of why it exits. Note that the arguments 552 | `add` and `after` in `on.exit()` are important for controlling the overwriting 553 | and ordering behavior of the expressions. 554 | 555 | ## Numerical computations 556 | 557 | ### Create step functions with `stepfun()` 558 | 559 | The `stepfun()` function is an effective tool for creating step functions, 560 | which can be particularly handy in survival analysis. 561 | For instance, say we have two survival curves generated from Kaplan-Meier 562 | estimators, and we want to determine the difference in survival probabilities 563 | at a given time. 564 | 565 | Create the survival curves using `survfit()`: 566 | 567 | ```r 568 | library("survival") 569 | 570 | fit_km <- survfit(Surv(stop, event == "pcm") ~ 1, data = mgus1, subset = (start == 0)) 571 | fit_cr <- survfit(Surv(stop, event == "death") ~ 1, data = mgus1, subset = (start == 0)) 572 | ``` 573 | 574 | Convert these survival curves into step functions: 575 | 576 | ```r 577 | step_km <- stepfun(fit_km$time, c(1, fit_km$surv)) 578 | step_cr <- stepfun(fit_cr$time, c(1, fit_cr$surv)) 579 | ``` 580 | 581 | With these step functions, it becomes straightforward to compute the 582 | difference in survival probabilities at specific times: 583 | 584 | ```r 585 | t <- 1:3 * 1000 586 | step_km(t) - step_cr(t) 587 | ``` 588 | 589 | ## Further reading 590 | 591 | - [Data Manipulation with R](https://doi.org/10.1007/978-0-387-74731-6) 592 | - [The R Inferno](https://www.burns-stat.com/documents/books/the-r-inferno/) 593 | - [stackoverflow: Stack Overflow's Greatest Hits](https://cran.r-project.org/package=stackoverflow) 594 | --------------------------------------------------------------------------------