Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Likelihood of stop #313

Draft
wants to merge 6 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/data-import.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ To load an existing database dump on S3, run:
.. code-block:: bash

dropdb traffic_stops_nc && createdb -E UTF-8 traffic_stops_nc
aws s3 cp s3://forwardjustice-trafficstops/trafficstops-staging_database.dump .
pg_restore -Ox -d traffic_stops_nc trafficstops-staging_database.dump
aws s3 cp s3://forwardjustice-trafficstops/traffic_stops_nc-20250204.dump .
pg_restore -Ox -d traffic_stops_nc traffic_stops_nc-20250204.dump


Raw NC Data (slower)
Expand Down
96 changes: 95 additions & 1 deletion nc/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@
from django import forms
from django.contrib import admin

from nc.models import Agency, Resource, ResourceFile, StopSummary
from nc.models import (
Agency,
ContrabandSummary,
LikelihoodStopSummary,
NCCensusProfile,
Resource,
ResourceFile,
StopSummary,
)


class AgencyAdmin(admin.ModelAdmin):
Expand Down Expand Up @@ -45,6 +53,24 @@ def agency_name(self, obj):
return obj.agency.name


@admin.register(ContrabandSummary)
class ContrabandSummaryAdmin(admin.ModelAdmin):
date_hierarchy = "date"
list_display = (
"id",
"date",
"agency",
"stop_purpose_group",
"driver_searched",
"driver_arrest",
"contraband_found",
)
list_filter = ("date", "stop_purpose_group")
list_select_related = ("agency",)
search_fields = ("id", "agency__name")
raw_id_fields = ("stop", "agency", "search", "contraband")


class InlineResourceFile(admin.StackedInline):
model = ResourceFile
fields = (
Expand Down Expand Up @@ -95,6 +121,74 @@ def save_related(self, request, form, formsets, change):
form.instance.agencies.set(form.cleaned_data["agencies"], clear=True)


@admin.register(NCCensusProfile)
class NCCensusProfileAdmin(admin.ModelAdmin):
list_display = (
"id",
"acs_id",
"location",
"geography",
"race",
"population",
"population_total",
"population_pct",
)
list_filter = ("geography", "race")
ordering = ("location",)
readonly_fields = (
"acs_id",
"location",
"geography",
"race",
"population",
"population_total",
"population_percent",
"source",
)
search_fields = ("location", "id", "acs_id")

@admin.display(ordering="population_percent")
def population_pct(self, obj):
return f"{obj.population_percent:.2%}"


@admin.register(LikelihoodStopSummary)
class LikelihoodStopSummaryAdmin(admin.ModelAdmin):
list_display = (
"id",
"agency_name",
"driver_race_comb",
"population",
"population_total",
"stops",
"stops_total",
"stop_rate",
"baseline_rate",
"stop_rate_ratio",
)
list_filter = ("driver_race_comb", "agency")
list_select_related = ("agency",)
readonly_fields = (
"id",
"agency",
"driver_race_comb",
"population",
"population_total",
"population_percent",
"stops",
"stops_total",
"stop_rate",
"baseline_rate",
"stop_rate_ratio",
)
search_fields = ("agency__name",)
ordering = ("agency__name", "driver_race_comb")

@admin.display(ordering="agency__name")
def agency_name(self, obj):
return obj.agency.name


admin.site.register(Agency, AgencyAdmin)
admin.site.register(StopSummary, StopSummaryAdmin)
admin.site.register(Resource, ResourceAdmin)
7 changes: 7 additions & 0 deletions nc/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django.apps import AppConfig


class InventoryConfig(AppConfig):
default_auto_field = "django.db.models.AutoField"
name = "nc"
verbose_name = "North Carolina"
31 changes: 31 additions & 0 deletions nc/migrations/0013_nccensusprofile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Generated by Django 3.2.25 on 2025-02-03 16:44

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('nc', '0012_contrabandsummary'),
]

operations = [
migrations.CreateModel(
name='NCCensusProfile',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('acs_id', models.CharField(max_length=32, verbose_name='ACS ID')),
('location', models.CharField(max_length=64)),
('geography', models.CharField(choices=[('state', 'State'), ('county', 'County'), ('place', 'Place')], max_length=16)),
('source', models.CharField(max_length=64)),
('race', models.CharField(max_length=32)),
('population', models.BigIntegerField()),
('population_total', models.BigIntegerField()),
('population_percent', models.FloatField()),
],
options={
'verbose_name': 'NC Census Profile',
'verbose_name_plural': 'NC Census Profiles',
},
),
]
53 changes: 53 additions & 0 deletions nc/migrations/0014_auto_20250204_1040.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Generated by Django 3.2.25 on 2025-02-04 15:40

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("nc", "0013_nccensusprofile"),
]

operations = [
migrations.CreateModel(
name="LikelihoodStopSummary",
fields=[
(
"id",
models.PositiveIntegerField(
primary_key=True, serialize=False, verbose_name="ID"
),
),
(
"driver_race_comb",
models.CharField(
choices=[
("Asian", "Asian"),
("Black", "Black"),
("Hispanic", "Hispanic"),
("Native American", "Native American"),
("Other", "Other"),
("White", "White"),
],
db_column="driver_race",
max_length=216,
verbose_name="Driver Race",
),
),
("population", models.PositiveIntegerField()),
("population_total", models.PositiveIntegerField()),
("population_percent", models.DecimalField(decimal_places=2, max_digits=5)),
("stops", models.PositiveIntegerField()),
("stops_total", models.PositiveIntegerField()),
("stop_rate", models.DecimalField(decimal_places=2, max_digits=5)),
("baseline_rate", models.DecimalField(decimal_places=2, max_digits=5)),
("stop_rate_ratio", models.DecimalField(decimal_places=2, max_digits=5)),
],
options={
"verbose_name": "Likelihood of Stop Summary",
"verbose_name_plural": "Likelihood of Stop Summaries",
"managed": False,
},
),
]
102 changes: 102 additions & 0 deletions nc/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -442,3 +442,105 @@ def __str__(self):
if self.file:
return f"{self.file.name} for {self.resource.title}"
return f"Resource file for {self.resource.title}"


class NCCensusProfile(models.Model):
class GeographyChoices(models.TextChoices):
STATE = "state", "State"
COUNTY = "county", "County"
PLACE = "place", "Place"

acs_id = models.CharField(verbose_name="ACS ID", max_length=32)
location = models.CharField(max_length=64)
geography = models.CharField(max_length=16, choices=GeographyChoices.choices)
source = models.CharField(max_length=64)
race = models.CharField(max_length=32)
population = models.BigIntegerField()
population_total = models.BigIntegerField()
population_percent = models.FloatField()

class Meta:
verbose_name = "NC Census Profile"
verbose_name_plural = "NC Census Profiles"

def __str__(self):
return f"{self.location} {self.race} people ({self.geography})"


class Race(models.TextChoices):
ASIAN = "Asian"
BLACK = "Black"
HISPANIC = "Hispanic"
NATIVE_AMERICAN = "Native American"
OTHER = "Other"
WHITE = "White"


LIKELIHOOD_STOP_SQL = """
WITH stop_summary AS (
SELECT
agency_id
, name AS agency
, agency.census_profile_id AS acs_id
, driver_race_comb AS driver_race
, sum(count) AS stops
, sum(sum(count)) OVER (PARTITION BY agency_id)::integer AS stops_total
, (sum(count) * 1.0) / sum(sum(count)) OVER (PARTITION BY agency_id) AS stops_percent
FROM nc_stopsummary summary
JOIN nc_agency agency ON (summary.agency_id = agency.id)
WHERE agency.census_profile_id IS NOT NULL
GROUP BY 1, 2, 3, 4
ORDER BY 2, 3
), stop_summary_with_acs AS (
SELECT
stops.*
, acs.*
, (stops * 1.0) / NULLIF(population, 0) AS stop_rate
FROM stop_summary stops
JOIN nc_nccensusprofile acs ON (stops.acs_id = acs.acs_id AND stops.driver_race = acs.race)
ORDER BY agency_id, driver_race
)
SELECT
row_number() over() AS id
, parent.agency_id
, parent.agency
, parent.driver_race
, parent.population
, parent.population_total
, parent.population_percent
, parent.stops
, parent.stops_total
, parent.stop_rate
, baseline.stop_rate AS baseline_rate
, (parent.stop_rate - baseline.stop_rate) / baseline.stop_rate AS stop_rate_ratio
FROM stop_summary_with_acs parent
JOIN stop_summary_with_acs baseline ON (baseline.driver_race = 'White' AND parent.agency = baseline.agency)
ORDER BY agency_id, driver_race
""" # noqa


class LikelihoodStopSummary(pg.ReadOnlyMaterializedView):
sql = LIKELIHOOD_STOP_SQL
# Don't create view with data, this will be manually managed
# and refreshed by the data import process
# https://github.com/mikicz/django-pgviews#with-no-data
with_data = False

id = models.PositiveIntegerField(verbose_name="ID", primary_key=True)
agency = models.ForeignKey("Agency", on_delete=models.DO_NOTHING)
driver_race_comb = models.CharField(
verbose_name="Driver Race", max_length=216, choices=Race.choices, db_column="driver_race"
)
population = models.PositiveIntegerField()
population_total = models.PositiveIntegerField()
population_percent = models.DecimalField(max_digits=5, decimal_places=2)
stops = models.PositiveIntegerField()
stops_total = models.PositiveIntegerField()
stop_rate = models.DecimalField(max_digits=5, decimal_places=2)
baseline_rate = models.DecimalField(max_digits=5, decimal_places=2)
stop_rate_ratio = models.DecimalField(max_digits=5, decimal_places=2)

class Meta:
managed = False
verbose_name = "Likelihood of Stop Summary"
verbose_name_plural = "Likelihood of Stop Summaries"
5,723 changes: 5,723 additions & 0 deletions nc/notebooks/2024-04-likelihood-of-stops/likelihood-of-stops.ipynb

Large diffs are not rendered by default.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions nc/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,9 @@
views.AgencyArrestsPercentageOfStopsPerContrabandTypeView.as_view(),
name="arrests-percentage-of-stops-per-contraband-type",
),
path(
"api/agency/<agency_id>/likelihood-of-stops/",
views.LikelihoodStopView.as_view(),
name="likelihood-of-stops",
),
]
1 change: 1 addition & 0 deletions nc/views/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from .arrests import * # noqa
from .likelihood import * # noqa
from .main import * # noqa
Loading