crimepy 0.2.0__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {crimepy-0.2.0 → crimepy-0.2.2}/CHANGELOG.md +45 -0
- {crimepy-0.2.0 → crimepy-0.2.2}/CITATION.cff +2 -2
- {crimepy-0.2.0 → crimepy-0.2.2}/PKG-INFO +3 -3
- {crimepy-0.2.0 → crimepy-0.2.2}/README.md +1 -1
- {crimepy-0.2.0 → crimepy-0.2.2}/docs/advanced.md +37 -5
- crimepy-0.2.2/docs/scripts/gen_ref_nav.py +40 -0
- {crimepy-0.2.0 → crimepy-0.2.2}/docs/tutorial.md +4 -2
- {crimepy-0.2.0 → crimepy-0.2.2}/pyproject.toml +1 -4
- crimepy-0.2.2/src/crimepy/__init__.py +39 -0
- {crimepy-0.2.0 → crimepy-0.2.2}/src/crimepy/analyze.py +5 -0
- {crimepy-0.2.0 → crimepy-0.2.2}/src/crimepy/cli.py +71 -8
- {crimepy-0.2.0 → crimepy-0.2.2}/src/crimepy/combine.py +24 -10
- {crimepy-0.2.0 → crimepy-0.2.2}/src/crimepy/nibrs/__init__.py +17 -11
- {crimepy-0.2.0 → crimepy-0.2.2}/src/crimepy/nibrs/cde.py +8 -2
- {crimepy-0.2.0 → crimepy-0.2.2}/src/crimepy/nibrs/codes.py +237 -111
- {crimepy-0.2.0 → crimepy-0.2.2}/src/crimepy/nibrs/layouts.py +171 -123
- {crimepy-0.2.0 → crimepy-0.2.2}/src/crimepy/nibrs/munge.py +38 -13
- {crimepy-0.2.0 → crimepy-0.2.2}/src/crimepy/nibrs/reader.py +122 -27
- {crimepy-0.2.0 → crimepy-0.2.2}/src/crimepy/nibrs/store.py +15 -3
- {crimepy-0.2.0 → crimepy-0.2.2}/src/crimepy/nibrs/tables.py +55 -11
- {crimepy-0.2.0 → crimepy-0.2.2}/src/crimepy/nibrs/techniques.py +80 -45
- {crimepy-0.2.0 → crimepy-0.2.2}/src/crimepy/plot.py +27 -1
- {crimepy-0.2.0 → crimepy-0.2.2}/src/crimepy/techniques.py +61 -33
- {crimepy-0.2.0 → crimepy-0.2.2}/src/crimepy/utilities.py +6 -0
- {crimepy-0.2.0 → crimepy-0.2.2}/tests/conftest.py +117 -30
- {crimepy-0.2.0 → crimepy-0.2.2}/tests/test_analysis.py +189 -11
- {crimepy-0.2.0 → crimepy-0.2.2}/tests/test_main.py +10 -2
- {crimepy-0.2.0 → crimepy-0.2.2}/tests/test_nibrs_reader.py +112 -2
- {crimepy-0.2.0 → crimepy-0.2.2}/tests/test_nibrs_store.py +292 -45
- {crimepy-0.2.0 → crimepy-0.2.2}/tests/test_utilities.py +23 -2
- {crimepy-0.2.0 → crimepy-0.2.2}/uv.lock +29 -28
- crimepy-0.2.0/docs/scripts/gen_ref_nav.py +0 -38
- crimepy-0.2.0/src/crimepy/__init__.py +0 -25
- {crimepy-0.2.0 → crimepy-0.2.2}/.cruft.json +0 -0
- {crimepy-0.2.0 → crimepy-0.2.2}/.editorconfig +0 -0
- {crimepy-0.2.0 → crimepy-0.2.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {crimepy-0.2.0 → crimepy-0.2.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {crimepy-0.2.0 → crimepy-0.2.2}/.github/dependabot.yml +0 -0
- {crimepy-0.2.0 → crimepy-0.2.2}/.github/workflows/ci.yml +0 -0
- {crimepy-0.2.0 → crimepy-0.2.2}/.github/workflows/publish.yml +0 -0
- {crimepy-0.2.0 → crimepy-0.2.2}/.github/workflows/validate_codecov.yml +0 -0
- {crimepy-0.2.0 → crimepy-0.2.2}/.gitignore +0 -0
- {crimepy-0.2.0 → crimepy-0.2.2}/.pre-commit-config.yaml +0 -0
- {crimepy-0.2.0 → crimepy-0.2.2}/.python-version +0 -0
- {crimepy-0.2.0 → crimepy-0.2.2}/CODE_OF_CONDUCT.md +0 -0
- {crimepy-0.2.0 → crimepy-0.2.2}/CONTRIBUTING.md +0 -0
- {crimepy-0.2.0 → crimepy-0.2.2}/LICENSE +0 -0
- {crimepy-0.2.0 → crimepy-0.2.2}/codecov.yml +0 -0
- {crimepy-0.2.0 → crimepy-0.2.2}/docs/changelog.md +0 -0
- {crimepy-0.2.0 → crimepy-0.2.2}/docs/code_of_conduct.md +0 -0
- {crimepy-0.2.0 → crimepy-0.2.2}/docs/contributing.md +0 -0
- {crimepy-0.2.0 → crimepy-0.2.2}/docs/css/material.css +0 -0
- {crimepy-0.2.0 → crimepy-0.2.2}/docs/css/mkdocstrings.css +0 -0
- {crimepy-0.2.0 → crimepy-0.2.2}/docs/index.md +0 -0
- {crimepy-0.2.0 → crimepy-0.2.2}/docs/license.md +0 -0
- {crimepy-0.2.0 → crimepy-0.2.2}/docs/recipes.md +0 -0
- {crimepy-0.2.0 → crimepy-0.2.2}/mkdocs.yml +0 -0
- {crimepy-0.2.0 → crimepy-0.2.2}/src/crimepy/options.py +0 -0
|
@@ -4,6 +4,51 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
<!-- insertion marker -->
|
|
6
6
|
|
|
7
|
+
## 0.2.2
|
|
8
|
+
|
|
9
|
+
* Fixed the `pydoclint` check of CI: the Deflate64 reader of
|
|
10
|
+
`crimepy.nibrs.reader` and the fake responses of the download tests are
|
|
11
|
+
dataclasses, so their arguments are documented only by the class, and the
|
|
12
|
+
"Yields" sections of `reader.tables` and the tests name the types that
|
|
13
|
+
they yield.
|
|
14
|
+
* Requires amos 0.6.2, and the lock file uses the latest bobbie (0.3.3),
|
|
15
|
+
bunches (0.3.0), camina (0.2.4), chrisjen (0.4.2), holden (0.2.6), miller
|
|
16
|
+
(0.2.3), nagata (0.2.1), and wonka (0.3.0). camina, chrisjen, and nagata
|
|
17
|
+
ship the `py.typed` marker, so they are gone from the `mypy` override for
|
|
18
|
+
untyped imports.
|
|
19
|
+
* Importing `crimepy` registers the codebook of every column of codes with
|
|
20
|
+
`amos.register_value_map`, so `amos`'s `map_values` replaces the codes of
|
|
21
|
+
the columns it names with their labels, with no map in the settings
|
|
22
|
+
(`columns = victim_race, offender_race`).
|
|
23
|
+
* `load_nibrs` keeps a label that is not a column of the loaded table as a
|
|
24
|
+
pending label (as amos 0.6.2 does), instead of dropping it, so a later
|
|
25
|
+
step such as `count_crimes` or `derive_columns` can make it.
|
|
26
|
+
* `crimepy.combine.stack` and `merge` keep a pending label, and the kinds
|
|
27
|
+
(`Dataset.kinds`, from amos's `set_kinds`) of the columns that are still
|
|
28
|
+
there, so dummies stay dummies.
|
|
29
|
+
* The tutorial scores its model with `measure`, since amos 0.6.2's metrics
|
|
30
|
+
are no longer techniques with an `apply`, and the advanced user guide shows
|
|
31
|
+
`map_values` with the codebooks.
|
|
32
|
+
|
|
33
|
+
## 0.2.1
|
|
34
|
+
|
|
35
|
+
* **Breaking:** `crimepy.nibrs.codes.STATES` is now the name of each numeric
|
|
36
|
+
state code (it was `STATE_NAMES`), and `codes.STATE_ABBREVIATIONS` is the
|
|
37
|
+
postal abbreviation (it was `STATES`).
|
|
38
|
+
* Each codebook of `crimepy.nibrs.codes.CODEBOOKS` is also public by itself
|
|
39
|
+
(such as `codes.RACES` and `codes.WEAPONS`). The "clearances" codebook is
|
|
40
|
+
`codes.EXCEPTIONAL_CLEARANCES`, and the "divisions" codebook is
|
|
41
|
+
`codes.SUBREGIONS`. `codes.OFFENSES` has the name, category, whom or what
|
|
42
|
+
it is against, and group of each offense code.
|
|
43
|
+
* `crimepy.nibrs.codes.OFFENSE_TYPES` lists the offense codes of each type:
|
|
44
|
+
whom or what they are against ("Person", "Property", "Society", and "Not
|
|
45
|
+
a Crime"), and "National Security Offenses", "Rape Offenses", "Sex
|
|
46
|
+
Offenses", and "Immigration Offenses".
|
|
47
|
+
* The code follows the house style throughout: lines of at most 80
|
|
48
|
+
characters (the tables in docstrings are now lists), section markers that
|
|
49
|
+
group the members of each module and class, and a full docstring for every
|
|
50
|
+
function and method, tests and private ones included.
|
|
51
|
+
|
|
7
52
|
## 0.2.0
|
|
8
53
|
|
|
9
54
|
* **Breaking:** crimepy keeps its data in amos's workspace (amos 0.6.0), not
|
|
@@ -12,8 +12,8 @@ authors:
|
|
|
12
12
|
family-names: Yung
|
|
13
13
|
email: coreyrayburnyung@gmail.com
|
|
14
14
|
orcid: "https://orcid.org/0000-0001-6524-9956"
|
|
15
|
-
version: 0.2.
|
|
16
|
-
date-released: 2026-
|
|
15
|
+
version: 0.2.2
|
|
16
|
+
date-released: 2026-11-01
|
|
17
17
|
license: Apache-2.0
|
|
18
18
|
repository-code: "https://github.com/WithPrecedent/crimepy"
|
|
19
19
|
url: "https://WithPrecedent.github.io/crimepy"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: crimepy
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Accessible tools for downloading, combining, munging, analyzing, and plotting crime data
|
|
5
5
|
Project-URL: Documentation, https://WithPrecedent.github.io/crimepy
|
|
6
6
|
Project-URL: Repository, https://github.com/WithPrecedent/crimepy
|
|
@@ -26,7 +26,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
26
26
|
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
27
27
|
Classifier: Topic :: Sociology
|
|
28
28
|
Requires-Python: >=3.11
|
|
29
|
-
Requires-Dist: amos[plots]>=0.6.
|
|
29
|
+
Requires-Dist: amos[plots]>=0.6.2
|
|
30
30
|
Requires-Dist: inflate64>=1.0
|
|
31
31
|
Requires-Dist: numpy>=1.26
|
|
32
32
|
Requires-Dist: pandas>=3.0
|
|
@@ -308,7 +308,7 @@ repository" button reads the same information from
|
|
|
308
308
|
@software{yung_crimepy,
|
|
309
309
|
author = {Yung, Corey Rayburn},
|
|
310
310
|
title = {crimepy: Accessible tools for downloading, combining, munging, analyzing, and plotting crime data},
|
|
311
|
-
version = {0.2.
|
|
311
|
+
version = {0.2.2},
|
|
312
312
|
year = {2026},
|
|
313
313
|
url = {https://github.com/WithPrecedent/crimepy},
|
|
314
314
|
license = {Apache-2.0}
|
|
@@ -270,7 +270,7 @@ repository" button reads the same information from
|
|
|
270
270
|
@software{yung_crimepy,
|
|
271
271
|
author = {Yung, Corey Rayburn},
|
|
272
272
|
title = {crimepy: Accessible tools for downloading, combining, munging, analyzing, and plotting crime data},
|
|
273
|
-
version = {0.2.
|
|
273
|
+
version = {0.2.2},
|
|
274
274
|
year = {2026},
|
|
275
275
|
url = {https://github.com/WithPrecedent/crimepy},
|
|
276
276
|
license = {Apache-2.0}
|
|
@@ -167,11 +167,39 @@ offenses = crimepy.nibrs.decode(
|
|
|
167
167
|
offenses, ['location'], {'location': {'20': 'Home', '13': 'Street'}})
|
|
168
168
|
```
|
|
169
169
|
|
|
170
|
+
Importing `crimepy` also registers every codebook with
|
|
171
|
+
`amos.register_value_map`, so `amos`'s `map_values` replaces the codes of
|
|
172
|
+
the columns it names with their labels, with no map in the settings:
|
|
173
|
+
|
|
174
|
+
```ini
|
|
175
|
+
[wrangler]
|
|
176
|
+
techniques = load_nibrs, map_values
|
|
177
|
+
|
|
178
|
+
[load_nibrs_parameters]
|
|
179
|
+
table = victims
|
|
180
|
+
years = 2023
|
|
181
|
+
decode = false
|
|
182
|
+
|
|
183
|
+
[map_values_parameters]
|
|
184
|
+
columns = victim_sex, victim_race, offender_race
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Unlike `decode_nibrs`, `map_values` sorts the categories rather than putting
|
|
188
|
+
them in the order of the codebook.
|
|
189
|
+
|
|
170
190
|
`offenses`, `arrestees`, and `group_b_arrests` also get the category of the
|
|
171
191
|
offense (`offense_category`, such as "Assault Offenses"), whom or what it is
|
|
172
192
|
against (`crime_against`: "Person", "Property", "Society", or "Not a
|
|
173
193
|
Crime"), and its group (`offense_group`: "A" or "B"), from the CDE's table of
|
|
174
194
|
offenses (with the prefix "arrest_" for arrests).
|
|
195
|
+
`crimepy.nibrs.codes.OFFENSE_TYPES` lists the offense codes that are against
|
|
196
|
+
each of them, and those of a few types that cut across the categories
|
|
197
|
+
("National Security Offenses", "Rape Offenses", "Sex Offenses", and
|
|
198
|
+
"Immigration Offenses"), such as for picking the crimes against property:
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
property_crimes = crimepy.nibrs.codes.OFFENSE_TYPES['Property']
|
|
202
|
+
```
|
|
175
203
|
|
|
176
204
|
## Where the data are saved
|
|
177
205
|
|
|
@@ -230,13 +258,17 @@ while the parquet files are read, so the rest is never loaded.
|
|
|
230
258
|
in the dataset's `tables` and a figure in its `figures`, under the
|
|
231
259
|
technique's name, and every step is recorded in its `history`. `count`
|
|
232
260
|
makes a new dataset of counts, whose label is the counts.
|
|
233
|
-
* `crimepy.combine.stack` and `merge` return a dataset with the label
|
|
234
|
-
groups of the first dataset given,
|
|
261
|
+
* `crimepy.combine.stack` and `merge` return a dataset with the label,
|
|
262
|
+
groups, and kinds (from `amos`'s `set_kinds`) of the first dataset given,
|
|
263
|
+
if their columns are still there.
|
|
264
|
+
* A label that is not yet a column (such as the "count" of `count_crimes`,
|
|
265
|
+
or a column that `derive_columns` makes) is pending, as `amos` keeps it:
|
|
266
|
+
`load_nibrs`, `stack`, and `merge` keep it until a later step makes it.
|
|
235
267
|
|
|
236
268
|
A dataset gathers the data, what was learned from it, and how, so it is the
|
|
237
|
-
record of an analysis. Any `amos` technique (a model, a validator, a
|
|
238
|
-
|
|
239
|
-
[tutorial](tutorial.md#model).
|
|
269
|
+
record of an analysis. Any `amos` technique (a model, a validator, or a plot
|
|
270
|
+
such as `shap_beeswarm`) can be applied to it directly, and a metric's
|
|
271
|
+
`measure` scores its predictions, as in the [tutorial](tutorial.md#model).
|
|
240
272
|
|
|
241
273
|
## The amos techniques
|
|
242
274
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Generate the code reference pages and navigation.
|
|
2
|
+
|
|
3
|
+
This code is adapted from pawamoy's packages. See, e.g.,
|
|
4
|
+
https://github.com/pawamoy/duty/blob/main/scripts/gen_ref_nav.py.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import mkdocs_gen_files
|
|
12
|
+
|
|
13
|
+
nav = mkdocs_gen_files.Nav()
|
|
14
|
+
mod_symbol = '<code class="doc-symbol doc-symbol-nav doc-symbol-module"></code>'
|
|
15
|
+
|
|
16
|
+
for path in sorted(Path('src').rglob('*.py')):
|
|
17
|
+
module_path = path.relative_to('src').with_suffix('')
|
|
18
|
+
doc_path = path.relative_to('src').with_suffix('.md')
|
|
19
|
+
full_doc_path = Path('reference', doc_path)
|
|
20
|
+
|
|
21
|
+
parts = tuple(module_path.parts)
|
|
22
|
+
|
|
23
|
+
if parts[-1] == '__init__':
|
|
24
|
+
parts = parts[:-1]
|
|
25
|
+
doc_path = doc_path.with_name('index.md')
|
|
26
|
+
full_doc_path = full_doc_path.with_name('index.md')
|
|
27
|
+
elif parts[-1].startswith('_'):
|
|
28
|
+
continue
|
|
29
|
+
|
|
30
|
+
nav_parts = [f'{mod_symbol} {part}' for part in parts]
|
|
31
|
+
nav[tuple(nav_parts)] = doc_path.as_posix()
|
|
32
|
+
|
|
33
|
+
with mkdocs_gen_files.open(full_doc_path, 'w') as fd:
|
|
34
|
+
ident = '.'.join(parts)
|
|
35
|
+
fd.write(f'::: {ident}')
|
|
36
|
+
|
|
37
|
+
mkdocs_gen_files.set_edit_path(full_doc_path, '..' / path)
|
|
38
|
+
|
|
39
|
+
with mkdocs_gen_files.open('reference/SUMMARY.md', 'w') as nav_file:
|
|
40
|
+
nav_file.writelines(nav.build_literate_nav())
|
|
@@ -130,10 +130,12 @@ features = incidents.data[columns].dropna().astype(int)
|
|
|
130
130
|
model_data = amos.Dataset.create(features, label = 'cleared', seed = 43)
|
|
131
131
|
splitters.Stratified().apply(model_data)
|
|
132
132
|
models.GradientBoosting().apply(model_data)
|
|
133
|
-
metrics.RocAuc().
|
|
134
|
-
model_data.metrics['roc_auc']
|
|
133
|
+
metrics.RocAuc().measure(model_data)
|
|
135
134
|
```
|
|
136
135
|
|
|
136
|
+
A metric is not a step: `measure` returns its score. In a settings file,
|
|
137
|
+
name it as a worker's `criterion` or in the `metrics` of a `scorecard`.
|
|
138
|
+
|
|
137
139
|
(`arrestee_count` all but decides `cleared`, so the score is high: a real
|
|
138
140
|
model of clearance would leave it out.)
|
|
139
141
|
|
|
@@ -33,7 +33,7 @@ keywords = [
|
|
|
33
33
|
"nibrs",
|
|
34
34
|
"ucr"]
|
|
35
35
|
dependencies = [
|
|
36
|
-
"amos[plots]>=0.6.
|
|
36
|
+
"amos[plots]>=0.6.2",
|
|
37
37
|
"inflate64>=1.0",
|
|
38
38
|
"numpy>=1.26",
|
|
39
39
|
"pandas>=3.0",
|
|
@@ -126,10 +126,7 @@ show_error_codes = true
|
|
|
126
126
|
# These packages are annotated but do not ship a py.typed marker.
|
|
127
127
|
module = [
|
|
128
128
|
"amos.*",
|
|
129
|
-
"camina.*",
|
|
130
|
-
"chrisjen.*",
|
|
131
129
|
"inflate64.*",
|
|
132
|
-
"nagata.*",
|
|
133
130
|
"pyarrow.*"]
|
|
134
131
|
follow_untyped_imports = true
|
|
135
132
|
ignore_missing_imports = true
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Accessible tools for crime data.
|
|
2
|
+
|
|
3
|
+
crimepy downloads, combines, munges, analyzes, and plots crime data.
|
|
4
|
+
|
|
5
|
+
Contents:
|
|
6
|
+
Project: an `amos.Project`, for a study of crime data.
|
|
7
|
+
analyze: counts crimes and computes rates, changes, and crosstabs.
|
|
8
|
+
combine: stacks and merges tables of crime data.
|
|
9
|
+
nibrs: tools for the FBI's National Incident-Based Reporting System.
|
|
10
|
+
options: default settings of crimepy.
|
|
11
|
+
plot: figures of crime data.
|
|
12
|
+
techniques: techniques that analyze crime data, on `amos` datasets.
|
|
13
|
+
utilities: small tools that the rest of crimepy shares.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
__version__ = '0.2.2'
|
|
20
|
+
|
|
21
|
+
__author__: str = 'Corey Rayburn Yung'
|
|
22
|
+
|
|
23
|
+
__all__: list[str] = [
|
|
24
|
+
'Project',
|
|
25
|
+
'analyze',
|
|
26
|
+
'combine',
|
|
27
|
+
'nibrs',
|
|
28
|
+
'options',
|
|
29
|
+
'plot',
|
|
30
|
+
'techniques',
|
|
31
|
+
'utilities']
|
|
32
|
+
|
|
33
|
+
# A study is an `amos.Project`, which crimepy offers under the same name.
|
|
34
|
+
from amos import Project
|
|
35
|
+
|
|
36
|
+
# Importing `nibrs`, `plot`, and `techniques` adds their techniques to the
|
|
37
|
+
# `amos` library, so they can be named in settings as soon as crimepy is
|
|
38
|
+
# imported.
|
|
39
|
+
from . import analyze, combine, nibrs, options, plot, techniques, utilities
|
|
@@ -42,6 +42,8 @@ import pandas as pd
|
|
|
42
42
|
|
|
43
43
|
from . import techniques, utilities
|
|
44
44
|
|
|
45
|
+
""" Public Functions """
|
|
46
|
+
|
|
45
47
|
|
|
46
48
|
def count(
|
|
47
49
|
data: amos.Dataset | pd.DataFrame,
|
|
@@ -275,6 +277,9 @@ def coverage(
|
|
|
275
277
|
return _describe(techniques.Coverage(), agencies, by = by, months = months)
|
|
276
278
|
|
|
277
279
|
|
|
280
|
+
""" Private Functions """
|
|
281
|
+
|
|
282
|
+
|
|
278
283
|
def _describe(
|
|
279
284
|
technique: amos.Describer,
|
|
280
285
|
data: amos.Dataset | pd.DataFrame,
|
|
@@ -27,6 +27,8 @@ from collections.abc import Sequence
|
|
|
27
27
|
|
|
28
28
|
from . import nibrs
|
|
29
29
|
|
|
30
|
+
""" Public Functions """
|
|
31
|
+
|
|
30
32
|
|
|
31
33
|
def main(arguments: Sequence[str] | None = None) -> int:
|
|
32
34
|
"""Runs the command line interface.
|
|
@@ -48,8 +50,17 @@ def main(arguments: Sequence[str] | None = None) -> int:
|
|
|
48
50
|
return int(parsed.run(parsed) or 0)
|
|
49
51
|
|
|
50
52
|
|
|
53
|
+
""" Private Functions """
|
|
54
|
+
|
|
55
|
+
|
|
51
56
|
def _parser() -> argparse.ArgumentParser:
|
|
52
|
-
"""Returns the parser of the command line arguments.
|
|
57
|
+
"""Returns the parser of the command line arguments.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
The parser, with a subcommand for each command, each of which sets
|
|
61
|
+
"run" to the function that runs it.
|
|
62
|
+
|
|
63
|
+
"""
|
|
53
64
|
parser = argparse.ArgumentParser(
|
|
54
65
|
prog = 'crimepy',
|
|
55
66
|
description = (
|
|
@@ -111,7 +122,12 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
111
122
|
|
|
112
123
|
|
|
113
124
|
def _add_folder(parser: argparse.ArgumentParser) -> None:
|
|
114
|
-
"""Adds the "--folder" argument to `parser`.
|
|
125
|
+
"""Adds the "--folder" argument to `parser`.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
parser: the parser of a command that uses the data folder.
|
|
129
|
+
|
|
130
|
+
"""
|
|
115
131
|
parser.add_argument(
|
|
116
132
|
'--folder', type = pathlib.Path, default = None,
|
|
117
133
|
help = 'the data folder (by default, the "crime" folder of the data '
|
|
@@ -119,14 +135,27 @@ def _add_folder(parser: argparse.ArgumentParser) -> None:
|
|
|
119
135
|
|
|
120
136
|
|
|
121
137
|
def _add_years(parser: argparse.ArgumentParser) -> None:
|
|
122
|
-
"""Adds the "years" argument to `parser`.
|
|
138
|
+
"""Adds the "years" argument to `parser`.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
parser: the parser of a command that takes years.
|
|
142
|
+
|
|
143
|
+
"""
|
|
123
144
|
parser.add_argument(
|
|
124
145
|
'years', nargs = '+',
|
|
125
146
|
help = 'years or ranges of years, such as 2022 or 2015-2023')
|
|
126
147
|
|
|
127
148
|
|
|
128
149
|
def _convert(arguments: argparse.Namespace) -> int:
|
|
129
|
-
"""Converts master files to parquet files.
|
|
150
|
+
"""Converts master files to parquet files.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
arguments: the parsed arguments of the "convert" command.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
The exit status, 0.
|
|
157
|
+
|
|
158
|
+
"""
|
|
130
159
|
converted = nibrs.convert(
|
|
131
160
|
arguments.years, arguments.folder, tables = arguments.tables,
|
|
132
161
|
overwrite = arguments.overwrite)
|
|
@@ -137,7 +166,15 @@ def _convert(arguments: argparse.Namespace) -> int:
|
|
|
137
166
|
|
|
138
167
|
|
|
139
168
|
def _download(arguments: argparse.Namespace) -> int:
|
|
140
|
-
"""Downloads master files.
|
|
169
|
+
"""Downloads master files.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
arguments: the parsed arguments of the "download" command.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
The exit status, 0.
|
|
176
|
+
|
|
177
|
+
"""
|
|
141
178
|
for path in nibrs.download(
|
|
142
179
|
arguments.years, arguments.folder, overwrite = arguments.overwrite):
|
|
143
180
|
print(path)
|
|
@@ -145,7 +182,16 @@ def _download(arguments: argparse.Namespace) -> int:
|
|
|
145
182
|
|
|
146
183
|
|
|
147
184
|
def _export(arguments: argparse.Namespace) -> int:
|
|
148
|
-
"""Writes a table to a csv or parquet file.
|
|
185
|
+
"""Writes a table to a csv or parquet file.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
arguments: the parsed arguments of the "export" command.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
The exit status: 0 if the table was written, or 2 if the output is
|
|
192
|
+
not a csv or parquet file.
|
|
193
|
+
|
|
194
|
+
"""
|
|
149
195
|
suffix = arguments.output.suffix.lower()
|
|
150
196
|
if suffix not in ('.csv', '.parquet'):
|
|
151
197
|
print('the output must be a .csv or .parquet file', file = sys.stderr)
|
|
@@ -169,7 +215,15 @@ def _export(arguments: argparse.Namespace) -> int:
|
|
|
169
215
|
|
|
170
216
|
|
|
171
217
|
def _project(arguments: argparse.Namespace) -> int:
|
|
172
|
-
"""Runs an amos project from a settings file.
|
|
218
|
+
"""Runs an amos project from a settings file.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
arguments: the parsed arguments of the "project" command.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
The exit status, 0.
|
|
225
|
+
|
|
226
|
+
"""
|
|
173
227
|
import amos # noqa: PLC0415
|
|
174
228
|
|
|
175
229
|
project = amos.Project.create(arguments.settings)
|
|
@@ -180,7 +234,16 @@ def _project(arguments: argparse.Namespace) -> int:
|
|
|
180
234
|
|
|
181
235
|
|
|
182
236
|
def _years(arguments: argparse.Namespace) -> int: # noqa: ARG001
|
|
183
|
-
"""Lists the years of NIBRS master files that the FBI has.
|
|
237
|
+
"""Lists the years of NIBRS master files that the FBI has.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
arguments: the parsed arguments of the "years" command, which are
|
|
241
|
+
not used.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
The exit status, 0.
|
|
245
|
+
|
|
246
|
+
"""
|
|
184
247
|
found = nibrs.available_years()
|
|
185
248
|
print(f'{found[0]}-{found[-1]}')
|
|
186
249
|
return 0
|
|
@@ -6,8 +6,9 @@ and `pandas.merge` does the same to the keys it joins on. These functions
|
|
|
6
6
|
keep the categories, so combined tables stay small and keep their labels.
|
|
7
7
|
|
|
8
8
|
They take `amos` datasets or data frames and return an `amos.Dataset`, which
|
|
9
|
-
keeps the label, task,
|
|
10
|
-
columns are still there.
|
|
9
|
+
keeps the label, task, groups, and kinds (see `amos`'s `set_kinds`) of the
|
|
10
|
+
first dataset given when its columns are still there. A label that is still
|
|
11
|
+
pending (not yet a column) stays pending.
|
|
11
12
|
|
|
12
13
|
Contents:
|
|
13
14
|
merge: joins two tables, keeping their categories.
|
|
@@ -26,6 +27,8 @@ from pandas.api.types import union_categoricals
|
|
|
26
27
|
|
|
27
28
|
from . import utilities
|
|
28
29
|
|
|
30
|
+
""" Public Functions """
|
|
31
|
+
|
|
29
32
|
|
|
30
33
|
def stack(
|
|
31
34
|
tables: Iterable[amos.Dataset | pd.DataFrame],
|
|
@@ -64,7 +67,8 @@ def stack(
|
|
|
64
67
|
if source is not None:
|
|
65
68
|
names = list(labels) if labels is not None else list(range(len(frames)))
|
|
66
69
|
frames = [
|
|
67
|
-
f.assign(**{source: name})
|
|
70
|
+
f.assign(**{source: name})
|
|
71
|
+
for f, name in zip(frames, names, strict = True)]
|
|
68
72
|
columns = list(dict.fromkeys(c for f in frames for c in f.columns))
|
|
69
73
|
categorical = [
|
|
70
74
|
c for c in columns
|
|
@@ -114,7 +118,8 @@ def merge(
|
|
|
114
118
|
keys = [on] if isinstance(on, str) else list(on)
|
|
115
119
|
restore = {}
|
|
116
120
|
items = [left, right]
|
|
117
|
-
left
|
|
121
|
+
left = utilities.to_frame(left).copy()
|
|
122
|
+
right = utilities.to_frame(right).copy()
|
|
118
123
|
for key in keys:
|
|
119
124
|
kinds = (left[key].dtype, right[key].dtype)
|
|
120
125
|
if any(isinstance(k, pd.CategoricalDtype) for k in kinds):
|
|
@@ -122,13 +127,17 @@ def merge(
|
|
|
122
127
|
left[key] = left[key].astype('object')
|
|
123
128
|
right[key] = right[key].astype('object')
|
|
124
129
|
joined = left.merge(
|
|
125
|
-
right, on = keys,
|
|
130
|
+
right, on = keys,
|
|
131
|
+
how = how, suffixes = suffixes, # type: ignore[arg-type]
|
|
126
132
|
validate = validate) # type: ignore[arg-type]
|
|
127
133
|
for key in restore:
|
|
128
134
|
joined[key] = joined[key].astype('category')
|
|
129
135
|
return _dataset(joined, items)
|
|
130
136
|
|
|
131
137
|
|
|
138
|
+
""" Private Functions """
|
|
139
|
+
|
|
140
|
+
|
|
132
141
|
def _dataset(
|
|
133
142
|
data: pd.DataFrame,
|
|
134
143
|
items: Sequence[amos.Dataset | pd.DataFrame]) -> amos.Dataset:
|
|
@@ -139,16 +148,21 @@ def _dataset(
|
|
|
139
148
|
items: the datasets and data frames that were combined.
|
|
140
149
|
|
|
141
150
|
Returns:
|
|
142
|
-
A dataset with the label, task, and
|
|
143
|
-
`items` whose label
|
|
144
|
-
none if no dataset was given).
|
|
151
|
+
A dataset with the label, task, groups, and kinds of the first
|
|
152
|
+
dataset in `items` whose label, groups, and columns with kinds
|
|
153
|
+
are columns of `data` (and with none if no dataset was given). A
|
|
154
|
+
pending label (one that is not yet a column) stays pending.
|
|
145
155
|
|
|
146
156
|
"""
|
|
147
157
|
first = next((i for i in items if isinstance(i, amos.Dataset)), None)
|
|
148
158
|
if first is None:
|
|
149
159
|
return amos.Dataset.create(data)
|
|
150
|
-
|
|
160
|
+
kept = first.label in data.columns or not first.has_label
|
|
161
|
+
label = first.label if kept else None
|
|
151
162
|
groups = [g for g in first.groups if g in data.columns]
|
|
152
|
-
|
|
163
|
+
dataset = amos.Dataset.create(
|
|
153
164
|
data, label = label, task = first.task if label else None,
|
|
154
165
|
seed = first.seed, groups = groups or None)
|
|
166
|
+
dataset.kinds = {
|
|
167
|
+
c: k for c, k in first.kinds.items() if c in data.columns}
|
|
168
|
+
return dataset
|
|
@@ -7,17 +7,23 @@ publishes a master file of every year's reports from 1991 on, which `crimepy`
|
|
|
7
7
|
downloads, parses, and makes into flat tables, each loaded as an
|
|
8
8
|
`amos.Dataset` (whose `data` is a `pandas` data frame):
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
10
|
+
* `agencies`: one row for each agency (its batch header): location,
|
|
11
|
+
population, and months reported.
|
|
12
|
+
* `administrative`: one row for each incident: date, hour, numbers of
|
|
13
|
+
segments, and exceptional clearance.
|
|
14
|
+
* `offenses`: one row for each offense of an incident: offense, location,
|
|
15
|
+
weapons, and bias motivations.
|
|
16
|
+
* `properties`: one row for each property loss of an incident: type of loss,
|
|
17
|
+
description, value, and drugs.
|
|
18
|
+
* `victims`: one row for each victim: offenses, age, sex, race, injuries,
|
|
19
|
+
and relationships to offenders.
|
|
20
|
+
* `offenders`: one row for each offender: age, sex, race, and ethnicity.
|
|
21
|
+
* `arrestees`: one row for each arrestee of an incident: date and type of
|
|
22
|
+
arrest, offense, and person.
|
|
23
|
+
* `group_b_arrests`: one row for each arrestee of a Group "B" offense.
|
|
24
|
+
* `window_clearances`, `window_properties`, and `window_arrestees`: one row
|
|
25
|
+
for each partial report of an incident that began before its agency
|
|
26
|
+
reported to NIBRS.
|
|
21
27
|
|
|
22
28
|
The tables of an incident share "data_year", "ori", and "incident_number",
|
|
23
29
|
and `crimepy.nibrs.tables` joins them into tables for analysis (such as one
|
|
@@ -55,6 +55,9 @@ _BROKEN: tuple[type[Exception], ...] = (
|
|
|
55
55
|
requests.Timeout)
|
|
56
56
|
|
|
57
57
|
|
|
58
|
+
""" Public Functions """
|
|
59
|
+
|
|
60
|
+
|
|
58
61
|
def available_years(*, timeout: float | None = None) -> list[int]:
|
|
59
62
|
"""Returns the years of NIBRS master files that the CDE has.
|
|
60
63
|
|
|
@@ -131,8 +134,7 @@ def download(
|
|
|
131
134
|
if missing:
|
|
132
135
|
message = (
|
|
133
136
|
f'the CDE has NIBRS master files for {first} to {last}, so there '
|
|
134
|
-
f'are none for {missing}'
|
|
135
|
-
)
|
|
137
|
+
f'are none for {missing}')
|
|
136
138
|
raise ValueError(message)
|
|
137
139
|
paths = []
|
|
138
140
|
for year in wanted:
|
|
@@ -174,6 +176,9 @@ def download_documentation(
|
|
|
174
176
|
return path
|
|
175
177
|
|
|
176
178
|
|
|
179
|
+
""" Private Functions """
|
|
180
|
+
|
|
181
|
+
|
|
177
182
|
def _fetch(
|
|
178
183
|
key: str,
|
|
179
184
|
path: pathlib.Path,
|
|
@@ -327,6 +332,7 @@ def _year_range(timeout: float) -> tuple[int, int]:
|
|
|
327
332
|
|
|
328
333
|
Raises:
|
|
329
334
|
requests.HTTPError: if the CDE refuses the request.
|
|
335
|
+
ValueError: if the CDE does not list a NIBRS master file.
|
|
330
336
|
|
|
331
337
|
Returns:
|
|
332
338
|
The first and last years.
|