Skip to main content

scor

Calculate scores for numeric values or items, and get the “total score” (aka arithmetic mean or weighted arithmetic mean) from multiple scores.

Concept and vision

Imagine you

  • have a long list of items to work on, and you want to prioritize them
  • want to show the most relevant items to a user before showing more

For example, let’s look at npm packages. Possible criteria are:

  • number of maintainers
  • number of dependencies (direct/transient)
  • time since last published version
  • version (major < 1?) / dist-tags
  • weekly downloads
  • source code repo attributes (e.g. GitHub stars/forks)
  • quality?
  • …?

The different relevant values come in very different “shapes”. Once all the data is gathered per package, depending on the use case the different values are more or less relevant.

I experienced that such a “rating system”, or “weighted average score”, is not so easy to get completely right from scratch alongside collecting the data. It also involves a lot of repetitive code that easily leaks its abstractions into the rest of the code.

scor simplifies this by making certain assumptions:

  • All values are within a certain range (min <= value <= max).
    • only numeric values are accepted, everything else throws
  • To use the (different) values as a score and easily compare all of them,
    they need to be converted into the same range: \ between 0(value <= min) and 1 (value >= max)
    • If the range is “empty” (min === max), the score is always 0
    • values that are “not numeric” (see isNumeric) result in a score of 0
  • The user fully controls the conversion of item to value (toValue):
    • get deeply nested fields
    • calculate from multiple fields
    • convert data to a numeric value
    • need the highest value to be the lowest score: -1 * value
    • need some logarithmic scale: Math.log10(value)
  • The user fully controls min and max values,
    but they can be derived from items (also using toValue, see getItemRange).
  • All options for scor are optional and can be configured as a second step
  • Fail as early as possible (by throwing a specific Error):
    • using a method that requires an optional value which has not been configured
    • a required value is not numeric (beside cases mentioned above)
  • A Scor is immutable, “mutations” create a new instance.
  • A Scor never keeps references to the items it is scoring.
  • Multiple Scors can easily be combined into a single overall weighted score per item, e.g. to use it for sorting

Usage

(This has not been published as a package yet, but you can of course fetch the code from GitHub.)

import { createToMean, distributeWeights, scorForItems } from "scor"; // I plan to publish to deno.land soon
import { getPackagesData } from "./npm.ts";

const packages = await getPackagesData();

const scors = {
  downloads: scorForItems(
    // toValue converts an item to a numeric value, in this case with a log10 scale
    (p) => Math.log10(p.downloads),
    packages,
  ),
  maintainers: scorForItems((p) => p.maintainers.length, packages),
};

// one way to calculate indivudual scores for each item
const scores = packages.map((p) => ({
  name: p.name,
  downloadScore: scors.downloads.forItem(p),
  maintainerScore: scors.maintainers.forItem(p),
}));

// or calculate the arithmetic mean per item
const scorePerItem = packages.map(createToMean(scors));

// or the weighted arithmetic mean
const weightedScorePerItem = packages.map(createToMean(
  scors,
  { downloads: 0.75, maintainers: 0.25 },
));

// or as a list wihtout keys
const scorsList = [
  scorForItems(
    (p) => Math.log10(p.downloads),
    packages,
  ),
  scorForItems((p) => p.maintainers.length, packages),
];

const weightedScorePerItemL = packages.map(
  createToMean(scorsList, [0.75, 0.25]),
);

// if you have many weights and some should be distributed:
distributeWeights(
  [0.5, undefined, undefined],
); // => [0.5, 0.25, 0.25]
distributeWeights(
  { first: 0.7, second: undefined, third: undefined },
); // => {first: 0.7, second: 0.15, third: 0.15}

TODOs

Contributions are welcome!