Le sac postal d'ingénierie
Épisode 5 : Je. FAIRE. PAS. COMME. ARAIGNÉES!!! (Et les complots d'araignées peuvent aussi faire une randonnée.)
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...Je ne peux pas ignorer les complots d'araignées pour toujours, alors c'est parti...
De temps en temps, nous, ingénieurs système, sommes confrontés à des questions intéressantes qui sortiraient quelque peu du cadre habituel des problèmes. JMP usage. Ces applications sont généralement intelligentes et montrent souvent que l'utilisation des données ne se limite pas à des problèmes commerciaux ou techniques. D’autres fois, les questions ne sont que des problèmes inattendus et difficiles. Ces « boules courbes » (comme j'aime les appeler) se présentent sous de nombreuses formes différentes : problèmes de codage, analyses intéressantes, façons de visualiser les données… vous voyez l'idée.
Parfois, je reçois des questions sur des problèmes auxquels je ne veux vraiment pas répondre. La requête d'aujourd'hui se trouve dans cette petite pile, et j'ai évité celle-ci pendant années ! Le sujet de cette entrée dans The Mail Bag est généralement considéré comme un complot effrayant et désagréable par la communauté des visualisations de données, donc je suppose qu'il est approprié pour un article d'Halloween.
Étant donné que ce type d’intrigue apparaît régulièrement pour les clients du secteur des cosmétiques et de l’analyse des consommateurs dans mon travail quotidien, je ne peux pas l’ignorer éternellement. J'en profite donc pour aider un certain nombre de groupes d'utilisateurs à New York et au New Jersey. Mais je dois dire… Je n’aime vraiment pas les intrigues d’araignées.
La question
Comme je l'ai dit, j'ai reçu cette question plus souvent que je ne voudrais le mentionner. Donc, je vais sauter l'e-mail de questions, vous devrez simplement me croire qu'il y en a eu plusieurs.
Il y a également eu des demandes en personne.
Et quelques posts sur la communauté JMP.
Ouais.
Vous trouverez ci-dessous un exemple tiré d'un article évalué par des pairs que quelqu'un m'a donné comme référence pour ce qu'il aimerait voir :
(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. Brasser. 2012 ; 118 : 325-333) Note de l'auteur à tout spécialiste de la visualisation de données qui pourrait lire ceci : Veuillez me pardonner. Je sais que c'est un graphique horrible.
La réponse
Ma réponse générale à ces demandes s'inscrit historiquement dans la lignée de ce que la plupart des experts en visualisation de données ont dit, à savoir décourager les gens d'utiliser ce graphique dans la mesure du possible. Cependant, récemment, j’ai commencé à réfléchir un peu différemment à cette question persistante. Depuis, j'ai adopté une approche différente du problème.
Ma philosophie actuelle concernant les spider plots est que je suis disposé à aider les gens à comprendre comment en créer un si je peux également leur montrer de meilleures façons de présenter les mêmes informations en cours de route. Comme ce fut le cas avec l'autre question de visualisation à laquelle j'ai répondu , pour moi, le processus de réalisation de ce graphique a été bien plus instructif que le résultat réel.
JE DÉTESTE. ARAIGNÉES.
OK, j'ai (ou plutôt j'avais) une forte antipathie pour les araignées depuis de nombreuses années. Je ne qualifierais pas cela d’arachnophobie à part entière, mais pendant de nombreuses années, j’enverrais tout ce qui avait huit pattes (et parfois six ; on ne peut jamais en être trop sûr !) avec des préjugés extrêmes. Je n'ai donné aucune pitié. Je n'ai pas donné de quartier. JE. A pris. Non. Prisonniers. Quoi qu'il en soit, j'ai fini par m'en remettre. Et, à l’exception des tiques (araignées vampires !), J'ai fait la paix avec mes amis à huit pattes. En général, nous restons à l'écart les uns des autres maintenant. J'aimerais penser qu'une partie de mon aversion pour les complots d'araignées venait de mon problème avec les araignées, mais il se pourrait aussi qu'elles soient une douleur royale et je savais instinctivement qu'en créer un serait un peu effrayant. Cela ne veut pas dire qu'il n'y a pas de problèmes de codage intéressants impliqués dans leur construction, en particulier si vous voulez qu'ils aient l'interactivité que les utilisateurs attendent de JMP (juste que je savais que le code lui-même allait être un peu intimidant). Quoi qu'il en soit, examinons le problème.
Cadrage du problème
Mettre en pratique ce que je prêche , j'ai commencé par un énoncé du problème :
Je souhaite créer un complément JMP qui affiche un graphique en araignée et d'autres moyens possibles de visualiser un ensemble de données dans un cadre que je pourrai ajouter ultérieurement.
L'étape suivante consiste à créer une liste de tâches des différentes choses que je dois faire :
- Obtenez les dimensions du graphique auprès de l’utilisateur.
- Obtenez une colonne d'étiquette de l'utilisateur pour chaque sous-ensemble de données.
- Construisez un graphique en toile d'araignée interactif pour les dimensions fournies par chaque sous-ensemble.
- Construisez un graphique radial interactif pour les dimensions fournies par chaque sous-ensemble.
- Construisez un graphique parallèle interactif pour les dimensions fournies par chaque sous-ensemble.
- Construisez un tableau interactif de corrélations par paires pour les dimensions fournies.
L'analyse du problème a révélé de bonnes nouvelles, en particulier, je peux transférer une grande partie de ces éléments vers JMP si j'utilise Application Builder, ce qui facilitera également l'ajout de nouvelles visualisations ultérieurement si nécessaire. Les éléments difficiles concernaient principalement les deux premiers graphiques et les rendaient interactifs. Les graphiques de JMP fonctionnent dans des systèmes de coordonnées cartésiennes ( X et oui coordonnées). Ces graphiques sont, à toutes fins utiles, dans un système de coordonnées polaires ( r et coordonnées φ). En conséquence, je vais avoir besoin de code pour effectuer des conversions entre les systèmes de coordonnées cartésiennes et polaires. Commençons par là.
Fonctions de transformation de coordonnées
Voici la fonction que j'ai utilisée pour travailler entre les deux systèmes de coordonnées :
polarToRect = Function( {r, t},
Eval List( {r Cosine( t ), r Sine( t )} )
);
C'est assez simple. Exactement ce que vous trouveriez dans un manuel de géométrie. C'est un petit morceau de code plutôt sympa, dans la mesure où il prend le rayon (r) et l'angle (t) et renvoie une paire de coordonnées (x, y). BTW, cette fonction se trouve également dans une application à laquelle je ferai référence plus tard et qui est incluse dans les exemples d'applications JMP.
Étant donné que je travaille dans Application Builder, à peu près tout ce que je vais faire sera soit une fonction, soit une expression. Tout dans ce projet est également spectaculairement répétitif, ce qui rendra les parties itératives de mon code plus faciles à lire si j'appelle simplement des fonctions.
Les morceaux faciles
Si vous regardez la finale source de l'application , vous pouvez voir que j'utilise certaines parties existantes de JMP : Graph Builder pour un tracé parallèle et Multivariate pour les corrélations par paires. Ils ont été créés presque exclusivement à l'aide du code généré par JMP ou des fonctionnalités d'Application Builder. Nous allons donc d’abord les éliminer.
En raison de la façon dont Graph Builder traite les colonnes dans ce cas et du fait que je dois être capable de gérer un nombre arbitraire de variables, il est plus facile pour moi de créer le script Graph Builder en tant qu'expression, puis de l'ajouter au script principal. afficher l’arbre au bon endroit. Voici le code pour cela :
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( \!"\!" )} )
)
))"
)
)
);
);
Notez que tout ce que je fais est de formater les variables sous forme de chaînes et de les brancher aux bons endroits dans le code, puis de l'exécuter. Le résultat final est un tracé parallèle entièrement interactif dans mon application avec très peu de codage de ma part. J'ai essayé de créer manuellement un scénario parallèle ; ça a marché, mais à peine. Je ne le recommande pas. Utilisez simplement Graph Builder et Ajouter() . J'utilise également le Éval Insertion() construction.C'est un peu plus facile à lire que de construire la chaîne à l'aide de concaténations.
Le tracé multivarié n'est qu'un simple paramétrage de la plateforme multivariée dans JMP. Je n'ai même pas eu besoin d'écrire du code pour obtenir ce bit ! Il y a un Webinaire sur la maîtrise avancée de JMP cela montre comment faire cela avec de simples clics de souris. Encore une fois, je fais simplement ces éléments de cette façon pour plus de commodité et parce que je ne voulais vraiment pas passer énormément de temps sur cette partie de l'application. Il ne s'agit pas d'être paresseux, mais d'être un codeur efficace. JMP vous donne le code, alors autant gagner du temps.
Le tracé radial
Le tracé radial est en fait un portage (avec quelques modifications) d'un exemple d'application inclus dans le répertoire d'échantillons de JMP. La grosse modification est que j'utilise Segment de marqueur() au lieu de Marqueur() . Et c’est en fait un point important. Il est possible de faire Marqueur() interactif en utilisant certaines commandes de capture de la souris et un peu de logique, mais Segment de marqueur() gère tout ça automatiquement . Il vous suffit de lui indiquer quelle table de données vous devez surveiller.
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;
);
Comme vous pouvez le voir, BEAUCOUP de code concerne les transformations radiales, etc.La partie intéressante de cette discussion n'arrive que lorsque vous êtes presque à la fin de la fonction. (Indice : recherchez le Dessiner des marqueurs commentaire.) J'ajoute un Segment de marqueur() à la pile d’éléments graphiques. Si vous cliquez avec le bouton droit sur le tracé radial dans le rapport terminé et sélectionnez Personnaliser , vous pouvez le voir là-dedans :

La raison pour laquelle Segment de marqueur() est si puissant, c'est qu'il a ça États de ligne() référence dedans. N'oubliez pas que tout dans JMP est lié via la table de données via les états de ligne. Ainsi, en indiquant à JMP avec quelle table de données le rapport doit communiquer, je peux synchroniser tous les graphiques du rapport et bénéficier de toute l'interactivité à laquelle je suis habitué avec JMP. Et c'est simplement en passant des marqueurs aux segments de marqueurs (ce qui m'a époustouflé quand je l'ai fait fonctionner).
L'intrigue de l'araignée
OK, nous n'avons rien d'autre dont je puisse parler à part ce foutu complot d'araignée. Alors, allons-y.
Puisqu'un tracé en araignée est essentiellement un tracé parallèle enroulé autour d'un axe central, nous allons plus ou moins devoir créer un tracé parallèle – avec tous les points projetés sur le système de coordonnées polaires. Et si cela vous semble douloureux, vous avez raison. C'est. C'est aussi vraiment, vraiment difficile pour votre cerveau de traiter les informations radiales. Ainsi, ce seul changement – du cartésien au polaire – rend les cartes en toile d’araignée beaucoup plus difficiles à lire. Quoi qu'il en soit, je garderai le discours sur les mérites de ce graphique pour une autre fois. Passons au code !
La bonne nouvelle est que comme je ne m'occupe en réalité que d'une seule dimension par variable, j'ai un contrôle total sur la angulaire partie du système de coordonnées. Il y a donc un travail de transformation là-bas, mais ce n'est pas si grave. Cela devient compliqué lorsque vous devez considérer que l'utilisateur peut souhaiter afficher une échelle plus large que celle que les données couvrent réellement, par exemple, les valeurs sont comprises entre 3 et 5 mais l'échelle possible est de 1 à 7. J'ai pu gérer cela en recherchant une propriété de colonne d'axe, puis en obtenant les valeurs max et min à partir de là. Comme ce n’est pas un point critique du récit, vous pouvez voir comment cela a été fait dans le code source que j’ai inclus avec le complément.
Les rayons radiaux et les lignes de référence ne sont que des lignes tracées à l'aide du Doubler() commande. La partie la plus difficile de tout cela a été de rendre les toiles de chaque ligne interactives et de faire fonctionner la légende. J'ai rendu les lignes interactives en employant un Segment de ligne() et un Segment de marqueur() . La légende était une réutilisation d'un très vieux morceau de JMP que j'ai rencontré récemment, appelé Row Legend. Regardons chacun d'eux.
Voici le code pour créer les lignes sur le graphique en araignée. Comme pour le graphique radial, il faut beaucoup de manipulation des données juste pour les mettre dans un format qui ait du sens à représenter graphiquement :
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;
);
Les bits importants sont les segments de ligne et de marqueur vers la fin. J'ai dû dessiner chaque ligne de connexion individuellement. Il y a un Segment de chemin() cela aurait rendu les choses vraiment faciles, mais cela crée un polygone rempli si vous essayez de le colorer. Donc, c'était des lignes et des marqueurs ! En utilisant des segments graphiques, il est très facile de se connecter à la table de données via l'opérateur Row States. Désormais, étant donné que tous les graphiques examinent les états de ligne de la table de données principale, il devient possible de les colorer tous simultanément en attribuant des couleurs à chaque état de ligne de la table de données.
Lorsque vous cliquez avec le bouton droit sur certains graphiques dans JMP, l'option Légende des lignes s'affiche. Il est présent dans JMP depuis un certain temps et fait deux choses. Tout d’abord, il colore les lignes du tableau de données par colonne (comme le fait l’option du menu Triangle rouge dans le tableau de données). Deuxièmement, cela crée une petite légende à côté de la visualisation. C'est tout ce que j'avais à faire pour synchroniser les couleurs les unes avec les autres sur quatre graphiques. Voici le 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;
);
Étant donné que tous mes graphiques examinent le même tableau de données pour les états de ligne, ils héritent tous automatiquement de la coloration attribuée par la légende des lignes ! Super lisse.
Envelopper les choses
Et c'est tout. Pour les scripteurs, la principale chose que j'aimerais que vous retiriez de cela est la puissance des segments graphiques. De nombreux éléments qui rendent JMP si spécial sont liés à ce que ces fonctions peuvent faire. Pour tout le monde, il y a maintenant un complément Spider-Plot . Je suis assez fier de la façon dont cela s'est passé. Mais s’il vous plaît, ne l’utilisez pas. Il existe de bien meilleures façons de visualiser ce type de données. 
Note de l'auteur
Aucune araignée n'a été blessée lors de la rédaction de ce blog, même si certaines ont peut-être pris ombrage d'être regroupées avec des insectes (à six pattes) ou des tiques (également des arachnides, mais ce sont des araignées vampires, donc j'aimerais penser que même les araignées les détestent).