Inspecting Data Structure
In Lesson 2 you built the trail-club roster yourself, so you knew its shape by heart. But most of the time an object just lands in your lap: you read a CSV, or you call a function and it hands something back, and you have no idea what you are holding. Is it a vector or a table? How big is it? What is inside?
This lesson is the inspector's toolkit: six small functions that answer the six questions you ask of any R object, no matter how strange. Type one of them and the object tells you exactly what it is.
By the end of this lesson you will be able to:
- Read any object's shape at a glance with
str() - Tell
class()(the kind R treats it as) fromtypeof()(the storage underneath) - Measure size correctly with
length(),dim(),nrow()andncol(), and list its parts withnames()andattributes()
Prerequisites: you can run a line of R and assign with <-, and you know that a vector holds one type, a list holds mixed named slots, and a data frame is a list of equal-length columns. The club roster below is the object you are about to x-ray.
str() shows you the whole shape in one glance
When an unfamiliar object lands in front of you, the first thing to type is str(), short for structure. It prints a compact map: the kind of object, how big it is, and the type and first few values of every part, all in a handful of lines, without flooding your screen.
Let us build this club roster for real and look at it. Each lesson runs in a fresh R session, so we make the data right here (run it once, then the later steps can inspect it). Notice we slip in one new column, level, an ordered factor of skill grades, so you can see how str() reports a trickier type too.
Read that top line as a headline: it is a data.frame with 4 obs. (4 observations, the rows) of 5 variables (the columns). Then one line per column: name is chr (character), age and visits are int (integer), level is an ordered factor with 3 levels, and member is logi (logical). In six lines you know the whole object.
The real point of str() is that it does not care what you give it. The same call maps a bare vector, a list, anything:
str() is the single most useful thing to type when you meet an object you do not recognise. Reach for it before anything else; the rest of this lesson zooms in on the individual questions it answers.Read the headline
You run str(club) and the top line reads 'data.frame': 4 obs. of 5 variables. Without running anything else, how many rows and columns does club have?
str() reports rows as observations and columns as variables, so 4 obs. of 5 variables means 4 rows and 5 columns. You read the whole shape off one line.str() already tells you: the headline "4 obs. of 5 variables" is exactly the row and column count. The dedicated functions just return those same numbers on their own.class() is the kind; typeof() is the storage
There are two ways to ask "what type is this?", and they answer different questions. class() tells you the kind of thing R treats the object as, the label that decides how functions behave toward it. typeof() tells you the raw storage type underneath, what the object is really made of.
For most simple vectors they agree, but for the interesting objects they split apart, and that gap explains a lot of R:
A data frame is a list underneath (exactly as you saw in Lesson 2), but it carries the class "data.frame" so that printing, [ , ] indexing and nrow() all behave table-like. The level factor is the same story, even more striking:
A factor looks like words on screen, but it is stored as integer codes (2 1 3 3) plus a list of labels. Its class is what makes it print and behave as categories. So class() answers "how does R treat this?" and typeof() answers "what is it built from?"
class() can even return several values (here "ordered" "factor"): R tries each class in turn to find a matching behaviour. typeof() always returns exactly one base storage type. When code behaves oddly, check class(); when you care how data is actually held, check typeof().Same answer, or different?
class(club) returns "data.frame". What does typeof(club) return, and why?
class is "data.frame" (how R treats it), but the storage is a list of columns, so typeof(club) is "list". That split is why $ reaches a column just like a list slot.typeof(club) is "list", whatever the columns happen to hold.length() for one dimension, dim() for two
Size has two flavours, and picking the wrong tool is a classic beginner trap. A one-dimensional thing (a vector) has a length(), the count of its elements. A two-dimensional thing (a data frame or matrix) has a dim(), its rows and columns.
The catch: a data frame is a list of columns, so length() of a data frame counts its columns, not its rows. That surprises everyone once.
For a table you almost always want dim(), or the two named shortcuts nrow() and ncol(), which are never ambiguous:
And the flip side: a plain vector has no second dimension, so asking for its dim() gives you NULL, R's way of saying "there is nothing here". That NULL is a useful signal that you are holding a 1-D object, not a table.
length() of a data frame is its column count, not its row count. To count rows (the usual question, "how many records do I have?"), use nrow(). Reaching for length() on a table is one of the most common quiet mistakes in beginner R.Count the people, not the columns
club has 5 columns and 4 people, so length(club) returns the misleading 5. You want the number of rows, one per club member. Replace the blank with the function that counts rows.
Show answer
nrow(club)
#> [1] 4names() lists the parts; attributes() reveals everything
Two questions remain: what are the parts called, and what extra information is R quietly stashing on the object? names() answers the first, returning the labels, the column names of a data frame or the slot names of a list:
attributes() answers the second, and it is the deepest look of all. Every R object is really its data plus a set of attributes, little tags of metadata hanging off it. attributes() shows them all at once:
Look closely: the column names you got from names(), and the class you got from class(), are both just attributes. So is a data frame's row.names, and so are a factor's levels:
class(), dim(), names() and a factor's levels are not separate magic, they are all attributes, and attributes() is the one call that lays them all bare. str() is really just a friendly summary of the data and these attributes together.Watch the shape change as you edit the columns
Here is the roster again, but now you can take it apart. Toggle a column off and the object is genuinely different: a new dim, a shorter names, one fewer type in the list, the same equal-length rows. That is exactly what your toolkit reports. Toggle the buttons, watch the dimension and the per-column types update, then press Run to inspect that exact data frame with str().
Drop a column and you lose a variable (the names shrink, ncol falls by one); every remaining column still has the same number of rows. The shape you read with dim(), names() and str() is just this picture in words.
X-ray an object you did not build
Now the payoff: the toolkit reads anything, even an object you never made by hand. Fit a simple model and inspect it. First, run this to see what a fitted model actually is:
Show answer
names(fit)
#> [1] "coefficients" "residuals" "effects" "rank"
#> [5] "fitted.values" "assign" "qr" "df.residual"
#> [9] "xlevels" "call" "terms" "model"
fit$coefficients
#> (Intercept) wt
#> 37.285126 -5.344472References
A few trustworthy places to take this further, all free:
- The R Language Definition: Objects - the definitive account of R's types, modes and attributes, straight from the R project.
- An Introduction to R: Objects, their modes and attributes - the canonical first treatment of
class,typeofand attributes. - Advanced R (2e): Vectors - how attributes, the S3
class, and the data-frame-as-list view all fit together. - R for Data Science (2e): Base R - inspecting and reaching into objects in everyday analysis.
Lesson 3 complete
You now have the inspector's toolkit for any R object. str() gives the one-glance map; class() and typeof() separate the kind from the storage; length(), dim(), nrow() and ncol() measure size (remembering that length() of a data frame counts columns); and names() plus attributes() reveal the labels and the hidden metadata, the realisation that every object is just data + attributes. You even x-rayed a fitted model and found it was a list all along.
Next, Lesson 4: Matrices and Arrays. You will meet R's other rectangle, the all-one-type grid: how to build it, index it by row and column, and reduce it with apply().