Criando figuras científicas usando Matplotlib e Seaborn

Feb 05 2023
Dados bem apresentados são um dos aspectos mais importantes da pesquisa científica. Em Python, as bibliotecas Matplotlib e Seaborn são poderosas bibliotecas de visualização de dados que podem ser usadas juntas para produzir figuras de qualidade com recursos personalizáveis.

Dados bem apresentados são um dos aspectos mais importantes da pesquisa científica. Em Python, as bibliotecas Matplotlib e Seaborn são poderosas bibliotecas de visualização de dados que podem ser usadas juntas para produzir figuras de qualidade com recursos personalizáveis.

Matplotlib x Seaborn

Quando usei Matplotlib e Seaborn pela primeira vez para criar figuras, não estava claro para mim por que e quando você usaria um ou outro. Eu recomendaria usar ambos. O Seaborn oferece uma ampla variedade de tipos de plotagem e paletas de cores e deve ser usado para realmente plotar seus dados, enquanto o Matplotlib permite formatar os detalhes da plotagem - tamanhos de fonte, legendas de figuras e texto de eixos.

Este artigo começará com como criar gráficos muito básicos e aumentar lentamente a complexidade para mostrar como aproveitar a personalização possível com Matplotlib e Seaborn. Para demonstrar, usaremos um conjunto de dados de gravações de EEG de roedores com diversas variáveis, como diferenças de genótipos, canais de eletrodos e IDs de animais. Isso nos dará uma intuição de como apresentar os dados com os quais os cientistas trabalham no dia a dia.

O artigo está organizado da seguinte forma:

  1. formate seus dados para plotagem
  2. criar um enredo
  3. executar reformatação básica (remover bordas de plotagem, rotular eixos e definir limites de eixos)
  4. executar reformatação mais detalhada (personalizando parâmetros de legenda, escalas de eixo, tamanhos de fonte)
  5. personalização de paletas marítimas
  6. criando subtramas
  7. composição de figuras complexas (subtramas e figure.subplot_mosaic)

A primeira etapa é formatar corretamente seus dados em um dataframe do pandas, um formato conhecido como dados organizados . Se você quiser ler mais sobre como organizar seus dados, este é um ótimo artigo . A variável independente (eixo x) deve estar em uma coluna e a variável dependente (eixo y) em outra coluna. Se existirem várias categorias para o seu conjunto de dados (por exemplo, Animal_ID, Channel, Genotype), essas categorias também devem estar em colunas separadas e você pode usar o parâmetro Seaborn hue (exemplos mostrados abaixo) para plotar várias variáveis ​​em um gráfico.

Criando um gráfico

Começaremos plotando o DataFrame mostrado acima usando as configurações padrão do Matplotlib e Seaborn com muito pouca formatação adicional. sns.lineplot( ) traçará um gráfico de linha dos dados armazenados no D ataFrame onde 'Frequency' e 'Power' são colunas no DataFrame, e hue permite que você plote várias variantes (genótipo neste caso) em um gráfico. O eixo y foi definido para uma escala logarítmica devido à forma como as frequências mais baixas e mais altas são representadas no espectro de potência dos dados de EEG, a potência é maior em baixas frequências e torna-se cada vez menor à medida que a frequência aumenta.

fig, axs = plt.subplots(1,1, figsize=(20,15))
sns.lineplot(data= df, x='Frequency', y='Power',hue = 'Genotype', 
              errorbar = ("se"), linewidth = 2)
axs.set_yscale('log')
fig.savefig('basic_power_plot.jpg')

      
                
Figure 1. EEG Power Spectrum plot using Seaborn and Matplotlib’s default parameters and no additional reformatting.

O gráfico acima mostra o básico dos dados, mas é difícil de entender. Geralmente não está claro o que o gráfico está tentando mostrar devido à grande quantidade de espaços em branco, tamanhos de fonte pequenos, falta de eixos de rótulos ou título do gráfico. Os primeiros passos para tornar este gráfico mais legível serão remover a borda desnecessária da figura e definir limites e rótulos para os eixos x e y.

sns.set_style("white")
fig, axs = plt.subplots(1, 1, figsize=(20,15), sharex = True, sharey = True)
sns.lineplot(data = df, x = 'Frequency', y = 'Power', hue = 'Genotype', 
             errorbar = ('se'), linewidth = 4)

#remove the figure border
sns.despine()

#set limits and labels for the x and y axis
axs.set_yscale('log')
axs.set_xlim(1, 48)
axs.set_ylim(10**-2, 10**3)
axs.set_xlabel("Frequency (Hz)")
axs.set_ylabel("log Power (\\u03bc$\\mathregular{V^{2}}$)")

#save figure 
fig.savefig('basic_plot.jpg')

      
                
Figure 2. Basic reformatting of Figure 1 to change axes limits and labels and remove outer plot border.

Além de simplesmente alterar os limites dos eixos, seu gráfico pode ser formatado adicionando:

  • um título geral da figura,
  • rótulos de fonte de eixos
  • um objeto de legenda para editar os detalhes da legenda
  • aumentando os pesos das linhas dos eixos e atualizando todos os caracteres de fonte dos eixos de uma só vez.

Também vou alterar os x-ticks para aumentar em incrementos de 6 até 48. A função axs.set_xlim() especifica os valores mínimo e máximo do eixo x, mas se você quiser especificar os valores que os ticks aumentam por você pode usar a função xticks .

sns.set_style("white", rc = {'font.size': 20})
fig, axs = plt.subplots(1, 1, figsize=(20,15), sharex = True, sharey = True)
sns.lineplot(data = df, x = 'Frequency', y = 'Power', hue = 'Genotype', 
errorbar = ('se'), linewidth = 4, ax = axs)

#remove outer border
sns.despine()

#axes customisation
axs.set_yscale('log')
axs.set_xlim(1, 48)
axs.set_ylim(10**-2, 10**3)
axs.set_xlabel("Frequency (Hz)", fontsize = 25)
axs.set_ylabel("log Power (\\u03bc$\\mathregular{V^{2}}$)", fontsize = 25)

#update the fontsize of all characters on x and y axis 
plt.rcParams.update({'font.size': 20})

#changing x-ticks
tick_values = list(range(1, 54, 6))
label_list = ['1', '6', '12', '18', '24', '30', '36', '42', '48']
axs.set_xticks(ticks = tick_values, labels = label_list)

#include an overall plot title
plt.suptitle('Condition 1 Overall Average', fontsize = 30, fontweight = 'bold')

#customise the legend 
leg = plt.legend(loc = 'upper right', frameon = False)
leg.set_title('Genotype', prop = {'size': 25})
leg_lines = leg.get_lines()
leg_texts = leg.get_texts()
plt.setp(leg_lines[0], linewidth = 8)
plt.setp(leg_lines[1], linewidth = 8)
plt.setp(leg_texts, fontsize = 20)

#increase the width of the x and y axis 
for axis in ['bottom', 'left']:
    axs.spines[axis].set_linewidth(3)

#save figure
fig.savefig('detailed_reformatting_plot.jpg')

      
                
Figure 3. Customising the legend, font size and font weights.

A Seaborn oferece uma ampla variedade de tipos de plotagem, a capacidade de integrar estatísticas em suas plotagens (como erros padrão) e paletas de cores personalizadas. Para saber mais sobre as paletas de cores da Seaborn, confira a documentação .

Aqui estão alguns exemplos de paletas diferentes:

royal = ['darkseagreen', 'lightpink', 'coral', 'darkkhaki']
darjeeling = ['orange', 'lightgreen', 'skyblue', 'plum']
bright = ['lightblue', 'teal', 'orangered', 'darkred']
sns.palplot(royal)
sns.palplot(darjeeling)
sns.palplot(bright)

      
                
Figure 4. Examples of seaborn colour palettes

sns.set_style("white")
fig, axs = plt.subplots(1, 1, figsize=(20,15), sharex = True, sharey = True)

#specify colour palette parameters
palette_plot = ['darkred', 'plum']
hue_order = ['KO', 'WT']

#input colour palette parameters
sns.lineplot(data = df, x = 'Frequency (Hz)', y = 'Power', 
       hue = 'Genotype', hue_order = hue_order,errorbar = ('se'),
       palette = palette_plot, linewidth = 4, ax = axs)

#remove outer border
sns.despine()
axs.set_yscale('log')
sns.despine()
axs.set_xlim(1, 48)
axs.set_ylim(10**-2, 10**3)
axs.set_xlabel("Frequency (Hz)", fontsize = 25)
axs.set_ylabel("log Power (\\u03bc$\\mathregular{V^{2}}$)", fontsize = 25)

#update the fontsize of all characters on x and y axis 
plt.rcParams.update({'font.size': 20})

#changing x-ticks
tick_values = list(range(1, 54, 6))
label_list = ['1', '6', '12', '18', '24', '30', '36', '42', '48']
axs.set_xticks(ticks = tick_values, labels = label_list)

#include an overall plot title
plt.suptitle('Condition 1 Overall Average', fontsize = 30, fontweight = 'bold')

#customise the legend 
leg = plt.legend(loc = 'upper right', frameon = False)
leg.set_title('Genotype', prop = {'size': 25})
leg_lines = leg.get_lines()
leg_texts = leg.get_texts()
plt.setp(leg_lines[0], linewidth = 8)
plt.setp(leg_lines[1], linewidth = 8)
plt.setp(leg_texts, fontsize = 20)

#increase the width of the x and y axis 
for axis in ['bottom', 'left']:
    axs.spines[axis].set_linewidth(3)

      
                
Figure 5. Figure showing custom Seaborn colour palettes.

Antes de entrar em como criar subplots, é importante diferenciar entre os argumentos fig,ax = plt.subplots() e plt.plot(), pois eles são frequentemente usados ​​de forma intercambiável. No entanto, o plt.subplots() retorna uma tupla contendo uma figura e um objeto de eixos que torna mais fácil criar subplots e reformatá-los (objeto ax) ou alterar atributos de nível de figura (objeto fig) enquanto uma sintaxe ligeiramente diferente é usada para plt.plot(). Na minha opinião, é sempre melhor usar o método fig, ax = plt.subplots(). Se você gostaria de ler mais sobre isso, esta é uma ótima postagem no blog .

Criando subtramas

Agora que passamos por como criar e personalizar um único gráfico, podemos criar subplots com diferentes tipos de gráficos em uma figura. Para demonstrar, vamos plotar as diferenças sexuais entre os animais selvagens e knockout mostrados no gráfico acima.

Como ambos os gráficos compartilham os mesmos parâmetros para os eixos x e y, adicionei os parâmetros sharex e sharey à função subplots para compartilhar os limites entre os dois eixos do subplot. Junto com os eixos, a mesma legenda se aplica a ambos os gráficos, então removi a legenda do gráfico esquerdo e reformatei a legenda da direita. Também adicionei um título geral da figura e títulos para as subtramas individuais, com preenchimento adicionado ao título geral da figura para garantir que eles não se sobreponham.

sns.set_style("white") 
fig, axs = plt.subplots(1,2, figsize=(20,10), sharex = True, sharey=True)
genotype_palette = ['plum', 'teal']
hue_order = ['KO', 'WT']

#plot individual line plots for male and female data 
sns.lineplot(data= df_male, x='Frequency (Hz)', y='Power',hue = 'Genotype', 
             errorbar = ("se"), hue_order = hue_order, linewidth = 4, 
             palette = genotype_palette, ax = axs[0])
sns.lineplot(data= df_female, x='Frequency (Hz)', y='Power',hue = 'Genotype',
             errorbar = ("se"), hue_order = hue_order, linewidth = 4, 
             palette = genotype_palette, ax = axs[1])

#remove figure border
sns.despine()

#subplot titles
axs[0].set_title('Male', fontsize = 25, fontweight = 'bold')
axs[1].set_title('Female', fontsize = 25, fontweight = 'bold')

#include an overall plot title 
fig.suptitle('Condition 1 Sex Differences', y = 0.96, fontsize = 30, 
            fontweight = 'bold') 

#remove first plot legend and customise the legend for second plot
axs[0].get_legend().remove()

# customise legend 
leg_1 = axs[1].legend( loc = 'upper right', frameon = False)
leg_1.set_title('Genotype',prop={'size':20})
leg_lines = leg_1.get_lines()
plt.setp(leg_lines[0], linewidth=10)
plt.setp(leg_lines[1], linewidth=10)

#update the fontsize of all characters on x and y axis
plt.rcParams.update({'font.size': 20})

#set tick values - because of shared x and y axis you only have to set one axis 
tick_values = list(range(0, 54, 6))
label_list = ['0', '6', '12', '18', '24', '30', '36', '42', '48']
axs[0].set_xticks(ticks = tick_values, labels = label_list)

#specify axes details 
axs[0].set_yscale('log')
axs[0].set_xlim(1, 48)
axs[0].set_ylim(10**-1, 10**3)
axs[0].set_xlabel("Frequency (Hz)", fontsize = 20)
axs[1].set_xlabel("Frequency (Hz)", fontsize = 20)
axs[0].set_ylabel("log Power (\\u03bc$\\mathregular{V^{2}}$)", fontsize = 20)

#increase width of the x and y axis 
for axis in ['bottom','left']:
    axs[0].spines[axis].set_linewidth(2)
    axs[1].spines[axis].set_linewidth(2)

      
                
Figure 6. Plot showing subplots of different conditions

A função fig.subplot_mosaic() é útil para colocar seus eixos em uma grade não uniforme. Mostrarei primeiro um exemplo de 4 subplots usando a função de mosaico onde todos os subplots são do mesmo tamanho e, em seguida, mostrarei como você pode alterar isso. A principal diferença ao usar o mosaico são as linhas fig e ax_dict. Além disso, o mesmo código acima se aplica - mas há mais subtramas e, portanto, mais parâmetros a serem alterados. Você também pode criar sua própria folha de estilo para aplicar a todos os parâmetros, para que não precise alterá-los manualmente todas as vezes.

Formatos de arquivo vetorizados

A função de mosaico é útil para criar gráficos prontos para publicação, embora a maioria dos pesquisadores use um programa secundário (como o adobe illustrator) para reformatar as figuras. Se você deseja formatar imagens diretamente no adobe illustrator, salve suas imagens como um arquivo vetorizado (por exemplo, arquivo svg).

sns.set_style("white")
fig = plt.figure(figsize = (30,20), constrained_layout = True)
ax_dict = fig.subplot_mosaic(
            [
                ["genotype_plot", "channel_plot"],
                ["wt_bar_plot", "ko_bar_plot"],
            ],
)

#palette and hue for plot 1
palette_plot = ['plum', 'teal']
genotype_hue = ['KO', 'WT']

#palette and hue for plot 2
channel_hue = ['Somatosensory', 'Visual', 'Motor']
channel_palette = ['black', 'teal', 'red']

#genotype plot 
sns.lineplot(data = df, x = 'Frequency (Hz)', y = 'Power', hue = 'Genotype',
            hue_order = genotype_hue,errorbar = ('se'), linewidth = 4, 
            palette = palette_plot, ax = ax_dict["genotype_plot"])
#channel plot 
sns.lineplot(data = df, x = 'Frequency (Hz)', y = 'Power', 
             hue = 'Channel Region', hue_order = channel_hue, errorbar = ('se'),
             palette = channel_palette, linewidth = 2, 
             ax = ax_dict["channel_plot"])

#wt bar plot
sns.barplot(data = wt_df, x = 'Bands', y = 'Power', errorbar = ('se'),
            palette = palette_wt, ax = ax_dict["wt_bar_plot"])

#wt strip plot 
sns.stripplot(data = wt_df, x = 'Bands',y = 'Power', ax=ax_dict["wt_bar_plot"])

#ko bar plot 
sns.barplot(data = ko_df, x = 'Bands', y = 'Power', errorbar = ('se'), 
            ax = ax_dict["ko_bar_plot"], palette = palette_het)

#wt strip plot 
sns.stripplot(data = ko_df, x = 'Bands', y = 'Power', 
              ax = ax_dict["ko_bar_plot"])

#update the fontsize of all characters on x and y axis 
plt.rcParams.update({'font.size': 20})

#axes configurations
sns.despine()
ax_dict["genotype_plot"].set_yscale('log')
ax_dict["channel_plot"].set_yscale('log')
ax_dict["wt_bar_plot"].set_yscale('log')
ax_dict["ko_bar_plot"].set_yscale('log')

#axes limits
ax_dict["genotype_plot"].set_xlim(1, 48)
ax_dict["genotype_plot"].set_ylim(10**-2, 10**3)
ax_dict["channel_plot"].set_xlim(1, 48)
ax_dict["channel_plot"].set_ylim(10**-2, 10**3)

#axes labels
ax_dict["genotype_plot"].set_xlabel("Frequency (Hz)", fontsize = 25)
ax_dict["genotype_plot"].set_ylabel("log Power (\\u03bc$\\mathregular{V^{2}}$)", fontsize = 25)
ax_dict["channel_plot"].set_xlabel("Frequency (Hz)", fontsize = 25)
ax_dict["channel_plot"].set_ylabel("log Power (\\u03bc$\\mathregular{V^{2}}$)", fontsize = 25)
ax_dict["wt_bar_plot"].set_xlabel("Frequency Bands (Hz)", fontsize = 25)
ax_dict["wt_bar_plot"].set_ylabel("log Power (\\u03bc$\\mathregular{V^{2}}$)", fontsize = 25)
ax_dict["ko_bar_plot"].set_xlabel("Frequency Bands (Hz)", fontsize = 25)
ax_dict["ko_bar_plot"].set_ylabel("log Power (\\u03bc$\\mathregular{V^{2}}$)", fontsize = 25)

#individual plot titles
ax_dict["genotype_plot"].set_title('Condition 1', fontsize = 35, fontweight = 'bold')
ax_dict["channel_plot"].set_title('Power Across Regions', fontsize = 35, fontweight = 'bold')
ax_dict["wt_bar_plot"].set_title('Wild-type', fontsize = 35, fontweight = 'bold')
ax_dict["ko_bar_plot"].set_title('Knockout', fontsize = 35, fontweight = 'bold')

#genotype legend 
leg_genotype = ax_dict["genotype_plot"].legend(loc = 'upper right', frameon = False)
leg_genotype.set_title('Genotype', prop = {'size': 25})
leg_lines_1 = leg_genotype.get_lines()
plt.setp(leg_lines_1, linewidth = 8)

#custom legend for plot 2  
leg_ch_region = ax_dict["channel_plot"].legend(loc = 'upper right', frameon = False)
leg_ch_region.set_title('Channel Region', prop = {'size': 25})
leg_lines_2 = leg_ch_region.get_lines()
plt.setp(leg_lines_2, linewidth = 8)

#set custom x_tick labels for lower two plots 
label_list = ['band 1', 'band 2', 'band 3', 'band 4', 'band 5' ]
ax_dict["wt_bar_plot"].set_xticklabels(label_list)
ax_dict["ko_bar_plot"].set_xticklabels(label_list)

#change weight of x and y axis 
for axis in ['bottom','left']:
    ax_dict["genotype_plot"].spines[axis].set_linewidth(3)
    ax_dict["channel_plot"].spines[axis].set_linewidth(3)
    ax_dict["wt_bar_plot"].spines[axis].set_linewidth(3)
    ax_dict["ko_bar_plot"].spines[axis].set_linewidth(3)

      
                
Figure 7. Plot showing how to use Matplotlib’s mosaic function to create different plot types in a uniform grid

Figura 8. Figura mostrando layout não uniforme


#specify subplots on the inner right corner
inner = [
    ["wt_bar_plot"],
    ["ko_bar_plot"],
]

#specify order of all plots
outer_nested_mosaic = [
    ["genotype_plot", inner],
    ['channel_plot', 'channel_plot']
]
sns.set_style("white")
ax_dict = plt.figure(figsize = (30,20), constrained_layout = True).subplot_mosaic(
outer_nested_mosaic, empty_sentinel = None)
        
#palette and hue for plot 1
palette_plot = ['plum', 'teal']
genotype_hue = ['KO', 'WT']

#palette and hue for plot 2
channel_palette = ['black', 'teal', 'red']
channel_hue = ['Somatosensory', 'Visual', 'Motor']

#palette for plot 3 and 4 
palette_wt = sns.color_palette("viridis")
palette_het = sns.color_palette("rocket")
sns.lineplot(data = df, x = 'Frequency (Hz)', y = 'Power', hue = 'Genotype', hue_order = genotype_hue,
             errorbar = ('se'), linewidth = 6, palette = palette_plot, ax = ax_dict["genotype_plot"])
sns.lineplot(data = df, x = 'Frequency (Hz)', y = 'Power', hue = 'Channel Region', hue_order = channel_hue,
             errorbar = ('se'), palette = channel_palette, linewidth = 6, ax = ax_dict["channel_plot"])
sns.barplot(data = wt_df, x = 'Bands', y = 'Power', errorbar = ('se'),palette = palette_wt, 
           ax = ax_dict["wt_bar_plot"])
sns.stripplot(data = wt_df, x = 'Bands', y = 'Power',ax = ax_dict["wt_bar_plot"])
sns.barplot(data = ko_df, x = 'Bands', y = 'Power', errorbar = ('se'), ax = ax_dict["ko_bar_plot"],
           palette = palette_het)
sns.stripplot(data = ko_df, x = 'Bands', y = 'Power',ax = ax_dict["ko_bar_plot"])

#update the fontsize of all characters on x and y axis 
plt.rcParams.update({'font.size': 20})

#axes configurations
sns.despine()
ax_dict["genotype_plot"].set_yscale('log')
ax_dict["channel_plot"].set_yscale('log')
ax_dict["wt_bar_plot"].set_yscale('log')
ax_dict["ko_bar_plot"].set_yscale('log')

#axes limits
ax_dict["genotype_plot"].set_xlim(1, 48)
ax_dict["genotype_plot"].set_ylim(10**-2, 10**3)
ax_dict["channel_plot"].set_xlim(1, 48)
ax_dict["channel_plot"].set_ylim(10**-2, 10**3)

#axes labels
ax_dict["genotype_plot"].set_xlabel("Frequency (Hz)", fontsize = 25)
ax_dict["genotype_plot"].set_ylabel("log Power (\\u03bc$\\mathregular{V^{2}}$)", fontsize = 25)
ax_dict["channel_plot"].set_xlabel("Frequency (Hz)", fontsize = 25)
ax_dict["channel_plot"].set_ylabel("log Power (\\u03bc$\\mathregular{V^{2}}$)", fontsize = 25)
ax_dict["wt_bar_plot"].set_xlabel("Frequency Bands (Hz)", fontsize = 25)
ax_dict["wt_bar_plot"].set_ylabel("log Power (\\u03bc$\\mathregular{V^{2}}$)", fontsize = 25)
ax_dict["ko_bar_plot"].set_xlabel("Frequency Bands (Hz)", fontsize = 25)
ax_dict["ko_bar_plot"].set_ylabel("log Power (\\u03bc$\\mathregular{V^{2}}$)", fontsize = 25)

#individual plot titles
ax_dict["genotype_plot"].set_title('Condition 1', fontsize = 35, fontweight = 'bold')
ax_dict["channel_plot"].set_title('Power Across Regions', fontsize = 35, fontweight = 'bold')
ax_dict["wt_bar_plot"].set_title('Wild-type', fontsize = 35, fontweight = 'bold')
ax_dict["ko_bar_plot"].set_title('Knockout', fontsize = 35, fontweight = 'bold')

#genotype legend 
leg_genotype = ax_dict["genotype_plot"].legend(loc = 'upper right', frameon = False)
leg_genotype.set_title('Genotype', prop = {'size': 25})
leg_lines_1 = leg_genotype.get_lines()
plt.setp(leg_lines_1, linewidth = 8)

#custom legend for plot 2  
leg_ch_region = ax_dict["channel_plot"].legend(loc = 'upper right', frameon = False)
leg_ch_region.set_title('Channel Region', prop = {'size': 25})
leg_lines_2 = leg_ch_region.get_lines()
plt.setp(leg_lines_2, linewidth = 8)

#set custom x_tick labels for lower two plots 
label_list = ['band 1', 'band 2', 'band 3', 'band 4', 'band 5' ]
ax_dict["wt_bar_plot"].set_xticklabels(label_list)
ax_dict["ko_bar_plot"].set_xticklabels(label_list)
for axis in ['bottom','left']:
    ax_dict["genotype_plot"].spines[axis].set_linewidth(3)
    ax_dict["channel_plot"].spines[axis].set_linewidth(3)
    ax_dict["wt_bar_plot"].spines[axis].set_linewidth(3)
    ax_dict["ko_bar_plot"].spines[axis].set_linewidth(3)

      
                
Figure 9. Plot showing how to use Matplotlib’s mosaic function to create different plot types in a non-uniform grid