package org.djutils.stats.summarizers.event; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.util.Random; import org.djutils.event.Event; import org.djutils.event.EventInterface; import org.djutils.event.EventListenerInterface; import org.djutils.event.EventType; import org.djutils.metadata.MetaData; import org.djutils.stats.ConfidenceInterval; import org.djutils.stats.DistNormalTable; import org.djutils.stats.summarizers.quantileaccumulator.FullStorageAccumulator; import org.djutils.stats.summarizers.quantileaccumulator.NoStorageAccumulator; import org.junit.Test; /** * The TallyTest test the tally. *
* Copyright (c) 2002-2022 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
* for project information https://simulation.tudelft.nl. The DSOL
* project is distributed under a three-clause BSD-style license, which can be found at
*
* https://simulation.tudelft.nl/dsol/3.0/license.html.
* @author Alexander Verbraeck
* @author Peter Knoppers
*/
public class EventBasedTallyTest
{
/** an event to fire. */
private static final EventType VALUE_EVENT = new EventType("VALUE_EVENT", MetaData.NO_META_DATA);
/** Test the event based tally. */
@SuppressWarnings("checkstyle:methodlength")
@Test
public void testEventBasedTally()
{
String description = "THIS TALLY IS TESTED";
EventBasedTally tally = new EventBasedTally(description);
// check the description
assertTrue(tally.toString().contains(description));
assertEquals(description, tally.getDescription());
assertTrue(tally.toString().startsWith("EventBasedTally"));
// now we check the initial values
assertTrue(Double.valueOf(tally.getMin()).isNaN());
assertTrue(Double.valueOf(tally.getMax()).isNaN());
assertTrue(Double.valueOf(tally.getSampleMean()).isNaN());
assertTrue(Double.valueOf(tally.getSampleVariance()).isNaN());
assertTrue(Double.valueOf(tally.getPopulationVariance()).isNaN());
assertTrue(Double.valueOf(tally.getSampleStDev()).isNaN());
assertTrue(Double.valueOf(tally.getPopulationStDev()).isNaN());
assertTrue(Double.valueOf(tally.getSampleSkewness()).isNaN());
assertTrue(Double.valueOf(tally.getPopulationSkewness()).isNaN());
assertTrue(Double.valueOf(tally.getSampleKurtosis()).isNaN());
assertTrue(Double.valueOf(tally.getPopulationKurtosis()).isNaN());
assertTrue(Double.valueOf(tally.getSampleExcessKurtosis()).isNaN());
assertTrue(Double.valueOf(tally.getPopulationExcessKurtosis()).isNaN());
assertEquals(0, tally.getSum(), 0);
assertEquals(0L, tally.getN());
assertNull(tally.getConfidenceInterval(0.95));
assertNull(tally.getConfidenceInterval(0.95, ConfidenceInterval.LEFT_SIDE_CONFIDENCE));
assertNull(tally.getConfidenceInterval(0.95, ConfidenceInterval.RIGHT_SIDE_CONFIDENCE));
assertNull(tally.getConfidenceInterval(0.95, ConfidenceInterval.BOTH_SIDE_CONFIDENCE));
// We first fire a wrong event
try
{
tally.notify(new Event(VALUE_EVENT, "ERROR", "ERROR"));
fail("tally should react on events.value !instanceOf Double");
}
catch (Exception exception)
{
assertNotNull(exception);
}
// Now we fire some events
try
{
tally.notify(new Event(VALUE_EVENT, "EventBasedTallyTest", 1.1));
assertFalse("mean is now available", Double.isNaN(tally.getSampleMean()));
assertTrue("sample variance is not available", Double.isNaN(tally.getSampleVariance()));
assertFalse("variance is not available", Double.isNaN(tally.getPopulationVariance()));
assertTrue("skewness is not available", Double.isNaN(tally.getPopulationSkewness()));
tally.notify(new Event(VALUE_EVENT, "EventBasedTallyTest", 1.2));
assertFalse("sample variance is now available", Double.isNaN(tally.getSampleVariance()));
assertTrue("sample skewness is not available", Double.isNaN(tally.getSampleSkewness()));
assertFalse("skewness is available", Double.isNaN(tally.getPopulationSkewness()));
assertTrue("kurtosis is not available", Double.isNaN(tally.getPopulationKurtosis()));
tally.notify(new Event(VALUE_EVENT, "EventBasedTallyTest", 1.3));
assertFalse("skewness is now available", Double.isNaN(tally.getSampleSkewness()));
assertFalse("kurtosis is now available", Double.isNaN(tally.getPopulationKurtosis()));
assertTrue("sample kurtosis is not available", Double.isNaN(tally.getSampleKurtosis()));
tally.notify(new Event(VALUE_EVENT, "EventBasedTallyTest", 1.4));
assertFalse("sample kurtosis is now available", Double.isNaN(tally.getSampleKurtosis()));
tally.notify(new Event(VALUE_EVENT, "EventBasedTallyTest", 1.5));
tally.notify(new Event(VALUE_EVENT, "EventBasedTallyTest", 1.6));
tally.notify(new Event(VALUE_EVENT, "EventBasedTallyTest", 1.7));
tally.notify(new Event(VALUE_EVENT, "EventBasedTallyTest", 1.8));
tally.notify(new Event(VALUE_EVENT, "EventBasedTallyTest", 1.9));
tally.notify(new Event(VALUE_EVENT, "EventBasedTallyTest", 2.0));
tally.notify(new Event(VALUE_EVENT, "EventBasedTallyTest", 1.0));
}
catch (Exception exception)
{
fail(exception.getMessage());
}
// Now we check the tally
assertEquals(2.0, tally.getMax(), 1.0E-6);
assertEquals(1.0, tally.getMin(), 1.0E-6);
assertEquals(11, tally.getN());
assertEquals(16.5, tally.getSum(), 1.0E-6);
assertEquals(1.5, tally.getSampleMean(), 1.0E-6);
assertEquals(0.110000, tally.getSampleVariance(), 1.0E-6);
assertEquals(0.331662, tally.getSampleStDev(), 1.0E-6);
assertEquals(1.304003602, tally.getConfidenceInterval(0.05)[0], 1E-05);
assertEquals(1.695996398, tally.getConfidenceInterval(0.05)[1], 1E-05);
assertEquals(1.335514637, tally.getConfidenceInterval(0.10)[0], 1E-05);
assertEquals(1.664485363, tally.getConfidenceInterval(0.10)[1], 1E-05);
assertEquals(1.356046853, tally.getConfidenceInterval(0.15)[0], 1E-05);
assertEquals(1.643953147, tally.getConfidenceInterval(0.15)[1], 1E-05);
assertEquals(1.432551025, tally.getConfidenceInterval(0.50)[0], 1E-05);
assertEquals(1.567448975, tally.getConfidenceInterval(0.50)[1], 1E-05);
assertEquals(1.474665290, tally.getConfidenceInterval(0.80)[0], 1E-05);
assertEquals(1.525334710, tally.getConfidenceInterval(0.80)[1], 1E-05);
assertEquals(1.493729322, tally.getConfidenceInterval(0.95)[0], 1E-05);
assertEquals(1.506270678, tally.getConfidenceInterval(0.95)[1], 1E-05);
assertEquals(1.304003602, tally.getConfidenceInterval(0.05, ConfidenceInterval.BOTH_SIDE_CONFIDENCE)[0], 1E-05);
assertEquals(1.695996398, tally.getConfidenceInterval(0.05, ConfidenceInterval.BOTH_SIDE_CONFIDENCE)[1], 1E-05);
assertEquals(1.432551025, tally.getConfidenceInterval(0.50, ConfidenceInterval.BOTH_SIDE_CONFIDENCE)[0], 1E-05);
assertEquals(1.567448975, tally.getConfidenceInterval(0.50, ConfidenceInterval.BOTH_SIDE_CONFIDENCE)[1], 1E-05);
assertEquals(1.493729322, tally.getConfidenceInterval(0.95, ConfidenceInterval.BOTH_SIDE_CONFIDENCE)[0], 1E-05);
assertEquals(1.506270678, tally.getConfidenceInterval(0.95, ConfidenceInterval.BOTH_SIDE_CONFIDENCE)[1], 1E-05);
assertEquals(1.304003602, tally.getConfidenceInterval(0.025, ConfidenceInterval.LEFT_SIDE_CONFIDENCE)[0], 1E-05);
assertEquals(1.500000000, tally.getConfidenceInterval(0.025, ConfidenceInterval.LEFT_SIDE_CONFIDENCE)[1], 1E-05);
assertEquals(1.432551025, tally.getConfidenceInterval(0.25, ConfidenceInterval.LEFT_SIDE_CONFIDENCE)[0], 1E-05);
assertEquals(1.500000000, tally.getConfidenceInterval(0.25, ConfidenceInterval.LEFT_SIDE_CONFIDENCE)[1], 1E-05);
assertEquals(1.474665290, tally.getConfidenceInterval(0.40, ConfidenceInterval.LEFT_SIDE_CONFIDENCE)[0], 1E-05);
assertEquals(1.500000000, tally.getConfidenceInterval(0.40, ConfidenceInterval.LEFT_SIDE_CONFIDENCE)[1], 1E-05);
assertEquals(1.500000000, tally.getConfidenceInterval(0.025, ConfidenceInterval.RIGHT_SIDE_CONFIDENCE)[0], 1E-05);
assertEquals(1.695996398, tally.getConfidenceInterval(0.025, ConfidenceInterval.RIGHT_SIDE_CONFIDENCE)[1], 1E-05);
assertEquals(1.500000000, tally.getConfidenceInterval(0.25, ConfidenceInterval.RIGHT_SIDE_CONFIDENCE)[0], 1E-05);
assertEquals(1.567448975, tally.getConfidenceInterval(0.25, ConfidenceInterval.RIGHT_SIDE_CONFIDENCE)[1], 1E-05);
assertEquals(1.500000000, tally.getConfidenceInterval(0.40, ConfidenceInterval.RIGHT_SIDE_CONFIDENCE)[0], 1E-05);
assertEquals(1.525334710, tally.getConfidenceInterval(0.40, ConfidenceInterval.RIGHT_SIDE_CONFIDENCE)[1], 1E-05);
// we check the input of the confidence interval
try
{
tally.getConfidenceInterval(0.95, null);
fail("null is not defined as side of confidence level");
}
catch (Exception exception)
{
assertTrue(exception.getClass().equals(NullPointerException.class));
}
try
{
assertNull(tally.getConfidenceInterval(-0.95));
fail("should have reacted on wrong confidence level -0.95");
}
catch (Exception exception)
{
assertTrue(exception.getClass().equals(IllegalArgumentException.class));
}
try
{
assertNull(tally.getConfidenceInterval(1.14));
fail("should have reacted on wrong confidence level 1.14");
}
catch (Exception exception)
{
assertTrue(exception.getClass().equals(IllegalArgumentException.class));
}
assertTrue(Math.abs(tally.getSampleMean() - 1.5) < 10E-6);
// Let's compute the standard deviation
double varianceAccumulator = 0;
for (int i = 0; i < 11; i++)
{
varianceAccumulator = Math.pow(1.5 - (1.0 + i / 10.0), 2) + varianceAccumulator;
}
assertEquals(varianceAccumulator / 10.0, tally.getSampleVariance(), 1.0E-6);
assertEquals(Math.sqrt(varianceAccumulator / 10.0), tally.getSampleStDev(), 1.0E-6);
assertEquals(varianceAccumulator / 11.0, tally.getPopulationVariance(), 1.0E-6);
assertEquals(Math.sqrt(varianceAccumulator / 11.0), tally.getPopulationStDev(), 1.0E-6);
}
/**
* Test produced events by EventBasedTally.
*/
@Test
public void testTallyEventProduction()
{
EventBasedTally tally = new EventBasedTally("testTally");
assertEquals(tally, tally.getSourceId());
ObservationEventListener oel = new ObservationEventListener();
tally.addListener(oel, StatisticsEvents.OBSERVATION_ADDED_EVENT);
assertEquals(0, oel.getObservationEvents());
EventType[] types = new EventType[] {StatisticsEvents.N_EVENT, StatisticsEvents.MIN_EVENT, StatisticsEvents.MAX_EVENT,
StatisticsEvents.POPULATION_MEAN_EVENT, StatisticsEvents.POPULATION_VARIANCE_EVENT,
StatisticsEvents.POPULATION_SKEWNESS_EVENT, StatisticsEvents.POPULATION_KURTOSIS_EVENT,
StatisticsEvents.POPULATION_STDEV_EVENT, StatisticsEvents.SUM_EVENT, StatisticsEvents.SAMPLE_MEAN_EVENT,
StatisticsEvents.SAMPLE_VARIANCE_EVENT, StatisticsEvents.SAMPLE_SKEWNESS_EVENT,
StatisticsEvents.SAMPLE_KURTOSIS_EVENT, StatisticsEvents.SAMPLE_STDEV_EVENT};
LoggingEventListener[] listeners = new LoggingEventListener[types.length];
for (int i = 0; i < types.length; i++)
{
listeners[i] = new LoggingEventListener();
tally.addListener(listeners[i], types[i]);
}
for (int i = 1; i <= 10; i++)
{
tally.ingest(10 * i);
}
assertEquals(10, oel.getObservationEvents());
// values from: https://atozmath.com/StatsUG.aspx
Object[] expectedValues =
new Object[] {10L, 10.0, 100.0, 55.0, 825.0, 0.0, 1.7758, 28.7228, 550.0, 55.0, 916.6667, 0.0, 1.5982, 30.2765};
for (int i = 0; i < types.length; i++)
{
assertEquals("Number of events for listener " + types[i], 10, listeners[i].getNumberOfEvents());
assertEquals("Event sourceId for listener " + types[i], tally, listeners[i].getLastEvent().getSourceId());
assertEquals("Event type for listener " + types[i], types[i], listeners[i].getLastEvent().getType());
if (expectedValues[i] instanceof Long)
{
assertEquals("Final value for listener " + types[i], expectedValues[i],
listeners[i].getLastEvent().getContent());
}
else
{
double e = ((Double) expectedValues[i]).doubleValue();
double c = ((Double) listeners[i].getLastEvent().getContent()).doubleValue();
assertEquals("Final value for listener " + types[i], e, c, 0.001);
}
}
}
/**
* Test EventBasedTally with the NoStorageAccumulator.
*/
@Test
public void testNoStorageAccumulator()
{
EventBasedTally tally = new EventBasedTally("test with the NoStorageAccumulator", new NoStorageAccumulator());
assertTrue("mean of no data is NaN", Double.isNaN(tally.getSampleMean()));
try
{
tally.getQuantile(0.5);
fail("getQuantile of no data should have resulted in an IllegalArgumentException");
}
catch (IllegalArgumentException iae)
{
// Ignore expected exception
}
tally.notify(new Event(VALUE_EVENT, "EventBasedTallyTest", 90.0));
assertEquals("mean of one value is that value", 90.0, tally.getSampleMean(), 0);
try
{
tally.getQuantile(0.5);
fail("getQuantile of one value should have resulted in an IllegalArgumentException");
}
catch (IllegalArgumentException iae)
{
// Ignore expected exception
}
tally.notify(new Event(VALUE_EVENT, "EventBasedTallyTest", 110.0));
assertEquals("mean of two value", 100.0, tally.getSampleMean(), 0);
assertEquals("50% quantile", 100.0, tally.getQuantile(0.5), 0);
/*-
double sigma = tally.getSampleStDev();
double mu = tally.getSampleMean();
// For loop below makes painfully clear where the getQuantile method fails
// Values are from last table in https://en.wikipedia.org/wiki/Standard_normal_table
for (double probability : new double[] {1 - 5.00000E-1, 1 - 1.58655E-1, 1 - 2.27501E-2, 1 - 1.34990E-3, 1 - 3.16712E-5,
1 - 2.86652E-7, 1 - 9.86588E-10, 1 - 1.27981E-12, 1 - 6.22096E-16, 1 - 1.12859E-19, 1 - 7.61985E-24})
{
double x = tally.getQuantile(probability);
System.out.println(String.format("probability=%19.16f 1-probability=%19.16f, x=%19.14f, sigmaCount=%19.16f",
probability, 1 - probability, x, (x - mu) / sigma));
}
// Output shows that the inverse cumulative probability function works fine up to about 8 sigma
*/
assertEquals("84% is about one sigma", 1, DistNormalTable.getInverseCumulativeProbability(0, 1, 0.84), 0.01);
assertEquals("16% is about minus one sigma", -1, DistNormalTable.getInverseCumulativeProbability(0, 1, 0.16), 0.01);
// Test for the problem that Peter Knoppers had in Tritapt where really small rounding errors caused sqrt(-1e-14).
double value = 166.0 / 25.0;
tally.initialize();
tally.notify(new Event(VALUE_EVENT, "EventBasedTallyTest", value));
tally.notify(new Event(VALUE_EVENT, "EventBasedTallyTest", value));
tally.notify(new Event(VALUE_EVENT, "EventBasedTallyTest", value));
tally.notify(new Event(VALUE_EVENT, "EventBasedTallyTest", value));
tally.initialize();
// Throw a lot of pseudo-randomly normally distributed values in and see if the expected mean and stddev come out
double mean = 123.456;
double stddev = 234.567;
Random random = new Random(123456);
for (int sample = 0; sample < 10000; sample++)
{
value = generateGaussianNoise(mean, stddev, random);
tally.notify(new Event(VALUE_EVENT, "EventBasedTallyTest", value));
}
assertEquals("mean should approximately match", mean, tally.getSampleMean(), stddev / 10);
assertEquals("stddev should approximately match", stddev, tally.getSampleStDev(), stddev / 10);
}
/**
* Test the Event based tally with the FullStorageAccumulator.
*/
@Test
public void testFullStorageAccumulator()
{
EventBasedTally tally =
new EventBasedTally("EventBasedTally for FullStorageAccumulator test", new FullStorageAccumulator());
// Insert values from 0.0 .. 100.0 (step 1.0)
for (int step = 0; step <= 100; step++)
{
tally.notify(new Event(VALUE_EVENT, "EventBasedTallyTest", 1.0 * step));
}
for (double probability : new double[] {0.0, 0.01, 0.1, 0.49, 0.5, 0.51, 0.9, 0.99, 1.0})
{
double expected = 100 * probability;
double got = tally.getQuantile(probability);
assertEquals("quantile should match", expected, got, 0.00001);
}
try
{
tally.getQuantile(-0.01);
fail("negative probability should have thrown an exception");
}
catch (IllegalArgumentException iae)
{
// Ignore expected exception
}
try
{
tally.getQuantile(1.01);
fail("Probability > 1 should have thrown an exception");
}
catch (IllegalArgumentException iae)
{
// Ignore expected exception
}
assertTrue("toString returns something descriptive",
new FullStorageAccumulator().toString().startsWith("FullStorageAccumulator"));
}
/**
* Generate normally distributed values. Derived from https://en.wikipedia.org/wiki/Box%E2%80%93Muller_transform
* @param mu double; mean
* @param sigma double; standard deviation
* @param random Random; entropy source
* @return double; one pseudo random value
*/
double generateGaussianNoise(final double mu, final double sigma, final Random random)
{
final double epsilon = Double.MIN_VALUE;
final double twoPi = Math.PI * 2;
double u1, u2;
do
{
u1 = random.nextDouble();
u2 = random.nextDouble();
}
while (u1 <= epsilon);
return mu + sigma * Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(twoPi * u2);
}
/** The listener that counts the OBSERVATION_ADDED_EVENT events and checks correctness. */
class ObservationEventListener implements EventListenerInterface
{
/** */
private static final long serialVersionUID = 1L;
/** counter for the event. */
private int observationEvents = 0;
@Override
public void notify(final EventInterface event)
{
assertTrue(event.getType().equals(StatisticsEvents.OBSERVATION_ADDED_EVENT));
assertTrue("Content of the event has a wrong type, not DOuble: " + event.getContent().getClass(),
event.getContent() instanceof Double);
assertTrue("SourceId of the event has a wrong type, not EventBasedTally: " + event.getSourceId().getClass(),
event.getSourceId() instanceof EventBasedTally);
this.observationEvents++;
}
/**
* @return countEvents
*/
public int getObservationEvents()
{
return this.observationEvents;
}
}
}