From d548d18ac09d622b41d29b62c5b93e8a6c42655b Mon Sep 17 00:00:00 2001 From: Geoff Hing Date: Sat, 2 Aug 2014 17:54:29 -0400 Subject: [PATCH] Export Community Area GeoJSON Add a management command and some custom manager and queryset methods for exporting Community Area GeoJSON. Adds a dependency on django-geojson, though we only use their serializer class. Addresses https://github.com/sc3/cook-convictions/issues/48 --- convictions.ipynb | 478 +++++++++++++++++- .../management/commands/export_ca_geojson.py | 31 ++ convictions_data/models.py | 104 +++- requirements.txt | 1 + 4 files changed, 608 insertions(+), 6 deletions(-) create mode 100644 convictions_data/management/commands/export_ca_geojson.py diff --git a/convictions.ipynb b/convictions.ipynb index 5f2a11e..ed6b783 100644 --- a/convictions.ipynb +++ b/convictions.ipynb @@ -1,7 +1,7 @@ { "metadata": { "name": "", - "signature": "sha256:0fb36f492872624df55bac86923091cb6bcabb619050ab5918fdada3598e881b" + "signature": "sha256:b2d4844d4d84d8ad82c1e1480e2919b93f4e69c6d184a34be82ac3253042346c" }, "nbformat": 3, "nbformat_minor": 0, @@ -12,12 +12,14 @@ "cell_type": "code", "collapsed": false, "input": [ - "from convictions_data.models import Disposition" + "from convictions_data.models import Disposition\n", + "import matplotlib.pyplot as plt\n", + "%matplotlib inline" ], "language": "python", "metadata": {}, "outputs": [], - "prompt_number": 1 + "prompt_number": 3 }, { "cell_type": "markdown", @@ -25,11 +27,11 @@ "source": [ "## What are we counting here?\n", "\n", - "tl;dr Records != Convictions != People\n", + "tl;dr: **Records != Convictions != People**\n", "\n", "Each record represents a disposition, which is refers to a court's final determination of a case or issue. Our data set appears to only have dispositions that are related to charges where there was a conviction. So, we don't have dispositions related to charges where someone was found not guilty. \n", "\n", - "There can be multiple records for a single case. We believe that this occurs for three reasons: 1) Multiple statutes associated with a conviction; 2) Multiple counts of a single statute associated with a conviction; and 3) Additional appearances in court after a conviction occurs, which get represented as changes in the disposition." + "There can be multiple records for a single case." ] }, { @@ -5858,6 +5860,472 @@ ], "prompt_number": 8 }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "# Get Cannabis Convictions\n", + "mj_convictions = Conviction.objects.filter(iucr_code__startswith=\"18\")\n", + "print(\"There are only {} convictions related to the Cannabis Control Act??\".format(mj_convictions.count()))" + ], + "language": "python", + "metadata": {}, + "outputs": [ + { + "output_type": "stream", + "stream": "stdout", + "text": [ + "There are only 38 convictions related to the Cannabis Control Act??\n" + ] + } + ], + "prompt_number": 22 + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Community Areas" + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "from pprint import pprint\n", + "import numpy as np\n", + "import pandas as pd\n", + "\n", + "cas = CommunityArea.objects.all()\n", + "# We can add custom annotations that aggregate conviction counts to community area.\n", + "cas = cas.with_conviction_annotations()\n", + "\n", + "# Let's get the range of per-capita convictions to help pick colors for our coropleth\n", + "convictions_per_capita = [ca.convictions_per_capita for ca in cas.order_by('convictions_per_capita')]\n", + "num_bins = 5\n", + "out, bins = pd.cut(convictions_per_capita, num_bins, retbins=True)\n", + "\n", + "print(bins)\n", + "print(convictions_per_capita[0], convictions_per_capita[-1])" + ], + "language": "python", + "metadata": {}, + "outputs": [ + { + "output_type": "stream", + "stream": "stdout", + "text": [ + "[ 0.00293543 0.04502037 0.08689594 0.12877151 0.17064707 0.21252264]\n", + "0.003144806231554502 0.21252263906856403\n" + ] + } + ], + "prompt_number": 67 + }, + { + "cell_type": "heading", + "level": 1, + "metadata": {}, + "source": [ + "Without IUCR code" + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "disp_miss = Disposition.objects.in_analysis().filter(iucr_code=\"\").values_list('final_statute').distinct()\n", + "disp_miss.count()" + ], + "language": "python", + "metadata": {}, + "outputs": [ + { + "metadata": {}, + "output_type": "pyout", + "prompt_number": 13, + "text": [ + "1560" + ] + } + ], + "prompt_number": 13 + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "disp_all = Disposition.objects.in_analysis().values_list('final_statute').distinct()\n", + "disp_all.count()" + ], + "language": "python", + "metadata": {}, + "outputs": [ + { + "metadata": {}, + "output_type": "pyout", + "prompt_number": 16, + "text": [ + "2339" + ] + } + ], + "prompt_number": 16 + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "# SELECT final_statute, COUNT(*) AS num_dispositions\n", + "# FROM convictions_data_disposition\n", + "# WHERE initial_date >= '2005-01-01' AND chrgdispdate >= '2005-01-01'\n", + "# AND iucr_code = \"\"\n", + "# GROUP BY final_statute\n", + "# ORDER BY num_dispositions DESC;\n", + "wo_iucr = Disposition.objects.in_analysis().filter(iucr_code=\"\").values(\"final_statute\").annotate(num_dispositions=Count('id')).order_by('-num_dispositions')\n", + "for d in wo_iucr[:10]:\n", + " print(d['final_statute'], d['num_dispositions'])" + ], + "language": "python", + "metadata": {}, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Drug charges by amount" + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "d = Conviction.objects.filter(final_statute='720-550/4(a)')\n", + "print(\"{} convictions for possession of cannabis, less than 2.5 grams\".format(d.count()))\n", + "\n", + "d = Conviction.objects.filter(final_statute='720-550/5(a)')\n", + "print(\"{} convictions for man/del of cannabis, less than 2.5 grams\".format(d.count()))" + ], + "language": "python", + "metadata": {}, + "outputs": [ + { + "output_type": "stream", + "stream": "stdout", + "text": [ + "15 convictions for possession of cannabis, less than 2.5 grams\n", + "24 convictions for man/del of cannabis, less than 2.5 grams" + ] + }, + { + "output_type": "stream", + "stream": "stdout", + "text": [ + "\n" + ] + } + ], + "prompt_number": 58 + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "d = Conviction.objects.filter(final_statute='720-550/4(b)')\n", + "d.count()\n", + "print(\"{} convictions for possession of cannabis\".format(d.count()))\n", + "\n", + "d = Conviction.objects.filter(final_statute='720-550/5(b)')\n", + "d.count()\n", + "print(\"{} convictions for man/del of cannabis\".format(d.count()))" + ], + "language": "python", + "metadata": {}, + "outputs": [ + { + "output_type": "stream", + "stream": "stdout", + "text": [ + "107 convictions for possession of cannabis\n", + "298 convictions for man/del of cannabis" + ] + }, + { + "output_type": "stream", + "stream": "stdout", + "text": [ + "\n" + ] + } + ], + "prompt_number": 59 + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "d = Conviction.objects.filter(final_statute='720-550/4(c)')\n", + "print(\"{} convictions for poss cannabis\".format(d.count()))\n", + "\n", + "d = Conviction.objects.filter(final_statute='720-550/5(c)')\n", + "print(\"{} convictions for man/del cannabis\".format(d.count()))" + ], + "language": "python", + "metadata": {}, + "outputs": [ + { + "output_type": "stream", + "stream": "stdout", + "text": [ + "479 convictions for poss cannabis\n", + "716 convictions for man/del cannabis" + ] + }, + { + "output_type": "stream", + "stream": "stdout", + "text": [ + "\n" + ] + } + ], + "prompt_number": 61 + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "d = Conviction.objects.filter(final_statute='720-550/4(d)')\n", + "d.count()\n", + "\n", + "print(\"{} convictions for poss cannabis\".format(d.count()))\n", + "\n", + "d = Conviction.objects.filter(final_statute='720-550/5(d)')\n", + "d.count()\n", + "\n", + "print(\"{} convictions for man/del cannabis\".format(d.count()))\n" + ], + "language": "python", + "metadata": {}, + "outputs": [ + { + "output_type": "stream", + "stream": "stdout", + "text": [ + "961 convictions for poss cannabis\n", + "770 convictions for man/del cannabis" + ] + }, + { + "output_type": "stream", + "stream": "stdout", + "text": [ + "\n" + ] + } + ], + "prompt_number": 64 + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "# possession of cannabis, 500 - 2000 grams\n", + "d = Conviction.objects.filter(final_statute='720-550/4(e)')\n", + "print(d.count())\n", + "\n", + "d = Conviction.objects.filter(final_statute='720-550/5(e)')\n", + "print(d.count()" + ], + "language": "python", + "metadata": {}, + "outputs": [ + { + "metadata": {}, + "output_type": "pyout", + "prompt_number": 65, + "text": [ + "145" + ] + } + ], + "prompt_number": 65 + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "# possession of cannabis, less than 2000 - 5000 grams\n", + "d = Conviction.objects.filter(final_statute='720-550/4(f)')\n", + "d.count()" + ], + "language": "python", + "metadata": {}, + "outputs": [ + { + "metadata": {}, + "output_type": "pyout", + "prompt_number": 48, + "text": [ + "17" + ] + } + ], + "prompt_number": 48 + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "# possession of cannabis, 5000+ grams\n", + "d = Conviction.objects.filter(final_statute='720-550/4(g)')\n", + "d.count()" + ], + "language": "python", + "metadata": {}, + "outputs": [ + { + "metadata": {}, + "output_type": "pyout", + "prompt_number": 49, + "text": [ + "15" + ] + } + ], + "prompt_number": 49 + }, + { + "cell_type": "heading", + "level": 2, + "metadata": {}, + "source": [ + "Possession vs. delivery in the Cannabis Control Act" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Possession vs. delivery in the Cannabis Control Act (e.g. 720-550/4(a-c)\n", + "vs. 720-550/5(a-c) & 5.2(c-e)), particularly when looking at possession vs \n", + "delivery of the same amounts of cannabis (under 30 g, or over 30g) " + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "def statute_range(chapter, act_prefix, section, subsection_start, subsection_end):\n", + " \"\"\"Helper function to create a list representing a range of statutes\"\"\"\n", + " return [\"{}-{}/{}({})\".format(chapter, act_prefix, section, chr(subsection))\n", + " for subsection in range(ord(subsection_start), ord(subsection_end)+1)]\n", + "\n", + "possession_statutes = statute_range(720, 550, 4, 'a', 'c')\n", + "delivery_statutes = statute_range(720, 550, 5, 'a', 'c') + statute_range(720, 550, 5.2, 'c', 'e')\n", + "all_statutes = possession_statutes + delivery_statutes\n", + "\n", + "cca_convictions = Conviction.objects.filter(final_statute__in=all_statutes)\n", + "annotated = cca_convictions.values('final_statute', 'final_desc).annotate(num_convictions=Count('id'))\n", + "print(annotated)\n", + "statute2convictions = {c['final_statute']: c['num_convictions'] for c in annotated}\n", + "print(statute2convictions)" + ], + "language": "python", + "metadata": {}, + "outputs": [ + { + "output_type": "stream", + "stream": "stdout", + "text": [ + "[{'num_convictions': 15, 'final_statute': '720-550/4(a)'}, {'num_convictions': 107, 'final_statute': '720-550/4(b)'}, {'num_convictions': 479, 'final_statute': '720-550/4(c)'}, {'num_convictions': 24, 'final_statute': '720-550/5(a)'}, {'num_convictions': 298, 'final_statute': '720-550/5(b)'}, {'num_convictions': 716, 'final_statute': '720-550/5(c)'}, {'num_convictions': 251, 'final_statute': '720-550/5.2(c)'}, {'num_convictions': 905, 'final_statute': '720-550/5.2(d)'}, {'num_convictions': 14, 'final_statute': '720-550/5.2(e)'}]\n", + "{'720-550/4(b)': 107, '720-550/5(c)': 716, '720-550/5(b)': 298, '720-550/4(c)': 479, '720-550/5.2(c)': 251, '720-550/5.2(d)': 905, '720-550/5.2(e)': 14, '720-550/4(a)': 15, '720-550/5(a)': 24}" + ] + }, + { + "output_type": "stream", + "stream": "stdout", + "text": [ + "\n" + ] + } + ], + "prompt_number": 89 + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "plt.plot([1,2,3,4])" + ], + "language": "python", + "metadata": {}, + "outputs": [ + { + "metadata": {}, + "output_type": "pyout", + "prompt_number": 4, + "text": [ + "[]" + ] + }, + { + "metadata": {}, + "output_type": "display_data", + "png": "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\n", + "text": [ + "" + ] + } + ], + "prompt_number": 4 + }, + { + "cell_type": "heading", + "level": 2, + "metadata": {}, + "source": [ + "Controlled Substances Act vs. Cannibis Control Act: Delivery to persons under 18 " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "How does delivery to persons under 18 under the controlled substances act (720-570/407) compared to delivery for a person under 18 of cannabis (720-550/7) ? " + ] + }, + { + "cell_type": "heading", + "level": 2, + "metadata": {}, + "source": [ + "Possession of meth vs. possession of a controlled substance" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Possession of meth (720-646/60) vs. possession of a controlled substance like heroine or cocaine (720-570/402)" + ] + }, + { + "cell_type": "heading", + "level": 2, + "metadata": {}, + "source": [ + "Difference between manufacturing and criminal drug conspiracy" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Difference between manufacturing (720-570/401, 401.1 & 401.5) and criminal drug conspiracy (720-570/405, 405.1 & 405.2)" + ] + }, { "cell_type": "code", "collapsed": false, diff --git a/convictions_data/management/commands/export_ca_geojson.py b/convictions_data/management/commands/export_ca_geojson.py new file mode 100644 index 0000000..44d9054 --- /dev/null +++ b/convictions_data/management/commands/export_ca_geojson.py @@ -0,0 +1,31 @@ +from optparse import make_option + +from django.core.management.base import BaseCommand + +from convictions_data.models import CommunityArea + +class Command(BaseCommand): + help = ("Export a geoJSON file of Chicago Community areas with conviction " + "attributes") + + option_list = BaseCommand.option_list + ( + make_option('--name', + default=None, + help="Name of a single community area to export", + ), + make_option('--simplify', + type="float", + default=0.002, + help="Tolerance value for simplifying geometries" + ), + ) + + + def handle(self, *args, **options): + qs = CommunityArea.objects.all() + + if options['name'] is not None: + qs = qs.filter(name=options['name']) + + self.stdout.write(qs.geojson(simplify=options['simplify'])) + diff --git a/convictions_data/models.py b/convictions_data/models.py index 1531fa8..cd6e7f5 100644 --- a/convictions_data/models.py +++ b/convictions_data/models.py @@ -9,9 +9,13 @@ from django.db.models import Q, Min from django.contrib.gis.db import models as geo_models from django.conf import settings +from django.contrib.gis.db.models.query import GeoQuerySet from django.contrib.gis.geos import Point from django.core.paginator import Paginator +from djgeojson.serializers import Serializer as GeoJSONSerializer + + from convictions_data.geocoders import BatchOpenMapQuest from convictions_data.cleaner import CityStateCleaner, CityStateSplitter from convictions_data.statute import get_iucr @@ -750,13 +754,112 @@ class CensusFieldsMixin(geo_models.Model): class Meta: abstract = True +class CommunityAreaQuerySet(GeoQuerySet): + GEOJSON_FIELDS = [ + 'number', + 'name', + 'total_population', + 'num_convictions', + 'convictions_per_capita', + 'num_homicides', + 'boundary', + ] + """ + Fields included in GeoJSON export + """ + + def with_conviction_annotations(self): + """ + Annotate the QuerySet with counts based on related convictions stats + + Returns: + A QuerySet with the following annotated fields added to the models: + + * num_convictions: Total number of convictions in the geography. + * convictions_per_capita: Population-adjusted count of all + convictions. + + + """ + this_table = self.model._meta.db_table + conviction_table = Conviction._meta.db_table + annotated_qs = self + # Use the ``extra()`` QuerySet method to annotate this QuerySet + # with aggregates based on a filtered, joined table. + # This method was suggested by + # http://timmyomahony.com/blog/filtering-annotations-django/ + + # First, define some SQL strings to make this stuff a little easier + # to read. + matches_this_id_where_sql = ('{conviction_table}.community_area_id = ' + '{this_table}.id').format(conviction_table=conviction_table, this_table=this_table) + + # It seems like we could just do the following query with the Count() + # aggregator, but the ORM adds the extra value from the select in the + # GROUP BY clause which breaks all kinds of stuff. + # + # I think this is reflected as + # https://code.djangoproject.com/ticket/11916 + num_convictions_sql = ('SELECT COUNT({conviction_table}.id) ' + 'FROM {conviction_table} ' + 'WHERE {matches_this_id} ' + ).format(conviction_table=conviction_table, this_table=this_table, + matches_this_id=matches_this_id_where_sql) + convictions_per_capita_sql = ('SELECT CAST(COUNT("{conviction_table}"."id") AS FLOAT) / ' + '"{this_table}"."total_population" ' + 'FROM {conviction_table} ' + 'WHERE {matches_this_id} ' + ).format(conviction_table=conviction_table, this_table=this_table, + matches_this_id=matches_this_id_where_sql) + num_homicides_sql = ('SELECT COUNT({conviction_table}.id) ' + 'FROM {conviction_table} ' + 'WHERE {matches_this_id} ' + 'AND {conviction_table}.iucr_category = "Homicide"' + ).format(conviction_table=conviction_table, + matches_this_id=matches_this_id_where_sql) + + annotated_qs = annotated_qs.extra(select={ + 'num_convictions': num_convictions_sql, + 'convictions_per_capita': convictions_per_capita_sql, + 'num_homicides': num_homicides_sql, + }) + + return annotated_qs + + def geojson(self, simplify=0.0): + """ + Serialize models in this QuerySet as a GeoJSON FeatureCollection. + + Args: + simplify (float): Tolerance value to use when simplifying the + geometry fields of the models. Default is 0. + + Returns: + GeoJSON string representing a FeatureCollection containing each + model as a feature. + + """ + # Use a ValuesQuerySet so pk, model name and other cruft aren't + # included in the serialized output. + vqs = self.with_conviction_annotations().values(*self.GEOJSON_FIELDS) + + return GeoJSONSerializer().serialize(vqs, + simplify=simplify, + geometry_field='boundary') + class CommunityAreaManager(geo_models.GeoManager): + def get_queryset(self): + return CommunityAreaQuerySet(self.model, using=self._db) + def aggregate_census_fields(self): for ca in self.get_query_set(): ca.aggregate_census_fields() ca.save() + def geojson(self, simplify=0.0): + return self.get_queryset().geojson(simplify=simplify) + class CommunityArea(CensusFieldsMixin, geo_models.Model): """ @@ -812,7 +915,6 @@ class CensusTractManager(geo_models.GeoManager): def set_community_area_relations(self): for tract in self.get_query_set().all(): ca = CommunityArea.objects.get(number=tract.community_area_number) - # BOOKMARK tract.community_area = ca tract.save() diff --git a/requirements.txt b/requirements.txt index 6ad0903..35a7c2a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ Django>=1.6.5 us>=0.6 geopy>=0.99 South>=0.8,<1.0 +django-geojson==2.6.0