The US has a peculiar relationship with guns where we frequently observe nontrivial spikes in firearm sales. These are triggered (pun intended) by various political, economic, and social events at the time.

With 2020 being an especially chaotic year, I wanted to explore how that phenomenon is reflected in American gun purchases thus far.

Load modules

import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

Retrieve data

I was unable to find confirmed and accurate gun purchase data that is free on the web. However, we can use data from the FBI’s National Instant Criminal Background Check System (NICS) as a proxy.

Note #1: the assumption we are making here is most background checks will result in the sale of a firearm.

The NICS data is stored in PDF format but thanks to the BuzzFeed News team they already wrote the code to parse and sanitize these files. What is even more amazing is how they went the extra mile to update these statistics on a regular basis.

nics_raw = pd.read_csv("https://raw.githubusercontent.com/BuzzFeedNews/nics-firearm-background-checks/master/data/nics-firearm-background-checks.csv")

nics = nics_raw

nics.month = pd.to_datetime(nics.month, infer_datetime_format=True)

Let’s verify what variables are at our disposal:

nics_raw.dtypes
## month                        datetime64[ns]
## state                                object
## permit                              float64
## permit_recheck                      float64
## handgun                             float64
## long_gun                            float64
## other                               float64
## multiple                              int64
## admin                               float64
## prepawn_handgun                     float64
## prepawn_long_gun                    float64
## prepawn_other                       float64
## redemption_handgun                  float64
## redemption_long_gun                 float64
## redemption_other                    float64
## returned_handgun                    float64
## returned_long_gun                   float64
## returned_other                      float64
## rentals_handgun                     float64
## rentals_long_gun                    float64
## private_sale_handgun                float64
## private_sale_long_gun               float64
## private_sale_other                  float64
## return_to_seller_handgun            float64
## return_to_seller_long_gun           float64
## return_to_seller_other              float64
## totals                                int64
## dtype: object

With this many variables our analysis can go a million different ways. We’ll limit our scope to a few dimensions and keep them in mind for a future article.

What is the trend in firearm background checks?

nics_total = nics[['month', 'totals']]
nics_total = nics_total.groupby('month').agg('sum').reset_index()

plt.figure(figsize=(15, 10))

sns.lineplot(x='month', y='totals', data=nics_total, marker='o', color='seagreen')
plt.title("Total NICS Firearm Background Checks")
plt.xlabel("")
plt.ylabel("")
plt.figtext(0.9, 0.05, "by: @eeysirhc", horizontalalignment="right")
plt.figtext(0.9, 0.03, "source: Federal Bureau of Investigation", horizontalalignment="right")

plt.show()

It is quite apparent the 2000s have a steady flow of background checks but that gradually increased over time from 2008 onward. My mind immediately recalls a few major events during those time frames:

  • 2000 to 2008 - the Bush administration and the country as a whole was focused on a post-9/11 world with two foreign wars in the Middle East
  • 2008 to 2016 - mass shootings plagued the country with the Obama administration attempting to drive domestic policy related to gun control
  • 2016 to present - background checks are fairly stable throughout the Trump administration but there is a significant jump in 2020

What type of gun receives the most background checks?

nics_guntype = nics[['month', 'handgun', 'long_gun']]
nics_guntype = nics_guntype.groupby('month').agg('sum').reset_index()

nics_guntype_parsed = (pd.melt(nics_guntype, id_vars=['month'], 
    value_vars=['handgun', 'long_gun'], var_name='type'))

plt.figure(figsize=(15, 10))

sns.lineplot(x='month', y='value', hue='type', data=nics_guntype_parsed, marker='o')
plt.title("Total NICS Firearm Background Checks by Type")
plt.xlabel("")
plt.ylabel("")
plt.figtext(0.9, 0.05, "by: @eeysirhc", horizontalalignment="right")
plt.figtext(0.9, 0.03, "source: Federal Bureau of Investigation", horizontalalignment="right")

plt.show()

According to this chart, it appears long guns have remained relatively stable for the past two decades. In contrast, we observe an increase in handgun purchases from 2008, surpassing long guns by 2015, and exhibiting a surge in early 2020.

What is the ratio of handgun to long guns?

To better compare the two types of guns we can normalize our values as a ratio: how many handguns are purchased for each long gun (and vice versa)?

nics_ratio = nics_guntype
nics_ratio['handgun_ratio'] = nics_guntype['handgun'] / nics_guntype['long_gun']
nics_ratio['longgun_ratio'] = nics_guntype['long_gun'] / nics_guntype['handgun']

nics_ratio = (pd.melt(nics_ratio, id_vars=['month'], 
    value_vars=['handgun_ratio', 'longgun_ratio'], var_name='type'))

plt.figure(figsize=(15, 10)) 

sns.lineplot(x='month', y='value', hue='type', data=nics_ratio, marker='o')
plt.title("NICS Firearm Background Checks by Ratio of Firearm Type")
plt.xlabel("")
plt.ylabel("")
plt.figtext(0.9, 0.05, "by: @eeysirhc", horizontalalignment="right")
plt.figtext(0.9, 0.03, "source: Federal Bureau of Investigation", horizontalalignment="right")
plt.legend()

plt.show()

This is quite interesting: even though total long gun background checks have remained flat, the ratio has fallen dramatically.

Indeed, just 20 years ago Americans were purchasing 4-6 long guns for every handgun. By 2020 this trend has flipped where we get about 4 handguns for every long gun!

What caused the influx of gun sales in 2020?

nics_thisyear = nics_guntype_parsed[nics_guntype_parsed['month'] >= '2019-01-01']

plt.figure(figsize=(15, 10)) 

sns.lineplot(x='month', y='value', hue='type', data=nics_thisyear, marker='o');
plt.title("NICS Firearm Background Checks by Type since 2019")
plt.axvline(x="2020-03-01", color="grey", linestyle='dashed')
plt.axvline(x="2020-06-01", color="grey", linestyle='dashed')
plt.xlabel("")
plt.ylabel("")
plt.ylim(bottom=0, top=2e6);
plt.figtext(0.9, 0.05, "by: @eeysirhc", horizontalalignment="right")
plt.figtext(0.9, 0.03, "source: Federal Bureau of Investigation", horizontalalignment="right")

plt.show()

The plot above highlights two major spikes in firearm sales - especially for handguns.

If we focus just on March there is a +92% year-over-year increase for handguns and +74% for long guns.

nics_guntype['index_year'] = pd.DatetimeIndex(nics_guntype['month']).year
nics_guntype['index_month'] = pd.DatetimeIndex(nics_guntype['month']).month
nics_guntype = nics_guntype[['month', 'handgun', 'long_gun', 'index_month']]

(nics_guntype[nics_guntype['index_month'] == 3]
    .sort_values('month', ascending=False).head())
##          month    handgun  long_gun  index_month
## 256 2020-03-01  1392677.0  758073.0            3
## 244 2019-03-01   727226.0  435698.0            3
## 232 2018-03-01   781452.0  540979.0            3
## 220 2017-03-01   751866.0  461273.0            3
## 208 2016-03-01   736630.0  430837.0            3

So, how are Americans coping with 2020 thus far?

We are buying lots and lots of guns which, admittedly, is 100% expected.

Moment of zen

And just for fun…

nics_state = (nics[['month', 'state', 'handgun', 'long_gun']]
    .groupby(['month', 'state']).agg('sum').reset_index())

nics_state['handgun_ratio'] = nics_state['handgun'] / nics_state['long_gun']
nics_state['longgun_ratio'] = nics_state['long_gun'] / nics_state['handgun']

states = ['California', 'Tennessee', 'Vermont', 'New Jersey', 'Texas', 'Arizona', 'Georgia', 'Florida', 'Alabama']

nics_state_filter = nics_state[nics_state['state'].isin(states)]

nics_state_parsed = (pd.melt(nics_state_filter, id_vars=['month', 'state'],
    value_vars=['handgun_ratio', 'longgun_ratio'], var_name='type'))

plt.figure(figsize=(15,10))

p = sns.FacetGrid(nics_state_parsed, col="state", hue="type", col_wrap=3)
p = (p.map(plt.plot, "month", "value").add_legend()
    .set_xlabels("").set_ylabels(""))
plt.figtext(0.95, 0.02, "by: @eeysirhc", horizontalalignment="right")
plt.figtext(0.95, 0.00, "source: Federal Bureau of Investigation", horizontalalignment="right")

plt.show()