Creating Plot Templates/Classifier Models

pyrolite provides a system for creating and using plot templates/classifier models based on a series of polygons in variable space (e.g., the TAS diagram). A variety of diagram templates/ classifiers are available, but you can also create your own.

In this tutorial, we’ll go through the process of creating a diagram template from scratch, as a demonstration of how you might create your own for your use - or to later contribute to the collection in pyrolite.

The basis for most diagrams and classifiers is the class PolygonClassifier; the docstring-based help text is a good place to start to understand what we’ll need to put it together:

from pyrolite.util.classification import PolygonClassifier

help(PolygonClassifier)
Help on class PolygonClassifier in module pyrolite.util.classification:

class PolygonClassifier(builtins.object)
 |  PolygonClassifier(name=None, axes=None, fields=None, scale=1.0, transform=None, mode=None, **kwargs)
 |
 |  A classifier model built form a series of polygons defining specific classes.
 |
 |  Parameters
 |  -----------
 |  name : :class:`str`
 |      A name for the classifier model.
 |  axes : :class:`dict`
 |      Mapping from plot axes to variables to be used for labels.
 |  fields : :class:`dict`
 |      Dictionary describing indiviudal polygons, with identifiers as keys and
 |      dictionaries containing 'name' and 'fields' items.
 |  scale : :class:`float`
 |      Default maximum scale for the axes. Typically 100 (wt%) or 1 (fractional).
 |  xlim : :class:`tuple`
 |      Default x-limits for this classifier for plotting.
 |  ylim : :class:`tuple`
 |      Default y-limits for this classifier for plotting.
 |
 |  Methods defined here:
 |
 |  __init__(self, name=None, axes=None, fields=None, scale=1.0, transform=None, mode=None, **kwargs)
 |      Initialize self.  See help(type(self)) for accurate signature.
 |
 |  add_to_axes(self, ax=None, fill=False, axes_scale=1.0, add_labels=False, which_labels='ID', which_ids=None, **kwargs)
 |      Add the fields from the classifier to an axis.
 |
 |      Parameters
 |      ----------
 |      ax : :class:`matplotlib.axes.Axes`
 |          Axis to add the polygons to.
 |      fill : :class:`bool`
 |          Whether to fill the polygons.
 |      axes_scale : :class:`float`
 |          Maximum scale for the axes. Typically 100 (for wt%) or 1 (fractional).
 |      add_labels : :class:`bool`
 |          Whether to add labels for the polygons.
 |      which_labels : :class:`str`
 |          Which data to use for field labels - field 'name' or 'ID'.
 |      which_ids : :class:`list`
 |          List of field IDs corresponding to the polygons to add to the axes object.
 |          (e.g. for TAS, ['F', 'T1'] to plot the Foidite and Trachyte fields).
 |          An empty list corresponds to plotting all the polygons.
 |
 |      Returns
 |      --------
 |      ax : :class:`matplotlib.axes.Axes`
 |
 |  predict(self, X, data_scale=None)
 |      Predict the classification of samples using the polygon-based classifier.
 |
 |      Parameters
 |      -----------
 |      X : :class:`numpy.ndarray` | :class:`pandas.DataFrame`
 |          Data to classify.
 |      data_scale : :class:`float`
 |          Maximum scale for the data. Typically 100 (wt%) or 1 (fractional).
 |
 |      Returns
 |      -------
 |      :class:`pandas.Series`
 |          Series containing classifer predictions. If a dataframe was input,
 |          it inherit the index.
 |
 |  ----------------------------------------------------------------------
 |  Readonly properties defined here:
 |
 |  axis_components
 |      Get the axis components used by the classifier.
 |
 |      Returns
 |      -------
 |      :class:`tuple`
 |          Ordered names for axes used by the classifier.
 |
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |
 |  __dict__
 |      dictionary for instance variables
 |
 |  __weakref__
 |      list of weak references to the object

The key things you’ll need to construct a classifer are:

  1. a name

  2. a specification of what the axes correspond to,

  3. and a dictionary of fields (dictionaries containing a ‘name’ and coordinates defining the polygon).

We can also optionally specify the x and y limits, which are specific to plotting. Here we’ll put together a simple classifier model with just two fields, and add this to a matplotlib axis. You can optionally specify names/labels for each field, here we opt to just use some basic IDs (A and B), so these are what will be added to the plot:

clf = PolygonClassifier(
    name="DemoClassifier",
    axes={"x": "SiO2", "y": "MgO"},
    fields={
        "A": {
            "poly": [[0, 75], [0, 100], [50, 100], [50, 75]],
        },
        "B": {
            "poly": [[0, 25], [0, 75], [25, 75], [25, 25]],
        },
    },
    xlim=(0, 100),
    ylim=(0, 100),
)
ax = clf.add_to_axes(add_labels=True)
ax.figure
templates
<Figure size 640x480 with 1 Axes>

While we’re individually passing each of these arguments to PolygonClassifier, we can also pass a dictionary of keyword arguments:

cfg = dict(
    name="DemoClassifier",
    axes={"x": "SiO2", "y": "MgO"},
    fields={
        "A": {
            "poly": [[0, 75], [0, 100], [50, 100], [50, 75]],
        },
        "B": {
            "poly": [[0, 25], [0, 75], [25, 75], [25, 25]],
        },
    },
    xlim=(0, 100),
    ylim=(0, 100),
)

clf = PolygonClassifier(**cfg)

Each of the built-in models are saved as JSON files, and loaded in a manner as above; we can replicate that here - saving our configuration to JSON then loading it up again. We’ll use a temporary directory here, but you can save it wherever you like (note the pyrolite templates live under /data/models in the repository); once you’ve got a template working how you’d like, consider submitting it!

import json

from pyrolite.util.general import temp_path

tmp = temp_path()
with open(tmp / "demo_classifier.json", "w") as f:
    f.write(json.dumps(cfg))


with open(tmp / "demo_classifier.json", "r") as f:
    clf = PolygonClassifier(**json.load(f))

clf.add_to_axes(add_labels=True).figure
templates
<Figure size 640x480 with 1 Axes>

Ternary Templates

While it’s slightly more work, you can also generate ternary templates using a very simliar pattern to the bivariate ones above. The principal differences are that you’ll need to specify three axes (t, l, r), specify a ‘ternary’ transform, and have coordinates for polygons in the ternary space - each with three values. For example, here are two fields from the UDSA soil texture triangle:

cfg = {
    "axes": {"t": "Clay", "l": "Sand", "r": "Silt"},
    "transform": "ternary",
    "fields": {
        "sand": {"name": "Sand", "poly": [[0, 100, 0], [10, 90, 0], [0, 85, 15]]},
        "loamy-sand": {
            "name": "Loamy Sand",
            "poly": [[10, 90, 0], [0, 85, 15], [0, 70, 30], [15, 85, 0]],
        },
    },
}
PolygonClassifier(**cfg).add_to_axes(add_labels=True).figure
templates
/home/docs/checkouts/readthedocs.org/user_builds/pyrolite/checkouts/main/pyrolite/comp/codata.py:43: UserWarning: Non-positive entries found. Closure operation assumes all positive entries.
  warnings.warn(

<Figure size 640x480 with 1 Axes>

Total running time of the script: (0 minutes 0.674 seconds)