工程郵袋
第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 如此特別的許多功能都包含在這些函數的功能中。 對其他人來說,現在有蜘蛛圖插件。 我對結果感到非常自豪。 但是,請不要使用它。 有很多更好的方法來視覺化此類數據。 
作者註
在撰寫此部落格時,沒有蜘蛛受到傷害,儘管有些人可能會對與昆蟲(六足)或蜱蟲(也是蜘蛛,但它們是吸血蜘蛛)歸為一類感到不滿,所以我想即使是蜘蛛討厭它們)。