{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "#Chemical similarity search in MongoDB\n", "\n", "### myChEMBL team, ChEMBL group, EMBL-EBI.\n", "\n", "##What is this?\n", "\n", "This IPython Notebook is intended to provide an overview of how to perform time efficient chemical searches in MongoDB. It is based on the [blog post](http://blog.matt-swain.com/post/87093745652/chemical-similarity-search-in-mongodb) by Matt Swain." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Some necessary imports first. Here we import some Python core libraries, IPython utilities and plotting helpers." ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Populating the interactive namespace from numpy and matplotlib\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ ":0: FutureWarning: IPython widgets are experimental and may change in the future.\n" ] } ], "source": [ "%matplotlib inline\n", "%pylab inline\n", "from IPython.display import Image\n", "from IPython.html.widgets import FloatProgress\n", "from IPython.display import display\n", "import matplotlib.pyplot as plt\n", "import pandas as pd\n", "from scipy.interpolate import interp1d\n", "import random\n", "import time\n", "import sys\n", "import os\n", "from itertools import groupby" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We are using RDKit for all chemoinformatics calculations:" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "collapsed": true }, "outputs": [], "source": [ "from rdkit import Chem\n", "from rdkit.Chem import AllChem" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "For connecting to ChEMBL database we will use Django ORM, described in [notebook 08](https://github.com/chembl/mychembl/blob/master/ipython_notebooks/08_myChEMBL_Django_ORM.ipynb). We have to import ChEMBL Django model as well:" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "collapsed": true }, "outputs": [], "source": [ "sys.path.append('/home/chembl/ipynb_workbench')\n", "os.environ.setdefault(\"DJANGO_SETTINGS_MODULE\", \"settings\")\n", "import chembl_migration_model\n", "from chembl_migration_model.models import *\n", "from chembl_core_model.models import CompoundMols" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "For connecting to mongoDB we will use pymongo." ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "collapsed": true }, "outputs": [], "source": [ "import pymongo\n", "from bson.binary import Binary\n", "from bson.objectid import ObjectId\n", "from pymongo import MongoClient" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here we establish connection with mongoDB and create a new database called 'similarity':" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "collapsed": true }, "outputs": [], "source": [ "client = MongoClient()\n", "db = client.similarity" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Populating the MongoDB database\n", "\n", "Our first task will be to retrieve all compounds from ChEMBL and insert them to MongoDB collection. For each compound in ChEMBL we will select its CHEMBL ID and SMILES string, we will use Django ORM to retrieve this data like this:" ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "collapsed": true }, "outputs": [], "source": [ "smiles = MoleculeDictionary.objects.values_list('compoundstructures__canonical_smiles', 'chembl_id')\n", "smi_count = smiles.count()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If the molecules are already loaded we won't do anything. Otherwise we will iterate over all compounds in ChEMBL and use RDKit to create a binary object (we will use it later to compute fingerprints) and SMILES (RDKit uses different algorithm for generating SMILES than the one used at ChEMBL). There are some compounds in ChEMBL db, which current can't be handled by RDKit, this is why we are using `try`/`except` blocks. It is not good practice to catch all exceptions (having single `except` keyword is called pockemon exception handling) but this is not mission critical code. So for each compound we will store a document containing SMILES, RDKit object and CHEMBL ID." ] }, { "cell_type": "code", "execution_count": 13, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "populating mongodb collection with compounds from chembl...\n", "1455713 molecules loaded successfully\n" ] } ], "source": [ "if 'molecules' not in db.collection_names():\n", " print 'populating mongodb collection with compounds from chembl...'\n", " sys.stdout.flush()\n", " molecules = db.molecules\n", " percentile = int(smi_count / 100)\n", " pbar = FloatProgress(min=0, max=smi_count)\n", " display(pbar)\n", " chunk_size = 100\n", " chunk = []\n", " for i, item in enumerate(smiles):\n", " if not (i % percentile):\n", " pbar.value = (i + 1)\n", " try:\n", " rdmol = Chem.MolFromSmiles(item[0])\n", " except:\n", " continue\n", " if not rdmol:\n", " continue\n", " mol_data = {\n", " 'smiles': Chem.MolToSmiles(rdmol, isomericSmiles=True),\n", " 'chembl_id': item[1],\n", " 'rdmol': Binary(rdmol.ToBinary()),\n", " }\n", " chunk.append(mol_data)\n", " if len(chunk) == chunk_size:\n", " molecules.insert_many(chunk)\n", " chunk = []\n", " molecules.insert_many(chunk)\n", " chunk = [] \n", " pbar.value = smi_count\n", " print '%s molecules loaded successfully' % molecules.count()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Chemical similarity basics\n", "\n", "In practice, efficient calculation of the [similarity](https://en.wikipedia.org/wiki/Chemical_similarity) of any two molecules is achieved using molecular fingerprints that encode structural information about the molecule as a series of bits (0 or 1). Typically these bits represent the presence or absence of particular patterns or substructures — two molecules that contain more of the same patterns will have more bits in common, indicating that they are more similar. The most commonly used measure for quantifying the similarity of two fingerprints is the [Tanimoto (a.k.a. Jaccard) coefficient](https://en.wikipedia.org/wiki/Jaccard_index), given by:\n", "\n", "$$T = \\frac{N_{ab}}{N_{a} + N_{b} - N_{ab}}$$\n", "\n", "where $N_{a}$ and $N_{b}$ are the number of ‘on’ fingerprint bits (i.e corresponding to the presence of a pattern) in the fingerprints of molecule $a$ and molecule $b$ respectively. $N_{ab}$ is the number of fingerprint bits common to the fingerprints of both molecule $a$ and $b$. The Tanimoto coefficient ranges from 0 when the fingerprints have no bits in common, to 1 when the fingerprints are identical." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##Generating fingerprints\n", "\n", "[Morgan (a.k.a. circular) fingerprints](http://www.rdkit.org/docs/GettingStartedInPython.html#morgan-fingerprints-circular-fingerprints) are an [ECFP](http://pubs.acs.org/doi/abs/10.1021/ci100050t)-like family of fingerprints that RDKit can generate. Instead of being based on bits that correspond to a fixed set of predefined substructural keys, ECFPs consist of integer codes that are generated using a hashing procedure from the local environments around each atom in multiple circular layers up to a given radius. Given a radius (e.g. 2), RDKit will return a Morgan fingerprint as a count vector that consists of a list of unique integers codes, each with a count that corresponds to the number of times the corresponding substructure occurs in the molecule:\n", "\n", " mfp = AllChem.GetMorganFingerprint(rdmol, 2)\n", "\n", "While this is the most information-rich representation of this fingerprint, there are two things to note:\n", "\n", "Firstly, each fingerprint consists of typically less than 100 integer codes that are sparsely mapped over a vast space of $2^{32}$ possible integers. Storing these in the database as explicit bit vectors would be hugely inefficient and impractical (even impossible?), so RDKit also has the ability to fold the fingerprints down to a fixed length bit vector. Fortunately, for our purposes it is actually preferable to store the fingerprints in MongoDB as an array of just the ‘on’ bit positions, negating most of the problems with sparse fingerprints and making it possible to use unfolded fingerprints. Nevertheless, it is worth investigating the impact that folding has on both query performance and quality of the query results.\n", "\n", "Secondly, it’s worth considering how best to use the count information associated with each integer code. For now, we have just discarded this information and treated the fingerprint as a traditional on/off bit vector. However, it shouldn’t be too difficult to retain and use this information when calculating similarities. We expect this could improve the quality of the query results, but could potentially have a negative impact on query performance.\n", "\n", "It's important to pre-calculate the fingerprint for every molecule in the database so it doesn’t have to be redundantly calculated every time a search is performed, we can append fingerprint information to each document in the `molecules` collection:" ] }, { "cell_type": "code", "execution_count": 18, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "precalculating fingerprints...\n", "...done\n" ] } ], "source": [ "print 'precalculating fingerprints...'\n", "sys.stdout.flush()\n", "mol_count = molecules.count()\n", "percentile = int(mol_count / 100)\n", "pbar = FloatProgress(min=0, max=mol_count)\n", "display(pbar)\n", "for i, molecule in enumerate(db.molecules.find()):\n", " if not (i % percentile):\n", " pbar.value = (i + 1)\n", " rdmol = Chem.Mol(molecule['rdmol'])\n", " mfp = list(AllChem.GetMorganFingerprintAsBitVect(rdmol, 2, nBits=2048).GetOnBits())\n", " db.molecules.update_one({'_id': molecule['_id']}, {\"$set\":{\"mfp\":{'bits': mfp, 'count': len(mfp)}}} )\n", "pbar.value = mol_count \n", "print '...done'" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Figure below shows the distribution of the number of ‘on’ bits in the fingerprint for each molecule calculated for the entire ChEMBL database. Folded fingerprints show similar distributions, but with slightly lower mean values due to collisions where two or more different integer codes get folded onto the same bit position in the smaller fixed range." ] }, { "cell_type": "code", "execution_count": 84, "metadata": { "collapsed": false }, "outputs": [ { "data": { "image/png": "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\n62DPn7eqekFVPWeZYx5TVT+zXm0CgI0g6ACAxfbdJE+vqvsM+72G117xtapq20EcfmaSX+nup+yn\n7lVJlvzhfCnd/dTu/laSeyc56yDPfX53f/YgTnnc5LR+XHdftILzD0oNZjz20O7+w+5+2zKHHp/k\nZ1ffOgBYXIIOAFhsu5P8UZKX7luxb4+Mqrp1+L69qj5UVe+uqi9U1Sur6jlVtauqPl1VD526zE9V\n1RVV9bmqeupw/qFV9e+H4/+yqn516rofqar/O8ln9tOeZw/Xv7KqXjmU/VaSJyV5c1W9at9zuvu/\nJbl1mfegkxxWVf+1qq6uqjfsCQCq6rohBHplkmOq6pNVdV5V3a+qPjzsX1lVP76f9u6sqsftee+q\n6t9V1aeq6i+q6of2OfaHkrwtyeOHHh0PneX8qjqmqi4f3pd/V1Xfnrrmv5p6j3cMZUcPfxbnJ7ky\nyVHDtV9dVX819Ci571T7X1NVVyR5SVXtqKrfmKp7ZVV9dLjej1fVnZK8Isnpw/vyzGXedwDYlAQd\nALD4Xp/kF6rqsH3K9+2RMb3/6CQvSPLITHpMHNPdJyZ5Y5IXD8dUkod09+OTPDXJf6yqu2TSA+Mb\nw/EnJnl+VR09nHN8krO7+xHTN66qB2QSNvxkksdmEgic2t2vSPKxJD/f3f96JS9+aOeJSV6U5Lgk\nxyT5X6decyd5WZIvdPfx3f2yJD+f5JLuPn54Lz61n+tOv193S/IX3f3YJB9O8vwfOLD7K0l+JclH\nhh4dX5zx/NcmeU13PzrJ9d9/QVUnJXnY8B4fn+SEqvqJofphSf6v7v6x7v7b4dpXdPePJflQkj1D\nlzrJnbr78d396qn3Yk/dod39hCS/nuTl3b07yb9N8qfD+/Sf9/OeAMCmJ+gAgAXX3d9OckGSsw/i\ntCu6++bu/m6Sa5O8byj/qyRH77l0kouGe1yb5ItJfiTJSUn+eVV9MsnlSY7I5IfvJNnV3X+zn/s9\nPskHu/uW7v5ekj9J8k+n6mcagrGEXd19XXffnuTCJPv20Nj3+lck+eWazGfy6O5ertfId7v7vcP2\nx7P3PVrqHrOc/8QkewKFC6eOPynJScN7/PEkj8je9/hvunvX1LG3J3nHsP2f8oOv/R05sD8bvn9i\nqj21zOsAgE1P0AEAm8N/yKSnxd2nym7L8FleVYckufNU3Xemtm+f2r89yVLza+zpEfCi4bf+x3f3\nMd39/qH875c4b/oH6MoP9nhYaj6QH6irqhOHoRWfrKqn5Qd7Kuy59u1LXC/d/ZEkP5HkxiRvXW6S\nzkyGCO2x3Hu0Vuf/7tR7/PDufstQfqD3OLnj+7rUsXv+zL83Y3sAYBQEHQCwCXT31zPpfXFm9v6g\ne12SE4btU5Lc6SAvW0meOcx5eUyShya5OpPeH2ftmXC0qh5eVXdb5lpXJHlyVd2nqg5NckYmwyxm\nbcf3dfeuqQDgPUP9icP8FYckOT3J/7vPNb6d5J7fv2DVg5N8tbvfmMlwneNnbMtauzzJzw3bZ0yV\nvy/J86rq7klSVQ+sqn98gGsckmTPfBo/n+QjS9xvud4a38rU+wQAYyToAIDFNv3b+99Pct+p/T/O\nJFz4VCZDJG49wHn7Xm96Hoe/TbIrycVJXjAMdXljkquSfKKqrkzyhkx6BOzbs2LvRbu/nOScJB/M\nZD6Mjw0hxZKq6iOZBDhPqarrq+qnD9DmK5L8wdCuL3T3u6ZfZ3ffkuT/GyYefVWS7Uk+VVWfSPKs\nTObKWMq+vU/29zoP+PqXOP/Xk/yL4c/omCTfHNp7WZK3J/mLqvp0Ju/BPfZzrWTSa+PE4c9ieyYT\nis7yOvZX/sEkx5mMFIAxq+61XKUOAIA9ququ3f0/hu0zkpze3U8/yGt8u7v1wgCAGRmvCQAwPydU\n1R9kMqTk60met4Jr+K0UABwEPToAAACA0TBHBwAAADAagg4AAABgNAQdAAAAwGgIOgAAAIDREHQA\nAAAAoyHoAAAAAEbj/wc5BtjxadGjdQAAAABJRU5ErkJggg==\n", "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "response = db.molecules.aggregate([{'$group': {'_id': '$mfp.count', 'total': {'$sum': 1}}}])\n", "data = pd.DataFrame().from_dict([r for r in response])\n", "fig = plt.figure()\n", "fig.set_size_inches(18, 6)\n", "plt.ylabel('Number of molecules')\n", "plt.xlabel('Number of 1-bits in fingerprint')\n", "h = plt.hist(data._id, weights=data.total, histtype='bar', bins=105, rwidth=2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "For each molecule in our collection we will also store the total number of bits in the fingerprint (i.e. $N_{b}$, needed to calculate the Tanimoto coefficient). The code below produces a collection called `mfp_counts` with documents that have a count field and an `_id` that corresponds to the fingerprint bit." ] }, { "cell_type": "code", "execution_count": 85, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "computing fingerprint bit counts...\n", "... done\n" ] } ], "source": [ "print 'computing fingerprint bit counts...'\n", "sys.stdout.flush()\n", "counts = {}\n", "mol_count = molecules.count()\n", "percentile = int(mol_count / 100)\n", "pbar = FloatProgress(min=0, max=mol_count)\n", "display(pbar)\n", "for molecule in db.molecules.find():\n", " if not (i % percentile):\n", " pbar.value = (i + 1)\n", " for bit in molecule['mfp']['bits']:\n", " counts[bit] = counts.get(bit, 0) + 1\n", "\n", "counts_it = counts.items()\n", "chunk_size = 100\n", "for chunk in [counts_it[i:i + chunk_size] for i in range(0, len(counts_it), chunk_size)]:\n", " db.mfp_counts.insert_many([{'_id': k, 'count': v} for k,v in chunk])\n", "pbar.value = mol_count \n", "print '... done' " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "MongoDB query performance can be further improved by adding an index on the specific fields that will be queried, in this case the fingerprint bits and counts:" ] }, { "cell_type": "code", "execution_count": 20, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "u'mfp.count_1'" ] }, "execution_count": 20, "metadata": {}, "output_type": "execute_result" } ], "source": [ "db.molecules.create_index('mfp.bits')\n", "db.molecules.create_index('mfp.count')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "There are two further efficiency tricks that work by placing constraints on easily searchable properties of fingerprints to filter out the most obviously unsuitable molecules before going through the expensive process of calculating the exact Tanimoto coefficient:\n", "\n", "1. Given a query fingerprint with $N_{a}$ bits, any molecule in the database must have $N_{b}$ bits within the range $TN_{a} \\leqslant N_{b} \\leqslant \\frac{N_{a}}{T}$ to have a similarity above a threshold $T$. This is because if the number of bits in each fingerprint differ by too much, it would be impossible for the similarity to be above the threshold even in the most ideal case where one fingerprint is an exact subset of the other.\n", "\n", "2. Out of any $N_{a} - TN_{a} + 1$ bits chosen at random from the query fingerprint $a$, at least one must be present in any other fingerprint $b$ to achieve a similarity above the threshold $T$. This is because even if all the remaining other $TN_{a} - 1$ bits are in common, it still would not be enough for the similarity to be above the threshold (as shown by the previous constraint).\n", "\n", "For the second constraint, the bits chosen from the query fingerprint don’t actually need to be random — in fact, we can optimise even further by ensuring we choose the rarest fingerprint bits, thus restricting the query set as much as possible." ] }, { "cell_type": "code", "execution_count": 21, "metadata": { "collapsed": false }, "outputs": [], "source": [ "def similarity_client(smiles, threshold=0.8):\n", " \"\"\"Perform a similarity search on the client, with initial screening to improve performance.\"\"\"\n", " if not smiles:\n", " return\n", " mol = Chem.MolFromSmiles(smiles)\n", " if not mol:\n", " return\n", " qfp = list(AllChem.GetMorganFingerprintAsBitVect(mol, 2, nBits=2048).GetOnBits())\n", " qn = len(qfp) # Number of bits in query fingerprint\n", " qmin = int(ceil(qn * threshold)) # Minimum number of bits in results fingerprints\n", " qmax = int(qn / threshold) # Maximum number of bits in results fingerprints\n", " ncommon = qn - qmin + 1 # Number of fingerprint bits in which at least one must be in common\n", " # Get list of bits where at least one must be in result fp. Use least popular bits if possible.\n", " if db.mfp_counts:\n", " reqbits = [count['_id'] for count in db.mfp_counts.find({'_id': {'$in': qfp}}).sort('count', 1).limit(ncommon)]\n", " else:\n", " reqbits = qfp[:ncommon]\n", " results = []\n", " for fp in db.molecules.find({'mfp.bits': {'$in': reqbits}, 'mfp.count': {'$gte': qmin, '$lte': qmax}}):\n", " intersection = len(set(qfp) & set(fp['mfp']['bits']))\n", " pn = fp['mfp']['count']\n", " tanimoto = float(intersection) / (pn + qn - intersection)\n", " if tanimoto >= threshold:\n", " results.append((tanimoto, fp['chembl_id'], fp['smiles']))\n", " return results" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Just checking how it works for aspirin..." ] }, { "cell_type": "code", "execution_count": 22, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "[(1.0, u'CHEMBL25', u'CC(=O)Oc1ccccc1C(=O)O'),\n", " (0.8571428571428571, u'CHEMBL350343', u'CC(=O)Oc1ccccc1C(=O)Oc1ccccc1C(=O)O')]" ] }, "execution_count": 22, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# aspirin\n", "similarity_client('O=C(Oc1ccccc1C(=O)O)C')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## The MongoDB aggregation framework\n", "\n", "One issue with the query method outlined above is that most of the time-consuming work is performed by the client rather than the database server. The server performs the initial filtering stages and then just sends thousands of molecules over to the client to calculate the exact similarities and produce the actual results. This makes the overall performance mostly dependent on the speed of the client or application server; it also has the potential for a network bottleneck when transferring such a huge amount of data for each query.\n", "\n", "The solution to this problem is to use the MongoDB aggregation framework, which allows a pipeline of tasks to be performed on the MongoDB server. This way, the MongoDB server does all the work and only the actual query results are sent to the client.\n", "\n", "In his [blog post at Datablend](http://datablend.be/?p=265), Davy proposed an aggregation pipeline for similarity searching that Rajarshi found to be disappointingly slow in [his benchmarks](http://blog.rguha.net/?p=1261). However, in the time since Davy wrote his blog post, MongoDB version 2.6 introduced some new aggregation features that may have better performance. Of particular interest is the `$setIntersection` operator, which is exactly what we need to calculate the number of bits in common between two fingerprints.\n", "\n", "Here’s a similarity search aggregation pipeline that uses these new features: " ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def similarity_search_fp(smiles, threshold=0.8):\n", " \"\"\"Perform a similarity search using aggregation framework.\"\"\"\n", " if not smiles:\n", " return\n", " mol = Chem.MolFromSmiles(smiles)\n", " if not mol:\n", " return\n", " qfp = list(AllChem.GetMorganFingerprintAsBitVect(mol, 2, nBits=2048).GetOnBits()) \n", " qn = len(qfp) # Number of bits in query fingerprint\n", " qmin = int(ceil(qn * threshold)) # Minimum number of bits in results fingerprints\n", " qmax = int(qn / threshold) # Maximum number of bits in results fingerprints\n", " ncommon = qn - qmin + 1 # Number of fingerprint bits in which at least 1 must be in common\n", " if db.mfp_counts:\n", " reqbits = [count['_id'] for count in db.mfp_counts.find({'_id': {'$in': qfp}}).sort('count', 1).limit(ncommon)]\n", " else:\n", " reqbits = qfp[:ncommon]\n", " aggregate = [\n", " {'$match': {'mfp.count': {'$gte': qmin, '$lte': qmax}, 'mfp.bits': {'$in': reqbits}}},\n", " {'$project': {\n", " 'tanimoto': {'$let': {\n", " 'vars': {'common': {'$size': {'$setIntersection': ['$mfp.bits', qfp]}}},\n", " 'in': {'$divide': ['$$common', {'$subtract': [{'$add': [qn, '$mfp.count']}, '$$common']}]}\n", " }},\n", " 'smiles': 1,\n", " 'chembl_id': 1\n", " }},\n", " {'$match': {'tanimoto': {'$gte': threshold}}}\n", " ]\n", " response = db.molecules.aggregate(aggregate)\n", " return [(r['tanimoto'], r['smiles'], r['chembl_id']) for r in response]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Checking improved function with aspirin:" ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "[(1.0, u'CC(=O)Oc1ccccc1C(=O)O', u'CHEMBL25'),\n", " (0.8571428571428571, u'CC(=O)Oc1ccccc1C(=O)Oc1ccccc1C(=O)O', u'CHEMBL350343')]" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "similarity_search_fp('O=C(Oc1ccccc1C(=O)O)C')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Benchmarks\n", "\n", "In order to perform benchmarks we will select 1000 random compound SMILES from ChEMBL. Note, that because in MongoDB collection we stored RDKit-generated SMILES, in most cases result SMILES will differ. For each discrete value of threshold, we will perform the same benchmark 5 times and save mean value." ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "collapsed": false }, "outputs": [], "source": [ "sample_size = 1000\n", "rand_smpl = [ smiles[i][0] for i in sorted(random.sample(xrange((smiles.count())), sample_size)) ]\n", "repetitions = 5" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We are now ready to run actual benchmarks. This can take several hours to compute, depending on hardware." ] }, { "cell_type": "code", "execution_count": 42, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "measuring performance for similarity 0.7\n", "measuring performance for similarity 0.75\n", "measuring performance for similarity 0.8\n", "measuring performance for similarity 0.85\n", "measuring performance for similarity 0.9\n", "measuring performance for similarity 0.95\n" ] } ], "source": [ "timings = []\n", "for thresh in [0.7, 0.75, 0.8, 0.85, 0.9, 0.95]:\n", " print 'measuring performance for similarity {0}'.format(thresh)\n", " sys.stdout.flush()\n", " rep_times = []\n", " for i in range(repetitions):\n", " start = time.time()\n", " for sample in rand_smpl:\n", " _ = similarity_search_fp(sample, thresh)\n", " stop = time.time()\n", " rep_times.append(stop-start)\n", " timings.append((thresh, np.mean(rep_times)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Once this is complete, we can take a look at results:" ] }, { "cell_type": "code", "execution_count": 14, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[(0.7, 2959.67804813385), (0.75, 2161.8768181800842), (0.8, 1224.4110579490662), (0.85, 625.453076839447), (0.9, 298.62185406684875), (0.95, 74.62168312072754)]\n" ] } ], "source": [ "print timings" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Plotting them will give us a better idea about time complexity:" ] }, { "cell_type": "code", "execution_count": 87, "metadata": { "collapsed": false }, "outputs": [ { "data": { "image/png": "iVBORw0KGgoAAAANSUhEUgAAAZIAAAF/CAYAAABjWE+mAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJzt3Xm8VWP///HXp3MaJSHKUCKh0OBQhnAiZbiVIUMyR7mp\nE77GuiNTPzLcFJlSMt9mGjThVNzI3RxNIlQ0mEp0mj6/P/Y6bDnVPmcPa+993s/H4zxa+9pr7fVe\nbc6na11rXcvcHRERkbKqEHYAERHJbCokIiISFxUSERGJiwqJiIjERYVERETiokIiIiJxSVohMbMq\nZvaJmU03s8/N7P8F7TuZ2Tgzm29mY82sZtQ2N5vZAjOba2Zto9rzzGxW8N5DycosIiKll7RC4u5r\ngdbu3gxoArQ2s1bATcA4d98PeDd4jZk1Bs4BGgMnAoPMzIKPexTo4u4NgYZmdmKycouISOkk9dSW\nu/8WLFYCcoCfgPbAsKB9GHBasNwBeNHd17v7IuALoKWZ7QZs7+6Tg/WeidpGRERCltRCYmYVzGw6\nsAx4390/A2q7+7JglWVA7WB5d2Bx1OaLgT1KaF8StIuISBrITeaHu/smoJmZ7QCMMbPWm73vZqY5\nWkREMlhSC0kxd//FzEYCecAyM6vj7t8Hp62WB6stAepGbbYnkZ7IkmA5un3J5vtQQRIRKRt3t22v\ntWXJvGqrVvEVWWZWFTgBmAa8DVwUrHYR8Gaw/DZwrplVMrO9gYbAZHf/HlhlZi2DwfcLorb5C3fP\nup8RIybQtm1v9trrWNq27c2IERNCz5SMn1tvvTX0DDo+HVt5PL5ESGaPZDdgmJlVIFKwnnX3d81s\nGvCymXUBFgFnA7j752b2MvA5sAG40v88yiuBp4GqwCh3H53E3Glj5MiJ9Ow5hoUL7wL68vXXfVm4\nsDcAp5xyTLjhREQCSSsk7j4LOKSE9h+BNlvYph/Qr4T2KcDBic6Y7gYMGBspIhU2gG8Ah4UL72Lg\nwD4qJCKSNnRnexorKgrqfIuBcMwksE0ArF2bE2Kq5MjPzw87QlJl8/Fl87FB9h9fIqiQpLHKlTdE\nFj69EvbZBMdFTmtVqbIxxFTJke3/s2bz8WXzsUH2H18iqJCksYKCtjRo0Bs2Vob/vAGNX2WXdifT\no8cJYUcTEflDSi7/lbIpHgcZOLAPa9fmsPGbE5h99EtUabQ+5GQiIn+yRF3+FTYz82w5lq2ZsGgC\nZ71yFoUXF9J4l8ZhxxGRDGdmeLreRyLJcWz9Y7mv7X2c8sIpLPt12bY3EBFJMhWSDHRh0wu5oMkF\ndHipA7+v/z3sOCJSzunUVoZydzq/3pkNmzbwUseXqGD6N4GIlJ5ObZVjZsaQDkNYunopfQv7hh1H\nRMoxFZIMViW3Cq+d/RpDpg1h5PyRYccRkXJKhSTD1a5em/90/A+Xvn0pX/70ZdhxRKQcUiHJAkfV\nO4perXrR8eWOGnwXkZTTYHuWcHc6vdaJ7Spux1Mdngo7johkCA22yx/MjMHtB/PR4o94aqoKiYik\njnokWWbuyrkcPfRoxpw/hkN2+9ss/iIif6EeifzNAbUOYNDJg+j4ckd+/P3HsOOISDmgHkmWunbM\ntcz7YR7DOw3XzYoiskXqkcgW3dPmHlYVreKeD+4JO4qIZDn1SLLYt798y6FPHsqITiM4bI/Dwo4j\nImlIPRLZqro71GXgSQPp/Hpnfl33a9hxRCRLqUdSDlz85sVUyqnEE6c+EXYUEUkz6pFITAacNIB3\nv3qXN+e+GXYUEclCKiTlQI3KNXj29Ge5YsQVLF29NOw4IpJlVEjKiSPrHkm3vG5c8tYlbPJNYccR\nkSyiQlKO9Dm2D6uKVjHwk4FhRxGRLKLB9nJm4Y8LOfypw3nvwvc4uPbBYccRkZBpsF1KrcFODejf\npj/nvX4eazesDTuOiGQBFZJy6OJmF7P/zvvT570+YUcRkSygU1vl1Io1Kzj40YMZ3mm47noXKcd0\nakvKbJftduGBdg9w6duXsm7jurDjiEgGUyEpxzod1In6NevTb1K/sKOISAbTqa1ybsmqJTR7vJmu\n4hIpp3RqS+K2R4096HdcPy59+1I2bNoQdhwRyUAqJMJlh1zG9pW258GPHww7iohkIJ3aEiByo2LL\nwS35qMtHNNy5YdhxRCRFdGpLEqbBTg3ofXRvLht+mebiEpFSUSGRPxS0LGDdxnU8/r/Hw44iIhlE\np7bkLz5f8TnHDD2Gqd2mUm+HemHHEZEk06ktSbjGuzSmoGUBBe8UhB1FRDKECon8zQ1H3cBnKz5j\n5PyRYUcRkQygQiJ/UyW3CgNPGkjB6AJ+X/972HFEJM2pkEiJTtz3RJrVaUb/D/uHHUVE0pwG22WL\nvvnlG5o/3pzJl02mwU4Nwo4jIkmgwXZJqno71OP6I6+nYHQBKtIisiUqJLJV1x5xLQt/XMjb894O\nO4qIpCkVEtmqSjmVePjkh+k5uie/rf8t7DgikoZUSGSb2uzThpZ7ttRzS0SkREkrJGZW18zeN7PP\nzGy2mRUE7X3NbLGZTQt+Tora5mYzW2Bmc82sbVR7npnNCt57KFmZZcseaPsAj/3vMeb/MD/sKCKS\nZpJ21ZaZ1QHquPt0M6sOTAFOA84GVrv7A5ut3xh4ATgM2AMYDzR0dzezyUB3d59sZqOAAe4+erPt\nddVWkt333/sY9+U4RncejVlcF3mISJpI66u23P17d58eLP8KzCFSIABKCt0BeNHd17v7IuALoKWZ\n7QZs7+6Tg/WeIVKQJMV6tuzJklVLeH3O62FHEZE0kpIxEjOrDzQHPg6aepjZDDN7ysxqBm27A4uj\nNltMpPBs3r6EPwuSpFDFnIo8eOKD3DD+Boo2FIUdR0TSRNILSXBa61WgZ9AzeRTYG2gGfAfcn+wM\nkjht9mlDo1qNeHjyw2FHEZE0kZvMDzezisBrwHPu/iaAuy+Pen8wMDx4uQSoG7X5nkR6IkuC5ej2\nJSXtr2/fvn8s5+fnk5+fH+8hSAnuPeFejh56NBc1u4ha1WqFHUdESqGwsJDCwsKEfmYyB9sNGAb8\n4O7XRLXv5u7fBcvXAIe5+3lRg+0t+HOwfd9gsP0ToACYDIxEg+2h6z6qO4Yx8OSBYUcRkTgkYrA9\nmYWkFTARmAkU76QX0InIaS0HvgK6ufuyYJtewKXABiKnwsYE7XnA00BVYJS7/+1hGSokqbVizQoa\nD2rMpEsmcUCtA8KOIyJllNaFJNVUSFLvvv/ex8SvJ/J2J02fIpKp0vryX8l+PVr0YPby2bz75bth\nRxGREKmQSJlVzq1M/xP6839j/4+NmzaGHUdEQqJCInE5s9GZVK9UnWEzhoUdRURCojESidvkJZM5\n7aXTmN9jPtUrVQ87joiUgsZIJC202KMFrfdurcfyipRT6pFIQhQ/lnfGFTPYs8ae295ARNKCeiSS\nNurtUI+uh3Slb2HfsKOISIqpRyIJ89PvP7Hfw/vxwSUfsH+t/cOOIyIxUI9E0sqOVXfk2sOv5ZbC\nW8KOIiIppEIiCVXQsoCJX09k6ndTw44iIimiQiIJtV2l7eh9dG96v9c77CgikiIqJJJwXfO6Mnfl\nXCZ+PTHsKCKSAiokknCVcipxW/5t9Hq3F7oAQiT7qZBIUnQ+uDM/rf2JUQtGhR1FRJJMhUSSIqdC\nDne2vpPe7/Vmk28KO46IJJEKiSTNaQecRqWcSrz82cthRxGRJFIhkaQxM/od348+7/dh/cb1YccR\nkSRRIZGkarNPG+rtUI+npz8ddhQRSRJNkSJJ98niT+j4Skfmd59P1YpVw44jIlE0RYpkhJZ7tiRv\ntzwen/J42FFEJAnUI5GUmPbdNE554RQWFixUr0QkjahHIhmj+W7NOWyPwxg8dXDYUUQkwdQjkZSZ\nsnQKHV7qwBcFX1Alt0rYcUQE9Ugkw+TtnkezOs0YMm1I2FFEJIHUI5GUmrxkMh1f7siCHguonFs5\n7Dgi5Z56JJJxWuzRggN3PVD3lYhkEfVIJOU++vYjOr3Wifk95lMpp1LYcUTKNfVIJCMdUfcI9tt5\nP56Z8UzYUUQkAVRIJBS3HHsL/Sb10xxcIllAhURC0apeK/becW+em/lc2FFEJE4qJBKaW465hbsm\n3cWGTRvCjiIicdBgu4Tq4Pubw7S67Lz4ECpX3kBBQVtOOeWYsGOJlBuJGGzPTVQYkdIaOXIiP77e\nmKV5n8ILb4DnsHBhbwAVE5EMolNbEpoBA8ay9L/Pwa+14aCXAFi48C4GDhwXcjIRKQ0VEglNUVEu\nYDCxDxxzF1jk2e5r1+aEG0xESkWFREJTuXIwyL7wBFhXHQ54A4AqVTaGmEpESkuFREJTUNCWBg16\nAwYT+sAxd7JPg5vp0eOEsKOJSClosF1CUzygPnBgH35fW4FpNb7j/Nt20EC7SIbR5b+SNl79/FXu\n++99fNTlI8ziuhpRRGKkubYkq5zR6AxWFa1i/Jfjw44iIqWgQiJpo4JVoNfRvbhz0p1hRxGRUlAh\nkbRy7kHnsmTVEiZ+PTHsKCISIxUSSSu5FXK5udXN3DlRvRKRTKFCImnngqYXMO+HeXyy+JOwo4hI\nDFRIJO1UyqnEjUfdyF2T7go7iojEQIVE0tKlzS9lyndTmP799LCjiMg2qJBIWqqSW4XrjrhOvRKR\nDJC0QmJmdc3sfTP7zMxmm1lB0L6TmY0zs/lmNtbMakZtc7OZLTCzuWbWNqo9z8xmBe89lKzMkl66\n5nVl4tcT+XzF52FHEZGtSGaPZD1wjbsfCBwOXGVmjYCbgHHuvh/wbvAaM2sMnAM0Bk4EBtmftzc/\nCnRx94ZAQzM7MYm5JU1sV2k7rj38Wl3BJZLmklZI3P17d58eLP8KzAH2ANoDw4LVhgGnBcsdgBfd\nfb27LwK+AFqa2W7A9u4+OVjvmahtJMtd1eIq3v3qXeasmBN2FBHZgpSMkZhZfaA58AlQ292XBW8t\nA2oHy7sDi6M2W0yk8GzeviRol3KgeqXqXHP4Ndwx8Y6wo4jIFiS9kJhZdeA1oKe7r45+L5hlUTMt\nylZdddhVjP9yPHNXzg07ioiUIKnTyJtZRSJF5Fl3fzNoXmZmddz9++C01fKgfQlQN2rzPYn0RJYE\ny9HtS0raX9++ff9Yzs/PJz8/PwFHIWHbvvL2f/RKnj/j+bDjiGS0wsJCCgsLE/qZSZtGPhgoHwb8\n4O7XRLX3D9ruMbObgJruflMw2P4C0ILIqavxwL7u7mb2CVAATAZGAgPcffRm+9M08llsddFqGgxo\nwMRLJnJArQPCjiOSNRIxjfxWC4mZ7QqcBRwD1CdyGuprYCLwirsv38q2rYL1ZvLn6aubiRSDl4F6\nwCLgbHf/OdimF3ApsIHIqbAxQXse8DRQFRjl7gUl7E+FJMv1m9SPz1d8znNnPBd2FJGskdRCYmZP\nAQ2Ad4j88v8OMGA3Ir2GE4Ev3P2yeAIkigpJ9ltVtIp9B+zLpEsmsX+t/cOOI5IVkl1Imrj7zG0E\n2OY6qaJCUj70m9SPOSvn8Ozpz4YdRSQrJP3UVgk73AnYM12KRzQVkvKhuFfywaUfsN/O+4UdRyTj\npeRRu2Y2wcxqBEVkCjDYzP4dz05FyqpG5Rr0bNlTd7uLpJFY7iPZwd1XAWcAz7h7C6BNcmOJbFmP\nlj1454t3mP/D/LCjiAixFZKc4H6Ps4lcegu6iVBCpF6JSHqJpZDcDowBFrr7ZDNrACxIbiyRrevR\nQr0SkXSRtBsSU02D7eXPnRPvZO7KubqvRCQOKblqy8z2AXoQuSGxeEoVd/f28ew40VRIyp/VRavZ\nd+C+vH/R+zTepXHYcUQyUqoKyUxgMDAb2BQ0u7tPiGfHiaZCUj71/7A//1v6P14+6+Wwo4hkpFQV\nksnBlVppTYWkfFqzbg37DtyX0Z1H07RO07DjiGScVBWSC4hMlTIGKCpud/ep8ew40VRIyq8HP36Q\nwkWFvHnum9teWUT+IlWF5G7gAiJPLCw+tYW7t45nx4mmQlJ+rd2wln0H7Mub577JobsfGnYckYyS\nqkKyEGjk7uvi2VGyqZCUb4M+HcSI+SMY1XlU2FFEMkpKpkgBZgE7xrMTkWTr0rwLn634jI++/Sjs\nKCLlTiw9kglAE+BT/hwj0eW/knYGTx3MS7NfYvyF48OOIpIxUnVqK7+EZl3+K2ln/cb1NHqkEYPb\nDya/fn7YcUQyQrKfR7LN38yxrJMqaRRFQvTMjGd4cuqTTLx4IpGnPYvI1iR7jKTQzK43s7899MHM\n9jezG4G06pWIdD64MyvWrGDcl+PCjiJSbmytkLQFfgAeMbPvzGy+mS0ws++Ah4FlaDp5STM5FXLo\nm9+XPu/3QT1UkdSIadJGM8sBagUvV7r7xqSmKgOd2pJim3wTTR9rSr/j+nHq/qeGHUckraXq8l/c\nfaO7Lwt+0q6IiESrYBW4s/Wd/Ov9f7HJN217AxGJS0yFRCTTtN+/PVVzq/LS7JfCjiKS9VRIJCuZ\nGf2O78ct79/C+o3rw44jktViKiRmVt/M2gTL1cysRnJjicTvuL2PY+8d92bItCFhRxHJatssJGbW\nFXgFeDxo2hN4I5mhRBKl33H9uGPiHfy+/vewo4hkrVh6JFcBrYBVAO4+H9g1maFEEuWwPQ6jxR4t\neOTTR8KOIpK1YikkRe7+x3NIzCwX0HW2kjHuPO5O+n/Yn1/W/hJ2FJGsFEshmWBmvYFqZnYCkdNc\nw5MbSyRxGu/SmJMbnsz9H90fdhSRrBTLpI05QBcid7pD5EmJg9Pt7j/dkChbs+jnReQ9kcecq+aw\n63Y6MytSLCWz/2YKFRLZlh6jepBbIZd/n/jvsKOIpI1UTSN/KnA7UB/IDZrd3dPqEmAVEtmW73/9\nngMHHci0btOot0O9sOOIpIVUPmr3dGC2e/rON6FCIrHo9W4vlq9ZzuD2g8OOIpIWUjXX1mLgs3Qu\nIiKxuv7I63lr3lvMXTk37CgiWSOWHsnhRE5tvQ+sC5rd3R9IcrZSUY9EYnXPB/cweelkXjv7tbCj\niIQuVT2SO4BfgSpA9eBn+3h2KhKmgpYFTF4ymY++/SjsKCJZIZYeyWx3PyhFecpMPRIpjaHThjJ0\n+lAmXDxBj+SVci1VPZJRZtYunp2IpJsLm17Ij7//yIj5I8KOIpLxYumR/ApUIzI+Ujwfty7/lYw3\nYv4Ibhx/IzOvmElOhZyw44iEIiU9Enev7u4V3L2Ku28f/KRVEREpi1MansLOVXdm2IxhYUcRyWhb\n7JGYWSN3n2Nmh5T0vrtPTWqyUlKPRMri48Ufc9YrZzG/+3yqVqwadhyRlEvqDYlm9qS7X25mhZQw\n26+7t45nx4mmQiJldebLZ9Ji9xbc2OrGsKOIpFyq7myv4u5rt9UWNhUSKat5K+fRamgr5nWfx05V\ndwo7jkhKpeqqrf/G2CaSkfavtT9nNjqTfpP6hR1FJCNt7dTWbsDuwPPAeYAROcVVA3jM3Q9IVchY\nqEci8fhu9Xcc9OhBTO06lb1q7hV2HJGUSfYYyUXAxcChwP+i3loNPO3ur8ez40RTIZF49XmvD9+s\n+oZhp+kqLik/UjVG0tHdX41nJ6mgQiLxWlW0iv0G7seY88fQtE7TsOOIpIQebBVFhUQS4ZHJj/DW\nvLcYc/4YTZ0i5UKqBtvLzMyGmNkyM5sV1dbXzBab2bTg56So9242swVmNtfM2ka155nZrOC9h5KZ\nWcq3rnld+eaXbxizcEzYUUQyRlILCTAUOHGzNgcecPfmwc87AGbWGDgHaBxsM8j+/Cfho0AXd28I\nNDSzzT9TJCEq5lSk/wn9uW7sdWzYtCHsOCIZYZuFxMymmNlVZrZjaT/c3ScBP5X0sSW0dQBedPf1\n7r4I+AJoGVw9tr27Tw7WewY4rbRZRGJ16n6nUqtaLYZOGxp2FJGMEEuP5FxgD+BTM3vJzNpZ/CeP\ne5jZDDN7ysxqBm27E3kaY7HFwX43b18StIskhZlxf9v7ubXwVlYXrQ47jkjay93WCu6+AOhlZv8C\n/gEMATaZ2RDgIXf/sZT7fJTIExch8tCs+4EupfyMEvXt2/eP5fz8fPLz8xPxsVIO5e2ex/H7HE//\nD/tzx3F3hB1HJGEKCwspLCxM6GfGdNWWmTUFLgFOAsYALwCtgPPdvdk2tq0PDHf3g7f2npndBODu\ndwfvjQZuBb4G3nf3RkF7J+BYd79is8/SVVuSUN/88g3NH2/OjCtmsGeNPcOOI5IUKblqy8ymAP8G\nJgNN3L3A3T929/uAr0q7w2DMo9jpQPEVXW8D55pZJTPbG2gITHb374FVZtYyOKV2AfBmafcrUlr1\ndqhHt7xu/Ou9f4UdRSStbbVHYmYVgJvcvUyTEJnZi8CxQC1gGZEeRj7QjMjVW18B3dx9WbB+L+BS\nYAPQ093HBO15wNNAVWCUuxeUsC/1SCThim9SfKfzOzTfrXnYcUQSLlV3tk9x97x4dpIKKiSSLI9+\n+iivznmV8ReM102KknVSdUPiODO7zszqmtlOxT/x7FQkk1yedzlLVy9l1IJRYUcRSUux9EgWUfKD\nrfZOUqYyUY9EkmnE/BHcMO4GZv5zJrkVtnmxo0jG0FxbUVRIJJncnTbPtuGMA87gqhZXhR1HJGFS\nNUayHXAtUC949G5DYH93HxHPjhNNhUSSbeaymbR5pg1zu8/VkxQla6RqjGQosA44Mni9FLgrnp2K\nZKImtZtwRqMzuH3C7dteWaQciaWQNHD3e4gUE9x9TXIjiaSvO1rfwfOznmfuyrlhRxFJG7EUkiIz\nq1r8wswaAEXJiySSvnbZbhdubnUz1465NuwoImkjlkLSFxgN7GlmLwDvATcmM5RIOuveojtf/PgF\n7yx4J+woImkh1rm2agGHBy8/dveVSU1VBhpsl1QaPm84N4y/gZlXzKRiTsWw44iUWarm2jqWyMOm\nVgc/jc3smHh2KpLp/rHfP6hboy6DPh0UdhSR0MVy+e8I/rwhsQrQApji7sclOVupqEciqfbZ8s/I\nH5bP3KvmsnO1ncOOI1ImodyQaGZ1iTyH5Ix4dpxoKiQShu6jugPw8MkPh5xEpGzCKiQGfF78fJB0\noUIiYVj520oaPdKIwosKOXDXA8OOI1JqqbqzfWDUywpEpoD/yt3Pj2fHiaZCImF56OOHGLlgJGPO\nH6PZgSXjpKqQXBz1cgOwyN0/iGenyaBCImFZv3E9TR5rwt3H302HAzqEHUekVDRpYxQVEgnTuIXj\n6DaiG59d+RlVK1bd9gYiaSJVPZJZRK7aKmlH7u5N4gmQKCokErYz/nMGzes0p8+xfcKOIhKzVBWS\ne4kUkmeJFJPOwVuDgu0XxRMgUVRIJGyLfl5E3hN5TO06lb1q7hV2HJGYpKqQTHf3Zpu1TXP3tHqA\ntQqJpIPbCm9j1vJZvHr2q2FHEYlJqqaRNzNrFfXiKEo+zSVS7t1w1A1M+W4K478cH3YUkZSJpUeS\nR+SZJDsETT8Dl7j71CRnKxX1SCRdvDn3TXq924sZV8zQPFyS9lJ61ZaZ1QRw95/j2WGyqJBIunB3\nTnz+RNo1aMe1R2i6eUlvuvw3igqJpJN5K+dx1JCjmH3lbOpUrxN2HJEtUiGJokIi6eaGcTcwdd4s\ncobnUVSUS+XKGygoaMspp2jybEkfiSgkuYkKIyJ/ddjvx/HAt4+ycU4f+PZIABYu7A2gYiJZJdYH\nWx0F1OfPwuPu/kwSc5WaeiSSbtq1+xdjv2sERzwAT04Gzwna+zB69B0hpxOJSNWDrZ4D7gWOAg4N\nfg6LZ6ci5UFRUS7MOg/WbweHPvZH+9q1OSGmEkm8WE5t5QGN9c99kdKpXHkDYDDiMbj4WJh7Oqze\nnSpVNoYdTSShYrkhcTawW7KDiGSbgoK2NGjQG1Y0hind4MSradCgFz16nBB2NJGEiqVHsgvwuZlN\nBoqCNnf39smLJZL5igfUBw7sw5p1zpQG4zm/w6EaaJesE8ud7fkltbt7YRLylJkG2yXdjV049o+p\n5qtVrBZ2HBFA95H8hQqJZILzXjuPejvU4+42d4cdRQRI3VVbR5jZp2b2q5mtN7NNZrYqnp2KlFcP\ntHuAIdOGMGvZrLCjiCRMLIPtDwPnAQuAKkAXIs8iEZFSqlO9Dne0voNuI7qxyTeFHUckIWIpJLj7\nAiDH3Te6+1DgxOTGEslel+ddDsDgqYNDTiKSGLEUkjVmVhmYYWb9zexa9DwSkTKrYBV47B+P8a/3\n/sWyX5eFHUckbrEUkguD9boDvwF7AmcmM5RItmtSuwmXNLuEa8dqmnnJfLHOtVUNqOvu85IfqWx0\n1ZZkmjXr1nDwowfz8MkPc3LDk8OOI+VUqq7aag9MA8YEr5ub2dvx7FREYLtK2/FU+6foOrwrP/3+\nU9hxRMoslhsSpwLHAe+7e/Ogbba7H5SCfDFTj0QyVfdR3Vm9bjXDThsWdhQph1LSIwHWl/B4XV23\nKJIgd7e5mw+++YDh84aHHUWkTGIpJJ+ZWWcg18wamtlA4L9JziVSblSvVJ2hHYZyxcgr+PH3H8OO\nI1JqsRSSHsCBRCZsfBFYBVydzFAi5c0xex1Dx0YdKXinIOwoIqWmubZE0sSadWto9ngz+rfpz+mN\nTg87jpQTSZ200cyGA07JNx+m3TTyKiSSDT745gPOfuVsZv5zJrWq1Qo7jpQDyS4kK4DFRE5nfVLc\nHPzp7j4hnh0nmgqJZIv/G/N/LP11KS+e+WLYUaQcSPZVW7sBvYCDgAeBE4AV7l4YaxExsyFmtszM\nZkW17WRm48xsvpmNNbOaUe/dbGYLzGyumbWNas8zs1nBew+V9iBFMsmdx93J1O+m8urnr4YdRSQm\nWywk7r7B3d9x9wuBw4EvgAlm1r0Un1/SBI83AePcfT/g3eA1ZtYYOAdoHGwzyMyKq+SjQBd3bwg0\nNDNNGilZq2rFqjzd4Wm6j+rO979+H3YckW3a6lVbZlbFzM4EngOuAh4C3oj1w919ErD5LbvtgeI7\nr4YBpwXLHYAX3X29uy8iUrhamtluwPbuPjlY75mobUSy0hF1j+DyQy7nojcv0nTzkva2WEjM7Fki\n94s0B25BKrSnAAAW9klEQVR398Pc/Q53XxLnPmu7e/GUp8uA2sHy7kTGZIotBvYooX1J0C6S1W7N\nv5XVRat54KMHwo4islVb65F0BhoCPYH/mtnqqJ+EPCExGB3XCLlICXIr5PLCmS/Q/8P+fLrk07Dj\niGxR7pbecPeYHnpVBsvMrI67fx+ctloetC8B6kattyeRnsiSYDm6vcReUd++ff9Yzs/PJz8/P3Gp\nRUJQv2Z9Hjn5ETq91omp3aZSo3KNsCNJhissLKSwsDChn5n0GxLNrD4w3N0PDl73B35w93vM7Cag\nprvfFAy2vwC0IHLqajywr7u7mX0CFACTgZHAAHcfvdl+dPmvZK2uw7vy2/rfePb0Z/nzGhSR+KVq\n0sYyM7MXiYyz7G9m35rZJcDdwAlmNp/IrMJ3A7j758DLwOfAO8CVUZXhSmAwkefGf7F5ERHJdg+e\n+CBTv5vKszOfDTuKyN9oihSRDDFr2SyOe+Y4Prz0Q/bbeb+w40iWSPseiYgkzsG1D+a2/Ns499Vz\nKdpQFHYckT+okIhkkH8e+k/2qrkXvd7tFXYUkT+okIhkEDPjqfZP8eqcV3l9zuthxxEBVEhEMs5O\nVXfitbNfo9uIbny+4vOw44iokIhkokN3P5R7T7iX0/9zOr+s/SXsOFLO6aotkQzWfVR3vvnlG948\n900qmP5dKKWnq7ZEyrkH2j3AT2t/4o4Jd4QdRcoxFRKRDFYppxKvnPUKg6cNZvi84WHHkXJKhUQk\nw9WpXodXznqFLm93Yf4P88OOI+WQColIFjh8z8O567i7OO2l01hdtDrsOFLOaLBdJIt0G96NFb+t\n4NWzX9Xgu8REg+0i8hcDThrAit9WcPP4m8OOIuWIColIFqmcW5k3z3mTt+a9xSOTHwk7jpQTW3yw\nlYhkpp2r7cyozqNoNaQVdXeoS/v924cdSbKceiQiWWifHffhrXPfosvbXZi8ZHLYcSTLqZCIZKnD\n9jiMoR2GctpLp7Hwx4Vhx5EspkIiksX+sd8/uOXYWzjp+ZNY+dvKsONIltLlvyLlwE3jb2LSN5MY\nf8F4qlasGnYcSSOJuPxXhUSkHNjkmzj/9fMp2ljEyx1fJqdCTtiRJE3oPhIRiUkFq8DQDkNZVbSK\nS966hI2bNoYdSbKIColIOVE5tzJvnfsW3676lm4jurHJN4UdSbKEColIOVKtYjWGdxrO3JVzuWrk\nVeh0sCSCColIOVO9UnVGdR7F9GXT6Tm6p4qJxE2FRKQcqlG5Bu90foePFn/EdWOvUzGRuKiQiJRT\nNavUZMz5Y3hv0Xv0ereXiomUmQqJSDm2U9WdGHfBOEYuGMmthbeqmEiZ6D4SEWH5muW0eaYNx+19\nHA+0e0DPMilHdENiFBUSkfj8vPZn2r/Ynj1r7MnTpz1NpZxKYUeSFNANiSKSMMVjJr9v+J1TXjhF\nj+yVmKmQiMgfqlasyitnvcLeNfem9bDWLF+zPOxIkgFUSETkL3Ir5PL4Px7n5IYnc9SQo/jypy/D\njiRpToVERP7GzLi99e1c3fJqjh56NNO/nx52JEljKiQiskVXtbiKB9s9yAnPnsCbc98MO46kKV21\nJSLbNHnJZDq+3JELmlzA7a1v1zT0WUSX/0ZRIRFJruVrlnPOq+dQOacyz5/xPDtX2znsSJIAKiRR\nVEhEkm/Dpg3cNP4mXp/zOlfv3ouRgxdRVJRL5cobKChoyymnHBN2RCmlRBSS3ESFEZHsl1shl/va\n3kfOsupcM7WATd8/ATPPB2Dhwt4AKiblkAbbRaTUpj+3gU1DJsOxt8FJPSCniIUL72LgwHFhR5MQ\nqJCISKkVFeXC8oPgyU+hxhK4vCXUmsPatRqEL490aktESq1y5Q2RhbU14T+vQd6TcMkxrPi+Be6O\nWVyn3CXDqEciIqVWUNCWBg16B68MpnSl7run89tB8zjrlbP48fcfQ80nqaWrtkSkTEaOnMjAgeNY\nuzaHKlU20qPHCRzfrsUfV3U9d8ZzHLOXBt7TnS7/jaJCIpI+Ri0YRZe3u3BZ88u45dhbqJhTMexI\nsgUqJFFUSETSy/e/fs8lb13CD7/9wHNnPMd+O+8XdiQpgZ5HIiJpq071Oow6bxQXNb2Io4YcxRNT\nntCjfLOUeiQiknRzVszh/DfOZ/ftd+ep9k+x63a7hh1JAuqRiEhGaLRLIz7q8hEH73owTR9ryoj5\nI8KOJAkUWo/EzBYBq4CNwHp3b2FmOwH/AfYCFgFnu/vPwfo3A5cG6xe4+9jNPk89EpEMMOnrSVz4\n5oW03act97e7n+qVqocdqVzL9B6JA/nu3tzdWwRtNwHj3H0/4N3gNWbWGDgHaAycCAwyM/WmRDLQ\n0XsdzfRu01m3aR1NH2vKpK8nhR1J4hT2L+PNq2B7YFiwPAw4LVjuALzo7uvdfRHwBdACEclIO1TZ\ngaEdhvLvdv/mnFfP4fqx17N2w9qwY0kZhd0jGW9m/zOzy4O22u6+LFheBtQOlncHFkdtuxjYIzUx\nRSRZ2u/fnhlXzOCrn78i74k8piydEnYkKYMwC8lR7t4cOAm4ysyOjn4zGPDY2qCHBkREssAu2+3C\nK2e9Qq9WvTjp+ZO4rfA21m9cH3YsKYXQJm109++CP1eY2RtETlUtM7M67v69me0GLA9WXwLUjdp8\nz6DtL/r27fvHcn5+Pvn5+ckJLyIJZWZ0btKZ/Pr5dHm7C0c8dQRDOwzl4NoHhx0t6xQWFlJYWJjQ\nzwzlqi0zqwbkuPtqM9sOGAvcBrQBfnD3e8zsJqCmu98UDLa/QKTY7AGMB/aNvkxLV22JZAd3Z/DU\nwfR6rxc9W/bkxqNu1BQrSZSxU6SY2d7AG8HLXOB5d/9/weW/LwP1+Pvlv72IXP67Aejp7mM2+0wV\nEpEs8s0v39B1eFeWr1nO06c9TZPaTcKOlJUytpAkgwqJSPZxd4ZOH8qN42+kR4se3NzqZvVOEkyF\nJIoKiUj2WrxqMd1GdGPp6qUM7TCUZnWahR0pa6iQRFEhEclu7s6zM5/lurHXcfkhl9Pn2D5Uya0S\ndqyMl+l3touIxMzMuLDphcz850zm/zhfd8WnEfVIRCQjvTHnDXq804NT9zuVe064hxqVa4QdKSOp\nRyIi5dbpjU5n9pWz2egbOXDQgQyfNzzsSOWWeiQikvHe/+p9uo7oyiG7HcKD7R5kt+13CztSxlCP\nREQEaL13a2ZeMZN9au5Dk8ea8Nj/HmOTbwo7VrmhHomIZJXZy2fTbUQ33J3H//G4plnZBvVIREQ2\nc9CuBzHpkklc3Oxijn/meG4afxO/rf8t7FhZTYVERLJOBatA17yuzPznTL755RsOHHQgoxaMCjtW\n1tKpLRHJemMXjuXKkVfSpHYTHjzxQertUC/sSGlDp7ZERGLQtkFbZl85m2Z1mnHI44dw9wd3s27j\nurBjZQ31SESkXPnypy8peKeAhT8tZNDJg2i9d+uwI4VKc21FUSERkVi5O2/Pe5ueo3tyZN0jub/t\n/eX23hOd2hIRKQMzo8MBHfj8qs/Zu+beNHmsCfd+eK9Od5WReiQiUu4t+GEB1469lvk/zOfBdg9y\nUsOTGDlyIgMGjKWoKJfKlTdQUNCWU045JuyoCadTW1FUSEQkXqMWjOLq0VdTc2Mtlg1ryjfTH/3j\nvQYNevPQQ+2yrpiokERRIRGRRFi3cR0HXXYSC3adAVMuh0m9YV11ANq168Po0XeEnDCxNEYiIpJg\nlXIqsftXR8OgWVBjCXTfH5oPAdvI2rU5YcdLS7lhBxARSTeVK2+AX3eDN56BPSZDu2uh5QDW/HBQ\n2NHSknokIiKbKShoS4MGvSMvlrSAIZOoPW8fFh/yPqe+eCpzV84NN2Ca0RiJiEgJRo6cyMCB41i7\nNocqVTbSo8cJtGnXkocnP8zdH97NOQeew63H3sou2+0SdtS4aLA9igqJiKTKyt9WcvuE23lh1gsU\ntCzgmsOvYfvK24cdq0w02C4iEoJa1Wox4KQBfHLZJ8z/YT4NBzbkoY8fomhDUdjRQqEeiYhInGYu\nm0nv93ozc9lM+h7blwuaXkBuhcy4lkmntqKokIhI2D785kNufvdmVvy2gjta38EZjc6ggqX3iR8V\nkigqJCKSDtyd0V+M5pbCW1i7YS23HHMLZzY+M20LigpJFBUSEUkn7s47X7zDbRNuY826NfQ5pg8d\nG3ckp0J63dSoQhJFhURE0pG7M2bhGG6bcBu/rP2FPsf04ewDz06bgqJCEkWFRETSmbsz7stx3Dbh\nNlasWcH1R17PhU0vpHJu5VBzqZBEUSERkUzg7kz8eiL3fHgP07+fTs+WPbni0CvYocoOoeRRIYmi\nQiIimWbmspn0/7A/73zxDl2ad+Hqw69m9+13T2kG3ZAoIpLBmtRuwnNnPMeUrlMo2lDEQYMO4tK3\nLmXG9zPCjlYq6pGIiKSJlb+t5IkpTzDo00Hsu9O+FLQsoP3+7ZN6c6NObUVRIRGRbLF+43pen/M6\nD33yEEtXL+Wqw67iskMuY8eqOyZ8XyokUVRIRCQbfbrkUwZMHsCI+SM4q/FZdMvrRt7ueQn7fBWS\nKCokIpLNvlv9HUOmDeHJqU9Sq1otuuZ1pdNBneKedViFJIoKiYiUBxs3bWTcl+N4fMrjFC4q5OzG\nZ9M1r2uZeykqJFFUSESkvFm6eilDpw3lyalPsmPVHbmo6UV0OqgTtavXjvkzVEiiqJCISHm1yTdR\nuKiQYTOG8dbct2hVrxUXNr2Q9vu3p0pula1uq0ISRYVERAR+Xfcrb8x5g2dmPsOUpVPo2LgjnQ/u\nTKt6rf4yv9fIkRMZMGAsY8fepUJSTIVEROSvFq9azHMzn+Ol2S+xfM1yOjbuyNkHns1PMzdwzdXj\nWLjwLkA9kj+okIiIbNm8lfN45fNXePmzl5n/7VcUTesCn50N3x6lQlJMhUREJDaHnXwV//utNhz4\nMgz6THNtiYhI6ey0cUeYcAsMmp2Qz1MhEREpZwoK2tKgQe+EfV7yZgITEZG0dMopxwAwcGAfxoyJ\n//MyZozEzE4EHgRygMHufs9m72uMRESklMrN80jMLAd4GDgRaAx0MrNG4aZKrcLCwrAjJJWOL3Nl\n87FB9h9fImREIQFaAF+4+yJ3Xw+8BHQIOVNKZft/zDq+zJXNxwbZf3yJkCmFZA/g26jXi4M2EREJ\nWaYUEg1+iIikqYwYbDezw4G+7n5i8PpmYFP0gLuZpf+BiIikoXJxZ7uZ5QLzgOOBpcBkoJO7zwk1\nmIiIZMZ9JO6+wcy6A2OIXP77lIqIiEh6yIgeiYiIpK+MGGw3sxPNbK6ZLTCzG7ewTr6ZTTOz2WZW\nGNW+yMxmBu9NTlnoUtjW8ZnZdUH+aWY2y8w2mFnNWLYNW5zHlg3fXS0zG21m04P/Ni+Oddt0EOfx\nZcP3t6OZvWFmM8zsEzM7MNZt00Gcxxf79+fuaf1D5FTWF0B9oCIwHWi02To1gc+APYPXtaLe+wrY\nKezjiOf4Nlv/H8D4smybSceWLd8d0Bf4f8FyLeAHIqeU0/q7i/f4suj7uxfoEyzvnyn/78V7fKX9\n/jKhRxLLzYjnAa+5+2IAd1+52ftxXZGQZKW92fI84MUybptq8RxbsUz/7r4DagTLNYAf3H1DjNuG\nLZ7jK5bp318j4H0Ad58H1DezXWPcNmxlPb5dot6P6fvLhEISy82IDYGdzOx9M/ufmV0Q9Z4D44P2\ny5OctSxivtnSzKoB7YDXSrttSOI5NsiO7+5J4EAzWwrMAHqWYtuwxXN8kB3f3wzgDAAzawHsBewZ\n47Zhi+f4oBTfXyZctRXL1QAVgUOIXB5cDfjIzD529wVAK3dfGlTZcWY2190nJTFvaZXmaodTgQ/c\n/ecybBuGeI4N4Ch3/y7Dv7tewHR3zzezBkSOo2mScyVKmY/P3VeTHd/f3cBDZjYNmAVMAzbGuG3Y\n4jk+KMXvzkzokSwB6ka9rkukskb7Fhjr7r+7+w/ARKApgLsvDf5cAbxBpLuXTmI5vmLn8tdTP6XZ\nNgzxHBvu/l3wZyZ/d0cCrwC4+0Ii5533D9ZL5+8O4ju+rPj+3H21u1/q7s3d/UJgF2BhLNumgbIe\n35fBe7H/7gx7QCiGAaNcIl9cfaASJQ8YHQCMJzK4VI1IZW0cLG8frLMd8CHQNuxjKu3xBevtQGQg\ns2ppt83QY8uK7w54ALg1WK4d/I+8U7p/dwk4vmz5/nYAKgXLlwNPl+a/7Qw+vlJ9f6EfbIx/IScR\nubP9C+DmoK0b0C1qneuIXLk1CygI2vYJ/vKmA7OLt023nxiP7yLghVi2Taefsh4bsHc2fHdErmQa\nTuRc9CzgvEz57uI5vmz5fw84Inh/LvAqsEOWfX8lHl9p///TDYkiIhKXTBgjERGRNKZCIiIicVEh\nERGRuKiQiIhIXFRIREQkLiokIiISFxUSyQhm1juYpnxGMK31YUH7k2bWqBSfk2dmDwXLF5vZwFLm\niN7+WDM7opTbd4jOa2aFZpZXms+IYR/1zWxWKbd52szOLKE938yGJy6dZKNMmGtLyrngl/UpQHN3\nX29mOwGVAdy9VJMBuvsUYErxy1LmyN1s+9bAauCjUnzM6URu4Ct+wuc2MwT73bCt9eLksWQRKYl6\nJJIJ6gArPTIVNu7+owfzOAX/oj8kWP7VzPoHPZdxZna4mU0ws4VmdmqwTvS/sP+YItvMTjWzj81s\narDtrkF7XzN71sw+AJ4JeiHDzWwvIncIXxNs08rMvjSz3GC7GsHrnKh9HElkcsp7g232Cd46K3io\n0DwzaxWse7GZvW1m7xKZMK+amQ0J1ptqZu2D9Q4M2qYFvbUGwWfmmNkTwd/FGDOrEqzfLDjOGWb2\nugUPEYv++7DIw5DmmNkUIoVPZKtUSCQTjAXqBr9oHzGzY6Lei/5XdDXgXXc/iEhP4XbgOCK/DG/f\nxj4mufvh7n4I8B/ghqj3DgCOd/fzCH7ZuvvXwGPAA+5+iLt/ABQS6TlBZBLK19y9eCZV3P2/wNvA\ndcE2XwZv5bh7S+Bq4Nao/TYHznT31sC/gmNrGRzTvcHU+92Ah9y9OZBHZKI+iDxa4eHg7+JnoPi0\n1TPA9e7elMiUJtH786DgPAH8w93ziBRx9VRkq1RIJO25+xoivyS7AiuA/5jZRSWsus7dxwTLs4D3\ng1/ks4lMXLc1dc1srJnNJDJvW+Pi3QNvu3vRFraLfvDPYOCSYPliYGgM2wC8Hvw5dbOc4/zPafXb\nAjcF032/T+TUXj0ip9V6mdkNQH13Xxus/5W7zwyWpxB5YFENInMpFU8FPgyILspGpGh+5ZGZfAGe\nKyGvyF+okEhGcPdN7j7B3fsC3fnzX9jR1kctbwLWFW/LtscDBwID3L0JkX/lV41677cYM/6XyC/s\nfCK9jM+3tOpmr4uL1MbNcq7ZbL0zPDLdd3N3r+/uc939RSKny34HRplZ680+s/hzc/i7kgrE5tlU\nRGSbVEgk7ZnZfmbWMKqpObAowbupASwNli+O3v1WtlkNbL9Z2zPA88CQrWxTYwvvbc0YoOCPUGbN\ngz/3dvev3H0g8BZwMCWfijJ3XwX8VDwOA1xA5HRcMScyC2z9qPGbTmXIKuWMColkgurA02b2mZnN\nIHL6pW8J623+C9S3sRx9pVJf4BUz+x+R02clrbP56+HA6cFAd/Ev5xeAHfn7s+eLvQRcb2ZTon5Z\nl5R58/3eAVQ0s5lmNhu4LWg/OxhQnwYcSKSQGVv+u7iIyPjKDKAJm40dBafwugIjg8H2ZSV8lshf\naBp5kQQys47Aqe5e0hiOSFbSfSQiCRLc3NgOODnsLCKppB6JiIjERWMkIiISFxUSERGJiwqJiIjE\nRYVERETiokIiIiJxUSEREZG4/H9p202XHmSOAAAAAABJRU5ErkJggg==\n", "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "thresholds = np.array([t[0] for t in reversed(timings)])\n", "times = np.array([t[1] for t in reversed(timings)])\n", "fig = plt.figure()\n", "fig.set_size_inches(6, 6)\n", "xnew = np.linspace(thresholds.min(), thresholds.max(), num=41, endpoint=True)\n", "f2 = interp1d(thresholds, times, kind='quadratic')\n", "plt.plot(thresholds, times, 'o', xnew, f2(xnew), '-')\n", "plt.ylabel('Mean query time (ms)')\n", "plt.xlabel('Similarity threshold')\n", "plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Although this looks better than benchmarks performed by Rajarshi Guha, it's still disappointing. Apart from very high threshold values, all other data points indicate very long query times. The overall shape of the curve indicates polynomial time complexity with respect to decreasing threshold.\n", "\n", "It's worth noting, that the results presented on the chart above are actually higher then original results presented by Matt Swain. This is most probably because Matt [defined functions](https://github.com/mcs07/mongodb-chemistry/blob/master/mchem/profile.py#L26) with a signatures like this:\n", "\n", " profile_similarity(mols, fingerprinter, fp_collection, result_collection, threshold=0.8, count_collection=None)\n", " \n", "So expecting binary RDKit mol object while all similarity functions defined in this notebook have signature like:\n", "\n", " similarity_search_fp(smiles, threshold=0.8)\n", " \n", "So expecting SMILES string as input. This require additional expensive step of parsing SMILES string and converting it to RDKit mol. The second form is much more useful in practical applications." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Introducing Locality-sensitive hashing\n", "\n", "Basically the problem of finding similar compounds is a variation of nearest neighbour search. One of the methods that are helpful in solving this kind of problem is called [Locality-sensitive hashing](https://en.wikipedia.org/wiki/Locality-sensitive_hashing).\n", "\n", "The idea behind LSH is pretty simple - while most hashes we use everyday ([cryptographic hashes](https://en.wikipedia.org/wiki/Cryptographic_hash_function) like [SHA](https://en.wikipedia.org/wiki/Secure_Hash_Algorithm)) generate completely different values for documents even when they differ only slightly, LSH tries to generate similar hash values for similar documents, so to maximize the probability of [hash collision](https://en.wikipedia.org/wiki/Collision_%28computer_science%29).\n", "\n", "Since we are using Tanimoto coefficient for measuring similarity, it makes sense to use min-wise independent permutations locality sensitive hashing scheme. In fact, even the very short [Wikipedia article](https://en.wikipedia.org/wiki/Jaccard_index) about Tanimoto (Jaccard) index, mentions [MinHash](https://en.wikipedia.org/wiki/MinHash) as an algorithm that \"may be used to efficiently compute an accurate estimate of the Jaccard similarity coefficient of pairs of sets\"." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Improving time complexity using MinHash\n", "\n", "In order to define new version of similarity search based on LSH we need to define some functions. We ported them from the code presented in [article by Ciumac Sergiu](http://www.codeproject.com/Articles/206507/Duplicates-detector-via-audio-fingerprinting#worksinspiration). They generate random permutations, reorder fingerprint bits according to permutations and save the position of the first '1' bit in reordered fingerprints. Then compress the list of the '1' position into smaller list by composing one bigger number from several smaller using binary left shift operation." ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "collapsed": false }, "outputs": [], "source": [ "def get_permutations(len_permutations=2048, num_permutations=100):\n", " return map(lambda _: np.random.permutation(2048), range(num_permutations))\n", "\n", "def get_min_hash(mol, permutations):\n", " qfp_bits = [int(n) for n in AllChem.GetMorganFingerprintAsBitVect(mol, 2, nBits=2048)]\n", " min_hash = []\n", " for perm in permutations:\n", " for idx, i in enumerate(perm):\n", " if qfp_bits[i]:\n", " min_hash.append(idx)\n", " break \n", " return min_hash\n", "\n", "def hash_to_buckets(min_hash, num_buckets=25, nBits=2048):\n", " if len(min_hash) % num_buckets:\n", " raise Exception('number of buckets must be divisiable by the hash length')\n", " buckets = []\n", " hash_per_bucket = int(len(min_hash) / num_buckets)\n", " num_bits = (nBits-1).bit_length()\n", " if num_bits * hash_per_bucket > sys.maxint.bit_length():\n", " raise Exception('numbers are too large to produce valid buckets')\n", " for b in range(num_buckets):\n", " buckets.append(reduce(lambda x,y: (x << num_bits) + y, min_hash[b:(b + hash_per_bucket)]))\n", " return buckets\n", " " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can now generate some random permutations and save them to the separate MongoDB collection: " ] }, { "cell_type": "code", "execution_count": 131, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 131, "metadata": {}, "output_type": "execute_result" } ], "source": [ "db.permutations.insert_many([{'_id':i, 'permutation': perm.tolist()} for i, perm in enumerate(get_permutations())])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Having our permutations, we can precompute locality-sensitive hashes for each molecule and add them to molecule document:" ] }, { "cell_type": "code", "execution_count": 248, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "precalculating locality-sensitive hashing groups...\n", "...done\n" ] } ], "source": [ "permutations = [p['permutation'] for p in db.permutations.find()]\n", "print 'precalculating locality-sensitive hashing groups...'\n", "sys.stdout.flush()\n", "mol_count = db.molecules.count()\n", "percentile = int(mol_count / 100)\n", "pbar = FloatProgress(min=0, max=mol_count)\n", "display(pbar)\n", "for i, molecule in enumerate(db.molecules.find()):\n", " if not (i % percentile):\n", " pbar.value = (i + 1)\n", " rdmol = Chem.Mol(molecule['rdmol'])\n", " min_hash = get_min_hash(rdmol, permutations)\n", " hash_groups = hash_to_buckets(min_hash)\n", " db.molecules.update_one({'_id': molecule['_id']}, {\"$set\":{\"lsh\" : hash_groups}} )\n", "pbar.value = mol_count \n", "print '...done'" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we can use generated hashes to create inverted indexes - 25 separate collection storing documents indexed by hash and having a list of documents as a value. It's worth noting that we could generate hashes and store them in inverted indexes in one step, instead of writing uncompressed and compressed hashes to molecules documents. This would certainly reduce the time required to complete all preparations but we would like to investigate further in the future." ] }, { "cell_type": "code", "execution_count": 366, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "constructing hash tables...\n", "...done\n" ] } ], "source": [ "print 'constructing hash maps...'\n", "sys.stdout.flush()\n", "mol_count = db.molecules.count()\n", "percentile = int(mol_count / 100)\n", "pbar = FloatProgress(min=0, max=mol_count)\n", "display(pbar)\n", "for i, molecule in enumerate(db.molecules.find()):\n", " if not (i % percentile):\n", " pbar.value = (i + 1)\n", " hash_groups = molecule[\"lsh\"]\n", " for n_hash, lsh_hash in enumerate(hash_groups):\n", " db['hash_' + str(n_hash)].update_one({'_id':lsh_hash}, {'$push':{'molecules': molecule['_id']}}, True)\n", "pbar.value = mol_count\n", "print '...done'" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "After completing all those preliminary steps we can finally define our new `similarity_search_lsh` function:" ] }, { "cell_type": "code", "execution_count": 13, "metadata": { "collapsed": false }, "outputs": [], "source": [ "permutations = [p['permutation'] for p in db.permutations.find()]\n", "\n", "def similarity_search_lsh(smiles, threshold=0.8):\n", " \"\"\"Perform a similarity search using aggregation framework.\"\"\"\n", " if not smiles:\n", " return\n", " mol = Chem.MolFromSmiles(smiles)\n", " if not mol:\n", " return\n", " fp = AllChem.GetMorganFingerprintAsBitVect(mol, 2, nBits=2048)\n", " qfp = list(fp.GetOnBits())\n", " qfp_bits = [int(n) for n in fp]\n", " min_hash = []\n", " for perm in permutations:\n", " for idx, i in enumerate(perm):\n", " if qfp_bits[i]:\n", " min_hash.append(idx)\n", " break\n", " hash_groups = hash_to_buckets(min_hash) \n", " qn = len(qfp) # Number of bits in query fingerprint\n", " qmin = int(ceil(qn * threshold)) # Minimum number of bits in results fingerprints\n", " qmax = int(qn / threshold) # Maximum number of bits in results fingerprints\n", " ncommon = qn - qmin + 1 # Number of fingerprint bits in which at least 1 must be in common\n", " if db.mfp_counts:\n", " reqbits = [count['_id'] for count in db.mfp_counts.find({'_id': {'$in': qfp}}).sort('count', 1).limit(ncommon)]\n", " else:\n", " reqbits = qfp[:ncommon]\n", " \n", " nested_res = [ list(i)[0]['molecules'] for i in \n", " [db['hash_' + str(i)].find({'_id':h},{'molecules':1}) for i,h in enumerate(hash_groups)]]\n", " \n", " hashed_ids = [ObjectId(x) for x in (set([str(item) for sublist in nested_res for item in sublist]))]\n", " aggregate = [\n", " {'$match': {'_id':{'$in': hashed_ids}, 'mfp.count': {'$gte': qmin, '$lte': qmax}, 'mfp.bits': {'$in': reqbits}}},\n", " {'$project':{ \n", " 'tanimoto': {'$let': {\n", " 'vars': {'common': {'$size': {'$setIntersection': ['$mfp.bits', qfp]}}},\n", " 'in': {'$divide': ['$$common', {'$subtract': [{'$add': [qn, '$mfp.count']}, '$$common']}]}\n", " }},\n", " 'smiles': 1,\n", " 'chembl_id': 1}},\n", " {'$match': {'tanimoto': {'$gte': threshold}}},\n", " ]\n", " response = db.molecules.aggregate(aggregate)\n", " return [(r['tanimoto'], r['smiles'], r['chembl_id']) for r in response]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Traditionally, checking results for aspirin:" ] }, { "cell_type": "code", "execution_count": 14, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "[(1.0, u'CC(=O)Oc1ccccc1C(=O)O', u'CHEMBL25'),\n", " (0.8571428571428571, u'CC(=O)Oc1ccccc1C(=O)Oc1ccccc1C(=O)O', u'CHEMBL350343')]" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "similarity_search_lsh('O=C(Oc1ccccc1C(=O)O)C')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Benchmarks\n", "\n", "In order to check the performance of our `similarity_search_lsh` function, we will use the same set of 1000 compounds we were using to test `similarity_search_fp`." ] }, { "cell_type": "code", "execution_count": 62, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "measuring performance for similarity 0.7\n", "measuring performance for similarity 0.75\n", "measuring performance for similarity 0.8\n", "measuring performance for similarity 0.85\n", "measuring performance for similarity 0.9\n", "measuring performance for similarity 0.95\n" ] } ], "source": [ "timings_lsh = []\n", "for thresh in [0.7, 0.75, 0.8, 0.85, 0.9, 0.95]: #[0.7, 0.75, 0.8, 0.85, 0.9, 0.95]:\n", " print 'measuring performance for similarity {0}'.format(thresh)\n", " sys.stdout.flush()\n", " rep_times = []\n", " for i in range(repetitions):\n", " start = time.time()\n", " for sample in rand_smpl:\n", " _ = similarity_search_lsh(sample, thresh)\n", " stop = time.time()\n", " rep_times.append(stop-start)\n", " timings_lsh.append((thresh, np.mean(rep_times)))" ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "[(0.7, 108.79886355400086),\n", " (0.75, 106.44789280891419),\n", " (0.8, 95.1254898071289),\n", " (0.85, 90.56010603904724),\n", " (0.9, 84.14158177375793),\n", " (0.95, 77.25984625816345)]" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "timings_lsh" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Plotting the results..." ] }, { "cell_type": "code", "execution_count": 24, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[(0.7, 108.79886355400086), (0.75, 106.44789280891419), (0.8, 95.1254898071289), (0.85, 90.56010603904724), (0.9, 84.14158177375793), (0.95, 77.25984625816345)]\n", "[ 77.25984626 84.14158177 90.56010604 95.12548981 106.44789281\n", " 108.79886355]\n" ] }, { "data": { "image/png": "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\nAElFTkSuQmCC\n", "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "thresholds = np.array([t[0] for t in reversed(timings_lsh)])\n", "times = np.array([t[1] for t in reversed(timings_lsh)])\n", "print timings_lsh\n", "print times\n", "fig = plt.figure()\n", "fig.set_size_inches(6, 6)\n", "xnew = np.linspace(thresholds.min(), thresholds.max(), num=41, endpoint=True)\n", "f2 = interp1d(thresholds, times, kind='slinear')\n", "plt.plot(thresholds, times, 'o', xnew, f2(xnew), '-')\n", "plt.ylabel('Median query time (ms)')\n", "plt.xlabel('Similarity threshold')\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This looks way better than the time complexity of `similarity_search_fp` but we will check them both on the same plot later..." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Accuracy\n", "\n", "We can check how accurate is function `similarity_search_lsh` when compared to `similarity_search_fp`. We will check number of discrepancies defined as the size of symmetrical [set difference](https://docs.python.org/2/library/sets.html#set-objects) between results from both functions. We will measure total number of discrepancies over a set of 1000 random compounds as a function of similarity threshold." ] }, { "cell_type": "code", "execution_count": 15, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "measuring discrepancies for similarity 0.7\n", "measuring discrepancies for similarity 0.75\n", "measuring discrepancies for similarity 0.8\n", "measuring discrepancies for similarity 0.85\n", "measuring discrepancies for similarity 0.9\n", "measuring discrepancies for similarity 0.95\n" ] } ], "source": [ "discrepancies = []\n", "discrepancies_details = []\n", "for thresh in [0.7, 0.75, 0.8, 0.85, 0.9, 0.95]: #[0.7, 0.75, 0.8, 0.85, 0.9, 0.95]:\n", " print 'measuring discrepancies for similarity {0}'.format(thresh)\n", " sys.stdout.flush()\n", " rep_discrepancies = []\n", " rep_coherent = []\n", " rep_err_compounds = []\n", " for i in range(1):\n", " dis_sum = 0\n", " sum_sum = 0\n", " err_comp_sum = 0\n", " for sample in rand_smpl:\n", " sim_lsh = similarity_search_lsh(sample, thresh)\n", " sim_fp = similarity_search_fp(sample, thresh)\n", " lsh_res = set([x[2] for x in sim_lsh if (x and len(x) > 2)]) if sim_lsh else set()\n", " reg_res = set([x[2] for x in sim_fp if (x and len(x) > 2)]) if sim_fp else set()\n", " difference = (lsh_res^reg_res)\n", " if len(difference):\n", " err_comp_sum += 1\n", " dis_sum += len(difference)\n", " sum_sum += len(lsh_res|reg_res)\n", " discrepancies_details.append((sample, thresh, difference))\n", " rep_discrepancies.append(dis_sum)\n", " rep_coherent.append(sum_sum)\n", " rep_err_compounds.append(err_comp_sum)\n", " discrepancies.append((thresh, np.mean(rep_discrepancies), np.mean(rep_coherent), np.mean(rep_err_compounds)))" ] }, { "cell_type": "code", "execution_count": 16, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[(0.7, 101.0, 8106.0, 54.0), (0.75, 25.0, 4981.0, 14.0), (0.8, 1.0, 3001.0, 1.0), (0.85, 0.0, 1898.0, 0.0), (0.9, 0.0, 1454.0, 0.0), (0.95, 0.0, 1255.0, 0.0)]\n" ] } ], "source": [ "print discrepancies" ] }, { "cell_type": "code", "execution_count": 17, "metadata": { "collapsed": false }, "outputs": [ { "data": { "image/png": "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\n0rwLXQ/tStcWXTnykCNpWq8pjbIa0aBugwqvfSreW7y/J/T1tq9ZvHHxAQls0YZFNKjbgJ6H9+S0\nDqcxsNNA+rXpp1XCA0m9yrSZZQALge8Dq4BPgOHuPj+qTD4wxt3zzaw/cIe7D6iorpn9FVjv7n81\nsxuAQ9z9t6WOraQjtc7kydOZMGEqO3dmUK/eXq68chBnDD6BOz++k5vfuZlLjrmE60+6nsMbHh52\nqHGxfvt63i56m2lF0ygoKmDV1lWc0v4UBnYcyMntT6bbYd1olNUorjG4O6u3ruaLr76IJLyiaSza\nsIgBbQeQ1zGPgR0H0rd1X+pm1I1rHLVVsiedE4Cb3H1wsP1bAHe/JarMPcA0d58UbC8A8oBO5dUN\nypzm7mvNrBVQ4O4H/ImopCPJZu23a7mp4CYmzZ3ET3r8hGtOuIbcFrlhhxWzyZOnlzkdfNOOTTwz\n/xkem/0Yn6/5nJPbn8zAjgPJ65hHn1Z9asXtIDbv3Mz05dOZtmwaBcsL+HLLlwztNpSRPUdySodT\nqGN1ym1fqihp35QpNyf1lOk2wIqo7ZVA/xjKtAFaV1C3pbuvDV6vBVrWVMAiYWnZqCX3nH0Pf8z7\nI3d+fCcnPnAip7Q/hd+c+BtOaHdC2OFV6D+GDjN3MrN4BJ2W3Mi8HbMYdOQgxvYbS35uPtmZ2eEG\nW4Zm9ZpxTtdzOKfrOQCs2LKCJ+Y8wdjXxrJxx0b61T+Zj+9vyMpP7wMiv8epNN39wO+v+gvWhjll\nOtauRixZ1cr6vKA7oy6NpIyWjVryp+/9iaKrivhep+8x8tmRnPzAyTw3/zl2790ddnhlGj9+CoXL\nxkHO63DupXBta9a2+4ZvP23Gl7/+kqeHPc2Puv2oViacsrRr2o7rT7qemb+YyasjX2XGZ1+y8uQ3\n4IqekdmHzYpSarr7+PFTKFxzNfS9p0Y+L8yeziqgXdR2OyI9lorKtA3K1C1j/6rg9Voza+XuX5nZ\nEcC6sg4+bty4/a/z8vLIy8uregtEQtIwqyFj+o3hF31/wXPzn+PvH/ydn734M87ucjZDuw3ljJwz\nqF+3fqgx7tm3h7eL3mZ2x1fh2nthUw7MuQDeuhm2tqbFaeOSft25ow8/mvaLB7Fs+n9Du/eh52Nw\n2fGwoSuFO5qyfvt6Dm1waNhhHpTtxdu55dFbmL7pbuhzG2ysmeHcMM/pZBKZDHA6sBr4mIonEgwA\n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\ngg==\n", "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "thresholds = np.array([t[0] for t in reversed(discrepancies)])\n", "errors = np.array([t[1] / t[2] for t in reversed(discrepancies)])\n", "fig = plt.figure()\n", "fig.set_size_inches(6, 6)\n", "xnew = np.linspace(thresholds.min(), thresholds.max(), num=41, endpoint=True)\n", "f2 = interp1d(thresholds, errors, kind='quadratic')\n", "plt.plot(thresholds, errors, 'o', xnew, f2(xnew), '-')\n", "plt.ylabel('Percent of discrepancies')\n", "plt.xlabel('Similarity threshold')\n", "plt.show()" ] }, { "cell_type": "code", "execution_count": 19, "metadata": { "collapsed": false }, "outputs": [ { "data": { "image/png": "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\nRK5CYII=\n", "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "thresholds = np.array([t[0] for t in reversed(discrepancies)])\n", "errors = np.array([t[3] / 10.0 for t in reversed(discrepancies)])\n", "fig = plt.figure()\n", "fig.set_size_inches(6, 6)\n", "xnew = np.linspace(thresholds.min(), thresholds.max(), num=41, endpoint=True)\n", "f2 = interp1d(thresholds, errors, kind='quadratic')\n", "plt.plot(thresholds, errors, 'o', xnew, f2(xnew), '-')\n", "plt.ylabel('Percent of discrepancies')\n", "plt.xlabel('Similarity threshold')\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "From threshold 0.8 and more there are almost no discrepancies. The highest number of differences is for 0.7 - there are some discrepancies in results for 16% of compounds." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Comparing with different methods" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We are now ready to compare the time complexity of `similarity_search_lsh` function and `similarity_search_fp` on a single chart." ] }, { "cell_type": "code", "execution_count": 15, "metadata": { "collapsed": false }, "outputs": [ { "data": { "image/png": "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\nv2BdpAkn1t7MI8ecSqfcFmGXBihI9qAgEZFU98qXs/jNguksibTkSF/Dn48YyNA23UKtSUESR0Ei\nIuli1vqvuH72eD4oaUKL0rXcckgnbuh6PJFI9Y9apSCJoyARkXSztmgLN818i39vjpLlpVzapC73\n9vwRDXLqVFsNCpI4ChIRSVcl0VL+MH88969cycZIY/JrbeKBHidzRF6bpO9bQRJHQSIimeCl5TO5\nbeEnfB5pRafoav730B5Jfba8giSOgkREMsnCTau5YdY7jN+RS250Mz9v2Yw7jx5GrazsKt2PgiSO\ngkREMtG24h3cOudtnvh2M9sj9RhWdzsP9hhGh9zmVbJ9BUkcBYmIZLonl0zhzsXzWZbVisN8NX/s\n2oez2nWv1DYVJHEUJCJSU8zZsIIbZ4/nveJGNCrdyDWtW3L7kScf0GkvBUmcTA2SiW9O5NUHXsV2\nGF7bOfuGsznhRyeEXVaVyfTjE0mmrcVF/GbWWzyzbis7rC7D6hXxQAVPe1VFkFRtr41UqYlvTmT0\njaO5aMlFu+c9t+Q5gP3+si0t3Y57KeDBKyYrKxezH/43U1KyBbDgswhmkeBndrnLV4XKHF+6UFBK\nMjXIqcPDfc7hYeDxxVP43yXz6TR1Kt18NX88rC9ntj26WupQiySF3XDKDZwz9pzYm6cvhVo7IauU\nHfW+o36j2riXMGjQRsx+OKroe+/VBwgCASAWBgMHricS+eHfD++/3wj3EsBxjxILnyjHH19U7vYn\nT25CLHiyMcsKfmbTt+/n5W5/9uyTgQiRSC3McohEavHxhJkccuPD4HvezfvKKa/w68faBNusFawT\n+9mixQVxx/S9rVvnlFm2NpFILbKyGiYtCPen3KDs9BwX3H9BRoRJpodkuh5fRU97qUWS4WxH3Hf7\nuz9AaRaUZDOh71juenUkZjlA+UMqHH/8dxXa13HHbarQ8v36fQGU4l6yx6u80AFo1+63RKPFuO8k\nGt2JezFrF6/jkPL++y1ySko2Eo3uJBrdgfuu9XbQosWFP1jc3fnss4uD7e6IW28ngwb98LjcnY8+\naveDkDKrRc+ek38QPO7OwoU/IxLJwWxXEOZglkP79neUu/yaNc/wyYTnuahdP2jzXuy7i0a46KML\neeXBV/f4heTubN06M/i3ywqCOfaqW7dTufWXlGwg1mLMims9RohEapf771/VMr01mc7Hd3Tjg3k3\n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\n", "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "thresholds = np.array([t[0] for t in reversed(timings)])\n", "times = np.array([t[1] for t in reversed(timings)])\n", "fig = plt.figure()\n", "fig.set_size_inches(6, 6)\n", "xnew = np.linspace(thresholds.min(), thresholds.max(), num=41, endpoint=True)\n", "f2 = interp1d(thresholds, times, kind='cubic')\n", "plt.plot(thresholds, times, 'o', xnew, f2(xnew), '-')\n", "\n", "thresholds_lsh = np.array([t[0] for t in reversed(timings_lsh)])\n", "times_lsh = np.array([t[1] for t in reversed(timings_lsh)])\n", "xnew_lsh = np.linspace(thresholds_lsh.min(), thresholds_lsh.max(), num=41, endpoint=True)\n", "f3 = interp1d(thresholds_lsh, times_lsh, kind='cubic')\n", "\n", "_, swain = plt.plot(thresholds, times, 'o', xnew, f2(xnew), '-')\n", "_, lsh = plt.plot(thresholds_lsh, times_lsh, 'o', xnew_lsh, f3(xnew_lsh), '--')\n", "plt.legend([swain, lsh], ['Swain', 'LSH'])\n", "plt.ylabel('Median query time (ms)')\n", "plt.xlabel('Similarity threshold')\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To make this comparison complete, we can include time results for RDKit postgreSQL cartridge as well. Here we will use Django ORM, which will call the cartridge:" ] }, { "cell_type": "code", "execution_count": 67, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "measuring performance for similarity 0.9\n", "measuring performance for similarity 0.95\n" ] } ], "source": [ "repetitions = 1\n", "timings_cartrdge = []\n", "for thresh in [0.7, 0.75, 0.8, 0.85, 0.9, 0.95]:\n", " print 'measuring performance for similarity {0}'.format(thresh)\n", " sys.stdout.flush()\n", " rep_times = []\n", " for i in range(repetitions):\n", " start = time.time()\n", " for sample in rand_smpl:\n", " _ = len(CompoundMols.objects.similar_to(sample, int(thresh * 100)).values_list('molecule_id', 'similarity'))\n", " stop = time.time()\n", " rep_times.append(stop-start)\n", " timings_cartrdge.append((thresh, np.mean(rep_times)))" ] }, { "cell_type": "code", "execution_count": 17, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "[(0.7, 1153.354385995865),\n", " (0.75, 1223.412336397171),\n", " (0.8, 1194.9570102214814),\n", " (0.85, 1228.4131727695465),\n", " (0.9, 1115.4482998847961),\n", " (0.95, 1162.2547509670258)]" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "timings_cartrdge" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can now plot all three curves:" ] }, { "cell_type": "code", "execution_count": 18, "metadata": { "collapsed": false }, "outputs": [ { "data": { "image/png": "iVBORw0KGgoAAAANSUhEUgAAAgIAAAHuCAYAAADtK2SoAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJzs3Xl8FPX9x/HXd3MQAiEJhPsGETkFQQ4FCSCHSkWqCCoI\notV6AG1/Vq3WSr212HoVLxBBhVatt3IJBlCRU5D7UkASbsJNQpL9/v6YBQIGCGR3Z4/38/GYx87O\nzs68s5DMZ7/zne8Yay0iIiISnTxuBxARERH3qBAQERGJYioEREREopgKARERkSimQkBERCSKqRAQ\nERGJYgErBIwxCcaYucaYxcaYFcaYp3zLyxtjphlj1hhjphpjUgq95y/GmLXGmFXGmO6Flrcyxiz1\nvfZCoDKLiIhEm4AVAtbaHKCztbYF0BzobIzpADwATLPWng9M9z3HGNMY6Ac0BnoCo4wxxre5V4Bb\nrbUNgAbGmJ6Byi0iIhJNAnpqwFp7yDcbD8QA2cDVwDjf8nHANb753sBEa22etXYDsA5oa4ypCiRZ\na+f51htf6D0iIiJSAgEtBIwxHmPMYmAb8LW1djlQ2Vq7zbfKNqCyb74asLnQ2zcD1YtYnulbLiIi\nIiUUG8iNW2u9QAtjTDIwxRjT+aTXrTHGL2Mc+2s7IiIi4cRaa8681qkF5aoBa+1e4AugFbDNGFMF\nwNfsv923WiZQs9DbauC0BGT65gsvzzzFfjQVY3rkkUdczxAOkz4nfU76rPQ5hfrkD4G8aiDt6BUB\nxpjSQDfgB+BTYJBvtUHAx775T4H+xph4Y0xdoAEwz1q7FdhnjGnr6zw4sNB7REREpAQC2SJQFZjh\n6yMwF/jMWjsdeBroZoxZA3TxPcdauwJ4D1gBTALussfLnbuA0cBaYJ21dnIAc0eskSOeoWdaPf7z\n9PP0TKvHyBHPuB1JRERcFrA+AtbapcBFRSzfDVx+ivc8CTxZxPKFQDN/Z4wmI0c8w+InnmZy/h4y\ngPTcvQx44mlGAveOuN/ldKEpPT3d7QhhQZ9T8emzKh59TsFl/HWOwW3GGBspP0sg9Eyrx+RdP7Ol\nfHmq7t59bPkVFeoxaed6F5OJiMi5MsZgw6GzoLgvId9LgcdDt5EjGd/92KCNlMovcDGViIQbY4wm\nl6ZACejlgxI6cmI9xHi9vPf3v5P+r39Re9s2Oi1ZQm5sjNvRRCTMqPU1+AJZCKhFIEpcfs8dDIhN\nofHGjUx4/HGu/9vf6FW3IV3vud3taCIi4iL1EYgiI0c8w/SXX6dUfgErr2zLT/2vYVmnzjRMruJ2\nNBEJE75z0m7HiDqn+tz90UdAhUAUazv9TVblefilSz/KxZd2O46IhAEVAu4IZCGgUwNR7NvOg0gi\nn+Zfv4vX63U7joiIuECFQBSL9cSwOL0fO208XWaNdzuOiEhIS0pKYsOGDW7H8DsVAlEuLSGJ79t2\n5dvcMtw5/0O344iInLNvvvmGSy65hJSUFCpUqECHDh1YsGCB37a/f/9+6tSp47fthQoVAkLT1Op8\n0Oh8XtsTw0urZrodR0TkrO3bt49evXoxfPhwsrOzyczM5JFHHqFUqVJuRwt5KgQEgN61LuTJamUY\n/ks207JWuh1HROSsrFmzBmMM/fr1wxhDQkIC3bp1o1mzZtSuXZtFixYB8O677+LxeFi50vk7N2bM\nGPr06QPAvHnzaN++PampqVSrVo2hQ4eSl5d3bB8ej4effvoJgMGDB3P33XfTq1cvypUrR7t27Y69\nFm5UCMgxDzS5nAFlc7lq6Y+s3bvN7TgiIsXWsGFDYmJiGDx4MJMnTyY7O/vYa+np6WRkZAAwc+ZM\n6tevz8yZM489P3pvg9jYWF544QV27drFnDlzmD59OqNGjTrlPv/73/8yYsQIsrOzOe+883jooYcC\n9vMFki4flF9pPX0MP+cZtnS7mfgYDT4pIsed6fJB4zvglpQ9hxsPrVq1imeeeYavvvqKrVu3cuWV\nV/LGG2/w+eef88knn/DJJ5/QuHFj7r33XqZNm8bEiROpU6cOH3/8MS1atPjV9p5//nlmzZrFhx86\n/ac8Hg/r1q2jXr163HLLLcTFxfH6668DMGnSJP70pz8da2nwt0BePqi/8vIr36TfTNVp79B11tvM\n7nyL23FEJIycywHcXy644ALGjh0LwOrVqxkwYAB/+MMfeOyxx7j33nvZunUrBQUF9O3blxEjRrBx\n40b27t17rAhYs2YNf/rTn1i4cCGHDh0iPz+f1q1bn3J/lStXPjZfunRpDhw4ENgfMEB0akB+JSEm\njtltu/NdXjIjfpzsdhwRkbPWsGFDBg0axPLly6lfvz6JiYm89NJLdOrUiaSkJKpUqcLrr79Ox44d\nj73nzjvvpHHjxqxbt469e/fyxBNPRMUYKyoEpEhNU6vzcp2KPLoth4yta9yOIyJyWqtXr+af//wn\nmZmZAPzyyy9MnDiRdu3aAXDZZZfx8ssv06lTJ8DpN1D4OcCBAwdISkoiMTGRVatW8corr5xyf5F0\nKlqFgJzSned35JrSB7ly8Tx25xx0O46IyCklJSUxd+5c2rZtS9myZWnfvj3NmzfnueeeA5wD/4ED\nB7jssssA6NSp0wnPAUaOHMmECRMoV64ct99+O/379z/hrn8nz598R8BA3iEwkNRZUE7L6/VSZ9qb\nJBhY0/02t+OIiMt0rwF36F4D4hqPx8O8y/qy0VuGwd+/53YcERHxMxUCckZVSifzUbNmjN+fwLs/\nz3c7joiI+JEKASmWK6s35Q8VLIPXbGD9/u1uxxERET9RHwE5K62+GsMv+ZDVfTCxnhi344hIkKmP\ngDvUR0BCxuxOA8glhh6z3nY7ioiI+IEKATkriXGl+PriLmQcSeap5dPcjiMiIiWkQkDO2kUVavFc\nzVT+mnWAZdmZbscREZESUCEg5+QPjdJpF5NNl7mTo2IIThGRSKVCQM7ZtI43cZB4+s/5r9tRRETk\nHKkQkHOWGFeKD5u34oNDZfj0lx/djiMiUaxOnTpMnz79V8uffPJJ6tWrR1JSEjVr1qR///7HXktP\nT2fMmDEnrJ+RkUHNmjUDnjeUqBCQEulRrTEDyh6m3/Kl7Dty2O04IhKlihr7f9y4cbzzzjtMnz6d\n/fv3s2DBAi6//PLTvicaqRCQEnurbV9SyKHr7AluRxEROWbBggX06NGDunXrAlC5cmVuu033TDmZ\nCgEpMY/HQ0b7q1hUkMpzK2a4HUdEBIB27doxfvx4Ro4cyYIFCygoKPjVOhocCWLdDiCRoWFyFR6v\nnsT9m7O5puZ26idVcjuSiLggI8M/Te3p6SU/QN90000YYxg7diwjRowgISGB++67j/vuuw9wioBh\nw4Zx7733HntPfn4+qampJd53ONEQw+JXLb8aw44Cy6ZuQ/B41OAkEmlCdYjhunXrMmbMGLp06VLk\n6wUFBXz00UfcdNNNfP7553Tr1o3OnTszcOBAhgwZcmy9mTNnMmDAAH755ZdgRS8WDTEsYePry25g\np03k9vkfuh1FROSYmJgYrrvuOpo3b86yZctOuV4oFjmBpkJA/ColPpF3GzXizX3xzNiy2u04IhJF\njhw5Qk5OzrFp9OjRfPnll+zfvx+v18ukSZNYvnw5bdu2PfaeaDzwn0x9BMTvrq3dkt6/rKD3krns\nqFSPhJg4tyOJSBS48sorT3jeqFEjUlNTWbFiBQUFBdSpU4dXX32VSy655Ng6RV0+GG2XFKqPgARE\nvreAylPH0SzBQ0b6YLfjiIifhGofgUinPgISdmI9MUy7+HJm5aUw/qe5bscREZFTUCEgAXNRhVrc\nkeLljjXrOJCX43YcEREpggoBCah/t7qGJHLo/e1/3I4iIiJFUCEgAeXxePisVRe+zivPJ5uWuB1H\nREROokJAAq5txbr0L3uIm1Ys4UhBvttxRESkEBUCEhTj2/YlFi99v9MpAhGRUKJCQIIi1hPD/y5s\nx2c55Zi+ZZXbcURExEeFgARN16oXcFXCXq5bMgev1+t2HBERQYWABNn/LrmBPGK4ee77bkcRERFU\nCEiQxcfE8nbj5kw4UJr5Oze4HUdEpEiDBw/m4YcfdjtGUKgQkKDrU6sFneJ202vBdJ0iEJGIlJ8f\nPldIqRAQV3x2aX/2kcA9Cz92O4qIRIA6derw9NNP06RJE8qXL8+QIUPIzc0F4I033qBBgwZUqFCB\n3r17s2XLlmPv++Mf/0jlypVJTk6mefPmLF++nNdff50JEybw7LPPkpSURO/evQFYtGgRLVu2pFy5\nclx//fX069fvWKtBRkYGNWrU4Nlnn6Vq1arceuutWGt5+umnOe+880hLS6Nfv35kZ2cDkJOTw4AB\nA0hLSyM1NZU2bdqwffv2IH9qDhUC4oqycQm82qA+r+7xsHxPlttxRCQCTJgwgalTp7J+/XrWrFnD\n448/zowZM3jwwQd5//332bJlC7Vr16Z///4ATJkyhdmzZ7N27Vr27t3L+++/T4UKFbj99tu56aab\nuP/++9m/fz+ffPIJR44coU+fPgwZMoTs7GxuuOEGPv744xPuVLht2zays7PZtGkTr732Gi+++CKf\nfvops2bNYsuWLaSmpnL33XcDMG7cOPbt28fmzZvZvXs3r732GqVLl3blc1MhIK4ZVL8drT27uGLu\nl25HERE/McY/09nv13DPPfdQvXp1UlNTeeihh5g4cSITJkzg1ltvpUWLFsTHx/PUU08xZ84cNm3a\nRHx8PPv372flypV4vV4aNmxIlSpVjm2z8N3+vv/+ewoKChg6dCgxMTH06dOHNm3anJDB4/Hw97//\nnbi4OBISEnjttdd4/PHHqVatGnFxcTzyyCN88MEHFBQUEB8fz65du1i7di3GGFq2bElSUtI5f+4l\n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\nOOw9iAAAAABJRU5ErkJggg==\n", "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "thresholds = np.array([t[0] for t in reversed(timings)])\n", "times = np.array([t[1] for t in reversed(timings)])\n", "fig = plt.figure()\n", "fig.set_size_inches(8, 8)\n", "xnew = np.linspace(thresholds.min(), thresholds.max(), num=41, endpoint=True)\n", "f2 = interp1d(thresholds, times, kind='quadratic')\n", "plt.plot(thresholds, times, 'o', xnew, f2(xnew), '-')\n", "\n", "thresholds_lsh = np.array([t[0] for t in reversed(timings_lsh)])\n", "times_lsh = np.array([t[1] for t in reversed(timings_lsh)])\n", "xnew_lsh = np.linspace(thresholds_lsh.min(), thresholds_lsh.max(), num=41, endpoint=True)\n", "f3 = interp1d(thresholds_lsh, times_lsh, kind='quadratic')\n", "\n", "thresholds_car = np.array([t[0] for t in reversed(timings_cartrdge)])\n", "times_car = np.array([t[1] for t in reversed(timings_cartrdge)])\n", "xnew_car = np.linspace(thresholds_car.min(), thresholds_car.max(), num=41, endpoint=True)\n", "f4 = interp1d(thresholds_car, times_car, kind='quadratic')\n", "\n", "_, swain = plt.plot(thresholds, times, 'o', xnew, f2(xnew), '-')\n", "_, lsh = plt.plot(thresholds_lsh, times_lsh, 'o', xnew_lsh, f3(xnew_lsh))\n", "_, cartridge = plt.plot(thresholds_car, times_car, 'o', xnew_car, f4(xnew_car))\n", "plt.ylabel('Mean query time (ms)')\n", "plt.xlabel('Similarity threshold')\n", "plt.legend([swain, lsh, cartridge], ['Swain', 'LSH', 'postgres'])\n", "plt.show()" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 2", "language": "python", "name": "python2" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 2 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython2", "version": "2.7.5" } }, "nbformat": 4, "nbformat_minor": 0 }