Full Example for Python Beginners#
The intent of this Jupyter notebook is to both show how to analyze Fairfax arrests drunk in public data but also to provide building blocks that can be reused for other analyses#
This document is a Jupyter notebook. It is essentially an environment where you can: 1. Run code 2. Display the results as text, plots, or other visualizations 3. Display text
Code and text are organized into cells which can either be: 1. Text cells (like this one) 2. Code cells (like the next one)
Cells can be run individually to run the code in that cell. Running of the cells is cumulative in that if a variable x was created in a cell that was previously run, it can later be used in another cell.
NOTE: Cells are intended to be run in order (but don’t have to be). If cells are run out of order, the user must beware of unintended consequences if variables get overwritten. If cell 1 sets x=1, cell 2 sets x = x+3, and cell 3 sets x = 2x, I would expect x to be (1+3)2=8 after running cell 3 but if I run cell 1, then cell 2 twice, and then cell 3, x would actually be (1+3+3)2=14. There are legitimate reasons to run code out of order (i.e. if you need to fix code in a prior cell and don’t want/need to start over); you just have to beware that variables are not being reused/overwritten in a way that gives unintended results*
To run a cell, you can: 1. Click the play button located next to the cell 2. Click CTRL-ENTER on the keyboard 3. Click SHIFT-ENTER on the keyboard which will run the code in the cell AND advance to the next cell. Clicking SHIFT-ENTER while a text cell is active will just advance to the next cell Note that the cell that runs is the one that is active (for code cells, the one where the cursor is at).
To create new cells, you can: 1. Use the menus around the cells to add cells before or after the current one 2. If you are running cells, clicking SHIFT-ENTER while the currently last cell is active will run that cell and create a new one after it
[1]:
# This is a comment. It is not code and does not run
# Let's load in the libraries that we will use
# This loads in the Open Police Data package and renames it as opd (for convenience, the as opd
# could be left off and the package would be referred openpolicedata)
# OpenPoliceData is a package for loading in police datasets
import openpolicedata as opd
# Same deal except for the pandas package. pandas is a library for manipulating, analyzing, and plotting tabular data
import pandas as pd
Let’s find what data is available for a source (usually a police department, sometimes a state). We will create the source and then print out a table of available data.
Pay attention to TableType and Year. Those will be needed to reference the data that you want to load later.
[2]:
# Create a source. Sources know what date is availabe in the package, where to find it, and how to load it.
# This source is for Fairfax County's open data
# The name to the left of the equals sign is the variable that is generated. You use this name when you want to use it below.
src = opd.Source("Fairfax County")
# Sources have a datasets property that is a table of available datasets from the source
# Print out the available datasets
# If you don't output to a variable, it will print out below. If we put x = src.datasets here, it would set the variable x to be
# src.datasets instead of printing it out
# In a Jupyter notebook, if you don't output something to a variable (i.e. x=src.datasets would output src.datasets to x), it will automatically print it out
# (NOTE: this can only be done once per cell. Only the last item that isn't outputted to the variable will be printed. To print multiple things, use the print function)
src.datasets
[2]:
| State | SourceName | Agency | AgencyFull | TableType | Year | Description | DataType | URL | date_field | dataset_id | agency_field | min_version | readme | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | Virginia | Fairfax County | Fairfax County | Fairfax County Police Department | TRAFFIC WARNINGS | 2019 | Traffic Warnings issued by Fairfax County Police | CSV | https://www.fairfaxcounty.gov/police/sites/pol... | <NA> | <NA> | <NA> | 0.3.1 | https://www.fcpod.org/pages/crime-data |
| 2 | Virginia | Fairfax County | Fairfax County | Fairfax County Police Department | TRAFFIC WARNINGS | 2020 | Traffic Warnings issued by Fairfax County Police | CSV | https://www.fairfaxcounty.gov/police/sites/pol... | <NA> | <NA> | <NA> | 0.3.1 | https://www.fcpod.org/pages/crime-data |
| 3 | Virginia | Fairfax County | Fairfax County | Fairfax County Police Department | TRAFFIC WARNINGS | 2021 | Traffic Warnings issued by Fairfax County Police | CSV | https://www.fairfaxcounty.gov/police/sites/pol... | <NA> | <NA> | <NA> | 0.3.1 | https://www.fcpod.org/pages/crime-data |
| 4 | Virginia | Fairfax County | Fairfax County | Fairfax County Police Department | TRAFFIC CITATIONS | 2019 | Traffic Citations issued by Fairfax County Police | CSV | https://www.fairfaxcounty.gov/police/sites/pol... | <NA> | <NA> | <NA> | 0.3.1 | https://www.fcpod.org/pages/crime-data |
| 5 | Virginia | Fairfax County | Fairfax County | Fairfax County Police Department | TRAFFIC CITATIONS | 2020 | Traffic Citations issued by Fairfax County Police | CSV | https://www.fairfaxcounty.gov/police/sites/pol... | <NA> | <NA> | <NA> | 0.3.1 | https://www.fcpod.org/pages/crime-data |
| 6 | Virginia | Fairfax County | Fairfax County | Fairfax County Police Department | TRAFFIC CITATIONS | 2021 | Citations issued within 2021. Data was collect... | CSV | https://www.fairfaxcounty.gov/police/sites/pol... | <NA> | <NA> | <NA> | 0.3.1 | https://www.fcpod.org/pages/crime-data |
| 327 | Virginia | Fairfax County | Fairfax County | Fairfax County Police Department | ARRESTS | 2016 | <NA> | ArcGIS | https://services9.arcgis.com/kYvfX7YK8OobHItA/... | <NA> | <NA> | <NA> | 0.2 | <NA> |
| 328 | Virginia | Fairfax County | Fairfax County | Fairfax County Police Department | ARRESTS | 2017 | <NA> | ArcGIS | https://services9.arcgis.com/kYvfX7YK8OobHItA/... | <NA> | <NA> | <NA> | 0.2 | <NA> |
| 329 | Virginia | Fairfax County | Fairfax County | Fairfax County Police Department | ARRESTS | 2018 | <NA> | ArcGIS | https://services9.arcgis.com/kYvfX7YK8OobHItA/... | <NA> | <NA> | <NA> | 0.2 | <NA> |
| 330 | Virginia | Fairfax County | Fairfax County | Fairfax County Police Department | ARRESTS | 2019 | <NA> | CSV | https://www.fairfaxcounty.gov/police/sites/pol... | <NA> | <NA> | <NA> | 0.3.1 | https://www.fcpod.org/pages/crime-data |
| 331 | Virginia | Fairfax County | Fairfax County | Fairfax County Police Department | ARRESTS | 2020 | <NA> | CSV | https://www.fairfaxcounty.gov/police/sites/pol... | <NA> | <NA> | <NA> | 0.3.1 | https://www.fcpod.org/pages/crime-data |
| 332 | Virginia | Fairfax County | Fairfax County | Fairfax County Police Department | ARRESTS | 2021 | <NA> | CSV | https://www.fairfaxcounty.gov/police/sites/pol... | <NA> | <NA> | <NA> | 0.3.1 | https://www.fcpod.org/pages/crime-data |
| 333 | Virginia | Fairfax County | Fairfax County | Fairfax County Police Department | ARRESTS | 2022 | <NA> | CSV | https://www.fairfaxcounty.gov/police/sites/pol... | <NA> | <NA> | <NA> | 0.3.1 | https://www.fcpod.org/pages/crime-data |
Now let’s load in some data
[ ]:
# date and table_type should match values in src.datasets
t = src.load(date=2022, table_type="ARRESTS")
# You can check the type of something with the type function
# table is a table of data that was just loaded.
# The table is a DataFrame from the pandas Python package. Any of the techniques applied to t.table can be applied to other pandas DataFrames
# The below print statement prints out the text in the string except it replaces {type(t.table)} with the type of df_all
print(f"df_all is a {type(t.table)}")
# head() prints out the first 5 rows. If you want to print out a different # of rows (n) use head(n)
t.table.head()
df_all is a <class 'pandas.core.frame.DataFrame'>
| OBJECTID | Arrest ID | Case Number | Arrest Date | Arrest Time | Arrestee Race | Arrestee Sex | Arrestee Age | Arrestee Ethnicity | Resident | ... | X Coordinate | Y Coordinate | Station Name | Station Number | IDENTIFIER | DISTRICT_1 | BOS Web Address | BOS Phone Number | BOS Name | Year | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 203307 | 2022004449 | 20221020083 | 4/12/2022 0:00 | 1002 | W | M | 21 | N | R | ... | 11820093.77 | 6986983.588 | WEST SPRINGFIELD | 7 | 1 | BRADDOCK | www.fairfaxcounty.gov/braddock | 703-425-9300 | James R. Walkinshaw | 2022 |
| 1 | 203308 | 2022015055 | NaN | 10/31/2022 0:00 | 149 | W | M | 23 | H | R | ... | 11857615.41 | 6976984.900 | FRANCONIA | 6 | 1 | BRADDOCK | www.fairfaxcounty.gov/braddock | 703-425-9300 | James R. Walkinshaw | 2022 |
| 2 | 203309 | 2022011243 | 20222330169 | 8/21/2022 0:00 | 2246 | W | M | 44 | H | R | ... | 11849051.88 | 6982528.121 | WEST SPRINGFIELD | 7 | 1 | BRADDOCK | www.fairfaxcounty.gov/braddock | 703-425-9300 | James R. Walkinshaw | 2022 |
| 3 | 203310 | 2022012244 | 20222510269 | 9/8/2022 0:00 | 1834 | W | M | 32 | N | R | ... | 11850035.76 | 7018552.107 | MCLEAN | 3 | 3 | DRANESVILLE | www.fairfaxcounty.gov/dranesville | 703-356-0551 | John W. Foust | 2022 |
| 4 | 203311 | 2022018574 | 20223640022 | 12/29/2022 0:00 | 1713 | W | M | 30 | H | R | ... | 11857640.57 | 6977108.718 | FRANCONIA | 6 | 1 | BRADDOCK | www.fairfaxcounty.gov/braddock | 703-425-9300 | James R. Walkinshaw | 2022 |
5 rows × 35 columns
See what columns are in the table
[4]:
t.table.columns
[4]:
Index(['OBJECTID', 'Arrest ID', 'Case Number', 'Arrest Date', 'Arrest Time',
'Arrestee Race', 'Arrestee Sex', 'Arrestee Age', 'Arrestee Ethnicity',
'Resident', 'PSA', 'Patrol Area', 'ESZ (Emergency Service Zones)',
'Arrest Type', 'Sequence Number', 'Statute', 'Statute Description',
'IBR Code', 'IBR Description', 'Felony or Misdemeanor',
'Arresting Officer Number', 'Officer Sex', 'Officer Race',
'Arrest Charge ID', 'Arrestee Name ID', 'X Coordinate', 'Y Coordinate',
'Station Name', 'Station Number', 'IDENTIFIER', 'DISTRICT_1',
'BOS Web Address ', 'BOS Phone Number', 'BOS Name', 'Year'],
dtype='object')
The Arrests table doesn’t have a column that combines race and ethnicity, let’s create one
[5]:
# We need to combine race and ethnicity. We'll need to create a function to do that
def combine_raceeth(x): # This defines a function called combine_raceeth with 1 input called x
# The function starts here
# In Python, the start and stop of sections of code (such as functions and for loops) is indicated by indenting
# x is the input to this function. It is expected to be a row (which are actually represented as pandas Series) of a DataFrame
# that contains a value for "Arrestee Ethnicity" and "Arrestee Race"
# The return keyword outputs whatever is to the right of it
# If ethnicity is Hispanic or Unknown return the ethnicity
# Otherwise, return the race
if x["Arrestee Ethnicity"] in ["H", "U"]:
return x["Arrestee Ethnicity"]
else:
return x["Arrestee Race"]
# The function ends here because the next lines are not indented
# Let's test out our function
rows_check = [0, 1] # Indices of rows that contain good test cases for the 2022 Arrests data. In Python, indices start from 0 (not 1)
# This for loop runs len(rows_check) times (where len(rows_check) gives the length of rows_check)
# In each iteration of the for loop, i is set to the next value in rows_check
for i in rows_check:
# iloc allows access to values of the table/DataFrame by row (1st value) and column (2nd value) indices. The colon is shorthand for all indices
# so here we are getting all columns for row i
row = t.table.iloc[i,:]
race_eth = combine_raceeth(row)
race = row["Arrestee Race"]
eth = row["Arrestee Ethnicity"]
print(f"Race ethnicity result for race {race} and ethnicity {eth} is {race_eth}")
Race ethnicity result for race W and ethnicity N is W
Race ethnicity result for race W and ethnicity H is H
[6]:
# Before we add the column, let's create a copy of t.table. It's good practice not to change the original copy
df_all = t.table.copy()
[7]:
# Now, let's create a new column in our date frame for combined race ethnicity
# df_all[["Arrestee Race","Arrestee Ethnicity"]] extracts just the race and ethnicity columns into a temporary new DataFrame
# This is purely for efficiency reasons and is not necessary
# The .apply(combine_raceeth, axis=1) applies the function combine_raceeth to each row (as opposed to column because axis=1) of df[["Arrestee Race","Arrestee Ethnicity"]]
# This outputs a new columns (or pandas Series)
# The new column is called race_eth
df_all["race_eth"] = df_all[["Arrestee Race","Arrestee Ethnicity"]].apply(combine_raceeth,axis=1)
# Let's check out how many of each unique value of race_eth there is in the entire Arrests table
df_all["race_eth"].value_counts()
[7]:
B 10162
H 7490
W 6264
A 981
U 735
I 26
Name: race_eth, dtype: int64
Let’s try to figure out what the statute codes are associated with Drunk in Public arrests.
[8]:
# Let's find statute descriptions with the word Drunk in them
# The words used could be changed to search for other statute codes
word_to_find = "drunk"
# df_all['Statute Description'] accesses column 'Statute Description' in df_all
# .str allows functions to be applied to strings (i.e. text)
# lower() makes all the values in the string lower case (so that we don't have to worry if they are spelled Drunk or drunk)
col = df_all['Statute Description'].str.lower()
# .contains(x) searches for the substring x in each value in the column
# The result is a column where the value is True if df_all['Statute Description'] contains the word drunk and False if not
has_word = col.str.contains(word_to_find.lower())
print(f"The first 5 rows of has_word are {has_word.head()}")
# The [has_word] after df_all['Statute Description'] creates a new column that only contains the cells of df_all['Statute Description'] where has_word is True
# .unique()
unique_statute_desc = df_all['Statute Description'][has_word].unique()
# The \n creates a new line after the text is printed
print(f"\nWords containing {word_to_find} are {unique_statute_desc}\n")
# If we want the statutes associated with the Statute Descriptions containing drunk, we can do the following
# Get columns Statute Description and Statute
# Only keep the rows where has_word is True
# drop_duplicates() only keeps the rows that have unique values
unique_pairs = df_all[['Statute Description','Statute']][has_word].drop_duplicates()
print(f"unique_pairs is also a {type(unique_pairs)}")
unique_pairs
The first 5 rows of has_word are 0 False
1 True
2 False
3 False
4 True
Name: Statute Description, dtype: bool
Words containing drunk are ['DRUNK IN PUBLIC OR PROFANE'
'ALCOHOL: INTERDICTED PERSON DRUNK IN PUBLIC'
'DRUNK IN PUBLIC OR PROFANE - 3X IN ONE YEAR']
unique_pairs is also a <class 'pandas.core.frame.DataFrame'>
[8]:
| Statute Description | Statute | |
|---|---|---|
| 1 | DRUNK IN PUBLIC OR PROFANE | 5/1/2001 |
| 5555 | DRUNK IN PUBLIC OR PROFANE | 5-1-1 (18.2-388 |
| 7351 | ALCOHOL: INTERDICTED PERSON DRUNK IN PUBLIC | 4.1-322 |
| 12458 | DRUNK IN PUBLIC OR PROFANE | 18.2-388 |
| 13512 | DRUNK IN PUBLIC OR PROFANE - 3X IN ONE YEAR | 5-1-1(B) |
We can see that there are 3 Statute codes associated with the description DRUNK IN PUBLIC OR PROFANE: 5/1/2001, 5-1-1 (1.2-388, and 18.2-388 NOTE: The value 5/1/2001 indicates that someone saved this data in Excel which autocompleted 5-1-1 to 5/1/2001 because Excel thought it was a date
Let’s search for the word public to see if we can find other variations on Drunk in Public
[9]:
# Let's find statute descriptions with the word Drunk in them
word_to_find = "public"
col = df_all['Statute Description'].str.lower()
has_word = col.str.contains(word_to_find.lower())
# Let's also exclude words contain "drunk" since we already found those
has_drunk = col.str.contains("drunk")
# Now, we have values containing drunk but we want values that don't contain drunk
# The ~ before has_drunk converts all the Falses to Trues and Trues to Falses
not_drunk = ~has_drunk
has_word_but_not_drunk = has_word & (~has_drunk)
unique_pairs = df_all[['Statute Description','Statute']][has_word_but_not_drunk].drop_duplicates()
unique_pairs
[9]:
| Statute Description | Statute | |
|---|---|---|
| 22 | PROFANE SWEARING OR INTOXICATION IN PUBLIC | 18.2-388 |
| 436 | PUBLIC RECORDS: FORGERY | 18.2-168 |
| 673 | FIREARM: SHOOT IN PUBLIC PLACE, CAUSE INJURY | 18.2-280(A) |
| 849 | CARRY HANDGUN IN PUBLIC UNDER THE INFLUENCE | 18.2-308.012 |
| 885 | ALCH BEV TEND/DRK IN PUBLIC | 5/1/2027 |
| 1636 | FIREARM: CARRY LOADED IN CERTAIN PUBLIC AREAS | 18.2-287.4(ii) |
| 2059 | PUBLIC RECORDS: EMPLOY AS TRUE, FORGERY | 18.2-168 |
| 2320 | ALCOHOL: DRINKING/OFFER DRINK IN PUBLIC PLACE | 4.1-308 |
| 2756 | BRIBE:OFFER/ACCEPT/SOLICIT PUBLIC/PARTY OFFICIAL | 18.2-447 |
| 6558 | FIREARM: SHOOT IN PUBLIC PLACE, NOT CAUSE INJURY | 18.2-280(A) |
| 7917 | MARIJUANA POSS/CONSUME PUBLIC PLACE 3RD+ OFFENSE | 4.1-1108(B) |
| 8572 | EXPECTORATE/SPIT UPON PUBLIC PLACE | 18.2-322 |
| 13241 | ENTER/REMAIN IN PUBLIC TRANSIT, TRESPASS | 18.2-160.2 |
| 13801 | PUBLIC BUILDING: DAMAGE, <$1000 | 18.2-138 |
| 20321 | PROFANE SWEARING OR INTOXICATION IN PUBLIC | 5/1/2001 |
We can now see that there is also PROFANE SWEARING OR INTOXICATION IN PUBLIC, ALCH BEV TEND/DRK IN PUBLIC (which introduces a new Statute code), and ALCOHOL: DRINKING/OFFER DRINK IN PUBLIC PLACE (which introduces a new Statute code)
Now, let’s find all Statute codes that contain 5/1/2001, 5-1-1, 5/1/2027, 4.1-308, or 18.2-388
[10]:
# This is a list of strings
# The statute codes could be changed to look for other arrests
statutes = ["5/1/2001", "5-1-1", "18.2-388", "5/1/2027"]
print(f"The number of values in the list statutes is {len(statutes)}")
has_any_statute = None
# This for loop will run once for each value in statutes
# In each iteration, the value of s will change to the next value in statutes
for s in statutes:
print(f"The value of s in this iteration is {s}")
contains_statute = df_all["Statute"].str.contains(s)
if has_any_statute is None:
# In the 1st iteration, set has_statute to contains_statute
has_any_statute = contains_statute
else:
# The | does an OR operation between each value of has_any_statute and the corresponding value of contains_statute
# If one or both values are True, the resulting value is True. Otherwise, it is False
# has_any_statute is replaced with the result of the OR operation
has_any_statute = has_any_statute | contains_statute
# .sum() Takes the sum of the column has_any_statute
# For boolean (True or False) values, True = 1 and False = 0 so .sum() counts the number of Trues
print(f"The number of rows that contain any of the statutes after checking statute {s} is {has_any_statute.sum()}")
# Let's check that this worked properly
df_all[['Statute Description','Statute']][has_any_statute].drop_duplicates()
The number of values in the list statutes is 4
The value of s in this iteration is 5/1/2001
The number of rows that contain any of the statutes after checking statute 5/1/2001 is 1943
The value of s in this iteration is 5-1-1
The number of rows that contain any of the statutes after checking statute 5-1-1 is 1946
The value of s in this iteration is 18.2-388
The number of rows that contain any of the statutes after checking statute 18.2-388 is 2318
The value of s in this iteration is 5/1/2027
The number of rows that contain any of the statutes after checking statute 5/1/2027 is 2375
[10]:
| Statute Description | Statute | |
|---|---|---|
| 1 | DRUNK IN PUBLIC OR PROFANE | 5/1/2001 |
| 22 | PROFANE SWEARING OR INTOXICATION IN PUBLIC | 18.2-388 |
| 885 | ALCH BEV TEND/DRK IN PUBLIC | 5/1/2027 |
| 5555 | DRUNK IN PUBLIC OR PROFANE | 5-1-1 (18.2-388 |
| 12458 | DRUNK IN PUBLIC OR PROFANE | 18.2-388 |
| 13512 | DRUNK IN PUBLIC OR PROFANE - 3X IN ONE YEAR | 5-1-1(B) |
| 20321 | PROFANE SWEARING OR INTOXICATION IN PUBLIC | 5/1/2001 |
[11]:
# Let's create a DataFrame that only contains rows of the original table containing our statutes
# df is a common variable name for a DataFrame
df = df_all[has_any_statute]
[12]:
# Just reminding about available columns
df.columns
[12]:
Index(['OBJECTID', 'Arrest ID', 'Case Number', 'Arrest Date', 'Arrest Time',
'Arrestee Race', 'Arrestee Sex', 'Arrestee Age', 'Arrestee Ethnicity',
'Resident', 'PSA', 'Patrol Area', 'ESZ (Emergency Service Zones)',
'Arrest Type', 'Sequence Number', 'Statute', 'Statute Description',
'IBR Code', 'IBR Description', 'Felony or Misdemeanor',
'Arresting Officer Number', 'Officer Sex', 'Officer Race',
'Arrest Charge ID', 'Arrestee Name ID', 'X Coordinate', 'Y Coordinate',
'Station Name', 'Station Number', 'IDENTIFIER', 'DISTRICT_1',
'BOS Web Address ', 'BOS Phone Number', 'BOS Name', 'Year', 'race_eth'],
dtype='object')
[13]:
# Let's check the number of misdemeanors and felonies for drunk in public offenses
df["Felony or Misdemeanor"].value_counts()
[13]:
M 2375
Name: Felony or Misdemeanor, dtype: int64
All drunk in publics charges are misdemeanors
[14]:
# Let's check the# of each arrests type
df["Arrest Type"].value_counts()
[14]:
ON-VIEW ARREST 2074
TAKEN INTO CUSTODY 218
SUMMONED/ CITED 83
Name: Arrest Type, dtype: int64
[15]:
# Let's breakdown by race and ethnicity
# value_counts identifies all unique values and their counts for a column
df["race_eth"].value_counts()
[15]:
H 1149
W 571
B 555
A 50
U 43
I 7
Name: race_eth, dtype: int64
[16]:
# We can also normalize the value counts (i.e. show the ratios for each race/ethnicity)
df["race_eth"].value_counts(normalize=True)
[16]:
H 0.483789
W 0.240421
B 0.233684
A 0.021053
U 0.018105
I 0.002947
Name: race_eth, dtype: float64
[17]:
# Or if you'd rather have the value as a percent just multiply by 100
df["race_eth"].value_counts(normalize=True)*100
[17]:
H 48.378947
W 24.042105
B 23.368421
A 2.105263
U 1.810526
I 0.294737
Name: race_eth, dtype: float64
[18]:
# Let's break this down by race_eth AND Arrest Type
# The .groupby creates groups of unique combinations of the column names passed to it
# You can then apply a function to each group.
# In this case, we apply the function size to the group, which just outputs the number of items in the group
# For example, let's assume we had two columns "Animal" which is ["Dog", "Cat", "Dog", "Dog"] and "Color" which is ["Brown", "Brown", "White", "Brown"]
# 3 groups would be created [("Dog", "Brown"), ("Cat", "Brown"), ("Dog, "White)]
# The size of those groups would be [2, 1, 1] since there are 2 rows with "Dog" and "Brown" and 1 of each other combination
# NOTE: You can learn more about all of the techniques used here by web searches. Most of the techniques are from the pandas library
# so if you google "python pandas groupby", you will find documentation with explanations and examples
df_sizes = df.groupby(["race_eth","Arrest Type"]).size()
df_sizes
[18]:
race_eth Arrest Type
A ON-VIEW ARREST 46
TAKEN INTO CUSTODY 4
B ON-VIEW ARREST 476
SUMMONED/ CITED 18
TAKEN INTO CUSTODY 61
H ON-VIEW ARREST 1008
SUMMONED/ CITED 53
TAKEN INTO CUSTODY 88
I ON-VIEW ARREST 6
TAKEN INTO CUSTODY 1
U ON-VIEW ARREST 38
SUMMONED/ CITED 1
TAKEN INTO CUSTODY 4
W ON-VIEW ARREST 500
SUMMONED/ CITED 11
TAKEN INTO CUSTODY 60
dtype: int64
[19]:
# This doesn't print very nicely. Let's fix that.
df_sizes = df.groupby(["race_eth","Arrest Type"]).size().unstack()
# Unstack takes ArrestType and makes it a column
df_sizes
[19]:
| Arrest Type | ON-VIEW ARREST | SUMMONED/ CITED | TAKEN INTO CUSTODY |
|---|---|---|---|
| race_eth | |||
| A | 46.0 | NaN | 4.0 |
| B | 476.0 | 18.0 | 61.0 |
| H | 1008.0 | 53.0 | 88.0 |
| I | 6.0 | NaN | 1.0 |
| U | 38.0 | 1.0 | 4.0 |
| W | 500.0 | 11.0 | 60.0 |
[20]:
# THe above looks better but...
# 1. Combinations of race_eth and ArrestType that don't exist contain NaN (not a number) values
# 2. The values are counts (integers) but are displayed as decimals (floats)
# This lists the types of each columns
df_sizes.dtypes
[20]:
Arrest Type
ON-VIEW ARREST float64
SUMMONED/ CITED float64
TAKEN INTO CUSTODY float64
dtype: object
[21]:
# fillna replaces null values (including NaN) with a value. We replace with 0 since there are 0 cases where NaN values are
# convert_dtypes is a pandas function that sets the data type to the best possible type which in this case is integer (Int64 for 64 bit integer)
# since all of the float values have 0's after the decimal place
df_sizes = df_sizes.fillna(0).convert_dtypes()
df_sizes.dtypes
[21]:
Arrest Type
ON-VIEW ARREST Int64
SUMMONED/ CITED Int64
TAKEN INTO CUSTODY Int64
dtype: object
[22]:
# Now let's re-display the table
df_sizes
[22]:
| Arrest Type | ON-VIEW ARREST | SUMMONED/ CITED | TAKEN INTO CUSTODY |
|---|---|---|---|
| race_eth | |||
| A | 46 | 0 | 4 |
| B | 476 | 18 | 61 |
| H | 1008 | 53 | 88 |
| I | 6 | 0 | 1 |
| U | 38 | 1 | 4 |
| W | 500 | 11 | 60 |
[23]:
# This table could be formatted further (such as change names of rows from A, B, H,... to Asian, Black, Latino,...)
# However, let's just assume that you'll do any formatting in Excel
# To output any DataFrame to a CSV file so that it can be loaded into Excel (or read back into a DataFrame later), you can use to_csv
filename = "RaceVsArrestType.csv"
# Save to RaceVsArrestType.csv in whatever directory where this Jupyter notebook file exists
df_sizes.to_csv(filename)
# NOTE: if you want to save to another directory
# The \ character is a special character that indicates that is used in combination with the character that follows it
# For example, we used \n earlier to create a new line
# Therefore, if Python finds a \ in your string, it will not print a \, it will use that \ in combination with the next character
# This is annoying because Windows uses \ to separate folder names
# There are 2 ways around this:
# 1. Use \\ instead of \. The first slash indicates to base what is show on the next character. The next character of \ indicates that a slash is to be shown.
# Example: "C:\\Documents\\RaceVsArrestType.csv" would be put the CSV file in C:\Documents
# 2. Preface the string with the letter r to indicate a raw string that does not use \ formating
# Example: r"C:\Documents\RaceVsArrestType.csv"
[24]:
# Again, we might want to show percentages instead of counts.
# You might try the following:
df_sizes / df_sizes.sum() * 100
# However, this divides each column by its sum to give the % of each arrest type that is each race (i.e. columns add to 100)
# However, you probably want to know if a certain group is more likely to get a certain type of arrest
[24]:
| Arrest Type | ON-VIEW ARREST | SUMMONED/ CITED | TAKEN INTO CUSTODY |
|---|---|---|---|
| race_eth | |||
| A | 2.217936 | 0.0 | 1.834862 |
| B | 22.95082 | 21.686747 | 27.981651 |
| H | 48.601736 | 63.855422 | 40.366972 |
| I | 0.289296 | 0.0 | 0.458716 |
| U | 1.832208 | 1.204819 | 1.834862 |
| W | 24.108004 | 13.253012 | 27.522936 |
[25]:
# This takes a little more work but there are 2 fairly simple ways to do it
# transpose swaps the columns and rows so now our columns are race/ethnicity and the operation in the above cell would work properly
df_trans = df_sizes.transpose()
result = df_trans / df_trans.sum() * 100
# Let's transpose back so that race/ethnicity are rows again
result.transpose()
[25]:
| Arrest Type | ON-VIEW ARREST | SUMMONED/ CITED | TAKEN INTO CUSTODY |
|---|---|---|---|
| race_eth | |||
| A | 92.0 | 0.0 | 8.0 |
| B | 85.765766 | 3.243243 | 10.990991 |
| H | 87.72846 | 4.612707 | 7.658834 |
| I | 85.714286 | 0.0 | 14.285714 |
| U | 88.372093 | 2.325581 | 9.302326 |
| W | 87.565674 | 1.926445 | 10.507881 |
[26]:
# Method #2: Use the divide command
# sum() sums down the rows (axis=0) by default, set axis=1 to sum across the columns
# divide(x, axis=0) divides df_sizes by x and axis=0 indicates that what we are dividing by contains a separate value for each row
df_sizes.divide(df_sizes.sum(axis=1), axis=0) * 100
[26]:
| Arrest Type | ON-VIEW ARREST | SUMMONED/ CITED | TAKEN INTO CUSTODY |
|---|---|---|---|
| race_eth | |||
| A | 92.0 | 0.0 | 8.0 |
| B | 85.765766 | 3.243243 | 10.990991 |
| H | 87.72846 | 4.612707 | 7.658834 |
| I | 85.714286 | 0.0 | 14.285714 |
| U | 88.372093 | 2.325581 | 9.302326 |
| W | 87.565674 | 1.926445 | 10.507881 |
There are a few other things that you might want to do#
It may possible that some people are charge with Drunk in Public multiple times in the same arrest. Maybe you only want to count each one once
It also may be possible that (A) people who arrested for being Drunk in Public were actually arrested for doing something more serious and the Drunk in Public was only a secondary charge OR (B) the presence of other charges affects Arrest Type
Check if the same people are being arrested repeatedly for drunk in public using the Arrestee Name ID column
RELATED NOTE Regarding Point 2 Above: It was previously shown that when someone is arrested for driving without a license (DWOL), they are almost always also charged with something else. This means that Fairfax County Police were rarely arresting someone for only DWOL (they may give a citation or warning instead). It was not examined what the something else was but since DWOL is pretty minor, it was assumed that Fairfax County Police mainly were arresting people for more serious offenses and in those cases was also charging for DWOL rather than giving a citation for DWOL.
It was previously found that an arrest of an individual for multiple charges can be identified: 1. The Case # indicates all charges associated with a single incidents. Multiple people can receive a charge for a given Case #. 2. The Arrest ID is unique to an individual for a given case #. However, the Arrest ID may be re-used for a different case #’s 3. We can identify all the charges for an individual in a single arrest by finding unique combinations of case # and arrest ID
[27]:
# Let's first remove any cases where someone is charged for Drunk In Public more than once
# We can do this by dropping duplicate pairs of "Case Number" and "Arrest ID" from df (our DataFrame that contains only rows for Drunk in Public)
# Before we do this with Case Number and Arrest ID, we should check if there are any empty/null values in Case Number and Arrest ID
# isnull() returns True if the value at the row and column is null (which in this case is equivalent to empty).
# df[["Case Number", "Arrest ID"]].isnull() will have 2 columns: the first will indicate where Case Number is null and the 2nd where Arrest ID is null
# .sum() will sum down the columns to give us the number of nulls
num_nulls = df[["Case Number", "Arrest ID"]].isnull().sum()
print(f"Case # has {num_nulls[0]} nulls out of {len(df)} rows")
print(f"Arrest ID has {num_nulls[1]} nulls out of {len(df)} rows")
Case # has 1008 nulls out of 2375 rows
Arrest ID has 0 nulls out of 2375 rows
Arrest ID has no null which is good. Case number has a lot of nulls. We need to figure out how to deal with these. We could: 1. Assume that Arrest IDs are mostly unique and ignore this issue assuming that it will have little effect 2. Incorporate some other information that increases likelihood that we have an arrest ID for a single case # (which is unknown) and Arrest ID. Perhaps, this could be the date and/or time of an arrest but then we would have to check that the date and/or time don’t change for a single case # and Arrest ID.
Let’s test out option 1 first because it’s easier
[28]:
# Let's go back to the full arrests table and check how many repeated arrest IDs there are
# Recall that .drop_duplicates() keeps only the unique rows (i.e. the combination of values in the row are unique)
tmp = t.table[["Case Number", "Arrest ID"]].drop_duplicates()
# Let's get the value counts
vc = tmp["Arrest ID"].value_counts()
# Let's keep only the value counts that are repeated (i.e. there are more than 1 of them)
vc = vc[vc>1]
# len gets the length of something which for DataFrames and Series gets us the number of rows
# The :.2f in the 3rd calculated value below just formats the result to only show 2 decimal places
print(f"There are {len(vc)} Arrest IDs that are repeated a total of {vc.sum()} times out of ({len(tmp)} {vc.sum()/len(tmp)*100:.2f}%) rows in our table")
There are 3 Arrest IDs that are repeated a total of 6 times out of (15451 0.04%) rows in our table
0.04% means that the Arrest ID is almost unique so Option 1 above is an acceptable way of dealing with the empty Case #’s
[29]:
# Now let's check if there are any cases where someone is charged with Drunk in Public multiple times
# .duplicated returns True for duplicated cases for each row based on the columns indicated in subset
# In this case, we are looking for duplicated Case Number / Arrest ID pairs
# keep=False indicates to mark all values that are duplicated as True. You can also choose to mark one (either 'first' or 'last') as False which is useful in some cases
has_multiple_cases = df.duplicated(subset=["Case Number", "Arrest ID"], keep=False)
# Create a new table with only the duplicates
df_multiple = df[has_multiple_cases]
# Find the # of unique Case Number / Arrest ID pairs in df_multiple by finding the # of rows in the table after duplicate values of Case # / Arrest ID have been removed
num_unique_cases = len(df_multiple[["Case Number", "Arrest ID"]].drop_duplicates())
print(f"There are {num_unique_cases} cases where a person is charged multiple times for Drunk in Public for a total of {len(df_multiple)} out of {len(df)} charges")
There are 10 cases where a person is charged multiple times for Drunk in Public for a total of 20 out of 2375 charges
There aren’t many cases where someone is charged more than once for Drunk in Public. It’s not going to much of a difference whether you keep or drop them. I would just drop them because in case someone asks about it, answering that you dropped them will likely be the most satisfactory answer and also add confidence to the audience about your decision-making during the analysis
Before we drop them, let’s explore the duplicated cases a bit…
[30]:
# Let's look at the statute description and race/ethnicity for duplicated cases
df_multiple.groupby(["race_eth", "Statute Description"]).size().unstack()
[30]:
| Statute Description | ALCH BEV TEND/DRK IN PUBLIC | DRUNK IN PUBLIC OR PROFANE |
|---|---|---|
| race_eth | ||
| B | 3 | 3 |
| H | 6 | 6 |
| W | 1 | 1 |
For all duplicated cases, someone was charged with both ALCH BEV TEND/DRK IN PUBLIC and DRUNK IN PUBLIC OR PROFANE. I think it’s safe to drop these.
[31]:
# Let's create a new table where we have dropped the duplicates
# drop_duplicates(subset=["Case Number", "Arrest ID"]) only considers columns Case Number and Arrest ID when dropping duplicates.
# When we were using drop_duplicates before with no inputs (i.e. drop_duplicates()), it was only keeping the row if the entire row was unique
# Now, we keep rows where Case Number and Arrest ID are unique
df_no_dups = df.drop_duplicates(subset=["Case Number", "Arrest ID"])
[32]:
# Let's look at arrest types by race/ethnicity for our table with duplicates dropped
# Same operation previously done across multiple cells rolled up into a single line of code
df_sizes_no_dups = df_no_dups.groupby(["race_eth","Arrest Type"]).size().unstack().fillna(0).convert_dtypes()
print("Percentages BEFORE duplicates dropped:")
# Putting the below in parentheses allows calling functions on the resulting DataFrame after multiplication
# .round(1) rounds to 1 decimal place
print((df_sizes.divide(df_sizes.sum(axis=1), axis=0) * 100).round(1))
print("\n\nPercentages AFTER duplicates dropped:")
print((df_sizes_no_dups.divide(df_sizes_no_dups.sum(axis=1), axis=0) * 100).round(1))
Percentages BEFORE duplicates dropped:
Arrest Type ON-VIEW ARREST SUMMONED/ CITED TAKEN INTO CUSTODY
race_eth
A 92.0 0.0 8.0
B 85.8 3.2 11.0
H 87.7 4.6 7.7
I 85.7 0.0 14.3
U 88.4 2.3 9.3
W 87.6 1.9 10.5
Percentages AFTER duplicates dropped:
Arrest Type ON-VIEW ARREST SUMMONED/ CITED TAKEN INTO CUSTODY
race_eth
A 92.0 0.0 8.0
B 85.7 3.3 11.1
H 87.7 4.6 7.7
I 85.7 0.0 14.3
U 88.4 2.3 9.3
W 87.5 1.9 10.5
Not suprisingly the change is small because we only dropped a few cases
Now, let’s check the Arrestee Name ID to see how often the same people are getting arrested for Drunk in Public
[33]:
# The result of value_counts is sorted
# If a number is input to head(), the number of rows at the top of the tail that are printed is that number instead of the default 5
df_no_dups["Arrestee Name ID"].value_counts().head(10)
[33]:
123804471 49
2558299 24
123912580 13
124523924 13
285512 12
124356229 10
2610946 10
3129951 9
3030597 9
124246336 9
Name: Arrestee Name ID, dtype: int64
Some of the people arrested for Drunk in Public are being arrested many times
[34]:
# Let's see what percentage of the arrests are for the same people
# This could be varied to evaluate different cases
threshold = 2
vc = df_no_dups["Arrestee Name ID"].value_counts()
# Remove value counts that do not exceed the threshold
vc_exceed_thresh = vc[vc>=threshold]
count = vc_exceed_thresh.sum()
total = len(df_no_dups)
print(f"{count} charges out of {total} total ({count/total*100:.2f}%) are for individuals arrested at least {threshold} times")
929 charges out of 2365 total (39.28%) are for individuals arrested at least 2 times
A few things could be done with this information including: 1. Examine race/ethnicity only counting each individual once 2. Examine race/ethnicity of individuals arrested more than once
[35]:
# Let's look at only counting each individual once first
df_each_ind_once = df_no_dups.drop_duplicates(subset="Arrestee Name ID")
print(f"Percentages for all cases (Total: {len(df_no_dups)}):")
print(df_no_dups["race_eth"].value_counts(normalize=True).round(3)*100)
print(f"\n\nPercentages for unique individuals (Total: {len(df_each_ind_once)}):")
print(df_each_ind_once["race_eth"].value_counts(normalize=True).round(3)*100)
Percentages for all cases (Total: 2365):
H 48.3
W 24.1
B 23.3
A 2.1
U 1.8
I 0.3
Name: race_eth, dtype: float64
Percentages for unique individuals (Total: 1717):
H 49.0
W 23.5
B 22.5
U 2.4
A 2.3
I 0.2
Name: race_eth, dtype: float64
The percentages for each race are pretty similar for both all cases and when only unique individuals are counted
Now, let’s focus on individuals arrests multiple times
[48]:
threshold = 2 # This could be varied to evaluate different cases
vc = df_no_dups["Arrestee Name ID"].value_counts()
# Remove value counts that do not exceed the threshold
vc_exceed_thresh = vc[vc>=threshold]
# The row labels are termed indices and can be accessed with .index
name_ids_for_exceeds = vc_exceed_thresh.index
# Let's create a DataFrame that only contains the names in name_ids_for_exceeds
# .isin(name_ids_for_exceeds) is True for values that are in name_ids_for_exceeds
has_matching_name = df_no_dups["Arrestee Name ID"].isin(name_ids_for_exceeds)
# Let's only keep one row per name so that each is only counted once when we look at race/ethnicity
df_exceeds = df_no_dups[has_matching_name].drop_duplicates(subset="Arrestee Name ID")
print(f"Counts for all cases (Total: {len(df_no_dups)}):")
print(df_no_dups["race_eth"].value_counts())
print(f"\n\nCounts for individuals arrests multiple times (Total: {len(df_exceeds)}):")
print(df_exceeds["race_eth"].value_counts())
print(f"\n\nPercentages for all cases (Total: {len(df_no_dups)}):")
print(df_no_dups["race_eth"].value_counts(normalize=True).round(3)*100)
print(f"\n\nPercentages for individuals arrests multiple times (Total: {len(df_exceeds)}):")
print(df_exceeds["race_eth"].value_counts(normalize=True).round(3)*100)
Counts for all cases (Total: 2365):
H 1143
W 570
B 552
A 50
U 43
I 7
Name: race_eth, dtype: int64
Counts for individuals arrests multiple times (Total: 281):
H 127
B 80
W 64
A 7
U 2
I 1
Name: race_eth, dtype: int64
Percentages for all cases (Total: 2365):
H 48.3
W 24.1
B 23.3
A 2.1
U 1.8
I 0.3
Name: race_eth, dtype: float64
Percentages for individuals arrests multiple times (Total: 281):
H 45.2
B 28.5
W 22.8
A 2.5
U 0.7
I 0.4
Name: race_eth, dtype: float64
Of people arrests at least twice, there is a slightly higher proportion that are Black and slighly lower that are Latino and White
Finally, let’s check the frequency that Drunk in Public arrests are also accompanied by other charges
[37]:
# This is going to be a little trickier. We need to look up the case numbers in our Drunk in Public DataFrame df_no_dups
# that are also in our DataFrame of all arrests df_all
def arrest_isin(x, other_df):
# Find cases where the case # and arrest ID of x are in other_df
# Case number can sometimes be null and nulls don't always equal other nulls so handle this case separately
if pd.isnull(x["Case Number"]):
# If x["Case Number"] is null / empty, this part of the code will run
case_num_matches = other_df["Case Number"].isnull()
else:
# If x["Case Number"] is NOT null / empty, this part of the code will run
# The double equals sign == indicates to compare if 2 things are equal
# In this case, it compares each value of the column Case Number in other_df to the single value of x["Case Number"] since x will be single row of df_no_dups
case_num_matches = other_df["Case Number"]==x["Case Number"]
arrest_id_matches = other_df["Arrest ID"]==x["Arrest ID"]
# Find cases where both case number and arrest ID match
matches = case_num_matches & arrest_id_matches
# If any value in matches is True, then the Case Number and Arrest ID are in other_df and we want to return True
# .any() returns True if any value in matches is True and False otherwise
return matches.any()
# If you use apply without the args command, the function will have 1 input (an individual row). This is what we did last time
# By setting args, we can pass in more than 1 argument. The value of other_df in arrest_isin will be df_no_dups
matches_in_full_table = df_all[["Case Number", "Arrest ID"]].apply(arrest_isin, axis=1, args=(df_no_dups,))
num_matches = matches_in_full_table.sum()
print(f"Found {num_matches} values in the entire Arrests Table with the same Arrest ID and Case # as any of the {len(df_no_dups)} Drunk in Public arrests")
print(f"Since all the Drunk in Public arrests will also be matches, there are {num_matches - len(df_no_dups)} additional arrests found (which includes previously dropped duplicates)")
Found 3447 values in the entire Arrests Table with the same Arrest ID and Case # as any of the 2365 Drunk in Public arrests
Since all the Drunk in Public arrests will also be matches, there are 1082 additional arrests found (which includes previously dropped duplicates)
Let’s look at: 1. What the other charges are in cases involved in drunk in public 2. The race/ethnicity of individuals arrestsed for only drunk in public
[38]:
# To examine the other charges, let's create a table from the matches that excludes the Drunk in Public Arrests
df_other = df_all[matches_in_full_table]
# In order to remove Drunk in Public, let's exclude the values of Statute that are also in our Drunk in Public table
df_other = df_other[~df_other["Statute"].isin(df_no_dups["Statute"].unique())]
print(f"There are {len(df_other)} charges identified that are not Drunk in Public\n")
vc = df_other["Statute Description"].value_counts()
print(f"There are {len(vc)} unique statutes found with the most popular being:")
vc.head(10)
There are 1072 charges identified that are not Drunk in Public
There are 127 unique statutes found with the most popular being:
[38]:
ASSAULT: ON LAW ENF/JUDGE/DOC/FIRE/EMERG PERSON 76
ASSAULT: (MISDEMEANOR) 75
OBSTRUCT JUSTICE: W/O THREATS/FORCE 71
DISORDERLY CONDUCT 62
TRESPASS: AFTER BEING FORBIDDEN TO DO SO 60
DRUGS: POSSESS SCH I OR II 58
PETIT LARCENY: <$200 NOT FROM A PERSON 49
FALSE IDENTIFY SELF TO LAW ENFORCEMENT 48
TRESPASS-POSTED OR ADVISED TO LV 39
OBSTRUCT JUSTICE: W/ THREATS/FORCE 35
Name: Statute Description, dtype: int64
[39]:
# Now, let's look at the race/ethnicity of individuals only charged with Drunk in Public
# We can use the same arrestisin function but in reverse
matches_single_charge = df_no_dups[["Case Number", "Arrest ID"]].apply(arrest_isin, axis=1, args=(df_other,))
print(f"There are {matches_single_charge.sum()} arrets for Drunk in Public with 1 charge out of {len(df_no_dups)} ({matches_single_charge.sum()/len(df_no_dups)*100:.2f}%)")
There are 670 arrets for Drunk in Public with 1 charge out of 2365 (28.33%)
[42]:
df_single_charge = df_no_dups[matches_single_charge]
df_sizes_single_charge = df_single_charge.groupby(["race_eth","Arrest Type"]).size().unstack().fillna(0).convert_dtypes()
print("Counts for all arrests for Drunk in Public:")
print(df_sizes_no_dups)
print("\n\nCounts for single-charge arrests for Drunk in Public:")
print(df_sizes_single_charge)
print("\n\nPercentages for all arrests for Drunk in Public:")
print((df_sizes_no_dups.divide(df_sizes_no_dups.sum(axis=1), axis=0) * 100).round(1))
print("\n\nPercentages for single-charge arrests for Drunk in Public:")
print((df_sizes_single_charge.divide(df_sizes_single_charge.sum(axis=1), axis=0) * 100).round(1))
Counts for all arrests for Drunk in Public:
Arrest Type ON-VIEW ARREST SUMMONED/ CITED TAKEN INTO CUSTODY
race_eth
A 46 0 4
B 473 18 61
H 1002 53 88
I 6 0 1
U 38 1 4
W 499 11 60
Counts for single-charge arrests for Drunk in Public:
Arrest Type ON-VIEW ARREST SUMMONED/ CITED TAKEN INTO CUSTODY
race_eth
A 7 0 0
B 171 4 25
H 278 3 23
I 3 0 0
U 8 0 1
W 131 2 14
Percentages for all arrests for Drunk in Public:
Arrest Type ON-VIEW ARREST SUMMONED/ CITED TAKEN INTO CUSTODY
race_eth
A 92.0 0.0 8.0
B 85.7 3.3 11.1
H 87.7 4.6 7.7
I 85.7 0.0 14.3
U 88.4 2.3 9.3
W 87.5 1.9 10.5
Percentages for single-charge arrests for Drunk in Public:
Arrest Type ON-VIEW ARREST SUMMONED/ CITED TAKEN INTO CUSTODY
race_eth
A 100.0 0.0 0.0
B 85.5 2.0 12.5
H 91.4 1.0 7.6
I 100.0 0.0 0.0
U 88.9 0.0 11.1
W 89.1 1.4 9.5
[44]:
# Let's check the names for the single charge arrests
df_single_charge["Arrestee Name ID"].value_counts().head(10)
[44]:
123804471 10
2610946 8
123912580 8
124246336 6
2465016 4
124341716 4
285512 4
124271338 4
124389730 4
124296344 4
Name: Arrestee Name ID, dtype: int64
[45]:
# Let's see what percentage of the arrests are for the same people
# This could be varied to evaluate different cases
threshold = 2
vc = df_single_charge["Arrestee Name ID"].value_counts()
# Remove value counts that do not exceed the threshold
vc_exceed_thresh = vc[vc>=threshold]
count = vc_exceed_thresh.sum()
total = len(df_single_charge)
print(f"{count} charges out of {total} total ({count/total*100:.2f}%) are for individuals arrested at least {threshold} times")
print("This is lower than for all Drunk in Public arrests")
189 charges out of 670 total (28.21%) are for individuals arrested at least 2 times
[50]:
threshold = 2 # This could be varied to evaluate different cases
vc = df_single_charge["Arrestee Name ID"].value_counts()
# Remove value counts that do not exceed the threshold
vc_exceed_thresh = vc[vc>=threshold]
# The row labels are termed indices and can be accessed with .index
name_ids_for_exceeds = vc_exceed_thresh.index
# Let's create a DataFrame that only contains the names in name_ids_for_exceeds
# .isin(name_ids_for_exceeds) is True for values that are in name_ids_for_exceeds
has_matching_name = df_single_charge["Arrestee Name ID"].isin(name_ids_for_exceeds)
# Let's only keep one row per name so that each is only counted once when we look at race/ethnicity
df_exceeds = df_single_charge[has_matching_name].drop_duplicates(subset="Arrestee Name ID")
print(f"Counts for all cases (Total: {len(df_single_charge)}):")
print(df_single_charge["race_eth"].value_counts())
print(f"\n\nCounts for individuals arrests multiple times (Total: {len(df_exceeds)}):")
print(df_exceeds["race_eth"].value_counts())
print(f"\n\nPercentages for all cases (Total: {len(df_single_charge)}):")
print(df_single_charge["race_eth"].value_counts(normalize=True).round(3)*100)
print(f"\n\nPercentages for individuals arrests multiple times (Total: {len(df_exceeds)}):")
print(df_exceeds["race_eth"].value_counts(normalize=True).round(3)*100)
Counts for all cases (Total: 670):
H 304
B 200
W 147
U 9
A 7
I 3
Name: race_eth, dtype: int64
Counts for individuals arrests multiple times (Total: 68):
H 35
B 23
W 9
I 1
Name: race_eth, dtype: int64
Percentages for all cases (Total: 670):
H 45.4
B 29.9
W 21.9
U 1.3
A 1.0
I 0.4
Name: race_eth, dtype: float64
Percentages for individuals arrests multiple times (Total: 68):
H 51.5
B 33.8
W 13.2
I 1.5
Name: race_eth, dtype: float64