Il sacco della posta dell'ingegneria
Episodio 5: Io. FARE. NON. COME. RAGNI!!! (E anche le trame dei ragni possono fare un'escursione.)
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...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 esulano un po' dalla gamma tipica di JMP utilizzo. Queste applicazioni sono generalmente intelligenti e spesso dimostrano come l'utilizzo dei dati non sia solo per problemi aziendali o tecnici. Altre volte, le domande sono solo problemi inaspettati e stimolanti. Queste “palle curve” (come mi piace chiamarle) si presentano in molte forme diverse: problemi di codifica, analisi interessanti, modi di visualizzare i dati… hai capito.
Di tanto in tanto ricevo domande su problemi a cui non voglio davvero rispondere. La domanda di oggi è in quella piccola pila e questa l'ho evitata per anni ! L'argomento di questo articolo in The Mail Bag è generalmente considerato una trama spaventosa e spiacevole dalla comunità dei dati, quindi immagino 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, sto cogliendo questa opportunità per aiutare un certo numero di gruppi di utenti a New York e nel New Jersey. Ma devo dire che... non mi piacciono proprio le trame di ragni.
La domanda
Come ho detto, ho ricevuto questa domanda più spesso di quanto mi interessi menzionare. Quindi salterò l'e-mail di domanda vera e propria, dovrai solo fidarti di me che ce ne sono state diverse.
C'erano anche alcune richieste di persona.
E alcuni post sulla community JMP.
Sì.
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) 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. Ist. Birra. 2012; 118: 325–333) Nota dell'autore a qualsiasi specialista di visualizzazione dei dati che potrebbe leggere questo: Per favore perdonami. So che questo è un grafico terribile.
La risposta
La mia risposta generale a queste richieste storicamente cade sulla falsariga di ciò che ha affermato la maggior parte degli esperti di visualizzazione dei dati, ovvero scoraggiare le persone dall'utilizzare questo grafico, se possibile. Tuttavia, recentemente, ho iniziato a pensare a questa domanda persistente in modo leggermente diverso. Da allora ho adottato un approccio diverso al problema.
La mia attuale filosofia riguardo alle trame di ragno è che sono aperto ad aiutare le persone a vedere come realizzarne una se posso anche mostrare loro modi migliori per presentare le stesse informazioni lungo il percorso. Come è avvenuto con all'altra domanda sulla visualizzazione a cui ho risposto , per me, il processo di creazione di questo grafico è stato molto più istruttivo del risultato reale.
IO ODIO. RAGNI.
OK, ho (o meglio, ho avuto) una forte antipatia per i ragni per molti anni. Non lo definirei proprio un caso di aracnofobia in piena regola, ma per molti anni avrei eliminato qualsiasi cosa con otto zampe (e occasionalmente sei; non si è mai troppo sicuri!) con estremo pregiudizio. Non ho avuto pietà. Non ho dato tregua. IO. Preso. No. Prigionieri. Comunque alla fine l'ho superato. E, ad eccezione delle zecche (ragni vampiro!), Ho fatto pace con i miei amici a otto zampe. In genere adesso restiamo lontani l'uno dall'altro. Mi piace pensare che parte della mia avversione per le trame sui ragni derivi dal mio problema con i ragni, ma potrebbe anche essere che siano una seccatura reale e istintivamente sapevo che realizzarne una sarebbe stato un po' spaventoso. Questo non vuol dire che non ci siano alcuni problemi di codifica interessanti coinvolti nella loro costruzione, soprattutto se si vuole 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 visualizzi uno spider plot e altri possibili modi di visualizzare un set di dati in un framework a cui posso aggiungere in seguito.
Il passaggio successivo è creare un elenco completo delle diverse cose che devo fare:
- Ottieni le dimensioni per il grafico dall'utente.
- Ottieni una colonna etichetta 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 ciascun 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 davvero semplice aggiungere nuove visualizzazioni in un secondo momento, se necessario. Le parti più impegnative riguardavano principalmente i primi due grafici e il loro modo di renderli interattivi. I grafici di JMP funzionano in sistemi di coordinate cartesiane ( X E sì coordinate). Questi grafici sono, a tutti gli effetti, in un sistema di coordinate polari ( R e coordinate φ). Di conseguenza, avrò bisogno 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 potresti trovare in un libro di geometria. È un codice piuttosto carino, 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 farò riferimento in seguito, inclusa nelle applicazioni di esempio JMP.
Poiché sto lavorando 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à più facile leggere le parti iterative del mio codice se sto solo chiamando funzioni.
Le parti facili
Se dai un'occhiata al finale fonte dell'app , puoi vedere che sto sfruttando alcune parti esistenti di JMP: Generatore di grafici per un grafico parallelo e Multivariato per le correlazioni a coppie. Sono stati creati, quasi esclusivamente, utilizzando il codice generato da JMP o le funzionalità dell'Application Builder. Quindi prima li toglieremo di mezzo.
A causa del modo in cui il Costruttore di grafici tratta le colonne in questo caso e del fatto che devo essere in grado di gestire un numero arbitrario di variabili, è più semplice per me creare lo script del Costruttore di grafici come un'espressione e quindi aggiungerlo allo script principale 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( \!"\!" )} )
)
))"
)
)
);
);
Nota che tutto ciò che sto facendo è formattare le variabili come stringhe, 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 il Costruttore di grafici e Aggiungere() . Sto anche usando il Inserisci valutazione() costruire.È un po' più semplice da leggere rispetto alla costruzione della stringa utilizzando le concatenazioni.
Il grafico multivariato è solo una semplice parametrizzazione della piattaforma multivariata in JMP. Non ho nemmeno dovuto scrivere codice per ottenere questo pezzo! C'e 'un Webinar sulla padronanza avanzata di JMP che mostra come farlo con semplici clic del mouse. Ancora una volta, sto eseguendo queste operazioni in questo modo solo per comodità e perché non volevo dedicare molto tempo a questa parte dell'app. Non è essere pigro: è essere un programmatore efficiente. JMP ti fornisce il codice, quindi potresti anche risparmiare un po' di tempo.
La trama radiale
Il grafico radiale è in realtà una conversione (con alcune modifiche) di un'applicazione di esempio inclusa nella directory degli esempi di JMP. La grande modifica è che sto usando Segmento marcatore() invece di Indicatore() . E questo è in realtà un punto importante. È possibile farlo Indicatore() interattivo utilizzando alcuni comandi di acquisizione del mouse e un po' di logica, ma Segmento marcatore() gestisce tutto questo automaticamente . Devi solo dirgli quale tabella 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 avviene finché non sei quasi alla fine della funzione. (Suggerimento: cerca il Disegna marcatori commento.) Sto aggiungendo a Segmento marcatore() allo stack degli elementi del grafico. 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 è quello Segmento marcatore() è così potente che ce l'ha Stati riga() riferimento in esso. Ricorda, tutto in JMP è collegato tramite la tabella dati tramite gli stati delle righe. Pertanto, indicando a JMP con quale tabella dati deve comunicare il report, posso sincronizzare tutti i grafici del report e usufruire di tutta l'interattività a cui sono abituato con JMP. Ed è semplicemente passando da Marker a Marker Segments (cosa che mi ha fatto impazzire quando l'ho fatto funzionare).
La trama del ragno
OK, non abbiamo nient'altro di cui posso parlare tranne quella dannata trama del ragno. Quindi, entriamo nel merito.
Poiché uno spider plot è 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 sembra doloroso, hai ragione. È. È anche davvero Veramente difficile per il tuo cervello elaborare le informazioni radiali. Quindi, quell’unico cambiamento – da cartesiano a polare – rende le carte a ragno molto più difficili da leggere. Comunque mi salvo il discorso sul merito di questo grafico per un'altra volta. Al codice!
La buona notizia è che, poiché in realtà mi occupo solo di 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 visualizzata più ampia di quella effettivamente coperta dai dati, ad esempio, i valori sono compresi tra 3 e 5 ma la scala possibile è tra 1 e 7. Sono stato in grado di gestirlo cercando una proprietà della colonna dell'asse e ottenendo da lì i valori massimo e minimo. Dato che questo non è un punto critico nella narrazione, puoi vedere come è stato fatto nel codice sorgente che ho incluso nel componente aggiuntivo.
I raggi radiali e le linee di riferimento sono semplicemente linee disegnate utilizzando il Linea() comando. La parte più difficile di tutta questa faccenda è stata rendere interattive le reti per ogni riga e far funzionare la legenda. Ho reso le linee interattive impiegando a Segmento di linea() e un Segmento marcatore() . La legenda era una riproposizione di un vecchio pezzo di JMP in cui mi sono imbattuto di recente chiamato Row Legend. Diamo un'occhiata a ciascuno di essi.
Ecco il codice per creare le linee sulla carta ragno. Come con il grafico radiale, è necessaria molta manipolazione dei dati solo per inserirli in un formato che abbia senso rappresentarlo 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 Linea e Marker verso la fine. Ho dovuto disegnare ciascuna linea di collegamento individualmente. C'è un Segmento del percorso() ciò lo avrebbe reso davvero semplice, ma crea un poligono pieno se provi a colorarlo. Quindi, Linee e Indicatori era! Utilizzando i segmenti grafici, è davvero semplice ricollegarsi alla tabella dati tramite l'operatore Stati riga. Ora, poiché tutti i grafici esaminano gli stati delle righe della tabella dati principale, diventa possibile colorarli tutti simultaneamente assegnando colori a ciascuno stato delle righe nella tabella dati.
Quando fai clic con il pulsante destro del mouse su alcuni grafici in JMP, sarà disponibile 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 del menu Triangolo rosso nella tabella dati). In secondo luogo, crea una piccola legenda accanto alla visualizzazione. Questo è tutto quello 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 esaminano la stessa tabella dati per gli stati delle righe, ereditano tutti automaticamente la colorazione assegnata dalla legenda della riga! Super liscio.
Concludendo le cose
E questo è tutto. Per gli scripter, la cosa principale che vorrei che otteneste è la potenza dei segmenti grafici. Molti degli aspetti che rendono JMP così speciale sono racchiusi in ciò che queste funzioni possono fare. Per tutti gli altri, c'è adesso un componente aggiuntivo della trama del ragno . Sono piuttosto orgoglioso di come è andata a finire. Ma per favore non usarlo. Esistono modi molto migliori per visualizzare questo tipo di dati. 
Nota dell'autore
Nessun ragno è stato maltrattato durante la stesura di questo blog, anche se alcuni potrebbero essersi offesi per essere stati messi insieme a insetti (a sei zampe) o zecche (anche aracnidi, ma sono ragni vampiri, quindi mi piace pensare che anche i ragni li odiano).