Merge pull request #832 from CosmusMutuku/week4-exercise
Week_6_Exercise
This commit is contained in:
621
week6/community-contributions/week_6_exercise_revised.py
Normal file
621
week6/community-contributions/week_6_exercise_revised.py
Normal file
@@ -0,0 +1,621 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Week_6_exercise_revised.ipynb
|
||||
|
||||
Automatically generated by Colab.
|
||||
|
||||
Original file is located at
|
||||
https://colab.research.google.com/drive/1GaV053HB8l-Wd3J3o9BcOAjC009Qk_W0
|
||||
"""
|
||||
|
||||
#installations
|
||||
!pip install --upgrade pip
|
||||
!pip install datasets==3.0.1 anthropic transformers accelerate pandas tqdm numpy
|
||||
|
||||
#imports
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import random
|
||||
import time
|
||||
from typing import Optional, List, Dict, Any, Tuple
|
||||
from sklearn.model_selection import train_test_split
|
||||
import anthropic
|
||||
from datasets import load_dataset
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from tqdm import tqdm
|
||||
import seaborn as sns
|
||||
|
||||
#TEMPORARY: Hard-coded keys
|
||||
|
||||
#I hid my keys, you can replace your keys with 'sk' and 'hf'
|
||||
os.environ["ANTHROPIC_API_KEY"] = "sk"
|
||||
os.environ["HF_TOKEN"] = "hf"
|
||||
|
||||
|
||||
# Anthropic Client
|
||||
try:
|
||||
client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
|
||||
print("Anthropic client initialized")
|
||||
except Exception as e:
|
||||
raise ImportError("Please install anthropic: !pip install anthropic") from e
|
||||
|
||||
#some Basic configrations used throughtout the notebook
|
||||
RANDOM_SEED = 42
|
||||
# medium test size
|
||||
TEST_SIZE = 50
|
||||
CLAUDE_MODEL = "claude-opus-4-20250514"
|
||||
MAX_TOKENS = 300
|
||||
|
||||
random.seed(RANDOM_SEED)
|
||||
np.random.seed(RANDOM_SEED)
|
||||
|
||||
# Load my dataset, the Aplliances in my case
|
||||
dataset = load_dataset("McAuley-Lab/Amazon-Reviews-2023", "raw_meta_Appliances", split="full")
|
||||
#using Pandas to create a dataframe
|
||||
df = dataset.to_pandas()
|
||||
#see the data
|
||||
df.head()
|
||||
|
||||
# Let clean the Price column and have it as a Price-clean
|
||||
df["price_clean"] = pd.to_numeric(df["price"], errors="coerce")
|
||||
|
||||
#check the number of rows In the ddata
|
||||
print("Dataset size:", len(df))
|
||||
|
||||
#check The featues in the data
|
||||
print(df.columns.tolist())
|
||||
|
||||
#checking some info
|
||||
print(df.info())
|
||||
|
||||
print("Price-related columns found:", [c for c in df.columns if "price" in c])
|
||||
|
||||
print("Missing price_clean:", df["price_clean"].isna().sum(), "rows")
|
||||
|
||||
# Price distribution visualization (Zoomed histogram)
|
||||
plt.figure(figsize=(10,5))
|
||||
df[df["price_clean"] < 200]["price_clean"].hist(bins=50)
|
||||
plt.title("Price Distribution")
|
||||
plt.xlabel("Price ($)")
|
||||
plt.ylabel("Frequency")
|
||||
plt.show()
|
||||
|
||||
# Keep only rows where price_clean is not null
|
||||
df_model = df.dropna(subset=["price_clean"]).copy()
|
||||
|
||||
# come up with a ptompt text combined
|
||||
def combine_text(row):
|
||||
title = row["title"] or ""
|
||||
features = " ".join(row["features"]) if isinstance(row["features"], list) else ""
|
||||
description = " ".join(row["description"]) if isinstance(row["description"], list) else ""
|
||||
return f"{title}\n\nFEATURES: {features}\n\nDESCRIPTION: {description}"
|
||||
|
||||
df_model["text"] = df_model.apply(combine_text, axis=1)
|
||||
|
||||
# Retain what's needed
|
||||
df_model = df_model[["text", "price_clean"]].reset_index(drop=True)
|
||||
|
||||
# check the model dataset size
|
||||
print(len(df_model))
|
||||
df_model.head(5)
|
||||
|
||||
# Splitting the data into Training and test
|
||||
train_df, test_df = train_test_split(
|
||||
df_model,
|
||||
test_size=0.10, # 10% test split
|
||||
random_state=RANDOM_SEED
|
||||
)
|
||||
|
||||
#Training
|
||||
len(train_df)
|
||||
|
||||
#Testing
|
||||
len(test_df)
|
||||
|
||||
# make the test a list for better samplng
|
||||
test_records = test_df.to_dict(orient="records")
|
||||
|
||||
# Pricing system Prompt
|
||||
|
||||
def build_prompt(item_text: str) -> str:
|
||||
return f"""
|
||||
You are a pricing analyst. Given a marketplace product listing, estimate the item's correct fair market price in KES.
|
||||
|
||||
Return ONLY a number, no currency sign, no explanation.
|
||||
|
||||
Product details:
|
||||
\"\"\"
|
||||
{item_text}
|
||||
\"\"\"
|
||||
"""
|
||||
|
||||
def estimate_price_claude(item_text: str) -> Optional[float]:
|
||||
try:
|
||||
prompt = build_prompt(item_text)
|
||||
|
||||
response = client.messages.create(
|
||||
model=CLAUDE_MODEL,
|
||||
max_tokens=MAX_TOKENS,
|
||||
messages=[
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
)
|
||||
|
||||
raw_output = response.content[0].text.strip()
|
||||
|
||||
# Extract first valid number from model response
|
||||
match = re.search(r"\d+(\.\d+)?", raw_output.replace(",", ""))
|
||||
return float(match.group(0)) if match else None
|
||||
|
||||
except Exception as e:
|
||||
print("Error:", e)
|
||||
return None
|
||||
|
||||
client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
|
||||
|
||||
# Filter and Sample 100 usable Rows
|
||||
df_usable = df[df["price_clean"].notna()].copy()
|
||||
sample_df = df_usable.sample(100, random_state=42).reset_index(drop=True)
|
||||
|
||||
#empty predriction list for them to be stored
|
||||
predictions = []
|
||||
|
||||
#Getting the prices
|
||||
def extract_price(text):
|
||||
"""Extract the first valid float from Claude's reply."""
|
||||
match = re.search(r"\d+(\.\d+)?", text.replace(",", ""))
|
||||
return float(match.group(0)) if match else None
|
||||
|
||||
# Getting the predictions
|
||||
for i, row in tqdm(sample_df.iterrows(), total=len(sample_df)):
|
||||
title = row["title"]
|
||||
desc = " ".join(row["description"]) if isinstance(row["description"], list) else str(row["description"])
|
||||
feat = " ".join(row["features"]) if isinstance(row["features"], list) else str(row["features"])
|
||||
cats = " ".join(row["categories"]) if isinstance(row["categories"], list) else str(row["categories"])
|
||||
|
||||
prompt = f"""
|
||||
You are estimating the USD retail price of an appliance part.
|
||||
|
||||
Analyze the information and respond with **only a single number** (no currency symbol, no text, no explanation).
|
||||
|
||||
TITLE: {title}
|
||||
DESCRIPTION: {desc}
|
||||
FEATURES: {feat}
|
||||
CATEGORIES: {cats}
|
||||
|
||||
Your response must be only a number like: 29.99
|
||||
"""
|
||||
|
||||
response = client.messages.create(
|
||||
model=CLAUDE_MODEL,
|
||||
max_tokens=50,
|
||||
messages=[{"role": "user", "content": prompt}]
|
||||
)
|
||||
|
||||
raw = response.content[0].text.strip()
|
||||
pred_price = extract_price(raw)
|
||||
|
||||
predictions.append({
|
||||
"title": title,
|
||||
"true_price": row["price_clean"],
|
||||
"claude_price": pred_price,
|
||||
"raw_reply": raw
|
||||
})
|
||||
|
||||
# Saving output in a csv nw
|
||||
result_df = pd.DataFrame(predictions)
|
||||
result_df.to_csv("claude_price_predictions_100.csv", index=False)
|
||||
|
||||
# Show preview
|
||||
display(result_df.head())
|
||||
|
||||
# Error metrics
|
||||
valid = result_df[result_df["claude_price"].notna()]
|
||||
mae = np.mean(np.abs(valid["true_price"] - valid["claude_price"]))
|
||||
rmse = np.sqrt(np.mean((valid["true_price"] - valid["claude_price"])**2))
|
||||
pct_within_20 = np.mean(np.abs(valid["true_price"] - valid["claude_price"]) <= 20) * 100
|
||||
|
||||
print(f"\nValid predictions: {len(valid)}/{len(result_df)}")
|
||||
print(f"MAE: {mae:.2f}")
|
||||
print(f"RMSE: {rmse:.2f}")
|
||||
print(f"% within $20: {pct_within_20:.1f}%")
|
||||
|
||||
"""The model returned a price every single time:
|
||||
|
||||
|
||||
|
||||
1. -->MAE = 22.52 On average Claude is off by 22.52 from the true price
|
||||
2. -->RMSE = 44.11 Big errors exist on some items — a sign of occasional wild guesses
|
||||
2. -->RMSE = 44.11 Big errors exist on some items — a sign of occasional wild guesses
|
||||
2. -->72% within $20 Claude predicts reasonable accuracy on most products, but 28% are far off.
|
||||
|
||||
;
|
||||
|
||||
1. Strengths- Model is somehow decent with zero/low fine-tuning. It understood the task, 72% within $20 on a dataset it’s never seen is a good baseline
|
||||
1. Weaknesses- Too many rounded “classic” retail numbers (24.99, 89.99, 14.99, 29.99). Seems not to deeply use features, category, or rating. Also the RMSE is high → meaning a few really bad errors are dragging performance
|
||||
|
||||
Improvements
|
||||
|
||||
1. Prompt enhancements
|
||||
2. Multi-shot and also better structuring
|
||||
3. Fine-tuning with local model
|
||||
"""
|
||||
|
||||
#Now we build a persona Prompt
|
||||
def build_pricing_prompt(examples: list, new_title: str) -> str:
|
||||
"""
|
||||
Build a multi-shot prompt for the E-commerce Market Analyst persona.
|
||||
Each example has (title, price).
|
||||
"""
|
||||
few_shots = "\n".join(
|
||||
[f"Product: {t}\nEstimated fair market price: ${p:.2f}" for t, p in examples]
|
||||
)
|
||||
|
||||
system_prompt = (
|
||||
"You are a meticulous Data-Driven Market Analyst who estimates realistic, data-based "
|
||||
"product prices for online marketplaces. You base estimates on comparable items and "
|
||||
"avoid outliers. Return only the price number."
|
||||
)
|
||||
|
||||
user_prompt = (
|
||||
f"{system_prompt}\n\nHere are recent examples:\n{few_shots}\n\n"
|
||||
f"Now estimate a fair market price for this product:\n"
|
||||
f"Product: {new_title}\n\n"
|
||||
"Respond with only a number, no text or symbols."
|
||||
)
|
||||
return user_prompt
|
||||
|
||||
#10-shot predictios
|
||||
subset_10 = df.dropna(subset=["price_clean"]).sample(10, random_state=42).reset_index(drop=True)
|
||||
few_shots_3 = subset_10.sample(3, random_state=42)[["title", "price_clean"]].values.tolist()
|
||||
results_10 = []
|
||||
|
||||
for i, row in tqdm(subset_10.iterrows(), total=len(subset_10)):
|
||||
prompt = build_pricing_prompt(few_shots_3, row["title"])
|
||||
try:
|
||||
resp = client.messages.create(
|
||||
model=CLAUDE_MODEL,
|
||||
max_tokens=MAX_TOKENS,
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
)
|
||||
reply = resp.content[0].text.strip()
|
||||
pred = float(reply.replace("$", "").strip())
|
||||
except Exception:
|
||||
pred, reply = np.nan, None
|
||||
results_10.append({"title": row["title"], "true_price": row["price_clean"], "pred_price": pred, "raw": reply})
|
||||
|
||||
df10 = pd.DataFrame(results_10).dropna(subset=["pred_price"])
|
||||
|
||||
mae10 = np.mean(np.abs(df10.pred_price - df10.true_price))
|
||||
|
||||
rmse10 = np.sqrt(np.mean((df10.pred_price - df10.true_price)**2))
|
||||
|
||||
pct20_10 = np.mean(np.abs(df10.pred_price - df10.true_price) <= 20) * 100
|
||||
|
||||
print(f"MAE={mae10:.2f}, RMSE={rmse10:.2f}, %within$20={pct20_10:.1f}%")
|
||||
df10.head()
|
||||
|
||||
#30 shot
|
||||
subset_30 = df.dropna(subset=["price_clean"]).sample(30, random_state=42).reset_index(drop=True)
|
||||
few_shots_5 = subset_30.sample(5, random_state=42)[["title", "price_clean"]].values.tolist()
|
||||
results_30 = []
|
||||
|
||||
for i, row in tqdm(subset_30.iterrows(), total=len(subset_30)):
|
||||
prompt = build_pricing_prompt(few_shots_5, row["title"])
|
||||
try:
|
||||
resp = client.messages.create(
|
||||
model=CLAUDE_MODEL,
|
||||
max_tokens=MAX_TOKENS,
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
)
|
||||
reply = resp.content[0].text.strip()
|
||||
pred = float(reply.replace("$", "").strip())
|
||||
except Exception:
|
||||
pred, reply = np.nan, None
|
||||
results_30.append({"title": row["title"], "true_price": row["price_clean"], "pred_price": pred, "raw": reply})
|
||||
|
||||
df30 = pd.DataFrame(results_30).dropna(subset=["pred_price"])
|
||||
|
||||
mae30 = np.mean(np.abs(df30.pred_price - df30.true_price))
|
||||
|
||||
rmse30 = np.sqrt(np.mean((df30.pred_price - df30.true_price)**2))
|
||||
|
||||
pct20_30 = np.mean(np.abs(df30.pred_price - df30.true_price) <= 20) * 100
|
||||
|
||||
print(f"MAE={mae30:.2f}, RMSE={rmse30:.2f}, %within$20={pct20_30:.1f}%")
|
||||
df30.head()
|
||||
|
||||
#50 Shot s
|
||||
subset_50 = df.dropna(subset=["price_clean"]).sample(50, random_state=42).reset_index(drop=True)
|
||||
few_shots_8 = subset_50.sample(8, random_state=42)[["title", "price_clean"]].values.tolist()
|
||||
results_50 = []
|
||||
|
||||
for i, row in tqdm(subset_50.iterrows(), total=len(subset_50)):
|
||||
prompt = build_pricing_prompt(few_shots_8, row["title"])
|
||||
try:
|
||||
resp = client.messages.create(
|
||||
model=CLAUDE_MODEL,
|
||||
max_tokens=MAX_TOKENS,
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
)
|
||||
reply = resp.content[0].text.strip()
|
||||
pred = float(reply.replace("$", "").strip())
|
||||
except Exception:
|
||||
pred, reply = np.nan, None
|
||||
results_50.append({"title": row["title"], "true_price": row["price_clean"], "pred_price": pred, "raw": reply})
|
||||
|
||||
df50 = pd.DataFrame(results_50).dropna(subset=["pred_price"])
|
||||
|
||||
mae50 = np.mean(np.abs(df50.pred_price - df50.true_price))
|
||||
|
||||
rmse50 = np.sqrt(np.mean((df50.pred_price - df50.true_price)**2))
|
||||
|
||||
pct20_50 = np.mean(np.abs(df50.pred_price - df50.true_price) <= 20) * 100
|
||||
|
||||
print(f"MAE={mae50:.2f}, RMSE={rmse50:.2f}, %within$20={pct20_50:.1f}%")
|
||||
df50.head()
|
||||
|
||||
#Improved Ptompt and comparin the 10,30, &50 shot hints
|
||||
def build_strict_prompt(few_shots, test_title):
|
||||
shots_text = "\n".join([f"Title: {t}\nPrice: ${p:.2f}" for t, p in few_shots])
|
||||
return f"""
|
||||
You are an expert e-commerce product pricing analyst. Your job is to predict the most realistic market price for a product based purely on its title.
|
||||
|
||||
Here are reference examples:
|
||||
{shots_text}
|
||||
|
||||
Now predict the price for:
|
||||
Title: {test_title}
|
||||
|
||||
RULES:
|
||||
- Return ONLY a single number.
|
||||
- No dollar sign.
|
||||
- No text, no reasoning, no words.
|
||||
- Format: 123.45
|
||||
"""
|
||||
|
||||
def run_eval(name, subset, shot_count):
|
||||
few = subset.sample(shot_count, random_state=42)[["title", "price_clean"]].values.tolist()
|
||||
results = []
|
||||
|
||||
for _, row in tqdm(subset.iterrows(), total=len(subset), desc=f"{name}"):
|
||||
prompt = build_strict_prompt(few, row["title"])
|
||||
try:
|
||||
resp = client.messages.create(
|
||||
model=CLAUDE_MODEL,
|
||||
max_tokens=MAX_TOKENS,
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
)
|
||||
reply = resp.content[0].text.strip()
|
||||
pred = float(reply)
|
||||
except Exception:
|
||||
pred, reply = np.nan, None
|
||||
|
||||
results.append({"title": row["title"], "true": row["price_clean"], "pred": pred})
|
||||
|
||||
df = pd.DataFrame(results).dropna(subset=["pred"])
|
||||
mae = np.mean(np.abs(df.pred - df.true))
|
||||
rmse = np.sqrt(np.mean((df.pred - df.true)**2))
|
||||
pct20 = np.mean(np.abs(df.pred - df.true) <= 20) * 100
|
||||
return df, mae, rmse, pct20
|
||||
|
||||
# Run 10 / 30 / 50
|
||||
subset10 = df.dropna(subset=["price_clean"]).sample(10, random_state=1).reset_index(drop=True)
|
||||
subset30 = df.dropna(subset=["price_clean"]).sample(30, random_state=2).reset_index(drop=True)
|
||||
subset50 = df.dropna(subset=["price_clean"]).sample(50, random_state=3).reset_index(drop=True)
|
||||
|
||||
df10, mae10, rmse10, pct10 = run_eval("RUN10", subset10, 3)
|
||||
df30, mae30, rmse30, pct30 = run_eval("RUN30", subset30, 6)
|
||||
df50, mae50, rmse50, pct50 = run_eval("RUN50", subset50, 8)
|
||||
|
||||
#compare
|
||||
comparison = pd.DataFrame([
|
||||
{"shots": 10, "MAE": mae10, "RMSE": rmse10, "%≤$20": pct10},
|
||||
{"shots": 30, "MAE": mae30, "RMSE": rmse30, "%≤$20": pct30},
|
||||
{"shots": 50, "MAE": mae50, "RMSE": rmse50, "%≤$20": pct50},
|
||||
])
|
||||
|
||||
print(comparison)
|
||||
comparison
|
||||
|
||||
"""The model becomes confused by too many examples, became more biased toward random values and less less stable and less accurate.
|
||||
Hypothesis: Possibly the dataset has high variance (many unrelated categories), and the model benefits from small, clean, representative few-shots, not large few-shots.
|
||||
"""
|
||||
|
||||
#Rechecking the variance in the data
|
||||
prices = df["price_clean"].dropna()
|
||||
print(prices.describe(percentiles=[0.25, 0.5, 0.75, 0.9, 0.95]))
|
||||
|
||||
print("\nSkewness:", prices.skew())
|
||||
print("Kurtosis:", prices.kurt())
|
||||
|
||||
# Plot histogram
|
||||
plt.figure(figsize=(12,4))
|
||||
sns.histplot(prices, bins=50)
|
||||
plt.title("Histogram — Full Dataset Price Distribution")
|
||||
plt.xlabel("Price ($)")
|
||||
plt.ylabel("Frequency")
|
||||
plt.show()
|
||||
|
||||
# Plot boxplot
|
||||
plt.figure(figsize=(10,2))
|
||||
sns.boxplot(x=prices)
|
||||
plt.title("Boxplot — Full Dataset Price Spread")
|
||||
plt.show()
|
||||
|
||||
"""Testing fewer shots to check fr the optimal"""
|
||||
|
||||
def run_few_shot_test(df_subset, shots, model=CLAUDE_MODEL):
|
||||
few_shots = df_subset.sample(shots, random_state=42)[["title", "price_clean"]].values.tolist()
|
||||
results = []
|
||||
|
||||
for _, row in df_subset.iterrows():
|
||||
prompt = build_pricing_prompt(few_shots, row["title"])
|
||||
try:
|
||||
resp = client.messages.create(
|
||||
model=model,
|
||||
max_tokens=MAX_TOKENS,
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
)
|
||||
reply = resp.content[0].text.strip()
|
||||
pred = float(reply.replace("$", "").strip())
|
||||
except:
|
||||
pred, reply = np.nan, None
|
||||
|
||||
results.append({"title": row["title"], "true": row["price_clean"], "pred": pred})
|
||||
|
||||
df_res = pd.DataFrame(results).dropna()
|
||||
mae = np.mean(np.abs(df_res.pred - df_res.true))
|
||||
rmse = np.sqrt(np.mean((df_res.pred - df_res.true)**2))
|
||||
pct20 = np.mean(np.abs(df_res.pred - df_res.true) <= 20) * 100
|
||||
return df_res, mae, rmse, pct20
|
||||
|
||||
#Tabulate the 2 shot results
|
||||
df2, mae2, rmse2, pct2 = run_few_shot_test(subset_50, shots=2)
|
||||
print("2-SHOT RESULTS → MAE={:.2f}, RMSE={:.2f}, %≤$20={:.1f}%".format(mae2, rmse2, pct2))
|
||||
df2.head()
|
||||
|
||||
#5 shot results
|
||||
df5, mae5, rmse5, pct5 = run_few_shot_test(subset_50, shots=5)
|
||||
print("5-SHOT RESULTS → MAE={:.2f}, RMSE={:.2f}, %≤$20={:.1f}%".format(mae5, rmse5, pct5))
|
||||
df5.head()
|
||||
|
||||
#7 shot results
|
||||
df7, mae7, rmse7, pct7 = run_few_shot_test(subset_50, shots=7)
|
||||
print("7-SHOT RESULTS → MAE={:.2f}, RMSE={:.2f}, %≤$20={:.1f}%".format(mae7, rmse7, pct7))
|
||||
df7.head()
|
||||
|
||||
#Tabulate all the shots to choose the optimal or if there is Any need for the shots
|
||||
|
||||
results_summary = [
|
||||
{"shots": 0, "MAE": 22.52, "RMSE": 44.11, "%≤$20": 72.0}, # baseline
|
||||
{"shots": 2, "MAE": mae2, "RMSE": rmse2, "%≤$20": pct2},
|
||||
{"shots": 5, "MAE": mae5, "RMSE": rmse5, "%≤$20": pct5},
|
||||
{"shots": 7, "MAE": mae7, "RMSE": rmse7, "%≤$20": pct7},
|
||||
{"shots": 10, "MAE": 16.27, "RMSE": 38.59, "%≤$20": 90.0},
|
||||
{"shots": 30, "MAE": 135.73, "RMSE": 606.78, "%≤$20": 70.0},
|
||||
{"shots": 50, "MAE": 42.54, "RMSE": 136.61, "%≤$20": 72.0},
|
||||
]
|
||||
|
||||
df_comparison = pd.DataFrame(results_summary)
|
||||
df_comparison = df_comparison.sort_values("shots").reset_index(drop=True)
|
||||
df_comparison
|
||||
|
||||
"""1. 0-shot baseline: MAE 22.52, %≤$20 72%
|
||||
|
||||
2. Very low few-shots (2, 5): Surprisingly worse than baseline (MAE ↑, %≤$20 ↓), likely due to variance and poor example selection.
|
||||
|
||||
3. 7-shot: Improves over baseline slightly, MAE 19.91, %≤$20 back to 72%
|
||||
|
||||
4. 10-shot: Best performance overall — MAE 16.27, %≤$20 jumps to 90%! Clearly the few-shot hints are helping here.
|
||||
|
||||
5. 30-shot: Performance collapses (MAE 135.73, RMSE 606.78) — too many examples may confuse the model.
|
||||
|
||||
6. 50-shot: Slightly better than 30-shot but still worse than 10-shot.
|
||||
|
||||
|
||||
Conclusion: Optimal few-shot count is 10 for this dataset and prompt style.
|
||||
"""
|
||||
|
||||
#Further finetuning of the selected 10-shot
|
||||
|
||||
def build_finetune_prompt(few_shots: list, target_title: str, max_chars=800):
|
||||
"""
|
||||
few_shots: list of dicts {"title":..., "price_clean":...}
|
||||
target_title: title string
|
||||
"""
|
||||
parts = ["You are an e-commerce pricing expert. Estimate product prices in USD accurately. Output only a number."]
|
||||
parts.append("\nExamples:")
|
||||
for ex in few_shots:
|
||||
parts.append(f"- {ex['title']}: {ex['price_clean']}")
|
||||
parts.append("\nPredict price for the following product:")
|
||||
parts.append(f"Title: {target_title}")
|
||||
prompt = "\n".join(parts)
|
||||
if len(prompt) > max_chars:
|
||||
return prompt[:max_chars] + "..."
|
||||
return prompt
|
||||
|
||||
# Sample 10-shot prompts for fine-tuning
|
||||
finetune_examples = []
|
||||
subset_10 = df.dropna(subset=["price_clean"]).sample(100, random_state=42).reset_index(drop=True) # 100 products for initial fine-tuning
|
||||
|
||||
for idx, row in subset_10.iterrows():
|
||||
# Pick 10 random examples from subset for few-shot
|
||||
few_shots = subset_10.drop(idx).sample(10, random_state=idx)[["title","price_clean"]].to_dict(orient="records")
|
||||
prompt = build_finetune_prompt(few_shots, row["title"])
|
||||
finetune_examples.append({
|
||||
"prompt": prompt,
|
||||
"completion": str(row["price_clean"])
|
||||
})
|
||||
|
||||
print("Sample fine-tuning example:")
|
||||
print(finetune_examples[0])
|
||||
|
||||
with open("finetune_10shot.jsonl", "w") as f:
|
||||
for ex in finetune_examples:
|
||||
f.write(json.dumps(ex) + "\n")
|
||||
print("(10-shot format).finetuned")
|
||||
|
||||
# Evaluate enhanced 10-shot prompt on sample
|
||||
results_finetune_test = []
|
||||
|
||||
for idx, row in subset_10.iterrows():
|
||||
few_shots = subset_10.drop(idx).sample(10, random_state=idx)[["title","price_clean"]].to_dict(orient="records")
|
||||
prompt = build_finetune_prompt(few_shots, row["title"])
|
||||
try:
|
||||
resp = client.messages.create(
|
||||
model=CLAUDE_MODEL,
|
||||
max_tokens=MAX_TOKENS,
|
||||
messages=[{"role": "user", "content": prompt}]
|
||||
)
|
||||
reply = resp.content[0].text.strip()
|
||||
pred = float(reply.replace("$","").strip())
|
||||
except Exception:
|
||||
pred, reply = np.nan, None
|
||||
results_finetune_test.append({"title": row["title"], "true_price": row["price_clean"], "pred": pred, "raw": reply})
|
||||
|
||||
df_finetune_test = pd.DataFrame(results_finetune_test).dropna(subset=["pred"])
|
||||
mae_ft = np.mean(np.abs(df_finetune_test.pred - df_finetune_test.true_price))
|
||||
rmse_ft = np.sqrt(np.mean((df_finetune_test.pred - df_finetune_test.true_price)**2))
|
||||
pct20_ft = np.mean(np.abs(df_finetune_test.pred - df_finetune_test.true_price) <= 20) * 100
|
||||
|
||||
print(f"Finetuned 10-shot performance: MAE={mae_ft:.2f}, RMSE={rmse_ft:.2f}, %≤$20={pct20_ft:.1f}%")
|
||||
|
||||
"""Multi-shot prompting (10 examples in the prompt) without fine-tuning performed much better.
|
||||
|
||||
|
||||
Next trial: Prompt optimization
|
||||
"""
|
||||
|
||||
#prompt optimization seems like th eonly choice
|
||||
def build_pricing_prompt_alt(few_shots: list, target_title: str) -> str:
|
||||
"""
|
||||
Build an alternative multi-shot pricing prompt for Claude.
|
||||
|
||||
few_shots: list of dicts with keys 'title' and 'price_clean'
|
||||
target_title: product title to predict the price for
|
||||
"""
|
||||
parts = []
|
||||
|
||||
# Instruction with a slightly different phrasing
|
||||
parts.append("Act as an expert e-commerce pricing analyst.")
|
||||
parts.append("Given product titles and their prices, predict the price in USD for the new product.")
|
||||
parts.append("Only provide the numeric price. No extra text, explanations, or symbols.")
|
||||
|
||||
# Format the examples differently: numbered list
|
||||
parts.append("\nExample prices:")
|
||||
for i, ex in enumerate(few_shots, start=1):
|
||||
parts.append(f"{i}. {ex['title']} — ${ex['price_clean']:.2f}")
|
||||
|
||||
# Target product
|
||||
parts.append("\nPredict the price for this product:")
|
||||
parts.append(f"Title: {target_title}")
|
||||
parts.append("Price (USD):")
|
||||
|
||||
# Combine into single prompt
|
||||
prompt = "\n".join(parts)
|
||||
return prompt
|
||||
|
||||
"""eda"""
|
||||
Reference in New Issue
Block a user