Evaluate fitting on experimental data
To evaluate PyCheMelt’s performance on an experimental dataset the ACBP dataset from Hamborg et al. (2020) was processed and used. This datasets provides 16 thermal denaturation curves with differing concentrations of GuHCl. This dataset was thus used to fit the parameters in a 2 dimensional fashion to the curves regarding temperature and chemical denaturation.
[5]:
import numpy as np
import pandas as pd
from plotly.subplots import make_subplots
from pychemelt import Monomer
from pychemelt.utils.plotting import *
from pychemelt.utils.processing import (
get_colors_from_numeric_values,
combine_sequences
)
import plotly.graph_objs as go
[6]:
from pathlib import Path
data_file = Path("../../test_files/20191202_ACBP_15C_95C_processed.xlsx")
if not data_file.exists():
data_file = Path("test_files/20191202_ACBP_15C_95C_processed.xlsx")
data_file
[6]:
PosixPath('../../test_files/20191202_ACBP_15C_95C_processed.xlsx')
[7]:
def display_figure_static(fig, format="png", width=800, height=600, show_interactive=False):
"""
Display a Plotly figure in both interactive and static formats.
This function is useful for Jupyter notebooks that need to render properly
on GitHub, where interactive Plotly figures may not display.
Parameters
----------
fig : go.Figure
Plotly figure object to display
format : str, default "png"
Image format for static display ("png", "svg", "jpeg")
show_interactive : bool, default True
Whether to also show the interactive version
"""
try:
# Try to import display functionality
from IPython.display import Image, display
# Show interactive version first (for local Jupyter)
if show_interactive:
fig.show()
# Display static version (for GitHub compatibility)
static_image = fig.to_image(format=format, width=width, height=height, scale=2)
display(Image(static_image))
except ImportError:
# Fallback to regular show if not in Jupyter
print("IPython not available, showing interactive figure only")
fig.show()
except Exception as e:
print(f"Error creating static image: {e}")
print("Showing interactive figure only")
fig.show()
[4]:
# creating a Sample object for the storage and processing of the DSF data
sample = Monomer()
# reading the data
sample.read_file(data_file)
sample.set_denaturant_concentrations()
# Selecting conditions
sample.set_signal(['330nm', '350nm'])
#using all curves except the curve without denaturant as in the paper
sample.select_conditions([True]*15 + [False])
#Plotting the selected signals against the temperature
fig = plot_unfolding(sample)
display_figure_static(fig)
[8]:
# Reducing the amount of data and preprocessing steps for a faster analysis
sample.pre_fit = False
sample.max_points = 200
sample.expand_multiple_signal()
# For the estimates of the baselines, linear was used for the folded protein and exponential for the unfolded.
# the exponential baseline for the unfolded was preferred over quadratic as it previously was shown to generalize
# better and ran faster in during testing.
sample.estimate_baseline_parameters(
native_baseline_type='linear',
unfolded_baseline_type='exponential',
)
# estimations of parameters
sample.estimate_derivative()
sample.guess_Tm()
# All local fit for parameter estimation
sample.fit_thermal_unfolding_local()
# Setting the number of residues of the protein for an initial estiamte of the Cp value
sample.n_residues = 86 # doi: 10.1042/bj2450857
sample.guess_Cp()
sample.set_signal_id()
[9]:
fitting_results = {
"Tm (°C)":[],
"ΔH (kcal/mol)":[],
"Cp (kcal/mol/°C)":[],
"m-value (kcal/mol/M)":[],
"ΔG-H2O (kcal/mol)":[],
}
# Fit with global thermodynamic parameters, global slopes and global baselines
sample.fit_thermal_unfolding_global()
sample.fit_thermal_unfolding_global_global()
sample.fit_thermal_unfolding_global_global_global()
# Save the fitted thermodynamic parameters
Tm = sample.params_df.iloc[0,1]
dH = sample.params_df.iloc[1,1]
Cp = sample.params_df.iloc[2,1]
m = sample.params_df.iloc[3,1]
dG = sample.dg_df.iloc[50,0]
fitting_results["Tm (°C)"].append(Tm)
fitting_results["ΔH (kcal/mol)"].append(dH)
fitting_results["Cp (kcal/mol/°C)"].append(Cp)
fitting_results["m-value (kcal/mol/M)"].append(m)
fitting_results["ΔG-H2O (kcal/mol)"].append(dG)
plot_unfolding(sample)
Data type cannot be displayed: application/vnd.plotly.v1+json
[10]:
# Estimate the asymmetric confidence intervals for the fitted parameters using the profile likelihood method
sample.calculate_confidence_intervals(percentage=0.95)
print(sample.ci_df)
Parameter Lower_CI_95.0% Value Upper_CI_95.0%
0 Tm (°C) 53.56 53.63 53.71
1 ΔHm (kcal / mol) 75.29 75.87 76.46
2 ΔCp (kcal / mol K) 1.01 1.03 1.05
3 m-value (kcal / mol / M) 3.17 3.19 3.22
[11]:
# Estimate parameter errors using leave-one-out cross-validation
sample.leave_one_out_cross_validation()
print(sample.loo_df)
Parameter Leave_one_out_mean Leave_one_out_std N fits
0 Tm (°C) 53.629784 0.071063 15
1 ΔHm (kcal / mol) 75.970935 0.629319 15
2 ΔCp (kcal / mol / K) 1.030101 0.015496 15
3 m-value (kcal / mol / M) 3.197493 0.016590 15
[12]:
# modifying the plot_unfolding method for the generation of the figure
# Set defaults for configuration objects
plot_config = PlotConfig()
axis_config = AxisConfig()
layout_config = LayoutConfig()
legend_config = LegendConfig()
# Extract the minimum and maximum denaturation concentration
concs = sample.denaturant_concentrations
min_conc = np.min(concs)
max_conc = np.max(concs)
colors = get_colors_from_numeric_values(concs, min_conc, max_conc)
n_subplots = sample.nr_signals
# Set number of rows: 2 if less than 8 plots, else 3
nrows = 2 if n_subplots < 9 else 3
nrows = min(nrows, n_subplots) # Do not exceed the number of plots - case n equal 1
ncols = int(np.ceil(n_subplots / nrows))
row_arr = np.arange(1, nrows + 1)
col_arr = np.arange(1, ncols + 1)
# Row and column counters for subplotting
row_col_info = combine_sequences(row_arr, col_arr)
subplot_titles = sample.signal_names
fig = make_subplots(
rows=nrows,
cols=ncols,
shared_xaxes=True,
shared_yaxes=False,
vertical_spacing=layout_config.vertical_spacing,
subplot_titles=subplot_titles)
subplot_idx = 0
fittings_done = sample.global_fit_params is not None
ys_fit = None
nr_den = sample.nr_den
for i in range(n_subplots):
row = row_col_info[subplot_idx][0]
col = row_col_info[subplot_idx][1]
ys_fit = sample.predicted_lst_multiple[i]
xs = sample.temp_lst_expanded[i*nr_den:(i+1)*nr_den]
ys = sample.signal_lst_expanded[i*nr_den:(i+1)*nr_den]
for j,conc in enumerate(concs):
color = colors[j]
x = xs[j]
y = ys[j]
fig.add_trace(
go.Scatter(
x=x, y=y, mode='markers',
marker=dict(size=plot_config.marker_size, color=color),
name=f'{conc:.2f} M',
showlegend=False
),
row=row, col=col
)
# count np.nans in ys_fit
ys_fit_j = ys_fit[j]
fig.add_trace(
go.Scatter(
x=x, y=ys_fit_j, mode='lines',
line=dict(color='black', width=plot_config.line_width),
showlegend=False,
hoverinfo='skip',
hovertemplate=None
),
row=row, col=col
)
subplot_idx += 1
# Add an annotation inside the subplot showing the subfigure numeration
axis_index = i + 1
# Plotly expects 'x domain'/'y domain' (no '1') for the first axis; for others use 'xN domain'
if axis_index == 1:
xref = 'x domain'
yref = 'y domain'
else:
xref = f'x{axis_index} domain'
yref = f'y{axis_index} domain'
# Add panel label (A, B, C, etc.)
panel_label = chr(65 + i) # 65 is ASCII code for 'A'
fig.add_annotation(
x=-0.05,
y=1.1,
xref=xref,
yref=yref,
text=f'<b>{panel_label})</b>',
showarrow=False,
align='left',
font=dict(size=18, color='black')
)
# Update subplot layout with white background and axis styling
fig.update_layout(
font_family="Roboto",
plot_bgcolor='white',
paper_bgcolor='white',
legend=dict(font=dict(size=plot_config.font_size - 1))
)
for i in range(n_subplots):
row = row_col_info[i][0]
col = row_col_info[i][1]
# Set the x-axis title only for the last row
title_text_x = 'Temperature (°C)' if row == nrows else ''
# Set the y-axis title only for the first column
title_text_y = 'Signal' if col == 1 else ''
fig.update_xaxes(
title_text=title_text_x,
showgrid=axis_config.showgrid_x,
gridwidth=axis_config.gridwidth,
gridcolor='lightgray',
showline=True,
linewidth=axis_config.linewidth,
linecolor='black',
zeroline=False,
tickcolor='black',
ticks="outside",
tickwidth=axis_config.tickwidth,
ticklen=axis_config.ticklen,
title_font_size=plot_config.font_size,
tickfont_size=plot_config.font_size,
col = col,
row = row
)
fig.update_yaxes(
title_text=title_text_y,
showgrid=axis_config.showgrid_y,
gridwidth=axis_config.gridwidth,
gridcolor='lightgray',
showline=True,
linewidth=axis_config.linewidth,
linecolor='black',
zeroline=False,
tickcolor='black',
ticks="outside",
tickwidth=axis_config.tickwidth,
ticklen=axis_config.ticklen,
title_font_size=plot_config.font_size,
tickfont_size=plot_config.font_size,
nticks = axis_config.n_y_axis_ticks,
col=col,
row=row
)
# Build colorbar dict using legend_config values (orientation and x/y position)
# Choose sensible anchors depending on orientation
_xanchor = 'center' if legend_config.color_bar_orientation == 'h' else 'left'
_yanchor = 'top' if legend_config.color_bar_orientation == 'h' else 'middle'
colorbar_dict = dict(
title='[Denaturant] (M)',
tickvals=[min_conc, 0.5*(min_conc + max_conc), max_conc],
ticktext=[f"{min_conc:.2g}", f"{(min_conc + max_conc) * 0.5:.2g}", f"{max_conc:.2g}"],
len=legend_config.color_bar_length,
outlinewidth=1,
ticks='outside',
tickfont=dict(size=plot_config.font_size - 1),
orientation=legend_config.color_bar_orientation,
x=legend_config.color_bar_x_pos,
y=legend_config.color_bar_y_pos,
xanchor=_xanchor,
yanchor=_yanchor
)
fig.add_trace(
go.Scatter(
x=[None], y=[None], mode='markers',
marker=dict(
colorscale='Viridis',
cmin=min_conc,
cmax=max_conc,
colorbar=colorbar_dict
),
showlegend=False,
hoverinfo='skip'
),
row=1, col=1
)
# Adding the thermodynamic parameters of the fitted formula
fig.add_annotation(
x=legend_config.color_bar_x_pos ,
y=legend_config.color_bar_y_pos + 0.25, # move ABOVE colorbar
xref="paper",
yref="paper",
xanchor="center",
yanchor="bottom",
showarrow=False,
align="left",
text=(
" <b>Global slopes and baselines</b><br>"
f" <i>T</i><sub>m</sub> = {Tm:.1f} (°C) <br>"
f" Δ<i>H</i> = {dH:.1f} (kcal/mol) <br>"
f" Δ<i>C</i><sub>p</sub> = {Cp:.1f} (kcal/mol/°C) <br>"
f" <i>m</i> = {m:.1f} (kcal/mol/M) <br>"
f" Δ<i>G</i><sub>H<sub>2</sub>O</sub> = {dG:.1f} (kcal/mol) <br> "
f" at T = 25 °C"
),
font=dict(
size=plot_config.font_size + 1,
color="black"
),
bgcolor="white",
bordercolor="black",
borderwidth=1,
opacity=0.95
)
#changing the subplot title sizes
def is_subplot_title(x):
return x.text in ["330nm", "350nm"]
fig.update_annotations(
selector=is_subplot_title,
patch=dict(font=dict(size=plot_config.font_size + 10))
)
fig = config_fig(
fig,
plot_config.width,
plot_config.height,
plot_config.type
)
display_figure_static(fig,height=800, width=1000)