diff --git a/spotlight-client/src/contentModels/Metric.ts b/spotlight-client/src/contentModels/Metric.ts index 06581e07..26c90e32 100644 --- a/spotlight-client/src/contentModels/Metric.ts +++ b/spotlight-client/src/contentModels/Metric.ts @@ -17,21 +17,40 @@ import assertNever from "assert-never"; import { MetricTypeIdList, TenantContent, TenantId } from "../contentApi/types"; -import fetchMetrics, { RawMetricData } from "../fetchMetrics"; -import * as transforms from "./transforms"; import { - AnyRecord, - CollectionMap, DemographicsByCategoryRecord, + fetchMetrics, HistoricalPopulationBreakdownRecord, - MetricMapping, + parolePopulationCurrent, + parolePopulationHistorical, + paroleProgramParticipationCurrent, + paroleRevocationReasons, + paroleSuccessRateDemographics, + paroleSuccessRateMonthly, PopulationBreakdownByLocationRecord, + prisonAdmissionReasons, + prisonPopulationCurrent, + prisonPopulationHistorical, + prisonReleaseTypes, + prisonStayLengths, + probationPopulationCurrent, + probationPopulationHistorical, + probationProgramParticipationCurrent, + probationRevocationReasons, + probationSuccessRateDemographics, + probationSuccessRateMonthly, ProgramParticipationCurrentRecord, + RawMetricData, + recidivismRateAllFollowup, + recidivismRateConventionalFollowup, RecidivismRateRecord, + sentencePopulationCurrent, SentenceTypeByLocationRecord, + sentenceTypesCurrent, SupervisionSuccessRateDemographicsRecord, SupervisionSuccessRateMonthlyRecord, -} from "./types"; +} from "../metricsApi"; +import { AnyRecord, CollectionMap, MetricMapping } from "./types"; type DataTransformer = (rawData: RawMetricData) => RecordFormat[]; @@ -149,7 +168,7 @@ export function createMetricMapping({ >({ ...metadata, tenantId, - dataTransformer: transforms.sentencePopulationCurrent, + dataTransformer: sentencePopulationCurrent, sourceFileName: "sentence_type_by_district_by_demographics", }); break; @@ -157,7 +176,7 @@ export function createMetricMapping({ metricMapping[metricType] = new Metric({ ...metadata, tenantId, - dataTransformer: transforms.sentenceTypesCurrent, + dataTransformer: sentenceTypesCurrent, sourceFileName: "sentence_type_by_district_by_demographics", }); break; @@ -167,7 +186,7 @@ export function createMetricMapping({ >({ ...metadata, tenantId, - dataTransformer: transforms.prisonPopulationCurrent, + dataTransformer: prisonPopulationCurrent, sourceFileName: "incarceration_population_by_facility_by_demographics", }); @@ -178,7 +197,7 @@ export function createMetricMapping({ >({ ...metadata, tenantId, - dataTransformer: transforms.probationPopulationCurrent, + dataTransformer: probationPopulationCurrent, sourceFileName: "supervision_population_by_district_by_demographics", }); break; @@ -188,7 +207,7 @@ export function createMetricMapping({ >({ ...metadata, tenantId, - dataTransformer: transforms.parolePopulationCurrent, + dataTransformer: parolePopulationCurrent, sourceFileName: "supervision_population_by_district_by_demographics", }); break; @@ -198,7 +217,7 @@ export function createMetricMapping({ >({ ...metadata, tenantId, - dataTransformer: transforms.prisonPopulationHistorical, + dataTransformer: prisonPopulationHistorical, sourceFileName: "incarceration_population_by_month_by_demographics", }); break; @@ -208,7 +227,7 @@ export function createMetricMapping({ >({ ...metadata, tenantId, - dataTransformer: transforms.probationPopulationHistorical, + dataTransformer: probationPopulationHistorical, sourceFileName: "supervision_population_by_month_by_demographics", }); break; @@ -218,7 +237,7 @@ export function createMetricMapping({ >({ ...metadata, tenantId, - dataTransformer: transforms.parolePopulationHistorical, + dataTransformer: parolePopulationHistorical, sourceFileName: "supervision_population_by_month_by_demographics", }); break; @@ -228,7 +247,7 @@ export function createMetricMapping({ >({ ...metadata, tenantId, - dataTransformer: transforms.probationProgramParticipationCurrent, + dataTransformer: probationProgramParticipationCurrent, sourceFileName: "active_program_participation_by_region", }); break; @@ -238,7 +257,7 @@ export function createMetricMapping({ >({ ...metadata, tenantId, - dataTransformer: transforms.paroleProgramParticipationCurrent, + dataTransformer: paroleProgramParticipationCurrent, sourceFileName: "active_program_participation_by_region", }); break; @@ -248,7 +267,7 @@ export function createMetricMapping({ >({ ...metadata, tenantId, - dataTransformer: transforms.probationSuccessRateMonthly, + dataTransformer: probationSuccessRateMonthly, sourceFileName: "supervision_success_by_month", }); break; @@ -258,7 +277,7 @@ export function createMetricMapping({ >({ ...metadata, tenantId, - dataTransformer: transforms.paroleSuccessRateMonthly, + dataTransformer: paroleSuccessRateMonthly, sourceFileName: "supervision_success_by_month", }); break; @@ -268,7 +287,7 @@ export function createMetricMapping({ >({ ...metadata, tenantId, - dataTransformer: transforms.probationSuccessRateDemographics, + dataTransformer: probationSuccessRateDemographics, sourceFileName: "supervision_success_by_period_by_demographics", }); break; @@ -278,7 +297,7 @@ export function createMetricMapping({ >({ ...metadata, tenantId, - dataTransformer: transforms.paroleSuccessRateDemographics, + dataTransformer: paroleSuccessRateDemographics, sourceFileName: "supervision_success_by_period_by_demographics", }); break; @@ -286,7 +305,7 @@ export function createMetricMapping({ metricMapping[metricType] = new Metric({ ...metadata, tenantId, - dataTransformer: transforms.probationRevocationReasons, + dataTransformer: probationRevocationReasons, sourceFileName: "supervision_revocations_by_period_by_type_by_demographics", }); @@ -295,7 +314,7 @@ export function createMetricMapping({ metricMapping[metricType] = new Metric({ ...metadata, tenantId, - dataTransformer: transforms.paroleRevocationReasons, + dataTransformer: paroleRevocationReasons, sourceFileName: "supervision_revocations_by_period_by_type_by_demographics", }); @@ -304,7 +323,7 @@ export function createMetricMapping({ metricMapping[metricType] = new Metric({ ...metadata, tenantId, - dataTransformer: transforms.prisonAdmissionReasons, + dataTransformer: prisonAdmissionReasons, sourceFileName: "incarceration_population_by_admission_reason", }); break; @@ -312,7 +331,7 @@ export function createMetricMapping({ metricMapping[metricType] = new Metric({ ...metadata, tenantId, - dataTransformer: transforms.prisonReleaseTypes, + dataTransformer: prisonReleaseTypes, sourceFileName: "incarceration_releases_by_type_by_period", }); break; @@ -320,7 +339,7 @@ export function createMetricMapping({ metricMapping[metricType] = new Metric({ ...metadata, tenantId, - dataTransformer: transforms.recidivismRateAllFollowup, + dataTransformer: recidivismRateAllFollowup, sourceFileName: "recidivism_rates_by_cohort_by_year", }); break; @@ -328,7 +347,7 @@ export function createMetricMapping({ metricMapping[metricType] = new Metric({ ...metadata, tenantId, - dataTransformer: transforms.recidivismRateConventionalFollowup, + dataTransformer: recidivismRateConventionalFollowup, sourceFileName: "recidivism_rates_by_cohort_by_year", }); break; @@ -336,7 +355,7 @@ export function createMetricMapping({ metricMapping[metricType] = new Metric({ ...metadata, tenantId, - dataTransformer: transforms.prisonStayLengths, + dataTransformer: prisonStayLengths, sourceFileName: "incarceration_lengths_by_demographics", }); break; diff --git a/spotlight-client/src/contentModels/transforms.ts b/spotlight-client/src/contentModels/transforms.ts deleted file mode 100644 index 708106a7..00000000 --- a/spotlight-client/src/contentModels/transforms.ts +++ /dev/null @@ -1,338 +0,0 @@ -// Recidiviz - a data platform for criminal justice reform -// Copyright (C) 2020 Recidiviz, Inc. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// ============================================================================= - -/** - * Functions for transforming raw metrics API response data - * into the data types defined for their corresponding Metric objects - * @module - */ - -import { ValuesType } from "utility-types"; -import { RawMetricData } from "../fetchMetrics"; -import { - AgeIdentifier, - DemographicsByCategoryRecord, - GenderIdentifier, - HistoricalPopulationBreakdownRecord, - PopulationBreakdownByLocationRecord, - ProgramParticipationCurrentRecord, - RaceIdentifier, - RecidivismRateRecord, - SentenceTypeByLocationRecord, - SupervisionSuccessRateDemographicsRecord, - SupervisionSuccessRateMonthlyRecord, -} from "./types"; - -function recordIsParole(record: ValuesType) { - return record.supervision_type === "PAROLE"; -} - -function recordIsProbation(record: ValuesType) { - return record.supervision_type === "PROBATION"; -} - -function extractDemographicFields(record: ValuesType) { - return { - // we are trusting the API to return valid strings here, not validating them - raceOrEthnicity: record.race_or_ethnicity as RaceIdentifier, - gender: record.gender as GenderIdentifier, - ageBucket: record.age_bucket as AgeIdentifier, - }; -} - -export function sentencePopulationCurrent( - rawRecords: RawMetricData -): PopulationBreakdownByLocationRecord[] { - return rawRecords.map((record) => { - return { - locality: record.district, - population: Number(record.total_population_count), - ...extractDemographicFields(record), - }; - }); -} - -export function prisonPopulationCurrent( - rawRecords: RawMetricData -): PopulationBreakdownByLocationRecord[] { - return rawRecords.map((record) => { - return { - locality: record.facility, - population: Number(record.total_population), - ...extractDemographicFields(record), - }; - }); -} - -function createSupervisionPopulationRecord(record: ValuesType) { - return { - locality: record.district, - population: Number(record.total_supervision_count), - ...extractDemographicFields(record), - }; -} - -export function probationPopulationCurrent( - rawRecords: RawMetricData -): PopulationBreakdownByLocationRecord[] { - return rawRecords - .filter(recordIsProbation) - .map(createSupervisionPopulationRecord); -} - -export function parolePopulationCurrent( - rawRecords: RawMetricData -): PopulationBreakdownByLocationRecord[] { - return rawRecords - .filter(recordIsParole) - .map(createSupervisionPopulationRecord); -} - -function createHistoricalPopulationRecord(record: ValuesType) { - return { - date: record.population_date, - count: Number(record.population_count), - ...extractDemographicFields(record), - }; -} - -export function prisonPopulationHistorical( - rawRecords: RawMetricData -): HistoricalPopulationBreakdownRecord[] { - return rawRecords.map(createHistoricalPopulationRecord); -} - -export function probationPopulationHistorical( - rawRecords: RawMetricData -): HistoricalPopulationBreakdownRecord[] { - return rawRecords - .filter(recordIsProbation) - .map(createHistoricalPopulationRecord); -} - -export function parolePopulationHistorical( - rawRecords: RawMetricData -): HistoricalPopulationBreakdownRecord[] { - return rawRecords - .filter(recordIsParole) - .map(createHistoricalPopulationRecord); -} - -export function sentenceTypesCurrent( - rawRecords: RawMetricData -): SentenceTypeByLocationRecord[] { - return rawRecords.map((record) => { - return { - dualSentenceCount: Number(record.dual_sentence_count), - incarcerationCount: Number(record.incarceration_count), - locality: record.district, - probationCount: Number(record.probation_count), - ...extractDemographicFields(record), - }; - }); -} - -function createProgramParticipationRecord(record: ValuesType) { - return { - count: Number(record.participation_count), - locality: record.region_id, - }; -} - -export function probationProgramParticipationCurrent( - rawRecords: RawMetricData -): ProgramParticipationCurrentRecord[] { - return rawRecords - .filter(recordIsProbation) - .map(createProgramParticipationRecord); -} - -export function paroleProgramParticipationCurrent( - rawRecords: RawMetricData -): ProgramParticipationCurrentRecord[] { - return rawRecords - .filter(recordIsParole) - .map(createProgramParticipationRecord); -} - -function createSupervisionSuccessRateMonthlyRecord( - record: ValuesType -) { - return { - locality: record.district, - year: Number(record.projected_year), - month: Number(record.projected_month), - rateNumerator: Number(record.successful_termination_count), - rateDenominator: Number(record.projected_completion_count), - rate: Number(record.success_rate), - }; -} - -export function probationSuccessRateMonthly( - rawRecords: RawMetricData -): SupervisionSuccessRateMonthlyRecord[] { - return rawRecords - .filter(recordIsProbation) - .map(createSupervisionSuccessRateMonthlyRecord); -} - -export function paroleSuccessRateMonthly( - rawRecords: RawMetricData -): SupervisionSuccessRateMonthlyRecord[] { - return rawRecords - .filter(recordIsParole) - .map(createSupervisionSuccessRateMonthlyRecord); -} - -function createSupervisionSuccessRateDemographicRecord( - record: ValuesType -) { - return { - rate: Number(record.success_rate), - rateDenominator: Number(record.projected_completion_count), - rateNumerator: Number(record.successful_termination_count), - locality: record.district, - ...extractDemographicFields(record), - }; -} - -export function probationSuccessRateDemographics( - rawRecords: RawMetricData -): SupervisionSuccessRateDemographicsRecord[] { - return rawRecords - .filter(recordIsProbation) - .map(createSupervisionSuccessRateDemographicRecord); -} - -export function paroleSuccessRateDemographics( - rawRecords: RawMetricData -): SupervisionSuccessRateDemographicsRecord[] { - return rawRecords - .filter(recordIsParole) - .map(createSupervisionSuccessRateDemographicRecord); -} - -function getCategoryTransposeFunction( - fields: { fieldName: string; categoryLabel: string }[] -) { - return (records: RawMetricData) => { - // these come in as wide records that we need to transpose to long - const recordsByCategory = records.map((record) => { - return fields.map(({ fieldName, categoryLabel }) => { - return { - category: categoryLabel, - count: Number(record[fieldName]), - ...extractDemographicFields(record), - }; - }); - }); - - return recordsByCategory.flat(); - }; -} - -const revocationReasonFields = [ - { categoryLabel: "abscond", fieldName: "absconsion_count" }, - { categoryLabel: "offend", fieldName: "new_crime_count" }, - { categoryLabel: "technical", fieldName: "technical_count" }, - { categoryLabel: "unknown", fieldName: "unknown_count" }, -]; - -export function probationRevocationReasons( - rawRecords: RawMetricData -): DemographicsByCategoryRecord[] { - return getCategoryTransposeFunction(revocationReasonFields)( - rawRecords.filter(recordIsProbation) - ); -} - -export function paroleRevocationReasons( - rawRecords: RawMetricData -): DemographicsByCategoryRecord[] { - return getCategoryTransposeFunction(revocationReasonFields)( - rawRecords.filter(recordIsParole) - ); -} - -const prisonAdmissionFields = [ - { categoryLabel: "newAdmission", fieldName: "new_admission_count" }, - { categoryLabel: "paroleRevoked", fieldName: "parole_revocation_count" }, - { - categoryLabel: "probationRevoked", - fieldName: "probation_revocation_count", - }, - { categoryLabel: "other", fieldName: "other_count" }, -]; - -export function prisonAdmissionReasons( - rawRecords: RawMetricData -): DemographicsByCategoryRecord[] { - return getCategoryTransposeFunction(prisonAdmissionFields)(rawRecords); -} - -const prisonReleaseFields = [ - { categoryLabel: "transfer", fieldName: "external_transfer_count" }, - { categoryLabel: "completion", fieldName: "sentence_completion_count" }, - { categoryLabel: "parole", fieldName: "parole_count" }, - { categoryLabel: "probation", fieldName: "probation_count" }, - { categoryLabel: "death", fieldName: "death_count" }, -]; - -export function prisonReleaseTypes( - rawRecords: RawMetricData -): DemographicsByCategoryRecord[] { - return getCategoryTransposeFunction(prisonReleaseFields)(rawRecords); -} - -export function recidivismRateAllFollowup( - rawRecords: RawMetricData -): RecidivismRateRecord[] { - return rawRecords.map((record) => { - return { - followupYears: Number(record.followup_years), - rateNumerator: Number(record.recidivated_releases), - rate: Number(record.recidivism_rate), - rateDenominator: Number(record.releases), - releaseCohort: Number(record.release_cohort), - ...extractDemographicFields(record), - }; - }); -} - -export function recidivismRateConventionalFollowup( - rawRecords: RawMetricData -): RecidivismRateRecord[] { - return recidivismRateAllFollowup(rawRecords).filter(({ followupYears }) => - [1, 3, 5].includes(followupYears) - ); -} - -const prisonStayLengthFields = [ - { categoryLabel: "lessThanOne", fieldName: "years_0_1" }, - { categoryLabel: "oneTwo", fieldName: "years_1_2" }, - { categoryLabel: "twoThree", fieldName: "years_2_3" }, - { categoryLabel: "threeFive", fieldName: "years_3_5" }, - { categoryLabel: "fiveTen", fieldName: "years_5_10" }, - { categoryLabel: "tenTwenty", fieldName: "years_10_20" }, - { categoryLabel: "moreThanTwenty", fieldName: "years_20_plus" }, -]; - -export function prisonStayLengths( - rawRecords: RawMetricData -): DemographicsByCategoryRecord[] { - return getCategoryTransposeFunction(prisonStayLengthFields)(rawRecords); -} diff --git a/spotlight-client/src/contentModels/types.ts b/spotlight-client/src/contentModels/types.ts index eb39936d..e367461d 100644 --- a/spotlight-client/src/contentModels/types.ts +++ b/spotlight-client/src/contentModels/types.ts @@ -16,6 +16,16 @@ // ============================================================================= import { CollectionTypeId } from "../contentApi/types"; +import { + DemographicsByCategoryRecord, + HistoricalPopulationBreakdownRecord, + PopulationBreakdownByLocationRecord, + ProgramParticipationCurrentRecord, + RecidivismRateRecord, + SentenceTypeByLocationRecord, + SupervisionSuccessRateMonthlyRecord, + SupervisionSuccessRateDemographicsRecord, +} from "../metricsApi"; import type Collection from "./Collection"; import type Metric from "./Metric"; @@ -29,84 +39,6 @@ export type CollectionMap = Map; // Metric types // ======================================= -type TotalIdentifier = "ALL"; - -export type RaceIdentifier = - | TotalIdentifier - | "AMERICAN_INDIAN_ALASKAN_NATIVE" - | "ASIAN" - | "BLACK" - | "HISPANIC" - | "NATIVE_HAWAIIAN_PACIFIC_ISLANDER" - | "WHITE" - | "OTHER"; -export type GenderIdentifier = TotalIdentifier | "FEMALE" | "MALE"; -export type AgeIdentifier = - | TotalIdentifier - | "<25" - | "25-29" - | "30-34" - | "35-39" - | "40<"; - -type DemographicFields = { - raceOrEthnicity: RaceIdentifier; - gender: GenderIdentifier; - ageBucket: AgeIdentifier; -}; - -type LocalityFields = { - locality: string; -}; - -type RateFields = { - rateDenominator: number; - rateNumerator: number; - rate: number; -}; - -export type PopulationBreakdownByLocationRecord = DemographicFields & - LocalityFields & { - population: number; - }; - -export type SentenceTypeByLocationRecord = DemographicFields & - LocalityFields & { - dualSentenceCount: number; - incarcerationCount: number; - probationCount: number; - }; - -export type HistoricalPopulationBreakdownRecord = DemographicFields & { - date: string; - count: number; -}; - -export type ProgramParticipationCurrentRecord = LocalityFields & { - count: number; -}; - -export type SupervisionSuccessRateDemographicsRecord = DemographicFields & - LocalityFields & - RateFields; - -export type SupervisionSuccessRateMonthlyRecord = LocalityFields & - RateFields & { - month: number; - year: number; - }; - -export type DemographicsByCategoryRecord = DemographicFields & { - category: string; - count: number; -}; - -export type RecidivismRateRecord = DemographicFields & - RateFields & { - releaseCohort: number; - followupYears: number; - }; - export type AnyRecord = | DemographicsByCategoryRecord | HistoricalPopulationBreakdownRecord diff --git a/spotlight-client/src/metricsApi/DemographicsByCategoryRecord.ts b/spotlight-client/src/metricsApi/DemographicsByCategoryRecord.ts new file mode 100644 index 00000000..3f424864 --- /dev/null +++ b/spotlight-client/src/metricsApi/DemographicsByCategoryRecord.ts @@ -0,0 +1,117 @@ +// Recidiviz - a data platform for criminal justice reform +// Copyright (C) 2020 Recidiviz, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// ============================================================================= + +import { RawMetricData } from "./fetchMetrics"; +import { + DemographicFields, + extractDemographicFields, + recordIsProbation, + recordIsParole, +} from "./utils"; + +export type DemographicsByCategoryRecord = DemographicFields & { + category: string; + count: number; +}; + +function getCategoryTransposeFunction( + fields: { fieldName: string; categoryLabel: string }[] +) { + return (records: RawMetricData) => { + // these come in as wide records that we need to transpose to long + const recordsByCategory = records.map((record) => { + return fields.map(({ fieldName, categoryLabel }) => { + return { + category: categoryLabel, + count: Number(record[fieldName]), + ...extractDemographicFields(record), + }; + }); + }); + + return recordsByCategory.flat(); + }; +} + +const revocationReasonFields = [ + { categoryLabel: "abscond", fieldName: "absconsion_count" }, + { categoryLabel: "offend", fieldName: "new_crime_count" }, + { categoryLabel: "technical", fieldName: "technical_count" }, + { categoryLabel: "unknown", fieldName: "unknown_count" }, +]; + +export function probationRevocationReasons( + rawRecords: RawMetricData +): DemographicsByCategoryRecord[] { + return getCategoryTransposeFunction(revocationReasonFields)( + rawRecords.filter(recordIsProbation) + ); +} + +export function paroleRevocationReasons( + rawRecords: RawMetricData +): DemographicsByCategoryRecord[] { + return getCategoryTransposeFunction(revocationReasonFields)( + rawRecords.filter(recordIsParole) + ); +} + +const prisonAdmissionFields = [ + { categoryLabel: "newAdmission", fieldName: "new_admission_count" }, + { categoryLabel: "paroleRevoked", fieldName: "parole_revocation_count" }, + { + categoryLabel: "probationRevoked", + fieldName: "probation_revocation_count", + }, + { categoryLabel: "other", fieldName: "other_count" }, +]; + +export function prisonAdmissionReasons( + rawRecords: RawMetricData +): DemographicsByCategoryRecord[] { + return getCategoryTransposeFunction(prisonAdmissionFields)(rawRecords); +} + +const prisonReleaseFields = [ + { categoryLabel: "transfer", fieldName: "external_transfer_count" }, + { categoryLabel: "completion", fieldName: "sentence_completion_count" }, + { categoryLabel: "parole", fieldName: "parole_count" }, + { categoryLabel: "probation", fieldName: "probation_count" }, + { categoryLabel: "death", fieldName: "death_count" }, +]; + +export function prisonReleaseTypes( + rawRecords: RawMetricData +): DemographicsByCategoryRecord[] { + return getCategoryTransposeFunction(prisonReleaseFields)(rawRecords); +} + +const prisonStayLengthFields = [ + { categoryLabel: "lessThanOne", fieldName: "years_0_1" }, + { categoryLabel: "oneTwo", fieldName: "years_1_2" }, + { categoryLabel: "twoThree", fieldName: "years_2_3" }, + { categoryLabel: "threeFive", fieldName: "years_3_5" }, + { categoryLabel: "fiveTen", fieldName: "years_5_10" }, + { categoryLabel: "tenTwenty", fieldName: "years_10_20" }, + { categoryLabel: "moreThanTwenty", fieldName: "years_20_plus" }, +]; + +export function prisonStayLengths( + rawRecords: RawMetricData +): DemographicsByCategoryRecord[] { + return getCategoryTransposeFunction(prisonStayLengthFields)(rawRecords); +} diff --git a/spotlight-client/src/metricsApi/HistoricalPopulationBreakdownRecord.ts b/spotlight-client/src/metricsApi/HistoricalPopulationBreakdownRecord.ts new file mode 100644 index 00000000..4296e968 --- /dev/null +++ b/spotlight-client/src/metricsApi/HistoricalPopulationBreakdownRecord.ts @@ -0,0 +1,60 @@ +// Recidiviz - a data platform for criminal justice reform +// Copyright (C) 2020 Recidiviz, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// ============================================================================= + +import { ValuesType } from "utility-types"; +import { RawMetricData } from "./fetchMetrics"; +import { + DemographicFields, + extractDemographicFields, + recordIsParole, + recordIsProbation, +} from "./utils"; + +export type HistoricalPopulationBreakdownRecord = DemographicFields & { + date: string; + count: number; +}; + +function createHistoricalPopulationRecord(record: ValuesType) { + return { + date: record.population_date, + count: Number(record.population_count), + ...extractDemographicFields(record), + }; +} + +export function prisonPopulationHistorical( + rawRecords: RawMetricData +): HistoricalPopulationBreakdownRecord[] { + return rawRecords.map(createHistoricalPopulationRecord); +} + +export function probationPopulationHistorical( + rawRecords: RawMetricData +): HistoricalPopulationBreakdownRecord[] { + return rawRecords + .filter(recordIsProbation) + .map(createHistoricalPopulationRecord); +} + +export function parolePopulationHistorical( + rawRecords: RawMetricData +): HistoricalPopulationBreakdownRecord[] { + return rawRecords + .filter(recordIsParole) + .map(createHistoricalPopulationRecord); +} diff --git a/spotlight-client/src/metricsApi/PopulationBreakdownByLocationRecord.ts b/spotlight-client/src/metricsApi/PopulationBreakdownByLocationRecord.ts new file mode 100644 index 00000000..34a7f13c --- /dev/null +++ b/spotlight-client/src/metricsApi/PopulationBreakdownByLocationRecord.ts @@ -0,0 +1,79 @@ +// Recidiviz - a data platform for criminal justice reform +// Copyright (C) 2020 Recidiviz, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// ============================================================================= + +import { ValuesType } from "utility-types"; +import { RawMetricData } from "./fetchMetrics"; +import { + DemographicFields, + extractDemographicFields, + LocalityFields, + recordIsParole, + recordIsProbation, +} from "./utils"; + +export type PopulationBreakdownByLocationRecord = DemographicFields & + LocalityFields & { + population: number; + }; + +export function sentencePopulationCurrent( + rawRecords: RawMetricData +): PopulationBreakdownByLocationRecord[] { + return rawRecords.map((record) => { + return { + locality: record.district, + population: Number(record.total_population_count), + ...extractDemographicFields(record), + }; + }); +} + +export function prisonPopulationCurrent( + rawRecords: RawMetricData +): PopulationBreakdownByLocationRecord[] { + return rawRecords.map((record) => { + return { + locality: record.facility, + population: Number(record.total_population), + ...extractDemographicFields(record), + }; + }); +} + +function createSupervisionPopulationRecord(record: ValuesType) { + return { + locality: record.district, + population: Number(record.total_supervision_count), + ...extractDemographicFields(record), + }; +} + +export function probationPopulationCurrent( + rawRecords: RawMetricData +): PopulationBreakdownByLocationRecord[] { + return rawRecords + .filter(recordIsProbation) + .map(createSupervisionPopulationRecord); +} + +export function parolePopulationCurrent( + rawRecords: RawMetricData +): PopulationBreakdownByLocationRecord[] { + return rawRecords + .filter(recordIsParole) + .map(createSupervisionPopulationRecord); +} diff --git a/spotlight-client/src/metricsApi/ProgramParticipationCurrentRecord.ts b/spotlight-client/src/metricsApi/ProgramParticipationCurrentRecord.ts new file mode 100644 index 00000000..c2749cd4 --- /dev/null +++ b/spotlight-client/src/metricsApi/ProgramParticipationCurrentRecord.ts @@ -0,0 +1,47 @@ +// Recidiviz - a data platform for criminal justice reform +// Copyright (C) 2020 Recidiviz, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// ============================================================================= + +import { ValuesType } from "utility-types"; +import { RawMetricData } from "./fetchMetrics"; +import { LocalityFields, recordIsParole, recordIsProbation } from "./utils"; + +export type ProgramParticipationCurrentRecord = LocalityFields & { + count: number; +}; + +function createProgramParticipationRecord(record: ValuesType) { + return { + count: Number(record.participation_count), + locality: record.region_id, + }; +} + +export function probationProgramParticipationCurrent( + rawRecords: RawMetricData +): ProgramParticipationCurrentRecord[] { + return rawRecords + .filter(recordIsProbation) + .map(createProgramParticipationRecord); +} + +export function paroleProgramParticipationCurrent( + rawRecords: RawMetricData +): ProgramParticipationCurrentRecord[] { + return rawRecords + .filter(recordIsParole) + .map(createProgramParticipationRecord); +} diff --git a/spotlight-client/src/metricsApi/RecidivismRateRecord.ts b/spotlight-client/src/metricsApi/RecidivismRateRecord.ts new file mode 100644 index 00000000..decd1824 --- /dev/null +++ b/spotlight-client/src/metricsApi/RecidivismRateRecord.ts @@ -0,0 +1,52 @@ +// Recidiviz - a data platform for criminal justice reform +// Copyright (C) 2020 Recidiviz, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// ============================================================================= + +import { RawMetricData } from "./fetchMetrics"; +import { + DemographicFields, + extractDemographicFields, + RateFields, +} from "./utils"; + +export type RecidivismRateRecord = DemographicFields & + RateFields & { + releaseCohort: number; + followupYears: number; + }; + +export function recidivismRateAllFollowup( + rawRecords: RawMetricData +): RecidivismRateRecord[] { + return rawRecords.map((record) => { + return { + followupYears: Number(record.followup_years), + rateNumerator: Number(record.recidivated_releases), + rate: Number(record.recidivism_rate), + rateDenominator: Number(record.releases), + releaseCohort: Number(record.release_cohort), + ...extractDemographicFields(record), + }; + }); +} + +export function recidivismRateConventionalFollowup( + rawRecords: RawMetricData +): RecidivismRateRecord[] { + return recidivismRateAllFollowup(rawRecords).filter(({ followupYears }) => + [1, 3, 5].includes(followupYears) + ); +} diff --git a/spotlight-client/src/metricsApi/SentenceTypeByLocationRecord.ts b/spotlight-client/src/metricsApi/SentenceTypeByLocationRecord.ts new file mode 100644 index 00000000..ec52c25a --- /dev/null +++ b/spotlight-client/src/metricsApi/SentenceTypeByLocationRecord.ts @@ -0,0 +1,44 @@ +// Recidiviz - a data platform for criminal justice reform +// Copyright (C) 2020 Recidiviz, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// ============================================================================= + +import { RawMetricData } from "./fetchMetrics"; +import { + DemographicFields, + extractDemographicFields, + LocalityFields, +} from "./utils"; + +export type SentenceTypeByLocationRecord = DemographicFields & + LocalityFields & { + dualSentenceCount: number; + incarcerationCount: number; + probationCount: number; + }; + +export function sentenceTypesCurrent( + rawRecords: RawMetricData +): SentenceTypeByLocationRecord[] { + return rawRecords.map((record) => { + return { + dualSentenceCount: Number(record.dual_sentence_count), + incarcerationCount: Number(record.incarceration_count), + locality: record.district, + probationCount: Number(record.probation_count), + ...extractDemographicFields(record), + }; + }); +} diff --git a/spotlight-client/src/metricsApi/SupervisionSuccessRateDemographicsRecord.ts b/spotlight-client/src/metricsApi/SupervisionSuccessRateDemographicsRecord.ts new file mode 100644 index 00000000..807e6701 --- /dev/null +++ b/spotlight-client/src/metricsApi/SupervisionSuccessRateDemographicsRecord.ts @@ -0,0 +1,59 @@ +// Recidiviz - a data platform for criminal justice reform +// Copyright (C) 2020 Recidiviz, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// ============================================================================= + +import { ValuesType } from "utility-types"; +import { RawMetricData } from "./fetchMetrics"; +import { + DemographicFields, + extractDemographicFields, + LocalityFields, + RateFields, + recordIsParole, + recordIsProbation, +} from "./utils"; + +export type SupervisionSuccessRateDemographicsRecord = DemographicFields & + LocalityFields & + RateFields; + +function createSupervisionSuccessRateDemographicRecord( + record: ValuesType +) { + return { + rate: Number(record.success_rate), + rateDenominator: Number(record.projected_completion_count), + rateNumerator: Number(record.successful_termination_count), + locality: record.district, + ...extractDemographicFields(record), + }; +} + +export function probationSuccessRateDemographics( + rawRecords: RawMetricData +): SupervisionSuccessRateDemographicsRecord[] { + return rawRecords + .filter(recordIsProbation) + .map(createSupervisionSuccessRateDemographicRecord); +} + +export function paroleSuccessRateDemographics( + rawRecords: RawMetricData +): SupervisionSuccessRateDemographicsRecord[] { + return rawRecords + .filter(recordIsParole) + .map(createSupervisionSuccessRateDemographicRecord); +} diff --git a/spotlight-client/src/metricsApi/SupervisionSuccessRateMonthlyRecord.ts b/spotlight-client/src/metricsApi/SupervisionSuccessRateMonthlyRecord.ts new file mode 100644 index 00000000..413622d2 --- /dev/null +++ b/spotlight-client/src/metricsApi/SupervisionSuccessRateMonthlyRecord.ts @@ -0,0 +1,60 @@ +// Recidiviz - a data platform for criminal justice reform +// Copyright (C) 2020 Recidiviz, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// ============================================================================= + +import { ValuesType } from "utility-types"; +import { RawMetricData } from "./fetchMetrics"; +import { + LocalityFields, + RateFields, + recordIsProbation, + recordIsParole, +} from "./utils"; + +export type SupervisionSuccessRateMonthlyRecord = LocalityFields & + RateFields & { + month: number; + year: number; + }; + +function createSupervisionSuccessRateMonthlyRecord( + record: ValuesType +) { + return { + locality: record.district, + year: Number(record.projected_year), + month: Number(record.projected_month), + rateNumerator: Number(record.successful_termination_count), + rateDenominator: Number(record.projected_completion_count), + rate: Number(record.success_rate), + }; +} + +export function probationSuccessRateMonthly( + rawRecords: RawMetricData +): SupervisionSuccessRateMonthlyRecord[] { + return rawRecords + .filter(recordIsProbation) + .map(createSupervisionSuccessRateMonthlyRecord); +} + +export function paroleSuccessRateMonthly( + rawRecords: RawMetricData +): SupervisionSuccessRateMonthlyRecord[] { + return rawRecords + .filter(recordIsParole) + .map(createSupervisionSuccessRateMonthlyRecord); +} diff --git a/spotlight-client/src/fetchMetrics.test.ts b/spotlight-client/src/metricsApi/fetchMetrics.test.ts similarity index 96% rename from spotlight-client/src/fetchMetrics.test.ts rename to spotlight-client/src/metricsApi/fetchMetrics.test.ts index 8ba12419..d5162bf5 100644 --- a/spotlight-client/src/fetchMetrics.test.ts +++ b/spotlight-client/src/metricsApi/fetchMetrics.test.ts @@ -15,8 +15,8 @@ // along with this program. If not, see . // ============================================================================= -import fetchMetrics from "./fetchMetrics"; -import { waitForTestServer } from "./testUtils"; +import { fetchMetrics } from "./fetchMetrics"; +import { waitForTestServer } from "../testUtils"; test("returns fetched metrics", async () => { await waitForTestServer(); diff --git a/spotlight-client/src/fetchMetrics.ts b/spotlight-client/src/metricsApi/fetchMetrics.ts similarity index 95% rename from spotlight-client/src/fetchMetrics.ts rename to spotlight-client/src/metricsApi/fetchMetrics.ts index 88cfe341..d9a42e7a 100644 --- a/spotlight-client/src/fetchMetrics.ts +++ b/spotlight-client/src/metricsApi/fetchMetrics.ts @@ -15,7 +15,7 @@ // along with this program. If not, see . // ============================================================================= -import { TenantId } from "./contentApi/types"; +import { TenantId } from "../contentApi/types"; /** * All data comes back from the server as string values; @@ -34,7 +34,7 @@ type FetchMetricOptions = { /** * Retrieves the metric data provided for this application in the `/spotlight-api` package. */ -export default async function fetchMetrics({ +export async function fetchMetrics({ metricNames, tenantId, }: FetchMetricOptions): Promise { diff --git a/spotlight-client/src/metricsApi/index.ts b/spotlight-client/src/metricsApi/index.ts new file mode 100644 index 00000000..7538429d --- /dev/null +++ b/spotlight-client/src/metricsApi/index.ts @@ -0,0 +1,26 @@ +// Recidiviz - a data platform for criminal justice reform +// Copyright (C) 2020 Recidiviz, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// ============================================================================= + +export * from "./DemographicsByCategoryRecord"; +export * from "./HistoricalPopulationBreakdownRecord"; +export * from "./PopulationBreakdownByLocationRecord"; +export * from "./ProgramParticipationCurrentRecord"; +export * from "./RecidivismRateRecord"; +export * from "./SentenceTypeByLocationRecord"; +export * from "./SupervisionSuccessRateDemographicsRecord"; +export * from "./SupervisionSuccessRateMonthlyRecord"; +export * from "./fetchMetrics"; diff --git a/spotlight-client/src/metricsApi/utils.ts b/spotlight-client/src/metricsApi/utils.ts new file mode 100644 index 00000000..ace5ba04 --- /dev/null +++ b/spotlight-client/src/metricsApi/utils.ts @@ -0,0 +1,74 @@ +// Recidiviz - a data platform for criminal justice reform +// Copyright (C) 2020 Recidiviz, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// ============================================================================= + +import { ValuesType } from "utility-types"; +import { RawMetricData } from "./fetchMetrics"; + +type TotalIdentifier = "ALL"; + +type RaceIdentifier = + | TotalIdentifier + | "AMERICAN_INDIAN_ALASKAN_NATIVE" + | "ASIAN" + | "BLACK" + | "HISPANIC" + | "NATIVE_HAWAIIAN_PACIFIC_ISLANDER" + | "WHITE" + | "OTHER"; +type GenderIdentifier = TotalIdentifier | "FEMALE" | "MALE"; +type AgeIdentifier = + | TotalIdentifier + | "<25" + | "25-29" + | "30-34" + | "35-39" + | "40<"; + +export type DemographicFields = { + raceOrEthnicity: RaceIdentifier; + gender: GenderIdentifier; + ageBucket: AgeIdentifier; +}; + +export type LocalityFields = { + locality: string; +}; + +export type RateFields = { + rateDenominator: number; + rateNumerator: number; + rate: number; +}; + +export function extractDemographicFields( + record: ValuesType +): DemographicFields { + return { + // we are trusting the API to return valid strings here, not validating them + raceOrEthnicity: record.race_or_ethnicity as RaceIdentifier, + gender: record.gender as GenderIdentifier, + ageBucket: record.age_bucket as AgeIdentifier, + }; +} + +export function recordIsParole(record: ValuesType): boolean { + return record.supervision_type === "PAROLE"; +} + +export function recordIsProbation(record: ValuesType): boolean { + return record.supervision_type === "PROBATION"; +}