13 min read

Categories

Contents

Introduction: what is a Windrose?

A windrose, or a polar rose plot, is polar histogram that shows the distribution of wind speeds and directions over a period of time. It is a useful tool for visualising how wind speed and direction are typically distributed at a given location. The data is divided into direction intervals, and the frequency of wind speeds in each direction is represented by the radius of the bars. Typically the data is also binned into speed intervals and plotted as a set of stacked bars, where each bar represents a speed interval. The height of the bar is the frequency of wind speeds in that interval, and the radius of the bar is the sum of the frequencies of wind speeds in that interval.

In this tutorial we will learn:

  • How to convert raw wind speed and direction data into a format suitable for plotting a windrose
  • How to plot a windrose using Matplotlib and Plotly
  • How to use Plotly and ipywidgets to create an interactive windrose

This blogpost can be found as a Colab notebook here.

Data preparation

The data used in this tutorial is available here. To following along download it and replace filepath below with the location at which you saved the data.

Data source

We have a dataframe containing wind speed and direction data measured at Coimbatore airport in India.

  • SPD - wind speed in m/s
  • DIR - wind direction in degrees
  • MM - month of the year between 1 and 12

Let us take a look at the first few rows of the data:

import calendar
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

# Modify to point to location where your data is saved 
filepath = 'coimbatore-airport-2016.csv' 
# Load data
df = pd.read_csv(filepath)

# Display the first few rows of the dataframe
df.head()
NAME HR_TIME YYYY MM DD HR MN TEMP DEWP SPD DIR
0 433210-99999 2016010100 2016 1 1 0 0 65 62 5 360
1 433210-99999 2016010100 2016 1 1 0 0 64 63 5 360
2 433210-99999 2016010100 2016 1 1 0 30 64 63 3 360
3 433210-99999 2016010101 2016 1 1 1 0 64 63 3 360
4 433210-99999 2016010102 2016 1 1 2 30 66 63 3 20

Histogram Creation

First, we need to transform our raw wind data, which is a crucial step in constructing a windrose plot.

The windrose_histogram function partitions the raw wind speed and direction data into bins, counts the frequency of observations in each bin, and optionally normalizes these frequencies. The result is a 2D histogram, ideal for creating windrose plots.

def windrose_histogram(wspd, wdir, speed_bins=12, normed=False, norm_axis=None):
    """
    Compute a windrose histogram given wind speed and direction data.
    
    wspd: array of wind speeds
    wdir: array of wind directions
    speed_bins: Integer or Sequence, defines the bin edges for the wind speed (default is 12 equally spaced bins)
    normed: Boolean, optional, whether to normalize the histogram values. (default is False)
    norm_axis: Integer, optional, the axis along which the histograms are normalized (default is None) 
    """

    # If speed_bins is an integer, we create linearly spaced bins from 0 to max speed
    if isinstance(speed_bins, int):
        speed_bins = np.linspace(0, wspd.max(), speed_bins)

    num_spd = len(speed_bins)
    num_angle = 16

    # Shift wind directions by 11.25 degrees (one sector) to ensure proper alignment
    wdir_shifted = (wdir + 11.25) % 360

    angle_bins = np.linspace(0, 360, num_angle + 1)

    # Generate a 2D histogram using the defined speeds bins and shifted wind directions
    hist, *_ = np.histogram2d(wspd, wdir_shifted, bins=(speed_bins, angle_bins))

    # Normalize if required
    if normed:
        hist /= hist.sum(axis=norm_axis, keepdims=True)
        hist *= 100

    return hist, angle_bins, speed_bins

This windrose_histogram function partitions the wind speed and direction data into bins, counts the frequency of observations in each bin, and optionally normalizes these frequencies. The result is a 2D histogram, ideal for creating windrose plots.

Data Transformation

Before diving into the intricacies of windrose plotting, we must first transform our raw wind data into a structured format that is more suitable for analysis. To achieve this, we’ll employ the following function:

DIRECTION_NAMES = ("N","NNE","NE","ENE"
                   ,"E","ESE","SE","SSE"
                   ,"S","SSW","SW","WSW"
                   ,"W","WNW","NW","NNW")

DIRECTION_ANGLES = np.arange(0, 2*np.pi, 2*np.pi/16)

# Mapping from direction name to angles in radians
NAME2ANGLE = dict(zip(
    DIRECTION_NAMES,
    DIRECTION_ANGLES
))

def make_wind_df(data_df, num_partitions, max_speed=None, normed=False, norm_axis=None, month=None):
    """
    This function transforms raw wind speed and direction data into a DataFrame for windrose plotting.
    
    data_df: Dataframe containing wind data
    num_partitions: Integer, number of partitions to divide the wind speed data
    max_speed: Float, optional, maximum wind speed to be included in the partitions
    normed: Boolean, optional, whether to normalize the frequency values
    norm_axis: Integer, optional, the axis along which the histograms are normalized  
    """
    
    if month is not None:
        data_df = data_df[data_df.MM==month]
        
    wspd = data_df['SPD'].values
    wdir = data_df['DIR'].values

    # If max_speed is not specified, we use the maximum value in the 'wspd' data. 
    # Otherwise, we include all speeds up to and including max_speed.
    # Additional partitions are created to handle outliers.
    if max_speed is None:
        speed_bins = np.linspace(0, wspd.max(), num_partitions + 1)
    else:
        speed_bins = np.append(np.linspace(0, max_speed, num_partitions + 1), np.inf)
    
    # windrose_histogram function is called to partition data based on the bins created.
    # Additional parameters control how the frequency values are normalised

    h, *_ = windrose_histogram(wspd, wdir, speed_bins, normed=normed, norm_axis=norm_axis)

    # A dataframe is formed containing the histogram data. Column names are for the directions 
    wind_df = pd.DataFrame(data=h, columns=DIRECTION_NAMES)

    # speed_bin_names stores speed range strings to describe each interval
    # e.g. when speed_bins is [0, 3, 6], speed_bin_names is ['0-3', '3-6', '>6']
    speed_bin_names = []
    speed_bins_rounded = [round(i, 2) for i in speed_bins]
    for start, end in zip(speed_bins_rounded[:-1], speed_bins_rounded[1:]):
        speed_bin_names.append(f'{start:g}-{end:g}' if end < np.inf else f'>{start:g}')

    wind_df['strength'] = speed_bin_names

    # Reshapes data for plotting. Now, each row represents one sector of the windrose
    wind_df = wind_df.melt(id_vars=['strength'], var_name='direction', value_name='frequency')

    return wind_df

Here is what the dataframe looks like

make_wind_df(df, num_partitions=4).head(12)
strength direction frequency
0 0-6.25 N 189.0
1 6.25-12.5 N 14.0
2 12.5-18.75 N 0.0
3 18.75-25 N 0.0
4 0-6.25 NNE 648.0
5 6.25-12.5 NNE 145.0
6 12.5-18.75 NNE 1.0
7 18.75-25 NNE 0.0
8 0-6.25 NE 815.0
9 6.25-12.5 NE 389.0
10 12.5-18.75 NE 15.0
11 18.75-25 NE 0.0

Making the plots

Using Matplotlib

Matplotlib is a versatile and comprehensive library in Python that allows for a wide variety of plotting types, such as scatter plots, bar charts, and line charts.

While Matplotlib does not have out-of-the-box capabilities to create a windrose, thanks to the library’s flexibility, we can make a plot which resembles a windrose using polar bar diagrams, where the height of the bars demonstrates the frequency of the wind speed, and the orientation of the bars represents the wind direction.

A polar plot is a diagram in which data points are represented in a polar coordinate system. In this system, a point’s location is determined by its distance from the centre (known as the radial coordinate $\rho$ or $r$) and by the angle from a reference direction (known as the angular coordinate $\phi$ or $\theta$).

Here is an example of a polar plot which shows an Archimedean spiral which can be represented by a linear polar equation

\[r(\phi) = a + b\phi\]

We plot it with

\[a = 0, b = \frac{1}{2\pi} \quad \text{and} \quad a = 2, b = \frac{1}{4\pi}\]
phi = np.linspace(0, 10*np.pi, 1001)
avals = [0, 2]
bvals = [1/(2*np.pi), 1/(4*np.pi)]
blabels = ['2\pi', '4\pi']

fig, axes = plt.subplots(1, 2, subplot_kw={'projection': 'polar'}, figsize=(10,4))
for clr, axis, a, b, bl in zip(['mediumblue', 'darkorange'], axes, avals, bvals, blabels):
    axis.plot(phi, a + b*phi, linewidth=2, color=clr);
    axis.set_title(f'$r(\phi) = {(str(a) + " + ")  if a > 0 else ""}'
      +"\\frac{\phi}{" + f'{bl}' +' }$' )
plt.suptitle(' Archimedean spirals');

png

In the context of windroses, we use polar plots for illustrating the wind direction (angular coordinate) and the wind speed (radial coordinate).

def matplotlib_windrose(data_df, num_partitions=4, max_speed=4, month=None):
    """
    Function to create a windrose plot using matplotlib.
    
    Args:
        data_df: Dataframe containing wind data
        num_partitions: number of partitions for wind strength
        max_speed: maximum wind speed to be considered while partitioning the wind strength
        month: (optional) a string representing the month, used in defining the DataFrame for plotting

    Returns:
        fig: a matplotlib Figure object containing the windrose plot 
    """

    # calls the make_wind_df function to create a dataframe which 
    # reshapes raw wind speed and direction data for windrose plotting
    wind_df2 = make_wind_df(data_df=data_df, 
                        num_partitions=num_partitions, 
                        max_speed=max_speed, 
                        normed=True,
                        month=month)
    
    wind_df2['frequency'] = wind_df2['frequency']/ 100

    # modifies the dataframe by extracting start strength based on certain conditions in each strength value
    wind_df2 = wind_df2.assign(strength_start=[float(x.split('-')[0] if '-' in x else x[1:])
                                           for x in wind_df2.strength.values])
    
    # converts the mapped direction names to angles and sorts the dataframe according to the strength start
    wind_df2['angle'] = wind_df2.direction.map(NAME2ANGLE)
    wind_df2 = wind_df2.sort_values(by='strength_start')
    
    # The command below does three things sequentially:
    # 1. It sorts the dataframe 'wind_df2' in ascending order according to 'strength_start'.
    # 2. It groups this sorted dataframe by 'direction' so the each group contains the sorted values of 'strength_start' for the corresponding direction.
    # 3. It cumulatively sums the frequencies within each group (i.e., each unique wind direction) direction. 
    # The cumulative frequency value for a given strength_start value is the sum of the frequencies of all the strength_start values that are less than or equal to the given strength_start value. The reason for doing this is to enable us to plot stacked bars in the windrose plot.
    wind_df2['cumulative_frequency'] = wind_df2.sort_values(by='strength_start').groupby('direction').frequency.cumsum()
    
    # Use black background 
    with plt.style.context('dark_background'):
        # create a polar plot (windrose) with given figure size
        fig, axis = plt.subplots(figsize=(8, 8), ncols=1, subplot_kw=dict(projection="polar"))
        
        # adds a grid to the plot in white since background will be dark
        axis.grid(color='white')

        # extracts values from the 'strength_start' column of the dataframe
        strength_starts = wind_df2.strength_start.values  

        # defines the colormap to be used for different strength partitions in the windrose
        colours = plt.cm.magma_r(
            np.linspace(0, 1,  wind_df2.strength.nunique())
        )

        # groups the dataframe by 'strength'
        strength_splits = wind_df2.groupby('strength')

        # plots a bar chart for each unique strength value in the windrose plot 
        # in descending order of strength to make the bars appear stacked on top of each other
        # since the frequency values are cumulative, the bars are stacked on top of each other
        for clr, strength in list(zip(colours, wind_df2.strength.unique()))[::-1]:
            split = strength_splits.get_group(strength)
            # Note that the width is slightly less than 22.5 degrees (i.e., 360/16) so that the bars are slightly separated from each other which makes the plot look better
            # zorder=2 ensures grid does not appear on top of the bars
            axis.bar(split['angle'].values, 
                     split['cumulative_frequency'].values, 
                     color=clr, 
                     label=strength, 
                     width=np.deg2rad(19), 
                     edgecolor='black',
                     linewidth=0.5,
                     zorder=2)

        # sets direction names as xticks in the windrose plot
        axis.set_xticks(DIRECTION_ANGLES)
        axis.set_xticklabels(DIRECTION_NAMES)
        

        # adds a legend to the plot
        handles, labels = axis.get_legend_handles_labels()
        axis.legend(
           handles[::-1], 
           labels[::-1],
           loc='upper left', 
           bbox_to_anchor=(1.1, 1.1)
        );

        # sets the zero location of the theta axis to North
        axis.set_theta_zero_location('N')

        # sets the direction of the theta axis to clockwise (-1)
        axis.set_theta_direction(-1)

        axis.set_title('Wind rose ({})'.format(
                               calendar.month_name[month] if month in range(1, 13) else 'Annual'
                           ))
        
        axis.set_rlabel_position(135)
        yticks = axis.get_yticks()
        ytick_labels = [('{:.2f}'.format(round(i * 100, 2))[:-1]).rstrip('0').rstrip('.') + '%' for i in yticks]
        axis.set_yticks( yticks)
        axis.set_yticklabels(ytick_labels)
        
    # returns the figure containing the windrose plot
    return fig

To use the matplotlib_windrose function, you pass in the wind dataframe plus additional arguments to vary the appearance of the plot. Below we shall see how to add interactivity so that we can vary these parameters and update the plot each time.

matplotlib_windrose(df, num_partitions=8, max_speed=25);

png

Implementing with Plotly

For an alternative approach, we turn to Plotly, another Python library known for its advanced plotting capabilities. We can leverage Plotly Express to achieve a straightforward realisation of a windrose plot.

The plotly_windrose function operates similarly to the make_wind_df function but utilizing Plotly’s ‘px.bar_polar’ to create the windrose plot, enhancing the plot with interactive capabilities.

import plotly.express as px

def plotly_windrose(data_df, num_partitions=4, max_speed=4, month=None):
    """
    Function to generate a windrose plot using Plotly.
    
    Args:
        data_df: Dataframe containing wind data
        num_partitions: number of partitions for wind strength
        max_speed: maximum wind speed to be considered while partitioning the wind strength
        month: (optional) a string representing the month, used in defining the DataFrame for plotting

    Returns:
        plot: a Plotly Figure object containing the windrose plot 
    """
    
    # Convert raw data into a windrose-friendly format
    wind_df = make_wind_df(data_df=data_df, 
                       num_partitions=num_partitions, 
                       max_speed=max_speed, 
                       normed=True,
                       month=month
                     )
    # Normalize frequency to a percentage
    wind_df['frequency'] = wind_df['frequency'] / 100
    
    
    # Sort strength bins in order
    strengths = sorted(wind_df.strength.unique(), key=lambda x:float(x.split('-')[0] if '-' in x else x[1:]))
    
    # defines the colormap to be used for different strength partitions in the windrose
    colours =px.colors.sample_colorscale(px.colors.get_colorscale('Magma_r'), len(strengths))
    
    colour_dict = dict(zip(strengths, colours))

    # Create a polar bar plot with the specified properties
    fig = px.bar_polar(wind_df, r="frequency", theta="direction", 
                       color="strength",
                       color_discrete_map=colour_dict,
                       template="plotly_dark",
                       title='Wind rose ({})'.format(
                           calendar.month_name[month] if month in range(1, 13) else 'Annual'
                       )
                   )
    # Update the polar plot parameters to enhance readability and esthetics
    fig.update_polars(
        radialaxis_angle = -45, # To rotate the radial axis
        radialaxis_tickangle=-45, # To rotate the tick labels on radial axis
        radialaxis_tickformat=',.0%', # To change the tick labels to percentages
        radialaxis_tickfont_color='white', # To change tick labels to white for better readability,
    )
    
    # Make figure square
    fig.update_layout(
        autosize=False, # To prevent automatic adjustment of figure size
        width=500, # To set figure width to 500 pixels
        height=500, # To set figure height to 500 pixels
    )
    
    return fig

Creating a plot with plotly_windrose we can see that it looks very similar to the previous plot but with Plotly’s interactive features.

plotly_windrose(df, num_partitions=8, max_speed=25)


Interactive plotting using Matplotlib and Jupyter Widgets

The plots so far have shown windroses using the entire dataset. However, wind patterns can vary significantly depending on the time of year. We can use Jupyter widgets to create an interactive windrose that allows us to select the month of the year to plot.

In addition, we can also choose the number of partitions and the maximum wind speed to be included in the plot. These settings will give use greater control over the appearance of the windrose.

Widgets are interactive controls for python within Jupyter notebooks, allowing you to dynamically change variables and view the changes in output.

To create an interactive widget, we can use the interact function of the ipywidgets library. The interact function automatically creates user interface (UI) controls for function arguments, and then calls the function with those arguments when you manipulate the controls interactively.

In addition our widget will let us choose which of the two plotting libraries we want to use, Matplotlib or Plotly.

from ipywidgets import interact, fixed, IntSlider, Dropdown, Layout, Output

month_dict = {('Annual' if i == 0 else calendar.month_name[i]):i  for 
              i in range(13)}




def interactive_windrose(num_partitions, max_speed, month, method):
    fn = {'Plotly': plotly_windrose, 'Matplotlib': matplotlib_windrose}[method]
    f = fn(df, num_partitions=num_partitions, 
                        max_speed=max_speed, month=month if month > 0 else None)
    if method == 'Plotly':
        f.show()

interact(interactive_windrose, 
    num_partitions=IntSlider(value=5, min=2, max=33, description='No. partitions', 
                             layout=Layout(width='400px'),
                              style=dict(description_width='initial')),
    max_speed=IntSlider(value=17, min=2, max=33, description='Max wind speed (m/s)', 
                        layout=Layout(width='450px'),
                        style=dict(description_width='initial')),
    month=Dropdown(options=month_dict, value=3, description='Month',
                   layout=Layout(margin="0px 0px 10px 0px"),
                        style=dict(description_width='initial')),
    method=Dropdown(options=['Plotly', 'Matplotlib'], value='Plotly', description='Method',
                   layout=Layout(margin="0px 0px 25px 0px"),
                        style=dict(description_width='initial'))
);

This is what the widget looks like. You can try it out for yourself in the Colab for this blog.

Screenshot of widget showing windrose for March plotted with Plotly with num_partitions=5, max_speed=17

Conclusion

In this tutorial, we have walked through the process of plotting a windrose, a polar plot which shows the distribution of wind speeds and directions over time. We covered the essential knowledge behind windroses, walked through some methods of data preparation, and learned to generate windroses using two notable Python libraries - Matplotlib and Plotly. Furthermore, we touched upon how to add interactivity to our windrose plots with Jupyter widgets, enhancing their usefulnesses. The techniques covered in this tutorial can be leveraged in meteorology, climate studies, aviation and other fields requiring wind data analysis and visualisation.