La bolsa de correo de ingeniería
Episodio 5: Yo. HACER. NO. COMO. ARAÑAS!!! (Y las parcelas de arañas también pueden mejorar).
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...No puedo ignorar las tramas de arañas para siempre, así que allá vamos...
De vez en cuando, los ingenieros de sistemas nos topamos con preguntas interesantes que en cierto modo quedarían fuera del rango típico de JMP uso. Estas aplicaciones son generalmente inteligentes y a menudo demuestran que el uso de datos no es sólo para problemas técnicos o comerciales. Otras veces, las preguntas son simplemente problemas inesperados y desafiantes. Estas “bolas curvas” (como me gusta llamarlas) vienen en muchas formas diferentes: problemas de codificación, análisis interesantes, formas de visualizar datos… ya entiendes la idea.
De vez en cuando, recibo preguntas sobre problemas que realmente no quiero responder. La consulta de hoy está en ese pequeño montón y la he evitado durante años ! El tema de esta entrada en The Mail Bag es generalmente considerado como una trama aterradora y desagradable por parte de la comunidad de visualización de datos, por lo que supongo que es apropiado para una publicación de Halloween.
Dado que este tipo de trama aparece regularmente para los clientes de cosméticos y análisis de consumo en mi trabajo diario, no puedo ignorarla para siempre. Entonces, aprovecho esto como una oportunidad para ayudar a varios grupos de usuarios en Nueva York y Nueva Jersey. Pero tengo que decir…realmente no me gustan las tramas de arañas.
La pregunta
Como dije, he recibido esta pregunta más veces de las que quisiera mencionar. Entonces, voy a omitir el correo electrónico de preguntas en sí, solo tendrán que confiar en mí porque ha habido varios.
También hubo algunas solicitudes en persona.
Y algunas publicaciones en la comunidad JMP.
Sí.
A continuación se muestra un ejemplo de un artículo revisado por pares que alguien me dio como referencia de lo que le gustaría ver:
(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. Elaborar cerveza. 2012; 118: 325–333) Nota del autor para cualquier especialista en visualización de datos que pueda estar leyendo esto: Por favor, perdóneme. Sé que este es un gráfico horrible.
La respuesta
Mi respuesta general a estas solicitudes históricamente coincide con lo que han dicho la mayoría de los expertos en visualización de datos, que es disuadir a las personas de utilizar este gráfico, si es posible. Sin embargo, recientemente comencé a pensar en esta pregunta persistente de manera un poco diferente. Desde entonces he adoptado un enfoque diferente respecto del problema.
Mi filosofía actual sobre los diagramas de araña es que estoy abierto a ayudar a las personas a ver cómo hacer uno si también puedo mostrarles mejores formas de presentar la misma información a lo largo del camino. Como fue el caso de la otra pregunta de visualización que respondí Para mí, el proceso de elaboración de este gráfico fue mucho más instructivo que el resultado real.
ODIO. ARAÑAS.
Bueno, tengo (o más bien tuve) una fuerte antipatía por las arañas durante muchos años. No lo llamaría un caso completo de aracnofobia, pero durante muchos años despacharía cualquier cosa con ocho patas (y ocasionalmente seis; ¡nunca puedes estar seguro!) con extremo prejuicio. No tuve piedad. No di cuartel. I. Tomó. No. Prisioneros. De todos modos, al final lo superé. Y, con la excepción de las garrapatas (¡arañas vampiro!), He hecho las paces con mis amigos de ocho patas. Por lo general, ahora nos mantenemos fuera del camino del otro. Me gustaría pensar que parte de mi aversión a las tramas de arañas se debe a mi problema con las arañas, pero también podría ser que sean un dolor real e instintivamente supe que hacer una sería un poco aterrador. Eso no quiere decir que no haya algunos problemas de codificación interesantes involucrados en su construcción, particularmente si quieres que tengan la interactividad que los usuarios esperan de JMP (sólo que sabía que el código en sí iba a ser un poco intimidante). De todos modos, echemos un vistazo al problema.
Alcance del problema
Practicando lo que predico , Comencé con un planteamiento del problema:
Quiero crear un complemento JMP que muestre un diagrama de araña y otras formas posibles de visualizar un conjunto de datos en un marco que pueda agregar más adelante.
El siguiente paso es crear una lista de tareas pendientes de las diferentes cosas que debo hacer:
- Obtenga las dimensiones para graficar del usuario.
- Obtenga una columna de etiqueta del usuario para cada subconjunto de datos.
- Construya un gráfico de araña interactivo para las dimensiones proporcionadas por cada subconjunto.
- Construya un gráfico radial interactivo para las dimensiones proporcionadas por cada subconjunto.
- Construya un gráfico paralelo interactivo para las dimensiones proporcionadas por cada subconjunto.
- Construya una tabla interactiva de correlaciones por pares para las dimensiones proporcionadas.
Analizar el alcance del problema reveló algunas buenas noticias, específicamente, puedo descargar muchas de estas cosas a JMP si uso Application Builder, lo que también hará que sea muy fácil agregar nuevas visualizaciones más adelante si es necesario. Las partes desafiantes se centraron principalmente en los dos primeros gráficos y en hacerlos interactivos. Los gráficos de JMP funcionan en sistemas de coordenadas cartesianas ( X y y coordenadas). Esos gráficos están, para todos los efectos, en un sistema de coordenadas polares ( r y coordenadas φ). Como resultado, voy a necesitar algo de código para realizar conversiones entre sistemas de coordenadas cartesianas y polares. Empecemos por ahí.
Funciones de transformación de coordenadas
Aquí está la función que utilicé para trabajar entre los dos sistemas de coordenadas:
polarToRect = Function( {r, t},
Eval List( {r Cosine( t ), r Sine( t )} )
);
Es bastante simple. Justo lo que encontrarías en un libro de texto de geometría. Es un código bastante bueno, ya que toma el radio (r) y el ángulo (t) y devuelve un par de coordenadas (x,y). Por cierto, esta función también se encuentra en una aplicación a la que haré referencia más adelante y que está incluida en las aplicaciones de muestra de JMP.
Como estoy trabajando en Application Builder, casi todo lo demás que haré será una función o una expresión. Todo en este proyecto también es espectacularmente repetitivo, por lo que hará que las partes iterativas de mi código sean más fáciles de leer si solo invoco funciones.
Las partes fáciles
Si echas un vistazo a la final. fuente de la aplicación , puede ver que estoy aprovechando algunas partes existentes de JMP: Graph Builder para un gráfico paralelo y Multivariado para las correlaciones por pares. Fueron creados, casi exclusivamente, utilizando código generado por JMP o capacidades en Application Builder. Entonces, primero los quitaremos del camino.
Debido a la forma en que Graph Builder trata las columnas en este caso, y al hecho de que tengo que poder manejar una cantidad arbitraria de variables, es más fácil para mí crear el script Graph Builder como una expresión y luego agregarlo al archivo principal. mostrar el árbol en el lugar correcto. Aquí está el código para eso:
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( \!"\!" )} )
)
))"
)
)
);
);
Observe que todo lo que estoy haciendo es formatear las variables como cadenas, conectarlas en los lugares correctos del código y luego ejecutarlo. El resultado final es una trama paralela totalmente interactiva en mi aplicación con muy poca codificación de mi parte. Intenté crear un guión paralelo manualmente; Funcionó, pero apenas. No lo recomiendo. Simplemente use Graph Builder y Adjuntar() . También estoy usando el Insertar evaluación() construir.Es un poco más fácil de leer que construir la cadena mediante concatenaciones.
El gráfico multivariado es simplemente una parametrización simple de la plataforma multivariada en JMP. ¡Ni siquiera tuve que escribir código para obtener este bit! Hay un Seminario web de Masterización Avanzada de JMP que muestra cómo hacerlo con solo hacer clic con el mouse. Nuevamente, estoy haciendo estas partes de esta manera por conveniencia y porque realmente no quería gastar mucho tiempo en esta parte de la aplicación. No se trata de ser perezoso, sino de ser un codificador eficiente. JMP le proporciona el código, por lo que también podría ahorrarse algo de tiempo.
La trama radial
El gráfico radial es en realidad una adaptación (con algunas modificaciones) de una aplicación de ejemplo que se incluye en el directorio de muestras de JMP. La gran modificación es que estoy usando Marcador Seg() en lugar de Marcador() . Y ese es realmente un punto importante. Es posible hacer Marcador() interactivo usando algunos comandos de captura del mouse y algo de lógica, pero Marcador Seg() maneja todo esto automáticamente . Sólo tienes que decirle qué tabla de datos necesitas que supervise.
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;
);
Como puede ver, MUCHO código se ocupa de las transformaciones radiales, etc.Lo interesante de esta discusión no ocurre hasta que casi llegas al final de la función. (Pista: busque el Dibujar marcadores comentario.) Estoy agregando un Marcador Seg() a la pila de elementos del gráfico. Si hace clic derecho en el gráfico radial en el informe terminado y selecciona Personalizar , puedes verlo ahí:

La razón que Marcador Seg() es tan poderoso es que tiene eso Estados de fila() referencia en el mismo. Recuerde, todo en JMP está vinculado a través de la tabla de datos mediante estados de fila. Entonces, al indicarle a JMP con qué tabla de datos debe comunicarse el informe, puedo sincronizar todos los gráficos del informe y tener toda la interactividad a la que estoy acostumbrado con JMP. Y es simplemente pasando de Marcadores a Segmentos de Marcadores (lo que me dejó alucinado cuando lo puse a funcionar).
La trama de la araña
Bien, no tenemos nada más de lo que pueda hablar excepto esa maldita trama de araña. Entonces, entremos en ello.
Dado que un diagrama de araña es básicamente un diagrama paralelo enrollado alrededor de un eje central, más o menos vamos a tener que hacer un diagrama paralelo, con todos los puntos proyectados en el sistema de coordenadas polares. Y si eso suena doloroso, tienes razón. Es. También es realmente, en realidad Al cerebro le resulta difícil procesar la información radial. Entonces, ese cambio (de cartesiano a polar) hace que los gráficos de araña sean mucho más difíciles de leer. De todos modos, dejaré el discurso sobre los méritos de este gráfico para otro momento. ¡Al código!
La buena noticia es que como en realidad sólo estoy tratando con una dimensión por variable, tengo control total sobre la angular parte del sistema de coordenadas. Entonces, hay algo de trabajo de transformación allí, pero no es tan malo. Se vuelve complicado cuando hay que considerar que el usuario podría querer que se muestre una escala más amplia que la que realmente cubren los datos, por ejemplo, los valores están entre 3 y 5 pero la escala posible es de 1 a 7. Pude manejar eso buscando una propiedad de columna de eje y luego obteniendo los valores máximo y mínimo desde allí. Dado que ese no es un punto crítico en la narrativa, puede ver cómo se hizo en el código fuente que incluí con el complemento.
Los radios radiales y las líneas de referencia son solo líneas dibujadas usando el Línea() dominio. La parte más difícil de todo esto fue hacer que las redes de cada fila fueran interactivas y hacer que la leyenda funcionara. Hice las líneas interactivas empleando un Línea Seg() y un Marcador Seg() . La leyenda fue una reutilización de una pieza muy antigua de JMP que encontré recientemente llamada Row Legend. Veamos cada uno de ellos.
Aquí está el código para crear las líneas en el gráfico de araña. Al igual que con el gráfico radial, se necesita mucha manipulación de datos solo para ponerlos en un formato que tenga sentido para graficar:
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;
);
Los bits importantes son los segmentos de Línea y Marcador hacia el final. Tuve que dibujar cada línea de conexión individualmente. Hay un Segmento de ruta() Eso habría hecho que esto fuera realmente fácil, pero crea un polígono relleno si intentas colorearlo. Entonces, ¡Líneas y Marcadores fue! Al utilizar Segmentos gráficos, es realmente fácil volver a conectarse a la tabla de datos a través del operador Estados de fila. Ahora, debido a que todos los gráficos analizan los estados de las filas de la tabla de datos principal, es posible colorearlos todos simultáneamente asignando colores a cada estado de las filas en la tabla de datos.
Cuando hace clic derecho en algunos de los gráficos en JMP, aparecerá la opción Leyenda de fila. Ha estado en JMP por un tiempo y hace dos cosas. Primero, colorea las filas de la tabla de datos por columna (como lo hace la opción Menú Triángulo Rojo en la tabla de datos). En segundo lugar, crea una pequeña leyenda junto a la visualización. Eso es todo lo que tuve que hacer para sincronizar los colores entre sí en cuatro gráficos. Aquí está el código:
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;
);
Dado que todos mis gráficos analizan la misma tabla de datos para los estados de las filas, ¡todos heredan automáticamente el color que asigna la leyenda de la fila! Súper resbaladizo.
Resumiendo las cosas
Y eso es. Para los programadores de scripts, lo principal que me gustaría que sacaran de esto es el poder de los segmentos gráficos. Muchas de las partes que hacen que JMP sea tan especial están envueltas en lo que esas funciones pueden hacer. Para todos los demás, ahora hay un complemento de trama de araña . Estoy bastante orgulloso de cómo resultó. Pero por favor no lo uses. Hay formas mucho mejores de visualizar ese tipo de datos. 
Nota del autor
Ninguna araña resultó dañada al escribir este blog, aunque algunos pueden haberse sentido ofendidos por ser agrupadas con insectos (seis patas) o garrapatas (también arácnidos, pero son arañas vampiro, así que me gustaría pensar que incluso las arañas los odian).