Week 3 Hate Crime Analysis

Author

George Bothos

Load Libraries

library(tidyverse)
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.2.1     ✔ readr     2.2.0
✔ forcats   1.0.1     ✔ stringr   1.6.0
✔ ggplot2   4.0.3     ✔ tibble    3.3.1
✔ lubridate 1.9.5     ✔ tidyr     1.3.2
✔ purrr     1.2.2     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(knitr)

Load the Data

hatecrimes <- read_csv("NYPD_Hate_Crimes_19-26.csv")
Rows: 4029 Columns: 14
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (9): Record Create Date, Patrol Borough Name, County, Law Code Category ...
dbl (4): Full Complaint ID, Complaint Year Number, Month Number, Complaint P...
lgl (1): Arrest Date

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.

Cleaning the Data

names(hatecrimes) <- tolower(names(hatecrimes))
names(hatecrimes) <- gsub(" ", "", names(hatecrimes))
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>

Exploring Bias Motives

So, the bias motive description will be identifying the group that is targeted by each of the hate crimes that are reported. Furthermore, the following code is going to count exactly hwo many of these incidents appear in each of the appropriate cetegories they fit into.

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

The First Bar Graph of the Bias Motives

This bar graph is going to show us the number of reported incidents accounted for every bias motive that exists int he data set.

ggplot(hatecrimes, aes(x = biasmotivedescription)) +
  geom_bar()

Top 10 Bias Motives

Because when looking at the original graph we can see it just has way too many categories, this graph is going to only show us the ten bias motives with the highest count to make it more focused and for us to be able to see the data more clearly.

bias_count |>
  head(10) |>
  ggplot(aes(x = biasmotivedescription, y = n)) +
  geom_col()

Arranging and Rotating the Bars

Now I am ordering the bars by their count and turning the graph sideways so that the labels are easier to read.

bias_count |>
  head(10) |>
  ggplot(aes(x = reorder(biasmotivedescription, n), y = n)) +
  geom_col() +
  coord_flip()

Adding Labels to the Bar Graph

This version is going to add a clear label, title, and data source.

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 State Division of Criminal Justice Services")

Adding Color and Changing the Theme

This version now adds color to it and is going to use a much simpler background that will make everything a lot easier to read.

bias_count |>
  head(10) |>
  ggplot(aes(x = reorder(biasmotivedescription, n), y = n)) +
  geom_col(fill = "salmon") +
  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 State Division of Criminal Justice Services") +
  theme_minimal()

Adding Count Labels

This graph is now going to show the exact count next to each of the bars and it will remove the horizontal axis numbers.

bias_count |>
  head(10) |>
  ggplot(aes(x = reorder(biasmotivedescription, n), y = n)) +
  geom_col(fill = "salmon") +
  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 State Division of Criminal Justice Services") +
  theme_minimal() +
  geom_text(aes(label = n), hjust = -.05, size = 3) +
  theme(axis.text.x = element_blank())

Hate Crimes by Year

This next section will just focus on four of the most common bias motives and will compare the reported counts each of them have by year.

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

Hate Crimes by County

This table will now compare the four bias motives we selected across the five New York City counties.

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

Hate Crimes by Year and County

Now this table will compare the four bias motives we selected by both year and county.

hate2 <- hatecrimes |>
  filter(biasmotivedescription %in% c("ANTI-JEWISH",
                                      "ANTI-MALE HOMOSEXUAL (GAY)",
                                      "ANTI-ASIAN",
                                      "ANTI-BLACK")) |>
  group_by(complaintyearnumber, county) |>
  count(biasmotivedescription) |>
  arrange(desc(n))

hate2
# A tibble: 127 × 4
# Groups:   complaintyearnumber, county [35]
   complaintyearnumber 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

Compare Hate Crime Types by Year

This graph will now compare the four bias motives we selected across the years that appear in the data set.

ggplot(data = hate2) +
  geom_bar(aes(x = complaintyearnumber,
               y = n,
               fill = biasmotivedescription),
           position = "dodge",
           stat = "identity") +
  labs(fill = "Hate Crime Type",
       y = "Number of Hate Crime Incidents",
       title = "Hate Crime Types in New York by Year",
       caption = "Source: NY State Division of Criminal Justice Services")

Comparing Hate Crime Types by County

This graph will now compare the four bias motives we selected across the New York City counties.

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",
       x = "County",
       title = "Hate Crime Types in New York by County",
       caption = "Source: NY State Division of Criminal Justice Services")

Compare Years and Counties with Facets

Now we will have a graph that will separate the results by county which will make it easier for us to compare the selected bias motives when looking at them across many different years.

ggplot(data = hate2) +
  geom_bar(aes(x = complaintyearnumber,
               y = n,
               fill = biasmotivedescription),
           position = "dodge",
           stat = "identity") +
  facet_wrap(~county) +
  labs(fill = "Hate Crime Type",
       x = "Year",
       y = "Number of Hate Crime Incidents",
       title = "Hate Crime Types by Year and County",
       caption = "Source: NY State Division of Criminal Justice Services")

Loading Census Population Data

The census data set here is providing us with population totals that we can then later combine with the hate crimes data.

nypop <- read_csv("nyc_census_pop_2020.csv")
Rows: 62 Columns: 4
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (2): Area Name, Population Percent Change
num (2): 2020 Census Population, Population Change

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
head(nypop)
# A tibble: 6 × 4
  `Area Name`  2020 Census Populati…¹ `Population Change` Population Percent C…²
  <chr>                         <dbl>               <dbl> <chr>                 
1 Albany Coun…                 314848               10644 3.50%                 
2 Allegany Co…                  46456               -2490 -5.09%                
3 Bronx County                1472654               87546 6.32%                 
4 Broome Coun…                 198683               -1917 -0.96%                
5 Cattaraugus…                  77042               -3275 -4.08%                
6 Cayuga Coun…                  76248               -3778 -4.72%                
# ℹ abbreviated names: ¹​`2020 Census Population`, ²​`Population Percent Change`

Clean Census County Names

We have to first clean the county names so that we can later match them in the hate crimes data.

nypop$`Area Name` <- gsub(" County", "", nypop$`Area Name`)

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

First Attempt to Join the Data sets

This is my first join which checks whether the county names match between the two data sets.

datajoin <- left_join(hate2, nypop2, by = c("county"))

datajoin
# A tibble: 127 × 5
# Groups:   complaintyearnumber, county [35]
   complaintyearnumber county biasmotivedescription     n 2020 Census Populati…¹
                 <dbl> <chr>  <chr>                 <int>                  <dbl>
 1                2024 KINGS  ANTI-JEWISH             152                     NA
 2                2024 NEW Y… 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 Y… ANTI-JEWISH             124                     NA
 8                2025 NEW Y… ANTI-JEWISH             110                     NA
 9                2022 NEW Y… ANTI-JEWISH             104                     NA
10                2021 NEW Y… ANTI-ASIAN               84                     NA
# ℹ 117 more rows
# ℹ abbreviated name: ¹​`2020 Census Population`

Fix County Name Capitalization

Now the county names are going to be changed to lowercase in both of the data sets so that they can match during our next join attempt.

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))))

Trying to Join the Data sets Again

Now that we have changed both of the sets of county names to lowercase, the population data should be able to match correctly.

datajoin <- left_join(hate_new, nypop_new, by = c("county"))

datajoin
# A tibble: 127 × 5
# Groups:   complaintyearnumber, county [35]
   complaintyearnumber county biasmotivedescription     n 2020 Census Populati…¹
                 <dbl> <fct>  <chr>                 <int>                  <dbl>
 1                2024 kings  ANTI-JEWISH             152                2736074
 2                2024 new y… 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 y… ANTI-JEWISH             124                1694251
 8                2025 new y… ANTI-JEWISH             110                1694251
 9                2022 new y… ANTI-JEWISH             104                1694251
10                2021 new y… ANTI-ASIAN               84                1694251
# ℹ 117 more rows
# ℹ abbreviated name: ¹​`2020 Census Population`

Calculating the Rates per 100,000 people

So this calculation is using the county population totals to help it find the hate crime rate per 100,000 people.

datajoinrate <- datajoin |>
  mutate(rate = n / `2020 Census Population` * 100000) |>
  arrange(desc(rate))

datajoinrate
# A tibble: 127 × 6
# Groups:   complaintyearnumber, county [35]
   complaintyearnumber county biasmotivedescription     n 2020 Census Populati…¹
                 <dbl> <fct>  <chr>                 <int>                  <dbl>
 1                2024 new y… ANTI-JEWISH             136                1694251
 2                2023 new y… ANTI-JEWISH             124                1694251
 3                2025 new y… ANTI-JEWISH             110                1694251
 4                2022 new y… ANTI-JEWISH             104                1694251
 5                2024 kings  ANTI-JEWISH             152                2736074
 6                2025 kings  ANTI-JEWISH             136                2736074
 7                2021 new y… ANTI-ASIAN               84                1694251
 8                2021 new y… ANTI-JEWISH              84                1694251
 9                2019 kings  ANTI-JEWISH             128                2736074
10                2023 kings  ANTI-JEWISH             126                2736074
# ℹ 117 more rows
# ℹ abbreviated name: ¹​`2020 Census Population`
# ℹ 1 more variable: rate <dbl>

This dataset had a lot of positive aspects. One of these positive aspects was that it was able to provide a lot of information that was very detailed about the different hate crimes in New York City that were reported. It was able to include: the month, year, county, police precinct, offense description, bias motive and offense category. All of these different variables make it possible to compare the different types of hate crimes that were happening across the different areas and the different years. Furthermore the census population data was also able to allow the raw counts to be turned into rates, which helped us and helped make the county comparisons a little more fair.
>
One of the major limitations to this dataset was that it was only able to include incidents that were actually being reported to and recorded by the police. Some of the victims may not have actually reported the hate crimes because of a lot of different factors, which can include fear, distrust, or even being uncertain about whether an incident qualifies as a hate crime. What this means is that the dataset cannot accurately show the true number of hate crimes. Also the arrest information contains a lot of missing values. Another limitation is that the county rates being used were using the 2020 population for every year even though the populations can change from year to year.
>
What I would do is:

  1. I would study whether certain bias motives are increasing during specific months of the year or maybe after big public events.

  2. I would be comparing the offense severity and the arrest outcome that happened across the different bias motive categories in the data.