Black Lives Matter. Please consider donating to Black Girls Code today.

Wind Rose with wind speed [m/s] and direction [deg] data columns. Need help

Hey all,

I am looking for a way to plot my wind rose with a dataset df with nearly 400k measurements. The two columns “wind_speed” and “wind_dir” I have are the columns containing the wind speed [m/s] and wind direction (in degrees). Could anyone suggest me a way how to plot such a wind rose?

I would like to use either plotly or plotly express in python.

I am having troubles understanding the r = ’ frequency’ thing. How do I obtain this series?

I’ll be grateful for your support!

1 Like

Hi @maciekmaj, the example in https://plot.ly/python/wind-rose-charts/ uses the px.data.wind() dataset, which corresponds to precomputed frequencies for bins of direction and speed, and not to individual measurements. To plot your wind rose, you first need to bin the data in terms of direction and wind speed, using pandas functions for this.

1 Like

Hi Emmanuelle,

Any idea how to bin the values of one column in terms of another two columns using pandas functions?
I know that its rather a pandas-related question (not plotly), but it’s quite specific for the wind rose data preparation using plotly tools. I am having exactly the same problem as @maciekmaj here.

Thank you for any help.

I managed to find a way to do it using pd.cut to bin the values accordingly and groupby to sort the binned values.

Hi @janmaj! Can you elaborate and provide a short example of how you prepared the data? It would be helpful for other people trying to make a custom windrose using plotly.

Thanks :slight_smile:

Well it’s definitely not an optimal code, I am not a programmer.
So, my data are from an offshore buoy which recorded wave heights and directions the waves come from.
Should be analogous to any other directional data.

#import data
df=pd.read_excel(r'C:\file.xlsx',sheet_name='sheetname')
df = df.replace(r'^\s*$', np.nan, regex=True) #converts blank cells to NaN
df.dropna(subset = ['magnitude(m/s)'], inplace=True) #removing rows where a cell in a given column has a NaN 

#creating bins for magnitudes and directions (directions in my dataset are in degrees, I am converting them to direction abbreviations like N, NNW, NNE, S etc

bins_mag= [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6]
bins_mag_labels = ['0.0-0.1','0.1-0.2','0.2-0.3','0.3-0.4','0.4-0.5', '0.5-0.6']

bins_dir = [0, 11.25, 33.75, 56.25, 78.75,101.25,123.75,146.25,168.75,191.25,213.75,236.25,258.75,281.25,303.75,326.25,348.75, 360.00]
bins_dir_labels = ['N','NNE','NE','ENE','E','ESE','SE','SSE','S','SSW','SW','WSW','W','WNW','NW','NNW','North']

df['mag_binned'] = pd.cut(df['magnitude(m/s)'], bins_mag, labels=bins_mag_labels)
df['dir_binned'] = pd.cut(df['direction(degrees)'],bins_dir, labels=bins_dir_labels)

dfe = df[['mag_binned', 'dir_binned','u_component(m/s)']].copy() #here i am creating a new dataframe, with necessary columns only (except the last one, which I will convert to frequencies column

dfe.rename(columns={'u_component(m/s)': 'freq'}, inplace=True)  #changing the last column to represent frequencies
g = dfe.groupby(['mag_binned','dir_binned']).count() #grouping
g.reset_index(inplace=True) 
g['percentage'] = g['freq']/g['freq'].sum()
g['percentage%'] = g['percentage']*100
g['Magnitude [m/s]'] = g['mag_binned']
print(g)
g = g.replace(r'North', 'N', regex=True) #replacing remaining Norths with N 

#Below there is a code to plot the wind rose

df = px.data.wind()
fig = px.bar_polar(g, r="percentage%", theta="dir_binned",
                   color="Magnitude [m/s]",
                   color_discrete_sequence= px.colors.sequential.Brwnyl)

#fig.update_polars(bgcolor='rgba(0,0,0,0)')
fig.update_layout(template=None,
    title_x=0.5,
    font_size=8,
    paper_bgcolor='rgba(0,0,0,0)',
    plot_bgcolor='rgba(0,0,0,0)',
    legend_font_size=10,
    legend_x=1,
    legend_y=0.89,
    legend_xanchor='right',
    polar_radialaxis_ticksuffix='%',
    polar = dict( radialaxis_angle = -180,
        radialaxis = dict(showgrid= True, showline= True, tickfont_color="Black", linecolor='rgba(70,70,70,0.5)', gridcolor="gray", tickangle = -180 , ticks="outside", showticklabels=True),
        angularaxis = dict(visible=False, showline=True, showgrid= False, linecolor="Gray", showticklabels=False, ticks ="")
    ),
    polar_angularaxis_rotation=90,
   
    font=dict(
        family="Arial",
        size=12,
        color="black")
)

fig.show()

#fig.savefig(r"path.eps",format='eps',dpi=900,bbox_inches='tight')

Here is my code, which appeared successful. I am also not a professional programmer, yet this code served me well and provided with a beautiful wind rose. I hope it helps anyone

Data prep:

import pandas as pd
import numpy as np
#%% function filtering the wind rose data based on the wind speed and direction (NEEDED BELOW)
def wind_dir_speed_freq(boundary_lower_speed, boundary_higher_speed, boundary_lower_direction, boundary_higher_direction):
    
    # mask for wind speed column
    log_mask_speed = (wind_rose_data[:,0] >= boundary_lower_speed) & (wind_rose_data[:,0] < boundary_higher_speed)
    # mask for wind direction
    log_mask_direction = (wind_rose_data[:,1] >= boundary_lower_direction) & (wind_rose_data[:,1] < boundary_higher_direction)
    
    # application of the filter on the wind_rose_data array
    return wind_rose_data[log_mask_speed & log_mask_direction]

#%% Wind rose

# Creating a pandas dataframe with 8 wind speed bins for each of the 16 wind directions.
# dataframe structure: direction | strength | frequency (radius)

wind_rose_df = pd.DataFrame(np.zeros((16*9, 3)), index = None, columns = ('direction', 'strength', 'frequency'))

directions = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW']
directions_deg = np.array([0, 22.5, 45, 72.5, 90, 112.5, 135, 157.5, 180, 202.5, 225, 247.5, 270, 292.5, 315, 337.5])
speed_bins = ['0-2 m/s', '2-4 m/s', '4-6 m/s', '6-8 m/s', '8-10 m/s', '10-12 m/s', '12-14 m/s', '14-25 m/s', '>25 m/s']

# filling in the dataframe with directions and speed bins
wind_rose_df.direction = directions * 9
wind_rose_df.strength = np.repeat(speed_bins, 16)

#creating a multiindex dataframe with frequencies

idx = pd.MultiIndex.from_product([speed_bins,
                                  directions_deg],
                                 names=['wind_speed_bins', 'wind_direction_bins'])
col = ['frequency']
frequencies_df = pd.DataFrame(0, idx, col)

wind_rose_data = meteo[['wind_speed_m/s', 'wind_direction_deg']].to_numpy()

# distance between the centre of the bin and its edge
step = 11.25

# converting data between 348.75 and 360 to negative
for i in range(len(wind_rose_data)):
    if directions_deg[-1] + step <= wind_rose_data[i,1] and wind_rose_data[i,1] < 360:
        wind_rose_data[i,1] = wind_rose_data[i,1] - 360

# determining the direction bins
bin_edges_dir = directions_deg - step
bin_edges_dir = np.append(bin_edges_dir, [directions_deg[-1]+step]) 
    
# determining speed bins ( the last bin is 50 as above those speeds the outliers were removed for the measurements)
threshold_outlier_rm = 50
bin_edges_speed = np.array([0, 2, 4, 6, 8, 10, 12, 14, 25, threshold_outlier_rm])

frequencies = np.array([])
# loop selecting given bins and calculating frequencies
for i in range(len(bin_edges_speed)-1):
    for j in range(len(bin_edges_dir)-1)
        bin_contents = wind_dir_speed_freq(bin_edges_speed[i], bin_edges_speed[i+1], bin_edges_dir[j], bin_edges_dir[j+1])
        
        # applying the filtering function for every bin and checking the number of measurements
        bin_size = len(bin_contents)
        frequency = bin_size/len(wind_rose_data)
        
        #obtaining the final frequencies of bin
        frequencies = np.append(frequencies, frequency)

# updating the frequencies dataframe
frequencies_df.frequency = frequencies*100 # [%]
wind_rose_df.frequency = frequencies*100 # [%]

# calling the PLOT function
"""
PLOTTING THE ROSES
"""
fig_wind_rose = wind_rose_fig(frequencies_df,
                               title = '<b>WRF Data: Wind Speed Distribution</b>',
                               filename= 'fig_wind_rose_WRF.png',
                               open_bool  = False)

The plotting function goes here:

import plotly.graph_objects as go
from plotly.offline import plot

def wind_rose_fig(frequencies_df, title, filename, open_bool):
    fig = go.Figure()
    
    fig.add_trace(go.Barpolar(
        r=frequencies_df.loc[('0-2 m/s'), 'frequency'],
        name='0-2 m/s',
        marker_color='#482878'))
    
    fig.add_trace(go.Barpolar(
        r=frequencies_df.loc[('2-4 m/s'), 'frequency'],
        name='2-4 m/s',
        marker_color='#3e4989'))
    
    fig.add_trace(go.Barpolar(
        r=frequencies_df.loc[('4-6 m/s'), 'frequency'],
        name='4-6 m/s',
        marker_color='#31688e'))
    
    fig.add_trace(go.Barpolar(
        r=frequencies_df.loc[('6-8 m/s'), 'frequency'],
        name='6-8 m/s',
        marker_color='#26828e'))
    
    fig.add_trace(go.Barpolar(
        r=frequencies_df.loc[('8-10 m/s'), 'frequency'],
        name='8-10 m/s',
        marker_color='#1f9e89'))
    
    fig.add_trace(go.Barpolar(
        r=frequencies_df.loc[('10-12 m/s'), 'frequency'],
        name='10-12 m/s',
        marker_color='#35b779'))
    
    fig.add_trace(go.Barpolar(
        r=frequencies_df.loc[('12-14 m/s'), 'frequency'],
        name='12-14 m/s',
        marker_color='#6ece58'))
    
    fig.add_trace(go.Barpolar(
        r=frequencies_df.loc[('14-25 m/s'), 'frequency'],
        name='14-25 m/s',
        marker_color='#b5de2b'))
    
    fig.add_trace(go.Barpolar(
        r=frequencies_df.loc[('>25 m/s'), 'frequency'],
        name='>25 m/s',
        marker_color='#fde725'))
    
    fig.update_traces(text=['North', 'NNE', 'NE', 'ENE', 'East', 'ESE', 'SE', 'SSE', 'South', 'SSW', 'SW', 'WSW', 'West', 'WNW', 'NW', 'NNW'])
    
    fig.update_layout(
        title=title,
        title_font_size=26,
        title_x = 0.463,
        legend_font_size=18,
        polar_radialaxis_ticksuffix='%',
        polar_angularaxis_rotation=90,
        polar_angularaxis_direction='clockwise',
        polar_angularaxis_tickmode = 'array',
        polar_angularaxis_tickvals=[0, 22.5, 45, 72.5, 90, 112.5, 135, 157.5, 180, 202.5, 225, 247.5, 270, 292.5, 315, 337.5],
        polar_angularaxis_ticktext=['<b>North</b>', 'NNE', '<b>NE</b>', 'ENE', '<b>East</b>', 'ESE', '<b>SE</b>', 'SSE', '<b>South</b>', 'SSW', '<b>SW</b>', 'WSW', '<b>West</b>', 'WNW', '<b>NW</b>', 'NNW'],
        polar_angularaxis_tickfont_size = 22,
        polar_radialaxis_tickmode = 'linear',
        polar_radialaxis_angle = 45,
        polar_radialaxis_tick0 = 5,
        polar_radialaxis_dtick = 5,
        polar_radialaxis_tickangle = 100,
        polar_radialaxis_tickfont_size = 14)
    
    fig.write_image(get_fig_dir() + filename, width = 1000, height = 800)
    fig.show(renderer = 'png', width = 1000, height = 800)
    return plot(fig, auto_open = open_bool)


#%% histogram with wind directions
def histogram_wind_dir_fig(data, name, color, title, xaxis_title, yaxis_title, filename, open_bool):
    fig = go.Figure()
    fig.add_trace(go.Histogram(
    x=data,
    histnorm='percent',
    name=name, # name used in legend and hover labels
    xbins=dict( # bins used for histogram
        size=3),
    marker_color=color))


    fig.update_layout(
        title_text = title, # title of plot
        title_x=0.5,
        title_font_size = 24,
        xaxis=dict(title = xaxis_title,
                   titlefont_size=20,
                   tickfont_size=16,
                   tickvals=[0,30,60,90,120,150,180,210,240,270,300,330,360],
                   ticktext=['N',30,60,'E',120,150,'S',210,240,'W',300,330,'N']), # xaxis label
        yaxis=dict(title = yaxis_title,
                   titlefont_size=20,
                   tickfont_size=16), # yaxis label
        bargap=0.2,) # gap between bars of adjacent location coordinates

    fig.write_image(get_fig_dir() + filename, width = 1200, height = 500)
    fig.show(renderer = 'png', width = 1200, height = 500)
    return plot(fig, auto_open = open_bool)

Thank you both @janmaj and @maciekmaj. I also used a solution almost identical to @janmaj’s one.
Having the answers here will help future plotly users to figuring out a way to do this :wink:

1 Like