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 samerange
: \ between0
(value <= min
) and1
(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
- If the range is "empty" (
- The user fully controls the conversion of
item
tovalue
(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
andmax
values,
but they can be derived fromitems
(also usingtoValue
, seegetItemRange
). - 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
Scor
s 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!
- publish to
deno.land/x/
and update docs - post about it and get feedback
- Add support for weighted sum?
- Add support for more kind of averages?
- publish to npm(?)