La cassetta postale di ingegneria
Episodio 5: I. FARE. NON. PIACE. RAGNI !!! (E anche le trame dei ragni possono fare un'escursione.)
Non posso ignorare le trame dei ragni per sempre, quindi eccoci qui ...
Di tanto in tanto, noi ingegneri di sistema ci imbattiamo in domande interessanti che cadono un po 'al di fuori della tipica gamma di JMP utilizzo. Queste applicazioni sono generalmente intelligenti e spesso dimostrano come l'utilizzo dei dati non sia solo per problemi tecnici o aziendali. Altre volte, le domande sono solo problemi inaspettati e impegnativi. Queste "palle curve" (come mi piace chiamarle) sono disponibili in molte forme diverse: problemi di codifica, analisi interessanti, modi di visualizzare i dati ... hai un'idea.
Di tanto in tanto, ricevo domande su problemi a cui davvero non voglio rispondere. La domanda di oggi è in quel mucchietto e ho evitato questa per anni ! L'argomento di questa voce in The Mail Bag è generalmente considerato come una trama spaventosa e spiacevole dalla comunità di data viz, quindi immagino che sia appropriato per un post di Halloween.
Poiché questo tipo di trama si presenta regolarmente per i clienti di cosmetici e analisi dei consumatori nel mio lavoro quotidiano, non posso ignorarlo per sempre. Quindi, colgo questa opportunità per aiutare un numero di gruppi di utenti a New York e nel New Jersey. Ma devo dire che ... davvero non mi piacciono le trame dei ragni.
La domanda
Come ho detto, ho ricevuto questa domanda più spesso di quanto vorrei menzionare. Quindi, salterò l'effettiva e-mail della domanda, devi solo fidarti di me che ce ne sono state diverse.
C'erano anche alcune richieste di persona.
E alcuni post sulla community di JMP.
Si.
Di seguito è riportato un esempio tratto da un documento sottoposto a revisione paritaria che qualcuno mi ha fornito come riferimento per ciò che vorrebbe vedere:
(J. Inst. Brew. 2012; 118: 325–333) Nota dell'autore per qualsiasi specialista della visualizzazione dei dati che potrebbe leggere questo: Per favore perdonami. So che questo è un grafico orribile.
La risposta
La mia risposta generale a queste richieste rientra storicamente sulla falsariga di quanto affermato dalla maggior parte degli esperti di visualizzazione dei dati, ovvero scoraggiare le persone dall'usare questa trama, se possibile. Tuttavia, di recente, ho iniziato a pensare a questa domanda persistente in modo leggermente diverso. Da allora ho preso una strada diversa sul problema.
La mia attuale filosofia sulle trame dei ragni è che sono aperto ad aiutare le persone a vedere come crearne uno se posso anche mostrare loro modi migliori per presentare le stesse informazioni lungo la strada. Come nel caso di l'altra domanda sulla visualizzazione a cui ho risposto , per me, il processo di creazione di questo grafico è stato molto più istruttivo del risultato effettivo.
IO ODIO. RAGNI.
OK, ho (o meglio, ho avuto) una forte antipatia per i ragni per molti anni. Non lo definirei un vero e proprio caso di aracnofobia, ma per molti anni spedirei qualsiasi cosa con otto gambe (e occasionalmente sei; non si può mai essere troppo sicuri!) Con estremo pregiudizio. Non ho dato pietà. Non ho dato tregua. IO. Ha preso. No. Prigionieri. Comunque, alla fine l'ho superato. E, ad eccezione delle zecche (ragni vampiri!), Ho fatto pace con i miei amici a otto zampe. In genere ci teniamo lontani l'uno dall'altro. Mi piace pensare che parte della mia avversione alle trame dei ragni derivi dal mio problema con i ragni, ma potrebbe anche essere che sono un dolore reale e istintivamente sapevo che crearne uno sarebbe stato un po 'spaventoso. Questo non vuol dire che non ci siano alcuni problemi di codifica interessanti coinvolti nella loro costruzione, in particolare se vuoi che abbiano l'interattività che gli utenti si aspettano da JMP (solo che sapevo che il codice stesso sarebbe stato un po 'intimidatorio). Comunque, diamo un'occhiata al problema.
Individuazione del problema
Mettere in pratica ciò che predico , Ho iniziato con una dichiarazione del problema:
Voglio creare un componente aggiuntivo JMP che mostri un grafico a ragno e altri possibili modi di visualizzare un set di dati in un framework che posso aggiungere in seguito.
Il passaggio successivo consiste nel creare un elenco punch delle diverse cose che devo fare:
- Ottieni le dimensioni per la rappresentazione grafica dall'utente.
- Ottieni una colonna di etichette dall'utente per ogni sottoinsieme di dati.
- Costruisci un grafico a ragno interattivo per le dimensioni fornite da ciascun sottoinsieme.
- Costruisci un grafico radiale interattivo per le dimensioni fornite da ciascun sottoinsieme.
- Costruisci un grafico parallelo interattivo per le dimensioni fornite da ogni sottoinsieme.
- Costruisci una tabella interattiva di correlazioni a coppie per le dimensioni fornite.
L'analisi del problema ha rivelato alcune buone notizie, in particolare, posso scaricare molte di queste cose su JMP se utilizzo Application Builder, il che renderà anche molto facile aggiungere nuove visualizzazioni in seguito, se necessario. I pezzi impegnativi riguardavano principalmente i primi due grafici e li rendevano interattivi. I grafici di JMP funzionano nei sistemi di coordinate cartesiane ( X e y coordinate). Quei grafici sono, a tutti gli effetti, in un sistema di coordinate polari ( r e φ coordinate). Di conseguenza, avrò bisogno di un po 'di codice per convertire avanti e indietro tra i sistemi di coordinate cartesiane e polari. Cominciamo da lì.
Funzioni di trasformazione delle coordinate
Ecco la funzione che ho usato per lavorare tra i due sistemi di coordinate:
polarToRect = Function( {r, t},
Eval List( {r Cosine( t ), r Sine( t )} )
);
È abbastanza semplice. Proprio quello che troveresti da un libro di testo di geometria. È un bel po 'di codice, in quanto prende il raggio (r) e l'angolo (t) e restituisce una coppia di coordinate (x, y). A proposito, questa funzione è anche in un'applicazione a cui faccio riferimento in seguito e inclusa nelle applicazioni di esempio JMP.
Poiché lavoro in Application Builder, quasi tutto il resto che farò sarà una funzione o un'espressione. Tutto ciò che riguarda questo progetto è anche straordinariamente ripetitivo, quindi renderà le parti iterative del mio codice più facili da leggere se sto solo chiamando funzioni.
I bit facili
Se dai un'occhiata alla finale fonte dell'app , puoi vedere che sto sfruttando alcune parti esistenti di JMP: Graph Builder per un grafico parallelo e Multivariate per le correlazioni a coppie. Sono stati creati, quasi esclusivamente, utilizzando il codice o le funzionalità generate da JMP nell'Application Builder. Quindi, prima li toglieremo di mezzo.
A causa del modo in cui il Generatore di grafici tratta le colonne in questo caso e del fatto che devo essere in grado di gestire un numero arbitrario di variabili, è più facile per me costruire lo script del Costruttore di grafici come un'espressione e quindi aggiungerlo al visualizzare l'albero nella posizione corretta. Ecco il codice per questo:
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( \!"\!" )} )
)
))"
)
)
);
);
Si noti che tutto ciò che sto facendo è formattare le variabili come stringhe e inserirle nei punti giusti del codice e quindi eseguirlo. Il risultato finale è una trama parallela completamente interattiva nella mia app con pochissima codifica da parte mia. Ho provato a scrivere manualmente una trama parallela; ha funzionato, ma appena. Non lo consiglio. Basta usare Graph Builder e Aggiungere() . Sto anche usando il Inserisci valutazione () costruire.È un po 'più facile da leggere che costruire la stringa usando le concatenazioni.
Il grafico multivariato è solo una semplice parametrizzazione della piattaforma multivariata in JMP. Non ho nemmeno dovuto scrivere codice per ottenere questo bit! C'e 'un Webinar JMP sulla masterizzazione avanzata che mostra come farlo con solo clic del mouse. Ancora una volta, sto solo facendo questi bit in questo modo per comodità e perché non volevo davvero dedicare una quantità enorme di tempo a questa parte dell'app. Non è essere pigri, è essere un programmatore efficiente. JMP ti fornisce il codice, quindi potresti anche risparmiare un po 'di tempo.
Il grafico radiale
Il grafico radiale è in realtà una porta (con alcune modifiche) di un'app di esempio inclusa nella directory degli esempi di JMP. La grande modifica è che sto usando Marker Seg () invece di Marcatore () . E questo è effettivamente un punto importante. È possibile fare Marcatore () interattivo utilizzando alcuni comandi di acquisizione del mouse e una certa logica, ma Marker Seg () gestisce tutto questo automaticamente . Devi solo dirgli quale tabella di dati devi monitorare.
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;
);
Come puoi vedere, MOLTO codice ha a che fare con le trasformazioni radiali, ecc.La parte interessante di questa discussione non accade finché non sei quasi alla fine della funzione. (Suggerimento: cerca il file Disegna marcatori commento.) Aggiungo un file Marker Seg () allo stack Elementi grafici. Se si fa clic con il pulsante destro del mouse sul grafico radiale nel report finito e si seleziona personalizzare , puoi vederlo lì:

Il motivo per cui Marker Seg () è così potente è che ce l'ha Row States () riferimento in esso. Ricorda, tutto in JMP è collegato tramite la tabella dati tramite gli stati delle righe. Quindi, dicendo a JMP con quale tabella di dati il report deve comunicare, posso sincronizzare tutti i grafici nel report e avere tutta l'interattività a cui sono abituato con JMP. Ed è solo passando da Marker a Marker Segments (cosa che mi ha lasciato senza fiato quando l'ho fatto funzionare).
La trama del ragno
OK, non abbiamo nient'altro di cui posso parlare tranne quel dannato complotto del ragno. Quindi, entriamo in questo.
Poiché un grafico a ragno è fondamentalmente un grafico parallelo avvolto attorno all'asse centrale, dovremo più o meno creare un grafico parallelo, con tutti i punti proiettati sul sistema di coordinate polari. E, se suona doloroso, hai ragione. È. È anche davvero, veramente difficile per il tuo cervello elaborare le informazioni radiali. Quindi, quel cambiamento - da cartesiano a polare - rende le carte ragno significativamente più difficili da leggere. Comunque, risparmierò per un'altra volta il discorso sui meriti di questo grafico. Al codice!
La buona notizia è che dal momento che ho a che fare solo con una dimensione per variabile, ho il pieno controllo su angolare parte del sistema di coordinate. Quindi, c'è del lavoro di trasformazione lì, ma non è poi così male. Diventa complicato quando è necessario considerare che l'utente potrebbe desiderare una scala più ampia visualizzata rispetto ai dati effettivamente coperti, ad esempio, i valori sono compresi tra 3 e 5 ma la scala possibile è da 1 a 7. Sono stato in grado di gestirlo cercando una proprietà della colonna dell'asse e quindi ottenendo i valori massimo e minimo da lì. Dal momento che non è un punto critico nella narrazione, puoi vedere come è stato fatto nel codice sorgente che ho incluso con il componente aggiuntivo.
I raggi radiali e le linee di riferimento sono solo linee tracciate usando il Linea() comando. La parte più difficile di tutta questa operazione è stata rendere interattivi i web per ogni riga e far funzionare la legenda. Ho reso le linee interattive utilizzando un file Linea Seg () e a Marker Seg () . La leggenda era una riproposizione di un vecchio pezzo di JMP che ho incontrato di recente chiamato Row Legend. Diamo un'occhiata a ciascuno di essi.
Ecco il codice per creare le linee sul grafico a ragno. Come con il grafico radiale, è necessaria molta manipolazione dei dati solo per ottenerlo in un formato che abbia senso rappresentare graficamente:
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;
);
I bit importanti sono i segmenti Line e Marker verso la fine. Ho dovuto disegnare singolarmente ogni linea di collegamento. C'è un Segmento percorso () questo avrebbe reso tutto molto semplice, ma crea un poligono pieno se provi a colorarlo. Quindi, Lines and Markers era! Utilizzando i segmenti grafici, è davvero facile ricollegarsi alla tabella dei dati tramite l'operatore Row States. Ora, poiché tutti i grafici stanno guardando gli stati delle righe dalla tabella dati principale, diventa possibile colorarli tutti simultaneamente assegnando colori a ciascuno Stato riga nella tabella dati.
Quando fai clic con il pulsante destro del mouse su alcuni grafici in JMP, sarà presente l'opzione Legenda riga. È in JMP da un po 'e fa due cose. Innanzitutto, colora le righe nella tabella dati per colonna (come fa l'opzione Menu triangolo rosso nella tabella dati). In secondo luogo, crea una piccola leggenda accanto alla visualizzazione. Questo è tutto ciò che dovevo fare per sincronizzare i colori tra loro su quattro grafici. Ecco il codice:
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;
);
Poiché tutti i miei grafici guardano alla stessa tabella di dati per gli stati delle righe, ereditano tutti automaticamente la colorazione assegnata dalla Legenda delle righe! Super lucido.
Concludendo le cose
E questo è tutto. Per gli sceneggiatori, la cosa principale che vorrei che otteniate da questo è la potenza dei segmenti grafici. Molte delle parti che rendono JMP così speciale sono racchiuse in ciò che queste funzioni possono fare. Per tutti gli altri, adesso c'è un componente aggiuntivo della trama di ragno . Sono abbastanza orgoglioso di come è andata a finire. Ma per favore non usarlo. Esistono modi molto migliori per visualizzare quel tipo di dati.;)
Nota dell'autore
Nessun ragno è stato danneggiato nella scrittura di questo blog, anche se alcuni potrebbero essersi offesi di essere stati ammassati con insetti (sei zampe) o zecche (anche aracnidi, ma sono ragni vampiri, quindi mi piacerebbe pensare che anche i ragni li odiano).