add skewness and kurtosis to value metric in insights explorer

This commit is contained in:
MaysWind
2026-04-16 01:24:07 +08:00
parent 02d8b132f5
commit 7a821abbb6
30 changed files with 181 additions and 54 deletions
+3 -2
View File
@@ -81,8 +81,9 @@ import {
import { NumeralSystem, DecimalSeparator } from '@/core/numeral.ts'; import { NumeralSystem, DecimalSeparator } from '@/core/numeral.ts';
import type { CurrencyPrependAndAppendText } from '@/core/currency.ts'; import type { CurrencyPrependAndAppendText } from '@/core/currency.ts';
import { DEFAULT_DECIMAL_NUMBER_COUNT } from '@/consts/numeral.ts'; import { DEFAULT_DECIMAL_NUMBER_COUNT, AMOUNT_FACTOR } from '@/consts/numeral.ts';
import { TRANSACTION_MIN_AMOUNT, TRANSACTION_MAX_AMOUNT } from '@/consts/transaction.ts'; import { TRANSACTION_MIN_AMOUNT, TRANSACTION_MAX_AMOUNT } from '@/consts/transaction.ts';
import { isNumber, replaceAll } from '@/lib/common.ts'; import { isNumber, replaceAll } from '@/lib/common.ts';
import { evaluateExpressionToAmount } from '@/lib/evaluator.ts'; import { evaluateExpressionToAmount } from '@/lib/evaluator.ts';
import type { ComponentDensity, InputVariant } from '@/lib/ui/desktop.ts'; import type { ComponentDensity, InputVariant } from '@/lib/ui/desktop.ts';
@@ -297,7 +298,7 @@ function getFormattedValue(value: number): string {
function getDisplayCurrencyPrependAndAppendText(): CurrencyPrependAndAppendText | null { function getDisplayCurrencyPrependAndAppendText(): CurrencyPrependAndAppendText | null {
const numericCurrentValue = parseAmountFromLocalizedNumerals(currentValue.value); const numericCurrentValue = parseAmountFromLocalizedNumerals(currentValue.value);
const isPlural = numericCurrentValue !== 100 && numericCurrentValue !== -100; const isPlural = numericCurrentValue !== AMOUNT_FACTOR && numericCurrentValue !== -AMOUNT_FACTOR;
return getAmountPrependAndAppendText(props.currency, isPlural); return getAmountPrependAndAppendText(props.currency, isPlural);
} }
+2 -1
View File
@@ -83,6 +83,7 @@ import { useI18n } from '@/locales/helpers.ts';
import { useI18nUIComponents, isiOS } from '@/lib/ui/mobile.ts'; import { useI18nUIComponents, isiOS } from '@/lib/ui/mobile.ts';
import { NumeralSystem } from '@/core/numeral.ts'; import { NumeralSystem } from '@/core/numeral.ts';
import { AMOUNT_FACTOR } from '@/consts/numeral.ts';
import { ALL_CURRENCIES } from '@/consts/currency.ts'; import { ALL_CURRENCIES } from '@/consts/currency.ts';
import { isNumber } from '@/lib/common.ts'; import { isNumber } from '@/lib/common.ts';
import logger from '@/lib/logger.ts'; import logger from '@/lib/logger.ts';
@@ -385,7 +386,7 @@ function confirm(): boolean {
finalValue = previous - current; finalValue = previous - current;
break; break;
case '×': case '×':
finalValue = Math.trunc(previous * current / 100); finalValue = Math.trunc(previous * current / AMOUNT_FACTOR);
break; break;
default: default:
finalValue = previous; finalValue = previous;
+1
View File
@@ -2,6 +2,7 @@ import type { HiddenAmount } from '@/core/numeral.ts';
export const DEFAULT_DECIMAL_NUMBER_COUNT: number = 2; export const DEFAULT_DECIMAL_NUMBER_COUNT: number = 2;
export const MAX_SUPPORTED_DECIMAL_NUMBER_COUNT: number = 2; export const MAX_SUPPORTED_DECIMAL_NUMBER_COUNT: number = 2;
export const AMOUNT_FACTOR: number = 10 ** MAX_SUPPORTED_DECIMAL_NUMBER_COUNT;
export const DISPLAY_HIDDEN_AMOUNT: HiddenAmount = '***'; export const DISPLAY_HIDDEN_AMOUNT: HiddenAmount = '***';
export const INCOMPLETE_AMOUNT_SUFFIX: string = '+'; export const INCOMPLETE_AMOUNT_SUFFIX: string = '+';
+5 -1
View File
@@ -324,7 +324,9 @@ export enum TransactionExplorerValueMetricType {
SourceAmountInterquartileRange = 'sourceAmountInterquartileRange', SourceAmountInterquartileRange = 'sourceAmountInterquartileRange',
SourceAmountVariance = 'sourceAmountVariance', SourceAmountVariance = 'sourceAmountVariance',
SourceAmountStandardDeviation = 'sourceAmountStandardDeviation', SourceAmountStandardDeviation = 'sourceAmountStandardDeviation',
SourceAmountCoefficientOfVariation = 'sourceAmountCoefficientOfVariation' SourceAmountCoefficientOfVariation = 'sourceAmountCoefficientOfVariation',
SourceAmountSkewness = 'sourceAmountSkewness',
SourceAmountKurtosis = 'sourceAmountKurtosis'
} }
export class TransactionExplorerValueMetric implements NameValue { export class TransactionExplorerValueMetric implements NameValue {
@@ -356,6 +358,8 @@ export class TransactionExplorerValueMetric implements NameValue {
public static readonly SourceAmountVariance = new TransactionExplorerValueMetric('Variance', TransactionExplorerValueMetricType.SourceAmountVariance, false, false, false); public static readonly SourceAmountVariance = new TransactionExplorerValueMetric('Variance', TransactionExplorerValueMetricType.SourceAmountVariance, false, false, false);
public static readonly SourceAmountStandardDeviation = new TransactionExplorerValueMetric('Standard Deviation', TransactionExplorerValueMetricType.SourceAmountStandardDeviation, false, false, false); public static readonly SourceAmountStandardDeviation = new TransactionExplorerValueMetric('Standard Deviation', TransactionExplorerValueMetricType.SourceAmountStandardDeviation, false, false, false);
public static readonly SourceAmountCoefficientOfVariation = new TransactionExplorerValueMetric('Coefficient of Variation', TransactionExplorerValueMetricType.SourceAmountCoefficientOfVariation, false, false, false); public static readonly SourceAmountCoefficientOfVariation = new TransactionExplorerValueMetric('Coefficient of Variation', TransactionExplorerValueMetricType.SourceAmountCoefficientOfVariation, false, false, false);
public static readonly SourceAmountSkewness = new TransactionExplorerValueMetric('Skewness', TransactionExplorerValueMetricType.SourceAmountSkewness, false, false, false);
public static readonly SourceAmountKurtosis = new TransactionExplorerValueMetric('Kurtosis', TransactionExplorerValueMetricType.SourceAmountKurtosis, false, false, false);
public static readonly Default = TransactionExplorerValueMetric.SourceAmountSum; public static readonly Default = TransactionExplorerValueMetric.SourceAmountSum;
+2 -1
View File
@@ -1,3 +1,4 @@
import { AMOUNT_FACTOR } from '@/consts/numeral.ts';
import { TRANSACTION_MIN_AMOUNT, TRANSACTION_MAX_AMOUNT } from '../consts/transaction.ts'; import { TRANSACTION_MIN_AMOUNT, TRANSACTION_MAX_AMOUNT } from '../consts/transaction.ts';
import { replaceAll } from './common.ts'; import { replaceAll } from './common.ts';
@@ -10,7 +11,7 @@ type OperatorAndParenthesis = Operator | '(' | ')';
const maxAllowedDecimalCount = 6; const maxAllowedDecimalCount = 6;
const normalizeFactor: number = 1000000; const normalizeFactor: number = 1000000;
const normalizedDecimalsMaxZeroString: string = '000000'; const normalizedDecimalsMaxZeroString: string = '000000';
const normalizedNumberToAmountFactor: number = 10000; // 1000000 / 100 const normalizedNumberToAmountFactor: number = normalizeFactor / AMOUNT_FACTOR;
const operatorPriority: Record<Operator, number> = { const operatorPriority: Record<Operator, number> = {
'+': 1, '+': 1,
+79
View File
@@ -1,3 +1,5 @@
import { reversed } from '@/core/base.ts';
export function mean<T>(values: T[], valueFn: (item: T) => number): number { export function mean<T>(values: T[], valueFn: (item: T) => number): number {
if (values.length < 1) { if (values.length < 1) {
return 0; return 0;
@@ -59,3 +61,80 @@ export function sumMaxN<T>(sortedValues: T[], n: number, valueFn: (item: T) => n
return sum; return sum;
} }
export function cumulativePercentage<T>(sortedValues: T[], percentageThreshold: number, totalValue: number, valueFn: (item: T) => number): number {
if (sortedValues.length < 1 || percentageThreshold < 0 || percentageThreshold > 1) {
return 0;
}
const thresholdValue: number = percentageThreshold * totalValue;
let cumulativeValue: number = 0;
let cumulativeCount: number = 0;
for (const item of reversed(sortedValues)) {
cumulativeValue += valueFn(item);
cumulativeCount++;
if (cumulativeValue >= thresholdValue) {
return 100.0 * cumulativeCount / sortedValues.length;
}
}
return 0;
}
export function varianceAndStandardDeviation<T>(values: T[], meanValue: number, valueFn: (item: T) => number): { variance: number; standardDeviation: number } {
if (values.length < 1) {
return { variance: 0, standardDeviation: 0 };
}
let sumOfSquaredDifferences: number = 0;
for (const item of values) {
const difference: number = valueFn(item) - meanValue;
sumOfSquaredDifferences += difference * difference;
}
const variance: number = sumOfSquaredDifferences / values.length;
const standardDeviation: number = Math.sqrt(variance);
return { variance, standardDeviation };
}
export function coefficientOfVariation(standardDeviation: number, meanValue: number): number | undefined {
if (meanValue === 0) {
return undefined;
}
return standardDeviation / meanValue;
}
export function skewness<T>(values: T[], meanValue: number, standardDeviation: number, valueFn: (item: T) => number): number {
if (values.length < 1 || standardDeviation === 0) {
return 0;
}
let sumOfCubedDifferences: number = 0;
for (const item of values) {
const difference: number = valueFn(item) - meanValue;
sumOfCubedDifferences += Math.pow(difference, 3);
}
return sumOfCubedDifferences / (values.length * Math.pow(standardDeviation, 3));
}
export function kurtosis<T>(values: T[], meanValue: number, variance: number, valueFn: (item: T) => number): number {
if (values.length < 1 || variance === 0) {
return 0;
}
let sumOfQuarticDifferences: number = 0;
for (const item of values) {
const difference: number = valueFn(item) - meanValue;
sumOfQuarticDifferences += Math.pow(difference, 4);
}
return sumOfQuarticDifferences / (values.length * Math.pow(variance, 2));
}
+10 -7
View File
@@ -6,6 +6,8 @@ import {
DigitGroupingSymbol DigitGroupingSymbol
} from '@/core/numeral.ts'; } from '@/core/numeral.ts';
import { AMOUNT_FACTOR } from '@/consts/numeral.ts';
import { DEFAULT_DECIMAL_NUMBER_COUNT, MAX_SUPPORTED_DECIMAL_NUMBER_COUNT, DISPLAY_HIDDEN_AMOUNT } from '@/consts/numeral.ts'; import { DEFAULT_DECIMAL_NUMBER_COUNT, MAX_SUPPORTED_DECIMAL_NUMBER_COUNT, DISPLAY_HIDDEN_AMOUNT } from '@/consts/numeral.ts';
import { isDefined, isString, isNumber, replaceAll, removeAll } from './common.ts'; import { isDefined, isString, isNumber, replaceAll, removeAll } from './common.ts';
@@ -115,7 +117,7 @@ export function parseAmount(str: string, options: NumberFormatOptions): number {
let decimalSeparatorPos = str.indexOf(decimalSeparator); let decimalSeparatorPos = str.indexOf(decimalSeparator);
if (decimalSeparatorPos < 0) { if (decimalSeparatorPos < 0) {
return sign * numeralSystem.parseInt(str) * 100; return sign * numeralSystem.parseInt(str) * AMOUNT_FACTOR;
} else if (decimalSeparatorPos === 0) { } else if (decimalSeparatorPos === 0) {
str = numeralSystem.digitZero + str; str = numeralSystem.digitZero + str;
decimalSeparatorPos++; decimalSeparatorPos++;
@@ -125,13 +127,13 @@ export function parseAmount(str: string, options: NumberFormatOptions): number {
const decimals = str.substring(decimalSeparatorPos + 1, str.length); const decimals = str.substring(decimalSeparatorPos + 1, str.length);
if (decimals.length < 1) { if (decimals.length < 1) {
return sign * numeralSystem.parseInt(integer) * 100; return sign * numeralSystem.parseInt(integer) * AMOUNT_FACTOR;
} else if (decimals.length === 1) { } else if (decimals.length === 1) {
return sign * numeralSystem.parseInt(integer) * 100 + sign * numeralSystem.parseInt(decimals) * 10; return sign * numeralSystem.parseInt(integer) * AMOUNT_FACTOR + sign * numeralSystem.parseInt(decimals) * AMOUNT_FACTOR / 10;
} else if (decimals.length === 2) { } else if (decimals.length === 2) {
return sign * numeralSystem.parseInt(integer) * 100 + sign * numeralSystem.parseInt(decimals); return sign * numeralSystem.parseInt(integer) * AMOUNT_FACTOR + sign * numeralSystem.parseInt(decimals);
} else { } else {
return sign * numeralSystem.parseInt(integer) * 100 + sign * numeralSystem.parseInt(decimals.substring(0, 2)); return sign * numeralSystem.parseInt(integer) * AMOUNT_FACTOR + sign * numeralSystem.parseInt(decimals.substring(0, 2));
} }
} }
@@ -252,9 +254,10 @@ export function formatPercent(value: number, precision: number, lowPrecisionValu
export function getAmountWithDecimalNumberCount(amount: number, decimalNumberCount: number): number { export function getAmountWithDecimalNumberCount(amount: number, decimalNumberCount: number): number {
if (decimalNumberCount === 0) { if (decimalNumberCount === 0) {
return Math.trunc(amount / 100) * 100; return Math.trunc(amount / AMOUNT_FACTOR) * AMOUNT_FACTOR;
} else if (decimalNumberCount === 1) { } else if (decimalNumberCount === 1) {
return Math.trunc(amount / 10) * 10; const factor = AMOUNT_FACTOR / 10;
return Math.trunc(amount / factor) * factor;
} }
return amount; return amount;
+2
View File
@@ -1824,6 +1824,8 @@
"Variance": "Varianz", "Variance": "Varianz",
"Standard Deviation": "Standardabweichung", "Standard Deviation": "Standardabweichung",
"Coefficient of Variation": "Variationskoeffizient", "Coefficient of Variation": "Variationskoeffizient",
"Skewness": "Skewness",
"Kurtosis": "Kurtosis",
"Account List": "Kontoliste", "Account List": "Kontoliste",
"This Week": "Diese Woche", "This Week": "Diese Woche",
"This Month": "Dieser Monat", "This Month": "Dieser Monat",
+2
View File
@@ -1824,6 +1824,8 @@
"Variance": "Variance", "Variance": "Variance",
"Standard Deviation": "Standard Deviation", "Standard Deviation": "Standard Deviation",
"Coefficient of Variation": "Coefficient of Variation", "Coefficient of Variation": "Coefficient of Variation",
"Skewness": "Skewness",
"Kurtosis": "Kurtosis",
"Account List": "Account List", "Account List": "Account List",
"This Week": "This Week", "This Week": "This Week",
"This Month": "This Month", "This Month": "This Month",
+2
View File
@@ -1824,6 +1824,8 @@
"Variance": "Variance", "Variance": "Variance",
"Standard Deviation": "Standard Deviation", "Standard Deviation": "Standard Deviation",
"Coefficient of Variation": "Coefficient of Variation", "Coefficient of Variation": "Coefficient of Variation",
"Skewness": "Skewness",
"Kurtosis": "Kurtosis",
"Account List": "Lista de Cuentas", "Account List": "Lista de Cuentas",
"This Week": "Esta Semana", "This Week": "Esta Semana",
"This Month": "Este Mes", "This Month": "Este Mes",
+2
View File
@@ -1824,6 +1824,8 @@
"Variance": "Variance", "Variance": "Variance",
"Standard Deviation": "Standard Deviation", "Standard Deviation": "Standard Deviation",
"Coefficient of Variation": "Coefficient of Variation", "Coefficient of Variation": "Coefficient of Variation",
"Skewness": "Skewness",
"Kurtosis": "Kurtosis",
"Account List": "Liste des comptes", "Account List": "Liste des comptes",
"This Week": "Cette semaine", "This Week": "Cette semaine",
"This Month": "Ce mois", "This Month": "Ce mois",
+2 -2
View File
@@ -170,7 +170,7 @@ import {
import type { LocaleDefaultSettings } from '@/core/setting.ts'; import type { LocaleDefaultSettings } from '@/core/setting.ts';
import type { ErrorResponse } from '@/core/api.ts'; import type { ErrorResponse } from '@/core/api.ts';
import { DISPLAY_HIDDEN_AMOUNT, INCOMPLETE_AMOUNT_SUFFIX } from '@/consts/numeral.ts'; import { AMOUNT_FACTOR, DISPLAY_HIDDEN_AMOUNT, INCOMPLETE_AMOUNT_SUFFIX } from '@/consts/numeral.ts';
import { UTC_TIMEZONE, ALL_TIMEZONES } from '@/consts/timezone.ts'; import { UTC_TIMEZONE, ALL_TIMEZONES } from '@/consts/timezone.ts';
import { ALL_CURRENCIES } from '@/consts/currency.ts'; import { ALL_CURRENCIES } from '@/consts/currency.ts';
import { DEFAULT_EXPENSE_CATEGORIES, DEFAULT_INCOME_CATEGORIES, DEFAULT_TRANSFER_CATEGORIES } from '@/consts/category.ts'; import { DEFAULT_EXPENSE_CATEGORIES, DEFAULT_INCOME_CATEGORIES, DEFAULT_TRANSFER_CATEGORIES } from '@/consts/category.ts';
@@ -2189,7 +2189,7 @@ export function useI18n() {
const currencyName = getCurrencyName(finalCurrencyCode); const currencyName = getCurrencyName(finalCurrencyCode);
if (isNumber(value)) { if (isNumber(value)) {
const isPlural: boolean = value !== 100 && value !== -100; const isPlural: boolean = value !== AMOUNT_FACTOR && value !== -AMOUNT_FACTOR;
const textualValue = formatAmount(value, numberFormatOptions); const textualValue = formatAmount(value, numberFormatOptions);
if (!finalCurrencyCode) { if (!finalCurrencyCode) {
+2
View File
@@ -1824,6 +1824,8 @@
"Variance": "Variance", "Variance": "Variance",
"Standard Deviation": "Standard Deviation", "Standard Deviation": "Standard Deviation",
"Coefficient of Variation": "Coefficient of Variation", "Coefficient of Variation": "Coefficient of Variation",
"Skewness": "Skewness",
"Kurtosis": "Kurtosis",
"Account List": "Elenco account", "Account List": "Elenco account",
"This Week": "Questa settimana", "This Week": "Questa settimana",
"This Month": "Questo mese", "This Month": "Questo mese",
+2
View File
@@ -1824,6 +1824,8 @@
"Variance": "Variance", "Variance": "Variance",
"Standard Deviation": "Standard Deviation", "Standard Deviation": "Standard Deviation",
"Coefficient of Variation": "Coefficient of Variation", "Coefficient of Variation": "Coefficient of Variation",
"Skewness": "Skewness",
"Kurtosis": "Kurtosis",
"Account List": "口座リスト", "Account List": "口座リスト",
"This Week": "今週", "This Week": "今週",
"This Month": "今月", "This Month": "今月",
+2
View File
@@ -1824,6 +1824,8 @@
"Variance": "Variance", "Variance": "Variance",
"Standard Deviation": "Standard Deviation", "Standard Deviation": "Standard Deviation",
"Coefficient of Variation": "Coefficient of Variation", "Coefficient of Variation": "Coefficient of Variation",
"Skewness": "Skewness",
"Kurtosis": "Kurtosis",
"Account List": "ಖಾತೆಗಳ ಪಟ್ಟಿ", "Account List": "ಖಾತೆಗಳ ಪಟ್ಟಿ",
"This Week": "ಈ ವಾರ", "This Week": "ಈ ವಾರ",
"This Month": "ಈ ತಿಂಗಳು", "This Month": "ಈ ತಿಂಗಳು",
+2
View File
@@ -1824,6 +1824,8 @@
"Variance": "Variance", "Variance": "Variance",
"Standard Deviation": "Standard Deviation", "Standard Deviation": "Standard Deviation",
"Coefficient of Variation": "Coefficient of Variation", "Coefficient of Variation": "Coefficient of Variation",
"Skewness": "Skewness",
"Kurtosis": "Kurtosis",
"Account List": "계좌 목록", "Account List": "계좌 목록",
"This Week": "이번 주", "This Week": "이번 주",
"This Month": "이번 달", "This Month": "이번 달",
+2
View File
@@ -1824,6 +1824,8 @@
"Variance": "Variance", "Variance": "Variance",
"Standard Deviation": "Standard Deviation", "Standard Deviation": "Standard Deviation",
"Coefficient of Variation": "Coefficient of Variation", "Coefficient of Variation": "Coefficient of Variation",
"Skewness": "Skewness",
"Kurtosis": "Kurtosis",
"Account List": "Rekeningenlijst", "Account List": "Rekeningenlijst",
"This Week": "Deze week", "This Week": "Deze week",
"This Month": "Deze maand", "This Month": "Deze maand",
+2
View File
@@ -1824,6 +1824,8 @@
"Variance": "Variância", "Variance": "Variância",
"Standard Deviation": "Desvio Padrão", "Standard Deviation": "Desvio Padrão",
"Coefficient of Variation": "Coeficiente de Variação", "Coefficient of Variation": "Coeficiente de Variação",
"Skewness": "Skewness",
"Kurtosis": "Kurtosis",
"Account List": "Lista de Contas", "Account List": "Lista de Contas",
"This Week": "Esta Semana", "This Week": "Esta Semana",
"This Month": "Este Mês", "This Month": "Este Mês",
+2
View File
@@ -1824,6 +1824,8 @@
"Variance": "Variance", "Variance": "Variance",
"Standard Deviation": "Standard Deviation", "Standard Deviation": "Standard Deviation",
"Coefficient of Variation": "Coefficient of Variation", "Coefficient of Variation": "Coefficient of Variation",
"Skewness": "Skewness",
"Kurtosis": "Kurtosis",
"Account List": "Список счетов", "Account List": "Список счетов",
"This Week": "На этой неделе", "This Week": "На этой неделе",
"This Month": "В этом месяце", "This Month": "В этом месяце",
+2
View File
@@ -1824,6 +1824,8 @@
"Variance": "Variance", "Variance": "Variance",
"Standard Deviation": "Standard Deviation", "Standard Deviation": "Standard Deviation",
"Coefficient of Variation": "Coefficient of Variation", "Coefficient of Variation": "Coefficient of Variation",
"Skewness": "Skewness",
"Kurtosis": "Kurtosis",
"Account List": "Seznam računov", "Account List": "Seznam računov",
"This Week": "Ta teden", "This Week": "Ta teden",
"This Month": "Ta mesec", "This Month": "Ta mesec",
+2
View File
@@ -1824,6 +1824,8 @@
"Variance": "Variance", "Variance": "Variance",
"Standard Deviation": "Standard Deviation", "Standard Deviation": "Standard Deviation",
"Coefficient of Variation": "Coefficient of Variation", "Coefficient of Variation": "Coefficient of Variation",
"Skewness": "Skewness",
"Kurtosis": "Kurtosis",
"Account List": "கணக்குகளின் பட்டியல்", "Account List": "கணக்குகளின் பட்டியல்",
"This Week": "இந்த வாரம்", "This Week": "இந்த வாரம்",
"This Month": "இந்த மாதம்", "This Month": "இந்த மாதம்",
+2
View File
@@ -1824,6 +1824,8 @@
"Variance": "Variance", "Variance": "Variance",
"Standard Deviation": "Standard Deviation", "Standard Deviation": "Standard Deviation",
"Coefficient of Variation": "Coefficient of Variation", "Coefficient of Variation": "Coefficient of Variation",
"Skewness": "Skewness",
"Kurtosis": "Kurtosis",
"Account List": "รายการบัญชี", "Account List": "รายการบัญชี",
"This Week": "สัปดาห์นี้", "This Week": "สัปดาห์นี้",
"This Month": "เดือนนี้", "This Month": "เดือนนี้",
+2
View File
@@ -1824,6 +1824,8 @@
"Variance": "Variance", "Variance": "Variance",
"Standard Deviation": "Standard Deviation", "Standard Deviation": "Standard Deviation",
"Coefficient of Variation": "Coefficient of Variation", "Coefficient of Variation": "Coefficient of Variation",
"Skewness": "Skewness",
"Kurtosis": "Kurtosis",
"Account List": "Hesap Listesi", "Account List": "Hesap Listesi",
"This Week": "Bu Hafta", "This Week": "Bu Hafta",
"This Month": "Bu Ay", "This Month": "Bu Ay",
+2
View File
@@ -1824,6 +1824,8 @@
"Variance": "Variance", "Variance": "Variance",
"Standard Deviation": "Standard Deviation", "Standard Deviation": "Standard Deviation",
"Coefficient of Variation": "Coefficient of Variation", "Coefficient of Variation": "Coefficient of Variation",
"Skewness": "Skewness",
"Kurtosis": "Kurtosis",
"Account List": "Список рахунків", "Account List": "Список рахунків",
"This Week": "Цього тижня", "This Week": "Цього тижня",
"This Month": "Цього місяця", "This Month": "Цього місяця",
+2
View File
@@ -1824,6 +1824,8 @@
"Variance": "Variance", "Variance": "Variance",
"Standard Deviation": "Standard Deviation", "Standard Deviation": "Standard Deviation",
"Coefficient of Variation": "Coefficient of Variation", "Coefficient of Variation": "Coefficient of Variation",
"Skewness": "Skewness",
"Kurtosis": "Kurtosis",
"Account List": "Danh sách tài khoản", "Account List": "Danh sách tài khoản",
"This Week": "Tuần này", "This Week": "Tuần này",
"This Month": "Tháng này", "This Month": "Tháng này",
+2
View File
@@ -1824,6 +1824,8 @@
"Variance": "方差", "Variance": "方差",
"Standard Deviation": "标准差", "Standard Deviation": "标准差",
"Coefficient of Variation": "变异系数", "Coefficient of Variation": "变异系数",
"Skewness": "偏度",
"Kurtosis": "峰度",
"Account List": "账户列表", "Account List": "账户列表",
"This Week": "本周", "This Week": "本周",
"This Month": "本月", "This Month": "本月",
+2
View File
@@ -1824,6 +1824,8 @@
"Variance": "變異數", "Variance": "變異數",
"Standard Deviation": "標準差", "Standard Deviation": "標準差",
"Coefficient of Variation": "變異係數", "Coefficient of Variation": "變異係數",
"Skewness": "偏度",
"Kurtosis": "峰度",
"Account List": "帳戶清單", "Account List": "帳戶清單",
"This Week": "本週", "This Week": "本週",
"This Month": "本月", "This Month": "本月",
+35 -38
View File
@@ -8,7 +8,7 @@ import { useTransactionCategoriesStore } from './transactionCategory.ts';
import { useTransactionTagsStore } from './transactionTag.ts'; import { useTransactionTagsStore } from './transactionTag.ts';
import { useExchangeRatesStore } from './exchangeRates.ts'; import { useExchangeRatesStore } from './exchangeRates.ts';
import { type BeforeResolveFunction, itemAndIndex, reversed, keys, values } from '@/core/base.ts'; import { type BeforeResolveFunction, itemAndIndex, keys, values } from '@/core/base.ts';
import { NumeralSystem, AmountFilterType } from '@/core/numeral.ts'; import { NumeralSystem, AmountFilterType } from '@/core/numeral.ts';
import { type DateTime, DateRangeScene, DateRange } from '@/core/datetime.ts'; import { type DateTime, DateRangeScene, DateRange } from '@/core/datetime.ts';
import { TimezoneTypeForStatistics } from '@/core/timezone.ts'; import { TimezoneTypeForStatistics } from '@/core/timezone.ts';
@@ -20,6 +20,7 @@ import {
TransactionExplorerValueMetric, TransactionExplorerValueMetric,
DEFAULT_TRANSACTION_EXPLORER_DATE_RANGE DEFAULT_TRANSACTION_EXPLORER_DATE_RANGE
} from '@/core/explorer.ts'; } from '@/core/explorer.ts';
import { AMOUNT_FACTOR } from '@/consts/numeral.ts';
import { ALL_CURRENCIES } from '@/consts/currency.ts'; import { ALL_CURRENCIES } from '@/consts/currency.ts';
import { type Account } from '@/models/account.ts'; import { type Account } from '@/models/account.ts';
@@ -46,7 +47,12 @@ import {
import { import {
median, median,
percentile, percentile,
sumMaxN sumMaxN,
cumulativePercentage,
varianceAndStandardDeviation,
coefficientOfVariation,
skewness,
kurtosis
} from '@/lib/math.ts'; } from '@/lib/math.ts';
import { import {
getUtcOffsetByUtcOffsetMinutes, getUtcOffsetByUtcOffsetMinutes,
@@ -726,26 +732,15 @@ export const useExplorersStore = defineStore('explorers', () => {
} }
if (sourceAmounts.length > 0) { if (sourceAmounts.length > 0) {
const eightyPercentAmountThreshold: number = 0.8 * statisticData.totalAmount; statisticData.transactionsFor80PercentAmount = cumulativePercentage(sourceAmounts, 0.8, statisticData.totalAmount, item => item);
let cumulativeAmount: number = 0;
let cumulativeCount: number = 0;
for (const amount of reversed(sourceAmounts)) {
cumulativeAmount += amount;
cumulativeCount++;
if (cumulativeAmount >= eightyPercentAmountThreshold) {
statisticData.transactionsFor80PercentAmount = 100.0 * cumulativeCount / sourceAmounts.length;
break;
}
}
} }
if (sourceAmounts.length > 0) { if (sourceAmounts.length > 0) {
const averageAmountForVarianceCalculation: number = statisticData.totalAmount / sourceAmounts.length / 100.0; const averageAmountForVarianceCalculation: number = statisticData.totalAmount / sourceAmounts.length / AMOUNT_FACTOR;
const sumOfSquaredDifferences: number = sourceAmounts.reduce((sum, amount) => sum + Math.pow(amount / 100.0 - averageAmountForVarianceCalculation, 2), 0); const { variance, standardDeviation } = varianceAndStandardDeviation(sourceAmounts, averageAmountForVarianceCalculation, item => item / AMOUNT_FACTOR);
statisticData.variance = sumOfSquaredDifferences / sourceAmounts.length; statisticData.variance = variance;
statisticData.standardDeviation = Math.sqrt(statisticData.variance); statisticData.standardDeviation = standardDeviation;
statisticData.coefficientOfVariation = averageAmountForVarianceCalculation !== 0 ? statisticData.standardDeviation / averageAmountForVarianceCalculation : undefined; statisticData.coefficientOfVariation = coefficientOfVariation(standardDeviation, averageAmountForVarianceCalculation);
} }
return statisticData; return statisticData;
@@ -892,7 +887,12 @@ export const useExplorersStore = defineStore('explorers', () => {
} else { } else {
value = 0; value = 0;
} }
} else if (valueMetric === TransactionExplorerValueMetric.SourceAmountQ1Amount || valueMetric === TransactionExplorerValueMetric.SourceAmountQ3Amount || valueMetric === TransactionExplorerValueMetric.SourceAmount10thPercentile || valueMetric === TransactionExplorerValueMetric.SourceAmount90thPercentile || valueMetric === TransactionExplorerValueMetric.SourceAmount95thPercentile || valueMetric === TransactionExplorerValueMetric.SourceAmount99thPercentile) { } else if (valueMetric === TransactionExplorerValueMetric.SourceAmountQ1Amount
|| valueMetric === TransactionExplorerValueMetric.SourceAmountQ3Amount
|| valueMetric === TransactionExplorerValueMetric.SourceAmount10thPercentile
|| valueMetric === TransactionExplorerValueMetric.SourceAmount90thPercentile
|| valueMetric === TransactionExplorerValueMetric.SourceAmount95thPercentile
|| valueMetric === TransactionExplorerValueMetric.SourceAmount99thPercentile) {
if (allSourceAmountsInDefaultCurrency.length > 0) { if (allSourceAmountsInDefaultCurrency.length > 0) {
allSourceAmountsInDefaultCurrency.sort((a, b) => a - b); allSourceAmountsInDefaultCurrency.sort((a, b) => a - b);
@@ -932,18 +932,7 @@ export const useExplorersStore = defineStore('explorers', () => {
} else if (valueMetric === TransactionExplorerValueMetric.TransactionsForEightyPercentOfSourceAmount) { } else if (valueMetric === TransactionExplorerValueMetric.TransactionsForEightyPercentOfSourceAmount) {
if (allSourceAmountsInDefaultCurrency.length > 0) { if (allSourceAmountsInDefaultCurrency.length > 0) {
allSourceAmountsInDefaultCurrency.sort((a, b) => a - b); allSourceAmountsInDefaultCurrency.sort((a, b) => a - b);
const eightyPercentAmountThreshold: number = 0.8 * totalSourceAmountSumInDefaultCurrency; value = cumulativePercentage(allSourceAmountsInDefaultCurrency, 0.8, totalSourceAmountSumInDefaultCurrency, item => item);
let cumulativeAmount: number = 0;
let cumulativeCount: number = 0;
for (const amount of reversed(allSourceAmountsInDefaultCurrency)) {
cumulativeAmount += amount;
cumulativeCount++;
if (cumulativeAmount >= eightyPercentAmountThreshold) {
value = 100.0 * cumulativeCount / allSourceAmountsInDefaultCurrency.length;
break;
}
}
} else { } else {
value = 0; value = 0;
} }
@@ -962,17 +951,25 @@ export const useExplorersStore = defineStore('explorers', () => {
} else { } else {
value = 0; value = 0;
} }
} else if (valueMetric === TransactionExplorerValueMetric.SourceAmountVariance || valueMetric === TransactionExplorerValueMetric.SourceAmountStandardDeviation || valueMetric === TransactionExplorerValueMetric.SourceAmountCoefficientOfVariation) { } else if (valueMetric === TransactionExplorerValueMetric.SourceAmountVariance
|| valueMetric === TransactionExplorerValueMetric.SourceAmountStandardDeviation
|| valueMetric === TransactionExplorerValueMetric.SourceAmountCoefficientOfVariation
|| valueMetric === TransactionExplorerValueMetric.SourceAmountSkewness
|| valueMetric === TransactionExplorerValueMetric.SourceAmountKurtosis) {
if (allSourceAmountsInDefaultCurrency.length > 0) { if (allSourceAmountsInDefaultCurrency.length > 0) {
const averageSourceAmountInDefaultCurrency = totalSourceAmountSumInDefaultCurrency / allSourceAmountsInDefaultCurrency.length / 100.0; const averageSourceAmountInDefaultCurrency = totalSourceAmountSumInDefaultCurrency / allSourceAmountsInDefaultCurrency.length / AMOUNT_FACTOR;
const sumOfSquaredDifferences = allSourceAmountsInDefaultCurrency.reduce((sum, amount) => sum + Math.pow(amount / 100.0 - averageSourceAmountInDefaultCurrency, 2), 0); const { variance, standardDeviation } = varianceAndStandardDeviation(allSourceAmountsInDefaultCurrency, averageSourceAmountInDefaultCurrency, item => item / AMOUNT_FACTOR);
if (valueMetric === TransactionExplorerValueMetric.SourceAmountVariance) { if (valueMetric === TransactionExplorerValueMetric.SourceAmountVariance) {
value = sumOfSquaredDifferences / allSourceAmountsInDefaultCurrency.length value = variance;
} else if (valueMetric === TransactionExplorerValueMetric.SourceAmountStandardDeviation) { } else if (valueMetric === TransactionExplorerValueMetric.SourceAmountStandardDeviation) {
value = Math.sqrt(sumOfSquaredDifferences / allSourceAmountsInDefaultCurrency.length); value = standardDeviation;
} else if (valueMetric === TransactionExplorerValueMetric.SourceAmountCoefficientOfVariation) { } else if (valueMetric === TransactionExplorerValueMetric.SourceAmountCoefficientOfVariation) {
value = averageSourceAmountInDefaultCurrency !== 0 ? Math.sqrt(sumOfSquaredDifferences / allSourceAmountsInDefaultCurrency.length) / averageSourceAmountInDefaultCurrency : 0; value = coefficientOfVariation(standardDeviation, averageSourceAmountInDefaultCurrency) ?? 0;
} else if (valueMetric === TransactionExplorerValueMetric.SourceAmountSkewness) {
value = skewness(allSourceAmountsInDefaultCurrency, averageSourceAmountInDefaultCurrency, standardDeviation, item => item / AMOUNT_FACTOR);
} else if (valueMetric === TransactionExplorerValueMetric.SourceAmountKurtosis) {
value = kurtosis(allSourceAmountsInDefaultCurrency, averageSourceAmountInDefaultCurrency, variance, item => item / AMOUNT_FACTOR);
} }
} else { } else {
value = 0; value = 0;
+2 -1
View File
@@ -171,6 +171,7 @@ import { useExchangeRatesPageBase } from '@/views/base/ExchangeRatesPageBase.ts'
import { useExchangeRatesStore } from '@/stores/exchangeRates.ts'; import { useExchangeRatesStore } from '@/stores/exchangeRates.ts';
import { NumeralSystem } from '@/core/numeral.ts'; import { NumeralSystem } from '@/core/numeral.ts';
import { AMOUNT_FACTOR } from '@/consts/numeral.ts';
import type { LocalizedLatestExchangeRate } from '@/models/exchange_rate.ts'; import type { LocalizedLatestExchangeRate } from '@/models/exchange_rate.ts';
@@ -302,7 +303,7 @@ function getFinalConvertedAmount(toExchangeRate: LocalizedLatestExchangeRate, di
let exchangeRateAmount: number | '' | null = 0; let exchangeRateAmount: number | '' | null = 0;
try { try {
exchangeRateAmount = getConvertedAmount(baseAmount.value / 100, fromExchangeRate, toExchangeRate); exchangeRateAmount = getConvertedAmount(baseAmount.value / AMOUNT_FACTOR, fromExchangeRate, toExchangeRate);
} catch (ex) { } catch (ex) {
exchangeRateAmount = 0; exchangeRateAmount = 0;
logger.warn('failed to convert amount by exchange rates, original base amount is ' + baseAmount.value, ex) logger.warn('failed to convert amount by exchange rates, original base amount is ' + baseAmount.value, ex)
+2 -1
View File
@@ -138,6 +138,7 @@ import { useExchangeRatesStore } from '@/stores/exchangeRates.ts';
import { TextDirection } from '@/core/text.ts'; import { TextDirection } from '@/core/text.ts';
import { NumeralSystem } from '@/core/numeral.ts'; import { NumeralSystem } from '@/core/numeral.ts';
import { AMOUNT_FACTOR } from '@/consts/numeral.ts';
import { TRANSACTION_MIN_AMOUNT, TRANSACTION_MAX_AMOUNT } from '@/consts/transaction.ts'; import { TRANSACTION_MIN_AMOUNT, TRANSACTION_MAX_AMOUNT } from '@/consts/transaction.ts';
import type { LocalizedLatestExchangeRate } from '@/models/exchange_rate.ts'; import type { LocalizedLatestExchangeRate } from '@/models/exchange_rate.ts';
@@ -277,7 +278,7 @@ function remove(customExchangeRate: LocalizedLatestExchangeRate | null, confirm:
function getFinalConvertedAmount(toExchangeRate: LocalizedLatestExchangeRate, displayLocalizedDigits: boolean): string { function getFinalConvertedAmount(toExchangeRate: LocalizedLatestExchangeRate, displayLocalizedDigits: boolean): string {
const fromExchangeRate = exchangeRatesStore.latestExchangeRateMap[baseCurrency.value]; const fromExchangeRate = exchangeRatesStore.latestExchangeRateMap[baseCurrency.value];
const exchangeRateAmount = getConvertedAmount(baseAmount.value / 100, fromExchangeRate, toExchangeRate); const exchangeRateAmount = getConvertedAmount(baseAmount.value / AMOUNT_FACTOR, fromExchangeRate, toExchangeRate);
if (!exchangeRateAmount) { if (!exchangeRateAmount) {
if (displayLocalizedDigits) { if (displayLocalizedDigits) {