    

 ## On this page

  

 

 # Plot data by category

 Last update: 16.07.2025 

Export pedigree data and tensile test results for composite materials, process the data in a `pandas` DataFrame, and plot summaries of the data (including layup information) using `matplotlib` and `seaborn`.

## Get data from Granta MI

Connect to Granta MI and specify a database.

```python
from GRANTA_MIScriptingToolkit import granta as mpy

mi = mpy.connect("http://my.server.name/mi_servicelayer", autologon=True)
training_db = mi.get_db('MI Training')
training_db.unit_system = 'Metric'

```

Specify the records and attributes you want to export from the first table. We want to use attributes describing the layup for all records in the *Composite Pedigree* table.

```python
comp_pedigree = training_db.get_table('Composite Pedigree')
pedigree_records = comp_pedigree.all_records()
pedigree_attributes = ['Panel number', 'Ply count', 'Panel thickness, average (measured)',
                       'Ply thickness, average (measured)']
comp_pedigree.bulk_fetch(pedigree_records, attributes=pedigree_attributes)

```

Specify the records you want to export from the second table. Here, a link group is used to export only records linked to records in the first table.

Alternatively, you can specify a table with no link groups to the original table, as long as you can join the two datasets later on. For example, we could search for all records created after a certain date, or tested on a certain machine.

```python
link_group = 'Tensile test data'

# Use a set comprehension to ensure we only select each record once
tensile_test_records = {tr for pr in pedigree_records for tr in pr.links[link_group]}
tensile_test_records = list(tensile_test_records)

```

Specify the attributes you want to export from the second table. *Panel number* will be used to relate the tensile tests to the layup data.

```python
tensile_test_data = training_db.get_table('Tensile Test Data')
tensile_attributes = ['Panel number', 'Specimen ID', "Young's Modulus (11-axis)", 'Ultimate Tensile Strength',
                      'Test Type']
tensile_test_data.bulk_fetch(tensile_test_records, attributes=tensile_attributes)

```

## Use DataFrames to pre-process and append the data

### Build a separate DataFrame for each table

Create a DataFrame for each Granta MI table. The DataFrames are built from two separate lists of dictionaries:

```python
import pandas as pd
from functools import partial

def get_attr_vals(attributes, r):
    return {mi_attr: r.attributes[mi_attr].value for mi_attr in attributes}

get_pedigree_vals = partial(get_attr_vals, pedigree_attributes)
get_tensile_vals = partial(get_attr_vals, tensile_attributes)

pedigree_data = [get_pedigree_vals(r) for r in pedigree_records]
tensile_data = [get_tensile_vals(r) for r in tensile_test_records]

```

Create a DataFrame `pedigree_df` that contains the pedigree records. Set the DataFrame index to be the *Panel number* attribute value and sort by the index.

```python
pedigree_df = pd.DataFrame(pedigree_data).set_index('Panel number').sort_index()
pedigree_df.head()

```

    Ply count Panel thickness, average (measured) Ply thickness, average (measured)   Panel number        LBJ13 6 1.397000 0.232029   LBJ14 6 1.431636 0.238044   LBJ42 6 1.438564 0.240007   LBJ53 6 1.385455 0.231279   LBJ62 6 1.410677 0.234618  

Create a second DataFrame `tensile_df` that contains the tensile test results. Again, set the index to be the *Panel number* attribute value and sort by both the index and the *Specimen ID*.

```python
tensile_df = pd.DataFrame(tensile_data).set_index('Panel number').sort_values(by=["Panel number", "Specimen ID"])
tensile_df.head()

```

    Specimen ID Young's Modulus (11-axis) Ultimate Tensile Strength Test Type   Panel number         LBJ13 LBJ1311A 47.594514 1582.870937 0° tension   LBJ13 LBJ1312A 47.353197 1542.122918 0° tension   LBJ13 LBJ1313A 47.387671 1612.952766 0° tension   LBJ14 LBJ1413A \[\] 1613.614663 0° tension   LBJ14 LBJ1415A \[\] 1611.201497 0° tension  

### Join the dataframes

Use the `dataframe.join()` method to create a DataFrame with both sets of data. The join type will default to a left-join if not specified; this is fine in our case because the records were linked via a smart link in Granta MI, and so the set of indices is the same for both DataFrames.

```python
df = pedigree_df.join(tensile_df)
df.head()

```

    Ply count Panel thickness, average (measured) Ply thickness, average (measured) Specimen ID Young's Modulus (11-axis) Ultimate Tensile Strength Test Type   Panel number            LBJ13 6 1.397000 0.232029 LBJ1311A 47.594514 1582.870937 0° tension   LBJ13 6 1.397000 0.232029 LBJ1312A 47.353197 1542.122918 0° tension   LBJ13 6 1.397000 0.232029 LBJ1313A 47.387671 1612.952766 0° tension   LBJ14 6 1.431636 0.238044 LBJ1413A \[\] 1613.614663 0° tension   LBJ14 6 1.431636 0.238044 LBJ1415A \[\] 1611.201497 0° tension  

Finally, create a `units` dict so you can create plots with meaningful axis labels.

```python
attr_defs = [comp_pedigree.attributes[attr] for attr in pedigree_attributes] + \
            [tensile_test_data.attributes[attr] for attr in tensile_attributes]
units = {attr.name: attr.unit for attr in attr_defs if attr.unit}
units

```

*Previous cell output:*

```output
{'Panel thickness, average (measured)': 'mm',
 'Ply thickness, average (measured)': 'mm',
 "Young's Modulus (11-axis)": 'GPa',
 'Ultimate Tensile Strength': 'MPa'}

```

## Plot the data

This code snippet is optional, and is included to increase the size of all plots generated in this notebook.

```python
import matplotlib.pyplot as plt
plt.rcParams['figure.dpi'] = 125

```

### Investigate the relationship between two properties

Create a scatter plot with `matplotlib` comparing *Ultimate Tensile Strength* of the panels to *Ply thickness, average*.

```python
import matplotlib.pyplot as plt

ply_thickness_unit = units['Ply thickness, average (measured)']
ftu_unit = units["Ultimate Tensile Strength"]
ply_thickness_label = rf'Average ply thickness / ${ply_thickness_unit}$'
ftu_label = rf'$F_{{tu}}$ / ${ftu_unit}$'

fig, ax = plt.subplots()
ax.scatter(data=df, x="Ply thickness, average (measured)", y="Ultimate Tensile Strength", alpha=0.75, s=100)
ax.set_xlabel(ply_thickness_label)
ax.set_ylabel(ftu_label)
_ = ax.set_title(r'$F_{tu}$ vs Average ply thickness')

```

![png](/sites/default/files/git_ansys/granta_mi_scripting_toolkit_4_1_and_notebooks_1/02_Plot_data_by_category_28_0.png)

There is a significant difference between the results at 1200 - 1600 MPa and the group below 100 MPa. We need to identify what causes this difference, and then represent the data in a clearer way.

### Investigate the impact of other properties

First, examine the underlying data; use the `DataFrame.describe()` method to view a statistical summary of the data in each column.

```python
df.describe(include='all')

```

    Ply count Panel thickness, average (measured) Ply thickness, average (measured) Specimen ID Young's Modulus (11-axis) Ultimate Tensile Strength Test Type     count 67.000000 67.000000 67.000000 67 67 67.000000 67   unique NaN NaN NaN 67 19 NaN 2   top NaN NaN NaN LBJ1311A \[\] NaN 0° tension   freq NaN NaN NaN 1 49 NaN 56   mean 6.492537 1.534167 0.235604 NaN NaN 1288.110261 NaN   std 1.119701 0.294450 0.005039 NaN NaN 557.743977 NaN   min 6.000000 1.379415 0.229948 NaN NaN 44.629768 NaN   25% 6.000000 1.385455 0.231279 NaN NaN 1353.613343 NaN   50% 6.000000 1.410677 0.234618 NaN NaN 1545.653034 NaN   75% 6.000000 1.438564 0.240007 NaN NaN 1603.610369 NaN   max 9.000000 2.213429 0.245727 NaN NaN 1639.559637 NaN  

The *Test Type* column summary shows that the most frequent value is '0° tension' (as expected), but there are 9 occurrences of a second test type.

To identify all possible values in the *Test Type* column, use `DataFrame['Test Type'].unique()`.

```python
df['Test Type'].unique()

```

*Previous cell output:*

```output
array(['0° tension', '90° tension'], dtype=object)

```

Although there is only one populated attribute for *Ultimate Tensile Strength* and *Young's modulus*, the *Test Type* column shows that the tensile tests were performed in two different directions.

Since composite materials are often highly orthotropic, the direction of the test must be taken into account when plotting the results. We can filter the DataFrame based on the *Test Type* column when plotting:

```python
ftu_0_label = rf'$F_{{tu}}, 0^{{\circ}}$ / ${ftu_unit}$'
df_0_tension = df[df['Test Type'] == '0° tension']

fig, ax = plt.subplots()
ax.scatter(data=df_0_tension,
           x="Ply thickness, average (measured)",
           y="Ultimate Tensile Strength",
           alpha=0.75,
           s=100)
ax.set_xlabel(ply_thickness_label)
ax.set_ylabel(ftu_0_label)
_ = ax.set_title(r'$F_{tu}, 0^{{\circ}}$ vs Average ply thickness')

```

![png](/sites/default/files/git_ansys/granta_mi_scripting_toolkit_4_1_and_notebooks_1/02_Plot_data_by_category_36_0.png)

We can also overlay the 90° *Ultimate Tensile Strength* results, using `.twinx()` to plot them on a separate y-axis.

```python
ftu_90_label = rf'$F_{{tu}}, 90^{{\circ}}$ / ${ftu_unit}$'
df_90_tension = df[df['Test Type'] == '90° tension']

fig, ax1 = plt.subplots()
ax1.scatter(data=df_0_tension,
            x="Ply thickness, average (measured)",
            y="Ultimate Tensile Strength",
            color="blue",
            marker="o",
            alpha=0.75,
            s=100)
ax2 = plt.twinx()
ax2.scatter(data=df_90_tension,
            x="Ply thickness, average (measured)",
            y="Ultimate Tensile Strength",
            color="orange",
            marker="x",
            alpha=0.75,
            s=100)

ax1.set_xlabel(ply_thickness_label)
ax1.set_ylabel(ftu_0_label)
ax2.set_ylabel(ftu_90_label)

h1, l1 = ax1.get_legend_handles_labels()
h2, l2 = ax2.get_legend_handles_labels()

ax1.legend(h1+h2, [ftu_0_label, ftu_90_label], loc='lower right')
_ = ax1.set_title(r'$F_{tu}, 0^{{\circ}}$ and $90^{{\circ}}$ vs Average ply thickness')

```

![png](/sites/default/files/git_ansys/granta_mi_scripting_toolkit_4_1_and_notebooks_1/02_Plot_data_by_category_38_0.png)