UNPKG

benford-law

Version:

A simple library to check if a dataset follows the Benford's law

123 lines (122 loc) 5.18 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.processBenfordLaw = exports.generateBenfordLawNumbers = exports.generateBenfordLawNumber = void 0; /** * Benford's Law expected probabilities for first digits (1-9) * Source: log10(1 + 1/d) where d is the digit */ const benfordProb = { '1': 0.301, '2': 0.176, '3': 0.125, '4': 0.097, '5': 0.079, '6': 0.067, '7': 0.058, '8': 0.051, '9': 0.046, }; /** * Range for generating Benford-distributed numbers * Numbers between 1 and 1000 follow Benford's law well */ const MIN_BENFORD_RANGE = 1; const MAX_BENFORD_RANGE = 1000; const generateRandomNumber = (minimum, maximum) => Math.random() * (maximum - minimum) + minimum; /** * Extracts the first significant digit from a number * @param number - The number to extract the first digit from * @returns The first significant digit (1-9), or throws an error if invalid * @throws Error if the number is zero, negative, NaN, or infinite */ const getFirstDigit = (number) => { if (!Number.isFinite(number)) { throw new Error('Number must be finite'); } if (number <= 0) { throw new Error('Number must be positive and non-zero'); } // Convert to string and remove decimal point to get first significant digit const absNumber = Math.abs(number); const normalized = absNumber >= 1 ? absNumber : absNumber * Math.pow(10, Math.ceil(-Math.log10(absNumber))); const firstDigit = parseInt(normalized.toString()[0], 10); if (firstDigit < 1 || firstDigit > 9) { throw new Error('Invalid first digit extracted'); } return firstDigit; }; /** * Generates a single random number that follows Benford's Law * Uses logarithmic distribution to ensure first digits follow Benford's Law * @returns A number between 1 and 1000 following Benford's Law */ const generateBenfordLawNumber = () => Math.exp(generateRandomNumber(Math.log(MIN_BENFORD_RANGE), Math.log(MAX_BENFORD_RANGE))); exports.generateBenfordLawNumber = generateBenfordLawNumber; /** * Generates an array of random numbers that follow Benford's Law * @param length - The number of random numbers to generate (must be > 0) * @returns An array of numbers following Benford's Law * @throws Error if length is not a positive integer */ const generateBenfordLawNumbers = (length) => { if (!Number.isInteger(length) || length <= 0) { throw new Error('Length must be a positive integer'); } // Immutable approach: use Array.from instead of push return Array.from({ length }, () => (0, exports.generateBenfordLawNumber)()); }; exports.generateBenfordLawNumbers = generateBenfordLawNumbers; /** * Analyzes a dataset to determine if it follows Benford's Law * @param numbers - Array of positive numbers to analyze * @param threshold - Maximum acceptable deviation from Benford's probabilities (default: 0.01) * @param benfordProbabilities - Expected probabilities for each first digit (default: standard Benford) * @returns Analysis results including whether the dataset follows Benford's Law * @throws Error if the array is empty or contains invalid numbers */ const processBenfordLaw = (numbers, threshold = 0.01, benfordProbabilities = benfordProb) => { // Validation if (!Array.isArray(numbers) || numbers.length === 0) { throw new Error('Numbers array must be non-empty'); } if (threshold <= 0 || threshold >= 1) { throw new Error('Threshold must be between 0 and 1'); } // Extract first digits (will throw if any number is invalid) const firstDigits = numbers.map(getFirstDigit); // Count occurrences of each first digit // Use mutable approach within function scope for performance const firstDigitCounts = {}; for (const digit of firstDigits) { const key = String(digit); firstDigitCounts[key] = (firstDigitCounts[key] || 0) + 1; } // Calculate probabilities const totalCount = numbers.length; const firstDigitProbabilities = {}; for (const [digit, count] of Object.entries(firstDigitCounts)) { firstDigitProbabilities[digit] = count / totalCount; } // Calculate accuracy (deviation from Benford's law) const firstDigitAccuracies = {}; for (const [digit, probability] of Object.entries(firstDigitProbabilities)) { const benfordProbability = benfordProbabilities[digit]; if (benfordProbability !== undefined) { firstDigitAccuracies[digit] = Math.abs(benfordProbability - probability); } } // Check if dataset follows Benford's law const isBenford = Object.entries(benfordProbabilities).every(([digit, probability]) => { const firstDigitProbability = firstDigitProbabilities[digit]; // If a digit doesn't appear at all, probability is 0 const actualProbability = firstDigitProbability || 0; return Math.abs(actualProbability - probability) < threshold; }); return { isFollowingBenfordLaw: isBenford, firstDigitProbabilities, firstDigitCounts, firstDigitAccuracies, }; }; exports.processBenfordLaw = processBenfordLaw;