Coverage for src/crimepy/analyze.py: 100%
29 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-30 18:01 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-30 18:01 +0000
1"""Analyses of crime data that researchers often need.
3Each function applies one of the `amos` techniques of `crimepy.techniques`,
4so the same analysis can be named in a project's settings. The functions
5take an `amos.Dataset` or a data frame with one row for each event (an
6incident, offense, victim, or arrest), such as those of `crimepy.nibrs.load`
7and `crimepy.nibrs.tables.build`:
9* `count`: the number of rows in each group and period, as a new dataset.
10* `trend`: counts over time, one column for each group.
11* `rates`: counts per 100,000 people (or another base).
12* `crime_rates`: the rate of crime in each group, using the population of the
13 agencies that reported for the whole year.
14* `clearance_rates`: the share of incidents cleared in each group.
15* `percent_change`: the change from one period to the next.
16* `crosstab`: counts (or shares) of two variables against each other.
17* `coverage`: how many agencies reported, and how many people they serve.
19Each function but `count` returns a table. Given a dataset, it also stores
20the table in the dataset's `tables`, under the technique's name (such as
21"clearance_rates").
23Contents:
24 clearance_rates: the share of incidents cleared in each group.
25 count: the number of rows in each group and period.
26 coverage: the agencies that reported and the people they serve.
27 crime_rates: crimes per 100,000 people, by group.
28 crosstab: counts or shares of two variables against each other.
29 percent_change: the change from one period to the next.
30 rates: counts per 100,000 people.
31 trend: counts over time, one column for each group.
33"""
35from __future__ import annotations
37from collections.abc import Sequence
38from typing import Any, Literal
40import amos
41import pandas as pd
43from . import techniques, utilities
45""" Public Functions """
48def count(
49 data: amos.Dataset | pd.DataFrame,
50 by: Sequence[str] | str | None = None,
51 *,
52 date: str | None = None,
53 freq: str | None = None,
54 weight: str | None = None,
55 name: str = 'count') -> amos.Dataset:
56 """Returns the number of rows in each group (and period).
58 Applies `crimepy.techniques.CountCrimes` to a copy of the data, so a
59 dataset that is passed keeps its rows.
61 Args:
62 data: one row for each event.
63 by: columns to group by. Defaults to `None`, which counts all rows
64 (in each period, if `freq` is set).
65 date: column of dates that `freq` groups by. Defaults to `None`, in
66 which case it is "incident_date" if `freq` is set.
67 freq: length of each period, as a `pandas` frequency ("YS" for years,
68 "QS" for quarters, "MS" for months, "W" for weeks, "D" for days).
69 Defaults to `None`, which does not group by period.
70 weight: column whose values are summed instead of counting rows.
71 Defaults to `None`.
72 name: name of the column of counts. Defaults to "count".
74 Returns:
75 A dataset with a column for each group (and "period") and the counts,
76 which are its label. Groups with no rows are left out.
78 """
79 return techniques.CountCrimes().apply(
80 utilities.to_dataset(utilities.to_frame(data)), label = name, by = by,
81 date = date, freq = freq, weight = weight, name = name)
84def trend(
85 data: amos.Dataset | pd.DataFrame,
86 by: str | None = None,
87 *,
88 date: str = 'incident_date',
89 freq: str = 'MS',
90 top: int | None = None) -> pd.DataFrame:
91 """Returns counts over time, with one column for each group.
93 Args:
94 data: one row for each event.
95 by: column whose values become columns. Defaults to `None`, which
96 makes one column, "count".
97 date: column of dates. Defaults to "incident_date".
98 freq: length of each period, as a `pandas` frequency. Defaults to
99 "MS" (months).
100 top: keep only the groups with the most rows. Defaults to `None`,
101 which keeps them all.
103 Returns:
104 A table indexed by the start of each period, with a column of counts
105 for each group (0 in periods with none).
107 """
108 return _describe(
109 techniques.CrimeTrend(), data, by = by, date = date, freq = freq,
110 top = top)
113def rates(
114 counts: amos.Dataset | pd.DataFrame,
115 population: amos.Dataset | pd.DataFrame | str,
116 *,
117 on: Sequence[str] | str | None = None,
118 count: str = 'count',
119 per: float = 100_000,
120 name: str = 'rate') -> pd.DataFrame:
121 """Returns counts per `per` people.
123 Args:
124 counts: a table of counts (such as one from `count`).
125 population: a column of `counts` with the population, or a table of
126 populations to join to `counts` on `on`, with a "population"
127 column.
128 on: the columns that join `population` to `counts`. Defaults to
129 `None`, in which case they are the columns that both have (other
130 than "population").
131 count: the column of counts. Defaults to "count".
132 per: the number of people that rates are per. Defaults to 100,000.
133 name: name of the column of rates. Defaults to "rate".
135 Returns:
136 `counts`, with the population (if it was joined) and the rates
137 (missing where the population is 0 or missing).
139 """
140 return _describe(
141 techniques.PopulationRates(), counts, population = population, on = on,
142 count = count, per = per, name = name)
145def crime_rates(
146 events: amos.Dataset | pd.DataFrame,
147 agencies: amos.Dataset | pd.DataFrame | None = None,
148 by: Sequence[str] | str = ('data_year', 'state'),
149 *,
150 months: int = 12,
151 per: float = 100_000,
152 folder: Any = None) -> pd.DataFrame:
153 """Returns the number of events per `per` people in each group.
155 Only agencies that reported in at least `months` months of the year (see
156 "months_active" in `crimepy.nibrs.munge`) count, both for the events and
157 for the population, so that the rate is not lowered by agencies that did
158 not report.
160 Args:
161 events: one row for each event (such as an incident), with
162 "data_year" and "agency_ori" (or "ori").
163 agencies: the agencies, with "data_year", "ori", "population", and
164 "months_active". Defaults to `None`, in which case the agencies
165 of the years (and states) of `events` are loaded.
166 by: columns of the agencies to group by. Defaults to ("data_year",
167 "state").
168 months: the fewest months an agency must have reported in. Defaults
169 to 12.
170 per: the number of people that rates are per. Defaults to 100,000.
171 folder: the data folder to load agencies from. Defaults to `None`, in
172 which case it is `crimepy.utilities.data_folder()`.
174 Returns:
175 A table with a column for each group, "count", "population",
176 "agencies", and "rate".
178 """
179 return _describe(
180 techniques.CrimeRates(), events, agencies = agencies, by = by,
181 months = months, per = per, folder = folder)
184def clearance_rates(
185 incidents: amos.Dataset | pd.DataFrame,
186 by: Sequence[str] | str | None = None,
187 *,
188 cleared: str = 'cleared') -> pd.DataFrame:
189 """Returns the share of incidents that were cleared in each group.
191 Args:
192 incidents: one row for each incident, with a column saying whether
193 it was cleared (such as from `crimepy.nibrs.tables.incidents`).
194 by: columns to group by. Defaults to `None`, which uses all rows.
195 cleared: the column saying whether each incident was cleared.
196 Defaults to "cleared".
198 Returns:
199 A table with a column for each group, "incidents", "cleared", and
200 "clearance_rate" (from 0 to 1).
202 """
203 return _describe(
204 techniques.ClearanceRates(), incidents, by = by, cleared = cleared)
207def percent_change(
208 data: amos.Dataset | pd.DataFrame | pd.Series,
209 periods: int = 1) -> pd.DataFrame | pd.Series:
210 """Returns the percent change from one period to the next.
212 Args:
213 data: counts (or rates) in each period, in order, such as a table
214 from `trend`.
215 periods: the number of periods to compare across (12 compares each
216 month with the same month of the year before). Defaults to 1.
218 Returns:
219 The change, in percent, with the index of `data`.
221 """
222 if isinstance(data, pd.Series):
223 return data.pct_change(periods = periods, fill_method = None) * 100
224 return _describe(techniques.PercentChange(), data, periods = periods)
227def crosstab(
228 data: amos.Dataset | pd.DataFrame,
229 rows: str,
230 columns: str,
231 *,
232 normalize: bool | Literal['all', 'index', 'columns'] = False,
233 margins: bool = False) -> pd.DataFrame:
234 """Returns counts (or shares) of two variables against each other.
236 Args:
237 data: one row for each event.
238 rows: the column whose values become rows.
239 columns: the column whose values become columns.
240 normalize: False for counts, True for shares of all rows, "index"
241 for shares of each row, or "columns" for shares of each column.
242 Defaults to `False`.
243 margins: whether to add totals. Defaults to `False`.
245 Returns:
246 The table.
248 """
249 return _describe(
250 techniques.Crosstab(), data, rows = rows, columns = columns,
251 normalize = normalize, margins = margins)
254def coverage(
255 agencies: amos.Dataset | pd.DataFrame,
256 by: Sequence[str] | str = ('data_year', 'state'),
257 *,
258 months: int = 12) -> pd.DataFrame:
259 """Returns how many agencies reported and how many people they serve.
261 Agencies covered by another agency (whose crimes the other agency
262 reports) are left out, so that people are not counted twice.
264 Args:
265 agencies: the agencies, with "population" and "months_active".
266 by: columns to group by. Defaults to ("data_year", "state").
267 months: the months that an agency must have reported in to count as
268 reporting for the year. Defaults to 12.
270 Returns:
271 A table with a column for each group, "agencies", "reporting" (the
272 agencies that reported in at least `months` months),
273 "population", "population_reporting", and "share_reporting" (the
274 share of the population served by agencies that reported).
276 """
277 return _describe(techniques.Coverage(), agencies, by = by, months = months)
280""" Private Functions """
283def _describe(
284 technique: amos.Describer,
285 data: amos.Dataset | pd.DataFrame,
286 **kwargs: Any) -> pd.DataFrame:
287 """Applies a describer to `data` and returns its table.
289 Args:
290 technique: the describer.
291 data: a dataset (which keeps the table in its `tables`) or a data
292 frame.
293 **kwargs: parameters for the describer.
295 Returns:
296 The table.
298 """
299 dataset = technique.apply(utilities.to_dataset(data), **kwargs)
300 return dataset.tables[technique.name]