logo Netzschleuder network catalog, repository, and centrifuge

History of chicago_police_complaints

The changes to the source code of this entry in the repository, newest first, each with the differences it made to the entry's files. They are also listed on GitLab.

[entries] Add the date when each entry was added, and remove icon_hash by Tiago de Paula Peixoto beff7c9
The dates are those of the commits that first added the entries' files
(2020-08-11 for the entries of the initial commit); renamed entries keep
the date of their first name.

Changed: none
diff --git a/entries/chicago_police_complaints/__init__.py b/entries/chicago_police_complaints/__init__.py
index 74ab459..e0de840 100644
--- a/entries/chicago_police_complaints/__init__.py
+++ b/entries/chicago_police_complaints/__init__.py
@@ -102,7 +102,7 @@ citation = [('Invisible Institute, "Citizens Police Data Project: Chicago police
              'https://doi.org/10.1177/0002716219901171')]
 # No license is stated (public records obtained with FOIA requests)
 upstream_license = None
-icon_hash = None
+date_added = '2026-10-31'
 
 # The unified data of the repository at the commit of 2025-09-01 (files in
 # Git LFS)
[entries] Prefix the properties in the descriptions with (Node), (Edge) or (Graph) by Tiago de Paula Peixoto bc3dbbd
The lists of properties at the end of the descriptions marked edge
properties with "Edge" but left node properties unmarked, or grouped
them in "Nodes: `a`, ...; `b`, ..." items, so the kind of a property
was often unclear. Every item now starts with (Node), (Edge) or (Graph),
followed by the property names, a colon and the explanation, one item
per property or group of properties; qualifications (kinds of nodes,
networks) follow the names in parentheses. Properties that were only
named got a short explanation, and a few items that named the wrong
kind of property were corrected.
diff --git a/entries/chicago_police_complaints/__init__.py b/entries/chicago_police_complaints/__init__.py
index cc3ee17..74ab459 100644
--- a/entries/chicago_police_complaints/__init__.py
+++ b/entries/chicago_police_complaints/__init__.py
@@ -30,14 +30,70 @@ Left out are the investigators and the police witnesses of the complaints (not m
 
 The networks have the following properties:
 
-- Nodes: `is_complaint`, `is_incident` or `is_award`, whether the node is a complaint, incident or award (otherwise an officer).
-- Officers: `uid`, the officer's id in the data of the Invisible Institute (-1 for the other nodes); `has_record`, whether the officer is in the file of profiles; `officer_id`, the id of the officer on the CPDP site (`https://cpdp.co/officer/<officer_id>`; -1 if unknown); `first_name`, `last_name`, `middle_initial`, `middle_initial2` and `suffix_name`; `appointed_date`, `resignation_date`, `org_hire_date` and `start_date`, the dates of appointment, resignation, hire (by any police organization) and start in the CPD, as given (`M/D/YY`, with two-digit years); `birth_year` (-1 if unknown); `gender`; `race`; `current_star`, the last star (badge) number; `cleaned_rank`, the standardized rank; `current_rank` and `current_unit`, the last rank and unit; `active`, whether the officer is active (1, or 0); `profile_count`, the number of datasets in which the officer appears. These are the most common values over the files, as determined by the Invisible Institute.
-- Complaints (`complaints`): `cr_id`, the complaint register number (-1 for officers); `complaint_date`, `closed_date` and `incident_date`, the dates of filing, closure and incident (Unix time; -1 if unknown); `incident_time`, the time of the incident (as given); `days_to_closure`; `address`, the block address of the incident (number and street); `beat`, the police beat; `city`; `location_code`, the type of location; `old_complaint_address`, the address in the older format; `latitude` and `longitude`, the geocoded block address (NaN if unknown); `current_status`; `investigating_agency`, the last agency investigating the complaint, and `investigating_agencies`, all of them; `complainant_type`, `CIVILIAN` or `CPD EMPLOYEE`; `filename`, the last FOIA response with the complaint.
-- Edges of `complaints`: `complaint_category` and `complaint_code`, the category of the allegation against the officer (e.g. `Excessive Force`); `complaint_categories` and `complaint_codes`, all categories and codes listed for the complaint in all files; `disciplined`, whether the complaint resulted in discipline; `final_finding` (e.g. `SU` sustained, `NS` not sustained, `UN` unfounded, `EX` exonerated, `NAF` no affidavit, `AC` administratively closed), `final_outcome` (e.g. `Reprimand`, `5 Day Suspension`) and `final_outcome_desc`; `recc_finding` and `recc_outcome`, the recommended finding and outcome; `days`, the recommended days of suspension (NaN if none); `cv`, the version of the complaint files from which the row comes.
-- Incidents (`use_of_force`): `event_no`, the event number (-1 if none); `incident_date` (Unix time of the day) and `incident_time` (as given); `block`, `direction` and `street`, the block address; `beat`; `latitude` and `longitude`, the geocoded block address (NaN if unknown); `indoor_or_outdoor`, `lighting_condition`, `weather_condition`, `location` and `location_recode`, the type of location. These are the values of the first report on the incident; the reports of different officers sometimes differ, and their own values are edge properties.
-- Edges of `use_of_force` (the reports): `trr_id`, the id of the report; `date` (Unix time of the day) and `time`; the place, as for the incidents (`block`, `direction`, `street`, `beat`, `latitude`, `longitude`, `indoor_or_outdoor`, `lighting_condition`, `weather_condition`, `location`, `location_recode`); `trr_status`, the status of the report; `firearm_used` and `taser`, whether the officer used a firearm or a taser (1 or 0); `notify_DET_division`, `notify_OEMC`, `notify_OP_command` and `notify_district_sergeant`, whether the detective division, the emergency management office, the OP command or the district sergeant were notified (as given; empty if not); `number_of_officers_using_firearm`, `number_of_weapons_discharged` and `total_number_of_shots` (-1 if not given); `party_fired_first`; `officer_assigned_beat`, `officer_on_duty`, `officer_in_uniform`, `officer_injured`, `officer_rank`, `officer_unit_id` and `officer_unit_detail_id`, the officer at the time of the incident (-1 if unknown); `subject_armed`, `subject_injured`, `subject_alleged_injury`, `subject_age`, `subject_birth_year`, `subject_gender` and `subject_race`, the subject of the force; `member_actions` and `subject_actions`, the actions of the officer (e.g. `VERBAL COMMANDS`, `TAKE DOWN/EMERGENCY HANDCUFFING`, `TASER`) and of the subject (e.g. `PULLED AWAY`), each listed once; `max_action_category`, the highest level of the officer's actions, from 0 (member presence) to 6 (firearm; NaN if not given, as in the older files); `subject_weapons`, the types of weapons of the subject (e.g. `HANDS/FISTS`); `cv`, the version of the files from which the report comes.
-- Awards (`awards`): `award_id`, the id of the award; `award_type`; `award_start_date` and `award_end_date` (Unix time; -1 if unknown), the values of the first record of the award.
-- Edges of `awards`: `current_award_status` (`FINAL`, `DELETED`, `DENIED`, `PENDING APPROVAL` or `PRELIMINARY`), `award_type`, `award_request_date` and `ceremony_date` (Unix time; -1 if unknown), and `requester_full_name`, the name of the officer who requested the award (`REDACTED` for some), as recorded for the officer."""
+- (Node) `is_complaint`, `is_incident` or `is_award`: whether the node is a complaint, incident or award (otherwise an officer).
+- (Node) `uid` (officers): the officer's id in the data of the Invisible Institute (-1 for the other nodes).
+- (Node) `has_record` (officers): whether the officer is in the file of profiles.
+- (Node) `officer_id` (officers): the id of the officer on the CPDP site (`https://cpdp.co/officer/<officer_id>`; -1 if unknown).
+- (Node) `first_name`, `last_name`, `middle_initial`, `middle_initial2` and `suffix_name` (officers): the name of the officer.
+- (Node) `appointed_date`, `resignation_date`, `org_hire_date` and `start_date` (officers): the dates of appointment, resignation, hire (by any police organization) and start in the CPD, as given (`M/D/YY`, with two-digit years).
+- (Node) `birth_year` (officers): the year of birth (-1 if unknown).
+- (Node) `gender`, `race` (officers): the gender and race of the officer.
+- (Node) `current_star` (officers): the last star (badge) number.
+- (Node) `cleaned_rank` (officers): the standardized rank.
+- (Node) `current_rank` and `current_unit` (officers): the last rank and unit.
+- (Node) `active` (officers): whether the officer is active (1, or 0).
+- (Node) `profile_count` (officers): the number of datasets in which the officer appears.
+- (Node) The properties of the officers are the most common values over the files, as determined by the Invisible Institute.
+- (Node) `cr_id` (complaints, in `complaints`): the complaint register number (-1 for officers).
+- (Node) `complaint_date`, `closed_date` and `incident_date` (complaints): the dates of filing, closure and incident (Unix time; -1 if unknown).
+- (Node) `incident_time` (complaints): the time of the incident (as given).
+- (Node) `days_to_closure` (complaints): the number of days to closure.
+- (Node) `address` (complaints): the block address of the incident (number and street).
+- (Node) `beat` (complaints): the police beat.
+- (Node) `city` (complaints): the city.
+- (Node) `location_code` (complaints): the type of location.
+- (Node) `old_complaint_address` (complaints): the address in the older format.
+- (Node) `latitude` and `longitude` (complaints): the geocoded block address (NaN if unknown).
+- (Node) `current_status` (complaints): the current status of the complaint.
+- (Node) `investigating_agency` (complaints): the last agency investigating the complaint.
+- (Node) `investigating_agencies` (complaints): all agencies investigating the complaint.
+- (Node) `complainant_type` (complaints): `CIVILIAN` or `CPD EMPLOYEE`.
+- (Node) `filename` (complaints): the last FOIA response with the complaint.
+- (Edge) `complaint_category` and `complaint_code` (`complaints`): the category of the allegation against the officer (e.g. `Excessive Force`).
+- (Edge) `complaint_categories` and `complaint_codes` (`complaints`): all categories and codes listed for the complaint in all files.
+- (Edge) `disciplined` (`complaints`): whether the complaint resulted in discipline.
+- (Edge) `final_finding` (`complaints`): the final finding (e.g. `SU` sustained, `NS` not sustained, `UN` unfounded, `EX` exonerated, `NAF` no affidavit, `AC` administratively closed).
+- (Edge) `final_outcome` and `final_outcome_desc` (`complaints`): the final outcome (e.g. `Reprimand`, `5 Day Suspension`) and its description.
+- (Edge) `recc_finding` and `recc_outcome` (`complaints`): the recommended finding and outcome.
+- (Edge) `days` (`complaints`): the recommended days of suspension (NaN if none).
+- (Edge) `cv` (`complaints`): the version of the complaint files from which the row comes.
+- (Node) `event_no` (incidents, in `use_of_force`): the event number (-1 if none).
+- (Node) `incident_date` and `incident_time` (incidents): the date (Unix time of the day) and time (as given) of the incident.
+- (Node) `block`, `direction` and `street` (incidents): the block address.
+- (Node) `beat` (incidents): the police beat.
+- (Node) `latitude` and `longitude` (incidents): the geocoded block address (NaN if unknown).
+- (Node) `indoor_or_outdoor`, `lighting_condition`, `weather_condition`, `location` and `location_recode` (incidents): the type of location.
+- (Node) The properties of the incidents are the values of the first report on the incident; the reports of different officers sometimes differ, and their own values are edge properties.
+- (Edge) `trr_id` (`use_of_force`, the reports): the id of the report.
+- (Edge) `date` and `time` (`use_of_force`): the date (Unix time of the day) and time.
+- (Edge) `block`, `direction`, `street`, `beat`, `latitude`, `longitude`, `indoor_or_outdoor`, `lighting_condition`, `weather_condition`, `location`, `location_recode` (`use_of_force`): the place, as for the incidents.
+- (Edge) `trr_status` (`use_of_force`): the status of the report.
+- (Edge) `firearm_used` and `taser` (`use_of_force`): whether the officer used a firearm or a taser (1 or 0).
+- (Edge) `notify_DET_division`, `notify_OEMC`, `notify_OP_command` and `notify_district_sergeant` (`use_of_force`): whether the detective division, the emergency management office, the OP command or the district sergeant were notified (as given; empty if not).
+- (Edge) `number_of_officers_using_firearm`, `number_of_weapons_discharged` and `total_number_of_shots` (`use_of_force`): the numbers of officers using a firearm, of weapons discharged and of shots (-1 if not given).
+- (Edge) `party_fired_first` (`use_of_force`): the party that fired first.
+- (Edge) `officer_assigned_beat`, `officer_on_duty`, `officer_in_uniform`, `officer_injured`, `officer_rank`, `officer_unit_id` and `officer_unit_detail_id` (`use_of_force`): the officer at the time of the incident (-1 if unknown).
+- (Edge) `subject_armed`, `subject_injured`, `subject_alleged_injury`, `subject_age`, `subject_birth_year`, `subject_gender` and `subject_race` (`use_of_force`): the subject of the force.
+- (Edge) `member_actions` and `subject_actions` (`use_of_force`): the actions of the officer (e.g. `VERBAL COMMANDS`, `TAKE DOWN/EMERGENCY HANDCUFFING`, `TASER`) and of the subject (e.g. `PULLED AWAY`), each listed once.
+- (Edge) `max_action_category` (`use_of_force`): the highest level of the officer's actions, from 0 (member presence) to 6 (firearm; NaN if not given, as in the older files).
+- (Edge) `subject_weapons` (`use_of_force`): the types of weapons of the subject (e.g. `HANDS/FISTS`).
+- (Edge) `cv` (`use_of_force`): the version of the files from which the report comes.
+- (Node) `award_id` (awards, in `awards`): the id of the award.
+- (Node) `award_type`, `award_start_date` and `award_end_date` (awards): the type of the award and its start and end dates (Unix time; -1 if unknown), the values of the first record of the award.
+- (Edge) `current_award_status` (`awards`): the status of the award (`FINAL`, `DELETED`, `DENIED`, `PENDING APPROVAL` or `PRELIMINARY`), as recorded for the officer.
+- (Edge) `award_type` (`awards`): the type of the award, as recorded for the officer.
+- (Edge) `award_request_date` and `ceremony_date` (`awards`): the dates of the request and of the ceremony (Unix time; -1 if unknown), as recorded for the officer.
+- (Edge) `requester_full_name` (`awards`): the name of the officer who requested the award (`REDACTED` for some), as recorded for the officer."""
 tags = ['Social', 'Offline', 'Affiliation', 'Bipartite', 'Unweighted', 'Multigraph', 'Timestamps', 'Metadata']
 url = ['https://github.com/invinst/chicago-police-data', 'https://cpdp.co/']
 citation = [('Invisible Institute, "Citizens Police Data Project: Chicago police data." GitHub repository (2025)',
[entries] Remove unneeded code from the entries by Tiago de Paula Peixoto 2c69f19
Unneeded "global files" statements, unused imports and variables,
f-strings without placeholders, duplicate keys in foodweb_little_rock
(keeping the order and the values used), and the second definition of
get_lines() in twitter_higgs. Nothing changes in the networks or
descriptions.

Changed: none
diff --git a/entries/chicago_police_complaints/__init__.py b/entries/chicago_police_complaints/__init__.py
index 6c4bc5d..cc3ee17 100644
--- a/entries/chicago_police_complaints/__init__.py
+++ b/entries/chicago_police_complaints/__init__.py
@@ -161,7 +161,6 @@ def column(d, c, vtype):
 @coerce_props()
 @annotate()
 def parse(alts=None):
-    global files
     name = __name__.split(".")[-1]
     def read(fname):
         with open_upstream_file(name, fname, "rb") as f:
[entries] New entry: chicago_police_complaints by Tiago de Paula Peixoto c7a910d
diff --git a/entries/chicago_police_complaints/__init__.py b/entries/chicago_police_complaints/__init__.py
new file mode 100644
index 0000000..6c4bc5d
--- /dev/null
+++ b/entries/chicago_police_complaints/__init__.py
@@ -0,0 +1,278 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2026 Tiago de Paula Peixoto <tiago@skewed.de>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Affero General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option) any
+# later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Affero General Public License along
+# with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from .. import *
+import pandas
+
+title = "Chicago police complaints, use of force and awards (1967-2023)"
+description = """Networks of the officers of the Chicago Police Department (CPD) and the complaints of misconduct against them, the reports of their use of force, and their awards, from the Citizens Police Data Project (CPDP) of the Invisible Institute, which obtained the records through Freedom of Information Act (FOIA) requests and litigation, and matched the officers across the files of several FOIA responses to unique ids. The entry contains three bipartite networks, each with all 37,545 officers of the matched profiles (also those without complaints, reports or awards):
+
+- `complaints`: the nodes are officers and complaints (complaint register, CR, numbers), and an edge connects a complaint with each officer accused in it. Complaints are filed by civilians or by CPD employees, and investigated by the CPD's Bureau of Internal Affairs (BIA) or civilian review agencies (OPS, IPRA, COPA). Officers accused together in complaints form the co-accusal networks used in studies of police misconduct. The network contains all 234,971 complaints (also the 89,776 complaints whose accused officers are unknown), with 263,315 accusations, from 1967 to 2023 (most complaints from 2000 onward). Four accused officers are missing from the profiles; they are included as nodes without attributes (`has_record` false).
+- `use_of_force`: the nodes are officers and incidents in which they used force, from the Tactical Response Reports (TRR) of 2004 to 2023 that officers file after using force. An edge connects an officer with an incident for each report of the officer on it (one per subject: an officer and an incident can be connected by several edges). The incidents are the CPD event numbers of the reports; the 15,252 reports without event number are incidents of their own. There are 65,816 incidents and 98,055 reports; the 809 reports without officer are left out (557 incidents have no reports with officers, and are included without edges).
+- `awards`: the nodes are officers and awards (e.g. honorable mentions, commendations, attendance and crime reduction awards), and an edge connects an award with each officer who received it (or for whom it was requested): 113,775 awards and 807,173 officer awards, from 1967 to 2020.
+
+Left out are the investigators and the police witnesses of the complaints (not matched to officer ids), the complainants, victims and civilian witnesses; of the use of force, the reports that are only in the secondary files of the TRR (without officer), the subject records, the details of the weapon discharges, and the chain of review of the reports (by other officers, not matched to officer ids); and the salaries and unit histories. The officers are named, as in the public records.
+
+The networks have the following properties:
+
+- Nodes: `is_complaint`, `is_incident` or `is_award`, whether the node is a complaint, incident or award (otherwise an officer).
+- Officers: `uid`, the officer's id in the data of the Invisible Institute (-1 for the other nodes); `has_record`, whether the officer is in the file of profiles; `officer_id`, the id of the officer on the CPDP site (`https://cpdp.co/officer/<officer_id>`; -1 if unknown); `first_name`, `last_name`, `middle_initial`, `middle_initial2` and `suffix_name`; `appointed_date`, `resignation_date`, `org_hire_date` and `start_date`, the dates of appointment, resignation, hire (by any police organization) and start in the CPD, as given (`M/D/YY`, with two-digit years); `birth_year` (-1 if unknown); `gender`; `race`; `current_star`, the last star (badge) number; `cleaned_rank`, the standardized rank; `current_rank` and `current_unit`, the last rank and unit; `active`, whether the officer is active (1, or 0); `profile_count`, the number of datasets in which the officer appears. These are the most common values over the files, as determined by the Invisible Institute.
+- Complaints (`complaints`): `cr_id`, the complaint register number (-1 for officers); `complaint_date`, `closed_date` and `incident_date`, the dates of filing, closure and incident (Unix time; -1 if unknown); `incident_time`, the time of the incident (as given); `days_to_closure`; `address`, the block address of the incident (number and street); `beat`, the police beat; `city`; `location_code`, the type of location; `old_complaint_address`, the address in the older format; `latitude` and `longitude`, the geocoded block address (NaN if unknown); `current_status`; `investigating_agency`, the last agency investigating the complaint, and `investigating_agencies`, all of them; `complainant_type`, `CIVILIAN` or `CPD EMPLOYEE`; `filename`, the last FOIA response with the complaint.
+- Edges of `complaints`: `complaint_category` and `complaint_code`, the category of the allegation against the officer (e.g. `Excessive Force`); `complaint_categories` and `complaint_codes`, all categories and codes listed for the complaint in all files; `disciplined`, whether the complaint resulted in discipline; `final_finding` (e.g. `SU` sustained, `NS` not sustained, `UN` unfounded, `EX` exonerated, `NAF` no affidavit, `AC` administratively closed), `final_outcome` (e.g. `Reprimand`, `5 Day Suspension`) and `final_outcome_desc`; `recc_finding` and `recc_outcome`, the recommended finding and outcome; `days`, the recommended days of suspension (NaN if none); `cv`, the version of the complaint files from which the row comes.
+- Incidents (`use_of_force`): `event_no`, the event number (-1 if none); `incident_date` (Unix time of the day) and `incident_time` (as given); `block`, `direction` and `street`, the block address; `beat`; `latitude` and `longitude`, the geocoded block address (NaN if unknown); `indoor_or_outdoor`, `lighting_condition`, `weather_condition`, `location` and `location_recode`, the type of location. These are the values of the first report on the incident; the reports of different officers sometimes differ, and their own values are edge properties.
+- Edges of `use_of_force` (the reports): `trr_id`, the id of the report; `date` (Unix time of the day) and `time`; the place, as for the incidents (`block`, `direction`, `street`, `beat`, `latitude`, `longitude`, `indoor_or_outdoor`, `lighting_condition`, `weather_condition`, `location`, `location_recode`); `trr_status`, the status of the report; `firearm_used` and `taser`, whether the officer used a firearm or a taser (1 or 0); `notify_DET_division`, `notify_OEMC`, `notify_OP_command` and `notify_district_sergeant`, whether the detective division, the emergency management office, the OP command or the district sergeant were notified (as given; empty if not); `number_of_officers_using_firearm`, `number_of_weapons_discharged` and `total_number_of_shots` (-1 if not given); `party_fired_first`; `officer_assigned_beat`, `officer_on_duty`, `officer_in_uniform`, `officer_injured`, `officer_rank`, `officer_unit_id` and `officer_unit_detail_id`, the officer at the time of the incident (-1 if unknown); `subject_armed`, `subject_injured`, `subject_alleged_injury`, `subject_age`, `subject_birth_year`, `subject_gender` and `subject_race`, the subject of the force; `member_actions` and `subject_actions`, the actions of the officer (e.g. `VERBAL COMMANDS`, `TAKE DOWN/EMERGENCY HANDCUFFING`, `TASER`) and of the subject (e.g. `PULLED AWAY`), each listed once; `max_action_category`, the highest level of the officer's actions, from 0 (member presence) to 6 (firearm; NaN if not given, as in the older files); `subject_weapons`, the types of weapons of the subject (e.g. `HANDS/FISTS`); `cv`, the version of the files from which the report comes.
+- Awards (`awards`): `award_id`, the id of the award; `award_type`; `award_start_date` and `award_end_date` (Unix time; -1 if unknown), the values of the first record of the award.
+- Edges of `awards`: `current_award_status` (`FINAL`, `DELETED`, `DENIED`, `PENDING APPROVAL` or `PRELIMINARY`), `award_type`, `award_request_date` and `ceremony_date` (Unix time; -1 if unknown), and `requester_full_name`, the name of the officer who requested the award (`REDACTED` for some), as recorded for the officer."""
+tags = ['Social', 'Offline', 'Affiliation', 'Bipartite', 'Unweighted', 'Multigraph', 'Timestamps', 'Metadata']
+url = ['https://github.com/invinst/chicago-police-data', 'https://cpdp.co/']
+citation = [('Invisible Institute, "Citizens Police Data Project: Chicago police data." GitHub repository (2025)',
+             'https://github.com/invinst/chicago-police-data'),
+            ('L. Zhao, A. V. Papachristos, "Network Position and Police Who Shoot." The Annals of the American Academy of Political and Social Science 687, 89-112 (2020)',
+             'https://doi.org/10.1177/0002716219901171')]
+# No license is stated (public records obtained with FOIA requests)
+upstream_license = None
+icon_hash = None
+
+# The unified data of the repository at the commit of 2025-09-01 (files in
+# Git LFS)
+upstream_prefix = 'https://media.githubusercontent.com/media/invinst/chicago-police-data/1a552953d489ca4866da76ac91754cc18c71014d/data/unified_data'
+PROFILES = 'officers/final-profiles.csv'
+files = [(('complaints/complaints-accused.csv', 'complaints/complaints-complaints.csv',
+           PROFILES), 'complaints', None),
+         (('TRR/trr_main.csv', 'TRR/trr_actions_responses.csv',
+           'TRR/trr_subject_weapons.csv', PROFILES), 'use_of_force', None),
+         (('awards/awards.csv', PROFILES), 'awards', None)]
+
+def fetch_upstream(force=False):
+    return fetch_upstream_files(__name__.split(".")[-1], upstream_prefix, files,
+                                force)
+
+def unix(s, fmt="%Y-%m-%d"):
+    t = pandas.to_datetime(s.where(s != ""), format=fmt, utc=True,
+                           errors="coerce")
+    return numpy.where(t.isna(), -1,
+                       (t - pandas.Timestamp(0, tz="UTC")) // pandas.Timedelta(seconds=1)).astype("int64")
+
+def num(s, dtype="int64"):
+    x = pandas.to_numeric(s, errors="coerce")
+    if dtype == "int64":
+        return x.fillna(-1).astype("int64").values
+    return x.astype("float64").values
+
+def uid(s):
+    "Officer UIDs as strings of integers (some files write them as floats)."
+    return pandas.Series(num(s), index=s.index).astype(str).where(s != "", "")
+
+DEFAULTS = {"string": "", "double": numpy.nan, "bool": 0}
+
+def set_vp(g, pname, vtype, vals, offset):
+    """Create the vertex property pname, with the values vals for the vertices
+    offset, offset + 1, ..., and the default value for the others."""
+    p = g.new_vp(vtype, val=DEFAULTS.get(vtype, -1))
+    if vtype == "string":
+        for i, x in enumerate(vals):
+            p[g.vertex(offset + i)] = x
+    else:
+        p.a[offset:offset + len(vals)] = vals
+    g.vp[pname] = p
+
+def set_ep(g, pname, vtype, vals):
+    """Create the edge property pname with the values vals, in the order in
+    which the edges were added."""
+    p = g.new_ep(vtype)
+    if vtype in ("string", "vector<string>"):
+        eidx = g.edge_index
+        for e in g.edges():
+            p[e] = vals[eidx[e]]
+    else:
+        p.a = vals
+    g.ep[pname] = p
+
+def officer_graph(o, uids, kind, nother):
+    """A graph with the officers of the profiles o (and those of uids missing
+    from them) followed by nother nodes of another kind, marked by is_<kind>.
+    Returns the graph and the map from UIDs to vertex indices."""
+    missing = sorted(set(uids) - set(o.UID) - {""}, key=int)
+    o = pandas.concat([o, pandas.DataFrame({"UID": missing})]).fillna("")
+    no = len(o)
+    g = Graph(directed=False)
+    g.add_vertex(no + nother)
+    g.vp[f"is_{kind}"] = g.new_vp("bool")
+    g.vp[f"is_{kind}"].a[no:] = True
+    set_vp(g, "uid", "int64_t", num(o.UID), 0)
+    set_vp(g, "has_record", "bool", (~o.UID.isin(missing)).values.astype("int"), 0)
+    set_vp(g, "officer_id", "int64_t", num(o.officer_id), 0)
+    for col in ["first_name", "last_name", "middle_initial", "middle_initial2",
+                "suffix_name", "appointed_date", "resignation_date",
+                "org_hire_date", "start_date", "gender", "race",
+                "cleaned_rank", "current_rank", "current_unit"]:
+        set_vp(g, col, "string", o[col].values, 0)
+    set_vp(g, "birth_year", "int64_t", num(o.birth_year), 0)
+    set_vp(g, "current_star", "int64_t", num(o.current_star), 0)
+    set_vp(g, "active", "int16_t", num(o.current_status), 0)
+    set_vp(g, "profile_count", "int64_t", num(o.profile_count), 0)
+    return g, {u: i for i, u in enumerate(o.UID)}, no
+
+# columns of the use-of-force reports: (name, type)
+TRR_PLACE = [("block", "string"), ("direction", "string"), ("street", "string"),
+             ("beat", "int64_t"), ("latitude", "double"), ("longitude", "double"),
+             ("indoor_or_outdoor", "string"), ("lighting_condition", "string"),
+             ("weather_condition", "string"), ("location", "string"),
+             ("location_recode", "string")]
+TRR_REPORT = [("trr_status", "string"), ("firearm_used", "int16_t"),
+              ("notify_DET_division", "string"), ("notify_OEMC", "string"),
+              ("notify_OP_command", "string"),
+              ("notify_district_sergeant", "string"),
+              ("number_of_officers_using_firearm", "int64_t"),
+              ("number_of_weapons_discharged", "int64_t"),
+              ("total_number_of_shots", "int64_t"),
+              ("party_fired_first", "string"), ("taser", "int16_t"),
+              ("officer_assigned_beat", "string"),
+              ("officer_on_duty", "int16_t"), ("officer_in_uniform", "int16_t"),
+              ("officer_injured", "int16_t"), ("officer_rank", "string"),
+              ("officer_unit_id", "int64_t"),
+              ("officer_unit_detail_id", "int64_t"),
+              ("subject_armed", "string"), ("subject_injured", "string"),
+              ("subject_alleged_injury", "string"), ("subject_age", "int64_t"),
+              ("subject_birth_year", "int64_t"), ("subject_gender", "string"),
+              ("subject_race", "string"), ("cv", "int64_t")]
+
+def column(d, c, vtype):
+    if vtype == "string":
+        return d[c].values
+    return num(d[c], "double" if vtype == "double" else "int64")
+
+@cache_network()
+@coerce_props()
+@annotate()
+def parse(alts=None):
+    global files
+    name = __name__.split(".")[-1]
+    def read(fname):
+        with open_upstream_file(name, fname, "rb") as f:
+            return pandas.read_csv(f, dtype=str, keep_default_na=False)
+    for fnames, alt, fmt in files:
+        if alts is not None and alt not in alts:
+            continue
+        o = read(PROFILES)
+        if alt == "complaints":
+            yield alt, parse_complaints(read, fnames, o)
+        elif alt == "use_of_force":
+            yield alt, parse_use_of_force(read, fnames, o)
+        else:
+            yield alt, parse_awards(read, fnames, o)
+
+def parse_complaints(read, fnames, o):
+    faccused, fcomplaints, fprofiles = fnames
+    a, c = read(faccused), read(fcomplaints)
+    g, oidx, no = officer_graph(o, a.UID, "complaint", len(c))
+    cidx = {r: i + no for i, r in enumerate(c.cr_id)}
+
+    set_vp(g, "cr_id", "int64_t", num(c.cr_id), no)
+    for col in ["complaint_date", "closed_date", "incident_date"]:
+        set_vp(g, col, "int64_t", unix(c[col]), no)
+    set_vp(g, "days_to_closure", "int64_t", num(c.days_to_closure), no)
+    address = (c.add1 + " " + c.add2).str.strip()
+    set_vp(g, "address", "string", address.values, no)
+    for col in ["incident_time", "beat", "city", "location_code",
+                "old_complaint_address", "current_status",
+                "investigating_agency", "investigating_agencies",
+                "complainant_type", "filename"]:
+        set_vp(g, col, "string", c[col].values, no)
+    set_vp(g, "latitude", "double", num(c.latitude, "double"), no)
+    set_vp(g, "longitude", "double", num(c.longitude, "double"), no)
+
+    g.add_edge_list([(oidx[u], cidx[r]) for u, r in zip(a.UID, a.cr_id)])
+    for col in ["complaint_category", "complaint_code", "complaint_categories",
+                "complaint_codes", "final_finding", "final_outcome",
+                "final_outcome_desc", "recc_finding", "recc_outcome"]:
+        set_ep(g, col, "string", a[col].values)
+    set_ep(g, "disciplined", "bool", (a.disciplined == "True").values)
+    set_ep(g, "days", "double", num(a.days, "double"))
+    set_ep(g, "cv", "int16_t", num(a.cv))
+    return g
+
+def parse_use_of_force(read, fnames, o):
+    fmain, factions, fweapons, fprofiles = fnames
+    m = read(fmain)
+    m["UID"] = uid(m.UID)
+    # incidents: the event numbers, or the report itself if it has none
+    ev = num(m.event_no)
+    m["incident"] = numpy.where(ev >= 0, ev.astype(str), "trr " + m.trr_id)
+    inc = m.drop_duplicates("incident")
+    g, oidx, no = officer_graph(o, m.UID, "incident", len(inc))
+    iidx = {k: i + no for i, k in enumerate(inc.incident)}
+
+    set_vp(g, "event_no", "int64_t", num(inc.event_no), no)
+    set_vp(g, "incident_date", "int64_t", unix(inc.trr_date, "%m/%d/%y"), no)
+    set_vp(g, "incident_time", "string", inc.trr_time.values, no)
+    for c, vt in TRR_PLACE:
+        set_vp(g, c, vt, column(inc, c, vt), no)
+
+    m = m[m.UID != ""]
+    g.add_edge_list([(oidx[u], iidx[k]) for u, k in zip(m.UID, m.incident)])
+    set_ep(g, "trr_id", "string", m.trr_id.values)
+    set_ep(g, "date", "int64_t", unix(m.trr_date, "%m/%d/%y"))
+    set_ep(g, "time", "string", m.trr_time.values)
+    for c, vt in TRR_PLACE + TRR_REPORT:
+        set_ep(g, c, vt, column(m, c, vt))
+
+    # the actions of the officer and of the subject, and the subject's weapons
+    acts = read(factions)
+    acts = acts[acts.trr_id.isin(set(m.trr_id))]
+    def lists(d, col):
+        # the rows are often repeated (as many times as there are subjects
+        # or files); each value is listed once
+        d = d[d[col] != ""].drop_duplicates(["trr_id", col])
+        return d.groupby("trr_id", sort=False)[col].agg(list).to_dict()
+    member = acts[acts.person == "Member Action"]
+    mact = lists(member, "action")
+    sact = lists(acts[acts.person == "Subject Action"], "action")
+    cat = pandas.to_numeric(member.action_category, errors="coerce")
+    maxcat = cat.groupby(member.trr_id).max().to_dict()
+    del acts, member
+    weapons = read(fweapons)
+    weap = lists(weapons[weapons.trr_id.isin(set(m.trr_id))], "weapon_type")
+    del weapons
+    set_ep(g, "member_actions", "vector<string>",
+           [mact.get(t, []) for t in m.trr_id])
+    set_ep(g, "max_action_category", "double",
+           numpy.array([maxcat.get(t, numpy.nan) for t in m.trr_id], dtype="float64"))
+    set_ep(g, "subject_actions", "vector<string>",
+           [sact.get(t, []) for t in m.trr_id])
+    set_ep(g, "subject_weapons", "vector<string>",
+           [weap.get(t, []) for t in m.trr_id])
+    return g
+
+def parse_awards(read, fnames, o):
+    fawards, fprofiles = fnames
+    a = read(fawards)
+    a["UID"] = uid(a.UID)
+    aw = a.drop_duplicates("award_id")
+    g, oidx, no = officer_graph(o, a.UID, "award", len(aw))
+    aidx = {k: i + no for i, k in enumerate(aw.award_id)}
+    set_vp(g, "award_id", "int64_t", num(aw.award_id), no)
+    set_vp(g, "award_type", "string", aw.award_type.values, no)
+    for c in ["award_start_date", "award_end_date"]:
+        set_vp(g, c, "int64_t", unix(aw[c]), no)
+    g.add_edge_list([(oidx[u], aidx[k]) for u, k in zip(a.UID, a.award_id)])
+    for c in ["current_award_status", "award_type", "requester_full_name"]:
+        set_ep(g, c, "string", a[c].values)
+    for c in ["award_request_date", "ceremony_date"]:
+        set_ep(g, c, "int64_t", unix(a[c]))
+    return g