From 42905e6d75535fb356db160979874d6953967cb2 Mon Sep 17 00:00:00 2001 From: Mohammed Ismail Date: Wed, 11 Sep 2024 14:30:37 +0530 Subject: [PATCH 1/2] added charting for inequality metrics --- policyengine/charts/__init__.py | 0 policyengine/charts/inequality.py | 309 ++++++++++++++++++ .../economic_impact/economic_impact.py | 63 +++- 3 files changed, 370 insertions(+), 2 deletions(-) create mode 100644 policyengine/charts/__init__.py create mode 100644 policyengine/charts/inequality.py diff --git a/policyengine/charts/__init__.py b/policyengine/charts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/policyengine/charts/inequality.py b/policyengine/charts/inequality.py new file mode 100644 index 0000000..62bb04c --- /dev/null +++ b/policyengine/charts/inequality.py @@ -0,0 +1,309 @@ +import plotly.graph_objects as go +from policyengine_core.charts.formatting import * + +class GiniImpactChart(): + def __init__(self, data=None) -> None: + if data is None: + raise ValueError("Data must be provided") + self.baseline = data.get('baseline') + self.reform = data.get('reform') + self.change = data.get('change') + self.change_percentage = data.get('change_percentage') + + def generate_chart_data(self): + # Generate hover text based on the values + hover_texts = [ + f"The reform would increase the Gini index of net income by {self.change_percentage}% from {self.baseline} to {self.reform}%" + if self.change_percentage > 0 + else f"The reform would decrease the Gini index of net income by {self.change_percentage}% from {self.baseline} to {self.reform}%" + + ] + + fig = go.Figure() + + # Add bar trace with adjustable width + fig.add_trace(go.Bar( + x=["Gini Index"], # Label for the x-axis + y=[self.change_percentage], + marker=dict( + color=[BLUE if self.change_percentage > 0 else GRAY ], # Conditional color + line=dict(width=1), + ), + width=0.3, # Adjust this value between 0 and 1 to change the bar width + text=[f"{self.change_percentage}%"], # Display values as text + textposition='outside', # Position text outside the bars + hovertemplate=f"%{{x}}

%{{customdata}}", + customdata=hover_texts + )) + + # Update the y-axis to show percentages with one decimal place + fig.update_layout( + yaxis=dict( + tickformat=".1f", # Show y-values with one decimal place + ticksuffix="%", + title="Relative change" # Add percentage symbol + ), + + hoverlabel=dict( + bgcolor="white", # Background color of the hover label + font=dict( + color="black", # Text color of the hover label + size=16, # Font size + ), + ) # Optional: add a title to the chart + ) + + # Apply custom formatting function + # Ensure format_fig function is defined elsewhere or comment out this line if not used + format_fig(fig) + + return fig + # Display the figure + #fig.show() + + +class Top10PctImpactChart(): + def __init__(self, data=None) -> None: + if data is None: + raise ValueError("Data must be provided") + self.baseline = data.get('baseline') + self.reform = data.get('reform') + self.change = data.get('change') + self.change_percentage = data.get('change_percentage') + # Data + + def generate_chart_data(self): + # Generate hover text based on the values + hover_texts = [ + f"The reform would increase the Gini index of net income by {self.change_percentage}% from {self.baseline} to {self.reform}%" + if self.change_percentage > 0 + else f"The reform would decrease the Gini index of net income by {self.change_percentage}% from {self.baseline} to {self.reform}%" + + ] + + fig = go.Figure() + + # Add bar trace with adjustable width + fig.add_trace(go.Bar( + x=["Top 10% share"], + y=[self.change_percentage], + marker=dict( + color=[BLUE if self.change_percentage > 0 else GRAY ], # Conditional color + line=dict(width=1), + ), + width=0.3, # Adjust this value between 0 and 1 to change the bar width + text=[f"{self.change_percentage}%"], # Display values as text + textposition='outside', # Position text outside the bars + hovertemplate=f"%{{x}}

%{{customdata}}", + customdata=hover_texts + )) + + # Update the y-axis to show percentages with one decimal place + fig.update_layout( + yaxis=dict( + tickformat=".1f", # Show y-values with one decimal place + ticksuffix="%", + title="Relative change" # Add percentage symbol + ), + + hoverlabel=dict( + bgcolor="white", # Background color of the hover label + font=dict( + color="black", # Text color of the hover label + size=16, # Font size + ), + ) # Optional: add a title to the chart + ) + + # Apply custom formatting function + # Ensure format_fig function is defined elsewhere or comment out this line if not used + format_fig(fig) + + return fig + # Display the figure + #fig.show() + +class Top1PctImpactChart(): + def __init__(self, data=None) -> None: + if data is None: + raise ValueError("Data must be provided") + self.baseline = data.get('baseline') + self.reform = data.get('reform') + self.change = data.get('change') + self.change_percentage = data.get('change_percentage') + # Data + + def generate_chart_data(self): + # Generate hover text based on the values + hover_texts = [ + f"The reform would increase the Gini index of net income by {self.change_percentage}% from {self.baseline} to {self.reform}%" + if self.change_percentage > 0 + else f"The reform would decrease the Gini index of net income by {self.change_percentage}% from {self.baseline} to {self.reform}%" + + ] + + fig = go.Figure() + + # Add bar trace with adjustable width + fig.add_trace(go.Bar( + x=["Top 1% share"], # Label for the x-axis + y=[self.change_percentage], + marker=dict( + color=[BLUE if self.change_percentage > 0 else GRAY ], # Conditional color + line=dict(width=1), + ), + width=0.3, # Adjust this value between 0 and 1 to change the bar width + text=[f"{self.change_percentage}%"], # Display values as text + textposition='outside', # Position text outside the bars + hovertemplate=f"%{{x}}

%{{customdata}}", + customdata=hover_texts + )) + + # Update the y-axis to show percentages with one decimal place + fig.update_layout( + yaxis=dict( + tickformat=".1f", # Show y-values with one decimal place + ticksuffix="%", + title="Relative change" # Add percentage symbol + ), + + hoverlabel=dict( + bgcolor="white", # Background color of the hover label + font=dict( + color="black", # Text color of the hover label + size=16, # Font size + ), + ) # Optional: add a title to the chart + ) + + # Apply custom formatting function + # Ensure format_fig function is defined elsewhere or comment out this line if not used + format_fig(fig) + + return fig + # Display the figure + #fig.show() + +class InequalityImpactChart(): + def __init__(self, data=None) -> None: + if data is None: + raise ValueError("Data must be provided") + self.baseline = data.get('baseline') + self.reform = data.get('reform') + self.change = data.get('change') + self.change_percentage = data.get('change_percentage') + + + def generate_chart_data(self): + # Generate hover text based on the values + hover_texts = [ + f"The reform would increase the Gini index of net income by {self.change_percentage}% from {self.baseline} to {self.reform}%" + if self.change_percentage > 0 + else f"The reform would decrease the Gini index of net income by {self.change_percentage}% from {self.baseline} to {self.reform}%" + + ] + + fig = go.Figure() + + # Add bar trace with adjustable width + fig.add_trace(go.Bar( + x=["Gini index , Top 1% share ,Top 10% share"], + y=[self.change_percentage], + marker=dict( + color=[BLUE if self.change_percentage > 0 else GRAY ], # Conditional color + line=dict(width=1), + ), + width=0.3, # Adjust this value between 0 and 1 to change the bar width + text=[f"{self.change_percentage}%"], # Display values as text + textposition='outside', # Position text outside the bars + hovertemplate=f"%{{x}}

%{{customdata}}", + customdata=hover_texts + )) + + # Update the y-axis to show percentages with one decimal place + fig.update_layout( + yaxis=dict( + tickformat=".1f", # Show y-values with one decimal place + ticksuffix="%", + title="Relative change" # Add percentage symbol + ), + + hoverlabel=dict( + bgcolor="white", # Background color of the hover label + font=dict( + color="black", # Text color of the hover label + size=16, # Font size + ), + ) # Optional: add a title to the chart + ) + + # Apply custom formatting function + # Ensure format_fig function is defined elsewhere or comment out this line if not used + format_fig(fig) + + return fig + # Display the figure + #fig.show() + +class InequalityImpactChart: + def __init__(self, data=None) -> None: + if data is None: + raise ValueError("Data must be provided") + + # Expecting data to contain baseline, reform, change, and change_percentage for each metric + self.data = data + + def generate_chart_data(self): + # Data for the x-axis labels + metrics = ["Gini index", "Top 1% share", "Top 10% share"] + + # Extract the change percentages, baseline, and reform values for hover text + change_percentages = [self.data[metric]['change_percentage'] for metric in metrics] + baseline_values = [self.data[metric]['baseline'] for metric in metrics] + reform_values = [self.data[metric]['reform'] for metric in metrics] + + # Generate hover text for each metric + hover_texts = [ + f"The reform would increase the {metric} by {change_percentages[i]}% from {baseline_values[i]} to {reform_values[i]}%" + if change_percentages[i] > 0 + else f"The reform would decrease the {metric} by {change_percentages[i]}% from {baseline_values[i]} to {reform_values[i]}%" + for i, metric in enumerate(metrics) + ] + + # Create the bar chart figure + fig = go.Figure() + + # Add a bar trace for the change percentages of each metric + fig.add_trace(go.Bar( + x=metrics, # Labels for each metric + y=change_percentages, # Change percentages for each metric + marker=dict( + color=[BLUE if change_percentages[i] > 0 else GRAY for i in range(len(change_percentages))], # Conditional color for each bar + line=dict(width=1), + ), + text=[f"{percent}%" for percent in change_percentages], # Display percentage as text + textposition='outside', # Position text outside the bars + hovertemplate=f"%{{x}}

%{{customdata}}", + customdata=hover_texts # Hover text for each bar + )) + + # Update layout for the chart + fig.update_layout( + yaxis=dict( + tickformat=".1f", # Show y-values with one decimal place + ticksuffix="%", + title="Relative change" # Add percentage symbol + ), + hoverlabel=dict( + bgcolor="white", # Background color of the hover label + font=dict( + color="black", # Text color of the hover label + size=16, # Font size + ), + ), + title="Impact of Reform on Inequality Metrics" # Add a title to the chart + ) + + format_fig(fig) + + return fig \ No newline at end of file diff --git a/policyengine/economic_impact/economic_impact.py b/policyengine/economic_impact/economic_impact.py index d185047..199b624 100644 --- a/policyengine/economic_impact/economic_impact.py +++ b/policyengine/economic_impact/economic_impact.py @@ -76,7 +76,10 @@ from .winners_and_losers.by_wealth_decile.by_wealth_decile import ByWealthDecile -from typing import Dict +from typing import Dict, Type, Union + +from policyengine.charts.inequality import Top1PctImpactChart, GiniImpactChart, Top10PctImpactChart, InequalityImpactChart + class EconomicImpact: """ @@ -170,6 +173,24 @@ def __init__(self, reform: dict, country: str, dataset: str = None) -> None: } + + self.chart_generators: Dict[str, Type] = { + "inequality/gini": GiniImpactChart, + "inequality/top_1_pct_share": Top1PctImpactChart, + "inequality/top_10_pct_share": Top10PctImpactChart, + "inequality": InequalityImpactChart, + } + + self.composite_metrics: Dict[str, Dict[str, str]] = { + "inequality": { + "Gini index": "inequality/gini", + "Top 1% share": "inequality/top_1_pct_share", + "Top 10% share": "inequality/top_10_pct_share", + } + } + + self.metric_results: Dict[str, any] = {} + def _get_simulation_class(self) -> type: """ Get the appropriate Microsimulation class based on the country code. @@ -203,4 +224,42 @@ def calculate(self, metric: str) -> dict: """ if metric not in self.metric_calculators: raise ValueError(f"Unknown metric: {metric}") - return self.metric_calculators[metric].calculate() + + if metric not in self.metric_results: + result = self.metric_calculators[metric].calculate() + self.metric_results[metric] = result + + return self.metric_results[metric] + + def _calculate_composite_metric(self, metric: str) -> dict: + if metric not in self.composite_metrics: + raise ValueError(f"Unknown composite metric: {metric}") + + composite_data = {} + for key, sub_metric in self.composite_metrics[metric].items(): + composite_data[key] = self.calculate(sub_metric) + + return composite_data + + def chart(self, metric: str) -> dict: + if metric in self.composite_metrics: + data = self._calculate_composite_metric(metric) + elif metric in self.chart_generators: + data = self.calculate(metric) + else: + raise ValueError(f"Unknown metric for charting: {metric}") + + chart_generator = self.chart_generators.get(metric) + if not chart_generator: + raise ValueError(f"No chart generator found for metric: {metric}") + + return chart_generator(data=data).generate_chart_data() + + def add_metric(self, metric: str, calculator: object, chart_generator: Type = None): + self.metric_calculators[metric] = calculator + if chart_generator: + self.chart_generators[metric] = chart_generator + + def add_composite_metric(self, name: str, components: Dict[str, str], chart_generator: Type): + self.composite_metrics[name] = components + self.chart_generators[name] = chart_generator \ No newline at end of file From 27f961d9b0088e3e38c4d2b7dadc46c5beeb23a4 Mon Sep 17 00:00:00 2001 From: Mohammed Ismail Date: Thu, 12 Sep 2024 20:23:30 +0530 Subject: [PATCH 2/2] removed the individual metrics --- policyengine/charts/inequality.py | 244 ------------------ .../economic_impact/economic_impact.py | 5 +- 2 files changed, 1 insertion(+), 248 deletions(-) diff --git a/policyengine/charts/inequality.py b/policyengine/charts/inequality.py index 62bb04c..c206c1d 100644 --- a/policyengine/charts/inequality.py +++ b/policyengine/charts/inequality.py @@ -1,250 +1,6 @@ import plotly.graph_objects as go from policyengine_core.charts.formatting import * -class GiniImpactChart(): - def __init__(self, data=None) -> None: - if data is None: - raise ValueError("Data must be provided") - self.baseline = data.get('baseline') - self.reform = data.get('reform') - self.change = data.get('change') - self.change_percentage = data.get('change_percentage') - - def generate_chart_data(self): - # Generate hover text based on the values - hover_texts = [ - f"The reform would increase the Gini index of net income by {self.change_percentage}% from {self.baseline} to {self.reform}%" - if self.change_percentage > 0 - else f"The reform would decrease the Gini index of net income by {self.change_percentage}% from {self.baseline} to {self.reform}%" - - ] - - fig = go.Figure() - - # Add bar trace with adjustable width - fig.add_trace(go.Bar( - x=["Gini Index"], # Label for the x-axis - y=[self.change_percentage], - marker=dict( - color=[BLUE if self.change_percentage > 0 else GRAY ], # Conditional color - line=dict(width=1), - ), - width=0.3, # Adjust this value between 0 and 1 to change the bar width - text=[f"{self.change_percentage}%"], # Display values as text - textposition='outside', # Position text outside the bars - hovertemplate=f"%{{x}}

%{{customdata}}", - customdata=hover_texts - )) - - # Update the y-axis to show percentages with one decimal place - fig.update_layout( - yaxis=dict( - tickformat=".1f", # Show y-values with one decimal place - ticksuffix="%", - title="Relative change" # Add percentage symbol - ), - - hoverlabel=dict( - bgcolor="white", # Background color of the hover label - font=dict( - color="black", # Text color of the hover label - size=16, # Font size - ), - ) # Optional: add a title to the chart - ) - - # Apply custom formatting function - # Ensure format_fig function is defined elsewhere or comment out this line if not used - format_fig(fig) - - return fig - # Display the figure - #fig.show() - - -class Top10PctImpactChart(): - def __init__(self, data=None) -> None: - if data is None: - raise ValueError("Data must be provided") - self.baseline = data.get('baseline') - self.reform = data.get('reform') - self.change = data.get('change') - self.change_percentage = data.get('change_percentage') - # Data - - def generate_chart_data(self): - # Generate hover text based on the values - hover_texts = [ - f"The reform would increase the Gini index of net income by {self.change_percentage}% from {self.baseline} to {self.reform}%" - if self.change_percentage > 0 - else f"The reform would decrease the Gini index of net income by {self.change_percentage}% from {self.baseline} to {self.reform}%" - - ] - - fig = go.Figure() - - # Add bar trace with adjustable width - fig.add_trace(go.Bar( - x=["Top 10% share"], - y=[self.change_percentage], - marker=dict( - color=[BLUE if self.change_percentage > 0 else GRAY ], # Conditional color - line=dict(width=1), - ), - width=0.3, # Adjust this value between 0 and 1 to change the bar width - text=[f"{self.change_percentage}%"], # Display values as text - textposition='outside', # Position text outside the bars - hovertemplate=f"%{{x}}

%{{customdata}}", - customdata=hover_texts - )) - - # Update the y-axis to show percentages with one decimal place - fig.update_layout( - yaxis=dict( - tickformat=".1f", # Show y-values with one decimal place - ticksuffix="%", - title="Relative change" # Add percentage symbol - ), - - hoverlabel=dict( - bgcolor="white", # Background color of the hover label - font=dict( - color="black", # Text color of the hover label - size=16, # Font size - ), - ) # Optional: add a title to the chart - ) - - # Apply custom formatting function - # Ensure format_fig function is defined elsewhere or comment out this line if not used - format_fig(fig) - - return fig - # Display the figure - #fig.show() - -class Top1PctImpactChart(): - def __init__(self, data=None) -> None: - if data is None: - raise ValueError("Data must be provided") - self.baseline = data.get('baseline') - self.reform = data.get('reform') - self.change = data.get('change') - self.change_percentage = data.get('change_percentage') - # Data - - def generate_chart_data(self): - # Generate hover text based on the values - hover_texts = [ - f"The reform would increase the Gini index of net income by {self.change_percentage}% from {self.baseline} to {self.reform}%" - if self.change_percentage > 0 - else f"The reform would decrease the Gini index of net income by {self.change_percentage}% from {self.baseline} to {self.reform}%" - - ] - - fig = go.Figure() - - # Add bar trace with adjustable width - fig.add_trace(go.Bar( - x=["Top 1% share"], # Label for the x-axis - y=[self.change_percentage], - marker=dict( - color=[BLUE if self.change_percentage > 0 else GRAY ], # Conditional color - line=dict(width=1), - ), - width=0.3, # Adjust this value between 0 and 1 to change the bar width - text=[f"{self.change_percentage}%"], # Display values as text - textposition='outside', # Position text outside the bars - hovertemplate=f"%{{x}}

%{{customdata}}", - customdata=hover_texts - )) - - # Update the y-axis to show percentages with one decimal place - fig.update_layout( - yaxis=dict( - tickformat=".1f", # Show y-values with one decimal place - ticksuffix="%", - title="Relative change" # Add percentage symbol - ), - - hoverlabel=dict( - bgcolor="white", # Background color of the hover label - font=dict( - color="black", # Text color of the hover label - size=16, # Font size - ), - ) # Optional: add a title to the chart - ) - - # Apply custom formatting function - # Ensure format_fig function is defined elsewhere or comment out this line if not used - format_fig(fig) - - return fig - # Display the figure - #fig.show() - -class InequalityImpactChart(): - def __init__(self, data=None) -> None: - if data is None: - raise ValueError("Data must be provided") - self.baseline = data.get('baseline') - self.reform = data.get('reform') - self.change = data.get('change') - self.change_percentage = data.get('change_percentage') - - - def generate_chart_data(self): - # Generate hover text based on the values - hover_texts = [ - f"The reform would increase the Gini index of net income by {self.change_percentage}% from {self.baseline} to {self.reform}%" - if self.change_percentage > 0 - else f"The reform would decrease the Gini index of net income by {self.change_percentage}% from {self.baseline} to {self.reform}%" - - ] - - fig = go.Figure() - - # Add bar trace with adjustable width - fig.add_trace(go.Bar( - x=["Gini index , Top 1% share ,Top 10% share"], - y=[self.change_percentage], - marker=dict( - color=[BLUE if self.change_percentage > 0 else GRAY ], # Conditional color - line=dict(width=1), - ), - width=0.3, # Adjust this value between 0 and 1 to change the bar width - text=[f"{self.change_percentage}%"], # Display values as text - textposition='outside', # Position text outside the bars - hovertemplate=f"%{{x}}

%{{customdata}}", - customdata=hover_texts - )) - - # Update the y-axis to show percentages with one decimal place - fig.update_layout( - yaxis=dict( - tickformat=".1f", # Show y-values with one decimal place - ticksuffix="%", - title="Relative change" # Add percentage symbol - ), - - hoverlabel=dict( - bgcolor="white", # Background color of the hover label - font=dict( - color="black", # Text color of the hover label - size=16, # Font size - ), - ) # Optional: add a title to the chart - ) - - # Apply custom formatting function - # Ensure format_fig function is defined elsewhere or comment out this line if not used - format_fig(fig) - - return fig - # Display the figure - #fig.show() - class InequalityImpactChart: def __init__(self, data=None) -> None: if data is None: diff --git a/policyengine/economic_impact/economic_impact.py b/policyengine/economic_impact/economic_impact.py index 199b624..c913e96 100644 --- a/policyengine/economic_impact/economic_impact.py +++ b/policyengine/economic_impact/economic_impact.py @@ -78,7 +78,7 @@ from typing import Dict, Type, Union -from policyengine.charts.inequality import Top1PctImpactChart, GiniImpactChart, Top10PctImpactChart, InequalityImpactChart +from policyengine.charts.inequality import InequalityImpactChart class EconomicImpact: @@ -175,9 +175,6 @@ def __init__(self, reform: dict, country: str, dataset: str = None) -> None: self.chart_generators: Dict[str, Type] = { - "inequality/gini": GiniImpactChart, - "inequality/top_1_pct_share": Top1PctImpactChart, - "inequality/top_10_pct_share": Top10PctImpactChart, "inequality": InequalityImpactChart, }