Source code for postreise.plot.plot_bar_generation_vs_capacity

import matplotlib.pyplot as plt
import pandas as pd
from powersimdata.network.model import ModelImmutables, area_to_loadzone
from powersimdata.scenario.scenario import Scenario

from postreise.analyze.generation.capacity import sum_capacity_by_type_zone
from postreise.analyze.generation.summarize import sum_generation_by_type_zone


[docs]def plot_bar_generation_vs_capacity( areas, area_types=None, scenario_ids=None, scenario_names=None, time_range=None, time_zone=None, custom_data=None, resource_types=None, resource_labels=None, horizontal=False, plot_show=True, ): """Plot any number of scenarios as bar or horizontal bar charts with two columns per scenario - generation and capacity. :param list/str areas: list of area(s), each area is one of *loadzone*, *state*, *state abbreviation*, *interconnect*, *'all'*. :param list/str area_types: list of area_type(s), each area_type is one of *'loadzone'*, *'state'*, *'state_abbr'*, *'interconnect'*, defaults to None. :param int/list/str scenario_ids: list of scenario id(s), defaults to None. :param list/str scenario_names: list of scenario name(s) of same len as scenario ids, defaults to None. :param tuple time_range: [start_timestamp, end_timestamp] where each time stamp is pandas.Timestamp/numpy.datetime64/datetime.datetime. If None, the entire time range is used for the given scenario. :param str time_zone: new time zone, defaults to None, which uses UTC. :param list custom_data: list of dictionaries with each element being hand-generated data as returned by :func:`make_gen_cap_custom_data`, defaults to None. :param list/str resource_types: list of resource type(s) to show, defaults to None, which shows all available resources in the area of the corresponding scenario. :param dict resource_labels: a dictionary with keys being resource_types and values being labels to show in the plots, defaults to None, which uses resource_types as labels. :param bool horizontal: display bars horizontally, default to False. :return: (*matplotlib.axes.Axes*) -- axes object of the plot. :raises TypeError: if ``resource_labels`` is not a dict. :raises ValueError: if length of ``area_types`` and ``areas`` is different. if length of ``scenario_names`` and ``scenario_ids`` is different. if only one scenario is provided with no ``custom_data``. """ if isinstance(areas, str): areas = [areas] if isinstance(area_types, str): area_types = [area_types] if not area_types: area_types = [None] * len(areas) if len(areas) != len(area_types): raise ValueError("area_types must have same size as areas") if not scenario_ids: scenario_ids = [] if isinstance(scenario_ids, (int, str)): scenario_ids = [scenario_ids] if isinstance(scenario_names, str): scenario_names = [scenario_names] if scenario_names and len(scenario_names) != len(scenario_ids): raise ValueError("scenario_names must have same size as scenario_ids") if not custom_data: custom_data = {} if len(scenario_ids) + len(custom_data) <= 1: raise ValueError("two scenario and/or custom data must be provided") if isinstance(resource_types, str): resource_types = [resource_types] if not resource_labels: resource_labels = dict() if not isinstance(resource_labels, dict): raise TypeError("resource_labels must be a dict") all_loadzone_data = {} scenario_data = {} for i, sid in enumerate(scenario_ids): scenario = Scenario(sid) mi = ModelImmutables(scenario.info["grid_model"]) all_loadzone_data[sid] = { "gen": sum_generation_by_type_zone(scenario, time_range, time_zone).rename( columns=mi.zones["id2loadzone"] ), "cap": sum_capacity_by_type_zone(scenario).rename( columns=mi.zones["id2loadzone"] ), } scenario_data[sid] = { "name": scenario_names[i] if scenario_names else scenario.info["name"], "grid_model": mi.model, "gen": {"label": "Generation", "unit": "TWh", "data": {}}, "cap": {"label": "Capacity", "unit": "GW", "data": {}}, } for area, area_type in zip(areas, area_types): for sid in scenario_ids: zone_list = list( area_to_loadzone(scenario_data[sid]["grid_model"], area, area_type) ) scenario_data[sid]["gen"]["data"][area] = ( all_loadzone_data[sid]["gen"][zone_list] .sum(axis=1) .divide(1e6) .astype("float") .round(2) .to_dict() ) scenario_data[sid]["cap"]["data"][area] = ( all_loadzone_data[sid]["cap"][zone_list] .sum(axis=1) .divide(1e3) .astype("float") .round(2) .to_dict() ) for c_data in custom_data: scenario_data[c_data["name"]] = c_data for area in areas: if not resource_types: area_resource_types = sorted( set( r for sd in scenario_data.values() for side in ["gen", "cap"] for r, v in sd[side]["data"][area].items() if v > 0 ) ) else: area_resource_types = resource_types ax_data_list = [] for side in ["gen", "cap"]: ax_data = {} for sd in scenario_data.values(): # If we don't have data for a resource type, set it to 0 ax_data[sd["name"]] = [ sd[side]["data"][area].get(r, 0) for r in area_resource_types ] ax_data_list.append( { "title": f"""{sd[side]["label"]} ({sd[side]["unit"]})""", "labels": [resource_labels.get(r, r) for r in area_resource_types], "values": ax_data, "unit": sd[side]["unit"], } ) if horizontal: axes = _construct_hbar_visuals(area, ax_data_list) else: axes = _construct_bar_visuals(area, ax_data_list) if plot_show: plt.show() else: return axes
def _construct_bar_visuals(zone, ax_data_list): """Plot bar chart based on formatted data. :param str zone: the zone name :param list ax_data_list: a list of labels and values for each axis of the plot """ num_scenarios = len(ax_data_list[0]["values"].keys()) num_resource_types = len(ax_data_list[0]["labels"]) fig, axes = plt.subplots( 1, 2, figsize=(1.5 * num_scenarios * num_resource_types, 6) ) plt.suptitle(zone, fontsize=30, verticalalignment="bottom") plt.subplots_adjust(wspace=3 / (num_scenarios * num_resource_types)) for ax_data, ax in zip(ax_data_list, axes): df = pd.DataFrame(ax_data["values"], index=ax_data["labels"]) df.plot(kind="bar", ax=ax, edgecolor="white", linewidth=2) ax.set_title(ax_data["title"], fontsize=25) ax.tick_params(axis="both", which="both", labelsize=20) ax.spines["right"].set_visible(False) ax.spines["top"].set_visible(False) ax.spines["left"].set_visible(False) ax.set_xlabel("") ax.set_xticklabels( ax.get_xticklabels(), rotation=45, horizontalalignment="right" ) ax.set_yticks([]) ax.set_ylim(top=1.3 * ax.get_ylim()[1]) ax.legend(bbox_to_anchor=(-0.03, -0.4), loc="upper left", fontsize=16) for p in ax.patches: b = p.get_bbox() ax.annotate( _get_bar_display_val(b.y1), ((b.x1 + b.x0) / 2, b.y1 + 0.02 * ax.get_ylim()[1]), fontsize=10, rotation="horizontal", horizontalalignment="center", ) axes[1].get_legend().remove() return axes def _construct_hbar_visuals(zone, ax_data_list): """Plot horizontal bar chart based on formatted data. :param str zone: the zone name. :param list ax_data_list: a list of labels and values for each axis of the plot """ num_scenarios = len(ax_data_list[0]["values"].keys()) num_resource_types = len(ax_data_list[0]["labels"]) fig, axes = plt.subplots( 1, 2, figsize=(20, 0.7 * num_scenarios * num_resource_types) ) plt.suptitle(zone, fontsize=30, verticalalignment="bottom") plt.subplots_adjust(wspace=1) for ax_data, ax in zip(ax_data_list, axes): df = pd.DataFrame(ax_data["values"], index=ax_data["labels"]) df.plot(kind="barh", ax=ax, edgecolor="white", linewidth=2) ax.set_title(ax_data["title"], fontsize=25) ax.tick_params(axis="y", which="both", labelsize=20) ax.set_xticklabels("") ax.set_ylabel("") ax.spines["right"].set_visible(False) ax.spines["top"].set_visible(False) ax.spines["bottom"].set_visible(False) ax.set_xticks([]) handles, labels = ax.get_legend_handles_labels() ax.legend( reversed(handles), reversed(labels), bbox_to_anchor=(-0.03, 0), loc="upper left", fontsize=16, ) for p in ax.patches: b = p.get_bbox() ax.annotate( _get_bar_display_val(b.x1), (b.x1, b.y1 - 0.02), fontsize=14, verticalalignment="top", ) return axes def _get_bar_display_val(val): """Format the display value for a single bar. :param float val: the original value. :return: (*int/float*) -- the formatted value. """ if val >= 10: return int(round(val, 0)) rounded = round(val, 1) return rounded if rounded > 0 else 0
[docs]def make_gen_cap_custom_data(areas, label, gen_data=None, cap_data=None): """Format custom data for :func:`plot_bar_generation_vs_capacity`. :param list/str areas: list of interest area(s). :param str label: the name of the custom scenario to be shown in the plot. :param pandas.DataFrame gen_data: generation data with rows being resource types and columns being areas in TWh, default to None. :param pandas.DataFrame cap_data: capacity data with rows being reousrce types and columns being areas in GW, defaults to None :return: (*dict*) -- formatted custom data. """ if isinstance(areas, str): areas = [areas] if gen_data is None: gen_data = pd.DataFrame() if cap_data is None: cap_data = pd.DataFrame() gen = dict() cap = dict() for area in areas: if area in gen_data.columns: gen.update(gen_data[[area]].round(2).to_dict()) else: gen[area] = {} if area in cap_data.columns: cap.update(cap_data[[area]].round(2).to_dict()) else: cap[area] = {} return { "name": label, "gen": {"label": "Generation", "unit": "TWh", "data": gen}, "cap": {"label": "Capacity", "unit": "GW", "data": cap}, }