Project: Car Price Analysis
A mini-project inspired by notebooks 04/05: extract the brand from CarName, fix brand typos, compare average price by brand and fuel, build price bands with pd.cut, and measure correlation with engine size and horsepower.
Notebooks 04 and 05 explore a used-car dataset: the first big step is pulling the brand out of the car name with df['name'].apply(lambda x: x.split(' ')[0]), then comparing prices brand by brand with groupby. Here we do the same on a 15-car sample in the style of the popular CarPrice dataset (columns CarName, fueltype, enginesize, horsepower, price), with price in ₹ lakh.
text
CarName fueltype enginesize horsepower price
0 maruti swift petrol 1197 89 6.5
1 maruti baleno petrol 1197 89 7.2
2 Maruti ciaz petrol 1462 103 9.5
3 hyundai i20 petrol 1197 82 7.8
4 hyundai creta diesel 1493 114 13.5
5 hyundai verna diesel 1493 113 12.0
6 honda city petrol 1498 119 12.5
7 honda amaze diesel 1498 99 8.9
8 toyota innova diesel 2393 148 19.5
9 toyouta fortuner diesel 2755 201 33.0
10 toyota glanza petrol 1197 89 7.0
11 mahindra xuv700 diesel 2184 182 21.0
12 mahindra thar diesel 2184 130 15.5
13 vw polo petrol 999 109 7.5
14 volkswagen virtus petrol 1498 148 14.5Spot the traps already: Maruti vs maruti, the typo toyouta, and vw vs volkswagen — exactly the kind of mess the real CarPrice file contains.
Definition
This mini-project answers pricing questions about cars: which brands are expensive, how prices split into bands, and which numeric features (
enginesize,horsepower) move together with price. Key ideas: feature extraction (making a new column — brand — out of an existing one), binning (turning a number into ordered categories withpd.cut), and correlation (a number from −1 to +1 measuring how strongly two columns rise or fall together).
Questions:
- What is the average price per brand?
- How many cars fall in Budget / Mid-range / Premium bands — and which brands sit where?
- How strongly is price related to engine size and horsepower?
- Are diesel cars pricier than petrol ones?
Syntax — tools used in this project
python
s.str.split(" ").str[0] # first word (vectorised)
s.apply(lambda x: x.split(" ")[0]) # same, the notebook way
s.replace({"old": "new"}) # fix typos
df.groupby("brand")["price"].agg(["count", "mean", "min", "max"])
pd.cut(s, bins=[...], labels=[...]) # numbers → ordered bands
pd.crosstab(df["brand"], df["band"])
df[cols].corr() # correlation matrix
df[cols].corrwith(df["price"]) # each column vs one target| Function / parameter | Meaning | Default |
|---|---|---|
pd.cut(x, bins) | bins = list of edges; each interval is (left, right] | required |
labels | names for the bins (one fewer than the edges) | interval labels |
right | include the right edge in each bin | True |
include_lowest | include the very first left edge | False |
df.corr(method=) | "pearson" (linear), "spearman" (rank), "kendall" | "pearson" |
df.corr(numeric_only=) | ignore non-numeric columns | False |
Step 1 — extract and clean the brand
python
cars["brand"] = cars["CarName"].str.split(" ").str[0].str.lower()
print(cars["brand"].unique())
cars["brand"] = cars["brand"].replace({"toyouta": "toyota", "vw": "volkswagen"})
print(cars["brand"].value_counts())text
['maruti' 'hyundai' 'honda' 'toyota' 'toyouta' 'mahindra' 'vw'
'volkswagen']
brand
maruti 3
hyundai 3
toyota 3
honda 2
mahindra 2
volkswagen 2
Name: count, dtype: int64How it works: .str.split(" ") turns each name into a list of words, .str[0] takes the first word, and .str.lower() merges Maruti/maruti. Then unique() exposes the typos, and replace fixes them — 8 raw "brands" become the true 6. (The notebook's apply(lambda x: x.split(' ')[0]) gives the same first words; the .str version is vectorised.)
Note (pandas 3): the
split(...).str[0]route produces anobjectcolumn (it passes through Python lists). Add.astype("str")if you want the dedicatedstrdtype.
Step 2 — average price by brand
python
print(cars.groupby("brand")["price"].agg(["count", "mean", "min", "max"])
.sort_values("mean", ascending=False).round(2))text
count mean min max
brand
toyota 3 19.83 7.0 33.0
mahindra 2 18.25 15.5 21.0
hyundai 3 11.10 7.8 13.5
volkswagen 2 11.00 7.5 14.5
honda 2 10.70 8.9 12.5
maruti 3 7.73 6.5 9.5How it works: Toyota has the highest mean (19.83 lakh) — but its range is 7.0 to 33.0. One expensive Fortuner pulls the average up. Showing count, min and max next to the mean stops you from over-reading averages built on 2–3 cars.
Step 3 — price bands with pd.cut
python
cars["band"] = pd.cut(cars["price"], bins=[0, 8, 15, 50],
labels=["Budget", "Mid-range", "Premium"])
print(cars["band"].value_counts())
print(pd.crosstab(cars["brand"], cars["band"]))text
band
Mid-range 6
Budget 5
Premium 4
Name: count, dtype: int64
band Budget Mid-range Premium
brand
honda 0 2 0
hyundai 1 2 0
mahindra 0 0 2
maruti 2 1 0
toyota 1 0 2
volkswagen 1 1 0How it works: the edges [0, 8, 15, 50] make three intervals (0, 8], (8, 15], (15, 50] — the right edge is included, so a car at exactly 8.0 lakh is Budget. The result is an ordered category column (Budget < Mid-range < Premium). The crosstab shows Mahindra is all Premium and Maruti mostly Budget.
Step 4 — correlation with engine size and horsepower
python
print(cars[["price", "enginesize", "horsepower"]].corr().round(2))
print(cars.corr(numeric_only=True)["price"].sort_values(ascending=False).round(2))text
price enginesize horsepower
price 1.00 0.93 0.95
enginesize 0.93 1.00 0.87
horsepower 0.95 0.87 1.00
price 1.00
horsepower 0.95
enginesize 0.93
Name: price, dtype: float64How it works: corr() computes the Pearson correlation for every pair of numeric columns. The diagonal is always 1 (a column with itself). Price vs horsepower = 0.95 — a very strong positive relationship: more powerful cars cost more. numeric_only=True skips text columns like CarName (without it, pandas 3 raises an error on text).
| Value of r | Meaning |
|---|---|
| +0.7 to +1 | strong positive — both rise together |
| +0.3 to +0.7 | moderate positive |
| −0.3 to +0.3 | weak / no linear relationship |
| −1 to −0.3 | negative — one rises as the other falls |
Note: correlation is not causation, and 15 cars is a tiny sample. Horsepower and engine size are also correlated with each other (0.87) — big engines tend to be powerful.
Step 5 — fuel type comparison
python
print(cars.groupby("fueltype")["price"].mean().round(2))
print(cars.pivot_table(index="brand", columns="fueltype", values="price", aggfunc="mean").round(1))text
fueltype
diesel 17.63
petrol 9.06
Name: price, dtype: float64
fueltype diesel petrol
brand
honda 8.9 12.5
hyundai 12.8 7.8
mahindra 18.2 NaN
maruti NaN 7.7
toyota 26.2 7.0
volkswagen NaN 11.0How it works: diesel cars average almost double the price — but the pivot table shows why: the diesel group contains the big SUVs (Fortuner, XUV700). NaN means that brand has no car of that fuel type in the sample. Honda is the exception, where the petrol City costs more than the diesel Amaze.
Findings
| Question | Answer (15-car sample) |
|---|---|
| Most expensive brand on average | Toyota (19.83 lakh) — pulled up by one Fortuner |
| Cheapest brand | Maruti (7.73 lakh) |
| Price bands | 5 Budget · 6 Mid-range · 4 Premium |
| Strongest price driver | horsepower (r = 0.95), then engine size (0.93) |
| Fuel | diesel avg 17.63 vs petrol 9.06 — mostly because SUVs are diesel |
Common mistakes
- ❌ Grouping by brand before fixing case and typos. ✅
lower()+unique()+replace()first; otherwise "toyouta" becomes its own brand. - ❌
pd.cutwithlabelsthe same length asbins. ✅ Labels = number of edges − 1. - ❌ Forgetting that bins exclude the left edge — a value equal to the first edge becomes NaN. ✅ Start below the minimum (here 0) or use
include_lowest=True. - ❌
df.corr()on a frame with text columns. ✅df.corr(numeric_only=True)or select numeric columns. - ❌ Reporting means of 2–3 cars as facts. ✅ Show
count,min,maxnext to the mean.
Interview tip
Q: What is the difference between pd.cut and pd.qcut?
A: pd.cut uses fixed edges you choose (or equal-width bins), so groups can have very different sizes. pd.qcut uses quantiles, so each bin gets roughly the same number of rows — e.g. pd.qcut(price, 3, labels=["Low", "Mid", "High"]) gives three equal-sized groups.
Key points to remember
- Feature extraction: brand = CarName.str.split(' ').str[0] (or apply with a lambda, as in the notebook).
- Normalise case and fix typos (lower, unique, replace) BEFORE grouping, or one brand splits into several.
- groupby('brand')['price'].agg(['count','mean','min','max']) puts every average in context.
- pd.cut(price, bins=[0, 8, 15, 50], labels=[...]) creates ordered price bands; bins are (left, right].
- pd.crosstab(brand, band) shows how brands spread across bands.
- df.corr(numeric_only=True) gives Pearson correlations from −1 to +1; price vs horsepower was 0.95 here.
- Correlation is not causation, and small groups make averages unreliable.
Try it yourself
Extract and clean the brand
Average price by brand and price bands
Correlation and fuel type
Practice: value for money
Revision Flashcards
Tap a card to flip it and see the answer.
