SPEA V706 — Statistics for Research in Public Affairs
dt[i, j, by]:=.SD, and keysdata.tableConcise. Operations that take several steps elsewhere fit in one expression.
Fast and Memory Efficient. Written in C and optimized for large data sets.
Almost no dependencies. It relies on base R and nothing else, so it installs cleanly and keeps working across versions. Given W3, this is the part we like most.
data.tablepacman::p_load(data.table, here)
# read a file from disk — fast, and guesses types well
dt <- fread(here("raw_data", "wages.csv"))
# build one from scratch
dt <- data.table(id = 1:3, wage = c(18, 31, 44))
# convert an existing data.frame
data(wage1, package = "wooldridge")
setDT(wage1) # convert in place, no copyfread() returns a data.table directlysetDT(df) converts in place, without making a copy. More on that nextEvery data.table query has three slots:
i — which rows do you want?j — what to doby — stratify into groups first, then do j within eachRead it as English: “take rows i, do j, grouped by by.”
``r dt = data.table(id = 1:5, wage = c(18, 31, 44, 22, 27), educ = c(12, 16, 14, 12, 18), female dt
This doesn't modify anything: it just prints out the first few rows of the table.
## Variations on the basic theme
Print out a few subsets of the data table, or a collapsed summary of the data table.
```r
dt[wage > 20] # i argument only means, "limit the sample to rows where wage > 20"
dt[, mean(wage)] # empty i means us all rows, j means take the average of the wage over all rows.
dt[, .(avg = mean(wage)), by = female] # collapse by sex, take the avrage wage.
dt[educ >= 16, .(avg = mean(wage)), by = female] # all three
dt$dt itself. They return a new table or a single value, and leave dt alone# limit the sample to rows where wage > 20 and assign the result back to dt. This "deletes" rows where wage <= 20.
dt = dt[wage > 20]
# limit the sample to rows where educ >= 16
# collapse by sex, take the average wage
# assign the result back to dt.
# This "deletes" rows where educ < 16 and overwrites the original dt with the new collapsed summary.
dt =dt[educ >= 16, .(avg = mean(wage)), by = female] dt[i, ]order() in the i slot sorts the result, and leaves dt itself alonedata.table has a by-reference version:set prefix. That is the tell that something changed in placedt[, j]j Does More Than One JobThe j slot is where you say what you want back.
.() is shorthand for list(), and it bundles the results into columns.(), a bare expression returns a raw value.N is the number of rows in the current group...cols means “look for cols outside the table, not for a column named cols”../ in a file path, one level upBase R usually works on copy-on-modify principles.
data.table works on modify-by-reference principles, where possible.
setnames() changes the table itself; the set prefix again.() changes only what comes back, which is often what you want:=:= adds, changes, or deletes a column in place.
dt directly[] on the end when you want to see what you just didNULL removes the column from the table, in placeGive := an i, and it changes only those rows.
i, then assign in j. Nothing else in the table is toucheddt[wage > 100, wage := NA]Play by Play 1. Create an object: the number 25 2. Create a name and point it at the object: my_number <- 25 3. Create a second name for the same object: my_second_number <- my_number 4. Change the object through one of the names: my_number <- 30
When you do step 4, R creates a new object 30, and points the name “my_number” at it.
The name “my_second_number” still points at the original object 25.
This is called copy-on-modify because it makes a new copy of the object only when you actually modify it.
How can you tell that this is how it works?
Look at the memory addresses of the two names before and after the change.
pryr::address(my_number) # address of the object my_number points to
pryr::address(my_second_number) # address of the object my_second_number points to
my_number <- 30 # change the object my_number points to
pryr::address(my_number) # address of the object my_number points to
pryr::address(my_second_number) # address of the object my_second_number points todt <- data.table(x = 1:5, y = 6:10)
pryr::address(dt) # address of the object df points to
dt_copy <- dt # another name for the same object
pryr::address(dt_copy) # address of the object df_copy points to
dt$x <- dt$x * 10 # change the object df points to
pryr::address(dt) # address of the object df points to
pryr::address(dt_copy) # address of the object df_copy points toThe $ operator is copy-on-modify, so it creates a new copy of the data.table when you modify it.
:=:= modifies in place, so a second name for a table is not a second table.
dt <- data.table(x = 1:5, y = 6:10)
pryr::address(dt) # address of the object df points to
dt_copy <- dt # another name for the same object
pryr::address(dt_copy) # address of the object df_copy points to
dt[, x := x * 10] # change the object df points to
pryr::address(dt) # address of the object df points to
pryr::address(dt_copy) # address of the object df_copy points to copy() is explicit, and it is the only way to get a genuinely independent tabledata.table will not do it behind your backAnswer a multi-step question by hanging brackets off each other.
[...] takes the result of the one before itdt[, j, by]byAdd by, and j runs separately within each group.
.(), or you get a column called V1.(), the same way you bundle columns.() and the result has a readable column instead of educ >= 16Combine by with := to write a group-level number onto every row.
.SDYou want the mean of six columns.
.SD is the Subset of the Data: the current group’s rows, as a table.SDcols limits it to the columns you namelapply(.SD, mean) runs mean on each of those columns in turn.SD, Continued.SDcols accepts a function like is.numeric, or a range like wage:exper(cols) := say “use the values in cols as the column names”keybykeyby works exactly like by, and also sorts the result and sets a keyA key is an ordering of the rows that makes subsetting extremely fast.
setkey() sorts the table in place, so there is the set prefix once morex = and y = so the direction of the join is unambiguousby = is the key column both tables shareall.x = TRUE keeps every row of x, matched or not. That is a left join| What you want | Arguments |
|---|---|
| Keep only matched rows (inner) | merge(x, y, by = "id") |
Keep everything in x (left) |
all.x = TRUE |
Keep everything in y (right) |
all.y = TRUE |
| Keep everything (full) | all = TRUE |
| Direction | Function |
|---|---|
| wide → long | melt() |
| long → wide | dcast() |
melt: Wide to Longid.vars names the columns that stay. Everything else gets stackedvariable and valuedcast: Long to Wideid ~ year means one row per id, one column per yearvalue.var says which column fills the cellsdcast can aggregate as it goes: dcast(long, id ~ year, fun.aggregate = mean)pacman::p_load(data.table, here)
dt <- fread(here("raw_data", "wages.csv"))
dt[, lwage := log(wage)]
dt <- dt[!is.na(lwage) & educ > 0]
panel <- merge(x = dt, y = states, by = "state", all.x = TRUE)
stopifnot(nrow(panel) == nrow(dt))
panel[, .(avg_lwage = mean(lwage), n = .N),
keyby = .(state, female)]dt[i, j, by] — rows, then compute, then within groups:= and set assigns by referenceby groups, .SD handles many columns, setkey() makes the big jobs fastmerge(x =, y =, by =, all.x =) joins, and you check nrow() afterwardsmelt() goes wide to long, dcast() goes back