-
Notifications
You must be signed in to change notification settings - Fork 347
Dealing with probabilistic tests #478
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9da8eb1
b4da480
e60f0b5
05bdf25
e9fedb6
de57b71
409ffc9
0f997f3
118a380
d0c0187
c66d1c3
1e5e9a2
9bd6a7f
587bb63
b76b6af
4963b1b
931aad9
96d94bc
c6da416
e56b612
46390c9
97f06eb
96e953a
6653e65
a750192
9e599c1
185eab3
bdb7354
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| package com.twitter.algebird | ||
|
|
||
| import org.scalacheck.{ Gen, Prop, Properties, Test } | ||
| import org.scalacheck.util.Pretty | ||
|
|
||
| trait ApproximateProperty { | ||
| type Exact | ||
| type Approx | ||
| type Input | ||
| type Result | ||
|
|
||
| def exactGenerator: Gen[Exact] | ||
| def inputGenerator(e: Exact): Gen[Input] | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what is this exact value corresponding to?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Exact is useful here so that we can generate appropriate test inputs. For example, suppose we're testing CMS[Long]. We need to generate some inputs so that we can query I'm not sure if this is the best way to do this.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. but why couldn't we do something like: // I need an exact value for my implementation:
def needExact(e: Exact): Gen[Input]
// but I could still implement this:
def inputGenerator: Gen[Input] = exactGenerator.flatMap(needExact) Am I missing something? Putting this in the trait as is seems to be constraining more than is needed. Why can't we just get by with |
||
|
|
||
| def makeApproximate(e: Exact): Approx | ||
|
|
||
| def exactResult(e: Exact, i: Input): Result | ||
| def approximateResult(a: Approx, i: Input): ApproximateSet[Result] | ||
| } | ||
|
|
||
| object ApproximateProperty { | ||
|
|
||
| /** | ||
| * Generates a list of exactly n Ts. | ||
| * Useful because `Gen.listOfN(n, gen).sample` gives us Option[List[T]], | ||
| * while we often want List[T]. | ||
| */ | ||
| private def genListOf[T](n: Int, gen: Gen[T]): List[T] = { | ||
| Gen.listOfN(n, gen).sample match { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm concerned that this is not using the Gen.Parameters, so I'm afraid that when scalacheck gives us an example input that fails a test, we can't check it at the REPL. Is there some way to plumb through the Random instance from Gen.Parameters (I think has it), so that we are not using a time-seeded Random?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This might not a problem since I'm not using scalacheck's error reporting mechanism. (I had to write my own hacky error reporter using scalacheck args.)
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That link just gives me "This is not an active repository". Are there some creds needed to see it? Can you post a screenshot?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It actually looks like the travis settings somehow got broken for algebird, it seems to be disabled in travis. /cc @caniszczyk |
||
| case Some(xs) => xs | ||
| case _ => genListOf(n, gen) | ||
| } | ||
| } | ||
|
|
||
| private def successesAndProbabilities(a: ApproximateProperty, objectReps: Int, inputReps: Int): List[(Int, Double, List[String])] = | ||
| genListOf(objectReps, a.exactGenerator) | ||
| .flatMap { exact => | ||
| val approx = a.makeApproximate(exact) | ||
| genListOf(inputReps, a.inputGenerator(exact)).flatMap { input => | ||
| val approxResult = a.approximateResult(approx, input) | ||
| val exactResult = a.exactResult(exact, input) | ||
|
|
||
| val success = approxResult.contains(exactResult) | ||
| if (success.withProb == 0.0) { | ||
| None | ||
| } else { | ||
| val successInt = if (success.isTrue) 1 else 0 | ||
| val messages = if (success.isTrue) List() else List(s"Exact result: $exactResult. Approx result: $approxResult.") | ||
| Some((successInt, success.withProb, messages)) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| def toProp(a: ApproximateProperty, objectReps: Int, inputReps: Int, falsePositiveRate: Double): Prop = | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A better name for |
||
| new Prop { | ||
| def apply(params: Gen.Parameters) = { | ||
| require(0 <= falsePositiveRate && falsePositiveRate <= 1) | ||
|
|
||
| val list = successesAndProbabilities(a, objectReps, inputReps) | ||
| val n = list.length | ||
|
|
||
| val monoid = implicitly[Monoid[(Int, Double, List[String])]] | ||
| val (successes, sumOfProbabilities, exacts) = monoid.sum(list) | ||
|
|
||
| // Computed from Hoeffding's inequality, might be inaccurate | ||
| // TODO Make sure this is correct | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let's link to: and add the result; So, I think there is a slight error in your version (n is in the numerator in yours).
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. diff = n * fprate, so it's correct in the numerator. My only concern here is that the formula is for the Bernoulli version of Hoeffding's. Since we're taking separate probability values from each result, we aren't guaranteed that they'll be uniform (very possibly they won't be). But maybe it's "close enough."
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The general case (https://en.wikipedia.org/wiki/Hoeffding%27s_inequality#General_case) is (here I used So I think Oscar's right.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Whoops, I read Joe's comment again, and he's right.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree. I overlooked the I think the bound applies perfectly, with the assumption that our |
||
| val diff = scala.math.sqrt(-n * scala.math.log(falsePositiveRate) / 2.0) | ||
|
|
||
| val success = if (successes >= (sumOfProbabilities - diff)) Prop.Proof else Prop.False | ||
|
|
||
| // Args that get printed when Scalacheck runs the test | ||
| val argsList: List[(String, String)] = { | ||
| val results = List(("Successes", s"$successes (out of $n)"), | ||
| ("Expected successes", "%.2f".format(sumOfProbabilities)), | ||
| ("Required successes", "%.2f".format(sumOfProbabilities - diff))) | ||
|
|
||
| val exampleFailures = | ||
| if (success == Prop.False) | ||
| List(("Example failures:\n >", exacts.take(5).mkString("\n >"))) | ||
| else List() | ||
|
|
||
| val zeroProbTests = objectReps * inputReps - n | ||
| val testsReturnedZeroProb = | ||
| if (zeroProbTests > 0) { | ||
| List(("Omitted results", s"${zeroProbTests}/${objectReps * inputReps} tests returned an Approximate with probability 0. These tests have been omitted from the calculation.")) | ||
| } else List() | ||
|
|
||
| results ++ exampleFailures ++ testsReturnedZeroProb | ||
| } | ||
|
|
||
| val args = argsList.map { | ||
| case (name, value) => | ||
| Prop.Arg(name, value, 0, value, Pretty.prettyAny(value), Pretty.prettyAny(value)) | ||
| } | ||
|
|
||
| Prop.Result(success, args = args) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. scalacheck prints these args when the test is run, so that we can see the number of successes, etc. when we run the tests |
||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Converts a list of ApproximateProperties to a scalacheck Prop that | ||
| * fails if too many of the ApproximateProperties fail. | ||
| * TODO use `new Prop` like the above `toProp` method so that we can | ||
| * have useful error messages. | ||
| */ | ||
| def toProp(a: Seq[ApproximateProperty], objectReps: Int, inputReps: Int, falsePositiveRate: Double): Prop = { | ||
| require(0 <= falsePositiveRate && falsePositiveRate <= 1) | ||
|
|
||
| val list = a.flatMap { approximateProp => | ||
| successesAndProbabilities(approximateProp, objectReps, inputReps) | ||
| } | ||
| val monoid = implicitly[Monoid[(Int, Double, List[String])]] | ||
| val (successes, sumOfProbabilities, _) = monoid.sum(list) | ||
| val n = list.length | ||
|
|
||
| (sumOfProbabilities - successes) > scala.math.sqrt(n * scala.math.log(falsePositiveRate) / -2) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * All tests that use ApproximateProperty should extend from this class so that | ||
| * the scalacheck property is run exactly once. | ||
| */ | ||
| abstract class ApproximateProperties(name: String) extends Properties(name) { | ||
| def overrideParameters(p: Test.Parameters): Test.Parameters = | ||
| p.withMinSuccessfulTests(1) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is really nice and quite well designed. Good work.