Investigate NICS and US Census Data

Dan Peck
September 16, 2020

Table of Contents

Introduction

The first dataset I'm going to explore is from the FBI's National Instant Criminal Background Check System (NICS). NICS is used to check eligibility for the purchase and possession of firearms and explosives. More information about NICS can be found at the official site: https://www.fbi.gov/services/cjis/nics. The dataset I'll be using is composed of the number of background checks conducted by month and state for various kinds of transactions, such as obtaining a permit or buying one or more firearms. The dataset comes with an important disclaimer from the FBI that the data is for the number of background checks initiated, not the number of firearms sold (https://www.fbi.gov/file-repository/nics_firearm_checks_-_month_year_by_state_type.pdf/view):

It is important to note that the statistics within this chart represent the number of firearm background checks initiated through the NICS. They do not represent the number of firearms sold. Based on varying state laws and purchase scenarios, a one-to-one correlation cannot be made between a firearm background check and a firearm sale [emphasis added].

To supplement the data from NICS, I will also be exploring US Census data. This dataset contains census data for each state in 2010 and estimations for 2016, as well as various demographic information during that timespan. I plan to use the census data to calculate the number of NICS background checks conducted per capita for each state, then investigate the relationships between background checks per capita and several educational and economic factors.

Specifically, my variables and research questions are as follows:

Dependent Variables:

  • The total number of background checks conducted
  • The total number of background checks conducted per capita

Independent Variables:

  • Time factors - spanning from 1998 to 2017, comparing 2010 and 2016
  • Educational factors - percent of high school graduates, percent of four-year college graduates
  • Economic factors - median household income, per capita income, percent of persons in poverty

Research Questions:

  1. What is the overall trend in the country for background checks over time?
  2. Which states conduct the highest number of background checks per capita?
  3. Which states are experiencing the largest increase in background checks per capita?
  4. How are educational factors correlated with background checks per capita?
  5. How are economic factors correlated with background checks per capita?

This project was created with python 3.7.7 and makes use of the NumPy, Pandas, Matplotlib, and Seaborn libraries.

In [1]:
import numpy as np
import pandas as pd

import matplotlib.pyplot as plt
import seaborn as sns
%pylab inline
%matplotlib inline
Populating the interactive namespace from numpy and matplotlib

Data Wrangling and Cleaning

I will start by importing the raw CSV files using the pandas read_csv function. While inspecting the CSV files in excel prior to starting the project, I noticed the census data had several missing values indicated by letters under "Value Flags." I will replace these letters with NANs using the na_values parameter.

In [2]:
raw_nics_data = pd.read_csv('gun-data.csv')
raw_cen_data = pd.read_csv('u.s.-census-data.csv', na_values=['D', 'F', 'FN', 'S', 'X', 'Z'])

NANs are classified as floats in pandas, so I will convert the raw census dataframe to string data types for easier manipulation later.

In [3]:
raw_cen_data = raw_cen_data.applymap(str)

Next, I will check the raw NICS data for structure and cleanliness. I notice that the range of dates in 'month' is from November 1998 to September 2017. From previewing the census data in Excel, I know I am primarily interested in the years 2010 and 2016, so I will need to aggregate the NICS data by year later on.

In [4]:
raw_nics_data.head()
Out[4]:
month state permit permit_recheck handgun long_gun other multiple admin prepawn_handgun ... returned_other rentals_handgun rentals_long_gun private_sale_handgun private_sale_long_gun private_sale_other return_to_seller_handgun return_to_seller_long_gun return_to_seller_other totals
0 2017-09 Alabama 16717.0 0.0 5734.0 6320.0 221.0 317 0.0 15.0 ... 0.0 0.0 0.0 9.0 16.0 3.0 0.0 0.0 3.0 32019
1 2017-09 Alaska 209.0 2.0 2320.0 2930.0 219.0 160 0.0 5.0 ... 0.0 0.0 0.0 17.0 24.0 1.0 0.0 0.0 0.0 6303
2 2017-09 Arizona 5069.0 382.0 11063.0 7946.0 920.0 631 0.0 13.0 ... 0.0 0.0 0.0 38.0 12.0 2.0 0.0 0.0 0.0 28394
3 2017-09 Arkansas 2935.0 632.0 4347.0 6063.0 165.0 366 51.0 12.0 ... 0.0 0.0 0.0 13.0 23.0 0.0 0.0 2.0 1.0 17747
4 2017-09 California 57839.0 0.0 37165.0 24581.0 2984.0 0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 123506

5 rows × 27 columns

In [5]:
raw_nics_data.tail()
Out[5]:
month state permit permit_recheck handgun long_gun other multiple admin prepawn_handgun ... returned_other rentals_handgun rentals_long_gun private_sale_handgun private_sale_long_gun private_sale_other return_to_seller_handgun return_to_seller_long_gun return_to_seller_other totals
12480 1998-11 Virginia 0.0 NaN 14.0 2.0 NaN 8 0.0 NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN 24
12481 1998-11 Washington 1.0 NaN 65.0 286.0 NaN 8 1.0 NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN 361
12482 1998-11 West Virginia 3.0 NaN 149.0 251.0 NaN 5 0.0 NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN 408
12483 1998-11 Wisconsin 0.0 NaN 25.0 214.0 NaN 2 0.0 NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN 241
12484 1998-11 Wyoming 8.0 NaN 45.0 49.0 NaN 5 0.0 NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN 107

5 rows × 27 columns

In [6]:
raw_nics_data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 12485 entries, 0 to 12484
Data columns (total 27 columns):
 #   Column                     Non-Null Count  Dtype  
---  ------                     --------------  -----  
 0   month                      12485 non-null  object 
 1   state                      12485 non-null  object 
 2   permit                     12461 non-null  float64
 3   permit_recheck             1100 non-null   float64
 4   handgun                    12465 non-null  float64
 5   long_gun                   12466 non-null  float64
 6   other                      5500 non-null   float64
 7   multiple                   12485 non-null  int64  
 8   admin                      12462 non-null  float64
 9   prepawn_handgun            10542 non-null  float64
 10  prepawn_long_gun           10540 non-null  float64
 11  prepawn_other              5115 non-null   float64
 12  redemption_handgun         10545 non-null  float64
 13  redemption_long_gun        10544 non-null  float64
 14  redemption_other           5115 non-null   float64
 15  returned_handgun           2200 non-null   float64
 16  returned_long_gun          2145 non-null   float64
 17  returned_other             1815 non-null   float64
 18  rentals_handgun            990 non-null    float64
 19  rentals_long_gun           825 non-null    float64
 20  private_sale_handgun       2750 non-null   float64
 21  private_sale_long_gun      2750 non-null   float64
 22  private_sale_other         2750 non-null   float64
 23  return_to_seller_handgun   2475 non-null   float64
 24  return_to_seller_long_gun  2750 non-null   float64
 25  return_to_seller_other     2255 non-null   float64
 26  totals                     12485 non-null  int64  
dtypes: float64(23), int64(2), object(2)
memory usage: 2.6+ MB

I just need the columns for month, state, and totals, so I will make a new dataframe with these columns and convert the 'month' column to datetimes.

In [7]:
nics_data = raw_nics_data[["month", "state", "totals"]].copy()
nics_data["month"] = pd.to_datetime(nics_data["month"])
In [8]:
nics_data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 12485 entries, 0 to 12484
Data columns (total 3 columns):
 #   Column  Non-Null Count  Dtype         
---  ------  --------------  -----         
 0   month   12485 non-null  datetime64[ns]
 1   state   12485 non-null  object        
 2   totals  12485 non-null  int64         
dtypes: datetime64[ns](1), int64(1), object(1)
memory usage: 292.7+ KB

I'm primarily interested in NICS data from the years 2010 and 2016, so I will create two dataframes for totals from each year, then merge them together.

In [9]:
year_2010 = nics_data['month'].dt.year == 2010
nics_data_2010 = nics_data[year_2010]
In [10]:
nics_data_2010 = nics_data_2010.groupby('state').sum()
nics_data_2010.rename(columns={"totals": "2010 NICS totals"}, inplace=True)
nics_data_2010.head()
Out[10]:
2010 NICS totals
state
Alabama 308607
Alaska 65909
Arizona 206050
Arkansas 191448
California 816399
In [11]:
year_2016 = nics_data['month'].dt.year == int(2016)
nics_data_2016 = nics_data[year_2016]
In [12]:
nics_data_2016 = nics_data_2016.groupby('state').sum()
nics_data_2016.rename(columns={"totals": "2016 NICS totals"}, inplace=True)
nics_data_2016.head()
Out[12]:
2016 NICS totals
state
Alabama 616947
Alaska 87647
Arizona 416279
Arkansas 266014
California 2377167
In [13]:
nics_data = nics_data_2010.merge(nics_data_2016, left_index=True, right_index=True)
nics_data.index.name = None
nics_data.head()
Out[13]:
2010 NICS totals 2016 NICS totals
Alabama 308607 616947
Alaska 65909 87647
Arizona 206050 416279
Arkansas 191448 266014
California 816399 2377167

Later, I will merge this NICS data dataframe with a cleansed subset of the census data.

Now I will check the raw census data for structure and cleanliness. Right away, I notice the dataframe needs to be transposed to put the states as the row index in order to match the NICS data. I'll also set the column headers to the proper descriptions in the first row and drop the first two rows.

In [14]:
raw_cen_data.head()
Out[14]:
Fact Fact Note Alabama Alaska Arizona Arkansas California Colorado Connecticut Delaware ... South Dakota Tennessee Texas Utah Vermont Virginia Washington West Virginia Wisconsin Wyoming
0 Population estimates, July 1, 2016, (V2016) nan 4,863,300 741,894 6,931,071 2,988,248 39,250,017 5,540,545 3,576,452 952,065 ... 865454 6651194 27,862,596 3,051,217 624,594 8,411,808 7,288,000 1,831,102 5,778,708 585,501
1 Population estimates base, April 1, 2010, (V2... nan 4,780,131 710,249 6,392,301 2,916,025 37,254,522 5,029,324 3,574,114 897,936 ... 814195 6346298 25,146,100 2,763,888 625,741 8,001,041 6,724,545 1,853,011 5,687,289 563,767
2 Population, percent change - April 1, 2010 (es... nan 1.70% 4.50% 8.40% 2.50% 5.40% 10.20% 0.10% 6.00% ... 0.063 0.048 10.80% 10.40% -0.20% 5.10% 8.40% -1.20% 1.60% 3.90%
3 Population, Census, April 1, 2010 nan 4,779,736 710,231 6,392,017 2,915,918 37,253,956 5,029,196 3,574,097 897,934 ... 814180 6346105 25,145,561 2,763,885 625,741 8,001,024 6,724,540 1,852,994 5,686,986 563,626
4 Persons under 5 years, percent, July 1, 2016, ... nan 6.00% 7.30% 6.30% 6.40% 6.30% 6.10% 5.20% 5.80% ... 0.071 0.061 7.20% 8.30% 4.90% 6.10% 6.20% 5.50% 5.80% 6.50%

5 rows × 52 columns

In [15]:
trans_cen_data = raw_cen_data.transpose()
In [16]:
trans_cen_data.columns = trans_cen_data.iloc[0]
trans_cen_data.drop(['Fact', 'Fact Note'], inplace=True)
trans_cen_data.columns.name = None
In [17]:
trans_cen_data.head()
Out[17]:
Population estimates, July 1, 2016, (V2016) Population estimates base, April 1, 2010, (V2016) Population, percent change - April 1, 2010 (estimates base) to July 1, 2016, (V2016) Population, Census, April 1, 2010 Persons under 5 years, percent, July 1, 2016, (V2016) Persons under 5 years, percent, April 1, 2010 Persons under 18 years, percent, July 1, 2016, (V2016) Persons under 18 years, percent, April 1, 2010 Persons 65 years and over, percent, July 1, 2016, (V2016) Persons 65 years and over, percent, April 1, 2010 ... nan Value Flags - nan nan nan nan nan nan nan
Alabama 4,863,300 4,780,131 1.70% 4,779,736 6.00% 6.40% 22.60% 23.70% 16.10% 13.80% ... nan nan nan nan nan nan nan nan nan nan
Alaska 741,894 710,249 4.50% 710,231 7.30% 7.60% 25.20% 26.40% 10.40% 7.70% ... nan nan nan nan nan nan nan nan nan nan
Arizona 6,931,071 6,392,301 8.40% 6,392,017 6.30% 7.10% 23.50% 25.50% 16.90% 13.80% ... nan nan nan nan nan nan nan nan nan nan
Arkansas 2,988,248 2,916,025 2.50% 2,915,918 6.40% 6.80% 23.60% 24.40% 16.30% 14.40% ... nan nan nan nan nan nan nan nan nan nan
California 39,250,017 37,254,522 5.40% 37,253,956 6.30% 6.80% 23.20% 25.00% 13.60% 11.40% ... nan nan nan nan nan nan nan nan nan nan

5 rows × 85 columns

I will trim this dataframe down to only the columns I need, which include those related to population data for 2010 and 2016, as well as the education and economic related factors.

In [18]:
cen_data = trans_cen_data[["Population estimates, July 1, 2016,  (V2016)",
                           "Population, Census, April 1, 2010",
                           "Land area in square miles, 2010",
                           "Population per square mile, 2010",
                           "High school graduate or higher, percent of persons age 25 years+, 2011-2015",
                           "Bachelor's degree or higher, percent of persons age 25 years+, 2011-2015",
                           "Median household income (in 2015 dollars), 2011-2015",
                           "Per capita income in past 12 months (in 2015 dollars), 2011-2015",
                           "Persons in poverty, percent"]].copy()
In [19]:
cen_data.info()
<class 'pandas.core.frame.DataFrame'>
Index: 50 entries, Alabama to Wyoming
Data columns (total 9 columns):
 #   Column                                                                       Non-Null Count  Dtype 
---  ------                                                                       --------------  ----- 
 0   Population estimates, July 1, 2016,  (V2016)                                 50 non-null     object
 1   Population, Census, April 1, 2010                                            50 non-null     object
 2   Land area in square miles, 2010                                              50 non-null     object
 3   Population per square mile, 2010                                             50 non-null     object
 4   High school graduate or higher, percent of persons age 25 years+, 2011-2015  50 non-null     object
 5   Bachelor's degree or higher, percent of persons age 25 years+, 2011-2015     50 non-null     object
 6   Median household income (in 2015 dollars), 2011-2015                         50 non-null     object
 7   Per capita income in past 12 months (in 2015 dollars), 2011-2015             50 non-null     object
 8   Persons in poverty, percent                                                  50 non-null     object
dtypes: object(9)
memory usage: 3.9+ KB

When I initially attempted to convert the datatypes from objects to floats, I discovered that some states (such as New Jersey) input whole percents while others (such as New Mexico) input decimals. I also need to strip the commas and dollar signs from several columns before doing the conversion.

In [20]:
cen_data.iloc[27:33, :]
Out[20]:
Population estimates, July 1, 2016, (V2016) Population, Census, April 1, 2010 Land area in square miles, 2010 Population per square mile, 2010 High school graduate or higher, percent of persons age 25 years+, 2011-2015 Bachelor's degree or higher, percent of persons age 25 years+, 2011-2015 Median household income (in 2015 dollars), 2011-2015 Per capita income in past 12 months (in 2015 dollars), 2011-2015 Persons in poverty, percent
Nevada 2,940,058 2,700,551 109,781.18 24.6 85.10% 23.00% $51,847 $26,541 13.80%
New Hampshire 1,334,795 1,316,470 8,952.65 147 92.30% 34.90% $66,779 $34,362 7.30%
New Jersey 8,944,469 8,791,894 7,354.22 1,195.50 88.60% 36.80% $72,093 $36,582 10.40%
New Mexico 2081015 2059179 121298.15 17 0.842 0.263 44963 24012 0.198
New York 19745289 19378102 47126.4 411.2 0.856 0.342 59269 33236 0.147
North Carolina 10146788 9535483 48617.91 196.1 0.858 0.284 46868 25920 0.154

I will use applymap to remove the percents, commas, and dollar signs and convert each value to a float.

In [21]:
def convert_to_float(cell):
    cell = cell.replace(',', '')
    cell = cell.replace('$', '')
    if '%' in cell:
        cell = round(float(cell.replace('%', ''))/100, 3)
    cell = float(cell)
    return cell
In [22]:
cen_data = cen_data.applymap(convert_to_float)
cen_data.iloc[27:33, :]
Out[22]:
Population estimates, July 1, 2016, (V2016) Population, Census, April 1, 2010 Land area in square miles, 2010 Population per square mile, 2010 High school graduate or higher, percent of persons age 25 years+, 2011-2015 Bachelor's degree or higher, percent of persons age 25 years+, 2011-2015 Median household income (in 2015 dollars), 2011-2015 Per capita income in past 12 months (in 2015 dollars), 2011-2015 Persons in poverty, percent
Nevada 2940058.0 2700551.0 109781.18 24.6 0.851 0.230 51847.0 26541.0 0.138
New Hampshire 1334795.0 1316470.0 8952.65 147.0 0.923 0.349 66779.0 34362.0 0.073
New Jersey 8944469.0 8791894.0 7354.22 1195.5 0.886 0.368 72093.0 36582.0 0.104
New Mexico 2081015.0 2059179.0 121298.15 17.0 0.842 0.263 44963.0 24012.0 0.198
New York 19745289.0 19378102.0 47126.40 411.2 0.856 0.342 59269.0 33236.0 0.147
North Carolina 10146788.0 9535483.0 48617.91 196.1 0.858 0.284 46868.0 25920.0 0.154
In [23]:
cen_data.info()
<class 'pandas.core.frame.DataFrame'>
Index: 50 entries, Alabama to Wyoming
Data columns (total 9 columns):
 #   Column                                                                       Non-Null Count  Dtype  
---  ------                                                                       --------------  -----  
 0   Population estimates, July 1, 2016,  (V2016)                                 50 non-null     float64
 1   Population, Census, April 1, 2010                                            50 non-null     float64
 2   Land area in square miles, 2010                                              50 non-null     float64
 3   Population per square mile, 2010                                             50 non-null     float64
 4   High school graduate or higher, percent of persons age 25 years+, 2011-2015  50 non-null     float64
 5   Bachelor's degree or higher, percent of persons age 25 years+, 2011-2015     50 non-null     float64
 6   Median household income (in 2015 dollars), 2011-2015                         50 non-null     float64
 7   Per capita income in past 12 months (in 2015 dollars), 2011-2015             50 non-null     float64
 8   Persons in poverty, percent                                                  50 non-null     float64
dtypes: float64(9)
memory usage: 3.9+ KB

The census data includes data for 50 states, however, the NICS data has data from 50 states plus 5 additional territories. I will make a set of these additional territories and drop their corresponding rows from the NICS dataframe.

In [24]:
print ('Number of states:', len(nics_data.index.unique()))
nics_data.index.unique()
Number of states: 55
Out[24]:
Index(['Alabama', 'Alaska', 'Arizona', 'Arkansas', 'California', 'Colorado',
       'Connecticut', 'Delaware', 'District of Columbia', 'Florida', 'Georgia',
       'Guam', 'Hawaii', 'Idaho', 'Illinois', 'Indiana', 'Iowa', 'Kansas',
       'Kentucky', 'Louisiana', 'Maine', 'Mariana Islands', 'Maryland',
       'Massachusetts', 'Michigan', 'Minnesota', 'Mississippi', 'Missouri',
       'Montana', 'Nebraska', 'Nevada', 'New Hampshire', 'New Jersey',
       'New Mexico', 'New York', 'North Carolina', 'North Dakota', 'Ohio',
       'Oklahoma', 'Oregon', 'Pennsylvania', 'Puerto Rico', 'Rhode Island',
       'South Carolina', 'South Dakota', 'Tennessee', 'Texas', 'Utah',
       'Vermont', 'Virgin Islands', 'Virginia', 'Washington', 'West Virginia',
       'Wisconsin', 'Wyoming'],
      dtype='object')
In [25]:
not_in_cen = set()

for i in nics_data.index:
    if i not in cen_data.index:
        not_in_cen.add(i)

not_in_cen
Out[25]:
{'District of Columbia',
 'Guam',
 'Mariana Islands',
 'Puerto Rico',
 'Virgin Islands'}
In [26]:
nics_data = nics_data[~nics_data.index.isin(not_in_cen)]
In [27]:
print ('Number of states:', len(nics_data.index.unique()), '\n')
print (nics_data.index.unique(), '\n')
Number of states: 50 

Index(['Alabama', 'Alaska', 'Arizona', 'Arkansas', 'California', 'Colorado',
       'Connecticut', 'Delaware', 'Florida', 'Georgia', 'Hawaii', 'Idaho',
       'Illinois', 'Indiana', 'Iowa', 'Kansas', 'Kentucky', 'Louisiana',
       'Maine', 'Maryland', 'Massachusetts', 'Michigan', 'Minnesota',
       'Mississippi', 'Missouri', 'Montana', 'Nebraska', 'Nevada',
       'New Hampshire', 'New Jersey', 'New Mexico', 'New York',
       'North Carolina', 'North Dakota', 'Ohio', 'Oklahoma', 'Oregon',
       'Pennsylvania', 'Rhode Island', 'South Carolina', 'South Dakota',
       'Tennessee', 'Texas', 'Utah', 'Vermont', 'Virginia', 'Washington',
       'West Virginia', 'Wisconsin', 'Wyoming'],
      dtype='object') 

Finally, I will merge the census and NICS data together, as well as add columns to calculate the totals per capita in each year.

In [28]:
merged_data = cen_data.merge(nics_data, left_index=True, right_index=True)

merged_data['2010 NICS per capita'] = round(merged_data['2010 NICS totals']/merged_data['Population, Census, April 1, 2010'], 3)
merged_data['2016 NICS per capita'] = round(merged_data['2016 NICS totals']/merged_data['Population estimates, July 1, 2016,  (V2016)'], 3)
In [29]:
merged_data.info()
<class 'pandas.core.frame.DataFrame'>
Index: 50 entries, Alabama to Wyoming
Data columns (total 13 columns):
 #   Column                                                                       Non-Null Count  Dtype  
---  ------                                                                       --------------  -----  
 0   Population estimates, July 1, 2016,  (V2016)                                 50 non-null     float64
 1   Population, Census, April 1, 2010                                            50 non-null     float64
 2   Land area in square miles, 2010                                              50 non-null     float64
 3   Population per square mile, 2010                                             50 non-null     float64
 4   High school graduate or higher, percent of persons age 25 years+, 2011-2015  50 non-null     float64
 5   Bachelor's degree or higher, percent of persons age 25 years+, 2011-2015     50 non-null     float64
 6   Median household income (in 2015 dollars), 2011-2015                         50 non-null     float64
 7   Per capita income in past 12 months (in 2015 dollars), 2011-2015             50 non-null     float64
 8   Persons in poverty, percent                                                  50 non-null     float64
 9   2010 NICS totals                                                             50 non-null     int64  
 10  2016 NICS totals                                                             50 non-null     int64  
 11  2010 NICS per capita                                                         50 non-null     float64
 12  2016 NICS per capita                                                         50 non-null     float64
dtypes: float64(11), int64(2)
memory usage: 8.0+ KB
In [30]:
merged_data.head()
Out[30]:
Population estimates, July 1, 2016, (V2016) Population, Census, April 1, 2010 Land area in square miles, 2010 Population per square mile, 2010 High school graduate or higher, percent of persons age 25 years+, 2011-2015 Bachelor's degree or higher, percent of persons age 25 years+, 2011-2015 Median household income (in 2015 dollars), 2011-2015 Per capita income in past 12 months (in 2015 dollars), 2011-2015 Persons in poverty, percent 2010 NICS totals 2016 NICS totals 2010 NICS per capita 2016 NICS per capita
Alabama 4863300.0 4779736.0 50645.33 94.4 0.843 0.235 43623.0 24091.0 0.171 308607 616947 0.065 0.127
Alaska 741894.0 710231.0 570640.95 1.2 0.921 0.280 72515.0 33413.0 0.099 65909 87647 0.093 0.118
Arizona 6931071.0 6392017.0 113594.08 56.3 0.860 0.275 50255.0 25848.0 0.164 206050 416279 0.032 0.060
Arkansas 2988248.0 2915918.0 52035.48 56.0 0.848 0.211 41371.0 22798.0 0.172 191448 266014 0.066 0.089
California 39250017.0 37253956.0 155779.22 239.1 0.818 0.314 61818.0 30318.0 0.143 816399 2377167 0.022 0.061

Exploratory Data Analysis

Here are my research questions:

  1. What is the overall trend in the country for background checks over time?
  2. Which states conduct the highest number of background checks per capita?
  3. Which states are experiencing the largest increase in background checks per capita?
  4. How are economic factors correlated with background checks per capita?
  5. How are educational factors correlated with background checks per capita?

1. What is the overall trend in the country for background checks over time?

I will use the raw NICS data to answer this question. The first line plot shows a steady increase in background checks from 1998 to 2017. Noticing the yearly cyclical pattern, I decided to create a second plot showing the mean number of background checks grouped by month. December is the busiest month of the year, March is the second busiest month of the year, and the summer months of May, June, and July are the least busy months of the year.

In [31]:
#Sets style and font sizes for plots
plt.style.use("seaborn")

SMALL_SIZE = 12
MEDIUM_SIZE = 14
BIG_SIZE = 18

plt.rc('font', size=SMALL_SIZE)          # controls default text sizes
plt.rc('axes', titlesize=MEDIUM_SIZE)    # fontsize of the axes title
plt.rc('axes', labelsize=MEDIUM_SIZE)    # fontsize of the x and y labels
plt.rc('xtick', labelsize=SMALL_SIZE)    # fontsize of the tick labels
plt.rc('ytick', labelsize=SMALL_SIZE)    # fontsize of the tick labels
plt.rc('legend', fontsize=MEDIUM_SIZE)   # legend fontsize
plt.rc('figure', titlesize=BIG_SIZE)     # fontsize of the figure title
In [32]:
nics_x = sorted(raw_nics_data['month'].unique())
nics_y = raw_nics_data.groupby('month')['totals'].sum()

plt.figure("Total # of Background Checks by Year", figsize=(12, 4))
plt.suptitle("Total # of Background Checks Over Time")
plt.ticklabel_format(style='plain')
plt.plot(nics_x, nics_y)
plt.xticks(nics_x[2::12], rotation=45)
plt.xlabel('Year')
plt.ylabel('Total # of Background Checks')
plt.grid(True)
In [33]:
#convert datatype and extract month
import calendar

month_as_dtmonth = raw_nics_data.month.astype('datetime64').dt.month

nics_x = sorted(month_as_dtmonth.unique())
nics_y = raw_nics_data.groupby(month_as_dtmonth)['totals'].mean()

#convert to month names
month_names_x = []
for month in nics_x:
     month_names_x.append(calendar.month_name[month])

#plot data
plt.figure(figsize=(10, 3.5))
plt.suptitle("Average # of Background Checks by Month")
plt.ticklabel_format(style='plain')
plt.plot(month_names_x, nics_y)
plt.xticks(month_names_x[::1], rotation=45)
plt.xlabel('Month')
plt.ylabel('Avg # of Background Checks')
plt.grid(True)

2. Which states conduct the highest number of background checks per capita?

I will start by taking the average of the NICS per capita columns from 2010 and 2016, sort the series, and print the top and bottom 5 ranked states.

Note that Kentucky is significantly higher than the rest of the states because Kentucky is the only state that conducts monthly background checks on people with concealed-carry permits. This phenomenon will be explored further in question 3.

In [34]:
avg_bgc_per_capita = round((merged_data['2010 NICS per capita'] + merged_data['2016 NICS per capita'])/2, 3)

print('The top 5 states with the highest number of background checks per capita are:')
print (avg_bgc_per_capita.sort_values(ascending=False).head(),'\n')

print('The bottom 5 states with the lowest number of background checks per capita are:')
print(avg_bgc_per_capita.sort_values().head())
The top 5 states with the highest number of background checks per capita are:
Kentucky         0.690
Utah             0.149
Indiana          0.135
Montana          0.116
West Virginia    0.109
dtype: float64 

The bottom 5 states with the lowest number of background checks per capita are:
Hawaii          0.010
New Jersey      0.010
New York        0.016
Rhode Island    0.020
Maryland        0.021
dtype: float64
In [35]:
avg_bgc_per_capita.sort_values(ascending=False).plot.bar(figsize=(12, 3),
                                                     ylim=(0,1),
                                                     title='Background Checks per capita by State');

3. Which states are experiencing the largest increase in background checks per capita?

I will start by finding the difference in background checks per capita between 2010 and 2016 using the merged dataset. Then I will calculate the national average, the top 5 states with the greatest difference, and the bottom 5 states with the least difference.

In [36]:
difference_per_capita = merged_data['2016 NICS per capita'] - merged_data['2010 NICS per capita']
difference_per_capita.head()
Out[36]:
Alabama       0.062
Alaska        0.025
Arizona       0.028
Arkansas      0.023
California    0.039
dtype: float64
In [37]:
national_avg = round(difference_per_capita.mean(), 3)
print ('The national average increase of background checks per capita for all states between 2010 and 2016 is '+str(national_avg)+'.')
The national average increase of background checks per capita for all states between 2010 and 2016 is 0.038.
In [38]:
print ('The top 10 states with the greatest increase are:\n')
print (difference_per_capita.sort_values(ascending=False).head())
The top 10 states with the greatest increase are:

Kentucky         0.279
Indiana          0.164
Illinois         0.096
New Hampshire    0.063
Wisconsin        0.063
dtype: float64
In [39]:
print ('The bottom 10 states with the lowest increase are:')
print ('(Note that Utah is the only state to experience a decrease)\n')
print(difference_per_capita.sort_values().head())
The bottom 10 states with the lowest increase are:
(Note that Utah is the only state to experience a decrease)

Utah         -0.103
Hawaii        0.004
New York      0.008
New Jersey    0.008
Nevada        0.011
dtype: float64

The next plot compares the historical trend of total NICS checks over time for the top 3 states (Kentucky, Illinois, Indiana) and the only state to experience a decrease (Utah) compared with the national average.

In [40]:
#create new dataframes for the national average and individual states
raw_avg = raw_nics_data.groupby('month')['totals'].mean()
raw_kentucky = raw_nics_data[raw_nics_data.state=='Kentucky'].sort_values(by='month')
raw_indiana = raw_nics_data[raw_nics_data.state=='Indiana'].sort_values(by='month')
raw_illinois = raw_nics_data[raw_nics_data.state=='Illinois'].sort_values(by='month')
raw_utah = raw_nics_data[raw_nics_data.state=='Utah'].sort_values(by='month')

#create subplot variables for x and y axis
avg_x = raw_avg.index
avg_y = raw_avg
kentucky_x = raw_kentucky['month']
kentucky_y = raw_kentucky['totals']
illinois_x = raw_illinois['month']
illinois_y = raw_illinois['totals']
indiana_x = raw_indiana['month']
indiana_y = raw_indiana['totals']
utah_x = raw_utah['month']
utah_y = raw_utah['totals']

#plot data
plt.figure(figsize=(12, 5))
plt.suptitle("Total # of Background Checks Over Time for Specific States")
plt.ticklabel_format(style='plain')
plt.plot(avg_x, raw_avg, label='Average')
plt.plot(kentucky_x, kentucky_y, label='Kentucky', alpha=.8)
plt.plot(indiana_x, indiana_y, label='Indiana', alpha=.8)
plt.plot(illinois_x, illinois_y, label='Illinois', alpha=.8)
plt.plot(utah_x, utah_y, label='Utah', alpha=.8, color='gold')
plt.axvspan('2010-01', '2016-12', color='brown', alpha=0.2)   #creates shaded background for 2010-01 to 2016-12
plt.legend(loc='best')
plt.xticks(avg_x[2::12], rotation=45)
plt.xlabel('Year')
plt.ylabel('Total # of Background Checks')
plt.grid(True)

Above, the shaded part represents the time period between January 2010 and December 2016.

Kentucky(green) saw a notable increase around 2006, which was when the state started requiring monthly background checks for concealed-carry permits according to Jacob Ryan(2015) from 89.3 WFPL (https://wfpl.org/kentucky-background-checks-stand-out/):

But outside of higher-than-average gun sales in the state, one reason Kentucky reports such a high number of checks annually — more than double the number of Texas and California combined so far this year — is a policy that requires automatic monthly background checks on every holder of concealed-carry permits in the commonwealth.

Kentucky appears to be the only state with such a policy.

Utah(yellow) saw a period of notable increase during 2010 and 2011 before returning to more stable levels. I was unable to find a reason for this spike in background checks, but its existence in the data explains why Utah experienced a decrease in background checks when comparing 2010 and 2016.

After considering this information, both Kentucky and Utah appear to be anomalies that don't represent the general behavior of other states.

4. How are educational factors correlated with background checks per capita?

To answer questions 4 and 5, I will first create a series of the average difference in background checks per capita between 2010 and 2016 to use as the dependent variable (This is the same calculation and variable used for question 2). I chose to take the average because several of the dependent variables from the census data are reported for the timespan of 2011-2015. However, due to the anomalies discovered in question 3, I will create a new dataframe of 48 states without Kentucky and Utah.

In [41]:
avg_bgc_per_capita = (merged_data['2016 NICS per capita'] + merged_data['2010 NICS per capita'])/2
avg_bgc_per_capita.sort_values(ascending=False).head()
Out[41]:
Kentucky         0.6895
Utah             0.1485
Indiana          0.1350
Montana          0.1165
West Virginia    0.1090
dtype: float64
In [42]:
#new dataframe without Kentucky and Utah
merged_data_48 = merged_data.drop(['Kentucky', 'Utah'])
In [43]:
#recalculate average difference in background checks per capita between 2010 and 2016
avg_bgc_per_capita = (merged_data_48['2016 NICS per capita'] + merged_data_48['2010 NICS per capita'])/2
avg_bgc_per_capita.sort_values(ascending=False).head()
Out[43]:
Indiana          0.1350
Montana          0.1165
West Virginia    0.1090
Alaska           0.1055
South Dakota     0.1035
dtype: float64

Next, I will create a function to calculate the Pearson's r correlation coefficient and create a corresponding scatterplot for two variables. Then, I will call this function to compare each independent variable with the dependent variable of background checks per capita.

In [44]:
def pearsons_r(corr_x, corr_y, x_label, y_label):
    output = round(numpy.corrcoef(corr_x, corr_y)[0,1], 3)
    print("The Pearson's r correlation coefficient between\n{}\nand\n{}\nis:".format(x_label, y_label), output)
    sns.regplot(corr_x,corr_y).set(xlabel=x_label,
                                   ylabel=y_label);

I'm now ready to investigate the correlation between educational factors and background checks per capita.

First, I will look at the column for the percent of high school graduates or higher persons age 25 years and older between 2011-2015. The correlation coefficient is 0.168, meaning there's a very slight tendency for the higher percentage of high school graduates, the higher number of background checks per capita in that state. However, 0.168 is so small that the relationship is considered to be insignificant or non-existent.

In [45]:
high_school_x = merged_data_48["High school graduate or higher, percent of persons age 25 years+, 2011-2015"]
high_school_y = avg_bgc_per_capita

pearsons_r(high_school_x, high_school_y, "% of High School Graduate or Higher", "Background Checks per capita")
The Pearson's r correlation coefficient between
% of High School Graduate or Higher
and
Background Checks per capita
is: 0.168

Next, I will look at the column for the percent of Bachelor's degree or higher for persons age 25 years and older between 2011-2015. The correlation coefficient is -0.416, meaning there's a moderate tendency of the higher percent of four-year college graduates, the lower number of background checks per capita in that state. -0.416 is considered to be a relationship of moderate strength and certainly more significant than the % of high school graduates.

In [46]:
bachelor_x = merged_data_48["Bachelor's degree or higher, percent of persons age 25 years+, 2011-2015"]
bachelor_y = avg_bgc_per_capita

pearsons_r(bachelor_x, bachelor_y, "% of Bachelor's Degree or Higher", "Background Checks per capita")
The Pearson's r correlation coefficient between
% of Bachelor's Degree or Higher
and
Background Checks per capita
is: -0.416

5. How are economic factors correlated with background checks per capita?

I will continue the same process for analyzing economic factors as I've done with educational factors.

First, I will look at the column for median household income between 2011 and 2015. The correlation coefficient is -0.350, meaning there's a moderate tendency of the higher median household income, the lower number of background checks per capita in that state. -0.350 is considered to be a relationship of moderate strength, though slightly weaker than seen for the percentage of four-year college graduates in a state.

In [47]:
median_income_x = merged_data_48["Median household income (in 2015 dollars), 2011-2015"]
median_income_y = avg_bgc_per_capita

pearsons_r(median_income_x, median_income_y, "Median Household Income", "Background Checks per capita")
The Pearson's r correlation coefficient between
Median Household Income
and
Background Checks per capita
is: -0.35

Next, I will look at the column for per capita income in the past 12 months between 2011 and 2015. The correlation coefficient is -0.329, meaning there's a moderate tendency of the higher per capita income, the lower number of background checks per capita in that state. It's not surprising to see a similar strength relationship between median household income(-0.350) and per capita income(-0.329).

In [48]:
per_capita_income_x = merged_data_48["Per capita income in past 12 months (in 2015 dollars), 2011-2015"]
per_capita_income_y = avg_bgc_per_capita

pearsons_r(per_capita_income_x, per_capita_income_y, "Per capita Income", "Background Checks per capita")
The Pearson's r correlation coefficient between
Per capita Income
and
Background Checks per capita
is: -0.329

Finally, I will look at the column for percent of persons in poverty. The correlation coefficient is only -0.140, meaning there's a very slight tendency of the more people in poverty, the more background checks per capita in that state. -0.140 is the weakest relationship of those I investigated and is considered to be insignificant or non-existent.

In [49]:
poverty_x = merged_data_48["Persons in poverty, percent"]
poverty_y = avg_bgc_per_capita

pearsons_r(poverty_x, poverty_y, "% of Persons in Poverty", "Background Checks per capita")
The Pearson's r correlation coefficient between
% of Persons in Poverty
and
Background Checks per capita
is: 0.14

Conclusions

Combining the FBI's NICS data with the US Census data allowed me to not only analyze the total number of background checks conducted, but also the number of background checks conducted per capita for each state. Furthermore, I was able to explore correlations between background checks per capita and several demographic factors, specifically statistics on each state population's educational and economic situation.

One limitation of this investigation was that different states have different laws and requirements for conducting background checks and gun sales. This was particularly evident in the case of Kentucky, where checks are conducted monthly for people with concealed-carry permits. Furthermore, background checks are conducted for a variety of reasons and purchasing situations, not just a single gun sale, so a one-to-one relationship between NICS background checks and firearm sales cannot be made.

Nevertheless, some interesting trends and did emerge from this investigation. First, the data showed a clear upward trend in the total number of background checks being conducted throughout the US from 1998 to 2017. During that timespan, the busiest month for background checks was December, presumably due to the annual shopping spree leading into the winter holidays. Furthermore, when comparing the average number of background checks conducted per capita in 2010 and 2016, all states (except for the anomaly of Utah) experienced an increase as well.

The educational factor with the strongest relationship to background checks per capita was the percentage of four-year college graduates in a state. The Pearson's r correlation coefficient was -0.416, moderately indicating that states with higher percentages of college graduates tend to conduct a lower number of NICS background checks per capita.

The economic factor with the strongest relationship to background checks per capita was the median household income in a state. The Pearson's r correlation coefficient was -0.350, moderately indicating that states with higher median household incomes tend to conduct a lower number of NICS background checks per capita.