Le sac postal d'ingénierie
Épisode 5: I. FAIRE. NE PAS. COMME. LES ARAIGNÉES!!! (Et les parcelles d'araignées peuvent aussi faire une randonnée.)
Je ne peux pas ignorer les intrigues d'araignées pour toujours, alors allons-y ...
De temps en temps, nous, ingénieurs système, sommes confrontés à des questions intéressantes qui sortiraient quelque peu de la gamme typique des JMP usage. Ces applications sont généralement intelligentes et montrent souvent comment l'utilisation des données n'est pas uniquement destinée à 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 différentes formes: problèmes de codage, analyses intéressantes, manières 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 est dans cette petite pile, et j'ai évité celle-ci pour ans ! 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 données, donc je suppose que c'est approprié pour un message d'Halloween.
Étant donné que ce type d'intrigue apparaît régulièrement pour les clients de la cosmétique et de l'analyse des consommateurs dans mon travail quotidien, je ne peux pas l'ignorer pour toujours. 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 veux en parler. Donc, je vais sauter l'e-mail de question, vous devrez juste me faire confiance qu'il y en a eu plusieurs.
Il y a également eu des demandes en personne.
Et quelques articles sur la communauté JMP.
Ouais.
Vous trouverez ci-dessous un exemple 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. 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 affreux.
La réponse
Ma réponse générale à ces demandes s'inscrit historiquement dans le sens de ce que la plupart des experts en visualisation de données ont dit, c'est-à-dire décourager les gens d'utiliser ce graphique si 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 sur le problème.
Ma philosophie actuelle concernant les tracés d'araignées est que je suis ouvert à aider les gens à 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 c'était le cas avec l'autre question de visualisation à laquelle j'ai répondu , pour moi, le processus de création de ce tableau a été beaucoup plus instructif que le résultat réel.
JE DÉTESTE. LES ARAIGNÉES.
OK, j'ai (ou plutôt, j'avais) une forte antipathie pour les araignées pendant de nombreuses années. Je n'appellerais pas cela un cas à part entière d'arachnophobie, mais pendant de nombreuses années, j'enverrais n'importe quoi avec huit pattes (et parfois six; vous ne pouvez jamais être trop sûr!) Avec des préjugés extrêmes. Je n'ai donné aucune pitié. Je n'ai pas donné de quart. JE. A pris. Non, les prisonniers. Bref, 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. Nous restons généralement à l'écart les uns des autres maintenant. J'aimerais penser qu'une partie de mon aversion pour les intrigues d'araignées provenait de mon problème avec les araignées, mais il se pourrait aussi qu'elles soient une douleur royale et je savais instinctivement que faire 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.
Étude du problème
Pratiquer ce que je prêche , J'ai commencé par une déclaration de problème:
Je souhaite créer un complément JMP qui affiche un tracé d'araignée et d'autres moyens possibles de visualiser un ensemble de données dans un cadre que je pourrai ajouter plus tard.
La prochaine étape consiste à créer une liste des différentes choses que je dois faire:
- Obtenez les dimensions pour le graphique de l'utilisateur.
- Obtenez une colonne d'étiquette de l'utilisateur pour chaque sous-ensemble de données.
- Construisez un graphique en 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.
La portée 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 plus tard si nécessaire. Les éléments difficiles concernaient principalement les deux premiers graphiques et les rendaient interactifs. Les graphes de JMP fonctionnent dans des systèmes de coordonnées cartésiens ( X et y 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 d'un peu de code pour convertir entre les systèmes de coordonnées cartésiens et polaires. Commençons par là.
Coordonner les fonctions de transformation
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 trouverez dans un manuel de géométrie. C'est un petit bout de code assez sympa, en ce sens qu'il prend le rayon (r) et l'angle (t) et renvoie une paire de coordonnées (x, y). BTW, cette fonction est également dans une application que je référence plus tard et qui est incluse dans les exemples d'applications JMP.
Parce 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 de l'Application Builder. Donc, nous allons les éliminer en premier.
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 m'est plus facile de créer le script Graph Builder en tant qu'expression, puis de l'ajouter au fichier 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, c'est formater les variables sous forme de chaînes et les brancher aux bons endroits dans le code, puis 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 rédiger manuellement un scénario parallèle; cela a fonctionné, mais à peine. Je ne le recommande pas. Utilisez simplement Graph Builder et Ajouter() . J'utilise également le Insertion d'évaluation () 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'une 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 JMP Advanced Mastering qui montre comment faire cela avec juste des clics de souris. Encore une fois, je fais ces morceaux de cette façon pour plus de commodité et parce que je ne voulais vraiment pas passer beaucoup de temps sur cette partie de l'application. Ce n'est pas paresseux - c'est être un codeur efficace. JMP vous donne le code, vous pouvez donc aussi gagner du temps.
Le tracé radial
Le tracé radial est en fait un port (avec quelques modifications) d'un exemple d'application inclus dans le répertoire des exemples de JMP. La grande modification est que j'utilise Marqueur Seg () au lieu de Marqueur() . Et c'est en fait un point important. Il est possible de faire Marqueur() interactif en utilisant des commandes de capture de la souris et de la logique, mais Marqueur Seg () gère tout ça automatiquement . Il vous suffit de lui indiquer la table de données dont vous avez besoin pour 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 du code traite des transformations radiales, etc.La partie intéressante de cette discussion ne se produit que lorsque vous êtes presque à la fin de la fonction. (Indice: recherchez le Dessiner des marqueurs commentaire.) J'ajoute un Marqueur Seg () à la pile des é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 Marqueur Seg () est si puissant qu'il a cela États de ligne () référence en elle. N'oubliez pas que tout dans JMP est lié via la table de données via des é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 juste en passant des marqueurs aux segments de marqueurs (ce qui m'a époustouflé quand je l'ai fait fonctionner).
L'intrigue d'araignée
OK, nous n'avons rien d'autre dont je puisse parler, à part ce complot d'araignée. Alors, allons-y.
Comme un tracé d'araignée est essentiellement un tracé parallèle enroulé autour de l'axe central, nous allons plus ou moins devoir faire un tracé parallèle - avec tous les points projetés sur le système de coordonnées polaires. Et, si cela semble douloureux, vous avez raison. Il est. C'est aussi vraiment, vraiment difficile pour votre cerveau de traiter les informations radiales. Donc, ce seul changement - de cartésien à polaire - rend les cartes d'araignée beaucoup plus difficiles à lire. Quoi qu'il en soit, je conserverai le discours sur les mérites de ce graphique pour une autre fois. Sur le code!
La bonne nouvelle, c'est que puisque je n'ai vraiment affaire qu'à une seule dimension par variable, j'ai un contrôle total sur le angulaire partie du système de coordonnées. Donc, il y a du travail de transformation là-bas, mais ce n'est pas si mal. Cela devient compliqué lorsque vous devez considérer que l'utilisateur peut vouloir une échelle plus large que ce 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 Ligne() commander. La partie la plus difficile de tout cela a été de rendre interactifs les sites Web de chaque ligne et de faire fonctionner la légende. J'ai rendu les lignes interactives en employant un Ligne Seg () et un Marqueur Seg () . La légende était une réutilisation d'un très vieux morceau de JMP que j'ai rencontré récemment appelé une légende de ligne. 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 a du sens pour le graphique:
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 Line et Marker vers la fin. J'ai dû dessiner chaque ligne de connexion individuellement. Il y a un Chemin Seg () cela aurait rendu cela vraiment facile, mais cela crée un polygone rempli si vous essayez de le colorer. Donc, Lines and Markers c'était! En utilisant des segments graphiques, il est très facile de se reconnecter à la table de données via l'opérateur Row States. Maintenant, étant donné que tous les graphiques examinent les états de ligne de la table de données principale, il devient possible de les colorier tous simultanément en attribuant des couleurs à chaque état de ligne dans la table de données.
Lorsque vous cliquez avec le bouton droit sur certains des graphiques dans JMP, il y aura l'option Légende de ligne. Il est dans JMP depuis un moment et fait deux choses. Tout d'abord, il colore les lignes de la table de données par colonne (comme le fait l'option de menu Triangle rouge dans la table 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 regardent la même table de données pour les états de ligne, ils héritent tous automatiquement de la coloration attribuée par la légende de ligne! Super lisse.
Emballer les choses
Et c'est tout. Pour les scénaristes, la principale chose que j'aimerais que vous retiriez est la puissance des segments graphiques. Un grand nombre des é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 de parcelle d'araignée . Je suis assez fier du résultat. Mais 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, bien que certaines aient peut-être été choquées d'avoir été regroupées avec des insectes (six pattes) ou des tiques (également des arachnides, mais ce sont des araignées vampires, alors j'aimerais penser que même les araignées les détestent).