From d34fc788ab182ad3a68dd4f4beb0ba8098670b91 Mon Sep 17 00:00:00 2001
From: Jon <vitale.jonathan@ymail.com>
Date: Mon, 24 Jul 2023 19:26:13 +0200
Subject: [PATCH] Add material and solution for workshop week 6

---
 week6/material/burglar_alarm.py | 102 +++++++++++
 week6/material/rain_umbrella.py |  34 ++++
 week6/solution/exercise1-2.py   | 306 ++++++++++++++++++++++++++++++++
 week6/solution/exercise3.py     |  92 ++++++++++
 week6/solution/exercise4.py     |  64 +++++++
 week6/solution/exercise5.py     |  59 ++++++
 6 files changed, 657 insertions(+)
 create mode 100644 week6/material/burglar_alarm.py
 create mode 100644 week6/material/rain_umbrella.py
 create mode 100644 week6/solution/exercise1-2.py
 create mode 100644 week6/solution/exercise3.py
 create mode 100644 week6/solution/exercise4.py
 create mode 100644 week6/solution/exercise5.py

diff --git a/week6/material/burglar_alarm.py b/week6/material/burglar_alarm.py
new file mode 100644
index 0000000..e5a5b3e
--- /dev/null
+++ b/week6/material/burglar_alarm.py
@@ -0,0 +1,102 @@
+from pgmpy.models import BayesianNetwork
+from pgmpy.factors.discrete.CPD import TabularCPD
+
+# Let's create the Bayesian network model
+burglar_alarm_bn = BayesianNetwork()
+
+# Adding the nodes to the network
+burglar_alarm_bn.add_nodes_from(['Burglary', 'Earthquake', 'Alarm', 'JohnCalls', 'MaryCalls'])
+
+# Adding dependencies
+burglar_alarm_bn.add_edge('Burglary', 'Alarm')
+burglar_alarm_bn.add_edge('Earthquake', 'Alarm')
+burglar_alarm_bn.add_edge('Alarm', 'JohnCalls')
+burglar_alarm_bn.add_edge('Alarm', 'MaryCalls')
+
+# Conditional probability tables
+
+burglary_cpd = TabularCPD(
+    variable='Burglary',
+    variable_card=2,
+    values=[[0.001], [0.999]]
+)
+
+eq_cpd = TabularCPD(
+    variable='Earthquake',
+    variable_card=2,
+    values=[[0.002], [0.998]]
+)
+
+alarm_cpd = TabularCPD(
+    variable='Alarm',
+    variable_card=2,
+    evidence=['Burglary', 'Earthquake'],
+    evidence_card=[2, 2],
+    values=[
+        # B=true^E=true, B=true^E=false, B=false^E=true, B=false^E=false
+        [0.95, 0.94, 0.29, 0.001], # Alarm true
+        [0.05, 0.06, 0.71, 0.999] # Alarm false
+    ]
+)
+
+jc_cpd = TabularCPD(
+    variable='JohnCalls',
+    variable_card=2,
+    evidence=['Alarm'],
+    evidence_card=[2],
+    values=[
+        # Alarm=true, Alarm=false
+        [0.9, 0.05], # JC true
+        [0.1, 0.95] # JC false
+    ]
+)
+
+mc_cpd = TabularCPD(
+    variable='MaryCalls',
+    variable_card=2,
+    evidence=['Alarm'],
+    evidence_card=[2],
+    values=[
+        # Alarm=true, Alarm=false
+        [0.7, 0.01], # MC true
+        [0.3, 0.99] # MC false
+    ]
+)
+
+burglar_alarm_bn.add_cpds(burglary_cpd, eq_cpd, alarm_cpd, jc_cpd, mc_cpd)
+
+# checking that everything is ok
+try:
+    model_check = burglar_alarm_bn.check_model()
+except Exception as e:
+    model_check = e
+
+if model_check == True:
+    print("The Bayesian network model does not have any error")
+else:
+    print("Warning! There is an error in the Bayesian network model: {0}".format(e))
+
+# Probabilistic inference
+from pgmpy.inference import VariableElimination
+
+inference = VariableElimination(burglar_alarm_bn)
+
+# Probability of Alarm being on or off
+query = inference.query(variables=['Alarm'])
+print("What is the probability for the alarm to become activated?")
+print(query)
+
+# Probability of Alarm being on or off given JohnCalls=true
+query = inference.query(variables=['Alarm'], evidence={'JohnCalls': 0})
+print("What is the probability for the alarm to become activated given that John called?")
+print(query)
+
+# Probability of Burglary and Alarm given JohnCalls=true
+query = inference.query(variables=['Burglary','Alarm'], evidence={'JohnCalls': 0})
+print("What is the probability of a burglar in the apartment and the alarm to be active given that John called?")
+print(query)
+
+# MAP query
+query = inference.map_query(variables=['JohnCalls', 'MaryCalls'], evidence={'Burglary': 0})
+print("Who would more likely call us when a burglar is in our house?")
+print(query)
\ No newline at end of file
diff --git a/week6/material/rain_umbrella.py b/week6/material/rain_umbrella.py
new file mode 100644
index 0000000..3823ddf
--- /dev/null
+++ b/week6/material/rain_umbrella.py
@@ -0,0 +1,34 @@
+from pomegranate.distributions import Categorical
+from pomegranate.hmm import DenseHMM
+import numpy as np
+
+model = DenseHMM()
+
+states = ['rain', 'not_rain']
+
+# emission probability distributions for the two hidden states
+rain = Categorical([[0.9, 0.1]]) # 90% chances of seeing the umbrella if it's raining
+not_rain = Categorical([[0.2, 0.8]]) # 20% chances of seeing the umbrella if it's not raining
+model.add_distributions([rain, not_rain])
+
+# Initial probabilities for state X0
+model.add_edge(model.start, rain, 0.5)
+model.add_edge(model.start, not_rain, 0.5)
+
+# Transition probabilities
+model.add_edge(rain, rain, 0.7)
+model.add_edge(rain, not_rain, 0.3)
+model.add_edge(not_rain, rain, 0.3)
+model.add_edge(not_rain, not_rain, 0.7)
+
+# Using the following observations
+observations = ['umbrella', 'umbrella', 'not_umbrella', 'umbrella', 'not_umbrella', 'not_umbrella', 'not_umbrella']
+X = np.array([[[['umbrella', 'not_umbrella'].index(label)] for label in observations]])
+
+# Making a prediction of the most likely sequence of states that generated the observations
+y_hat = model.predict(X)
+predictions = [states[y] for y in y_hat[0].tolist()]
+
+# Results
+print("Observations: {}".format(observations))
+print("hmm pred: {}".format(predictions))
\ No newline at end of file
diff --git a/week6/solution/exercise1-2.py b/week6/solution/exercise1-2.py
new file mode 100644
index 0000000..bff4f79
--- /dev/null
+++ b/week6/solution/exercise1-2.py
@@ -0,0 +1,306 @@
+"""
+Exercise 1:
+
+From the scenario we can identify the following RVs:
+
+SpottingUnicorn, PureHeartWizard, SpottingTarantula, TimeOfDay, BraveWizard, WizardHouse
+
+Now we can specify the ranges for these RVs:
+
+UnicornSighting: {true, false}
+PureHeart: {true, false}
+AcromantulaSighting: {true, false}
+TimeOfDay: {day, night}
+House: {hufflepuff, gryffindor, ravenclaw, slytherin}
+Brave: {true, false}
+
+Dependencies:
+
+UnicornSighting depends on PureHeart
+PureHeart depends on House
+AcromantulaSighting depends on TimeOfDay and Brave
+Brave depends on House
+
+We can specify the following probability distributions given by the scenario:
+"""
+
+pd = {}
+
+pd['UnicornSighting=true|PureHeart=false'] = 1/400
+pd['UnicornSighting=false|PureHeart=false'] = 1 - pd['UnicornSighting=true|PureHeart=false']
+pd['UnicornSighting=true|PureHeart=true'] = 0.1
+pd['UnicornSighting=false|PureHeart=true'] = 1 - pd['UnicornSighting=true|PureHeart=true']
+pd['TimeOfDay=day'] = 0.5 # assuming uniform distribution
+pd['TimeOfDay=night'] = 0.5 # assuming uniform distribution
+
+probs = [0.01, 0.025, 0.05, 0.1]
+i = 0
+for t in ['day', 'night']:
+    for brave in ['true', 'false']:
+        pd['AcromantulaSighting=true|TimeOfDay={0},Brave={1}'.format(t, brave)] = probs[i]
+        pd['AcromantulaSighting=false|TimeOfDay={0},Brave={1}'.format(t, brave)] = 1 - probs[i]
+        i += 1
+
+houses = ['gryffindor', 'ravenclaw', 'slytherin', 'hufflepuff']
+for i, prob in enumerate([0.002, 0.002, 0.0001, 0.01]):
+    pd['PureHeart=true|House={0}'.format(houses[i])] = prob
+    pd['PureHeart=false|House={0}'.format(houses[i])] = 1 - prob
+for i, prob in enumerate([0.05, 0.02, 0.02, 0.001]):
+    pd['Brave=true|House={0}'.format(houses[i])] = prob
+    pd['Brave=false|House={0}'.format(houses[i])] = 1 - prob
+for house in houses:
+    pd['House={0}'.format(house)] = 0.25
+
+print("\n>>>Given probability distributions:")
+for key, value in pd.items():
+    print('P({0}) = {1}'.format(key, value))
+
+"""
+Exercise 2: Inference
+
+Now we can proceed to infer new information from the one available.
+"""
+
+print('\n>>>Inferred probability distributions:')
+
+"""
+1. Computing P(PureHeart):
+
+We can find thid distribution by marginalising the joint distribution P(PureHeart, House):
+
+P(PureHeart) = sum_h P(PureHeart, House)
+= sum_h P(PureHeart | House)P(House)
+
+"""
+for heart in ['true', 'false']:
+    cur_key = 'PureHeart={0}'.format(heart)
+    pd[cur_key] = 0
+    for house in houses:
+        pd[cur_key] += pd['PureHeart={0}|House={1}'.format(heart, house)]*pd['House={0}'.format(house)]
+    
+    print('P({0}) = {1}'.format(cur_key, pd[cur_key]))
+
+"""
+2. Computing P(UnicornSighting):
+
+We can estimate the prior by marginalising the joint distribution P(UnicornSighting, PureHeart):
+
+P(UnicornSighting) = sum_h P(UnicornSighting, PureHeart = h)
+= sum_h P(UnicornSighting | PureHeart = h)P(Heart = h)
+"""
+for unicorn in ['true', 'false']:
+    cur_key = 'UnicornSighting={0}'.format(unicorn)
+    pd[cur_key] = 0
+    for heart in ['true', 'false']:
+        pd[cur_key] += pd['UnicornSighting={0}|PureHeart={1}'.format(unicorn, heart)]*pd['PureHeart={0}'.format(heart)]
+    print('P({0}) = {1}'.format(cur_key, pd[cur_key]))
+"""
+
+3. Computing P(Brave):
+
+We can marginalise this variable from the joint distribution
+
+P(Brave, House) = P(Brave | House)P(House)
+
+so
+
+P(Brave) = sum_h P(Brave | House = h)P(House = h)
+"""
+
+pd['Brave=true'] = 0
+pd['Brave=false'] = 0
+
+for house in houses:
+    for brave in ['true', 'false']:
+        pd['Brave={0}'.format(brave)] += pd['Brave={0}|House={1}'.format(brave, house)]*pd['House={0}'.format(house)]
+
+print("P(Brave=true) = {0}".format(pd['Brave=true']))
+print("P(Brave=false) = {0}".format(pd['Brave=false']))
+
+"""
+4. Computing P(House | Brave):
+
+Given the Bayes' rule,
+
+P(House | Brave) = P(Brave | House)P(House) / P(Brave)
+
+Therefore
+"""
+
+for house in houses:
+    for brave in ['true', 'false']:
+        cur_key = 'House={0}|Brave={1}'.format(house, brave)
+        pd[cur_key] = (pd['Brave={0}|House={1}'.format(brave,house)]*pd['House={0}'.format(house)])/pd['Brave={0}'.format(brave)]
+        print('P({0}) = {1}'.format(cur_key, pd[cur_key]))
+
+"""
+5. Computing P(AcromantulaSighting):
+
+We can marginalise AcromantulaSighting from the joint distribution P(AcromantulaSighting, TimeOfDay, Brave)
+
+P(A) = sum_t sum_b P(A, T=t, B=b) = sum_t sum_b P(A | T=t, B=b)P(T=t | B=b)P(B=b)
+
+TimeOfDay is independent from Brave because Brave is a non-descentent of TimeOfDay and
+TimeOfDay does not have parents, therefore we can simplify the formula:
+
+P(A) = sum_t sum_b P(A | T=t, B=b)P(T=t)P(B=b)
+
+"""
+for acromantula in ['true', 'false']:
+    cur_key = 'AcromantulaSighting={0}'.format(acromantula)
+    pd[cur_key] = 0
+    for t in ['day', 'night']:
+        for brave in ['true', 'false']:
+            pd[cur_key] += pd['AcromantulaSighting={0}|TimeOfDay={1},Brave={2}'.format(acromantula, t, brave)]*pd['TimeOfDay={0}'.format(t)]*pd["Brave={0}".format(brave)]
+    print('P({0}) = {1}'.format(cur_key, pd[cur_key]))
+
+"""
+6. Computing P(AcromantulaSighting | TimeOfDay):
+
+As per the Bayes' rule
+
+P(AcromantulaSighiting | TimeOfDay) = P(AcromantulaSighting, TimeOfDay) / P(TimeOfDay)
+
+We can compute P(AcromantulaSighting, TimeOfDay) by marginalising the joint distribution
+
+P(AcromantulaSighting, TimeOfDay, Brave) = P(AcromantulaSighting | TimeOfDay, Brave)P(TimeOfDay)P(Brave)
+
+P(AcromantulaSighting, TimeOfDay) = sum_b P(AcromantulaSighting | TimeOfDay, Brave=b)P(TimeOfDay)P(Brave=b)
+"""
+
+for acromantula in ['true', 'false']:
+    for t in ['day', 'night']:
+        cur_key = 'AcromantulaSighting={0},TimeOfDay={1}'.format(acromantula, t)
+        pd[cur_key] = 0
+        for brave in ['true', 'false']:
+            pd[cur_key] += pd['AcromantulaSighting={0}|TimeOfDay={1},Brave={2}'.format(acromantula, t, brave)]*pd['TimeOfDay={0}'.format(t)]*pd['Brave={0}'.format(brave)]
+        print('P({0}) = {1}'.format(cur_key, pd[cur_key]))
+"""
+Now we can plug this in and compute the posteriori
+"""
+for acromantula in ['true', 'false']:
+    for t in ['day', 'night']:
+        cur_key = 'AcromantulaSighting={0}|TimeOfDay={1}'.format(acromantula, t)
+        pd[cur_key] = pd['AcromantulaSighting={0},TimeOfDay={1}'.format(acromantula, t)]/pd['TimeOfDay={0}'.format(t)]
+        print('P({0}) = {1}'.format(cur_key, pd[cur_key]))
+"""
+7. Computing P(AcromantulaSighting | Brave):
+
+As per the Bayes' rule
+
+P(AcromantulaSighting | Brave) = P(AcromantulaSighting, Brave) / P(Brave)
+
+We can compute P(AcromantulaSighting, Brave) by marginalising P(AcromantulaSighting, TimeOfDay, Brave)
+
+P(AcromantulaSighting, Brave) = sum_t P(AcromantulaSighting, TimeOfDay=t, Brave)
+P(AcromantulaSighting, Brave) = sum_t P(AcromantulaSighting | TimeOfDay=t, Brave)P(TimeOfDay = t)P(Brave)
+"""
+for acromantula in ['true', 'false']:
+    for brave in ['true', 'false']:
+        cur_key = 'AcromantulaSighting={0},Brave={1}'.format(acromantula, brave)
+        pd[cur_key] = 0
+        for t in ['day', 'night']:
+            pd[cur_key] += pd['AcromantulaSighting={0}|TimeOfDay={1},Brave={2}'.format(acromantula, t, brave)]*pd['TimeOfDay={0}'.format(t)]*pd['Brave={0}'.format(brave)]
+        print('P({0}) = {1}'.format(cur_key, pd[cur_key]))
+"""
+Now we can plug it in the bayes rule
+"""
+for acromantula in ['true', 'false']:
+    for brave in ['true', 'false']:
+        cur_key = 'AcromantulaSighting={0}|Brave={1}'.format(acromantula, brave)
+        pd[cur_key] = pd['AcromantulaSighting={0},Brave={1}'.format(acromantula, brave)]/pd['Brave={0}'.format(brave)]
+        print('P({0}) = {1}'.format(cur_key, pd[cur_key]))
+
+"""
+8. Computing P(AcromantulaSighting | House):
+
+P(A | H) = P(A, H) / P(H)
+
+P(A, H) = sum_b P(A, B=b, H) = sum_b P(A | B=b, H)P(B=b| H)P(H)
+
+In this scenario, A is conditionally independent from H given B, in fact, if we know that a student is brave, we don't
+need to know their house to infer the likelihood of spotting an acromantula. Therefore:
+
+P(A, H) = P(H) sum_b P(A | B=b)P(B=b | H)
+
+By putting all together
+
+P(A | H) = [P(H) sum_b P(A | B=b)P(B=b | H)] / P(H) = sum_b P(A | B=b)P(B=b | H)
+"""
+for acromantula in ['true', 'false']:
+    for house in houses:
+        cur_key = 'AcromantulaSighting={0}|House={1}'.format(acromantula, house)
+        if cur_key not in pd:
+            pd[cur_key] = 0
+        for brave in ['true', 'false']:
+            pd[cur_key] += pd['AcromantulaSighting={0}|Brave={1}'.format(acromantula, brave)]*pd['Brave={0}|House={1}'.format(brave,house)]
+        print('P({0}) = {1}'.format(cur_key, pd[cur_key]))
+
+"""
+9. Computing P(AcromantulaSighting | TimeOfDay, House):
+
+We can use the Bayes' rule:
+
+P(A | T, H) = P(T, H, A) / P(T, H)
+
+We know that TimeOfDay is independent from House so
+
+P(A | T, H) = P(T, H, A) / (P(T)P(H))
+
+We can decompose the joint distribution P(T, H, A) with the chain rule
+
+P(T, H, A) = P(T | H, A)P(H | A)P(A)
+
+We know that TimeOfDay is independent from House, so we obtain
+
+P(T, H, A) = P(T | A)P(H | A)P(A)
+
+We need to compute P(T | A) and P(H | A). We can compute them
+by using the Bayes' rule:
+
+P(T | A) = P(A, T) / P(A)
+
+P(H | A) = P(A | H)P(H) / P(A)
+"""
+for t in ['day', 'night']:
+    for acromantula in ['true', 'false']:
+        cur_key = 'TimeOfDay={0}|AcromantulaSighting={1}'.format(t, acromantula)
+        pd[cur_key] = pd['AcromantulaSighting={0},TimeOfDay={1}'.format(acromantula, t)]/pd['AcromantulaSighting={0}'.format(acromantula)]
+        print('P({0}) = {1}'.format(cur_key, pd[cur_key]))
+
+for house in houses:
+    for acromantula in ['true', 'false']:
+        cur_key = 'House={0}|AcromantulaSighting={1}'.format(house, acromantula)
+        pd[cur_key] = (pd['AcromantulaSighting={0}|House={1}'.format(acromantula, house)]*pd['House={0}'.format(house)])/pd['AcromantulaSighting={0}'.format(acromantula)]
+        print('P({0}) = {1}'.format(cur_key, pd[cur_key]))
+
+"""
+
+And now we can finally compute P(A | T, H) by putting all together
+
+P(A | T, H) = (P(T | A)P(H | A)P(A)) / (P(T)P(H))
+"""
+for acromantula in ['true', 'false']:
+    for t in ['day', 'night']:
+        for house in houses:
+            cur_key = 'AcromantulaSighting={0}|TimeOfDay={1},House={2}'.format(acromantula, t, house)
+            pd[cur_key] = (pd['TimeOfDay={0}|AcromantulaSighting={1}'.format(t, acromantula)]*pd['House={0}|AcromantulaSighting={1}'.format(house, acromantula)]*pd['AcromantulaSighting={0}'.format(acromantula)]) / \
+            (pd['TimeOfDay={0}'.format(t)]*pd['House={0}'.format(house)])
+            print('P({0}) = {1}'.format(cur_key, pd[cur_key]))
+
+"""
+10. Harry Potter is from Gryffindor. We already have P(A | T, H) so we don't need to compute it again
+
+"""
+print("\n>>>Answers:")
+
+print("1. What are the chances of a Hogwarts' student to posses a pure heart?", pd['PureHeart=true'])
+print("2. What is the likelihood of spotting a unicorn in the Forbidden Forest?", pd['UnicornSighting=true'])
+print("3. What is the likelihood for a Hogwarts' student to be brave?", pd['Brave=true'])
+print("4. What are the chances for a brave student to come from the Gryffindor house?", pd['House=gryffindor|Brave=true'])
+print("5. What are the chances of seeing an acromantula in the Forbidden Forest?", pd['AcromantulaSighting=true'])
+print("6. How likely is it to spot an acromantula at night?", pd['AcromantulaSighting=true|TimeOfDay=night'])
+print("7. What are the chances for a brave student to encounter an acromantula?", pd['AcromantulaSighting=true|Brave=true'])
+print("8. What is the likelihood of encountering an acromantula for a Ravenclaw student?", pd['AcromantulaSighting=true|House=ravenclaw'])
+print("9. To what extent the likelihood of encountering an acromantula for a Ravenclaw student increases at night?", "It increases of " + str(pd['AcromantulaSighting=true|TimeOfDay=night,House=ravenclaw']/pd['AcromantulaSighting=true|House=ravenclaw']) + " times")
+print("10. What is the likelihood for Harry Potter to encounter an acromantula when sneaking out from his dormroom during the night?", pd['AcromantulaSighting=true|TimeOfDay=night,House=gryffindor'])
\ No newline at end of file
diff --git a/week6/solution/exercise3.py b/week6/solution/exercise3.py
new file mode 100644
index 0000000..a1a0ec8
--- /dev/null
+++ b/week6/solution/exercise3.py
@@ -0,0 +1,92 @@
+from pgmpy.models import BayesianNetwork
+from pgmpy.factors.discrete.CPD import TabularCPD
+
+# Creating a Bayesian network and its nodes
+hogwarts_bn = BayesianNetwork()
+hogwarts_bn.add_nodes_from(
+    ['UnicornSighting', 'PureHeart', 'AcromantulaSighting',
+      'TimeOfDay', 'House', 'Brave']
+)
+
+# Adding dependencies
+hogwarts_bn.add_edge('House', 'PureHeart')
+hogwarts_bn.add_edge('House', 'Brave')
+hogwarts_bn.add_edge('House', 'PureHeart')
+hogwarts_bn.add_edge('PureHeart', 'UnicornSighting')
+hogwarts_bn.add_edge('Brave', 'AcromantulaSighting')
+hogwarts_bn.add_edge('TimeOfDay', 'AcromantulaSighting')
+
+# Adding CPD tables to nodes
+time_cpd = TabularCPD(
+    variable='TimeOfDay',
+    variable_card=2,
+    values=[[0.5], [0.5]]
+)
+hogwarts_bn.add_cpds(time_cpd)
+
+house_cpd = TabularCPD(
+    variable='House',
+    variable_card=4,
+    values=[[0.25], [0.25], [0.25], [0.25]]
+)
+hogwarts_bn.add_cpds(house_cpd)
+
+unicorn_cpd = TabularCPD(
+    variable='UnicornSighting',
+    variable_card=2,
+    evidence=['PureHeart'],
+    evidence_card=[2],
+    values=[
+        [0.1, 1/400], # UnicornSighting True
+        [1 - 0.1, 1 - (1/400)] # UnicornSighting False
+    ]
+)
+hogwarts_bn.add_cpds(unicorn_cpd)
+
+heart_cpd = TabularCPD(
+    variable='PureHeart',
+    variable_card=2,
+    evidence=['House'],
+    evidence_card=[4],
+    values=[
+        [0.002, 0.002, 0.0001, 0.01], # PureHeart true
+        [1 - 0.002, 1 - 0.002, 1 - 0.0001, 1 - 0.01] # PureHeart false
+    ]
+)
+hogwarts_bn.add_cpds(heart_cpd)
+
+brave_cpd = TabularCPD(
+    variable='Brave',
+    variable_card=2,
+    evidence=['House'],
+    evidence_card=[4],
+    values=[
+        [0.05, 0.02, 0.02, 0.001], # Brave true
+        [1 - 0.05, 1 - 0.02, 1 - 0.02, 1 - 0.001] # Brave false
+    ]
+)
+hogwarts_bn.add_cpds(brave_cpd)
+
+acromantula_cpd = TabularCPD(
+    variable='AcromantulaSighting',
+    variable_card=2,
+    evidence=['TimeOfDay', 'Brave'],
+    evidence_card=[2, 2],
+    values=[
+        [0.01, 0.025, 0.05, 0.1], # AcromantulaSighting true
+        [1 - 0.01, 1 - 0.025, 1 - 0.05, 1 - 0.1] # AcromantulaSighting false
+    ]
+)
+hogwarts_bn.add_cpds(acromantula_cpd)
+
+# checking that everything is ok
+try:
+    model_check = hogwarts_bn.check_model()
+except Exception as e:
+    model_check = e
+
+if __name__ == "__main__":
+    if model_check == True:
+        print("The Bayesian network model does not have any error")
+    else:
+        print("Warning! There is an error in the Bayesian network model: {0}".format(e))
\ No newline at end of file
diff --git a/week6/solution/exercise4.py b/week6/solution/exercise4.py
new file mode 100644
index 0000000..1c1aec9
--- /dev/null
+++ b/week6/solution/exercise4.py
@@ -0,0 +1,64 @@
+from pgmpy.inference import VariableElimination
+from exercise3 import hogwarts_bn
+
+inference = VariableElimination(hogwarts_bn)
+
+# Question 1
+query = inference.query(variables=['PureHeart'])
+print("1. What are the chances of a Hogwarts' student to posses a pure heart?")
+print(query)
+
+# Question 2
+query = inference.query(variables=['UnicornSighting'])
+print("2. What is the likelihood of spotting a unicorn in the Forbidden Forest?")
+print(query)
+
+# Question 3
+query = inference.query(variables=['Brave'])
+print("3. What is the likelihood for a Hogwarts' student to be brave?")
+print(query)
+
+# Question 4
+query = inference.query(variables=['House'], evidence={'Brave': 0})
+print("4. What are the chances that a brave student comes from the Gryffindor house?")
+print(query)
+
+# Question 5
+query = inference.query(variables=['AcromantulaSighting'])
+print("5. What are the chances of seeing an acromantula in the Forbidden Forest?")
+print(query)
+
+# Question 6
+query = inference.query(variables=['AcromantulaSighting'], evidence={'TimeOfDay': 1})
+print("6. How likely is it to spot an acromantula at night?")
+print(query)
+
+# Question 7
+query = inference.query(variables=['AcromantulaSighting'], evidence={'Brave': 0})
+print("7. What are the chances for a brave student to encounter an acromantula?")
+print(query)
+
+# Question 8
+query = inference.query(variables=['AcromantulaSighting'], evidence={'House': 1})
+print("8. What is the likelihood of encountering an acromantula for a Ravenclaw student?")
+print(query)
+
+# Question 9
+temp = inference.query(variables=['AcromantulaSighting'], evidence={'TimeOfDay':1, 'House': 1})
+print("9. To what extent the likelihood of encountering an acromantula for a Ravenclaw student increases at night?")
+print("It increases of ", temp.values[0]/query.values[0], " times")
+
+# Question 10
+query = inference.query(variables=['AcromantulaSighting'], evidence={'TimeOfDay':1, 'House': 0})
+print("10. What is the likelihood for Harry Potter to encounter an acromantula when sneaking out from his dormroom during the night?")
+print(query)
+
+# Question 11
+query = inference.map_query(variables=['AcromantulaSighting', 'UnicornSighting'], evidence={'TimeOfDay': 1, 'House': 2})
+print("11. Consider Draco Malfoy sneaking out to go Forbidden Forest at night. Which scenario most likely happen? a. He will spot an acromantula and a unicorn, b. He will spot an acromantula only, c. He will spot a unicorn only, d. He will not spot any creature.")
+print(query)
+
+# Question 12
+query = inference.map_query(variables=['House'], evidence={'Brave': 0, 'PureHeart': 0})
+print("12. To which house a brave and pure hearted student is more likely to be allocated by the Sorting Hat?")
+print(query)
diff --git a/week6/solution/exercise5.py b/week6/solution/exercise5.py
new file mode 100644
index 0000000..0c3038d
--- /dev/null
+++ b/week6/solution/exercise5.py
@@ -0,0 +1,59 @@
+from pomegranate.distributions import Categorical
+from pomegranate.hmm import DenseHMM
+import numpy as np
+
+from pgmpy.inference import VariableElimination
+from exercise3 import hogwarts_bn
+
+inference = VariableElimination(hogwarts_bn)
+
+hogwarts_hmm = DenseHMM()
+
+states = ['gryffindor', 'ravenclaw', 'slytherin', 'hufflepuff']
+
+# Possible observations: 
+# 0. AcromantulaSighting=true, UnicornSighting=true (AU)
+# 1. AcromantulaSighting=true, UnicornSighting=false (AX)
+# 2. AcromantulaSighting=false, UnicornSighting=true (XU)
+# 4. AcromantulaSighting=false, UnicornSighting=false (XX)
+
+# emission probability distributions for the two hidden states
+state_dists = [None, None, None, None]
+
+for i, house in enumerate(states):
+    query = inference.query(variables=['AcromantulaSighting','UnicornSighting'], evidence={'House': i, 'TimeOfDay': 1} )
+    val = query.values
+    probabilities = [val[0,0], val[0,1], val[1,0], val[1,1]]
+    print(np.array(probabilities).sum())
+    cur_state_dist = Categorical([probabilities])
+    state_dists[i] = cur_state_dist
+
+hogwarts_hmm.add_distributions(state_dists)
+
+# Transition probabilities
+# 25% uniform chances for a student to be allocated to a house
+
+for i in range(4):
+    hogwarts_hmm.add_edge(hogwarts_hmm.start, state_dists[i], 0.25)
+
+# and then it is impossible to change house, so we keep the chances very low
+for i in range(4):
+    for j in range(4):
+        if i == j:
+            prob = 0.997
+        else:
+            prob = 0.001
+        hogwarts_hmm.add_edge(state_dists[i],state_dists[j], prob)
+
+
+# Using the following observations
+observations = ['00', '00', '00', 'X0', '00', '00', 'X0', '00', '0X', '00']
+X = np.array([[[['XX', 'X0', '0X', '00'].index(label)] for label in observations]])
+
+# Making a prediction of the most likely sequence of states that generated the observations
+y_hat = hogwarts_hmm.predict(X)
+predictions = [states[y] for y in y_hat[0].tolist()]
+
+# Results
+print("Observations: {}".format(observations))
+print("hmm pred: {}".format(predictions))
\ No newline at end of file
-- 
GitLab