Netflix ETL Pipeline with Power BI

Build a Python ETL pipeline that cleans Netflix data and powers a dashboard.

Introduction

30 Second Summary

Every time you open Netflix, an algorithm decides what to show you based on thousands of titles, genres, and viewing patterns. Behind that experience is a massive amount of messy data that someone had to clean, organize, and analyze before it became useful.

In this project, you will build a complete data pipeline that takes a raw dataset of 8,807 Netflix titles, fixes real-world quality problems like missing values and inconsistent formats, loads the clean data into a database, and visualizes it all in an interactive dashboard.

What You'll Build

You'll run a single Python script that transforms a messy CSV into structured analytics tables, then open Power BI to explore charts showing how Netflix's content library exploded after 2015, which countries dominate production, and how genres shift over time.

By the end of this project, you'll have:

  • A Python ETL pipeline you can run to automatically download, clean, and load 8,807 Netflix titles into a structured SQLite database.
  • SQL validation queries that prove your transformations worked correctly, catching issues that raw code alone might miss.
  • An interactive Power BI dashboard with charts showing content type distribution, yearly growth trends, top producing countries, and genre breakdowns.
  • Secret Mission: Compute year-over-year growth rates and content age metrics that reveal how quickly Netflix adds content after its original release.

Are there any prerequisites?

You'll need Python and Power BI Desktop installed on your machine before starting. Step 1 walks you through installing the pandas library, which is the only additional dependency.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters.

Set Up Your Project Environment

To build your Netflix ETL pipeline, you need a clean workspace with the right libraries installed.

In this step, you'll create a dedicated project folder, install pandas for data manipulation, and set up your main script file with its initial imports.

In this step, get ready to:
  • Create a netflix-etl project folder on your Desktop.
  • Install the pandas library for data processing.
  • Create etl_pipeline.py with its initial imports.
Create your project folder
  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir netflix-etl
cd netflix-etl
  • Confirm you're in the right folder by running:
pwd

You should see a path ending in netflix-etl.

Not seeing the right path?

Make sure you ran both mkdir netflix-etl and cd netflix-etl as two separate commands. If the folder already exists from a previous attempt, skip mkdir and just run cd netflix-etl.

Windows users: if cd ~/Desktop fails, make sure you're using PowerShell (not Command Prompt). PowerShell supports the ~ shortcut for your home folder.

help me fix my terminal path

Now open this folder in your code editor.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Select the netflix-etl folder on your Desktop, then click Open.

Why VS Code?

You can use any code editor you prefer. VS Code is shown here because it's free and widely used. If you already have a preferred editor, open the netflix-etl folder in that instead.

Install pandas

Your pipeline needs pandas to read, clean, and transform the Netflix dataset. It's the core library for tabular data manipulation in Python.

  • Install pandas by running this command in your terminal:
pip install pandas

After the install finishes, you should see output ending with a line like Successfully installed pandas-... followed by a version number.

Seeing 'pip not found'?

On macOS and Linux, try pip3 install pandas instead. Some systems use pip3 for Python 3 packages.

On Windows, make sure Python was added to your PATH during installation. You can verify by running python --version first.

help me fix my pip install

Create the pipeline script

With pandas installed, you're ready to create your main script file and set up the imports your pipeline needs.

  • In VS Code, click File, then New File.
  • Save the file immediately as etl_pipeline.py inside your netflix-etl folder.
  • Add the initial imports by copying this code into the file:
import pandas as pd
import sqlite3
import urllib.request
import os

What do these imports do?

  • pandas is the data manipulation library you'll use to clean and transform the Netflix dataset.
  • sqlite3 connects to a local SQLite database (built into Python, no extra install needed).
  • urllib.request downloads the raw CSV file from the internet.
  • os checks whether files already exist on disk before downloading again.
  • Save etl_pipeline.py.

Before you run the next command, what do you expect to see if pandas was installed correctly?

  • Verify your setup works by running this command in your terminal:
python -c "import pandas; print(pandas.__version__)"

You should see a version number printed, such as 3.0.5 (or 2.3.3 if you're on Python 3.9 or 3.10). Any version number means your setup is working.

Seeing an import error?

If you see ModuleNotFoundError: No module named 'pandas', the install from the previous substep may not have completed. Run pip install pandas again and check for error messages in the output.

If python opens the wrong version, try python3 -c "import pandas; print(pandas.__version__)" instead.

help me fix my pandas import

✔️ Awesome, I've got everything!

Great. Make sure you've saved etl_pipeline.py with all four imports.

ⓧ I'd like to double check the full code

Here's what your etl_pipeline.py should look like at this point:

import pandas as pd
import sqlite3
import urllib.request
import os

Your environment is ready. Next up, you'll write the extraction code that pulls real Netflix data from the internet and explore the quality issues hiding inside it.

Extract and Explore the Raw Data

Your environment is ready. Now it is time to bring in data.

Before you can fix messy data, you need to see exactly what is broken. In this step, you will download the Netflix dataset and run a diagnostic that reveals real-world quality problems your pipeline must solve.

In this step, get ready to:
  • Write a function that downloads the Netflix CSV from a public URL and loads it into a DataFrame.
  • Print data quality diagnostics showing missing values and mixed formats.
  • Run the pipeline and observe the real quality issues in the dataset.
Write the extraction function

The first stage of any ETL pipeline is extraction. This means pulling raw data from its source into your program so you can inspect and transform it.

The Netflix dataset lives on a public GitHub URL. Your function will download it once to a local file, then load it into a pandas DataFrame and print diagnostic information about its quality.

  • In etl_pipeline.py, add the following code below your existing import lines:
# ============================================================
# EXTRACT
# ============================================================

def extract_data():
    """Download and load the Netflix dataset from GitHub."""
    url = "https://raw.githubusercontent.com/rfordatascience/tidytuesday/master/data/2021/2021-04-20/netflix_titles.csv"
    csv_path = "netflix_titles_raw.csv"

    if not os.path.exists(csv_path):
        print("Downloading Netflix dataset...")
        urllib.request.urlretrieve(url, csv_path)
        print("Download complete.")

    df = pd.read_csv(csv_path)
    print(f"\n--- EXTRACTION COMPLETE ---")
    print(f"Shape: {df.shape}")
    print(f"\nColumn types:\n{df.dtypes}")
    print(f"\nNull counts:\n{df.isnull().sum()}")
    print(f"\nSample duration values:\n{df['duration'].head(10).tolist()}")
    return df

What does this code do?

This function handles three responsibilities:

  • Download logic: os.path.exists() checks whether the CSV is already on disk. If not, urllib.request.urlretrieve() downloads it. This avoids re-downloading every time you run the pipeline.
  • Exploration prints: df.shape shows row and column counts. df.isnull().sum() reveals how many values are missing in each column.
  • Duration sample: Printing the first 10 duration values exposes the mixed-format problem. Movies show values like 90 min while TV shows use 2 Seasons. Both live in the same column.
Add the pipeline runner

The function exists, but nothing calls it yet. You need an entry point that runs when you execute the script from your terminal.

  • Add the following code at the bottom of etl_pipeline.py, below the extract_data() function:
# ============================================================
# RUN PIPELINE
# ============================================================

if __name__ == "__main__":
    print("=" * 60)
    print("NETFLIX ETL PIPELINE")
    print("=" * 60)

    raw_df = extract_data()

    print("\n" + "=" * 60)
    print("PIPELINE COMPLETE - Ready for Power BI!")
    print("=" * 60)

What does this runner block do?

The if __name__ == "__main__": guard ensures this code only runs when you execute the file directly (e.g. python etl_pipeline.py). It will not run if another file imports your functions later.

Right now it calls extract_data() and stores the result in raw_df. You will add transform and load calls here in later steps.

Run the pipeline and explore the output
  • Save etl_pipeline.py.

Before you run this, what do you think the null counts will reveal? Will every column be fully populated, or will some have thousands of missing values?

  • Run the pipeline by executing this command in your terminal:
python etl_pipeline.py

You should see output that starts with the download confirmation, then prints the DataFrame shape as (8807, 12). The null counts reveal 2,634 missing directors, 825 missing cast entries, and 831 missing country values. The duration sample shows mixed formats like 90 min and 2 Seasons in the same column.

Why do these quality issues matter?

These are not bugs in your code. They are real-world data quality problems that exist in the source dataset. The 2,634 missing directors mean Netflix did not record that information for roughly 30% of its titles.

The mixed duration formats are especially tricky. You cannot compare 90 min to 2 Seasons as numbers until you split them apart. Your transform step will fix all of these problems.

Not seeing any output?

  • Make sure you saved etl_pipeline.py before running the command.
  • Check that your terminal is inside the netflix-etl folder. Run ls to confirm etl_pipeline.py is listed.
  • If you see a ModuleNotFoundError for pandas, run pip install pandas again in the same terminal.

Why is my ETL pipeline not printing any output when I run python etl_pipeline.py?

✔️ Awesome, I've got everything!

Great. Double check that you have saved etl_pipeline.py and that the file netflix_titles_raw.csv now exists in your project folder.

ⓧ I'd like to double check the full code

Your etl_pipeline.py file should have these sections in order. Scroll up and confirm each one is present:

  • The four import lines at the top (pandas, sqlite3, urllib.request, os).
  • The # EXTRACT section header followed by the extract_data() function definition.
  • The # RUN PIPELINE section header followed by the if __name__ == "__main__": block that calls extract_data().

You have your raw data downloaded and you can see exactly what is wrong with it. Next up, you will write transformation code that fixes every one of these quality issues.

Transform the Data

Your extraction code has revealed the mess lurking in the raw data. There are 2,634 titles with no director, dates stored as plain text strings, and durations that mix "90 min" with "2 Seasons" in the same column.

In this step, you will build a transform_data() function that solves every one of these problems programmatically. By the end, your messy DataFrame becomes two clean, structured tables ready for loading.

In this step, get ready to:
  • Clean missing values and parse dates into proper datetime objects.
  • Split the duration column into numeric values and unit labels.
  • Explode multi-value genre strings into a normalized table.
Clean missing values and parse dates

The biggest data quality issue is null values scattered across multiple columns. Leaving them as NaN causes downstream errors in grouping and filtering. Filling them with a consistent placeholder like "Unknown" keeps those rows queryable.

  • In etl_pipeline.py, find the comment that says # ============================================================ above # RUN PIPELINE.
  • Add the following code above that comment to start the transform function:
# ============================================================
# TRANSFORM
# ============================================================

def transform_data(df):
    """Clean and transform the raw Netflix data."""
    # --- Fill missing values ---
    df["director"] = df["director"].fillna("Unknown")
    df["cast"] = df["cast"].fillna("Unknown")
    df["country"] = df["country"].fillna("Unknown")
    df["date_added"] = df["date_added"].fillna("Unknown")
    df["rating"] = df["rating"].fillna("Unknown")
    df["duration"] = df["duration"].fillna("Unknown")

    # --- Parse dates ---
    df["date_added_parsed"] = pd.to_datetime(
        df["date_added"], format="mixed", errors="coerce"
    )
    df["year_added"] = df["date_added_parsed"].dt.year
    df["month_added"] = df["date_added_parsed"].dt.month

What does this code do?

The .fillna("Unknown") calls replace every null value in those six columns with the string "Unknown". This prevents errors when you later group or filter by these fields.

The pd.to_datetime() call converts text like "September 25, 2021" into a proper datetime object. The format="mixed" parameter handles inconsistent date formats, and errors="coerce" turns unparseable values into NaT (Not a Time) instead of crashing.

Split the duration column and extract country

The duration column mixes two units in one field. Movies say "90 min" while TV shows say "2 Seasons". You need separate numeric and unit columns so you can filter and aggregate properly.

  • Directly below the date parsing code you just added, paste the following:
    # --- Split duration into value and unit ---
    df["duration_value"] = df["duration"].str.extract(r"(\d+)").astype(float)
    df["duration_unit"] = df["duration"].apply(
        lambda x: "min" if "min" in str(x) else ("Seasons" if "Season" in str(x) else "Unknown")
    )

    # --- Extract primary country (first listed) ---
    df["primary_country"] = df["country"].apply(
        lambda x: x.split(",")[0].strip() if x != "Unknown" else "Unknown"
    )

What does this code do?

  • The regex r"(\d+)" extracts just the number from strings like "90 min" or "2 Seasons", storing it as a float in duration_value.
  • The lambda checks whether the string contains "min" or "Season" and assigns the appropriate unit label to duration_unit.
  • The primary_country extraction takes just the first country from multi-country entries like "United States, India" because many titles list multiple co-producing countries.
Explode genres into a separate table

Each title has its genres crammed into one string like "Dramas, International Movies, Thrillers". To count genres or filter by them, you need one row per title-genre pair. This is called normalization in database design.

  • Below the primary country code, paste the following to finish the transform function:
    # --- Explode genres into a separate table ---
    genres_df = df[["show_id", "listed_in"]].copy()
    genres_df["genre"] = genres_df["listed_in"].str.split(", ")
    genres_df = genres_df.explode("genre")
    genres_df = genres_df[["show_id", "genre"]].reset_index(drop=True)

    # --- Select final columns for titles table ---
    titles_df = df[[
        "show_id", "type", "title", "director", "cast", "country",
        "primary_country", "date_added_parsed", "release_year", "rating",
        "duration_value", "duration_unit", "year_added", "month_added",
        "description"
    ]].copy()

    titles_df = titles_df.rename(columns={"date_added_parsed": "date_added"})

    print(f"\n--- TRANSFORMATION COMPLETE ---")
    print(f"Titles table shape: {titles_df.shape}")
    print(f"Genres table shape: {genres_df.shape}")
    print(f"Null values in date_added: {titles_df['date_added'].isnull().sum()}")
    print(f"Duration split - sample values: {titles_df['duration_value'].head(5).tolist()}")
    print(f"Duration split - sample units: {titles_df['duration_unit'].head(5).tolist()}")

    return titles_df, genres_df

What does this code do?

  • The .str.split(", ") turns each genre string into a list, and .explode("genre") creates a new row for each item in that list. One title with three genres becomes three rows in genres_df.
  • The final column selection picks only the columns the analytics layer needs. Renaming date_added_parsed to date_added gives it a clean name for Power BI.
  • The function returns two DataFrames: titles_df (one row per title) and genres_df (one row per title-genre pair).

Now you need to wire the transform function into your pipeline so it actually runs.

  • Scroll down to the # RUN PIPELINE section at the bottom of your file.
  • Find this line:
    raw_df = extract_data()
  • Add this line directly below it:
    titles_df, genres_df = transform_data(raw_df)
  • Save etl_pipeline.py.

Before you run the script, what do you expect to see printed for the genres table shape? Remember that each title can have multiple genres.

  • Run your pipeline by executing this command:
python etl_pipeline.py

You should see the extraction output first, followed by the transformation stats. Look for "TRANSFORMATION COMPLETE" with the titles table shape of (8807, 15), a genres table with 27,000+ rows, 0 null values in date_added being printed as 10 (the coerced unparseable entries), and sample duration values like [90.0, 1.0, 104.0, ...].

Seeing a NameError or AttributeError?

  • If you see NameError: name 'transform_data' is not defined, make sure your transform_data function is defined above the if __name__ == "__main__": block, not inside it.
  • If you see an indentation error, confirm every line inside transform_data() is indented with 4 spaces. Mixing tabs and spaces causes silent failures.
  • If the duration values show all NaN, check that the .fillna("Unknown") for the duration column comes before the .str.extract() call.

Why is my transform function throwing an error?

Why did 10 dates stay null?

The original dataset has 10 rows where date_added was truly empty (not just a string). Even after filling with "Unknown", pd.to_datetime() cannot parse the word "Unknown" into a date, so errors="coerce" turns them into NaT. This is the expected behavior for the 10 rows that had no date at all.

✔️ Awesome, I've got everything!

Great. Double check you have saved etl_pipeline.py with both the extract_data() and transform_data() functions.

ⓧ I'd like to double check the full code

Here is the complete etl_pipeline.py at this point. Scroll up and confirm your file has all these sections in order: EXTRACT function, TRANSFORM function, RUN PIPELINE block.

  • The extract_data() function (downloads CSV and prints shape/nulls).
  • The transform_data(df) function with: fillna calls, date parsing, duration split, primary country extraction, genres explosion, final column selection, print statements, and return.
  • The if __name__ == "__main__": block calling both extract_data() and transform_data(raw_df).

Your transformation layer is solid. The raw mess is now two clean, structured tables. Next up, you will load them into a SQLite database and validate the results with SQL queries.

Load into SQLite and Validate

Your transformation code reshapes the raw Netflix data into clean DataFrames. But how do you know the transformations actually worked correctly across all 8,807 rows?

Loading into a database lets you run SQL queries to validate the data independently of pandas. In this step, you'll write the final "L" in ETL: loading your clean data into a SQLite database, verifying it with SQL, and exporting CSVs that Power BI can read directly.

In this step, get ready to:
  • Create a SQLite database and write your cleaned DataFrames into structured tables.
  • Run SQL validation queries to confirm record counts, type breakdowns, and genre rankings.
  • Export analytics-ready CSV files for Power BI consumption.
Create the SQLite database and write tables

SQLite is a lightweight database engine built into Python's standard library. You already imported sqlite3 at the top of your file in a previous step, so no extra installation is needed.

  • In etl_pipeline.py, find the comment line that says # RUN PIPELINE.
  • Add the following load_data function above that comment block, with one blank line separating it from the transform function above:
# ============================================================
# LOAD
# ============================================================

def load_data(titles_df, genres_df):
    """Load data into SQLite and export to CSV."""
    db_path = "netflix.db"

    # --- Write to SQLite ---
    conn = sqlite3.connect(db_path)
    titles_df.to_sql("titles", conn, if_exists="replace", index=False)
    genres_df.to_sql("genres", conn, if_exists="replace", index=False)

    # --- Validate with SQL queries ---
    print(f"\n--- LOAD COMPLETE (SQLite: {db_path}) ---")

    conn.close()

What does this code do?

  • sqlite3.connect("netflix.db") creates a new database file in your project folder. If the file already exists, it opens it instead.
  • to_sql("titles", conn, if_exists="replace", index=False) writes the entire DataFrame into a database table called titles. The replace option means re-running the pipeline overwrites old data cleanly.
  • index=False prevents pandas from adding an extra row-number column to your database tables.

Now you need to call this function from the pipeline runner so it actually executes.

  • Scroll down to the # RUN PIPELINE section at the bottom of the file.
  • Find this line:
    titles_df, genres_df = transform_data(raw_df)
  • Add the following line directly below it:
    load_data(titles_df, genres_df)
  • Save etl_pipeline.py.
  • Run the pipeline to confirm the database is created:
python etl_pipeline.py

You should see the extraction and transformation output from before, followed by a new line: --- LOAD COMPLETE (SQLite: netflix.db) ---.

  • Confirm the database file exists by running:
ls

You should see netflix.db listed alongside etl_pipeline.py and netflix_titles_raw.csv.

Don't see netflix.db?

  • Check that you added the load_data(titles_df, genres_df) call in the runner section. Without it, the function is defined but never executed.
  • Make sure your terminal is inside the netflix-etl folder when you run the script.
  • Help me debug why netflix.db is not being created.
Add validation SQL queries

The database now holds your data, but you have not verified it independently. SQL queries let you confirm record counts and distributions without relying on pandas.

  • In the load_data function, find the line that says print(f"\n--- LOAD COMPLETE (SQLite: {db_path}) ---").
  • Replace everything from that line down to conn.close() (including both lines) with the following code:
    # --- Validate with SQL queries ---
    print(f"\n--- LOAD COMPLETE (SQLite: {db_path}) ---")

    result = conn.execute("SELECT COUNT(*) FROM titles").fetchone()
    print(f"Total titles: {result[0]}")

    result = conn.execute(
        "SELECT type, COUNT(*) as count FROM titles GROUP BY type"
    ).fetchall()
    print(f"By type: {result}")

    result = conn.execute(
        "SELECT genre, COUNT(*) as count FROM genres GROUP BY genre ORDER BY count DESC LIMIT 5"
    ).fetchall()
    print(f"Top 5 genres: {result}")

    conn.close()

What do these queries check?

  • SELECT COUNT(*) FROM titles confirms all 8,807 rows made it into the database without any being dropped.
  • GROUP BY type splits the count into Movies vs TV Shows. If these numbers don't match the original dataset, something went wrong during transformation.
  • The top 5 genres query validates the exploded genres table. Each title can appear in multiple genres, so this table has far more rows than the titles table.
  • Save etl_pipeline.py and run the pipeline again:
python etl_pipeline.py

In the LOAD COMPLETE section of the output, you should now see three validation results: Total titles: 8807, the type breakdown showing Movies and TV Shows, and the top 5 genres ranked by count.

Seeing an OperationalError?

  • If you see "no such table: titles", the write step failed silently. Make sure titles_df.to_sql() appears before your validation queries in the function.
  • Check that your indentation is consistent. All lines inside load_data should be indented by 4 spaces.
  • Help me debug my SQL validation queries in load_data.
Export CSV files for Power BI

Power BI Desktop has no native SQLite connector. Connecting to SQLite would require installing a third-party ODBC driver and configuring a DSN, which adds complexity unrelated to your learning goal.

Why CSV instead of connecting Power BI directly to SQLite?

Power BI's native Text/CSV connector requires zero setup. You select Get Data, choose your CSV, and load. The SQLite path would require a separate ODBC driver install, 64-bit DSN configuration, and architecture-mismatch troubleshooting. CSV keeps the focus on data engineering, not driver debugging.

  • In the load_data function, find the conn.close() line.
  • Add the following code directly below conn.close(), still inside the function:
    # --- Export to CSV for Power BI ---
    titles_df.to_csv("titles_clean.csv", index=False)
    genres_df.to_csv("genres_clean.csv", index=False)
    print(f"\nCSV files exported: titles_clean.csv, genres_clean.csv")

What does this code do?

  • to_csv() writes each DataFrame to a comma-separated file that Power BI can read natively.
  • index=False prevents pandas from writing an extra index column into the CSV. Power BI would import that as a meaningless number column.

Before you run the pipeline, what do you expect to see printed in the terminal this time?

  • Save etl_pipeline.py and run the full pipeline:
python etl_pipeline.py

At the end of the output, you should see CSV files exported: titles_clean.csv, genres_clean.csv printed after the SQL validation results.

  • Confirm both CSV files were created by running:
ls

You should see titles_clean.csv and genres_clean.csv alongside netflix.db in your project folder.

CSV files not appearing?

  • Make sure the CSV export lines are indented inside the load_data function (4 spaces in). If they sit at the top level, Python treats them as module-level code that never runs in the function context.
  • Verify you placed the export lines after conn.close(), not before. The connection must be closed before you export (the CSVs come from the DataFrames, not the database).
  • Help me figure out why my CSV files are not being exported.

✔️ Awesome, I've got everything!

Great. Double check you've saved etl_pipeline.py and that your terminal shows all three output sections (EXTRACTION, TRANSFORMATION, and LOAD COMPLETE) when you run the script.

ⓧ I'd like to double check the full code

Scroll up through your etl_pipeline.py and confirm these sections are present in this order:

  • Imports at the top: pandas, sqlite3, urllib.request, os.
  • EXTRACT section: extract_data() function that downloads and loads the CSV.
  • TRANSFORM section: transform_data(df) function that cleans nulls, parses dates, splits duration, and explodes genres.
  • LOAD section: load_data(titles_df, genres_df) function that writes to SQLite, runs 3 validation queries, then exports 2 CSV files.
  • RUN PIPELINE section: calls all three functions in order (extract_data(), transform_data(raw_df), load_data(titles_df, genres_df)).

Your ETL pipeline is fully operational. The raw CSV goes in, clean structured data comes out in a database and as analytics-ready CSVs. Next up, you'll connect these CSVs to Power BI and build an interactive dashboard.

Build the Power BI Dashboard

Your ETL pipeline now transforms raw data into clean, structured tables and exports them as CSV files.

The pipeline is only valuable if it delivers insights someone can see and interact with. In this step, you connect Power BI Desktop to your clean CSV output and build an interactive dashboard that proves the entire ETL flow works end-to-end.

In this step, get ready to:
  • Load your CSV files into Power BI Desktop as data tables.
  • Create a pie chart and a line chart showing content type distribution and yearly growth.
  • Create two bar charts showing top producing countries and top genres.
Load your CSV files into Power BI

Power BI Desktop needs data before it can visualize anything. You will load the two CSV files your pipeline exported.

Why CSV instead of connecting to SQLite directly?

Power BI Desktop has no native SQLite connector. Connecting to SQLite requires installing a third-party ODBC driver, configuring a 64-bit DSN in Windows ODBC Administrator, and troubleshooting common architecture mismatch errors.

Power BI's native Text/CSV connector requires zero additional setup and teaches the same visualization concepts. That is why your pipeline exports CSV files alongside the database.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Power BI Desktop and press Enter to open it.
  • In the Home ribbon, click Get Data.
  • Select Text/CSV from the list.
  • Navigate to your netflix-etl folder on the Desktop and select titles_clean.csv.
  • Click Open.

A preview window appears showing a sample of the CSV data. This confirms Power BI detected the columns correctly.

  • Click Load to import the data into the model.

Now repeat the same process for the genres file.

  • Click Get Data in the Home ribbon again.
  • Select Text/CSV.
  • Navigate to netflix-etl and select genres_clean.csv.
  • Click Open, then click Load.

Look at the Fields pane on the right side of the screen. You should see two tables listed: titles_clean and genres_clean.

Don't see both tables in the Fields pane?

Make sure you clicked Load (not Transform Data) on both CSV previews. If you accidentally opened the Power Query Editor, click Close & Apply on the Home tab to return to the report canvas.

Help me troubleshoot my missing tables.

Create a pie chart and line chart

With both tables loaded, you can start building visuals. The first two charts answer fundamental questions: what type of content dominates Netflix, and how fast did they add content over time?

  • Click on an empty area of the report canvas to make sure no visual is selected.
  • In the Visualizations pane, click the Pie chart icon.
  • In the Fields pane, expand titles_clean.
  • Drag the type field into the Legend well in the Visualizations pane.
  • Drag the show_id field into the Values well.

Your pie chart now shows two slices: one for Movie and one for TV Show. The Movies slice is noticeably larger, representing 6,131 of the 8,807 titles.

Why use show_id for the count?

Power BI automatically applies a Count aggregation when you drag a text field like show_id into the Values well. Each unique ID represents one title, so counting them gives the total number of titles per type.

Now create the line chart to show how content grew over time.

  • Click on an empty area of the canvas (away from the pie chart).
  • In the Visualizations pane, click the Line chart icon.
  • Drag year_added from titles_clean into the X-axis well.
  • Drag show_id into the Y-axis well.

The line chart shows content additions per year. You should see a dramatic upward curve starting around 2015, peaking in 2019-2020.

Resize visuals by dragging their corners

Click a visual to select it, then drag its corner handles to resize it. Position your pie chart and line chart side by side on the top half of the canvas so you have room for two more charts below.

Line chart shows a single data point?

Power BI may have placed year_added into a date hierarchy (Year, Quarter, Month). Click the small downward arrow on the X-axis field and select the column name directly (not the hierarchy). You want to display year_added as a continuous number, not a date hierarchy.

Help me fix my line chart axis.

Create bar charts and arrange the dashboard

The final two charts reveal geographic and genre patterns. You will filter each to show only the top 10 values so the charts stay readable.

  • Click on an empty area of the canvas below your existing charts.
  • In the Visualizations pane, click the Clustered bar chart icon.
  • Drag primary_country from titles_clean into the Y-axis well.
  • Drag show_id into the X-axis well.

The chart shows every country, which is too many bars to be useful. Apply a Top N filter to show only the 10 countries with the most titles.

  • With the bar chart selected, look at the Filters pane on the right.
  • Under Filters on this visual, find primary_country.
  • Expand the Filter type dropdown and select Top N.
  • In the Show items field, make sure Top is selected and enter 10.
  • Drag show_id from the Fields pane into the By value area of the filter.
  • Click Apply filter.

The chart now shows only the top 10 countries. The United States should appear at the top with the longest bar.

Now create the genres bar chart using the same approach.

  • Click on an empty area of the canvas next to the countries chart.
  • In the Visualizations pane, click the Clustered bar chart icon.
  • In the Fields pane, expand genres_clean.
  • Drag genre into the Y-axis well.
  • Drag show_id into the X-axis well.
  • In the Filters pane under Filters on this visual, find genre.
  • Change the Filter type to Top N.
  • Set Show items to Top 10.
  • Drag show_id into the By value area.
  • Click Apply filter.

The genres chart now shows the top 10 genres by title count. International Movies and Dramas should appear near the top.

  • Arrange all four visuals on a single page: pie chart and line chart on the top row, two bar charts on the bottom row.
  • Save your file by pressing Ctrl+S (Windows) or Cmd+S (macOS).
  • Save it inside your netflix-etl folder with a name like netflix_dashboard.pbix.

What does this dashboard prove?

Each visual maps to a stage of your ETL pipeline. The pie chart proves your type column was preserved correctly. The line chart proves year_added was parsed from the raw date string. The country chart proves primary_country extraction worked. The genre chart proves the explode operation created one row per genre.

If any transformation had failed silently, these visuals would show unexpected gaps or wrong totals. The dashboard is your validation layer.

Before you check your final result, predict what you expect to see: do you think Movies or TV Shows dominate the Netflix catalog?

Your dashboard should now display four populated visuals on a single page. The pie chart shows Movies (roughly 70%) vs TV Shows (roughly 30%). The line chart shows an explosive growth curve peaking around 2019. The countries bar chart has the United States at the top. The genres bar chart shows International Movies as the most tagged genre.

Visuals are empty or showing unexpected data?

Check that you are dragging fields from the correct table. The countries and type charts use titles_clean fields, while the genres chart uses genres_clean fields. Mixing tables in one visual without a defined relationship can produce blank or incorrect results.

Also confirm you ran your ETL pipeline (python etl_pipeline.py) before loading the CSV files. If the CSVs are empty or incomplete, your visuals will be too.

Help me debug my empty visuals.

Your ETL pipeline now has a visual proof layer. Anyone can open this dashboard and immediately see the content trends hiding in the raw Netflix data. Next up, you will take this further with advanced growth calculations in the Secret Mission.

Secret mission

Year-Over-Year Growth Analysis

Your dashboard shows how many titles Netflix added each year. But a raw count doesn't reveal acceleration or deceleration. In this secret mission, you'll compute year-over-year growth rates and content age metrics to uncover deeper patterns in Netflix's content strategy.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • netflix-etl project folder (pipeline script, SQLite database, raw and cleaned CSV files, Power BI file).
  • pandas Python library (installed via pip).

Keep everything running

No action needed. Choose this if you plan to keep iterating on your dashboard or re-running the pipeline with updated data.

  • Your netflix-etl folder on your Desktop contains the pipeline script, database, CSVs, and your Power BI file.
  • The pandas library remains installed and available for other Python projects.
  • You can reopen the .pbix file any time in Power BI Desktop to view or modify your dashboard.

Pause - I'll come back to this later

Since everything is local files, there is nothing to shut down or pause. Your project folder will sit on disk until you return.

  • Close Power BI Desktop if it is still open to free up memory.
  • Close your code editor and terminal.
  • When you come back, reopen the .pbix file to pick up where you left off.

Delete - I don't want to use this again

Remove all project files and the pandas library to start fresh.

  • Delete the project folder by running this command in your terminal.

macOS

rm -rf ~/Desktop/netflix-etl

Windows

rmdir /s /q $HOME\Desktop\netflix-etl
  • Uninstall the pandas library by running:
pip uninstall pandas
  • Confirm the removal by typing y when prompted.

Nice Work!

Nice Work!

You just built a complete data engineering pipeline from scratch. Raw, messy Netflix data went in one end, and a polished interactive dashboard came out the other.

Here's what you learned:

  • Built a Python ETL pipeline that extracts 8,807 Netflix titles, cleans real-world data quality problems (2,634 missing directors, inconsistent date formats, mixed-unit duration fields), and loads structured results into a SQLite database.
  • Validated your transformations with SQL queries and exported analytics-ready CSV files that Power BI loads directly with zero additional configuration.
  • Created an interactive Power BI dashboard with visualizations showing content type distribution, yearly growth trends, top producing countries, and genre breakdowns.
  • Secret Mission: Computed year-over-year growth rates and content age metrics for advanced trend analysis, proving you can derive business insights beyond simple counts.

Ready to quiz yourself?