Skip to content

Could binning millions of values be faster? #454

Description

@mbostock

Currently the bin transform can be slow (several seconds) when binning millions of values. Since rendering is fast (when there are only a few bins), the data transform dwarfs the rendering cost and this might be a good candidate for optimization. For example, if the bin thresholds are uniformly-spaced, and dense, can we use quantization instead of bisection to create the bins?

This is arguably an issue for d3-array (d3.bin) instead of Plot, but there might be other smarts we can do in Plot, so I figured I would start with an issue here.

Related #451.

Activity

  1. added a commit that references this issue on Jul 13, 2021
  2. Fil commented on Jul 13, 2021

    @Fil
    Contributor

    I've tested quantization in d3-array: it's about 10 lines of code, and almost 2x faster on an array of 30 million values with 1000 bins. See d3/d3-array#220 and d3p at https://observablehq.com/@d3/faster-bins-454

    However, if the only reduction we do afterwards is to a length, we've wasted a lot of memory trying to keep track of the bin's contents, pushing each value into its bin. By just incrementing an object {x0, x1, length: 0}, we are 5x faster than the original. See d3l at https://observablehq.com/@d3/faster-bins-454

    Of course with d3l we lose any possibility of reducing the binned data beyond returning its length, so integrating this approach seems to require a bit more code, like a specific code path for "binning numbers uniformly and getting a count".

  3. mbostock commented on Jul 13, 2021

    @mbostock
    MemberAuthor

    Nice work @Fil!

  4. mbostock commented on Apr 1, 2022

    @mbostock
    MemberAuthor

    Fixed by d3/d3-array#220.

  5. mbostock commented on Jan 14, 2023

    @mbostock
    MemberAuthor

    There still seem to be plenty of cases where grouping is orders of magnitude faster than binning; we could have a heuristic to switch to grouping under the hood to optimize. For example, with 1.2M observations, this takes ~300 ms:

    Plot.plot({
      y: {
        transform: d => d / 1000
      },
      marks: [
        Plot.line(data, Plot.groupX({y: "max"}, {x: d => d3.utcMonth(d.dateTime), y: "value"})),
        Plot.ruleY([0])
      ]
    })

    but this takes ~15.2 seconds:

    Plot.plot({
      y: {
        transform: d => d / 1000
      },
      marks: [
        Plot.line(data, Plot.binX({y: "max"}, {x: "dateTime", y: "value", thresholds: d3.utcMonth})),
        Plot.ruleY([0])
      ]
    })

    And even this, which should be using the “fast” path of quantization, still takes ~12 seconds:

    Plot.plot({
      y: {
        transform: d => d / 1000
      },
      marks: [
        Plot.line(data, Plot.binX({y: "max"}, {
          x: d => d.dateTime.getUTCFullYear() * 12 + d.dateTime.getUTCMonth(),
          y: "value",
          interval: 1
        })),
        Plot.ruleY([0])
      ]
    })

    So we should investigate and look for further performance improvements when binning.

    There also should be some improvements possible if we avoid materializing the binned data; in most cases we only need the reduced aggregates only.

    Examples: https://observablehq.com/d/e4668f3ec01c184ca

  6. mbostock commented on Jan 15, 2023

    @mbostock
    MemberAuthor

    Simple test case that takes ~7 seconds to bin:

    import * as Plot from "@observablehq/plot";
    
    const dates = new Array(1e6);
    const start = +new Date("2020-01-01");
    const end = +new Date("2021-01-01");
    for (let i = 0; i < dates.length; ++i) dates[i] = new Date(Math.random() * (end - start) + start);
    
    export async function bin1m() {
      return Plot.plot({
        marks: [
          Plot.rectY(dates, Plot.binX())
        ]
      });
    }

    Looks like most of the work is happening here:

    return [x0, x1, set.size ? (I) => I.filter(set.has, set) : binempty];

    Which is getting called here:

    const bb = fx(g);

    const b = fy(bb);

    I suspect things are slow because we’re basically precomputing the bins as a Set, and then filtering the index against the bins. I’m guessing we probably just want to avoid d3.bin entirely and apply a different strategy within Plot (since we support two-dimensional, cumulative binning, faceting, etc.).

  7. mbostock commented on Jan 15, 2023

    @mbostock
    MemberAuthor

    #1225 improves the performance from ~15 seconds to ~300 ms, which seems much better!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestquestionFurther information is needed

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions