Practice Quiz
Sessions 1–4 — what to expect
How the real quiz works
Fifteen minutes. Ten questions. No computer, no notes. Seven multiple choice, three short answer of two or three sentences.
Short answers ask for two things. The question says what the second one is.
Ten questions follow. Try each before opening the answer.
1. A report with no warnings
#| warning: false
#| message: false
states <- st_read("states.shp")
rates <- read.csv("indicators.csv")
joined <- left_join(states, rates, by = "state_code")Your report renders cleanly, every time.
A colleague runs those same three lines in the console, where there are no chunk options, and sees two warnings you have never seen.
What do those two options actually change?
- A. Only what the document shows. The problems warned about are untouched.
- B. The code itself, so those warnings no longer happen at all.
- C. Nothing. They are only valid in the YAML header, not in a chunk.
- D. Into a separate warnings file you can read after rendering.
A.
These options control what appears in the rendered document. They do not change the code. Every problem R wanted to report is still there, doing whatever it was going to do.
R is good at warning you. Field names abbreviated for ESRI Shapefile driver. Although coordinates are longitude/latitude, st_within assumes that they are planar. Both are real, both matter, and both vanish under warning: false.
Put these options on the chunk that loads your packages, where the output really is noise. Not on a chunk that reads, joins or measures anything.
Not B: suppressing a warning does not fix what caused it, and the colleague at the console is the proof. Not C: chunk options work per chunk, which is what the #| header is for. Not D: there is no warnings file, and the output is discarded.
Session 1, “Code Chunks”.
2. A join that stops dead
cdmx <- st_as_sf(data.frame(lon = -99.1332, lat = 19.4326),
coords = c("lon", "lat"), crs = 4326)
states_projected <- st_transform(states, 6372)
st_join(cdmx, states_projected)
#> Error in st_geos_binop("intersects", x, y, sparse = sparse, ...) :
#> st_crs(x) == st_crs(y) is not TRUEEarlier in the same script, st_distance() on two unprojected points returned a sensible answer in metres without complaint. And you have run st_join() on two unprojected layers many times, quite happily.
Why does st_join fail here when st_distance did not?
- A.
st_join()needs a projected CRS, and EPSG:4326 is geographic, not projected. - B.
st_distance()had both inputs in one CRS;st_join()must combine two that disagree. - C.
st_transform()dropped the CRS metadata, so the states layer now has none at all. - D.
st_join()needs an explicitjoin =argument whenever the two layers differ.
B.
sf handles geodesic mathematics well. Ask for a distance between two points stored in degrees and it returns metres, because both inputs share one CRS. CRS mistakes rarely corrupt your numbers quietly. They stop you the moment you ask two layers in different CRSs to meet.
st_transform() one of them so both agree.
Not A: its second half is true, 4326 is geographic, but st_join() works in either kind of CRS as long as both layers share one. Not C: st_transform() sets a CRS, it never removes one. Not D: join = picks the predicate and defaults to st_intersects, and has nothing to do with CRS.
Session 2, “Why This Matters: The Error You Will Hit”, “st_transform Is the Fix”.
3. A layer from nowhere
states <- st_read("states.shp")
st_crs(states)$input
#> [1] NAThe layer draws in exactly the right shape, in exactly the right place.
What is missing, and why does it matter?
- A. The
.shxindex, sosfhad to guess the order of the features. - B. The
crs =argument, whichst_read()needs for every shapefile. - C. The
.dbffile, so the attribute table came back empty. - D. The
.prjfile, sosfdoes not know what the coordinates mean.
D.
The CRS lives in the .prj. Without it, sf reads the geometry perfectly and has no idea what the numbers mean, so st_crs() returns NA.
Every measurement after that is a bare number with no units:
st_area(states)[1]
#> 1.0402 # no units, no warning, no errorThat is not 1.04 km², or 1.04 m². It is the arithmetic of unlabelled coordinates. The map looks right because the shape and the relative positions are right. Only the meaning is gone.
Check st_crs(x)$input on everything you read. Set it with st_set_crs() if you know what it should have been.
Not A: .shx is mandatory, and without it the file will not open. Not B: shapefiles normally carry their own CRS, which is what the .prj is for. Not C: the attributes are all present.
Session 2, “A Shapefile Is Not One File”, “The .prj Is Just a Line of Text”.
4. A map that draws the eye
You fill a choropleth of homicide rates like this:
scale_fill_viridis_c(option = "turbo")The data is right and the classification is sensible. But looking at the finished map, the states that jump out at you are the ones in the middle of the distribution, not the worst ones.
Why, and what would you use instead?
- A.
turbois a rainbow: its brightest colours sit mid-scale. Useviridis. - B. The scale runs the wrong way; add
direction = -1to darken the top end. - C. There are too few classes; raise the classification from five to nine.
- D.
turbois discrete, so a continuous rate gets banded. Use the_cversion.
A.
turbo is a rainbow, and rainbows are not ordered. Its brightest colours sit in the middle of the scale, so the reader’s eye goes to the average states and slides over the extremes.
Rainbows fail all three tests a thematic palette has to pass. They are not perceptually uniform, so equal steps in the data do not look like equal steps in colour. They are not colourblind-safe, and about 8% of men have some colour vision deficiency. And they lose their order on a greyscale printer.
viridis passes all three.
Not B: reversing the scale fixes a real problem, but not this one. Turbo’s trouble is in the middle, and flipping the ends will not move it. Not C: more classes would give you more rainbow. Not D: the _c in scale_fill_viridis_c already means continuous.
Session 3, “Why viridis?”, “Direction Matters”.
5. What comes back
You have a layer of municipios and a layer of hospitals. Each of these questions is answered by one sf function.
Which one creates a shape that did not exist in either layer?
- A.
st_within()— is this hospital inside that municipio, yes or no? - B.
st_area()— how many square kilometres is this municipio? - C.
st_buffer()— everywhere within 10 km of this hospital. - D.
st_join()— which municipio does each hospital fall in?
C.
“Everywhere within 10 km” is a place that did not exist until you asked for it, so the answer has to be a new shape. st_buffer() is a constructor: geometry in, new geometry out.
The four families are worth keeping straight, because knowing which one you are in tells you what to do next.
| It returns | The family | Examples |
|---|---|---|
TRUE/FALSE, or indices |
predicates | st_intersects, st_within |
| a number | measures | st_area, st_distance |
| a new geometry | constructors | st_buffer, st_centroid, st_union |
| a table | joins and filters | st_join, st_filter |
A join is a predicate plus bookkeeping. st_join(x, y, join = st_within) runs the predicate and then attaches columns, which is why the predicate is an argument.
Not A: a predicate answers yes or no, and you use it to filter rows or drive a join. Not B: a measure answers with a number, and you put it in a column. Not D: a join answers with a table. It does hand back geometry, but the geometry that comes out of a join is the geometry that went in.
Session 4, “What a Spatial Function Hands Back”.
6. A layer that will not open
You email a colleague results.shp as an attachment. They reply that R cannot open it.
What is most likely missing, and what would have avoided this?
- A. The
.prj, which holds the CRS; set it withst_set_crs()before sending. - B. The
.dbf, which holds the geometry; re-export withdelete_dsn = TRUE. - C. Nothing is missing. They need to call
st_read(..., quiet = TRUE)on it. - D. The companion files. A shapefile is never one; send a GeoPackage.
D.
A shapefile is not one file. .shp (the geometry), .shx (an index) and .dbf (the attributes) are all mandatory, and .prj (the CRS) is needed in practice. Four files, linked only by a shared basename. Attach the .shp on its own and there is nothing to open.
A GeoPackage is one file that travels as one attachment, and st_write(results, "results.gpkg") picks the driver from the extension.
This is the same failure as embed-resources in session 1, in different clothes. Something that works on your machine because the rest of it is still sitting in the folder next door.
Not A: a missing .prj is a real and common problem, and it is question 3 on this sheet, but the layer still opens. It just has no CRS. Here it will not open at all, so a mandatory file is gone. Not B: the .dbf holds the attributes. Not C: quiet = TRUE suppresses the message about what was read, and cannot conjure missing files.
Session 2, “A Shapefile Is Not One File”; Session 3, “Which Format Should You Use?”.
7. A point on the border
edge <- st_sf(geometry = st_sfc(st_point(c(5, 2.5))))
lengths(st_within(edge, municipios))
#> [1] 0
lengths(st_intersects(edge, municipios))
#> [1] 2municipios holds two municipios that share a border, and this point sits exactly on it. Neither result is a bug.
What is each one telling you?
- A.
st_within()failed because a point has no area of its own. - B.
st_intersects()double-counted, which means the two polygons must overlap. - C.
st_within()wants the point strictly inside;st_intersects()counts a touch. - D.
st_within()needssparse = FALSE; that zero is an artefact of the call.
C.
The border belongs to both polygons and to the interior of neither. st_within asks whether the point is inside, and the honest answer is no, twice. st_intersects asks whether they touch, and the honest answer is yes, twice.
So one predicate loses the point and the other counts it twice, and both are right. You have to pick one and say which. With st_within your totals fall short of the number of points. With st_intersects they can exceed it.
For most data this never comes up, because points rarely land exactly on a line. That is what makes it awkward: the code works for years, and then one dataset arrives with coordinates snapped to the boundary.
Not A: st_within works fine for a point inside a polygon. Not B: the question says the two share a border, and a shared edge is enough to intersect both. Not D: sparse = FALSE returns a matrix instead of a list, and the matches are the same.
Session 4, “Two Predicates, Side by Side”, “What a Spatial Function Hands Back”.
8. An L-shaped state
Short answer — two or three sentences.
You label each state at its centroid. One state is shaped like an L.
st_within(st_centroid(L), L)
#> Sparse geometry binary predicate list of length 1, where the predicate
#> was `within'
#> 1: (empty)
st_within(st_point_on_surface(L), L)
#> Sparse geometry binary predicate list of length 1, where the predicate
#> was `within'
#> 1: 1Why does the label land outside its own state? And for what use of such a point would that not matter?
The centroid is the average of the shape, not a point in it. For a concave shape, an L or a crescent or a state wrapped around a bay, that average can land in the notch, outside the polygon. 1: (empty) says exactly that: the centroid is within nothing. So the label is drawn off its own state, possibly on top of the neighbour, and nothing errors.
st_point_on_surface() returns a point guaranteed to be inside, which is what a label anchor needs.
When it would not matter. Accept any use where being inside is irrelevant: a centre-to-centre distance matrix, a spatial weights or adjacency calculation, a gravity measure, the anchor end of a leader line, or compact shapes where the centroid is inside anyway. The cost of switching is that st_point_on_surface() is not the centre of mass, so it can sit close to an edge and is not comparable across polygons as an average location.
Full credit for the diagnosis plus any reasonable use. Half credit for the diagnosis alone.
Session 4, “Centroids Can Lie”.
9. Points per polygon
Short answer — two or three sentences.
Two adjacent polygons, west and east. Three points, all of them in west. You count points per polygon, expecting two rows, one for each polygon:
st_join(points, polygons) %>%
st_drop_geometry() %>%
count(id)
#> id n
#> 1 west 3Only one row comes back. east is not there at all, not even as a zero.
Why is east missing, and how would you get a row for it?
The points are on the left of the join, so the result has one row per point. A polygon that caught nothing never produces a row, and count() cannot count a group it never sees. A missing row is not the same as a zero: it draws grey like a data error, and it drops out of any mean or denominator.
Counting from the polygon side is the fix:
polygons$n <- lengths(st_intersects(polygons, points))
#> id n
#> 1 west 3
#> 2 east 0Worth showing in class: reversing the join is the natural fix and it does keep east, but plain count() then reports east as 1, because the left join gives it one row of NA attributes.
st_join(polygons, points) %>% st_drop_geometry() %>% count(id)
#> id n
#> 1 east 1 <-- one NA row, not one point
#> 2 west 3Full credit for explaining that the points are on the left, so an empty polygon never produces a row, plus any way of getting a row for every polygon. The reversed join counts as correct. The east 1 subtlety is a bonus, not a requirement.
Session 4, “Counting Points per Polygon”.
10. Ready to publish?
Short answer — two or three sentences.
Your choropleth is correct. The right variable, a sensible classification, a viridis fill. You are about to paste it into a report.
- The title reads “Homicide rate by state”
- The legend is labelled “rate”
- There is no caption
Name two things a reader still needs, and say why each one matters.
Accept any two, with a reason for each.
- A title that states the finding, not the variable name. “Homicide is concentrated in six states” tells the reader what to see. “Homicide rate by state” makes them work it out, and most will not.
- Units in the legend. “rate” is not a unit.
per 100,000 inhabitantsis, and without it a reader cannot tell 91 from 9.1, or know whether the figure is annual. - A source in the caption, and the period in the subtitle. A map with no provenance cannot be checked. A map with no date cannot be compared to anything.
- How missing states are shown, so a grey state is not read as a zero.
- The classification method, since the breaks drive the whole visual pattern.
theme_void(), because latitude and longitude gridlines are chart junk on a thematic map, andcoord_sf(crs = 6372)rather than publishing a Mexican map in Web Mercator.
The reason carries the mark. “Add units” is a checklist tick. “Add units, because 91 and 9.1 are indistinguishable without them” is the answer.
Session 3, “The Checklist”.
How to prepare
You cannot revise for this by memorising functions.
What it rewards is having run the code in sessions 1 to 4 and having looked at what came back. Every question is something that goes wrong quietly: a map that draws but is empty, a join that succeeds but matches nothing, a number that prints but is a sixth too large.