From effe1db03e3adf4e36e699d59ade2cf63f1429f6 Mon Sep 17 00:00:00 2001 From: Laurent Date: Fri, 29 May 2020 16:44:46 +0200 Subject: [PATCH] First draft for optimal duration computation, add apache math dependency --- app/build.gradle | 3 + .../android/calculus/Calculator.kt | 25 +++- .../pokeranalytics/android/calculus/Stat.kt | 16 ++- .../OptimalDurationCalculator.kt | 123 ++++++++++++++++++ .../android/model/realm/ComputableResult.kt | 27 ++-- .../android/model/realm/Session.kt | 2 + 6 files changed, 174 insertions(+), 22 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 559a13e2..be6e4aef 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -138,6 +138,9 @@ dependencies { // CSV Parser: https://mvnrepository.com/artifact/org.apache.commons/commons-csv implementation 'org.apache.commons:commons-csv:1.7' + // Polynomial Regression + implementation 'org.apache.commons:commons-math3:3.6.1' + // Instrumented Tests androidTestImplementation 'androidx.test:core:1.2.0' androidTestImplementation 'androidx.test:runner:1.2.0' 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 7185f0d9..b934a185 100644 --- a/app/src/main/java/net/pokeranalytics/android/calculus/Calculator.kt +++ b/app/src/main/java/net/pokeranalytics/android/calculus/Calculator.kt @@ -21,6 +21,8 @@ import net.pokeranalytics.android.util.extensions.startOfDay import java.util.* import kotlin.math.max import kotlin.math.min +import kotlin.math.pow +import kotlin.math.sqrt /** * The class performing statIds computation @@ -119,7 +121,7 @@ class Calculator { val computeStandardDeviation: Boolean get() { this.stats.forEach { - if (it == STANDARD_DEVIATION_BB_PER_100_HANDS || it == STANDARD_DEVIATION || it == STANDARD_DEVIATION_HOURLY) { + if (it.isStandardDeviation) { return true } } @@ -335,6 +337,11 @@ class Calculator { ) ) } + var averageBB = 0.0 + if (bbSessionCount > 0) { + averageBB = bbSum / bbSessionCount + results.addStat(AVERAGE_NET_BB, averageBB) + } val shouldIterateOverComputables = (options.progressValues == Options.ProgressValues.STANDARD || options.computeLongestStreak) @@ -561,15 +568,19 @@ class Calculator { // Session var stdSum = 0.0 + var stdBBSum = 0.0 var stdBBper100HandsSum = 0.0 computables.forEach { session -> - stdSum += Math.pow(session.ratedNet - average, 2.0) - stdBBper100HandsSum += Math.pow(session.bbPer100Hands - bbPer100Hands, 2.0) + stdSum += (session.ratedNet - average).pow(2.0) + stdBBSum += (session.bbNet - averageBB).pow(2.0) + stdBBper100HandsSum += (session.bbPer100Hands - bbPer100Hands).pow(2.0) } - val standardDeviation = Math.sqrt(stdSum / computables.size) - val standardDeviationBBper100Hands = Math.sqrt(stdBBper100HandsSum / computables.size) + val standardDeviation = sqrt(stdSum / computables.size) + val standardDeviationBB = sqrt(stdBBSum / computables.size) + val standardDeviationBBper100Hands = sqrt(stdBBper100HandsSum / computables.size) results.addStat(STANDARD_DEVIATION, standardDeviation) + results.addStat(STANDARD_DEVIATION_BB, standardDeviationBB) results.addStat(STANDARD_DEVIATION_BB_PER_100_HANDS, standardDeviationBBper100Hands) // Session Set @@ -578,9 +589,9 @@ class Calculator { sessionSets.forEach { set -> val ssStats = SSStats(set, computableGroup.query) val sHourlyRate = ssStats.hourlyRate - hourlyStdSum += Math.pow(sHourlyRate - hourlyRate, 2.0) + hourlyStdSum += (sHourlyRate - hourlyRate).pow(2.0) } - val hourlyStandardDeviation = Math.sqrt(hourlyStdSum / sessionSets.size) + val hourlyStandardDeviation = sqrt(hourlyStdSum / sessionSets.size) results.addStat(STANDARD_DEVIATION_HOURLY, hourlyStandardDeviation) } 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 1043c434..0034a5b3 100644 --- a/app/src/main/java/net/pokeranalytics/android/calculus/Stat.kt +++ b/app/src/main/java/net/pokeranalytics/android/calculus/Stat.kt @@ -52,6 +52,7 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres BB_SESSION_COUNT(26), TOTAL_BUYIN(27), RISK_OF_RUIN(28), + STANDARD_DEVIATION_BB(29), ; companion object : IntSearchable { @@ -119,6 +120,7 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres STANDARD_DEVIATION -> R.string.standard_deviation STANDARD_DEVIATION_HOURLY -> R.string.standard_deviation_per_hour STANDARD_DEVIATION_BB_PER_100_HANDS -> R.string.standard_deviation_bb_per_100_hands + STANDARD_DEVIATION_BB -> R.string.bb_standard_deviation HANDS_PLAYED -> R.string.number_of_hands LOCATIONS_PLAYED -> R.string.locations_played LONGEST_STREAKS -> R.string.longest_streaks @@ -169,7 +171,7 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres } // white amountsr AVERAGE_BUYIN, STANDARD_DEVIATION, STANDARD_DEVIATION_HOURLY, - STANDARD_DEVIATION_BB_PER_100_HANDS, TOTAL_BUYIN -> { + STANDARD_DEVIATION_BB_PER_100_HANDS, STANDARD_DEVIATION_BB, TOTAL_BUYIN -> { return TextFormat(value.toCurrency(currency)) } LONGEST_STREAKS -> { @@ -200,6 +202,7 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres NUMBER_OF_GAMES -> R.string.number_of_records NET_RESULT -> R.string.total STANDARD_DEVIATION -> R.string.net_result + STANDARD_DEVIATION_BB -> R.string.average_net_result_bb_ STANDARD_DEVIATION_HOURLY -> R.string.hour_rate_without_pauses STANDARD_DEVIATION_BB_PER_100_HANDS -> R.string.net_result_bb_per_100_hands WIN_RATIO, HOURLY_DURATION -> return this.localizedTitle(context) @@ -235,12 +238,21 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres val hasProgressGraph: Boolean get() { return when (this) { - HOURLY_DURATION, AVERAGE_HOURLY_DURATION, + HOURLY_DURATION, AVERAGE_HOURLY_DURATION, STANDARD_DEVIATION_BB, STANDARD_DEVIATION, STANDARD_DEVIATION_HOURLY, STANDARD_DEVIATION_BB_PER_100_HANDS -> false else -> true } } + val isStandardDeviation: Boolean + get() { + return when (this) { + STANDARD_DEVIATION, STANDARD_DEVIATION_BB, + STANDARD_DEVIATION_HOURLY, STANDARD_DEVIATION_BB_PER_100_HANDS -> true + else -> false + } + } + val legendHideRightValue: Boolean get() { diff --git a/app/src/main/java/net/pokeranalytics/android/calculus/optimalduration/OptimalDurationCalculator.kt b/app/src/main/java/net/pokeranalytics/android/calculus/optimalduration/OptimalDurationCalculator.kt index 4a5f611a..d468be9c 100644 --- a/app/src/main/java/net/pokeranalytics/android/calculus/optimalduration/OptimalDurationCalculator.kt +++ b/app/src/main/java/net/pokeranalytics/android/calculus/optimalduration/OptimalDurationCalculator.kt @@ -1,2 +1,125 @@ package net.pokeranalytics.android.calculus.optimalduration +import io.realm.Realm +import net.pokeranalytics.android.calculus.Calculator +import net.pokeranalytics.android.calculus.Stat +import net.pokeranalytics.android.exceptions.PAIllegalStateException +import net.pokeranalytics.android.model.filter.Query +import net.pokeranalytics.android.model.filter.QueryCondition +import net.pokeranalytics.android.model.realm.Session +import org.apache.commons.math3.fitting.PolynomialCurveFitter +import org.apache.commons.math3.fitting.WeightedObservedPoints +import java.util.* +import kotlin.math.pow + +class OptimalDurationCalculator { + + companion object { + + fun start(session: Session, completion: (Double) -> (Unit)) { + + if (!session.isCashGame()) { + throw PAIllegalStateException("this only makes sense for cash game sessions") + } + + val realm = Realm.getDefaultInstance() + + val isLive = session.bankroll?.live ?: true + + val query = Query().add(QueryCondition.IsCash) // cash game + query.add(if (isLive) { QueryCondition.IsLive } else { QueryCondition.IsOnline }) // live / online + query.add(QueryCondition.EndDateNotNull) // ended + query.add(QueryCondition.BigBlindNotNull) // has BB value + + val sessions = query.queryWith(realm.where(Session::class.java)).findAll() + val sessionsByDuration = sessions.groupBy { + it.netDuration // TODO select a value per 15 min or something... + } + + // define if we have enough sessions + if (sessions.size < 50) { + return + } + + val options = Calculator.Options() + options.query = query + val report = Calculator.computeStats(realm, options) + val stdBB = report.results.firstOrNull()?.computedStat(Stat.STANDARD_DEVIATION_BB)?.value + + val p = polynomialRegression(sessions, stdBB) + + var bestAverage = 0.0 + var bestHourlyRate = 0.0 + var bestDuration = 0.0 + var maxDuration = 0.0 + + sessionsByDuration.keys.forEach { + + val duration = it.toDouble() + if (duration < 0) { // TODO define whats valid + return + } + + val averageResult = getBB(duration, p) + val hourly = averageResult / duration + if (averageResult > bestAverage && hourly > 2 / 3 * bestHourlyRate) { + bestAverage = averageResult + bestDuration = duration + } + + if (duration > 0 && hourly > bestHourlyRate) { + bestHourlyRate = hourly + } + if (duration > maxDuration){ + maxDuration = duration + } + + } + + if (bestDuration > 0.0) { + completion(bestDuration) + } + + realm.close() + + } + + private fun getBB(netDuration: Double, polynomial: DoubleArray): Double { + var y = 0.0 + for (i in polynomial.indices) { + y += polynomial[i] * netDuration.pow(i) + } + return y + } + + private fun polynomialRegression(sessions: List, bbStandardDeviation: Double?): DoubleArray { + + val stdBB = bbStandardDeviation ?: Double.MAX_VALUE + + val points = WeightedObservedPoints() + val now = Date().time + + sessions.forEach { + var weight = 5.0 + + val endTime = it.endDate?.time ?: 0L + + val age = now - endTime + if (age > 2 * 365 * 24 * 3600 * 1000L) { // if more than 2 years loses 1 point + weight -= 1.0 + } + if (it.bbNet > 2 * stdBB) { // if very big result loses 3 points + weight -= 3.0 + } + + points.add(weight, it.netDuration.toDouble(), it.bbNet) + + } + + // polynomial of 7 degree, same as iOS + return PolynomialCurveFitter.create(7).fit(points.toList()) + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/ComputableResult.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/ComputableResult.kt index ccca2ccb..5beffd8b 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/ComputableResult.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/ComputableResult.kt @@ -6,19 +6,19 @@ import net.pokeranalytics.android.model.filter.QueryCondition open class ComputableResult() : RealmObject(), Filterable { - var ratedNet: Double = 0.0 + var ratedNet: Double = 0.0 - var bbNet: BB = 0.0 + var bbNet: BB = 0.0 - var hasBigBlind: Int = 0 + var hasBigBlind: Int = 0 - var isPositive: Int = 0 + var isPositive: Int = 0 - var ratedBuyin: Double = 0.0 + var ratedBuyin: Double = 0.0 - var estimatedHands: Double = 0.0 + var estimatedHands: Double = 0.0 - var bbPer100Hands: BB = 0.0 + var bbPer100Hands: BB = 0.0 var session: Session? = null @@ -35,7 +35,8 @@ open class ComputableResult() : RealmObject(), Filterable { this.bbNet = session.bbNet this.hasBigBlind = if (session.cgBigBlind != null) 1 else 0 this.estimatedHands = session.estimatedHands - this.bbPer100Hands = session.bbNet / (session.numberOfHandsPerHour * session.hourlyDuration) * 100 + this.bbPer100Hands = + session.bbNet / (session.numberOfHandsPerHour * session.hourlyDuration) * 100 } @@ -51,11 +52,11 @@ open class ComputableResult() : RealmObject(), Filterable { companion object { - fun fieldNameForQueryType(queryCondition: Class < out QueryCondition >): String? { - Session.fieldNameForQueryType(queryCondition)?.let { - return "session.$it" - } - return null + fun fieldNameForQueryType(queryCondition: Class): String? { + Session.fieldNameForQueryType(queryCondition)?.let { + return "session.$it" + } + return null } } 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 d887521b..a14e17de 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 @@ -116,6 +116,8 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat CustomFieldAmountQuery::class.java, CustomFieldNumberQuery::class.java -> "customFieldEntries.numericValue" CustomFieldQuery::class.java -> "customFieldEntries.customFields.id" DateNotNull::class.java -> "startDate" + EndDateNotNull::class.java -> "endDate" + BigBlindNotNull::class.java -> "cgBigBlind" else -> null } }