工程邮袋
第5集:I. 做。 不。 喜欢。 蜘蛛!!! (蜘蛛区也可以远足。)
我不能永远忽略蜘蛛图,所以我们开始...
我们的系统工程师不时地遇到一些有趣的问题,这些问题可能会超出JMP用法。 这些应用程序通常都很聪明,经常使人们了解如何使用数据不仅用于业务或技术问题。 在其他时候,这些问题只是意外的,具有挑战性的问题。 这些“曲线球”(我喜欢称呼它们)有许多不同的形式:编码问题,有趣的分析,可视化数据的方式……您就明白了。
有时,我会收到一些我真的不想回答的问题。 今天的查询很少,我已经避免了年份! 数据可视化社区通常将“邮件袋”中该条目的主题视为恐怖,令人不快的情节,因此我认为它适合万圣节帖子。
由于在我的日常工作中会定期为化妆品和消费者分析客户显示这种情节,所以我不能永远忽略它。 因此,我以此为契机,为纽约和新泽西州的许多用户群体提供了帮助。 但是我不得不说……我真的不喜欢蜘蛛剧情。
问题
就像我说的那样,我收到这个问题的频率比想提的要高。 因此,我将跳过实际的问题电子邮件,您只需要相信我有几个问题即可。
也有一些亲自要求。
以及JMP社区上的一些帖子。
是的
以下是一个经过同行评审的论文示例,该论文有人给我作为他们希望看到的参考:
(J. 研究所 酿造。 2012; 118:325–333)给任何可能正在阅读此书的数据可视化专家的注解:请原谅。 我知道这是一个可怕的图。
响应
从历史上看,我对这些要求的一般回应都遵循大多数数据可视化专家所说的那样,这是在所有可能的情况下,劝阻人们不要使用此图。 但是,最近,我开始对这个持续存在的问题有所不同。 从那以后,我对这个问题采取了不同的策略。
我目前关于蜘蛛图的理念是,如果我也能向人们展示更好的方式来展示相同的信息,我愿意帮助人们了解如何制作一个。 就像情况一样我回答的另一个可视化问题,对我而言,制作此图表的过程比实际结果更具启发性。
我恨。 蜘蛛。
好吧,多年来我对蜘蛛有强烈的反感。 我不会完全称呼它为蜘蛛恐惧症,但多年来,我会极度偏见地派遣八只脚(有时只有六只脚;您永远不能确定!)的东西。 我没有怜悯。 我没有给四分之一。 一世。 拿。 不,囚犯。 无论如何,我最终克服了它。 而且,除了of虫(吸血鬼蜘蛛!)之外, 我与八足的朋友和睦相处。 现在,我们通常只是保持彼此的距离。 我想认为,我对蜘蛛图的厌恶部分来自于我对蜘蛛的问题,但也可能是它们是一种皇家痛苦,我本能地知道制造一个蜘蛛会有些吓人。 这并不是说在构建它们时不会涉及一些有趣的编码问题,特别是如果您希望它们具有用户希望从JMP获得的交互性(只是我知道代码本身会有些吓人)。 无论如何,让我们看一下问题。
解决问题
练习我的讲道,我从一个问题陈述开始:
我想制作一个JMP加载项,它显示蜘蛛图和可视化框架中数据集的其他可能方式,以后可以添加到该框架中。
下一步是创建我需要做的不同事情的清单:
- 从用户处获取用于绘制图形的尺寸。
- 从用户获取每个数据子集的标签列。
- 为每个子集提供的尺寸构造一个交互式蜘蛛图。
- 为每个子集提供的尺寸构造一个交互式径向图。
- 为每个子集提供的尺寸构造一个交互式平行图。
- 为提供的维度构造成对相关的交互式表格。
范围内的问题揭示了一些好消息,特别是,如果我使用Application Builder,则可以将很多这些东西卸载到JMP,这也将使以后在需要时添加新的可视化变得非常容易。 最具挑战性的部分大多围绕前两个图表,并使它们具有交互性。 JMP的图形在笛卡尔坐标系中工作( X和ÿ坐标)。 出于所有意图和目的,这些图均处于极坐标系中( [R和φ坐标)。 结果,我将需要一些代码在笛卡尔坐标系和极坐标系之间来回转换。 让我们从这里开始。
坐标变换功能
这是我用于两个坐标系之间的函数:
polarToRect = Function( {r, t},
Eval List( {r Cosine( t ), r Sine( t )} )
);
这很简单。 正是您从几何教科书中找到的内容。 这是一段非常不错的代码,它需要半径(r)和角度(t)并返回一个坐标对(x,y)。 顺便说一句,此功能也在JMP示例应用程序中包含的我稍后引用的应用程序中。
因为我在Application Builder中工作,所以我要做的几乎所有其他事情都是函数或表达式。 该项目的所有内容都具有重复性,因此,如果我只是调用函数,它将使代码的迭代部分更易于阅读。
易位
如果你看看决赛应用程式来源,您会看到我正在利用JMP的一些现有部分:使用Graph Builder进行并行绘制,使用Multivariate进行成对相关。 它们几乎都是使用JMP生成的代码或应用程序构建器中的功能创建的。 因此,我们将首先消除它们。
由于Graph Builder在这种情况下对待列的方式,以及我必须能够处理任意数量的变量的事实,对于我来说,将Graph Builder脚本构建为表达式然后将其附加到main更加容易在正确的位置显示树。 这是代码:
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( \!"\!" )} )
)
))"
)
)
);
);
注意,我要做的只是将变量格式化为字符串,然后将其插入代码中的正确位置,然后运行它。 最终结果是在我的应用程序中进行了完全交互式的并行绘图,而我编写的代码很少。 我试图手动编写一个平行图。它起作用了,但是几乎没有。 我不推荐。 只需使用Graph Builder,附加() 。 我也在用评估插入()构造。与使用串联构造字符串相比,它更容易阅读。
多元图只是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。 通过查找轴列属性,然后从那里获取最大值和最小值,我能够解决这个问题。 由于这不是叙述中的关键点,因此您可以在外接程序附带的源代码中看到如何做到这一点。
径向辐条和参考线只是使用线()命令。 整个过程中最困难的部分是使每一行的网络都具有交互性,并使图例起作用。 我通过使用行Seg()和一个标记段() 。 这个传说是对我遇到的一个非常古老的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;
);
由于我所有的图形都在同一个数据表中查找行状态,因此它们都将自动继承行图例分配的颜色! 超级光滑。
整理东西
就是这样。 对于脚本编写者,我希望您能从中受益的主要是Graphics Segments的功能。 这些功能可以做什么,使JMP如此特别的许多方面都包含在内。 对于其他所有人,现在有蜘蛛图加载项。 我为结果感到骄傲。 但是,请不要使用它。 有更好的可视化该类型数据的方法。;)
作者注
在本博客的写作中,没有蜘蛛受到伤害,尽管有些人可能不愿与昆虫(六足)或tick虫(也包括蜘蛛)混在一起,但它们是吸血鬼蜘蛛,所以我想蜘蛛讨厌他们)。