{ "cells": [ { "cell_type": "code", "execution_count": 1, "id": "71f85f2a-423f-44d2-b80d-da9ac8d3961a", "metadata": {}, "outputs": [], "source": [ "import simpy\n", "import random\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", "import pandas as pd\n", "from enum import Enum\n", "import os\n", "import shutil\n", "\n", "# Types of cache\n", "class CacheType(Enum):\n", " LRU = 1\n", " RANDOM_EVICTION = 2\n", "experiment_name = ''\n", "\n", "# Constants\n", "SEED = 42\n", "DATABASE_OBJECTS = 100 # Number of objects in the database\n", "ACCESS_COUNT_LIMIT = 10 # Total time to run the simulation\n", "EXPERIMENT_BASE_DIR = \"./experiments/\"\n", "TEMP_BASE_DIR = \"./.aoi_cache/\"\n", "\n", "ZIPF_CONSTANT = 2 # Shape parameter for the Zipf distribution (controls skewness) Needs to be: 1< \n", "\n", "# Set random seeds\n", "random.seed(SEED)\n", "np.random.seed(SEED)\n", "\n", "# Initialize simulation environment\n", "env = simpy.Environment()\n", "\n", "os.makedirs(TEMP_BASE_DIR, exist_ok=True)" ] }, { "cell_type": "markdown", "id": "9a37d7a3-3e11-4b89-8dce-6091dd38b16f", "metadata": {}, "source": [ "How to set certain parameters for specific scenarios\n", "\n", "\n", "| Name | Cache Capacity | MAX_REFRESH_RATE | cache_type | CACHE_TTL |\n", "| -------------------- | -------------------- | ---------------- | ------------------------- | --------- |\n", "| Default | DATABASE_OBJECTS | 1< | CacheType.LRU | 5 |\n", "| No Refresh | DATABASE_OBJECTS | 0 | CacheType.LRU | 5 |\n", "| Infinite TTL | DATABASE_OBJECTS / 2 | 0 | CacheType.LRU | 0 |\n", "| Random Eviction (RE) | DATABASE_OBJECTS / 2 | 1< | CacheType.RANDOM_EVICTION | 5 |\n", "| RE without Refresh | DATABASE_OBJECTS / 2 | 0 | CacheType.RANDOM_EVICTION | 5 |\n", "\n", "\n" ] }, { "cell_type": "code", "execution_count": 2, "id": "3d0ab5b1-162a-42c8-80a3-d31f763101f1", "metadata": {}, "outputs": [], "source": [ "# Configuration (Just example, will be overwritten in next block\n", "\n", "CACHE_CAPACITY = DATABASE_OBJECTS # Maximum number of objects the cache can hold\n", "\n", "# MAX_REFRESH_RATE is used as the maximum for a uniform\n", "# distribution for mu.\n", "# If MAX_REFRESH_RATE is 0, we do not do any refreshes.\n", "MAX_REFRESH_RATE = 0\n", "\n", "cache_type = CacheType.LRU\n", "\n", "# CACHE_TTL is used to determin which TTL to set when an\n", "# object is pulled into the cache\n", "# If CACHE_TTL is set to 0, the TTL is infinite\n", "CACHE_TTL = 5\n" ] }, { "cell_type": "code", "execution_count": 3, "id": "3ff299ca-ec65-453b-b167-9a0f7728a207", "metadata": {}, "outputs": [], "source": [ "configurations = {\n", " \"default\": (DATABASE_OBJECTS, 10, CacheType.LRU, 5),\n", " \"No Refresh\": (DATABASE_OBJECTS, 0, CacheType.LRU, 5),\n", " \"Infinite TTL\": (int(DATABASE_OBJECTS / 2), 0, CacheType.LRU, 0),\n", " \"Random Eviction\": (int(DATABASE_OBJECTS / 2), 10, CacheType.RANDOM_EVICTION, 5),\n", " \"RE without Refresh\": (int(DATABASE_OBJECTS / 2), 0, CacheType.RANDOM_EVICTION, 5),\n", " \"No Refresh (0.5s ttl)\": (DATABASE_OBJECTS, 0, CacheType.LRU, 0.5),\n", " \"No Refresh (1.0s ttl)\": (DATABASE_OBJECTS, 0, CacheType.LRU, 1),\n", " \"No Refresh (2.0s ttl)\": (DATABASE_OBJECTS, 0, CacheType.LRU, 2),\n", " \"No Refresh (3.0s ttl)\": (DATABASE_OBJECTS, 0, CacheType.LRU, 3),\n", " \"No Refresh (4.0s ttl)\": (DATABASE_OBJECTS, 0, CacheType.LRU, 4),\n", " \"No Refresh (5.0s ttl)\": (DATABASE_OBJECTS, 0, CacheType.LRU, 5),\n", "}\n", "\n", "experiment_name = \"No Refresh (0.5s ttl)\"\n", "config = configurations[experiment_name]\n", "\n", "CACHE_CAPACITY = config[0]\n", "MAX_REFRESH_RATE = config[1]\n", "cache_type = config[2]\n", "CACHE_TTL = config[3]\n" ] }, { "cell_type": "code", "execution_count": 4, "id": "5cea042f-e9fc-4a1e-9750-de212ca70601", "metadata": {}, "outputs": [], "source": [ "class Database:\n", " def __init__(self):\n", " # Each object now has a specific refresh rate 'mu'\n", " self.data = {i: f\"Object {i}\" for i in range(1, DATABASE_OBJECTS + 1)}\n", " self.lambda_values = {i: np.random.zipf(ZIPF_CONSTANT) for i in range(1, DATABASE_OBJECTS + 1)} # Request rate 'lambda' for each object\n", " # Refresh rate 'mu' for each object\n", " if MAX_REFRESH_RATE == 0:\n", " self.mu_values = {i: 0 for i in range(1,DATABASE_OBJECTS + 1)} \n", " else:\n", " self.mu_values = {i: np.random.uniform(1, MAX_REFRESH_RATE) for i in range(1, DATABASE_OBJECTS + 1)}\n", " self.next_request = {i: np.random.exponential(self.lambda_values[i]) for i in range(1, DATABASE_OBJECTS + 1)}\n", "\n", "\n", " def get_object(self, obj_id):\n", " # print(f\"[{env.now:.2f}] Database: Fetched {self.data.get(obj_id, 'Unknown')} for ID {obj_id}\")\n", " return self.data.get(obj_id, None)" ] }, { "cell_type": "code", "execution_count": 5, "id": "499bf543-b2c6-4e4d-afcc-0a6665ce3ae1", "metadata": {}, "outputs": [], "source": [ "class Cache:\n", " def __init__(self, env, db, cache_type):\n", " self.cache_type = cache_type\n", " self.env = env\n", " self.db = db\n", " self.storage = {} # Dictionary to store cached objects\n", " self.ttl = {} # Dictionary to store TTLs\n", " self.age = {} # Dictionary to store age of each object\n", " self.cache_size_over_time = [] # To record cache state at each interval\n", " self.cache_next_request_over_time = []\n", " self.request_log = {i: [] for i in range(1, DATABASE_OBJECTS + 1)}\n", " self.hits = {i: 0 for i in range(1, DATABASE_OBJECTS + 1)} # Track hits per object\n", " self.misses = {i: 0 for i in range(1, DATABASE_OBJECTS + 1)} # Track misses per object\n", " self.cumulative_age = {i: 0 for i in range(1, DATABASE_OBJECTS + 1)} # Track cumulative age per object\n", " self.access_count = {i: 0 for i in range(1, DATABASE_OBJECTS + 1)} # Track access count per object\n", " self.next_refresh = {} # Track the next refresh time for each cached object\n", " \n", " def get(self, obj_id):\n", " if obj_id in self.storage and \\\n", " (self.ttl[obj_id] > env.now or CACHE_TTL == 0):\n", " # Cache hit: increment hit count and update cumulative age\n", " self.hits[obj_id] += 1\n", " self.cumulative_age[obj_id] += self.age[obj_id]\n", " self.access_count[obj_id] += 1\n", " else:\n", " # Cache miss: increment miss count\n", " self.misses[obj_id] += 1\n", " self.access_count[obj_id] += 1\n", " \n", " # Fetch the object from the database if it’s not in cache\n", " obj = self.db.get_object(obj_id)\n", " \n", " # If the cache is full, evict the oldest object\n", " if len(self.storage) >= CACHE_CAPACITY:\n", " if self.cache_type == CacheType.LRU:\n", " self.evict_oldest()\n", " elif self.cache_type == CacheType.RANDOM_EVICTION:\n", " self.evict_random()\n", " \n", " # Add the object to cache, set TTL, reset age, and schedule next refresh\n", " self.storage[obj_id] = obj\n", " if CACHE_TTL != 0:\n", " self.ttl[obj_id] = env.now + CACHE_TTL\n", " else:\n", " self.ttl[obj_id] = 0\n", " self.age[obj_id] = 0\n", " if MAX_REFRESH_RATE != 0:\n", " self.next_refresh[obj_id] = env.now + np.random.exponential(self.db.mu_values[obj_id]) # Schedule refresh\n", "\n", " \n", " def evict_oldest(self):\n", " \"\"\"Remove the oldest item from the cache to make space.\"\"\"\n", " oldest_id = max(self.age, key=self.age.get) # Find the oldest item by age\n", " print(f\"[{env.now:.2f}] Cache: Evicting oldest object {oldest_id} to make space at {self.ttl[oldest_id]:.2f}\")\n", " del self.storage[oldest_id]\n", " del self.ttl[oldest_id]\n", " del self.age[oldest_id]\n", "\n", " def evict_random(self):\n", " \"\"\"Remove a random item from the cache to make space.\"\"\"\n", " random_id = np.random.choice(list(self.storage.keys())) # Select a random key from the cache\n", " print(f\"[{env.now:.2f}] Cache: Evicting random object {random_id} to make space at {self.ttl[random_id]:.2f}\")\n", " del self.storage[random_id]\n", " del self.ttl[random_id]\n", " del self.age[random_id]\n", " \n", " def refresh_object(self, obj_id):\n", " \"\"\"Refresh the object from the database to keep it up-to-date. TTL is increased on refresh.\"\"\"\n", " obj = self.db.get_object(obj_id)\n", " self.storage[obj_id] = obj\n", " if CACHE_TTL != 0:\n", " self.ttl[obj_id] = env.now + CACHE_TTL\n", " else:\n", " self.ttl[obj_id] = 0\n", " self.age[obj_id] = 0\n", " # print(f\"[{env.now:.2f}] Cache: Refreshed object {obj_id}\")\n", " \n", " def age_objects(self):\n", " \"\"\"Increment age of each cached object.\"\"\"\n", " for obj_id in list(self.age.keys()):\n", " if CACHE_TTL != 0:\n", " if self.ttl[obj_id] > env.now:\n", " self.age[obj_id] += 1\n", " # print(f\"[{env.now:.2f}] Cache: Object {obj_id} aged to {self.age[obj_id]}\")\n", " else:\n", " # Remove object if its TTL expired\n", " # print(f\"[{env.now:.2f}] Cache: Object {obj_id} expired\")\n", " del self.storage[obj_id]\n", " del self.ttl[obj_id]\n", " del self.age[obj_id]\n", " else:\n", " self.age[obj_id] += 1\n", " \n", " def record_cache_state(self):\n", " \"\"\"Record the current cache state (number of objects in cache) over time.\"\"\"\n", " self.cache_size_over_time.append((env.now, len(self.storage)))\n", " self.cache_next_request_over_time.append((env.now, self.db.next_request.copy()))" ] }, { "cell_type": "code", "execution_count": 6, "id": "7286d498-aa6c-4efb-bb28-fe29736eab64", "metadata": {}, "outputs": [], "source": [ "def age_cache_process(env, cache):\n", " \"\"\"Process that ages cache objects over time, removes expired items, and refreshes based on object-specific intervals.\"\"\"\n", " while True:\n", " cache.age_objects() # Age objects and remove expired ones\n", "\n", "\n", " if MAX_REFRESH_RATE != 0:\n", " # Refresh objects based on their individual refresh intervals\n", " for obj_id in list(cache.storage.keys()):\n", " # Check if it's time to refresh this object based on next_refresh\n", " if env.now >= cache.next_refresh[obj_id]:\n", " cache.refresh_object(obj_id)\n", " # Schedule the next refresh based on the object's mu\n", " cache.next_refresh[obj_id] = env.now + np.random.exponential(cache.db.mu_values[obj_id])\n", " \n", " cache.record_cache_state() # Record cache state at each time step\n", " yield env.timeout(1) # Run every second\n" ] }, { "cell_type": "code", "execution_count": 7, "id": "687f5634-8edf-4337-b42f-bbb292d47f0f", "metadata": {}, "outputs": [], "source": [ "def client_request_process(env, cache, event):\n", " \"\"\"Client process that makes requests for objects from the cache.\"\"\"\n", " lowest_lambda_object = max(cache.db.lambda_values.items(), key=lambda x: x[1])\n", " lowest_lambda_object = [lowest_lambda_object] if isinstance(lowest_lambda_object, int) else lowest_lambda_object\n", " while True:\n", " obj_id, next_request = min(cache.db.next_request.items(), key=lambda x: x[1])\n", " yield env.timeout(next_request - env.now)\n", " if env.now >= next_request:\n", " # print(f\"[{env.now:.2f}] Client: Requesting object {obj_id}\")\n", " cache.get(obj_id)\n", " \n", " # print(f\"[{env.now:.2f}] Client: Schedule next request for {obj_id}\")\n", " next_request = env.now + np.random.exponential(cache.db.lambda_values[obj_id])\n", " cache.request_log[obj_id].append(next_request)\n", " cache.db.next_request[obj_id] = next_request\n", " if all(cache.access_count[obj] >= ACCESS_COUNT_LIMIT for obj in lowest_lambda_object):\n", " event.succeed()" ] }, { "cell_type": "code", "execution_count": 8, "id": "c8516830-9880-4d9e-a91b-000338baf9d6", "metadata": { "scrolled": true }, "outputs": [], "source": [ "# Instantiate components\n", "db = Database()\n", "cache = Cache(env, db, cache_type)\n", "stop_event = env.event()" ] }, { "cell_type": "code", "execution_count": 9, "id": "2ba34b36-9ed5-4996-9600-11dfd25d8e60", "metadata": { "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "CPU times: user 434 ms, sys: 57.9 ms, total: 492 ms\n", "Wall time: 491 ms\n" ] } ], "source": [ "%%time\n", "\n", "# Start processes\n", "env.process(age_cache_process(env, cache))\n", "env.process(client_request_process(env, cache, stop_event))\n", "\n", "# Run the simulation\n", "env.run(until=stop_event)" ] }, { "cell_type": "code", "execution_count": 10, "id": "3b6f7c1f-ea54-4496-bb9a-370cee2d2751", "metadata": { "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Object 1: Hit Rate = 0.34, Average Age = 0.26\n", "Object 2: Hit Rate = 0.10, Average Age = 0.36\n", "Object 3: Hit Rate = 0.32, Average Age = 0.24\n", "Object 4: Hit Rate = 0.35, Average Age = 0.26\n", "Object 5: Hit Rate = 0.19, Average Age = 0.24\n", "Object 6: Hit Rate = 0.32, Average Age = 0.25\n", "Object 7: Hit Rate = 0.10, Average Age = 0.21\n", "Object 8: Hit Rate = 0.32, Average Age = 0.26\n", "Object 9: Hit Rate = 0.34, Average Age = 0.24\n", "Object 10: Hit Rate = 0.34, Average Age = 0.22\n", "Object 11: Hit Rate = 0.22, Average Age = 0.24\n", "Object 12: Hit Rate = 0.35, Average Age = 0.31\n", "Object 13: Hit Rate = 0.32, Average Age = 0.23\n", "Object 14: Hit Rate = 0.34, Average Age = 0.27\n", "Object 15: Hit Rate = 0.20, Average Age = 0.20\n", "Object 16: Hit Rate = 0.18, Average Age = 0.20\n", "Object 17: Hit Rate = 0.30, Average Age = 0.25\n", "Object 18: Hit Rate = 0.33, Average Age = 0.25\n", "Object 19: Hit Rate = 0.12, Average Age = 0.27\n", "Object 20: Hit Rate = 0.35, Average Age = 0.26\n", "Object 21: Hit Rate = 0.35, Average Age = 0.21\n", "Object 22: Hit Rate = 0.35, Average Age = 0.29\n", "Object 23: Hit Rate = 0.35, Average Age = 0.20\n", "Object 24: Hit Rate = 0.20, Average Age = 0.18\n", "Object 25: Hit Rate = 0.35, Average Age = 0.26\n", "Object 26: Hit Rate = 0.32, Average Age = 0.21\n", "Object 27: Hit Rate = 0.34, Average Age = 0.24\n", "Object 28: Hit Rate = 0.09, Average Age = 0.17\n", "Object 29: Hit Rate = 0.34, Average Age = 0.25\n", "Object 30: Hit Rate = 0.32, Average Age = 0.28\n", "Object 31: Hit Rate = 0.33, Average Age = 0.26\n", "Object 32: Hit Rate = 0.09, Average Age = 0.31\n", "Object 33: Hit Rate = 0.35, Average Age = 0.25\n", "Object 34: Hit Rate = 0.07, Average Age = 0.30\n", "Object 35: Hit Rate = 0.33, Average Age = 0.26\n", "Object 36: Hit Rate = 0.32, Average Age = 0.30\n", "Object 37: Hit Rate = 0.37, Average Age = 0.22\n", "Object 38: Hit Rate = 0.14, Average Age = 0.39\n", "Object 39: Hit Rate = 0.05, Average Age = 0.20\n", "Object 40: Hit Rate = 0.33, Average Age = 0.27\n", "Object 41: Hit Rate = 0.12, Average Age = 0.30\n", "Object 42: Hit Rate = 0.07, Average Age = 0.25\n", "Object 43: Hit Rate = 0.22, Average Age = 0.32\n", "Object 44: Hit Rate = 0.33, Average Age = 0.25\n", "Object 45: Hit Rate = 0.30, Average Age = 0.34\n", "Object 46: Hit Rate = 0.33, Average Age = 0.27\n", "Object 47: Hit Rate = 0.02, Average Age = 0.00\n", "Object 48: Hit Rate = 0.32, Average Age = 0.24\n", "Object 49: Hit Rate = 0.31, Average Age = 0.27\n", "Object 50: Hit Rate = 0.33, Average Age = 0.23\n", "Object 51: Hit Rate = 0.12, Average Age = 0.25\n", "Object 52: Hit Rate = 0.04, Average Age = 0.33\n", "Object 53: Hit Rate = 0.33, Average Age = 0.27\n", "Object 54: Hit Rate = 0.33, Average Age = 0.25\n", "Object 55: Hit Rate = 0.35, Average Age = 0.24\n", "Object 56: Hit Rate = 0.38, Average Age = 0.26\n", "Object 57: Hit Rate = 0.31, Average Age = 0.26\n", "Object 58: Hit Rate = 0.00, Average Age = 0.00\n", "Object 59: Hit Rate = 0.19, Average Age = 0.15\n", "Object 60: Hit Rate = 0.31, Average Age = 0.20\n", "Object 61: Hit Rate = 0.13, Average Age = 0.00\n", "Object 62: Hit Rate = 0.35, Average Age = 0.25\n", "Object 63: Hit Rate = 0.32, Average Age = 0.23\n", "Object 64: Hit Rate = 0.25, Average Age = 0.34\n", "Object 65: Hit Rate = 0.35, Average Age = 0.28\n", "Object 66: Hit Rate = 0.04, Average Age = 0.33\n", "Object 67: Hit Rate = 0.34, Average Age = 0.21\n", "Object 68: Hit Rate = 0.00, Average Age = 0.00\n", "Object 69: Hit Rate = 0.33, Average Age = 0.33\n", "Object 70: Hit Rate = 0.33, Average Age = 0.26\n", "Object 71: Hit Rate = 0.23, Average Age = 0.23\n", "Object 72: Hit Rate = 0.33, Average Age = 0.24\n", "Object 73: Hit Rate = 0.34, Average Age = 0.29\n", "Object 74: Hit Rate = 0.33, Average Age = 0.20\n", "Object 75: Hit Rate = 0.10, Average Age = 0.29\n", "Object 76: Hit Rate = 0.20, Average Age = 0.30\n", "Object 77: Hit Rate = 0.21, Average Age = 0.21\n", "Object 78: Hit Rate = 0.17, Average Age = 0.20\n", "Object 79: Hit Rate = 0.04, Average Age = 0.00\n", "Object 80: Hit Rate = 0.34, Average Age = 0.28\n", "Object 81: Hit Rate = 0.30, Average Age = 0.26\n", "Object 82: Hit Rate = 0.06, Average Age = 0.12\n", "Object 83: Hit Rate = 0.18, Average Age = 0.25\n", "Object 84: Hit Rate = 0.34, Average Age = 0.29\n", "Object 85: Hit Rate = 0.33, Average Age = 0.26\n", "Object 86: Hit Rate = 0.19, Average Age = 0.31\n", "Object 87: Hit Rate = 0.33, Average Age = 0.20\n", "Object 88: Hit Rate = 0.21, Average Age = 0.25\n", "Object 89: Hit Rate = 0.35, Average Age = 0.23\n", "Object 90: Hit Rate = 0.33, Average Age = 0.28\n", "Object 91: Hit Rate = 0.22, Average Age = 0.31\n", "Object 92: Hit Rate = 0.20, Average Age = 0.22\n", "Object 93: Hit Rate = 0.14, Average Age = 0.26\n", "Object 94: Hit Rate = 0.32, Average Age = 0.29\n", "Object 95: Hit Rate = 0.20, Average Age = 0.31\n", "Object 96: Hit Rate = 0.34, Average Age = 0.21\n", "Object 97: Hit Rate = 0.33, Average Age = 0.27\n", "Object 98: Hit Rate = 0.00, Average Age = 0.00\n", "Object 99: Hit Rate = 0.12, Average Age = 0.33\n", "Object 100: Hit Rate = 0.23, Average Age = 0.25\n" ] } ], "source": [ "statistics = []\n", "# Calculate and print hit rate and average age for each object\n", "for obj_id in range(1, DATABASE_OBJECTS + 1):\n", " if cache.access_count[obj_id] != 0:\n", " hit_rate = cache.hits[obj_id] / max(1, cache.access_count[obj_id]) # Avoid division by zero\n", " avg_age = cache.cumulative_age[obj_id] / max(1, cache.hits[obj_id]) # Only average over hits\n", " print(f\"Object {obj_id}: Hit Rate = {hit_rate:.2f}, Average Age = {avg_age:.2f}\")\n", " statistics.append({\"obj_id\": obj_id,\"hit_rate\": hit_rate,\"avg_age\": avg_age})" ] }, { "cell_type": "code", "execution_count": 11, "id": "b2d18372-cdba-4151-ae32-5bf45466bf94", "metadata": {}, "outputs": [], "source": [ "stats = pd.DataFrame(statistics)\n", "stats.to_csv(f\"{TEMP_BASE_DIR}/hit_age.csv\",index=False)\n", "stats.drop(\"obj_id\", axis=1).describe().to_csv(f\"{TEMP_BASE_DIR}/overall_hit_age.csv\")" ] }, { "cell_type": "code", "execution_count": 12, "id": "80971714-44f1-47db-9e89-85be7c885bde", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
| \n", " | access_count | \n", "hits | \n", "misses | \n", "mu | \n", "lambda | \n", "hit_rate | \n", "
|---|---|---|---|---|---|---|
| 1 | \n", "664 | \n", "224 | \n", "440 | \n", "0 | \n", "1 | \n", "10.38 | \n", "
| 2 | \n", "212 | \n", "22 | \n", "190 | \n", "0 | \n", "3 | \n", "32.39 | \n", "
| 3 | \n", "673 | \n", "218 | \n", "455 | \n", "0 | \n", "1 | \n", "34.60 | \n", "
| 4 | \n", "711 | \n", "246 | \n", "465 | \n", "0 | \n", "1 | \n", "19.13 | \n", "
| 5 | \n", "345 | \n", "66 | \n", "279 | \n", "0 | \n", "2 | \n", "31.74 | \n", "
| ... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "
| 95 | \n", "351 | \n", "70 | \n", "281 | \n", "0 | \n", "2 | \n", "33.89 | \n", "
| 96 | \n", "717 | \n", "243 | \n", "474 | \n", "0 | \n", "1 | \n", "32.53 | \n", "
| 97 | \n", "664 | \n", "216 | \n", "448 | \n", "0 | \n", "1 | \n", "0.00 | \n", "
| 98 | \n", "23 | \n", "0 | \n", "23 | \n", "0 | \n", "37 | \n", "11.60 | \n", "
| 99 | \n", "181 | \n", "21 | \n", "160 | \n", "0 | \n", "4 | \n", "22.85 | \n", "
99 rows × 6 columns
\n", "