Graficar datos

En el último episodio utilizamos un proveedor de tipo CSV para obtener un conjunto de datos interesantes sobre planetas fuera de nuestro sistema sola r.

Tracemos esos datos.

Aquí está Plotly

Usaremos Plotly como biblioteca de trazado.

Puedes consultar la documentación aquí, ¡está llena de ejemplos!

La figura en Plotly se define como un tipo de unión discriminada:

type GenericChart =
    | Chart of Trace * Layout * Config * DisplayOptions
    | MultiChart of Trace list * Layout * Config * DisplayOptions

Tenga en cuenta que el gráfico MultiChar (es decir, un gráfico con diferentes conjuntos o Traces) es igual que un único Chart, excepto que contiene una Trace list en lugar de un único Trace.

De los documentos:

  • Trace es, en principio, la representación de un conjunto de datos en un gráfico, que incluye los datos en sí, el color y la forma de la visualización, etc.
  • Layout es todo lo que hay en el gráfico que no es específico del conjunto de datos, p. la forma y el estilo de los ejes, el título del gráfico, etc.
  • Config es un objeto que configura propiedades de alto nivel del gráfico, como hacer que todos los elementos del gráfico sean editables o la barra de herramientas en la parte superior.
  • DisplayOptions es un objeto que contiene metainformación sobre cómo funciona el documento html que contiene el gráfico.

La biblioteca de gráficos Plotly está adaptada a la web y está especialmente diseñada para que los gráficos estén disponibles en ella sin problemas. Como tal, genera un formato HTML, con mucha interactividad.

#r "nuget: Plotly.NET, 4.2.0"
#r "nuget: Plotly.NET.Interactive, 4.2.0"
Installed Packages
  • Plotly.NET, 4.2.0
  • Plotly.NET.Interactive, 4.2.0
Loading extensions from `/Users/flavioc/.nuget/packages/plotly.net.interactive/4.2.0/lib/netstandard2.1/Plotly.NET.Interactive.dll`
open Plotly.NET

Comencemos simplemente trazando algunos puntos:

let x = [1;2;5]
let y = [3;4;2]
Chart.Point(x,y)
Chart.Line(x,y)

¿Qué tal trazar varios grupos de puntos?

let x = [1;2;3]
let y1 = [3;4;2]
let y2 = [5;6;4]
[Chart.Point(x, y1);
Chart.Point(x, y2)]
|> Chart.combine

¡Hermoso! Entonces, la figura de los dos conjuntos es solo la combinación de los conjuntos en una lista de gráficos (ahora es el momento de preguntarse por qué la programación funcional es tan buena…).

Censo de exoplanetas

Nos gustaría poder reproducir el gráfico del Censo de exoplanetas que se puede encontrar en Sitio de la NASA sobre exoplanetas:

Exoplanet Census

Como puedes ver, es una gráfica de las masas de los planetas respecto al período orbital, con una escala logarítmica en ambos ejes. Además, el color diferencia el método utilizado para detectar el planeta. Por otra parte, hay un control deslizante en la parte inferior que filtra los planetas descubiertos hasta un año determinado.

Todavía necesitamos un poco de procesamiento de nuestros “planetas” para sintetizar los datos para graficar. Necesitamos construir nuestros datos x e y para cada tipo de planeta y luego dividirlos según el método de detección. Las coordenadas de las abscisas vienen dadas por la propiedad .Pl_orbper, mientras que nuestras y son las de .Pl_masse. También debemos realizar un seguimiento del método de detección, .Discoverymethod. Finalmente, dado que la figura varía según la posición del control deslizante que representa el año del descubrimiento, también necesitaríamos capturar esos datos.

Usemos el Type Provider con los datos consolidados que obtuvimos en el episodio anterior:

#r "nuget: FSharp.Data"

open FSharp.Data
Installed Packages
  • FSharp.Data, 6.4.0
[<Literal>]
let exoplanetsFile = "../data/consolidatedExoplanets.csv"

type ExoTypeProvider = FSharp.Data.CsvProvider<exoplanetsFile, HasHeaders=true, PreferOptionals=true>
let planets = ExoTypeProvider.GetSample()

Obteniendo el año del descubrimiento

Nuestro proveedor de tipos nos proporciona el período orbital, la masa y el método de descubrimiento directamente en algunos de los campos del tipo: .Pl_orbper, .Pl_masse, .Discoverymethod respectivamente. ¿Dónde está el año en nuestros datos primitivos? Inspeccionemos la primera entrada y veamos qué obtenemos:

planets.Rows |> Seq.item 0
(OGLE-TR-10 b, Published Confirmed, <a refstr=KONACKI_ET_AL__2005 href=https://ui.adsabs.harvard.edu/abs/2005ApJ...624..372K/abstract target=ref> Konacki et al. 2005 </a>, , Some(222.47977142857142857142857143), Some(3.1013125714285714285714285714), Transit, False, Some(1344.9700000), )
Item1
OGLE-TR-10 b
Item2
Published Confirmed
Item3
<a refstr=KONACKI_ET_AL__2005 href=https://ui.adsabs.harvard.edu/abs/2005ApJ...624..372K/abstract target=ref> Konacki et al. 2005 </a>
Item4
<null>
Item5
Some(222.47977142857142857142857143)
Value
222.47977142857142857142857143
Item6
Some(3.1013125714285714285714285714)
Value
3.1013125714285714285714285714
Item7
Transit
Rest
(False, Some(1344.9700000), )
Item1
False
Item2
Some(1344.9700000)
Value
1344.9700000
Item3
<null>

Parece que el año está incluido en la información de la publicación. En este caso, está contenido en la string html:

<a refstr=KONACKI_ET_AL__2005 href=https://ui.adsabs.harvard.edu/abs/2005ApJ...624..372K/abstract target=ref> Konacki et al. 2005 </a>

Puede que la forma más sencilla de recuperar el año sea dividir la cadena por espacios y usar expresiones regulares para obtener la cadena que representa un año (es decir, una cadena de cuatro caracteres que contiene solo números). Para ello construimos una función que dada la referencia html, devuelve el año

let getYearOfDiscovery (ref: string) = 
    let found = RegularExpressions.Regex.Matches(ref, " \d{4}")
    match found.Count with
    | 0 -> None
    | 1 -> Some (found.[0].Value.Trim(' ') |> int) 
    | _ -> failwith "More than one year found"
    

Estamos utilizando aquí todo el poder de .NET a través de RegularExpressions.Regex. Buscamos en la cadena un número de cuatro dígitos precedido por un espacio con la expresión regular ` \d{4}`. Si bien esto parece bastante aventurado, una inspección rápida de los datos nos demuestra que tenemos razón. Es hora de agradecer a la NASA por brindarnos datos tan uniformes. En otros casos uno podría tener menos suerte…

Probémoslo en el ejemplo:

let ref = "<a refstr=KONACKI_ET_AL__2005 href=https://ui.adsabs.harvard.edu/abs/2005ApJ...624..372K/abstract target=ref> Konacki et al. 2005 </a>"
getYearOfDiscovery ref    
Some(2005)
Value
2005

Comprobemos si cada exoplaneta tiene un año de descubrimiento dentro de la referencia. Para ello, obtenemos todos los años de descubrimiento recorriendo el conjunto completo de datos. Luego, podemos dividir el resultado con List.partition en una lista de aquellos exoplanetas con un año de descubrimiento y aquellos sin él:

let years = 
    planets.Rows
    |> Seq.map (fun row -> row.Disc_refname)
    |> Seq.map (fun ref ->  getYearOfDiscovery ref,ref)

let exoWithYears, exoWithoutYears = 
    years
    |> Seq.toList
    |> List.partition (fun (y,r) -> y.IsSome) // partition into ones that have a year and ones that don't


exoWithoutYears.Length

4

Entonces hay algunos casos (cuatro en realidad) que no tienen un año:

exoWithoutYears.DisplayTable()
Item1Item2
<null>
<a refstr=Q1_Q12_KOI_TABLE href=https://exoplanetarchive.ipac.caltech.edu/docs/Kepler_KOI_docs.html target=ref>Q1-Q12 KOI Table</a>
<null>
<a refstr=Q1_Q12_KOI_TABLE href=https://exoplanetarchive.ipac.caltech.edu/docs/Kepler_KOI_docs.html target=ref>Q1-Q12 KOI Table</a>
<null>
<a refstr=Q1_Q12_KOI_TABLE href=https://exoplanetarchive.ipac.caltech.edu/docs/Kepler_KOI_docs.html target=ref>Q1-Q12 KOI Table</a>
<null>
<a refstr=Q1_Q12_KOI_TABLE href=https://exoplanetarchive.ipac.caltech.edu/docs/Kepler_KOI_docs.html target=ref>Q1-Q12 KOI Table</a>

Según la documentación, se trata de tablas de objeto de interés Kepler, es decir, eventos descubiertos que pueden considerarse analizados preliminarmente pero no constituyen exoplanetas confirmados. Dado que nuestra función getYearOfDiscovery devuelve un valor de Option, podemos usar Seq.choose para eliminarlos adecuadamente del conjunto.

Filtrar los datos

Ahora tenemos todas las herramientas para recopilar los datos de nuestro gráfico. Para ello es útil definir nuestro propio tipo para acotar los datos con los que estamos tratando:

type ExoCensusData =
    {
        DiscoveryMethodName : string 
        OrbitTimes: decimal // decimal asegura al menos 28 bits, y es un formato _decimal_, no binario
        Masses: decimal  
        YearOfDiscovery: int 
    }

y crear una secuencia de ellos. Primero, usamos una tupla para guardar los datos intermedios de la tabla, ya que hay algunos valores de opción, y luego los filtramos a nuestro ExoCensusData:

let data =
    planets.Rows 
    |> Seq.map (fun row -> (row.Discoverymethod, row.Pl_orbper, row.Pl_masse, getYearOfDiscovery row.Disc_refname))
    |> Seq.choose (fun (method, period, mass, year) -> 
        match period,mass,year with
        | Some p, Some m, Some y -> Some {DiscoveryMethodName = method; OrbitTimes = p; Masses = m; YearOfDiscovery = y}
        | _ -> None)
    

Aquí, pasamos una función a Seq.choose que mantiene solo los elementos que tienen el valor Some en todos los campos opcionales, .Pl_orbper, Pl._masse y año de descubrimiento.

data.DisplayTable()
DiscoveryMethodNameOrbitTimesMassesYearOfDiscovery
Transit
3.1013125714285714285714285714
222.47977142857142857142857143
2005
Imaging
73275.446666666666666666666667
3000.00000
2008
Transit
12.33334285
271.05000
2012
Transit
6.02978806
3.78000
2014
Transit
11.578912324
19.80000
2014
Transit
10.558297226
14.70000
2014
Radial Velocity
3185.41500000
7838.32346
2012
Transit
3.89905200
271.74465
2020
Transit
3.1186028214285714285714285714
297.603516
2009
Transit
2.1751799028571428571428571429
292.1623025
2012
Transit
3.0395806166666666666666666667
200.86409333333333333333333333
2011
Transit
3.474474166
237.734225
2011
Transit
1.9550955128571428571428571429
480.45875
2011
Transit
2.7908485271428571428571428571
676.97790
2015
Transit
3.752094845
176.51058833333333333333333333
2009
Transit
3.6528224283333333333333333333
180.41896
2010
Transit
3.76483029
176.203472
2010
Transit
2.2783146666666666666666666667
139.84520
2019
Transit
4.2041948666666666666666666667
292.72143
2019
Transit
2.484194492
340.545795
2013
(1474 more)

Graficando todos los datos

Ahora tenemos nuestros datos muy bien distribuidos en una secuencia de elementos del tipo ExoCensusData. Comencemos graficando todos los datos primero, para ver cómo funciona el formato de la figura, y agregaremos el control deslizante después.

Recuerde que para usar la función Chart.Point, necesitamos tener nuestras x e y para cada traza juntas. Además, necesitamos tener un trace para cada tipo de descubrimiento. Vayamos paso a paso, primero agrupando nuestros datos por método de descubrimiento.

let dataByDiscoveryMethod = 
    data 
    |> Seq.groupBy (fun exoData -> exoData.DiscoveryMethodName)

y luego creando los datos de la figura con un tipo de ayuda:

type ExoTraces = 
    {
        DiscoveryMethodName : string 
        OrbitTimes : seq<decimal>
        Masses : seq<decimal>
        }
let exoTraces = 
    dataByDiscoveryMethod
    |> Seq.map (fun (method, data) -> 
            let orbits = data |> Seq.map (fun exoData -> exoData.OrbitTimes)
            let masses = data |> Seq.map (fun exoData -> exoData.Masses)
            {DiscoveryMethodName = method; OrbitTimes = orbits; Masses = masses})

Solo necesitamos mapear nuevamente desde exoTraces a Chart.Points y Chart.Combine

exoTraces
|> Seq.map (fun exo -> Chart.Point(exo.OrbitTimes,exo.Masses))
|> Chart.combine

¡Hermoso! Ahora podemos ver por qué necesitamos una escala logarítmica. Para hacer eso usaremos un objeto de Layout de Plotly:

open Plotly.NET.LayoutObjects // this namespace contains all object abstractions for layout styling

Se definen dos valores LinearAxis, uno para cada eje:

let orbPeriodAxis =
    LinearAxis.init (
        Title = Title.init (Text = "ORBIT PERIOD (EARTH DAYS)"),
        AxisType = StyleParam.AxisType.Log,
        ShowLine = true,
        ShowGrid = false,
        Range = StyleParam.Range.MinMax (-2, 8),
        Ticks = StyleParam.TickOptions.Outside
    )

let massLogAxis =
    LinearAxis.init (
        Title = Title.init (Text = "PLANET MASS (EARTH MASSES)"),
        AxisType = StyleParam.AxisType.Log,
        ShowLine = true,
        ShowGrid = false,
        Ticks = StyleParam.TickOptions.Outside
    )

Establecemos la escala logarítmica para ambos ejes con AxisType = StyleParam.AxisType.Log. Además, fijamos el Rango para el eje x (el período orbital) con Range . Tenga en cuenta que, dado que el eje está en escala logarítmica, el rango StyleParam.Range.MinMax (-2, 8) va desde 0,01 ($10^{-2}$) hasta 100 millones ($10^{8}$). Todos los demás argumentos se explican por sí solos.

exoTraces
|> Seq.map (fun exo -> Chart.Point(exo.OrbitTimes,exo.Masses))
|> Chart.combine
|> Chart.withXAxis orbPeriodAxis
|> Chart.withYAxis massLogAxis

Abramos los círculos de la figura.

let openCircle = 
    StyleParam.MarkerSymbol.Modified(
            StyleParam.MarkerSymbol.Circle,
            StyleParam.SymbolStyle.Open
        )

Y podemos usar el argumento Name para cambiar el nombre de las leyendas de cada rastro por el nombre del método de descubrimiento que ya tenemos en nuestro tipo:

exoTraces
|> Seq.map (fun exo -> 
                Chart.Point(exo.OrbitTimes,exo.Masses, Name = exo.DiscoveryMethodName)
                |> Chart.withMarkerStyle(Symbol=openCircle))
|> Chart.combine
|> Chart.withXAxis orbPeriodAxis
|> Chart.withYAxis massLogAxis

¡Cambiemos la relación de aspecto y ya casi terminamos! Para hacer eso, necesitamos usar un ConfigObject:

open Plotly.NET.ConfigObjects
let layout =
    Layout.init(
                Width = 1000,
                Height = 500
    )

exoTraces
|> Seq.map (fun exo -> 
                Chart.Point(exo.OrbitTimes,exo.Masses, Name = exo.DiscoveryMethodName)
                |> Chart.withMarkerStyle(Symbol=openCircle))
|> Chart.combine
|> Chart.withXAxis orbPeriodAxis
|> Chart.withYAxis massLogAxis
|> Chart.withLayout layout 

¡Listo! En la siguiente parte, agregaremos el control deslizante…

results matching ""

    No results matching ""