Salvador Dali
Salvador Dali

Reputation: 222591

Jupyter notebook display two pandas tables side by side

I have two pandas dataframes and I would like to display them in Jupyter notebook.

Doing something like:

display(df1)
display(df2)

Shows them one below another:

enter image description here

I would like to have a second dataframe on the right of the first one. There is a similar question, but it looks like there a person is satisfied either with merging them in one dataframe of showing the difference between them.

This will not work for me. In my case dataframes can represent completely different (non-comparable elements) and the size of them can be different. Thus my main goal is to save space.

Upvotes: 154

Views: 149056

Answers (15)

LetHimCook
LetHimCook

Reputation: 1

I did just come accross this because I wanted to display multiple data frames next to each other in vs code notebooks. @pwb2103 asked for this as well and I created a solution that works for me. It is a slight modification of @ntg 's answer.

def display_side_by_side(*args,titles=cycle([''])):
    # Add div to put output in a flexbox with the required properties.
    open_div = '<div style="display: flex; justify-content: center; align-items: flex-start";>'
    close_div = "</div>"
    html_str=''
    for df,title in zip(args, chain(titles,cycle(['</br>'])) ):
        # Put everything in a div and add the headline before to appear on top
        html_str+=f'<div>'
        html_str+=f'<h2 style="text-align: center;">{title}</h2>'
        html_str+='<th style="text-align:center;"><td style="vertical-align:top">'
        html_str+=df.to_html().replace('table','table style="display:inline"')
        html_str+='</td></th></div>'
        # Add spacing to tables.
        html_str+='<span style="display:inline-block; width: 10px;"></span>'
    # Put everything together.
    display_html(f"{open_div}{html_str}{close_div}",raw=True)

Upvotes: 0

gibbone
gibbone

Reputation: 2700

Starting from pandas 0.17.1 the visualization of DataFrames can be directly modified with pandas styling methods

To display two DataFrames side by side you must use set_table_attributes with the argument "style='display:inline'" as suggested in ntg answer. This will return two Styler objects. To display the aligned dataframes just pass their joined HTML representation through the display_html method from IPython.

With this method is also easier to add other styling options. Here's how to add a caption, as requested here:

import numpy as np
import pandas as pd   
from IPython.display import display_html 

df1 = pd.DataFrame(np.arange(12).reshape((3,4)),columns=['A','B','C','D',])
df2 = pd.DataFrame(np.arange(16).reshape((4,4)),columns=['A','B','C','D',])

df1_styler = df1.style.set_table_attributes("style='display:inline'").set_caption('Caption table 1')
df2_styler = df2.style.set_table_attributes("style='display:inline'").set_caption('Caption table 2')
    
display_html(df1_styler._repr_html_()+df2_styler._repr_html_(), raw=True)

aligned dataframes pandas styler with caption

Upvotes: 82

Kusko25
Kusko25

Reputation: 11

Throwing my contribution into the mix with a (long) one liner that requires minimal imports

from IPython.display import display_html
lambda dfs,titles=[]:display_html(f'<table><thead>{"".join(f"<th>{title}</th>" for title in titles)}</thead><tbody>{"".join("""<td style="vertical-align: top;">"""+f"{df.to_html()}</td>" for df in dfs)}</tbody></table>',raw=True)

Edit:
And just to prove I'm going mad with power here it is with line wrap when more than col tables are passed

display_cols = lambda dfs,titles=[],cols=100:display_html("".join(f'<table><thead>{"".join(f"<th>{title}</th>" for title in titles_slice)}</thead><tbody>{"".join("""<td style="vertical-align: top;">"""+f"{df.to_html()}</td>" for df in dfs_slice)}</tbody></table>' for titles_slice,dfs_slice in [(titles[:len(dfs)][i:i+cols],dfs[i:i+cols]) for i in range(0,len(dfs),cols)]),raw=True)

Upvotes: 0

ntg
ntg

Reputation: 14095

I have ended up writing a function that can do this: [update: added titles based on suggestions (thnx @Antony_Hatchkins et al.)]

from IPython.display import display_html
from itertools import chain,cycle
def display_side_by_side(*args,titles=cycle([''])):
    html_str=''
    for df,title in zip(args, chain(titles,cycle(['</br>'])) ):
        html_str+='<th style="text-align:center"><td style="vertical-align:top">'
        html_str+=f'<h2 style="text-align: center;">{title}</h2>'
        html_str+=df.to_html().replace('table','table style="display:inline"')
        html_str+='</td></th>'
    display_html(html_str,raw=True)
  

Example usage:

df1 = pd.DataFrame(np.arange(12).reshape((3,4)),columns=['A','B','C','D',])
df2 = pd.DataFrame(np.arange(16).reshape((4,4)),columns=['A','B','C','D',])
display_side_by_side(df1,df2,df1, titles=['Foo','Foo Bar']) #we left 3rd empty...

enter image description here

Upvotes: 200

Antony Hatchkins
Antony Hatchkins

Reputation: 33994

This adds (optional) headers, index and Series support to @nts's answer:

from IPython.display import display_html

def mydisplay(dfs, names=[], index=False):
    def to_df(x):
        if isinstance(x, pd.Series):
            return pd.DataFrame(x)
        else:
            return x
    html_str = ''
    if names:
        html_str += ('<tr>' + 
                     ''.join(f'<td style="text-align:center">{name}</td>' for name in names) + 
                     '</tr>')
    html_str += ('<tr>' + 
                 ''.join(f'<td style="vertical-align:top"> {to_df(df).to_html(index=index)}</td>' 
                         for df in dfs) + 
                 '</tr>')
    html_str = f'<table>{html_str}</table>'
    html_str = html_str.replace('table','table style="display:inline"')
    display_html(html_str, raw=True)

enter image description here

Upvotes: 12

Rich Lysakowski PhD
Rich Lysakowski PhD

Reputation: 3093

enter image description hereHere's another variation of the display_side_by_side() function introduced by @Anton Golubev that combines gibbone (to set styles and captions) and stevi (adding space), I added an extra argument to change spacing between tables at run-time.

from IPython.core.display import display, HTML

def display_side_by_side(dfs:list, captions:list, tablespacing=5):
    """Display tables side by side to save vertical space
    Input:
        dfs: list of pandas.DataFrame
        captions: list of table captions
    """
    output = ""
    for (caption, df) in zip(captions, dfs):
        output += df.style.set_table_attributes("style='display:inline'").set_caption(caption)._repr_html_()
        output += tablespacing * "\xa0"
    display(HTML(output))
    
display_side_by_side([df1, df2, df3], ['caption1', 'caption2', 'caption3'])

The tablespacing=5 default argument value (shown = 5 here) determines the vertical spacing between tables.

Upvotes: 16

MSorro
MSorro

Reputation: 170

@zarak code is pretty small but affects the layout of the whole notebook. Other options are a bit messy for me.

I've added some clear CSS to this answer affecting only current cell output. Also you are able to add anything below or above dataframes.

from ipywidgets import widgets, Layout
from IPython import display
import pandas as pd
import numpy as np

# sample data
df1 = pd.DataFrame(np.random.randn(8, 3))
df2 = pd.DataFrame(np.random.randn(8, 3))

# create output widgets
widget1 = widgets.Output()
widget2 = widgets.Output()

# render in output widgets
with widget1:
    display.display(df1.style.set_caption('First dataframe'))
    df1.info()
with widget2:
    display.display(df2.style.set_caption('Second dataframe'))
    df1.info()


# add some CSS styles to distribute free space
box_layout = Layout(display='flex',
                    flex_flow='row',
                    justify_content='space-around',
                    width='auto'
                   )
    
# create Horisontal Box container
hbox = widgets.HBox([widget1, widget2], layout=box_layout)

# render hbox
hbox

enter image description here

Upvotes: 11

Benjamin Arner
Benjamin Arner

Reputation: 61

I decided to add some extra functionality to Yasin's elegant answer, where one can choose both the number of cols and rows; any extra dfs are then added to the bottom. Additionally one can choose in which order to fill the grid (simply change fill keyword to 'cols' or 'rows' as needed)

import pandas as pd
from IPython.display import display,HTML

def grid_df_display(list_dfs, rows = 2, cols=3, fill = 'cols'):
    html_table = "<table style='width:100%; border:0px'>{content}</table>"
    html_row = "<tr style='border:0px'>{content}</tr>"
    html_cell = "<td style='width:{width}%;vertical-align:top;border:0px'>{{content}}</td>"
    html_cell = html_cell.format(width=100/cols)

    cells = [ html_cell.format(content=df.to_html()) for df in list_dfs[:rows*cols] ]
    cells += cols * [html_cell.format(content="")] # pad

    if fill == 'rows': #fill in rows first (first row: 0,1,2,... col-1)
        grid = [ html_row.format(content="".join(cells[i:i+cols])) for i in range(0,rows*cols,cols)]

    if fill == 'cols': #fill columns first (first column: 0,1,2,..., rows-1)
        grid = [ html_row.format(content="".join(cells[i:rows*cols:rows])) for i in range(0,rows)]

    display(HTML(html_table.format(content="".join(grid))))

    #add extra dfs to bottom
    [display(list_dfs[i]) for i in range(rows*cols,len(list_dfs))]

list_dfs = []
list_dfs.extend((pd.DataFrame(2*[{"x":"hello"}]), 
             pd.DataFrame(2*[{"x":"world"}]), 
             pd.DataFrame(2*[{"x":"gdbye"}])))

grid_df_display(3*list_dfs)

test output

Upvotes: 4

Arzanico
Arzanico

Reputation: 141

Extension of antony's answer If you want to limit de visualization of tables to some numer of blocks by row, use the maxTables variable.enter image description here

def mydisplay(dfs, names=[]):

    count = 0
    maxTables = 6

    if not names:
        names = [x for x in range(len(dfs))]

    html_str = ''
    html_th = ''
    html_td = ''

    for df, name in zip(dfs, names):
        if count <= (maxTables):
            html_th += (''.join(f'<th style="text-align:center">{name}</th>'))
            html_td += (''.join(f'<td style="vertical-align:top"> {df.to_html(index=False)}</td>'))
            count += 1
        else:
            html_str += f'<tr>{html_th}</tr><tr>{html_td}</tr>'
            html_th = f'<th style="text-align:center">{name}</th>'
            html_td = f'<td style="vertical-align:top"> {df.to_html(index=False)}</td>'
            count = 0


    if count != 0:
        html_str += f'<tr>{html_th}</tr><tr>{html_td}</tr>'


    html_str += f'<table>{html_str}</table>'
    html_str = html_str.replace('table','table style="display:inline"')
    display_html(html_str, raw=True)

Upvotes: 3

Anton Golubev
Anton Golubev

Reputation: 1433

Combining approaches of gibbone (to set styles and captions) and stevi (adding space) I made my version of function, which outputs pandas dataframes as tables side-by-side:

from IPython.core.display import display, HTML

def display_side_by_side(dfs:list, captions:list):
    """Display tables side by side to save vertical space
    Input:
        dfs: list of pandas.DataFrame
        captions: list of table captions
    """
    output = ""
    combined = dict(zip(captions, dfs))
    for caption, df in combined.items():
        output += df.style.set_table_attributes("style='display:inline'").set_caption(caption)._repr_html_()
        output += "\xa0\xa0\xa0"
    display(HTML(output))

Usage:

display_side_by_side([df1, df2, df3], ['caption1', 'caption2', 'caption3'])

Output:

enter image description here

Upvotes: 38

crystal
crystal

Reputation: 101

Gibbone's answer worked for me! If you want extra space between the tables go to the code he proposed and add this "\xa0\xa0\xa0" to the following code line.

display_html(df1_styler._repr_html_()+"\xa0\xa0\xa0"+df2_styler._repr_html_(), raw=True)

Upvotes: 4

Dinis Cruz
Dinis Cruz

Reputation: 4279

I ended up using HBOX

import ipywidgets as ipyw

def get_html_table(target_df, title):
    df_style = target_df.style.set_table_attributes("style='border:2px solid;font-size:10px;margin:10px'").set_caption(title)
    return df_style._repr_html_()

df_2_html_table = get_html_table(df_2, 'Data from Google Sheet')
df_4_html_table = get_html_table(df_4, 'Data from Jira')
ipyw.HBox((ipyw.HTML(df_2_html_table),ipyw.HTML(df_4_html_table)))

Upvotes: 4

Private
Private

Reputation: 2694

Here is Jake Vanderplas' solution I came across just the other day:

import numpy as np
import pandas as pd

class display(object):
    """Display HTML representation of multiple objects"""
    template = """<div style="float: left; padding: 10px;">
    <p style='font-family:"Courier New", Courier, monospace'>{0}</p>{1}
    </div>"""

    def __init__(self, *args):
        self.args = args

    def _repr_html_(self):
        return '\n'.join(self.template.format(a, eval(a)._repr_html_())
                     for a in self.args)

    def __repr__(self):
       return '\n\n'.join(a + '\n' + repr(eval(a))
                       for a in self.args)

Credit: https://github.com/jakevdp/PythonDataScienceHandbook/blob/master/notebooks/03.08-Aggregation-and-Grouping.ipynb

Upvotes: 12

Yasin Z&#228;hringer
Yasin Z&#228;hringer

Reputation: 641

My solution just builds a table in HTML without any CSS hacks and outputs it:

import pandas as pd
from IPython.display import display,HTML

def multi_column_df_display(list_dfs, cols=3):
    html_table = "<table style='width:100%; border:0px'>{content}</table>"
    html_row = "<tr style='border:0px'>{content}</tr>"
    html_cell = "<td style='width:{width}%;vertical-align:top;border:0px'>{{content}}</td>"
    html_cell = html_cell.format(width=100/cols)

    cells = [ html_cell.format(content=df.to_html()) for df in list_dfs ]
    cells += (cols - (len(list_dfs)%cols)) * [html_cell.format(content="")] # pad
    rows = [ html_row.format(content="".join(cells[i:i+cols])) for i in range(0,len(cells),cols)]
    display(HTML(html_table.format(content="".join(rows))))

list_dfs = []
list_dfs.append( pd.DataFrame(2*[{"x":"hello"}]) )
list_dfs.append( pd.DataFrame(2*[{"x":"world"}]) )
multi_column_df_display(2*list_dfs)

Output

Upvotes: 14

zarak
zarak

Reputation: 3003

You could override the CSS of the output code. It uses flex-direction: column by default. Try changing it to row instead. Here's an example:

import pandas as pd
import numpy as np
from IPython.display import display, HTML

CSS = """
.output {
    flex-direction: row;
}
"""

HTML('<style>{}</style>'.format(CSS))

Jupyter image

You could, of course, customize the CSS further as you wish.

If you wish to target only one cell's output, try using the :nth-child() selector. For example, this code will modify the CSS of the output of only the 5th cell in the notebook:

CSS = """
div.cell:nth-child(5) .output {
    flex-direction: row;
}
"""

Upvotes: 111

Related Questions