PLAINFINCH learn by building

Project: a sightings report

You will build a short report file from a CSV of bird sightings using counts, a top three and a filter by place.

What are you building?

The Riverside Bird Club keeps a log of every sighting in sightings.csv: the date, the species, the place and how many birds were counted. June, who does the records, wants a short report for the April newsletter with three parts. First, how many times each species was seen, most frequent first. Second, the three sightings with the biggest counts. Third, every sighting at Mill Pond, where the club is planning a walk.

Every part is one pipeline built from tools you already know: sed, cut, sort, uniq, head and grep. The new skill is putting them together and saving the results into one file in the right order. When you’re done, report.txt should be about 17 lines and sightings.csv should be exactly as it started.

Work the way you would on a real job. Look at the data first, build each pipeline on screen until the output is right, and only then send it into the file.

How should you look at the data first?

Start with head -n 3 sightings.csv. It shows the header and two rows, so you know the separator is a comma and which field holds what. Counting from 1, field 2 is the species and field 4 is the count. wc -l sightings.csv says 13 lines, which means 12 sightings plus the header.

$ head -n 3 sightings.csv
date,species,place,count
2026-03-01,heron,Mill Pond,2
2026-03-01,kingfisher,River Walk,1

How do you count sightings per species?

Build it one stage at a time and run it after each addition. sed 1d sightings.csv drops the header. Adding | cut -d, -f2 keeps only the species. Adding | sort puts the names in order so all the herons sit together. | uniq -c collapses each run of identical lines into one line with a count in front. A final | sort -rn sorts by that count, biggest first.

The count from uniq -c is padded with spaces on the left, which is normal. The two birds seen twice, kingfisher and goldfinch, are tied, and sort -rn breaks the tie by comparing the rest of the line in reverse, which puts kingfisher first.

$ sed 1d sightings.csv | cut -d, -f2 | sort | uniq -c | sort -rn
      4 heron
      3 mallard
      2 kingfisher
      2 goldfinch
      1 little egret
FigureThe species pipeline as five boxes: sed 1d drops the header, cut keeps the species, sort groups names, uniq -c counts each run, and sort -rn puts 4 heron and 3 mallard on top.The species pipeline as five boxes: sed 1d drops the header, cut keeps the species, sort groups names, uniq -c counts each run, and sort -rn puts 4 heron and 3 mallard on top.
Each stage hands its lines to the next. Delete stages from the end with the Up arrow and you see exactly these intermediate lists.

Why does each stage of that pipeline matter?

Leave out sed 1d and the header word species is counted as a bird, with a count of 1. Leave out the first sort and uniq -c sees heron, kingfisher, mallard, heron and so on, never two herons in a row, so every line gets a count of 1. Leave out the last sort -rn and the list comes out in alphabetical order, which doesn’t answer the question of which bird is most common.

This is why building on screen helps. Each stage’s output is the next stage’s input, and when the final result looks wrong you can delete stages from the end with the Up arrow until you find the one that misbehaves. The Explain view shows each part of the pipe separately if you lose track.

A note on counting. This report counts how many times each species was logged, rather than how many birds in total. Fourteen mallards in one sighting still counts as one line. Adding up field 4 per species needs a tool such as awk on a real computer, and it’s a fair question to ask June which number she wants.

How do you find the biggest counts?

Here you sort whole lines by one field. sort -t, -k4 -nr means: fields are separated by commas (-t,), sort by field 4 (-k4), compare as numbers (-n), largest first (-r). Then head -n 3 keeps the top three. Drop the header first, or it sorts as a line with the word count where a number should be.

Without -n, sort compares text character by character, and 9 comes after 1 the way z comes after a. The list would start with the 9 goldfinches and put 14 and 11 much lower. Whenever you sort numbers, check that -n is there.

$ sed 1d sightings.csv | sort -t, -k4 -nr | head -n 3
2026-03-02,mallard,Mill Pond,14
2026-03-07,mallard,Mill Pond,11
2026-03-05,goldfinch,Old Orchard,9

How do you pick out the Mill Pond sightings?

This part is a plain filter. grep 'Mill Pond' sightings.csv prints the six lines that contain the place name. Keep the quotes, because without them the shell splits Mill Pond into two words and grep searches for Mill in a file called Pond. grep -c 'Mill Pond' sightings.csv prints 6 if you only want the number.

Check what the pattern could catch by accident. A place called Mill Pond Lane would match too. Here the log is small enough to read, but on a long file, tighten the pattern to ',Mill Pond,' so it matches the whole field with its commas.

How do you put the three parts into one file?

Use > once, for the first line, and >> for everything after it. > creates the file or empties an existing one. >> adds to the end. A short heading before each part, written with echo, makes the report readable when June opens it.

echo 'Sightings per species' > report.txt
sed 1d sightings.csv | cut -d, -f2 | sort | uniq -c | sort -rn >> report.txt
echo 'Top 3 counts' >> report.txt
sed 1d sightings.csv | sort -t, -k4 -nr | head -n 3 >> report.txt
echo 'Mill Pond sightings' >> report.txt
grep 'Mill Pond' sightings.csv >> report.txt

How do you check the report before sending it?

Read it with cat report.txt and compare each part with what you saw on screen. wc -l report.txt should say 17: three headings, five species, three top counts and six Mill Pond lines. If it says 6, a stray > replaced the earlier parts. If it says more than 17, you probably ran a >> line twice. Start again from the echo ... > line, which empties the file for you.

Also confirm you didn’t touch the source. Nothing in these commands writes to sightings.csv, but a mistyped > such as grep 'Mill Pond' > sightings.csv would wipe it. The Files view marks any changed file, so a glance there tells you the log is safe.

Once the six lines work, they are a good candidate for a script that June can run every month. A later lesson on scripts shows how to save them in report.sh and run it with one command.

What would an AI assistant get wrong here?

Asked for “counts per species”, assistants often write cut -d, -f2 sightings.csv | sort | uniq -c. That looks right and counts the header. Some reach for awk or a Python script, which works on a real computer but hides the steps. Others sort numbers without -n, or use sort -k4 without -t,, in which case sort splits on spaces and field 4 is whatever comes after the third space. Run each pipeline on screen, read the first and last lines of the output, and check one number by hand, such as the four heron lines you can find with grep -c heron sightings.csv.

Field notes

  • Look at the first lines of the data before writing any pipeline.
  • sed 1d drops the header so it isn't counted as data.
  • sort -t, -k4 -nr sorts rows by the fourth field as numbers, biggest first.
  • Use > for the first part of the report and >> for every part after.

Easy to get wrong

The header is counted as a species, and without sort the repeated names are never next to each other, so each gets a count of 1.

Without -n the counts sort as text, so 9 lands above 14 and 11.

A single > empties the report before writing, wiping the species counts and top three you added earlier.

Questions people ask

Why are there spaces in front of the counts?

uniq -c pads the count so the names line up in a column. It’s normal, and sort -n ignores leading spaces.

Can I sort by species name and then by count?

Yes. Give sort more than one key, for example sort -t, -k2,2 -k4,4nr, which sorts by field 2 alone and then by field 4 as numbers, largest first. The practice terminal handles one -k key, so try this on a real computer.

How do I make this into a monthly job?

Put the six commands in a file called report.sh, make it runnable with chmod +x report.sh and run ./report.sh. The scripts lesson covers that step by step.

What if the log uses tabs instead of commas?

cut uses tab by default, so drop -d,. For sort, use -t$'\t' in bash to set a tab as the separator.

Keep going