diff --git a/RunningStatistics.Tests/CountMap/TestCountMap_Extensions.cs b/RunningStatistics.Tests/CountMap/TestCountMap_Extensions.cs index e8025a0..9a91aa7 100644 --- a/RunningStatistics.Tests/CountMap/TestCountMap_Extensions.cs +++ b/RunningStatistics.Tests/CountMap/TestCountMap_Extensions.cs @@ -208,7 +208,49 @@ public void Mode_ReturnsObservationWithHighestCount() Assert.Equal(2, countMap.Mode()); } + + [Fact] + public void Quantile_ReturnsCorrectValues_EvenNumberOfUniqueObs() + { + var countMap = new CountMap(); + countMap.Fit(1, 2); + countMap.Fit(2, 2); + countMap.Fit(3, 1); + countMap.Fit(4, 2); + + Assert.Equal(1, countMap.Quantile(0.0)); + Assert.Equal(1, countMap.Quantile(0.1)); + Assert.Equal(1, countMap.Quantile(0.2)); + Assert.Equal(2, countMap.Quantile(0.3)); + Assert.Equal(2, countMap.Quantile(0.4)); + Assert.Equal(2, countMap.Quantile(0.5)); + Assert.Equal(3, countMap.Quantile(0.6)); + Assert.Equal(3, countMap.Quantile(0.7)); + Assert.Equal(4, countMap.Quantile(0.8)); + Assert.Equal(4, countMap.Quantile(0.9)); + Assert.Equal(4, countMap.Quantile(1.0)); + } + [Fact] + public void Quantile_ReturnsCorrectValues_OddNumberOfUniqueObs() + { + var countMap = new CountMap(); + countMap.Fit(1, 2); + countMap.Fit(2, 3); + countMap.Fit(3, 1); + + Assert.Equal(1, countMap.Quantile(0.00)); + Assert.Equal(1, countMap.Quantile(0.10)); + Assert.Equal(1, countMap.Quantile(0.25)); + Assert.Equal(2, countMap.Quantile(0.35)); + Assert.Equal(2, countMap.Quantile(0.50)); + Assert.Equal(2, countMap.Quantile(0.75)); + Assert.Equal(3, countMap.Quantile(0.85)); + Assert.Equal(3, countMap.Quantile(0.90)); + Assert.Equal(3, countMap.Quantile(0.95)); + Assert.Equal(3, countMap.Quantile(1.00)); + } + [Fact] public void Median_ReturnsCorrectMedianObservation() { diff --git a/RunningStatistics/Extensions/CountMapExtensions.cs b/RunningStatistics/Extensions/CountMapExtensions.cs index da3d38e..2e5b536 100644 --- a/RunningStatistics/Extensions/CountMapExtensions.cs +++ b/RunningStatistics/Extensions/CountMapExtensions.cs @@ -431,36 +431,99 @@ public static TObs Mode(this CountMap countMap) where TObs : notnull return mode ?? throw new NullReferenceException(); #endif } - - public static TObs Median(this CountMap countMap) where TObs : notnull + + /// + /// Compute the p-th quantile of a CountMap. + /// If p is less than or equal to 0, then the minimum key is returned. + /// If p is greater than or equal to 1, then the maximum key is returned. + /// + /// The count map to compute the quantile for. + /// The quantile to compute. Must be between 0 and 1. + /// The comparer to use for ordering the keys. + /// If null, the default comparer is used. + /// The type of the observations in the count map. + /// The p-th quantile of the count map. + /// + /// Thrown if the count map is empty or if the quantile cannot be found. + /// + public static TObs Quantile(this CountMap countMap, double p, IComparer? comparer) where TObs : notnull { if (countMap.Nobs == 0) { - throw new Exception("Nobs = 0. The median does not exist."); + throw new Exception("The count map is empty. The quantile does not exist."); } + if (p <= 0) return countMap.MinKey(comparer); + if (p >= 1) return countMap.MaxKey(comparer); + + var cdf = 0.0; + var nobs = (double)countMap.Nobs; + if (countMap.Count % 2 == 0) { - var cdf = 0.0; - - foreach (var kvp in countMap.OrderBy(kvp => kvp.Key)) + foreach (var kvp in countMap.OrderBy(kvp => kvp.Key, comparer)) { - cdf += (double)kvp.Value / countMap.Nobs; - if (cdf >= 0.5) return kvp.Key; + cdf += kvp.Value / nobs; + if (cdf >= p) return kvp.Key; } } else { - var cdf = 0.0; - - foreach (var kvp in countMap.OrderBy(kvp => kvp.Key)) + foreach (var kvp in countMap.OrderBy(kvp => kvp.Key, comparer)) { - cdf += (double)kvp.Value / countMap.Nobs; - if (cdf > 0.5) return kvp.Key; + cdf += kvp.Value / nobs; + if (cdf > p) return kvp.Key; } - } - + } + // This should be unreachable... - throw new Exception("Not able to find the median of the count map."); + throw new Exception($"Not able to find the {p} quantile of the count map."); + } + + /// + /// Compute the p-th quantile of a CountMap. + /// If p is less than or equal to 0, then the minimum key is returned. + /// If p is greater than or equal to 1, then the maximum key is returned. + /// + /// The count map to compute the quantile for. + /// The quantile to compute. Must be between 0 and 1. + /// The type of the observations in the count map. + /// The p-th quantile of the count map. + /// + /// This method uses the comparer associated with the CountMap. If no comparer + /// was specified when the CountMap was created, the default comparer is used instead. + /// + public static TObs Quantile(this CountMap countMap, double p) where TObs : notnull + { + return countMap.Quantile(p, countMap.Comparer); + } + + /// + /// Compute the median of a CountMap using the specified comparer. + /// This is equivalent to calling Quantile(0.5, comparer). + /// + /// The count map to compute the median for. + /// The comparer to use for ordering the keys. + /// If null, the default comparer is used. + /// The type of the observations in the count map. + /// The median of the count map. + public static TObs Median(this CountMap countMap, IComparer? comparer) where TObs : notnull + { + return countMap.Quantile(0.5, comparer); + } + + /// + /// Compute the median of a CountMap. + /// + /// The count map to compute the median for. + /// The type of the observations in the count map. + /// The median of the count map. + /// + /// This method uses the comparer associated with the CountMap. If no comparer + /// was specified when the CountMap was created, the default comparer is used instead. + /// + public static TObs Median(this CountMap countMap) where TObs : notnull + { + return countMap.Median(countMap.Comparer); } } \ No newline at end of file