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.
Files changed (58) hide show
  1. {crimepy-0.2.0 → crimepy-0.2.2}/CHANGELOG.md +45 -0
  2. {crimepy-0.2.0 → crimepy-0.2.2}/CITATION.cff +2 -2
  3. {crimepy-0.2.0 → crimepy-0.2.2}/PKG-INFO +3 -3
  4. {crimepy-0.2.0 → crimepy-0.2.2}/README.md +1 -1
  5. {crimepy-0.2.0 → crimepy-0.2.2}/docs/advanced.md +37 -5
  6. crimepy-0.2.2/docs/scripts/gen_ref_nav.py +40 -0
  7. {crimepy-0.2.0 → crimepy-0.2.2}/docs/tutorial.md +4 -2
  8. {crimepy-0.2.0 → crimepy-0.2.2}/pyproject.toml +1 -4
  9. crimepy-0.2.2/src/crimepy/__init__.py +39 -0
  10. {crimepy-0.2.0 → crimepy-0.2.2}/src/crimepy/analyze.py +5 -0
  11. {crimepy-0.2.0 → crimepy-0.2.2}/src/crimepy/cli.py +71 -8
  12. {crimepy-0.2.0 → crimepy-0.2.2}/src/crimepy/combine.py +24 -10
  13. {crimepy-0.2.0 → crimepy-0.2.2}/src/crimepy/nibrs/__init__.py +17 -11
  14. {crimepy-0.2.0 → crimepy-0.2.2}/src/crimepy/nibrs/cde.py +8 -2
  15. {crimepy-0.2.0 → crimepy-0.2.2}/src/crimepy/nibrs/codes.py +237 -111
  16. {crimepy-0.2.0 → crimepy-0.2.2}/src/crimepy/nibrs/layouts.py +171 -123
  17. {crimepy-0.2.0 → crimepy-0.2.2}/src/crimepy/nibrs/munge.py +38 -13
  18. {crimepy-0.2.0 → crimepy-0.2.2}/src/crimepy/nibrs/reader.py +122 -27
  19. {crimepy-0.2.0 → crimepy-0.2.2}/src/crimepy/nibrs/store.py +15 -3
  20. {crimepy-0.2.0 → crimepy-0.2.2}/src/crimepy/nibrs/tables.py +55 -11
  21. {crimepy-0.2.0 → crimepy-0.2.2}/src/crimepy/nibrs/techniques.py +80 -45
  22. {crimepy-0.2.0 → crimepy-0.2.2}/src/crimepy/plot.py +27 -1
  23. {crimepy-0.2.0 → crimepy-0.2.2}/src/crimepy/techniques.py +61 -33
  24. {crimepy-0.2.0 → crimepy-0.2.2}/src/crimepy/utilities.py +6 -0
  25. {crimepy-0.2.0 → crimepy-0.2.2}/tests/conftest.py +117 -30
  26. {crimepy-0.2.0 → crimepy-0.2.2}/tests/test_analysis.py +189 -11
  27. {crimepy-0.2.0 → crimepy-0.2.2}/tests/test_main.py +10 -2
  28. {crimepy-0.2.0 → crimepy-0.2.2}/tests/test_nibrs_reader.py +112 -2
  29. {crimepy-0.2.0 → crimepy-0.2.2}/tests/test_nibrs_store.py +292 -45
  30. {crimepy-0.2.0 → crimepy-0.2.2}/tests/test_utilities.py +23 -2
  31. {crimepy-0.2.0 → crimepy-0.2.2}/uv.lock +29 -28
  32. crimepy-0.2.0/docs/scripts/gen_ref_nav.py +0 -38
  33. crimepy-0.2.0/src/crimepy/__init__.py +0 -25
  34. {crimepy-0.2.0 → crimepy-0.2.2}/.cruft.json +0 -0
  35. {crimepy-0.2.0 → crimepy-0.2.2}/.editorconfig +0 -0
  36. {crimepy-0.2.0 → crimepy-0.2.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  37. {crimepy-0.2.0 → crimepy-0.2.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  38. {crimepy-0.2.0 → crimepy-0.2.2}/.github/dependabot.yml +0 -0
  39. {crimepy-0.2.0 → crimepy-0.2.2}/.github/workflows/ci.yml +0 -0
  40. {crimepy-0.2.0 → crimepy-0.2.2}/.github/workflows/publish.yml +0 -0
  41. {crimepy-0.2.0 → crimepy-0.2.2}/.github/workflows/validate_codecov.yml +0 -0
  42. {crimepy-0.2.0 → crimepy-0.2.2}/.gitignore +0 -0
  43. {crimepy-0.2.0 → crimepy-0.2.2}/.pre-commit-config.yaml +0 -0
  44. {crimepy-0.2.0 → crimepy-0.2.2}/.python-version +0 -0
  45. {crimepy-0.2.0 → crimepy-0.2.2}/CODE_OF_CONDUCT.md +0 -0
  46. {crimepy-0.2.0 → crimepy-0.2.2}/CONTRIBUTING.md +0 -0
  47. {crimepy-0.2.0 → crimepy-0.2.2}/LICENSE +0 -0
  48. {crimepy-0.2.0 → crimepy-0.2.2}/codecov.yml +0 -0
  49. {crimepy-0.2.0 → crimepy-0.2.2}/docs/changelog.md +0 -0
  50. {crimepy-0.2.0 → crimepy-0.2.2}/docs/code_of_conduct.md +0 -0
  51. {crimepy-0.2.0 → crimepy-0.2.2}/docs/contributing.md +0 -0
  52. {crimepy-0.2.0 → crimepy-0.2.2}/docs/css/material.css +0 -0
  53. {crimepy-0.2.0 → crimepy-0.2.2}/docs/css/mkdocstrings.css +0 -0
  54. {crimepy-0.2.0 → crimepy-0.2.2}/docs/index.md +0 -0
  55. {crimepy-0.2.0 → crimepy-0.2.2}/docs/license.md +0 -0
  56. {crimepy-0.2.0 → crimepy-0.2.2}/docs/recipes.md +0 -0
  57. {crimepy-0.2.0 → crimepy-0.2.2}/mkdocs.yml +0 -0
  58. {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.0
16
- date-released: 2026-10-30
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.0
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.0
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.0},
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.0},
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 and
234
- groups of the first dataset given, if their columns are still there.
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 metric,
238
- or a plot such as `shap_beeswarm`) can be applied to it directly, as in the
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().apply(model_data)
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.0",
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, and groups of the first dataset given when its
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}) for f, name in zip(frames, names, strict = True)]
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, right = utilities.to_frame(left).copy(), utilities.to_frame(right).copy()
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, how = how, suffixes = suffixes, # type: ignore[arg-type]
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 groups of the first dataset in
143
- `items` whose label and groups are columns of `data` (and with
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
- label = first.label if first.label in data.columns else None
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
- return amos.Dataset.create(
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
- | Table | One row for each |
11
- | --- | --- |
12
- | `agencies` | agency (its batch header): location, population, and months reported |
13
- | `administrative` | incident: date, hour, numbers of segments, and exceptional clearance |
14
- | `offenses` | offense of an incident: offense, location, weapons, and bias motivations |
15
- | `properties` | property loss of an incident: type of loss, description, value, and drugs |
16
- | `victims` | victim: offenses, age, sex, race, injuries, and relationships to offenders |
17
- | `offenders` | offender: age, sex, race, and ethnicity |
18
- | `arrestees` | arrestee of an incident: date and type of arrest, offense, and person |
19
- | `group_b_arrests` | arrestee of a Group "B" offense |
20
- | `window_clearances`, `window_properties`, `window_arrestees` | partial report of an incident that began before its agency reported to NIBRS |
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.