Given these DataFrames:
m_df = pd.DataFrame({
'Morning': ['Jack', 'Emma', 'Theo'],
'Tennis Score': [10, 5, 5],
'Baseball Score': [2, 6, 8]
})
n_df = pd.DataFrame({
'Noon': ['Jack', 'Emma', 'Theo'],
'Tennis Score': [5, 8, 12],
'Baseball Score': [4, 2, 9]
})
e_df = pd.DataFrame({
'Evening': ['Jack', 'Emma', 'Theo'],
'Tennis Score': [3, 6, 8],
'Baseball Score': [3, 7, 3]
})
They should be concat together and each DataFrame should receive an indicator column. Additionally, the Morning, Noon, and Evening columns should be renamed so that they align in a single column:
keys = ['Morning', 'Noon', 'Evening']
plot_df = pd.concat(
[df_.assign(id=label)
.rename(columns={label: 'Player'})
.set_index('Player')
for df_, label in zip([m_df, n_df, e_df], keys)]
).reset_index()
plot_df:
Player Tennis Score Baseball Score id
0 Jack 10 2 Morning
1 Emma 5 6 Morning
2 Theo 5 8 Morning
3 Jack 5 4 Noon
4 Emma 8 2 Noon
5 Theo 12 9 Noon
6 Jack 3 3 Evening
7 Emma 6 7 Evening
8 Theo 8 3 Evening
Then pivot into wide-form from long, reindex to get index in order Morning, Noon, and Evening instead of alphabetical order, swaplevel and sort_index so that columns are grouped by player and not by score type:
plot_df = (
plot_df.pivot(index='id', columns='Player')
.reindex(keys)
.swaplevel(0, 1, 1)
.sort_index(level=0, axis=1)
.rename_axis(columns=["Player", 'Score Type'])
)
plot_df:
Player Emma Jack Theo
Score Type Baseball Score Tennis Score Baseball Score Tennis Score Baseball Score Tennis Score
id
Morning 6 5 2 10 8 5
Noon 2 8 4 5 9 12
Evening 7 6 3 3 3 8
This can be simply plotted:
imports:
import pandas as pd
from matplotlib import pyplot as plt
Plotting Code:
fig, ax = plt.subplots()
plot_df.plot(kind='bar', rot=0, ax=ax, xlabel='', ylabel='Score')
plt.tight_layout()
plt.show()

Alternatively update the facecolor and hatch of the patches:
imports:
import numpy as np
import pandas as pd
from matplotlib import pyplot as plt
plotting code:
fig, ax = plt.subplots()
plot_df.plot(kind='bar', rot=0, ax=ax, legend=False, xlabel='', ylabel='Score')
players = plot_df.columns.get_level_values(0).unique()
score_types = plot_df.columns.get_level_values(1).unique()
# Create hatches (should be same length as types of scores)
hatches = np.tile(np.repeat(['/', '.'], plot_df.shape[0]), len(players))
# Create Colors (should be same number of colours as number of players)
colours = np.repeat(['green', 'pink', 'purple'],
len(score_types) * plot_df.shape[0])
# Iterate over patches, colours, and hatches to set the facecolor and hatch
for patch, colour, hatch in zip(ax.patches, colours, hatches):
patch.set_facecolor(colour)
patch.set_hatch(hatch)
# Add legend:
ax.legend(loc=1)
plt.tight_layout()
plt.show()

Or even further with custom legends:
imports:
import numpy as np
import pandas as pd
from matplotlib import pyplot as plt
from matplotlib.lines import Line2D
from matplotlib.patches import Patch
plotting code:
fig, ax = plt.subplots()
plot_df.plot(kind='bar', rot=0, ax=ax, legend=False, xlabel='', ylabel='Score')
players = plot_df.columns.get_level_values(0).unique()
score_types = plot_df.columns.get_level_values(1).unique()
colours = ['green', 'pink', 'purple']
hatches = ['/', '.']
iter_colours = np.repeat(colours, len(score_types) * plot_df.shape[0])
iter_hatches = np.tile(np.repeat(hatches, plot_df.shape[0]), len(players))
for patch, colour, hatch in zip(ax.patches, iter_colours, iter_hatches):
patch.set_facecolor(colour)
patch.set_hatch(hatch)
# Add legends:
player_legend = ax.legend(
[Line2D([0], [0], color=colour, lw=4) for colour in colours],
players, title='Players', loc=1)
score_legend = ax.legend(
[Patch(hatch=hatch, facecolor='white') for hatch in hatches],
score_types, loc=2, title='Score Type', labelspacing=.65)
for patch in score_legend.get_patches():
patch.set_height(14)
patch.set_y(-3)
ax.add_artist(player_legend)
plt.tight_layout()
plt.show()

Complete Working Example with 5 players from import to plt.show():
import numpy as np
import pandas as pd
from matplotlib import pyplot as plt
m_df = pd.DataFrame({
'Morning': ['Jack', 'Emma', 'Theo', 'Matt', 'Thomas'],
'Tennis Score': [10, 5, 5, 7, 9],
'Baseball Score': [2, 6, 8, 2, 4]
})
n_df = pd.DataFrame({
'Noon': ['Jack', 'Emma', 'Theo', 'Matt', 'Thomas'],
'Tennis Score': [5, 8, 12, 3, 10],
'Baseball Score': [4, 2, 9, 5, 6]
})
e_df = pd.DataFrame({
'Evening': ['Jack', 'Emma', 'Theo', 'Matt', 'Thomas'],
'Tennis Score': [3, 6, 8, 4, 7],
'Baseball Score': [3, 7, 3, 9, 5]
})
# concat
keys = ['Morning', 'Noon', 'Evening']
plot_df = pd.concat(
[df_.assign(id=label)
.rename(columns={label: 'Player'})
.set_index('Player')
for df_, label in zip([m_df, n_df, e_df], keys)]
).reset_index()
# pivot to wide
plot_df = (
plot_df.pivot(index='id', columns='Player')
.reindex(keys)
.swaplevel(0, 1, 1)
.sort_index(level=0, axis=1)
.rename_axis(columns=["Player", 'Score Type'])
)
fig, ax = plt.subplots()
plot_df.plot(kind='bar', rot=0, ax=ax, legend=False, xlabel='', ylabel='Score')
players = plot_df.columns.get_level_values(0).unique()
score_types = plot_df.columns.get_level_values(1).unique()
hatches = np.tile(np.repeat(['/', '.'], plot_df.shape[0]), len(players))
colours = np.repeat(['red', 'green', 'blue', 'orange', 'pink'],
len(score_types) * plot_df.shape[0])
for patch, colour, hatch in zip(ax.patches, colours, hatches):
patch.set_facecolor(colour)
patch.set_hatch(hatch)
ax.legend(loc=1)
plt.tight_layout()
plt.show()
