Source code for pydiana.tools.graph_tools

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