{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Create Classes, Instantiate  and  add Instances - Zoo example\n",
    "\n",
    "### Classes\n",
    "- use CamelCase when naming classes\n",
    "- documentation should be included for each class briefly describing its purpose and functionality\n",
    "- classes need an initialization statement\n",
    "    \n",
    "    class MyClass:\n",
    "\n",
    "    ''' this is where documentation information should go'''\n",
    "\n",
    "    def __init__(self,value, additional_autofunction_arguments):\n",
    "        #define an attribute with the contents of the value (also called an instance)\n",
    "        self.attribute = value\n",
    "        #define a function that will happen automatically (becuse it is under the init function) also called a non-public function\n",
    "        self.autofunction = self._function() #non-public functions require underscore before according to PEP-8\n",
    "        #write the function\n",
    "    def_function(self):\n",
    "        return function(self.attribute)\n",
    "\n",
    "#### Child Classes\n",
    "- classes that inherit the attributes of its parent\n",
    "\n",
    "    class ChildClass(MyClass):  #using MyClass in the arguments allows access to that class\n",
    "        def __init__(self):\n",
    "        # You can call the parent class init so all functions in the parent class will run\n",
    "        MyClass.__init__(self,value, additional_autofunction_arguments):\n",
    "        #then create as many instances of the child class\n",
    "        self.child_attribute = value\n",
    "        \n",
    "**you can utilize super.()__init__(self) when calling a parent class after it has been identified in the class: class ChildClass(MyClass)**\n",
    "        \n",
    "#### Grandchild Classes\n",
    "- same inheiritencce properties, if you inherit the properties of a child, you will also inherit the properties of its parent.\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Examples of Self\n",
    "\n",
    "#### Introduce and Eat Breakfast\n",
    "    class Person():\n",
    "\n",
    "        def say_hello(self):\n",
    "            return \"Hi, how are you?\"\n",
    "\n",
    "        def eat_breakfast(self):\n",
    "            self.hungry = False\n",
    "            return \"Yum that was delish!\"\n",
    "\n",
    "    gail = Person()\n",
    "    print(\"1.\", vars(gail))\n",
    "    gail.name = \"Gail\"\n",
    "    gail.age = 29\n",
    "    gail.weight = 'None of your business!'\n",
    "    print(\"2.\", gail.say_hello())\n",
    "    print(\"3.\", gail.eat_breakfast())\n",
    "    print(\"4.\", vars(gail))\n",
    "    \n",
    "\n",
    "#### Calling Instance methods with Self\n",
    "class Person():\n",
    "\n",
    "    def eat_sandwhich(self):\n",
    "        if (self.hungry):\n",
    "            self.relieve_hunger()\n",
    "            return \"Wow, that really hit the spot! I am so full, but more importantly, I'm not hangry anymore!\"\n",
    "        else:\n",
    "            return \"Oh, I don't think I can eat another bite. Thank you, though!\"\n",
    "    \n",
    "    def relieve_hunger(self):\n",
    "        print(\"Hunger is being relieved\")\n",
    "        self.hungry = False\n",
    "\n",
    "the_snail = Person()\n",
    "the_snail.name = \"the Snail\"\n",
    "the_snail.hungry = True\n",
    "print(\"1. \", the_snail.hungry)\n",
    "print(\"2. \", the_snail.eat_sandwhich())\n",
    "print(\"3. \", the_snail.hungry)\n",
    "print(\"4. \", the_snail.eat_sandwhich())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Getters and Setters \n",
    "    class BankAccount():\n",
    "\n",
    "        def set_balance(self, amount):\n",
    "            print(\"SETTING BALANCE\")\n",
    "            self._balance += amount\n",
    "\n",
    "        def get_balance(self):\n",
    "            print(\"GETTING BALANCE\")\n",
    "            return self._balance\n",
    "\n",
    "\n",
    "       def make_withdrawal(self, amount_requested):\n",
    "            if (self.check_min_bal(amount_requested)):\n",
    "                return self.check_min_bal(amount_requested)\n",
    "            if (self.check_max_withdrawal(amount_requested)):\n",
    "                return self.check_max_withdrawal(amount_requested)\n",
    "            else: \n",
    "    # ----------- NOTE THE CHANGE FROM self.set_balance(amount) TO self.balance = amount --------- #\n",
    "                self.balance = -amount_requested\n",
    "                return f\"${amount_requested}\"\n",
    "                \n",
    "        def check_min_bal(self, amount_requested):\n",
    "    # ----------- NOTE THE CHANGE FROM self.get_balance() TO self.balance = --------------- #\n",
    "            if ((self.balance - amount_requested) > self._minimum_balance): \n",
    "                return False\n",
    "            else:\n",
    "                return f\"Sorry, you do not have enough funds to withdrawal ${amount_requested} and maintain your minimum balance of ${self._minimum_balance}\"\n",
    "\n",
    "        def check_max_withdrawal(self, amount_requested):\n",
    "            if (self._max_withdrawal > amount_requested):\n",
    "                return False\n",
    "            else:\n",
    "                return f\"Sorry, your maximum withdraw amount is {self._max_withdrawal}\"\n",
    "\n",
    "        def make_deposit(self, amount_to_deposit):\n",
    "            try: \n",
    "                (float(amount_to_deposit))\n",
    "    # ----------- NOTE THE CHANGE FROM self.set_balance(amount) TO self.balance = amount ----------- #\n",
    "                self.balance = float(amount_to_deposit)\n",
    "                return f\"Thank you for the deposit of ${amount_to_deposit}. Your balance is now: ${self._balance}\"\n",
    "            except:\n",
    "                return f\"{amount_to_deposit} is not a number\"\n",
    "\n",
    "    # ----------- HERE is where we are using the property() function-------------------------- #\n",
    "        balance = property(get_balance, set_balance)\n",
    "\n",
    "\n",
    "\n",
    "    # just a non-class function that makes an account and initializes its properties... what a good idea\n",
    "    def make_account():\n",
    "        new_account = BankAccount()\n",
    "        new_account._balance = 0\n",
    "        new_account._minimum_balance = 250\n",
    "        new_account._max_withdrawal = 150 \n",
    "        return new_account\n",
    "\n",
    "    account_three = make_account()\n",
    "    print(\"1.\", account_three.get_balance())\n",
    "    print(\"2.\", account_three.set_balance(1000)) # returns None since assignment returns None\n",
    "    print(\"3.\", account_three.get_balance())\n",
    "    print(\"4.\", account_three.make_withdrawal(1000))\n",
    "    print(\"5.\", account_three.make_withdrawal(100))\n",
    "    print(\"6.\", account_three.make_withdrawal(300))\n",
    "    print(\"7.\", account_three.make_deposit(250))\n",
    "    print(\"8.\", account_three.make_deposit(2.50))\n",
    "    print(\"9.\", account_three.make_deposit(\"hello\"))\n",
    "    print(\"10.\", vars(account_three))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Init (defined attributes)\n",
    "\n",
    "#### By using the __init__ method, we can initialize instances of objects with defined attributes\n",
    "\n",
    "    #Define Passenger Class Here\n",
    "    class Passenger():\n",
    "\n",
    "        def __init__(self, first, last, email, rides_taken):\n",
    "            self.first = first\n",
    "            self.last = last\n",
    "            self.email = email\n",
    "            self.rides_taken = rides_taken\n",
    "    rebecca_black = Passenger(\"Rebecca\", \"Black\", \"rebecca.black@gmail.com\", 0) # initialize Rebecca Black here\n",
    "    print(rebecca_black.first) # \"Rebecca\"\n",
    "    print(rebecca_black.last) # \"Black\"\n",
    "    print(rebecca_black.email) # \"rebecca.black@gmail.com\"\n",
    "    print(rebecca_black.rides_taken) # 0"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Creating a Domain Model\n",
    "\n",
    "#### Defining Complex Methods with Attributes in a Class\n",
    "\n",
    "**Note: make sure before creating complex environments to map an outline to better understand functionality and placement**\n",
    "\n",
    "    class Customer():\n",
    "        def __init__(self, name=None, orders=[], location=None):\n",
    "            self.name=name\n",
    "            self.orders = orders\n",
    "            self.location = location\n",
    "            self.total_spent = sum([i['item_cost']*i['quantity'] for i in orders])\n",
    "        def add_order(self, item_name, item_cost, quantity):\n",
    "            self.orders.append({'item_name': item_name, 'item_cost':item_cost, 'quantity':quantity})\n",
    "            self.total_spent += item_cost * quantity\n",
    "\n",
    "    class Business():\n",
    "        def __init__(self, name=None, biz_type=None, city=None, customers = []):\n",
    "            self.name = name\n",
    "            self.biz_type = biz_type\n",
    "            self.city = city\n",
    "            self.customers = customers\n",
    "        def add_customer(self, customer):\n",
    "            self.customers.append(customer)\n",
    "        #add top_n customers function\n",
    "        def top_n_customers(self, n):\n",
    "            top_n = sorted(self.customers, key = lambda x: x.total_spent, reverse=True)[:n]\n",
    "            for c in top_n:\n",
    "                print(c.name, c.total_spent)\n",
    "\n",
    "    #create instance\n",
    "    startup = Business('etsy_store2076', 'crafts')\n",
    "    customer1 = Customer(name='Bob', orders=[])\n",
    "    customer1.add_order('sweater', 24.99, 1)\n",
    "    customer1.orders\n",
    "    [{'item_cost': 24.99, 'item_name': 'sweater', 'quantity': 1}]\n",
    "    customer1.total_spent\n",
    "\n",
    "    #Generating Customers and orders at scale\n",
    "    import numpy as np\n",
    "    names = ['Liam',  'Emma', 'Noah','Olivia','William','Ava',\n",
    "             'James','Isabella','Logan','Sophia','Benjamin','Mia','Mason',\n",
    "             'Charlotte','Elijah','Amelia','Oliver','Evelyn','Jacob','Abigail]']\n",
    "    items = [('sweater',50), ('scarf', 35), ('gloves', 20), ('hat', 20)]\n",
    "\n",
    "    for i in range(10):\n",
    "        customer = Customer(name=np.random.choice(names)) #Create a customer\n",
    "        n_orders = np.random.randint(1,5) #Create an order or two, or three, or four, or five!\n",
    "        for order_n in range(n_orders):\n",
    "            idx = np.random.choice(len(items)) #np.random.choice doesn't work with nested lists; workaround\n",
    "            item = items[idx]\n",
    "            item_name = item[0]\n",
    "            item_price = item[1]\n",
    "            quantity = np.random.randint(1,4)\n",
    "            customer.add_order(item_name, item_price, quantity)\n",
    "        #Add the customer to our business\n",
    "        startup.add_customer(customer)\n",
    "\n",
    "    #testing the top n customers function\n",
    "    startup.top_n_customers(5)\n",
    "    startup.top_n_customers(50)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Instance Objects and Class Methods\n",
    "\n",
    "    class Dog:\n",
    "\n",
    "        _species = \"canine\" #class variable\n",
    "\n",
    "        def __init__(self):\n",
    "            self._species = \"I'm a dog INSTANCE\" #instance variable\n",
    "\n",
    "        @classmethod\n",
    "        def species(cls):\n",
    "            return cls._species #class method\n",
    "\n",
    "\n",
    "    new_dog = Dog() #instance object\n",
    "    print(\"1. ---\", Dog._species, \"--- This is the dog **class** directly accessing its class variable\")\n",
    "    print(\"2. ---\", new_dog._species, \"--- This is an **instance object** of the dog class accessing its own instance variable\")\n",
    "    print(\"3. ---\", Dog.species(), \"--- This is the dog class invoking the species *class method* to access its class variable\")\n",
    "    print(\"4. ---\", new_dog.species(), \"--- This is an **instance object** of the dog class invoking the *class method*\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Inheiritance (Dont Repeat Yourself)\n",
    "\n",
    "    class Animal(object): #superclass that will inheirit other object definitions and will contain attributes of all of the subclasses associated with it\n",
    "\n",
    "        def __init__(self, name, weight):\n",
    "            self.name = name\n",
    "            self.weight = weight\n",
    "            self.species = None\n",
    "            self.size = None\n",
    "            self.food_type = None\n",
    "            self.nocturnal = False\n",
    "\n",
    "        def sleep(self):\n",
    "            if self.nocturnal:\n",
    "                print(\"{} sleeps during the day!\".format(self.name))\n",
    "            else:\n",
    "                print(\"{} sleeps during the night!\".format(self.name))\n",
    "\n",
    "        def eat(self, food):\n",
    "            if self.food_type == 'omnivore':\n",
    "                print(\"{} the {} thinks {} is Yummy!\".format(self.name, self.species, food))\n",
    "            elif (food == 'meat' and self.food_type == \"carnivore\") or (food == 'plants' and self.food_type == 'herbivore'):\n",
    "                print(\"{} the {} thinks {} is Yummy!\".format(self.name, self.species, food))\n",
    "            else:\n",
    "                print(\"I don't eat this!\")\n",
    "                \n",
    "    class Elephant(Animal): #subclass as part of Animal superclass\n",
    "\n",
    "        def __init__(self, name, weight):\n",
    "            super().__init__(name, weight)\n",
    "            self.size = 'enormous'\n",
    "            self.species = 'elephant'\n",
    "            self.food_type = 'herbivore'\n",
    "            self.nocturnal = False\n",
    "\n",
    "    class Tiger(Animal): #subclass as part of Animal superclass\n",
    "\n",
    "        def __init__(self, name, weight):\n",
    "            super().__init__(name, weight)\n",
    "            self.size = 'large'\n",
    "            self.species = 'tiger'\n",
    "            self.food_type = 'carnivore'\n",
    "            self.nocturnal = True\n",
    "\n",
    "    class Raccoon(Animal): #subclass as part of Animal superclass\n",
    "\n",
    "        def __init__(self, name, weight):\n",
    "            super().__init__(name, weight)\n",
    "            self.size = 'small'\n",
    "            self.species = 'raccoon'\n",
    "            self.food_type = 'omnivore'\n",
    "            self.nocturnal = True\n",
    "\n",
    "    class Gorilla(Animal): #subclass as part of Animal superclass\n",
    "\n",
    "        def __init__(self, name, weight):\n",
    "            super().__init__(name, weight)\n",
    "            self.size = 'large'\n",
    "            self.species = 'gorilla'\n",
    "            self.food_type = 'herbivore'\n",
    "            self.nocturnal = False\n",
    "\n",
    "    def add_animal_to_zoo(zoo, animal_type, name, weight): #adds animals to the zoo\n",
    "        animal = None\n",
    "        if animal_type == \"Gorilla\":\n",
    "            animal = Gorilla(name, weight)\n",
    "        elif animal_type == \"Raccoon\":\n",
    "            animal = Raccoon(name, weight)\n",
    "        elif animal_type == \"Tiger\":\n",
    "            animal = Tiger(name, weight)\n",
    "        else:\n",
    "            animal = Elephant(name, weight)\n",
    "\n",
    "        zoo.append(animal)\n",
    "\n",
    "        return zoo\n",
    "\n",
    "    #adding animals to the zoo\n",
    "    to_create = ['Elephant', 'Elephant', 'Raccoon', 'Raccoon', 'Gorilla', 'Tiger', 'Tiger', 'Tiger']\n",
    "\n",
    "    zoo = []\n",
    "\n",
    "    for i in to_create:\n",
    "        zoo = add_animal_to_zoo(zoo, i, \"name\", 100)\n",
    "\n",
    "    zoo\n",
    "\n",
    "    def feed_animals(zoo, time='Day'): #feed animals function\n",
    "        for animal in zoo:\n",
    "            if time == 'Day':\n",
    "                # CASE: Daytime feeding--Only feed the animals that aren't nocturnal\n",
    "                if animal.nocturnal == False:\n",
    "                    # If the animal is a carnivore, feed it \"meat\".  Otherwise, feed it \"plants\"\n",
    "                    if animal.food_type == 'carnivore':\n",
    "                        animal.eat('meat')\n",
    "                    else:\n",
    "                        animal.eat('plants')\n",
    "            else:\n",
    "                # CASE: Night-time feeding--feed only the nocturnal animals! \n",
    "                if animal.nocturnal == True:\n",
    "                    if animal.food_type == 'carnivore':\n",
    "                        animal.eat('meat')\n",
    "                    else:\n",
    "                        animal.eat('plants')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## OOP Disease Simulation with notes\n",
    "\n",
    "    import numpy as np\n",
    "    import pandas as pd\n",
    "    from tqdm.autonotebook import tqdm\n",
    "    np.random.seed(0)\n",
    "\n",
    "\n",
    "#### The Assumptions of Our Model:\n",
    "\n",
    "- Vaccines are 100% effective.\n",
    "- Infected individuals that recover from the disease are now immune to catching the disease a second time (think Chickenpox)\n",
    "- Dead invidiuals are not contagious.\n",
    "- All infections happen from person-to-person interaction\n",
    "- All individuals interact with the same amount of people each day\n",
    "- The r0 value (pronounced \"R-nought\") is a statistic from the Centers for Disease Control that estimates the average number of people an infected person will infect before they are no longer contagious. For this value, we assume:\n",
    "- That this number is out of 100 people\n",
    "- That this statistic is accurate\n",
    "\n",
    "\n",
    "#### Building our Person class\n",
    "Our Person class should have the following attributes:\n",
    "\n",
    "- is_alive = True\n",
    "- is_vaccinated, a boolean value which will be determined by generating a random value between 0 and 1. We will then compare this to (1 - pct_vaccinated), a variable that should be passed in at instantiation time. If the random number is greater, then this attribute should be True-- otherwise, False\n",
    "- is_infected = False\n",
    "- has_been_infected = False\n",
    "- newly_infected = False\n",
    "\n",
    "    class Person(object):\n",
    "\n",
    "        def __init__(self):\n",
    "            self.is_alive = True\n",
    "            self.is_infected = False\n",
    "            self.has_been_infected = False\n",
    "            self.newly_infected = False\n",
    "            self.is_vaccinated = False\n",
    "\n",
    "        def get_vaccinated(self, pct_vaccinated):\n",
    "            if (1 - pct_vaccinated) < np.random.random():\n",
    "                self.is_vaccinated = True\n",
    "\n",
    "\n",
    "#### Creating our Simulation Class\n",
    "\n",
    "##### Writing our __init__ method\n",
    "Our init method should take in the following arguments at instantiation time:\n",
    "\n",
    "- self\n",
    "- population_size\n",
    "- disease_name\n",
    "- r0\n",
    "- mortality_rate\n",
    "- total_time_steps\n",
    "- pct_vaccinated\n",
    "- num_initial_infected\n",
    "\n",
    "##### Attributes\n",
    "\n",
    "- The attributes self.disease_name, self.mortality_rate, and self.total_time_steps should be set to the corresponding arguments passed in at instantiation time.\n",
    "- The attribute self.r0 should be set to set to r0 / 100 to convert the number to a decimal between 0 and 1.\n",
    "- keeping track of what time step the simulation is on (self.current_time_step: set to 0)\n",
    "- current number of infected during this time step (self._total_infected_counter: set to 0)\n",
    "- the total number of people that have been infected at any time during the simulation. (self.current_infected_counter: set to 0)\n",
    "- create an array to hold all of the Person objects in our simulation. Set self.population equal to an empty list.\n",
    "- create the population, and determining if they are healthy, vaccinated, or infected.\n",
    "\n",
    "    class Simulation(object):\n",
    "\n",
    "        def __init__(self, population_size, disease_name, r0, mortality_rate,  total_time_steps, pct_vaccinated, num_initial_infected):\n",
    "            self.r0 = r0 / 100.\n",
    "            self.disease_name = disease_name\n",
    "            self.mortality_rate = mortality_rate\n",
    "            self.total_time_steps = total_time_steps\n",
    "            self.current_time_step = 0\n",
    "            self.total_infected_counter = 0\n",
    "            self.current_infected_counter = 0\n",
    "            self.dead_counter = 0\n",
    "            self.population = []\n",
    "            # This attribute is used in a function that is provided for you in order to log statistics from each time_step.  \n",
    "            # Don't touch it!\n",
    "            self.time_step_statistics_df = pd.DataFrame()\n",
    "        \n",
    "            # Create a for loop the size of the population we want in this simulation\n",
    "            for i in range(population_size):\n",
    "                # Create new person\n",
    "                new_person = Person()\n",
    "                # We'll add infected persons to our simulation first.  Check if the current number of infected are equal to the \n",
    "                # num_initial_infected parameter.  If not, set new_person to be infected\n",
    "                if self.current_infected_counter != num_initial_infected:\n",
    "                    new_person.is_infected = True\n",
    "                    # dont forget to increment both infected counters!\n",
    "                    self.total_infected_counter += 1\n",
    "                    self.current_infected_counter += 1\n",
    "                # if new_person is not infected, determine if they are vaccinated or not by using their `get_vaccinated` method\n",
    "                # Then, append new_person to self.population\n",
    "                else:\n",
    "                    new_person.get_vaccinated(pct_vaccinated)\n",
    "                self.population.append(new_person)\n",
    "\n",
    "            print(\"-\" * 50)\n",
    "            print(\"Simulation Initiated!\")\n",
    "            print(\"-\" * 50)\n",
    "            self._get_sim_statistics()\n",
    "        \n",
    "        \n",
    "    \n",
    "        def _get_sim_statistics(self):\n",
    "        #In the interest of time, this method has been provided for you.  No extra code needed.\n",
    "            num_infected = 0\n",
    "            num_dead = 0\n",
    "            num_vaccinated = 0\n",
    "            num_immune = 0\n",
    "            for i in self.population:\n",
    "                if i.is_infected:\n",
    "                    num_infected += 1\n",
    "                if not i.is_alive:\n",
    "                    num_dead += 1\n",
    "                if i.is_vaccinated:\n",
    "                    num_vaccinated += 1\n",
    "                    num_immune += 1\n",
    "                if i.has_been_infected:\n",
    "                    num_immune += 1\n",
    "            assert num_infected == self.current_infected_counter\n",
    "            assert num_dead == self.dead_counter\n",
    "        \n",
    "        \n",
    "            print(\"\")\n",
    "            print(\"Summary Statistics for Time Step {}\".format(self.current_time_step))\n",
    "            print(\"\")\n",
    "            print(\"-\" * 50)\n",
    "            print(\"Disease Name: {}\".format(self.disease_name))\n",
    "            print(\"R0: {}\".format(self.r0 * 100))\n",
    "            print(\"Mortality Rate: {}%\".format(self.mortality_rate * 100))\n",
    "            print(\"Total Population Size: {}\".format(len(self.population)))\n",
    "            print(\"Total Number of Vaccinated People: {}\".format(num_vaccinated))\n",
    "            print(\"Total Number of Immune: {}\".format(num_immune))\n",
    "            print(\"Current Infected: {}\".format(num_infected))\n",
    "            print(\"Deaths So Far: {}\".format(num_dead))   \n",
    "\n",
    "#### Building Our Simulation's Behavior\n",
    "For any given time step, our simulation should complete the following steps in order:\n",
    "- Loop through each living person in the population 1A.\n",
    "- If the person is currently infected: 1B. \n",
    "- Select another random person from the population. 2B. \n",
    "- If this person is alive, not infected, unvaccinated, and hasn't been infected before: 1C. \n",
    "- Generate a random number between 0 and 1. If this random number is greater than (1 - self.r0), then mark this new person as newly infected 3B.\n",
    "- If the person is vaccinated, currently infected, or has been infected in a previous round of the simulation, do nothing. 2A. \n",
    "- Repeat the step above until the infected person has interacted with 100 random living people from the population.\n",
    "Once every infected person has interacted with 100 random living people, resolve all current illnesses and new infections 2A. \n",
    "- For each person that started this round as infected, generate a random number between 0 and 1. \n",
    "- If that number is greater than (1 - mortality rate), then that person has been killed by the disease. They should be marked as dead. Otherwise, they stay alive, and can longer catch the disease. 2B. \n",
    "- All people that were infected this round move from newly_infected to is_infected.\n",
    "\n",
    "\n",
    "#### infected_interaction() Function\n",
    "\n",
    "- Initialize a counter called num_interactions to 0.\n",
    "- Select a random person from self.population.\n",
    "- Check is the person is alive. If the person is dead, we will not count this as an interaction.\n",
    "- If the random person is alive and not vaccinated, generate a a random number between 0 and 1. If the random number is greater than (1 - self.r0), change the random person's newly_infected attribute to True.\n",
    "- Increment num_interactions by 1. Do not increment any of the infected counters in the simulation class--we'll have another method deal with those.\n",
    "- Complete the infected_interaction() method in the cell below. Comments have been provided to help you write it.\n",
    "\n",
    "HINT: To randomly select an item from a list, use np.random.choice()!\n",
    "\n",
    "    def infected_interaction(self, infected_person):\n",
    "        num_interactions = 0\n",
    "        while num_interactions < 100:\n",
    "            # Randomly select a person from self.population\n",
    "            random_person = np.random.choice(self.population)\n",
    "            # This only counts as an interaction if the random person selected is alive.  If the person is dead, we do nothing, \n",
    "            # and the counter doesn't increment, repeating the loop and selecting a new person at random.\n",
    "            # check if the person is alive.\n",
    "            if random_person.is_alive:\n",
    "                # CASE: Random person is not vaccinated, and has not been infected before, making them vulnerable to infection\n",
    "                if random_person.is_vaccinated == False and random_person.has_been_infected == False:\n",
    "                    # Generate a random number between 0 and 1\n",
    "                    random_number = np.random.random()\n",
    "                    # If random_number is greater than or equal to (1 - self.r0), set random person as newly_infected\n",
    "                    if random_number >= (1 - self.r0):\n",
    "                        random_person.newly_infected = True\n",
    "                # Dont forget to increment num_interactions, and make sure it's at this level of indentation\n",
    "                num_interactions += 1\n",
    "\n",
    "    #Add this function to our Simulation class\n",
    "    Simulation.infected_interaction = infected_interaction\n",
    "\n",
    "#### _resolve_states() Function\n",
    "\n",
    "- Iterate through every person in the population.\n",
    "- Check if the person is alive (since we dont need to bother checking anything for the dead ones)\n",
    "- If the person is infected, we need to resolve whether they survive the infection or die from it.\n",
    "- Generate a random number between 0 and 1.\n",
    "- If this number is greater than (1 - self.mortality_rate), the person has died.\n",
    "- Set the person's .is_alive and .is_infected attributes both to False.\n",
    "- Increment the simulation's self.dead_counter attribute by 1.\n",
    "- Decrement the simulation's self.current_infected_counter attribute by 1.\n",
    "- Else, the person has survived the infection and is now immune to future infections.\n",
    "- Set the person's is_infected attribute to False\n",
    "- Set the person's has_been_infected attribute to True\n",
    "- Decrement the simulation's self.current_infected_counter by 1.\n",
    "- If the person is newly infected:\n",
    "- Set the person's newly_infected attribute to False\n",
    "- Set the person's is_infected attribute to True\n",
    "- Increment total_infected_counter and current_infected_counter by 1.\n",
    "\n",
    "        def _resolve_states(self):\n",
    "            \"\"\"\n",
    "        Every person in the simulation falls into 1 of 4 states at any given time:\n",
    "        1. Dead \n",
    "        2. Alive and not infected\n",
    "        3. Currently infected\n",
    "        4. Newly Infected\n",
    "\n",
    "        States 1 and 2 need no resolving, but State 3 will resolve by either dying or surviving the disease, and State 4 will resolve\n",
    "        by turning from newly infected to currently infected.\n",
    "\n",
    "        This method will be called at the end of each time step.  All states must be resolved before the next time step can begin.\n",
    "        \"\"\"\n",
    "        # Iterate through each person in the population\n",
    "        for person in self.population:\n",
    "            # We only need to worry about the people that are still alive\n",
    "            if person.is_alive: \n",
    "                # CASE: Person was infected this round.  We need to stochastically determine if they die or recover from the disease\n",
    "                # Check if person is_infected\n",
    "                if person.is_infected:\n",
    "                    # Generate a random number\n",
    "                    random_number = np.random.random()\n",
    "                    # If random_number is >= (1 - self.mortality_rate), set the person to dead and increment the simulation's death\n",
    "                    # counter\n",
    "                    if random_number >= (1 - self.mortality_rate):\n",
    "                        # Set is_alive and in_infected both to False\n",
    "                        person.is_alive = False\n",
    "                        person.is_infected = False\n",
    "                            # Don't forget to increment self.dead_counter, and decrement self.current_infected_counter\n",
    "                        self.dead_counter += 1\n",
    "                        self.current_infected_counter -= 1\n",
    "                    else:\n",
    "                        # CASE: They survive the disease and recover.  Set is_infected to False and has_been_infected to True\n",
    "                        person.is_infected = False\n",
    "                        person.has_been_infected = True\n",
    "                        # Don't forget to decrement self.current_infected_counter!\n",
    "                        self.current_infected_counter -= 1\n",
    "                # CASE: Person was newly infected during this round, and needs to be set to infected before the start of next round\n",
    "                elif person.newly_infected:\n",
    "                    # Set is_infected to True, newly_infected to False, and increment both self.current_infected_counter and \n",
    "                    # self.total_infected_counter\n",
    "                    person.is_infected = True\n",
    "                    person.newly_infected = False\n",
    "                    self.current_infected_counter += 1\n",
    "                    self.total_infected_counter += 1    \n",
    "    \n",
    "    #Add this function to our Simulation class           \n",
    "    Simulation._resolve_states = _resolve_states\n",
    "\n",
    "#### _time_step() Function\n",
    "\n",
    "- Iterate through each person in the population\n",
    "- If the person is alive and infected, call self.infected_interaction() and pass in this infected person\n",
    "- Once we have looped through every person, call self._resolve_states() to resolve all outstanding states and prepare for the next round.\n",
    "- Log the statistics from this round by calling self._log_time_step_statistics(). This function has been provided for you further down the notebook.\n",
    "- Increment self.current_time_step.\n",
    "\n",
    "    def _time_step(self):\n",
    "        \"\"\"\n",
    "        Compute 1 time step of the simulation. This function will make use of the helper methods we've created above. \n",
    "\n",
    "        The steps for a given time step are:\n",
    "        1.  Iterate through each person in self.population.\n",
    "            - For each infected person, call infected_interaction() and pass in that person.\n",
    "        2.  Use _resolve_states() to resolve all states for the newly infected and the currently infected.\n",
    "        3. Increment self.current_time_step by 1. \n",
    "        \"\"\"\n",
    "        # Iterate through each person in the population\n",
    "        for person in self.population:\n",
    "            # Check only for people that are alive and infected\n",
    "            if person.is_alive and person.is_infected:\n",
    "                # Call self.infecteed_interaction() and pass in this infected person\n",
    "                self.infected_interaction(person)\n",
    "\n",
    "        # Once we've made it through the entire population, call self._resolve_states()\n",
    "        self._resolve_states()\n",
    "\n",
    "        # Now, we're almost done with this time step.  Log summary statistics, and then increment self.current_time_step by 1.\n",
    "        self._log_time_step_statistics()\n",
    "        self.current_time_step += 1\n",
    "\n",
    "    #Add this function to our Simulation class\n",
    "    Simulation._time_step = _time_step\n",
    "\n",
    "#### log_time_step_statistics\n",
    "\n",
    "    def _log_time_step_statistics(self, write_to_file=False):\n",
    "        # Gets the current number of dead,\n",
    "        # CASE: Round 0 of simulation, need to create and Structure DataFrame\n",
    "    #     if self.time_step_statistics_df == None:\n",
    "    #         import pandas as pd\n",
    "    #         self.time_step_statistics_df = pd.DataFrame()\n",
    "    # #         col_names = ['Time Step', 'Currently Infected', \"Total Infected So Far\" \"Alive\", \"Dead\"]\n",
    "    # #         self.time_step_statistics_df.columns = col_names\n",
    "    #     # CASE: Any other round\n",
    "    #     else:\n",
    "            # Compute summary statistics for currently infected, alive, and dead, and append them to time_step_snapshots_df\n",
    "        row = {\n",
    "            \"Time Step\": self.current_time_step,\n",
    "            \"Currently Infected\": self.current_infected_counter,\n",
    "            \"Total Infected So Far\": self.total_infected_counter,\n",
    "            \"Alive\": len(self.population) - self.dead_counter,\n",
    "            \"Dead\": self.dead_counter\n",
    "        }\n",
    "        self.time_step_statistics_df = self.time_step_statistics_df.append(row, ignore_index=True)\n",
    "\n",
    "        if write_to_file:\n",
    "            self.time_step_statistics_df.to_csv(\"simulation.csv\", mode='w+')\n",
    "\n",
    "    Simulation._log_time_step_statistics = _log_time_step_statistics\n",
    "\n",
    "#### run()\n",
    "\n",
    "- Start a for loop that runs self.total_time_steps number of times. \n",
    "- Display a message telling the user the time step that it is currently working on.\n",
    "- Call self._time_step()\n",
    "- Once the simuluation has finished, write the DataFrame containing the summary statistics from each step to a csv file.\n",
    "\n",
    "\n",
    "    def run(self):\n",
    "        \"\"\"\n",
    "        The main function of the simulation.  This will run the simulation starting at time step 0, calculating\n",
    "        and logging the results of each time step until the final time_step is reached. \n",
    "        \"\"\"\n",
    "\n",
    "        for _ in tqdm(range(self.total_time_steps)):\n",
    "            # Print out the current time step \n",
    "            print(\"Beginning Time Step {}\".format(self.current_time_step))\n",
    "            # Call our `_time_step()` function\n",
    "            self._time_step()\n",
    "\n",
    "        # Simulation is over--log results to a file by calling _log_time_step_statistics(write_to_file=True)\n",
    "        self._log_time_step_statistics(write_to_file=True)\n",
    "\n",
    "    #Add the run() function to our Simulation class.\n",
    "    Simulation.run = run\n",
    "\n",
    "#### Running Our Simulation\n",
    "\n",
    "create a simulation with the following parameters:\n",
    "\n",
    "Population size of 2000\n",
    "Disease name is Ebola\n",
    "r0 value of 2\n",
    "Mortality rate of 0.5\n",
    "20 time steps\n",
    "Vaccination Rate of 0.85\n",
    "50 initial infected\n",
    "\n",
    "    sim = Simulation(2000, \"Ebola\", 2, 0.5, 20, .85, 50)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "learn-env",
   "language": "python",
   "name": "learn-env"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.9"
  },
  "toc": {
   "base_numbering": 1,
   "nav_menu": {},
   "number_sections": true,
   "sideBar": true,
   "skip_h1_title": false,
   "title_cell": "Table of Contents",
   "title_sidebar": "Contents",
   "toc_cell": false,
   "toc_position": {},
   "toc_section_display": true,
   "toc_window_display": true
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}