library(tidyverse)
library(knitr)
setwd("~/Schol Stuff/Montgomery College 2025/Data 110 Data Visualization/Hate Crimes HW")
hatecrimes <- read_csv('NYPD_Hate_Crimes_19-26.csv')NY Hate Crimes Tutorial
NY Hate Crimes, 2019-2026
About the data set
This dataset looks at all types of hate crimes in New York counties by the type of hate crime from 2019 to 2026. https://data.cityofnewyork.us/Public-Safety/NYPD-Hate-Crimes/bqiq-cu78/about_data
However, the set is flawed as it relies on either the victim first reporting a crime to the police, or, if they did so, the police to recognize the crime as a hate crime. And, if those two events happened, for the it to be recognized as a hate crime against the correct group/characteristic.
We know there is bias in the data. What can we do with it?
Clean up the data:
Make all headers lowercase and remove spaces
use tolower to make all the column names lowercase, and gsub to remove the spaces
names(hatecrimes) <- tolower(names(hatecrimes))
names(hatecrimes) <- gsub(" ","",names(hatecrimes)) #replace the space with no space in the names of hatecrimes columns
head(hatecrimes)# A tibble: 6 × 14
fullcomplaintid complaintyearnumber monthnumber recordcreatedate
<dbl> <dbl> <dbl> <chr>
1 2.02e14 2019 1 1/23/2019
2 2.02e14 2019 2 2/25/2019
3 2.02e14 2019 2 2/27/2019
4 2.02e14 2019 4 4/16/2019
5 2.02e14 2019 6 6/20/2019
6 2.02e14 2019 7 7/31/2019
# ℹ 10 more variables: complaintprecinctcode <dbl>, patrolboroughname <chr>,
# county <chr>, lawcodecategorydescription <chr>, offensedescription <chr>,
# pdcodedescription <chr>, biasmotivedescription <chr>,
# offensecategory <chr>, arrestdate <lgl>, arrestid <chr>
] ## Explore the Bias Motive (biasmotivedescription)
bias_count <- hatecrimes |>
select(biasmotivedescription) |>
group_by(biasmotivedescription) |>
count() |>
arrange(desc(n))
head(bias_count)# A tibble: 6 × 2
# Groups: biasmotivedescription [6]
biasmotivedescription n
<chr> <int>
1 ANTI-JEWISH 1906
2 ANTI-MALE HOMOSEXUAL (GAY) 489
3 ANTI-ASIAN 401
4 ANTI-BLACK 315
5 ANTI-OTHER ETHNICITY 168
6 ANTI-MUSLIM 156
Visualize thse counts as a bar graph
ggplot(hatecrimes, aes(x = biasmotivedescription)) +
geom_bar()Use inclusion/exclusion criteria to filter
There are 29 different motives, some with only one or two crimes. Filter for the top ten using the bias_count subset with geom_col()
bias_count |>
head(10) |>
ggplot(aes(x = biasmotivedescription, y = n)) +
geom_col() Arrange the bars according to height, and rotate
Use ‘reorder’ and ‘coord_flip’
bias_count |>
head(10) |>
ggplot(aes(x = reorder(biasmotivedescription, n), y = n)) +
geom_col() +
coord_flip()Add title, caption for data source, and x-axis label
bias_count |>
head(10) |>
ggplot(aes(x = reorder(biasmotivedescription, n), y = n)) +
geom_col() +
coord_flip() +
labs(x = "",
y = "counts of hate crime types based on motive",
title = "Bar Graph of Hate Crimes from 2019-2026",
subtitle = "Counts Based on the Hate Crime Motive",
caption = "Source: NY Division of Criminal Justice Services")Finally add color and change the theme
bias_count |>
head(10) |>
ggplot(aes(x = reorder(biasmotivedescription, n), y = n)) +
geom_col(fill = "darkred") +
coord_flip() +
labs(x = "",
y = "counts of hate crime types based on motive",
title = "Bar Graph of Hate Crimes from 2019-2026",
subtitle = "Counts Based on the Hate Crime Motive",
caption = "Source: NY Division of Criminal Justice Services") +
theme_bw()Add annotations for counts and remove the x-axis values
Aso expanding the y axis limit so the count for the anti-Jewish crimes fit on the plot.
bias_count |>
head(10) |>
ggplot(aes(x = reorder(biasmotivedescription, n), y = n)) +
geom_col(fill = "darkred") +
ylim(0,2050) +
coord_flip() +
labs(x = "",
y = "counts of hate crime types based on motive",
title = "Bar Graph of Hate Crimes from 2019-2026",
subtitle = "Counts Based on the Hate Crime Motive",
caption = "Source: NY Division of Criminal Justice Services") +
theme_bw() +
geom_text(aes(label = n), hjust = -.05, size = 3.6) +
theme(axis.text.x = element_blank())Look deeper into crimes against Jewish, Asian, Black people, and gay males
Remember to mind spelling.
First check the year totals
hate_year <- hatecrimes|>
filter(biasmotivedescription %in% c("ANTI-JEWISH", "ANTI-MALE HOMOSEXUAL (GAY)", "ANTI-ASIAN", "ANTI-BLACK")) |>
group_by(complaintyearnumber) |>
count(biasmotivedescription) |>
arrange(desc(n))
hate_year# A tibble: 28 × 3
# Groups: complaintyearnumber [7]
complaintyearnumber biasmotivedescription n
<dbl> <chr> <int>
1 2024 ANTI-JEWISH 371
2 2023 ANTI-JEWISH 343
3 2025 ANTI-JEWISH 320
4 2022 ANTI-JEWISH 279
5 2019 ANTI-JEWISH 252
6 2021 ANTI-JEWISH 215
7 2021 ANTI-ASIAN 150
8 2020 ANTI-JEWISH 126
9 2023 ANTI-MALE HOMOSEXUAL (GAY) 116
10 2022 ANTI-ASIAN 91
# ℹ 18 more rows
Then check the county totals
hate_county <- hatecrimes |>
filter(biasmotivedescription %in% c("ANTI-JEWISH", "ANTI-MALE HOMOSEXUAL (GAY)", "ANTI-ASIAN", "ANTI-BLACK")) |>
group_by(county) |>
count(biasmotivedescription) |>
arrange(desc(n))
hate_county# A tibble: 20 × 3
# Groups: county [5]
county biasmotivedescription n
<chr> <chr> <int>
1 KINGS ANTI-JEWISH 798
2 NEW YORK ANTI-JEWISH 651
3 QUEENS ANTI-JEWISH 289
4 NEW YORK ANTI-MALE HOMOSEXUAL (GAY) 237
5 NEW YORK ANTI-ASIAN 228
6 KINGS ANTI-MALE HOMOSEXUAL (GAY) 120
7 KINGS ANTI-BLACK 99
8 BRONX ANTI-JEWISH 92
9 QUEENS ANTI-MALE HOMOSEXUAL (GAY) 91
10 KINGS ANTI-ASIAN 80
11 NEW YORK ANTI-BLACK 79
12 QUEENS ANTI-ASIAN 78
13 RICHMOND ANTI-JEWISH 76
14 QUEENS ANTI-BLACK 75
15 BRONX ANTI-MALE HOMOSEXUAL (GAY) 35
16 RICHMOND ANTI-BLACK 35
17 BRONX ANTI-BLACK 27
18 BRONX ANTI-ASIAN 10
19 RICHMOND ANTI-MALE HOMOSEXUAL (GAY) 6
20 RICHMOND ANTI-ASIAN 5
Check information combining totals from counties and years
hate2 <- hatecrimes |>
filter(biasmotivedescription %in% c("ANTI-JEWISH", "ANTI-MALE HOMOSEXUAL (GAY)", "ANTI-ASIAN", "ANTI-BLACK")) |>
group_by(complaintyearnumber, county) |>
rename(year = complaintyearnumber) |> # shorted the name of this column so I can see all the columns in output without having to scroll sideways (working in minimized half-screen windows)
count(biasmotivedescription) |>
arrange(desc(n))
hate2# A tibble: 127 × 4
# Groups: year, county [35]
year county biasmotivedescription n
<dbl> <chr> <chr> <int>
1 2024 KINGS ANTI-JEWISH 152
2 2024 NEW YORK ANTI-JEWISH 136
3 2025 KINGS ANTI-JEWISH 136
4 2019 KINGS ANTI-JEWISH 128
5 2023 KINGS ANTI-JEWISH 126
6 2022 KINGS ANTI-JEWISH 125
7 2023 NEW YORK ANTI-JEWISH 124
8 2025 NEW YORK ANTI-JEWISH 110
9 2022 NEW YORK ANTI-JEWISH 104
10 2021 NEW YORK ANTI-ASIAN 84
# ℹ 117 more rows
Plot these four types of hate crimes together
position = “dodge” makes side by side bars stat = “identity” lets you plot bars for each group for each year (2019-2026) labs title titles the entire plot, labs fill titles the legend
ggplot(data = hate2) +
geom_bar(aes(x=year, y=n, fill = biasmotivedescription),
position = "dodge", stat = "identity") +
labs(fill = "Hate Crime Type",
y = "Number of Nate Crime Incidents",
title = "Hate Crimie Type in NY Counties Between 2019 -2026",
caption = "Source: NY State Division of Criminal Justice Service") +
scale_fill_brewer(palette = "Dark2") +
theme_bw()What about the counties?
Make the bar graph by county instead of year
ggplot(data = hate2) +
geom_bar(aes(x = county, y = n, fill = biasmotivedescription),
position = "dodge", stat = "identity") +
labs(fill = "Hate Crime Type",
y = "Number of Hate Crime INcidents",
title = "Hate Crime in NY Counties Between 2019-2026",
caption = "Source: NY State Divisin of Criminal Justice Services") +
scale_fill_brewer(palette = "Accent") +
theme(axis.text.x = element_text(angle = 45)) +
theme_minimal()The Hightest Counts
We can see that the highest counts of hate crimes against Jewish, Asian, and Black people took place in Kings County (Brooklyn) and New York City
Put it all together with yeras and counties using “facet”
ggplot(data = hate2) +
geom_bar(aes(x=year, y=n, fill = biasmotivedescription), #
position = "dodge", stat = "identity") +
facet_wrap(~county) +
labs(fill = "Hate Crime Type",
y = "Number of Hate Crime INcidents",
title = "Hate Crime Type in NY Counties Between 20219-2026",
caption = "Source: NY State Division of Criminal Justice Services") +
scale_fill_brewer(palette = "Dark2") +
theme_bw()How would calculations be affected by looking at hate crimes in counties per year by population densities?
Bring in census data for populations of New York counties. These are estimates from the 2010 census.
setwd("~/Schol Stuff/Montgomery College 2025/Data 110 Data Visualization/Hate Crimes HW")
nypop <- read_csv("nyc_census_pop_2020.csv")Clean the county name to match the other dataset
Rename the variable “Area name” as “county” so it matches the first dataset
nypop$`Area Name`<- gsub( " County", "", nypop$`Area Name`) #X County becomes X
nypop2 <- nypop |>
rename(county = `Area Name`)|>
select(county, `2020 Census Population`)
head(nypop2)# A tibble: 6 × 2
county `2020 Census Population`
<chr> <dbl>
1 Albany 314848
2 Allegany 46456
3 Bronx 1472654
4 Broome 198683
5 Cattaraugus 77042
6 Cayuga 76248
##Join the hate2 data with nypop2
datajoin <- left_join(hate2, nypop2, by=c("county"))
datajoin# A tibble: 127 × 5
# Groups: year, county [35]
year county biasmotivedescription n `2020 Census Population`
<dbl> <chr> <chr> <int> <dbl>
1 2024 KINGS ANTI-JEWISH 152 NA
2 2024 NEW YORK ANTI-JEWISH 136 NA
3 2025 KINGS ANTI-JEWISH 136 NA
4 2019 KINGS ANTI-JEWISH 128 NA
5 2023 KINGS ANTI-JEWISH 126 NA
6 2022 KINGS ANTI-JEWISH 125 NA
7 2023 NEW YORK ANTI-JEWISH 124 NA
8 2025 NEW YORK ANTI-JEWISH 110 NA
9 2022 NEW YORK ANTI-JEWISH 104 NA
10 2021 NEW YORK ANTI-ASIAN 84 NA
# ℹ 117 more rows
It didnt work: new column has NA values
The counties are all in uppercase in hate 2, and mixed in nypop
hate_new <- hate2 |>
mutate(county = as.factor(str_to_lower(as.character(county))))
nypop_new <- nypop2 |>
mutate(county = as.factor(str_to_lower(as.character(county))))Try joining again
datajoin <- left_join(hate_new, nypop_new, by=c("county"))
datajoin# A tibble: 127 × 5
# Groups: year, county [35]
year county biasmotivedescription n `2020 Census Population`
<dbl> <fct> <chr> <int> <dbl>
1 2024 kings ANTI-JEWISH 152 2736074
2 2024 new york ANTI-JEWISH 136 1694251
3 2025 kings ANTI-JEWISH 136 2736074
4 2019 kings ANTI-JEWISH 128 2736074
5 2023 kings ANTI-JEWISH 126 2736074
6 2022 kings ANTI-JEWISH 125 2736074
7 2023 new york ANTI-JEWISH 124 1694251
8 2025 new york ANTI-JEWISH 110 1694251
9 2022 new york ANTI-JEWISH 104 1694251
10 2021 new york ANTI-ASIAN 84 1694251
# ℹ 117 more rows
Calculate the rate of incidents per 100,000. Then arrange in descending order
datajoinrate <- datajoin |>
mutate(rate = n / `2020 Census Population`* 100000) |>
arrange(desc(rate))
datajoinrate# A tibble: 127 × 6
# Groups: year, county [35]
year county biasmotivedescription n `2020 Census Population` rate
<dbl> <fct> <chr> <int> <dbl> <dbl>
1 2024 new york ANTI-JEWISH 136 1694251 8.03
2 2023 new york ANTI-JEWISH 124 1694251 7.32
3 2025 new york ANTI-JEWISH 110 1694251 6.49
4 2022 new york ANTI-JEWISH 104 1694251 6.14
5 2024 kings ANTI-JEWISH 152 2736074 5.56
6 2025 kings ANTI-JEWISH 136 2736074 4.97
7 2021 new york ANTI-ASIAN 84 1694251 4.96
8 2021 new york ANTI-JEWISH 84 1694251 4.96
9 2019 kings ANTI-JEWISH 128 2736074 4.68
10 2023 kings ANTI-JEWISH 126 2736074 4.61
# ℹ 117 more rows
Your Turn!
As mentioned at the top, this data set is biased as populations that don’t trust the police may not report crimes against them, or police may not recognize a crime as a hate crime, or the police may miscategorize a crime. I noticed the offense category column, and that it identifies anti-transgender and -gender conforming crimes as “gender” based and anti-homosexuality crimes as “sexual orientation”. This is excellent, as it shows that at least someone in the NYPD who understands that sexual orientation and gender identity are not the same thing and that there are distinct populations within the LGBTQ community, however it also means that there is no aggregate LGTBQ+ category. There are five (counting anti-GNC) bias motives descriptions across two offense categories.
I’d like to do further analysis on the anti-lgbtq+ crimes. I suspect that the anti-transgender crimes are under reported: 79 seems suspiciously low; it’s possible crimes against transwomen were improperly classified as anti–male homosexual. I would like to first find the number of crimes against the aggregate and each population by year (and county).
I’d also like to analyze the crimes by offense code or pd code description, and see if the severity of crimes is consistent across populations and years
```