Improvements

powerreport
Laurent 3 years ago
parent af1024c7bc
commit f5115e53e4
  1. 11
      app/src/main/java/net/pokeranalytics/android/calculus/Calculator.kt
  2. 69
      app/src/main/java/net/pokeranalytics/android/calculus/ReportWhistleBlower.kt
  3. 2
      app/src/main/java/net/pokeranalytics/android/calculus/Stat.kt
  4. 297
      app/src/main/java/net/pokeranalytics/android/calculus/optimalduration/CashGameOptimalDurationCalculator.kt
  5. 38
      app/src/main/java/net/pokeranalytics/android/model/LiveOnline.kt
  6. 2
      app/src/main/java/net/pokeranalytics/android/model/extensions/SessionExtensions.kt
  7. 2
      app/src/main/java/net/pokeranalytics/android/model/migrations/PokerAnalyticsMigration.kt
  8. 24
      app/src/main/java/net/pokeranalytics/android/model/realm/Performance.kt
  9. 8
      app/src/main/java/net/pokeranalytics/android/ui/fragment/ReportsFragment.kt
  10. 5
      app/src/main/java/net/pokeranalytics/android/ui/modules/session/SessionFragment.kt
  11. 7
      app/src/main/java/net/pokeranalytics/android/ui/view/RowViewType.kt
  12. 11
      app/src/main/java/net/pokeranalytics/android/ui/view/rows/StaticReport.kt

@ -241,6 +241,11 @@ class Calculator {
val results = ComputedResults(computableGroup, options.shouldManageMultiGroupProgressValues)
val computables = computableGroup.computables(realm, options.shouldSortValues)
if (computables.size == 0) { // we don't want to return stats with 0 as a value when comparing best performances
return results
}
// Timber.d("#### Start computing group, ${computables.size} computables")
results.addStat(NUMBER_OF_GAMES, computables.size.toDouble())
// computables.forEach {
@ -606,10 +611,10 @@ class SSStats(sessionSet: SessionSet, query: Query) { // Session Set Stats
if (setSessions.size == filteredSessions.size) {
this.initStatsWithSet(sessionSet)
} else {
ratedNet = filteredSessions.sumByDouble { it.computableResult?.ratedNet ?: 0.0 }
bbSum = filteredSessions.sumByDouble { it.bbNet }
ratedNet = filteredSessions.sumOf { it.computableResult?.ratedNet ?: 0.0 }
bbSum = filteredSessions.sumOf { it.bbNet }
hourlyDuration = filteredSessions.hourlyDuration
estimatedHands = filteredSessions.sumByDouble { it.estimatedHands }
estimatedHands = filteredSessions.sumOf { it.estimatedHands }
}
}
}

@ -7,11 +7,15 @@ import io.realm.RealmResults
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import net.pokeranalytics.android.calculus.optimalduration.CashGameOptimalDurationCalculator
import net.pokeranalytics.android.model.LiveOnline
import net.pokeranalytics.android.model.realm.CustomField
import net.pokeranalytics.android.model.realm.Performance
import net.pokeranalytics.android.model.realm.Result
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.ui.fragment.PerformanceKey
import net.pokeranalytics.android.ui.view.rows.StaticReport
import net.pokeranalytics.android.util.extensions.formattedHourlyDuration
import timber.log.Timber
import kotlin.coroutines.CoroutineContext
@ -137,21 +141,28 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
Timber.d(">>> launch report = $report")
when (report) {
StaticReport.OptimalDuration -> launchOptimalDuration(realm, report)
else -> launchDefaultReport(realm, report)
}
}
private fun launchDefaultReport(realm: Realm, report: StaticReport) {
val options = Calculator.Options(
stats = report.stats,
criterias = report.criteria
)
val result = Calculator.computeStats(realm, options = options)
analyseReport(realm, report, result)
analyseDefaultReport(realm, report, result)
}
private fun analyseReport(realm: Realm, staticReport: StaticReport, result: Report) {
when (staticReport.uniqueIdentifier) {
StaticReport.OptimalDuration.uniqueIdentifier -> analyseOptimalDuration(staticReport, result)
else -> analyseDefaultReport(realm, staticReport, result)
private fun launchOptimalDuration(realm: Realm, report: StaticReport) {
LiveOnline.values().forEach { key ->
val duration = CashGameOptimalDurationCalculator.start(key.isLive)
duration?.let {
analyseOptimalDuration(realm, report, key, it)
}
}
}
@ -166,7 +177,7 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
val customField: CustomField? =
(staticReport as? StaticReport.CustomFieldList)?.customField
var query = realm.where(Performance::class.java)
.equalTo("statId", stat.uniqueIdentifier)
.equalTo("key", stat.uniqueIdentifier)
.equalTo("reportId", staticReport.uniqueIdentifier)
customField?.let {
@ -174,15 +185,16 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
}
val currentPerf = query.findFirst()
val performanceQuery = computedResults.group.query
val performanceName = performanceQuery.getName(this.context, " ")
var storePerf = true
currentPerf?.let {
Timber.d("cr name = ${computedResults.group.query.getName(this.context)}")
currentPerf.name?.let {
if (computedResults.group.query.defaultName == it) {
storePerf = false
}
}
Timber.d("cr objectId = ${computedResults.group.query.objectId}")
currentPerf.objectId?.let {
if (computedResults.group.query.objectId == it) {
storePerf = false
@ -191,8 +203,8 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
if (storePerf) {
realm.executeTransaction {
currentPerf.name = computedResults.group.query.getName(this.context)
currentPerf.objectId = computedResults.group.query.objectId
currentPerf.name = performanceName
currentPerf.objectId = performanceQuery.objectId
currentPerf.customFieldId = customField?.id
}
this.whistleBlower.notify(currentPerf)
@ -204,8 +216,8 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
val performance = Performance(
staticReport,
stat,
computedResults.group.query.getName(this.context),
computedResults.group.query.objectId,
performanceName,
performanceQuery.objectId,
customField?.id,
null
)
@ -219,7 +231,34 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
}
private fun analyseOptimalDuration(staticReport: StaticReport, result: Report) {
private fun analyseOptimalDuration(realm: Realm, staticReport: StaticReport, key: PerformanceKey, duration: Double) {
var storePerf: Boolean = true
val performance = realm.where(Performance::class.java)
.equalTo("reportId", staticReport.uniqueIdentifier)
.equalTo("key", key.value)
.findFirst()
val formattedDuration = (duration / 3600 / 1000).formattedHourlyDuration()
performance?.let { perf ->
if (perf.value == duration) {
storePerf = false
}
if (storePerf) {
realm.executeTransaction {
perf.name = formattedDuration
perf.value = duration
}
}
}
if (storePerf) {
val perf = Performance(staticReport, key, name = formattedDuration, value = duration)
realm.executeTransaction { it.copyToRealm(perf) }
this.whistleBlower.notify(perf)
}
}

@ -103,7 +103,7 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
}
override val value: String = this.uniqueIdentifier.toString()
override val value: Int = this.uniqueIdentifier
override val resId: Int?
get() {

@ -21,150 +21,163 @@ import kotlin.math.round
*/
class CashGameOptimalDurationCalculator {
companion object {
private const val bucket = 60 * 60 * 1000L // the duration of bucket
private const val bucketInterval = 4 // number of duration tests inside the bucket to find the best duration
private const val minimumValidityCount = 10 // the number of sessions inside a bucket to start having a reasonable average
private const val intervalValidity = 3 // the minimum number of unit between the shortest & longest valid buckets
private const val polynomialDegree = 7 // the degree of the computed polynomial
/***
* Starts the calculation
* [isLive] is a boolean to indicate if we're looking at live or online games
* return a duration or null if it could not be computed
*/
fun start(isLive: Boolean): Double? {
val realm = Realm.getDefaultInstance()
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.BiggestBetNotNull) // has BB value
val sessions = query.queryWith(realm.where(Session::class.java)).findAll()
val sessionsByDuration = sessions.groupBy {
val dur = round((it.netDuration / bucket).toDouble()) * bucket
companion object {
private const val bucket = 60 * 60 * 1000L // the duration of bucket
private const val bucketInterval =
4 // number of duration tests inside the bucket to find the best duration
private const val minimumValidityCount =
10 // the number of sessions inside a bucket to start having a reasonable average
private const val intervalValidity =
3 // the minimum number of unit between the shortest & longest valid buckets
private const val polynomialDegree = 7 // the degree of the computed polynomial
/***
* Starts the calculation
* [isLive] is a boolean to indicate if we're looking at live or online games
* return a duration or null if it could not be computed
*/
fun start(isLive: Boolean): Double? {
val realm = Realm.getDefaultInstance()
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.BiggestBetNotNull) // has BB value
val sessions = query.queryWith(realm.where(Session::class.java)).findAll()
val sessionsByDuration = sessions.groupBy {
val dur = round((it.netDuration / bucket).toDouble()) * bucket
// Timber.d("Stop notif > key: $dur")
dur
}
// define validity interval
var start: Double? = null
var end: Double? = null
var validBuckets = 0
val hkeys = sessionsByDuration.keys.map { it / 3600 / 1000.0 }.sorted()
Timber.d("Stop notif > keys: $hkeys ")
for (key in sessionsByDuration.keys.sorted()) {
val sessionCount = sessionsByDuration[key]?.size ?: 0
if (start == null && sessionCount >= minimumValidityCount) {
start = key
}
if (sessionCount >= minimumValidityCount) {
end = key
validBuckets++
}
}
Timber.d("Stop notif > validBuckets: $validBuckets ")
if (!(start != null && end != null && (end - start) >= intervalValidity)) {
Timber.d("Stop notif > invalid setup: $start / $end ")
return null
}
// define if we have enough sessions
if (sessions.size < 50) {
Timber.d("Stop notif > not enough sessions: ${sessions.size} ")
return null
}
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
val keys = sessionsByDuration.keys.filter { it >= start && it <= end }.sorted()
for (key in keys) {
val sessionCount = sessionsByDuration[key]?.size ?: 0
if (sessionCount < minimumValidityCount / 2) continue // if too few sessions we don't consider the duration valid
for (i in 0 until bucketInterval) {
val duration = key + i * bucket / bucketInterval
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) {
return bestDuration
}
Timber.d("Stop notif > not found, best duration: $bestDuration")
realm.close()
return null
}
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<Session>, 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)
}
dur
}
// define validity interval
var start: Double? = null
var end: Double? = null
var validBuckets = 0
val hkeys = sessionsByDuration.keys.map { it / 3600 / 1000.0 }.sorted()
Timber.d("Stop notif > keys: $hkeys ")
for (key in sessionsByDuration.keys.sorted()) {
val sessionCount = sessionsByDuration[key]?.size ?: 0
if (start == null && sessionCount >= minimumValidityCount) {
start = key
}
if (sessionCount >= minimumValidityCount) {
end = key
validBuckets++
}
}
Timber.d("Stop notif > validBuckets: $validBuckets ")
if (!(start != null && end != null && (end - start) >= intervalValidity)) {
Timber.d("Stop notif > invalid setup: $start / $end ")
return null
}
// define if we have enough sessions
if (sessions.size < 50) {
Timber.d("Stop notif > not enough sessions: ${sessions.size} ")
return null
}
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
val keys = sessionsByDuration.keys.filter { it >= start && it <= end }.sorted()
for (key in keys) {
val sessionCount = sessionsByDuration[key]?.size ?: 0
if (sessionCount < minimumValidityCount / 2) continue // if too few sessions we don't consider the duration valid
for (i in 0 until bucketInterval) {
val duration = key + i * bucket / bucketInterval
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) {
return bestDuration
}
Timber.d("Stop notif > not found, best duration: $bestDuration")
realm.close()
return null
}
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<Session>,
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(polynomialDegree).fit(points.toList())
}
// polynomial of 7 degree, same as iOS
return PolynomialCurveFitter.create(polynomialDegree).fit(points.toList())
}
}
}
}

@ -0,0 +1,38 @@
package net.pokeranalytics.android.model
import net.pokeranalytics.android.R
import net.pokeranalytics.android.ui.fragment.PerformanceKey
import net.pokeranalytics.android.util.enumerations.IntIdentifiable
import net.pokeranalytics.android.util.enumerations.IntSearchable
enum class LiveOnline(override var uniqueIdentifier: Int) : PerformanceKey, IntIdentifiable {
LIVE(0),
ONLINE(1);
companion object : IntSearchable<LiveOnline> {
override fun valuesInternal(): Array<LiveOnline> {
return values()
}
}
override val resId: Int?
get() {
return when (this) {
LIVE -> R.string.live
ONLINE -> R.string.online
}
}
override val value: Int
get() {
return this.uniqueIdentifier
}
val isLive: Boolean
get() {
return (this == LIVE)
}
}

@ -130,7 +130,7 @@ val AbstractList<Session>.hourlyDuration: Double
val interval = TimeInterval(it.startDate!!, it.endDate!!, it.breakDuration)
intervals.update(interval)
}
return intervals.sumByDouble { it.hourlyDuration }
return intervals.sumOf { it.hourlyDuration }
}
class TimeInterval(var start: Date, var end: Date, var breakDuration: Long) {

@ -301,7 +301,7 @@ class PokerAnalyticsMigration : RealmMigration {
schema.addField("id", String::class.java).setRequired("id", true)
schema.addPrimaryKey("id")
schema.addField("reportId", Int::class.java).setRequired("report", true)
schema.addField("statId", Int::class.java).setRequired("stat", true)
schema.addField("key", Int::class.java).setRequired("key", true)
schema.addField("name", String::class.java).setNullable("name", true)
schema.addField("objectId", String::class.java).setNullable("objectId", true)
schema.addField("customFieldId", String::class.java).setNullable("customFieldId", true)

@ -3,20 +3,20 @@ package net.pokeranalytics.android.model.realm
import io.realm.Realm
import io.realm.RealmObject
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.model.LiveOnline
import net.pokeranalytics.android.ui.fragment.PerformanceKey
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.rows.StaticReport
import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.extensions.lookupForNameInAllTablesById
import java.util.*
open class Performance() : RealmObject(), RowRepresentable {
open class Performance() : RealmObject() {
var id: String = UUID.randomUUID().toString()
constructor(
report: StaticReport,
stat: Stat,
key: PerformanceKey,
name: String? = null,
objectId: String? = null,
customFieldId: String? = null,
@ -24,7 +24,7 @@ open class Performance() : RealmObject(), RowRepresentable {
) : this() {
this.reportId = report.uniqueIdentifier
this.statId = stat.uniqueIdentifier
this.key = key.value
this.name = name
this.objectId = objectId
this.customFieldId = customFieldId
@ -33,7 +33,7 @@ open class Performance() : RealmObject(), RowRepresentable {
}
var reportId: Int = 0
var statId: Int = 0
var key: Int = 0
var name: String? = null
var objectId: String? = null
var customFieldId: String? = null
@ -49,19 +49,17 @@ open class Performance() : RealmObject(), RowRepresentable {
return NULL_TEXT
}
val performanceKey: PerformanceKey
get() {
return Stat.valueByIdentifier(this.statId)
}
val stat: Stat
get() {
return Stat.valueByIdentifier(this.statId)
return Stat.valueByIdentifier(this.key.toInt())
}
override val resId: Int?
val resId: Int?
get() {
return this.performanceKey.resId
return when (this.reportId) {
StaticReport.OptimalDuration.uniqueIdentifier -> LiveOnline.valueByIdentifier(this.key).resId
else -> stat.resId
}
}
}

@ -44,7 +44,7 @@ import java.util.*
interface PerformanceKey {
val resId: Int?
val value: String
val value: Int
}
data class ReportSection(var report: StaticReport, var performances: MutableList<PerformanceRow>)
@ -167,7 +167,6 @@ class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSourc
}
// Rows
private fun updateRows() {
@ -234,7 +233,10 @@ class ReportsFragment : DeletableItemFragment(), StaticRowRepresentableDataSourc
when (row) {
is PerformanceRow -> {
val reportName = row.localizedTitle(requireContext())
launchComputation(row.report.criteria, reportName, row.performance.stat)
val report = row.report
if (report.hasGraph) {
launchComputation(report.criteria, reportName, row.performance.stat)
}
}
is ReportSetup -> {
val display = ReportDisplay.values()[row.display]

@ -11,6 +11,7 @@ import androidx.appcompat.content.res.AppCompatResources
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.DiffUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
@ -405,13 +406,13 @@ class SessionFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepr
Timber.d("Start optimal duration finding attempt...")
val isLive = this.currentSession.isLive
GlobalScope.launch(coroutineContext) {
CoroutineScope(coroutineContext).launch {
var optimalDuration: Double? = null
val cr = GlobalScope.async {
optimalDuration = CashGameOptimalDurationCalculator.start(isLive)
}
optimalDuration = CashGameOptimalDurationCalculator.start(isLive)
cr.await()
if (!isDetached) {

@ -686,6 +686,13 @@ enum class RowViewType(private var layoutRes: Int) : ViewIdentifier {
itemView.findViewById<AppCompatImageView>(R.id.badge)?.let {
it.isVisible = row.badge
}
itemView.findViewById<AppCompatImageView>(R.id.nextArrow)?.let {
it.visibility = if (row.report.hasGraph) {
View.VISIBLE
} else {
View.GONE
}
}
val listener = View.OnClickListener {
adapter.delegate?.onRowSelected(position, row)

@ -56,8 +56,7 @@ sealed class StaticReport(override var uniqueIdentifier: Int) : RowRepresentable
}
val basicReports: Set<StaticReport> = setOf(General, Blinds, TournamentBuyin,
DayOfWeek, Location, TournamentType, Game, TableSize, Duration, OptimalDuration)
val basicReports: Set<StaticReport> = setOf(General)
}
@ -114,4 +113,12 @@ sealed class StaticReport(override var uniqueIdentifier: Int) : RowRepresentable
}
}
val hasGraph: Boolean
get() {
return when (this) {
OptimalDuration -> false
else -> true
}
}
}
Loading…
Cancel
Save