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.
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")gold.csvtickets.csvtry_it.py