Public
Edited
Apr 7
Insert cell
Insert cell
import {vl} from "@vega/vega-lite-api-v5"
Insert cell
import {printTable} from "@uwdata/data-utilities"
Insert cell
NYPD_Data = FileAttachment("allegations_202007271729.csv").csv()
Insert cell
printTable(NYPD_Data.slice(0,5))
Insert cell
Insert cell
vl.markBar({ opacity: .8, strokeWidth: 0.7 })
.data(NYPD_Data)
.params(vl.selectInterval().bind('scales'),
vl.selectSingle('BoroughSelect')
.fields('borough')
.init({ borough: 'All' })
.bind(vl.menu(['All', 'Brooklyn', 'Manhattan', 'Queens', 'Bronx', 'Staten Island']).name('Select Borough: '))
)
.transform(
vl.calculate(
"datum.precinct >= 1 && datum.precinct <= 39 ? 'Manhattan' : " +
"datum.precinct >= 40 && datum.precinct <= 59 ? 'Bronx' : " +
"datum.precinct >= 60 && datum.precinct <= 99 ? 'Brooklyn' : " +
"datum.precinct >= 100 && datum.precinct <= 119 ? 'Queens' : " +
"'Staten Island'" )
.as('borough'),
vl.filter("datum.borough == BoroughSelect.borough || BoroughSelect.borough == 'All'"),
vl.calculate("datum.precinct == 0 || datum.precinct == 1000 || datum.precinct == '' ? 'Other' : datum.precinct")
.as("precinct_clean"),
vl.calculate("datum.board_disposition == 0 || datum.board_disposition == '' ? 'Other' : datum.board_disposition")
.as("board_disposition_clean"),
vl.aggregate([{ op: "count", as: "count" }]).groupby(["unique_mos_id", "precinct_clean"]),
vl.filter("datum.count > 1"),
vl.window({ op: "rank", as: "rank" })
.sort([{ field: "count", order: "descending" }])
.groupby(["precinct_clean"])
)
.encode(
vl.y().fieldQ("count").sort('-count').axis({ grid: true, title: "Number of Complaints" }),
vl.x().fieldO("precinct_clean").sort("ascending").axis({ grid: true, title: "Precinct" }),
vl.color()
.fieldQ("count")
.sort(vl.fieldQ("rank").order("ascending"))
.scale({domain: [5, 10, 40], range: ["blue", "white", "red"]})
.legend(true)
.title("# of Complaints/Officer"),
vl.order()
.fieldQ("rank")
.sort("ascending"),
vl.tooltip(["unique_mos_id", "precinct_clean", "count"])
)
.width(730)
.height(600)
.title({ text: "Complaints Against NYPD Officers, 1985-2020", subtitle: "Data organized per officer/ precinct. Only includes officers with 2 or more complaints. Select a borough to narrow results." })
.render()
Insert cell
Insert cell
Insert cell
NYPDArea = vl.markArea({ opacity: .3})
.data(NYPD_Data)
.transform(
vl.filter("datum.precinct != null && datum.year_received != null"),
vl.filter("datum.outcome_description != 'No arrest made or summons issued' && datum.outcome_description != 'Parking summons issued' && datum.outcome_description != ''"),
vl.calculate("datum.board_disposition == 0 || datum.board_disposition == '' ? 'Other' : datum.board_disposition")
.as("board_disposition_clean"),

vl.calculate("datum.board_disposition_clean == 'Exonerated' || datum.board_disposition_clean == 'Unsubstantiated' ? 'No Wrongdoing Detected' : datum.board_disposition_clean")
.as("board_disposition_combined"),
vl.calculate("datum.board_disposition_combined == 'Substantiated (No Recommendations)' || datum.board_disposition_combined == 'Substantiated (Instructions)' || datum.board_disposition_combined == 'Substantiated (Formalized Training)' || datum.board_disposition_combined == 'Substantiated (Command Lvl Instructions)' || datum.board_disposition_combined == 'Substantiated (Command Discipline)' || datum.board_disposition_combined == 'Substantiated (Command Discipline B)' || datum.board_disposition_combined == 'Substantiated (Command Discipline A)' || datum.board_disposition_combined == 'Substantiated (Charges)' || datum.board_disposition_combined == 'Substantiated (MOS Unidentified)' ? 'Substantiated' : datum.board_disposition_combined")
.as("board_disposition_final"),
vl.aggregate([{ op: "count", as: "count" }]).groupby(["board_disposition_final", "fado_type"])
// vl.filter("isValid(datum.precinct_clean) && isValid(datum.year_received)")
)

.encode(
vl.y().fieldQ("count").stack("null").axis({ grid: true, title: "Number of Complaints" }),
vl.x().fieldO("fado_type").sort(vl.field("fado_type").order("descending")).axis({ grid: true, title: "Type of Complaint" }),
vl.color()
.fieldN("board_disposition_final")
.scale({
domain: ["No Wrongdoing Detected", "Substantiated"],
range: ["blue", "black" ]
})
.sort("descending")
.legend(true).title("Outcome of Complaint"),
vl.tooltip(["board_disposition_final", "count"])
)
;
Insert cell
lineData = [
{ fado_type: 'Abuse of Authority', count: 58309 },
{ fado_type: 'Discourtesy', count: 11936 },
{ fado_type: 'Force', count: 14797 },
{ fado_type: 'Offensive Language', count: 2743 }
];
Insert cell
lineLabel = vl.markText({
align: 'left',
dx: -330,
dy: -500,
fontSize: 15,
fontWeight: 'bold',
color: 'red'
})
.data([{ fado_type: "Abuse of Authority", count: 400 }])
.encode(
vl.x().fieldO("fado_type").sort(vl.field("fado_type").order("descending")),
vl.y().fieldQ("count"),
vl.text().value("Complaints against England/Wales Police")
);
Insert cell
lineLabelNYPD = vl.markText({
align: 'left',
dx: -350,
dy: -60,
fontSize: 12,
fontWeight: 'bold',
color: 'purple'
})
.data([{ fado_type: "Abuse of Authority", count: 400 }])
.encode(
vl.x().fieldO("fado_type").sort(vl.field("fado_type").order("descending")),
vl.y().fieldQ("count"),
vl.text().value("Complaints against NYPD over 35 years")
);
Insert cell
EnglandWalesLine = vl.markLine({ color: 'red'})
.data(lineData)
.encode(
vl.x().fieldO("fado_type").sort(vl.field("fado_type").order("descending")),
vl.y().fieldQ("count"),
vl.tooltip(["fado_type", "count"])
);
Insert cell
vl.layer(EnglandWalesLine, NYPDArea, lineLabel, lineLabelNYPD)
.width(650)
.height(600)
.title({ text: "35 Years of Complaints By Criminals Against NYPD Officers Compared to One Year of Complaints Against the England/Wales Police Department", subtitle: "NYPD Officers Have Far Less Valid Complaints against them Compared to their Global Counterparts" })
.render();
Insert cell
Insert cell
Insert cell
Insert cell
vl.markLine()
.data(NYPD_Data)
.transform(
vl.calculate("datum.precinct == 0 || datum.precinct == 1000 || datum.precinct == '' ? 'Other' : datum.precinct")
.as("precinct_clean"),
vl.filter("datum.precinct_clean != null"),
vl.filter("isValid(datum.precinct_clean)")
)
.encode(
vl.x().fieldN('precinct').title("Precinct"),
vl.y().aggregate('count').title("Number of reports")
)
.width(800)
.height(400)
.render()
Insert cell
vl.markBar()
.data(NYPD_Data)
.encode(
vl.x().fieldN('shield_no').aggregate('count').title("Total"),
vl.y().distinct('shield_no').aggregate('count').title("Number of Reports")
)
.width(400)
.height(400)
.render()
Insert cell
vl.markBar({ opacity: 0.8, strokeWidth: 0.7 })
.data(NYPD_Data)
.params(vl.selectInterval().bind('scales'))
.transform(
vl.calculate("datum.precinct == 0 || datum.precinct == 1000 || datum.precinct == '' ? 'Other' : datum.precinct")
.as("precinct_clean"),
vl.calculate("datum.board_disposition == 0 || datum.board_disposition == '' ? 'Other' : datum.board_disposition")
.as("board_disposition_clean"),
vl.aggregate([{ op: "count", as: "count" }]).groupby(["unique_mos_id", "precinct_clean"]),
)
.encode(
vl.y().fieldQ("count").axis({ grid: true, title: "Total Complaints" }),
vl.x().fieldO("precinct_clean").sort(vl.field("precinct_numeric")).axis({ grid: true, title: "Precinct" }),
vl.color().fieldN("unique_mos_id").legend(true).title("Officer"),
vl.tooltip(["board_disposition_combined", "precinct_clean", "count"])
)
.width(800)
.height(600)
.title({ text: "Complaints By Precinct", subtitle: "Segmented by Outcome" })
.render()
Insert cell
vl.markCircle({ opacity: 0.4, strokeWidth: 0.7 })
.data(NYPD_Data)
.params(vl.selectInterval().bind('scales'))
.transform(
vl.calculate("datum.precinct == 0 || datum.precinct == 1000 || datum.precinct == '' ? 'Other' : datum.precinct")
.as("precinct_clean"),
vl.aggregate([{ op: "count", as: "count" }]).groupby(["precinct_clean", "year_received"]),
vl.filter("datum.precinct_clean != null && datum.year_received != null"),
vl.filter("isValid(datum.precinct_clean) && isValid(datum.year_received)")
)
.encode(
vl.x().fieldT("year_received").axis({ grid: true, title: "Year Received" }),
vl.y().fieldO("precinct_clean").sort("descending").axis({ grid: true, title: "Precinct" }),
vl.size().fieldQ("count").scale({ range: [10, 1000] }),
vl.color().fieldN("precinct_clean").legend(true),
vl.tooltip(["precinct_clean", "year_received", "count"])
)
.width(800)
.height(600)
.title({ text: "Complaints Over Time", subtitle: "By Precinct" })
.render()
Insert cell
vl.markBar({ opacity: 0.8, strokeWidth: 0.7 })
.data(NYPD_Data)
.params(vl.selectInterval().bind('scales'))
.transform(
vl.calculate("datum.precinct == 0 || datum.precinct == 1000 || datum.precinct == '' ? 'Other' : datum.precinct")
.as("precinct_clean"),
vl.calculate("datum.board_disposition == 0 || datum.board_disposition == '' ? 'Other' : datum.board_disposition")
.as("board_disposition_clean"),
vl.calculate("datum.board_disposition_clean == 'Substantiated (No Recommendations)' || datum.board_disposition_clean == 'Substantiated (Instructions)' || datum.board_disposition_clean == 'Substantiated (Formalized Training)' || datum.board_disposition_clean == 'Substantiated (Command Lvl Instructions)' || datum.board_disposition_clean == 'Substantiated (Command Discipline)' || datum.board_disposition_clean == 'Substantiated (Command Discipline B)' || datum.board_disposition_clean == 'Substantiated (Command Discipline A)' || datum.board_disposition_clean == 'Substantiated (Charges)' || datum.board_disposition_clean == 'Substantiated (MOS Unidentified)' ? 'Substantiated' : datum.board_disposition_clean")
.as("board_disposition_combined"),
vl.calculate("parseInt(datum.precinct_clean)").as("precinct_numeric"),
vl.aggregate([{ op: "count", as: "count" }]).groupby(["board_disposition_combined", "precinct_clean"])
// vl.filter("datum.precinct_clean != null && datum.year_received != null"),
// vl.filter("isValid(datum.precinct_clean) && isValid(datum.year_received)")
)
.encode(
vl.y().fieldQ("count").axis({ grid: true, title: "Total Complaints" }),
vl.x().fieldO("precinct_clean").sort(vl.field("precinct_numeric")).axis({ grid: true, title: "Precinct" }),
vl.color().fieldN("board_disposition_combined").legend(true).title("Board Disposition"),
vl.tooltip(["board_disposition_combined", "precinct_clean", "count"])
)
.width(800)
.height(600)
.title({ text: "Complaints By Precinct", subtitle: "Segmented by Outcome" })
.render()
Insert cell
vl.markRect()
.data(NYPD_Data)
.params(vl.selectInterval().bind('scales'))
.transform(
vl.calculate("datum.precinct == 0 || datum.precinct == 1000 || datum.precinct == '' ? 'Other' : datum.precinct")
.as("precinct_clean"),
vl.calculate("datum.board_disposition == 0 || datum.board_disposition == '' ? 'Other' : datum.board_disposition")
.as("board_disposition_clean"),
vl.calculate("datum.board_disposition_clean == 'Substantiated (No Recommendations)' || datum.board_disposition_clean == 'Substantiated (Instructions)' || datum.board_disposition_clean == 'Substantiated (Formalized Training)' || datum.board_disposition_clean == 'Substantiated (Command Lvl Instructions)' || datum.board_disposition_clean == 'Substantiated (Command Discipline)' || datum.board_disposition_clean == 'Substantiated (Command Discipline B)' || datum.board_disposition_clean == 'Substantiated (Command Discipline A)' || datum.board_disposition_clean == 'Substantiated (Charges)' || datum.board_disposition_clean == 'Substantiated (MOS Unidentified)' ? 'Substantiated' : datum.board_disposition_clean")
.as("board_disposition_combined"),
// vl.calculate("parseInt(datum.precinct_clean)").as("precinct_numeric"),
vl.aggregate([{ op: "count", as: "count" }]).groupby(["board_disposition_combined", "precinct_clean", "fado_type"])
)
.spacing(30)
.encode(
vl.x().fieldO("precinct_clean").axis({ grid: true, title: "Precinct", tickBand: "extent" }),
vl.y().fieldN("fado_type").sort(vl.field("fado_type")).axis({ grid: true, title: "Type", tickBand: "extent" }),
vl.color().fieldN("board_disposition_combined").legend(true).title("Type of Complaint"),
vl.tooltip(["board_disposition_combined", "precinct_clean", "fado_type", "count"])
)
.width(800)
.height(600)
.title({ text: "Complaints By Precinct", subtitle: "Segmented by Outcome" })
.render()
Insert cell
vl.markCircle({ opacity: 0.3, strokeWidth: 0.5 })
.data(NYPD_Data)
.params(vl.selectInterval().bind('scales'))
.transform(
vl.calculate("datum.precinct == 0 || datum.precinct == 1000 || datum.precinct == '' ? 'Other' : datum.precinct")
.as("precinct_clean"),
vl.calculate("datum.board_disposition == 0 || datum.board_disposition == '' ? 'Other' : datum.board_disposition")
.as("board_disposition_clean"),
vl.calculate("datum.board_disposition_clean == 'Substantiated (No Recommendations)' || datum.board_disposition_clean == 'Substantiated (Instructions)' || datum.board_disposition_clean == 'Substantiated (Formalized Training)' || datum.board_disposition_clean == 'Substantiated (Command Lvl Instructions)' || datum.board_disposition_clean == 'Substantiated (Command Discipline)' || datum.board_disposition_clean == 'Substantiated (Command Discipline B)' || datum.board_disposition_clean == 'Substantiated (Command Discipline A)' || datum.board_disposition_clean == 'Substantiated (Charges)' || datum.board_disposition_clean == 'Substantiated (MOS Unidentified)' ? 'Substantiated' : datum.board_disposition_clean")
.as("board_disposition_combined"),
vl.aggregate([{ op: "count", as: "count" }]).groupby(["precinct_clean", "fado_type", "board_disposition_combined"])
)
.encode(
vl.x().fieldO("precinct_clean").axis({ title: "Precinct", grid: false }),
vl.y().fieldO("fado_type").axis({ title: "Type of Complaint", grid: false }),
vl.size().fieldQ("count").scale({ range: [10, 5000] }).title("Number of Complaints"),
vl.color().fieldN("board_disposition_combined").legend({ title: "Outcome" }),
vl.tooltip(["precinct_clean", "fado_type", "board_disposition_combined", "count"])
)
.width(800)
.height(600)
.title({ text: "Complaints By Precinct", subtitle: "Segmented by Outcome" })
.render()
Insert cell
vl.markBar({ opacity: 0.8, strokeWidth: 0.7 })
.data(NYPD_Data)
.params(vl.selectInterval().bind('scales'))
.transform(
vl.calculate("datum.board_disposition == 0 || datum.board_disposition == '' ? 'Other' : datum.board_disposition")
.as("board_disposition_clean"),
vl.calculate("datum.board_disposition_clean == 'Substantiated (No Recommendations)' || datum.board_disposition_clean == 'Substantiated (Instructions)' || datum.board_disposition_clean == 'Substantiated (Formalized Training)' || datum.board_disposition_clean == 'Substantiated (Command Lvl Instructions)' || datum.board_disposition_clean == 'Substantiated (Command Discipline)' || datum.board_disposition_clean == 'Substantiated (Command Discipline B)' || datum.board_disposition_clean == 'Substantiated (Command Discipline A)' || datum.board_disposition_clean == 'Substantiated (Charges)' || datum.board_disposition_clean == 'Substantiated (MOS Unidentified)' ? 'Substantiated' : datum.board_disposition_clean")
.as("board_disposition_combined"),
vl.calculate("parseInt(datum.precinct_clean)").as("precinct_numeric"),
vl.aggregate([{ op: "count", as: "count" }]).groupby(["board_disposition_combined", "fado_type"])
// vl.filter("datum.precinct_clean != null && datum.year_received != null"),
// vl.filter("isValid(datum.precinct_clean) && isValid(datum.year_received)")
)
.encode(
vl.y().fieldQ("count").axis({ grid: true, title: "Total Complaints" }),
vl.x().fieldO("fado_type").sort(vl.field("fado_type")).axis({ grid: true, title: "fado_type" }),
vl.color().fieldN("board_disposition_combined").legend(true).title("Board Disposition"),
vl.tooltip(["board_disposition_combined", "precinct_clean", "count"])
)
.width(800)
.height(600)
.title({ text: "Complaints By Precinct", subtitle: "Segmented by Outcome" })
.render()
Insert cell
vl.markCircle({ opacity: 0.3, stroke: 'black', strokeWidth: 0.5 })
.data(NYPD_Data)
.params(vl.selectInterval().bind('scales'))
.transform(
vl.calculate("datum.precinct == 0 || datum.precinct == 1000 || datum.precinct == '' ? 'Other' : datum.precinct")
.as("precinct_clean"),
vl.calculate("datum.board_disposition == 0 || datum.board_disposition == '' ? 'Other' : datum.board_disposition")
.as("board_disposition_clean"),
vl.calculate("datum.board_disposition_clean == 'Substantiated (No Recommendations)' || datum.board_disposition_clean == 'Substantiated (Instructions)' || datum.board_disposition_clean == 'Substantiated (Formalized Training)' || datum.board_disposition_clean == 'Substantiated (Command Lvl Instructions)' || datum.board_disposition_clean == 'Substantiated (Command Discipline)' || datum.board_disposition_clean == 'Substantiated (Command Discipline B)' || datum.board_disposition_clean == 'Substantiated (Command Discipline A)' || datum.board_disposition_clean == 'Substantiated (Charges)' || datum.board_disposition_clean == 'Substantiated (MOS Unidentified)' ? 'Substantiated' : datum.board_disposition_clean")
.as("board_disposition_combined"),

vl.aggregate([{ op: "count", as: "count" }]).groupby(["precinct_clean", "fado_type", "board_disposition_combined"])
)
.encode(
vl.x().fieldO("precinct_clean").axis({ title: "Precinct", grid: false }),
vl.y().fieldO("fado_type").axis({ title: "Type of Complaint", grid: false }),
vl.y2().fieldQ("adjusted_y"),
vl.size().fieldQ("count").scale({ range: [10, 5000] }).title("Number of Complaints"),
vl.color().fieldN("board_disposition_combined").legend({ title: "Outcome" }),
vl.tooltip(["precinct_clean", "fado_type", "board_disposition_combined", "count"])
)
.width(800)
.height(600)
.title({ text: "Complaints By Precinct", subtitle: "Segmented by Outcome" })
.render()

Insert cell
vl.markArea()
.data(NYPD_Data)
.params(vl.selectInterval().bind('scales'))
.transform(
vl.filter("datum.precinct != null && datum.year_received != null"),
vl.calculate("datum.board_disposition == 0 || datum.board_disposition == '' ? 'Other' : datum.board_disposition")
.as("board_disposition_clean"),
vl.calculate("datum.board_disposition_clean == 'Substantiated (No Recommendations)' || datum.board_disposition_clean == 'Substantiated (Instructions)' || datum.board_disposition_clean == 'Substantiated (Formalized Training)' || datum.board_disposition_clean == 'Substantiated (Command Lvl Instructions)' || datum.board_disposition_clean == 'Substantiated (Command Discipline)' || datum.board_disposition_clean == 'Substantiated (Command Discipline B)' || datum.board_disposition_clean == 'Substantiated (Command Discipline A)' || datum.board_disposition_clean == 'Substantiated (Charges)' || datum.board_disposition_clean == 'Substantiated (MOS Unidentified)' ? 'Substantiated' : datum.board_disposition_clean")
.as("board_disposition_combined"),
vl.aggregate([{ op: "count", as: "count" }]).groupby(["board_disposition_combined", "fado_type"])
// vl.filter("isValid(datum.precinct_clean) && isValid(datum.year_received)")
)

.encode(
vl.y().fieldQ("count").axis({ grid: true, title: "Total Complaints" }),
vl.x().fieldO("fado_type").sort(vl.field("fado_type")).axis({ grid: true, title: "fado_type" }),
vl.color().fieldN("board_disposition_combined").legend(true).title("Board Disposition"),
vl.tooltip(["board_disposition_combined", "count", "fado_type"])
)
.width(800)
.height(600)
.title({ text: "Complaints By Precinct", subtitle: "Segmented by Outcome" })
.render()
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more