Source code for rosely.windrose

# -*- coding: utf-8 -*-
"""
Tools for creating interactive wind rose diagrams and summary statistics using plotly and pandas.
"""

from pathlib import Path
import numpy as np
import pandas as pd
import plotly.express as px
from plotly.offline import plot
import plotly.io as pio

[docs]class WindRose(object): """ Manage data for calculating wind statistics and provide simple interface for creating customizable wind rose diagrams. Attributes: df (:obj:`pandas.DataFrame`): arbitrary :obj:`pandas.DataFrame` that is assigned to a :obj:`WindRose` object that must contain wind speed and direction columns before using other :obj:`WindRose` methods. theta_labels (list): 16 point compass labels for wind rose diagrams. theta_angles (:obj:`numpy.ndarray`): array of 11.25 degree intervals for 16 point compass. wind_df (:obj:`pandas.DataFrame`): calculated wind statistics produced by :meth:`WindRose.calc_stats` and used by :meth:`WindRose.plot`. """ # 16 point compass for now theta_labels = [ 'N','NNE','NNE','NE','NE','ENE','ENE','E','E','ESE','ESE','SE','SE', 'SSE','SSE','S','S','SSW','SSW','SW','SW','WSW','WSW','W','W','WNW', 'WNW','NW','NW','NNW','NNW','N' ] theta_angles = np.arange(0, 360.1, 11.25) def __init__(self, df=None): if df is not None and not isinstance(df, pd.DataFrame): raise TypeError("Must assign a pandas.DataFrame object") self._df = df self._plot_ready = False self.wind_df = None @property def df(self): """ :obj:`pandas.DataFrame` containing input time series wind data needed to run :meth:`WindRose.plot`. """ if isinstance(self._df, pd.DataFrame): return self._df @df.setter def df(self, df): if not isinstance(df, pd.DataFrame): raise TypeError("Must assign a pandas.DataFrame object") self._df = df
[docs] def calc_stats(self, normed=True, bins=9, variable_names=None): """ Calculate wind speed and direction bins needed for generating wind rose diagrams. After running :meth:`WindRose.calc_stats` with different options a new instance attribute :attr:`WindRose.wind_df` is generated that contains the binned wind speed statistics. This attribute is in the form of a :obj:`pandas.DataFrame` and can be used to create a histogram or saved to disk. Keyword Arguments: normed (bool): default True. If True compute wind speed/direction frequency bins that are normalized to sum to 100. If False frequency bins are counts of occurences of wind speed/direction. bins (int or list): default 9. Number of wind speed and direction bins to calculate. 9 is used because most :mod:`plotly` color sequences are lenght 9 or 10 which are later used by :meth:`WindRose.plot` variable_names (None or dict): default None. If none the wind speed and wind direction columns in :attr:`WindRose.df` should be named 'ws' and 'wd' respectively. Otherwise a dictionary that maps the respective columns to 'ws' and 'wd' should be provided. Returns: None Example: Assuming you have a :obj:`pandas.DataFrame` loaded that has wind speed and direction columns titled 'wind_speed' and 'wind_direction' and the dataframe is named ``df``: >>> from rosely import WindRose >>> WR = WindRose(df) >>> names = {'wind_speed':'ws', 'wind_direction':'wd'} >>> WR.calc_stats(normed=False, bins=8, variable_names=names) Now ``WR.wind_df`` should have the appropirate statistics and the :meth:`WindRose.plot` will use these statistics for the polar stacked histogram (wind rose). """ wind = self._df.copy() if not isinstance(wind, pd.DataFrame): print( 'Must assign a pandas.DataFrame to the WindRose object with ' 'wind speed and direction before running calculations.' ) return if variable_names: wind.rename(columns=variable_names, inplace=True) if not {'ws', 'wd'}.issubset(wind.columns): raise KeyError('ERROR: missing "ws" and "wd" columns, aborting.') wind = wind.replace([np.inf, -np.inf], np.nan).dropna( subset=["ws", "wd"], how="any" ) spd_bins = pd.cut(wind.ws, bins=bins).dropna() spd_bins.name = 'spd_bins' wind = wind.join(spd_bins) dir_bins = pd.cut(wind.wd, bins=WindRose.theta_angles).dropna() dir_bins.name = 'dir_bins' wind = wind.join(dir_bins) def rename_bins(x): return '{:.2f}-{:.2f}'.format(x.left,x.right) wind.spd_bins = wind.spd_bins.apply(rename_bins) wind = wind.groupby(['dir_bins','spd_bins']).count().dropna() wind['direction'] = wind.index.get_level_values('dir_bins') wind['speed'] = wind.index.get_level_values('spd_bins') wind = wind[ ['direction','speed', 'ws'] ].droplevel(0).reset_index().drop('spd_bins',axis=1) wind.rename(columns={'ws':'frequency'}, inplace=True) # rename to compass directions tmp = wind.groupby('direction').count() tmp['labels'] = WindRose.theta_labels mapping_dict = tmp.labels.to_dict() wind.direction = wind.direction.map(mapping_dict) if normed: wind.frequency /= wind.frequency.sum() wind.frequency *= 100 wind.frequency = wind.frequency.round(2) if isinstance(bins, list): bins = len(bins) self.n_bins = bins self.wind_df = wind self._plot_ready = True
[docs] def plot(self, output_type='save', out_file=None, colors='Plasma', template='plotly_dark', colors_reversed=True, **kwargs): """ Create interactive wind rose diagrams with easily customizable options using Plotly's polar bar chart. Keyword Arguments: output_type (str): default 'save'. If 'save' save graph to ``out_file``. Other options: 'show' will show in a new tab in web browser or within a Jupyter Notebook, and 'return' will return the plotly figure for further manual customization/modification or use in custom workflows like saving as a subplot with other plot figures. out_file (None or str): default None. If ``output_type='save'`` then save to specified path, if None save to current working directory as "windrose.html". colors (str): default 'Plasma'. Name of Plotly color swatch or sequence to use for coloring bins from center outward on wind rose. See :ref:`Tutorial` for examples and all options. Can also pass a list of hex or rgb colors of your own. template (str): default 'plotly_dark'. Name of Plotly template for background theme/colors on wind rose. colors_reversed (bool): True. If True reverse the colors in ``colors``. The first color in the sequence will be used for the lightest wind speed bin. **kwargs: other keyword arguments are passed to the ``plotly.express.bar_polar`` plot function, e.g. title or width. Returns (None or :obj:`plotly.graph_objects.Figure`) Example: Assuming a :obj:`pandas.DataFrame` object called "df" has been loaded and contains columns 'ws' and 'wd' with wind speed and direction, >>> from rosely import WindRose >>> WR = WindRose(df) # df already loaded pandas dataframe >>> # if you skip running WR.calc_stats the defaults will be used >>> WR.plot(output_type='show', colors='Greens', >>> colors_reversed=False) This will produced a normalized frequency wind rose (frequency 0-100 percent) with 9 bins. To specify the to use count frequency or a different number of bins use the :meth:`WindRose.calc_stats` method before running :meth:`WindRose.plot`. Tip: To see a list of all provided color sequences provided by Plotly, >>> import plotly.express as px >>> px.colors.sequential.swatches() All of the listed color schemes can be passed to the ``colors`` arugment of :meth:`WindRose.plot`. """ if template not in pio.templates.keys(): print('ERROR: invalid plotly template use one of:\n{}' .format(', '.join(pio.templates.keys()))) return if not self._plot_ready: print( 'Wind speed and direction statistics have not been calculated, ' 'Calculating them now using default parameters.' ) self.calc_stats() if not hasattr(px.colors.sequential, colors) and not \ isinstance(colors, list): print( 'ERROR: {} is not a valid plotly color sequence, using default.' ) colors = px.colors.sequential.Plasma elif isinstance(colors, str): colors = getattr(px.colors.sequential, colors) if self.n_bins > len(colors): print( 'Warning: number of bins exceed number of colors, some ' 'colors may repeat.' ) if colors_reversed: colors = colors[::-1] fig = px.bar_polar( self.wind_df, r="frequency", theta="direction", color="speed", template=template, color_discrete_sequence=colors, category_orders={'direction': [ 'N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'] }, **kwargs ) # handle file save for accessing from instance variable if out_file is None and output_type == 'save': out_file = Path.cwd()/'windrose.html' out_dir = out_file.parent if not out_dir.is_dir(): out_dir.mkdir(parents=True, exist_ok=True) # if out_file is to a non-existent directory create parents elif out_file is not None and output_type == 'save': out_dir = Path(out_file).parent if not out_dir.is_dir(): out_dir.mkdir(parents=True, exist_ok=True) if output_type == 'save': pio.write_html(fig, str(out_file), auto_open=False) self.out_file = out_file elif output_type == 'show': fig.show() elif output_type == 'return': return fig