Lesson 5 · Pandas Projects
Real Estate Data Analysis with Pandas: Clean, Explore, Visualise
A complete, standalone tutorial: build a messy synthetic listings dataset, clean it properly, then analyze pricing with pandas. No prior pandas experience…
- CoursePandas Projects
- Lesson5 of 10
- Video18 min
- FormatJupyter notebook · 15 code cells
What you'll learn
Data
No separate download needed — the notebook creates or downloads everything it uses.
📓 Full notebook
Download .ipynbPandas for Real Estate: Cleaning and Analyzing Housing Listings#
- A complete, standalone tutorial: build a messy synthetic listings dataset, clean it properly, then analyze pricing with pandas.
- No prior pandas experience needed. Let's jump straight in.
Before You Start#
- Open a new Jupyter Notebook in VS Code and select your Python interpreter as the kernel.
- If pandas isn't installed yet, open a terminal in VS Code and run: pip install pandas
Part 1: Building the Dataset#
import pandas as pd
import numpy as np
print(pd.__version__)
Generating Synthetic Listings#
rng = np.random.default_rng(seed=66)
n_listings = 250
neighborhoods = ['Riverside', 'Hillcrest', 'Oakwood', 'Lakeview', 'Downtown']
bedrooms = rng.integers(1, 6, size=n_listings)
bathrooms = np.clip(bedrooms - rng.integers(0, 2, size=n_listings), 1, None)
sqft = (bedrooms * rng.uniform(380, 520, size=n_listings) + rng.normal(0, 150, size=n_listings)).round(0)
year_built = rng.integers(1960, 2025, size=n_listings)
days_on_market = rng.integers(3, 180, size=n_listings)
print(sqft[:5])
base_price = 220 * sqft + (2024 - year_built) * -80 + rng.normal(0, 15000, size=n_listings)
listing_price = np.clip(base_price, 80000, None).round(-2)
listings = pd.DataFrame({
'listing_id': range(5001, 5001 + n_listings),
'neighborhood': rng.choice(neighborhoods, size=n_listings),
'bedrooms': bedrooms,
'bathrooms': bathrooms,
'sqft': sqft,
'year_built': year_built,
'listing_price': listing_price,
'days_on_market': days_on_market
})
print(listings.shape)
Injecting Missing Values and Outliers#
missing_idx = rng.choice(listings.index, size=15, replace=False)
listings.loc[missing_idx, 'sqft'] = np.nan
outlier_idx = rng.choice(listings.index, size=5, replace=False)
listings.loc[outlier_idx, 'listing_price'] = listings.loc[outlier_idx, 'listing_price'] * 10
listings.to_csv('housing_listings.csv', index=False)
listings = pd.read_csv('housing_listings.csv')
listings.head()
Part 2: First Look at the Data#
print(listings.shape)
listings.isna().sum()
listings['listing_price'].describe()
Part 3: Cleaning the Data#
neighborhood_median_sqft = listings.groupby('neighborhood')['sqft'].transform('median')
listings['sqft'] = listings['sqft'].fillna(neighborhood_median_sqft)
listings['sqft'].isna().sum()
Detecting and Capping Price Outliers#
q1 = listings['listing_price'].quantile(0.25)
q3 = listings['listing_price'].quantile(0.75)
iqr = q3 - q1
upper_bound = q3 + 1.5 * iqr
outliers = listings[listings['listing_price'] > upper_bound]
print(len(outliers))
outliers[['listing_id', 'neighborhood', 'sqft', 'listing_price']]
listings['listing_price'] = listings['listing_price'].clip(upper=upper_bound)
listings.loc[outlier_idx, 'listing_price']
Part 4: Price Per Square Foot#
listings['price_per_sqft'] = (listings['listing_price'] / listings['sqft']).round(2)
listings[['neighborhood', 'sqft', 'listing_price', 'price_per_sqft']].head()
Part 5: Correlation Analysis#
numeric_cols = ['bedrooms', 'bathrooms', 'sqft', 'year_built', 'listing_price', 'days_on_market']
correlations = listings[numeric_cols].corr()
correlations['listing_price'].sort_values(ascending=False)
Part 6: Price Tiers and Neighborhood Summary#
listings['price_tier'] = pd.qcut(listings['listing_price'], q=4, labels=['Budget', 'Mid-Range', 'Upper-Mid', 'Premium'])
listings['price_tier'].value_counts().sort_index()
neighborhood_summary = listings.groupby('neighborhood').agg(
listings_count=('listing_id', 'count'),
avg_price=('listing_price', 'mean'),
avg_price_per_sqft=('price_per_sqft', 'mean'),
avg_days_on_market=('days_on_market', 'mean')
).round(1).sort_values('avg_price', ascending=False)
neighborhood_summary
Part 7: Visualizing the Results#
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(8, 5))
ax.scatter(listings['sqft'], listings['listing_price'], alpha=0.5, color='teal')
ax.set_title('Listing Price vs. Square Footage')
ax.set_xlabel('Square Feet')
ax.set_ylabel('Listing Price ($)')
plt.tight_layout()
plt.savefig('price_vs_sqft.png', dpi=150)
plt.close(fig)
print('Saved price_vs_sqft.png')
fig, ax = plt.subplots(figsize=(8, 5))
neighborhood_summary['avg_price'].plot(kind='bar', ax=ax, color='steelblue')
ax.set_title('Average Listing Price by Neighborhood')
ax.set_ylabel('Average Price ($)')
plt.tight_layout()
plt.savefig('avg_price_by_neighborhood.png', dpi=150)
plt.close(fig)
print('Saved avg_price_by_neighborhood.png')
Wrap-Up: What You Learned#
- Generating a realistic, deliberately messy synthetic listings dataset, then saving and reloading with to_csv and read_csv.
- Detecting missing values with isna, and filling them intelligently with groupby plus transform.
- Detecting price outliers with the IQR method, and capping them with clip instead of deleting rows.
- Deriving a normalized metric, price per square foot, and running a full correlation analysis with corr.
- Bucketing with qcut for balanced quartile tiers, and a multi-statistic neighborhood summary.
- A scatter plot and a bar chart, both presentation-ready, with matplotlib.
- You went from a messy synthetic listings feed to a fully cleaned, analyzed housing report. If you want the next dataset in this series to land in your feed automatically, subscribing is the move see you in the next one.
Found this useful?
All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.



