public static DensityHistogram Build([NotNull] this IDensityHistogramBuilder builder, IReadOnlyList <double> values,
                                             IReadOnlyList <double> weights, int binCount)
        {
            Assertion.NotNull(nameof(builder), builder);

            return(builder.Build(new Sample(values, weights), binCount));
        }
Example #2
0
        public ModalityData DetectModes(Sample sample, [CanBeNull] IDensityHistogramBuilder densityHistogramBuilder, bool diagnostics = false)
        {
            Assertion.NotNull(nameof(sample), sample);
            if (sample.Values.Max() - sample.Values.Min() < 1e-9)
            {
                throw new ArgumentException($"{nameof(sample)} should contain at least two different elements", nameof(sample));
            }

            densityHistogramBuilder ??= QuantileRespectfulDensityHistogramBuilder.Instance;
            int    binCount   = (int)Math.Round(1 / precision);
            var    histogram  = densityHistogramBuilder.Build(sample, binCount);
            var    bins       = histogram.Bins;
            double binArea    = 1.0 / bins.Count;
            var    binHeights = bins.Select(bin => bin.Height).ToList();

            var diagnosticsBins = diagnostics
                ? histogram.Bins.Select(b => new LowlandModalityDiagnosticsData.DiagnosticsBin(b)).ToArray()
                : Array.Empty <LowlandModalityDiagnosticsData.DiagnosticsBin>();

            var peaks = new List <int>();

            for (int i = 1; i < binCount - 1; i++)
            {
                if (binHeights[i] > binHeights[i - 1] && binHeights[i] >= binHeights[i + 1])
                {
                    peaks.Add(i);
                    if (diagnostics)
                    {
                        diagnosticsBins[i].IsPeak = true;
                    }
                }
            }

            RangedMode GlobalMode(double location) => new RangedMode(location, histogram.GlobalLower, histogram.GlobalUpper, sample);

            RangedMode LocalMode(double location, double left, double right)
            {
                var modeValues  = new List <double>();
                var modeWeights = sample.IsWeighted ? new List <double>() : null;

                for (int i = 0; i < sample.SortedValues.Count; i++)
                {
                    double value = sample.SortedValues[i];
                    if (left <= value && value <= right)
                    {
                        modeValues.Add(value);
                        modeWeights?.Add(sample.SortedWeights[i]);
                    }
                }

                var modeSample = modeWeights == null ? new Sample(modeValues) : new Sample(modeValues, modeWeights);

                return(new RangedMode(location, left, right, modeSample));
            }

            ModalityData Result(IReadOnlyList <RangedMode> modes) => diagnostics
                ? new LowlandModalityDiagnosticsData(modes, histogram, diagnosticsBins)
                : new ModalityData(modes, histogram);

            switch (peaks.Count)
            {
            case 0:
                return(Result(new[] { GlobalMode(bins[binHeights.WhichMax()].Middle) }));

            case 1:
                return(Result(new[] { GlobalMode(bins[peaks.First()].Middle) }));

            default:
            {
                var modeLocations = new List <double>();
                var cutPoints     = new List <double>();

                bool TrySplit(int peak0, int peak1, int peak2)
                {
                    int    left = peak1, right = peak2;
                    double height = Math.Min(binHeights[peak1], binHeights[peak2]);

                    while (left < right && binHeights[left] > height)
                    {
                        left++;
                    }
                    while (left < right && binHeights[right] > height)
                    {
                        right--;
                    }

                    if (diagnostics)
                    {
                        for (int i = left; i <= right; i++)
                        {
                            diagnosticsBins[i].WaterLevel = height;
                        }
                    }

                    double width         = bins[right].Upper - bins[left].Lower;
                    double totalArea     = width * height;
                    int    totalBinCount = right - left + 1;
                    double totalBinArea  = totalBinCount * binArea;
                    double binProportion = totalBinArea / totalArea;

                    if (binProportion < sensitivity)
                    {
                        modeLocations.Add(bins[peak0].Middle);
                        cutPoints.Add(bins[binHeights.WhichMin(peak1, peak2 - peak1)].Middle);

                        if (diagnostics)
                        {
                            diagnosticsBins[peak0].IsMode = true;
                            for (int i = left; i <= right; i++)
                            {
                                diagnosticsBins[i].IsLowland = true;
                            }
                        }

                        return(true);
                    }

                    return(false);
                }

                var previousPeaks = new List <int> {
                    peaks[0]
                };
                for (int i = 1; i < peaks.Count; i++)
                {
                    int currentPeak = peaks[i];

                    while (previousPeaks.Any() && binHeights[previousPeaks.Last()] < binHeights[currentPeak])
                    {
                        if (TrySplit(previousPeaks.First(), previousPeaks.Last(), currentPeak))
                        {
                            previousPeaks.Clear();
                        }
                        else
                        {
                            previousPeaks.RemoveAt(previousPeaks.Count - 1);
                        }
                    }

                    if (previousPeaks.Any() && binHeights[previousPeaks.Last()] > binHeights[currentPeak])
                    {
                        if (TrySplit(previousPeaks.First(), previousPeaks.Last(), currentPeak))
                        {
                            previousPeaks.Clear();
                        }
                    }

                    previousPeaks.Add(currentPeak);
                }

                modeLocations.Add(bins[previousPeaks.First()].Middle);
                if (diagnostics)
                {
                    diagnosticsBins[previousPeaks.First()].IsMode = true;
                }

                var modes = new List <RangedMode>();
                switch (modeLocations.Count)
                {
                case 0:
                    modes.Add(GlobalMode(bins[binHeights.WhichMax()].Middle));
                    break;

                case 1:
                    modes.Add(GlobalMode(modeLocations.First()));
                    break;

                default:
                {
                    modes.Add(LocalMode(modeLocations.First(), histogram.GlobalLower, cutPoints.First()));
                    for (int i = 1; i < modeLocations.Count - 1; i++)
                    {
                        modes.Add(LocalMode(modeLocations[i], cutPoints[i - 1], cutPoints[i]));
                    }
                    modes.Add(LocalMode(modeLocations.Last(), cutPoints.Last(), histogram.GlobalUpper));
                }
                break;
                }

                return(Result(modes));
            }
            }
        }