Advertisement
Guest User

Untitled

a guest
Apr 7th, 2020
199
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 6.19 KB | None | 0 0
  1. import random
  2. import typing
  3.  
  4.  
  5. class NoBannerError(Exception):
  6.     pass
  7.  
  8.  
  9. class EmptyBannerStorageError(Exception):
  10.     pass
  11.  
  12.  
  13. class BannerStat:
  14.     def __init__(self, clicks: int, shows: int):
  15.         self._clicks = clicks
  16.         self._shows = shows
  17.  
  18.     def add_click(self) -> None:
  19.         if self._shows > self.clicks:
  20.             self._clicks += 1
  21.  
  22.     def add_show(self) -> None:
  23.         self._shows += 1
  24.  
  25.     @property
  26.     def clicks(self) -> int:
  27.         return self._clicks
  28.  
  29.     @property
  30.     def shows(self) -> int:
  31.         return self._shows
  32.  
  33.     def compute_ctr(self, default_ctr: float) -> float:
  34.         """
  35.        Compute banner CTR (click through rate) as clicks / shows
  36.        If banner has zero shows - return `default_ctr`
  37.        """
  38.         if self.shows == 0:
  39.             return default_ctr
  40.         else:
  41.             return self.clicks / self.shows
  42.  
  43.  
  44. class Banner:
  45.     def __init__(self, banner_id: str, cost: int, stat: typing.Optional[BannerStat] = None):
  46.         self._banner_id = banner_id
  47.         self._cost = cost
  48.         self._stat = stat if stat is not None else BannerStat(0, 0)
  49.  
  50.     @property
  51.     def banner_id(self) -> str:
  52.         return self._banner_id
  53.  
  54.     @property
  55.     def cost(self) -> int:
  56.         return self._cost
  57.  
  58.     @property
  59.     def stat(self) -> BannerStat:
  60.         return self._stat
  61.  
  62.  
  63. class BannerStorage:
  64.     def __init__(self, banners: typing.Iterable[Banner], default_ctr: float = 0.1):
  65.         self._banner_dict = {b.banner_id: b for b in banners}
  66.         self._banner_id_list = [b.banner_id for b in banners]
  67.         self._default_ctr = default_ctr
  68.  
  69.     def is_empty(self) -> bool:
  70.         return len(self._banner_dict) == 0
  71.  
  72.     def add_click(self, banner_id: str) -> None:
  73.         if banner_id not in self._banner_dict:
  74.             raise NoBannerError("Unknown banner {}!".format(banner_id))
  75.  
  76.         self._banner_dict[banner_id].stat.add_click()
  77.  
  78.     def add_show(self, banner_id: str) -> None:
  79.         if banner_id not in self._banner_dict:
  80.             raise NoBannerError("Unknown banner {}!".format(banner_id))
  81.  
  82.         self._banner_dict[banner_id].stat.add_show()
  83.  
  84.     def get_banner(self, banner_id: str) -> Banner:
  85.         if banner_id not in self._banner_dict:
  86.             raise NoBannerError("Unknown banner {}!".format(banner_id))
  87.  
  88.         return self._banner_dict[banner_id]
  89.  
  90.     def banner_with_highest_cpc(self) -> Banner:
  91.         """
  92.        :return: banner with highest CPC(cost per click = cost * CTR))
  93.        """
  94.  
  95.         if self.is_empty():
  96.             raise NoBannerError("Storage is empty!")
  97.  
  98.         selected_banner = self._banner_dict[self._banner_id_list[0]]
  99.         selected_cpc = selected_banner.stat.compute_ctr(self._default_ctr) * selected_banner.cost
  100.  
  101.         for banner_id in self._banner_id_list:
  102.             current_banner = self._banner_dict[banner_id]
  103.             current_cpc = current_banner.stat.compute_ctr(self._default_ctr) * current_banner.cost
  104.             if current_cpc > selected_cpc:
  105.                 selected_cpc = current_cpc
  106.                 selected_banner = current_banner
  107.  
  108.         return selected_banner
  109.  
  110.     def random_banner(self) -> Banner:
  111.         if self.is_empty():
  112.             raise NoBannerError("Storage is empty!")
  113.  
  114.         return self._banner_dict[random.choice(self._banner_id_list)]
  115.  
  116.     def print_stats(self) -> None:
  117.         for b in self._banner_dict.values():
  118.             print("Id:", b.banner_id, "Cost", b.cost, "Shows", b.stat.shows, "Clicks", b.stat.clicks)
  119.  
  120.     def get_banners(self) -> typing.List[Banner]:
  121.         return list(self._banner_dict.values())
  122.  
  123.  
  124. class EpsilonGreedyBannerEngine:
  125.     """
  126.    Banner engine that with 1 - epsilon probability shows banner with highest CPC (cost per click = cost * CTR)
  127.    With epsilon probability shows random banner to gather more stats
  128.    """
  129.     def __init__(self, banner_storage: BannerStorage, random_banner_probability: float):
  130.         """
  131.        :param banner_storage: None empty banner storage
  132.        :param random_banner_probability: 1.0 - every show is random. 0.0 - every show is greedy
  133.        """
  134.         self._epsilon = random_banner_probability
  135.         if banner_storage.is_empty():
  136.             raise EmptyBannerStorageError()
  137.         self._storage = banner_storage
  138.  
  139.         self._show_count = sum([banner.stat.shows for banner in self._storage.get_banners()])
  140.         self._total_cost = sum([banner.cost * banner.stat.clicks for banner in self._storage.get_banners()])
  141.  
  142.     def show_banner(self) -> str:
  143.         """
  144.        Engine is asked to show banner.
  145.        Engine selects banner with epsilon-greedy algorithms and updates banner show statistics.
  146.        """
  147.         if random.random() < self._epsilon:
  148.             selected_banner = self._storage.random_banner()
  149.         else:
  150.             selected_banner = self._storage.banner_with_highest_cpc()
  151.  
  152.         self._storage.add_show(selected_banner.banner_id)
  153.         self._show_count += 1
  154.  
  155.         return selected_banner.banner_id
  156.  
  157.     def send_click(self, banner_id: str) -> None:
  158.         """
  159.        Web page sends user click conformation for `banner_id` and engine must update banner click statistics
  160.        Important! Web page can send incorrect `banner_id`. Engine must not fail in that case!
  161.        """
  162.         try:
  163.             self._storage.add_click(banner_id)
  164.             self._total_cost += self._storage.get_banner(banner_id).cost
  165.         except NoBannerError:
  166.             pass
  167.  
  168.     def banner_shows(self, banner_id: str) -> int:
  169.         try:
  170.             banner = self._storage.get_banner(banner_id)
  171.             return banner.stat.shows
  172.         except NoBannerError:
  173.             pass
  174.  
  175.     def banner_clicks(self, banner_id: str) -> int:
  176.         try:
  177.             banner = self._storage.get_banner(banner_id)
  178.             return banner.stat.clicks
  179.         except NoBannerError:
  180.             pass
  181.  
  182.     @property
  183.     def shown_count(self) -> int:
  184.         """
  185.        :return: Total shows since start
  186.        """
  187.         return self._show_count
  188.  
  189.     @property
  190.     def total_cost(self) -> int:
  191.         """
  192.        :return: Total earned money since start
  193.        """
  194.         return self._total_cost
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement