All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.iheart.thomas.analysis.monitor.ExperimentKPIState.scala Maven / Gradle / Ivy

The newest version!
package com.iheart.thomas
package analysis
package monitor

import com.iheart.thomas.analysis.Probability
import cats.implicits._
import ExperimentKPIState.{ArmState, Key}
import cats.data.NonEmptyList
import cats.{Functor, MonadThrow}
import com.iheart.thomas.abtest.Error.NotFound
import com.iheart.thomas.utils.time.Period
import enumeratum._

import java.time.Instant
import ExperimentKPIState.ArmsState
import cats.effect.kernel.Clock
import com.iheart.thomas.analysis.monitor.ExperimentKPIState.Specialization.RealtimeMonitor

case class ExperimentKPIState[+KS <: KPIStats](
    key: Key,
    arms: ArmsState[KS],
    dataPeriod: Period,
    lastUpdated: Instant,
    start: Instant) {

  def armsStateMap: Map[ArmName, KS] =
    arms.map(as => (as.name, as.kpiStats)).toList.toMap

  def distribution: Map[ArmName, Probability] =
    arms.toList.mapFilter(as => as.likelihoodOptimum.map((as.name, _))).toMap

  def getArm(armName: ArmName): Option[ArmState[KS]] =
    arms.find(_.name === armName)

  def asConversions: Option[ExperimentKPIState[Conversions]] =
    arms.head.kpiStats match {
      case _: Conversions =>
        Some(asInstanceOf[ExperimentKPIState[Conversions]])
      case _ => None
    }

  def asPerUserSamplesLnSummary
      : Option[ExperimentKPIState[PerUserSamplesLnSummary]] =
    arms.head.kpiStats match {
      case _: PerUserSamplesLnSummary =>
        Some(asInstanceOf[ExperimentKPIState[PerUserSamplesLnSummary]])
      case _ => None
    }
}

object ExperimentKPIState {
  type ArmsState[+KS <: KPIStats] = NonEmptyList[ArmState[KS]]
  case class Key(
      feature: FeatureName,
      kpi: KPIName,
      specialization: Specialization = RealtimeMonitor) {
    lazy val toStringKey: String =
      kpi.n + "|" + feature + "|" + specialization.entryName
  }
  object Key {
    def parse(string: String): Option[Key] = {
      string.split('|').toList match {
        case kn :: fn :: sp :: Nil =>
          Specialization.withNameOption(sp).map {
            Key(fn, KPIName(kn), _)
          }
        case _ => None
      }
    }
  }

  sealed trait Specialization extends EnumEntry

  object Specialization
      extends Enum[Specialization]
      with enumeratum.CatsEnum[Specialization] {

    val values = findValues
    case object RealtimeMonitor extends Specialization
    case object BanditCurrent extends Specialization
  }

  def init[F[_]: Clock: Functor, KS <: KPIStats](
      key: Key,
      arms: NonEmptyList[ArmState[KS]],
      dataPeriod: Period
    ): F[ExperimentKPIState[KS]] =
    utils.time
      .now[F]
      .map(now => ExperimentKPIState[KS](key, arms, dataPeriod, now, now))

  case class ArmState[+KS <: KPIStats](
      name: ArmName,
      kpiStats: KS,
      likelihoodOptimum: Option[Probability]) {

    def sampleSize: Long = {
      (kpiStats: KPIStats) match {
        case Conversions(_, total)                => total
        case PerUserSamplesLnSummary(_, _, count) => count
      }
    }
  }
}

trait ExperimentKPIStateDAO[F[_], KS <: KPIStats] {

  def get(key: Key): F[ExperimentKPIState[KS]]
  def all: F[Vector[ExperimentKPIState[KS]]]
  def find(key: Key): F[Option[ExperimentKPIState[KS]]]

  def upsert(
      key: Key
    )(update: (ArmsState[KS], Period) => (ArmsState[KS], Period)
    )(ifEmpty: => (ArmsState[KS], Period)
    ): F[ExperimentKPIState[KS]]

  def updateOptimumLikelihood(
      key: Key,
      likelihoods: Map[ArmName, Probability]
    ): F[ExperimentKPIState[KS]]

  def delete(key: Key): F[Option[ExperimentKPIState[KS]]]

}

trait AllExperimentKPIStateRepo[F[_]] {

  def delete(key: Key): F[Option[ExperimentKPIState[KPIStats]]]
  def all: F[Vector[ExperimentKPIState[KPIStats]]]
  def find(key: Key): F[Option[ExperimentKPIState[KPIStats]]]
  def get(key: Key): F[ExperimentKPIState[KPIStats]]

  def updateOptimumLikelihood(
      key: Key,
      likelihoods: Map[ArmName, Probability]
    ): F[ExperimentKPIState[KPIStats]]
}

object AllExperimentKPIStateRepo {
  implicit def default[F[_]: MonadThrow](
      implicit cRepo: ExperimentKPIStateDAO[F, Conversions],
      pRepo: ExperimentKPIStateDAO[F, PerUserSamplesLnSummary],
      kPIRepo: AllKPIRepo[F]
    ): AllExperimentKPIStateRepo[F] =
    new AllExperimentKPIStateRepo[F] {
      class PerformPartial[A](key: Key) {
        def apply[B <: A, C <: A](ifC: F[B], ifA: F[C]): F[A] =
          kPIRepo.get(key.kpi).flatMap {
            case _: ConversionKPI   => ifC.widen
            case _: AccumulativeKPI => ifA.widen
          }
      }
      def perform[A](key: Key): PerformPartial[A] =
        new PerformPartial[A](key)

      def delete(key: Key): F[Option[ExperimentKPIState[KPIStats]]] = {
        perform[Option[ExperimentKPIState[KPIStats]]](key)(
          cRepo.delete(key),
          pRepo.delete(key)
        )
      }

      def all: F[Vector[ExperimentKPIState[KPIStats]]] =
        for {
          cStates <- cRepo.all
          pStates <- pRepo.all
        } yield (cStates.widen[ExperimentKPIState[KPIStats]] ++ pStates
          .widen[ExperimentKPIState[KPIStats]])

      def find(key: Key): F[Option[ExperimentKPIState[KPIStats]]] =
        perform[Option[ExperimentKPIState[KPIStats]]](key)(
          cRepo.find(key),
          pRepo.find(key)
        )

      def get(key: Key): F[ExperimentKPIState[KPIStats]] =
        find(key).flatMap(
          _.liftTo[F](NotFound(key.toStringKey + " is not found in DB"))
        )

      def updateOptimumLikelihood(
          key: Key,
          likelihoods: Map[ArmName, Probability]
        ): F[ExperimentKPIState[KPIStats]] = perform[ExperimentKPIState[KPIStats]](
        key
      )(
        cRepo.updateOptimumLikelihood(key, likelihoods),
        pRepo.updateOptimumLikelihood(key, likelihoods)
      )
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy