Orthopedic Implant Pricing Dashboard
Compare medical device vendor pricing and build a Tableau procurement dashboard.
Introduction
30 Second Summary
Hospitals spend millions on orthopedic implants every year. When two manufacturers sell nearly identical hip or knee replacements at wildly different prices, the procurement team needs a clear way to spot the gaps.
In this project, you will build a pricing intelligence dashboard that compares orthopedic implant costs between two medical device manufacturers, helping procurement teams identify savings opportunities at a glance.
What You'll Build
A data analysis pipeline in Google Colab that cleans hospital purchase data using pandas, builds a competitor pricing matrix, projects price trends with scikit-learn, and exports everything to an interactive Tableau dashboard.
By the end of this project, you'll have:
- A cleaned, normalized pricing dataset comparing orthopedic implant prices across two competing manufacturers.
- A product-by-product pricing comparison matrix with price gap calculations and a simple linear price trend projection.
- An interactive Tableau dashboard visualizing the competitor pricing matrix, price gaps by category, and projected pricing trends.
- Secret Mission: Build a savings projection panel showing potential annual cost reduction with vendor-switch recommendations.
Are there any prerequisites?
Basic Python familiarity (variables, functions, loops) is helpful. No prior pandas or Tableau experience is required.
Set Up Colab and Load the Dataset
Before you can analyze pricing data, you need a workspace where you can write and run Python code. Google Colab provides a free, cloud-based notebook environment that comes pre-loaded with data science libraries like pandas and NumPy.
In this step, you'll create your analysis notebook and load two datasets from GitHub: hospital purchase records and a vendor product catalog.
In this step, get ready to:
- Create and name your Google Colab notebook.
- Import the Python libraries needed for data analysis.
- Load and inspect two datasets from GitHub.
Open Google Colab and create a new notebook
- Open your browser and navigate to colab.research.google.com.
- Sign in with your Google account if prompted.
- Click File in the top menu bar.
- Select New notebook.
- Click the notebook title at the top left (it defaults to "Untitled") and rename it to Ortho_Implant_Pricing_Analysis.
You should see an empty notebook with a single code cell ready for input.
Not seeing the notebook?
Make sure you are signed into a Google account. Colab requires a Google login to create and save notebooks.
If the page does not load, try a different browser or disable any ad-blocking extensions that might interfere with Google services.
Help me access Google Colab.
Import libraries and load the dataset
Your notebook needs four Python libraries to power the analysis. Colab comes with all of them pre-installed, so you just need to import them.
- Import the data science libraries by pasting this code into the first cell of your notebook:
# --- Cell 1: Import Libraries ---
import pandas as pd
import numpy as np
from sklearn.linear_model import LinearRegression
import matplotlib.pyplot as plt
What does this code do?
- pandas (imported as pd) handles tabular data in structures called DataFrames.
- numpy (imported as np) provides numerical operations like comparisons and rounding.
- LinearRegression from scikit-learn fits trend lines you'll use for price projections later.
- matplotlib.pyplot (imported as plt) is a plotting library for creating charts.
- Run the cell by pressing Shift+Enter.
If no error appears, the imports loaded successfully. The cell shows a small green checkmark on the left.
Seeing an ImportError?
All four libraries come pre-installed in Google Colab. If you see an import error, make sure you are running this in Colab and not a local Python installation.
Check for typos in the import statements. The most common mistake is misspelling sklearn or forgetting the underscore in linear_model.
Help me fix my import error in Colab.
Now you'll load two datasets directly from GitHub. The first contains hospital purchase records. The second is a vendor product catalog with list and contract prices.
- Click the + Code button below your first cell to add a new code cell.
- Load both datasets from GitHub by pasting this code into the new cell:
# --- Cell 2: Load Data from GitHub ---
purchases_url = "https://raw.githubusercontent.com/pnascimento9596/ortho-implant-benchmarking/main/data/sample_data/hospital_purchases.csv"
catalog_url = "https://raw.githubusercontent.com/pnascimento9596/ortho-implant-benchmarking/main/data/sample_data/vendor_catalog.csv"
purchases = pd.read_csv(purchases_url)
catalog = pd.read_csv(catalog_url)
print("Purchases shape:", purchases.shape)
print("Catalog shape:", catalog.shape)
What does this code do?
The two URL variables point to raw CSV files hosted on GitHub. The pd.read_csv() function downloads each file and converts it into a DataFrame you can query and manipulate.
The .shape attribute prints the number of rows and columns so you can immediately confirm the data loaded correctly.
- Run this cell by pressing Shift+Enter.
You should see output confirming the dataset sizes: purchases shows (320, 15) and catalog shows (74, 10).
Seeing a connection error or wrong shape?
If you see a connection or HTTP error, check that your Colab notebook has internet access. Colab connects to the internet by default, but corporate networks sometimes block outgoing requests.
If the shape numbers do not match (320, 15) and (74, 10), double-check that you copied the full URL strings without any line breaks or extra spaces.
Help me troubleshoot my data loading error.
Inspect the loaded data
Before cleaning or transforming anything, you need to understand what each dataset contains. Running a quick inspection reveals column names, data types, and a preview of actual values.
- Click + Code to add another new cell below.
- Inspect the structure of both datasets by pasting this code into the new cell:
# --- Cell 3: Initial Inspection ---
print("\n--- Purchases Head ---")
print(purchases.head())
print("\n--- Purchases Info ---")
print(purchases.info())
print("\n--- Catalog Head ---")
print(catalog.head())
print("\n--- Catalog Info ---")
print(catalog.info())
What does this code do?
- .head() shows the first 5 rows of each DataFrame so you can see real data values.
- .info() prints column names, data types, and non-null counts. This tells you which columns hold numbers vs text and where missing values might hide.
- Run this cell by pressing Shift+Enter.
You should see a table preview of each dataset followed by a summary of columns and their types.
What are the key columns?
- vendor_name identifies which manufacturer (e.g., Stryker, DePuy Synthes) supplied the implant.
- product_category groups implants into types: Hip, Knee, Spine, or Trauma.
- unit_price is what the hospital actually paid per implant.
- list_price and contract_price (in the catalog) are the published and negotiated rates from the vendor.
- clinical_equivalency_group matches functionally equivalent products across vendors so you can compare apples to apples.
Output looks jumbled or cut off?
Colab sometimes truncates wide DataFrames. This is normal for display purposes. The data is still complete in memory.
If .info() shows unexpected null counts or wrong data types, do not worry. You'll handle data quality issues in the next step.
Help me understand my inspection output.
✔️ Awesome, I've got everything!
Great. Make sure your notebook has all 3 cells and that each ran without errors before moving on.
ⓧ I'd like to double check the full code
Here is the full code across all 3 cells. Compare yours against this:
# --- Cell 1: Import Libraries ---
import pandas as pd
import numpy as np
from sklearn.linear_model import LinearRegression
import matplotlib.pyplot as plt
# --- Cell 2: Load Data from GitHub ---
purchases_url = "https://raw.githubusercontent.com/pnascimento9596/ortho-implant-benchmarking/main/data/sample_data/hospital_purchases.csv"
catalog_url = "https://raw.githubusercontent.com/pnascimento9596/ortho-implant-benchmarking/main/data/sample_data/vendor_catalog.csv"
purchases = pd.read_csv(purchases_url)
catalog = pd.read_csv(catalog_url)
print("Purchases shape:", purchases.shape)
print("Catalog shape:", catalog.shape)
# --- Cell 3: Initial Inspection ---
print("\n--- Purchases Head ---")
print(purchases.head())
print("\n--- Purchases Info ---")
print(purchases.info())
print("\n--- Catalog Head ---")
print(catalog.head())
print("\n--- Catalog Info ---")
print(catalog.info())
Your analysis workspace is ready and the data is loaded. Next up, you'll explore the data to understand which vendors and product categories are available for comparison.
Explore and Understand the Data
Your data is loaded and you've confirmed both DataFrames are intact. Now it's time to explore what's actually inside them.
Before building any comparison matrix, you need to understand which vendors exist, how products are categorized, and whether the data is clean enough to trust. This step is about building that foundation.
In this step, get ready to:
- Identify which vendors and product categories exist in the data.
- Understand pricing attributes and calculate stats by vendor.
- Check data quality for missing values, duplicates, and equivalency group coverage.
Identify available vendors and product categories
The vendor_name column is the competitor dimension you'll compare against. The product_category column groups products into families (Hip, Knee, Spine, Trauma). The clinical_equivalency_group column matches equivalent products across vendors so you can compare apples to apples.
- Create a new code cell in your notebook and explore vendors and categories by running this code:
# --- Cell 4: Explore Vendors and Categories ---
print("\n--- Vendors in Purchases ---")
print(purchases['vendor_name'].value_counts())
print("\n--- Product Categories ---")
print(purchases['product_category'].value_counts())
print("\n--- Vendors in Catalog ---")
print(catalog['vendor_name'].value_counts())
What does this code do?
The value_counts() method counts how many rows belong to each unique value in a column. This tells you which manufacturers appear most frequently and how purchases break down by product type.
Running it on both purchases and catalog lets you see which vendors have both purchase history and catalog entries available for comparison.
- Run the cell by pressing the play button or Shift+Enter.
- You should see a count breakdown for each vendor in the purchases data, four product categories (Hip, Knee, Spine, Trauma), and a vendor count from the catalog.
Seeing a NameError?
This means the purchases or catalog DataFrame hasn't been created yet. Make sure you ran Cell 2 (the data loading cell) before this one.
In Google Colab, cells must be run in order. Click on Cell 2 and run it first, then come back to this cell.
Help me fix a NameError in my Colab notebook
Understand the pricing attributes
The two DataFrames track pricing differently. In purchases, the unit_price column is what the hospital actually paid per implant. In catalog, list_price is the published price and contract_price is the negotiated rate.
- Create a new code cell and calculate pricing statistics by vendor by running this code:
# --- Cell 5: Pricing Stats by Vendor ---
vendor_stats = purchases.groupby('vendor_name')['unit_price'].agg(['mean', 'min', 'max', 'count'])
print("\n--- Price Stats by Vendor ---")
print(vendor_stats)
What does this code do?
The groupby('vendor_name') splits the data into groups, one per vendor. Then .agg(['mean', 'min', 'max', 'count']) calculates four statistics on unit_price for each group.
This gives you a quick sense of how each vendor's pricing compares at a high level. Big differences in average price or count will tell you which comparison is most meaningful.
- Run the cell.
- You should see a table with one row per vendor showing their mean, min, max, and count of unit prices.
Seeing a KeyError for unit_price?
Make sure the column name is spelled exactly as unit_price with an underscore. Column names are case-sensitive in pandas.
Help me fix a KeyError in my pandas groupby
Check data quality
Before trusting any analysis, you need to know if the data has holes. Missing prices would throw off averages, duplicates would inflate counts, and mismatched equivalency groups would break your cross-vendor comparison.
- Create a new code cell and check data quality by running this code:
# --- Cell 6: Data Quality Check ---
print("\n--- Missing Values (Purchases) ---")
print(purchases.isnull().sum())
print("\n--- Missing Values (Catalog) ---")
print(catalog.isnull().sum())
print("\n--- Duplicate Rows (Purchases) ---")
print(purchases.duplicated().sum())
print("\n--- Unique Equivalency Groups ---")
print("Purchases:", purchases['clinical_equivalency_group'].nunique())
print("Catalog:", catalog['clinical_equivalency_group'].nunique())
What does this code do?
The .isnull().sum() chain counts missing values per column. The .duplicated().sum() counts exact duplicate rows.
The .nunique() call on clinical_equivalency_group tells you how many matchable product groups exist in each DataFrame. Overlap between these two numbers determines how many products you can actually compare side by side.
- Run the cell.
- You should see a per-column missing value count for both DataFrames, a duplicate count for purchases, and the number of unique equivalency groups in each dataset.
Output seems blank or incomplete?
If you only see partial output, scroll down in the cell output area. Colab sometimes collapses long outputs. Click the output to expand it fully.
If you see an error mentioning the column name, double-check that clinical_equivalency_group is spelled exactly as shown. You can run purchases.columns.tolist() to see all available column names.
Help me debug my data quality check output
✔️ Awesome, I've got everything!
Great. Double check you've run all cells in order from Cell 1 through Cell 6.
ⓧ I'd like to double check the full code
Here is the complete notebook code up to this point. Each section separated by a comment header corresponds to one cell in your Colab notebook.
# --- Cell 1: Import Libraries ---
import pandas as pd
import numpy as np
from sklearn.linear_model import LinearRegression
import matplotlib.pyplot as plt
# --- Cell 2: Load Data from GitHub ---
purchases_url = "https://raw.githubusercontent.com/pnascimento9596/ortho-implant-benchmarking/main/data/sample_data/hospital_purchases.csv"
catalog_url = "https://raw.githubusercontent.com/pnascimento9596/ortho-implant-benchmarking/main/data/sample_data/vendor_catalog.csv"
purchases = pd.read_csv(purchases_url)
catalog = pd.read_csv(catalog_url)
print("Purchases shape:", purchases.shape)
print("Catalog shape:", catalog.shape)
# --- Cell 3: Initial Inspection ---
print("\n--- Purchases Head ---")
print(purchases.head())
print("\n--- Purchases Info ---")
print(purchases.info())
print("\n--- Catalog Head ---")
print(catalog.head())
print("\n--- Catalog Info ---")
print(catalog.info())
# --- Cell 4: Explore Vendors and Categories ---
print("\n--- Vendors in Purchases ---")
print(purchases['vendor_name'].value_counts())
print("\n--- Product Categories ---")
print(purchases['product_category'].value_counts())
print("\n--- Vendors in Catalog ---")
print(catalog['vendor_name'].value_counts())
# --- Cell 5: Pricing Stats by Vendor ---
vendor_stats = purchases.groupby('vendor_name')['unit_price'].agg(['mean', 'min', 'max', 'count'])
print("\n--- Price Stats by Vendor ---")
print(vendor_stats)
# --- Cell 6: Data Quality Check ---
print("\n--- Missing Values (Purchases) ---")
print(purchases.isnull().sum())
print("\n--- Missing Values (Catalog) ---")
print(catalog.isnull().sum())
print("\n--- Duplicate Rows (Purchases) ---")
print(purchases.duplicated().sum())
print("\n--- Unique Equivalency Groups ---")
print("Purchases:", purchases['clinical_equivalency_group'].nunique())
print("Catalog:", catalog['clinical_equivalency_group'].nunique())
You now have a clear picture of the data landscape. You know which vendors to compare, what the pricing columns mean, and where the data has gaps. Next up, you'll filter down to your two target vendors and build the pricing comparison matrix.
Clean, Filter, and Build the Pricing Matrix
You now have a clear picture of your data landscape. You know which vendors appear most frequently, what product categories exist, and where the data quality gaps are.
In this step, you'll narrow the data to just two vendors and reshape it into a direct comparison matrix. This matrix is the core analytical artifact. It lines up clinically equivalent products side by side so a procurement team can instantly see which vendor is cheaper for each implant.
In this step, get ready to:
- Filter both DataFrames to the top 2 vendors and remove rows with missing prices.
- Build a pivot-table pricing matrix with price gap calculations.
- Enrich the matrix with product category, material, and FDA classification metadata.
Select two vendors and filter the data
A pricing comparison only works when you hold two competitors side by side. You need to select the two vendors with the most purchase volume so the comparison is statistically meaningful. You also need to remove any rows where the price is missing, since those would distort your averages.
- Create a new code cell in your notebook and filter to the top 2 vendors by running this code:
# --- Cell 7: Filter to Top 2 Vendors ---
top_vendors = purchases['vendor_name'].value_counts().head(2).index.tolist()
print(f"\nSelected vendors for comparison: {top_vendors}")
purchases_filtered = purchases[purchases['vendor_name'].isin(top_vendors)].copy()
catalog_filtered = catalog[catalog['vendor_name'].isin(top_vendors)].copy()
# Drop rows with missing prices
purchases_filtered = purchases_filtered.dropna(subset=['unit_price'])
catalog_filtered = catalog_filtered.dropna(subset=['contract_price'])
print(f"Filtered purchases: {purchases_filtered.shape[0]} rows")
print(f"Filtered catalog: {catalog_filtered.shape[0]} rows")
What does this code do?
The first line uses value_counts().head(2) to grab the names of the two vendors with the most purchase records. This is a dynamic selection, so the code adapts to whatever data is in the dataset.
The .isin(top_vendors) filter keeps only rows belonging to those two vendors. The .copy() prevents pandas warnings about modifying a filtered view.
Finally, dropna() removes any rows where the price is null. You cannot compare prices that do not exist.
- Run the cell by pressing Shift+Enter.
- You should see two vendor names printed (e.g., Stryker and DePuy Synthes) along with the filtered row counts for both DataFrames.
Seeing a KeyError or empty output?
Make sure you ran all the previous cells (Cells 1 through 6) first. The purchases and catalog DataFrames must already exist in memory.
If you see 0 rows after filtering, check that the column name vendor_name matches exactly. Column names are case-sensitive.
Help me debug my vendor filtering code
Build the pricing comparison matrix using pivot tables
A pivot table reshapes your data so each row represents a product (identified by its clinical equivalency group) and each column shows a vendor's average price. This layout makes it trivial to compare prices across vendors for the same product.
- Create a new code cell and build the pricing matrix by running this code:
# --- Cell 8: Build Pricing Comparison Matrix ---
pricing_matrix = purchases_filtered.pivot_table(
index='clinical_equivalency_group',
columns='vendor_name',
values='unit_price',
aggfunc='mean'
).reset_index()
# Rename columns for clarity
vendor_1 = top_vendors[0]
vendor_2 = top_vendors[1]
pricing_matrix.columns.name = None
# Calculate price gap
pricing_matrix['price_gap'] = pricing_matrix[vendor_1] - pricing_matrix[vendor_2]
pricing_matrix['price_gap_pct'] = (
(pricing_matrix[vendor_1] - pricing_matrix[vendor_2]) / pricing_matrix[vendor_1] * 100
).round(2)
pricing_matrix['cheaper_vendor'] = np.where(
pricing_matrix[vendor_1] <= pricing_matrix[vendor_2],
vendor_1,
vendor_2
)
# Drop rows where either vendor has no price (no comparison possible)
pricing_matrix = pricing_matrix.dropna(subset=[vendor_1, vendor_2])
print(f"\nPricing Matrix: {pricing_matrix.shape[0]} comparable products")
print(pricing_matrix.head(10))
What does this code do?
- pivot_table() groups all purchases by clinical_equivalency_group and calculates the mean unit_price per vendor. Each row becomes a product, each column becomes a vendor's average price.
- price_gap is the raw dollar difference (Vendor 1 minus Vendor 2). A positive value means Vendor 1 is more expensive.
- price_gap_pct expresses that gap as a percentage of Vendor 1's price, making it easy to spot big relative differences.
- cheaper_vendor labels each product with the winning vendor name. This powers the color-coding in Tableau later.
- The final dropna() removes products that only one vendor sells. You can only compare when both have a price.
- Run the cell by pressing Shift+Enter.
- You should see the number of comparable products printed, followed by a table showing the first 10 rows with vendor price columns, price_gap, price_gap_pct, and cheaper_vendor columns.
Seeing 0 comparable products?
This means no products have prices from both vendors. Check that Cell 7 produced non-zero filtered row counts for both DataFrames.
If you see a KeyError on the vendor name columns, confirm top_vendors still holds the correct values by printing it at the top of this cell.
Help me debug my pivot table pricing matrix
Enrich with category and material metadata
The pricing matrix currently only has product IDs and prices. To make it useful in Tableau, you need to attach descriptive metadata like product category, material type, and FDA classification. This lets you slice and filter the data by category in your dashboard.
- Create a new code cell and enrich the matrix with metadata by running this code:
# --- Cell 9: Enrich with Category Metadata ---
category_map = purchases_filtered.drop_duplicates(subset=['clinical_equivalency_group'])[
['clinical_equivalency_group', 'product_category', 'product_subcategory']
]
pricing_matrix = pricing_matrix.merge(category_map, on='clinical_equivalency_group', how='left')
# Add material and FDA class from catalog
material_map = catalog_filtered.drop_duplicates(subset=['clinical_equivalency_group'])[
['clinical_equivalency_group', 'fda_class', 'material']
]
pricing_matrix = pricing_matrix.merge(material_map, on='clinical_equivalency_group', how='left')
# Category summary
category_summary = purchases_filtered.groupby(['product_category', 'vendor_name'])['unit_price'].agg(
['mean', 'median', 'count']
).reset_index()
category_summary.columns = ['product_category', 'vendor_name', 'avg_price', 'median_price', 'purchase_count']
print("\n--- Category Summary ---")
print(category_summary)
What does this code do?
- category_map pulls the product_category and product_subcategory for each unique product from the purchases data. The drop_duplicates() ensures one row per product.
- material_map adds fda_class and material from the vendor catalog. These let you filter by Titanium vs Cobalt Chrome or by FDA Class II vs Class III in Tableau.
- category_summary aggregates average price, median price, and purchase count by category and vendor. This becomes a separate CSV for Tableau's category gap chart.
- Run the cell by pressing Shift+Enter.
- You should see the category summary table showing average price, median price, and purchase count broken down by product category (Hip, Knee, Spine, Trauma) and vendor.
- Verify your enriched matrix by adding one more line in a new cell:
print(f"Matrix shape: {pricing_matrix.shape}")
print(pricing_matrix.head(10))
You should see your matrix now includes product_category, product_subcategory, fda_class, and material columns alongside the vendor prices and gap calculations.
Seeing NaN values in the new columns?
Some NaN values in fda_class or material are normal. This happens when the catalog does not have metadata for every product in the purchases dataset. The how='left' join keeps all pricing matrix rows even if no catalog match exists.
If product_category is entirely NaN, confirm that purchases_filtered still has data by printing its shape. You may need to re-run Cell 7.
Help me debug my DataFrame merge
✔️ Awesome, I've got everything!
Great. Double check you have no unsaved changes in your notebook. Colab auto-saves, but you can press Cmd+S (macOS) or Ctrl+S (Windows) to force a save.
ⓧ I'd like to double check the full code
Here is the complete notebook code up to this point. Compare your cells against this:
# --- Cell 1: Import Libraries ---
import pandas as pd
import numpy as np
from sklearn.linear_model import LinearRegression
import matplotlib.pyplot as plt
# --- Cell 2: Load Data from GitHub ---
purchases_url = "https://raw.githubusercontent.com/pnascimento9596/ortho-implant-benchmarking/main/data/sample_data/hospital_purchases.csv"
catalog_url = "https://raw.githubusercontent.com/pnascimento9596/ortho-implant-benchmarking/main/data/sample_data/vendor_catalog.csv"
purchases = pd.read_csv(purchases_url)
catalog = pd.read_csv(catalog_url)
print("Purchases shape:", purchases.shape)
print("Catalog shape:", catalog.shape)
# --- Cell 3: Initial Inspection ---
print("\n--- Purchases Head ---")
print(purchases.head())
print("\n--- Purchases Info ---")
print(purchases.info())
print("\n--- Catalog Head ---")
print(catalog.head())
print("\n--- Catalog Info ---")
print(catalog.info())
# --- Cell 4: Explore Vendors and Categories ---
print("\n--- Vendors in Purchases ---")
print(purchases['vendor_name'].value_counts())
print("\n--- Product Categories ---")
print(purchases['product_category'].value_counts())
print("\n--- Vendors in Catalog ---")
print(catalog['vendor_name'].value_counts())
# --- Cell 5: Pricing Stats by Vendor ---
vendor_stats = purchases.groupby('vendor_name')['unit_price'].agg(['mean', 'min', 'max', 'count'])
print("\n--- Price Stats by Vendor ---")
print(vendor_stats)
# --- Cell 6: Data Quality Check ---
print("\n--- Missing Values (Purchases) ---")
print(purchases.isnull().sum())
print("\n--- Missing Values (Catalog) ---")
print(catalog.isnull().sum())
print("\n--- Duplicate Rows (Purchases) ---")
print(purchases.duplicated().sum())
print("\n--- Unique Equivalency Groups ---")
print("Purchases:", purchases['clinical_equivalency_group'].nunique())
print("Catalog:", catalog['clinical_equivalency_group'].nunique())
# --- Cell 7: Filter to Top 2 Vendors ---
top_vendors = purchases['vendor_name'].value_counts().head(2).index.tolist()
print(f"\nSelected vendors for comparison: {top_vendors}")
purchases_filtered = purchases[purchases['vendor_name'].isin(top_vendors)].copy()
catalog_filtered = catalog[catalog['vendor_name'].isin(top_vendors)].copy()
# Drop rows with missing prices
purchases_filtered = purchases_filtered.dropna(subset=['unit_price'])
catalog_filtered = catalog_filtered.dropna(subset=['contract_price'])
print(f"Filtered purchases: {purchases_filtered.shape[0]} rows")
print(f"Filtered catalog: {catalog_filtered.shape[0]} rows")
# --- Cell 8: Build Pricing Comparison Matrix ---
pricing_matrix = purchases_filtered.pivot_table(
index='clinical_equivalency_group',
columns='vendor_name',
values='unit_price',
aggfunc='mean'
).reset_index()
# Rename columns for clarity
vendor_1 = top_vendors[0]
vendor_2 = top_vendors[1]
pricing_matrix.columns.name = None
# Calculate price gap
pricing_matrix['price_gap'] = pricing_matrix[vendor_1] - pricing_matrix[vendor_2]
pricing_matrix['price_gap_pct'] = (
(pricing_matrix[vendor_1] - pricing_matrix[vendor_2]) / pricing_matrix[vendor_1] * 100
).round(2)
pricing_matrix['cheaper_vendor'] = np.where(
pricing_matrix[vendor_1] <= pricing_matrix[vendor_2],
vendor_1,
vendor_2
)
# Drop rows where either vendor has no price (no comparison possible)
pricing_matrix = pricing_matrix.dropna(subset=[vendor_1, vendor_2])
print(f"\nPricing Matrix: {pricing_matrix.shape[0]} comparable products")
print(pricing_matrix.head(10))
# --- Cell 9: Enrich with Category Metadata ---
category_map = purchases_filtered.drop_duplicates(subset=['clinical_equivalency_group'])[
['clinical_equivalency_group', 'product_category', 'product_subcategory']
]
pricing_matrix = pricing_matrix.merge(category_map, on='clinical_equivalency_group', how='left')
# Add material and FDA class from catalog
material_map = catalog_filtered.drop_duplicates(subset=['clinical_equivalency_group'])[
['clinical_equivalency_group', 'fda_class', 'material']
]
pricing_matrix = pricing_matrix.merge(material_map, on='clinical_equivalency_group', how='left')
# Category summary
category_summary = purchases_filtered.groupby(['product_category', 'vendor_name'])['unit_price'].agg(
['mean', 'median', 'count']
).reset_index()
category_summary.columns = ['product_category', 'vendor_name', 'avg_price', 'median_price', 'purchase_count']
print("\n--- Category Summary ---")
print(category_summary)
Your pricing matrix is ready. Each row is a directly comparable product, and the gap columns instantly reveal where money is being left on the table. Next up, you'll analyze pricing trends over time and project where prices are heading using linear regression.
Analyze Price Trends and Export Data
Your pricing matrix is built. You can now see which vendor is cheaper for each product. But procurement decisions are not just about today's prices.
In this step, you will analyze how prices are trending over time, project where they are heading using linear regression, and export everything for your Tableau dashboard.
In this step, get ready to:
- Analyze monthly pricing trends by vendor.
- Project future prices 3 months forward using linear regression.
- Export 3 analysis-ready CSV files and download them to your machine.
Analyze pricing trends over time
A static price comparison only tells half the story. Procurement teams need to know if a vendor's prices are climbing or dropping before locking in a contract.
- Create a new code cell in your notebook and add the following code to calculate monthly average prices per vendor:
# --- Cell 10: Price Trend Analysis ---
purchases_filtered['purchase_date'] = pd.to_datetime(purchases_filtered['purchase_date'])
purchases_filtered['year_month'] = purchases_filtered['purchase_date'].dt.to_period('M')
monthly_trends = purchases_filtered.groupby(['year_month', 'vendor_name'])['unit_price'].mean().reset_index()
monthly_trends['year_month'] = monthly_trends['year_month'].astype(str)
monthly_trends.columns = ['year_month', 'vendor_name', 'avg_price']
print("\n--- Monthly Trends ---")
print(monthly_trends.head(10))
What does this code do?
This cell transforms raw purchase dates into a monthly time series for each vendor.
- pd.to_datetime() converts the purchase_date strings into proper datetime objects so pandas can do time-based grouping.
- dt.to_period('M') collapses each date down to its month, creating a year_month column like 2024-01, 2024-02.
- The groupby calculates the average unit_price for each vendor in each month, giving you a clean trend line to analyze.
- Run the cell.
- You should see a table with columns year_month, vendor_name, and avg_price showing monthly averages for the first 10 rows.
Seeing a KeyError on purchase_date?
Make sure you are running this cell after Cell 7, where purchases_filtered is created. If you restarted the runtime, re-run all cells from Cell 1 onward.
Help me debug my price trend analysis cell.
Project future prices with linear regression
Monthly averages show where prices have been. A simple linear regression can show where they are heading.
Why linear regression instead of a full time-series model?
Advanced forecasting models like ARIMA or Prophet need months of dense historical data and significant tuning. Linear regression on monthly averages is a practical shortcut that produces a defensible short-term projection for procurement planning.
- Create a new code cell and add the following code to fit a linear model per vendor and project 3 months forward:
# --- Cell 11: Linear Regression Price Projection ---
trend_with_projection = []
for vendor in top_vendors:
vendor_data = monthly_trends[monthly_trends['vendor_name'] == vendor].copy()
vendor_data = vendor_data.sort_values('year_month').reset_index(drop=True)
vendor_data['month_num'] = range(1, len(vendor_data) + 1)
X = vendor_data[['month_num']].values
y = vendor_data['avg_price'].values
model = LinearRegression()
model.fit(X, y)
# Add actuals
for _, row in vendor_data.iterrows():
trend_with_projection.append({
'year_month': row['year_month'],
'vendor_name': vendor,
'avg_price': row['avg_price'],
'data_type': 'actual'
})
# Project 3 months forward
last_month_num = vendor_data['month_num'].max()
last_period = pd.Period(vendor_data['year_month'].iloc[-1], freq='M')
for i in range(1, 4):
future_month_num = last_month_num + i
projected_price = model.predict([[future_month_num]])[0]
future_period = last_period + i
trend_with_projection.append({
'year_month': str(future_period),
'vendor_name': vendor,
'avg_price': round(projected_price, 2),
'data_type': 'projected'
})
trends_df = pd.DataFrame(trend_with_projection)
print("\n--- Trends with Projections ---")
print(trends_df.tail(10))
What does this code do?
This cell loops through each vendor, fits a regression line to their monthly price history, then extends that line 3 months into the future.
- month_num assigns a simple numeric sequence (1, 2, 3...) to each month. This is the X input for the regression model.
- LinearRegression().fit(X, y) finds the best-fit straight line through the price data.
- model.predict() extends that line into future month numbers to get projected prices.
- The data_type column marks each row as actual or projected so Tableau can style them differently later.
- Run the cell.
- You should see the last 10 rows of trends_df printed. The final rows will have data_type set to projected with future month values.
Seeing a NameError on LinearRegression?
LinearRegression was imported in Cell 1 at the top of your notebook. If you restarted the Colab runtime, re-run Cell 1 first to reload the import.
Help me fix my linear regression projection cell.
Export and download CSV files for Tableau
Your analysis is complete inside Colab. Now you need to get the data out as CSV files that Tableau can read.
- Create a new code cell and add the following code to export all three DataFrames as CSV files:
# --- Cell 12: Export CSVs for Tableau ---
pricing_matrix.to_csv('pricing_matrix.csv', index=False)
trends_df.to_csv('pricing_trends.csv', index=False)
category_summary.to_csv('category_summary.csv', index=False)
print("\nExported: pricing_matrix.csv, pricing_trends.csv, category_summary.csv")
What does this code do?
- to_csv() writes each DataFrame to a CSV file in Colab's temporary file system.
- index=False prevents pandas from writing row numbers as an extra column. Tableau does not need them.
- Run the cell.
- You should see the confirmation message: Exported: pricing_matrix.csv, pricing_trends.csv, category_summary.csv.
The files are saved in Colab's runtime storage. Now you need to download them to your local machine.
- Create one more code cell and add the following code to trigger the downloads:
# --- Cell 13: Download Files ---
from google.colab import files
files.download('pricing_matrix.csv')
files.download('pricing_trends.csv')
files.download('category_summary.csv')
What does this code do?
The files.download() function is a Colab-specific utility that triggers your browser's download dialog for each file. This is how you move files from Colab's cloud runtime to your local machine.
- Run the cell.
- Your browser will download 3 CSV files. Check your Downloads folder to confirm all three are present.
Files not downloading?
Some browsers block multiple file downloads. If only 1 or 2 files appeared, re-run the cell and allow downloads when prompted by your browser.
If you see a FileNotFoundError, make sure Cell 12 ran successfully first. The export cell must create the files before you can download them.
Help me download my CSV files from Colab.
✔️ Awesome, I've got everything!
Great. Double check you have all three files in your Downloads folder before moving on.
ⓧ I'd like to double check the full code
Here is the complete notebook code up to and including this step. Compare it against what you have in your Colab notebook.
# --- Cell 1: Import Libraries ---
import pandas as pd
import numpy as np
from sklearn.linear_model import LinearRegression
import matplotlib.pyplot as plt
# --- Cell 2: Load Data from GitHub ---
purchases_url = "https://raw.githubusercontent.com/pnascimento9596/ortho-implant-benchmarking/main/data/sample_data/hospital_purchases.csv"
catalog_url = "https://raw.githubusercontent.com/pnascimento9596/ortho-implant-benchmarking/main/data/sample_data/vendor_catalog.csv"
purchases = pd.read_csv(purchases_url)
catalog = pd.read_csv(catalog_url)
print("Purchases shape:", purchases.shape)
print("Catalog shape:", catalog.shape)
# --- Cell 3: Initial Inspection ---
print("\n--- Purchases Head ---")
print(purchases.head())
print("\n--- Purchases Info ---")
print(purchases.info())
print("\n--- Catalog Head ---")
print(catalog.head())
print("\n--- Catalog Info ---")
print(catalog.info())
# --- Cell 4: Explore Vendors and Categories ---
print("\n--- Vendors in Purchases ---")
print(purchases['vendor_name'].value_counts())
print("\n--- Product Categories ---")
print(purchases['product_category'].value_counts())
print("\n--- Vendors in Catalog ---")
print(catalog['vendor_name'].value_counts())
# --- Cell 5: Pricing Stats by Vendor ---
vendor_stats = purchases.groupby('vendor_name')['unit_price'].agg(['mean', 'min', 'max', 'count'])
print("\n--- Price Stats by Vendor ---")
print(vendor_stats)
# --- Cell 6: Data Quality Check ---
print("\n--- Missing Values (Purchases) ---")
print(purchases.isnull().sum())
print("\n--- Missing Values (Catalog) ---")
print(catalog.isnull().sum())
print("\n--- Duplicate Rows (Purchases) ---")
print(purchases.duplicated().sum())
print("\n--- Unique Equivalency Groups ---")
print("Purchases:", purchases['clinical_equivalency_group'].nunique())
print("Catalog:", catalog['clinical_equivalency_group'].nunique())
# --- Cell 7: Filter to Top 2 Vendors ---
top_vendors = purchases['vendor_name'].value_counts().head(2).index.tolist()
print(f"\nSelected vendors for comparison: {top_vendors}")
purchases_filtered = purchases[purchases['vendor_name'].isin(top_vendors)].copy()
catalog_filtered = catalog[catalog['vendor_name'].isin(top_vendors)].copy()
# Drop rows with missing prices
purchases_filtered = purchases_filtered.dropna(subset=['unit_price'])
catalog_filtered = catalog_filtered.dropna(subset=['contract_price'])
print(f"Filtered purchases: {purchases_filtered.shape[0]} rows")
print(f"Filtered catalog: {catalog_filtered.shape[0]} rows")
# --- Cell 8: Build Pricing Comparison Matrix ---
pricing_matrix = purchases_filtered.pivot_table(
index='clinical_equivalency_group',
columns='vendor_name',
values='unit_price',
aggfunc='mean'
).reset_index()
# Rename columns for clarity
vendor_1 = top_vendors[0]
vendor_2 = top_vendors[1]
pricing_matrix.columns.name = None
# Calculate price gap
pricing_matrix['price_gap'] = pricing_matrix[vendor_1] - pricing_matrix[vendor_2]
pricing_matrix['price_gap_pct'] = (
(pricing_matrix[vendor_1] - pricing_matrix[vendor_2]) / pricing_matrix[vendor_1] * 100
).round(2)
pricing_matrix['cheaper_vendor'] = np.where(
pricing_matrix[vendor_1] <= pricing_matrix[vendor_2],
vendor_1,
vendor_2
)
# Drop rows where either vendor has no price (no comparison possible)
pricing_matrix = pricing_matrix.dropna(subset=[vendor_1, vendor_2])
print(f"\nPricing Matrix: {pricing_matrix.shape[0]} comparable products")
print(pricing_matrix.head(10))
# --- Cell 9: Enrich with Category Metadata ---
category_map = purchases_filtered.drop_duplicates(subset=['clinical_equivalency_group'])[
['clinical_equivalency_group', 'product_category', 'product_subcategory']
]
pricing_matrix = pricing_matrix.merge(category_map, on='clinical_equivalency_group', how='left')
# Add material and FDA class from catalog
material_map = catalog_filtered.drop_duplicates(subset=['clinical_equivalency_group'])[
['clinical_equivalency_group', 'fda_class', 'material']
]
pricing_matrix = pricing_matrix.merge(material_map, on='clinical_equivalency_group', how='left')
# Category summary
category_summary = purchases_filtered.groupby(['product_category', 'vendor_name'])['unit_price'].agg(
['mean', 'median', 'count']
).reset_index()
category_summary.columns = ['product_category', 'vendor_name', 'avg_price', 'median_price', 'purchase_count']
print("\n--- Category Summary ---")
print(category_summary)
# --- Cell 10: Price Trend Analysis ---
purchases_filtered['purchase_date'] = pd.to_datetime(purchases_filtered['purchase_date'])
purchases_filtered['year_month'] = purchases_filtered['purchase_date'].dt.to_period('M')
monthly_trends = purchases_filtered.groupby(['year_month', 'vendor_name'])['unit_price'].mean().reset_index()
monthly_trends['year_month'] = monthly_trends['year_month'].astype(str)
monthly_trends.columns = ['year_month', 'vendor_name', 'avg_price']
print("\n--- Monthly Trends ---")
print(monthly_trends.head(10))
# --- Cell 11: Linear Regression Price Projection ---
trend_with_projection = []
for vendor in top_vendors:
vendor_data = monthly_trends[monthly_trends['vendor_name'] == vendor].copy()
vendor_data = vendor_data.sort_values('year_month').reset_index(drop=True)
vendor_data['month_num'] = range(1, len(vendor_data) + 1)
X = vendor_data[['month_num']].values
y = vendor_data['avg_price'].values
model = LinearRegression()
model.fit(X, y)
# Add actuals
for _, row in vendor_data.iterrows():
trend_with_projection.append({
'year_month': row['year_month'],
'vendor_name': vendor,
'avg_price': row['avg_price'],
'data_type': 'actual'
})
# Project 3 months forward
last_month_num = vendor_data['month_num'].max()
last_period = pd.Period(vendor_data['year_month'].iloc[-1], freq='M')
for i in range(1, 4):
future_month_num = last_month_num + i
projected_price = model.predict([[future_month_num]])[0]
future_period = last_period + i
trend_with_projection.append({
'year_month': str(future_period),
'vendor_name': vendor,
'avg_price': round(projected_price, 2),
'data_type': 'projected'
})
trends_df = pd.DataFrame(trend_with_projection)
print("\n--- Trends with Projections ---")
print(trends_df.tail(10))
# --- Cell 12: Export CSVs for Tableau ---
pricing_matrix.to_csv('pricing_matrix.csv', index=False)
trends_df.to_csv('pricing_trends.csv', index=False)
category_summary.to_csv('category_summary.csv', index=False)
print("\nExported: pricing_matrix.csv, pricing_trends.csv, category_summary.csv")
# --- Cell 13: Download Files ---
from google.colab import files
files.download('pricing_matrix.csv')
files.download('pricing_trends.csv')
files.download('category_summary.csv')
Your data pipeline is complete. You have a pricing matrix, monthly trends with projections, and a category summary all saved locally. Next up, you will bring these files into Tableau and build an interactive dashboard that makes this analysis visual.
Build the Tableau Dashboard
Your analysis is complete and your CSV files are ready. Now it is time to bring the data to life visually.
In this step, you will connect your exported CSVs to Tableau Desktop and build an interactive pricing intelligence dashboard. The dashboard will combine a product-by-product pricing matrix, a category gap chart, and a trend projection chart into a single view that a procurement team can explore.
In this step, get ready to:
- Connect all three CSV data sources to Tableau Desktop.
- Build three worksheet visualizations (pricing matrix, category gaps, price trends).
- Assemble an interactive dashboard with a cross-sheet category filter.
Connect Tableau to your data
Each of your three CSVs serves a different analytical purpose. You will load them as separate data sources so each worksheet can draw from the right dataset.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Tableau and press Enter to open Tableau Desktop.
- On the Start page, in the Connect pane on the left, click Text File.
- Navigate to the folder where you downloaded your CSVs and select pricing_matrix.csv.
- Click Open.
Tableau opens the Data Source page and shows a preview of your pricing matrix data.
What is the Data Source page?
This page shows a preview of the first 1,000 rows in your file. Tableau automatically detects column types: text columns become dimensions (blue) and numeric columns become measures (green). You can verify your data loaded correctly here before building any views.
- Verify that numeric columns like price_gap and price_gap_pct show as number types (indicated by a # icon).
- Verify that text columns like clinical_equivalency_group and cheaper_vendor show as string types (indicated by an Abc icon).
Now add the second data source.
- In the top menu bar, click Data.
- Click New Data Source.
- In the Connect pane, click Text File again.
- Select pricing_trends.csv and click Open.
- Confirm that avg_price is detected as a number and year_month is detected as a string.
Repeat the process for the third file.
- Click Data > New Data Source once more.
- Click Text File, select category_summary.csv, and click Open.
- Confirm that avg_price, median_price, and purchase_count appear as numbers.
Why three separate data sources?
Each CSV has a different grain (level of detail). The pricing matrix has one row per product, the trends file has one row per month per vendor, and the category summary has one row per category per vendor. Keeping them as separate data sources lets each worksheet query the right level of detail without joins distorting the aggregation.
Columns showing the wrong type?
If a numeric column shows as text, click the data type icon above the column on the Data Source page and change it to Number (decimal). This can happen if any cell contained a stray space or special character.
help me fix column data types in Tableau
Build the pricing matrix view (Sheet 1)
The pricing matrix is the heart of the dashboard. It shows every comparable product as a row with side-by-side bars for each vendor's price, instantly revealing who is cheaper.
- Click the Sheet 1 tab at the bottom of the Tableau window to open a blank worksheet.
- Right-click the Sheet 1 tab and select Rename.
- Type Pricing Matrix and press Enter.
- In the Data pane on the left, make sure the pricing_matrix data source is selected at the top.
- Drag Clinical Equivalency Group from the Dimensions section to the Rows shelf.
- Drag both vendor price measure columns (the two columns named after each vendor) to the Columns shelf, one at a time.
You should now see a horizontal bar chart with one row per product and two bars per row (one per vendor).
- Drag Cheaper Vendor to the Color card on the Marks shelf.
- Drag Price Gap Pct to the Label card on the Marks shelf.
- Click the sort descending icon on the toolbar (or right-click the Clinical Equivalency Group field on Rows and select Sort) to sort by price gap with the biggest differences at the top.
What does the color encoding show?
Each bar is colored by which vendor has the lower price for that product. This lets a procurement analyst glance at the chart and instantly see the dominant vendor by color. Products with a large price_gap_pct label are the highest-priority candidates for renegotiation or switching.
Only seeing one bar per row?
Make sure you dragged both vendor price columns individually to the Columns shelf. If they ended up on the same axis, click the dropdown arrow on one of the pills on Columns and select Dual Axis or remove and re-add them as separate pills.
help me show two vendor columns as side-by-side bars in Tableau
Build the category gap chart and trend chart, then assemble the dashboard
With the matrix done, you will build two more worksheets and then combine all three into a single interactive dashboard.
- Click the new worksheet icon (the small tab with a + at the bottom of the screen) to create a new sheet.
- Rename this sheet to Category Gaps.
- In the Data pane, click the category_summary data source to switch to it.
- Drag Product Category to Columns.
- Drag Avg Price to Rows.
- Drag Vendor Name to the Color card on the Marks shelf.
You should see a grouped bar chart with one color per vendor, comparing average prices across Hip, Knee, Spine, and Trauma categories.
- Create another new worksheet and rename it to Price Trends.
- In the Data pane, click the pricing_trends data source to switch to it.
- Drag Year Month to Columns.
- Drag Avg Price to Rows.
- Drag Vendor Name to the Color card.
- Drag Data Type to the Line Style or Detail card on the Marks shelf so Tableau distinguishes actual from projected values.
Make projected lines dashed
To visually separate projected data from actuals, right-click the Data Type legend or drag it to the Path card. Then click the line style options and assign a dashed pattern to the "projected" value and a solid line to "actual". This tells stakeholders exactly where historical data ends and the forecast begins.
Now assemble all three sheets into a dashboard.
- Click the New Dashboard icon at the bottom of the screen (the icon with a grid pattern and a +).
- In the Dashboard pane on the left, you will see your three sheets listed under Sheets.
- Drag Pricing Matrix onto the dashboard canvas.
- Drag Category Gaps to the right of the Pricing Matrix (or below it, depending on your preferred layout).
- Drag Price Trends below the other two sheets.
- Double-click the dashboard title area at the top and type Orthopedic Implant Pricing Intelligence.
Finally, add a cross-sheet filter so selecting a category filters all three views simultaneously.
- On the dashboard, click the Category Gaps chart to select it.
- Click the small dropdown arrow in the upper-right corner of that sheet on the dashboard.
- Select Filters > Product Category.
- A filter card appears on the dashboard. Click the dropdown arrow on the filter card and select Apply to Worksheets > All Using Related Data Sources.
How does the cross-sheet filter work?
When you apply a filter to all worksheets using related data sources, Tableau checks for matching field names across your data sources. Because product_category exists in your pricing matrix and category summary data, selecting a category (like "Knee") filters both those sheets. The trends sheet filters too if it has a related dimension.
If a sheet does not respond to the filter, you can manually add product_category to its Filters shelf and configure it there.
Test the dashboard by selecting a single category from the filter card. All three views should update to show data for that category only.
Filter not affecting all sheets?
If one of the sheets does not respond to the filter, the field name might not match across data sources. Go to Data > Edit Relationships and manually map product_category between the affected data sources.
help me apply a dashboard filter across multiple data sources in Tableau
Your pricing intelligence dashboard is live. A procurement team can now explore vendor pricing by product and category, spot trends, and identify where the biggest savings opportunities exist.
Secret mission
Savings Projection Panel
Your dashboard shows pricing comparisons and trends, but procurement teams need one more thing: the dollar amount they could save by switching vendors. Build a savings projection panel that quantifies the total annual cost reduction opportunity across all product categories.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally and on Google Colab's free tier, so there are no ongoing costs. Decide whether to keep your resources for future reference, or delete them to free up space.
Resources you used:
- Google Colab notebook (stored in your Google Drive, no cost).
- Tableau workbook (local file on your machine).
- CSV files: pricing_matrix.csv, pricing_trends.csv, category_summary.csv, savings_summary.csv (local downloads, no cost).
Keep everything running
No action needed. Choose this if you plan to revisit the analysis, update it with new procurement data, or use the Tableau dashboard as a reference for future projects.
- Your Google Colab notebook stays in Google Drive and is accessible anytime.
- Your Tableau workbook and CSV files remain on your local machine.
- None of these resources incur any charges.
Pause - I'll come back to this later
Since all resources are either in Google Drive or saved locally, there is nothing to pause. Everything stays as-is at no cost until you return.
- Close the Colab browser tab when you are done working.
- Close Tableau Desktop.
- Your notebook and workbook will be exactly where you left them when you come back.
Delete - I don't want to use this again
Remove all project resources from your Google Drive and local machine.
- Delete the Google Colab notebook: open Google Drive, find Ortho_Implant_Pricing_Analysis in the Colab Notebooks folder, right-click it, and select Move to trash.
- Delete the local CSV files and Tableau workbook. On macOS, open Terminal and run the command below. On Windows, open PowerShell and use the equivalent command.
macOS/Linux:
cd ~/Downloads
rm -f pricing_matrix.csv pricing_trends.csv category_summary.csv savings_summary.csv
Windows (PowerShell):
cd ~\Downloads
Remove-Item pricing_matrix.csv, pricing_trends.csv, category_summary.csv, savings_summary.csv
- Delete the Tableau workbook file from wherever you saved it on your machine (check your Documents or Downloads folder).
- Empty your Google Drive trash to permanently remove the notebook.
Nice Work!
Nice Work!
You just built a complete healthcare procurement pricing intelligence dashboard from scratch. A hospital procurement team can now open your Tableau workbook and instantly see which orthopedic implant vendor is cheaper, by how much, and where prices are heading.
You've learned how to:
- Clean, filter, and normalize raw procurement data using pandas in Google Colab, then build a product-by-product pricing comparison matrix with price gap calculations across two medical device manufacturers.
- Apply linear regression from scikit-learn to project future pricing trends and export analysis-ready CSVs for visualization.
- Build an interactive Tableau dashboard combining a pricing matrix view, category gap analysis, and trend projections with cross-filtering across all sheets.
- Secret Mission: Built a savings projection panel quantifying the total annual cost reduction opportunity by switching to lower-cost clinically equivalent alternatives.
Ready to quiz yourself?