エンジニアリングメールバッグ
エピソード5:私。 行う。 いいえ。 お気に入り。 スパイダー!!! (そして、スパイダープロットもハイキングをすることができます。)
スパイダープロットを永遠に無視することはできないので、ここに行きます...
時々、私たちのシステムエンジニアは、典型的な範囲からいくらか外れる興味深い質問に遭遇します。 JMP使用法。 これらのアプリケーションは一般的に巧妙であり、データの使用がビジネスや技術的な問題だけではないことをしばしば思い起こさせます。 また、質問が予期せぬ、やりがいのある問題である場合もあります。 これらの「カーブボール」(私はそれらを呼ぶのが好きです)には、コーディングの問題、興味深い分析、データの視覚化の方法など、さまざまな形があります。
たまに、本当に答えたくない問題について質問を受けることがあります。 今日の質問はその小さな山の中にあり、私はこれを避けました年! メールバッグのこのエントリの主題は、データvizコミュニティによって一般的に恐ろしくて不快なプロットと見なされているので、ハロウィーンの投稿に適していると思います。
このタイプのプロットは、私の日常業務で化粧品や消費者分析の顧客に定期的に表示されるため、永遠に無視することはできません。 それで、私はこれをニューヨークとニュージャージーの多くのユーザーグループを助ける機会としてとらえています。 しかし、私は言わなければなりません…私は本当にスパイダープロットが好きではありません。
質問
私が言ったように、私は私が言及するよりも頻繁にこの質問を受け取りました。 だから、私は実際の質問の電子メールをスキップするつもりです、あなたはただいくつかあったことを私に信じなければならないでしょう。
直接のリクエストもありました。
そして、JMPコミュニティへのいくつかの投稿。
ええ。
以下は、誰かが見たいものの参照として私にくれた査読論文の例です。
(J. 研究所 醸造。 2012; 118:325–333)これを読んでいる可能性のあるデータ視覚化スペシャリストへの著者のメモ:ご容赦ください。 これはひどいグラフだと思います。
応答
これらの要求に対する私の一般的な応答は、歴史的に、ほとんどのデータ視覚化の専門家が言ったことに沿っています。これは、可能な限りこのプロットを使用することを人々に思いとどまらせることです。 しかし、最近、私はこの永続的な質問について少し違った考え方をし始めました。 それ以来、私はこの問題に別の取り組みをしました。
スパイダープロットについての私の現在の哲学は、途中で同じ情報を提示するためのより良い方法を人々に示すことができれば、人々がそれを作成する方法を理解するのを助けることにオープンであるということです。 の場合のように私が答えた他の視覚化の質問、私にとって、このチャートを作成するプロセスは、実際の結果よりもはるかに有益でした。
I.嫌い。 スパイダー。
OK、私は何年もの間、クモに対して強い反感を持っていました(というより、持っていました)。 私はそれを本格的なクモ恐怖症の症例とは言いませんが、何年もの間、私は極端な偏見を持って8本の足(時には6本の足;あなたは決して確信が持てません!)で何かを派遣しました。 私は容赦しませんでした。 私は四分の一を与えませんでした。 私。 取った。 いいえ。囚人。 とにかく、私は最終的にそれを乗り越えました。 そして、ダニ(吸血鬼のクモ!)を除いて、 私は8本足の友達と和解しました。 私たちは通常、お互いの邪魔にならないようにしています。 蜘蛛の陰謀に対する嫌悪感の一部は蜘蛛の問題から来たと思いたいのですが、それは王室の苦痛である可能性もあり、それを作るのは少し怖いことを本能的に知っていました。 特に、ユーザーがJMPに期待する双方向性を持たせたい場合は、それらの構築に関連する興味深いコーディングの問題がないというわけではありません(コード自体が少し威圧的になることを私は知っていました)。 とにかく、問題を見てみましょう。
問題の範囲
私が説教することを実践する、私は問題の説明から始めました:
後で追加できるフレームワークでデータセットを視覚化するスパイダープロットやその他の可能な方法を表示するJMPアドインを作成したいと思います。
次のステップは、私がする必要があるさまざまなことのパンチリストを作成することです。
- ユーザーからグラフの寸法を取得します。
- 各データサブセットのラベル列をユーザーから取得します。
- 各サブセットによって提供されたディメンションのインタラクティブなレーダーチャートを作成します。
- 各サブセットによって提供されたディメンションのインタラクティブなレーダーチャートを作成します。
- 各サブセットによって提供されたディメンションのインタラクティブな並列チャートを作成します。
- 提供されたディメンションのペアワイズ相関のインタラクティブテーブルを作成します。
問題をスコープすると、いくつかの良いニュースが明らかになりました。具体的には、Application Builderを使用すると、このようなものの多くをJMPにオフロードできます。これにより、必要に応じて後で新しい視覚化を簡単に追加することもできます。 やりがいのある部分は、ほとんどが最初の2つのグラフの周りにあり、それらをインタラクティブにしました。 JMPのグラフは、デカルト座標系で機能します(バツそしてy座標)。 これらのグラフは、すべての意図と目的のために、極座標系にあります( rおよびφ座標)。 その結果、デカルト座標系と極座標系の間で相互に変換するためのコードが必要になります。 そこから始めましょう。
座標変換関数
2つの座標系間で作業するために使用した関数は次のとおりです。
polarToRect = Function( {r, t},
Eval List( {r Cosine( t ), r Sine( t )} )
);
とても簡単です。 幾何学の教科書から見つけたものです。 これは、半径(r)と角度(t)を取り、座標ペア(x、y)を返すという点で、非常に優れたコードです。 ところで、この関数は、後で参照するJMPサンプルアプリケーションに含まれているアプリケーションにも含まれています。
私はApplicationBuilderで作業しているので、これから行うことのほとんどすべてが関数または式のいずれかになります。 このプロジェクトのすべても見事に反復的であるため、関数を呼び出すだけの場合は、コードの反復部分が読みやすくなります。
簡単なビット
決勝戦をご覧になった方アプリソース、JMPの既存の部分を活用していることがわかります。並列プロットにはグラフビルダー、ペアワイズ相関には多変量です。 これらは、ほとんど排他的に、アプリケーションビルダーでJMPによって生成されたコードまたは機能を使用して作成されました。 したがって、最初にそれらを邪魔にならないようにします。
この場合、Graph Builderが列を処理する方法と、任意の数の変数を処理できる必要があるため、Graph Builderスクリプトを式として作成し、それをメインに追加する方が簡単です。ツリーを正しい場所に表示します。 そのためのコードは次のとおりです。
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( \!"\!" )} )
)
))"
)
)
);
);
私が行っているのは、変数を文字列としてフォーマットし、コード内の適切な場所にプラグインして実行することだけであることに注意してください。 最終結果は、私のアプリで完全にインタラクティブな並列プロットになり、コーディングはほとんどありません。 並列プロットを手動でスクリプト化しようとしました。それはうまくいきましたが、かろうじて。 私はそれをお勧めしません。 グラフビルダーを使用してAppend() 。 私も使用していますEval Insert()構築します。連結を使用して文字列を作成するよりも、少し読みやすくなります。
多変量プロットは、JMPの多変量プラットフォームの単純なパラメーター化です。 このビットを取得するためにコードを書く必要さえありませんでした! ありますAdvanced MasteringJMPウェビナーこれは、マウスをクリックするだけでそれを行う方法を示しています。 繰り返しになりますが、私は便宜上、これらのビットをこのように実行しています。これは、アプリのこの部分に膨大な時間を費やしたくなかったためです。それは怠惰ではありません–それは効率的なコーダーです。 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とのすべての対話性を得ることができます。 そして、それはマーカーからマーカーセグメントに移動するだけです(それが機能するようになったとき、それは私の心を驚かせました)。
スパイダープロット
OK、ダングスパイダープロット以外に私が話すことができるものは何もありません。 それでは、始めましょう。
スパイダープロットは基本的に中心軸に巻き付けられた平行プロットであるため、多かれ少なかれ、すべての点が極座標系に投影された平行プロットを作成する必要があります。 そして、それが苦痛に聞こえるなら、あなたは正しいです。 です。 それも本当に、本当にあなたの脳が放射状の情報を処理するのは難しいです。 したがって、デカルト座標から極座標への1つの変更により、レーダーチャートが非常に読みにくくなります。 とにかく、このグラフのメリットについての談話をもう一度保存します。 コードに移りましょう!
幸いなことに、私は実際には変数ごとに1つのディメンションしか扱っていないため、角度座標系の一部。 ですから、そこにはいくつかの変革作業がありますが、それほど悪くはありません。 ユーザーがデータが実際にカバーするよりも広いスケールを表示したい場合があることを考慮する必要がある場合、それは厄介になります。たとえば、値は3〜5ですが、可能なスケールは1〜7です。 軸の列のプロパティを探し、そこから最大値と最小値を取得することで、これを処理することができました。 それは物語の重要なポイントではないので、私がアドインに含めたソースコードでそれがどのように行われたかを見ることができます。
ラジアルスポークと参照線は、ライン()コマンド。 この全体の中で最も難しい部分は、各行のWebをインタラクティブにし、凡例を機能させることでした。 を採用してラインをインタラクティブにしましたLine Seg()とマーカーセグメント() 。 伝説は、私が最近遭遇した、RowLegendと呼ばれる本当に古い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にあり、2つのことをします。 まず、データテーブルの行を列ごとに色分けします(データテーブルの赤い三角形メニューオプションのように)。 次に、視覚化の横に小さな凡例を作成します。 4つのグラフ間で色を互いに同期させるために私がしなければならなかったのはこれだけです。 コードは次のとおりです。
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を特別なものにしている多くのビットは、これらの関数が実行できることでまとめられています。 他のみんなのために、今ありますスパイダープロットアドイン。 私はそれがどのようになったかをかなり誇りに思っています。 ただし、使用しないでください。 そのタイプのデータを視覚化するためのはるかに優れた方法があります。;)
著者のメモ
このブログの執筆では、クモに害はありませんでしたが、昆虫(6本足)やマダニ(クモ類でもありますが、吸血鬼のクモです)と一緒になって恥ずかしい思いをした人もいます。クモはそれらを嫌います)。