From 1ccd36edc24fd8eb81fb6dc7c979906d34522632 Mon Sep 17 00:00:00 2001 From: Laurent Date: Thu, 14 Feb 2019 16:01:33 +0100 Subject: [PATCH] Added stats + calculation --- .../android/calculus/Calculator.kt | 118 +++++++++++++++--- .../android/calculus/Computable.kt | 4 + .../pokeranalytics/android/calculus/Stat.kt | 35 +++++- .../android/model/realm/Result.kt | 2 +- .../android/model/realm/Session.kt | 73 ++++++++--- .../android/model/realm/TimeFrame.kt | 61 ++++++++- .../android/model/realm/TimeFrameGroup.kt | 15 ++- .../pokeranalytics/android/ExampleUnitTest.kt | 43 ++++++- 8 files changed, 296 insertions(+), 55 deletions(-) diff --git a/app/src/main/java/net/pokeranalytics/android/calculus/Calculator.kt b/app/src/main/java/net/pokeranalytics/android/calculus/Calculator.kt index 3cb8d1ff..4dbdd4b0 100644 --- a/app/src/main/java/net/pokeranalytics/android/calculus/Calculator.kt +++ b/app/src/main/java/net/pokeranalytics/android/calculus/Calculator.kt @@ -1,5 +1,6 @@ package net.pokeranalytics.android.calculus +import net.pokeranalytics.android.calculus.Stat.* import net.pokeranalytics.android.model.realm.TimeFrameGroup @@ -25,6 +26,15 @@ class Calculator { var evolutionValues: EvolutionValues = EvolutionValues.NONE var displayedStats: List = listOf() + fun shouldComputeStandardDeviation() : Boolean { + this.displayedStats.forEach { stat -> + when (stat) { + STANDARD_DEVIATION, STANDARD_DEVIATION_HOURLY, STANDARD_DEVIATION_BB_PER_100_HANDS -> return true + } + } + return false + } + // var aggregation: Aggregation? = null } @@ -62,43 +72,113 @@ class Calculator { fun compute(group: SessionGroup, options: Options) : ComputedResults { val sessions: List = group.sessionGroup - val series: Set = setOf() // @todo get unique list of serie + val sessionGroups: Set = setOf() // @todo get unique list of serie var results: ComputedResults = ComputedResults() var sum: Double = 0.0 - var duration: Double = 0.0 + var totalHands: Double = 0.0 + var bbSum: Double = 0.0 + var bbSessionCount: Int = 0 + var winningSessionCount: Int = 0 + var totalBuyin: Double = 0.0 + // @todo add all stats // Compute for each session - sessions.forEach { item -> - sum += item.value - - when (options.evolutionValues) { - Options.EvolutionValues.STANDARD -> { - results.addEvolutionValue(sum, Stat.NETRESULT) - } - Options.EvolutionValues.DATED -> { - results.addEvolutionValue(sum, duration, Stat.NETRESULT) - } + var index: Int = 0 + sessions.forEach { s -> + index++; + + bbSessionCount += s.bigBlindSessionCount + if (s.value >= 0) { + winningSessionCount++ + } + totalBuyin += s.buyin + + if (options.evolutionValues != Options.EvolutionValues.NONE) { + + sum += s.value + totalHands += s.estimatedHands + bbSum += s.bbNetResult + + results.addEvolutionValue(sum / index, AVERAGE) + results.addEvolutionValue(index.toDouble(), NUMBER_OF_GAMES) + results.addEvolutionValue(bbSum / bbSessionCount, AVERAGE_NET_BB) + results.addEvolutionValue((winningSessionCount / index).toDouble(), WIN_RATIO) + results.addEvolutionValue(totalBuyin / index, AVERAGE_BUYIN) + results.addEvolutionValue(Stat.returnOnInvestment(sum, totalBuyin), ROI) } } // Compute for each serie - series.forEach { serie -> - duration += serie.duration + var duration: Double = 0.0 + var hourlyRate: Double = 0.0; var hourlyRateBB: Double = 0.0 + + index = 0; sum = 0.0; totalHands = 0.0; bbSum = 0.0; + sessionGroups.forEach { group -> + index++ + duration += group.duration + sum += group.netResult + totalHands += group.estimatedHands + bbSum += group.bbNetResult + + hourlyRate = sum / duration * 3600.0 + hourlyRateBB = bbSum / duration * 3600.0 if (options.evolutionValues != Options.EvolutionValues.NONE) { - results.addEvolutionValue(duration, Stat.DURATION) + results.addEvolutionValue(sum, duration, NETRESULT) + results.addEvolutionValue(sum / duration * 3600.0, duration, HOURLY_RATE) + results.addEvolutionValue(Stat.netBBPer100Hands(bbSum, totalHands), duration, NET_BB_PER_100_HANDS) + results.addEvolutionValue(hourlyRate, duration, HOURLY_RATE) + results.addEvolutionValue(index.toDouble(), duration, NUMBER_OF_GROUPS) + results.addEvolutionValue(group.duration, duration, DURATION) + results.addEvolutionValue(duration / index, duration, AVERAGE_DURATION) + results.addEvolutionValue(hourlyRateBB, duration, HOURLY_RATE_BB) } } + val average: Double = sum / sessions.size + // Create stats results.addStats(setOf( - ComputedStat(Stat.NETRESULT, sum), - ComputedStat(Stat.AVERAGE,sum / sessions.size), - ComputedStat(Stat.DURATION, duration) - )) + ComputedStat(NETRESULT, sum), + ComputedStat(HOURLY_RATE, hourlyRate), + ComputedStat(AVERAGE, average), + ComputedStat(DURATION, duration), + ComputedStat(NUMBER_OF_GROUPS, sessionGroups.size.toDouble()), + ComputedStat(NUMBER_OF_GAMES, sessions.size.toDouble()), + ComputedStat(AVERAGE_DURATION, (duration / 3600.0) / sessions.size), + ComputedStat(NET_BB_PER_100_HANDS, Stat.netBBPer100Hands(bbSum, totalHands)), + ComputedStat(HOURLY_RATE_BB, bbSum / duration * 3600.0), + ComputedStat(AVERAGE_NET_BB, bbSum / bbSessionCount), + ComputedStat(WIN_RATIO, (winningSessionCount / sessions.size).toDouble()), + ComputedStat(AVERAGE_BUYIN, totalBuyin / sessions.size), + ComputedStat(ROI, Stat.returnOnInvestment(sum, totalBuyin)) + + )) + + + // Standard Deviation + if (options.shouldComputeStandardDeviation()) { + + var stdSum: Double = 0.0 + sessions.forEach { s -> + stdSum += Math.pow(s.value - average, 2.0) + } + val standardDeviation: Double = Math.sqrt(stdSum / sessions.size) + + var hourlyStdSum: Double = 0.0 + sessionGroups.forEach { sg -> + hourlyStdSum += Math.pow(sg.hourlyRate - hourlyRate, 2.0) + } + val hourlyStandardDeviation: Double = Math.sqrt(hourlyStdSum / sessionGroups.size) + + results.addStats(setOf( + ComputedStat(STANDARD_DEVIATION, standardDeviation), + ComputedStat(Stat.STANDARD_DEVIATION_HOURLY, hourlyStandardDeviation) + )) + } return results } diff --git a/app/src/main/java/net/pokeranalytics/android/calculus/Computable.kt b/app/src/main/java/net/pokeranalytics/android/calculus/Computable.kt index fda109f8..2d341fa9 100644 --- a/app/src/main/java/net/pokeranalytics/android/calculus/Computable.kt +++ b/app/src/main/java/net/pokeranalytics/android/calculus/Computable.kt @@ -10,6 +10,10 @@ interface Summable { // An interface describing some class that can be computed interface SessionInterface : Summable { var serie: TimeFrameGroup + var estimatedHands: Double + var bbNetResult: Double + var bigBlindSessionCount: Int // 0 or 1 + var buyin: Double } /** diff --git a/app/src/main/java/net/pokeranalytics/android/calculus/Stat.kt b/app/src/main/java/net/pokeranalytics/android/calculus/Stat.kt index d3a9afda..a77de252 100644 --- a/app/src/main/java/net/pokeranalytics/android/calculus/Stat.kt +++ b/app/src/main/java/net/pokeranalytics/android/calculus/Stat.kt @@ -7,25 +7,48 @@ interface AnyStat { enum class Stat : AnyStat { NETRESULT, - HOURLYRATE, - DURATION, + HOURLY_RATE, AVERAGE, - STANDARDDEVIATION, - HOURLYSTANDARDDEVIATION; + NUMBER_OF_GROUPS, + NUMBER_OF_GAMES, + DURATION, + AVERAGE_DURATION, + NET_BB_PER_100_HANDS, + HOURLY_RATE_BB, + AVERAGE_NET_BB, + WIN_RATIO, + AVERAGE_BUYIN, + ROI, + STANDARD_DEVIATION, + STANDARD_DEVIATION_HOURLY, + STANDARD_DEVIATION_BB_PER_100_HANDS; fun label() : String = when (this) { NETRESULT -> "" - HOURLYRATE -> "" + HOURLY_RATE -> "" else -> throw IllegalArgumentException("Label not defined") } fun hasDistributionSorting() : Boolean { when (this) { - STANDARDDEVIATION, HOURLYSTANDARDDEVIATION -> return true + STANDARD_DEVIATION, STANDARD_DEVIATION_HOURLY, STANDARD_DEVIATION_BB_PER_100_HANDS -> return true else -> return false } } + companion object { + + fun returnOnInvestment(netResult: Double, buyin: Double) : Double { + return netResult / buyin + } + + fun netBBPer100Hands(netBB: Double, numberOfHands: Double) : Double { + return netBB / numberOfHands * 100 + } + + } + + } enum class CashSessionStat : AnyStat { diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/Result.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/Result.kt index f0fa0f89..548f27b0 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/Result.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/Result.kt @@ -36,7 +36,7 @@ open class Result : RealmObject() { } // The net (readonly) - var net: Double? = null + var net: Double = 0.0 // The transactions associated with the Result, impacting the result var transactions: RealmList = RealmList() diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/Session.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/Session.kt index 04e17243..01a66795 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/Session.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/Session.kt @@ -1,18 +1,13 @@ package net.pokeranalytics.android.model.realm -import io.realm.RealmList -import io.realm.Realm -import io.realm.RealmObject -import io.realm.RealmResults -import io.realm.Sort +import io.realm.* +import io.realm.annotations.Ignore import io.realm.annotations.PrimaryKey -import net.pokeranalytics.android.* import net.pokeranalytics.android.ui.adapter.components.DynamicRowDelegate import net.pokeranalytics.android.ui.adapter.components.DynamicRowInterface import net.pokeranalytics.android.ui.adapter.components.SessionRow import net.pokeranalytics.android.util.data.sessionDao import java.util.* -import java.util.UUID.randomUUID import kotlin.collections.ArrayList @@ -21,6 +16,9 @@ open class Session(comment: String = "") : RealmObject(), DynamicRowDelegate { @PrimaryKey var id = UUID.randomUUID().toString() + // The result of the main user + var result: Result? = null + // The time frame of the Session, i.e. the start & end date var timeFrame: TimeFrame? = null @@ -30,30 +28,27 @@ open class Session(comment: String = "") : RealmObject(), DynamicRowDelegate { // the date of creation of the app var creationDate: Date = Date() + // The bankroll hosting the results + var bankroll: Bankroll? = null + // The limit type: NL, PL... var limit: Int? = null - // The number of tables played at the same time - var numberOfTables: Int = 1 + // The game played during the Session + var game: Game? = null // The number of players at the table var tableSize: Int? = null - // The game played during the Session - var game: Game? = null + // the location where the session is played + var location: Location? = null - // The bankroll hosting the results - var bankroll: Bankroll? = null + // The number of tables played at the same time + var numberOfTables: Int = 1 // The hands list associated with the Session var hands: RealmList = RealmList() - // the location where the session is played - var location: Location? = null - - // The result of the main user - var result: Result? = null - // The list of opponents who participated to the session var opponents: RealmList = RealmList() @@ -85,6 +80,46 @@ open class Session(comment: String = "") : RealmObject(), DynamicRowDelegate { // The features of the tournament, like Knockout, Shootout, Turbo... var tournamentFeatures: RealmList = RealmList() + companion object { + + fun newInstance() : Session { + var session: Session = Session() + session.result = Result() + session.timeFrame = TimeFrame() + return session + } + + } + + @Ignore + var estimatedHands: Double = 0.0 + + @Ignore + var bbNetResult: Double = 0.0 + get() { + this.cgBigBlind?.let { bb -> + this.result?.let { result -> + return result.net / bb + } + } + return 0.0 + } + + @Ignore + var bigBlindSessionCount: Int = if (this.cgBigBlind != null) 1 else 0 +// get() = if (this.cgBigBlind != null) 1 else 0 + + @Ignore + var buyin: Double = 0.0 + get() { + this.result?.let { + it.buyin?.let { + return it + } + } + return 0.0 + } + override fun adapterRows(): ArrayList { val rows = ArrayList() diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/TimeFrame.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/TimeFrame.kt index 886028a7..32042c33 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/TimeFrame.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/TimeFrame.kt @@ -1,24 +1,79 @@ package net.pokeranalytics.android.model.realm import io.realm.RealmObject -import io.realm.annotations.PrimaryKey +import io.realm.RealmResults +import io.realm.annotations.LinkingObjects import java.util.* + + open class TimeFrame : RealmObject() { // A start date var startDate: Date = Date() + set(value) { + field = value + this.computeDuration() + if (this.session != null) { + this.notifyDateChange() + } + } // An end date var endDate: Date? = null + set(value) { + field = value + this.computeDuration() + if (this.session != null) { + this.notifyDateChange() + } + } // The break duration - var breakDuration: Double = 0.0 + var breakDuration: Long = 0L + set(value) { + field = value + this.computeDuration() + } // the total duration - var duration: Double = 0.0 + var duration: Long = 0L + private set // indicates a state of pause var paused: Boolean = false + @LinkingObjects("timeFrame") + private val session: RealmResults? = null + + @LinkingObjects("timeFrame") + private val group: RealmResults? = null + + private fun computeDuration() { + + var endDate: Date + if (this.endDate != null) { + endDate = this.endDate!! + } else { + endDate = Date() + } + this.duration = endDate.time - startDate.time - this.breakDuration + } + + private fun notifyDateChange() { +// val realm = Realm.getDefaultInstance() +// +// var query: RealmQuery = realm.where(TimeFrame::class.java) +// query.greaterThan("startDate", this.startDate.time) +// +// this.endDate?.let { endDate -> +// query.or() +// .greaterThan("startDate", endDate.time) +// .lessThan("endDate", endDate.time) +// } +// +// +// realm.close() + } + } diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/TimeFrameGroup.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/TimeFrameGroup.kt index 4cd6726e..02777b1e 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/TimeFrameGroup.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/TimeFrameGroup.kt @@ -3,8 +3,6 @@ package net.pokeranalytics.android.model.realm import io.realm.RealmList import io.realm.RealmObject import io.realm.annotations.Ignore -import io.realm.annotations.PrimaryKey -import java.util.* open class TimeFrameGroup() : RealmObject() { @@ -13,10 +11,21 @@ open class TimeFrameGroup() : RealmObject() { var timeFrame: TimeFrame? = null // The list of Session played within the group, i.e. played within the same time frame - var timeFrames: RealmList = RealmList() + var sessions: RealmList = RealmList() @Ignore // a duration shortcut var duration: Double = 0.0 + @Ignore // a netResult shortcut + var netResult: Double = 0.0 + + @Ignore // a duration shortcut + var hourlyRate: Double = 0.0 + + @Ignore + var estimatedHands: Double = 0.0 + + @Ignore + var bbNetResult: Double = 0.0 } diff --git a/app/src/test/java/net/pokeranalytics/android/ExampleUnitTest.kt b/app/src/test/java/net/pokeranalytics/android/ExampleUnitTest.kt index 4dc74ef7..b6fbf93a 100644 --- a/app/src/test/java/net/pokeranalytics/android/ExampleUnitTest.kt +++ b/app/src/test/java/net/pokeranalytics/android/ExampleUnitTest.kt @@ -1,17 +1,52 @@ package net.pokeranalytics.android +import net.pokeranalytics.android.calculus.* +import net.pokeranalytics.android.model.realm.TimeFrameGroup +import org.junit.Assert.fail import org.junit.Test -import org.junit.Assert.* - /** * Example local unit test, which will execute on the development machine (host). * * See [testing documentation](http://d.android.com/tools/testing). */ + class ExampleUnitTest { + + class Grade(someValue: Double) : SessionInterface { +// override var serie: Serie = Serie(TimeFrame()) + override var value: Double = someValue + + override var serie: TimeFrameGroup = TimeFrameGroup() + override var estimatedHands: Double = 0.0 + override var bbNetResult: Double = 0.0 + override var bigBlindSessionCount: Int = 0 // 0 or 1 + override var buyin: Double = 0.0 + + } + @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) + fun testStats() { + + val grades: List = listOf(Grade(someValue = 10.0), Grade(someValue = 20.0)) + val group = SessionGroup(name = "test", sessions = grades) + + val results: ComputedResults = Calculator.compute(group, Calculator.Options()) + + val sum = results.computedStat(Stat.NETRESULT) + if (sum != null) { + assert(sum.value == 30.0) + } else { + fail("No Net result stat") + } + + val average = results.computedStat(Stat.AVERAGE) + if (average != null) { + assert(average.value == 15.0) + } else { + fail("No AVERAGE stat") + } + } + }