26  What a column holds

What kind of thing does a column hold, and can you choose by that alone? It is the other question you can ask about a column without looking at a single row. kind is one of "number", "text", "truth" or "date". A timestamp column answers as a date, because a timestamp is a date carrying a time.

run('
  survey
    then pick where (kind is "number")
')
respondent q1_score q2_score q3_score q4_score q5_score q6_score q7_score q8_score
1 4 2 5 3 4 5 2 3
2 5 5 4 3 2 5 4 5
3 3 5 2 4 5 3 4 2
4 4 3 5 2 4 3 5 4
5 2 4 3 5 4 2 3 5
6 5 4 4 5 3 4 5 4
survey |> pick(where(kind == "number"))
respondent q1_score q2_score q3_score q4_score q5_score q6_score q7_score q8_score
1 4 2 5 3 4 5 2 3
2 5 5 4 3 2 5 4 5
3 3 5 2 4 5 3 4 2
4 4 3 5 2 4 3 5 4
5 2 4 3 5 4 2 3 5
6 5 4 4 5 3 4 5 4
survey >> pick(where(kind == "number"))
respondent q1_score q2_score q3_score q4_score q5_score q6_score q7_score q8_score
1 4 2 5 3 4 5 2 3
2 5 5 4 3 2 5 4 5
3 3 5 2 4 5 3 4 2
4 4 3 5 2 4 3 5 4
5 2 4 3 5 4 2 3 5
6 5 4 4 5 3 4 5 4

survey then pick where (kind is "number")

Both questions live in the same where, so they join.

run('
  survey
    then pick where ((kind is "number") and (name starts "q"))
')
q1_score q2_score q3_score q4_score q5_score q6_score q7_score q8_score
4 2 5 3 4 5 2 3
5 5 4 3 2 5 4 5
3 5 2 4 5 3 4 2
4 3 5 2 4 3 5 4
2 4 3 5 4 2 3 5
5 4 4 5 3 4 5 4
survey |> pick(where(kind == "number" & startsWith(name, "q")))
q1_score q2_score q3_score q4_score q5_score q6_score q7_score q8_score
4 2 5 3 4 5 2 3
5 5 4 3 2 5 4 5
3 5 2 4 5 3 4 2
4 3 5 2 4 3 5 4
2 4 3 5 4 2 3 5
5 4 4 5 3 4 5 4
survey >> pick(where((kind == "number") & name.starts("q")))
q1_score q2_score q3_score q4_score q5_score q6_score q7_score q8_score
4 2 5 3 4 5 2 3
5 5 4 3 2 5 4 5
3 5 2 4 5 3 4 2
4 3 5 2 4 3 5 4
2 4 3 5 4 2 3 5
5 4 4 5 3 4 5 4

Put it with summarize and you have the sentence people reach for most: average every number, whatever the numbers happen to be called.

run('
  survey
    then summarize where (kind is "number") as average(value)
')
respondent q1_score q2_score q3_score q4_score q5_score q6_score q7_score q8_score
3.5 3.833333 3.833333 3.833333 3.666667 3.666667 3.666667 3.833333 3.833333
survey |> summarize(where(kind == "number", average(value)))
respondent q1_score q2_score q3_score q4_score q5_score q6_score q7_score q8_score
3.5 3.833333 3.833333 3.833333 3.666667 3.666667 3.666667 3.833333 3.833333
survey >> summarize(where(kind == "number", average(value)))
respondent q1_score q2_score q3_score q4_score q5_score q6_score q7_score q8_score
3.5 3.833333 3.833333 3.833333 3.666667 3.666667 3.666667 3.833333 3.833333

That one line does not name a column at all, so it keeps working when the table gains one. Inside the calculation, value stands for each column the condition matched, and the next chapter is built on that word.

26.1 Case, and the two words that handle it

The name tests match exactly, so name starts "q" does not find a column called Q1_score. Mixed-case names are ordinary enough that you meet this early.

There is no flag for it, because a name is text and text has a case. Fold the case and ask the question.

mixed names one column Q1_score and the next q2_score:

Q1_score q2_score Region
4 5 West
run('
  mixed
    then pick where lower(name) starts "q"
')
Q1_score q2_score
4 5
mixed |> pick(where(startsWith(tolower(name), "q")))
Q1_score q2_score
4 5
mixed >> pick(where(lower(name).starts("q")))
Q1_score q2_score
4 5

mixed then pick where lower(name) starts "q"

lower and upper are ordinary words, so they work on a value in exactly the same way.

run('
  mixed
    then keep where (lower([Region]) is "west")
')
Q1_score q2_score Region
4 5 West
mixed |> keep(tolower(Region) == "west")
Q1_score q2_score Region
4 5 West
mixed >> keep(lower(col.Region) == "west")
Q1_score q2_score Region
4 5 West

Folding the case of every name automatically would have been the other option, and it is wrong. Two columns can differ only by case, and a grammar that pretends otherwise cannot treat them as the two columns they are. Asking for it is one word.

26.2 Why you have to write name

The same three words also test what is inside a column, and there the subject is the column itself.

run('
  survey
    then keep where ([region] starts "W")
')
respondent name region joined q1_score q2_score q3_score q4_score q5_score q6_score q7_score q8_score
1 ana West 2026-01-12 4 2 5 3 4 5 2 3
4 dee West 2026-03-15 4 3 5 2 4 3 5 4
survey |> keep(startsWith(region, "W"))
respondent name region joined q1_score q2_score q3_score q4_score q5_score q6_score q7_score q8_score
1 ana West 2026-01-12 4 2 5 3 4 5 2 3
4 dee West 2026-03-15 4 3 5 2 4 3 5 4
survey >> keep(col.region.starts("W"))
respondent name region joined q1_score q2_score q3_score q4_score q5_score q6_score q7_score q8_score
1 ana West 2026-01-12 4 2 5 3 4 5 2 3
4 dee West 2026-03-15 4 3 5 2 4 3 5 4

So starts means one thing, “this text begins with that text”, and the subject says what text is being asked about. Writing it is what keeps the two apart.

The alternative would have been pick(starting("q")), which is shorter and leaves nothing in the sentence to say whether starting is looking at a name or at a value. You would learn it from the position, which is the kind of rule this grammar removes rather than adds.

The tests are the same words in both languages. What differs is how a column’s name is reached, which is the same difference as everywhere else. R writes a bare name and Python writes col.name, so R spells this with base R’s startsWith and Python with a method.

26.3 A percent sign is a percent sign

The three tests read text literally, with no pattern language hiding inside. R borrows grepl’s shape to ask the question, and the reading is still the grammar’s: the marks that pattern languages treat as instructions, % and . among them, here only ever mean themselves.

note
grew 100% this year
flat since March
run('
  notes
    then keep where [note] contains "100%"
')
note
grew 100% this year
notes |> keep(grepl("100%", note))
note
grew 100% this year
notes >> keep(col.note.contains("100%"))
note
grew 100% this year

notes then keep where [note] contains "100%"

When the question really is a pattern, it belongs to the host language, a boundary the coverage appendix states with the rest of them.

26.4 One word, one seat

kind asks about a column, so the where that chooses columns is the one seat it has. Written into an ordinary row question, it is refused rather than guessed at:

collect(sales |> keep(kind == "number"))
Error:
! 
illegal: there is no column called `kind`. The table has: date, region, product, quantity, revenue, cost
  |
2 |   then keep where ([kind] is "number")
  |                     ^^^^
try:
    collect(sales >> keep(kind == "number"))
except GodError as refusal:
    print(refusal)

illegal: `kind` means what a column holds, and the `where` that chooses columns is the one place that asks. Write it as `pick where kind is "number"`
  |
2 |   then keep where (kind is "number")
  |                    ^^^^

The two messages differ, because a bare kind is not the same thing in the two languages. In Python it is the grammar’s own word, so the message hands the word back to its seat, with the working spelling written out. In R a bare name inside keep is a column, so its message is the one a missing column gets: there is no column called kind.