ns.Plate¶
The functionality of the Plate class was briefly described in Getting Started, but this section goes into more depth on its arguments, attributes, and specific use cases.
You can navigate this page using any of the links below:
Arguments¶
data | value_name | case | zero_padding | annotate | pandas_attrs¶
Attributes¶
df | mi_df | locations | annotations | values | column_dict¶
(for modifying the value_name, case, and zero_padding attributes, see their Arguments sections)
Plate-specific Methods¶
annotate_wells | normalize | set_as_location | set_as_value¶
Using DataFrame Methods from Plate¶
import ninetysix as ns
import pandas as pd
import numpy as np
Arguments
data¶
A functional Plate can be constructed in many of the same ways as a DataFrame, such as a path to a .csv or .xls(x) file, a dictionary, or a DataFrame itself.
At minimum, however, all that is needed is an array of well-value pairs. In this case, the columns are assumed to be 'well' and 'value' (unless a value_name argument is specified, see below).
# Well-value pairs
pairs = [
['A1', 1],
['A2', 2],
]
ns.Plate(pairs)
| well | row | column | value | |
|---|---|---|---|---|
| 0 | A1 | A | 1 | 1 |
| 1 | A2 | A | 2 | 2 |
# Zipped well-value pairs
wells = ['A1', 'A2']
values = [1, 2]
zipped = zip(wells, values)
ns.Plate(zipped)
| well | row | column | value | |
|---|---|---|---|---|
| 0 | A1 | A | 1 | 1 |
| 1 | A2 | A | 2 | 2 |
# Dictionary
data = {
'well': wells,
'value': values,
}
ns.Plate(data)
| well | row | column | value | |
|---|---|---|---|---|
| 0 | A1 | A | 1 | 1 |
| 1 | A2 | A | 2 | 2 |
value_name¶
The value_name argument allows you to specify what you want the value of your data to be named. This can either be a new name, if passing in well-value pairs:
ns.Plate(pairs, value_name='activity')
| well | row | column | activity | |
|---|---|---|---|---|
| 0 | A1 | A | 1 | 1 |
| 1 | A2 | A | 2 | 2 |
or it can specify which column in your data should be treated as the important values column, if you have many options:
# Dictionary with multiple potential 'value' columns
data = {
'well': wells,
'activity': values,
'norm_activity': [value/max(values) for value in values]
}
# 'activity' is set to the right
ns.Plate(data, value_name='activity')
| well | row | column | norm_activity | activity | |
|---|---|---|---|---|---|
| 0 | A1 | A | 1 | 0.5 | 1 |
| 1 | A2 | A | 2 | 1.0 | 2 |
Otherwise, if importing verbose data from a dictionary, DataFrame, or csv/xls(x) file, the right-most column is assumed to be the value_name:
pt = ns.Plate(data)
pt
| well | row | column | activity | norm_activity | |
|---|---|---|---|---|---|
| 0 | A1 | A | 1 | 1 | 0.5 |
| 1 | A2 | A | 2 | 2 | 1.0 |
The value_name is stored as an attribute, which can be overwritten at any time with another column in the data. (Read more about Plate attributes below.)
# Print the current value name
print(f'Old value name: {pt.value_name}')
# Change to another column
pt.value_name = 'activity'
# Check again
print(f'New value name: {pt.value_name}')
# Column rearranged
pt
Old value name: norm_activity New value name: activity
| well | row | column | norm_activity | activity | |
|---|---|---|---|---|---|
| 0 | A1 | A | 1 | 0.5 | 1 |
| 1 | A2 | A | 2 | 1.0 | 2 |
case¶
To help standardize the data, Plate tries to guess the case you want to use and applies that to auto-generated columns (in this case, well, row, column).
# Create Plate from well-value pairs with capitalized value_name
ns.Plate(pairs, value_name='Activity')
| Well | Row | Column | Activity | |
|---|---|---|---|---|
| 0 | A1 | A | 1 | 1 |
| 1 | A2 | A | 2 | 2 |
The case of the Plate columns can be strictly enforced via the case argument to help standardize multiple data sources that my not share the same case format.
(Warning: this may cause issues if any of the columns share the same name but different cases, such as 'fx' vs 'Fx'.)
# Non-standard formatting
pt = ns.Plate(
data = {
'WelL': wells,
'actiVIty': values,
'Norm_Activity': [value/max(values) for value in values]
},
case=str.lower # or 'lower'
)
pt
| well | row | column | activity | norm_activity | |
|---|---|---|---|---|---|
| 0 | A1 | A | 1 | 1 | 0.5 |
| 1 | A2 | A | 2 | 2 | 1.0 |
As with value_name, this is stored as an attribute and can be changed at any time:
# Print the current case (or None)
print(f'Old case: {pt.case}')
# Change the case
pt.case = 'title'
# Check again
print(f'New case: {pt.case}')
# Column cases changed
pt
Old case: <method 'lower' of 'str' objects> New case: title
| Well | Row | Column | Activity | Norm_Activity | |
|---|---|---|---|---|---|
| 0 | A1 | A | 1 | 1 | 0.5 |
| 1 | A2 | A | 2 | 2 | 1.0 |
zero_padding¶
There are two common formats for specifying the name of a 'well', either padded with a zero (like A01, to make all well names the same length) or not (like A1).
Plate takes the zero_padding argument to help standardize this when combining data from different sources:
pt = ns.Plate(
data=data,
zero_padding=True,
)
pt
| well | row | column | activity | norm_activity | |
|---|---|---|---|---|---|
| 0 | A01 | A | 1 | 1 | 0.5 |
| 1 | A02 | A | 2 | 2 | 1.0 |
Again, this can be changed at any time as needed:
# Print the current padding
print(f'Old padding: {pt.zero_padding}')
# Change the padding format
pt.zero_padding = False
# Check again
print(f'New padding: {pt.zero_padding}')
# Wells are not padded
pt
Old padding: True New padding: False
| well | row | column | activity | norm_activity | |
|---|---|---|---|---|---|
| 0 | A1 | A | 1 | 1 | 0.5 |
| 1 | A2 | A | 2 | 2 | 1.0 |
annotate¶
As shown in Getting Started, the Plate.annotate_wells() method will assign conditions to wells based on the given mapping. However, this mapping can also be passed during Plate construction:
mapping = {
'A1': 'foo',
'A2': 'bar',
}
# Pass into a new dictionary, where key = new column name
annotations = {
'mapping': mapping
}
ns.Plate(
data=data,
annotate=annotations
)
| well | row | column | activity | mapping | norm_activity | |
|---|---|---|---|---|---|---|
| 0 | A1 | A | 1 | 1 | foo | 0.5 |
| 1 | A2 | A | 2 | 2 | bar | 1.0 |
Note that you should match your zero padding style between your mapping and your plate, but it usually will work regardless:
# Create zero-padded plate with non-padded mapping
pt = ns.Plate(
data=data,
zero_padding=True,
annotate=annotations
)
pt
| well | row | column | activity | mapping | norm_activity | |
|---|---|---|---|---|---|---|
| 0 | A01 | A | 1 | 1 | foo | 0.5 |
| 1 | A02 | A | 2 | 2 | bar | 1.0 |
Additionally, here we see that the activity column is to the left of mapping, even though we know that mapping is an annotation. That's because we never specified activity as a value (which, again, are set to the right side of the DataFrame). We can do this either with the set_as_value method or values attribute, which you can read more about in their respective setions below.
pt = pt.set_as_value('activity')
pt
| well | row | column | mapping | activity | norm_activity | |
|---|---|---|---|---|---|---|
| 0 | A01 | A | 1 | foo | 1 | 0.5 |
| 1 | A02 | A | 2 | bar | 2 | 1.0 |
This organization scheme will become more apparent when we see the mi_df attribute.
pandas_attrs¶
This argument should, ideally, not need to be changed, but there are unique circumstances where it might need to be. If left as True, most pandas.DataFrame attributes and methods will be available directly to a Plate object and will return a new Plate object when possible. This is described more in the Using DataFrame Methods from Plate section.
This requires the pandas_flavor library, which should be automatically installed as a dependency of ninetysix.
If set to False, only native Plate functionality will be available from Plate. (However, all pandas.DataFrame functionality is still available from the Plate.df attribute, as shown below.)
Attributes
df¶
The actual DataFrame that holds the data is stored as Plate.df in the event that it is needed instead of the Plate object.
pt.df
| well | row | column | mapping | activity | norm_activity | |
|---|---|---|---|---|---|---|
| 0 | A01 | A | 1 | foo | 1 | 0.5 |
| 1 | A02 | A | 2 | bar | 2 | 1.0 |
# A Plate has type `ninetysix.plate.Plate`
type(pt)
ninetysix.plate.Plate
# The DataFrame is stored as Plate.df
type(pt.df)
pandas.core.frame.DataFrame
mi_df¶
The mi_df is a MultiIndexed DataFrame (read more here), which provides a more verbose description of the data.
pt.mi_df
| locations | annotations | values | |||
|---|---|---|---|---|---|
| row | column | mapping | activity | norm_activity | |
| well | |||||
| A01 | A | 1 | foo | 1 | 0.5 |
| A02 | A | 2 | bar | 2 | 1.0 |
In most cases this will not be necessary to look at. However, just as a DataFrame silently encodes data-type information for each column (e.g., each column can be a string, integer, etc.), the Plate.mi_df attribute keeps track of a general class of each column as it relates to each well, with the wells being set as the index of the DataFrame.
This can be convenient, as specific wells can be readily pulled through the .loc property of a DataFrame:
# Make a list (of any length) of interesting wells
wells_of_interest = ['A01']
# Pull these index locations
pt.mi_df.loc[wells_of_interest]
| locations | annotations | values | |||
|---|---|---|---|---|---|
| row | column | mapping | activity | norm_activity | |
| well | |||||
| A01 | A | 1 | foo | 1 | 0.5 |
High-level Descriptors¶
As seen in the mi_df above, columns are assigned to be a location (where a well is in the plate), annotation (general information about the well), or value (a usually quantitative measurement about the contents of the well).
locations¶
The Plate.locations attribute is a list of each column of location information in the Plate, usually well, row, and column.
pt.locations
['well', 'row', 'column']
However, any annotation can quickly be added to this list to reconstruct the data:
# Add a new annotation for plate number
pt['plate'] = 1
# Set this as a location
pt.locations += ['plate']
pt.mi_df
| locations | annotations | values | ||||
|---|---|---|---|---|---|---|
| row | column | plate | mapping | activity | norm_activity | |
| well | ||||||
| A01 | A | 1 | 1 | foo | 1 | 0.5 |
| A02 | A | 2 | 1 | bar | 2 | 1.0 |
annotations¶
These columns contain general information about each well, something that is neither a location or value.
pt.annotations
['mapping']
values¶
The values columns are those that contain (usually) quantitative information about each well and can be used for further processing (such as via the normalize method) and analysis/visualization. Although the value_name specifies the working value to be used for these processes, many of them accept lists of values, which can thus be passed as pt.values.
pt.values
['activity', 'norm_activity']
As with locations, any existing annotation can be assigned to be a value by altering the Plate.value attribute:
# Add a new quanitative annotation
pt['activity_2'] = 0
# Set this as a value
pt.values += ['activity_2']
pt.mi_df
| locations | annotations | values | |||||
|---|---|---|---|---|---|---|---|
| row | column | plate | mapping | activity | norm_activity | activity_2 | |
| well | |||||||
| A01 | A | 1 | 1 | foo | 1 | 0.5 | 0 |
| A02 | A | 2 | 1 | bar | 2 | 1.0 | 0 |
Because this was added to the end of the Plate.values attribute, it was assigned to be the new "important" value, i.e., the value_name.
pt.value_name
'activity_2'
We can also set Plate.values to be fewer columns, in which case the unspecified values are assumed to be wholly unimportant and removed:
pt.values = ['activity']
pt.mi_df
| locations | annotations | values | |||
|---|---|---|---|---|---|
| row | column | plate | mapping | activity | |
| well | |||||
| A01 | A | 1 | 1 | foo | 1 |
| A02 | A | 2 | 1 | bar | 2 |
column_dict¶
The mi_df column specifications are kept as the Plate.column_dict attribute:
pt.column_dict
{'locations': ['well', 'row', 'column', 'plate'],
'annotations': ['mapping'],
'values': ['activity']}
Plate-specific Methods
annotate_wells¶
The annotate_wells method is a robust way to assign conditions to the wells of your plates. The method takes a nested dictionary or excel spreadsheet as its single argument.
Annotations with nested dictionaries + well_regex¶
Using a nested dictionary, the inner dictionary (or dictionaries) represents the mapping from well to condition. The outer key for each inner dictionary represents the name of the new annotation column that is generated.
The inner dictionary allows two shorthands to simplify well-condition mapping: well_regex and an else key. With the well_regex function available from ns.parsers, wells keys written in the form [A-D][1,12] will be expanded to A1, A12, B1, ..., D12 and attached to their associated value. The else key will provide what the default (unspecified) wells are (else, default, other, standard are all accepted).
# Load a full plate of example data
pt = ns.Plate('example_data.csv')
# Specify control information
controls = {
'default': 'experiment',
'[A-D]10': 'standard',
'[E-H]10': 'negative',
}
# Pass into a new dictionary, where key = new column name
annotations = {
'controls': controls,
}
# Call annotate_wells method with the nested dict
pt = pt.annotate_wells(annotations)
pt
| well | row | column | controls | activity | |
|---|---|---|---|---|---|
| 0 | A1 | A | 1 | experiment | 11.90 |
| 1 | A2 | A | 2 | experiment | 6.87 |
| 2 | A3 | A | 3 | experiment | 8.30 |
| 3 | A4 | A | 4 | experiment | 8.57 |
| 4 | A5 | A | 5 | experiment | 7.84 |
| ... | ... | ... | ... | ... | ... |
| 91 | H8 | H | 8 | experiment | 12.34 |
| 92 | H9 | H | 9 | experiment | 8.06 |
| 93 | H10 | H | 10 | negative | 5.27 |
| 94 | H11 | H | 11 | experiment | 7.38 |
| 95 | H12 | H | 12 | experiment | 6.92 |
96 rows × 5 columns
Annotations with an Excel template¶
Although the well_regex syntax can be very flexible (and applied to any size plate, not just 96), a more permanent and distributable method is to use a spreadsheet to label each well. This is implemented in annotate_wells if passed an xls(x) file following a specific template. The current template with instructions can be found on the ninetysix GitHub repository and downloaded here, which takes the form of a 96-well plate, like:
| 1 | 2 | ... | ||
|---|---|---|---|---|
| column_1 | A | A1 annotation_1 | A2 annotation_1 | ... |
| column_2 | A1 annotation_2 | A2 annotation_2 | ... | |
| ... | ... | ... | ... | ... |
| column_1 | B | B1 annotation_1 | B2 annotation_1 | ... |
| ... | ... | ... | ... | ... |
An example with the same information as the controls dictionary above is applied below:
# Load a full plate of example data
pt = ns.Plate('example_data.csv')
# Add 'controls' info from excel spreadsheet
pt = pt.annotate_wells('example_annotations.xlsx')
pt
| well | row | column | controls | activity | |
|---|---|---|---|---|---|
| 0 | A1 | A | 1 | experiment | 11.90 |
| 1 | A2 | A | 2 | experiment | 6.87 |
| 2 | A3 | A | 3 | experiment | 8.30 |
| 3 | A4 | A | 4 | experiment | 8.57 |
| 4 | A5 | A | 5 | experiment | 7.84 |
| ... | ... | ... | ... | ... | ... |
| 91 | H8 | H | 8 | experiment | 12.34 |
| 92 | H9 | H | 9 | experiment | 8.06 |
| 93 | H10 | H | 10 | negative | 5.27 |
| 94 | H11 | H | 11 | experiment | 7.38 |
| 95 | H12 | H | 12 | experiment | 6.92 |
96 rows × 5 columns
Note that this spreadsheet can be modified in any way (e.g., number of wells, number of column names) as long as the general style is retained.
normalize¶
The normalize method acts to normalize values columns to (or around) 1. There are three primary ways of accomplishing this:
- Scaling the values such that the maximum value is 1 (no arguments).
- Translating and scaling the values such that they are between 0 and 1 (
zero=False). - Translating and scaling the values such that the mean value of specific groups are set to 0 and 1 (
toandzeroarguments with string values).
These were described in the Getting Started section, but the syntax of passing string arguments to to and zero are shown below:
# cmap sets color *and* layering of points ('standard is on top')
cmap = {
'standard': ns.Colors.green,
'negative': ns.Colors.orange,
'experiment': ns.Colors.blue,
}
pt.normalize(
to='controls=standard',
zero='controls=negative'
).plot_scatter(
ranked=True,
color='controls',
cmap=cmap,
value_name='normalized_activity'
)
set_as_location¶
In case you'd prefer to method chain rather than reassign the Plate.locations attribute as shown above, the set_as_location method can convert an annotation into a column, as shown below:
# Add plate annoation
pt['plate'] = 1
# Set as location in position 1 (0-indexed starting after 'well' column)
pt = pt.set_as_location('plate', idx=1)
pt
| well | row | plate | column | controls | activity | |
|---|---|---|---|---|---|---|
| 0 | A1 | A | 1 | 1 | experiment | 11.90 |
| 1 | A2 | A | 1 | 2 | experiment | 6.87 |
| 2 | A3 | A | 1 | 3 | experiment | 8.30 |
| 3 | A4 | A | 1 | 4 | experiment | 8.57 |
| 4 | A5 | A | 1 | 5 | experiment | 7.84 |
| ... | ... | ... | ... | ... | ... | ... |
| 91 | H8 | H | 1 | 8 | experiment | 12.34 |
| 92 | H9 | H | 1 | 9 | experiment | 8.06 |
| 93 | H10 | H | 1 | 10 | negative | 5.27 |
| 94 | H11 | H | 1 | 11 | experiment | 7.38 |
| 95 | H12 | H | 1 | 12 | experiment | 6.92 |
96 rows × 6 columns
set_as_value¶
The set_as_value method accomplishes the same thing as reassigning the Plate.values attribute in the same way as set_as_location, and allowing a new value_name to be set as well.
# Add new column
pt['activity_2'] = 0
# Set to a new value (also accepts a list) and set as value_name
pt = pt.set_as_value('activity_2', value_name='activity_2')
pt
| well | row | plate | column | controls | activity | activity_2 | |
|---|---|---|---|---|---|---|---|
| 0 | A1 | A | 1 | 1 | experiment | 11.90 | 0 |
| 1 | A2 | A | 1 | 2 | experiment | 6.87 | 0 |
| 2 | A3 | A | 1 | 3 | experiment | 8.30 | 0 |
| 3 | A4 | A | 1 | 4 | experiment | 8.57 | 0 |
| 4 | A5 | A | 1 | 5 | experiment | 7.84 | 0 |
| ... | ... | ... | ... | ... | ... | ... | ... |
| 91 | H8 | H | 1 | 8 | experiment | 12.34 | 0 |
| 92 | H9 | H | 1 | 9 | experiment | 8.06 | 0 |
| 93 | H10 | H | 1 | 10 | negative | 5.27 | 0 |
| 94 | H11 | H | 1 | 11 | experiment | 7.38 | 0 |
| 95 | H12 | H | 1 | 12 | experiment | 6.92 | 0 |
96 rows × 7 columns
Using DataFrame Methods from Plate
The entire pandas library is available from the Plate.df attribute, but to make things more convenient, many DataFrame methods have been directly added to Plate and will return a Plate object with the correct locations, annotations, and values lists. For example, you can merge a Plate with a DataFrame and return the same Plate back:
# Create a DataFrame with some control annotations
df_ann = pd.DataFrame({
'well': ['A10', 'B10', 'C10', 'D10', 'E10', 'F10', 'G10', 'H10'],
'controls': ['standard']*4 + ['negative']*4
})
df_ann
| well | controls | |
|---|---|---|
| 0 | A10 | standard |
| 1 | B10 | standard |
| 2 | C10 | standard |
| 3 | D10 | standard |
| 4 | E10 | negative |
| 5 | F10 | negative |
| 6 | G10 | negative |
| 7 | H10 | negative |
# Remove current 'controls' annotation from plate
del pt['controls']
pt.mi_df
| locations | values | ||||
|---|---|---|---|---|---|
| row | plate | column | activity | activity_2 | |
| well | |||||
| A1 | A | 1 | 1 | 11.90 | 0 |
| A2 | A | 1 | 2 | 6.87 | 0 |
| A3 | A | 1 | 3 | 8.30 | 0 |
| A4 | A | 1 | 4 | 8.57 | 0 |
| A5 | A | 1 | 5 | 7.84 | 0 |
| ... | ... | ... | ... | ... | ... |
| H8 | H | 1 | 8 | 12.34 | 0 |
| H9 | H | 1 | 9 | 8.06 | 0 |
| H10 | H | 1 | 10 | 5.27 | 0 |
| H11 | H | 1 | 11 | 7.38 | 0 |
| H12 | H | 1 | 12 | 6.92 | 0 |
96 rows × 5 columns
# Merge via left join, keeping all rows even if not in 'df_ann'
pt = pt.merge(df_ann, how='left')
# Check the mi_df
pt.mi_df
| locations | annotations | values | ||||
|---|---|---|---|---|---|---|
| row | plate | column | controls | activity | activity_2 | |
| well | ||||||
| A1 | A | 1 | 1 | NaN | 11.90 | 0 |
| A2 | A | 1 | 2 | NaN | 6.87 | 0 |
| A3 | A | 1 | 3 | NaN | 8.30 | 0 |
| A4 | A | 1 | 4 | NaN | 8.57 | 0 |
| A5 | A | 1 | 5 | NaN | 7.84 | 0 |
| ... | ... | ... | ... | ... | ... | ... |
| H8 | H | 1 | 8 | NaN | 12.34 | 0 |
| H9 | H | 1 | 9 | NaN | 8.06 | 0 |
| H10 | H | 1 | 10 | negative | 5.27 | 0 |
| H11 | H | 1 | 11 | NaN | 7.38 | 0 |
| H12 | H | 1 | 12 | NaN | 6.92 | 0 |
96 rows × 6 columns
# Convert NaN to previous default value (experiment)
pt = pt.replace({np.nan: 'experiment'})
pt
| well | row | plate | column | controls | activity | activity_2 | |
|---|---|---|---|---|---|---|---|
| 0 | A1 | A | 1 | 1 | experiment | 11.90 | 0 |
| 1 | A2 | A | 1 | 2 | experiment | 6.87 | 0 |
| 2 | A3 | A | 1 | 3 | experiment | 8.30 | 0 |
| 3 | A4 | A | 1 | 4 | experiment | 8.57 | 0 |
| 4 | A5 | A | 1 | 5 | experiment | 7.84 | 0 |
| ... | ... | ... | ... | ... | ... | ... | ... |
| 91 | H8 | H | 1 | 8 | experiment | 12.34 | 0 |
| 92 | H9 | H | 1 | 9 | experiment | 8.06 | 0 |
| 93 | H10 | H | 1 | 10 | negative | 5.27 | 0 |
| 94 | H11 | H | 1 | 11 | experiment | 7.38 | 0 |
| 95 | H12 | H | 1 | 12 | experiment | 6.92 | 0 |
96 rows × 7 columns
More information on optimizing your visualizations can be found on the Basic data visualization and Advanced data visualization pages.
Information on constructing and using multi-Plate objects can be found on the Plates page.