-1

I have KM plots using Lifelines that I want to fit 2 per portrait A4 sheet (there will be more than 3). Even though the figsize is the same they are all randomly different sizes and placing. 1st has expanded vertically, is nice and tight to the title but has silly wide gap to the "at risk". 2nd and 3rd are shrunk vertically, have an excessive wide gap at top to title and reduced gap to the "at risk". I want all of them to be close to the title at the top and close to "at risk" at the bottom.

Also I've had to add blank lines and a dummy plot to stop the last plot being truncated when in 2nd printer preview after clicking print from Juypter print preview.

Imports, Data, Configuration

import random
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
from matplotlib.ticker import (MultipleLocator, AutoMinorLocator)
from scipy.stats import norm
from lifelines import KaplanMeierFitter 
from lifelines.plotting import add_at_risk_counts
from lifelines.utils import median_survival_times
from lifelines.plotting import add_at_risk_counts

# cohort size 200
# subgroubs and events by time 60 months after EOT
# FD 12 months, no progression or withdrawal modelled
# poor size 12, 4 events, PFS 67%
# worst size 18, 10 events, PFS 44.4%
# medium size 104, 20 events, PFS 81%
# fair size 66, 6 events, PFS 91%
SG = 4
DUR = 60.0
FD = 12.0
CUT_OFF = 60
n_ = [18, 12, 104, 66]
e_ = [11, 4, 25, 4]
label_ = ['(u+d)-18', '(m+d)-12', '(u+w)-104', '(m+w)-66', 
         'high risk-134', 
         '(?+d)-30', '(?+w)-170', '(u+?)-122', '(m+?)=78']
FIG_X = 6.6
FIG_Y = 4.6

CI = False # True or False to show confidence intervals
SC = True # True or False to show censor tick marks

data_ = []
parm = ['T', 'E']
for i in range(0, SG):
    print('index', i)
    df_ = pd.DataFrame(columns=parm)
    ival_ = DUR/e_[i]
    # now for some events
    for n1 in range(1 , e_[i] + 1):
        df_ = pd.concat([df_, pd.DataFrame([FD + n1*ival_ , 1.0],index=parm).T], ignore_index=True)
    # and finally the censors
#    cen_  = n_[i] - e_[i]
    for n1 in range(int(FD + DUR), int(FD + DUR + n_[i] - e_[i])):
        x_ = random.randrange(0,1000)/1000
        time_ = 84 * norm.pdf(x_,0,1.1)/norm.pdf(0,0,1.1)
#        print(time_)
        df_ = pd.concat([df_, pd.DataFrame([time_ , 0.0],index=parm).T], ignore_index=True)
#        df_ = pd.concat([df_, pd.DataFrame([DUR + FD + 1 , 0.0],index=parm).T], ignore_index=True)
    data_.append(df_)

data_.append(pd.concat([data_[0], data_[1], data_[2]]))
data_.append(pd.concat([data_[0], data_[1]]))
data_.append(pd.concat([data_[2], data_[3]]))
data_.append(pd.concat([data_[0], data_[2]]))
data_.append(pd.concat([data_[1], data_[3]]))

Fig 1

fig1 = plt.figure(figsize=(FIG_X, FIG_Y))
ax = plt.subplot(111)
fig1.add_subplot(ax)
fig1.patch.set(linewidth=4, edgecolor='0.5')

kmf_ = []
median_ = []
median_conf_ival_ = []
pop_ = []

fig1.suptitle('NOW YOU CAN SEE IT!\nPFS - IGVH + del(17p)/TP53 Status\n',
             fontsize=14, fontweight ="bold", x=0, y=1. , ha="left")

for i in range(0, SG):
    kmf_.append(KaplanMeierFitter())
    kmf_[i].fit_right_censoring(data_[i]['T'], data_[i]['E'], label=label_[i])
    ax = kmf_[i].plot_survival_function(ax=ax, ci_show=CI, show_censors=SC)
    med_ = kmf_[i].median_survival_time_
    if med_ == np.inf:
        med_ = " NR  "
    median_.append(med_)
    median_conf_ival_.append(median_survival_times(kmf_[i].confidence_interval_))
    pop_.append(kmf_[i].cumulative_density_at_times(CUT_OFF).iloc[0])
    print(i, 'Median :', median_[i], ', PFS :', int(1000*(1-pop_[i]))/10,'% at', CUT_OFF,'months')
    print(len(median_conf_ival_[i]))
    print(median_conf_ival_[i].iloc[0])

ax.set_xticks(range(0, int(FD + DUR) + 1, 6))
ax.set_xlim([0, int(FD + DUR) + 2 ])
ax.set_ylim([0, 1.05])
ticks_y = ticker.FuncFormatter(lambda x, pos: '{0:g}'.format(x*100))
ax.yaxis.set_major_formatter(ticks_y)

#for i in range(0, SG_):
add_at_risk_counts(kmf_[0], kmf_[1], kmf_[2], kmf_[3], 
                   ax=ax, 
                   labels=[l for l in label_], 
                   rows_to_show=["At risk"])

ax.legend(loc="lower left")
ax.set_xlabel('Months'),
ax.grid(b=True, axis='x')
ax.grid(b=True, axis='y')
# For the minor ticks, use no labels; default NullFormatter.
ax.xaxis.set_minor_locator(MultipleLocator(1))
ax.yaxis.set_minor_locator(MultipleLocator(0.05))

plt.plot([12, 12], [0.5, 1.05])
plt.plot([CUT_OFF, CUT_OFF], [0, 1.05])

plt.show()
plt.tight_layout();

enter image description here

Fig 2

fig2 = plt.figure(figsize=(FIG_X, FIG_Y))
bx = plt.subplot(111)
fig2.add_subplot(bx)
   
# fig.set_edgecolor("green")
# frame around figure
fig2.patch.set(linewidth=4, edgecolor='0.5')

fig2.suptitle('HIDE THE BAD DATA IN THE POOR\nPFS with/out high-risk features\n',
             fontsize=14, fontweight ="bold", x=0, y=1. , ha="left")

for i in range(4, 6):
    kmf_.append(KaplanMeierFitter())
    kmf_[i].fit_right_censoring(data_[i-1]['T'], data_[i-1]['E'], label=label_[i-1])
    bx = kmf_[i].plot_survival_function(ax=bx, ci_show=CI, show_censors=SC)
    med_ = kmf_[i].median_survival_time_
    if med_ == np.inf:
        med_ = " NR  "
    median_.append(med_)
    median_conf_ival_.append(median_survival_times(kmf_[i].confidence_interval_))
    pop_.append(kmf_[i].cumulative_density_at_times(CUT_OFF).iloc[0])
    print(i, 'Median :', median_[i], ', PFS :', int(1000*(1-pop_[i]))/10,'% at', CUT_OFF,'months')
    print(len(median_conf_ival_[i]))
    print(median_conf_ival_[i].iloc[0])

bx.set_xticks(range(0, int(FD + DUR + 1), 6))
bx.set_xlim([0, int(FD + DUR) + 2 ])
bx.set_ylim([0, 1.05])
ticks_y = ticker.FuncFormatter(lambda x, pos: '{0:g}'.format(x*100))
bx.yaxis.set_major_formatter(ticks_y)

# now add the table:
# from lifelines.plotting import add_at_risk_counts
add_at_risk_counts(kmf_[4], kmf_[5], 
                   ax=bx, 
                   labels=[label_[3], label_[4]], 
                   rows_to_show=["At risk"])

bx.set_xlabel('Months'),
bx.grid(b=True, axis='x')
bx.grid(b=True, axis='y')
# For the minor ticks, use no labels; default NullFormatter.
bx.xaxis.set_minor_locator(MultipleLocator(1))
bx.yaxis.set_minor_locator(MultipleLocator(0.05))

plt.plot([12, 12], [0.5, 1.05])
plt.plot([CUT_OFF, CUT_OFF], [0, 1.05])
plt.tight_layout()
plt.show()

enter image description here

Fig 3

fig3 = plt.figure(figsize=(FIG_X, FIG_Y))
cx = plt.subplot(111)
fig3.add_subplot(cx)
   
# fig.set_edgecolor("green")
# frame around figure
fig3.patch.set(linewidth=4, edgecolor='0.5')

fig3.suptitle('TRADITIONAL PLOTS HIDING WORST RESPONSE WITH BETTER RESPONSE\nPFS - del(17p)/TP35 and IGVH status\n',
             fontsize=14, fontweight ="bold", x=0, y=1. , ha="left")

# plots, NOTE order
#cx = kmf_3c.plot_survival_function(ax=cx, ci_show=ci, show_censors=sc)
#cx = kmf_2c.plot_survival_function(ax=cx, ci_show=ci, show_censors=sc)
#cx = kmf_4c.plot_survival_function(ax=cx, ci_show=ci, show_censors=sc)
#cx = kmf_1c.plot_survival_function(ax=cx, ci_show=ci, show_censors=sc)

for i in range(6, 10):
    kmf_.append(KaplanMeierFitter())
    kmf_[i].fit_right_censoring(data_[i-1]['T'], data_[i-1]['E'], label=label_[i-1])
    cx = kmf_[i].plot_survival_function(ax=cx, ci_show=CI, show_censors=SC)
    med_ = kmf_[i].median_survival_time_
    if med_ == np.inf:
        med_ = " NR  "
    median_.append(med_)
    median_conf_ival_.append(median_survival_times(kmf_[i].confidence_interval_))
    pop_.append(kmf_[i].cumulative_density_at_times(CUT_OFF).iloc[0])
    print(i, 'Median :', median_[i], ', PFS :', int(1000*(1-pop_[i]))/10,'% at', CUT_OFF,'months')
    print(len(median_conf_ival_[i]))
    print(median_conf_ival_[i].iloc[0])

cx.set_xticks(range(0, int(FD + DUR) + 1, 6))
cx.set_xlim([0, int(FD + DUR) + 2 ])
cx.set_ylim([0, 1.05])
ticks_y = ticker.FuncFormatter(lambda x, pos: '{0:g}'.format(x*100))
cx.yaxis.set_major_formatter(ticks_y)

# now add the table:
add_at_risk_counts(kmf_[6], kmf_[7], kmf_[8], kmf_[9], 
                   ax=cx, 
                   labels=[label_[5], label_[6], label_[7], label_[8]],
                   rows_to_show=["At risk"])

cx.set_xlabel('Months'),
cx.grid(b=True, axis='x')
cx.grid(b=True, axis='y')
# For the minor ticks, use no labels; default NullFormatter.
cx.xaxis.set_minor_locator(MultipleLocator(1))
cx.yaxis.set_minor_locator(MultipleLocator(0.05))

plt.plot([12, 12], [0.5, 1.05])
plt.plot([CUT_OFF, CUT_OFF], [0, 1.05])
plt.tight_layout()
plt.show()

print('last line \n\n\n\n\n\n\n\n\n\n it is now')

enter image description here

Fig 4

fig4 = plt.figure(figsize=(FIG_X, FIG_Y))
dx = plt.subplot(111)
fig4.add_subplot(dx)
   
# fig.set_edgecolor("green")
# frame around figure
fig4.patch.set(linewidth=4, edgecolor='0.5')

fig4.suptitle('Dummy\n',
             fontsize=14, fontweight ="bold", x=0, y=1. , ha="left")

plt.plot([12, 12], [0.5, 1.05])
plt.plot([CUT_OFF, CUT_OFF], [0, 1.05])
plt.tight_layout()
plt.show()

enter image description here

Trenton McKinney
  • 56,955
  • 33
  • 144
  • 158
Peter Hill
  • 43
  • 1
  • 6
  • It's executable after fixing all of the `.grid(b=True, axis='x')` calls, by replacing `.grid(b=True, axis='x')` and `.grid(b=True, axis='y')` with `.grid(visible=True, axis='both')`. There is no `b` parameter. Certainly not minimal. – Trenton McKinney Sep 02 '23 at 18:43
  • In any case, **Even though the figsize is the same they are all randomly different sizes and placing** is true, however you fill each figure with randomly sized elements (e.g. a `table`, and `title`), which utilize the figure space. – Trenton McKinney Sep 02 '23 at 18:48
  • The tables are being added to the matplotlib figure by the lifelines package, and they don't appear to have all the positioning parameters typically available to [`.table`](https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.table.html). – Trenton McKinney Sep 02 '23 at 18:58

0 Answers0