import plotly.express as px
from plotly import graph_objects as go
import matplotlib.pyplot as plt
import matplotlib.colors
import numpy as np
import pandas as pd
import copy
import warnings
from pydiana.tools import pyroot_fitting as prf
import plotly.subplots as psb
from pydiana.shell.plotly_theme import plotly_color_palette
import pydiana.tools.math_tools as mts
import plotly.subplots as subplot
from pydiana.tools import pyRDataFrame as pyRD
from pydiana.tools import string_manip as strman
import json
import pickle
import ROOT
## part of the solution from Danilo ROOT
## this is just to create the namespace
ROOT.gInterpreter.Declare(f"namespace Internal::GlobalContainers {{ set<Long64_t> gAaaBbbCcc;}} ")
[docs]
def hex2rgba(color,opacity=1):
"""
Function for converting a HEX color in RGB and setting it's opacity
"""
rgbcol=""
rgb = []
color=color.split("#")[-1]
for i in (0, 2, 4):
decimal = int(color[i:i+2], 16)
rgb.append(decimal)
rgbcol=f'rgba({rgb[0]},{rgb[1]},{rgb[2]},{opacity})'
return rgbcol
"""
def rgb2hex(color):
hexcol=''
return hexcol
"""
[docs]
def plotly2dict(fig,outfile:str, savefullfig : bool=False):
"""
Function for exporting a plotly figure to a dictionary and saving it to file
Parameters
----------
fig : go.Figure() instance to save to file
outfile: path to the file to be created (if .pkl then pickle is used otherwise json to write text files)
savefullfig: calls the full_figure_for_development() function to save all parameters (usually not necessary because all user set parameters are used and the rest is just the estetical configuration)
"""
if savefullfig:
fig = fig.full_figure_for_development()
fig_dict = fig.to_dict()
if outfile.split(".")[-1]=='pkl':
with open(outfile, "wb") as fp:
pickle.dump(fig_dict,fp)
else:
class NumpyEncoder(json.JSONEncoder):
"""
Class of encoding Numpy Arrays in JSON
"""
def default(self, obj):
if isinstance(obj, np.ndarray):
return obj.tolist()
return json.JSONEncoder.default(self, obj)
with open(outfile, "w") as fp:
json.dump(fig_dict,fp, cls=NumpyEncoder)
[docs]
def plotly_from_dict(infile):
"""
Function for importing a figure saved to a file as dict (see plotly2dict function)
Parameters
----------
infile: path to the file to read
Returns
-------
plotly figure instance
"""
if infile.split(".")[-1]=='pkl':
with open(infile, "rb") as fp:
fig_dict= pickle.load(fp)
else:
with open(infile, "r") as fp:
# Load the dictionary from the file
fig_dict = json.load(fp)
return go.Figure(fig_dict)
#####################
# #
# Pretty Plots #
# #
#####################
[docs]
def make_hist_fit(df=None,
xvar=None,
func=None,
hist2fit:str=None,
xlabel:str='',
ylabel:str='',
title:str='',
bins=None,
binwidth=None,
sep_field:str='',
logx=False,
logy=False,
xrange:list=None,
get_detail_matrices:bool=True,
options:str='QLSB',
summary:bool=True,
residuals:bool=True,
location_x:float=0.8,
location_y:float=0.7,
style:str='bar',
show_errx:bool=False,
show_erry:bool=False,
showplot:bool = True,
cl:float=None,
skip_fitlabel:bool=None,
):
"""
Function for generating and fitting a histogram with a user defined function
Parameters
----------
df
xvar
hist2fit
xlabel
ylabel
title
bins
binwidth7
sep_field
logx
logy
xrange
get_detail_matrices
options
summary
residuals
location_x
location_y
style
showplot
cl
skip_fitlabel
Returns
-------
results from prf.fit_function
figure with histogram and overlayed fitted function (also with residuals and a box with the summary of the fit if enabled)
"""
hists,fig = make_hist(df=df,
xvar=xvar,
xlabel=xlabel,
ylabel=ylabel,
title=title,
bins=bins,
binwidth=binwidth,
sep_field=sep_field,
logx=logx,
logy=logy,
xrange=xrange,
normalization=1,
style=style,
show_errx=show_errx,
show_erry=show_erry,
showplot = False)
if hist2fit is None:
hist2fit = list(hists.keys())[0]
if xrange is None:
xrange = (np.min(hists[hist2fit]['bin_edges']),np.max(hists[hist2fit]['bin_edges']))
fit = prf.root_hist_fit(heights = hists[hist2fit]['counts'],
bin_edges = hists[hist2fit]['bin_edges'],
func = func,
get_detail_matrices=get_detail_matrices,
options=options,
)
fig = overlay_fit_func(fig,fit[0],func,showplot=False,cl=cl,skip_fitlabel=skip_fitlabel)
if residuals:
fig = make_hist_residuals(hist=prf.root_hist(hists[hist2fit]['counts'],
hists[hist2fit]['bin_edges']),
figin=fig,
func=fit[1],fit_result=fit[0],
showplot=False,
)
if summary:
fig = add_summary_legend(figin=fig,
df=fit[0],
x=location_x,
y=location_y,
)
if showplot:
fig.show()
return *fit,fig
##############################################################################################
[docs]
def plot_histogram(counts=None,
bin_edges=None,
figin=None,
xlabel:str=None,
ylabel:str=None,
title:str=None,
trace_label :str = None,
logx=False,
logy=False,
xrange:list=None,
style:str='bar',
show_errx:bool=False,
show_erry:bool=True,
error_y=None,
hist=None,
):
"""
Function for plotting a precomputed histogram
Parameters
----------
counts
bin_edges
figin
xlabel
ylabel
title
trace_label
logx
logy
xrange
style
show_errx
show_erry
error_y
hist ROOT histogram instance
Returns
-------
figure instance
"""
#using ROOT histogram as easy calculator for bin centers, content and error
err_x = None
err_y = None
if show_errx:
err_x=[]
if show_erry:
err_y=[]
#If hist is not None read some metadata
if hist is None:
if counts is not None and bin_edges is not None:
hist = prf.root_hist(height=counts,bin_edges=bin_edges,error_y=error_y)
else:
raise ValueError("Either a hist object is needed or counts and bin_edges!")
else:
if ylabel is None:
ylabel = pyRD.var2name(hist.GetYaxis().GetTitle())
if xlabel is None:
xlabel = pyRD.var2name(hist.GetXaxis().GetTitle())
if xrange is None:
xrange = (hist.GetXaxis().GetXmin(),
hist.GetXaxis().GetXmax())
if title is None:
title = pyRD.var2name(hist.GetTitle())
if trace_label is None:
trace_label = hist.GetName()
#Read data from histogram
x=[]
y=[]
binwidth=[]
for b in range(hist.GetNbinsX()):
x.append(hist.GetBinCenter(b))
y.append(hist.GetBinContent(b))
binwidth.append(hist.GetBinWidth(b))
if show_errx:
err_x.append(hist.GetBinWidth(b)/np.sqrt(12))
if show_erry:
err_y.append(hist.GetBinError(b))
del hist
#Plot parameters
trace_label=f"{trace_label}" ## to deal with not standard types
if figin is not None:
fig=copy.deepcopy(figin)
else:
fig = go.Figure()
if xrange is None:
xrange = (np.min(x),np.max(x))
#Plot histogram choosing plotting style
if style == 'bar':
color = plotly_color_palette[len(fig['data'])%len(plotly_color_palette)]
line_w=0
opacity=0.75
lcolor=color
if err_y is not None or err_x is not None:
line_w=0
#to increase the opacity of the error bars
rgb_color = matplotlib.colors.hex2color(color)
lcolor=f'rgba({rgb_color[0]},{rgb_color[1]},{rgb_color[2]},1.0)'
if err_x is not None:
err_x = dict(array =err_x,type='data',width=0,color=lcolor)
if err_y is not None:
err_y = dict(array =err_y,
type='data',
width=0,
color=lcolor)
fig.add_trace(go.Bar(x=x,
y=y,
name=trace_label,
marker_opacity=opacity,
marker_line_width=line_w,
marker_line_color=lcolor,
error_x=err_x,
error_y=err_y,
marker_color=color,
width=binwidth ))
fig.update_layout(barmode='overlay')
elif style=='root':
x = np.concatenate([[np.min(x)-binwidth[0]*0.5],
x,
[np.max(x)+binwidth[-1]*0.5]])
if err_x is not None:
err_x = np.concatenate([[0],err_x,[0]])
err_x = dict(array =err_x,type='data',width=0)
if err_y is not None:
err_y = np.concatenate([[0],err_y,[0]])
err_y = dict(array =err_y,type='data',width=0)
y = np.concatenate([[y[0]],y,[y[-1]]])
fig.add_trace(go.Scatter(y=y,x=x,
error_x=err_x,error_y=err_y,
name=trace_label,
mode='lines',
line=dict(width=1,shape='hvh'),
))
elif style=='scatter':
x=np.array([x[i] for i in range(len(x)) if y[i]>0])
if err_x is not None:
err_x=[err_x[i] for i in range(len(err_x)) if y[i]>0]
err_x = dict(array =np.array(err_x),type='data',width=0)
if err_y is not None:
err_y=[err_y[i] for i in range(len(err_y)) if y[i]>0]
err_y = dict(array =np.array(err_y),type='data',width=0)
y=np.array([y[i] for i in range(len(y)) if y[i]>0])
fig.add_trace(go.Scatter(y=y,x=x,
error_x=err_x,error_y=err_y,
name=trace_label,
mode='markers',
))
if title is not None:
fig.update_layout(title = title)
if xlabel is not None:
fig.update_xaxes(title=xlabel,range=xrange)
if ylabel is not None:
fig.update_yaxes(title=ylabel)
if logx:
xmin=np.min(xrange[0])
if xmin<0:
xmin = 0.01
fig.update_xaxes(type="log",
range=(np.log10(np.max((xmin,binwidth[0]/2))),np.log10(xrange[-1])))
if logy:
ymin = np.log10(min(y[np.where(y>0)[0]]))
ymax = np.log10(max(y))
fig.update_yaxes(type="log",
range=[min((ymin-0.5,-0.5)),
np.ceil(ymax)])
return fig
###############################################################################################
[docs]
def compute_bins(xrange, binwidth=None,bins=None,logbins:bool=False, events_number:int=None):
"""
Given a xrange and binwidth/nbins, this function gives bins array to be used in make_hist
NB: if both binwidth and nbins are None (or nbins <=1) and logbins is False,
then is taken into account events_number setting nbins = np.ceil(np.sqrt(events_number)).
If event events_number is None when it's needed, the function exit with an error.
Parameters
----------
xrange (array-like) : (start,end) of the range to consider
binwidth (int or float) : width of the bin (if None, bins is used to compute it)
bins (int or array-like) : number of bins to be used (only used if binwidth is None, otherwise is overwritten) or array of bins to be used (in this case the function only returns it)
logbins (bool) : enable exponential binning
events_number (int) : number of the events that will be inside the future histogram (see description for its use)
Returns
-------
bins (array-like) : bins array of the future histogram from make_hist. It returns n+1 numbers where "n" is the number of bins, since all the edges of the bins are returned (numpy compatibility)
"""
if isinstance(bins,(list,type(np.array(0)))):
binwidth = np.diff(bins)[1] if not logbins else None
return bins, binwidth
start = xrange[0]
end = xrange[1]
interval = end-start
cond_bins = (bins is not None and bins > 1)
cond_width = (binwidth is not None and binwidth > 0.)
if cond_bins: bins = int(np.floor(bins))
## H I
if not cond_bins and not cond_width:
if events_number is not None:
bins = np.ceil(np.sqrt(events_number))
cond_bins = True
else:
raise Warning("Pass some argument to this function to be able compute bins !")
# G
if not cond_bins and cond_width and not logbins:
bins = int(np.round(interval/binwidth,0))
cond_bins = True
# E
if not cond_bins and cond_width and logbins:
raise Warning("Logbins and binwidth are not yet compatible.")
# A C
if cond_bins and logbins:
if start <=0:
start = interval/10e5
bins = np.logspace(np.floor(np.log10(start)),np.ceil(np.log10(end)),bins)
bins+=start
binwidth=None
# B D
if cond_bins and not logbins:
if not cond_width:
binwidth = interval/bins
binwidth = float(f'{binwidth:.2g}') #to round binwidth to 2 significant figures
bins = np.arange(((start//binwidth) *binwidth)-binwidth,((end//binwidth) *binwidth)+2*binwidth,binwidth)
binwidth=np.diff(bins)[0] ## in dubbio se serve
return np.array(bins),binwidth
###############################################################################################
[docs]
def hist_RDataFrame (df,
xvar:str,
bins,
):
"""
Make the histogram using ROOT DataFrame and return the counts of the histogram to be plotted later on.
Parameters
----------
df : ROOT dataframe containing data
xvar (str) : name of the column of the ROOT dataframe to be used in the histogram
bins (array-like) : array (N+1 elements, with N number of bins) containing bin_edges to be used in the TH1 histogram
Returns
-------
counts (array-like) : array (N elements) containing the counts of the constructed TH1 histogram
"""
N = len(bins)-1
bins = np.array(bins) ## need to be a numpy array
hist = df.Histo1D(("hist","hist",N,bins),xvar) ## the first 2 strings are name and title of the histogram, irrelevant
counts = []
## TODO: check in ROOT future versions if there will be an ad hoc function instead of this for cicle
for i in range(N): ## remember: the first bin is bin 1 and the i starts from 0
counts.append(hist.GetBinContent(i+1))
counts = np.array(counts)
del hist
return counts
###############################################################################################
[docs]
def make_hist(df=None,
xvar=None,
xlabel:str='',
ylabel:str='',
title:str='',
bins=None,
binwidth=None,
sep_field:str='',
logx=False,
logy=False,
xrange:list=None,
normalization=1,
style:str='bar',
show_errx:bool=False,
show_erry:bool=False,
showplot = True):
"""
Interface for easily making pretty histograms ready to be fitted (compatible with ROOT dataframe)
Parameters
----------
df : pandas or ROOT dataframe containing the data
xvar : string containing the column of the dataframe to plot or array with the numbers that are the input to the histogram
xlabel (str) : string containing the xlabel of the plot
ylabel (str) : string containing the ylabel of the plot
title (str) : string containing the title of the plot
bins (int/array-like): If "int" contains the number of bins to use, if "array-like" contains the bin edges to use
sep_filed (str) : string containing the column of the dataset to be used to separate the different colors
logx (bool) : boolean to toggle logscale on the x-axis
logy (bool) : boolean to toggle logscale on the y-axis
xrange (array-like) : xrange to be used for the plot
normalization (float/array-like): normalization for the histogram
style (str): string indicating the style of the plot. If 'bar' then histogram is plotted like a barplot, if 'point' then histogram is plotted like a scatter plot.
showplot (bool) : Boolean to indicating if the plot must be shown or not
Returns
-------
hists (dict) : dictionary containing the bin edges and counts of each histogram separated by the keys contained in the column indicated by "sep_field" (if not specified the key will be "totalhist")
fig : figure instance (depends on the plotting package used)
"""
## back compatibility without ROOT RDataFrame objects
if df is None or isinstance(df,pd.DataFrame):
"""
SET BINS (pandas)
"""
if df is None and not isinstance(xvar,str):
df=pd.DataFrame()
df['x']=xvar
xvar='x'
df = df.dropna(subset=xvar)
if xrange is None:
xrange = (df[xvar].min(),df[xvar].max())
df = df[df[xvar]>=xrange[0]]
df = df[df[xvar]<=xrange[1]]
bins,binwidth = compute_bins(xrange, binwidth=binwidth, nbins=bins, logbins=False, events_number=len(df[xvar]))
"""
Compute histogram (pandas)
"""
data_separators=["totalhist",]
if (sep_field!='') and (sep_field in df.columns.values.tolist()):
data_separators =df[sep_field].unique()
hists ={}
for sep in data_separators:
tmp = df
if sep !="totalhist":
tmp = df[df[sep_field]==sep]
counts, bins = np.histogram(tmp[xvar], bins=bins)
hists[sep] = {'counts':counts,'bin_edges':bins}
else: ## enter here if df is a ROOT dataframe #TODO: use the correct condition on RDataFrame type
if not isinstance(xvar,str): raise Warning("You can't provide an array as x and a RDataFrame together")
## convert in root_names
sep_field = pyRD.name2var(sep_field)
xvar = pyRD.name2var(xvar)
"""
SET BINS and compute histogram (RDataFrame)
"""
if xrange is None:
xrange = (df.Min(xvar).GetValue(),df.Max(xvar).GetValue())
else:
df = df.Filter(f"{xvar} >= {xrange[0]} && {xvar} <= {xrange[1]}")
bins,binwidth = compute_bins(xrange, binwidth=binwidth, bins=bins, logbins=False, events_number=df.Count().GetValue())
data_separators=["totalhist",]
if (sep_field!='') and df.HasColumn(sep_field):
sep_field_type = df.GetColumnType(sep_field)
##XXX: part of the solution from Danilo ROOT
if hasattr(ROOT.Internal.GlobalContainers,f"gUvals_{sep_field_type}"):
getattr(ROOT.Internal.GlobalContainers,f"gUvals_{sep_field_type}").clear()
else:
ROOT.gInterpreter.Declare(f"namespace Internal::GlobalContainers {{ set<{sep_field_type}> gUvals_{sep_field_type};}} ")
data_separators = df.Filter(f"return Internal::GlobalContainers::gUvals_{sep_field_type}.emplace({sep_field}).second;").Take[sep_field_type](f"{sep_field}").GetValue()
hists ={}
for sep in data_separators:
if sep == "totalhist":
counts = hist_RDataFrame(df, xvar, bins)
else: ## so sep != "totalhist"
counts = hist_RDataFrame(df.Filter(f"{sep_field} == {sep}"), xvar, bins)
hists[sep] = {'counts':counts,'bin_edges':bins}
##delete root dataframe
del df
## convert back in diana names
sep_field = pyRD.var2name(sep_field)
xvar = pyRD.var2name(xvar)
"""
Preparing Plot
"""
if xlabel =="":
xlabel = xvar
if title == "":
title = f"Histogram of {xlabel}"
if ylabel == "":
if binwidth is not None:
ylabel = f"Counts/{binwidth:.2e}"
if "[" in xlabel:
ylabel += " ["+xlabel.split('[')[-1][:-1]+']'
if binwidth is None:
ylabel= "Counts"
fig = go.Figure()
for hh_lab in hists:
hh = hists[hh_lab]
fig = plot_histogram(counts = hh['counts'],
bin_edges=hh['bin_edges'],
figin=fig,
xlabel=xlabel,
ylabel=ylabel,
title=title,
trace_label=hh_lab,
logx=False,
logy=False,
style=style,
show_errx=show_errx,
show_erry=show_erry
)
if logx:
xmin = np.min([np.min(hists[hh_lab]['bin_edges']) for hh_lab in hists])
xmax = np.max([np.max(hists[hh_lab]['bin_edges']) for hh_lab in hists])
fig.update_xaxes(type="log",range=(np.log10(np.max((xmin,binwidth/2))),np.log10(xmax)) )
if logy:
ymin = [np.log10(min(hists[hh_lab]['counts'][np.where(hists[hh_lab]['counts']>0)[0]])) for hh_lab in hists]
ymax = [np.log10(max(hists[hh_lab]['counts'])) for hh_lab in hists]
if len(ymin)>0:
ymin = min(ymin)
if len(ymax)>0:
ymax = max(ymax)
fig.update_yaxes(type="log",
range=[min((ymin-0.5,-0.5)),
np.ceil(ymax)])
if sep_field !='':
fig.update_layout(legend_title_text=sep_field)
if len(list(hists.keys()))==1:
fig.update_layout(showlegend=False)
if showplot:
fig.show()
return hists,fig
###############################################################################################
[docs]
def make_scatter(y,
x=None,
err_x=None,
err_y=None,
df=None,
sep_field:str=None,
ylab='y',
xlab='x',
title='',
logx:bool=False,
logy:bool=False,
xrange=None,
yrange=None,
showplot=True,
return_scatter=False,
mode='p'):
"""
Function for making a scatterplot. (NOT COMPATIBLE WITH ROOT DATAFRAMES, maybe it's not needed)
Parameters
----------
y : array with the y variable
x : array with the x variable
err_y : array with the error on the y variable
err_x : array with the error on the x variable
ylab : string with the label of the y-axis
xlab : string with the label of the x-axis
title : string with the title of the plot
lohx : bool for toggling logscale on x
logy : bool for toggling logscale on y
xrange : range of x axis
yrange : range of y axis
showplot : bool for toggling showing of the plot
mode : string indicating of line to use: 'l' for lines, 'p' for points, 'lp' for lines and points
Returns
-------
figure instance of the decided library
"""
scatter={}
if ylab=='y' and isinstance(y,str):
ylab=y
if xlab=='x' and x is not None:
if isinstance(x,str):
xlab=x
if df is not None:
df = pyRD.convert2pandas(df,[x,y,err_x,err_y],sep_field)
data_separators=['totalscatter']
if sep_field is not None:
data_separators=df[sep_field].unique()
for sep in data_separators:
scatter[sep]={'x':None,'y':None,'err_x':None,'err_y':None}
tmp=df
if sep!='totalscatter':
tmp = df[df[sep_field]==sep]
scatter[sep]['y']=tmp[y].values
if x is not None:
scatter[sep]['x']=tmp[x].values
else:
np.arange(len(scatter[sep]['y']))
if err_x is not None:
scatter[sep]['err_x']=tmp[err_x].values
if err_y is not None:
scatter[sep]['err_y']=tmp[err_y].values
else:
scatter['totalscatter']={'x':None,'y':y,'err_x':err_x,'err_y':err_y}
if x is None:
scatter['totalscatter']['x'] = np.arange(len(y))
else:
scatter['totalscatter']['x'] = x
if xrange is None:
xmin=[]
xmax=[]
for sep in scatter:
idx_min=np.argmin(scatter[sep]['x'])
idx_max=np.argmax(scatter[sep]['x'])
xmin.append(scatter[sep]['x'][idx_min])
xmax.append(scatter[sep]['x'][idx_max])
if scatter[sep]['err_x'] is not None:
xmin[-1]-=1.1*scatter[sep]['err_x'][idx_min]
xmax[-1]+=1.1*scatter[sep]['err_x'][idx_max]
xrange = (np.min(xmin),np.max(xmax))
if yrange is None:
ymin=[]
ymax=[]
for sep in scatter:
idx_min=np.argmin(scatter[sep]['y'])
idx_max=np.argmax(scatter[sep]['y'])
ymin.append(scatter[sep]['y'][idx_min])
ymax.append(scatter[sep]['y'][idx_max])
if scatter[sep]['err_y'] is not None:
ymin[-1]-=1.1*scatter[sep]['err_y'][idx_min]
ymax[-1]+=1.1*scatter[sep]['err_y'][idx_max]
yrange = (np.min(ymin),np.max(ymax))
if mode == 'p':
mode = 'markers'
elif mode == 'lp' or mode == 'pl':
mode = 'lines+markers'
elif mode =='l':
mode='lines'
else:
warnings.warn(f"Linetype {mode} not supported, using markers")
mode='markers'
fig = go.Figure()
for sep in scatter:
x = scatter[sep]['x']
y = scatter[sep]['y']
err_x = scatter[sep]['err_x']
err_y = scatter[sep]['err_y']
if err_x is not None:
err_x = dict(array =err_x,type='data')
if err_y is not None:
err_y = dict(array =err_y,type='data')
fig.add_trace(go.Scatter(x=x,
y=y,
error_x=err_x,
error_y=err_y,
name=f"{sep}",
mode=mode))
fig.update_layout(title=title,showlegend=(len(scatter)>1))
fig.update_xaxes(title=xlab,range=xrange)
fig.update_yaxes(title=ylab,range=yrange)
if logx:
if xrange[0]<0:
xrange = (np.min(((xrange[1]-xrange[0])/1000,0.1)),xrange[1])
if xrange[1]<0:
xrange=(xrange[0],xrange[0]*10)
fig.update_xaxes(type='log',range=np.log10(xrange))
if logy:
if yrange[0]<0:
yrange = (np.min(((yrange[1]-yrange[0])/1000,0.1)),yrange[1])
if yrange[1]<0:
yrange=(yrange[0],yrange[0]*10)
fig.update_yaxes(type='log',range=np.log10(yrange))
if sep_field is not None:
fig.update_layout(legend_title_text=sep_field)
if showplot:
fig.show()
res = (fig)
if return_scatter:
res = (scatter,fig)
return res
##############################################################################################
[docs]
def make_fit_scatter(x,y,
func,
df=None,
err_x=None,err_y=None,
xlab='',ylab='',title='',
logx:bool=False,logy:bool=False,
showplot:bool=True, points2skip:list=None,
residuals:bool=True,summary:bool=True,
sort:bool=False,
fit_options: str = 'QSB',
get_detail_matrices:bool=False,
legendpos_x=0.8,legendpos_y=0.2,fitcolor:str=plotly_color_palette[1],
printchi2:bool=True,
unit_x:str = None,unit_y:str=None,
xrange=None,
cl:float = None,
skip_fitlabel:bool=False,
):
"""
Function for easily making fits of scatter plots
Parameters
----------
x: array containing the x coordinates of the data. If df is provided then x can be the string of the df column (if the unit is written in square brackets then it is automatically read)
y: array containing the y coordinates of the data. If df is provided then y can be the string of the df column (if the unit is written in square brackets then it is automatically read)
func : root function to be fitted
df : dataframe with the data to fit. If not provided then one can provide arrays to x and y
err_x : array containing the errors on the x coordinates. If df is provided then err_x can be the string of the df column
err_y : array containing the errors on the y coordinates. If df is provided then err_y can be the string of the df column
xlab : string with the x axis label
ylab : string with the y axis label
showplot : boolean to enable plot showing
points2skip : list with the indexes of which points need to be skipped for the fit (is sort=True in order of x)
residuals : bool for enabling the construnction of the residuals plot
summary : bool for enabling the plotting of the summary legend
fit_options : string with ROOT's fitting options
get_detailed matrices : boolean for enabling the returning of the fit matrices
legendpos_x : int specifying the x-position of the summary legend (from 0 to 1)
legendpos_y : int specifying the y-position of the summary legend (from 0 to 1)
cl : float between 0<cl<1 with confidence interval of fit to plot. If None it will not be plotted.
Returns
-------
return [fit_results,figure]
if get_detailed_matrices:
return [fit_results,matrices,figure]
"""
#points2fit=range(len(x))[:]
if df is not None:
df = pyRD.convert2pandas(df,[x,y,err_x,err_y])
if isinstance(x,str):
if unit_x is None:
unit_x = x.split('[')[-1].split(']')[0]
xlab=x
x = df[x].values
if xrange is None:
xmin = max((np.min(x),func.GetXmin()))
xmax = min((np.max(x),func.GetXmax()))
xrange=(xmin,xmax)
if isinstance(y,str):
if unit_y is None:
unit_y = y.split('[')[-1].split(']')[0]
ylab=y
y = df[y].values
if isinstance(err_y,str):
err_y = df[err_y].values
if isinstance(err_x,str):
err_x = df[err_x].values
else:
if xrange is None:
xmin = max((np.min(x),func.GetXmin()))
xmax = min((np.max(x),func.GetXmax()))
xrange=(xmin,xmax)
func.SetRange(min(xrange),max(xrange))
if err_x is not None:
if sort:
_,err_x=mts.sortmultivec(x,err_x)
err_x = np.array(err_x)
if err_y is not None:
if sort:
_,err_y=mts.sortmultivec(x,err_y)
err_y = np.array(err_y)
if sort:
x,y=mts.sortmultivec(x,y)
points2fit= [ i for i,xx in enumerate(x) if xx>=xrange[0] and xx<= xrange[1]]
if points2skip is not None:
points2fit = [i for i in points2fit if i not in points2skip]
y=np.array(y)
x=np.array(x)
fig = make_scatter(y=y,
x=x,
err_y=err_y,
err_x=err_x,
xlab=xlab,
ylab=ylab,
showplot=False,
title=title,
logx= logx,
logy= logy,
mode='p',
)
fig2 = make_scatter(y=y[points2fit],
x=x[points2fit],
err_y=err_y[points2fit] if err_y is not None else None,
err_x=err_x[points2fit] if err_x is not None else None,
xlab=xlab,
ylab=ylab,
showplot=False,
title=title,
logx= logx,
logy= logy,
mode='p',
)
scat = prf.root_scatter(y=y[points2fit],
x=x[points2fit],
yerr=err_y[points2fit] if err_y is not None else None,
xerr=err_x[points2fit] if err_x is not None else None,
)
fit,func,matrices=prf.root_fitting(graph=scat,
func=func,
get_detail_matrices=True,
options=fit_options,
)
fig = overlay_plots((fig,fig2),showplot=False,
labels={0:'data',1:'fitted data'},
colors={1:plotly_color_palette[3]}
)
if fitcolor is None:
fitcolor = plotly_color_palette[(len(fig['data']))%len(plotly_color_palette)]
fig = overlay_fit_func(fig,fit,func,showplot=False,color =fitcolor,cl=cl,skip_fitlabel=skip_fitlabel)
if residuals:
fig = make_residuals(fig,
func=func,
y=y,
x=x,
err_y=err_y,
fit_result=fit,
showplot=False,
points2fit=points2fit,
)
if summary:
fig = add_summary_legend(figin=fig,df=fit,showplot=False,y=legendpos_y,x=legendpos_x,printchi2=printchi2)
fig.update_yaxes(autorange=True)
fig.update_xaxes(autorange=True)
if showplot:
fig.show()
results = [fit,fig]
if get_detail_matrices:
results = [fit,matrices,fig]
del scat
return results
##############################################################################################
[docs]
def make_scatter_polyfit(x,y,
degree:int,
df =None,
name : str = None,
unit_x:str = None,
unit_y:str = None,
err_x=None,err_y=None,
xrange=None,
xlab='',ylab='',title='',
logx:bool=False,logy:bool=False,
showplot:bool=True, points2skip:list=None,
residuals:bool=True,summary:bool=True,
sort:bool=False,
fit_options: str = 'QSB',
get_detail_matrices:bool=False,
legendpos_x=0.8,legendpos_y=0.2,
printchi2:bool=True,
cl:float=None,
fitcolor:str=plotly_color_palette[1],
skip_fitlabel:bool=False,
):
"""
Function for easily making polinomial fits of scatter plots
Parameters
----------
x: array containing the x coordinates of the data. If df is provided then x can be the string of the df column (if the unit is written in square brackets then it is automatically read)
y: array containing the y coordinates of the data. If df is provided then y can be the string of the df column (if the unit is written in square brackets then it is automatically read)
degree : Degree of the polynomial to be fitted
df : dataframe with the data to fit. If not provided then one can provide arrays to x and y
err_x : array containing the errors on the x coordinates. If df is provided then err_x can be the string of the df column
err_y : array containing the errors on the y coordinates. If df is provided then err_y can be the string of the df column
xrange : fitting range, if not provided then all the range is used.
xlab : string with the x axis label
ylab : string with the y axis label
showplot : boolean to enable plot showing
points2skip : list with the indexes of which points need to be skipped for the fit (is sort=True in order of x)
residuals : bool for enabling the construnction of the residuals plot
summary : bool for enabling the plotting of the summary legend
fit_options : string with ROOT's fitting options
get_detailed matrices : boolean for enabling the returning of the fit matrices
legendpos_x : int specifying the x-position of the summary legend (from 0 to 1)
legendpos_y : int specifying the y-position of the summary legend (from 0 to 1)
cl : float between 0<cl<1 with confidence interval of fit to plot. If None it will not be plotted.
Returns
-------
return [fit_results,figure]
if get_detailed_matrices:
return [fit_results,matrices,figure]
"""
if name is None:
name =f'pol{degree}'
if df is not None:
df = pyRD.convert2pandas(df,[x,y,err_x,err_y])
if isinstance(x,str):
if unit_x is None:
unit_x = x.split('[')[-1].split(']')[0]
xlab=x
x = df[x].values
if xrange is None:
xmin = np.min(x)
xmax = np.max(x)
xrange=(xmin,xmax)
if isinstance(y,str):
if unit_y is None:
unit_y = y.split('[')[-1].split(']')[0]
ylab=y
y = df[y].values
if isinstance(err_y,str):
err_y = df[err_y].values
if isinstance(err_x,str):
err_x = df[err_x].values
else:
if xrange is None:
xmin = np.min(x)
xmax = np.max(x)
xrange=(xmin,xmax)
parnames={}
for i in range(degree+1):
parnames[i]=f'p{i}'
if unit_x is not None and unit_y is not None:
parnames[i]+='['+unit_y
if i >0:
parnames[i]+='/'+f"{unit_x}"
if i >1:
parnames[i]+=f"^{i}"
parnames[i]+=']'
parvals = {0:np.median(y)}
for i in range(1,degree+1):
dy=y
dx=np.diff(x)
for _ in range(i):
dy=np.diff(dy)
parvals[i]=np.median(dy/(dx[:len(dy)]**i))
pol = prf.root_func(
expr=f'pol{degree}',
name=name,
parnames = parnames,
parvals= parvals,#{i:0 for i in range(degree+1)},
xrange=xrange,
)
return make_fit_scatter(x=x,y=y,
func=pol,
err_x=err_x,err_y=err_y,
xlab=xlab,ylab=ylab,title=title,
logx=logx,logy=logy,
showplot=showplot, points2skip=points2skip,
residuals=residuals,summary=summary,
sort=sort,
fitcolor=fitcolor,
fit_options= fit_options,
get_detail_matrices=get_detail_matrices,
legendpos_x=legendpos_x,legendpos_y=legendpos_y,
printchi2=printchi2,cl=cl,skip_fitlabel=skip_fitlabel)
###############################################################################################
[docs]
def plot_heatmap(counts=None,
xbin_edges=None,
ybin_edges=None,
xlabel:str=None,
ylabel:str=None,
zlabel:str=None,
title:str=None,
trace_label :str = None,
logx=False,
logy=False,
logz=False,
xrange:list=None,
yrange:list=None,
zrange:list=None,
hist2d=None,
sidepannels:bool=True,
colorscale=None,
):
"""
Function for plotting a precomputed 2dhistogram
Parameters
----------
counts=None,
xbin_edges=None,
ybin_edges=None,
xlabel:str=None,
ylabel:str=None,
zlabel:str=None,
title:str=None,
trace_label :str = None,
logx=False,
logy=False,
logz=False,
xrange:list=None,
yrange:list=None,
hist2d=None,
Returns
-------
figure instance
"""
#If hist is not None read some metadata
if hist2d is None:
if counts is not None and xbin_edges is not None and ybin_edges is not None:
hist2d = prf.root_hist2d(height=counts,
xbin_edges=xbin_edges,
ybin_edges=ybin_edges,
name=trace_label,
title=title
)
else:
raise ValueError("Either a hist object is needed or counts and bin_edges!")
else:
if ylabel is None:
ylabel = pyRD.var2name(hist2d.GetYaxis().GetTitle())
if xlabel is None:
xlabel = pyRD.var2name(hist2d.GetXaxis().GetTitle())
if zlabel is None:
zlabel = pyRD.var2name(hist2d.GetZaxis().GetTitle())
if xrange is None:
xrange = (hist2d.GetXaxis().GetXmin(),
hist2d.GetXaxis().GetXmax())
if yrange is None:
yrange = (hist2d.GetYaxis().GetXmin(),
hist2d.GetYaxis().GetXmax())
if zrange is None:
zrange = (hist2d.GetMinimum(),
hist2d.GetMaximum())
if title is None:
title = pyRD.var2name(hist2d.GetTitle())
if trace_label is None:
trace_label = hist2d.GetName()
#Read data from histogram
x=[]
y=[]
z=[]
for biny in range(hist2d.GetNbinsY()):
z.append([])
y.append(hist2d.GetYaxis().GetBinCenter(biny))
for binx in range(hist2d.GetNbinsX()):
if biny==0:
x.append(hist2d.GetXaxis().GetBinCenter(binx))
z[biny].append(hist2d.GetBinContent(binx,biny))
del hist2d
#Plot parameters
trace_label=f"{trace_label}" ## to deal with not standard types
fig = go.Figure()
if xrange is None:
xrange = (np.min(x),np.max(x))
if yrange is None:
yrange = (np.min(y),np.max(y))
if zrange is None:
zrange = (np.min(z),np.max(z))
H4plot = [[h if h>0 else None for h in hh] for hh in z ]
if logz:
zrange=list(zrange)
if zrange[0] <=0:
zrange[0] = 0.001*zrange[1]
zrange = np.log10(zrange)
zrange = (np.floor(zrange[0]),np.ceil(zrange[1]))
H4plot = [[np.log10(h) if h is not None else None for h in hh]
for hh in H4plot ]
if logx:
xmin = min(xrange)
if xmin<=0:
xmin = 0.001 * max(xrange)
xrange=(xmin,max(xrange))
xrange=np.log10(xrange)
if logy:
ymin = min(yrange)
if ymin<=0:
ymin = 0.001 * max(yrange)
yrange=(ymin,max(yrange))
yrange=np.log10(yrange)
if sidepannels:
y_int =np.sum(z,axis=0)
x_int =np.sum(z,axis=1)
fig = subplot.make_subplots(rows=2,
cols=2,
column_widths=[0.9, 0.1],
row_heights=[0.1,0.9],
shared_xaxes=True,
shared_yaxes=True,
horizontal_spacing=0.03,
vertical_spacing=0.05,
)
fig.add_trace(go.Heatmap(z=H4plot,
y=y,
x=x,
colorbar_title={'text':zlabel,'side':'right'},
colorscale=colorscale,
zmax=zrange[1],
zmin=zrange[0],
),row=2,col=1)
fig.add_trace(go.Bar(y=y_int,
x=x,
marker_color=plotly_color_palette[0],
marker_line_width=0,
width=np.diff(x)[0],
),row=1,col=1)
fig.add_trace(go.Bar(y=y,
x=x_int,
orientation='h',
marker_color=plotly_color_palette[0],
marker_line_width=0,
width=np.diff(y)[0]*1.01,
),row=2,col=2)
fig.update_layout(title=title)
fig.update_xaxes(type = 'log' if logx else 'linear',
title=xlabel,range=xrange,row=2,col=1)
fig.update_yaxes(type = 'log' if logy else 'linear',
title=ylabel,range=yrange,row=2,col=1)
if logz:
fig.update_xaxes(type='log',
range = np.log10((0.5,np.max(x_int))),row=2,col=2)
fig.update_yaxes(type='log',
range = np.log10((0.5,np.max(y_int))),row=1,col=1)
fig.update_xaxes(nticks=2,row=2,col=2)
fig.update_yaxes(nticks=2,row=1,col=1)
else:
fig.add_trace(go.Heatmap(z=H4plot,
y=y,
x=x,
zmax=zrange[1],
zmin=zrange[0],
colorbar_title={'text':zlabel,'side':'right'},
colorscale=colorscale,
name=trace_label,
))
fig.update_layout(title=title)
fig.update_xaxes(type = 'log' if logx else 'linear',
title=xlabel,range=xrange)
fig.update_yaxes(type = 'log' if logy else 'linear',
title=ylabel,range=yrange)
if logz:
fig['data'][0]['colorbar']['tickvals']=np.arange(zrange[0],zrange[1]+1)
fig['data'][0]['colorbar']['ticktext']=["10<sup>"+f"{int(x)}</sup>" for x in np.arange(zrange[0],zrange[1]+1)]
fig.update_layout(showlegend=False)
return fig
###############################################################################################
[docs]
def make_heatmap(df=None,
y=None,
x=None,
bins=[None,None],
binwidths=[None,None],
ylab=None,
xlab=None,
title='',
logx:bool=False,
logy:bool=False,
logz:bool=False,
xrange=None,
yrange=None,
showplot=True,
sidepannels:bool=False,
colorscale=None,
logbinsx:bool=False,
logbinsy:bool=False,
zrange=None,
):
"""
Function for making an heatmap. (NOT COMPATIBLE WITH ROOT DATAFRAME, in progress)
Parameters
----------
y : array with the y variable
x : array with the x variable
ylab : string with the label of the y-axis
xlab : string with the label of the x-axis
title : string with the title of the plot
logx : bool for toggling logscale on x
logy : bool for toggling logscale on y
plot : string indicating the plotting library
xrange : range of x axis
yrange : range of y axis
showplot : bool for toggling showing of the plot
Returns
-------
figure instance of the decided library
"""
if isinstance(df,pd.DataFrame) or df is None:
if df is not None:
## da capire se va fatto o no
df = pyRD.convert2pandas(df,[x,y])
df = copy.deepcopy(df)
if ylab is None:
ylab=y
if x is None:
x = np.arange(len(df[y]))
xlab='Index'
df[xlab]=x
elif xlab is None:
xlab=x
else:
df=pd.DataFrame()
df[ylab]=y
y=ylab
if x is None:
x = np.arange(len(df[ylab]))
xlab='Index'
df[xlab]=x
else:
df[xlab]=x
x=xlab
if ylab is None:
ylab='y'
if xlab is None:
xlab='x'
df = df.dropna(subset=[x,y])
if len(df)==0:
warnings.warn(f"Zero-length dataset passed to function.")
return None
y=df[y].values
x=df[x].values
masky=np.array([True]*len(y))
maskx=np.array([True]*len(x))
if xrange is not None:
maskx = np.where((x>=min(xrange)) & (x<max(xrange)), [True]*len(x),[False]*len(x))
else:
xrange = (min(x),max(x))
if yrange is not None:
masky = np.where((y>=min(yrange)) & (y<max(yrange)), [True]*len(y),[False]*len(y))
else:
yrange=(min(y),max(y))
mask = maskx *masky
y=y[mask]
x=x[mask]
xedges,xbinwidth = compute_bins(xrange,binwidth=binwidths[0],
bins=bins[0],logbins=logbinsx, events_number=len(x))
yedges,ybinwidth = compute_bins(yrange,binwidth=binwidths[1],bins=bins[1],
logbins=logbinsy, events_number=len(y))
H, yedges, xedges = np.histogram2d(y,x,bins=[yedges,xedges])
hist2d = None
else: ## here df is ROOT dataframe
if not isinstance(x,str): raise Warning("You can't provide an array as x and a RDataFrame together")
if not isinstance(y,str): raise Warning("You can't provide an array as y and a RDataFrame together")
if xlab is None:
xlab = copy.deepcopy(x)
if ylab is None:
ylab = copy.deepcopy(y)
## convert in root_names
x = pyRD.name2var(x)
y = pyRD.name2var(y)
if xrange is None:
xrange = (df.Min(x).GetValue(),df.Max(x).GetValue())
else:
df = df.Filter(f"{x} >= {xrange[0]} && {x} <= {xrange[1]}")
if yrange is None:
yrange = (df.Min(y).GetValue(),df.Max(y).GetValue())
else:
df = df.Filter(f"{y} >= {yrange[0]} && {y} <= {yrange[1]}")
## XXX: chiedi a Giorgio se va bene che len(x)=len(y)
events_number = df.Count().GetValue()
xedges,xbinwidth = compute_bins(xrange,binwidth=binwidths[0],
bins=bins[0],logbins=logbinsx, events_number=events_number)
yedges,ybinwidth = compute_bins(yrange,binwidth=binwidths[1],bins=bins[1],
logbins=logbinsy, events_number=events_number)
## XXX: qui faccio l'analogo di hist_RDataFrame() ma mi serve anche l'hist2d
## per ora non faccio un'altra funzione, poi si vedrà se serve
nBins_x = len(xedges)-1
nBins_y = len(yedges)-1
## NB: inside the nested (...) there is a TH2DModel, made up by ( name, title, nbinsx, edges_xbins, nbinsy, edges_ybins )
hist2d = df.Histo2D(("hist2d","hist2d",nBins_x,xedges,nBins_y,yedges),x,y)
H = [[hist2d.GetBinContent(x+1,y+1) for x in range(nBins_x)] for y in range(nBins_y)]
H = np.array(H)
##delete root dataframe
del df
## convert back in diana names
x = pyRD.var2name(x)
y = pyRD.var2name(y)
xunit=''
yunit=''
if '[' in xlab and ']' in xlab:
xunit = xlab.split('[')[-1].split(']')[0]
if '[' in ylab and ']' in ylab:
yunit = ylab.split('[')[-1].split(']')[0]
cbartitle="Counts"
if xbinwidth is not None:
cbartitle+=f"/{xbinwidth:.2e} {xunit}"
if ybinwidth is not None:
cbartitle+=f"/{ybinwidth:.2e} {yunit}"
fig=plot_heatmap(counts=H,
xbin_edges=xedges,
ybin_edges=yedges,
xlabel=xlab,
ylabel=ylab,
zlabel=cbartitle,
title=title,
trace_label = None,
logx=logx,
logy=logy,
logz=logz,
xrange=xrange,
yrange=yrange,
zrange=zrange,
hist2d=hist2d,
sidepannels=sidepannels,
colorscale=colorscale,
)
if showplot:
fig.show()
## delete hist2d
del hist2d
## questi devono rimanere gli stessi
return {'counts':H,'Xbin_edges':xedges,'Ybin_edges':yedges},fig
###############################################################################################
[docs]
def make_fit_heatmap(df=None,
y=None,
x=None,
func=None,
bins=[100,100],
binwidths=[None,None],
ylab=None,
xlab=None,
title='',
logx:bool=False,
logy:bool=False,
logz:bool=False,
xrange=None,
yrange=None,
showplot=True,
sidepannels:bool=False,
summary:bool=False,
location_x=0.7,
location_y=0.7,
fit_options="QSLB",
colorscale=None,
cl:float=None,
logbinsx:bool=False,
logbinsy:bool=False,
get_detail_matrices:bool=False,
):
"""
Function for making a scatterplot.
Parameters
----------
y : array with the y variable
x : array with the x variable
func : function to fit
ylab : string with the label of the y-axis
xlab : string with the label of the x-axis
title : string with the title of the plot
logx : bool for toggling logscale on x
logy : bool for toggling logscale on y
plot : string indicating the plotting library
xrange : range of x axis
yrange : range of y axis
showplot : bool for toggling showing of the plot
Returns
-------
pandas dataframe with fit results and figure instance
"""
hist2ddict,fig = make_heatmap(df=df,
y=y,
x=x,
bins=bins,
binwidths=binwidths,
ylab=ylab,
xlab=xlab,
title=title,
logx=logx,
logy=logy,
logz=logz,
logbinsx=logbinsx,
logbinsy=logbinsy,
xrange=xrange,
yrange=xrange,
showplot=False,
sidepannels=False,
colorscale=colorscale)
hist2dfit = prf.root_hist2d(height=hist2ddict['counts'],
xbin_edges=hist2ddict['Xbin_edges'],
ybin_edges=hist2ddict['Ybin_edges'])
res = prf.root_fitting(graph=hist2dfit,func=func,
get_detail_matrices=get_detail_matrices,
options=fit_options)
fig = overlay_fit_func(figin=fig,fit=res[0],func=func,funlab=None,
showplot=showplot,color=None,summary=summary,
location_x=location_x,location_y=location_y,
cl=cl,skip_fitlabel=True)
del hist2dfit
res=list(res)
res.append(fig)
return res
#############################
# #
# Basic Tools #
# #
#############################
[docs]
def makelogaxis(*axis_range,maxticks=10):
"""
Function for making the labels for a log-axis
Parameters
----------
*axis_range : two numbers with the range of the axis
maxticks : maximum number of ticks to be returned
Returns
-------
1D array with the tick values
1D array with the tick labels
"""
axis_range =[min(axis_range),max(axis_range)]
if(axis_range[0]<=0):
axis_range[0] = axis_range[1]/1000
if axis_range[0]<=0:
axis_range = (0.1,0.2)
axis_range = np.log10(axis_range)
axis_range = (np.floor(axis_range[0]),np.ceil(axis_range[1]))
ticks = np.arange(axis_range[0],axis_range[1]+1)
labels = ["10<sup>"+f"{int(x)}</sup>" for x in ticks]
step = int(np.ceil(len(ticks)/maxticks))
return ticks[::step],labels[::step]
#############################
# #
# Graphical Manipulation #
# #
#############################
[docs]
def overlay_plots(figs:list,showplot=False,labels:dict=None,colors :dict = None,skip_traces:dict=None,autoyrescale:bool=False,autoxrescale:bool=False,useTotalIndex:bool=True,skip_label:dict=None):
"""
Function for overlaying plots (with matplotlib only works with scatter plots and if histograms are the first plot).
Parameters
----------
figs (array-like) : Array containing the list of pictures to overlay (they will be overlayed to the first one). All the plots must be produced from the same plotting package.
showplot (bool) : Boolean to toggle the showing of the plot
labels (dict) : Default to None. If not None, the added traces will be relabelled as indicated.
labcolorsels (dict) : Defacolors None. If not None, the added traces will be colored as indicated.
Returns
-------
fig = one plot containing all the overlay
"""
fig=copy.deepcopy(figs[0])
fig['data']=[]
if type(fig)!=type(go.Figure()):
raise Exception("Plot type not supported")
trnum = -1
for i,ff in enumerate(figs):
for j,tt in enumerate(ff['data']):
trnum +=1
if skip_traces is not None:
if i in list(skip_traces.keys()):
if j in skip_traces[i]:
continue
if skip_label is not None:
if i in list(skip_label.keys()):
if j in skip_label[i]:
tt['showlegend']=False
fig.add_trace(tt)
if useTotalIndex:
if labels is not None:
if trnum in labels:
fig['data'][-1]['name'] = labels[trnum]
if colors is not None:
if trnum in colors:
fig['data'][-1]['marker']['color'] = colors[trnum]
if not useTotalIndex:
if labels is not None:
for i,tt in enumerate(fig['data']):
if i in labels:
tt['name']=labels[i]
if colors is not None:
for i,tt in enumerate(fig['data']):
if i in colors:
tt['marker']['color']=colors[i]
if autoyrescale:
fig['layout']['yaxis'].update(autorange = True)
if autoxrescale:
fig['layout']['xaxis'].update(autorange = True)
if showplot:
fig.show()
return fig
###############################################################################################
[docs]
def overlay_func(figin,func,npoints:int=1000,
params=None,xrange:list=None,
funlab:str=None,showplot:bool=True,color:str=None,skip_fitlabel:bool=False):
"""
Function used to overlay a function to a plot
Parameters
----------
figin : input figure
func : instance of root function
npoints: number of points used to plot the function
params : dictionary of the values of the parameters
xrange : range in which to plot the function
funlab : label of the fit function for the plot
showplot : boolean to toggle the showplot
Returns
-------
fig : output figure
"""
fig=figin
if skip_fitlabel is None:
skip_fitlabel=False
if funlab==None:
funlab = func.GetName()
if funlab=='':
funlab='Function'
if xrange is None:
xrange = (func.GetXmin(),func.GetXmax())
x = np.linspace(min(xrange),max(xrange),npoints)
if isinstance(params,dict):
keys = np.sort(list(params.keys()))
params = [params[i] for i in keys]
y = prf.eval_root_func(func,x,params)
figfun = go.Figure()
figfun.add_trace(go.Scatter(x=x,y=y,name=funlab,showlegend=(not skip_fitlabel)))
fig = overlay_plots((fig,figfun),showplot=showplot,colors={len(fig['data']):color})
return fig
##############################################################################################
[docs]
def overlay_fit_func(figin,fit,func,funlab:str=None,showplot:bool=True,color:str=None,
summary:bool=False,location_x=0.7,location_y=0.7,cl=None,clopacity=0.3,skip_fitlabel:bool=False):
"""
Function used to overlay a fitted function to a plot
Parameters
----------
figin : input figure
fit : pandas dataframe
func : instance of root function
funlab : label of the fit function for the plot
showplot : boolean to toggle the showplot
summary: boolean for enabling summary fit legend
location_x: relative x position of fit legend
location_y: relative y position of fit legend
cl: if not None then plots a colorband around the function that specifies the confidence interval specified
Returns
-------
fig : output figure
"""
fig=figin
if funlab==None:
funlab = func.GetName()
if funlab=='':
funlab='Fit'
xmin = fit['Values']['MinX']
xmax = fit['Values']['MaxX']
numpoints = max(int(fit['Values']['NDF'])*100,1000)
params = fit['Values'][:-6]
if color is None:
color = plotly_color_palette[(len(fig['data']))%len(plotly_color_palette)]
fig= overlay_func(figin=fig,func=func,npoints=numpoints,
params=params,xrange=(xmin,xmax),
funlab=funlab,showplot=False,color=color,skip_fitlabel=skip_fitlabel)
if summary:
fig = add_summary_legend(figin=fig,
df=fit,
x=location_x,
y=location_y,
)
if cl is not None:
x =np.linspace(xmin,xmax,numpoints)
y = prf.eval_root_func(x=x,func=func,params=params)
confint=prf.extract_confidence_interval(x=x,
ndim=1,
cl=cl,
)
concolor = color
if concolor[0]=='#':
concolor = hex2rgba(color,clopacity)
ymin=y-confint
ymax=y+confint
fig.add_trace(go.Scatter(x=np.concatenate((x,x[::-1])),
y=np.concatenate((ymax,ymin[::-1])),
name=f'{cl*100:.1f}% CL',
line_width=0,
fill='toself',
marker_color=concolor,
line_color=concolor,
fillcolor=concolor,
))
fig.update_layout(showlegend=True)
if showplot:
fig.show()
return fig
###############################################################################################
[docs]
def rebin_histogram(histogram, rebin_factor : int = 3):
"""
Function for reducing histogram binning
Parameters
----------
histogram : dictionary with the histogram bin_edges and counts
rebin_factor : integer indicating how many bins will be summed together
Returns
-------
dictionary with histogram
"""
hh = histogram['counts']
bb = histogram['bin_edges']
if len(hh)%rebin_factor != 0:
print("Adjusting histogram length for rebinning")
bb = np.concatenate((bb,np.ones( rebin_factor-(len(hh)%rebin_factor)) *bb[-1]))
hh = np.concatenate((hh,np.zeros(rebin_factor-(len(hh)%rebin_factor))))
new_b = np.array(bb[::rebin_factor])
new_c = hh.reshape(-1,rebin_factor).sum(axis=1)
return {'counts':new_c,'bin_edges':new_b}
###############################################################################################
[docs]
def plot_matrix(matrix:list,ylabel:str='',xlabel:str='',
title:str='',colorscale:str='Viridis',exponentformat='power',
showplot:bool=False,**kwargs):
"""
Function for plotting a matrix as a heatmap.
Parameters
----------
matrix : 2D array with the matrix values
ylabel : string with the labels for the yaxis
xlabel : string with the labels for the xaxis
title : string with the title for the plot
colorscale : string indicating the colorscale
exponentformat : string indicating the type of colorscale to use
showplot : Boolean indicating wether to show or not the plot
**kwargs
Returns
-------
go.Figure Instance
"""
fig = go.Figure()
fig.add_trace(go.Heatmap(z=np.flip(matrix,axis=0),
colorbar_exponentformat=exponentformat,
colorscale=colorscale,
**kwargs ))
fig.update_layout(title=title)
fig.update_xaxes(title=xlabel)
fig.update_yaxes(title=ylabel)
if(showplot):
fig.show()
return fig
###############################################################################################
[docs]
def make_subplots(figures:list,rows:int=None,cols:int=None,
shared_xaxes:bool=False,shared_yaxes:bool=False,
x_title : str = None , y_title :str = None,
showplot:bool=False,shared_labels:bool=False,**kwargs):
"""
Utility function for making plotly subplots
Parameters
----------
figures : list of figures
rows : int indicating how many subplot rows
cols : int indicating how many subplot columns
shared_xaxes : bool for enabling xaxes sharing
shared_yaxes : bool for enabling yaxes sharing
x_title : string that sets the global x-axis title
y_title : string that sets the global y-axis title
showplot : bool indicating whether or not to show the plot
**kwargs
"""
if rows is None or cols is None:
print("Geometry of subplots not completetly specified, using default shape")
totalfig = len(figures)
rows = np.floor(np.sqrt(totalfig))
cols = np.floor(np.sqrt(totalfig))
while(rows*cols <totalfig):
cols+=1
if(rows*cols <totalfig):
rows+=1
titles=[]
xtitles = []
ytitles = []
layouts = []
for fig in figures:
xtitle = ''
ytitle = ''
title=''
try:
title=fig['layout']['title']['text']
except:
title=''
try:
xtitle = fig['layout']['xaxis']['title']['text']
except:
xtitle = ''
try:
ytitle = fig['layout']['yaxis']['title']['text']
except:
ytitle = ''
titles.append(title)
xtitles.append(xtitle)
ytitles.append(ytitle)
if x_title is None and shared_xaxes==True:
x_title = [x for x in xtitles if x != '']
if x_title != []:
x_title = x_title[0]
else:
x_title=None
if y_title is None and shared_yaxes==True:
y_title = [y for y in ytitles if y != '']
if y_title != []:
y_title = y_title[0]
else:
y_title=None
figure = subplot.make_subplots(rows=rows,
cols=cols,
subplot_titles=titles,
shared_xaxes=shared_xaxes,
shared_yaxes=shared_yaxes,
x_title=x_title,
y_title=y_title,
)
labels = {}
for i,fig in enumerate(figures):
row=i//cols
col=i-cols*row+1
row+=1
for trace in fig['data']:
figure.add_trace(trace,row=row,col=col)
if shared_labels:
trace_lab=trace['name']
showlabel =False
if trace_lab not in list(labels.keys()):
labels[trace_lab] = {'label':len(labels),'color':plotly_color_palette[len(labels)%len(plotly_color_palette)]}
showlabel=True
figure['data'][-1]['showlegend']=showlabel
figure['data'][-1]['legendgroup']=labels[trace_lab]['label']
figure['data'][-1]['marker_color']=labels[trace_lab]['color']
figure['data'][-1]['line_color']=labels[trace_lab]['color']
if x_title is None:
axis_label = 'xaxis'
if i >0:
axis_label+=str(i+1)
figure['layout'][axis_label]['title']=xtitles[i]
if y_title is None:
axis_label = 'yaxis'
if i >0:
axis_label+=str(i+1)
figure['layout'][axis_label]['title']=ytitles[i]
# figure.update_traces(row=row,col=col,**fig['layout'])
# figure.update_xaxes(row=row,col=col,**fig['layout']['xaxes'])
# figure.update_yaxes(row=row,col=col,**fig['layout']['xaxes'])
if showplot:
figure.show()
return figure
###############################################################################################
[docs]
def plot_fit_matrices(matrices,variables,showplot:bool=False,**kwargs):
"""
Utility function for plotting covariance and correlation matrices
Parameters
----------
matrices: dictionary containing the matrices title as keys and the 2D array of the matrix as elemets
variables: list containing the parameters' names
showplot : bool indicating whether or not to show the plot
"""
figures=[]
for mat in matrices:
if(mat=='Correlation'):
zmin=-1
zmax=1
xanchor=1
else:
zmin=np.min(matrices[mat])
zmax=np.max(matrices[mat])
xanchor=-0.2
fig = plot_matrix(matrices[mat],title=mat,zmin=zmin,zmax=zmax,
colorbar=dict(x=xanchor),x=variables,y=variables[::-1])
fig.update_layout(xaxis_dtick=1,yaxis_dtick=1)
figures.append(fig)
return make_subplots(figures,rows=1,cols=2,showplot=showplot)
#######################
# #
# Residual Plot #
# #
#######################
[docs]
def make_residuals(figin,x:list,y:list,
func,fit_result=None,
err_x:list=None,err_y:list=None,points2fit:list=None,
showplot:bool=True,marker:dict=None,errorbars:dict=None,unit:str=None):
"""
Function for making residual plot for fit.
Parameters
----------
figin : input plot to which append the residuals
x : x coordinates used for residual calculation
y : y coordinates used for residual calculation
func : input function
fit_result : DataFrame like the output of extract_fit_parameters. If None the default func parameters are used
showplot: bool for enabling plot showing
err_x : x errors used for residual calculation
err_y : y errors used for residual calculation
"""
x = np.array(x)
y = np.array(y)
if err_x is not None:
err_x = np.array(err_x)
err_x = dict(type="data",
array=err_x,
)
if errorbars is not None:
for cc in errorbars:
err_x[cc]=errorbars[cc]
if err_y is not None:
err_y = np.array(err_y)
err_y = dict(type="data",
array=err_y,
)
if errorbars is not None:
for cc in errorbars:
err_y[cc]=errorbars[cc]
params = None
if fit_result is not None:
params = list(fit_result['Values'][:-6])
residuals = (y-prf.eval_root_func(func,x,params))
fig = psb.make_subplots(rows=2, cols=1,row_heights=[0.7, 0.3], shared_xaxes=True, vertical_spacing=0.02)
[fig.add_trace(tr,row=1,col=1) for tr in figin['data']]
if marker is not None:
marker_data=copy.deepcopy(marker)
marker_data['color'] = plotly_color_palette[0]
marker_fit=copy.deepcopy(marker)
marker_fit['color'] = plotly_color_palette[3]
else:
marker_data = dict(color=plotly_color_palette[0])
marker_fit = dict(color=plotly_color_palette[3])
fig.add_trace(go.Scatter(
x=x,
y=residuals,
error_y=err_y,
error_x=err_x,
mode = 'markers',
marker = marker_data,#dict(color=plotly_color_palette[0]),
name='Residuals',
showlegend=False,
), row=2, col=1)
if points2fit is not None:
if err_y is not None:
err_y['array'] = err_y['array'][points2fit]
if err_x is not None:
err_x['array'] = err_x['array'][points2fit]
fig.add_trace(go.Scatter(
x=x[points2fit],
y=residuals[points2fit],
error_y=err_y,
error_x=err_x,
mode = 'markers',
marker = marker_fit,#dict(color=plotly_color_palette[3]),
name='Residuals',
showlegend=False,
), row=2, col=1)
fig['layout']['yaxis']=figin['layout']['yaxis']
fig['layout']['yaxis']['anchor']= 'x'
fig['layout']['yaxis']['domain']= [0.314, 1.0]
try:
fig.update_xaxes(title_text=figin['layout']['xaxis']['title']['text'], row=2,col=1)
except:
fig.update_xaxes(title='X',row=2,col=1)
try:
fig.update_layout(title=figin['layout']['title']['text'])
except:
fig.update_layout(title="")
if unit is None:
unit=''
try:
unit = ' ['+figin['layout']['yaxis']['title']['text'].split("[")[1].split("]")[0]+']'
except:
unit = ""
fig.update_yaxes(title_text=f"Residuals<br>{unit}", row=2,col=1)
fig.add_hline(y=0,col=1,row=2, line=dict(dash="dash", color="black"))
if showplot:
fig.show()
return fig
##############################################################################################
[docs]
def make_hist_residuals(hist,figin,
func,fit_result=None,
showplot:bool=True,
marker:dict=None,
errorbars:dict=None,
unit:str=None,
):
"""
Function for making residual plot for histograms.
Parameters
----------
hist : instance of TH1D containing the histogram used for the fitting. Coordinates and errors are taken from this object.
figin : input plot to which append the residuals
func : input function
fit_result : DataFrame like the output of extract_fit_parameters. If None the default func parameters are used
showplot: bool for enabling plot showing
"""
x= [hist.GetBinCenter(b) for b in range(1,int(hist.GetEntries())-1) if hist.GetBinContent(b)>0]
y= [hist.GetBinContent(b) for b in range(1,int(hist.GetEntries())-1) if hist.GetBinContent(b)>0]
err_x= [hist.GetBinWidth(b)/np.sqrt(12) for b in range(1,int(hist.GetEntries())-1) if hist.GetBinContent(b)>0]
err_y= [hist.GetBinError(b) for b in range(1,int(hist.GetEntries())-1) if hist.GetBinContent(b)>0]
xmin = func.GetXmin()
xmax = func.GetXmax()
points2fit =[b for b in range(len(x)) if x[b]>=xmin and x[b]<=xmax and y[b]>0]
del hist
if unit is None:
unit=''
try:
unit = ' [('+figin['layout']['yaxis']['title']['text'].split("[")[1].split("]")[0]+')<sup>-1</sup>]'
except:
unit = ""
return make_residuals(figin=figin,x=x,y=y,
func=func,fit_result=fit_result,
err_x=err_x,err_y=err_y,points2fit=points2fit,
showplot=showplot,marker=marker,errorbars=errorbars,unit=unit)
############################
# #
# Make Result Legend #
# #
############################
[docs]
def add_summary_legend(figin,df,x=0.8,y=0.05,xref = "x domain", yref = 'y domain',
align='left',
bordercolor="white",
borderwidth=0.7,
borderpad=4,
bgcolor="#ffe994",
opacity=1,
showlegend:bool=True,
showplot:bool=False,
printchi2:bool=True,
**kwargs,
):
"""
Function for adding a colored square in the plot with the result of the fit parameters. Everything in df[:-6] will be added to the plot summary legend.
Parameters
----------
figin : plotly figure to which to add the annotation
df : dataframe with fit results in the style of the one made by extract fit parameters
showplot : bool for enabling plot showing
printchi2 : bool for enabling reduced chi2 writing on plot
rest of the arguments are the ones for the plotly add_annotation() function.
Returns
-------
Plotly figure with the summary legend
"""
fig = copy.deepcopy(figin)
fig.update_layout(showlegend=showlegend)
df = copy.deepcopy(df.fillna('NAN'))
title = []
vars =df[:-6]
chi2=None
if(printchi2):
chi2 = df[-6:]['Values']['Reduced Chi2']
varnames = vars.index.tolist()
for v in varnames:
val = vars['Values'][v]
err = vars['Errors'][v]
unit= vars['Units'][v]
l = f'{val:.2e}'
if err!='NAN':
l =l+f' +/- {err:.2e}'
if unit not in ['','-',' ',np.nan,'NAN']:
if err!='NAN':
l = '('+l+') '+unit
else:
l+=' '+unit
l = f'{v} = {l}'
title.append(l)
title = '<br>'.join(title)
if chi2 is not None:
title +=f'<br>Reduced χ<sup>2</sup>={chi2:.2f}'
fig.add_annotation(
x=x,
y=y,
xref=xref,
yref=yref,
text=title,
align=align,
bordercolor=bordercolor,
borderwidth=borderwidth,
borderpad=borderpad,
bgcolor=bgcolor,
opacity=opacity,
**kwargs,
)
if showplot:
fig.show()
return fig
#######################
# #
# Summary Plotting #
# #
#######################
[docs]
def summary_plot(df, var:str,err_var:str=None,labels:str=None,color:str=None,
title:str=None,unit:str=None,round_digit:int = 1,
showMean:bool=True, strict_color:bool=False,
showSigma:bool=True,add2title:bool=True,showplot:bool=True,
showlegend:bool=False,separate_stats:bool=False,annotation_size=25,
annotation_positions = ['top right','top left','bottom right','bottom left'],
make_annotation:bool=False,left_margin=None, showchi2:bool=True,
):
"""
Function that automatically construct summary plot of a given var2plot.
If getError is False, var2plot is plotted with no error.
If getColor is False, plot all points of the same color (red).
(NOT COMPATIBLE WITH ROOT DATAFRAME, in progress)
Parameters
----------
df : Pandas dataframe with final results
var (str) : column name of df that is being plotted
err_var (str) : column name of df that has the errors of "var"
labels (str) : column name of df that has the y axis labels for the plot. If None sequential numbers will be used.
color (str) : column name of df with respect to which select the color of a datapoint
title (str) : title of the fig output, if None it is set to var
unit (str) : to indicate the unit by hand (otherwise searched in var)
round_digit (int) : how many digits to use when printing numbers
showMean (bool) : if True, plot a vertical line at x=mean(var2plot)
showSigma (bool) : if True, plot a vertical rectangle to show std of data [if showMean is False, then also this is False]
strict_color (bool) : use colors indicated in the column provided in "color". Otherwise the column in 'color' is just used to choose between different default colors base on content.
add2title (bool) : if True, add to title "(mean +- std) unit"
showplot (bool) : enable plot showing
Returns
----------
fig : plotly scatter of Channel vs df[var2plot]
"""
if var not in list(df.keys()):
raise Exception(f"{var} is not a column of df, aborting...")
if err_var not in list(df.keys()) and err_var is not None:
print(f"{err_var} is not a column of df, plotting with no errors.")
err_var = None
showchi2=False
if labels not in list(df.keys()) and labels is not None:
print(f"{labels} is not a column of df, using default labels.")
labels = None
if color not in list(df.keys()) and color is not None:
print(f"{color} is not a column of df, using default colors.")
color = None
if not showMean:
showchi2=False
if unit is None:
unit=''
try:
unit = var.split("[")[1].split("]")[0]
except:
print(f"NB: {var} has no unit (just a check, not an error)")
unit = ""
if title is None:
title = var
if annotation_positions is None:
annotation_positions = [None]
#saving arrays to plot for readability
totalmean = np.mean(df[var])
totalstd = np.std(df[var])
if showchi2:
totalchi2 = mts.calc_Chi2Mean(values = df[var],err_values=df[err_var])
df2plot={'Results':df}
fig = go.Figure()
labs={}
if color is not None:
df2plot = {}
for field in list(df[color].unique().tolist()):
df2plot[field] = df[df[color]==field]
for i,field in enumerate(df2plot):
data = df2plot[field]
errors_x=None
x2plot = data[var]
y2plot = np.arange(len(x2plot),dtype=int)
col = plotly_color_palette[i%len(plotly_color_palette)]
if err_var is not None:
errors_x = dict(type="data",
array=data[err_var].tolist(),
color=col,
)
if labels is not None:
labsvec = list(data[labels].unique())
else:
labsvec = data.index
y2plot = []
for ll in labsvec:
if ll in list(labs.keys()):
y2plot.append(labs[ll])
else:
if labs == {}:
y2plot.append(0)
else:
y2plot.append(max(labs.values())+1)
labs[ll]=y2plot[-1]
y2plot=[]
for ll in list(data.index):
line = data.loc[ll]
if labels is not None:
y2plot.append(labs[line[labels]])
else:
y2plot.append(labs[ll])
name = f"{field}"
if separate_stats:
mean=np.mean(x2plot)
std=np.std(x2plot)
chi2=0
annotation_text=''
if showMean and showSigma:
annotation_text="M = ("+str(np.round(mean,round_digit))+" +/- "+str(np.round(std,round_digit))+") "+unit
elif showMean and not showSigma:
annotation_text= 'M = '+str(np.round(mean,round_digit))+' '+unit
if showchi2:
chi2 = mts.calc_Chi2Mean(values = x2plot,err_values=data[err_var].tolist())
annotation_text+=f'<br> Chi2_M = {chi2:.2f}'
if showMean:
if make_annotation:
fig.add_vline(x=mean, line_width=3, line_dash="dash", line_color=col,
opacity=0.4,
annotation_text=annotation_text,
annotation_font= dict(color=col,size=annotation_size),
annotation_position=annotation_positions[i%len(annotation_positions)],
)
else:
fig.add_vline(x=mean, line_width=3, line_dash="dash", line_color=col,
opacity=0.4)
name += " : <br> "+annotation_text
if showSigma:
fig.add_vrect(x0=mean-std,
x1=mean+std,
line_width=0,
fillcolor=col,
opacity=0.1)
fig.add_trace(go.Scatter(x=x2plot,
y=y2plot,
error_x = errors_x,
mode = "markers",
marker_line_width=1,
marker_line_color=col,
marker_size=5,
name=name,
marker_color=col,
)
)
if not separate_stats:
if showMean and showSigma:
title+=" <br> Mean : ("+str(np.round(totalmean,round_digit))+" +/- "+str(np.round(totalstd,round_digit))+") "+unit
elif showMean and not showSigma:
title+= " <br> Mean : "+str(np.round(totalmean,round_digit))+' '+unit
if showMean:
fig.add_vline(x=totalmean, line_width=3, line_dash="dash", line_color='black', opacity=0.8)
if showSigma:
fig.add_vrect(x0=totalmean-totalstd, x1=totalmean+totalstd, line_width=0, fillcolor='yellow', opacity=0.2)
if showchi2 and not separate_stats:
title += f'\t Chi2_M = {totalchi2:2f}'
# last visual details of fig output
fig.update_layout(showlegend=showlegend, title_text=title,margin_l=left_margin)
fig.update_xaxes(title_text=var, mirror=True)
step = np.mean(np.diff(list(labs.values())))
fig.update_yaxes(title_text=labels, mirror=True,
type="category", tickmode="array",
range=(min(list(labs.values()))-step,max(labs.values())+step),
tickvals=list(labs.values()), ticktext=list(labs.keys()))
if showplot:
fig.show()
return fig