La bolsa de correo de ingeniería
Episodio 5: I. HACER. NO. ME GUSTA. ARAÑAS !!! (Y las parcelas de arañas también pueden ser una caminata).
No puedo ignorar las tramas de las arañas para siempre, así que aquí vamos ...
De vez en cuando, los ingenieros de sistemas nos encontramos con preguntas interesantes que estarían un poco fuera del rango típico de JMP uso. Estas aplicaciones son generalmente inteligentes y, a menudo, demuestran que el uso de datos no es solo para problemas comerciales o técnicos. Otras veces, las preguntas son 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.
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 he evitado esta por años ! El tema de esta entrada en The Mail Bag generalmente se considera 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 ignorarlo para siempre. Por lo tanto, 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 con más frecuencia de la que quisiera mencionar. Por lo tanto, voy a omitir el correo electrónico de la pregunta real, solo tendrá que confiar en mí que ha habido varios.
También hubo algunas solicitudes en persona.
Y algunas publicaciones en la comunidad JMP.
Si.
A continuación se muestra un ejemplo de un documento revisado por pares que alguien me dio como referencia de lo que les gustaría ver:
(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 terrible.
La respuesta
Mi respuesta general a estas solicitudes históricamente sigue la línea de lo que han dicho la mayoría de los expertos en visualización de datos, que es disuadir a las personas de usar este gráfico si es posible. Sin embargo, recientemente, comencé a pensar en esta pregunta persistente de manera un poco diferente. Desde entonces he tomado un rumbo diferente al problema.
Mi filosofía actual sobre las tramas de arañas es que estoy abierto a ayudar a las personas a ver cómo hacer una si también puedo mostrarles mejores formas de presentar la misma información en el camino. Como fue el caso con 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.
Está bien, 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 despachaba cualquier cosa con ocho patas (y ocasionalmente seis; ¡nunca puedes estar muy seguro!) Con un prejuicio extremo. No di piedad. No di cuartel. YO. Tomó. No. Prisioneros. De todos modos, eventualmente 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 alejados del camino del otro. Me gustaría pensar que parte de mi aversión a las tramas de arañas proviene de mi problema con las arañas, pero también podría ser que son 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, especialmente si desea que tengan la interactividad que los usuarios esperan de JMP (solo que sabía que el código en sí mismo iba a ser un poco intimidante). De todos modos, echemos un vistazo al problema.
Alcance del problema
Practicando lo que predico , Comencé con una declaración de 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 de las diferentes cosas que necesito 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.
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 realmente fácil agregar nuevas visualizaciones más adelante si es necesario. Las partes desafiantes se encontraban principalmente alrededor de los dos primeros gráficos y los hacían interactivos. Los gráficos de JMP funcionan en sistemas de coordenadas cartesianos ( 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 convertir entre sistemas de coordenadas cartesianas y polares. Empecemos por ahí.
Coordinar funciones de transformación
Aquí está la función que usé 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 agradable, 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 está en una aplicación a la que hago referencia más adelante y que se incluye en las aplicaciones de muestra de JMP.
Como estoy trabajando en Application Builder, casi todo lo demás que voy a hacer 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 estoy llamando a funciones.
Los bits 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 Multivariante para las correlaciones por pares. Fueron creados, casi exclusivamente, utilizando código generado por JMP o capacidades en Application Builder. Entonces, primero los sacaremos 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 un número arbitrario de variables, es más fácil para mí construir 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 y conectarlas en los lugares correctos del código y luego ejecutarlo. El resultado final es un gráfico paralelo completamente interactivo en mi aplicación con muy poca codificación de mi parte. Traté de escribir una trama paralela 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 usando concatenaciones.
El gráfico multivariante es solo una simple parametrización de la plataforma multivariante 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 hacer eso con solo clics del mouse. Nuevamente, solo estoy haciendo estas partes de esta manera por conveniencia y porque realmente no quería gastar una gran cantidad de tiempo en esta parte de la aplicación. No es perezoso, es un codificador eficiente. JMP le proporciona el código, por lo que es mejor que se ahorre algo de tiempo.
La trama radial
El gráfico radial es en realidad un puerto (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 Seg de marcador () 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 Seg de marcador () maneja todo esto automáticamente . Solo tiene que decirle qué tabla de datos necesita monitorear.
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 trata de las transformaciones radiales, etc.Lo interesante de esta discusión no sucede hasta que casi se ha terminado la función. (Sugerencia: busque el Dibujar marcadores comentario.) Estoy agregando un Seg de marcador () a la pila de elementos gráficos. Si hace clic con el botón derecho en el gráfico radial en el informe terminado y selecciona Personalizar , puedes verlo ahí:

La razón que Seg de marcador () es tan poderoso es que tiene eso Estados de fila () referencia en él. Recuerde, todo en JMP está vinculado a través de la tabla de datos mediante estados de fila. Entonces, al decirle 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ó sin aliento cuando lo hice funcionar).
La trama de la araña
De acuerdo, no tenemos nada más de lo que pueda hablar, excepto esa maldita trama de arañas. Entonces, entremos en ello.
Dado que una gráfica de araña es básicamente una gráfica paralela envuelta alrededor del eje central, más o menos vamos a tener que hacer una gráfica paralela, con todos los puntos proyectados sobre el sistema de coordenadas polares. Y, si eso suena doloroso, tienes razón. Está. También es realmente De Verdad difícil para tu cerebro procesar información radial. Entonces, ese cambio, de cartesiano a polar, hace que los gráficos de araña sean significativamente más difíciles de leer. De todos modos, guardaré el discurso sobre los méritos de este gráfico para otro momento. ¡Vamos al código!
La buena noticia es que, dado que en realidad solo estoy tratando con una dimensión por variable, tengo control total sobre la angular parte del sistema de coordenadas. Entonces, hay un trabajo de transformación allí, pero no es tan malo. Es complicado tener en cuenta que el usuario puede querer que se muestre una escala más amplia que la que 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 he incluido con el complemento.
Los radios radiales y las líneas de referencia son simplemente líneas dibujadas utilizando el Línea() mando. La parte más difícil de todo esto fue hacer interactivas las redes para cada fila y hacer que la leyenda funcionara. Hice las líneas interactivas empleando un Seg de línea () y un Seg de marcador () . La leyenda fue una reutilización de una pieza realmente 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 obtener 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 Seg de ruta () eso hubiera hecho esto realmente fácil, pero crea un polígono relleno si intentas colorearlo. Entonces, ¡Líneas y marcadores lo fue! Mediante el uso de segmentos gráficos, es muy 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 miran los estados de fila de la tabla de datos principal, es posible colorearlos todos simultáneamente asignando colores a cada estado de fila en la tabla de datos.
Cuando hace clic con el botón 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 en la tabla de datos por columna (como lo hace la opción Menú del 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 miran la misma tabla de datos para estados de fila, ¡todos heredan automáticamente el color que asigna la leyenda de fila! Super resbaladizo.
Terminando las cosas
Y eso es. Para los scripters, lo principal que me gustaría que obtuvieran es el poder de los segmentos gráficos. Muchos de los bits que hacen que JMP sea tan especial están relacionados con lo que pueden hacer esas funciones. Para todos los demás, hay ahora un complemento de trama de araña . Estoy bastante orgulloso de cómo resultó. Pero, por favor, no lo use. Hay formas mucho mejores de visualizar ese tipo de datos.;)
Nota del autor
No se dañaron arañas al escribir este blog, aunque algunas pueden haberse ofendido por estar 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).