From 42905e6d75535fb356db160979874d6953967cb2 Mon Sep 17 00:00:00 2001 From: Mohammed Ismail Date: Wed, 11 Sep 2024 14:30:37 +0530 Subject: [PATCH 1/5] 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/5] 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, } From 9884225b9fb651cec9aa8e77ef5a846607bc9b4d Mon Sep 17 00:00:00 2001 From: Mohammed Ismail Date: Tue, 17 Sep 2024 08:46:09 +0530 Subject: [PATCH 3/5] updated charts for poverty --- policyengine/charts/poverty/__init__.py | 0 policyengine/charts/poverty/deep/__init__.py | 0 policyengine/charts/poverty/deep/by_age.py | 107 ++++++++++++++++++ policyengine/charts/poverty/deep/by_gender.py | 103 +++++++++++++++++ .../charts/poverty/regular/__init__.py | 0 policyengine/charts/poverty/regular/by_age.py | 107 ++++++++++++++++++ .../charts/poverty/regular/by_gender.py | 103 +++++++++++++++++ .../economic_impact/economic_impact.py | 32 +++++- 8 files changed, 451 insertions(+), 1 deletion(-) create mode 100644 policyengine/charts/poverty/__init__.py create mode 100644 policyengine/charts/poverty/deep/__init__.py create mode 100644 policyengine/charts/poverty/deep/by_age.py create mode 100644 policyengine/charts/poverty/deep/by_gender.py create mode 100644 policyengine/charts/poverty/regular/__init__.py create mode 100644 policyengine/charts/poverty/regular/by_age.py create mode 100644 policyengine/charts/poverty/regular/by_gender.py diff --git a/policyengine/charts/poverty/__init__.py b/policyengine/charts/poverty/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/policyengine/charts/poverty/deep/__init__.py b/policyengine/charts/poverty/deep/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/policyengine/charts/poverty/deep/by_age.py b/policyengine/charts/poverty/deep/by_age.py new file mode 100644 index 0000000..fabe5c6 --- /dev/null +++ b/policyengine/charts/poverty/deep/by_age.py @@ -0,0 +1,107 @@ +import plotly.graph_objects as go +from policyengine_core.charts.formatting import * + +class DeepPovertyByAgeChart: + def __init__(self,country:str, data=None): + if data is None: + raise ValueError("Data must be provided") + + self.data = data + + def _get_color(self, value): + # All bars should be gray + return GRAY + + def _get_change_direction(self, value): + if value > 0: + return "increase" + elif value < 0: + return "decrease" + else: + return "no change" + + def ordinal_suffix(self, n): + """Return the ordinal suffix for an integer.""" + if 10 <= n % 100 <= 20: + suffix = 'th' + else: + suffix = {1: 'st', 2: 'nd', 3: 'rd'}.get(n % 10, 'th') + return suffix + + def generate_chart_data(self): + categories = list(self.data.keys()) + values = [self.data[cat]['change'] for cat in categories] + baselines = [self.data[cat]['baseline'] * 100 for cat in categories] + reforms = [self.data[cat]['reform'] * 100 for cat in categories] + + # Generate hover texts with baseline, reform, and percentage change + hover_texts = [ + f"This reform would {self._get_change_direction(val)} the percentage of {category.lower()} in poverty by {abs(val):.1f}% from {baseline:.1f}% to {reform:.1f}%" + for category, val, baseline, reform in zip(categories, values, baselines, reforms) + ] + + fig = go.Figure() + + values_in_pct = values # Use percentage values + colors = [self._get_color(value) for value in values] + + # Add bar chart with percentage values + fig.add_trace(go.Bar( + x=categories, + y=values_in_pct, + marker=dict(color=colors, line=dict(width=1)), + width=0.6, + text=[f"{abs(value):.1f}%" for value in values], # Display values as percentages + textposition='outside', + hovertemplate="%{x}

%{customdata}", # Hover shows category + customdata=hover_texts + )) + + # Update layout to reflect percentage values on y-axis + fig.update_layout( + yaxis=dict( + tickformat=",.1f%%", # Format y-axis as percentages with one decimal place + title="Percentage Change in Poverty" + ), + xaxis=dict( + title="Category" + ), + hoverlabel=dict( + bgcolor="white", + font=dict(color="black", size=16) + ), + title="Change in Poverty Percentage by Category" + ) + + format_fig(fig) # Keep the formatting logic from policyengine_core + return fig + + +# # Example data +# data = { +# 'Child': { +# 'Baseline': 0.32427219591395395, +# 'Reform': 0.33392168532001054, +# 'Change': 3.0 +# }, +# 'Adult': { +# 'Baseline': 0.17427822561729264, +# 'Reform': 0.17757158627182623, +# 'Change': 1.9 +# }, +# 'Senior': { +# 'Baseline': 0.12817646500651358, +# 'Reform': 0.1370685860340031, +# 'Change': 6.9 +# }, +# 'All': { +# 'Baseline': 0.19913534734369268, +# 'Reform': 0.20487670454940832, +# 'Change': 2.9 +# } +# } + +# # Generate chart for all categories +# chart = OverallChart(data=data) +# fig = chart.generate_chart_data() +# fig.show() diff --git a/policyengine/charts/poverty/deep/by_gender.py b/policyengine/charts/poverty/deep/by_gender.py new file mode 100644 index 0000000..40e1a61 --- /dev/null +++ b/policyengine/charts/poverty/deep/by_gender.py @@ -0,0 +1,103 @@ +import plotly.graph_objects as go +from policyengine_core.charts.formatting import * + +class DeepPovertyByGenderChart: + def __init__(self, country:str,data=None): + if data is None: + raise ValueError("Data must be provided") + + self.data = data + + def _get_color(self, value): + # All bars should be gray + return GRAY + + def _get_change_direction(self, value): + if value > 0: + return "increase" + elif value < 0: + return "decrease" + else: + return "no change" + + def ordinal_suffix(self, n): + """Return the ordinal suffix for an integer.""" + if 10 <= n % 100 <= 20: + suffix = 'th' + else: + suffix = {1: 'st', 2: 'nd', 3: 'rd'}.get(n % 10, 'th') + return suffix + + def generate_chart_data(self): + categories = list(self.data.keys()) + values = [self.data[cat]['change'] for cat in categories] + baselines = [self.data[cat]['baseline'] * 100 for cat in categories] + reforms = [self.data[cat]['reform'] * 100 for cat in categories] + + # Generate hover texts with baseline, reform, and percentage change + hover_texts = [ + f"This reform would {self._get_change_direction(val)} the percentage of {category.lower()} in poverty by {abs(val):.1f}% from {baseline:.1f}% to {reform:.1f}%" + for category, val, baseline, reform in zip(categories, values, baselines, reforms) + ] + + fig = go.Figure() + + values_in_pct = values # Use percentage values + colors = [self._get_color(value) for value in values] + + # Add bar chart with percentage values + fig.add_trace(go.Bar( + x=categories, + y=values_in_pct, + marker=dict(color=colors, line=dict(width=1)), + width=0.6, + text=[f"{abs(value):.1f}%" for value in values], # Display values as percentages + textposition='outside', + hovertemplate="Category %{x}

%{customdata}", # Hover shows category + customdata=hover_texts + )) + + # Update layout to reflect percentage values on y-axis + fig.update_layout( + yaxis=dict( + tickformat=",.1f%%", + ticksuffix = "%", # Format y-axis as percentages with one decimal place + title="Percentage Change in Poverty" + ), + xaxis=dict( + title="Category" + ), + hoverlabel=dict( + bgcolor="white", + font=dict(color="black", size=16) + ), + title="Change in Poverty Percentage by Category" + ) + + format_fig(fig) # Keep the formatting logic from policyengine_core + return fig + + +# # Example data +# data = { +# 'Male': { +# 'Baseline': 0.18412623468617267, +# 'Reform': 0.18932591339284738, +# 'Change': 2.8 +# }, +# 'Female': { +# 'Baseline': 0.21377616483057263, +# 'Reform': 0.22004590877186603, +# 'Change': 2.9 +# }, +# 'All': { +# 'Baseline': 0.19913534734369268, +# 'Reform': 0.20487670454940832, +# 'Change': 2.9 +# } +# } + +# # Generate chart for all categories +# chart = OverallChart(data=data) +# fig = chart.generate_chart_data() +# fig.show() diff --git a/policyengine/charts/poverty/regular/__init__.py b/policyengine/charts/poverty/regular/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/policyengine/charts/poverty/regular/by_age.py b/policyengine/charts/poverty/regular/by_age.py new file mode 100644 index 0000000..e60333b --- /dev/null +++ b/policyengine/charts/poverty/regular/by_age.py @@ -0,0 +1,107 @@ +import plotly.graph_objects as go +from policyengine_core.charts.formatting import * + +class RegularPovertyByAgeChart: + def __init__(self,country:str, data=None): + if data is None: + raise ValueError("Data must be provided") + + self.data = data + + def _get_color(self, value): + # All bars should be gray + return GRAY + + def _get_change_direction(self, value): + if value > 0: + return "increase" + elif value < 0: + return "decrease" + else: + return "no change" + + def ordinal_suffix(self, n): + """Return the ordinal suffix for an integer.""" + if 10 <= n % 100 <= 20: + suffix = 'th' + else: + suffix = {1: 'st', 2: 'nd', 3: 'rd'}.get(n % 10, 'th') + return suffix + + def generate_chart_data(self): + categories = list(self.data.keys()) + values = [self.data[cat]['change'] for cat in categories] + baselines = [self.data[cat]['baseline'] * 100 for cat in categories] + reforms = [self.data[cat]['reform'] * 100 for cat in categories] + + # Generate hover texts with baseline, reform, and percentage change + hover_texts = [ + f"This reform would {self._get_change_direction(val)} the percentage of {category.lower()} in poverty by {abs(val):.1f}% from {baseline:.1f}% to {reform:.1f}%" + for category, val, baseline, reform in zip(categories, values, baselines, reforms) + ] + + fig = go.Figure() + + values_in_pct = values # Use percentage values + colors = [self._get_color(value) for value in values] + + # Add bar chart with percentage values + fig.add_trace(go.Bar( + x=categories, + y=values_in_pct, + marker=dict(color=colors, line=dict(width=1)), + width=0.6, + text=[f"{abs(value):.1f}%" for value in values], # Display values as percentages + textposition='outside', + hovertemplate="%{x}

%{customdata}", # Hover shows category + customdata=hover_texts + )) + + # Update layout to reflect percentage values on y-axis + fig.update_layout( + yaxis=dict( + tickformat=",.1f%%", # Format y-axis as percentages with one decimal place + title="Percentage Change in Poverty" + ), + xaxis=dict( + title="Category" + ), + hoverlabel=dict( + bgcolor="white", + font=dict(color="black", size=16) + ), + title="Change in Poverty Percentage by Category" + ) + + format_fig(fig) # Keep the formatting logic from policyengine_core + return fig + + +# # Example data +# data = { +# 'Child': { +# 'Baseline': 0.32427219591395395, +# 'Reform': 0.33392168532001054, +# 'Change': 3.0 +# }, +# 'Adult': { +# 'Baseline': 0.17427822561729264, +# 'Reform': 0.17757158627182623, +# 'Change': 1.9 +# }, +# 'Senior': { +# 'Baseline': 0.12817646500651358, +# 'Reform': 0.1370685860340031, +# 'Change': 6.9 +# }, +# 'All': { +# 'Baseline': 0.19913534734369268, +# 'Reform': 0.20487670454940832, +# 'Change': 2.9 +# } +# } + +# # Generate chart for all categories +# chart = OverallChart(data=data) +# fig = chart.generate_chart_data() +# fig.show() diff --git a/policyengine/charts/poverty/regular/by_gender.py b/policyengine/charts/poverty/regular/by_gender.py new file mode 100644 index 0000000..a393c25 --- /dev/null +++ b/policyengine/charts/poverty/regular/by_gender.py @@ -0,0 +1,103 @@ +import plotly.graph_objects as go +from policyengine_core.charts.formatting import * + +class RegularPovertyByGenderChart: + def __init__(self, country:str,data=None): + if data is None: + raise ValueError("Data must be provided") + + self.data = data + + def _get_color(self, value): + # All bars should be gray + return GRAY + + def _get_change_direction(self, value): + if value > 0: + return "increase" + elif value < 0: + return "decrease" + else: + return "no change" + + def ordinal_suffix(self, n): + """Return the ordinal suffix for an integer.""" + if 10 <= n % 100 <= 20: + suffix = 'th' + else: + suffix = {1: 'st', 2: 'nd', 3: 'rd'}.get(n % 10, 'th') + return suffix + + def generate_chart_data(self): + categories = list(self.data.keys()) + values = [self.data[cat]['change'] for cat in categories] + baselines = [self.data[cat]['baseline'] * 100 for cat in categories] + reforms = [self.data[cat]['reform'] * 100 for cat in categories] + + # Generate hover texts with baseline, reform, and percentage change + hover_texts = [ + f"This reform would {self._get_change_direction(val)} the percentage of {category.lower()} in poverty by {abs(val):.1f}% from {baseline:.1f}% to {reform:.1f}%" + for category, val, baseline, reform in zip(categories, values, baselines, reforms) + ] + + fig = go.Figure() + + values_in_pct = values # Use percentage values + colors = [self._get_color(value) for value in values] + + # Add bar chart with percentage values + fig.add_trace(go.Bar( + x=categories, + y=values_in_pct, + marker=dict(color=colors, line=dict(width=1)), + width=0.6, + text=[f"{abs(value):.1f}%" for value in values], # Display values as percentages + textposition='outside', + hovertemplate="Category %{x}

%{customdata}", # Hover shows category + customdata=hover_texts + )) + + # Update layout to reflect percentage values on y-axis + fig.update_layout( + yaxis=dict( + tickformat=",.1f%%", + ticksuffix = "%", # Format y-axis as percentages with one decimal place + title="Percentage Change in Poverty" + ), + xaxis=dict( + title="Category" + ), + hoverlabel=dict( + bgcolor="white", + font=dict(color="black", size=16) + ), + title="Change in Poverty Percentage by Category" + ) + + format_fig(fig) # Keep the formatting logic from policyengine_core + return fig + + +# # Example data +# data = { +# 'Male': { +# 'Baseline': 0.18412623468617267, +# 'Reform': 0.18932591339284738, +# 'Change': 2.8 +# }, +# 'Female': { +# 'Baseline': 0.21377616483057263, +# 'Reform': 0.22004590877186603, +# 'Change': 2.9 +# }, +# 'All': { +# 'Baseline': 0.19913534734369268, +# 'Reform': 0.20487670454940832, +# 'Change': 2.9 +# } +# } + +# # Generate chart for all categories +# chart = OverallChart(data=data) +# fig = chart.generate_chart_data() +# fig.show() diff --git a/policyengine/economic_impact/economic_impact.py b/policyengine/economic_impact/economic_impact.py index c913e96..49b914b 100644 --- a/policyengine/economic_impact/economic_impact.py +++ b/policyengine/economic_impact/economic_impact.py @@ -79,6 +79,10 @@ from typing import Dict, Type, Union from policyengine.charts.inequality import InequalityImpactChart +from policyengine.charts.poverty.regular.by_age import RegularPovertyByAgeChart +from policyengine.charts.poverty.deep.by_age import DeepPovertyByAgeChart +from policyengine.charts.poverty.regular.by_gender import RegularPovertyByGenderChart +from policyengine.charts.poverty.deep.by_gender import DeepPovertyByGenderChart class EconomicImpact: @@ -176,6 +180,10 @@ def __init__(self, reform: dict, country: str, dataset: str = None) -> None: self.chart_generators: Dict[str, Type] = { "inequality": InequalityImpactChart, + "poverty/regular/by_age": RegularPovertyByAgeChart, + "poverty/regular/by_gender": RegularPovertyByGenderChart, + "poverty/deep/by_age": DeepPovertyByAgeChart, + "poverty/deep/by_gender": DeepPovertyByGenderChart, } self.composite_metrics: Dict[str, Dict[str, str]] = { @@ -183,6 +191,28 @@ def __init__(self, reform: dict, country: str, dataset: str = None) -> None: "Gini index": "inequality/gini", "Top 1% share": "inequality/top_1_pct_share", "Top 10% share": "inequality/top_10_pct_share", + }, + "poverty/regular/by_age": { + "Child": "poverty/regular/child", + "Adult": "poverty/regular/adult", + "Senior":"poverty/regular/senior", + "All": "poverty/regular/age/all" + }, + "poverty/regular/by_gender": { + "Male": "poverty/regular/male", + "Female": "poverty/regular/female", + "All": "poverty/regular/gender/all" + }, + "poverty/deep/by_age": { + "Child": "poverty/deep/child", + "Adult": "poverty/deep/adult", + "Senior":"poverty/deep/senior", + "All": "poverty/deep/age/all" + }, + "poverty/deep/by_gender": { + "Male": "poverty/deep/male", + "Female": "poverty/deep/female", + "All": "poverty/deep/gender/all" } } @@ -250,7 +280,7 @@ def chart(self, metric: str) -> dict: if not chart_generator: raise ValueError(f"No chart generator found for metric: {metric}") - return chart_generator(data=data).generate_chart_data() + return chart_generator(self.country,data=data).generate_chart_data() def add_metric(self, metric: str, calculator: object, chart_generator: Type = None): self.metric_calculators[metric] = calculator From dda2f27efa7c0b64b272ee1c60fcad3fd97d0412 Mon Sep 17 00:00:00 2001 From: Mohammed Ismail Date: Tue, 17 Sep 2024 17:30:04 +0530 Subject: [PATCH 4/5] Distribution Impact Charts --- .../charts/distributional_impact/__init__.py | 0 .../by_income_decile/__init__.py | 0 .../by_income_decile/average.py | 114 ++++++++++++++++++ .../by_income_decile/relative.py | 102 ++++++++++++++++ .../by_wealth_decile/__init__.py | 0 .../by_wealth_decile/average.py | 114 ++++++++++++++++++ .../by_wealth_decile/relative.py | 105 ++++++++++++++++ .../economic_impact/economic_impact.py | 8 ++ 8 files changed, 443 insertions(+) create mode 100644 policyengine/charts/distributional_impact/__init__.py create mode 100644 policyengine/charts/distributional_impact/by_income_decile/__init__.py create mode 100644 policyengine/charts/distributional_impact/by_income_decile/average.py create mode 100644 policyengine/charts/distributional_impact/by_income_decile/relative.py create mode 100644 policyengine/charts/distributional_impact/by_wealth_decile/__init__.py create mode 100644 policyengine/charts/distributional_impact/by_wealth_decile/average.py create mode 100644 policyengine/charts/distributional_impact/by_wealth_decile/relative.py diff --git a/policyengine/charts/distributional_impact/__init__.py b/policyengine/charts/distributional_impact/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/policyengine/charts/distributional_impact/by_income_decile/__init__.py b/policyengine/charts/distributional_impact/by_income_decile/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/policyengine/charts/distributional_impact/by_income_decile/average.py b/policyengine/charts/distributional_impact/by_income_decile/average.py new file mode 100644 index 0000000..4bda492 --- /dev/null +++ b/policyengine/charts/distributional_impact/by_income_decile/average.py @@ -0,0 +1,114 @@ +import plotly.graph_objects as go +from policyengine_core.charts.formatting import * + +class ByIncomeDecileAverageChart: + def __init__(self, country: str, data=None): + if data is None: + raise ValueError("Data must be provided") + + # Store values as they are (no percentage conversion) + for i in range(1, 12): + setattr(self, f"decile_{i}", data['average'][i]) + + self.country = country + + def _get_color(self, value): + if value is None or value == 0 or value < 0: + return GRAY + return BLUE + + def _get_change_direction(self, value): + if value > 0: + return "increase" + elif value < 0: + return "decrease" + else: + return "no change" + + def _get_currency_symbol(self): + if self.country.lower() == "us": + return "$" + elif self.country.lower() == "uk": + return "£" + else: + return "$" # Default to USD if country not recognized + + def ordinal_suffix(self, n): + """Return the ordinal suffix for an integer.""" + if 10 <= n % 100 <= 20: + suffix = 'th' + else: + suffix = {1: 'st', 2: 'nd', 3: 'rd'}.get(n % 10, 'th') + return suffix + + def generate_chart_data(self): + categories = [str(i) for i in range(1, 12)] + values = [getattr(self, f"decile_{i}") for i in range(1, 12)] + + # Filter out categories and values with zero difference + non_zero_data = [(cat, val) for cat, val in zip(categories, values) if val != 0] + + if not non_zero_data: + fig = go.Figure() + fig.add_annotation( + x=0.5, + y=0.5, + xref="paper", + yref="paper", + text="No differences to display", + showarrow=False, + font=dict(size=20) + ) + fig.update_layout( + title="Absolute change in household income", + xaxis=dict(visible=False), + yaxis=dict(visible=False) + ) + return fig + + non_zero_categories, non_zero_values = zip(*non_zero_data) + + # Get currency symbol based on country + currency_symbol = self._get_currency_symbol() + + # Generate hover texts with raw impact values and change direction + hover_texts = [ + f"This reform would {self._get_change_direction(val)} the income of households in the {i}{self.ordinal_suffix(int(i))} decile by {currency_symbol}{abs(val):,.1f}" + for i, val in zip(non_zero_categories, non_zero_values) + ] + + fig = go.Figure() + + values_in_bn = non_zero_values # No need to convert values + colors = [self._get_color(value) for value in non_zero_values] + + # Add bar chart with text formatted with currency symbol + fig.add_trace(go.Bar( + x=non_zero_categories, + y=values_in_bn, + marker=dict(color=colors, line=dict(width=1)), + width=0.6, + text=[f"{currency_symbol}{abs(value):,.1f}" for value in non_zero_values], # Display values with currency symbol + textposition='outside', + hovertemplate="Decile %{x}

%{customdata}", # Hover shows "Decile {x}" + customdata=hover_texts + )) + + # Update layout to include currency on y-axis + fig.update_layout( + yaxis=dict( + tickformat=",.0f", # No decimal places for the y-axis, add thousands separator + title=f"Absolute Impact on Income ({currency_symbol})" + ), + xaxis=dict( + title="Income Decile" + ), + hoverlabel=dict( + bgcolor="white", + font=dict(color="black", size=16) + ), + title="Absolute Change in Household Income by Decile" + ) + + format_fig(fig) # Keep the formatting logic from policyengine_core + return fig \ No newline at end of file diff --git a/policyengine/charts/distributional_impact/by_income_decile/relative.py b/policyengine/charts/distributional_impact/by_income_decile/relative.py new file mode 100644 index 0000000..6269be9 --- /dev/null +++ b/policyengine/charts/distributional_impact/by_income_decile/relative.py @@ -0,0 +1,102 @@ +import plotly.graph_objects as go +from policyengine_core.charts.formatting import * + + +class ByIncomeDecileRelativeChart: + def __init__(self, country: str, data=None): + if data is None: + raise ValueError("Data must be provided") + + # Convert the relative values to percentages and store them in attributes for each decile + for i in range(1, 12): + setattr(self, f"decile_{i}", data['relative'][i] * 100) + + self.country = country + + def _get_color(self, value): + if value is None or value == 0 or value < 0: + return GRAY + return BLUE + + def _get_change_direction(self, value): + if value > 0: + return "increase" + elif value < 0: + return "decrease" + else: + return "no change" + + def ordinal_suffix(self, n): + """Return the ordinal suffix for an integer.""" + if 10 <= n % 100 <= 20: + suffix = 'th' + else: + suffix = {1: 'st', 2: 'nd', 3: 'rd'}.get(n % 10, 'th') + return suffix + + + def generate_chart_data(self): + categories = [str(i) for i in range(1, 12)] + values = [getattr(self, f"decile_{i}") for i in range(1, 12)] + + # Filter out categories and values with zero difference + non_zero_data = [(cat, val) for cat, val in zip(categories, values) if val != 0] + + if not non_zero_data: + fig = go.Figure() + fig.add_annotation( + x=0.5, + y=0.5, + xref="paper", + yref="paper", + text="No differences to display", + showarrow=False, + font=dict(size=20) + ) + fig.update_layout( + title="Relative change in household income", + xaxis=dict(visible=False), + yaxis=dict(visible=False) + ) + return fig + + non_zero_categories, non_zero_values = zip(*non_zero_data) + + # Generate hover texts with formatted impact values and change direction + hover_texts = [ + f"This reform would {self._get_change_direction(val)} the income of households in the {i}{self.ordinal_suffix(int(i))} decile by {abs(val):,.1f}%" + for i, val in zip(non_zero_categories, non_zero_values) + ] + + fig = go.Figure() + + values_in_bn = non_zero_values # The values are already in percentages + colors = [self._get_color(value) for value in non_zero_values] + + fig.add_trace(go.Bar( + x=non_zero_categories, + y=values_in_bn, + marker=dict(color=colors, line=dict(width=1)), + width=0.6, + text=[f"{value:.1f}%" for value in non_zero_values], # Display values with one decimal place and percentage symbol + textposition='outside', + hovertemplate="Decile %{x}

%{customdata}", # Hover shows "Decile {x}" + customdata=hover_texts + )) + + # Update layout to show percentage on y-axis and format figure + fig.update_layout( + yaxis=dict( + tickformat=".1f%", # Format for one decimal place with percentage symbol + ticksuffix="%", + title="Relative Impact on Income (%)" + ), + xaxis=dict( + title="Income Decile" + ), + hoverlabel=dict( + bgcolor="white", + font=dict(color="black", size=16) + ), + title="Relative Change in Household Income by Decile" + ) \ No newline at end of file diff --git a/policyengine/charts/distributional_impact/by_wealth_decile/__init__.py b/policyengine/charts/distributional_impact/by_wealth_decile/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/policyengine/charts/distributional_impact/by_wealth_decile/average.py b/policyengine/charts/distributional_impact/by_wealth_decile/average.py new file mode 100644 index 0000000..5b0ab43 --- /dev/null +++ b/policyengine/charts/distributional_impact/by_wealth_decile/average.py @@ -0,0 +1,114 @@ +import plotly.graph_objects as go +from policyengine_core.charts.formatting import * + +class ByWealthDecileAverageChart: + def __init__(self, country: str, data=None): + if data is None: + raise ValueError("Data must be provided") + + # Store values as they are (no percentage conversion) + for i in range(1, 12): + setattr(self, f"decile_{i}", data['average'][i]) + + self.country = country + + def _get_color(self, value): + if value is None or value == 0 or value < 0: + return GRAY + return BLUE + + def _get_change_direction(self, value): + if value > 0: + return "increase" + elif value < 0: + return "decrease" + else: + return "no change" + + def _get_currency_symbol(self): + if self.country.lower() == "us": + return "$" + elif self.country.lower() == "uk": + return "£" + else: + return "$" # Default to USD if country not recognized + + def ordinal_suffix(self, n): + """Return the ordinal suffix for an integer.""" + if 10 <= n % 100 <= 20: + suffix = 'th' + else: + suffix = {1: 'st', 2: 'nd', 3: 'rd'}.get(n % 10, 'th') + return suffix + + def generate_chart_data(self): + categories = [str(i) for i in range(1, 12)] + values = [getattr(self, f"decile_{i}") for i in range(1, 12)] + + # Filter out categories and values with zero difference + non_zero_data = [(cat, val) for cat, val in zip(categories, values) if val != 0] + + if not non_zero_data: + fig = go.Figure() + fig.add_annotation( + x=0.5, + y=0.5, + xref="paper", + yref="paper", + text="No differences to display", + showarrow=False, + font=dict(size=20) + ) + fig.update_layout( + title="Absolute change in household income", + xaxis=dict(visible=False), + yaxis=dict(visible=False) + ) + return fig + + non_zero_categories, non_zero_values = zip(*non_zero_data) + + # Get currency symbol based on country + currency_symbol = self._get_currency_symbol() + + # Generate hover texts with raw impact values and change direction + hover_texts = [ + f"This reform would {self._get_change_direction(val)} the income of households in the {i}{self.ordinal_suffix(int(i))} decile by {currency_symbol}{abs(val):,.1f}" + for i, val in zip(non_zero_categories, non_zero_values) + ] + + fig = go.Figure() + + values_in_bn = non_zero_values # No need to convert values + colors = [self._get_color(value) for value in non_zero_values] + + # Add bar chart with text formatted with currency symbol + fig.add_trace(go.Bar( + x=non_zero_categories, + y=values_in_bn, + marker=dict(color=colors, line=dict(width=1)), + width=0.6, + text=[f"{currency_symbol}{abs(value):,.1f}" for value in non_zero_values], # Display values with currency symbol + textposition='outside', + hovertemplate="Decile %{x}

%{customdata}", # Hover shows "Decile {x}" + customdata=hover_texts + )) + + # Update layout to include currency on y-axis + fig.update_layout( + yaxis=dict( + tickformat=",.0f", # No decimal places for the y-axis, add thousands separator + title=f"Absolute Impact on Wealth ({currency_symbol})" + ), + xaxis=dict( + title="Wealth Decile" + ), + hoverlabel=dict( + bgcolor="white", + font=dict(color="black", size=16) + ), + title="Absolute Change in Household Income by Decile" + ) + + format_fig(fig) # Keep the formatting logic from policyengine_core + return fig \ No newline at end of file diff --git a/policyengine/charts/distributional_impact/by_wealth_decile/relative.py b/policyengine/charts/distributional_impact/by_wealth_decile/relative.py new file mode 100644 index 0000000..5377781 --- /dev/null +++ b/policyengine/charts/distributional_impact/by_wealth_decile/relative.py @@ -0,0 +1,105 @@ +import plotly.graph_objects as go +from policyengine_core.charts.formatting import * + + +class ByWealthDecileRelativeChart: + def __init__(self, country: str, data=None): + if data is None: + raise ValueError("Data must be provided") + + # Convert the relative values to percentages and store them in attributes for each decile + for i in range(1, 12): + setattr(self, f"decile_{i}", data['relative'][i] * 100) + + self.country = country + + def _get_color(self, value): + if value is None or value == 0 or value < 0: + return GRAY + return BLUE + + def _get_change_direction(self, value): + if value > 0: + return "increase" + elif value < 0: + return "decrease" + else: + return "no change" + + def ordinal_suffix(self, n): + """Return the ordinal suffix for an integer.""" + if 10 <= n % 100 <= 20: + suffix = 'th' + else: + suffix = {1: 'st', 2: 'nd', 3: 'rd'}.get(n % 10, 'th') + return suffix + + + def generate_chart_data(self): + categories = [str(i) for i in range(1, 12)] + values = [getattr(self, f"decile_{i}") for i in range(1, 12)] + + # Filter out categories and values with zero difference + non_zero_data = [(cat, val) for cat, val in zip(categories, values) if val != 0] + + if not non_zero_data: + fig = go.Figure() + fig.add_annotation( + x=0.5, + y=0.5, + xref="paper", + yref="paper", + text="No differences to display", + showarrow=False, + font=dict(size=20) + ) + fig.update_layout( + title="Relative change in household income", + xaxis=dict(visible=False), + yaxis=dict(visible=False) + ) + return fig + + non_zero_categories, non_zero_values = zip(*non_zero_data) + + # Generate hover texts with formatted impact values and change direction + hover_texts = [ + f"This reform would {self._get_change_direction(val)} the income of households in the {i}{self.ordinal_suffix(int(i))} decile by {abs(val):,.1f}%" + for i, val in zip(non_zero_categories, non_zero_values) + ] + + fig = go.Figure() + + values_in_bn = non_zero_values # The values are already in percentages + colors = [self._get_color(value) for value in non_zero_values] + + fig.add_trace(go.Bar( + x=non_zero_categories, + y=values_in_bn, + marker=dict(color=colors, line=dict(width=1)), + width=0.6, + text=[f"{value:.1f}%" for value in non_zero_values], # Display values with one decimal place and percentage symbol + textposition='outside', + hovertemplate="Decile %{x}

%{customdata}", # Hover shows "Decile {x}" + customdata=hover_texts + )) + + # Update layout to show percentage on y-axis and format figure + fig.update_layout( + yaxis=dict( + tickformat=".1f%", # Format for one decimal place with percentage symbol + ticksuffix="%", + title="Relative Impact on Wealth (%)" + ), + xaxis=dict( + title="Wealth Decile" + ), + hoverlabel=dict( + bgcolor="white", + font=dict(color="black", size=16) + ), + title="Relative Change in Household Income by Decile" + ) + + format_fig(fig) # Keep the formatting logic from policyengine_core + return fig diff --git a/policyengine/economic_impact/economic_impact.py b/policyengine/economic_impact/economic_impact.py index 49b914b..6e0d47c 100644 --- a/policyengine/economic_impact/economic_impact.py +++ b/policyengine/economic_impact/economic_impact.py @@ -83,6 +83,10 @@ from policyengine.charts.poverty.deep.by_age import DeepPovertyByAgeChart from policyengine.charts.poverty.regular.by_gender import RegularPovertyByGenderChart from policyengine.charts.poverty.deep.by_gender import DeepPovertyByGenderChart +from policyengine.charts.distributional_impact.by_income_decile.average import ByIncomeDecileAverageChart +from policyengine.charts.distributional_impact.by_income_decile.relative import ByIncomeDecileRelativeChart +from policyengine.charts.distributional_impact.by_wealth_decile.average import ByWealthDecileAverageChart +from policyengine.charts.distributional_impact.by_wealth_decile.relative import ByWealthDecileRelativeChart class EconomicImpact: @@ -184,6 +188,10 @@ def __init__(self, reform: dict, country: str, dataset: str = None) -> None: "poverty/regular/by_gender": RegularPovertyByGenderChart, "poverty/deep/by_age": DeepPovertyByAgeChart, "poverty/deep/by_gender": DeepPovertyByGenderChart, + "distributional/by_income/average": ByIncomeDecileAverageChart, + "distributional/by_income/relative": ByIncomeDecileRelativeChart, + "distributional/by_wealth/average": ByWealthDecileAverageChart, + "distributional/by_wealth/relative": ByWealthDecileRelativeChart } self.composite_metrics: Dict[str, Dict[str, str]] = { From 88ef7124e7ce18544f305442a308d7a756583d84 Mon Sep 17 00:00:00 2001 From: Mohammed Ismail Date: Tue, 17 Sep 2024 17:45:32 +0530 Subject: [PATCH 5/5] minor fixes --- .../charts/distributional_impact/by_income_decile/relative.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/policyengine/charts/distributional_impact/by_income_decile/relative.py b/policyengine/charts/distributional_impact/by_income_decile/relative.py index 6269be9..c7795ff 100644 --- a/policyengine/charts/distributional_impact/by_income_decile/relative.py +++ b/policyengine/charts/distributional_impact/by_income_decile/relative.py @@ -99,4 +99,6 @@ def generate_chart_data(self): font=dict(color="black", size=16) ), title="Relative Change in Household Income by Decile" - ) \ No newline at end of file + ) + format_fig(fig) # Keep the formatting logic from policyengine_core + return fig \ No newline at end of file