Analyzing Dealer Markups of Popular New Cars

Over the past year and a half, new cars have become dramatically more expensive. While changes in product mix, constraints in part supplies, and overall increases in quality have played into that, dealer markups have also become a prominent factor - especially with highly-demanded models.

New Car Price Index

Economic Background

With exciting cars like the Ford Bronco and new Corvette having hit the market, dealers have found themselves in an interesting position - sole vendors of an in-demand good, with limited supply. It doesn’t take an Econ major to see the outcome here -> dealers have been able to apply “market adjustment” fees on top of MSRP (crucially - which enough people will still pay!) to maximize their earnings per sale, much to the chagrin of their OEMs.

Getting Data to Analyze

To help us understand the scale of this trend, we can use data from Markups.org, a site that crowdsources dealer pricing from prospective car purchasers. After scraping all of the submissions on the site I could find, I was left with about 5,400 records to make sense of.

It’ll be no surprise to frequent readers of this blog that I immediately wanted to build visuals of the primary rollups of the data - so I went ahead and looked specifically at which models and makes have received the A) highest volume of markup reports, and B) highest dollar value of markups on average.

Take a look at the findings below, and keep in mind that there is definitely a significant bias in what random consumers choose to submit or not submit to Markups.org, so these trends should be taken directionally!

Visualizations

Makes with the Highest Markups

Models with the Highest Markups

Average Markup and Volume by Model

Code Resources

Fetch Markups.org Site

import requests
import pandas as pd
import requests

def fetch_results(page):
  base_url = "https://markups.org/all?items_per_page=100&page=%s" % page
  r = requests.get(base_url)
  return r.content

Grab Listing Info from Each Page

from bs4 import BeautifulSoup

def get_listings(page):
  soup = BeautifulSoup(fetch_results(page), 'html.parser')
  divs = soup.find_all("div", {"class" : "tw-flex tw-flex-wrap"})
  rows = []
  for div in divs:
    temp = []
    for string in div.stripped_strings:
      if string == 'Dealer Name:' or string == 'Markup:' or string == 'Location:' or string == 'Year':
        pass 
      else:
        temp.append(string.replace(': ', ''))
    if len(temp) == 6:
      rows.append(temp)
  return rows

Build Dataframe of Listings

data = []

for i in range(1, 55):
  data += get_listings(i)

df = pd.DataFrame(data, columns = ['dealer', 'price', 'state', 'year', 'make', 'model'])

Example of ggplot Visualization

library(scales)
library(tidyverse)

by_model %>% 
  filter(count >= 3) %>%
  head(30) %>% 
  slice_max(avg_markup, n = 10) %>%
  ggplot(aes(x = reorder(model, avg_markup), y = avg_markup, label = paste("$", round(avg_markup / 1000, 1), "k", sep = ""))) + 
    geom_col(fill = '#003f5c', alpha = 0.8) +
    geom_text(hjust = -0.1, size = 3, fontface = "bold", color = "grey20") + 
    coord_flip() + 
    scale_y_continuous(labels=scales::dollar_format(), breaks = pretty_breaks(n = 10), limits = c(0, 125000)) +
    theme_minimal() +
    theme(
      plot.title = element_text(size = 20, face = "bold"),
      plot.subtitle = element_text(size = 12),
      plot.caption = element_text(size = 8, colour = "grey60")
    ) +
    labs(
      title = "Which Models do Dealers Mark Up the Most?",
      subtitle = "Amidst cars with three or more reports, the new Hummer EV, 911 GT3, and G63\nsee the largest dealer markups over MSRP",
      x = "",
      y = "\nAverage Markup on top of MSRP",
      caption = "Data courtesy of Markups.org\nconormclaughlin.net"
    )