Skip to content

Instantly share code, notes, and snippets.

@anglilian
Created December 23, 2020 03:15
Show Gist options
  • Save anglilian/9a37970b526e52f18e53ee8ec11dff35 to your computer and use it in GitHub Desktop.
Save anglilian/9a37970b526e52f18e53ee8ec11dff35 to your computer and use it in GitHub Desktop.
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# CS110 Designing a Plagiarism Detector\n",
"### Ang Li-Lian"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Plagiarism is defined as taking credit for work that is not your own. It means that the work does not represent the merit or understanding of the individual submitting the work. Within an academic setting, this constitutes cheating and is immoral because the student seeks to gain an unfair advantage over their peers and receive grades that does not reflect their learning. In the working world, it could mean stealing intellectual property to profit as your own, robbing the original author of the rewards for their work.\n",
"\n",
"Plagiarism detectors strive to detect and prevent this behaviour. The algorithms below are designed for the context of a Minerva classroom where professors want to check if student's have copied from each other or reused assignments from their seniors. Therefore, unlike plagiarism detectors like Turnitin which checks for proper citation and has algorithms that can detect paraphrasing, the following algorithm functions to alert professors based on a set threshold of similarity of possible cases of plagiarism."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Longest Common Substrings with Rolling Hashing \n",
"This approach looks for the longest similar length of strings between two texts. It finds the pairs of indices between the two texts that make up the same word until the length of the substring is equal to the length of the shortest string or when it cannot find such a pair. It defines similarity as the percentage of text in the shorter string that is in the longer string. The algorithm works best to detect if two texts have longer sentences or paragraphs that are exactly the same between them.\n",
"\n",
"### Hash Table\n",
"Hash tables are used here to store each common substring because it significantly reduces the lookup and insert times compared to using a dictionary or list. The hash table adds an item by computing a hash function which determines the position in the table the item will be in. Instead of having to search through every element in the list to find the item which can take n searches, where n is the number of elements in the list, the lookup time can become constant depending on how the hash function is coded. This data structure significantly improves the complexity of the algorithm.\n",
"\n",
"The trade off is that it uses more space. To maintain low complexity for lookup, the load factor (ratio of items inserted to buckets available) should remain low. In the case of this implementation, every bucket will contain a list of elements with the same hash function. The higher the load factor, the longer the list within the bucket and the longer the lookup time. Increasing the load factor means increasing the size of the hash table.\n",
"\n",
"The hash table size depends on the modulus used for the hash function because any number divided by it will only yield a number between 0 and the modulus. Rather than have a specific number dictate the size of the table, I included a function grow_hash which will increase the size of the table by doubling the modulus function whenever the load factor is 75% which is usually the default. This means that once 75% of all slots in the table is filled, all the elements will be rehashed into a doubly large table. It increases the versatility of the algorithm to handle even large texts and maintains the complexity of the table. If more spaces are filled the probability of finding each substring when we make comparisons decreases because there are more different strings stored in each bucket.\n",
"\n",
"The extra memory used is justified in comparison to the time complexity it saves from polynomial to constant time. This is especially salient in the context of the problem since the texts being analysed can run up to tens of thousands of characters.\n",
"\n",
"### Class Structure\n",
"I used a class structure because the following versions of plagiarism detectors will use functions from the previous versions. By allowing each version to inherit from the previous version, it reduces duplicate code. Moreover, class structures group several different functions into one space, it also makes the code more readable when you can understand how separate functions work."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import re \n",
"\n",
"class hashing(object):\n",
" def __init__(self):\n",
" self.mod = 23 #modulus\n",
" self.d = 7 #ASCII base\n",
" self.overload = 0\n",
" self.method = self.rh_get_match\n",
" \n",
" def data_prep(self,x,y):\n",
" # Clean the input strings \n",
" x = x.lower()\n",
" x = re.sub('[^A-Za-z0-9]+', '', x).lstrip()\n",
"\n",
" y = y.lower()\n",
" y = re.sub('[^A-Za-z0-9]+', '', y).lstrip()\n",
" \n",
" if x == '' or y == '':\n",
" raise ValueError(\"Input string must contain alphanumeric characters.\")\n",
"\n",
" # x must be longer than y\n",
" if len(y)>len(x): \n",
" x,y = y,x\n",
" \n",
" return x,y\n",
" \n",
" def hash_key(self, string): #Hash function 1\n",
" p = [ord(string[i])*self.d**(len(string)-i-1) for i in range(len(string))]\n",
" p = sum(p)%self.mod\n",
" return p\n",
" \n",
" def rolling_hashing(self,p, string,k,i):\n",
" p = (p*self.d + ord(string[k+i]))%self.mod #Key after appending the next letter\n",
" p = (p-ord(string[i])*(self.d**(k)%self.mod))%self.mod #Key after removing the first letter\n",
" return p \n",
" \n",
" def hash_table(self,x,y,k):\n",
" hashtable = [[] for n in range(self.mod)]\n",
" p = self.hash_key(x[0:k])\n",
" hashtable[p].append(0)\n",
" \n",
" for i in range(len(x)-k):\n",
" if self.overload > 0.75*self.mod:\n",
" return self.grow_hash()\n",
" \n",
" p = self.rolling_hashing(p, x,k,i)\n",
" hashtable[p].append(i+1) #Add substring start position\n",
" \n",
" return hashtable\n",
" \n",
" def grow_hash(self):\n",
" self.mod = self.mod*2 #Double table size \n",
" self.overload = 0 #Reset counter of items in table\n",
" return False\n",
"\n",
" def rh_get_match(self,x,y,k):\n",
" x,y = self.data_prep(x,y)\n",
" \n",
" hashtable = self.hash_table(x,y,k)\n",
" \n",
" # Activates when hash table grows\n",
" if hashtable == False:\n",
" return self.rh_get_match(x,y,k)\n",
" \n",
" common =[] \n",
" \n",
" key = self.hash_key(y[0:k])\n",
" \n",
" for j in range(len(y)-k+1):\n",
" if hashtable[key]: #Checks if there are any hits \n",
" for i in hashtable[key]: #Checks if it is a spurious hit\n",
" if x[i:i+k] == y[j:j+k]: #Checks if both substrings are the same letters\n",
" common.append((i,j)) \n",
" \n",
" if len(y) > k and k+j<len(y): \n",
" key = self.rolling_hashing(key, y,k,j)\n",
" \n",
" return common\n",
" \n",
" def lcs(self,x,y):\n",
" x,y = self.data_prep(x,y)\n",
" \n",
" lcs_len = len(y) #Stores LCS length\n",
" \n",
" for k in range(1,len(y)): \n",
" common = self.method(x,y,k)\n",
" if not common: #Terminate when no longer common substring\n",
" lcs_len = k-1\n",
" break\n",
" \n",
" lcs_no = len(self.method(x,y,lcs_len)) #Number of LCS\n",
" \n",
" return (lcs_len*lcs_no)/len(x)*100 #Percentage similarity"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## How it Works as a Plagiarism Detector\n",
"The following is an example of how the algorithm works on two relatively long texts and displays the percentage of similarity."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"H1 = hashing()\n",
"\n",
"tangled = \"\"\"All those days watching from the windows\n",
"All those years outside looking in\n",
"All that time never even knowing\n",
"Just how blind I've been\n",
"\n",
"Now I'm here, blinking in the starlight\n",
"Now I'm here, suddenly I see\n",
"Standing here, it's all so clear\n",
"I'm where I'm meant to be\n",
"\n",
"And at last I see the light\n",
"And it's like the fog has lifted\n",
"And at last I see the light\n",
"And it's like the sky is new\n",
"And it's warm and real and bright\n",
"And the world has somehow shifted\n",
"All at once everything looks different\n",
"Now that I see you\n",
"\n",
"All those days, chasing down a daydream\n",
"All those years, living in a blur\n",
"All that time, never truly seeing\n",
"Things the way they were\n",
"Now she's here, shining in the starlight\n",
"Now she's here, suddenly I know\n",
"If she's here, it's crystal clear\n",
"I'm where I'm meant to go\n",
"\n",
"And at last I see the light\n",
"And it's like the fog has lifted\n",
"And at last I see the light\n",
"And it's like the sky is new\n",
"And it's warm and real and bright\n",
"And the world has somehow shifted\n",
"\n",
"All at once everything looks different\n",
"Now that I see you\n",
"Now that I see you\"\"\"\n",
"\n",
"healing = \"\"\"\n",
"Flower gleam and glow\n",
"Let your powers shine\n",
"Make the clock reverse\n",
"Bring back what once was mine\n",
"Heal what has been hurt\n",
"Change the fates design\n",
"Save what has been lost\n",
"Bring back what once was mine\n",
"What once was mine\n",
"\"\"\"\n",
"\n",
"print(f\"Similarity: {H1.lcs(healing, tangled)}%\") "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Cleaning text\n",
"The algorithm disregards spaces, punctuation and capitalisation of words, but takes into account alphanumeric characters. The case below outputs the indexes of the texts after removing the aforementioned characters."
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"H1.rh_get_match(\"2d,a,y is Mon D A Y!!!\", \"day\", 3)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Rejecting Bad Inputs\n",
"When an empty text or entirely non-alphanumeric text is input, the algorithm will return an error prompting the user to fix the inputs."
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"ename": "ValueError",
"evalue": "Input string must contain alphanumeric characters.",
"output_type": "error",
"traceback": [
"\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[1;31mValueError\u001b[0m Traceback (most recent call last)",
"\u001b[1;32m<ipython-input-4-6fda329224a1>\u001b[0m in \u001b[0;36m<module>\u001b[1;34m\u001b[0m\n\u001b[1;32m----> 1\u001b[1;33m \u001b[0mH1\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mlcs\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m\"healing\"\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m\"\"\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m",
"\u001b[1;32m<ipython-input-1-17e9d4015e31>\u001b[0m in \u001b[0;36mlcs\u001b[1;34m(self, x, y)\u001b[0m\n\u001b[0;32m 79\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 80\u001b[0m \u001b[1;32mdef\u001b[0m \u001b[0mlcs\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m,\u001b[0m\u001b[0mx\u001b[0m\u001b[1;33m,\u001b[0m\u001b[0my\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 81\u001b[1;33m \u001b[0mx\u001b[0m\u001b[1;33m,\u001b[0m\u001b[0my\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mdata_prep\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mx\u001b[0m\u001b[1;33m,\u001b[0m\u001b[0my\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 82\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 83\u001b[0m \u001b[0mlcs_len\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mlen\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0my\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;31m#Stores LCS length\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n",
"\u001b[1;32m<ipython-input-1-17e9d4015e31>\u001b[0m in \u001b[0;36mdata_prep\u001b[1;34m(self, x, y)\u001b[0m\n\u001b[0;32m 17\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 18\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mx\u001b[0m \u001b[1;33m==\u001b[0m \u001b[1;34m''\u001b[0m \u001b[1;32mor\u001b[0m \u001b[0my\u001b[0m \u001b[1;33m==\u001b[0m \u001b[1;34m''\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 19\u001b[1;33m \u001b[1;32mraise\u001b[0m \u001b[0mValueError\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m\"Input string must contain alphanumeric characters.\"\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 20\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 21\u001b[0m \u001b[1;31m# x must be longer than y\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n",
"\u001b[1;31mValueError\u001b[0m: Input string must contain alphanumeric characters."
]
}
],
"source": [
"H1.lcs(\"healing\", \"\")"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"ename": "ValueError",
"evalue": "Input string must contain alphanumeric characters.",
"output_type": "error",
"traceback": [
"\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[1;31mValueError\u001b[0m Traceback (most recent call last)",
"\u001b[1;32m<ipython-input-5-022ff9d7fc46>\u001b[0m in \u001b[0;36m<module>\u001b[1;34m\u001b[0m\n\u001b[1;32m----> 1\u001b[1;33m \u001b[0mH1\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mlcs\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m\"healing\"\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m\"!!!\"\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m",
"\u001b[1;32m<ipython-input-1-17e9d4015e31>\u001b[0m in \u001b[0;36mlcs\u001b[1;34m(self, x, y)\u001b[0m\n\u001b[0;32m 79\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 80\u001b[0m \u001b[1;32mdef\u001b[0m \u001b[0mlcs\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m,\u001b[0m\u001b[0mx\u001b[0m\u001b[1;33m,\u001b[0m\u001b[0my\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 81\u001b[1;33m \u001b[0mx\u001b[0m\u001b[1;33m,\u001b[0m\u001b[0my\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mdata_prep\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mx\u001b[0m\u001b[1;33m,\u001b[0m\u001b[0my\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 82\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 83\u001b[0m \u001b[0mlcs_len\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mlen\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0my\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;31m#Stores LCS length\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n",
"\u001b[1;32m<ipython-input-1-17e9d4015e31>\u001b[0m in \u001b[0;36mdata_prep\u001b[1;34m(self, x, y)\u001b[0m\n\u001b[0;32m 17\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 18\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mx\u001b[0m \u001b[1;33m==\u001b[0m \u001b[1;34m''\u001b[0m \u001b[1;32mor\u001b[0m \u001b[0my\u001b[0m \u001b[1;33m==\u001b[0m \u001b[1;34m''\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 19\u001b[1;33m \u001b[1;32mraise\u001b[0m \u001b[0mValueError\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m\"Input string must contain alphanumeric characters.\"\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 20\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 21\u001b[0m \u001b[1;31m# x must be longer than y\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n",
"\u001b[1;31mValueError\u001b[0m: Input string must contain alphanumeric characters."
]
}
],
"source": [
"H1.lcs(\"healing\", \"!!!\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Longest Common Substrings with Cuckoo Hashing\n",
"This approach uses the same idea but a different hash function to explore how it affects the complexity of the algorithm."
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [],
"source": [
"class cuckoo(hashing): \n",
" def __init__(self):\n",
" super().__init__()\n",
" self.i = 0\n",
" self.rehash_counter=0\n",
" self.method = self.regular_get_match\n",
" \n",
" def h2(self, item): #Hash function 2\n",
" p= 0\n",
" for char in item:\n",
" p = p*128+ord(char) + 2^self.i\n",
" return p%self.mod\n",
" \n",
" def insert(self, x,j,k, table,cycles):\n",
" key = self.hash_key(x[j[0]:j[0]+k]) \n",
"\n",
" # Empty index\n",
" if table[key] ==None: \n",
" table[key]= j\n",
" self.overload+=1\n",
" \n",
" # Substring already in in index\n",
" elif x[j[0]:j[0]+k] == x[table[key][0]:table[key][0]+k]: \n",
" table[key].extend(j)\n",
" \n",
" # Try second hash function\n",
" else: \n",
" table[key], j = j, table[key]\n",
" key = self.h2(x[j[0]:j[0]+k])\n",
"\n",
" # empty index\n",
" if table[key] ==None: \n",
" table[key] = j\n",
" self.overload +=1\n",
"\n",
" # Substring already in in index\n",
" elif x[j[0]:j[0]+k] == x[table[key][0]:table[key][0]+k]: \n",
" table[key].extend(j)\n",
"\n",
" # Try hash function 1 again\n",
" else: \n",
" cycles +=1\n",
" if cycles > 5: #Prevent infinite loop\n",
" return False\n",
" return self.insert(x,j,k,table,cycles)\n",
" \n",
" return True\n",
" \n",
" def grow_hash(self):\n",
" super().grow_hash()\n",
" self.rehash_counter = 0\n",
" self.i = 1\n",
" \n",
" def rehash(self):\n",
" self.i +=1 #Create variance in hash function 2\n",
" self.overload = 0\n",
" self.rehash_counter +=1\n",
" \n",
" def build_hashtable(self, x,k):\n",
" table = [None for n in range(self.mod)]\n",
" cycles = 0 \n",
"\n",
" for i in range(len(x)):\n",
" if self.rehash_counter > 4 or self.overload > 0.75*self.mod*2:\n",
" self.grow_hash()\n",
" return False\n",
" else:\n",
" if self.insert(x,[i],k, table,cycles) is False:\n",
" self.rehash()\n",
" return False\n",
" \n",
" return table\n",
" \n",
" def regular_get_match(self, x,y,k):\n",
" x,y = self.data_prep(x,y)\n",
" \n",
" hashtable = self.build_hashtable(x,k)\n",
"\n",
" # Activates to grow hash or change hash function 2\n",
" if hashtable == False:\n",
" return self.regular_get_match(x,y,k)\n",
" \n",
" common =[]\n",
"\n",
" for j in range(len(y)-k+1):\n",
" key1 = self.hash_key(y[j:j+k])\n",
" key2 = self.h2(y[j:j+k])\n",
" \n",
" common_list = []\n",
" \n",
" try: \n",
" # Checks if both substrings are the same letters\n",
" if x[hashtable[key1][0]:hashtable[key1][0]+k] == y[j:j+k]: \n",
" common_list.extend(hashtable[key1])\n",
" except:\n",
" pass\n",
" \n",
" try:\n",
" # Checks if both substrings are the same letters\n",
" if x[hashtable[key2][0]:hashtable[key2][0]+k] == y[j:j+k]: \n",
" common_list.extend(hashtable[key2]) \n",
" except:\n",
" pass\n",
" \n",
" if common_list:\n",
" common.append((common_list, j))\n",
"\n",
" return common\n",
" \n",
" def get_table(self,x,y,k):\n",
" common = self.regular_get_match(x,y,k)\n",
" x,y = self.data_prep(x,y)\n",
" \n",
" df = pd.DataFrame(common,columns = [\"x\",'y'])\n",
" \n",
" # Column for substring represented by index\n",
" df['X String'] = [x[common[i][0][0]:common[i][0][0]+k] for i in range(len(common))]\n",
" df['Y String'] = [y[common[i][1]: common[i][1]+k]for i in range(len(common))]\n",
" \n",
" return df"
]
},
{
"attachments": {
"Cuckoo%20hashing.png": {
"image/png": "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"
}
},
"cell_type": "markdown",
"metadata": {},
"source": [
"### How it works\n",
"\n",
"![Cuckoo%20hashing.png](attachment:Cuckoo%20hashing.png)\n",
"\n",
"The algorithm works using two hash functions. The first hash function here is computed using the ASCII code of the substring similar to the previous algorithm. The second hash function uses the idea of double hashing, but here to resolve infinite loops. Whenever the function runs into an infinite loop (defined here as bouncing between tables more than 5 times), then the function changes by increasing i by 1."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Second Hash Function\n",
"This is a good hash function because it a) is deterministic, returning the same key for the same input, b) slight variations output very different keys, c) it follows the assumption of simple uniform hashing, where the item has an equal chance of being inserted at any part of the table and d) has few collisions. The hash2(key) relies on a prime number to prevent its multiples from being disproportianately represented. "
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"5 20 22\n"
]
}
],
"source": [
"# Proof of different keys with slight variation\n",
"H2 = cuckoo()\n",
"print(H2.h2(\"cat\"), H2.h2(\"bat\"), H2.h2(\"ctt\"))"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {
"scrolled": true
},
"outputs": [],
"source": [
"# Proof of simple uniform hashing\n",
"import random \n",
"import matplotlib.pyplot as plt\n",
"\n",
"def h2(item, i):\n",
" p= 0\n",
" for char in item:\n",
" p = p*128+ord(char) + 2^i\n",
" return p%23\n",
"\n",
"letters = \"abcdefghijklmnopqrstuvwxyz\" \n",
"keys_h2=[]\n",
"\n",
"for i in range(3):\n",
" keys_h2.append([])\n",
" for x in range(10000):\n",
" item = random.choices(letters, k=3)\n",
" keys_h2[i].append(h2(item,i))"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAXoAAAD8CAYAAAB5Pm/hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDMuMC4yLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvOIA7rQAADL9JREFUeJzt3H+o3fV9x/Hna0bbsW71VxRJsqVb84f+M5UgAcdwOobasTioYBlrKEL2hwVLC5vrP91gA/1jtRSGkC1iOrq20nYzdMImUen2R92urfPHQjETp1nEpPijLaUb1vf+uJ9sd7lX77m/cu593+cDwvl+P+dz7/3ky8nzfPO955xUFZKkvn5q2guQJK0tQy9JzRl6SWrO0EtSc4Zekpoz9JLUnKGXpOYMvSQ1Z+glqbkt014AwMUXX1w7d+6c9jIkaUN58sknv1dVWxebty5Cv3PnTmZmZqa9DEnaUJL8xyTzvHQjSc0ZeklqztBLUnOGXpKaM/SS1Jyhl6TmDL0kNWfoJak5Qy9Jza2Ld8auxM67/m5ZX/fi3R9a5ZVI0vq04UOvs285T64+sUrTY+i1bnX831rHv5PWv00bes9KJW0Wmzb0Z4tPKJKmzdBL+ISs3nx5pSQ15xl9E/6Srzf/x7H+red/g4Ze0oqs9yeh9b6+s8HQb3LLPQuRtHEY+nXI+EpaTYZeZ4VPXtL0+KobSWrOM3pJ/8v/efVk6JfAfwSSNiJDLy2TT/zaKAy92jHA0v/nL2MlqTnP6KWm1vP/bNbzxwV05Bm9JDVn6CWpOS/dSNIZ1vNlr+WY+Iw+yTlJvpPkG2P/A0meSPJ8kq8kOW+Mv2fsHxv371ybpUuSJrGUSzd3Akfn7N8D3FtVu4DXgdvH+O3A61X1QeDeMU+SNCUThT7JduBDwF+O/QDXA18dUw4Bt4ztvWOfcf8NY74kaQomPaP/HPD7wNtj/yLgjap6a+wfB7aN7W3AywDj/jfHfEnSFCwa+iS/CZysqifnDi8wtSa4b+733Z9kJsnMqVOnJlqsJGnpJnnVzbXAbyW5GXgv8HPMnuGfn2TLOGvfDpwY848DO4DjSbYA7wdeO/ObVtUB4ADA7t275z0RSNKZur0a5mxZ9Iy+qv6wqrZX1U7gNuDRqvod4DHgw2PaPuChsX147DPuf7SqDLkkTclK3jD1B8Ankxxj9hr8wTF+ELhojH8SuGtlS5QkrcSS3jBVVY8Dj4/tF4BrFpjzY+DWVVibJGkV+BEIktScoZek5gy9JDVn6CWpOUMvSc0ZeklqztBLUnOGXpKaM/SS1Jyhl6TmDL0kNWfoJak5Qy9JzRl6SWrO0EtSc4Zekpoz9JLUnKGXpOYMvSQ1Z+glqTlDL0nNGXpJas7QS1Jzhl6SmjP0ktScoZek5gy9JDVn6CWpOUMvSc0ZeklqztBLUnOGXpKaM/SS1Jyhl6TmDL0kNWfoJam5RUOf5L1J/jnJvyZ5Lskfj/EPJHkiyfNJvpLkvDH+nrF/bNy/c23/CpKkdzPJGf1/AddX1S8DVwI3JtkD3APcW1W7gNeB28f824HXq+qDwL1jniRpShYNfc364dg9d/wp4Hrgq2P8EHDL2N479hn335Akq7ZiSdKSTHSNPsk5SZ4CTgKPAP8OvFFVb40px4FtY3sb8DLAuP9N4KLVXLQkaXIThb6qflJVVwLbgWuAyxeaNm4XOnuvMweS7E8yk2Tm1KlTk65XkrRES3rVTVW9ATwO7AHOT7Jl3LUdODG2jwM7AMb97wdeW+B7Haiq3VW1e+vWrctbvSRpUZO86mZrkvPH9k8Dvw4cBR4DPjym7QMeGtuHxz7j/kerat4ZvSTp7Niy+BQuAw4lOYfZJ4YHq+obSf4N+HKSPwG+Axwc8w8Cf5XkGLNn8retwbolSRNaNPRV9TRw1QLjLzB7vf7M8R8Dt67K6iRJK+Y7YyWpOUMvSc0ZeklqztBLUnOGXpKaM/SS1Jyhl6TmDL0kNWfoJak5Qy9JzRl6SWrO0EtSc4Zekpoz9JLUnKGXpOYMvSQ1Z+glqTlDL0nNGXpJas7QS1Jzhl6SmjP0ktScoZek5gy9JDVn6CWpOUMvSc0ZeklqztBLUnOGXpKaM/SS1Jyhl6TmDL0kNWfoJak5Qy9JzRl6SWrO0EtSc4ZekppbNPRJdiR5LMnRJM8luXOMX5jkkSTPj9sLxniSfD7JsSRPJ7l6rf8SkqR3NskZ/VvAp6rqcmAPcEeSK4C7gCNVtQs4MvYBbgJ2jT/7gftWfdWSpIktGvqqeqWqvj22fwAcBbYBe4FDY9oh4JaxvRf4Qs36FnB+kstWfeWSpIks6Rp9kp3AVcATwKVV9QrMPhkAl4xp24CX53zZ8TF25vfan2QmycypU6eWvnJJ0kQmDn2S9wFfAz5RVd9/t6kLjNW8gaoDVbW7qnZv3bp10mVIkpZootAnOZfZyH+xqr4+hl89fUlm3J4c48eBHXO+fDtwYnWWK0laqkledRPgIHC0qj47567DwL6xvQ94aM74R8erb/YAb56+xCNJOvu2TDDnWuB3gWeSPDXGPg3cDTyY5HbgJeDWcd/DwM3AMeBHwMdWdcWSpCVZNPRV9U8sfN0d4IYF5hdwxwrXJUlaJb4zVpKaM/SS1Jyhl6TmDL0kNWfoJak5Qy9JzRl6SWrO0EtSc4Zekpoz9JLUnKGXpOYMvSQ1Z+glqTlDL0nNGXpJas7QS1Jzhl6SmjP0ktScoZek5gy9JDVn6CWpOUMvSc0ZeklqztBLUnOGXpKaM/SS1Jyhl6TmDL0kNWfoJak5Qy9JzRl6SWrO0EtSc4Zekpoz9JLUnKGXpOYMvSQ1t2jok9yf5GSSZ+eMXZjkkSTPj9sLxniSfD7JsSRPJ7l6LRcvSVrcJGf0DwA3njF2F3CkqnYBR8Y+wE3ArvFnP3Df6ixTkrRci4a+qr4JvHbG8F7g0Ng+BNwyZ/wLNetbwPlJLlutxUqSlm651+gvrapXAMbtJWN8G/DynHnHx9g8SfYnmUkyc+rUqWUuQ5K0mNX+ZWwWGKuFJlbVgaraXVW7t27dusrLkCSdttzQv3r6ksy4PTnGjwM75szbDpxY/vIkSSu13NAfBvaN7X3AQ3PGPzpefbMHePP0JR5J0nRsWWxCki8B1wEXJzkOfAa4G3gwye3AS8CtY/rDwM3AMeBHwMfWYM2SpCVYNPRV9ZF3uOuGBeYWcMdKFyVJWj2+M1aSmjP0ktScoZek5gy9JDVn6CWpOUMvSc0ZeklqztBLUnOGXpKaM/SS1Jyhl6TmDL0kNWfoJak5Qy9JzRl6SWrO0EtSc4Zekpoz9JLUnKGXpOYMvSQ1Z+glqTlDL0nNGXpJas7QS1Jzhl6SmjP0ktScoZek5gy9JDVn6CWpOUMvSc0ZeklqztBLUnOGXpKaM/SS1Jyhl6TmDL0kNbcmoU9yY5LvJjmW5K61+BmSpMmseuiTnAP8OXATcAXwkSRXrPbPkSRNZi3O6K8BjlXVC1X138CXgb1r8HMkSRNYi9BvA16es398jEmSpmDLGnzPLDBW8yYl+4H9Y/eHSb67zJ93MfC9ZX5tVx6ThXlc5vOYzHdWj0nuWdGX/8Ikk9Yi9MeBHXP2twMnzpxUVQeAAyv9YUlmqmr3Sr9PJx6ThXlc5vOYzNfxmKzFpZt/AXYl+UCS84DbgMNr8HMkSRNY9TP6qnoryceBvwfOAe6vqudW++dIkiazFpduqKqHgYfX4nsvYMWXfxrymCzM4zKfx2S+dsckVfN+TypJasSPQJCk5jZ06P2ohfmSvJjkmSRPJZmZ9nqmIcn9SU4meXbO2IVJHkny/Li9YJprnIZ3OC5/lOQ/x+PlqSQ3T3ONZ1OSHUkeS3I0yXNJ7hzj7R4rGzb0ftTCu/q1qrqy20vEluAB4MYzxu4CjlTVLuDI2N9sHmD+cQG4dzxerhy/X9ss3gI+VVWXA3uAO0ZD2j1WNmzo8aMW9A6q6pvAa2cM7wUOje1DwC1ndVHrwDscl02rql6pqm+P7R8AR5l9F3+7x8pGDr0ftbCwAv4hyZPj3ceadWlVvQKz/8CBS6a8nvXk40meHpd2NvxliuVIshO4CniCho+VjRz6iT5qYRO6tqquZvaS1h1JfnXaC9K6dh/wS8CVwCvAn013OWdfkvcBXwM+UVXfn/Z61sJGDv1EH7Ww2VTViXF7EvgbZi9xCV5NchnAuD055fWsC1X1alX9pKreBv6CTfZ4SXIus5H/YlV9fQy3e6xs5ND7UQtnSPIzSX729DbwG8Cz7/5Vm8ZhYN/Y3gc8NMW1rBungzb8Npvo8ZIkwEHgaFV9ds5d7R4rG/oNU+OlYJ/j/z5q4U+nvKSpSvKLzJ7Fw+y7nv96Mx6TJF8CrmP2UwhfBT4D/C3wIPDzwEvArVW1qX4x+Q7H5TpmL9sU8CLwe6evT3eX5FeAfwSeAd4ew59m9jp9q8fKhg69JGlxG/nSjSRpAoZekpoz9JLUnKGXpOYMvSQ1Z+glqTlDL0nNGXpJau5/AH6GFSjEJEa3AAAAAElFTkSuQmCC\n",
"text/plain": [
"<Figure size 432x288 with 1 Axes>"
]
},
"metadata": {
"needs_background": "light"
},
"output_type": "display_data"
}
],
"source": [
"# Plot for second version of hash function at mod 1 \n",
"plt.hist(keys_h2[0], bins = 23)\n",
"plt.show()"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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\n",
"text/plain": [
"<Figure size 432x288 with 1 Axes>"
]
},
"metadata": {
"needs_background": "light"
},
"output_type": "display_data"
}
],
"source": [
"# Plot for second version of hash function at mod 2 \n",
"plt.hist(keys_h2[1], bins = 23)\n",
"plt.show()"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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\n",
"text/plain": [
"<Figure size 432x288 with 1 Axes>"
]
},
"metadata": {
"needs_background": "light"
},
"output_type": "display_data"
}
],
"source": [
"# Plot for second version of hash function at mod 3\n",
"plt.hist(keys_h2[2], bins = 23)\n",
"plt.show()"
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Average number of strings per bucket: 1.2863023096272581\n"
]
}
],
"source": [
"#Low number of collisions\n",
"\n",
"sent1 = random.choices(letters, k=10000)\n",
"sent2 = random.choices(letters, k=10000)\n",
"sent1= ''.join(sent1)\n",
"sent2 = ''.join(sent2)\n",
" \n",
"common = H2.regular_get_match(sent1,sent2, 3)\n",
"strings_in_bucket = 0\n",
"bucket = 0\n",
"\n",
"for word in common:\n",
" strings_in_bucket += len(word[0])\n",
"\n",
"print(f\"Average number of strings per bucket: {strings_in_bucket/len(common)}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Test (Data Cleaning)\n",
"The output will be the same with the rolling hash since many methods were inherited and only the method of storing was changed to examine complexity."
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[([1, 9], 0)]\n",
"Similarity: 25.0%\n"
]
}
],
"source": [
"print(H2.regular_get_match(\"2d,a,y is Mon D A Y!!!\", \"day\", 3))\n",
"print(f\"Similarity: {H2.lcs('2d,a,y is Mon D A Y!!!', 'day')}%\") "
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Similarity: 2.25%\n"
]
}
],
"source": [
"print(f\"Similarity: {H2.lcs(tangled, healing)}%\") "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Pinpointing Similarity\n",
"An additional function was added to this version to output which portions of texts and where in the text were similar in a nicely formatted table."
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {
"pixiedust": {
"displayParams": {}
},
"scrolled": true
},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>x</th>\n",
" <th>y</th>\n",
" <th>X String</th>\n",
" <th>Y String</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>[233, 279, 626, 672]</td>\n",
" <td>38</td>\n",
" <td>kethe</td>\n",
" <td>kethe</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>[349, 742]</td>\n",
" <td>66</td>\n",
" <td>atonc</td>\n",
" <td>atonc</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>[350, 743]</td>\n",
" <td>67</td>\n",
" <td>tonce</td>\n",
" <td>tonce</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>[234, 627]</td>\n",
" <td>103</td>\n",
" <td>ethef</td>\n",
" <td>ethef</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td>[349, 742]</td>\n",
" <td>148</td>\n",
" <td>atonc</td>\n",
" <td>atonc</td>\n",
" </tr>\n",
" <tr>\n",
" <th>5</th>\n",
" <td>[350, 743]</td>\n",
" <td>149</td>\n",
" <td>tonce</td>\n",
" <td>tonce</td>\n",
" </tr>\n",
" <tr>\n",
" <th>6</th>\n",
" <td>[349, 742]</td>\n",
" <td>163</td>\n",
" <td>atonc</td>\n",
" <td>atonc</td>\n",
" </tr>\n",
" <tr>\n",
" <th>7</th>\n",
" <td>[350, 743]</td>\n",
" <td>164</td>\n",
" <td>tonce</td>\n",
" <td>tonce</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" x y X String Y String\n",
"0 [233, 279, 626, 672] 38 kethe kethe\n",
"1 [349, 742] 66 atonc atonc\n",
"2 [350, 743] 67 tonce tonce\n",
"3 [234, 627] 103 ethef ethef\n",
"4 [349, 742] 148 atonc atonc\n",
"5 [350, 743] 149 tonce tonce\n",
"6 [349, 742] 163 atonc atonc\n",
"7 [350, 743] 164 tonce tonce"
]
},
"execution_count": 15,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"import pandas as pd\n",
"H2.get_table(tangled, healing, 5)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Complexity Analysis "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Rolling Hash\n",
"The insertion time for rolling hash is O(n). Computing the first hash function takes O(k) where k is the length of substring and every subsequent substring will take constant time computation. \n",
"\n",
"The lookup function runs through each m-k substring of the second string and checks through the sublist to determine if the item actually exists. Computing the key is the same as previously where it will take constant time because of rolling hash. If the table is large enough (which it is since the grow_hash function maintains the element to bucket ratio), the number of elements in the sublist will remain fairly low. Therefore lookup complexity is O(m).\n",
"\n",
"Since the context of the problem is a plagiarism detector, we can expect both texts to be about the same length. This effectively makes the total complexity of the cuckoo algorithm O(n)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Cuckoo Hash\n",
"The lookup time complexity is O(1) since there are only two places within the table to search for the item which are the indices output by the first and second hash function. The lookup function is used to determine where in the list to search for each substring.\n",
"\n",
"The insert time complexity is O(n) because if a space is occupied, each subsequent existing item has to be continuously moved until it finds an empty spot in the table. Without a cap, this could run infinitely, but in the current algorithm, there is a cap at [FILL] to prevent an infinite loop. Once that threshold is reached, the hash function will be modified and all items will be reinserted. Another way the algorithm was designed to maintain a low insertion complexity is by growing the hash table each time the load factor (ratio of elements to buckets) exceeds 0.75.\n",
"\n",
"However, because the alogrithm has to compute the hash key for each substring, which gives it a complexity of O(k) where k is the length of the substring. The substring goes through a for loop to sum up its value. This process becomes more significant as k increases since it is called at least once during each insertion and twice for every lookup. Therefore, the actual insert and lookup time is O(kn) and O(km) respectively.\n",
"\n",
"Since the context of the problem is a plagiarism detector, we can expect both texts to be about the same length. This effectively makes the total complexity of the cuckoo algorithm O(kn)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Rolling Hash vs Cuckoo Hash\n",
"Both hash functions have the same complexity of O(n), but the constant for cuckoo hash is dependent on the length of the substring whereas rolling hash is constant irregardless. The test below shows that when we hold k (length of substring) constant, and vary the length of the substring (n) both of them grow linearly. However, the time for cuckoo hash is higher than rolling hash because of the constant k."
]
},
{
"cell_type": "code",
"execution_count": 16,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"<matplotlib.legend.Legend at 0x232a1161588>"
]
},
"execution_count": 16,
"metadata": {},
"output_type": "execute_result"
},
{
"data": {
"image/png": "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K+mYp512GuUZAjB06FDefvvtyspaiSKphrov0Bw451wAVJULLriAK6+8kvHjxwMwd+5c1q5dS/v27aOcu/gTtmShqp+UtFVF5pxzrrxmzJhB9erVue666/bu69KlC7/4xS/4+OOP6du37979I0aM4NVXXwVg9uzZ9OrVi86dO9OjRw+2bdu2z3GnTJlCz549Wb9+PStXrqR379506tSJ3r178/333wOUur8srr/+etLT0+nYsSP33Vf4m/2OO+6gQ4cOdOrUidtuu23v/k8//ZRevXrRtm3bQEoZpZYsRGSmqp4sItuw3k97nwJUVesS5Wi7AAAX7UlEQVRVem6ccwkpGjOUL1iwgOOPP75Mx9yzZw+DBw9mwoQJdO/ena1bt3LQQQftfX7SpEk88cQTTJ06lUMOOYRf//rXXHHFFVx55ZW8/PLL3HzzzbzzzjuMGDGixP3FTZgwgZkzZ+59vHz58r33H3roIQ499FDy8vLo3bs38+fPp0WLFkyaNIklS5YgImzevHlv+pKmTa9MBypZ1AFQ1YNVtV6R7WAPFM65RLR06VKaNm1K9+7dAahXrx6pqfabesaMGTz22GNMmTKFQw45BIDPP/+cX/3qVwBcfvnley/8pe0vbvDgwftMVZ6eXjjrxptvvkm3bt3o2rUrCxcuZNGiRdSrV49atWpx9dVXk5GRQe3atfemL2na9Mp0oDaLis+365xzRGeG8o4dO5ZaHZOamkp+fuHsRbt27QKsnUNKmW66bdu2rFixgmXLlu1zUS+qtNeWtr803377LaNGjWL27NkccsghDB06lF27dpGamsoXX3zB9OnTGT9+PM888wwfffQRUPK06ZXpQCWLxiLy29K2SA4uIueKyFIRWS4i+610LiI1RWRC6PlZIpIW2t9DROaGtnkickG53p1zLmmdccYZ7N69mxdeeGHvvtmzZ/PJJ5/QunVrFi1axO7du9myZQvTp08H4Oijj2b16tV7F0Latm3b3jUiWrduTUZGBldccQULFy4EoFevXnsbz19//XVOPvnkA+6P1NatW6lTpw7169dn7dq1vP/++4BNVLhlyxb69OnD6NGj91k4KWgHKlmkYOtul2tVBxFJAcYAZ2HrYMwWkUxVXVQk2TBgk6oeKSJDgMeAwcACIF1Vc0WkKTBPRCaranArezjnEoqIMGnSJG699VYeffRRatWqRVpaGqNHj6Zly5ZcfPHFdOrUiXbt2tG1a1cAatSowYQJE7jpppvYuXMnBx10EB9++OHeYx511FG8/vrrDBo0iMmTJ/P0009z1VVX8fjjj9OoUSNeeeUVgFL3R6pz58507dqVjh070rZtW0466STAgteAAQPYtWsXqsqTTz5ZSZ9WeKVOUS4iX6pqt3IfWKQncL+qnhN6fCeAqj5SJM20UJrPRSQVWAM00iKZCk2R/l+g+YGChU9R7lxsScYpymNdRaYoP1A1VEXXiWsOFF27Ozu0r8Q0oUCwBWgIICIniMhC4GvgOi9VOOdc9BwoWPSu4LFLCjbFizGlplHVWaraEegO3CkitfY7gchwEckSkax169ZVMLvOOedKU2qwUNWNFTx2NtCyyOMWwOrS0oSqoeoD+5xXVRcDO4BjS8jjWFVNV9X0Ro0aVTC7zjnnShPJ3FDlNRtoJyJtRKQGMAQoPqdUJnBl6P5A4CNV1dBrUgFEpDVwFPBdgHl1zgUgiC6crnwq+reIZG6ocgn1ZBoBTMN6Vr2sqgtF5AEgS1UzseVax4nIcqxEMST08pOBO0QkB1tD4wZVXR9UXp1zla9WrVps2LCBhg0blnmcgatcqsqGDRuoVWu/2vyIldobKt54byjnYktOTg7Z2dl7B7y56KpVqxYtWrSgevXq++yPtDdUYCUL51xyq169Om3atIl2NlwlCbLNwjnnXILwYOGccy4sDxbOOefC8mDhnHMuLA8WzjnnwvJg4ZxzLiwPFs4558LyYOGccy4sDxbOOefC8mDhnHMuLA8WzjnnwvJg4ZxzLiwPFs4558LyYOGccy4sDxbOOefC8mDhnHMuLA8WzjnnwvJg4ZxzLiwPFs4558IKNFiIyLkislRElovIHSU8X1NEJoSenyUiaaH9Z4nIHBH5OnR7RpD5dM45d2CBBQsRSQHGAOcBHYBLRKRDsWTDgE2qeiTwJPBYaP96oJ+qHgdcCYwLKp/OOefCC7Jk0QNYrqorVHUPMB4YUCzNAOBvoftvA71FRFT1K1VdHdq/EKglIjUDzKtzzrkDCDJYNAd+KPI4O7SvxDSqmgtsARoWS3MR8JWq7g4on84558JIDfDYUsI+LUsaEemIVU2dXeIJRIYDwwFatWpVvlw655wLK8iSRTbQssjjFsDq0tKISCpQH9gYetwCmARcoarflHQCVR2rqumqmt6oUaNKzr5zzrkCQQaL2UA7EWkjIjWAIUBmsTSZWAM2wEDgI1VVEWkATAHuVNXPAsyjc865CAQWLEJtECOAacBi4E1VXSgiD4hI/1Cyl4CGIrIc+C1Q0L12BHAkcI+IzA1tjYPKq3POuQMT1eLNCPEpPT1ds7Kyop0N55yLKyIyR1XTw6XzEdzOOefC8mDhnHMuLA8WzjnnwvJg4ZxzLiwPFs4558LyYOGccy4sDxbOOefCCnJuKOecc0HIzYWvvoIZM2zr1g0eeijQU3qwcM65WJefD/PmFQaHTz+FrVvtuWOOgdNOCzwLHiyccy7W5OfDwoWFweGTT2DTJnuuXTsYMgROP92CxOGHV0mWPFg451y0qcKSJYXB4eOPYf16e65NG7jggsLg0KJFVLLowcI556JBFd59FyZMsOCwZo3tb9kS+vSx4HD66dC6dVSzWcCDhXPOVbUFC+CWW+Cjj6wa6YwzCoND27YgJa0LF10eLJxzrqps3gz33QdjxkD9+vDss3DNNZAa+5fi2M+hc87Fu7w8eOUVuPNO2LgRrr0W/vQnaNgw2jmLmA/Kc84ll5wcePhhuOgi+Mc/4Oefgz3f55/DCSdYCeLooyEry0oUcRQowIOFcy6ZzJ9vF+6774Z//xsuvdTaDK66yrqn5udX3rnWrIErr4ReveDHH+H11218RNeulXeOKuTBwjmX+HJy4MEHIT0dVq2CjAy7mH/8MQwaBG+/bd1S27aFe+6BZcvKf649e2DUKGjfHsaPhzvugKVL4Ve/ismG60h5sHDOJbYFC+DEEy0IXHSRDXa74AKoVg1OPRVeeskCx+uvw1FHWRXVUUdZieD5562NIVLTpkGnTnD77XbsBQvgkUegbt3g3l8V8WDhnEtMubl24e/WDX74wUoPb7wBhx22f9rate2X/7Rp8P33MHKkTadx/fXQtCkMHAiTJ1sJpSQrVsD558O551pj9pQplr5du2DfYxUKNFiIyLkislRElovIHSU8X1NEJoSenyUiaaH9DUVkhohsF5Fngsyjcy4BLVwIPXta28T559vjiy6K7LXNm1vJ4OuvYc4cCxiffgr9+9tzt9wCX35pg+p27LASS4cO8OGH8OijVpro0yfY9xcFoqrBHFgkBVgGnAVkA7OBS1R1UZE0NwCdVPU6ERkCXKCqg0WkDtAVOBY4VlVHhDtfenq6ZmVlBfFWnHPxIjfX2gvuuw/q1bNeR4MGVfy4OTnwwQfw2muQmWntEh07Wunjhx+soXzkSGjWrOLnqmIiMkdV08OlC7Jk0QNYrqorVHUPMB4YUCzNAOBvoftvA71FRFR1h6rOBHYFmD/nXCJZtAhOOsnGMvTrZ6WJyggUANWr2zHfesvaN55/Hho0gFatrFfV3/8el4GiLIIclNcc+KHI42zghNLSqGquiGwBGgLrA8yXcy6R5OXBn/8M995rDcnjx8PFFwfX8+iQQ2xQ3bXXBnP8GBVksCjpL1W8ziuSNKWfQGQ4MBygVatWkefMOZcYliyBoUNh1izr4fTcc9CkSbRzlZCCrIbKBloWedwCWF1aGhFJBeoDEfdTU9WxqpququmNGjWqYHadc4HbudNWeFu0CFauhA0bYNcuaywui7w8a5vo0gX+9z8biT1xogeKAAVZspgNtBORNsAqYAjwq2JpMoErgc+BgcBHGlSLu3MuOr79Ft5/H6ZOtVlWd+7cP021alCnTulb3br7Pp4+3abRGDDA2g+qaAGgZBZYsAi1QYwApgEpwMuqulBEHgCyVDUTeAkYJyLLsRLFkILXi8h3QD2ghoicD5xdtCeVcy5G7dkDM2dacJg6FRYvtv1t28LVV8PJJ9u0Gjt2lLxt377v/bVr9338889w6KEwbpz1QorjUdHxJLCus1XNu846F0WrVhWWHv71L7uo16hho5j79LGtXbvKubCr2lbNxxRXhki7zvoU5c65ssvNhf/+14LDlCk2QR/YKm+XXQbnnWcL+gQxzYWIlyaiwIOFcy4yW7bYMqBTp9q0GJs326I9J59sA9L69LGRzH4hT0geLJxzpcvJgX/+00Yuv/su7N5tjckXXmjB4cwzbcU3l/A8WDjn9qVq3VvHjbMuqT/9ZAv1XHONVTF17+7tBUnIg4VzzqxaZdN0v/aaTZVRo4ZNcXHFFTabao0a0c6hiyIPFs4ls+3bYdIkCxDTp1upolcvGwl98cXWRdU5PFg4l3zy8mDGDAsQGRk2fqFNG5tq+/LL4cgjo51DF4M8WDiXSFStUTonx7q3FtzPyYH162HCBKtqWrXKGqZ/9SurZjrpJO/F5A7Ig4VzsULVlvBcuRK++862gvs//GDTZJQUBIo+zs8/8DlSUmwMxJNPWntErVpV8MZcIvBg4VxVUYV16/YNAsUDw/bt+76mbl1IS7N1E+rUsXUVUlPtNtL7BY9r17auro0bV/lbd/HPg4VzFbV7t3UvLbqtXbvv/e+/t2BQfBK9Bg2gdWtrJ+jd2wJDWprtS0uztRO8esjFAA8WzpUkNxdWr7aL/I8/HjgYbNlS8jFq1bIpsxs3tpHNffoUBoGCgOAD2lyc8GDhktO2bRYICn7xF739/ntrAM7L2/c1InDYYXbxb9wYunUrDAYFW9HHdep4qcAlDA8WLvHk5FhpYNUqaxguGgQK7m/atO9rUlNtErxWrWym1Nat7X6rVra2cuPGNoo51f9lXHLyb76LH6qwdasFgexsuy1p++mn/Vdea9Cg8OJ/8smF9wuCwuGHW08h51yJPFi4YOTnW8Pv7t22bGbB/UgeF+zbuHH/QLBjx/7natgQmje3rVs3u23Rwm4LSgv16lX9Z+BcAvFg4couLw/WrNm/aqfoVryapzxSUwuDQOfO1kBc8LggIDRr5mMFnKsCHizc/nbs2PfCXzwYZGdbu0BR9esXVumcdBI0amQX8Zo1bSt6v7R9xR/Xru2zmzoXIzxYJIMdO2wwWKTbtm37vj4lxX7Jt2oFPXvuW9ffqpVV9XgXUOcSmgeLeKUKGzZYb5+i2+rV+1/8iw8EK1CjhpUACrYjjrDbJk327w3kvYCcS2qBXgFE5FzgKSAFeFFVHy32fE3gNeB4YAMwWFW/Cz13JzAMyANuVtVpQeY1pqjaQK/s7P2DQcGWnb1/EEhNtQt7wcW/Qwfr8lk0IBTdDj7YxwE45yISWLAQkRRgDHAWkA3MFpFMVV1UJNkwYJOqHikiQ4DHgMEi0gEYAnQEmgEfikh7VS02SipKVK2RNy/PRvrm5Vkd/s6dtv38s20F98PdFtzfurUwGBSfI6haNQsELVtCly42CVzLlvtuTZp4Hb9zLhBBlix6AMtVdQWAiIwHBgBFg8UA4P7Q/beBZ0REQvvHq+pu4FsRWR463ueVnsv582HIkMKLfiS34Wb2jERKijXg1q4NBx1kt3XrwtFHw1ln7R8Imjb1qiDnXNQEefVpDvxQ5HE2cEJpaVQ1V0S2AA1D+/9b7LXNA8llnTpw7LF28U5NLbwtej+S51JTCy/6kdxWrx7I23HOuSAEGSxKqgzXCNNE8lpEZDgwHKBVq1ZlzZ854gh4883yvdY555JEkBXc2UDLIo9bAKtLSyMiqUB9YGOEr0VVx6pquqqmN2rUqBKz7pxzrqggg8VsoJ2ItBGRGliDdWaxNJnAlaH7A4GPVFVD+4eISE0RaQO0A74IMK/OOecOILBqqFAbxAhgGtZ19mVVXSgiDwBZqpoJvASMCzVgb8QCCqF0b2KN4bnAjTHTE8o555KQaPHZOeNUenq6ZmVlRTsbzjkXV0Rkjqqmh0vnnfKdc86F5cHCOedcWB4snHPOheXBwjnnXFgJ08AtIuuAldHORww4DFgf7UzEEP889uWfRyH/LExrVQ07UC1hgoUzIpIVSc+GZOGfx7788yjkn0XZeDWUc865sDxYOOecC8uDReIZG+0MxBj/PPbln0ch/yzKwNssnHPOheUlC+ecc2F5sIgzItJSRGaIyGIRWSgit4T2Hyoi/xKR/4VuDwntFxF5WkSWi8h8EekW3XdQ+UQkRUS+EpH3Qo/biMis0GcxITTrMaFZjCeEPotZIpIWzXwHQUQaiMjbIrIk9B3pmazfDRH5Teh/ZIGIvCEitZL5u1FRHiziTy7wf6p6DHAicGNozfI7gOmq2g6YHnoMcB42xXs7bKGo56o+y4G7BVhc5PFjwJOhz2ITttY7FFnzHXgylC7RPAV8oKpHA52xzyXpvhsi0hy4GUhX1WOxma+HkNzfjYpRVd/ieAPeBc4ClgJNQ/uaAktD9/8KXFIk/d50ibBhC2NNB84A3sNWWVwPpIae7wlMC92fBvQM3U8NpZNov4dK/CzqAd8Wf0/J+N2gcMnmQ0N/6/eAc5L1u1EZm5cs4lioqNwVmAU0UdUfAUK3jUPJSloLPZj1zKNjNPA7ID/0uCGwWVVzQ4+Lvt991nwHCtZ8TxRtgXXAK6FquRdFpA5J+N1Q1VXAKOB74Efsbz2H5P1uVJgHizglInWBicCtqrr1QElL2JcQXeBEpC/wk6rOKbq7hKQawXOJIBXoBjynql2BHRRWOZUkYT+PULvMAKAN0Ayog1W7FZcs340K82ARh0SkOhYoXlfVjNDutSLSNPR8U+Cn0P6I1jOPUycB/UXkO2A8VhU1GmgQWtMd9n2/pa35niiygWxVnRV6/DYWPJLxu3Em8K2qrlPVHCAD6EXyfjcqzINFnBERwZajXayqTxR5quh65ldibRkF+68I9Xw5EdhSUCUR71T1TlVtoappWOPlR6p6KTADW9Md9v8sSlrzPSGo6hrgBxE5KrSrN7Y0cdJ9N7DqpxNFpHbof6bgs0jK70Zl8EF5cUZETgb+DXxNYT39XVi7xZtAK+wfZZCqbgz9ozwDnAv8DPxaVRNu/VkROQ24TVX7ikhbrKRxKPAVcJmq7haRWsA4rJ1nIzBEVVdEK89BEJEuwItADWAF8GvsR2HSfTdE5I/AYKwH4VfA1VjbRFJ+NyrKg4VzzrmwvBrKOedcWB4snHPOheXBwjnnXFgeLJxzzoXlwcI551xYHixcwhCR7QEff6iINCvy+DsROawCx3sjNNvrb4rtP0pEPhaRuaGZY8eG9ncRkT4HOF66iDxd3vw4dyCp4ZM450KGAguohFHOInI40EtVW5fw9NPYzKjvhtIeF9rfBUgHppZwvNTQGImEGSfhYouPs3AJQ0S2q2rdYvsaAc9jA9LA5tL6TETuD+1rG7odrapPh15zD3ApNrHcemwCuu+AV4FVwE5sxtLFwN+AfkB1bLDbkmLnr4VN/Z2ODQ77rarOEJH52NTgS4GbVPXfRV4zHxsgN6fIvhrAcuCgUB4eAY7B5j1KC+VzLIUDE8v0/lR1VGSfsktWXg3lEt1T2K/07sBF2OjmAkdj01b3AO4Tkeoikh5K1xW4ELvIo6pvY7/aL1XVLqq6M3SM9araDQsIt5Vw/htDrz8OuAT4WyiA9Ae+CR3r38Ve8yTwkYi8H1rAp4Gq7gHuBSaEXjMhlPZ4YICq/qqEc0f8/pwLx6uhXKI7E+hgM1sAUE9EDg7dn6Kqu4HdIvIT0AQ4GXi3IBiIyOQwxy+YyHEOdvEt7mTgLwCqukREVgLtgVJnClbVV0RkGjYNxwDgWhHpXEryzCKBq7jKeH/OAR4sXOKrhi1qs88FNRQ8dhfZlYf9P5Q0VfWBFByj4PXFlfV4AKjqauBl4GURWQAcW0rSHRHkrWj+ypUf57wayiW6fwIjCh6EJto7kJlAv9B6zXWBXxZ5bhtwcMkvK9WnWPsAItIeaz9YeqAXiMi5oWnoCxrCG2LtFOU5f3EHen/OlcqDhUsktUUku8j2W0LrMIe6qC4CrjvQAVR1NjZd9TysiikLWzUNrIH7+VCX1oMizNOzQIqIfA1MAIaGqoYO5GxggYjMw5b7vD00/fgMrEptrogMjvD8+wjz/pwrlfeGcq4YEamrqttFpDZWMhiuql9GO1+VJdHfnwuGt1k4t7+xItIBqAX8LQEvpIn+/lwAvGThnHMuLG+zcM45F5YHC+ecc2F5sHDOOReWBwvnnHNhebBwzjkXlgcL55xzYf0/eYZlS6cFAWIAAAAASUVORK5CYII=\n",
"text/plain": [
"<Figure size 432x288 with 1 Axes>"
]
},
"metadata": {
"needs_background": "light"
},
"output_type": "display_data"
}
],
"source": [
"# Plot for rolling hash vs cuckoo hash for length of string\n",
"import time\n",
"\n",
"hash1=[]\n",
"hash2=[]\n",
"\n",
"for x in range(50, 1000, 50):\n",
" time1 = 0\n",
" time2 = 0\n",
" for i in range(50): #Iterations\n",
" text1 = random.choices(letters, k=x)\n",
" text2 = random.choices(letters, k=x)\n",
" text1= ''.join(text1)\n",
" text2 = ''.join(text2)\n",
" \n",
" begin = time.time()\n",
" H1.rh_get_match(text1,text2, 4)\n",
" end = time.time()\n",
" time1+= (end-begin)\n",
"\n",
" begin = time.time()\n",
" H2.regular_get_match(text1, text2, 4)\n",
" end = time.time()\n",
" time2+= (end-begin)\n",
"\n",
" hash1.append(time1/50)\n",
" hash2.append(time2/50)\n",
" \n",
"k = [k for k in range(50, 1000, 50)]\n",
"plt.plot(k,hash1, color = 'red', label = 'Rolling Hash')\n",
"plt.plot(k,hash2, color = 'blue', label = 'Cuckoo Hash')\n",
"plt.xlabel(\"Length of String\")\n",
"plt.ylabel(\"Time\")\n",
"plt.legend()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"When we vary the length of substring instead and keep the length of the string constant, we can see that rolling hash runs at constant time (ie: its complexity is not dependent on the length of the substring) and the same with cuckoo hash except it has a larger constant."
]
},
{
"cell_type": "code",
"execution_count": 17,
"metadata": {
"scrolled": true
},
"outputs": [
{
"data": {
"text/plain": [
"<matplotlib.legend.Legend at 0x232a13f3dd8>"
]
},
"execution_count": 17,
"metadata": {},
"output_type": "execute_result"
},
{
"data": {
"image/png": "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\n",
"text/plain": [
"<Figure size 432x288 with 1 Axes>"
]
},
"metadata": {
"needs_background": "light"
},
"output_type": "display_data"
}
],
"source": [
"# Plot for rolling hash vs cuckoo hash for length of substring\n",
"\n",
"text1 = random.choices(letters, k=200)\n",
"text2 = random.choices(letters, k=200)\n",
"text1= ''.join(text1)\n",
"text2 = ''.join(text2)\n",
"\n",
"hash1=[]\n",
"hash2=[]\n",
"\n",
"for x in range(3, 50):\n",
" time1 = 0\n",
" time2 = 0\n",
" for i in range(50): #Iterations\n",
" begin = time.time()\n",
" H1.rh_get_match(text1,text2, x)\n",
" end = time.time()\n",
" time1+= (end-begin)\n",
"\n",
" begin = time.time()\n",
" H2.regular_get_match(text1, text2,x)\n",
" end = time.time()\n",
" time2+= (end-begin)\n",
"\n",
" hash1.append(time1/50)\n",
" hash2.append(time2/50)\n",
" \n",
"k = [k for k in range(3,50)]\n",
"plt.plot(k,hash1, color = 'red', label = 'Rolling Hash')\n",
"plt.plot(k,hash2, color = 'blue', label = 'Cuckoo Hash')\n",
"plt.xlabel('Length of Substring')\n",
"plt.ylabel(\"Time\")\n",
"plt.legend()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Therefore, rolling hash is a better plagiarism detector because even though they have the same time complexity cuckoo hash has a bigger constant which makes it always takes longer than rolling hash. Both have the same accuracy as proved before, outputing the same similarity. So if it was a choice between rolling hash and cuckoo hash as described here, rolling hash would no doubt win."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Levenshtein Distance (Dynamic Programming)\n",
"Levenshtein Distance measures how similar two texts by computing the minimum number of edits (substitution, insertion or deletion) are required to change one text to the other. \n",
"\n",
"It offers another approach to detecting plagiarism which works better with the way plagiarism usually occurs. It is too obvious for students to copy exact paragrpahs or chunks of texts from their peers. Usually, sentences are copied or paragraphs are paraphrased to sound as if it were their own. Compared to the approach of the longest substring which can fail to detect plagiarism if students paraphrase, edit distance is able to detect paraphrasing to far better, although it is certainly not perfect.\n",
"\n",
"### Why Dynamic Programming?\n",
"\n",
"A dynamic programming approach was chosen for three reasons.\n",
"\n",
"1) Globally optimal solution\n",
"This approach checks every possible combination to ensure we arrive at the minimum value.\n",
"\n",
"Contrast this with the greedy version of this algorithm which doesn't guarantee a globally optimal solution. One way is to substitute every character in s2 with s1 unless it matches, then delete any extra characters. \n",
"\n",
"Example (abbaa, aaa)\n",
"\n",
"Greedy solution (substitute -> delete): 4 steps\n",
"\n",
"Optimal solution: 2 steps\n",
"\n",
"It doesn't guarantee an optimal solution in the way a dynamic programming algorithm does.\n",
"\n",
"2) Time-memory trade off\n",
"\n",
"Dynamic programming uses a bottom-up approach where we find the solution to each subproblem (comparing a substring to another substring that gets increasingly long) and store each computation to use to build our next solution.\n",
"\n",
"If we did not store each computation, we would need to compare all n substrings of s1 with all m substrings of s2 which yields nm comparisons. For each of these comparisons, we would need to find which combination of edits yields the minimum steps by calculating the distance for each combo. This will take $O(3^{nm})$ to compute which is an exponential growth of time complexity.\n",
"\n",
"Between 'math' and 'cash', you would compare 'm' with 'c', 'ca', 'cas' and 'cash'. The first pair would require 3 comparisons of whether deleting, substituting or inserting would give the minimum edits. The second pair would repeat the first comparisons and add on another comparisons. Keep doing that and you can see why the number of operations increases exponentially.\n",
"\n",
"With dynamic programming, each computation is stored which increases the memory used by the algorithm, but for good reason. We would still make nm comparisons, but finding instead of calculating the minimum steps, we would reuse the solutions we gained previously. For example, if we know the minimum edits for 'abba' to 'aaa' to find the minimum edits for 'abbaa', we would only need to compare whether adding, deleting or inserting would yield the smallest edits. We can see the complexity from the code where there are two for loops through n and m that perform constant time operations which makes the complexity $O(nm)$. This is a big improvement from an exponential growth."
]
},
{
"cell_type": "code",
"execution_count": 18,
"metadata": {},
"outputs": [],
"source": [
"class levenshtein(hashing):\n",
" def __init__(self):\n",
" super().__init__()\n",
" \n",
" def edit_distance(self,x,y):\n",
" x,y = self.data_prep(x,y)\n",
" \n",
" m= [[None for i in range(len(y))] for i in range(len(x))] #Create matrix\n",
" \n",
" m[0][0] = 0\n",
"\n",
" for i in range(1,len(x)):\n",
" m[i][0] = i\n",
" \n",
" for j in range(1,len(y)):\n",
" m[0][j] = j\n",
"\n",
" for j in range(1,len(y)):\n",
" for i in range(1,len(x)):\n",
" if x[i] == y[j]:\n",
" change = 0\n",
" else:\n",
" change = 1\n",
" \n",
" # Choose the minimum edit type\n",
" m[i][j] = min(m[i-1][j-1] + change, #Substitute\n",
" m[i-1][j]+1, #Delete\n",
" m[i][j-1]+1) #Insert\n",
" \n",
" return m[-1][-1]/max(len(x), len(y)) #Percentage similarity "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Test #1\n",
"The algorithm passes all the same tests as the LCS approach since it utilises many of the same functions from class inheritance. It outputs how similar the two string are based on edit distance"
]
},
{
"cell_type": "code",
"execution_count": 19,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Similarity: 75.0%\n"
]
}
],
"source": [
"H3 = levenshtein()\n",
"similarity = H3.edit_distance(\"2d,a,y is Mon D A Y!!!\", \"day\")\n",
"print(f\"Similarity: {similarity*100}%\")"
]
},
{
"cell_type": "code",
"execution_count": 20,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Similarity: 83.625%\n"
]
}
],
"source": [
"similarity = H3.edit_distance(tangled, healing)\n",
"print(f\"Similarity: {similarity*100}%\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Test #2 Comparing Speeches"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The following test illustrates how edit distance could be used to compare similarity across a class's assignments using speeches as a proxy. Note that the results are reflected along the diagonal line because the algorithm functions the same regardless of which text is input first."
]
},
{
"cell_type": "code",
"execution_count": 21,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
" Faruqi Kennedy King Thunberg Havel\n",
"Faruqi NaN 0.857143 0.833333 0.875 0.666667\n",
"Kennedy 0.857143 NaN 0.714286 0.625 0.714286\n",
"King 0.833333 0.714286 NaN 0.625 0.800000\n",
"Thunberg 0.875000 0.625000 0.625000 NaN 0.750000\n",
"Havel 0.666667 0.714286 0.800000 0.750 NaN\n"
]
}
],
"source": [
"# Mehreen Faruqi - Black Lives Matter in Australia: https://bit.ly/CS110-Faruqi\n",
"# John F. Kennedy - The decision to go to the Moon: https://bit.ly/CS110-Kennedy\n",
"# Martin Luther King Jr. - I have a dream: https://bit.ly/CS110-King\n",
"# Greta Thunberg - UN Climate Summit message: https://bit.ly/CS110-Thunberg\n",
"# Vaclav Havel - Address to US Congress after the fall of Soviet Union: https://bit.ly/CS110-Havel\n",
"\n",
"import urllib.request\n",
"\n",
"speakers = ['Faruqi', 'Kennedy', 'King', 'Thunberg', 'Havel']\n",
"bad_chars = [';', ',', '.', '?', '!', '_', '[', ']', ':', '“', '”', '\"', '-', '-']\n",
"\n",
"m = []\n",
"\n",
"for speaker in speakers:\n",
" speech = urllib.request.urlopen(f'https://bit.ly/CS110-{speaker}')\n",
" row = []\n",
" for speaker2 in speakers:\n",
" if speaker2!= speaker:\n",
" speech2 = urllib.request.urlopen(f'https://bit.ly/CS110-{speaker2}')\n",
" similarity = H3.edit_distance(speaker, speaker2)\n",
" row.append(similarity)\n",
" else:\n",
" row.append(None)\n",
" \n",
" m.append(row)\n",
" \n",
"df = pd.DataFrame(m,index = speakers, columns = speakers)\n",
"print(df)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Plagiarism Detector Battles (Longest Common Substring vs Edit Distance)\n",
"So which should be used? \n",
"\n",
"The test below shows that edit distance's complexity grows at $O(n^2)$ whereas LCS grows at $O(n)$ which makes the latter computationally much faster especially when dealing with assignments that can run up to 4000 words or with Capstone projects 10,000 words which translated to characters runs in the magnitude of $10^5$. Additionally, you would have to make xC2 comparisons, where x is the number of students. If in one class there were 15 students that would be 105 comparisons. This makes the complexity growth especially salient.\n",
"\n",
"The memory used to store each solution in edit distance approach is also significantly more than LCS which doesn't store any values except for the most recent call."
]
},
{
"cell_type": "code",
"execution_count": 22,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"<matplotlib.legend.Legend at 0x232a146fba8>"
]
},
"execution_count": 22,
"metadata": {},
"output_type": "execute_result"
},
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAYsAAAEKCAYAAADjDHn2AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDMuMC4yLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvOIA7rQAAIABJREFUeJzt3Xd8VGX2+PHPSShBeomA0hWlBQJEFpEVXBRQKRaUIKJYQFDE8nUX/bGK4toLirIiqxhXFBBUYBHFgqgoCkFAmgUsgCC9iISS5Pz+eCbJJJlkEpLJTWbO+/WaF5lb5p65zNwzT7nPI6qKMcYYk58orwMwxhhT+lmyMMYYE5QlC2OMMUFZsjDGGBOUJQtjjDFBWbIwxhgTlCULY4wxQVmyMMYYE5QlC2OMMUGV8zqA4lKnTh1t0qSJ12EYY0yZsmLFit2qGhtsu7BJFk2aNCE5OdnrMIwxpkwRkV8Lsp1VQxljjAnKkoUxxpigLFkYY4wJKmzaLAI5fvw4W7du5ciRI16HYkyBxMTE0KBBA8qXL+91KMZkE9bJYuvWrVStWpUmTZogIl6HY0y+VJU9e/awdetWmjZt6nU4xmQT1tVQR44coXbt2pYoTJkgItSuXdtKwqZUCutkAViiMGWKfV5NaRX2ycIYY8LZW2/BG2+E/jiWLEKsSpUqXoeQafHixXz55Zd5rn/vvfdISEigZcuWtGjRgrvuuqsEoyua+fPn0759e9q1a0erVq148cUX891+8eLF9OnTp8Cvn5SUxLZt2/Jcf9999/HRRx8V+PWMKQ5ffAGDB8O//w1paaE9Vlg3cJvsFi9eTJUqVejSpUuudWvXrmXUqFG8++67tGjRgtTUVKZMmeJBlIV3/Phxhg8fzrJly2jQoAFHjx7ll19+KdZjJCUl0aZNG0455ZRc69LS0hg/fnyxHs+YYL7/Hvr1g0aNYM4ciI4O7fGsZOGBX3/9lR49etC2bVt69OjB5s2bARg6dCijR4+mS5cuNGvWjNmzZwOQnp7OzTffTOvWrenTpw8XXXRR5roVK1bQrVs3OnbsSK9evdi+fTsAEydOpFWrVrRt25bExER++eUXJk+ezIQJE4iPj+fzzz/PFtPjjz/O2LFjadGiBQDlypXj5ptvDhrvyJEjOe+882jWrBmffvop119/PS1btmTo0KGZr12lShXGjBlDx44dOf/881m2bBndu3enWbNmzJs3D3CdEa677jri4uJo3749n3zyCeAu0pdddhm9e/emefPm/OMf/8h1Pv/44w9SU1OpXbs2ABUrVuTMM8/MjDHjXGXEkuHgwYNceumltGrVihEjRpCenk5aWhpDhw6lTZs2xMXFMWHCBGbPnk1ycjKDBw8mPj6elJQUmjRpwvjx4+natSuzZs3KdpwmTZowbtw4OnToQFxcHN999x0Au3bt4oILLqBDhw7cdNNNNG7cmN27dxfik2OMs2MHXHihSxDvvQd16pTAQVU1LB4dO3bUnNavX5/15LbbVLt1K97HbbflOmZOlStXzrWsT58+mpSUpKqqL7/8svbv319VVa+99lodMGCApqWl6bp16/S0005TVdVZs2bphRdeqGlpabp9+3atUaOGzpo1S48dO6Znn3227ty5U1VVZ8yYodddd52qqtavX1+PHDmiqqr79u1TVdVx48bpE088ETDO9u3b66pVqwKuyy/egQMHanp6us6ZM0erVq2q3377raalpWmHDh105cqVqqoK6IIFC1RV9ZJLLtELLrhAjx07pqtWrdJ27dqpquqTTz6pQ4cOVVXVDRs2aMOGDTUlJUVfeeUVbdq0qe7fv19TUlK0UaNGunnz5lwx3nDDDRobG6uJiYk6bdo0TUtLy4xx1qxZuf4/PvnkE61YsaJu2rRJU1NT9fzzz9dZs2ZpcnKynn/++ZnbZ5y7bt266fLlyzOXN27cWB977LHM5/7Hady4sU6cOFFVVSdNmqQ33HCDqqrecsst+vDDD6uq6nvvvaeA7tq1K9d7yfa5NSaHQ4dUExJUK1VS/frror8ekKwFuMZaycIDS5cu5aqrrgJgyJAhLFmyJHPdJZdcQlRUFK1atWLHjh0ALFmyhCuuuIKoqCjq1avHeeedB8D333/P2rVrueCCC4iPj+df//oXW7duBaBt27YMHjyYadOmUa5c0Wob84u3b9++iAhxcXHUrVuXuLg4oqKiaN26dWZVUIUKFejduzcAcXFxdOvWjfLlyxMXF5e5zZIlSxgyZAgALVq0oHHjxvzwww8A9OjRg+rVqxMTE0OrVq349dfc45699NJLfPzxx3Tq1Iknn3yS66+/Puj76tSpE82aNSM6OppBgwaxZMkSmjVrxk8//cStt97K+++/T7Vq1fLcf+DAgXmuu+yyywDo2LFjtveYmJgIQO/evalZs2bQGI3xl5oKiYnwzTcwcyZ06lRyx46cNotnnvE6gjz5d5esWLFi5t8u6Wf9m5Oq0rp1a5YuXZpr3bvvvstnn33GvHnzePDBB1m3bl2+MbRu3ZoVK1bQrl27E4o3KioqW+xRUVGkpqYCUL58+cx9/Lfz3yav9+h/DIDo6OjMfXKKi4sjLi6OIUOG0LRpU5KSkihXrhzp6emZxzh27FjA95HxvGbNmqxevZqFCxcyadIk3nzzTaZOnRrweJUrVw4as3+8+b1HY4JRhVtvhfnzXYN2374le3wrWXigS5cuzJgxA4DXX3+drl275rt9165deeutt0hPT2fHjh0sXrwYgDPPPJNdu3ZlJovjx4+zbt060tPT2bJlC+eddx6PP/44+/fv59ChQ1StWpU//vgj4DH+/ve/8/DDD2f+mk9PT+fpp58+oXhPxLnnnsvrr78OwA8//MDmzZsz2x2COXToUOY5AVi1ahWNGzcGXPvBihUrAJg7dy7Hjx/P3G7ZsmX8/PPPpKenM3PmTLp27cru3btJT0/n8ssv58EHH+Sbb74ByPfcFVTXrl158803Afjggw/Yt29fkV7PRJbHHoPJk2HMGBg5suSPHzklC48cPnyYBg0aZD6/8847mThxItdffz1PPPEEsbGxvPLKK/m+xuWXX87HH39MmzZtOOOMM/jLX/5C9erVqVChArNnz2b06NEcOHCA1NRUbr/9ds444wyuvvpqDhw4gKpyxx13UKNGDfr27cuAAQOYO3cuzz33HH/9618zj9G2bVueeeYZBg0axOHDhxERLr74YoBCx3sibr75ZkaMGEFcXBzlypUjKSkpW4kiP6rK448/zk033USlSpWoXLkySUlJAAwbNoz+/fvTqVMnevToka00cPbZZ3P33XezZs0azj33XC699FLWrFnDddddl1kaeeSRRwDXUD5ixAgqVaoUsCRXEOPGjWPQoEHMnDmTbt26Ub9+fapWrXpCr2UiyxtvwD33wKBB8PDD3sQg4VI0TkhI0JyTH23YsIGWLVt6FFHxOnToEFWqVGHPnj106tSJL774gnr16nkdlimEo0ePEh0dTbly5Vi6dCkjR45k1apVubYLp8+tKbpPPoFeveCcc+D996GAv6EKTERWqGpCsO2sZFFG9OnTh/37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\n",
"text/plain": [
"<Figure size 432x288 with 1 Axes>"
]
},
"metadata": {
"needs_background": "light"
},
"output_type": "display_data"
}
],
"source": [
"# Plot for rolling hash vs levenshtein distance for length of string\n",
"hash1=[]\n",
"hash2=[]\n",
"\n",
"for x in range(50, 500, 50):\n",
" text1 = random.choices(letters, k=x)\n",
" text2 = random.choices(letters, k=x)\n",
" text1= ''.join(text1)\n",
" text2 = ''.join(text2)\n",
" \n",
" time1 = 0\n",
" time2 = 0\n",
" \n",
" for i in range(50): #Iterations\n",
" begin = time.time()\n",
" H1.lcs(text1,text2)\n",
" end = time.time()\n",
" time1+= (end-begin)\n",
"\n",
" begin = time.time()\n",
" H3.edit_distance(text1, text2)\n",
" end = time.time()\n",
" time2+= (end-begin)\n",
"\n",
" hash1.append(time1/50)\n",
" hash2.append(time2/50)\n",
" \n",
"k = [k for k in range(50, 500, 50)]\n",
"plt.plot(k,hash1, color = 'red', label = 'Longest Common Substring')\n",
"plt.plot(k,hash2, color = 'blue', label = 'Edit Distance')\n",
"plt.xlabel('Length of String')\n",
"plt.ylabel(\"Time\")\n",
"plt.legend()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The tradeoff comes with accuracy. Conceptually, edit distance does a far better job at determining how similar two texts are. The example below illustrates the difference. Sentence 2 is obviously a paraphrasing of sentence one which the edit distance detects much better than LCS. "
]
},
{
"cell_type": "code",
"execution_count": 23,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Edit: 72.72727272727273%\n",
"LCS: 27.27272727272727%\n"
]
}
],
"source": [
"sent1 = \"Come at me, bro\"\n",
"sent2 = \"My bro told me to come at him\"\n",
"\n",
"edit = H3.edit_distance(sent1, sent2)\n",
"lcs = H1.lcs(sent1,sent2)\n",
"\n",
"print(f\"Edit: {edit*100}%\")\n",
"print(f\"LCS: {lcs}%\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"I believe that the accuracy of the edit distance approach far outweighs LCS since it provides the closest solution to the aims of a plagiarism detector that is to detect similarity. The edit distance algorithm could potentially be optimised to improve its complexity, but with LCS no amount of optimisation will change the flaw in its approach. "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Considerations for Improvement\n",
"\n",
"One limitation is that it is difficult to check if a student has plagiarised from a variety of sources (or students), since the algorithms only check between two whole bodies of text. To extend the algorithm, the text can be broken into sections where each section is compared to other texts and returns a weighted score of the aggregate similarity of one text to a corpus of texts. \n",
"\n",
"Additionally, the edit distance algorithm could be extended to better detect similarities by detecting similar usage of words in cases when students paraphrase by swapping out words with their synonyms rather than just rearranging the sentence. Instead of analysing characters, the algorithm could analyse whole words and boil them down to root words. These root words can be compared to a dictionary of synonyms to determine how similar two texts are based on that. This would require more insights from a natural language processing perspective to ensure that the semantics of the words are retained."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Appendix\n",
"\\#DataStructures - Effectively used hash table to design a LCS approach to building a plagiarism detector. Clearly explained why a hash table was a better choice compared to a list. Provided evidence using complexity analysis why it is a better alternative despite the tradeoff with memory in the context of the problem.\n",
"\n",
"\\#PythonProgramming - Provided clear explanation to choice of using classes to code each version of the plagiarism detector. Code succcessfully passed all verification tests. Justified assumptions of how code works using test cases.\n",
"\n",
"I recognise that the execution of the cuckoo hash was not as elegant or clean as it could be, but given the time constraints, I left the code in a state where it was functional, but with room to be optimised for clarity.\n",
"\n",
"\\#DynamicProgramming - Applied dynamic programming to code the algorithm for Levenshtein distance which significantly reduced its complexity compared to a non-dynamic approach. Illustrated that the algorithm works as a plagiarism detector. Explained clearly how dynamic programming was preferable to a naive solution and a greedy approach within the context of the problem. Provided evidence for this through a complexity analysis and examination of the time-memory trade-off. \n",
"\n",
"\\#ComplexityAnalysis - Used appropriate notation to describe the complexity of each of the three versions of the plagiarism detector. Explained through the structure of the code and through timing the algorithms, why the asymptotic analysis was valid.\n",
"\n",
"\\#CodeReadability - Thoroughly commented code following guidelines and had consistent naming conventions. Included useful error messages for bad inputs. Used inheritance and polymorphism from Object Oriented Programming to reduce repeated code and make the overall document more readable.\n",
"\n",
"\\#ComputationalSolutions - Created a flowchart to illustrate how the cuckoo hash works to store all common substrings. Clearly explained the goal of the plagiarism detector and defined a scope for its use cases. Contrasted two different choices of hash functions to show how one hash function significantly improved the complexity. Contrasted two different approaches to creating a plagiarism detector, examining their tradeoffs and merits in the context of the problem.\n",
"\n",
"\\#ComputationalCritique - Analysed the growth of space and time requirements for each algorithm to determine which solution was most appropriate for a plagiarism detector. Provided well-justified explanation to why edit distance approach was better than LCS despite its worse time complexity and memory usage.\n",
"\n",
"\\#algorithms -Provided flowchart to better illustrate how the algorithm works step-by-step. Justified why edit distance was a better than LCS considering its space and time complexity and accuracy in relation to the context. Successfully implemented both approaches, with clearly explained code and in-line comments. Code written with class structure to reduce redundancy.\n",
"\n",
"\\#rightproblem - Clearly characterised the nature of the problem and specified on what scale the plagiarism detector works to solve the problem. Explained how the approach fails under certain contexts and provided suggestions on extensions. Corrected ineffective approach of LCS in terms of accuracy with the edit distance approach and justified why edit distance was a better solution.\n",
"\n",
"\\#optimization - Explained how edit distance optimised for the cases when LCS was not able to detect plagiarism. Successfully applied new approach and the degree to which it creates an effective plagiarism detector. Provided suggestions for improving the current plagiarism detector of edit distance using natural language processing."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## LO Reflections\n",
"My worst LO for the majority of the semester was for complexity analysis because it was complicated and I couldn't understand the equations. But with practice over every pre-class work and assignment, and attending office hours with TAs, I am more confident in my ability to come up with a complexity analysis given code and know how to run tests to determine time complexity. \n",
"\n",
"I would say I saw big improvement in my Python programming in understanding how the two 'hats' of practical vs engineer come into play while writing code to solve a problem. I believe my ability to use classes has drastically improved partially because of the Tries Tree assignment and also because of external coding projects. In general, my improvements in the LOs clearly reflect the improvement of my understanding of computational concepts!"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.1"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment