| import streamlit as st |
| import pandas as pd |
| import altair as alt |
| from pathlib import Path |
| import plotly.express as px |
|
|
| |
| st.set_page_config( |
| page_title="Analyze Crime Distributions", |
| page_icon="📊", |
| layout="wide" |
| ) |
| st.markdown(""" |
| <style> |
| .title { |
| text-align: center; |
| padding: 25px; |
| color: #2c3e50; |
| font-family: 'Source Sans Pro', sans-serif; |
| } |
| /* Paragraph/write-up styling */ |
| .description { |
| font-size: 18px; /* comfortable reading size */ |
| line-height: 1.6; /* good spacing */ |
| color: #4b4b4b; /* dark grey text */ |
| text-align: justify; /* nice full-justified look */ |
| padding: 0 10px 20px; /* side & bottom padding */ |
| font-family: 'Helvetica Neue', Arial, sans-serif; |
| } |
| .sectionheader { |
| font-family: 'Source Sans Pro', sans-serif; |
| font-size: 32px; |
| color: #2c3e50; |
| margin-top: 15px; |
| margin-bottom: 10px; |
| border-bottom: 3px solid #ccc; |
| padding-bottom: 8px; |
| } |
| </style> |
| """, unsafe_allow_html=True) |
| |
| st.markdown("<div class='title'><h1> LAPD Crime Insights Dashboard </h1></div>", unsafe_allow_html=True) |
|
|
| |
| st.markdown("""<div class='description'> This application provides a suite of interactive visualizations—pie charts, |
| bar charts, scatter plots, and more—that let you explore crime patterns in the LAPD dataset from multiple angles. |
| Quickly see which offense categories dominate, compare arrest rates against non-arrests, track how crime volumes change over time, and examine geographic hotspots. |
| These insights can help police departments, community organizations, and policymakers allocate resources more effectively and |
| design targeted strategies to improve public safety.</div>""",unsafe_allow_html=True) |
| |
| |
| st.markdown("<div class='sectionheader'> Dataset Information </div>", unsafe_allow_html=True) |
| st.markdown( |
| """ |
| <div class="description"> |
| <ul> |
| <li><strong>Source:</strong> LAPD crime incidents dataset</li> |
| <li><strong>Rows:</strong> one incident per row</li> |
| <li><strong>Columns:</strong> e.g. <code>crm_cd_desc</code> (crime type), <code>arrest</code> (boolean), <code>year</code>, <code>location_description</code>, etc.</li> |
| <li><strong>Purpose:</strong> Interactive exploration of top crime categories and arrest rates.</li> |
| </ul> |
| </div> |
| """, |
| unsafe_allow_html=True |
| ) |
|
|
| |
| DATA_PATH = Path(__file__).parent / "crime_data.csv" |
| REGION_DATA_PATH = Path(__file__).parent / "area_lookup.csv" |
| @st.cache_data |
| def load_data(): |
| return pd.read_csv(DATA_PATH) |
| def region_load_data(): |
| return pd.read_csv(REGION_DATA_PATH) |
| |
| if st.button("🔄 Refresh Data"): |
| st.cache_data.clear() |
| st.toast("Data is refreshed",icon="✅") |
| |
| |
| df = load_data() |
| lookup = region_load_data() |
| map_region = dict(zip(lookup["OBJECTID"], lookup["APREC"])) |
| map_precinct = dict(zip(lookup["OBJECTID"], lookup["PREC"])) |
|
|
| |
| df["RegionName"] = df["area"].map(map_region) |
| df["PrecinctCode"] = df["area"].map(map_precinct) |
|
|
| |
| print(df[["area", "RegionName", "PrecinctCode"]].head()) |
|
|
| if df.empty: |
| st.stop() |
| |
| |
| st.markdown("<div class='sectionheader'> Data Preview </div>", unsafe_allow_html=True) |
| st.markdown( |
| f"<div class='description'>" |
| f"Total records: <strong>{df.shape[0]:,}</strong> | " |
| f"Total columns: <strong>{df.shape[1]:,}</strong>" |
| f"</div>", |
| unsafe_allow_html=True |
| ) |
| st.dataframe(df.head()) |
|
|
| |
| st.markdown("<div class='sectionheader'> Top 10 Crime Types by Year </div>", unsafe_allow_html=True) |
|
|
| years = sorted(df["year"].dropna().astype(int).unique()) |
| |
| options = ["All"] + years |
|
|
| |
| selected_year = st.selectbox("Select Year", options, index=0) |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| if selected_year == "All": |
| filtered = df.copy() |
| else: |
| filtered = df[df["year"] == selected_year] |
|
|
| |
| top_crimes = ( |
| filtered["crm_cd_desc"] |
| .value_counts() |
| .nlargest(10) |
| .rename_axis("Crime Type") |
| .reset_index(name="Count") |
| ) |
| top_crimes["Percentage"] = top_crimes["Count"] / top_crimes["Count"].sum() |
|
|
| |
| st.markdown("### Key Metrics", unsafe_allow_html=True) |
| col1, col2, col3 = st.columns(3) |
| col1.metric( |
| label="Total Incidents", |
| value=f"{len(filtered):,}" |
| ) |
| col2.metric( |
| label="Unique Crime Types", |
| value=f"{filtered['crm_cd_desc'].nunique():,}" |
| ) |
| |
| top_share = top_crimes.iloc[0]["Percentage"] |
| col3.metric( |
| label=f"Share of Top Crime ({top_crimes.iloc[0]['Crime Type']})", |
| value=f"{top_share:.1%}" |
| ) |
|
|
| |
| fig = px.pie( |
| top_crimes, |
| names="Crime Type", |
| values="Count", |
| hole=0.4, |
| color_discrete_sequence=px.colors.sequential.Agsunset, |
| title=" " |
| ) |
|
|
| fig.update_traces( |
| textposition="outside", |
| textinfo="label+percent", |
| pull=[0.02] * len(top_crimes), |
| marker=dict(line=dict(color="white", width=1)) |
| ) |
|
|
| fig.update_layout( |
| legend_title_text="Crime Type", |
| margin=dict(t=40, b=40, l=20, r=20), |
| height=600, |
| width=450, |
| title_x=0.5 |
| ) |
|
|
| st.plotly_chart(fig, use_container_width=True) |
| st.markdown("""<div class="description"> The donut chart shows the share of the ten most frequent crime categories in the selected year. |
| At the center, you can see that Vehicle – Stolen is the single largest slice, accounting for roughly 18.7% of all incidents, |
| The remaining five categories each represent between 3%–5% of total incidents—these include miscellaneous crimes, criminal threats, |
| assault with a deadly weapon, burglary, and minor vandalism. By displaying both slice size and percentage labels, the chart makes it easy |
| to compare how dominant property‐related offenses are, versus violent or lesser‐common crimes, in that year’s LAPD data.</div>""",unsafe_allow_html=True) |
|
|