Skip to content

Commit

Permalink
Update IModel.py, setup.py and documentation.
Browse files Browse the repository at this point in the history
  • Loading branch information
manuelbieri committed Oct 20, 2021
2 parents 875dd22 + e6dd586 commit e3b82f2
Show file tree
Hide file tree
Showing 8 changed files with 1,353 additions and 184 deletions.
62 changes: 43 additions & 19 deletions Shelegia_Motta_2021/IModel.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,25 @@ class IModel:
Interface for all models in Shelegia and Motta (2021).
"""
def __init__(self):
self.ENTRANT_CHOICES: Final[Dict[str, Dict[str, str]]] = {"product": {"complement": "C", "substitute": "S", "indifferent": "I"}, "development": {"success": "Y", "failure": "N"}}
self.INCUMBENT_CHOICES: Final[Dict[str, Dict[str, str]]] = {"copy_product": {"copy": "©", "refrain": "Ø"}}
self.ENTRANT_CHOICES: Final[Dict[str, str]] = {"complement": "C", "substitute": "S", "indifferent": "I"}
"""
Contains all the possible product choices of the entrant.
- complement (C)
- substitute (S)
- Indifferent (I)
"""
self.INCUMBENT_CHOICES: Final[Dict[str, str]] = {"copy": "©", "refrain": "Ø"}
"""
Contains all the possible answers of the incumbent to the choice of the entrant.
- copy (©)
- refrain (Ø)
"""
self.DEVELOPMENT_OUTCOME: Final[Dict[str, str]] = {"success": "Y", "failure": "N"}
"""
Contains all the possible outcomes of the development for the chosen product of the entrant or the merged entity.
- success (Y)
- failure (N)
"""

@abc.abstractmethod
def _calculate_copying_fixed_costs_values(self) -> Dict[str, float]:
Expand Down Expand Up @@ -116,26 +133,34 @@ def get_utility_values(self) -> Dict[str, Dict[str, float]]:
"""

@abc.abstractmethod
def plot_incumbent_best_answers(self, axis: matplotlib.axes.Axes = None) -> matplotlib.axes.Axes:
def get_optimal_choice(self, A: float, F: float) -> Dict[str, str]:
"""
Plots the best answers of the incumbent to all possible actions of the entrant.
Return the optimal choice of the entrant and the incumbent based on a pair of assets of the entrant and fixed costs for copying of the incumbent.
The output dictionary will contain the following details:
- "entrant": choice of the entrant (possible choices listed in Shelegia_Motta_2021.IModel.IModel.ENTRANT_CHOICES))
- "incumbent": choice of the incumbent (possible choices listed in Shelegia_Motta_2021.IModel.IModel.INCUMBENT_CHOICES)
- "development": outcome of the development (possible outcomes listed in Shelegia_Motta_2021.IModel.IModel.DEVELOPMENT_OUTCOME)
Parameters
----------
axis : matplotlib.axes.Axes
Axis to draw the plot on. (optional)
A : float
Assets of the entrant.
F : float
Fixed costs for copying of the incumbent.
Returns
-------
matplotlib.axes.Axes
Axis containing the plot.
Dict[str, str]
Optimal choice of the entrant, the incumbent and the outcome of the development.
"""
pass

@abc.abstractmethod
def plot_equilibrium(self, axis: matplotlib.axes.Axes = None) -> matplotlib.axes.Axes:
def plot_incumbent_best_answers(self, axis: matplotlib.axes.Axes = None) -> matplotlib.axes.Axes:
"""
Plots the equilibrium path based on the choices of the entrant and incumbent.
Plots the best answers of the incumbent to all possible actions of the entrant.
Parameters
----------
Expand All @@ -150,9 +175,9 @@ def plot_equilibrium(self, axis: matplotlib.axes.Axes = None) -> matplotlib.axes
pass

@abc.abstractmethod
def plot_utilities(self, axis: matplotlib.axes.Axes = None) -> matplotlib.axes.Axes:
def plot_equilibrium(self, axis: matplotlib.axes.Axes = None) -> matplotlib.axes.Axes:
"""
Plots the utilities for different market configurations.
Plots the equilibrium path based on the choices of the entrant and incumbent.
Parameters
----------
Expand All @@ -167,20 +192,19 @@ def plot_utilities(self, axis: matplotlib.axes.Axes = None) -> matplotlib.axes.A
pass

@abc.abstractmethod
def get_optimal_choice(self, A: float, F: float) -> Dict[str, str]:
def plot_utilities(self, axis: matplotlib.axes.Axes = None) -> matplotlib.axes.Axes:
"""
Return the optimal choice of the entrant and the incumbent based on a pair of assets of the entrant ann fixed costs for copying of the incumbent.
Plots the utilities for different market configurations.
Parameters
----------
A : float
Assets of the entrant.
F : float
Fixed costs for copying of the incumbent.
axis : matplotlib.axes.Axes
Axis to draw the plot on. (optional)
Returns
-------
Optimal choice of the entrant and the incumbent.
matplotlib.axes.Axes
Axis containing the plot.
"""
pass

Expand Down
48 changes: 48 additions & 0 deletions Shelegia_Motta_2021/ModelTest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import unittest
from typing import Dict

from Shelegia_Motta_2021.IModel import IModel
from Shelegia_Motta_2021.Models import BaseModel


class BaseModelTest(unittest.TestCase):
@staticmethod
def setUpModel() -> IModel:
return BaseModel()

def setUp(self) -> None:
self.model: IModel = self.setUpModel()
self.copying_fixed_costs: Dict[str, float] = self.model.get_copying_fixed_costs_values()
self.assets: Dict[str, float] = self.model.get_asset_values()
self.utility: Dict[str, Dict[str, float]] = self.model.get_utility_values()

def test_invalid_A1b(self):
self.assertRaises(AssertionError, BaseModel, small_delta=0.2)
self.assertRaises(AssertionError, BaseModel, delta=0.2)

def test_invalid_A2(self):
self.assertRaises(AssertionError, BaseModel, K=0.3)

def test_path_indifferent_copy(self):
choice: Dict[str, str] = self.model.get_optimal_choice(A=self.assets["A-s"]*0.9, F=self.copying_fixed_costs["F(YN)c"]*0.9)
self.assertEqual(choice["entrant"], self.model.ENTRANT_CHOICES["indifferent"])
self.assertEqual(choice["incumbent"], self.model.INCUMBENT_CHOICES["copy"])
self.assertEqual(choice["development"], self.model.DEVELOPMENT_OUTCOME["failure"])

def test_path_kill_zone(self):
choice: Dict[str, str] = self.model.get_optimal_choice(A=self.assets["A-s"]*0.9, F=self.copying_fixed_costs["F(YN)c"]*1.1)
self.assertEqual(choice["entrant"], self.model.ENTRANT_CHOICES["complement"])
self.assertEqual(choice["incumbent"], self.model.INCUMBENT_CHOICES["refrain"])
self.assertEqual(choice["development"], self.model.DEVELOPMENT_OUTCOME["success"])

def test_path_substitute_refrain(self):
choice: Dict[str, str] = self.model.get_optimal_choice(A=self.assets["A-s"]*1.1, F=self.copying_fixed_costs["F(YN)c"]*1.1)
self.assertEqual(choice["entrant"], self.model.ENTRANT_CHOICES["substitute"])
self.assertEqual(choice["incumbent"], self.model.INCUMBENT_CHOICES["refrain"])
self.assertEqual(choice["development"], self.model.DEVELOPMENT_OUTCOME["success"])

def test_path_substitute_copy(self):
choice: Dict[str, str] = self.model.get_optimal_choice(A=self.assets["A-s"]*1.1, F=self.copying_fixed_costs["F(YN)c"]*0.9)
self.assertEqual(choice["entrant"], self.model.ENTRANT_CHOICES["substitute"])
self.assertEqual(choice["incumbent"], self.model.INCUMBENT_CHOICES["copy"])
self.assertEqual(choice["development"], self.model.DEVELOPMENT_OUTCOME["success"])
62 changes: 44 additions & 18 deletions Shelegia_Motta_2021/Models.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,20 +137,20 @@ def get_utility_values(self) -> Dict[str, Dict[str, float]]:
def get_optimal_choice(self, A: float, F: float) -> Dict[str, str]:
result: Dict[str, str] = {"entrant": "", "incumbent": "", "development": ""}
if self._copying_fixed_costs["F(YN)c"] <= F <= self._copying_fixed_costs["F(YN)s"] and A < self._assets["A-s"]:
result.update({"entrant": self.ENTRANT_CHOICES["product"]["complement"]})
result.update({"incumbent": self.INCUMBENT_CHOICES["copy_product"]["refrain"]})
result.update({"development": self.ENTRANT_CHOICES["development"]["success"]})
result.update({"entrant": self.ENTRANT_CHOICES["complement"]})
result.update({"incumbent": self.INCUMBENT_CHOICES["refrain"]})
result.update({"development": self.DEVELOPMENT_OUTCOME["success"]})
elif F <= self._copying_fixed_costs["F(YN)c"] and A < self._assets["A-s"]:
result.update({"entrant": self.ENTRANT_CHOICES["product"]["indifferent"]})
result.update({"incumbent": self.INCUMBENT_CHOICES["copy_product"]["copy"]})
result.update({"development": self.ENTRANT_CHOICES["development"]["failure"]})
result.update({"entrant": self.ENTRANT_CHOICES["indifferent"]})
result.update({"incumbent": self.INCUMBENT_CHOICES["copy"]})
result.update({"development": self.DEVELOPMENT_OUTCOME["failure"]})
else:
result.update({"entrant": self.ENTRANT_CHOICES["product"]["substitute"]})
result.update({"development": self.ENTRANT_CHOICES["development"]["success"]})
result.update({"entrant": self.ENTRANT_CHOICES["substitute"]})
result.update({"development": self.DEVELOPMENT_OUTCOME["success"]})
if F <= self._copying_fixed_costs["F(YY)s"]:
result.update({"incumbent": self.INCUMBENT_CHOICES["copy_product"]["copy"]})
result.update({"incumbent": self.INCUMBENT_CHOICES["copy"]})
else:
result.update({"incumbent": self.INCUMBENT_CHOICES["copy_product"]["refrain"]})
result.update({"incumbent": self.INCUMBENT_CHOICES["refrain"]})
return result

def _plot(self, coordinates: List[List[Tuple[float, float]]], labels: List[str],
Expand All @@ -171,7 +171,7 @@ def _plot(self, coordinates: List[List[Tuple[float, float]]], labels: List[str],
"""
if axis is None:
fig, axis = plt.subplots()
self._draw_horizontal_lines(axis)
self._draw_thresholds(axis)

for i, coordinates in enumerate(coordinates):
poly = plt.Polygon(coordinates, ec="k", color=self._get_color(i), label=labels[i])
Expand All @@ -191,8 +191,8 @@ def plot_incumbent_best_answers(self, axis: matplotlib.axes.Axes = None) -> matp
def _create_choice_answer_label(self, entrant: Literal["complement", "substitute", "indifferent"],
incumbent: Literal["copy", "refrain"],
development: Literal["success", "failure"]) -> str:
return self.ENTRANT_CHOICES["product"][entrant] + " $\\rightarrow$ " + self.INCUMBENT_CHOICES["copy_product"][
incumbent] + " $\\rightarrow$ " + self.ENTRANT_CHOICES["development"][development]
return self.ENTRANT_CHOICES[entrant] + " $\\rightarrow$ " + self.INCUMBENT_CHOICES[
incumbent] + " $\\rightarrow$ " + self.DEVELOPMENT_OUTCOME[development]

def _get_incumbent_best_answer_labels(self) -> List[str]:
return [
Expand Down Expand Up @@ -297,7 +297,7 @@ def plot_utilities(self, axis: matplotlib.axes.Axes = None) -> matplotlib.axes.A
color='w',
hatch='///',
edgecolor=self._get_color(counter),
label=utility_type)
label=self._convert_utility_label(utility_type))
max_indices: List[int] = list(
filter(lambda x: utility_values[x] == max(utility_values), range(len(utility_values))))
for max_index in max_indices:
Expand All @@ -307,20 +307,46 @@ def plot_utilities(self, axis: matplotlib.axes.Axes = None) -> matplotlib.axes.A
axis.set_ylabel('Utility')
axis.set_title('Utility levels for different Market Configurations')
axis.set_xticks(index + 1.5 * (bar_width + spacing))
axis.set_xticklabels(tuple(self._utility.keys()))
axis.legend()
axis.set_xticklabels(tuple([self._convert_market_configuration_label(i) for i in self._utility.keys()]))
axis.legend(bbox_to_anchor=(0, -0.3), loc="lower left", ncol=4)
axis.text(max(index) + 2 + 1.5 * (bar_width + spacing), self._utility["E(P)"]["W"]*0.5, self._get_market_configuration_annotations())

BaseModel._set_axis(axis)
# BaseModel._set_axis(axis)
plt.show()
return axis

@staticmethod
def _get_market_configuration_annotations() -> str:
return "$I_P$: Primary product sold by the incumbent\n" \
"$I_C$: Complementary product to $I_P$ potentially sold by the incumbent, which is copied from $E_C$\n" \
"$E_P$: Perfect substitute to $I_P$ potentially sold by the entrant\n" \
"$E_C$: Complementary product to $I_P$ currently sold by the entrant\n" \
"$\\tilde{E}_C$: Complementary product to $I_P$ potentially sold by the entrant\n"

@staticmethod
def _convert_utility_label(raw_label: str) -> str:
label: str = raw_label.replace("pi", "$\pi$")
label = label.replace("CS", "Consumer Surplus")
label = label.replace("W", "Welfare")
return label

@staticmethod
def _convert_market_configuration_label(raw_label: str) -> str:
labels: Dict[str] = {"basic": "$I_P;E_C$",
"I(C)": "$I_P+I_C;E_C$",
"E(P)": "$I_P;E_C+E_P$",
"I(C)E(P)": "$I_P+I_C;E_C+E_P$",
"E(C)": "$I_P;E_C+\\tilde{E}_C$",
"I(C)E(C)": "$I_P+I_C;E_C+\\tilde{E}_C$"}
return labels.get(raw_label, 'No valid market configuration')

def _get_x_max(self) -> float:
return round(self._assets['A-c'] * 1.3, 1)

def _get_y_max(self) -> float:
return round(self._copying_fixed_costs['F(YN)s'] * 1.3, 1)

def _draw_horizontal_lines(self, axis: matplotlib.axes.Axes) -> None:
def _draw_thresholds(self, axis: matplotlib.axes.Axes) -> None:
horizontal_line_x: float = self._get_x_max() + 0.05
vertical_line_y: float = self._get_y_max() + 0.15
axis.axhline(self._copying_fixed_costs['F(YN)s'], linestyle='--', color='k')
Expand Down
Loading

0 comments on commit e3b82f2

Please sign in to comment.