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 str
ing, int
eger, 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 (
to
andzero
arguments 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.