## 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'

# Display the first few rows of the dataframe

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'); 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('-') 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,
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); ### 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('-') 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_tickformat=',.0%', # To change the tick labels to percentages
)

# 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)