| // |
| // Copyright 2021 Google LLC |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| // |
| |
| package com.google.privacy.differentialprivacy; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| import static com.google.privacy.differentialprivacy.proto.SummaryOuterClass.MechanismType.LAPLACE; |
| import static org.junit.Assert.assertThrows; |
| import static org.mockito.ArgumentMatchers.anyDouble; |
| import static org.mockito.ArgumentMatchers.anyInt; |
| import static org.mockito.Mockito.when; |
| |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import org.junit.Before; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.JUnit4; |
| import org.mockito.Mock; |
| import org.mockito.junit.MockitoJUnit; |
| import org.mockito.junit.MockitoRule; |
| |
| /** Tests the confidence intervals provided by {@link BoundedQuantiles}. */ |
| @RunWith(JUnit4.class) |
| public class BoundedQuantilesConfidenceIntervalTest { |
| private static final double ARBITRARY_EPSILON = 5.5; |
| private static final double ARBITRARY_DELTA = 0.00001; |
| private static final int ARBITRARY_MAX_CONTRIBUTIONS_PER_PARTITION = 1; |
| private static final int ARBITRARY_MAX_PARTITIONS_CONTRIBUTED = 1; |
| private static final double ARBITRARY_LOWER = -2.68545; |
| private static final double ARBITRARY_UPPER = 2.68545; |
| private static final int ARBITRARY_TREE_HEIGHT = 10; |
| private static final int ARBITRARY_BRANCHING_FACTOR = 3; |
| private static final double ARBITRARY_RANK = 0.54321; |
| private static final double ARBITRARY_ALPHA = 0.23645; |
| private static final ImmutableList<Double> ARBITRARY_DISTRIBUTION = |
| ImmutableList.of( |
| -3.0, -3.0, -2.98, -2.97, -2.95, -2.95, -1.99, -1.94, -1.89, -0.5, -0.43, -0.43, 0.02, |
| 0.02, 0.021, 0.22, 0.22, 0.29, 0.32, 0.38, 0.68, 0.69, 0.81, 0.86, 0.99, 1.32, 22.1); |
| // Number of samples drawn for the statistical evaluation of the confidence intervals. |
| private static final int NUMBER_OF_SAMPLES = 2500; |
| private static final ImmutableList<Double> RANKS = |
| ImmutableList.of(0.0, 0.005, 0.01, 0.05, 0.25, 0.5, 0.75, 0.95, 0.99, 0.995, 1.0); |
| private static final ImmutableList<Double> ALPHAS = |
| ImmutableList.of(0.0001, 0.001, 0.01, 0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99); |
| // Minimum number of times the raw value needs to be contained in the confidence interval for a |
| // given alpha so that the statistical test accepts. The failure probability is less than 10^-6. |
| private static final ImmutableMap<Double, Integer> ACCEPT_THRESHOLDS = |
| new ImmutableMap.Builder<Double, Integer>() |
| .put(0.0001, 2494) |
| .put(0.001, 2486) |
| .put(0.01, 2447) |
| .put(0.05, 2319) |
| .put(0.1, 2175) |
| .put(0.25, 1769) |
| .put(0.5, 1130) |
| .put(0.75, 523) |
| .put(0.9, 181) |
| .put(0.95, 76) |
| .put(0.99, 4) |
| .build(); |
| |
| @Rule public final MockitoRule mocks = MockitoJUnit.rule(); |
| |
| private BoundedQuantiles.Params.Builder builder; |
| @Mock private Noise mockNoise; |
| |
| @Before |
| public void setUp() { |
| builder = |
| BoundedQuantiles.builder() |
| .epsilon(ARBITRARY_EPSILON) |
| .delta(ARBITRARY_DELTA) |
| .noise(new GaussianNoise()) |
| .maxPartitionsContributed(ARBITRARY_MAX_PARTITIONS_CONTRIBUTED) |
| .maxContributionsPerPartition(ARBITRARY_MAX_CONTRIBUTIONS_PER_PARTITION) |
| .lower(ARBITRARY_LOWER) |
| .upper(ARBITRARY_UPPER) |
| .treeHeight(ARBITRARY_TREE_HEIGHT) |
| .branchingFactor(ARBITRARY_BRANCHING_FACTOR); |
| } |
| |
| @Test |
| public void computeConfidenceInterval_noNoise_intervalMatchesQuantile() { |
| // Mock a noise mechanism that adds no noise and returns confidence intervals of size 0. |
| when(mockNoise.addNoise(anyDouble(), anyInt(), anyDouble(), anyDouble(), anyDouble())) |
| .thenAnswer(invocation -> invocation.getArguments()[0]); |
| when(mockNoise.computeConfidenceInterval( |
| anyDouble(), anyInt(), anyDouble(), anyDouble(), anyDouble(), anyDouble())) |
| .thenAnswer( |
| invocation -> |
| ConfidenceInterval.create( |
| (double) invocation.getArguments()[0], (double) invocation.getArguments()[0])); |
| |
| BoundedQuantiles quantiles = builder.noise(mockNoise).build(); |
| quantiles.addEntries(ARBITRARY_DISTRIBUTION); |
| |
| for (double rank : RANKS) { |
| double rawValue = quantiles.computeResult(rank); |
| ConfidenceInterval confidenceInterval = |
| quantiles.computeConfidenceInterval(rank, ARBITRARY_ALPHA); |
| |
| assertThat(confidenceInterval.lowerBound()).isEqualTo(rawValue); |
| assertThat(confidenceInterval.upperBound()).isEqualTo(rawValue); |
| } |
| } |
| |
| @Test |
| public void computeConfidenceInterval_zeroConfidenceLevel_intervalMatchesNoisedQuantile() { |
| // In general it is not possible to specify a confidence level of zero. To simulate this, we |
| // mock a noise mechanism that adds noise, but returns confidence intervals of size 0. |
| Noise noise = new GaussianNoise(); |
| when(mockNoise.addNoise(anyDouble(), anyInt(), anyDouble(), anyDouble(), anyDouble())) |
| .thenAnswer( |
| invocation -> |
| noise.addNoise( |
| (double) invocation.getArguments()[0], |
| (int) invocation.getArguments()[1], |
| (double) invocation.getArguments()[2], |
| (double) invocation.getArguments()[3], |
| (double) invocation.getArguments()[4])); |
| when(mockNoise.computeConfidenceInterval( |
| anyDouble(), anyInt(), anyDouble(), anyDouble(), anyDouble(), anyDouble())) |
| .thenAnswer( |
| invocation -> |
| ConfidenceInterval.create( |
| (double) invocation.getArguments()[0], (double) invocation.getArguments()[0])); |
| |
| BoundedQuantiles quantiles = builder.noise(mockNoise).build(); |
| quantiles.addEntries(ARBITRARY_DISTRIBUTION); |
| |
| for (double rank : RANKS) { |
| double rawValue = quantiles.computeResult(rank); |
| ConfidenceInterval confidenceInterval = |
| quantiles.computeConfidenceInterval(rank, ARBITRARY_ALPHA); |
| |
| assertThat(confidenceInterval.lowerBound()).isEqualTo(rawValue); |
| assertThat(confidenceInterval.upperBound()).isEqualTo(rawValue); |
| } |
| } |
| |
| @Test |
| public void computeConfidenceInterval_emptyDistribution_lowerBoundLessThanUpperBound() { |
| for (int i = 0; i < 1000; i++) { |
| BoundedQuantiles quantiles = builder.build(); |
| // Compute an arbitrary quantile to apply noise and enable confidence interval queries. |
| quantiles.computeResult(ARBITRARY_RANK); |
| |
| // Use a large alpha to increase the chance of a violation. |
| for (double rank : RANKS) { |
| assertThat(quantiles.computeConfidenceInterval(rank, /*alpha=*/ 0.99).lowerBound()) |
| .isAtMost(quantiles.computeConfidenceInterval(rank, /*alpha=*/ 0.99).upperBound()); |
| } |
| } |
| } |
| |
| @Test |
| public void computeConfidenceInterval_arbitraryDistribution_lowerBoundLessThanUpperBound() { |
| for (int i = 0; i < 1000; i++) { |
| BoundedQuantiles quantiles = builder.build(); |
| quantiles.addEntries(ARBITRARY_DISTRIBUTION); |
| // Compute an arbitrary quantile to apply noise and enable confidence interval queries. |
| quantiles.computeResult(ARBITRARY_RANK); |
| |
| // Use a large alpha to increase the chance of a violation. |
| for (double rank : RANKS) { |
| assertThat(quantiles.computeConfidenceInterval(rank, /*alpha=*/ 0.99).lowerBound()) |
| .isAtMost(quantiles.computeConfidenceInterval(rank, /*alpha=*/ 0.99).upperBound()); |
| } |
| } |
| } |
| |
| @Test |
| public void computeConfidenceInterval_resultWithinBounds() { |
| builder = builder.lower(1.0).upper(2.0); |
| |
| for (int i = 0; i < 1000; i++) { |
| BoundedQuantiles quantiles = builder.build(); |
| quantiles.addEntries(ImmutableList.of(1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0)); |
| quantiles.addEntries(ImmutableList.of(2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0)); |
| // Compute an arbitrary quantile to apply noise and enable confidence interval queries. |
| quantiles.computeResult(ARBITRARY_RANK); |
| |
| // To increase the chance of a violation, we use a small alpha and consider the confidence |
| // intervals of the min and max quantile, which match the bounds of the input range. |
| assertThat(quantiles.computeConfidenceInterval(/*rank=*/ 0.0, /*alpha=*/ 0.01).lowerBound()) |
| .isAtLeast(1.0); |
| assertThat(quantiles.computeConfidenceInterval(/*rank=*/ 1.0, /*alpha=*/ 0.01).upperBound()) |
| .isAtMost(2.0); |
| } |
| } |
| |
| @Test |
| public void computeConfidenceInterval_gaussianNoise_calledTwiceForSameAlpha_returnsSameResult() { |
| BoundedQuantiles quantiles = builder.noise(new GaussianNoise()).build(); |
| quantiles.addEntries(ARBITRARY_DISTRIBUTION); |
| // Compute an arbitrary quantile to apply noise and enable confidence interval queries. |
| quantiles.computeResult(ARBITRARY_RANK); |
| |
| for (double rank : RANKS) { |
| assertThat(quantiles.computeConfidenceInterval(rank, ARBITRARY_ALPHA)) |
| .isEqualTo(quantiles.computeConfidenceInterval(rank, ARBITRARY_ALPHA)); |
| } |
| } |
| |
| @Test |
| public void computeConfidenceInterval_laplaceNoise_calledTwiceForSameAlpha_returnsSameResult() { |
| BoundedQuantiles quantiles = builder.noise(new LaplaceNoise()).delta(null).build(); |
| quantiles.addEntries(ARBITRARY_DISTRIBUTION); |
| // Compute an arbitrary quantile to apply noise and enable confidence interval queries. |
| quantiles.computeResult(ARBITRARY_RANK); |
| |
| for (double rank : RANKS) { |
| assertThat(quantiles.computeConfidenceInterval(rank, ARBITRARY_ALPHA)) |
| .isEqualTo(quantiles.computeConfidenceInterval(rank, ARBITRARY_ALPHA)); |
| } |
| } |
| |
| @Test |
| public void |
| computeConfidenceInterval_gaussianNoise_resultForSmallAlphaContainedInResultForLargeAlpha() { |
| BoundedQuantiles quantiles = builder.noise(new GaussianNoise()).build(); |
| quantiles.addEntries(ARBITRARY_DISTRIBUTION); |
| // Compute an arbitrary quantile to apply noise and enable confidence interval queries. |
| quantiles.computeResult(ARBITRARY_RANK); |
| |
| for (double rank : RANKS) { |
| assertThat(quantiles.computeConfidenceInterval(rank, ARBITRARY_ALPHA * 0.5).lowerBound()) |
| .isAtMost(quantiles.computeConfidenceInterval(rank, ARBITRARY_ALPHA).lowerBound()); |
| assertThat(quantiles.computeConfidenceInterval(rank, ARBITRARY_ALPHA * 0.5).upperBound()) |
| .isAtLeast(quantiles.computeConfidenceInterval(rank, ARBITRARY_ALPHA).upperBound()); |
| } |
| } |
| |
| @Test |
| public void |
| computeConfidenceInterval_laplaceNoise_resultForSmallAlphaContainedInResultForLargeAlpha() { |
| BoundedQuantiles quantiles = builder.noise(new LaplaceNoise()).delta(null).build(); |
| quantiles.addEntries(ARBITRARY_DISTRIBUTION); |
| // Compute an arbitrary quantile to apply noise and enable confidence interval queries. |
| quantiles.computeResult(ARBITRARY_RANK); |
| |
| for (double rank : RANKS) { |
| assertThat(quantiles.computeConfidenceInterval(rank, ARBITRARY_ALPHA * 0.5).lowerBound()) |
| .isAtMost(quantiles.computeConfidenceInterval(rank, ARBITRARY_ALPHA).lowerBound()); |
| assertThat(quantiles.computeConfidenceInterval(rank, ARBITRARY_ALPHA * 0.5).upperBound()) |
| .isAtLeast(quantiles.computeConfidenceInterval(rank, ARBITRARY_ALPHA).upperBound()); |
| } |
| } |
| |
| @Test |
| public void computeConfidenceInterval_oneEntry_gaussianNoise_satisfiesConfidenceLevel() { |
| statisticallyAssertConfidenceLevel( |
| /* entries= */ ImmutableList.of(0.0), builder, new GaussianNoise()); |
| } |
| |
| @Test |
| public void computeConfidenceInterval_oneEntry_laplaceNoise_satisfiesConfidenceLevel() { |
| statisticallyAssertConfidenceLevel( |
| /* entries= */ ImmutableList.of(0.0), builder, new LaplaceNoise()); |
| } |
| |
| @Test |
| public void computeConfidenceInterval_uniformEntries_gaussianNoise_satisfiesConfidenceLevel() { |
| ImmutableList.Builder<Double> entriesBuilder = new ImmutableList.Builder<>(); |
| for (int j = 0; j <= 500; j++) { |
| entriesBuilder.add(j / 500.0); |
| } |
| ImmutableList<Double> entries = entriesBuilder.build(); |
| |
| statisticallyAssertConfidenceLevel(entries, builder, new GaussianNoise()); |
| } |
| |
| @Test |
| public void computeConfidenceInterval_uniformEntries_laplaceNoise_satisfiesConfidenceLevel() { |
| ImmutableList.Builder<Double> entriesBuilder = new ImmutableList.Builder<>(); |
| for (int j = 0; j <= 500; j++) { |
| entriesBuilder.add(j / 500.0); |
| } |
| ImmutableList<Double> entries = entriesBuilder.build(); |
| |
| statisticallyAssertConfidenceLevel(entries, builder, new LaplaceNoise()); |
| } |
| |
| @Test |
| public void computeConfidenceInterval_constantEntries_gaussianNoise_satisfiesConfidenceLevel() { |
| ImmutableList.Builder<Double> entriesBuilder = new ImmutableList.Builder<>(); |
| for (int j = 0; j <= 20; j++) { |
| entriesBuilder.add(1.5); |
| } |
| ImmutableList<Double> entries = entriesBuilder.build(); |
| |
| statisticallyAssertConfidenceLevel(entries, builder, new GaussianNoise()); |
| } |
| |
| @Test |
| public void computeConfidenceInterval_constantEntries_laplaceNoise_satisfiesConfidenceLevel() { |
| ImmutableList.Builder<Double> entriesBuilder = new ImmutableList.Builder<>(); |
| for (int j = 0; j <= 20; j++) { |
| entriesBuilder.add(1.5); |
| } |
| ImmutableList<Double> entries = entriesBuilder.build(); |
| |
| statisticallyAssertConfidenceLevel(entries, builder, new LaplaceNoise()); |
| } |
| |
| @Test |
| public void computeConfidenceInterval_bernoulliEntries_gaussianNoise_satisfiesConfidenceLevel() { |
| ImmutableList.Builder<Double> entriesBuilder = new ImmutableList.Builder<>(); |
| for (int j = 0; j <= 100; j++) { |
| entriesBuilder.add(1.0); |
| entriesBuilder.add(-1.0); |
| } |
| ImmutableList<Double> entries = entriesBuilder.build(); |
| |
| statisticallyAssertConfidenceLevel(entries, builder, new GaussianNoise()); |
| } |
| |
| @Test |
| public void computeConfidenceInterval_bernoulliEntries_laplaceNoise_satisfiesConfidenceLevel() { |
| ImmutableList.Builder<Double> entriesBuilder = new ImmutableList.Builder<>(); |
| for (int j = 0; j <= 100; j++) { |
| entriesBuilder.add(1.0); |
| entriesBuilder.add(-1.0); |
| } |
| ImmutableList<Double> entries = entriesBuilder.build(); |
| |
| statisticallyAssertConfidenceLevel(entries, builder, new LaplaceNoise()); |
| } |
| |
| @Test |
| public void computeConfidenceInterval_calledBeforeComputeResult_throwsException() { |
| BoundedQuantiles quantiles = builder.build(); |
| assertThrows( |
| IllegalStateException.class, |
| () -> quantiles.computeConfidenceInterval(ARBITRARY_RANK, ARBITRARY_ALPHA)); |
| } |
| |
| @Test |
| public void computeConfidenceInterval_calledAfterSerialization_throwsException() { |
| BoundedQuantiles quantiles = builder.build(); |
| quantiles.getSerializableSummary(); |
| assertThrows( |
| IllegalStateException.class, |
| () -> quantiles.computeConfidenceInterval(ARBITRARY_RANK, ARBITRARY_ALPHA)); |
| } |
| |
| private void statisticallyAssertConfidenceLevel( |
| List<Double> entries, BoundedQuantiles.Params.Builder builder, Noise noise) { |
| // Prepare a hit counter that counts the number of times a confidence interval contains the raw |
| // value keyed by rank and alpha. |
| Map<Double, Map<Double, Integer>> hitCounter = new HashMap<>(); |
| for (double rank : RANKS) { |
| hitCounter.put(rank, new HashMap<>()); |
| for (double alpha : ALPHAS) { |
| hitCounter.get(rank).put(alpha, /* hit count */ 0); |
| } |
| } |
| |
| // Mock a noise mechanism that adds no noise. |
| when(mockNoise.addNoise(anyDouble(), anyInt(), anyDouble(), anyDouble(), anyDouble())) |
| .thenAnswer(invocation -> invocation.getArguments()[0]); |
| |
| // Approximate the raw quantile values by querying a zero noise instance of the quantiles |
| // mechanism. |
| Map<Double, Double> rawValues = new HashMap<>(); |
| BoundedQuantiles zeroNoiseQuantiles = builder.noise(mockNoise).build(); |
| zeroNoiseQuantiles.addEntries(entries); |
| for (double rank : RANKS) { |
| rawValues.put(rank, zeroNoiseQuantiles.computeResult(rank)); |
| } |
| |
| builder.noise(noise); |
| if (noise.getMechanismType() == LAPLACE) { |
| builder.delta(null); |
| } |
| |
| // Sample the hit frequencies. |
| for (int i = 0; i < NUMBER_OF_SAMPLES; i++) { |
| BoundedQuantiles quantiles = builder.build(); |
| quantiles.addEntries(entries); |
| // Compute an arbitrary quantile to apply noise and enable confidence interval queries. |
| quantiles.computeResult(ARBITRARY_RANK); |
| |
| // Check whether the confidence intervals contain the respective raw value for all ranks and |
| // alphas. |
| for (double rank : RANKS) { |
| for (double alpha : ALPHAS) { |
| if (quantiles.computeConfidenceInterval(rank, alpha).lowerBound() <= rawValues.get(rank) |
| && rawValues.get(rank) |
| <= quantiles.computeConfidenceInterval(rank, alpha).upperBound()) { |
| hitCounter.get(rank).put(alpha, hitCounter.get(rank).get(alpha) + 1); |
| } |
| } |
| } |
| } |
| |
| // Assert that the hit frequency was sufficiently large for all ranks and alphas. |
| for (double rank : RANKS) { |
| for (double alpha : ALPHAS) { |
| assertThat(hitCounter.get(rank).get(alpha)).isAtLeast(ACCEPT_THRESHOLDS.get(alpha)); |
| } |
| } |
| } |
| } |