pandas + LLM on a Real CSV: Clean, Classify, Check, Report
Hands-on lab · IDE in your browser

pandas + LLM on a Real CSV: Clean, Classify, Check, Report

Take a messy helpdesk export from raw file to trustworthy numbers: load it as text to see the real problems, clean duplicates, blank rows, spellings, three date formats and money in three styles with pandas, label 152 tickets with a language model in numbered batches that cannot silently misalign, spot-check the labels against a hand-labelled sample with a confusion matrix, and answer the manager's questions with groupby.

Time
60 min
Checked steps
5
Level
Beginner
Setup
None
Read step 1

Hands-on labs require Pro · $29.99/mo · cancel anytime

Lab cockpit60 min · 5 stepsSession running
1 / 5 steps passingClean the export · step 2 of 5
tickets.py▶ Run✓ Check
# ---------- Step 2: clean ----------CHANNEL_FIXES = {"e-mail": "email", "telephone": "phone", "web chat": "chat", "livechat": "chat"}  def clean(df):    """A cleaned copy: no duplicate rows, text stripped, no blank messages, one spelling per channel,    `created` as datetimes, `refund_amount` as numbers (NaN when there is none), a fresh 0..n-1 index."""        
TerminalOutput

The job

Brightline Books exported a month of support tickets from its helpdesk, and the manager wants to know what customers write in about, through which channel, and what refunds cost. The export is typical: tickets exported twice, blank messages, a dozen spellings of three channels, dates in three formats and money written as £12.50, 12,50 or 12.50. No ticket has a topic. You clean the table with pandas, have a model label every ticket in batches, measure how often the labels are right against a colleague's hand-checked sample, and produce the numbers.

5 steps, each checked when you finish it

A check runs your work at the end of every step. Hints and the full solution are there if you get stuck.

  1. 1

    Look before you clean

    Brightline's helpdesk exported a month of support tickets to tickets.csv.

    You writeload()profile()
  2. 2

    Clean the export

    Every problem the profile found gets one line of pandas.

    You writeclean()
  3. 3

    Classify 152 tickets in 16 calls

    Every ticket needs a topic.

    You writeclassify_batch()classify_all()
  4. 4

    Spot-check against a person

    The model labelled 152 tickets and nobody has checked one.

    You writespot_check()
  5. 5

    Answer the manager's questions

    Now the table can answer questions.

    You writereport()

Step 1 as it appears in the lab

The lab’s own text. The hint and the solution stay inside the lab.

Step 1: Look before you clean

Brightline's helpdesk exported a month of support tickets to tickets.csv. The manager wants to know what customers write in about, through which channel, and what refunds cost. The answers are in this file, once it is clean and every ticket has a topic. pandas is the Python library for tables like this: a DataFrame is a table, and each column is a Series.

First, load it without letting pandas guess. By default read_csv turns anything that looks numeric into numbers and empty cells into NaN ("not a number"). On a messy export that hides problems: "12,50" becomes text in one row and a number in another. Loading every column as text (dtype=str) and empty cells as "" shows the file as it really is.

Do this

1. Write load(path): pd.read_csv(path, dtype=str, keep_default_na=False).

2. Write profile(df), a first look that returns:

  • "rows": len(df)
  • "duplicate_rows": rows that exactly repeat an earlier row, int(df.duplicated().sum())
  • "blank_messages": messages that are empty or only spaces: int((df["message"].str.strip() == "").sum())
  • "channel_spellings": sorted(df["channel"].unique())

.str gives a text column string methods that work on every row at once: df["message"].str.strip() strips all 167 messages in one go.

3. Run, read the output, and answer the questions below.

tickets.py, the file you edit91 lines
"""A month of Brightline support tickets: load, clean, classify with a model, check, report."""
import json
from concurrent.futures import ThreadPoolExecutor

import pandas as pd
from openai import OpenAI

CLIENT = OpenAI()
MODEL = "meta/llama-3.1-8b-instruct"
TOPICS = ["delivery", "refund", "damaged", "account", "recommendation", "other"]


# ---------- Step 1: load without letting pandas guess ----------
def load(path="tickets.csv"):
    """Every column as text, and empty cells as "" rather than NaN."""
    # TODO (Step 1): pd.read_csv(path, dtype=str, keep_default_na=False)
    raise NotImplementedError("Step 1: write load()")


def profile(df):
    """{"rows", "duplicate_rows", "blank_messages", "channel_spellings"} for a first look."""
    # TODO (Step 1): return a dict with
    #   "rows": len(df)
    #   "duplicate_rows": how many rows are exact copies of an earlier row (df.duplicated().sum(), as int)
    #   "blank_messages": how many messages are empty or only spaces (df["message"].str.strip() == "")
    #   "channel_spellings": sorted(df["channel"].unique())
    raise NotImplementedError("Step 1: write profile()")


# ---------- Step 2: clean ----------
CHANNEL_FIXES = {"e-mail": "email", "telephone": "phone", "web chat": "chat", "livechat": "chat"}


def clean(df):
    """A cleaned copy: no duplicate rows, text stripped, no blank messages, one spelling per channel,
    `created` as datetimes, `refund_amount` as numbers (NaN when there is none), a fresh 0..n-1 index."""
    raise NotImplementedError("clean() arrives in Step 2")


# ---------- Step 3: classify many rows with few calls ----------
SYSTEM = ("You label customer support messages for a bookshop. Topics:\n"
          "delivery: an order has not arrived, is late, or has no tracking update\n"
          "refund: wants money back, or was charged wrongly\n"
          "damaged: an item arrived damaged, faulty or incomplete\n"
          "account: logging in, passwords, account details, loyalty points\n"
          "recommendation: asks what to read or buy\n"
          "other: anything else\n"
          "When two topics fit, these rules decide:\n"
          "- an item that arrived damaged, wet, faulty or misprinted is damaged, even if they ask for a refund\n"
          "- wanting to cancel a late order and get the money back is refund\n"
          "- a login or loyalty card problem is account, whatever they were trying to do\n"
          "You get a numbered list of messages. Reply with one JSON object that maps each message number "
          "to its topic, and nothing else. Example for three messages: {\"1\": \"delivery\", \"2\": \"other\", \"3\": \"refund\"}")


def classify_batch(messages, client=None):
    """One call for a list of messages; returns one topic per message, in order.
    Raises ValueError unless the reply maps exactly the numbers 1..n to valid topics."""
    client = client or CLIENT
    raise NotImplementedError("classify_batch() arrives in Step 3")


def classify_one(message, client=None):
    """Fallback for a single message: the first topic word in the reply ("other" if there is none)."""
    client = client or CLIENT
    r = client.chat.completions.create(model=MODEL, temperature=0, max_tokens=5,
                                       messages=[{"role": "system", "content": SYSTEM.split("You get a numbered")[0]
                                                  + "Reply with the topic only, one word."},
                                                 {"role": "user", "content": message}])
    words = r.choices[0].message.content.lower().replace('"', " ").replace(".", " ").split()
    return next((w for w in words if w in TOPICS), "other")


def classify_all(messages, batch_size=10, client=None):
    """Topics for every message. Batches run side by side; a batch whose reply fails validation
    is redone one message per call with classify_one."""
    raise NotImplementedError("classify_all() arrives in Step 3")


# ---------- Step 4: spot-check against labels a person wrote ----------
def spot_check(df, gold):
    """Compare df's topic with gold's true_topic on the tickets in gold. Return
    {"checked", "accuracy" (3 places), "confusion" (pd.crosstab, true topics in rows, predicted in columns),
     "recall_by_topic" ({true topic: share of its tickets labelled correctly, 3 places})}."""
    raise NotImplementedError("spot_check() arrives in Step 4")


# ---------- Step 5: answer the manager's questions ----------
def report(df):
    """The numbers the manager asked for, as plain Python values."""
    raise NotImplementedError("report() arrives in Step 5")
Provided for you:gold.csvtickets.csvtry_it.py

Frequently asked questions

Why load every column as text first?

By default pandas guesses types and turns empty cells into NaN, which hides mixed formats such as 12,50 next to 12.50. Loading as text shows the file as it is; each column is then converted on purpose during cleaning.

Why classify rows in batches?

One call per row resends the same instructions every time. Ten numbered rows per call cuts calls and instruction tokens tenfold. Keying each answer by row number, and rejecting replies whose numbers do not match, stops a skipped row from shifting every label after it.

How do I know the model's labels are right?

Compare them with a random sample a person labelled. Accuracy gives the overall rate; a confusion matrix and per-topic recall show which topics are confused, which a single accuracy figure hides.

Do I need to know pandas already?

No. The lab introduces read_csv, string methods, boolean masks, to_datetime, to_numeric, merge, crosstab, value_counts and groupby as each is needed.

Using pandas and an LLM together on real data

A lot of practical AI work is a table: rows of text that need a label, a score or an extracted field, next to columns that need ordinary cleaning. pandas does the cleaning and the counting, and a language model does the reading. The skill is combining them so the result can be trusted. In this lab you profile a raw CSV before touching it, clean it step by step (duplicates, whitespace, category spellings, mixed date formats with day-first parsing, currency strings to numbers), classify every row with a hosted model in batches keyed by row number so labels cannot shift, fall back to single calls when a batch reply fails validation, measure accuracy against a hand-labelled sample with a confusion matrix and per-topic recall, and answer business questions with value_counts, groupby, median and weekly periods.