The world’s leading publication for data science, AI, and ML professionals.

How to Create Well-Styled Streamlit Dataframes, Part 1: Using the Pandas Styler

Streamlit and the pandas Styler object are not friends. But, we will change that!

I have always been a fan of the styler method in pandas. When I started building Streamlit apps, it was clear to me that I wanted to style my dataframes to aid in visualising dataframes, but… surprise! As of the time of writing, Streamlit st.dataframe() doesn’t support styler objects, only dataframe objects. Well, correction, it does support them, but the UI display is horrendous!

This is why I want to share with you my workarounds and ideas to building a well-styled dataframe in Streamlit. We will cover:

  1. How to add commas for separating thousands in numbers.
  2. How to display a number as a percentage (from 0.24 in the data to 24% in the UI)
  3. How to add currency symbols.
  4. How to add colour to the cells. Even better, I will share with you my favourite colour-grading function.
  5. How to add emojis! Yes, we cant live without emojis 😊 !

The st.dataframe() default view

Streamlit is actually pretty good at inferring the best display based on data types. Imagine the following dataframe:

mock_data = {
"Country": ["US", "IN", "BR", "ES", "AR", "IT"],
"Period_1": [50_000, 30_000, 17_000, 14_000, 22_000, 16_000], 
"Period_2": [52_000, 37_000, 16_000, 12_000, 21_000, 19_000], 
} 

df = pd.DataFrame(mock_data) 
df['Difference'] = df['Period_2'] - df['Period_1'] 
df['Percentage Change'] = np.round(((df['Period_2'] - df['Period_1']) / df['Period_1']), 2) 
df['Percentage Change rank'] = df['Percentage Change'].rank(method='dense', ascending=False).astype(int)

The default st.dataframe() view is already nice. For example, the thousand comma separator is correctly inferred.

Image by author
Image by author

But, we know we can make it better using Styler functions!

What happens if we st.dataframe() a styler object?

Before diving into how we style the dataframes, let’s simply convert the pandas dataframe to a Styler object and check what happens when we try to render the Styler object using the st.dataframe() method.

# Creating the styler object
df.copy().style
Image by author
Image by author

The display is now a bit messed up. For example:

  • No comma separators for big numbers
  • 6 decimal places for decimal numbers.

And this only happened because we went from a ‘dataframe’ type object to a ‘styler’ type object.

Formatting thousands separators and percentages

We want to visually understand at a glance orders of magnitude (i.e. thousands separators). And we don’t want to mentally multiply a decimal point by 100 so that you understand a percentage (i.e. figuring out that 0.24 is 24%).

The functions below will help you format numbers to better display in your styled dataframes.

def _format_with_thousands_commas(val): 
  return f'{val:,.0f}' 

def _format_as_percentage(val, prec=0): 
  return f'{val:.{prec}%}' 

thousands_cols = ['Period_1', 'Period_2', 'Difference'] 
perct_cols = ['Percentage Change'] 

styler_with_thousands_commas = (
  raw_styler
  .format(_format_with_thousands_commas, subset=thousands_cols) 
  .format(lambda x: _format_as_percentage(x, 2), subset=perct_cols) 
  )
Image by author
Image by author

This is looking better.

  • _Period1, _Period2 and Difference columns are now the same as the ` st.dataframe() display. So we have at least matched the default display.
  • But now, the Percentage Change column is nicer and easier to understand than the default decimal points one 🙂 .

Adding background colouring with gradients

A dataframe with colours => a well styled dataframe in Streamlit (Jiminy cricket speaking: an overload of colours can be harmful).

Our next step is to add a bit of colour to aid in our visual analysis. We could apply a cell background gradient with a simple cmap. The cmap by default will create a gradient based on the numbers on the series (ie, take min an max of the series). This is perfect if you numbers are all positive or negative, but what about the following use case?

  • Use green gradient colouring only for positive values
  • Use red gradient colouring only for negative values.

For this I created a simple function that I have re-used in multiple cases:

def _format_positive_negative_background_colour(val, min_value, max_value): 
  if val > 0: 
    # Normalize positive values to a scale of 0 to 1 
    normalized_val = (val - 0) / (max_value - 0) 
    # Create a gradient of green colors 
    color = plt.cm.Greens(normalized_val * 0.7) 
    color_hex = mcolors.to_hex(color) 
  elif val < 0: 
    # Normalize negative values to a scale of 0 to -1 
    normalized_val = (val - min_value) / (0 - min_value) 
    # Create a gradient of red colors 
    color = plt.cm.Reds_r(normalized_val * 0.7) 
    color_hex = mcolors.to_hex(color) 
  else: color_hex = 'white' # For zero values, set the background color to white 

  # Determine text color based on the darkness of the background color 
  r, g, b = mcolors.hex2color(color_hex) 
   if (r * 299 + g * 587 + b * 114) / 1000 > 0.5: # Use the formula for perceived brightness 
    text_color = 'black' 
   else: 
    text_color = 'white' 

  return f'background-color: {color_hex}; color: {text_color}' 

min_value_abs_diff = df['Difference'].min() 
max_value_abs_diff = df['Difference'].max() 

min_value_perct_diff = df['Percentage Change'].min() 
max_value_perct_diff = df['Percentage Change'].max() 

styler_with_colour_gradients = (
  df.copy().style 
  .map(lambda x: _format_positive_negative_background_colour(x, min_value_abs_diff, max_value_abs_diff), subset=['Difference']) 
  .map(lambda x: _format_positive_negative_background_colour(x, min_value_perct_diff, max_value_perct_diff), subset=['Percentage Change']) 
)
Image by author
Image by author

You can see that all positive numbers are coloured green and they have a shade from the lowest (light colour) to the highest (dark colour). The inverse applies to negative numbers; where they are coloured red but with lowest being the darkest colour.

In addition, the function also determines if the text colour should be white or black depending on the contrast it would make with the cell background colour.

Adding currency symbols

If you know you are dealing with currencies, why not add the currency sign to the data? This is what Excel does, and hey, Excel is an amazing product. Honestly, no irony there.

def _format_with_dollar_sign(val, prec=0): 
  return f'${val:,.{prec}f}' 

styler_with_dollar_sign = (
  df.copy().style 
  .format(_format_with_dollar_sign, subset=['Period_1', 'Period_2', 'Difference']) 
)
Image by author
Image by author

💲 Check the _Period__1, _Period__2 and _Differenc_e columns 💲

Function to add emojis

We live in the world of emojis, so there is no way your app can’t have a styled dataframe in Streamlit without an emoji in it. Jokes aside, emojis can also aid in our visual analysis. In this case, here is an example where I have used a gold, silver, and bronze medal emoji to highlight the highest positive percentage changes.

def _add_medal_emoji(val): 
  if val == 1: 
    return f"{val} 🥇" 
  elif val == 2: 
    return f"{val} 🥈" 
  elif val == 3: 
    return f"{val} 🥉" 
  else: return val 

styler_with_medal_emoji = (
  df.copy().style 
  .format(_add_medal_emoji, subset=['Percentage Change rank']) 
)
Image by author
Image by author

At a glance, you can detect which countries had the 1st, 2nd and 3rd biggest positive changes between Period 1 and Period 2 using the medal emojis.

Currently not supported: adding bars

Have you used bar charts in the cell background in Styler objects? Unfortunately, they are not supported in the st.dataframe() method. You can display them using the st.write() method with an HTML object, but, I don’t recommend it. Below you can see how it looks like.

Image by author
Image by author

Not only the UI is appalling… you completely loose all the st.dataframe() functionalities such as ordering and being able to format cells.

Summary

If Streamlit did support Styler objects, life would be much easier. But until then, I hope these ideas help you in your journey of creating great visualisations for your Streamlit apps. I will follow this post with building the equivalent pretty dataframe using AgGrid. Stay tuned because AgGrid is way cooler than the Styler object!

Where can you find the code?

In my repo and the live Streamlit app:

Further reading

Thanks for reading the article! If you are interested in more of my written content, here is an article capturing all of my other blogs posts organised by themes: Data Science team and project management, Data storytelling, Marketing & bidding science and Machine Learning & modelling.

All my written articles in one place

Stay tuned!

If you want to get notified when I release new written content, feel free to follow me on Medium or subscribe to my Substack newsletter. In addition, I would be very happy to chat on Linkedin!

Get notified with my latest written content about Data Science!

Jose’s Substack | Jose Parreño Garcia | Substack


Related Articles