This dataset represents the location and characteristics of major crime against persons such as homicide, shooting, robbery, aggravated assault, larceny, etc. within Baltimore City, MD between 2011 and 2024. The dataset is updated on a weekly basis, but in order to have data across full years of data, I concentrated on 2011 through 2023. There is 611,440 rows of data; each representing an individual crime. The information for each crime consists of the time and date of the crime, the crime code, a description of the crime, whether a weapon was involved, the street address of where the crime occurred, the neighborhood the address is within, the premise type, and the latitude and longitude coordinates.
First, an analysis of the number of crimes committed during each hour of the day. The visual shows how crimes against persons peak during late evening hours, with the highest number of crimes recorded at 6pm. A significant drop in crime is observed during the early morning hours from 4am to 6am.
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.ticker import FuncFormatter
df = pd.read_csv("C:\\Users\\lpatr\\OneDrive\\Documents\\Loyola\\DS736- Visualization\\Baltimore Crime Dataset\\New Baltimore Crime.csv",
usecols = ['CrimeDateTime', 'Description', 'Neighborhood'], skiprows = 0)
#clean up date column
df["CrimeDateTime"] = pd.to_datetime(df["CrimeDateTime"], infer_datetime_format=True, errors='coerce')
df["Hour"] = df.CrimeDateTime.dt.hour
df["Year"] = df.CrimeDateTime.dt.year
df["MonthName"] = df.CrimeDateTime.dt.strftime("%b")
df["WeekDay"] = df.CrimeDateTime.dt.strftime("%a")
#clean up NaN and get rid of decimals
df = df.dropna(subset=['Hour'])
df['Hour'] = df['Hour'].astype(int)
df['Year'] = df['Year'].astype(int)
#filter out years earlier than 2011 (typos)
df_filtered = df[df['Year'] >= 2011]
hourly_counts = df_filtered['Hour'].value_counts().sort_index().reset_index()
hourly_counts.columns = ['Hour', 'Count']
#bar chart of crimes commited per hour
fig = plt.figure(figsize = (18, 10))
x1 = fig.add_subplot(1,1,1)
plt.bar(hourly_counts.Hour, hourly_counts.Count, color = 'darkgreen')
plt.xlabel('Hour of the Day: 24 Hour Clock', fontsize = 18, labelpad = 20)
plt.ylabel('Count of Crimes Committed', fontsize = 18, labelpad = 20)
plt.title('Hourly Number of Crimes Against Persons in Baltimore City:\n2011 through 2023', fontsize = 24)
plt.xticks(hourly_counts.Hour, fontsize = 14)
plt.yticks(fontsize = 14)
x1.yaxis.set_major_formatter(FuncFormatter(lambda x, pos: f'{int(x):,}'))
for item, count in enumerate(hourly_counts['Count']):
x1.text(hourly_counts['Hour'][item], count, f'{count:,}',
ha='center', va='bottom', fontsize=10)
plt.show()
This visualization shows the total number of crimes committed over the years. There is a notable drop in the year 2020, which may correlate with the COVID-19 pandemic. This is followed by sharp increases in the years 2021 and 2022, with the total evening out to near-average numbers again in 2023.
#cut outliers, keep years 2011-2024
df_years = df.loc[(df['Year'] > 2010) & (df['Year'] < 2024)]
df_years = df_years['Year'].value_counts().sort_index().reset_index()
df_years.columns = ['Year', 'Count']
#line plot of total crimes per year
fig = plt.figure(figsize = (18, 10))
x2 = fig.add_subplot(1,1,1)
plt.plot(df_years.Year, df_years.Count, marker = 'o')
plt.xlabel('Year', fontsize = 18, labelpad = 20)
plt.ylabel('Number of Crimes Committed', fontsize = 18, labelpad = 20)
plt.title('Total Number of Crimes Committed in Baltimore City: 2011 - 2023', fontsize = 24)
plt.xticks(df_years.Year, fontsize = 14)
plt.yticks(fontsize = 14)
x2.yaxis.set_major_formatter(FuncFormatter(lambda x, pos: f'{int(x):,}'))
plt.show()
This visual further breaks down the previous visual, showing the monthly crime count for each year. A seasonal pattern is noticeable, with peaks often occurring in the summer months and lows during the winter months.
#create df to show crimes per month by year, establish month order and a color code
monthyear_counts = df_filtered.groupby(['Year', 'MonthName'])['Description'].count().reset_index(name = 'Count')
monthyear_counts = monthyear_counts.sort_values(by = ['Year', 'MonthName'])
monthyear_counts = monthyear_counts.loc[(monthyear_counts['Year'] > 2010) & (monthyear_counts['Year'] < 2024)]
month_order = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
monthyear_counts['MonthName'] = pd.Categorical(monthyear_counts['MonthName'], categories = month_order, ordered = True)
monthyear_counts = monthyear_counts.sort_values(by = ['Year', 'MonthName']).reset_index(drop = True)
colors = {'2011': 'blue', '2012': 'yellow', '2013': 'red', '2014': 'green', '2015': 'black', '2016': 'orange',
'2017': 'purple', '2018': 'pink', '2019': 'teal', '2020': 'cyan', '2021': 'sienna', '2022':
'navy', '2023': 'maroon'}
#multiple line graph
fig = plt.figure(figsize = (18, 14))
ax = fig.add_subplot(1,1,1)
for key, grp in monthyear_counts.groupby(['Year']):
grp.plot(ax = ax, kind = 'line', x = 'MonthName', y = 'Count', color = colors[str(key)],
label = str(key), marker = 'o')
plt.ylabel('Crime Count', fontsize = 20, labelpad = 10)
plt.title('Yearly Crime Count in Baltimore City, 2011-2023', fontsize = 22, pad = 15)
ax.set_xlabel('Month', fontsize = 20, labelpad = 10)
ax.set_xticks(np.arange(12))
ax.set_xticklabels(month_order, fontsize = 14)
plt.yticks(range(2000, monthyear_counts['Count'].max(), 250), fontsize = 14)
ax.yaxis.set_major_formatter(FuncFormatter(lambda x, pos: f'{int(x):,}'))
handles, labels = ax.get_legend_handles_labels()
ax.legend(handles, labels, loc = 'best', title = 'Year', bbox_to_anchor = (1.01, 1.01), fontsize = 16, title_fontsize = 16)
plt.figtext(0.2, 0.01, 'Note: The y-axis begins at 2,000 for visual clarity', ha = 'center', fontsize = 14, color = 'black')
plt.show()
This visual reveals the frequency of the various types of crimes each year. Crimes such as larceny and common assault are among the most prevalent, while incidents like shootings and homicides occur less frequently but are consistent.
#scatterplot of different crimes across the years
crime_counts = df.groupby(['Year', 'Description']).size().reset_index(name='Count')
crime_counts = crime_counts.loc[(crime_counts['Year'] > 2010) & (crime_counts['Year'] < 2024)]
#scale count column for easier graphing
crime_counts['half_count'] = crime_counts['Count'] / 2
plt.figure(figsize = (24, 18))
plt.scatter(crime_counts['Year'], crime_counts['Description'], cmap = 'magma', c = crime_counts['half_count'],
s = crime_counts['half_count'], edgecolors = 'black', marker = 'h')
cbar = plt.colorbar()
cbar.set_label('Number of Incidents', rotation = 270, fontsize = 18, color = 'black', labelpad = 25)
cbar.ax.tick_params(labelsize = 14)
plt.title('Analysis of Crimes in Baltimore City: 2011 - 2023', fontsize = 24)
plt.xlabel('Year', fontsize = 18, labelpad = 20)
plt.ylabel('Name of Crime', fontsize = 18)
plt.yticks(fontsize = 14)
plt.xticks(crime_counts.Year, fontsize = 14)
plt.show()
This donut chart highlights the top 5 crimes in Baltimore City for the year 2023. Common assault and larceny account for nearly half of the top crimes, followed by auto theft, aggravated assault, and larceny from auto.
#Donut Chart: top crimes and percentages of total
crime_counts_2023 = crime_counts.loc[(crime_counts['Year'] == 2023)].sort_values(by = 'Count', ascending = False)
crime_counts_2023_top5 = crime_counts_2023.iloc[:5].reset_index()
fig = plt.figure(figsize = (10, 10))
ax = fig.add_subplot(1,1,1)
crime_counts_2023_top5.plot(kind = 'pie', y = 'Count', labels=crime_counts_2023_top5['Description'],
ax = ax, autopct= lambda x: '{:.1f}%\n({:,})'.format(x, int((x/100)*crime_counts_2023_top5['Count'].sum())), wedgeprops = dict(edgecolor = 'w'),
textprops = dict(fontsize = 14), startangle = 180, legend = False)
hole = plt.Circle((0,0), 0.3, fc = 'white')
fig1 = plt.gcf()
fig1.gca().add_artist(hole)
ax.yaxis.set_visible(False)
plt.title('Top 5 Crimes in Baltimore City in 2023', pad = 20, fontsize = 22)
ax.text(0,0, 'Top 5 Crimes Total:\n' + f'{crime_counts_2023_top5.Count.sum():,}\n'
+ 'Total Crimes in 2023:\n' + f'{crime_counts_2023.Count.sum():,}',
ha = 'center', va = 'center', fontsize = 12)
ax.axis('equal')
plt.show()
Last is a bump chart showcasing the fluctuation in crime rankings across different neighborhoods. The number of crimes were added up for each year for each neighborhood. The neighborhood was then ranked based on the sum of its ranks over the years; the lower the score the higher the crime total in that neighborhood. The top 10 neighborhoods with the highest crime rankings were chosen for this visualization. Some neighborhoods show variable rankings indicating fluctuating crime rates, while others maintain a consistent position in the rankings, signifying a steady crime rate in that area.
#Bump Chart: Neighborhood Ranking of most crimes
neigh = df_filtered.groupby(['Year', 'Neighborhood']).size().reset_index(name='TotalCrimes')
#get rid of blank neighborhood rows
neigh = neigh[neigh['Neighborhood'].notna() & (neigh['Neighborhood'] != '')]
neigh = neigh.pivot(index = 'Neighborhood', columns = 'Year', values = 'TotalCrimes')
neigh_ranked = neigh.rank(0, ascending = False, method = 'min')
#Calculate a score based on the sum of ranks over the years---lower scores equal higher crime totals
rank_score = neigh_ranked.sum(axis = 1)
rank_score_df = rank_score.reset_index()
rank_score_df.columns = ['Neighborhood', 'Ranking']
top_neigh_df = rank_score_df.sort_values(by = 'Ranking').head(10)
#take top neighborhoods and extract them from neigh_ranked
top_10 = [neighborhood for neighborhood in top_neigh_df['Neighborhood']]
filtered_neigh_ranked = neigh_ranked.loc[top_10]
filtered_neigh_ranked = filtered_neigh_ranked.T
fig = plt.figure(figsize = (18, 16))
ax = fig.add_subplot(1,1,1)
filtered_neigh_ranked.plot(kind = 'line', markersize = 18, ax = ax, marker = 'o',
markeredgewidth = 1, linewidth = 6, markerfacecolor = 'white')
ax.invert_yaxis()
plt.ylabel('Monthly Ranking', fontsize = 20, labelpad = 10)
plt.title('Neighborhood Ranking of Crimes Committed in Baltimore City, 2011-2023', fontsize = 22, pad = 15)
ax.set_xlabel('Year', fontsize = 20, labelpad = 10)
ax.set_xticks(np.arange(2011, 2025, 1))
plt.xticks(fontsize = 12)
ax.set_yticks(np.arange(1, filtered_neigh_ranked.max().max() + 1, 2))
plt.yticks(fontsize = 12)
ax.legend(title = '', bbox_to_anchor = (0.3, 0.3), fontsize = 14)
plt.show()
The crime data from Baltimore City exhibits many trends across different crime types, neighborhoods, and times. Larceny and common assault are consistently the most frequent crimes. The data suggests a significant variance in crime occurrence during different hours of the day, with the highest rates predictably occurring in the evening hours. Neighborhood rankings fluctuate a bit, indicating changes in safety and crime patterns across different areas of the city.
A notable impact on crime trends is seen in 2020, probably due to the global pandemic’s restrictions and social changes. Most notably is the sharp decline in crime in the Inner Harbor, a common public place of gathering and shopping. A subsequent resurgence in the following years can be seen as restrictions are lifted. Seasonal trends suggest an increase in criminal activity during the warmer months, which could be attributed to increased outdoor activity and social interaction.
Overall, the data underscores the importance of continual monitoring and analysis of crime patterns. These crime trends and data points can be used to inform law enforcement and local government for use in strategizing public safety plans. While some crimes and trends have remained stable over the years, others have shown significant variability (looking at you Auto Theft), highlighting the need for adaptive and area-specific approaches to crime prevention and law enforcement.