1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 package org.hipparchus.stat.descriptive.rank;
23
24 import java.io.IOException;
25 import java.io.ObjectInputStream;
26 import java.io.Serializable;
27 import java.text.DecimalFormat;
28 import java.util.ArrayList;
29 import java.util.Arrays;
30 import java.util.Collection;
31 import java.util.Collections;
32 import java.util.List;
33
34 import org.hipparchus.analysis.UnivariateFunction;
35 import org.hipparchus.analysis.interpolation.LinearInterpolator;
36 import org.hipparchus.analysis.interpolation.NevilleInterpolator;
37 import org.hipparchus.analysis.interpolation.UnivariateInterpolator;
38 import org.hipparchus.exception.LocalizedCoreFormats;
39 import org.hipparchus.exception.MathIllegalArgumentException;
40 import org.hipparchus.stat.descriptive.AbstractStorelessUnivariateStatistic;
41 import org.hipparchus.stat.descriptive.StorelessUnivariateStatistic;
42 import org.hipparchus.util.MathArrays;
43 import org.hipparchus.util.MathUtils;
44 import org.hipparchus.util.Precision;
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59 public class PSquarePercentile extends AbstractStorelessUnivariateStatistic
60 implements StorelessUnivariateStatistic, Serializable {
61
62
63 private static final int PSQUARE_CONSTANT = 5;
64
65
66
67
68
69 private static final double DEFAULT_QUANTILE_DESIRED = 50d;
70
71
72 private static final long serialVersionUID = 20150412L;
73
74
75 private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("00.00");
76
77
78
79
80
81 private final List<Double> initialFive = new FixedCapacityList<>(PSQUARE_CONSTANT);
82
83
84
85
86
87
88 private final double quantile;
89
90
91
92
93
94 private transient double lastObservation;
95
96
97
98
99
100 private PSquareMarkers markers;
101
102
103
104
105 private double pValue = Double.NaN;
106
107
108
109
110 private long countOfObservations;
111
112
113
114
115
116
117
118 public PSquarePercentile(final double p) {
119 if (p > 100 || p < 0) {
120 throw new MathIllegalArgumentException(LocalizedCoreFormats.OUT_OF_RANGE,
121 p, 0, 100);
122 }
123 this.quantile = p / 100d;
124 }
125
126
127
128
129
130 PSquarePercentile() {
131 this(DEFAULT_QUANTILE_DESIRED);
132 }
133
134
135
136
137
138
139
140
141 public PSquarePercentile(PSquarePercentile original) {
142 super();
143
144 this.quantile = original.quantile;
145
146 if (original.markers != null) {
147 this.markers = original.markers.copySelf();
148 }
149
150 this.countOfObservations = original.countOfObservations;
151 this.pValue = original.pValue;
152 this.initialFive.addAll(original.initialFive);
153 }
154
155
156 @Override
157 public int hashCode() {
158 double result = getResult();
159 result = Double.isNaN(result) ? 37 : result;
160 final double markersHash = markers == null ? 0 : markers.hashCode();
161 final double[] toHash = {result, quantile, markersHash, countOfObservations};
162 return Arrays.hashCode(toHash);
163 }
164
165
166
167
168
169
170
171
172
173
174 @Override
175 public boolean equals(Object o) {
176 boolean result = false;
177 if (this == o) {
178 result = true;
179 } else if (o instanceof PSquarePercentile) {
180 PSquarePercentile that = (PSquarePercentile) o;
181 boolean isNotNull = markers != null && that.markers != null;
182 boolean isNull = markers == null && that.markers == null;
183 result = isNotNull ? markers.equals(that.markers) : isNull;
184
185
186 result = result && getN() == that.getN();
187 }
188 return result;
189 }
190
191
192
193
194
195
196
197
198 @Override
199 public void increment(final double observation) {
200
201 countOfObservations++;
202
203
204 this.lastObservation = observation;
205
206
207 if (markers == null) {
208 if (initialFive.add(observation)) {
209 Collections.sort(initialFive);
210 pValue = initialFive.get((int) (quantile * (initialFive.size() - 1)));
211 return;
212 }
213
214 markers = newMarkers(initialFive, quantile);
215 }
216
217 pValue = markers.processDataPoint(observation);
218 }
219
220
221
222
223
224
225
226 @Override
227 public String toString() {
228 synchronized (this) {
229 synchronized (DECIMAL_FORMAT) {
230 if (markers == null) {
231 return String.format("obs=%s pValue=%s",
232 DECIMAL_FORMAT.format(lastObservation),
233 DECIMAL_FORMAT.format(pValue));
234 } else {
235 return String.format("obs=%s markers=%s",
236 DECIMAL_FORMAT.format(lastObservation), markers.toString());
237 }
238 }
239 }
240 }
241
242
243 @Override
244 public long getN() {
245 return countOfObservations;
246 }
247
248
249 @Override
250 public PSquarePercentile copy() {
251 return new PSquarePercentile(this);
252 }
253
254
255
256
257
258
259 public double quantile() {
260 return quantile;
261 }
262
263
264
265
266
267 @Override
268 public void clear() {
269 markers = null;
270 initialFive.clear();
271 countOfObservations = 0L;
272 pValue = Double.NaN;
273 }
274
275
276
277
278 @Override
279 public double getResult() {
280 if (Double.compare(quantile, 1d) == 0) {
281 pValue = maximum();
282 } else if (Double.compare(quantile, 0d) == 0) {
283 pValue = minimum();
284 }
285 return pValue;
286 }
287
288
289
290
291 public double getQuantile() {
292 return quantile;
293 }
294
295
296
297
298 private double maximum() {
299 double val = Double.NaN;
300 if (markers != null) {
301 val = markers.height(PSQUARE_CONSTANT);
302 } else if (!initialFive.isEmpty()) {
303 val = initialFive.get(initialFive.size() - 1);
304 }
305 return val;
306 }
307
308
309
310
311 private double minimum() {
312 double val = Double.NaN;
313 if (markers != null) {
314 val = markers.height(1);
315 } else if (!initialFive.isEmpty()) {
316 val = initialFive.get(0);
317 }
318 return val;
319 }
320
321
322
323
324
325 private static class Markers implements PSquareMarkers, Serializable {
326
327
328
329 private static final long serialVersionUID = 1L;
330
331
332 private static final int LOW = 2;
333
334
335 private static final int HIGH = 4;
336
337
338
339
340
341
342 private final Marker[] markerArray;
343
344
345
346
347
348
349 private Markers(final Marker[] theMarkerArray) {
350 MathUtils.checkNotNull(theMarkerArray);
351 markerArray = theMarkerArray;
352 for (int i = 1; i < PSQUARE_CONSTANT; i++) {
353 markerArray[i].previous(markerArray[i - 1])
354 .next(markerArray[i + 1]).index(i);
355 }
356 markerArray[0].previous(markerArray[0])
357 .next(markerArray[1])
358 .index(0);
359 markerArray[5].previous(markerArray[4])
360 .next(markerArray[5])
361 .index(5);
362 }
363
364
365
366
367
368
369
370 private Markers(final List<Double> initialFive, final double p) {
371 this(createMarkerArray(initialFive, p));
372 }
373
374
375
376
377
378
379
380
381 private static Marker[] createMarkerArray(
382 final List<Double> initialFive, final double p) {
383 final int countObserved =
384 initialFive == null ? -1 : initialFive.size();
385 if (countObserved < PSQUARE_CONSTANT) {
386 throw new MathIllegalArgumentException(
387 LocalizedCoreFormats.INSUFFICIENT_OBSERVED_POINTS_IN_SAMPLE,
388 countObserved, PSQUARE_CONSTANT);
389 }
390 Collections.sort(initialFive);
391 return new Marker[] {
392 new Marker(),
393 new Marker(initialFive.get(0), 1, 0, 1),
394 new Marker(initialFive.get(1), 1 + 2 * p, p / 2, 2),
395 new Marker(initialFive.get(2), 1 + 4 * p, p, 3),
396 new Marker(initialFive.get(3), 3 + 2 * p, (1 + p) / 2, 4),
397 new Marker(initialFive.get(4), 5, 1, 5) };
398 }
399
400
401
402
403 @Override
404 public int hashCode() {
405 return Arrays.deepHashCode(markerArray);
406 }
407
408
409
410
411
412
413
414
415 @Override
416 public boolean equals(Object o) {
417 boolean result = false;
418 if (this == o) {
419 result = true;
420 } else if (o instanceof Markers) {
421 Markers that = (Markers) o;
422 result = Arrays.deepEquals(markerArray, that.markerArray);
423 }
424 return result;
425 }
426
427
428
429
430
431
432
433 @Override
434 public double processDataPoint(final double inputDataPoint) {
435
436
437 final int kthCell = findCellAndUpdateMinMax(inputDataPoint);
438
439
440 incrementPositions(1, kthCell + 1, 5);
441
442
443 updateDesiredPositions();
444
445
446 adjustHeightsOfMarkers();
447
448
449 return getPercentileValue();
450 }
451
452
453
454
455
456
457 @Override
458 public double getPercentileValue() {
459 return height(3);
460 }
461
462
463
464
465
466
467
468
469 private int findCellAndUpdateMinMax(final double observation) {
470 if (observation < height(1)) {
471 markerArray[1].markerHeight = observation;
472 return 1;
473 } else if (observation < height(2)) {
474 return 1;
475 } else if (observation < height(3)) {
476 return 2;
477 } else if (observation < height(4)) {
478 return 3;
479 } else if (observation <= height(5)) {
480 return 4;
481 } else {
482 markerArray[5].markerHeight = observation;
483 return 4;
484 }
485 }
486
487
488
489
490 private void adjustHeightsOfMarkers() {
491 for (int i = LOW; i <= HIGH; i++) {
492 estimate(i);
493 }
494 }
495
496
497
498
499 @Override
500 public double estimate(final int index) {
501 MathUtils.checkRangeInclusive(index, LOW, HIGH);
502 return markerArray[index].estimate();
503 }
504
505
506
507
508
509
510
511
512
513 private void incrementPositions(final int d, final int startIndex,
514 final int endIndex) {
515 for (int i = startIndex; i <= endIndex; i++) {
516 markerArray[i].incrementPosition(d);
517 }
518 }
519
520
521
522
523
524 private void updateDesiredPositions() {
525 for (int i = 1; i < markerArray.length; i++) {
526 markerArray[i].updateDesiredPosition();
527 }
528 }
529
530
531
532
533
534
535
536
537 private void readObject(ObjectInputStream anInputStream)
538 throws ClassNotFoundException, IOException {
539
540 anInputStream.defaultReadObject();
541
542 for (int i = 1; i < PSQUARE_CONSTANT; i++) {
543 markerArray[i].previous(markerArray[i - 1]).next(markerArray[i + 1]).index(i);
544 }
545 markerArray[0].previous(markerArray[0]).next(markerArray[1]).index(0);
546 markerArray[5].previous(markerArray[4]).next(markerArray[5]).index(5);
547 }
548
549
550
551
552
553
554
555 @Override
556 public double height(final int markerIndex) {
557 MathUtils.checkRangeInclusive(markerIndex, 1, markerArray.length - 1);
558 return markerArray[markerIndex].markerHeight;
559 }
560
561
562 @Override
563 public Markers copySelf() {
564 return new Markers(new Marker[] {
565 new Marker(),
566 markerArray[1].copySelf(),
567 markerArray[2].copySelf(),
568 markerArray[3].copySelf(),
569 markerArray[4].copySelf(),
570 markerArray[5].copySelf()
571 });
572
573 }
574
575
576
577
578
579
580 @Override
581 public String toString() {
582 return String.format("m1=[%s],m2=[%s],m3=[%s],m4=[%s],m5=[%s]",
583 markerArray[1].toString(), markerArray[2].toString(),
584 markerArray[3].toString(), markerArray[4].toString(),
585 markerArray[5].toString());
586 }
587
588 }
589
590
591
592
593 private static class Marker implements Serializable {
594
595
596
597
598 private static final long serialVersionUID = -3575879478288538431L;
599
600
601
602
603
604 private int index;
605
606
607
608
609
610 private double intMarkerPosition;
611
612
613
614
615
616 private double desiredMarkerPosition;
617
618
619
620
621
622 private double markerHeight;
623
624
625
626
627
628 private double desiredMarkerIncrement;
629
630
631
632
633
634 private transient Marker next;
635
636
637
638
639 private transient Marker previous;
640
641
642
643
644 private final UnivariateInterpolator nonLinear = new NevilleInterpolator();
645
646
647
648
649 private transient UnivariateInterpolator linear = new LinearInterpolator();
650
651
652
653
654 private Marker() {
655 this.next = this.previous = this;
656 }
657
658
659
660
661
662
663
664
665
666 private Marker(double heightOfMarker, double makerPositionDesired,
667 double markerPositionIncrement, double markerPositionNumber) {
668 this();
669 this.markerHeight = heightOfMarker;
670 this.desiredMarkerPosition = makerPositionDesired;
671 this.desiredMarkerIncrement = markerPositionIncrement;
672 this.intMarkerPosition = markerPositionNumber;
673 }
674
675
676
677
678
679
680
681
682 private Marker previous(final Marker previousMarker) {
683 MathUtils.checkNotNull(previousMarker);
684 this.previous = previousMarker;
685 return this;
686 }
687
688
689
690
691
692
693
694
695 private Marker next(final Marker nextMarker) {
696 MathUtils.checkNotNull(nextMarker);
697 this.next = nextMarker;
698 return this;
699 }
700
701
702
703
704
705
706
707 private Marker index(final int indexOfMarker) {
708 this.index = indexOfMarker;
709 return this;
710 }
711
712
713
714
715 private void updateDesiredPosition() {
716 desiredMarkerPosition += desiredMarkerIncrement;
717 }
718
719
720
721
722
723
724 private void incrementPosition(final int d) {
725 intMarkerPosition += d;
726 }
727
728
729
730
731
732
733 private double difference() {
734 return desiredMarkerPosition - intMarkerPosition;
735 }
736
737
738
739
740
741
742 private double estimate() {
743 final double di = difference();
744 final boolean isNextHigher =
745 next.intMarkerPosition - intMarkerPosition > 1;
746 final boolean isPreviousLower =
747 previous.intMarkerPosition - intMarkerPosition < -1;
748
749 if (di >= 1 && isNextHigher || di <= -1 && isPreviousLower) {
750 final int d = di >= 0 ? 1 : -1;
751 final double[] xval = { previous.intMarkerPosition, intMarkerPosition, next.intMarkerPosition };
752 final double[] yval = { previous.markerHeight, markerHeight, next.markerHeight };
753 final double xD = intMarkerPosition + d;
754
755 UnivariateFunction univariateFunction =
756 nonLinear.interpolate(xval, yval);
757 markerHeight = univariateFunction.value(xD);
758
759
760 if (isEstimateBad(yval, markerHeight)) {
761 int delta = xD - xval[1] > 0 ? 1 : -1;
762 final double[] xBad = { xval[1], xval[1 + delta] };
763 final double[] yBad = { yval[1], yval[1 + delta] };
764 MathArrays.sortInPlace(xBad, yBad);
765 univariateFunction = linear.interpolate(xBad, yBad);
766 markerHeight = univariateFunction.value(xD);
767 }
768 incrementPosition(d);
769 }
770 return markerHeight;
771 }
772
773
774
775
776
777
778
779
780
781 private boolean isEstimateBad(final double[] y, final double yD) {
782 return yD <= y[0] || yD >= y[2];
783 }
784
785
786
787
788
789
790
791
792
793 @Override
794 public boolean equals(Object o) {
795 boolean result = false;
796 if (this == o) {
797 result = true;
798 } else if (o instanceof Marker) {
799 Marker that = (Marker) o;
800
801 result = Double.compare(markerHeight, that.markerHeight) == 0;
802 result =
803 result &&
804 Double.compare(intMarkerPosition,
805 that.intMarkerPosition) == 0;
806 result =
807 result &&
808 Double.compare(desiredMarkerPosition,
809 that.desiredMarkerPosition) == 0;
810 result =
811 result &&
812 Double.compare(desiredMarkerIncrement,
813 that.desiredMarkerIncrement) == 0;
814
815 result = result && next.index == that.next.index;
816 result = result && previous.index == that.previous.index;
817 }
818 return result;
819 }
820
821
822 @Override
823 public int hashCode() {
824 return Arrays.hashCode(new double[] {markerHeight, intMarkerPosition,
825 desiredMarkerIncrement, desiredMarkerPosition, previous.index, next.index});
826 }
827
828
829
830
831
832
833
834
835 private void readObject(ObjectInputStream anInstream)
836 throws ClassNotFoundException, IOException {
837 anInstream.defaultReadObject();
838 previous=next=this;
839 linear = new LinearInterpolator();
840 }
841
842
843
844
845 public Marker copySelf() {
846 return new Marker(markerHeight, desiredMarkerPosition, desiredMarkerIncrement, intMarkerPosition);
847 }
848
849
850
851
852 @Override
853 public String toString() {
854 return String.format(
855 "index=%.0f,n=%.0f,np=%.2f,q=%.2f,dn=%.2f,prev=%d,next=%d",
856 (double) index, Precision.round(intMarkerPosition, 0),
857 Precision.round(desiredMarkerPosition, 2),
858 Precision.round(markerHeight, 2),
859 Precision.round(desiredMarkerIncrement, 2), previous.index,
860 next.index);
861 }
862 }
863
864
865
866
867
868
869
870
871 private static class FixedCapacityList<E> extends ArrayList<E> implements Serializable {
872
873
874
875
876 private static final long serialVersionUID = 2283952083075725479L;
877
878
879
880 private final int capacity;
881
882
883
884
885
886
887
888 FixedCapacityList(final int fixedCapacity) {
889 super(fixedCapacity);
890 this.capacity = fixedCapacity;
891 }
892
893
894
895
896
897
898
899
900 @Override
901 public boolean add(final E e) {
902 return size() < capacity && super.add(e);
903 }
904
905
906
907
908
909
910
911
912
913 @Override
914 public boolean addAll(Collection<? extends E> collection) {
915 boolean isCollectionLess =
916 collection != null &&
917 collection.size() + size() <= capacity;
918 return isCollectionLess && super.addAll(collection);
919 }
920
921
922 @Override
923 public boolean equals(final Object other) {
924 return super.equals(other) && capacity == ((FixedCapacityList<?>) other).capacity;
925 }
926
927
928 @Override
929 public int hashCode() {
930 return super.hashCode() + capacity;
931 }
932
933 }
934
935
936
937
938
939
940
941
942 public static PSquareMarkers newMarkers(final List<Double> initialFive, final double p) {
943 return new Markers(initialFive, p);
944 }
945
946
947
948
949
950
951 protected interface PSquareMarkers {
952
953
954
955
956
957 double getPercentileValue();
958
959
960
961
962
963
964 PSquareMarkers copySelf();
965
966
967
968
969
970
971
972
973 double height(int markerIndex);
974
975
976
977
978
979
980
981 double processDataPoint(double inputDataPoint);
982
983
984
985
986
987
988
989
990 double estimate(int index);
991 }
992 }