工程邮袋
第5集:I. 做。 不是。 喜欢。 蜘蛛!!! (蜘蛛地也可以远足。)
I can't ignore spider plots forever, so here we go...I can't ignore spider plots forever, so here we go...I can't ignore spider plots forever, so here we go...I can't ignore spider plots forever, so here we go...我不能永远忽视蜘蛛图,所以我们开始......
我们系统工程师时不时地会遇到有趣的问题,这些问题在某种程度上超出了典型的范围联合管理计划用法。 这些应用程序通常都很聪明,常常让人们明白使用数据不仅仅是为了解决业务或技术问题。 其他时候,这些问题只是意想不到的、具有挑战性的问题。 这些“曲线球”(我喜欢这样称呼它们)有许多不同的形式:编码问题、有趣的分析、可视化数据的方法……你明白了。
有时,我会收到一些我真的不想回答的问题。 今天的查询就在那一小堆中,我已经避免了这个查询年! 《邮袋》中这篇文章的主题通常被数据可视化社区视为可怕、令人不快的情节,所以我认为它适合万圣节帖子。
由于此类情节在我的日常工作中经常出现在化妆品和消费者分析客户中,因此我不能永远忽视它。 因此,我以此为契机来帮助纽约和新泽西州的一些用户群体。 但我不得不说……我真的不喜欢蜘蛛情节。
问题
就像我说的,我收到这个问题的次数比我愿意提及的还要多。 因此,我将跳过实际的问题电子邮件,您只需相信我已经有好几封了。
还有一些亲自提出的要求。
以及 JMP 社区上的一些帖子。
是的。
以下是来自同行评审论文的示例,有人给我作为他们希望看到的参考:
(J. Inst. Brew. 2012; 118: 325–333) Author's note to any data visualization specialist that may be reading this: Please forgive me. I know this is an awful graph.(J. Inst. Brew. 2012; 118: 325–333) Author's note to any data visualization specialist that may be reading this: Please forgive me. I know this is an awful graph.(J. Inst. Brew. 2012; 118: 325–333) Author's note to any data visualization specialist that may be reading this: Please forgive me. I know this is an awful graph.(J. Inst. Brew. 2012; 118: 325–333) Author's note to any data visualization specialist that may be reading this: Please forgive me. I know this is an awful graph.(J. 研究所。 酿造。 2012年; 118: 325–333) 作者致所有可能正在阅读本文的数据可视化专家的注释:请原谅我。 我知道这是一个糟糕的图表。
响应
从历史上看,我对这些请求的一般回应与大多数数据可视化专家所说的一致,即尽可能阻止人们使用此图。 然而,最近,我开始以不同的方式思考这个持续存在的问题。 从那时起,我对这个问题采取了不同的策略。
我目前关于蜘蛛图的理念是,如果我还能向他们展示更好的方法来呈现相同的信息,我愿意帮助人们了解如何制作蜘蛛图。 就像情况一样我回答的另一个可视化问题对我来说,制作这张图表的过程比实际结果更具启发性。
我恨。 蜘蛛。
好吧,多年来我一直(或者更确切地说,一直)对蜘蛛有强烈的反感。 我不太会把它称为全面的蜘蛛恐惧症,但多年来我都会带着极端的偏见对待任何有八条腿的东西(偶尔是六条腿;你永远不能太确定!)。 我没有怜悯。 我没有给予任何宽恕。 我。 拿。 不,囚犯。 无论如何,我最终还是克服了它。 而且,除了蜱虫(吸血鬼蜘蛛!), 我已经和我的八足朋友和好了。 现在我们通常只是互相保持距离。 我想我对蜘蛛情节的厌恶部分来自于我对蜘蛛的问题,但也可能是因为它们是一种巨大的痛苦,我本能地知道制作一个会有点可怕。 这并不是说构建它们时不存在一些有趣的编码问题,特别是如果您希望它们具有用户期望从 JMP 获得的交互性(只是我知道代码本身会有点令人生畏)。 无论如何,让我们看看这个问题。
确定问题范围
实践我所宣扬的,我从一个问题陈述开始:
我想要制作一个 JMP 插件,用于显示蜘蛛图以及在框架中可视化数据集的其他可能方式,我可以稍后添加到该框架中。
下一步是创建我需要做的不同事情的未完成清单:
- 从用户处获取绘图尺寸。
- 从用户处获取每个数据子集的标签列。
- 为每个子集提供的维度构建交互式蜘蛛图。
- 为每个子集提供的维度构建交互式径向图表。
- 为每个子集提供的维度构建交互式并行图表。
- 为提供的维度构建成对相关性的交互式表。
确定问题范围后发现了一些好消息,具体来说,如果我使用 Application Builder,我可以将很多此类内容卸载到 JMP,这也将使以后在需要时添加新的可视化变得非常容易。 具有挑战性的部分主要是围绕前两张图表并使它们具有交互性。 JMP 的图形在笛卡尔坐标系中工作( X和y坐标)。 出于所有意图和目的,这些图表均位于极坐标系中( r和 φ 坐标)。 因此,我将需要一些代码来在笛卡尔坐标系和极坐标系之间来回转换。 让我们从这里开始吧。
坐标变换函数
这是我用于在两个坐标系之间工作的函数:
polarToRect = Function( {r, t},
Eval List( {r Cosine( t ), r Sine( t )} )
);
这相当简单。 这正是您从几何教科书中找到的内容。 这是一段相当不错的代码,因为它采用半径 (r) 和角度 (t) 并返回坐标对 (x,y)。 顺便说一句,此函数也在我稍后引用的应用程序中,该应用程序包含在 JMP 示例应用程序中。
因为我在 Application Builder 中工作,所以我要做的几乎所有其他事情都将是函数或表达式。 关于这个项目的所有内容也都非常重复,因此如果我只是调用函数,它将使我的代码的迭代部分更易于阅读。
简单的部分
如果你看过决赛应用程序源,您可以看到我正在利用 JMP 的一些现有部分:图形生成器用于平行图,多变量用于成对相关性。 它们几乎完全是使用 JMP 生成的代码或 App 生成器中的功能创建的。 所以,我们首先要让他们离开。
由于图形生成器在这种情况下处理列的方式,以及我必须能够处理任意数量的变量这一事实,因此我更容易将图形生成器脚本构建为表达式,然后将其附加到主函数在正确的位置显示树。 这是代码:
drawParallel = Function( {pCol},
{default Local},
// build the list of variables with the format "X( :col1), X( :col2)" as a string
// also building a list of plot elements
colStr = "";
gElemStr = "";
For( i = 1, i <= N Items( pCol ), i++,
If( i == 1,
colStr = colStr || "X(" || Char( pCol[i] ) || ")";
gElemStr = gElemStr || "X(" || Char( i ) || ")";
,
colStr = colStr || ", X(" || Char( pCol[i] ) || ", Position( 1))";
gElemStr = gElemStr || ", X(" || Char( i ) || ")";
)
);
// Get the grouping variable as a string
xVarStr = Char( gCol );
// Insert the strings into the main string
Eval(
Parse(
Eval Insert(
"parallelVLB << Append(Graph Builder(
Size( 766, 256 ),
Show Control Panel( 0 ),
Show Legend( 0 ),
Variables( ^colStr^, Color( ^xVarStr^ ) ),
Elements( Parallel( ^gElemStr^, Legend( 3 ) ) ),
SendToReport(
Dispatch(
{},
\!"Graph Builder\!",
OutlineBox,
{Set Title( \!"Parallel Plot\!" ), Image Export Display( Normal )}
),
Dispatch( {}, \!"X title\!", TextEditBox, {Set Text( \!"Variables\!" )} ),
Dispatch( {}, \!"graph title\!", TextEditBox, {Set Text( \!"\!" )} )
)
))"
)
)
);
);
请注意,我所做的只是将变量格式化为字符串并将它们插入代码中的正确位置,然后运行它。 最终结果是我的应用程序中的完全交互式并行图,我只需要很少的编码。 我尝试手动编写平行图;它有效,但只是勉强有效。 我不推荐它。 只需使用图形生成器和附加() 。 我也在使用评估插入()构造。它比使用连接构造字符串更容易阅读。
多元图只是 JMP 中多元平台的简单参数化。 我什至不需要编写代码就可以得到这一点! 有一个高级母版 JMP 网络研讨会这展示了如何只需点击鼠标即可做到这一点。 再说一次,我这样做只是为了方便,因为我真的不想在应用程序的这一部分上花费大量时间。这不是懒惰——而是成为一名高效的编码员。 JMP 为您提供了代码,因此您不妨节省一些时间。
径向图
径向图实际上是 JMP 示例目录中包含的示例应用程序的端口(经过一些修改)。 最大的修改是我正在使用标记段()代替标记() 。 而且,这实际上是重要的一点。 有可能使标记()使用一些鼠标捕获命令和一些逻辑进行交互,但是标记段()处理这一切自动地。 你只需要告诉它你需要它监控哪个数据表。
drawFlies = Function( {nP, pCol, obj},
{default Local},
// Create a list of angles for the spider plot
th = ((1 :: nP) - 1);
th = Shape( th, nP, 1 );
th = 2 * Pi() * th / nP;
//Get the data from the table as a matrix.
sel_dat = J( N Row( DataTable1 ), N Items( pCol ), 0 );
For( i = 1, i <= N Items( pCol ), i++,
sel_dat[0, i] = pCol[i] << get as matrix
);
// now get a matrix with two columns and nvars rows to keep track of the min and max of each variable
min_max = J( nP, 2, 0 ); // column1: minimum, column2: maximum. row for each variable chosen
For( i = 1, i <= nP, i++,
min_max[i, 1] = Min( sel_dat[0, i] );
min_max[i, 2] = Max( sel_dat[0, i] );
);
// Create some empty matrices for the converted values
xMat = [];
yMat = [];
// Convert the data matrix
For( i = 1, i <= nR, i++,
current = sel_dat[i, 0];
// for each variable, subtract the minimum and then divide by the range.
adj = J( N Row( current ), 1, 0.0 ) + 1.0 * ((current - min_max[0, 1]`) (min_max[0, 2] - min_max[0, 1])`);
adj = adj`;
total = Sum( adj );
{x, y} = polarToRect( adj, th );
x = Sum( x ) / total;
y = Sum( y ) / total;
xMat = xMat |/ x;
yMat = yMat |/ y;
);
// Draw markers
annotateRad = Expr(
// Matrices and book-keeping variables
xCoord = xxx;
yCoord = yyy;
obj[FrameBox( 1 )] << Append Seg( Marker Seg( xxx, yyy, Row States( DataTable1 ) ) );
);
// Substitute values into the annotation expression
Substitute Into( annotateRad, Expr( xxx ), xMat, Expr( yyy ), yMat );
annotateRad;
);
正如您所看到的,很多代码都在处理径向变换等。直到函数快要结束时,这个讨论的有趣部分才会发生。 (提示:寻找绘制标记评论。)我正在附加一个标记段()到图形元素堆栈。 如果右键单击完成的报告中的径向图并选择定制,你可以在那里看到它:

原因是标记段()之所以如此强大,是因为它有行状态()参考其中。 请记住,JMP 中的所有内容都通过行状态通过数据表链接。 因此,通过告诉 JMP 报表需要与哪个数据表进行通信,我可以同步报表中的所有图表,并获得我习惯于使用 JMP 的所有交互性。 这只是从标记到标记段(当我开始工作时,这让我大吃一惊)。
蜘蛛情节
好吧,除了那个该死的蜘蛛情节之外,我们没有什么可谈的了。 那么,让我们开始吧。
由于蜘蛛图基本上是围绕中心轴包裹的平行图,因此我们或多或少必须制作一个平行图 - 将所有点投影到极坐标系上。 而且,如果这听起来很痛苦,那么你是对的。 这是。 也确实是这样,真的你的大脑很难处理径向信息。 因此,这一变化——从笛卡尔坐标变为极坐标——使得蜘蛛图变得更难阅读。 无论如何,我将在下次再讨论该图的优点。 上代码吧!
好消息是,由于我实际上只处理每个变量的一个维度,因此我可以完全控制有角的坐标系的一部分。 所以,那里有一些转型工作,但还没有那么糟糕。 当您需要考虑用户可能希望显示比数据实际覆盖的更宽的比例时,它会变得混乱,例如,值在 3 到 5 之间,但可能的比例是 1 到 7。 我能够通过查找轴列属性然后从那里获取最大值和最小值来处理这个问题。 由于这不是叙述中的关键点,因此您可以在我包含在加载项中的源代码中看到这是如何完成的。
径向辐条和参考线只是使用线()命令。 整个事情中最困难的部分是使每一行的网络具有交互性并使图例发挥作用。 我通过使用一个线段()和一个标记段() 。 这个图例是对我最近遇到的一个非常旧的 JMP 片段的重新利用,称为“行图例”。 让我们逐一看看。
以下是在蜘蛛图上创建线条的代码。 与径向图一样,需要进行大量数据操作才能将其转换为对图表有意义的格式:
drawTrails = Function( {nP, pCol, obj},
{default Local},
// Create a list of angles for the spider plot
th = ((1 :: nP) - 1);
th = Shape( th, nP, 1 );
th = 2 * Pi() * th / nP;
//Get the data from the table as a matrix.
sel_dat = J( N Row( DataTable1 ), N Items( pCol ), 0 );
For( i = 1, i <= N Items( pCol ), i++,
sel_dat[0, i] = pCol[i] << get as matrix
);
// now get a matrix with two columns and nvars rows to keep track of the min and max of each variable
min_max = J( nP, 2, 0 ); // column1: minimum, column2: maximum. row for each variable chosen
// Check if the user wants to use axis column property values for max and min.
If( axisProp == 0,
// Directly calculate the values from the data table.
For( i = 1, i <= nP, i++,
min_max[i, 1] = Min( sel_dat[0, i] );
min_max[i, 2] = Max( sel_dat[0, i] );
)
,
// If the user wants to use axis column property values
For( i = 1, i <= nP, i++,
// Pull the column name and check if the axis property is defined
col = pCol[i];
axisFlag = Contains( col << Get Properties List, Expr( Axis ) );
//If the axis property is present pull the values from the property
If( axisFlag == 1,
axisPresent = col << Get Property( "Axis" );
min_max[i, 1] = Eval( Extract Expr( axisPresent, Min( Wild() ) ) );
min_max[i, 2] = Eval( Extract Expr( axisPresent, Max( Wild() ) ) );
,
// if the property is not present, calculate the values directly from the data
min_max[i, 1] = Min( sel_dat[0, i] );
min_max[i, 2] = Max( sel_dat[0, i] );
);
)
);
// Create some empty matices for the converted values
xMat = [];
yMat = [];
// Convert the data matrix
For( i = 1, i <= nR, i++,
current = sel_dat[i, 0];
// for each variable, subtract the minimum and then divide by the range.
adj = J( N Row( current ), 1, 0.0 ) + 1.0 * ((current - min_max[0, 1]`) (min_max[0, 2] - min_max[0, 1])`);
adj = adj`;
// an error check form missing values in the vector
For( k = 1, k <= N Rows( adj ), k++,
If( Is Missing( adj[k] ),
adj[k] = 0
)
);
{x, y} = polarToRect( adj, th );
xMat = xMat |/ x;
yMat = yMat |/ y;
);
// Draw Lines and Markers for spider plot
annotateSpider = Expr(
// coordinate matrices
xMat = xxx;
yMat = yyy;
nParam = nnn;
nPoints = N Rows( xMat );
nGroups = nPoints / nParam;
// Reshape the matrices to make them easier to work with
xSMat = Shape( xMat, nGroups );
ySMat = Shape( yMat, nGroups );
// Create an empty string to hold a list (as a string)
pathList = "";
// Draw the lines as polygons using the reshaped matrix
For( i = 1, i <= nGroups, i++,
// Get the coordinates for a given path and add the first value to the end to close the path
xCoord = xSMat[i, 0];
xCoord = xCoord || xCoord[1];
yCoord = ySMat[i, 0];
yCoord = yCoord || yCoord[1];
//
obj[FrameBox( 1 )] << Append Seg( Line Seg( xCoord, yCoord, Row States( DataTable1, {i} ) ) );
);
// Draw the markers
For( i = 1, i <= nP, i++,
// Get the coordinates for a given path
xCoord = xSMat[0, i];
yCoord = ySMat[0, i];
// Run Markers Seg (need to move and reformat the matrix)
obj[FrameBox( 1 )] << Append Seg( Marker Seg( xCoord, yCoord, Row States( DataTable1 ) ) );
);
);
// Substitute values into the annotation expression
Substitute Into( annotateSpider, Expr( xxx ), xMat, Expr( yyy ), yMat, Expr( nnn ), nP );
// Run the annotation expression
annotateSpider;
);
重要的部分是接近末尾的线段和标记段。 我必须单独绘制每条连接线。 有一个路径段()这本来会让这变得非常容易,但如果你尝试给它着色,它会创建一个填充的多边形。 所以,就是线条和标记! 通过使用图形段,可以很容易地通过“行状态”运算符挂回数据表。 现在,由于所有图形都查看主数据表中的行状态,因此可以通过为数据表中的每个行状态分配颜色来同时对所有图形进行着色。
当您右键单击 JMP 中的某些图形时,将会出现“行图例”选项。 它在 JMP 中已经存在了一段时间并且做了两件事。 首先,它按列为数据表中的行着色(就像数据表中的红色三角形菜单选项一样)。 其次,它在可视化旁边创建了一个小图例。 这就是我所要做的就是使四个图表中的颜色彼此同步。 这是代码:
drawLegend = Function( {gVar, obj},
{default Local},
// extract the column name with the grouping variable
colName = gVar[1];
// pass the variables into the expression
annotateLegend = Expr(
obj[FrameBox( 1 )] << Row Legend( ggg, Color( 1 ), Continuous Scale( 0 ) )
);
// substitute into the expression
Substitute Into( annotateLegend, Expr( ggg ), colName );
// run the expression
annotateLegend;
);
由于我的所有图表都在查看相同的数据表中的行状态,因此它们都会自动继承行图例指定的颜色! 超级光滑。
把事情包起来
就是这样。 对于脚本编写者来说,我希望您了解的主要内容是图形段的强大功能。 JMP 如此特别的许多功能都包含在这些函数的功能中。 对于其他人来说,现在有蜘蛛图插件。 我对结果感到非常自豪。 但是,请不要使用它。 有很多更好的方法来可视化此类数据。 
作者注
在撰写此博客时,没有蜘蛛受到伤害,尽管有些人可能会对与昆虫(六足)或蜱虫(也是蜘蛛,但它们是吸血蜘蛛)归为一类感到不满,所以我想即使蜘蛛讨厌它们)。