0

I am trying to create a table that estimates profit from a DataFrame with buy and sell prices:

    import pandas as pd
    from df2img import df2img
    import matplotlib.pyplot as plt
    
    
    
    df = pd.DataFrame(
        {
            'Fruits': ['Apple', 'Apple', 'Apple', 'Orange', 'Banana', 'Orange'],
            'BuyPrice': [1000, 3000, 2400, 3000, 800, 1500],
            'SellPrice': [1200, 2800, 2500, 2500, 700, 1750]
        }
    )
    
    
    # Display DataFrame
    print('Original DataFrame:\n')
    print(df)
    
    # Add Profit percentage column
    df['Profit'] = (df['SellPrice']-df['BuyPrice'])*100/df['BuyPrice']
    df['Profit'] = df.apply(lambda x: "{:,.2f} %".format(x['Profit']), axis=1)
    
    # Rename column titles
    df = df.rename({'BuyPrice': 'Buy Price', 'SellPrice': 'Sell Price'}, axis=1)
    
    # Highlight positive and negative profits
    def highlight_cols(s):
        color = 'red' if type(s) != str and s < 0 else 'green'        
        return 'color: %s' % color
    
    df.style.applymap(highlight_cols, subset=['Profit'])


    print('\nFinal DataFrame:\n')
    print(df)
    
    
    # Now create an image file for the table
    df2img(
        df,
        file="table_fruits.png",
        header_color="white",
        header_bgcolor="orange",
        row_bgcolors=["lightgray", "white"],
        font_size=10.0,
        col_width=1.5,
        row_height=0.3
    )
    
    plt.show()

Here I want to color the positive profits with green and the negative ones with red. The df.style.applymap works only (though colors incorrectly) when I don't use df2img on a Jupyter notebook on the same cell. Thus the DataFrame Styler instruction does not pass to the final image file. Any not-so-complicated solution?

Output:

enter image description here

4
  • So I don't know what version of df2img you are using but the current one does not support the call that you are using. Beyond this, you do df['Profit'] = df.apply(lambda x: "{:,.2f} %".format(x['Profit']), axis=1) which converts the entire column to a string. For this reason in your highlighting function color = 'red' if type(s) != str and s < 0 else 'green' the type(s) != str ensures that all cells will be green. Commented Nov 13, 2021 at 23:20
  • @HenryEcker Yeah, that's true. But how can I format the floats later? Commented Nov 15, 2021 at 10:02
  • Do you need the plotly Table styles specifically? Is that why you're using df2img or would using all Styler operations and a different module to export to png be fine as well? The only reason I ask in the conditional styling is what the Styler is made for and it's much similar to use that then try to redefine the styles for plotly to undertsand. Commented Nov 15, 2021 at 14:22
  • Yes, I do. If the same can be done without a Styler, that would be okay as well. Commented Nov 15, 2021 at 22:47

1 Answer 1

2

A slight setup fix is needed. We do not want a percentage represented as a string when determining positive or negative. We also do not what a percentage stored multiplied by 100 because that affects formatting abilities later on.

The new setup can look like:

import numpy as np
import pandas as pd

df = pd.DataFrame({
    'Fruits': ['Apple', 'Apple', 'Apple', 'Orange', 'Banana', 'Orange'],
    'BuyPrice': [1000, 3000, 2400, 3000, 800, 1500],
    'SellPrice': [1200, 2800, 2500, 2500, 700, 1750]
})

# Add Profit percentage column
df['Profit'] = (df['SellPrice'] - df['BuyPrice']) / df['BuyPrice']

# Rename column titles
df = df.rename({'BuyPrice': 'Buy Price', 'SellPrice': 'Sell Price'}, axis=1)
Fruits Buy Price Sell Price Profit
0 Apple 1000 1200 0.2
1 Apple 3000 2800 -0.0666667
2 Apple 2400 2500 0.0416667
3 Orange 3000 2500 -0.166667
4 Banana 800 700 -0.125
5 Orange 1500 1750 0.166667

*If (for whatever reason) we need the values multiplied by 100, remember to divide back down before plotting and exporting to image.


I highly recommend against using Plotly Tables to perform this specific task as they are not as flexible when it comes to conditional formatting. This is much easier with dataframe_image and the built-in Styler object:

# pip install dataframe_image
import dataframe_image as dfi

# Highlight positive and negative profits
def highlight_cols(s):
    return np.where(s < 0, 'color: red', 'color:green')


# CSS for col_headings
headers = {
    'selector': 'th.col_heading',
    'props': 'background-color: orange; color: white;'
}

# CSS for rows
rows = [
    {
        'selector': 'tbody tr:nth-child(even)',
        'props': 'background-color: lightgray'
    },
    {
        'selector': 'tbody tr:nth-child(odd)',
        'props': 'background-color: white'
    }
]

styler = (
    df.reset_index()  # make current index a column
        .style  # Create Styler
        .hide_index()  # Hide new index (since old index is a column
        .apply(
            # apply highlighter function to Profit Column
            highlight_cols, axis=0, subset=['Profit']
        )
        .format(
            # apply percentage formatting to Profit Column
            formatter='{:.2%}', subset=['Profit']
        )
        .set_table_styles([headers, *rows])  # add CSS
)
# Export Styled Table to PNG
dfi.export(styler, 'table_fruits.png')

styled table created with  dataframe_image and Styler


With Plotly there is more overhead to allow for conditional styling, even though the static styling rules are more easily applied:

# pip install df2img
from df2img import df2img

# Define Format Strings for Columns
col_formats = {'Profit': '.2%'}

# Make index a column with label
plot_df = df.reset_index()

# Build dictionary of Font colours for _all_ cells
# Using default
font_colours_df = pd.DataFrame(
    'black',  # Set default font colour
    index=plot_df.index, columns=plot_df.columns
)
# Apply Colours to Profit Column
font_colours_df['Profit'] = np.where(
    plot_df['Profit'] < 0, 'red', 'green'
)
fig = df2img.plot_dataframe(
    plot_df,
    print_index=False,  # Hide new index (old index now column)
    row_fill_color=('white', 'lightgray'),
    tbl_header={
        'font': {'color': 'white'},
        'fill': {'color': 'orange'}
    },
    tbl_cells={
        'format': [
            # Conditionally build a complete list of foramt strings
            #  Based on col_formats dict and columns
            col_formats[c] if c in col_formats else None
            for c in plot_df.columns
        ],
        'font': {
            # Needs Transposed for colours to go to the correct cells
            'color': font_colours_df.T
        }
    },
    show_fig=False,  # Don't show in by default (since we're saving instead)
    fig_size=(500, 175)  # Set some reasonable Fig Size
)
df2img.save_dataframe(fig=fig, filename="table_fruits.png")

*Note Plotly has no notion of a subset. Styles for all cells must be explicitly declared. For tbl_cells.format that means that every column must be provided a format string (or None). For tbl_cells.font.color that means passing an entire DataFrame of colours to style a single column.

Nonetheless, the resulting Table is:

styled table created with  df2img/plotly

Sign up to request clarification or add additional context in comments.

5 Comments

Thanks. dataframe_image code is generating the desired output, however, with the following error messages. [1119/211545.184769:ERROR:xattr.cc(63)] setxattr org.chromium.crashpad.database.initialized on file /var/folders/mr/p9jqhp1x3jn62gn1t7h74q140000gn/T/: Operation not permitted (1) [1119/211545.188976:ERROR:file_io.cc(94)] ReadExactly: expected 8, observed 0 [1119/211545.192326:ERROR:xattr.cc(63)] setxattr org.chromium.crashpad.database.initialized on file /var/folders/mr/p9jqhp1x3jn62gn1t7h74q140000gn/T/: Operation not permitted (1) ... and more.
Hm. That seems like a permissions error to access a folder? I don't know I don't get any errors with dataframe_image when I run it.
Recently I have applied the Plotly based styling for a larger code. The condition works well for a certain number of rows but after that (i.e. green color for some of the negative and red for some of the positive values). Could you figure out if there exists any bug in your code?
This still performs correctly on my end with the provided sample and most recent versions of the libraries. The only conditional styling comes from font_colours_df['Profit'] = np.where(plot_df['Profit'] < 0, 'red', 'green') where the only comparison operator is < 0 there is no way for this to fail assuming the data is correct. If you can create a reproducible sample that replicates the problem, consider asking a new question someone might be able to find the issue.
Thanks, I got the issue. If I select an inappropriate figure size (e.g. fig_size=(300, 100)) in your example and then put display on a browser by adding fig.show() command, then while scrolling down, negative green values appear. However, in the saved png file, the out of the size part gets cut and we only see values with correct colors within the defined size. I don't know if one can set a size that automatically takes care of this and we don't have to scroll on the browser.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.