From 6439854098ba47334d0aa2aa8ac98da564b95d8b Mon Sep 17 00:00:00 2001 From: Zsailer Date: Wed, 6 Dec 2017 22:12:46 -0800 Subject: [PATCH 1/7] redesign core datamodel to support only dataframes --- ...ion to Genotype-Phenotype Map Module.ipynb | 428 +++++++++++++++++- gpmap/binary.py | 112 ++--- gpmap/gpm.py | 396 +--------------- 3 files changed, 475 insertions(+), 461 deletions(-) diff --git a/examples/Introduction to Genotype-Phenotype Map Module.ipynb b/examples/Introduction to Genotype-Phenotype Map Module.ipynb index 11834fb..8ef3830 100644 --- a/examples/Introduction to Genotype-Phenotype Map Module.ipynb +++ b/examples/Introduction to Genotype-Phenotype Map Module.ipynb @@ -24,9 +24,7 @@ { "cell_type": "code", "execution_count": 1, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "%matplotlib inline\n", @@ -66,9 +64,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "['AAA' 'CAA' 'GAA' 'TAA' 'ACA' 'CCA' 'GCA' 'TCA' 'AGA' 'CGA' 'GGA' 'TGA'\n", - " 'ATA' 'CTA' 'GTA' 'TTA' 'AAT' 'CAT' 'GAT' 'TAT' 'ACT' 'CCT' 'GCT' 'TCT'\n", - " 'AGT' 'CGT' 'GGT' 'TGT' 'ATT' 'CTT' 'GTT' 'TTT']\n" + "['AAA', 'CAA', 'GAA', 'TAA', 'ACA', 'CCA', 'GCA', 'TCA', 'AGA', 'CGA', 'GGA', 'TGA', 'ATA', 'CTA', 'GTA', 'TTA', 'AAT', 'CAT', 'GAT', 'TAT', 'ACT', 'CCT', 'GCT', 'TCT', 'AGT', 'CGT', 'GGT', 'TGT', 'ATT', 'CTT', 'GTT', 'TTT']\n" ] } ], @@ -112,9 +108,7 @@ { "cell_type": "code", "execution_count": 4, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "from gpmap import GenotypePhenotypeMap" @@ -135,6 +129,416 @@ ")" ] }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 AAA\n", + "8 CAA\n", + "16 GAA\n", + "24 TAA\n", + "2 ACA\n", + "10 CCA\n", + "18 GCA\n", + "26 TCA\n", + "4 AGA\n", + "12 CGA\n", + "20 GGA\n", + "28 TGA\n", + "6 ATA\n", + "14 CTA\n", + "22 GTA\n", + "30 TTA\n", + "1 AAT\n", + "9 CAT\n", + "17 GAT\n", + "25 TAT\n", + "3 ACT\n", + "11 CCT\n", + "19 GCT\n", + "27 TCT\n", + "5 AGT\n", + "13 CGT\n", + "21 GGT\n", + "29 TGT\n", + "7 ATT\n", + "15 CTT\n", + "23 GTT\n", + "31 TTT\n", + "dtype: object" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "gpm.genotypes" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 0.359774\n", + "1 0.123998\n", + "2 0.961551\n", + "3 0.297353\n", + "4 0.254202\n", + "5 0.300492\n", + "6 0.481804\n", + "7 0.366185\n", + "8 0.058473\n", + "9 0.827104\n", + "10 0.857249\n", + "11 0.093769\n", + "12 0.278262\n", + "13 0.146203\n", + "14 0.208120\n", + "15 0.712464\n", + "16 0.187421\n", + "17 0.238268\n", + "18 0.399313\n", + "19 0.357170\n", + "20 0.957250\n", + "21 0.706469\n", + "22 0.520430\n", + "23 0.512336\n", + "24 0.911219\n", + "25 0.854438\n", + "26 0.914308\n", + "27 0.886530\n", + "28 0.904489\n", + "29 0.689049\n", + "30 0.431133\n", + "31 0.373285\n", + "dtype: float64" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "gpm.phenotypes" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
genotypesphenotypesstdeviationsn_replicates
0AAA0.359774NaN1
1AAT0.123998NaN1
2ACA0.961551NaN1
3ACT0.297353NaN1
4AGA0.254202NaN1
5AGT0.300492NaN1
6ATA0.481804NaN1
7ATT0.366185NaN1
8CAA0.058473NaN1
9CAT0.827104NaN1
10CCA0.857249NaN1
11CCT0.093769NaN1
12CGA0.278262NaN1
13CGT0.146203NaN1
14CTA0.208120NaN1
15CTT0.712464NaN1
16GAA0.187421NaN1
17GAT0.238268NaN1
18GCA0.399313NaN1
19GCT0.357170NaN1
20GGA0.957250NaN1
21GGT0.706469NaN1
22GTA0.520430NaN1
23GTT0.512336NaN1
24TAA0.911219NaN1
25TAT0.854438NaN1
26TCA0.914308NaN1
27TCT0.886530NaN1
28TGA0.904489NaN1
29TGT0.689049NaN1
30TTA0.431133NaN1
31TTT0.373285NaN1
\n", + "
" + ], + "text/plain": [ + " genotypes phenotypes stdeviations n_replicates\n", + "0 AAA 0.359774 NaN 1\n", + "1 AAT 0.123998 NaN 1\n", + "2 ACA 0.961551 NaN 1\n", + "3 ACT 0.297353 NaN 1\n", + "4 AGA 0.254202 NaN 1\n", + "5 AGT 0.300492 NaN 1\n", + "6 ATA 0.481804 NaN 1\n", + "7 ATT 0.366185 NaN 1\n", + "8 CAA 0.058473 NaN 1\n", + "9 CAT 0.827104 NaN 1\n", + "10 CCA 0.857249 NaN 1\n", + "11 CCT 0.093769 NaN 1\n", + "12 CGA 0.278262 NaN 1\n", + "13 CGT 0.146203 NaN 1\n", + "14 CTA 0.208120 NaN 1\n", + "15 CTT 0.712464 NaN 1\n", + "16 GAA 0.187421 NaN 1\n", + "17 GAT 0.238268 NaN 1\n", + "18 GCA 0.399313 NaN 1\n", + "19 GCT 0.357170 NaN 1\n", + "20 GGA 0.957250 NaN 1\n", + "21 GGT 0.706469 NaN 1\n", + "22 GTA 0.520430 NaN 1\n", + "23 GTT 0.512336 NaN 1\n", + "24 TAA 0.911219 NaN 1\n", + "25 TAT 0.854438 NaN 1\n", + "26 TCA 0.914308 NaN 1\n", + "27 TCT 0.886530 NaN 1\n", + "28 TGA 0.904489 NaN 1\n", + "29 TGT 0.689049 NaN 1\n", + "30 TTA 0.431133 NaN 1\n", + "31 TTT 0.373285 NaN 1" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "gpm.df" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -436,9 +840,9 @@ ], "metadata": { "kernelspec": { - "display_name": "py3", + "display_name": "gpseer (Python 3)", "language": "python", - "name": "py3" + "name": "gpseer" }, "language_info": { "codemirror_mode": { @@ -450,7 +854,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.5.2" + "version": "3.5.4" } }, "nbformat": 4, diff --git a/gpmap/binary.py b/gpmap/binary.py index 7d46a27..07416f3 100644 --- a/gpmap/binary.py +++ b/gpmap/binary.py @@ -18,40 +18,10 @@ class BinaryMap(object): - """Constructs a binary representation of the genotype-phenotype map. Useful - for building networks, constructing epistasis models, and filling in - genotype-phenotype maps. - - Parameters - ---------- - GPM : GenotypePhenotypeMap object - The genotype phenotype map object to translate as Binary. - - Attributes - ---------- - length : int - length of the binary sequences - genotypes : np.array - array of binary genotype strings, ordered the same as input from GPM. - missing_genotypes : np.array - other genotypes possible by mutations, not given in the data. These are - often the genotypes to predict. - complete_genotypes : np.array - genotypes + missing_genotypes - phenotypes : np.array - phenotypes given by GPM, in the same order as GPM. - encoding : dict - mapping dictionary that takes - n_replicates : int - number of replicates. - logbase : callable - function for log transforming an array or value. - stdeviations : array - standard deviations of genotype phenotype map. """ - - def __init__(self, GPM, wildtype): - self._GPM = GPM + """ + def __init__(self, gpm, wildtype): + self.gpm = gpm self.wildtype = wildtype self.std = StandardDeviationMap(self) self.err = StandardErrorMap(self) @@ -70,32 +40,32 @@ def wildtype(self, wildtype): @property def n_replicates(self): """Get number of replicates""" - return self._GPM.n_replicates + return self.gpm.data.n_replicates @property def stdeviations(self): """Get standard deviations""" - return self._GPM.stdeviations + return self.gpm.data.stdeviations @property def length(self): """Get length of binary strings in space. """ - return self._length + return len(self.gpm.data.binary[0]) @property def genotypes(self): """Get Binary representation of genotypes. """ - return self._genotypes + return self.gpm.data.binary @property def phenotypes(self): """Get phenotypes of the map.""" - return self._GPM.phenotypes + return self.gpm.phenotypes @property def missing_genotypes(self): """Binary genotypes missing in the dataset """ - return self._missing_genotypes + return self.gpm.missing_data.binary @property def complete_genotypes(self): @@ -104,11 +74,7 @@ def complete_genotypes(self): Sorted in alphabetical order according to the GenotypePhenotypeMap.complete_genotypes attribute. """ - return self._complete_genotypes - - # ---------------------------------------------------------- - # Setter methods - # ---------------------------------------------------------- + return self.gpm.complete_data.binary def _build(self): """Builds a binary representation of the genotypes in @@ -118,40 +84,34 @@ def _build(self): **NOTE**: the ``complete_genotypes`` are sorted in alphabetic order. """ - self.encoding = encode_mutations(self._wildtype, self._GPM.mutations) + self.encoding = encode_mutations(self._wildtype, self.gpm.mutations) # Use encoding map to construct binary presentation for any type of # alphabet unsorted_genotypes, unsorted_binary = construct_genotypes( self.encoding) - # determine length of binary strings - self._length = len(unsorted_binary[0]) - - # Series of all possible genotypes and their binary representation - bins = pd.Series(unsorted_binary, index=unsorted_genotypes) - # Sort data in alphabetical order using the actual genotypes - # (not binary representation) - bins = bins.sort_index() - - # Aliases for (some) clarity below. - binary = self - true = self._GPM - - # Build complete genotype map - binary._complete_genotypes = bins.reset_index(drop=True) - true._complete_genotypes = pd.Series(bins.index) - mapping = {g: i for i, g in true._complete_genotypes.iteritems()} - - # Build observed genotype map - obs_index = np.array([mapping[g] for g in true._genotypes]) - true._genotypes = pd.Series(true._complete_genotypes, index=obs_index) - binary._genotypes = pd.Series(binary._complete_genotypes, - index=obs_index) - - # Missing - arr = np.arange(len(bins)) - missing_index = np.delete(arr, obs_index) - true._missing_genotypes = pd.Series(true._complete_genotypes, - index=missing_index) - binary._missing_genotypes = pd.Series(binary._complete_genotypes, - index=missing_index) + data = {'genotypes': unsorted_genotypes, + 'binary': unsorted_binary} + + gpm = self.gpm + gpm.complete_data = pd.DataFrame(data) + gpm.complete_data.sort_values('genotypes', inplace=True) + gpm.complete_data.reset_index(drop=True, inplace=True) + + # Mapping genotypes to index + mapping = dict(zip(gpm.complete_data.genotypes, + gpm.complete_data.index)) + + # Get index + observed_index = [mapping[g] for g in gpm.data.genotypes] + missing_index = set(gpm.complete_data.index).difference(observed_index) + + # Reset index of main data. + gpm.data.index = observed_index + # Add a column for binary representation of genotypes. + gpm.data['binary'] = pd.Series(gpm.complete_data.binary, + index=observed_index) + + # Create a dataframe for the missing data. + gpm.missing_data = pd.DataFrame(gpm.complete_data, + index=missing_index) diff --git a/gpmap/gpm.py b/gpmap/gpm.py index 96ad9bd..cd88c2f 100644 --- a/gpmap/gpm.py +++ b/gpmap/gpm.py @@ -22,41 +22,8 @@ import gpmap.binary as binary -DATA_KEYS = ("genotypes", "phenotypes", "stdeviations", "n_replicates") -OPTIONAL_KEYS = ("stdeviations", "n_replicates") - -# ---------------------------------------------------------- -# Exceptions -# ---------------------------------------------------------- - - -class LoadingException(Exception): - """Error when loading Genotype Phenotype map data. """ - -# ---------------------------------------------------------- -# Base class for constructing a genotype-phenotype map -# ---------------------------------------------------------- - - class GenotypePhenotypeMap(mapping.BaseMap): - """Main object for containing genotype-phenotype map data. Efficient - memory storage, fast-ish mapping, graphing, and simulations. - - Parameters - ---------- - wildtype : string - wildtype sequence. - genotypes : array-like - list of all genotypes in system. Must be a complete system. - phenotypes : array-like - List of phenotypes in the same order as genotypes. - mutations : dict - Dictionary that maps each site indice to their possible substitution - alphabet. - n_replicates : int - number of replicate measurements comprising the mean phenotypes - include_binary : bool (default=True) - Construct a binary representation of the space. + """ """ def __init__(self, wildtype, genotypes, phenotypes, stdeviations=None, @@ -69,19 +36,24 @@ def __init__(self, wildtype, genotypes, phenotypes, if mutations is not None: # Make sure the keys in the mutations dict are integers, not # strings. - self.mutations = dict([(int(key), val) + self._mutations = dict([(int(key), val) for key, val in mutations.items()]) else: mutant = utils.farthest_genotype(wildtype, genotypes) mutations = utils.binary_mutations_map(wildtype, mutant) - self.mutations = mutations + self._mutations = mutations + + # Set wildtype. + self._wildtype = wildtype - # Set initial properties fo GPM - self.wildtype = wildtype - self.genotypes = genotypes - self.phenotypes = phenotypes - self.n_replicates = n_replicates - self.stdeviations = stdeviations + # Store data in DataFrame + data = dict( + genotypes=genotypes, + phenotype=phenotypes, + n_replicates=n_replicates, + stdeviations=stdeviations + ) + self.data = pd.DataFrame(data) # Built the binary representation of the genotype-phenotype. # Constructs a complete sequence space and stores genotypes missing @@ -92,151 +64,15 @@ def __init__(self, wildtype, genotypes, phenotypes, # Construct the error maps self._add_error() - # ---------------------------------------------------------- - # Reading methods - # ---------------------------------------------------------- - - @classmethod - def read_dataframe(cls, wildtype, dataframe, **kwargs): - """Construct a GenotypePhenotypeMap from a dataframe.""" - # Required arguments - df = DataFrames - genotypes = df["genotypes"] - phenotypes = df["phenotypes"] - - # Search for optional columns - other_items = {} - for key in OPTIONAL_KEYS: - try: - other_items[key] = df[key] - except KeyError: - pass - - # Initialize object. - self = cls(wildtype, genotypes, phenotypes, **other_items) - return self - - @classmethod - def read_excel(cls, wildtype, fname, **kwargs): - """""" - df = pd.read_excel(fname) - self = cls.read_dataframe(wildtype, df) - return self - - @classmethod - def read_csv(cls, fname, **kwargs): - """""" - df = pd.read_csv(fname) - self = cls.read_dataframe(wildtype, df) - return self - - @classmethod - def read_json(cls, filename, **kwargs): - """Load a genotype-phenotype map directly from a json file. - The JSON metadata must include the following attributes - - Note - ---- - Keyword arguments override input that is loaded from the JSON file. - """ - # Open, json load, and close a json file - f = open(filename, "r") - data = json.load(f) - f.close() - - # Grab all properties from data-structure - necessary_args = ["wildtype", "genotypes", "phenotypes"] - options = { - "genotypes": [], - "phenotypes": [], - "wildtype": [], - "stdeviations": None, - "mutations": None, - "n_replicates": 1, - } - # Get all options for map and order them - for key in options: - # See if options are in json data - try: - options[key] = data[key] - except KeyError: - pass - # Override any properties with manually entered kwargs passed directly - # into method - options.update(kwargs) - args = [] - for arg in necessary_args: - val = options.pop(arg) - args.append(val) - # Create an instance - gpm = cls(args[0], args[1], args[2], **options) - return gpm - - # ---------------------------------------------------------- - # Writing methods - # ---------------------------------------------------------- - - def to_excel(self, filename, **kwargs): - """Write genotype-phenotype map to excel spreadsheet. - - Keyword arguments are passed directly to Pandas dataframe to_excel - method. - - Parameters - ---------- - filename : str - Name of file to write out. - """ - self.df.to_excel(filename, **kwargs) - - def to_csv(self, filename, **kwargs): - """Write genotype-phenotype map to csv spreadsheet. - - Keyword arguments are passed directly to Pandas dataframe to_csv - method. - - Parameters - ---------- - filename : str - Name of file to write out. - """ - self.df.to_csv(filename, **kwargs) - - def to_json(self, filename): - """Write genotype-phenotype map to json file. - """ - # Get metadata. - data = dict(wildtype=self.wildtype, - genotypes=list(self.genotypes), - phenotypes=list(self.phenotypes), - stdeviations=list(self.stdeviations), - mutations=self.mutations, - n_replicates=list(self.n_replicates.astype(float))) - - # Write to file - with open(filename, "w") as f: - json.dump(data, f) - - # ---------------------------------------------------------- - # Properties methods - # ---------------------------------------------------------- - - @property - def df(self): - """Genotype-phenotype data in a DataFrame.""" - # Build dataframe - data = {item: getattr(self, item) for item in DATA_KEYS} - return pd.DataFrame(data, columns=DATA_KEYS) - @property def length(self): """Get length of the genotypes. """ - return self._length + return len(self.wildtype) @property def n(self): """Get number of genotypes, i.e. size of the genotype-phenotype map.""" - return self._n + return len(self.genotypes) @property def wildtype(self): @@ -267,13 +103,13 @@ def mutations(self): @property def genotypes(self): """Get the genotypes of the system.""" - return self._genotypes + return self.data.genotypes @property def missing_genotypes(self): """Genotypes that are missing from the complete genotype-to-phenotype map.""" - return self._missing_genotypes + return self.missing_data.genotypes @property def complete_genotypes(self): @@ -283,7 +119,7 @@ def complete_genotypes(self): **NOTE** Can only be set by the BinaryMap object. """ try: - return self._complete_genotypes + return self.complete_data.genotypes except AttributeError: raise AttributeError("Looks like a BinaryMap has not been built " "yet for this map. Do this before asking for " @@ -292,82 +128,22 @@ def complete_genotypes(self): @property def phenotypes(self): """Get the phenotypes of the system. """ - return self._phenotypes + return self.data.phenotypes @property def stdeviations(self): """Get stdeviations""" - return self._stdeviations + return self.data.stdeviations @property def n_replicates(self): """Return the number of replicate measurements made of the phenotype""" - return self._n_replicates + return self.data.n_replicates @property def index(self): """Return numpy array of genotypes position. """ - return self.genotypes.index - - # ---------------------------------------------------------- - # Setter methods - # ---------------------------------------------------------- - - @genotypes.setter - def genotypes(self, genotypes): - """Set genotypes from ordered list of sequences. """ - self._n = len(genotypes) - self._length = len(genotypes[0]) - self._genotypes = pd.Series(genotypes) - - @wildtype.setter - def wildtype(self, wildtype): - """Set the reference genotype among the mutants in the system. """ - self._wildtype = wildtype - - @mutations.setter - def mutations(self, mutations): - """ Set the mutation alphabet for all sites in wildtype genotype. - - Examples - -------- - `mutations = { site_number : alphabet }``. If the site - alphabet is note included, the model will assume binary - between wildtype and derived:: - - mutations = { - 0: [alphabet], - 1: [alphabet], - - } - """ - if type(mutations) != dict: - raise TypeError("mutations must be a dict") - # make sure keys are ints - _mutations = {} - for key, val in mutations.items(): - _mutations[int(key)] = val - self._mutations = _mutations - - @phenotypes.setter - def phenotypes(self, phenotypes): - """Set phenotypes from ordered list of phenotypes.""" - self._phenotypes = pd.Series(phenotypes, index=self.index) - - @stdeviations.setter - def stdeviations(self, stdeviations): - """set stdeviations to array. If a single value, it is assumed that - all stdeviations are equal.""" - self._stdeviations = pd.Series(stdeviations, index=self.index) - - @n_replicates.setter - def n_replicates(self, n_replicates): - """Set the number of replicate measurements taken of phenotypes""" - self._n_replicates = pd.Series(n_replicates, index=self.index) - - # ------------------------------------------------------------ - # Hidden methods for mapping object - # ------------------------------------------------------------ + return self.data.index def _add_error(self): """Store error maps""" @@ -379,129 +155,3 @@ def add_binary(self, wildtype): the encoding pattern. Wildtype sites are represented as 0's. """ self.binary = binary.BinaryMap(self, wildtype) - - # ------------------------------------------------------------ - # Hidden methods for mapping object - # ------------------------------------------------------------ - - def sample(self, n_samples=1, genotypes=None, fraction=1.0, derived=True): - """Generate artificial data sampled from phenotype and percent error. - - Parameters - ---------- - n_samples : int - Number of samples to take from space - - fraction : float - fraction of space to sample. - - Returns - ------- - samples : Sample object - returns this object with all stats on experiment - """ - if genotypes is None: - # make sure fraction is float between 0 and 1 - if fraction < 0 or fraction > 1: - raise Exception("fraction is invalid.") - # fractional length of space. - frac_length = int(fraction * self.n) - # random genotypes and phenotypes to sample - random_indices = np.sort(np.random.choice(range(self.n), - size=frac_length, - replace=False)) - # If sample must include derived, set the last random_indice to - # self.n-1 - if derived: - random_indices[-1] = self.n-1 - else: - # Mapping from genotypes to indices - mapping = self.map("genotypes", "indices") - # Construct an array of genotype indices to sample - random_indices = [mapping[g] for g in genotypes] - - # initialize arrays - phenotypes = np.empty((len(random_indices), n_samples), dtype=float) - genotypes = np.empty((len(random_indices), n_samples), - dtype=' Date: Wed, 6 Dec 2017 22:25:37 -0800 Subject: [PATCH 2/7] cleaning last commit --- gpmap/binary.py | 50 ++++++++++++++++++++++++++++++++++++++++++------- gpmap/gpm.py | 41 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 83 insertions(+), 8 deletions(-) diff --git a/gpmap/binary.py b/gpmap/binary.py index 07416f3..a817dcd 100644 --- a/gpmap/binary.py +++ b/gpmap/binary.py @@ -18,7 +18,43 @@ class BinaryMap(object): - """ + """Object that contains the binary representation of a + genotype-phenotype map. + + Parameters + ---------- + GPM : GenotypePhenotypeMap object + The genotype phenotype map object to translate as Binary. + + wildtype : str + genotype used as reference for the binary map. + + Attributes + ---------- + length : int + length of the binary sequences + + genotypes : np.array + array of binary genotype strings, ordered the same as input from GPM. + + missing_genotypes : np.array + other genotypes possible by mutations, not given in the data. These are + often the genotypes to predict. + + complete_genotypes : np.array + genotypes + missing_genotypes + + phenotypes : np.array + phenotypes given by GPM, in the same order as GPM. + + encoding : dict + mapping dictionary that takes + + n_replicates : int + number of replicates. + + stdeviations : array + standard deviations of genotype phenotype map. """ def __init__(self, gpm, wildtype): self.gpm = gpm @@ -77,12 +113,11 @@ def complete_genotypes(self): return self.gpm.complete_data.binary def _build(self): - """Builds a binary representation of the genotypes in - GenotypePhenotypeMap object. Also enumerates genotypes not seen in - the genotype-phenotype map and exposes two new attributes, - ``missing_genotypes`` and ``complete_genotypes``. + """Main function to build the binary representation of set of + genotypes. Also the full set of genotypes, and creates two new + DataFrames: 'missing_data' and 'complete_data'. - **NOTE**: the ``complete_genotypes`` are sorted in alphabetic order. + Also reindexes `data` based on `complete_data`. """ self.encoding = encode_mutations(self._wildtype, self.gpm.mutations) # Use encoding map to construct binary presentation for any type of @@ -93,6 +128,7 @@ def _build(self): data = {'genotypes': unsorted_genotypes, 'binary': unsorted_binary} + # Store the complete genotype space in a DataFrame. gpm = self.gpm gpm.complete_data = pd.DataFrame(data) gpm.complete_data.sort_values('genotypes', inplace=True) @@ -102,7 +138,7 @@ def _build(self): mapping = dict(zip(gpm.complete_data.genotypes, gpm.complete_data.index)) - # Get index + # Get index of observed genotypes and missing genotypes. observed_index = [mapping[g] for g in gpm.data.genotypes] missing_index = set(gpm.complete_data.index).difference(observed_index) diff --git a/gpmap/gpm.py b/gpmap/gpm.py index cd88c2f..dbfc541 100644 --- a/gpmap/gpm.py +++ b/gpmap/gpm.py @@ -23,7 +23,46 @@ class GenotypePhenotypeMap(mapping.BaseMap): - """ + """Object for containing genotype-phenotype map data. + + Parameters + ---------- + wildtype : string + wildtype sequence. + + genotypes : array-like + list of all genotypes in system. Must be a complete system. + + phenotypes : array-like + List of phenotypes in the same order as genotypes. + + mutations : dict + Dictionary that maps each site indice to their possible substitution + alphabet. + + n_replicates : int + number of replicate measurements comprising the mean phenotypes + + include_binary : bool (default=True) + Construct a binary representation of the space. + + Attributes + ---------- + data : pandas.DataFrame + The core data object. Columns are 'genotypes', 'phenotypes', + 'n_replicates', 'stdeviations', and (option) 'binary'. + + complete_data : pandas.DataFrame (optional, created by BinaryMap) + A dataframe mapping the complete set of genotypes possible, given + the mutations dictionary. Two columns: 'genotypes' and 'binary'. + + missing_data : pandas.DataFrame (optional, created by BinaryMap) + A dataframe containing the set of missing genotypes; complte_data - + data. Two columns: 'genotypes' and 'binary'. + + binary : BinaryMap + object that gives you (the user) access to the binary representation + of the map. """ def __init__(self, wildtype, genotypes, phenotypes, stdeviations=None, From 8e6f7a02152b5b07bb3b22c77713620de185185f Mon Sep 17 00:00:00 2001 From: Zsailer Date: Thu, 7 Dec 2017 10:43:03 -0800 Subject: [PATCH 3/7] clean up simulation module to changes --- gpmap/simulate/base.py | 24 ++++++++------- gpmap/simulate/fuji.py | 34 +++++++++++++-------- gpmap/simulate/hoc.py | 9 ++++-- gpmap/simulate/mask.py | 45 ++++++++++++++-------------- gpmap/simulate/nk.py | 64 +++++++++++++++++++++++----------------- gpmap/simulate/random.py | 12 ++++++-- 6 files changed, 110 insertions(+), 78 deletions(-) diff --git a/gpmap/simulate/base.py b/gpmap/simulate/base.py index 1edadcf..11acc5c 100644 --- a/gpmap/simulate/base.py +++ b/gpmap/simulate/base.py @@ -3,6 +3,7 @@ from gpmap import utils from gpmap.gpm import GenotypePhenotypeMap + def random_mutation_set(length, alphabet_size=2): """Generate a random mutations dictionary for simulations. @@ -27,17 +28,20 @@ def random_mutation_set(length, alphabet_size=2): mutations[i] = alphabet return mutations + class BaseSimulation(GenotypePhenotypeMap): """ Build a simulated GenotypePhenotypeMap. Generates random phenotypes. """ + def __init__(self, wildtype, mutations, *args, **kwargs): # build genotypes genotypes = utils.mutations_to_genotypes(wildtype, mutations) phenotypes = np.empty(len(genotypes), dtype=float) - super(BaseSimulation, self).__init__(wildtype, genotypes, - phenotypes, - *args, - **kwargs) + super(BaseSimulation, self).__init__(wildtype, + genotypes, + phenotypes, + *args, + **kwargs) @classmethod def from_length(cls, length, alphabet_size=2, *args, **kwargs): @@ -58,7 +62,7 @@ def from_length(cls, length, alphabet_size=2, *args, **kwargs): wildtype = "".join([m[0] for m in mutations.values()]) self = cls(wildtype, mutations, *args, **kwargs) return self - + def set_stdeviations(self, sigma): """Add standard deviations to the simulated phenotypes, which can then be used for sampling error in the genotype-phenotype map. @@ -66,13 +70,13 @@ def set_stdeviations(self, sigma): Parameters ---------- sigma : float or array-like - Adds standard deviations to the phenotypes. If float, all phenotypes - are given the same stdeviations. Else, array must be same length as - phenotypes and will be assigned to each phenotype. + Adds standard deviations to the phenotypes. If float, all + phenotypes are given the same stdeviations. Else, array must be + same length as phenotypes and will be assigned to each phenotype. """ stdeviations = np.ones(len(self.phenotypes)) * sigma - self.stdeviations = stdeviations + self.data.stdeviations = stdeviations return self - + def build(self): raise Exception("must be implemented in subclass.") diff --git a/gpmap/simulate/fuji.py b/gpmap/simulate/fuji.py index 364243b..2094b36 100644 --- a/gpmap/simulate/fuji.py +++ b/gpmap/simulate/fuji.py @@ -3,21 +3,24 @@ from gpmap import utils from .base import random_mutation_set, BaseSimulation + class MountFujiSimulation(BaseSimulation): """Constructs a genotype-phenotype map from a Mount Fuji model. [1]_ - A Mount Fuji sets a "global" fitness peak (max) on a single genotype in the space. - The fitness goes down as a function of hamming distance away from this genotype, - called a "fitness field". The strength (or scale) of this field is linear and - depends on the parameters `field_strength`. Roughness can be added to the Mount - Fuji model using a random `roughness` parameter. This assigns a random + A Mount Fuji sets a "global" fitness peak (max) on a single genotype in + the space. The fitness goes down as a function of hamming distance away + from this genotype, called a "fitness field". The strength (or scale) of + this field is linear and depends on the parameters `field_strength`. + Roughness can be added to the Mount Fuji model using a random + `roughness` parameter. This assigns a random .. math:: f(g) = \\nu (g) - c \cdot d(g_0, g) - where $\\nu$ is the roughness parameter, $c$ is the field strength, and $d$ is the - hamming distance between genotype $g$ and the reference genotype. + where $\\nu$ is the roughness parameter, $c$ is the field strength, + and $d$ is the hamming distance between genotype $g$ and the + reference genotype. Parameters ---------- @@ -34,12 +37,16 @@ class MountFujiSimulation(BaseSimulation): References ---------- - _ [1] Szendro, Ivan G., et al. "Quantitative analyses of empirical fitness landscapes." - Journal of Statistical Mechanics: Theory and Experiment 2013.01 (2013): P01005. + _ [1] Szendro, Ivan G., et al. "Quantitative analyses of empirical fitness + landscapes." Journal of Statistical Mechanics: Theory and Experiment + 2013.01 (2013): P01005. """ - def __init__(self, wildtype, mutations, field_strength=1, roughness=None, *args, **kwargs): + + def __init__(self, wildtype, mutations, field_strength=1, roughness=None, + *args, **kwargs): # Call parent class. - super(MountFujiSimulation, self).__init__(wildtype, mutations, *args, **kwargs) + super(MountFujiSimulation, self).__init__(wildtype, mutations, + *args, **kwargs) # Set the field strength and roughness self._field_strength = field_strength self.set_roughness(roughness) @@ -52,7 +59,7 @@ def hamming(self): return self._hamming # calculate the hamming distance if not done already except AttributeError: - hd = np.empty(self.n,dtype=int) + hd = np.empty(self.n, dtype=int) for i, g in enumerate(self.genotypes): hd[i] = utils.hamming_distance(self.wildtype, g) self._hamming = hd @@ -92,5 +99,6 @@ def set_roughness(self, range=None): else: if type(range) is not tuple: Exception("range must be a tuple pair") - self._roughness = np.random.uniform(range[0], range[1], size=self.n) + self._roughness = np.random.uniform(range[0], range[1], + size=self.n) self.build() diff --git a/gpmap/simulate/hoc.py b/gpmap/simulate/hoc.py index e23b045..b7d20b3 100644 --- a/gpmap/simulate/hoc.py +++ b/gpmap/simulate/hoc.py @@ -1,12 +1,15 @@ from .nk import NKSimulation from .base import random_mutation_set + class HouseOfCardsSimulation(NKSimulation): """Construct a 'House of Cards' fitness landscape. """ - def __init__(self, wildtype, mutations, k_range=(0,1), *args, **kwargs): - super(NKSimulation, self).__init__(wildtype, mutations, *args, **kwargs) + + def __init__(self, wildtype, mutations, k_range=(0, 1), *args, **kwargs): + super(NKSimulation, self).__init__(wildtype, mutations, *args, + **kwargs) # Set parameters self.set_order(self.binary.length) - self.set_random_values(k_range=k_range) + self.set_random_values(k_range=k_range) self.build() diff --git a/gpmap/simulate/mask.py b/gpmap/simulate/mask.py index 6167e89..e031c46 100644 --- a/gpmap/simulate/mask.py +++ b/gpmap/simulate/mask.py @@ -2,45 +2,46 @@ import pandas as pd from .. import GenotypePhenotypeMap + def mask(gpm, mask_fraction): """Create a new GenotypePhenotypeMap object from a random subset of another - GenotypePhenotypeMap. - - - Returns + GenotypePhenotypeMap. + + + Returns ------- true_mask_fraction : float - the actual fraction used, since the space is discrete and likely won't be - the exact fraction given. - GenotypePhenotypeMap : + the actual fraction used, since the space is discrete and likely + won't be the exact fraction given. + GenotypePhenotypeMap : the new genotype-phenotype map. """ if mask_fraction > 1 or mask_fraction < 0: raise Exception("mask_fraction must between between 0 and 1.") - + # Calculate the number of genotypes to select - number_to_choose = int((1-mask_fraction) * gpm.n) - + number_to_choose = int((1 - mask_fraction) * gpm.n) + # Calculate the true fraction (since this is a discrete space.) - true_mask_fraction = 1 - float(number_to_choose)/gpm.n - + true_mask_fraction = 1 - float(number_to_choose) / gpm.n + # Randomly choose genotypes index = np.random.choice(gpm.index, number_to_choose, replace=False) - + # Check n_replicates datatype if type(gpm.n_replicates) == int: n_replicates = gpm.n_replicates - elif type(gpm.n_replicates) == pd.Series or type(gpm.n_replicates) == np.ndarray: + elif type(gpm.n_replicates) == pd.Series or + type(gpm.n_replicates) == np.ndarray: n_replicates = gpm.n_replicates[index].reset_index(drop=True) else: raise Exception("n_replicates are not a valid dtype.") - + # return Subset genotype return true_mask_fraction, GenotypePhenotypeMap( - gpm.wildtype, - gpm.genotypes[index].reset_index(drop=True), - gpm.phenotypes[index].reset_index(drop=True), - mutations=gpm.mutations, - stdeviations=gpm.stdeviations[index].reset_index(drop=True), - n_replicates=n_replicates) - + gpm.wildtype, + gpm.genotypes[index].reset_index(drop=True), + gpm.phenotypes[index].reset_index(drop=True), + mutations=gpm.mutations, + stdeviations=gpm.stdeviations[index].reset_index(drop=True), + n_replicates=n_replicates) diff --git a/gpmap/simulate/nk.py b/gpmap/simulate/nk.py index 5fcf53d..dc427b6 100644 --- a/gpmap/simulate/nk.py +++ b/gpmap/simulate/nk.py @@ -5,18 +5,21 @@ from gpmap import utils from .base import random_mutation_set, BaseSimulation + class NKSimulation(BaseSimulation): - """Generate genotype-phenotype map from NK fitness model. Creates a table with - binary sub-sequences that determine the order of epistasis in the model. + """Generate genotype-phenotype map from NK fitness model. Creates a table + with binary sub-sequences that determine the order of epistasis in the + model. The NK fitness landscape is created using a table with binary, length-K, - sub-sequences mapped to random values. All genotypes are binary with length N. - The fitness of a genotype is constructed by summing the values of all - sub-sequences that make up the genotype using a sliding window across the full genotype + sub-sequences mapped to random values. All genotypes are binary with + length N. The fitness of a genotype is constructed by summing the values + of all sub-sequences that make up the genotype using a sliding window + across the full genotype. - For example, imagine an NK simulation with N=5 and K=2. To construct the fitness - for the 01011 genotype, select the following sub-sequences from an NK table: - "01", "10", "01", "11", "10". Sum their values together. + For example, imagine an NK simulation with N=5 and K=2. To construct + the fitness for the 01011 genotype, select the following sub-sequences + from an NK table "01", "10", "01", "11", "10". Sum their values together. Parameters ---------- @@ -31,11 +34,14 @@ class NKSimulation(BaseSimulation): values : array array of values in the NK table. """ - def __init__(self, wildtype, mutations, K, k_range=(0,1), *args, **kwargs): - super(NKSimulation, self).__init__(wildtype, mutations, *args, **kwargs) + + def __init__(self, wildtype, mutations, K, k_range=(0, 1), + *args, **kwargs): + super(NKSimulation, self).__init__(wildtype, mutations, + *args, **kwargs) # Set parameters self.set_order(K) - self.set_random_values(k_range=k_range) + self.set_random_values(k_range=k_range) self.build() @property @@ -61,25 +67,28 @@ def set_order(self, K): self.K = K # point to order self.order = self.K - self._keys = np.array(["".join(r) for r in it.product('01', repeat=self.K)]) + self._keys = np.array(["".join(r) for r in + it.product('01', repeat=self.K)]) # Reset phenotypes - self.phenotypes = np.empty(self.n, dtype=float) + self.data['phenotypes'] = np.empty(self.n, dtype=float) - def set_random_values(self, k_range=(0,1)): - """Set the values of the NK table by drawing from a uniform distribution - between the given k_range. + def set_random_values(self, k_range=(0, 1)): + """Set the values of the NK table by drawing from a uniform + distribution between the given k_range. """ if hasattr(self, "keys") is False: - raise Exception("Need to set K first. Try `set_order` method." ) - self._values = np.random.uniform(k_range[0], k_range[1], size=len(self.keys)) + raise Exception("Need to set K first. Try `set_order` method.") + self._values = np.random.uniform(k_range[0], k_range[1], + size=len(self.keys)) self.build() def set_table_values(self, values): """Set the values of the NK table from a list/array of values. """ if len(values) != len(self.keys): - raise Exception("Length of the values do not equal the length of NK keys. " - "Length of keys is : %d" % (len(self.keys),)) + raise Exception("Length of the values do not equal the length of " + "NK keys. " + "Length of keys is : %d" % (len(self.keys),)) self._values = values self.build() @@ -88,8 +97,8 @@ def build(self): """ nk_table = self.nk_table # Check for even interaction - neighbor = int(self.order/2) - if self.order%2 == 0: + neighbor = int(self.order / 2) + if self.order % 2 == 0: pre_neighbor = neighbor - 1 else: pre_neighbor = neighbor @@ -98,16 +107,17 @@ def build(self): for i in range(len(self.genotypes)): f_total = 0 for j in range(self.length): - if j-pre_neighbor < 0: + if j - pre_neighbor < 0: pre = self.genotypes[i][-pre_neighbor:] - post = self.genotypes[i][j:neighbor+j+1] + post = self.genotypes[i][j:neighbor + j + 1] f = "".join(pre) + "".join(post) - elif j+neighbor > self.length-1: - pre = self.genotypes[i][j-pre_neighbor:j+1] + elif j + neighbor > self.length - 1: + pre = self.genotypes[i][j - pre_neighbor:j + 1] post = self.genotypes[i][0:neighbor] f = "".join(pre) + "".join(post) else: - f = "".join(self.genotypes[i][j-pre_neighbor:j+neighbor+1]) + f = "".join( + self.genotypes[i][j - pre_neighbor:j + neighbor + 1]) f_total += nk_table[f] phenotypes[i] = f_total self.phenotypes = phenotypes diff --git a/gpmap/simulate/random.py b/gpmap/simulate/random.py index 1a3f87c..4f3de1d 100644 --- a/gpmap/simulate/random.py +++ b/gpmap/simulate/random.py @@ -1,16 +1,22 @@ import numpy as np from .base import BaseSimulation + class RandomPhenotypesSimulation(BaseSimulation): """ Build a simulated GenotypePhenotypeMap. Generates random phenotypes. """ - def __init__(self, wildtype, mutations, phenotype_range=(0,1), *args, **kwargs): + + def __init__(self, wildtype, mutations, phenotype_range=(0, 1), + *args, **kwargs): # build genotypes - super(RandomPhenotypesSimulation, self).__init__(wildtype, mutations, *args, **kwargs) + super(RandomPhenotypesSimulation, self).__init__(wildtype, + mutations, + *args, **kwargs) self.phenotype_range = phenotype_range self.build() def build(self): """Build phenotypes""" low, high = self.phenotype_range[0], self.phenotype_range[1] - self.phenotypes = np.random.uniform(low, high, size=len(self.genotypes)) + self.data['phenotypes'] = np.random.uniform(low, high, + size=len(self.genotypes)) From 505e4e9f23bc2a17626e26337232c8329888edd3 Mon Sep 17 00:00:00 2001 From: Zsailer Date: Thu, 7 Dec 2017 10:46:42 -0800 Subject: [PATCH 4/7] important typo in data attribute --- gpmap/gpm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gpmap/gpm.py b/gpmap/gpm.py index dbfc541..e869c05 100644 --- a/gpmap/gpm.py +++ b/gpmap/gpm.py @@ -88,7 +88,7 @@ def __init__(self, wildtype, genotypes, phenotypes, # Store data in DataFrame data = dict( genotypes=genotypes, - phenotype=phenotypes, + phenotypes=phenotypes, n_replicates=n_replicates, stdeviations=stdeviations ) From dfe3f2046b7e033117b2efcb249a3ad622310994 Mon Sep 17 00:00:00 2001 From: Zsailer Date: Thu, 7 Dec 2017 13:58:57 -0800 Subject: [PATCH 5/7] default all attributes to nunmpy arrays --- README.md | 11 ++++++----- gpmap/binary.py | 10 +++++----- gpmap/gpm.py | 14 +++++++------- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 9ca2214..8a1e25c 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ -# Python API for analyzing and manipulating genotype-phenotype maps +# GPMap +*A Python API for managing genotype-phenotype map dat* [![Join the chat at https://gitter.im/harmslab/gpmap](https://badges.gitter.im/harmslab/gpmap.svg)](https://gitter.im/harmslab/gpmap?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Documentation Status](https://readthedocs.org/projects/gpmap/badge/?version=latest)](http://gpmap.readthedocs.io/en/latest/?badge=latest) -This package defines a standard data-structure for genotype-phenotype (GP) map data. -Subset, manipulate, extend, etc. GP maps. Calculate statistics, model evolutionary -trajectories, predict phenotypes (in combination with the epistasis package). Efficient memory usage, -using numpy arrays to store in memory. +Defines a flexible object for managing genotype-phenotype (GP) map data. At it's core, +`gpmap` stores the data in Pandas DataFrames and thus, interacts seamlessly with the +PyData egosystem. @@ -38,3 +38,4 @@ pip install -e . The following modules are required. Also, the examples/tutorials are written in Jupyter notebooks and require IPython to be install. * [Numpy](http://www.numpy.org/) +* [Pandas](https://pandas.pydata.org/) diff --git a/gpmap/binary.py b/gpmap/binary.py index a817dcd..a5ae505 100644 --- a/gpmap/binary.py +++ b/gpmap/binary.py @@ -76,12 +76,12 @@ def wildtype(self, wildtype): @property def n_replicates(self): """Get number of replicates""" - return self.gpm.data.n_replicates + return self.gpm.data.n_replicates.values @property def stdeviations(self): """Get standard deviations""" - return self.gpm.data.stdeviations + return self.gpm.data.stdeviations.values @property def length(self): @@ -91,17 +91,17 @@ def length(self): @property def genotypes(self): """Get Binary representation of genotypes. """ - return self.gpm.data.binary + return self.gpm.data.binary.values @property def phenotypes(self): """Get phenotypes of the map.""" - return self.gpm.phenotypes + return self.gpm.data.phenotypes.values @property def missing_genotypes(self): """Binary genotypes missing in the dataset """ - return self.gpm.missing_data.binary + return self.gpm.missing_data.binary.values @property def complete_genotypes(self): diff --git a/gpmap/gpm.py b/gpmap/gpm.py index e869c05..19fe38b 100644 --- a/gpmap/gpm.py +++ b/gpmap/gpm.py @@ -142,13 +142,13 @@ def mutations(self): @property def genotypes(self): """Get the genotypes of the system.""" - return self.data.genotypes + return self.data.genotypes.values @property def missing_genotypes(self): """Genotypes that are missing from the complete genotype-to-phenotype map.""" - return self.missing_data.genotypes + return self.missing_data.genotypes.values @property def complete_genotypes(self): @@ -158,7 +158,7 @@ def complete_genotypes(self): **NOTE** Can only be set by the BinaryMap object. """ try: - return self.complete_data.genotypes + return self.complete_data.genotypes.values except AttributeError: raise AttributeError("Looks like a BinaryMap has not been built " "yet for this map. Do this before asking for " @@ -167,22 +167,22 @@ def complete_genotypes(self): @property def phenotypes(self): """Get the phenotypes of the system. """ - return self.data.phenotypes + return self.data.phenotypes.values @property def stdeviations(self): """Get stdeviations""" - return self.data.stdeviations + return self.data.stdeviations.values @property def n_replicates(self): """Return the number of replicate measurements made of the phenotype""" - return self.data.n_replicates + return self.data.n_replicates.values @property def index(self): """Return numpy array of genotypes position. """ - return self.data.index + return self.data.index.values def _add_error(self): """Store error maps""" From d0bb535f0b66dd8029abcd07a0dd3121301e22ec Mon Sep 17 00:00:00 2001 From: Zsailer Date: Thu, 7 Dec 2017 14:03:13 -0800 Subject: [PATCH 6/7] add info about gmap --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8a1e25c..69b7a5d 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,17 @@ # GPMap -*A Python API for managing genotype-phenotype map dat* - [![Join the chat at https://gitter.im/harmslab/gpmap](https://badges.gitter.im/harmslab/gpmap.svg)](https://gitter.im/harmslab/gpmap?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Documentation Status](https://readthedocs.org/projects/gpmap/badge/?version=latest)](http://gpmap.readthedocs.io/en/latest/?badge=latest) -Defines a flexible object for managing genotype-phenotype (GP) map data. At it's core, -`gpmap` stores the data in Pandas DataFrames and thus, interacts seamlessly with the +*A Python API for managing genotype-phenotype map data* + +GPMap defines a flexible object for managing genotype-phenotype (GP) map data. At it's core, +it stores all data in Pandas DataFrames and thus, interacts seamlessly with the PyData egosystem. +To visualize genotype-phenotype objects created by GPMap, checkout [GPGraph](https://github.com/Zsailer/gpgraph). + ## Basic example From 84b936df08984749a8bf7f110b3a99ccce085a4e Mon Sep 17 00:00:00 2001 From: Zsailer Date: Thu, 7 Dec 2017 14:10:13 -0800 Subject: [PATCH 7/7] add image --- README.md | 25 +++++++++++++++++++++++-- docs/_img/dataframe.png | Bin 0 -> 46060 bytes 2 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 docs/_img/dataframe.png diff --git a/README.md b/README.md index 69b7a5d..07df992 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ PyData egosystem. To visualize genotype-phenotype objects created by GPMap, checkout [GPGraph](https://github.com/Zsailer/gpgraph). - + ## Basic example @@ -21,7 +21,28 @@ Import the package's base object. from gpmap import GenotypePhenotypeMap ``` -Load a dataset from disk. +Pass your data to the object. +```python + +# Data +wildtype = "AAA" +genotypes = ["AAA", "AAT", "ATA", "TAA", "ATT", "TAT", "TTA", "TTT"] +phenotypes = [0.1, 0.2, 0.2, 0.6, 0.4, 0.6, 1.0, 1.1] +stdeviations = [0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05] + +# Initialize the object +gpm = GenotypePhenotypeMap(wildtype, + genotypes, + phenotypes, + stdeviations=stdeviations) + +# Check out the data. +gpm.data +``` + + + +Or load a dataset from disk. ```python gpm = GenotypePhenotypeMap.read_json("data.json") ``` diff --git a/docs/_img/dataframe.png b/docs/_img/dataframe.png new file mode 100644 index 0000000000000000000000000000000000000000..d4b143b2266aadbf8b187269b23bcd1e14a2342a GIT binary patch literal 46060 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 psbXlnU7D3l_s$Rh|3W8!6Zl}hF{Vh4nje6FN^