R is not a general-purpose language that happens to do statistics. It is a statistics language that happens to be Turing-complete. This distinction shapes everything about it: the syntax, the data structures, the standard library, and the way R programmers think.
Born in the early 1990s at the University of Auckland as a free implementation of the S language (Bell Labs, 1976), R was designed by statisticians for statisticians. Ross Ihaka and Robert Gentleman wanted a language where a linear regression was one line of code, where every operation naturally vectorized over data, and where plotting was a first-class activity rather than an afterthought.
Today R has over 20,000 packages on CRAN, dominates academic statistics and bioinformatics, and remains the language of choice for anyone whose primary job is data analysis rather than software engineering.
I. The REPL and First Steps
R is interactive by default. You type an expression, R evaluates it and prints the result. There is no main() function, no compilation step, no boilerplate.
The [1] prefix tells you this is the first element of the result vector. In R, there are no scalars. The number 5 is a numeric vector of length 1. This is the most important thing to understand about R: everything is a vector.
The <- arrow is R's preferred assignment operator. It reads naturally: "x gets 42." The = sign also works for assignment, but <- is the convention and what you'll see in virtually all R code. The keyboard shortcut in RStudio is Alt+- (hyphen).
II. Vectors: The Fundamental Unit
In Python, the fundamental unit is the object. In C, it's the byte. In R, it's the vector. You create vectors with the c() function (for "combine"):
That last line is key. The comparison heights > 6.0 didn't compare a single value—it compared every element of the vector at once, returning a logical vector of the same length. This is vectorization, and it is how R wants you to write code.
Vectorized Operations
Almost every operation in R is vectorized: arithmetic, comparisons, math functions, string functions. When you write x + y where both are vectors, R adds them element-by-element. When you write sqrt(x), R computes the square root of every element. You rarely need explicit loops.
The recycling rule is powerful but dangerous. If the longer vector's length isn't a multiple of the shorter one's, R gives a warning—but still produces a result. This is a common source of subtle bugs.
Indexing: 1-Based and Powerful
R uses 1-based indexing. The first element is x[1], not x[0].
Negative indexing in R means exclusion, not counting from the end. x[-2] returns everything except the second element. This is the opposite of Python's convention.
III. Types and Coercion
R has six atomic vector types, listed here from most to least restrictive:
| Type | Example | Notes |
|---|---|---|
| logical | TRUE, FALSE | Can abbreviate as T, F (but don't—they can be overwritten) |
| integer | 1L, 42L | The L suffix forces integer storage |
| double | 3.14, 1e-5 | Default numeric type. numeric = double. |
| complex | 3+2i | Built-in complex number support |
| character | "hello" | Strings. No separate char type. |
| raw | as.raw(0xff) | Raw bytes. Rarely used directly. |
When you mix types in a vector, R silently coerces to the most general type. This hierarchy is: logical → integer → double → complex → character.
The fact that sum(x > 5) counts how many elements exceed 5 is not a trick—it's a direct consequence of logical-to-numeric coercion. This pattern appears everywhere in R.
Special Values
NA is one of R's most important features. It represents missing data—an everyday reality in statistics. Rather than crashing or silently producing wrong results, R forces you to decide how to handle missingness. Almost every statistical function has an na.rm parameter.
IV. Matrices and Arrays
Matrices are 2D vectors. They store a single type and support vectorized linear algebra.
Note the %*% operator for matrix multiplication. The plain * is always element-wise in R. R's linear algebra is backed by LAPACK and BLAS, so matrix operations are genuinely fast.
V. Lists: R's Heterogeneous Container
Vectors require all elements to be the same type. Lists can hold anything: different types, different lengths, even other lists.
The [ ] vs [[ ]] Distinction
Think of a list as a train with named cars. train[1] gives you a smaller train with just the first car (still a train/list). train[[1]] gives you the contents of the first car (the actual element). The $ operator is shorthand for [["name"]].
Lists are the building block of most complex R objects. Data frames are lists of equal-length vectors. Model outputs (lm(), t.test()) are lists. Almost any function that returns something structured returns a list.
VI. Data Frames: R's Killer Feature
The data frame is R's central data structure. It is a list of equal-length vectors—in other words, a table where each column can have a different type but all columns have the same number of rows.
Data frames predate Python's pandas by nearly two decades. When Wes McKinney created pandas in 2008, he was explicitly modeling it on R's data frame. The concept of a named, typed, rectangular data structure with row/column indexing is R's most influential contribution to programming.
Reading Data
VII. Factors
Factors represent categorical data—variables with a fixed set of possible values. They are R's way of encoding what statisticians call "categorical" or "nominal" variables.
Factors confused a generation of R users because read.csv() used to convert all strings to factors by default. This was changed in R 4.0 (2020), but you'll still encounter factor-related confusion in older code and tutorials.
VIII. Control Flow
Loops vs. Vectorization
In most languages, loops are the natural way to process collections. In R, loops are a code smell. If you find yourself writing for (i in 1:length(x)), there is almost certainly a vectorized alternative that is both faster and clearer. Use sum(), mean(), cumsum(), diff(), ifelse(), or the apply family instead. R's vectorized operations are written in C internally and can be 10-100x faster than equivalent loops.
IX. Functions
Functions are first-class objects in R. You create them with the function keyword and assign them to names like any other value.
R uses lexical scoping. A function can access variables from its enclosing environment (closure). Functions are commonly passed to other functions—this is the core of the apply family.
X. The Apply Family
The apply family replaces most loops. Each variant applies a function to elements of a data structure:
| Function | Input | Output | Use Case |
|---|---|---|---|
apply() | matrix/array | vector/matrix | Apply to rows or columns |
sapply() | vector/list | vector/matrix | Simplified output |
lapply() | vector/list | list | Always returns a list |
tapply() | vector + factor | array | Group by and summarize |
mapply() | multiple vectors | vector/matrix | Multivariate apply |
The apply family is powerful but the interface is inconsistent. This is why the tidyverse's purrr package (with map(), map_dbl(), etc.) exists—it provides a cleaner, more predictable alternative.
XI. Built-in Statistics
R is the only major language where a t-test is one function call. Statistical functions are not in a library you import—they're built into the language.
That linear regression output tells you everything a statistician needs: coefficients, standard errors, t-values, p-values, significance codes, and R-squared. In Python, this requires importing statsmodels and writing substantially more code.
The Formula Interface
The ~ (tilde) is R's formula syntax. It means "modeled as" or "predicted by." This interface is used throughout R's statistical functions:
The formula interface is elegant and powerful. mpg ~ wt + hp reads as "miles per gallon, as a function of weight and horsepower." No other language has anything like it.
XII. Plotting: Base R Graphics
R's base plotting is immediate and interactive. You call plot() and a window appears with your visualization. No imports, no setup.
Base R plotting has an imperative API: you create a plot, then add to it with points(), lines(), abline(), text(), legend(). It's not pretty by default, but it's fast to iterate on. For publication-quality graphics, most R users switch to ggplot2.
XIII. The Tidyverse
The tidyverse is a collection of packages by Hadley Wickham and collaborators that reimagines how R code should look. It is not part of base R, but it is so widely used that many people learn it first and base R second.
The pipe operator %>% is the tidyverse's signature. It takes the result of the left side and passes it as the first argument to the right side. This transforms deeply nested function calls into readable, linear pipelines.
dplyr: Data Manipulation
ggplot2: Grammar of Graphics
ggplot2 is arguably the most influential data visualization library ever written. It implements Leland Wilkinson's "Grammar of Graphics"—the idea that any statistical graphic can be described as a combination of data, aesthetic mappings, and geometric objects.
The + operator in ggplot2 layers components together. aes() maps data columns to visual properties. geom_*() functions add visual elements. This declarative approach means you describe what you want, not how to draw it.
tidyr: Reshaping Data
XIV. String Manipulation
Base R's string functions are functional but quirky—paste() for concatenation, nchar() instead of length(), gsub() for replacement. The stringr package (part of the tidyverse) provides a more consistent interface where every function starts with str_.
XV. Environments and Scoping
R's scoping rules are sometimes surprising. Every function creates a new environment. R searches for variables from the innermost scope outward:
The <<- superassignment operator searches parent environments until it finds the variable, then modifies it in place. Use it sparingly—it creates hidden side effects that make code harder to reason about.
XVI. Packages and CRAN
CRAN (Comprehensive R Archive Network) is R's package repository. It has over 20,000 packages, each of which has been checked for basic quality and cross-platform compatibility.
R vs. Python for Data Science
The R vs. Python debate is essentially: do you want a language designed for statistics that can also program, or a language designed for programming that can also do statistics? R has superior statistical modeling, formula syntax, built-in datasets, and visualization (ggplot2). Python has a vastly larger ecosystem, better software engineering tools, and dominates deep learning. In practice, many data scientists use both.
XVII. R Markdown and Reproducible Research
R Markdown (.Rmd files) lets you weave together prose, code, and output into a single document. This is R's solution to the reproducibility crisis in science: instead of copying results into a Word document, you write a document that generates its own results.
Quarto (.qmd) is the next-generation successor to R Markdown, supporting R, Python, Julia, and Observable JS in the same document. It was released in 2022 and is gradually replacing R Markdown.
XVIII. S3: R's Object System
R has multiple object systems (S3, S4, R5/Reference Classes, R6). S3 is the simplest and most widely used. It's informal, duck-typed, and built on lists with a class attribute.
S3 dispatch works by naming convention: when you call print(s) and s has class "student", R looks for print.student(). This is how summary() produces completely different output for data frames, linear models, and t-tests—each has its own summary.class method.
XIX. Error Handling
R distinguishes between errors (execution stops), warnings (execution continues but something is suspicious), and messages (informational). In interactive use, warnings are often deferred and printed after the result. In scripts, you should check for them explicitly.
XX. Putting It All Together
Here's a complete analysis that demonstrates how R's pieces fit together—reading data, cleaning, modeling, and visualizing:
In 30 lines of code: data loading, grouped descriptive statistics, a correlation matrix, a multivariate regression with R-squared, and a prediction with confidence interval. This is what R was built for.
XXI. What R Trades Away
R's statistical focus comes with real costs:
| Tradeoff | Consequence |
|---|---|
| Single-threaded by default | Computation-heavy tasks need explicit parallelization (parallel, future packages) |
| Everything in memory | Can't natively handle datasets larger than RAM (unlike Python + Dask or SQL) |
| Inconsistent base API | nchar() for string length, length() for vector length, nrow() for rows—no consistency |
| Copy-on-modify semantics | Modifying a large data frame can trigger a full copy (though R is smart about this internally) |
| 1-based indexing | Off-by-one confusion for anyone coming from C/Python/JavaScript |
| Weak software engineering tools | No static types, minimal IDE refactoring, package management is improving but still behind pip/npm |
| Slow loops | R loops are 10-100x slower than vectorized operations; you must learn the vectorized idioms |
These tradeoffs are reasonable if your primary job is data analysis, statistical modeling, or scientific reporting. They become painful if you're building a web application, a CLI tool, or a production data pipeline. R knows what it is, and the things it sacrifices are the things a statistician can afford to lose.
Summary
R is a language that reflects its users. Statisticians wanted a language where they could explore data interactively, fit models in one line, and produce publication-quality graphics without writing boilerplate. They got exactly that, at the cost of some of the niceties that general-purpose programmers take for granted.
The key ideas to remember:
- Everything is a vector. The number
5is a vector of length 1. This shapes every operation. - Vectorize, don't loop. If you're writing a
forloop, there's almost always a better way. - Data frames are central. They are R's answer to the spreadsheet: typed columns, named rows, and a rich API for filtering, grouping, and summarizing.
- The formula interface is unique.
mpg ~ wt + hpis more expressive than any other language's equivalent. - NA is a feature, not a bug. Missing data is a fact of life in statistics. R makes you deal with it explicitly.
- The tidyverse is a dialect. Modern R code uses pipes (
%>%), dplyr verbs, and ggplot2. Learn both base R and the tidyverse. - CRAN is deep. Whatever statistical method you need, someone has written an R package for it.