Der technische Postsack
Folge 5: Ich. TUN. NICHT. WIE. SPINNEN!!! (Und Spinnenparzellen können auch eine Wanderung vertragen.)
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...Ich kann Spider-Plots nicht ewig ignorieren, also los geht's ...
Hin und wieder stoßen wir Systemingenieure auf interessante Fragen, die etwas außerhalb des typischen Spektrums liegen würden JMP Verwendung. Diese Anwendungen sind im Allgemeinen clever und machen oft deutlich, dass die Nutzung von Daten nicht nur geschäftlichen oder technischen Problemen dient. In anderen Fällen handelt es sich bei den Fragen lediglich um unerwartete, herausfordernde Probleme. Diese „Kurvenbälle“ (wie ich sie gerne nenne) gibt es in vielen verschiedenen Formen: Codierungsprobleme, interessante Analysen, Möglichkeiten zur Datenvisualisierung … Sie verstehen, worauf es ankommt.
Gelegentlich bekomme ich Fragen zu Problemen, die ich wirklich nicht beantworten möchte. Die heutige Anfrage liegt in diesem kleinen Stapel, und ich habe diese hier gemieden Jahre ! Das Thema dieses Eintrags in „The Mail Bag“ wird von der Datenvisualisierungs-Community allgemein als eine gruselige, unangenehme Handlung angesehen, daher denke ich, dass es für einen Halloween-Beitrag angemessen ist.
Da solche Plots bei Kunden aus den Bereichen Kosmetik und Consumer Analytics in meiner täglichen Arbeit regelmäßig auftauchen, kann ich sie nicht ewig ignorieren. Deshalb nehme ich dies zum Anlass, einer Reihe von Benutzergruppen in New York und New Jersey zu helfen. Aber ich muss sagen ... ich mag Spinnenplots wirklich nicht.
Die Frage
Wie gesagt, ich habe diese Frage öfter erhalten, als ich erwähnen möchte. Deshalb überspringe ich die eigentliche Frage-E-Mail. Sie müssen mir einfach vertrauen, dass es mehrere davon gab.
Es gab auch einige persönliche Anfragen.
Und ein paar Beiträge in der JMP-Community.
Ja.
Unten finden Sie ein Beispiel aus einem von Experten begutachteten Artikel, den mir jemand als Referenz für das gegeben hat, was er gerne sehen würde:
(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. Inst. Brauen. 2012; 118: 325–333) Anmerkung des Autors an alle Datenvisualisierungsspezialisten, die dies lesen: Bitte verzeihen Sie mir. Ich weiß, das ist eine schreckliche Grafik.
Die Antwort
Meine allgemeine Antwort auf diese Anfragen entspricht in der Vergangenheit dem, was die meisten Datenvisualisierungsexperten gesagt haben, nämlich Menschen davon abzuhalten, diese Darstellung zu verwenden, wenn dies überhaupt möglich ist. Allerdings begann ich vor kurzem, etwas anders über diese hartnäckige Frage nachzudenken. Seitdem habe ich das Problem anders angegangen.
Meine aktuelle Philosophie in Bezug auf Spider-Plots ist, dass ich offen dafür bin, Menschen zu zeigen, wie man einen solchen erstellt, wenn ich ihnen dabei auch bessere Möglichkeiten zeigen kann, die gleichen Informationen zu präsentieren. Wie es bei der Fall war die andere Visualisierungsfrage, die ich beantwortet habe Für mich war der Prozess der Erstellung dieses Diagramms viel lehrreicher als das tatsächliche Ergebnis.
ICH HASSE. SPINNEN.
OK, ich habe (oder besser gesagt, hatte) seit vielen Jahren eine starke Abneigung gegen Spinnen. Ich würde es nicht wirklich als einen ausgewachsenen Fall von Spinnenphobie bezeichnen, aber viele Jahre lang habe ich alles mit acht Beinen (und gelegentlich auch sechs, da kann man sich nie ganz sicher sein!) mit extremen Vorurteilen erledigt. Ich habe keine Gnade gegeben. Ich habe kein Viertel gegeben. ICH. Nahm. Nein. Gefangene. Wie auch immer, ich habe es irgendwann überstanden. Und mit Ausnahme von Zecken (Vampirspinnen!), Ich habe Frieden mit meinen achtbeinigen Freunden geschlossen. Im Allgemeinen gehen wir uns jetzt einfach aus dem Weg. Ich würde gerne glauben, dass ein Teil meiner Abneigung gegen Spinnenplots von meinem Problem mit Spinnen herrührt, aber es könnte auch sein, dass sie eine echte Plage sind und ich instinktiv wusste, dass es ein bisschen beängstigend sein würde, einen solchen zu machen. Das heißt nicht, dass es bei ihrer Erstellung keine interessanten Codierungsprobleme gibt, insbesondere wenn man möchte, dass sie die Interaktivität bieten, die Benutzer von JMP erwarten (ich wusste nur, dass der Code selbst etwas einschüchternd sein würde). Wie auch immer, schauen wir uns das Problem an.
Das Problem eingrenzen
Ich übe, was ich predige Ich begann mit einer Problemstellung:
Ich möchte ein JMP-Add-In erstellen, das einen Spider-Plot und andere Möglichkeiten zur Visualisierung eines Datensatzes in einem Framework anzeigt, das ich später hinzufügen kann.
Der nächste Schritt besteht darin, eine Liste mit den verschiedenen Dingen zu erstellen, die ich tun muss:
- Holen Sie sich die Dimensionen für die grafische Darstellung vom Benutzer.
- Erhalten Sie vom Benutzer eine Beschriftungsspalte für jede Datenteilmenge.
- Erstellen Sie ein interaktives Spinnendiagramm für die bereitgestellten Dimensionen jeder Teilmenge.
- Erstellen Sie ein interaktives Radialdiagramm für die bereitgestellten Dimensionen für jede Teilmenge.
- Erstellen Sie ein interaktives Paralleldiagramm für die bereitgestellten Dimensionen für jede Teilmenge.
- Erstellen Sie eine interaktive Tabelle paarweiser Korrelationen für die bereitgestellten Dimensionen.
Die Eingrenzung des Problems ergab einige gute Neuigkeiten, insbesondere, dass ich viele dieser Dinge auf JMP auslagern kann, wenn ich Application Builder verwende, was es auch sehr einfach macht, später bei Bedarf neue Visualisierungen hinzuzufügen. Die herausfordernden Teile betrafen hauptsächlich die ersten beiden Diagramme und deren Interaktivität. Die Graphen von JMP funktionieren in kartesischen Koordinatensystemen ( X Und j Koordinaten). Diese Diagramme befinden sich praktisch in einem Polarkoordinatensystem ( R und φ-Koordinaten). Daher benötige ich Code, um zwischen kartesischen und polaren Koordinatensystemen hin und her zu konvertieren. Fangen wir dort an.
Koordinatentransformationsfunktionen
Hier ist die Funktion, die ich zum Arbeiten zwischen den beiden Koordinatensystemen verwendet habe:
polarToRect = Function( {r, t},
Eval List( {r Cosine( t ), r Sine( t )} )
);
Es ist ziemlich einfach. Genau das, was Sie in einem Geometrielehrbuch finden würden. Es ist ein ziemlich netter kleiner Code, da er den Radius (r) und den Winkel (t) annimmt und ein Koordinatenpaar (x,y) zurückgibt. Übrigens ist diese Funktion auch in einer Anwendung enthalten, auf die ich später verweise und die in den JMP-Beispielanwendungen enthalten ist.
Da ich im Application Builder arbeite, wird fast alles andere, was ich tun werde, entweder eine Funktion oder ein Ausdruck sein. Alles an diesem Projekt ist außerdem spektakulär repetitiv, sodass die iterativen Teile meines Codes leichter lesbar sind, wenn ich nur Funktionen aufrufe.
Die einfachen Teile
Wenn Sie sich das Finale ansehen App-Quelle , können Sie sehen, dass ich einige vorhandene Teile von JMP nutze: Graph Builder für eine parallele Darstellung und Multivariate für die paarweisen Korrelationen. Sie wurden fast ausschließlich mit JMP-generiertem Code oder Funktionen im Application Builder erstellt. Also werden wir sie zuerst aus dem Weg räumen.
Aufgrund der Art und Weise, wie Graph Builder in diesem Fall Spalten behandelt, und der Tatsache, dass ich in der Lage sein muss, eine beliebige Anzahl von Variablen zu verarbeiten, ist es für mich einfacher, das Graph Builder-Skript als Ausdruck zu erstellen und ihn dann an das Hauptskript anzuhängen Baum an der richtigen Stelle anzeigen. Hier ist der Code dafür:
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( \!"\!" )} )
)
))"
)
)
);
);
Beachten Sie, dass ich lediglich die Variablen als Zeichenfolgen formatiere, sie an den richtigen Stellen im Code einfüge und ihn dann ausführe. Das Endergebnis ist ein vollständig interaktiver Parallelplot in meiner App mit sehr wenig Programmieraufwand meinerseits. Ich habe versucht, manuell einen Parallelplot zu erstellen. es hat funktioniert, aber nur knapp. Ich empfehle es nicht. Verwenden Sie einfach Graph Builder und Anhängen() . Ich verwende auch die Eval Insert() bauen.Es ist etwas einfacher zu lesen, als die Zeichenfolge mithilfe von Verkettungen zu erstellen.
Der multivariate Plot ist lediglich eine einfache Parametrisierung der multivariaten Plattform in JMP. Ich musste nicht einmal Code schreiben, um dieses Teil zu bekommen! Dort ist ein Webinar „Advanced Mastering JMP“. Das zeigt, wie man das mit nur Mausklicks macht. Auch hier mache ich diese Teile nur aus Bequemlichkeitsgründen auf diese Weise und weil ich wirklich nicht viel Zeit mit diesem Teil der App verbringen wollte. Es geht nicht darum, faul zu sein – es geht darum, ein effizienter Programmierer zu sein. JMP stellt Ihnen den Code zur Verfügung, sodass Sie sich möglicherweise etwas Zeit sparen können.
Die radiale Handlung
Der Radialplot ist eigentlich eine Portierung (mit einigen Modifikationen) einer Beispiel-App, die im Beispielverzeichnis von JMP enthalten ist. Die große Modifikation ist, dass ich sie verwende Marker Seg() anstatt Marker() . Und das ist tatsächlich ein wichtiger Punkt. Es ist möglich, es zu machen Marker() interaktiv mit einigen Maus-Capture-Befehlen und etwas Logik, aber Marker Seg() kümmert sich um das alles automatisch . Sie müssen ihm lediglich mitteilen, welche Datentabelle überwacht werden soll.
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;
);
Wie Sie sehen können, befasst sich ein Großteil des Codes mit den radialen Transformationen usw.Der interessante Teil dieser Diskussion geschieht erst, wenn Sie fast am Ende der Funktion angelangt sind. (Hinweis: Suchen Sie nach Zeichnen Sie Markierungen Kommentar.) Ich füge einen hinzu Marker Seg() zum Diagrammelementstapel hinzufügen. Wenn Sie im fertigen Bericht mit der rechten Maustaste auf das Radialdiagramm klicken und auswählen Anpassen , Sie können es dort sehen:

Der Grund dass Marker Seg() ist so mächtig, dass es das hat Zeilenzustände() Verweis darin. Denken Sie daran, dass alles in JMP durch die Datentabelle über Zeilenzustände verknüpft ist. Indem ich JMP mitteile, mit welcher Datentabelle der Bericht kommunizieren muss, kann ich alle Diagramme im Bericht synchronisieren und habe die gesamte Interaktivität, die ich von JMP gewohnt bin. Und das geschieht einfach durch den Wechsel von Markern zu Markersegmenten (was mich umgehauen hat, als ich es zum Laufen gebracht habe).
Die Spinnenhandlung
Okay, wir haben nichts anderes, worüber ich reden kann, als diesen verdammten Spider-Plot. Also, lasst uns loslegen.
Da es sich bei einem Spinnendiagramm grundsätzlich um ein um die Mittelachse gewickeltes Paralleldiagramm handelt, müssen wir mehr oder weniger ein Paralleldiagramm erstellen – wobei alle Punkte auf das Polarkoordinatensystem projiziert werden. Und wenn das schmerzhaft klingt, haben Sie Recht. Es ist. Es ist auch wirklich, Wirklich Es fällt Ihrem Gehirn schwer, radiale Informationen zu verarbeiten. Diese eine Änderung – von kartesisch zu polar – macht Spinnendiagramme deutlich schwerer lesbar. Wie auch immer, ich werde die Diskussion über die Vorzüge dieser Grafik für ein anderes Mal aufheben. Weiter zum Code!
Die gute Nachricht ist, dass ich die volle Kontrolle darüber habe, da ich eigentlich nur mit einer Dimension pro Variable zu tun habe eckig Teil des Koordinatensystems. Es gibt also einige Transformationsarbeiten, aber so schlimm ist es nicht. Schwierig wird es, wenn man bedenken muss, dass der Benutzer möglicherweise einen größeren Maßstab angezeigt haben möchte, als die Daten tatsächlich abdecken, z. B. die Werte liegen zwischen 3 und 5, aber der mögliche Maßstab ist 1 bis 7. Ich konnte damit umgehen, indem ich nach einer Achsenspalteneigenschaft suchte und dann die Maximal- und Minimalwerte von dort erhielt. Da dies kein kritischer Punkt in der Erzählung ist, können Sie im Quellcode, den ich dem Add-In beigefügt habe, sehen, wie das gemacht wurde.
Die radialen Speichen und Referenzlinien sind nur Linien, die mit dem gezeichnet wurden Linie() Befehl. Der schwierigste Teil der ganzen Sache bestand darin, die Netze für jede Zeile interaktiv zu gestalten und die Legende zum Funktionieren zu bringen. Ich habe die Zeilen interaktiv gemacht, indem ich a verwendet habe Linie Seg() und ein Marker Seg() . Die Legende war eine Neuverwendung eines wirklich alten JMP-Stücks, das ich kürzlich gesehen habe und das als Zeilenlegende bezeichnet wird. Schauen wir uns jeden von ihnen an.
Hier ist der Code zum Erstellen der Linien im Spinnendiagramm. Wie beim Radialdiagramm sind umfangreiche Datenmanipulationen erforderlich, um sie in ein für die grafische Darstellung sinnvolles Format zu bringen:
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;
);
Die wichtigen Bits sind die Linien- und Markierungssegmente am Ende. Ich musste jede Verbindungslinie einzeln zeichnen. Da ist ein Pfad Seg() Das hätte es wirklich einfach gemacht, aber wenn man versucht, es einzufärben, entsteht ein gefülltes Polygon. Also, Linien und Markierungen! Durch die Verwendung von Grafiksegmenten ist es wirklich einfach, über den Zeilenstatusoperator eine Verbindung zur Datentabelle herzustellen. Da nun alle Diagramme die Zeilenzustände aus der Hauptdatentabelle betrachten, ist es möglich, sie alle gleichzeitig einzufärben, indem jedem Zeilenstatus in der Datentabelle Farben zugewiesen werden.
Wenn Sie mit der rechten Maustaste auf einige der Diagramme in JMP klicken, wird die Option „Zeilenlegende“ angezeigt. Es ist schon eine Weile in JMP und macht zwei Dinge. Erstens färbt es die Zeilen in der Datentabelle spaltenweise ein (wie es die Menüoption „Rotes Dreieck“ in der Datentabelle tut). Zweitens wird neben der Visualisierung eine kleine Legende erstellt. Das war alles, was ich tun musste, um die Farben in vier Diagrammen miteinander zu synchronisieren. Hier ist der Code:
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;
);
Da alle meine Diagramme dieselbe Datentabelle für Zeilenzustände betrachten, erben sie alle automatisch die von der Zeilenlegende zugewiesene Farbe! Super glatt.
Die Dinge zum Abschluss bringen
Und das ist es. Ich möchte, dass Skripter hier vor allem von der Leistungsfähigkeit der Grafiksegmente profitieren. Viele der Aspekte, die JMP so besonders machen, hängen davon ab, was diese Funktionen leisten können. Für alle anderen gibt es jetzt ein Spider-Plot-Add-In . Ich bin ziemlich stolz auf das Ergebnis. Aber bitte verwenden Sie es nicht. Es gibt viel bessere Möglichkeiten, diese Art von Daten zu visualisieren. 
Anmerkung des Verfassers
Bei der Erstellung dieses Blogs kamen keine Spinnen zu Schaden, auch wenn es einigen vielleicht übel genommen hat, mit Insekten (sechsbeinige Spinnen) oder Zecken (auch Spinnentieren) in einen Topf geworfen zu werden, aber es sind Vampirspinnen, also würde ich das auch gerne glauben Spinnen hassen sie).