Skip to main content

Make sense of a messy table

Inspect a messy synthetic spreadsheet, spot missing rows and unit traps, and check whether the summary matches the numbers.

1. Scenario

Pick a work role starter with synthetic material, or switch to Bring your own.

Outpatient phlebotomy wait-time log

Healthcare · Synthetic material

You manage the morning blood-test clinic and want to know why Tuesday's average wait time jumped on the weekly sheet.

visit_id,day,arrival_time,called_time,wait_minutes,notes
P-101,Mon,08:05,08:14,9,routine
P-102,Mon,08:12,08:23,11,routine
P-103,Tue,08:02,08:15,13,fasting
P-104,Tue,08:10,08:22,720,clock entered as 20:22 by mistake
P-105,Tue,08:18,08:29,11,routine
P-106,Wed,08:04,08:12,8,routine

2. Work area

Edit the briefs or setup below before you run.

Only the header row, inferred column types, and at most 20 sample rows leave your device. The analysis function runs in your browser on all rows.

6 columns · 25 total rows · 20 sample rows leave device

Answer and chart

Example. No model ran.

Physiotherapy had the most no-shows (6 of 25 total bookings). Bookings without an SMS reminder accounted for 9 of 9 no-shows, compared with 1 of 16 when an SMS was sent.

Inline SVG chartPhysiotherapy6Dermatology3Cardiology1
Steps in plain words
  1. 1. Group all 25 bookings by clinic and count how many rows have outcome equal to no-show.
  2. 2. Compare no-show rates between bookings where sms_sent is yes versus no.
  3. 3. Highlight Physiotherapy no-shows so you can verify the 6 rows by hand.
Inspect the analyze(rows) code that ran in your browser
function analyze(rows) {
  const byClinic = {};
  let smsYesTotal = 0;
  let smsYesNoShow = 0;
  let smsNoTotal = 0;
  let smsNoNoShow = 0;

  for (const row of rows) {
    const clinic = String(row.clinic || "Unknown");
    if (!byClinic[clinic]) {
      byClinic[clinic] = { clinic, bookings: 0, noShows: 0 };
    }
    byClinic[clinic].bookings += 1;
    const isNoShow = String(row.outcome) === "no-show";
    if (isNoShow) byClinic[clinic].noShows += 1;

    if (String(row.sms_sent) === "yes") {
      smsYesTotal += 1;
      if (isNoShow) smsYesNoShow += 1;
    } else {
      smsNoTotal += 1;
      if (isNoShow) smsNoNoShow += 1;
    }
  }

  const table = Object.values(byClinic).sort((a, b) => b.noShows - a.noShows);
  const chart = table.map((item) => ({ label: item.clinic, value: item.noShows }));
  const top = table[0] || { clinic: "None", noShows: 0 };

  return {
    answer: top.clinic + " had the most no-shows (" + top.noShows + " of " + rows.length + " total bookings). Bookings without an SMS reminder accounted for " + smsNoNoShow + " of " + smsNoTotal + " no-shows, compared with " + smsYesNoShow + " of " + smsYesTotal + " when an SMS was sent.",
    table,
    chart,
    checkOne: {
      label: "Total no-shows across all clinics",
      figure: smsYesNoShow + smsNoNoShow,
      column: "outcome",
      equals: "no-show",
      mode: "count"
    }
  };
}

Check one number by hand

Reported: 10 · Rows found: 10

Total no-shows across all clinics: Filter where outcome equals no-show. Below are all 10 matching rows from the 25-row spreadsheet so you can count or sum them yourself.

1. booking_id: BK-102 · clinic: Physiotherapy · slot: Afternoon · sms_sent: no · outcome: no-show · wait_days: 14
2. booking_id: BK-104 · clinic: Dermatology · slot: Afternoon · sms_sent: no · outcome: no-show · wait_days: 18
3. booking_id: BK-105 · clinic: Physiotherapy · slot: Afternoon · sms_sent: no · outcome: no-show · wait_days: 12
4. booking_id: BK-109 · clinic: Dermatology · slot: Afternoon · sms_sent: no · outcome: no-show · wait_days: 21
5. booking_id: BK-110 · clinic: Physiotherapy · slot: Afternoon · sms_sent: yes · outcome: no-show · wait_days: 15
6. booking_id: BK-111 · clinic: Cardiology · slot: Afternoon · sms_sent: no · outcome: no-show · wait_days: 11
7. booking_id: BK-114 · clinic: Physiotherapy · slot: Afternoon · sms_sent: no · outcome: no-show · wait_days: 16
8. booking_id: BK-119 · clinic: Physiotherapy · slot: Afternoon · sms_sent: no · outcome: no-show · wait_days: 19
9. booking_id: BK-123 · clinic: Dermatology · slot: Afternoon · sms_sent: no · outcome: no-show · wait_days: 20
10. booking_id: BK-124 · clinic: Physiotherapy · slot: Afternoon · sms_sent: no · outcome: no-show · wait_days: 17

3. Run

Nothing leaves your browser until you press Run.

Sends only the column header and at most twenty sample rows when you press run. to job data_code (claude-sonnet-5-5, effort medium). Raw text is processed in memory and not stored.

4. Inspect

See what left the browser, which model ran, and the receipt.

Example. No model ran. · source: offline
What left the browser

Nothing sent yet. When you run, only headers and at most 20 sample rows leave this device.

What stayed on device

All 25 rows stay in your browser and run inside the sandboxed Web Worker.

Model and job

data_code · claude-sonnet-5-5 (effort medium, max 1600 tokens). Haiku reads the schema and sample rows to propose checks you can verify against the full table.

Runs and source

0 runs used · source: offline · Example. No model ran.

5. Judge

Did you catch row P-104 (720 minutes caused by a 24-hour clock typo) before reporting Tuesday's average?

80%

50 is a coin flip. 99 is certain. Honest confidence scores best.

6. Debrief and Keep

What to check next time, the matching lesson, and keeping this setup as a recipe.

What to check next time

A single 12-hour/24-hour clock typo turns a 12-minute wait into 720 minutes. Always inspect extreme rows before trusting an average.

Keep as a recipe

Save the stronger brief and scenario setup to your device so you can reuse or adapt it in Keep.

Open Keep