@@ -4010,6 +4010,156 @@ def graph(
40104010 {"data" : data , "win" : win , "eid" : env , "opts" : opts }, endpoint = "events"
40114011 )
40124012
4013+ @pytorch_wrap
4014+ def parallel_coordinates (self , X , Y = None , win = None , env = None , opts = None ):
4015+ """
4016+ This function draws a parallel coordinates plot for comparing multiple
4017+ experiments across different parameters. Each row in the `NxM` matrix
4018+ `X` represents one experiment and each column represents a dimension
4019+ (e.g., a hyperparameter or metric).
4020+
4021+ An optional `N`-length vector `Y` supplies per-experiment color values
4022+ (e.g., accuracy or loss) so that the lines are shaded according to
4023+ a continuous colorscale.
4024+
4025+ The following `opts` are supported:
4026+
4027+ - `opts.dimensions`: `list` of dimension label strings (length M)
4028+ - `opts.colormap`: colorscale name (default `'Electric'`)
4029+ - `opts.reversescale`: reverse the colorscale direction (`bool`)
4030+ - `opts.ranges`: `dict` mapping dimension index to `[min, max]`
4031+ - `opts.constraintranges`: `dict` mapping dimension index to `[min, max]`
4032+ preset filter ranges
4033+ - `opts.tickvals`: `dict` mapping dimension index to a `list` of
4034+ tick positions
4035+ - `opts.ticktext`: `dict` mapping dimension index to a `list` of
4036+ tick labels (requires matching `tickvals`)
4037+ - `opts.max_experiments`: `int`, cap to top N experiments sorted by `Y`
4038+ descending (requires `Y`)
4039+ - `opts.title`: plot title
4040+ """
4041+ opts = {} if opts is None else opts
4042+ _title2str (opts )
4043+ _assert_opts (opts )
4044+
4045+ X = np .asarray (X , dtype = float )
4046+ assert X .ndim == 2 , "X must be a 2D matrix (N experiments x M dimensions)"
4047+ N , M = X .shape
4048+ assert N >= 1 , "X must have at least 1 experiment (row)"
4049+ assert M >= 2 , "X must have at least 2 dimensions (columns)"
4050+
4051+ if Y is not None :
4052+ Y = np .squeeze (np .asarray (Y , dtype = float ))
4053+ assert Y .ndim == 1 , "Y must be a 1D vector"
4054+ assert (
4055+ len (Y ) == N
4056+ ), "Length of Y (%d) must match number of rows in X (%d)" % (len (Y ), N )
4057+
4058+ max_exp = opts .get ("max_experiments" )
4059+ if max_exp is not None :
4060+ assert Y is not None , "opts.max_experiments requires Y to be provided"
4061+ if N > max_exp :
4062+ top_idx = np .argsort (Y )[::- 1 ][:max_exp ]
4063+ X = X [top_idx ]
4064+ Y = Y [top_idx ]
4065+ N = len (Y )
4066+
4067+ dim_labels = opts .get ("dimensions" )
4068+ if dim_labels is None :
4069+ dim_labels = ["Dim %d" % (i + 1 ) for i in range (M )]
4070+ assert isinstance (dim_labels , (list , tuple )), (
4071+ "opts.dimensions should be a list/tuple of length %d" % M
4072+ )
4073+ assert (
4074+ len (dim_labels ) == M
4075+ ), "Length of opts.dimensions (%d) must match number of columns in X (%d)" % (
4076+ len (dim_labels ),
4077+ M ,
4078+ )
4079+
4080+ opt_ranges = opts .get ("ranges" ) or {}
4081+ opt_constraints = opts .get ("constraintranges" ) or {}
4082+ opt_tickvals = opts .get ("tickvals" ) or {}
4083+ opt_ticktext = opts .get ("ticktext" ) or {}
4084+
4085+ dimensions = []
4086+ for i in range (M ):
4087+ col = X [:, i ]
4088+ col_min , col_max = float (np .nanmin (col )), float (np .nanmax (col ))
4089+ pad = (col_max - col_min ) * 0.05 if col_max > col_min else 0.5
4090+ dim = {
4091+ "values" : col .tolist (),
4092+ "label" : str (dim_labels [i ]),
4093+ "range" : opt_ranges .get (i , [col_min - pad , col_max + pad ]),
4094+ }
4095+ if i in opt_constraints :
4096+ dim ["constraintrange" ] = opt_constraints [i ]
4097+ if i in opt_tickvals :
4098+ dim ["tickvals" ] = opt_tickvals [i ]
4099+ if i in opt_ticktext :
4100+ assert (
4101+ i in opt_tickvals
4102+ ), "opts.ticktext[%d] requires matching opts.tickvals[%d]" % (i , i )
4103+ assert len (opt_ticktext [i ]) == len (opt_tickvals [i ]), (
4104+ "opts.ticktext[%d] and opts.tickvals[%d] must have the same length"
4105+ % (i , i )
4106+ )
4107+ dim ["ticktext" ] = opt_ticktext [i ]
4108+ dimensions .append (dim )
4109+
4110+ line_config = {}
4111+ if Y is not None :
4112+ line_config = {
4113+ "color" : Y .tolist (),
4114+ "colorscale" : opts .get ("colormap" , "Electric" ),
4115+ "showscale" : True ,
4116+ "cmin" : float (np .nanmin (Y )),
4117+ "cmax" : float (np .nanmax (Y )),
4118+ }
4119+ if opts .get ("reversescale" ):
4120+ line_config ["reversescale" ] = True
4121+
4122+ title = opts .get ("title" )
4123+ trace = {
4124+ "type" : "parcoords" ,
4125+ "dimensions" : dimensions ,
4126+ "line" : line_config if line_config else None ,
4127+ "labelfont" : {"size" : 13 },
4128+ "tickfont" : {"size" : 11 },
4129+ "rangefont" : {"size" : 11 },
4130+ }
4131+ if title :
4132+ trace ["domain" ] = {"y" : [0 , 0.85 ]}
4133+
4134+ data = [_scrub_dict (trace )]
4135+
4136+ layout = _opts2layout (opts )
4137+ layout .setdefault ("paper_bgcolor" , "white" )
4138+ layout .setdefault ("plot_bgcolor" , "white" )
4139+ if title :
4140+ layout ["title" ] = {
4141+ "text" : str (title ),
4142+ "x" : 0.5 ,
4143+ "xanchor" : "center" ,
4144+ }
4145+
4146+ has_colorbar = bool (line_config )
4147+ if "width" not in opts :
4148+ colorbar_room = 100 if has_colorbar else 0
4149+ opts ["width" ] = max (600 , M * 140 + colorbar_room )
4150+ if "height" not in opts :
4151+ opts ["height" ] = 450
4152+
4153+ return self ._send (
4154+ {
4155+ "data" : data ,
4156+ "win" : win ,
4157+ "eid" : env ,
4158+ "layout" : _scrub_dict (layout ),
4159+ "opts" : opts ,
4160+ }
4161+ )
4162+
40134163 @pytorch_wrap
40144164 def violin (self , X , win = None , env = None , opts = None ):
40154165 """
0 commit comments