Encouraging Contributions

StreetCred Team - October 30, 2018

We’ve long suspected it, and MapNYC finally confirmed: we New Yorkers love our services. Mostly doctors, because we’re hypochondriacs. Dining and shopping are close behind.

Want proof? Behold (and click around) the MapNYC category bubble chart:

.categories__container { --mapnyc-accent: #8428FF; --text-color: #2A2A2A; } .categories__chart { display: block; height: 60vw; margin: 0 auto; width: 100%; } .categories__chart-label { fill: #fff; font-weight: 300; opacity: 0; pointer-events: none; text-anchor: middle; transition: opacity 0.5s ease; } .label--count { font-weight: 400; } [data-depth="0"] .label--depth-1, [data-depth="1"] .label--depth-1, [data-depth="2"] .label--depth-2, [data-depth="3"] .label--depth-3 { opacity: 1; } .node { cursor: pointer; stroke-width: 0; transition: stroke-width 0.2s ease; } .node:hover { stroke: var(--mapnyc-accent); stroke-width: 1.5px; } @media (min-width: 85em) { .categories__chart { height: 50rem; } }

(() => { const resizer = onResize => { let timer; window.addEventListener("resize", () => { clearTimeout(timer); timer = setTimeout(onResize, 100); }); }; class Categories { constructor() { this.fetchData(); } async fetchData() { this.data = await d3.json( "https://s3.amazonaws.com/assets.streetcred.co/data/categories.json" ); this.render(); } getDiameter() { const svg = d3.select("#js-categories-chart"); const width = parseInt(svg.style("width")); const height = parseInt(svg.style("height")); return height < width ? height : width; } setupChart() { const svg = d3.select("#js-categories-chart"); svg.attr("data-depth", 0); const g = svg.append("g").attr("class", "categories__chart-group"); const diameter = this.getDiameter(); const pack = d3 .pack() .size([diameter - 20, diameter - 20]) .padding(2); const color = d3 .scaleLinear() .domain([-1, 5]) .range(["hsl(260, 90%, 95%)", "hsl(266, 100%, 65%)"]) .interpolate(d3.interpolateHcl); const root = d3 .hierarchy(this.data, d => d.categories) .sum(d => d.count) .sort((a, b) => b.value - a.value); const nodes = pack(root).descendants(); const zoom = view => { svg.attr("data-depth", view.depth); const transition = d3 .transition() .duration(750) .tween("zoom", d => { const i = d3.interpolateZoom(this.view, [ view.x, view.y, view.r * 2 + 20 ]); return t => this.zoomChartTo(i(t)); }); }; const circle = g .selectAll("circle") .data(nodes) .enter() .append("circle") .attr( "class", d => d.parent ? d.children ? "node" : "node node--leaf" : "node node--root" ) .style("fill", d => (d.children ? color(d.depth) : color(5))) .on("click", d => { if (root !== d) { zoom(d); d3.event.stopPropagation(); } }); const texts = g .selectAll("text") .data(nodes) .enter(); texts .append("text") .attr("class", d => `categories__chart-label label--depth-${d.depth}`) .attr("font-size", d => Math.max(d.r / 4, 13)) .text(d => d.data.name); texts .append("text") .attr( "class", d => `categories__chart-label label--count label--depth-${d.depth}` ) .attr("dy", "1em") .attr("font-size", d => Math.max(d.r / 5, 12)) .text(d => d.data.count); svg.on("click", () => zoom(root)); this.chart = { svg, pack, circle }; // Manually invoke this on first run to set the initial sizes. this.resizeChart(); this.zoomChartTo([root.x, root.y, root.r * 2 + 20]); } zoomChartTo(view) { this.view = view; const { svg, circle } = this.chart; const diameter = this.getDiameter(); const k = diameter / view[2]; const node = d3.selectAll( ".categories__chart-group circle, .categories__chart-group text" ); node.attr( "transform", d => `translate(${(d.x - view[0]) * k}, ${(d.y - view[1]) * k})` ); circle.attr("r", d => d.r * k); } resizeChart() { const { svg, pack } = this.chart; const margin = 20; const width = parseInt(svg.style("width")); const diameter = this.getDiameter(); d3.select(".categories__chart-group").attr( "transform", `translate(${width / 2}, ${diameter / 2})` ); pack.size([diameter - margin, diameter - margin]); if (this.view) { this.zoomChartTo(this.view); } } observed() {} render() { this.setupChart(); } } document.addEventListener("DOMContentLoaded", () => { const categories = new Categories(); resizer(() => { categories.resizeChart(); }); }); })();

These bubbles show the number of POIs created for each category during MapNYC, a month-long data collection contest that yielded data for over 20,000 places in New York. Each place was created by one participant and validated by at least two others. As we discussed in yesterdays post, we proved that people would create and validate places, but we also wanted to test if users could be directed to specific categories or neighborhoods. Would our platform, and eventually our protocol, be flexible enough for this?

Are there 2,464 doctors in the house?

If you click around the category bubbles above, you might be surprised to see such a large number of doctors. In fact, MapNYC ended with 2,464 doctors total, by far the largest category we collected. How did this happen?

On October 8, two weeks into the contest, we had only 52 doctors in the dataset. We sent an email to all participants notifying them that we would double points for all doctors created. Combined with special point bonuses for the boroughs (described below), this provided a strong incentive to map the real locations of doctors all over the city, especially in the outer boroughs.

As described in a previous post, commercial POI data about doctors is problematic and often of low quality:

We’ve heard from map providers how difficult it is to get high-quality location data about doctors. Frequently, this data is scraped from the web and doesn’t get at accurate office locations. We thought we’d see how the MapNYC community would respond to an incentive to help this problem.

We’d say that the MapNYC community responded very well, and the doctor challenge was one of the high points of the contest. We’ll be taking these learnings into future efforts to get high-quality, commercially viable data.

Brooklyn, Bronx, Queens and Staten!

New York City is a great testing ground for a lot of reasons, but the biggest is its diversity in all senses of the word. First, the geographic diversity: in one area you have tall buildings where GPS barely works, in another area you’re in the suburbs. If you dropped someone blindfolded in the middle of many neighborhoods here, they’d have a hard time guessing they were in the United States from the signs on the storefronts and languages being spoken on the streets (to be clear, this is a feature not a bug). There’s no better place to test something with global ambitions.

On October 1, one week into the contest, we were getting decent data in Manhattan, okay data in Brooklyn and Queens, very little data in the Bronx, and no data at all in Staten Island. It was not what we were aiming for in terms of coverage.

So we emailed all participants, letting them know that there would be an increased point bonus to create data in all five boroughs including Manhattan, but that creating places outside Manhattan would be worth even more. Ian on Twitter captured what we were up to, and quoted the point bonuses that we kept intact for the rest of MapNYC:

One of the most interesting things about StreetCred's MapNYC project is that they're actively trying to pull in data from under-served communities. It's a great precedent to set for the rest of the project. https://t.co/tQ128qWozR

— Ian Dees (@iandees) October 1, 2018

We credit this incentive adjustment for the full, rich map of the city you can see coming together in yesterdays post. Zooming in a bit, we can see the impact on the Bronx and Staten Island. Both saw an uptick in data collection after day 8. Staten Island shows interesting activity in an indoor shopping mall and all along the railway line.

By the end of MapNYC, we were able to get participants from across the city to create places in their own neighborhoods, and we also showed that incentives could drive people outside their normal routines to collect and validate data. In the end, here are the totals for each borough in the MapNYC contest:

.boroughs__figures * { box-sizing: border-box; } .boroughs__figures { --mapnyc-accent: #8428FF; display: flex; flex-wrap: wrap; justify-content: center; } .borough__figure { flex: 0 1 100%; margin: 0; padding: 1rem 0; position: relative; } .borough__shape { fill: url(#borough__gradient); stroke: var(--mapnyc-accent); stroke-width: 3; } .borough__info { line-height: 1.1; text-align: center; transform: translateY(-100%); } .borough__info b { text-shadow: 1px 1px 0 #fff, -1px -1px 0 #fff, 1px -1px 0 #fff, -1px 1px 0 #fff, 2px 2px 0 #fff, -2px -2px 0 #fff, 2px -2px 0 #fff, -2px 2px 0 #fff, 3px 3px 0 #fff, -3px -3px 0 #fff, 3px -3px 0 #fff, -3px 3px 0 #fff, 4px 4px 0 #fff, -4px -4px 0 #fff, 4px -4px 0 #fff, -4px 4px 0 #fff, 5px 5px 0 #fff, -5px -5px 0 #fff, 5px -5px 0 #fff, -5px 5px 0 #fff, 6px 6px 0 #fff, -6px -6px 0 #fff, 6px -6px 0 #fff, -6px 6px 0 #fff; font-weight: inherit; padding: 0.25rem; } .borough__name { font-size: 1.2rem; font-weight: 300; margin: 0; } .borough__count { color: var(--mapnyc-accent); font-size: 1.8rem; font-weight: 400; margin: 0; } @media (min-width: 37.55em) { .borough__figure { flex: 0 1 50%; padding: 2rem 1rem; } .figure__brooklyn { padding-left: 0.5rem; padding-top: 6rem; padding-right: 2rem; } .figure--staten-island { padding-top: 0; } .borough__info { position: absolute; text-align: left; transform: translateY(0); } .info--manhattan { top: 68%; left: 38%; } .info--brooklyn { top: 22%; left: 52%; } .info--queens { bottom: 44%; left: 22%; text-align: right; } .info--the-bronx { bottom: 30%; left: 42%; } .info--staten-island { top: 48%; left: 68%; } }

A graphic of Manhattan, NYC

Manhattan

11,739

A graphic of Brooklyn, NYC

Brooklyn

3,081

A graphic of Queens, NYC

Queens

2,810

A graphic of The Bronx, NYC

The Bronx

2,322

A graphic of Staten Island, NYC

Staten Island

455

Tomorrow, we’ll zoom in even further to explore how we drove better data accuracy at the street level. See you soon! Questions? Let us know.