---
title: "Data Analysis and Visualization with Bright Data SDK"
slug: data-analysis-and-visualization-with-bright-data-sdk
date: 2026-10-22T07:16:38+00:00
modified: 2026-10-22T07:16:42+00:00
permalink: https://brightdata.com/blog/web-data/data-analysis-and-visualization-with-bright-data-sdk
type: blog
---

[ Blog ](https://brightdata.com/blog "Blog") / [Web Data](https://brightdata.com/blog/web-data)







 [Web Data](https://brightdata.com/blog/web-data)

# Data Analysis and Visualization with Bright Data SDK

Learn how to fetch Zillow sales data with the Bright Data Python SDK and analyze San Diego home prices, ZIP codes, and rental yields in pandas.

 9 min read





 [ ](https://brightdata.com/blog/authors/antonello-zanini)

 [Antonello Zanini

Technical Writer

 ](https://brightdata.com/blog/authors/antonello-zanini)





 ![Data Analysis and Visualization with Bright Data SDK](https://media.brightdata.com/2026/10/data-analysis-visualization-bright-data-sdk.png)





In this tutorial, you will see:

- The data analysis scenario for exploring the San Diego housing market.
- Why Bright Data’s Python SDK simplifies access to structured, ready-to-use datasets.
- How to filter, retrieve, and export Zillow property sales data.
- How to clean the data, standardize units, and calculate useful metrics.
- How to visualize sale prices, geographic trends, Zestimate accuracy, and estimated rental yields.

Let’s dive in!

## Presenting the Scenario

Imagine you are a data analyst tasked with understanding the San Diego housing market. You want to answer key questions, such as: *What prices do homes actually sell for? Which neighborhoods have the highest prices per square meter? And how accurately do Zillow’s estimates predict final sale prices?*

To do so, you need a recent, reliable, rich dataset of sold homes. For similar tasks, Bright Data provides access to a [Zillow property information dataset](/products/datasets/zillow) containing more than 137 million records.

In the following steps, you will learn how to use the [Bright Data Python SDK](https://docs.brightdata.com/api-reference/SDK) to fetch a filtered snapshot of that dataset containing only the records relevant to the scenario. You will then load the data into pandas and build a [Jupyter Notebook](https://jupyter.org/) with the charts and analyses needed to answer your questions.

Along the way, you will also see how to clean the data, standardize measurement units, and calculate derived metrics to ensure fair and meaningful comparisons across neighborhoods.

## Why Use the Bright Data Python SDK?

Bright Data is a leading [dataset website](/blog/web-data/best-dataset-websites), particularly as a [real estate dataset provider](/blog/web-data/best-real-estate-data-providers). It offers an extensive collection of structured and frequently updated datasets containing billions of records of publicly available web data.

When working on a [data analysis and/or visualization project](/blog/web-data/analysis-with-python), the first step is to obtain the data you need. There are two main approaches:

- **Collect data on demand**: Build and run web scraping workflows to extract the information you need directly from websites.
- **Use existing datasets**: Get filtered, pre-collected datasets that are ready for analysis.

The Bright Data Python SDK supports both approaches. It provides a simplified, typed Python interface to access [Bright Data’s web data collection solutions](/products) and offers convenient methods to gather structured datasets directly from the [Bright Data marketplace](/products/datasets).

For more information, refer to the following resources:

- [*Python SDK documentation*](https://docs.brightdata.com/api-reference/SDK)
- [*Python SDK repository*](https://github.com/brightdata/sdk-python)
- [*Meet the Bright Data Python SDK: Scrape, Search, and Get Web Data*](/blog/ai/bright-data-python-sdk)

## Retrieve the Data for Analysis and Visualization

Follow the steps below to get started with your data analysis and visualization project, beginning with data retrieval.

### Prerequisites

Make sure you have:

- [Python 3.10+](https://www.python.org/downloads/) installed on your local machine.
- A [Bright Data account](/cp/start) with an API key configured. See [how to get your Bright Data API key](https://docs.brightdata.com/api-reference/authentication#how-do-i-generate-a-new-api-key).

Run the following command to install the Python packages used throughout this guide:

```none
pip install brightdata-sdk jupyter pandas numpy matplotlib seaborn plotly
```

These libraries provide the tools needed to access Bright Data datasets, manipulate and analyze data, and handle visualizations in a Jupyter Notebook.

To authenticate the Bright Data Python SDK, set your API key in the `BRIGHTDATA_API_KEY` environment variable. On macOS or Linux, run:

```none
export BRIGHTDATA_API_KEY="<YOUR_BRIGHT_DATA_API_KEY>"
```

On Windows, execute the following command in PowerShell:

```none
$env:BRIGHTDATA_API_KEY="<YOUR_BRIGHT_DATA_API_KEY>"
```

Replace `<YOUR_BRIGHT_DATA_API_KEY>` with your actual Bright Data API key.

For simplicity, we will assume you already have a local Python project with a [Jupyter Notebook set up](https://jupyter.org/install).

### Get the Target Data

The data of interest comes from the [Zillow properties listing information dataset](/cp/datasets/browse/gd_lfqkr8wm13ixtbd8f5) on Bright Data.

In this scenario, assume you are interested only in properties sold in San Diego within the past year.

In your Python project, create a script that uses the Bright Data SDK to request a dataset snapshot with your chosen filters, wait for the snapshot to become available, and export the resulting records:

```none
import asyncio
from datetime import date, timedelta
from brightdata import BrightDataClient
from brightdata.datasets import export

async def main():
    # Get the current date
    today = date.today()
    # Compute the start of the sales window of interest (last year)
    one_year_ago = today - timedelta(days=365)

    # Define the filters that every returned home must match
    filters = {
        "operator": "and",
        "filters": [
            {
                # Keep only homes in San Diego
                "name": "city",
                "operator": "=",
                "value": "San Diego",
            },
            {
                # Keep only homes in California
                "name": "state",
                "operator": "=",
                "value": "CA",
            },
            {
                # Keep only sold listings
                "name": "homeStatus",
                "operator": "includes",
                "value": "SOLD",
            },
            {
                # Keep only sales from the last year
                "name": "dateSold",
                "operator": ">=",
                "value": one_year_ago.isoformat(),
            },
        ],
    }

    async with BrightDataClient() as client:
        # Request up to 1,000 records from the filtered dataset
        snapshot_id = await client.datasets.zillow_properties(
            filter=filters,
            records_limit=1000,
        )

        # Wait for the snapshot to finish and download the records
        data = await client.datasets.zillow_properties.download(
            snapshot_id,
            timeout=600 # Wait up to 10 mins
        )

        # Confirm how many records were downloaded
        print(f"Downloaded {len(data)} records")

        # Save the records to a CSV file
        export(data, "properties.csv")

if __name__ == "__main__":
    asyncio.run(main())
```

The above script asynchronously requests a dataset snapshot, waits up to 10 minutes for completion, downloads the records, reports the number retrieved, and exports them to `properties.csv`, ready for analysis in pandas.

It employs the Bright Data SDK to fetch Zillow property records matching the specified filters: homes in San Diego, California, sold within the past 365 days. Behind the scenes, the SDK calls the [Bright Data Filter API](https://docs.brightdata.com/api-reference/marketplace-dataset-api/filter-dataset), which enables you to get filtered snapshots of Bright Data datasets.

Keep in mind that the record limit is set to 1,000, but you can adjust it to any number that suits your needs. You can also add new filters or modify the existing ones to refine the dataset and retrieve only the records relevant to your analysis.

**Note**: The `export()` helper function detects the output format from the file extension, so `.csv` produces a CSV file. Other supported data exports include JSON, NDJSON, and JSONL.

### Explore the Retrieved Dataset

Run the data retrieval script. A `properties.csv` file will appear in your project directory:

Open the file, and you will notice the 1,000 Zillow property records returned in structured CSV format. Each record contains approximately 140 columns. The most relevant are:

**Column****Description**`zipcode`, `latitude`, `longitude`Location of the home`homeType`Type of property, such as `SINGLE_FAMILY` or `CONDO``bedrooms`, `bathrooms`Number of bedrooms and bathrooms`price`Sold price in USD`livingAreaValue`, `livingAreaUnits`Interior size and its unit`lotAreaValue`, `lotAreaUnits`Lot size, in square feet or acres`yearBuilt`Year of construction`dateSoldString`Sale date (`YYYY-MM-DD`)`daysOnZillow`How long the listing was active`zestimate`, `rentZestimate`Zillow’s estimated value and monthly rent`taxAssessedValue`Value used for property taxesDiscover them all in the [“Dictionary” section](/cp/datasets/browse/gd_lfqkr8wm13ixtbd8f5?tab=dictionary) of the Bright Data control panel page for the Zillow properties listing dataset:

Mission complete! You now have the raw, structured data.

Next, you will learn how to clean and prepare it for analysis and visualization. Then, you will define 10+ scenarios to explore the San Diego real estate market and answer your key questions about home sale prices, neighborhood affordability, and the accuracy of Zillow estimates.

## Prepare the Data for Analysis and Visualization

Now that you have retrieved the dataset, open your notebook and prepare the dataset for data analysis. In a dedicated cell, perform these tasks:

1. Import the required libraries for data manipulation, statistical analysis, and visualization.
2. Configure data plotting settings.
3. Load the dataset by importing `properties.csv` into a pandas DataFrame.
4. Clean and standardize the data by handling missing values, correcting data types, and converting fields into consistent formats.
5. Create additional variables to support more effective analysis, comparisons, and visualizations.

Complete these steps with:

```none
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px
from matplotlib.ticker import FuncFormatter
from IPython.display import display

# Show axis prices as full numbers, for example 1,000,000
usd_fmt = FuncFormatter(lambda x, _: f"{x:,.0f}")

# Use a clean grid style for all charts
sns.set_theme(style="whitegrid")

# Conversion factor from square feet to square meters
SQFT_TO_M2 = 0.092903
# Conversion factor from acres to square feet
ACRE_TO_SQFT = 43560
# Ignore ZIP codes with fewer sales than this
MIN_SALES = 3

# Format a DataFrame's columns as text, then display it as a table
def show_table(frame, formats, title=""):
    # Work on a copy so the original data is unchanged
    out = frame.copy()
    # Apply each format to its column, showing missing values as a dash
    for col, fmt in formats.items():
        out[col] = out[col].map(lambda v, f=fmt: f.format(v) if pd.notna(v) else "-")
    # Print a heading above the table when a title is given
    if title:
        print(title)
    # Show the formatted table
    display(out)

# Load the CSV, keeping ZIP codes as text
df = pd.read_csv("properties.csv", dtype={"zipcode": str})

# List the columns that should be numbers
numeric_cols = ["price", "bedrooms", "bathrooms", "livingAreaValue", "lotAreaValue",
                "yearBuilt", "daysOnZillow", "zestimate", "rentZestimate",
                "taxAssessedValue", "latitude", "longitude"]
# Convert those columns to numbers, turning invalid values into NaN
df[numeric_cols] = df[numeric_cols].apply(pd.to_numeric, errors="coerce")

# Pad ZIP codes to 5 digits
df["zipcode"] = df["zipcode"].str.zfill(5)
# Convert the sale date text into a datetime
df["dateSold"] = pd.to_datetime(df["dateSoldString"])

# Find the rows where the living area is in square feet
is_sqft = df["livingAreaUnits"].eq("Square Feet")
# Convert square feet to m², and leave NaN for other units
df["living_area_m2"] = np.where(is_sqft, df["livingAreaValue"] * SQFT_TO_M2, np.nan)

# Convert lot sizes to square feet, whether they were in acres or already in square feet
lot_sqft = np.where(df["lotAreaUnits"].eq("Acres"),
                    df["lotAreaValue"] * ACRE_TO_SQFT,
                    df["lotAreaValue"])
# Convert the lot size to square meters
df["lot_m2"] = lot_sqft * SQFT_TO_M2

# Drop rows without a valid sold price
df = df[df["price"] > 0].copy()
# Calculate the sold price per square meter
df["price_per_m2"] = df["price"] / df["living_area_m2"]
# Calculate the gap between sold price and Zestimate as a percentage
df["zestimate_error_pct"] = ((df["price"] - df["zestimate"])
                             / df["zestimate"].where(df["zestimate"] > 0) * 100)
# Calculate annual rent as a percentage of the sold price
df["rent_yield_pct"] = df["rentZestimate"] * 12 / df["price"] * 100
# Group prices into bands, labeled with full numbers in USD
df["price_band"] = pd.cut(df["price"], bins=[0, 500_000, 1_000_000, 1_500_000, np.inf],
                          labels=["<500,000", "500,000-1,000,000",
                                  "1,000,000-1,500,000", ">1,500,000"])
# Group homes by construction era
df["age_band"] = pd.cut(df["yearBuilt"], bins=[0, 1949, 1979, 1999, np.inf],
                        labels=["<1950", "1950-1979", "1980-1999", "2000+"])

# Summarize how many homes and ZIP codes are in the cleaned dataset
print(f"{len(df)} sold homes across {df['zipcode'].nunique()} ZIP codes")

# Preview the key columns with prices and areas formatted
show_table(
    # Select the key columns for the preview
    df[["zpid", "streetAddress", "zipcode", "homeType", "bedrooms", "bathrooms",
        "price", "living_area_m2", "dateSold"]].head(),
    # Format prices as full numbers and areas with one decimal
    formats={"price": "{:,.0f}", "living_area_m2": "{:,.1f} m²"},
    title="Sample of sold homes",
)
```

Execute it, and you will get this result:

Notably, the above cell takes care of:

- **Normalizing measurement units**: Living areas and lot sizes are converted to square meters, so every size-based metric uses the same unit.
- **Handling missing values**: Zestimate errors are only computed when the Zestimate is greater than zero, which avoids division by zero.
- **Adding derived fields**: Price per m², rental yield, price bands, and age bands are added as columns to simplify data visualization.

Fantastic! You are ready to proceed with data analysis and visualization.

## Analysis and Visualization

Here, you will see 10+ practical ways to examine and visualize the dataset, uncovering patterns and insights into the San Diego housing market. Each section includes a brief overview, the cell code, the resulting output, and an interpretation of the findings.

### Distribution of Sold Prices

**Description**: Explore where most sold prices fall, with the median marked so you can see how skewed the market is.

**Cell**:

```none
plt.figure(figsize=(10, 6))

# Draw a histogram of prices with a smoothed density curve
sns.histplot(df["price"], bins=30, kde=True, color="steelblue")

# Calculate the median sold price
median = df["price"].median()

# Draw a dashed red line at the median, labeled with the full number
plt.axvline(median, color="red", linestyle="--", label=f"Median: {median:,.0f} USD")
# Show x-axis prices as full numbers
plt.gca().xaxis.set_major_formatter(usd_fmt)

plt.title("Distribution of Sold Prices in San Diego")
plt.xlabel("Sold price (USD)")
# Remove automatic passing on x axys
plt.xlim(left=0)
plt.ylabel("Number of homes")
plt.legend()
plt.show()
```

**Output**:

**Comment**: Most sales cluster between roughly 500,000 and 1,500,000 USD, peaking near 750,000 USD, while the 950,000 USD median sits higher. A long tail reaches nearly 9,500,000 USD. The distribution is right-skewed, pulling the mean above the median.

### Median Price by Home Type

**Description**: Compare the median and mean sold price across property types to see which home types drive the market.

**Cell**:

```none
# Group sales by property type and compute summary statistics
by_type = (df.groupby("homeType")
             # Count the homes, and take the median and mean price for each type
             .agg(homes=("zpid", "count"),
                  median_price=("price", "median"),
                  mean_price=("price", "mean"))
             # Sort the types from highest to lowest median price
             .sort_values("median_price", ascending=False))

show_table(by_type.reset_index(),
           formats={"median_price": "{:,.0f}", "mean_price": "{:,.0f}"},
           title="Sold price by home type (USD)")

plt.figure(figsize=(10, 6))
sns.barplot(data=by_type.reset_index(), x="homeType", y="median_price", color="steelblue")
plt.gca().yaxis.set_major_formatter(usd_fmt)
plt.xticks(rotation=30)
plt.title("Median Sold Price by Home Type")
plt.xlabel("Home type")
plt.ylabel("Median price (USD)")
plt.show()
```

**Output**:

**Comment**: Single-family homes are the most reliable benchmark, with a median of 1,249,500 USD across 526 sales. Multi-family and lot medians look higher but rest on far fewer sales (65 and 3), while manufactured homes are the cheapest at 205,750 USD, and condos sit at 622,500 USD.

### Top ZIP Codes by Median Sold Price

**Description**: Rank the 15 most expensive ZIP codes by median sold price. ZIP codes with very few sales are excluded to avoid misleading medians.

**Cell**:

```none
# Group sales by ZIP code and compute the sales count and median price
zip_stats = (df.groupby("zipcode")
               .agg(sales=("zpid", "count"), median_price=("price", "median"))
               # Drop ZIP codes with too few sales
               .query("sales >= @MIN_SALES")
               # Sort from most to least expensive
               .sort_values("median_price", ascending=False)
               # Keep only the top 15
               .head(15)
               # Turn the ZIP code index back into a column
               .reset_index())

show_table(zip_stats,
           formats={"median_price": "{:,.0f}"},
           title=f"Top 15 ZIP codes by median sold price (min. {MIN_SALES} sales)")

plt.figure(figsize=(10, 7))
sns.barplot(data=zip_stats, x="median_price", y="zipcode", color="steelblue")
plt.gca().xaxis.set_major_formatter(usd_fmt)
plt.title(f"Top 15 ZIP Codes by Median Sold Price (min. {MIN_SALES} sales)")
plt.xlabel("Median price (USD)")
plt.ylabel("ZIP code")
plt.show()
```

**Outputs**:

**Comment**: 92130, 92109, and 92106 lead, with medians of at least 1,800,000 USD. 92118 (Coronado) has the highest median, above 3,300,000 USD, but too few sales to rank. 92106 rests on only 17 sales, so its position may shift.

### Map of Where Homes Sold

**Description**: Plot every sale on a map, colored by sold price, to see where the most expensive homes are located.

**Cell**:

```none
# Copy the original data
map_df = df.copy()

for col in ["latitude", "longitude", "price"]:
    map_df[col] = pd.to_numeric(map_df[col], errors="coerce")

# Keep rows with valid coordinates and sale prices
map_df = map_df.dropna(subset=["latitude", "longitude", "price"])

fig = px.scatter_map(
    map_df,
    lat="latitude",
    lon="longitude",
    color="price",
    hover_name="streetAddress",
    hover_data={
        "zipcode": True,
        "homeType": True,
        "bedrooms": True,
        "price": ":,.0f",
        "latitude": False,
        "longitude": False,
    },
    labels={
        "price": "Sold price (USD)",
    },
    color_continuous_scale="Viridis",

    center={"lat": 32.8157, "lon": -117.1611},
    zoom=8.8,
    height=650,
    map_style="open-street-map",
    title="Where Homes Sold in San Diego (color = sold price in USD)",
)

fig.show(renderer="notebook")

# Define geographic cutoffs
longitude_cutoff = -117.2
latitude_cutoff = 32.8

# Divide properties into western and eastern groups
west = geo_df[geo_df["longitude"] < longitude_cutoff]
east = geo_df[geo_df["longitude"] >= longitude_cutoff]

# Divide properties into northern and southern groups
north = geo_df[geo_df["latitude"] >= latitude_cutoff]
south = geo_df[geo_df["latitude"] < latitude_cutoff]

# Display median prices and sample sizes
print("WESTERN VS. EASTERN PROPERTIES")
print("-" * 40)
print(f"West of longitude {longitude_cutoff}:")
print(f"  Median sold price: ${west['price'].median():,.0f}")
print(f"  Number of properties: {len(west):,}")

print(f"\nEast of or at longitude {longitude_cutoff}:")
print(f"  Median sold price: ${east['price'].median():,.0f}")
print(f"  Number of properties: {len(east):,}")

print("\nNORTHERN VS. SOUTHERN PROPERTIES")
print("-" * 40)
print(f"North of or at latitude {latitude_cutoff}:")
print(f"  Median sold price: ${north['price'].median():,.0f}")
print(f"  Number of properties: {len(north):,}")

print(f"\nSouth of latitude {latitude_cutoff}:")
print(f"  Median sold price: ${south['price'].median():,.0f}")
print(f"  Number of properties: {len(south):,}")
```

**Outputs**:

**Comment**: Homes west of longitude -117.2 have a median of about 1,380,000 USD, versus 900,000 USD inland. Northern sales (median 1,165,000 USD) are far pricier than southern ones (850,000 USD).

### Price per m² by ZIP Code

**Description**: Compare the spread of price per square meter across the 10 ZIP codes with the most sales.

**Cell**:

```none
# Find the 10 ZIP codes with the most sales
top_zips = df["zipcode"].value_counts().head(10).index
# Keep sales in those ZIP codes that have a valid price per m²
zip_m2 = df[df["zipcode"].isin(top_zips)].dropna(subset=["price_per_m2"])
# Order the ZIP codes by their median price per m², highest first
order = (zip_m2.groupby("zipcode")["price_per_m2"]
               # Take the median price per m² for each ZIP code
               .median()
               # Sort from highest to lowest
               .sort_values(ascending=False)
               # Keep the ZIP codes in that order
               .index)

plt.figure(figsize=(12, 6))
sns.boxplot(data=zip_m2, x="zipcode", y="price_per_m2", order=order,
            hue="zipcode", legend=False, palette="Set2", showfliers=False)
plt.gca().yaxis.set_major_formatter(usd_fmt)
plt.title("Sold Price per m² in the 10 Most Active ZIP Codes")
plt.xlabel("ZIP code")
plt.ylabel("Price per m² (USD)")
plt.show()
```

**Output**:

**Comment**: 92109 has the highest median price per m² (about 10,500 USD) and the widest spread. 92154 is the cheapest at about 4,900 USD.

### Price per m² vs. Living Area by ZIP Code for Single-Family Homes

**Description**: Test whether larger single-family homes sell for less per square meter according to ZIP code.

**Cell**:

```none
# Keep only single-family homes with both a price per m² and a living area
d = df[
    (df["homeType"] == "SINGLE_FAMILY")
].dropna(subset=["price_per_m2", "living_area_m2"]).copy()

# Keep the 10 most active ZIP codes as their own groups and put the rest in "Other"
d["zip_group"] = np.where(d["zipcode"].isin(top_zips), d["zipcode"], "Other")

zip_order = list(top_zips) + ["Other"]

fig = px.scatter(
    d,
    x="living_area_m2",
    y="price_per_m2",
    facet_col="zip_group",
    facet_col_wrap=4,
    category_orders={"zip_group": zip_order},
    hover_data={
        "zipcode": True,
        "price": ":,.0f",
        "living_area_m2": ":,.1f",
        "zip_group": False,
    },
    labels={
        "living_area_m2": "Living area (m²)",
        "price_per_m2": "Price per m² (USD)",
        "price": "Sold price (USD)",
    },
    height=1200,
    title="Price per m² vs. Living Area for Single-Family Homes by ZIP Code",
)

fig.for_each_annotation(
    lambda a: a.update(text=a.text.split("=")[-1])
)

fig.update_xaxes(
    matches=None,
    showticklabels=True,
    showline=True,
    tickformat=",.0f",
    title_text="Living area (m²)",
)

fig.update_yaxes(
    tickformat=",.0f",
    title_text="Price per m² (USD)",
)

fig.show(renderer="notebook")
```

**Output**:

**Comment**: Some ZIPs (e.g., 92154, 92126, 92103) show bigger homes costing less per m². 92127 is the exception, trending upward. In general, the correlation seems to be nearly zero, so the neighborhood matters more than size.

### Correlation Matrix of Home Features

**Description**: Check the Pearson correlation between price and the main physical and listing features.

**Cell**:

```none
corr_cols = {
    # Sold price in USD
    "price": "Price",
    # Number of bedrooms
    "bedrooms": "Bedrooms",
    # Number of bathrooms
    "bathrooms": "Bathrooms",
    "living_area_m2": "Living area",
    "lot_m2": "Lot size",
    # Construction year
    "yearBuilt": "Year built",
    # Zillow's estimated value
    "zestimate": "Zestimate",
    # Assessed value for taxes
    "taxAssessedValue": "Tax assessed value",
}
# Compute pairwise Pearson correlations between the renamed columns
corr = df[list(corr_cols)].rename(columns=corr_cols).corr()

plt.figure(figsize=(10, 8))
sns.heatmap(corr, annot=True, fmt=".2f", cmap="coolwarm", vmin=-1, vmax=1, linewidths=0.5)
plt.title("Correlation Matrix of Sold Home Features (Pearson)")
plt.show()
```

**Output**:

**Comment**: Zestimate (0.99) works almost like a copy of the price. Living area (0.79), bathrooms (0.56), and bedrooms (0.52) are the most useful price signals, while year built barely moves price (0.11).

### Price Band Mix in the Most Active ZIP Codes

**Description**: Show how the share of sales in each price band differs across the 10 most active ZIP codes.

**Cell**:

```none
# Keep sales from the 10 most active ZIP codes
top_df = df[df["zipcode"].isin(top_zips)]
# Compute the percentage of each price band within each ZIP code
mix = pd.crosstab(top_df["zipcode"], top_df["price_band"], normalize="index") * 100

ax = mix.plot(kind="barh", stacked=True, figsize=(10, 6),
              colormap="RdYlGn_r", width=0.8)
ax.set_xlabel("Share of sales (%)")
ax.set_ylabel("ZIP code")
ax.set_title("Price Band Mix in the 10 Most Active ZIP Codes")
ax.legend(title="Price band (USD)", bbox_to_anchor=(1.02, 1), loc="upper left")
plt.tight_layout()
plt.show()
```

**Output**:

**Comment:** ZIP 92154 is the most mid-range, with about 88% of sales between 500,000 and 1,000,000 USD. ZIPs 92130, 92109, and 92127 are mostly above 1,500,000 USD. Affordable sales are most common in 92115, with about a third under 500,000 USD.

### Days on Zillow by Price Band

**Description**: Check whether pricier homes take longer to sell, using days on Zillow by price band.

**Cell**:

```none
# Keep homes with both days on market and a price band
d = df.dropna(subset=["daysOnZillow", "price_band"])
# Compute Spearman correlation, which is Pearson correlation on ranks
rho = d["price"].rank().corr(d["daysOnZillow"].rank())

plt.figure(figsize=(10, 6))
sns.boxplot(data=d, x="price_band", y="daysOnZillow", hue="price_band",
            legend=False, palette="Oranges")
plt.title(f"Days on Zillow by Price Band (Spearman ρ with price = {rho:.2f})")
plt.xlabel("Price band (USD)")
plt.ylabel("Days on Zillow")
plt.show()
```

**Output**:

**Comment**: Days on Zillow barely change with price (Spearman ρ = -0.02). Median listing time stays between roughly 140 and 170 days in every band, with a wide spread from near zero to over 350 days. So price alone does not explain how fast a home sells.

### How Accurate Is the Zestimate?

**Description**: Measure the gap between each sold price and Zillow’s Zestimate to see how accurate the estimates are.

**Cell**:

```none
# Keep homes where the error could be computed
d = df.dropna(subset=["zestimate_error_pct"])
median_abs_error = d["zestimate_error_pct"].abs().median()

plt.figure(figsize=(10, 6))
sns.histplot(d["zestimate_error_pct"], bins=40, kde=True, color="purple")
# Mark zero error, where the estimate matches the sale price
plt.axvline(0, color="black", linestyle="--")
# Focus on errors between -20% and +20%
plt.xlim(-20, 20)
plt.title(f"Sold Price vs. Zestimate (median absolute error: {median_abs_error:.1f}%)")
plt.xlabel("(Sold price - Zestimate) / Zestimate (%)")
plt.ylabel("Number of homes")
plt.show()
```

**Output**:

**Comment**: The median absolute error is only 0.3%, so Zestimate is accurate for most homes. A handful of sales sit 20% or more from the estimate.

### Estimated Gross Rental Yield by ZIP Code

**Description**: Estimate the annual rent as a share of the sold price for each ZIP code, to compare rental returns.

**Cell**:

```none
# Group sales by ZIP code and compute the sales count and median yield
yield_stats = (df.groupby("zipcode")
                 .agg(sales=("zpid", "count"), median_yield=("rent_yield_pct", "median"))
                 # Drop ZIP codes with too few sales
                 .query("sales >= @MIN_SALES")
                 # Sort from highest to lowest yield
                 .sort_values("median_yield", ascending=False)
                 # Keep only the top 15
                 .head(15)
                 # Turn the ZIP code index back into a column
                 .reset_index())

show_table(yield_stats,
           formats={"median_yield": "{:.1f}%"},
           title="Top 15 ZIP codes by estimated gross rental yield")

plt.figure(figsize=(10, 7))
sns.barplot(data=yield_stats, x="median_yield", y="zipcode", color="seagreen")
plt.title("Estimated Gross Rental Yield by ZIP Code (Top 15)")
plt.xlabel("Gross yield (%) = rent Zestimate × 12 / sold price")
plt.ylabel("ZIP code")
plt.show()
```

**Output**:

**Comment**: The top 15 ZIPs yield between 5.3% and 6.7% gross, a narrow band. 92101 leads at 6.7%, followed by 92108 (6.5%) and 92154 (6.4%). Because the rent figures are Zillow estimates, treat these as directional rather than exact returns.

## Further Reading

If you are interested in similar approaches to web data analysis and visualization, using charts or AI, check out these guides:

- [*Filter a Crunchbase Dataset and Process It with AI for Prospecting New Clients*](/blog/web-data/prospecting-new-clients-with-crunchbase-dataset)
- [*Analysis of Amazon’s Best-Selling Products*](https://www.kaggle.com/code/brightdataml/analysis-of-amazon-s-best-selling-products)
- [*Crunchbase Data Analysis for Client Prospecting*](https://www.kaggle.com/code/brightdataml/crunchbase-data-analysis-for-client-prospecting/)

To build on this example, you could then [train a regression model to predict sale prices](https://www.kaggle.com/code/brightdataml/predicting-nvda-stock-prices-using-an-lstm), compare housing markets across two cities, or analyze historical price trends for individual properties.

## Conclusion

In this article, you learned how to use the [Bright Data Python SDK](https://docs.brightdata.com/api-reference/SDK) to retrieve structured, analysis-ready datasets tailored to your needs. Specifically, you saw how to get Zillow data on homes sold in San Diego, clean and prepare the data, and create more than 10 visualizations covering sale prices, locations, property sizes, valuations, and rental yields.

The Bright Data Python SDK provides programmatic access to Bright Data products, including its rich dataset marketplace. The SDK simplifies the data collection process, so you can focus on analysis rather than gathering and preparing data.

Fetch your results as CSV, JSON, JSONL, or other supported formats, then load them directly into a Jupyter Notebook for analysis and visualization, or pass them to AI tools for further analysis.

Create a Bright Data account to access the public web data you need for your data analysis projects!

Contact usStart for free

No credit card required











 [ ](https://www.linkedin.com/in/antonello-zanini/)

Antonello Zanini

 Technical Writer



  5.5 years experience



Antonello Zanini is a technical writer, editor, and software engineer with 5M+ views. Expert in technical content strategy, web development, and project management.



Expertise

  Web Development   Web Scraping   AI Integration



 [ View all articles ](https://brightdata.com/blog/authors/antonello-zanini)











 Table of Contents







Dedicated Scraper APIs &amp; No-Code Scrapers

Over 1000 scrapers for all popular domains. Simplify your web scraping.

[See pricing](/pricing/web-scraper "See pricing")

Just want data? Skip scraping.

Hundreds of ready-to-use datasets from all popular domains.

[See pricing](/pricing/datasets "See pricing")







 [ ](https://news.ycombinator.com/submitlink?t=Data+Analysis+and+Visualization+with+Bright+Data+SDK&u=https://brightdata.com/blog/web-data/data-analysis-and-visualization-with-bright-data-sdk) [ ](https://www.linkedin.com/shareArticle?mini=true&title=Data+Analysis+and+Visualization+with+Bright+Data+SDK&url=https://brightdata.com/blog/web-data/data-analysis-and-visualization-with-bright-data-sdk) [ ](http://www.reddit.com/submit?title=Data+Analysis+and+Visualization+with+Bright+Data+SDK&url=https://brightdata.com/blog/web-data/data-analysis-and-visualization-with-bright-data-sdk)







##  You might also be interested in

 [ ](https://brightdata.com/blog/ai/dspy-with-bright-data "DSPy MCP Server: Build a Web Research Agent with Bright Data")

 [AI





Satyam Tripathi

Technical Writer





### DSPy MCP Server: Build a Web Research Agent with Bright Data

Learn how to build a DSPy web research agent with the Bright Data MCP server, then evaluate it and optimize it with GEPA.



 22-Oct-2026

 29 min read

 ](https://brightdata.com/blog/ai/dspy-with-bright-data)

 [ ](https://brightdata.com/blog/ai/cherry-studio-with-bright-data "Cherry Studio MCP Tutorial: Add Bright Data for Web Search and Scraping")

 [AI





Satyam Tripathi

Technical Writer





### Cherry Studio MCP Tutorial: Add Bright Data for Web Search and Scraping

Learn how to connect Bright Data’s MCP server to Cherry Studio, turn it on per assistant, and make the model cite live web sources.



 20-Oct-2026

 21 min read

 ](https://brightdata.com/blog/ai/cherry-studio-with-bright-data)

 [ ](https://brightdata.com/blog/comparison/bright-data-cli-vs-python-sdk "Bright Data CLI vs Bright Data Python SDK: Choosing the Right Tool for Web Data")

 [Comparison





Antonello Zanini

Technical Writer





### Bright Data CLI vs Bright Data Python SDK: Choosing the Right Tool for Web Data

Bright Data’s CLI suits terminal workflows and AI agents, while the Python SDK suits pipelines. See how they compare on common scraping tasks.



 20-Oct-2026

 15 min read

 ](https://brightdata.com/blog/comparison/bright-data-cli-vs-python-sdk)
