diff --git a/examples/simple-mag/mag-distribution.png b/examples/simple-mag/mag-distribution.png
index a724ab1..c3246cc 100644
Binary files a/examples/simple-mag/mag-distribution.png and b/examples/simple-mag/mag-distribution.png differ
diff --git a/examples/simple-mag/train/index.html b/examples/simple-mag/train/index.html
index 8cdd2cd..65bb54d 100644
--- a/examples/simple-mag/train/index.html
+++ b/examples/simple-mag/train/index.html
@@ -1800,7 +1800,7 @@ This notebook shows that better results can be achieved without even using trans
importjsonfromtypingimportTuple
-fromgreat_ai.utilitiesimportclean,parallel_map
+fromgreat_ai.utilitiesimportclean,simple_parallel_mapfromtqdm.cliimporttqdm
@@ -1810,22 +1810,22 @@ This notebook shows that better results can be achieved without even using trans
return(clean(data_point["text"],convert_to_ascii=True),data_point["label"])
-withopen("mag/train.txt",encoding="utf-8")asf:
- training_data=list(tqdm(parallel_map(preprocess,f.readlines())))
+withopen("../../tutorial/data/train.txt",encoding="utf-8")asf:
+ training_data=simple_parallel_map(preprocess,f.readlines())X_train=[d[0]fordintraining_data]y_train=[d[1]fordintraining_data]
-withopen("mag/test.txt",encoding="utf-8")asf:
- test_data=list(tqdm(parallel_map(preprocess,f.readlines())))
+withopen("../../tutorial/data/test.txt",encoding="utf-8")asf:
+ test_data=simple_parallel_map(preprocess,f.readlines())X_test=[d[0]fordintest_data]y_test=[d[1]fordintest_data]
import json
from typing import Tuple
-from great_ai.utilities import clean, parallel_map
+from great_ai.utilities import clean, simple_parallel_map
from tqdm.cli import tqdm
@@ -1835,15 +1835,15 @@ def preprocess(line: str) -> Tuple[str, str]:
return (clean(data_point["text"], convert_to_ascii=True), data_point["label"])
-with open("mag/train.txt", encoding="utf-8") as f:
- training_data = list(tqdm(parallel_map(preprocess, f.readlines())))
+with open("../../tutorial/data/train.txt", encoding="utf-8") as f:
+ training_data = simple_parallel_map(preprocess, f.readlines())
X_train = [d[0] for d in training_data]
y_train = [d[1] for d in training_data]
-with open("mag/test.txt", encoding="utf-8") as f:
- test_data = list(tqdm(parallel_map(preprocess, f.readlines())))
+with open("../../tutorial/data/test.txt", encoding="utf-8") as f:
+ test_data = simple_parallel_map(preprocess, f.readlines())
X_test = [d[0] for d in test_data]
y_test = [d[1] for d in test_data]
@@ -1868,8 +1868,8 @@ y_test = [d[1] for d in test_data]
@@ -2268,6 +2270,41 @@ add_ground_truth(X, y, train_split_ratio=0.8, test_split_ratio=0.2)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Environment variable ENVIRONMENT is not set, defaulting to development mode ‼️
+Cannot find credentials files, defaulting to using ParallelTinyDbDriver
+The selected tracing database (ParallelTinyDbDriver) is not recommended for production
+Cannot find credentials files, defaulting to using LargeFileLocal
+GreatAI (v0.1.6): configured ✅
+ 🔩 tracing_database: ParallelTinyDbDriver
+ 🔩 large_file_implementation: LargeFileLocal
+ 🔩 is_production: False
+ 🔩 should_log_exception_stack: True
+ 🔩 prediction_cache_size: 512
+ 🔩 dashboard_table_size: 50
+You still need to check whether you follow all best practices before trusting your deployment.
+> Find out more at https://se-ml.github.io/practices
+
+
+
+
+
+
+
+
@@ -2290,7 +2327,7 @@ add_ground_truth(X, y, train_split_ratio=0.8, test_split_ratio=0.2)
from great_ai import query_ground_truth
+
+data = query_ground_truth("train")
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Environment variable ENVIRONMENT is not set, defaulting to development mode ‼️
+Cannot find credentials files, defaulting to using ParallelTinyDbDriver
+The selected tracing database (ParallelTinyDbDriver) is not recommended for production
+Cannot find credentials files, defaulting to using LargeFileLocal
+GreatAI (v0.1.6): configured ✅
+ 🔩 tracing_database: ParallelTinyDbDriver
+ 🔩 large_file_implementation: LargeFileLocal
+ 🔩 is_production: False
+ 🔩 should_log_exception_stack: True
+ 🔩 prediction_cache_size: 512
+ 🔩 dashboard_table_size: 50
+You still need to check whether you follow all best practices before trusting your deployment.
+> Find out more at https://se-ml.github.io/practices
+
from collections import Counter
+import matplotlib.pyplot as plt
+from great_ai.utilities import unique
+import numpy as np
+import matplotlib.ticker as mtick
+
+
+first = Counter(d.feedback[0] for d in data if len(d.feedback) > 0)
+second = Counter(d.feedback[1] for d in data if len(d.feedback) > 1)
+third = Counter(d.feedback[2] for d in data if len(d.feedback) > 2)
+
+domains = sorted(unique(domain for d in data for domain in d.feedback))
+
+first = [first[d] / len(data) for d in domains]
+second = [second[d] / len(data) for d in domains]
+third = [third[d] / len(data) for d in domains]
+
+# Configure matplotlib to have nice, high-resolution charts
+%matplotlib inline
+plt.rcParams["figure.facecolor"] = "white"
+plt.rcParams["font.size"] = 20
+plt.rcParams["figure.figsize"] = (14, 10)
+
+fig, ax = plt.subplots()
+
+plt.xticks(rotation=90)
+
+ax.bar(domains, first, label="Primary domain")
+ax.bar(domains, second, label="Secondary domain", bottom=first)
+ax.bar(domains, third, label="Tertiary domain", bottom=np.add(first, second))
+ax.yaxis.set_major_formatter(mtick.PercentFormatter(1))
+ax.legend()
+ax.set_ylabel("Ratio of all publications")
+fig.tight_layout()
+fig.savefig("ss-distribution.png", dpi=500)
+None
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Last update:
+ July 16, 2022
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/simple/stacked-bars/stacked-bars.ipynb b/examples/simple/stacked-bars/stacked-bars.ipynb
new file mode 100644
index 0000000..5d0e296
--- /dev/null
+++ b/examples/simple/stacked-bars/stacked-bars.ipynb
@@ -0,0 +1,117 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "\u001b[38;5;226mEnvironment variable ENVIRONMENT is not set, defaulting to development mode ‼️\u001b[0m\n",
+ "\u001b[38;5;226mCannot find credentials files, defaulting to using ParallelTinyDbDriver\u001b[0m\n",
+ "\u001b[38;5;226mThe selected tracing database (ParallelTinyDbDriver) is not recommended for production\u001b[0m\n",
+ "\u001b[38;5;226mCannot find credentials files, defaulting to using LargeFileLocal\u001b[0m\n",
+ "\u001b[38;5;39mGreatAI (v0.1.6): configured ✅\u001b[0m\n",
+ "\u001b[38;5;39m 🔩 tracing_database: ParallelTinyDbDriver\u001b[0m\n",
+ "\u001b[38;5;39m 🔩 large_file_implementation: LargeFileLocal\u001b[0m\n",
+ "\u001b[38;5;39m 🔩 is_production: False\u001b[0m\n",
+ "\u001b[38;5;39m 🔩 should_log_exception_stack: True\u001b[0m\n",
+ "\u001b[38;5;39m 🔩 prediction_cache_size: 512\u001b[0m\n",
+ "\u001b[38;5;39m 🔩 dashboard_table_size: 50\u001b[0m\n",
+ "\u001b[38;5;226mYou still need to check whether you follow all best practices before trusting your deployment.\u001b[0m\n",
+ "\u001b[38;5;226m> Find out more at https://se-ml.github.io/practices\u001b[0m\n"
+ ]
+ }
+ ],
+ "source": [
+ "from great_ai import query_ground_truth\n",
+ "\n",
+ "data = query_ground_truth(\"train\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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ftu0aaNx48apT58+2r59u0qWLKlTp07pp59+0ocffqgffvjhma/RwcFBbdu21aRJkyQ9uc3ZEu7u7hozZow++eQTeXp6qlmzZnJyctLGjRt169YtlS9fXr///rvJMS1atNCaNWv0448/qly5cmrUqJEMBoNWr16ts2fPqlmzZmrZsuUzX9PT9O3bV126dJGnp6eaNGmibNmyaefOnTp69Kjef/99rVu3Lt3ODQAAALAyjVdCq1attH//frVs2VK///67Jk+erO+//16nT59WYGCgpk6datF8uXPn1ooVK1S2bFnNmzdPQ4YM0ZAhQ1Ld9fm1117Tr7/+qvr162vHjh2aPHmyzp07p6lTp2rMmDHPe4nGleKCBQuqYcOGFh/fr18/LV68WB4eHpo3b57mzJmjN954Q7t27VLu3LmTPWbJkiWaMmWK8uTJoxkzZmj69OnKnTu3Jk+erMWLFz/X9TxN586dNXfuXBUsWFDz58/XokWL5Obmpr1796br7eUAAACAJBkSUntnFDKEl5eXIiIinjru2LFjSZ4Xxatr3rx5CgoK0uDBgzVixIiMLgcvGP8eAHilBCd/F9gLFxyd0RUASGepZTNWpoGXQGxsrL755hvZ2NhYfIs3AAAAAMvxzDSQhe3YsUNhYWEKDQ3VH3/8oR49eqhw4cIZXRYAAADw0iNMA1nYli1bNGzYMLm4uKhjx4766quvMrokAAAA4JVAmAaysODgYAUHB2d0GQAAAMArh2emAQAAAACwEGEaAAAAAAALEaYBAAAAALAQYTqL4zXhAPh3AAAA4MUjTGdh1tbWevz4cUaXASCDPX78WNbW1hldBgAAwCuFMJ2F5cyZU7dv387oMgBksNu3bytnzpwZXQYAAMArhTCdhbm4uOjmzZu6du2aYmJiuNUTeIUkJCQoJiZG165d082bN+Xi4pLRJQEAALxSeM90FmZra6siRYroxo0bioqKUlxcXEaXBOAFsra2Vs6cOVWkSBHZ2tpmdDkAAACvFLPDdFxcnB49eiQHBweT9m3btmnNmjVycHBQp06d5OHhkeZFImW2trYqWLCgChYsmNGlAAAAAMArw+zbvD/99FO5uLgoOjra2LZ06VLVqVNH3377rcaOHauqVavqwoUL6VIoAAAAAACZhdlhOjw8XP7+/nJycjK2DRs2TM7OzlqwYIG++uor3bp1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NmzQ0qVL5efnpy1btsjR0VHLly/XypUrNXv2bFlZWalnz55ycXFR9uzZ5efnp6NHjz7jtwEAAAAAyEzM3s070b1797RmzRodOHBA0dHRcnJykqenpxo1aiRHR8c0KWrSpEnq3bu3Xn/9de3cuVMuLi5PPSY2NlbVq1fX3r17FRISot69ez/1mFKlSunUqVM6deqUSpQokaS/WrVq2rVrl3bt2qW333471blu3LihsmXLKjAwUJMnT1afPn00bdo0jRs3TqVKlVL//v0VHR2tkydPys7OLsnxM2fO1MyZMyVJV69eferz6QAAAK8sdvMG8IKkyW7eiRwdHdWiRQu1aNHiuQtLzuTJk9W7d2+VLVtWW7duNStIS09uy/7444+1d+9ehYeHmxWmE1eeE1eo/y2x3dnZ+alz9erVS/b29hozZozu3bunadOmqXXr1urVq5ekJ9+br6+vFi9erPbt2yc5vlOnTurUqZMkmWyGBgAAAADIfMy+zftFCAkJUc+ePfXGG29o+/btKlCggEXH582bV5LMvs27dOnSkqSTJ08m6YuNjdXZs2dlY2OjYsWKpTrP+vXrtWjRIn333XfKkSOHzpw5o5iYGFWqVMk4JvH29yNHjphVGwAAAAAg80pxZTpx86/GjRsrZ86cxs/maNOmjcWFjB07VgMHDlTFihW1efNmubq6WjzHnj17JOmp4TdRrVq1tGjRIm3YsEEfffSRSV94eLju378vX19f2drapjhHdHS0OnfurA4dOuidd94x6UvcBVySHj58aO5lAAAAAAAyuRTDdLt27WQwGPTWW28pZ86cxs+pSUhIkMFgsDhMjxgxQl988YUqV66sTZs2pXpr94EDB1SxYsUk76XeunWrJkyYIElq1aqVSV90dLT++usvOTk5qWDBgsb2wMBADRgwQEuXLlXPnj2Nt1c/fPhQgwcPliR17do11do/+eQTSdL48eONbcWLF1f27Nn1008/qW/fvpKkdevWSZLKlSuX6nwAAAAAgMwvxTA9Z84cGQwGY/icO3duuhQwf/58ffHFF7K2tlaNGjU0adKkJGPc3d3Vrl07SVK/fv106tQp+fj4qHDhwpKk33//3fjO6BEjRsjHx8fk+FWrVikoKEht27bVvHnzjO25cuXSd999p8DAQPn5+al58+ZycXHR2rVrdeLECQUGBqpZs2Yp1r5l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lStydXXVp59+avG1AQAAAAAytywZps+cOaPKlStr7ty5qlq1qvr27atixYpp4sSJevvtt3X9+nWz5rl+/brefvttTZw4UcWLF1ffvn1VtWpVzZ07V5UrV1ZkZKTJ+LVr16pVq1ZydHRUly5dFBcXp48++kjr1yf9rWP37t3l4uKiESNGpMk1AwAAAAAyjywZprt166a///5bkyZN0urVqzVmzBht27ZNffv21YkTJzRo0CCz5vnPf/6jkydPql+/ftq6davGjBmj1atXa+LEifr777/VrVs3k/HTpk1TyZIltWPHDn399dfatWuXnJ2dNWXKFJNxy5cv18qVKzV79mzZ29un2XUDAAAAADKHLBemz5w5o02bNsnd3V3du3c36Rs2bJgcHR21cOFC3bt3L9V57t69q4ULF8rR0VHBwcEmfT169FDRokW1ceNGk9Xpc+fOqVKlSrKxefKouZOTk0qVKqVz584Zx9y4cUM9evRQt27dVKNGjee8WgAAAABAZpTlwvT27dslSXXr1pWVlWn5OXPmVLVq1XT//n3t2bMn1Xn27NmjBw8eqFq1asqZM6dJn5WVld59912T80lSkSJFdOjQIcXHx0uSbt++rZMnT6po0aLGMb169ZK9vb3GjBnz7BcJAAAAAMjUslyYPnHihCSpVKlSyfaXLFlSknTy5Mk0n6dLly46ceKEatSoof79+8vHx0e3bt1S165dJUnr16/XokWL9N133ylHjhwWXBUAAAAAICvJcmE6Ojpa0pNbrJOT2H7r1q00n6dRo0aaN2+ebt26palTp8pgMGjhwoV6//33FR0drc6dO6tDhw565513tGLFCr3++uuytraWu7u7Zs6cacllAgAAAAAyMd4zbaG2bduqbdu2Sdo/+eQTSdL48eN14MAB/d///Z+aNGmiKVOmaOXKlercubMKFSqk+vWTf8/fzJkzjYH7+PHj8vLySr+LeE6uaTDH1atXlTdv3mc+3str6FPHUKdlnlYrdVqGOv+HOk09b61ZpU7p1fk3NKvUKb08/y1Rp2Wo83+yyr+hWaVO6cX8G5qRoqKiUuzLcmE6ccU4cWX53xLbnZ2dX8g8krRlyxbNnj1b69atk5OTk8aPH6+cOXNq3rx5cnR0VK1atbRp0yaNHTs2xTDdqVMnderU6annell4eXkpIiIio8t4KupMW9SZtqgz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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "from collections import Counter\n",
+ "import matplotlib.pyplot as plt\n",
+ "from great_ai.utilities import unique\n",
+ "import numpy as np\n",
+ "import matplotlib.ticker as mtick\n",
+ "\n",
+ "\n",
+ "first = Counter(d.feedback[0] for d in data if len(d.feedback) > 0)\n",
+ "second = Counter(d.feedback[1] for d in data if len(d.feedback) > 1)\n",
+ "third = Counter(d.feedback[2] for d in data if len(d.feedback) > 2)\n",
+ "\n",
+ "domains = sorted(unique(domain for d in data for domain in d.feedback))\n",
+ "\n",
+ "first = [first[d] / len(data) for d in domains]\n",
+ "second = [second[d] / len(data) for d in domains]\n",
+ "third = [third[d] / len(data) for d in domains]\n",
+ "\n",
+ "# Configure matplotlib to have nice, high-resolution charts\n",
+ "%matplotlib inline\n",
+ "plt.rcParams[\"figure.facecolor\"] = \"white\"\n",
+ "plt.rcParams[\"font.size\"] = 20\n",
+ "plt.rcParams[\"figure.figsize\"] = (14, 10)\n",
+ "\n",
+ "fig, ax = plt.subplots()\n",
+ "\n",
+ "plt.xticks(rotation=90)\n",
+ "\n",
+ "ax.bar(domains, first, label=\"Primary domain\")\n",
+ "ax.bar(domains, second, label=\"Secondary domain\", bottom=first)\n",
+ "ax.bar(domains, third, label=\"Tertiary domain\", bottom=np.add(first, second))\n",
+ "ax.yaxis.set_major_formatter(mtick.PercentFormatter(1))\n",
+ "ax.legend()\n",
+ "ax.set_ylabel(\"Ratio of all publications\")\n",
+ "fig.tight_layout()\n",
+ "fig.savefig(\"ss-distribution.png\", dpi=500)\n",
+ "None"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3.10.4 ('.env': venv)",
+ "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.10.4"
+ },
+ "orig_nbformat": 4,
+ "vscode": {
+ "interpreter": {
+ "hash": "02dd6d3afbfa9fbbe1037d64ad9014965528a1ccad21929d6e72f466389a68ad"
+ }
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/examples/simple/train/index.html b/examples/simple/train/index.html
index c4fb0ea..473d970 100644
--- a/examples/simple/train/index.html
+++ b/examples/simple/train/index.html
@@ -1939,7 +1939,7 @@ y = [domain for d in data for domain in d.feedback]
Cannot find credentials files, defaulting to using ParallelTinyDbDriverThe selected tracing database (ParallelTinyDbDriver) is not recommended for productionCannot find credentials files, defaulting to using LargeFileLocal
-GreatAI (v0.1.4): configured ✅
+GreatAI (v0.1.6): configured ✅ 🔩 tracing_database: ParallelTinyDbDriver 🔩 large_file_implementation: LargeFileLocal 🔩 is_production: False
@@ -1963,7 +1963,7 @@ y = [domain for d in data for domain in d.feedback]
Bj\\\\\\\\\"{o}rn is \\\\t \\\\\\\\textit{happy} \ud83d\ude42 <3
', ... ignore_xml=True ... ) '
Bj\u00f6rn is happy \ud83d\ude42 lt;3
' Args: text: Text to be cleaned. ignore_xml: Do not process/remove XML-tags. ignore_latex: Do not process/remove LaTeX-tags. remove_brackets: Do not remove brackets ([]) convert_to_ascii: Strip (or convert) non-ascii characters. Returns: The cleaned input text with sensibly collapsed whitespace and optionally no markup. \"\"\" if not ignore_xml : text = re . sub ( r \"<[^>]*>\" , \" \" , text ) text = html . unescape ( text ) if not ignore_latex : text = text . replace ( \"%\" , \" \\\\ %\" ) # escape LaTeX comments before parsing as LaTeX try : text = latex . latex_to_text ( text , tolerant_parsing = True , strict_braces = False ) text = text . replace ( \" %s \" , \" \" ) except : logger . exception ( \"Latex parsing error\" ) if convert_to_ascii : text = unicodedata . normalize ( \"NFKD\" , text ) try : text . encode ( \"ASCII\" , errors = \"strict\" ) except UnicodeEncodeError : text = \"\" . join ([ c for c in text if not unicodedata . combining ( c )]) text = unidecode . unidecode ( text ) text = re . sub ( r \"\\b[a-zA-Z](?:[\\t ]+[a-zA-Z]\\b)+\" , lambda m : re . sub ( r \"[\\t ]\" , \"\" , m [ 0 ]), text ) # A R T I C L E => ARTICLE if remove_brackets : text = re . sub ( r \"\\[[^\\]]*\\]\" , \" \" , text ) # fix hypens: break- word => break-word text = re . sub ( r \"(\\S)-\\s+\" , r \"\\1-\" , text ) text = re . sub ( r \"\\s+-(\\S)\" , r \"-\\1\" , text ) # collapse whitespace text = re . sub ( r \"\\s+\" , \" \" , text ) # fix punctuation text = re . sub ( rf \" ([ { joined_left_punctuations } ])\" , r \"\\1\" , text ) text = re . sub ( rf \"([ { joined_right_punctuations } ]) \" , r \"\\1\" , text ) text = text . strip () return text get_sentences ( text , ignore_partial = False , true_case = False , remove_punctuation = False ) # Return the list of sentences found in the input text. Use syntok to segment the sentences. Further processing can be enabled with optional arguments. Examples: >>> get_sentences ( 'This is a sentence. This is a half' ) ['This is a sentence.', 'This is a half'] >>> get_sentences ( 'This is a sentence. This is a half' , ignore_partial = True ) ['This is a sentence.'] >>> get_sentences ( 'I like Apple.' , true_case = True , remove_punctuation = True ) ['i like Apple'] Parameters: Name Type Description Default text str Text to be segmented into sentences. required ignore_partial bool Filter out sentences that are not capitalised/don't end with a punctuation. False true_case bool Crude method: lowercase the first word of each sentence. False remove_punctuation bool Remove all kinds of punctuation. False Returns: Type Description List [ str ] The found sentences (with partial sentences optionally filtered out). Source code in great_ai/utilities/get_sentences.py 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 def get_sentences ( text : str , ignore_partial : bool = False , true_case : bool = False , remove_punctuation : bool = False , ) -> List [ str ]: \"\"\"Return the list of sentences found in the input text. Use [syntok](https://github.com/fnl/syntok) to segment the sentences. Further processing can be enabled with optional arguments. Examples: >>> get_sentences('This is a sentence. This is a half') ['This is a sentence.', 'This is a half'] >>> get_sentences('This is a sentence. This is a half', ignore_partial=True) ['This is a sentence.'] >>> get_sentences('I like Apple.', true_case=True, remove_punctuation=True) ['i like Apple'] Args: text: Text to be segmented into sentences. ignore_partial: Filter out sentences that are not capitalised/don't end with a punctuation. true_case: Crude method: lowercase the first word of each sentence. remove_punctuation: Remove all kinds of punctuation. Returns: The found sentences (with partial sentences optionally filtered out). \"\"\" tokenizer = Tokenizer ( emit_hyphen_or_underscore_sep = True , replace_not_contraction = False ) token_stream = tokenizer . tokenize ( text ) def process ( sentence : str ) -> str : if true_case : sentence = sentence [ 0 ] . lower () + sentence [ 1 :] # very crude method if remove_punctuation : sentence = re . sub ( punctuations_pattern , \" \" , sentence ) return sentence . strip () sentences = [ process ( tokenizer . to_text ( sentence )) for sentence in segment ( token_stream ) ] if ignore_partial : sentences = [ sentence for sentence in sentences if sentence [ 0 ] . isupper () and sentence [ - 1 ] in sentence_ending_punctuations ] return sentences predict_language ( text ) # Predict the language code from text. A thin wrapper over langcodes for convenient language tagging. Examples: >>> predict_language ( 'This is a sentence.' ) 'en' Parameters: Name Type Description Default text Optional [ str ] Text used for prediction. required Returns: Type Description str The predicted language code (en, en-US) or und if a prediction could not be made. Source code in great_ai/utilities/language/predict_language.py 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 def predict_language ( text : Optional [ str ]) -> str : \"\"\"Predict the language code from text. A thin wrapper over [langcodes](https://github.com/rspeer/langcodes) for convenient language tagging. Examples: >>> predict_language('This is a sentence.') 'en' Args: text: Text used for prediction. Returns: The predicted language code (en, en-US) or `und` if a prediction could not be made. \"\"\" if not text : return Language . make () . to_tag () try : language_code = detect ( text ) except LangDetectException : return Language . make () . to_tag () return Language . get ( language_code ) . to_tag () english_name_of_language ( language_code ) # Human-friendly English name of language from its language_code . A thin wrapper over langcodes for convenient language tagging. Examples: >>> english_name_of_language ( 'en-US' ) 'English (United States)' >>> english_name_of_language ( 'und' ) 'Unknown language' Parameters: Name Type Description Default language_code Optional [ str ] Language code, for example, returned by great_ai.utilities.language.predict_language.predict_language . required Returns: Type Description str English name of language. Source code in great_ai/utilities/language/english_name_of_language.py 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 def english_name_of_language ( language_code : Optional [ str ]) -> str : \"\"\"Human-friendly English name of language from its `language_code`. A thin wrapper over [langcodes](https://github.com/rspeer/langcodes) for convenient language tagging. Examples: >>> english_name_of_language('en-US') 'English (United States)' >>> english_name_of_language('und') 'Unknown language' Args: language_code: Language code, for example, returned by [great_ai.utilities.language.predict_language.predict_language][]. Returns: English name of language. \"\"\" if not language_code : language_code = \"und\" return Language . get ( language_code ) . display_name () is_english ( language_code ) # Decide whether the language_code is of an English language. A thin wrapper over langcodes for convenient language tagging. Examples: >>> is_english ( 'en-US' ) True >>> is_english ( None ) False >>> is_english ( 'und' ) False Parameters: Name Type Description Default language_code Optional [ str ] Language code, for example, returned by ` great_ai.utilities.language.predict_language.predict_language . required Returns: Type Description bool Boolean indicating whether it's English. Source code in great_ai/utilities/language/is_english.py 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 def is_english ( language_code : Optional [ str ]) -> bool : \"\"\"Decide whether the `language_code` is of an English language. A thin wrapper over [langcodes](https://github.com/rspeer/langcodes) for convenient language tagging. Examples: >>> is_english('en-US') True >>> is_english(None) False >>> is_english('und') False Args: language_code: Language code, for example, returned by `[great_ai.utilities.language.predict_language.predict_language][]. Returns: Boolean indicating whether it's English. \"\"\" if not language_code : language_code = \"und\" language_code = standardize_tag ( language_code ) return tag_distance ( language_code , \"en\" ) < 15 evaluate_ranking ( expected , actual_scores , target_recall , title = '' , disable_interpolation = False , axes = None , output_svg = None , reverse_order = False , plot = True ) # Render the Precision-Recall curve of a ranking. And improved version of scikit-learn's PR-curve Parameters: Name Type Description Default expected List [ T ] Expected ordering of the elements (rank if it's an integer, alphabetical if a string) required actual_scores List [ float ] Actual ranking scores (need not be on the same scale as expected ) required title Optional [ str ] Title of the plot. '' disable_interpolation bool Do not interpolate. False axes Optional [ plt . Axes ] Matplotlib axes for ploting inside a subplot. None output_svg Optional [ Path ] If specified, save the chart as an svg to the given Path. None reverse_order bool Reverse the ranking specified by expected . False plot bool Display a plot on the screen. True Returns: Type Description Dict [ T , float ] Precision values at given recall. Source code in great_ai/utilities/evaluate_ranking/evaluate_ranking.py 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 def evaluate_ranking ( expected : List [ T ], actual_scores : List [ float ], target_recall : float , title : Optional [ str ] = \"\" , disable_interpolation : bool = False , axes : Optional [ plt . Axes ] = None , output_svg : Optional [ Path ] = None , reverse_order : bool = False , plot : bool = True , ) -> Dict [ T , float ]: \"\"\"Render the Precision-Recall curve of a ranking. And improved version of scikit-learn's [PR-curve](https://scikit-learn.org/stable/auto_examples/model_selection/plot_precision_recall.html#sphx-glr-auto-examples-model-selection-plot-precision-recall-py) Args: expected: Expected ordering of the elements (rank if it's an integer, alphabetical if a string) actual_scores: Actual ranking scores (need not be on the same scale as `expected`) title: Title of the plot. disable_interpolation: Do not interpolate. axes: Matplotlib axes for ploting inside a subplot. output_svg: If specified, save the chart as an svg to the given Path. reverse_order: Reverse the ranking specified by `expected`. plot: Display a plot on the screen. Returns: Precision values at given recall. \"\"\" assert 0 <= target_recall <= 1 if plot and axes is None : fig = plt . figure ( figsize = ( 20 , 10 )) fig . patch . set_facecolor ( \"white\" ) ax = plt . axes () else : ax = axes classes = sorted ( unique ( expected ), reverse = reverse_order ) str_classes = [ str ( c ) for c in classes ] with matplotlib . rc_context ({ \"font.size\" : 20 }): if plot : ax . set_xmargin ( 0 ) draw_f1_iso_lines ( axes = ax ) results : Dict [ T , float ] = {} for i in range ( len ( classes ) - 1 ): binarized_expected = [ ( v < classes [ i ]) if reverse_order else ( v > classes [ i ]) for v in expected ] precision , recall , _ = precision_recall_curve ( binarized_expected , actual_scores ) if not disable_interpolation : for j in range ( 1 , len ( precision )): precision [ j ] = max ( precision [ j - 1 ], precision [ j ]) closest_recall_index = np . argmin ( np . abs ( recall - target_recall )) precision_at_closest_recall = precision [ closest_recall_index ] average_precision = average_precision_score ( binarized_expected , actual_scores ) results [ classes [ i ]] = precision_at_closest_recall if plot : ax . plot ( recall , precision , label = f \" { '|' . join ( str_classes [: i + 1 ]) } \u2194 { '|' . join ( str_classes [ i + 1 :]) } (P@ { target_recall : .2f } = { precision_at_closest_recall : .2f } , AP= { average_precision : .2f } )\" , ) if plot : ax . legend ( loc = \"upper right\" ) ax . axvline ( x = target_recall , linestyle = \"--\" , color = \"#55c6bb\" , linewidth = 2.0 ) if title is None : title = \"Ranking evaluation\" ax . set_title ( f ' { title } ( { \" < \" . join ( str_classes ) } )' , pad = 20 ) ax . set_xlabel ( \"Recall\" ) ax . set_ylabel ( \"Precision\" ) ax . set_xticks ([ target_recall ] + sorted ( ax . get_xticks ())) if plot and output_svg is None : if axes is None : plt . show () elif output_svg : plt . savefig ( output_svg , format = \"svg\" ) return results Parallel processing # Multiprocessing and multithreading-based parallelism with support for async functions. Its main purpose is to implement great_ai.GreatAI.process_batch , however, the parallel processing functions are also convenient for covering other types of mapping needs with a friendlier API than joblib or multiprocess . simple_parallel_map ( func , input_values , * , chunk_size = None , concurrency = None ) # Execute a map operation on an list mimicking the API of the built-in map() . A thin-wrapper over parallel_map . For more options, consult the documentation of parallel_map . Examples: >>> import math >>> list ( simple_parallel_map ( math . sqrt , [ 9 , 4 , 1 ])) [3.0, 2.0, 1.0] Parameters: Name Type Description Default func Callable [[ T ], Union [ V , Awaitable [ V ]]] The function that should be applied to each element of input_values . It can async , in that case, a new event loop is started for each chunk. required input_values Sequence [ T ] An iterable of items that func is applied to. required chunk_size Optional [ int ] Tune the number of items processed in each step. Larger numbers result in smaller communication overhead but less parallelism at the start and end. If chunk_size has a __len__ property, the chunk_size is calculated automatically if not given. None concurrency Optional [ int ] Number of new processes to start. Shouldn't be too much more than the number of physical cores. None Returns: Type Description List [ V ] An iterable of results obtained from applying func to each input value. Raises: Type Description WorkerException If there was an error in the func function in a background process. Source code in great_ai/utilities/parallel_map/simple_parallel_map.py 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 def simple_parallel_map ( func : Callable [[ T ], Union [ V , Awaitable [ V ]]], input_values : Sequence [ T ], * , chunk_size : Optional [ int ] = None , concurrency : Optional [ int ] = None , ) -> List [ V ]: \"\"\"Execute a map operation on an list mimicking the API of the built-in `map()`. A thin-wrapper over [parallel_map][great_ai.utilities.parallel_map.parallel_map.parallel_map]. For more options, consult the documentation of [parallel_map][great_ai.utilities.parallel_map.parallel_map.parallel_map]. Examples: >>> import math >>> list(simple_parallel_map(math.sqrt, [9, 4, 1])) [3.0, 2.0, 1.0] Args: func: The function that should be applied to each element of `input_values`. It can `async`, in that case, a new event loop is started for each chunk. input_values: An iterable of items that `func` is applied to. chunk_size: Tune the number of items processed in each step. Larger numbers result in smaller communication overhead but less parallelism at the start and end. If `chunk_size` has a `__len__` property, the `chunk_size` is calculated automatically if not given. concurrency: Number of new processes to start. Shouldn't be too much more than the number of physical cores. Returns: An iterable of results obtained from applying `func` to each input value. Raises: WorkerException: If there was an error in the `func` function in a background process. \"\"\" input_values = list ( input_values ) # in case the input is mistakenly not a sequence generator = parallel_map ( func = func , input_values = input_values , chunk_size = chunk_size , concurrency = concurrency , ) return list ( tqdm ( generator , total = len ( input_values ), ) ) parallel_map ( func , input_values , * , chunk_size = None , ignore_exceptions = False , concurrency = None , unordered = False ) # Execute a map operation on an iterable stream. A custom parallel map operation supporting both synchronous and async map functions. The func function is serialised with dill . Exceptions encountered in the map function are sent to the host process where they are either raised (default) or ignored. The new processes are forked if the OS allows it, otherwise, new Python processes are bootstrapped which can incur some startup cost. Each process processes a single chunk at once. Examples: >>> import math >>> list ( parallel_map ( math . sqrt , [ 9 , 4 , 1 ], concurrency = 2 )) [3.0, 2.0, 1.0] Parameters: Name Type Description Default func Callable [[ T ], Union [ V , Awaitable [ V ]]] The function that should be applied to each element of input_values . It can async , in that case, a new event loop is started for each chunk. required input_values Union [ Iterable [ T ], Sequence [ T ]] An iterable of items that func is applied to. required chunk_size Optional [ int ] Tune the number of items processed in each step. Larger numbers result in smaller communication overhead but less parallelism at the start and end. If chunk_size has a __len__ property, the chunk_size is calculated automatically if not given. None ignore_exceptions bool Ignore chunks if next() raises an exception on input_values . And return None if func raised an exception in a worker process. False concurrency Optional [ int ] Number of new processes to start. Shouldn't be too much more than the number of physical cores. None unordered bool Do not preserve the order of the elements, yield them as soon as they have been processed. This decreases the latency caused by difficult-to-process items. False Yields: Type Description Iterable [ Optional [ V ]] The next result obtained from applying func to each input value. May contain None -s if ignore_exceptions=True . May have different order than the input if unordered=True . Raises: Type Description WorkerException If there was an error in the func function in a background process and ignore_exceptions=False . Source code in great_ai/utilities/parallel_map/parallel_map.py 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 def parallel_map ( func : Callable [[ T ], Union [ V , Awaitable [ V ]]], input_values : Union [ Iterable [ T ], Sequence [ T ]], * , chunk_size : Optional [ int ] = None , ignore_exceptions : bool = False , concurrency : Optional [ int ] = None , unordered : bool = False , ) -> Iterable [ Optional [ V ]]: \"\"\"Execute a map operation on an iterable stream. A custom parallel map operation supporting both synchronous and `async` map functions. The `func` function is serialised with `dill`. Exceptions encountered in the map function are sent to the host process where they are either raised (default) or ignored. The new processes are forked if the OS allows it, otherwise, new Python processes are bootstrapped which can incur some startup cost. Each process processes a single chunk at once. Examples: >>> import math >>> list(parallel_map(math.sqrt, [9, 4, 1], concurrency=2)) [3.0, 2.0, 1.0] Args: func: The function that should be applied to each element of `input_values`. It can `async`, in that case, a new event loop is started for each chunk. input_values: An iterable of items that `func` is applied to. chunk_size: Tune the number of items processed in each step. Larger numbers result in smaller communication overhead but less parallelism at the start and end. If `chunk_size` has a `__len__` property, the `chunk_size` is calculated automatically if not given. ignore_exceptions: Ignore chunks if `next()` raises an exception on `input_values`. And return `None` if `func` raised an exception in a worker process. concurrency: Number of new processes to start. Shouldn't be too much more than the number of physical cores. unordered: Do not preserve the order of the elements, yield them as soon as they have been processed. This decreases the latency caused by difficult-to-process items. Yields: The next result obtained from applying `func` to each input value. May contain `None`-s if `ignore_exceptions=True`. May have different order than the input if `unordered=True`. Raises: WorkerException: If there was an error in the `func` function in a background process and `ignore_exceptions=False`. \"\"\" config = get_config ( function = func , input_values = input_values , chunk_size = chunk_size , concurrency = concurrency , ) ctx = ( mp . get_context ( \"fork\" ) if \"fork\" in mp . get_all_start_methods () else mp . get_context ( \"spawn\" ) ) ctx . freeze_support () manager = ctx . Manager () input_queue = manager . Queue ( config . concurrency * 2 ) output_queue = manager . Queue ( config . concurrency * 2 ) should_stop = ctx . Event () serialized_map_function = dill . dumps ( func , byref = True , recurse = False ) processes = [ ctx . Process ( name = f \"parallel_map_ { config . function_name } _ { i } \" , target = mapper_function , daemon = True , kwargs = dict ( input_queue = input_queue , output_queue = output_queue , should_stop = should_stop , func = serialized_map_function , ), ) for i in range ( config . concurrency ) ] for p in processes : p . start () try : yield from manage_communication ( input_values = input_values , chunk_size = config . chunk_size , input_queue = input_queue , output_queue = output_queue , unordered = unordered , ignore_exceptions = ignore_exceptions , ) should_stop . set () except WorkerException : should_stop . set () raise except Exception : for p in processes : p . terminate () p . kill () raise finally : for p in processes : p . join () # terminated processes have to be joined else they remain zombies p . close () manager . shutdown () threaded_parallel_map ( func , input_values , * , chunk_size = None , ignore_exceptions = False , concurrency = None , unordered = False ) # Execute a map operation on an iterable stream. Similar to parallel_map but uses threads instead of processes. Hence, it is not helpful in CPU-bound situations. A custom parallel map operation supporting both synchronous and async map functions. Exceptions encountered in the map function are sent to the host thread where they are either raised (default) or ignored. Each process processes a single chunk at once. Examples: >>> list ( threaded_parallel_map ( lambda x : x ** 2 , [ 1 , 2 , 3 ])) [1, 4, 9] Parameters: Name Type Description Default func Callable [[ T ], Union [ V , Awaitable [ V ]]] The function that should be applied to each element of input_values . It can async , in that case, a new event loop is started for each chunk. required input_values Union [ Iterable [ T ], Sequence [ T ]] An iterable of items that func is applied to. required chunk_size Optional [ int ] Tune the number of items processed in each step. Larger numbers result in smaller communication overhead but less parallelism at the start and end. If chunk_size has a __len__ property, the chunk_size is calculated automatically if not given. None ignore_exceptions bool Ignore chunks if next() raises an exception on input_values . And return None if func raised an exception in a worker process. False concurrency Optional [ int ] Number of new threads to start. None unordered bool Do not preserve the order of the elements, yield them as soon as they have been processed. This decreases the latency caused by difficult-to-process items. False Yields: Type Description Iterable [ Optional [ V ]] The next result obtained from applying func to each input value. May contain None -s if ignore_exceptions=True . May have different order than the input if unordered=True . Raises: Type Description WorkerException If there was an error in the func function in a background thread and ignore_exceptions=False . Source code in great_ai/utilities/parallel_map/threaded_parallel_map.py 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 def threaded_parallel_map ( func : Callable [[ T ], Union [ V , Awaitable [ V ]]], input_values : Union [ Iterable [ T ], Sequence [ T ]], * , chunk_size : Optional [ int ] = None , ignore_exceptions : bool = False , concurrency : Optional [ int ] = None , unordered : bool = False , ) -> Iterable [ Optional [ V ]]: \"\"\"Execute a map operation on an iterable stream. Similar to [parallel_map][great_ai.utilities.parallel_map.parallel_map.parallel_map] but uses threads instead of processes. Hence, it is not helpful in CPU-bound situations. A custom parallel map operation supporting both synchronous and `async` map functions. Exceptions encountered in the map function are sent to the host thread where they are either raised (default) or ignored. Each process processes a single chunk at once. Examples: >>> list(threaded_parallel_map(lambda x: x ** 2, [1, 2, 3])) [1, 4, 9] Args: func: The function that should be applied to each element of `input_values`. It can `async`, in that case, a new event loop is started for each chunk. input_values: An iterable of items that `func` is applied to. chunk_size: Tune the number of items processed in each step. Larger numbers result in smaller communication overhead but less parallelism at the start and end. If `chunk_size` has a `__len__` property, the `chunk_size` is calculated automatically if not given. ignore_exceptions: Ignore chunks if `next()` raises an exception on `input_values`. And return `None` if `func` raised an exception in a worker process. concurrency: Number of new threads to start. unordered: Do not preserve the order of the elements, yield them as soon as they have been processed. This decreases the latency caused by difficult-to-process items. Yields: The next result obtained from applying `func` to each input value. May contain `None`-s if `ignore_exceptions=True`. May have different order than the input if `unordered=True`. Raises: WorkerException: If there was an error in the `func` function in a background thread and `ignore_exceptions=False`. \"\"\" config = get_config ( function = func , input_values = input_values , chunk_size = chunk_size , concurrency = concurrency , ) input_queue : queue . Queue = queue . Queue ( config . concurrency * 2 ) output_queue : queue . Queue = queue . Queue ( config . concurrency * 2 ) should_stop = threading . Event () threads = [ threading . Thread ( name = f \"threaded_parallel_map_ { config . function_name } _ { i } \" , target = mapper_function , daemon = True , kwargs = dict ( input_queue = input_queue , output_queue = output_queue , should_stop = should_stop , func = func , ), ) for i in range ( config . concurrency ) ] for t in threads : t . start () yield from manage_communication ( input_values = input_values , chunk_size = config . chunk_size , input_queue = input_queue , output_queue = output_queue , unordered = unordered , ignore_exceptions = ignore_exceptions , ) should_stop . set () for t in threads : t . join ( 1 ) Composable parallel processing # Because both threaded_parallel_map and parallel_map have a streaming interface, it is easy to compose them and end up with, for example, a process for each CPU core with its own thread-pool or event-loop. Longer pipelines are also easy to imagine. The chunking methods help in these compositions. chunk ( values , chunk_size ) # Turn an iterable of items into an iterable of lists (chunks) of items. Each returned chunk is of length chunk_size except the last one the length of which is between 1 and chunk_size . Useful for parallel processing. Examples: >>> list ( chunk ( range ( 10 ), chunk_size = 3 )) [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]] Parameters: Name Type Description Default values Iterable [ T ] The stream of items to pack into chunks. required chunk_size int Desired length of each (but the last) chunk. required Yields: Type Description Iterable [ List [ T ]] The next chunk. Source code in great_ai/utilities/chunk.py 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 def chunk ( values : Iterable [ T ], chunk_size : int ) -> Iterable [ List [ T ]]: \"\"\"Turn an iterable of items into an iterable of lists (chunks) of items. Each returned chunk is of length `chunk_size` except the last one the length of which is between 1 and `chunk_size`. Useful for parallel processing. Examples: >>> list(chunk(range(10), chunk_size=3)) [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]] Args: values: The stream of items to pack into chunks. chunk_size: Desired length of each (but the last) chunk. Yields: The next chunk. \"\"\" assert chunk_size >= 1 result : List [ T ] = [] for v in values : result . append ( v ) if len ( result ) == chunk_size : yield result result = [] if len ( result ) > 0 : yield result unchunk ( chunks ) # Turn a stream of chunks of items into a stream of items (flatten operation). The inverse operation of chunk . Useful for parallel processing. Similar to itertools.chain but ignores None chunks. Examples: >>> list ( unchunk ([[ 0 , 1 , 2 ], [ 3 , 4 , 5 ], [ 6 , 7 , 8 ], [ 9 ]])) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] Parameters: Name Type Description Default chunks Iterable [ Optional [ Iterable [ T ]]] Stream of chunks to unpack. required Yields: Type Description Iterable [ T ] The next item in the flattened iterable. Source code in great_ai/utilities/unchunk.py 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 def unchunk ( chunks : Iterable [ Optional [ Iterable [ T ]]]) -> Iterable [ T ]: \"\"\"Turn a stream of chunks of items into a stream of items (flatten operation). The inverse operation of [chunk][great_ai.utilities.chunk.chunk]. Useful for parallel processing. Similar to itertools.chain but ignores `None` chunks. Examples: >>> list(unchunk([[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]])) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] Args: chunks: Stream of chunks to unpack. Yields: The next item in the flattened iterable. \"\"\" for chunk in chunks : if chunk is not None : yield from chunk Operations # ConfigFile # Bases: Mapping [ str , str ] A small and safe INI -style configuration loader with dict and ENV support. The values can be accessed using both dot- and index-notation. It is compatible with the dict interface. File format example: # comments are allowed everywhere key = value # you can leave or omit whitespace around the equal-sign my_hashtag = \"#great_ai\" # the r-value can be quoted with \" or ' or `. my_var = my_default_value # Default values can be given to env-vars, # see next line. The default value must come first. my_var = ENV : MY_ENV_VAR # If the value starts with the `ENV:` prefix, # it is looked up from the environment variables. Examples: >>> ConfigFile ( 'tests/utilities/data/simple.conf' ) ConfigFile(path=tests/utilities/data/simple.conf) {'zeroth_key': 'test', 'first_key': 'Andr\u00e1s'} >>> ConfigFile ( 'tests/utilities/data/simple.conf' ) . zeroth_key 'test' >>> ConfigFile ( 'tests/utilities/data/simple.conf' ) . second_key Traceback (most recent call last): ... KeyError : 'Key `second_key` is not found in configuration file ... >>> a = ConfigFile ( 'tests/utilities/data/simple.conf' ) >>> { ** a } {'zeroth_key': 'test', 'first_key': 'Andr\u00e1s'} Source code in great_ai/utilities/config_file/config_file.py 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 class ConfigFile ( Mapping [ str , str ]): \"\"\"A small and safe `INI`-style configuration loader with `dict` and `ENV` support. The values can be accessed using both dot- and index-notation. It is compatible with the `dict` interface. File format example: ```toml # comments are allowed everywhere key = value # you can leave or omit whitespace around the equal-sign my_hashtag = \"#great_ai\" # the r-value can be quoted with \" or ' or `. my_var = my_default_value # Default values can be given to env-vars, # see next line. The default value must come first. my_var = ENV:MY_ENV_VAR # If the value starts with the `ENV:` prefix, # it is looked up from the environment variables. ``` Examples: >>> ConfigFile('tests/utilities/data/simple.conf') ConfigFile(path=tests/utilities/data/simple.conf) {'zeroth_key': 'test', 'first_key': 'Andr\u00e1s'} >>> ConfigFile('tests/utilities/data/simple.conf').zeroth_key 'test' >>> ConfigFile('tests/utilities/data/simple.conf').second_key Traceback (most recent call last): ... KeyError: 'Key `second_key` is not found in configuration file ... >>> a = ConfigFile('tests/utilities/data/simple.conf') >>> {**a} {'zeroth_key': 'test', 'first_key': 'Andr\u00e1s'} \"\"\" ENVIRONMENT_VARIABLE_KEY_PREFIX = \"ENV\" def __init__ ( self , path : Union [ Path , str ], * , ignore_missing : bool = False ) -> None : \"\"\"Load and parse a configuration file. Everything is eager-loaded, thus, exceptions may be thrown here. Args: path: Local path of the configuration file. ignore_missing: Don't raise an exception on missing environment variables. Raises: FileNotFoundError: If there is no file at the specified path. ParseError: If the provided file does not conform to the expected format. KeyError: If there is duplication in the keys. ValueError: If an environment variable is referenced but it is not set in the system and `ignore_missing=False`. \"\"\" if not isinstance ( path , Path ): path = Path ( path ) if not path . exists (): raise FileNotFoundError ( path . absolute ()) self . _ignore_missing = ignore_missing self . _path = path self . _key_values : Dict [ str , str ] = {} self . _parse () @property def path ( self ) -> Path : \"\"\"Original path from where the configuration was loaded.\"\"\" return self . _path def _parse ( self ) -> None : with open ( self . _path , encoding = \"utf-8\" ) as f : lines : str = f . read () matches = pattern . findall ( lines ) for key , * values in matches : try : value = next ( v for v in values if v ) except StopIteration : raise ParseError ( f \"\"\"Cannot parse config file ( { self . _path . absolute () } ), error at key ` { key } `\"\"\" ) already_exists = key in self . _key_values if already_exists and not value . startswith ( f \" { self . ENVIRONMENT_VARIABLE_KEY_PREFIX } :\" ): raise KeyError ( f \"Key ` { key } ` has been already defined and its value is ` { self . _key_values [ key ] } `\" ) if value . startswith ( f \" { self . ENVIRONMENT_VARIABLE_KEY_PREFIX } :\" ): _ , value = value . split ( \":\" ) if value not in os . environ : issue = f \"\"\"The value of ` { key } ` contains the \" { self . ENVIRONMENT_VARIABLE_KEY_PREFIX } ` prefix but ` { value } ` is not defined as an environment variable\"\"\" if already_exists : logger . warning ( f \"\"\" { issue } , using the default value defined above (` { self . _key_values [ key ] } `)\"\"\" ) continue elif self . _ignore_missing : logger . warning ( issue ) else : raise ValueError ( f \" { issue } and no default value has been provided\" ) else : value = os . environ [ value ] self . _key_values [ key ] = value def __getattr__ ( self , key : str ) -> str : if key in self . _key_values : return self . _key_values [ key ] raise KeyError ( f \"Key ` { key } ` is not found in configuration file ( { self . _path . absolute () } )\" ) __getitem__ = __getattr__ def __iter__ ( self ) -> Iterator [ str ]: return iter ( self . _key_values ) def __len__ ( self ) -> int : return len ( self . _key_values ) def keys ( self ) -> KeysView [ str ]: return self . _key_values . keys () def values ( self ) -> ValuesView [ str ]: return self . _key_values . values () def items ( self ) -> ItemsView [ str , str ]: return self . _key_values . items () def __repr__ ( self ) -> str : return f \" { type ( self ) . __name__ } (path= { self . _path . as_posix () } ) { self . _key_values } \" __init__ ( path , * , ignore_missing = False ) # Load and parse a configuration file. Everything is eager-loaded, thus, exceptions may be thrown here. Parameters: Name Type Description Default path Union [ Path , str ] Local path of the configuration file. required ignore_missing bool Don't raise an exception on missing environment variables. False Raises: Type Description FileNotFoundError If there is no file at the specified path. ParseError If the provided file does not conform to the expected format. KeyError If there is duplication in the keys. ValueError If an environment variable is referenced but it is not set in the system and ignore_missing=False . Source code in great_ai/utilities/config_file/config_file.py 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 def __init__ ( self , path : Union [ Path , str ], * , ignore_missing : bool = False ) -> None : \"\"\"Load and parse a configuration file. Everything is eager-loaded, thus, exceptions may be thrown here. Args: path: Local path of the configuration file. ignore_missing: Don't raise an exception on missing environment variables. Raises: FileNotFoundError: If there is no file at the specified path. ParseError: If the provided file does not conform to the expected format. KeyError: If there is duplication in the keys. ValueError: If an environment variable is referenced but it is not set in the system and `ignore_missing=False`. \"\"\" if not isinstance ( path , Path ): path = Path ( path ) if not path . exists (): raise FileNotFoundError ( path . absolute ()) self . _ignore_missing = ignore_missing self . _path = path self . _key_values : Dict [ str , str ] = {} self . _parse () path () property # Original path from where the configuration was loaded. Source code in great_ai/utilities/config_file/config_file.py 83 84 85 86 @property def path ( self ) -> Path : \"\"\"Original path from where the configuration was loaded.\"\"\" return self . _path get_logger ( name , level = logging . INFO , disable_colors = False ) # Return a customised logger used throughout the GreatAI codebase. Uses colors, and only prints timestamps when not running inside notebook. Source code in great_ai/utilities/logger/get_logger.py 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 def get_logger ( name : str , level : int = logging . INFO , disable_colors : bool = False ) -> logging . Logger : \"\"\"Return a customised logger used throughout the GreatAI codebase. Uses colors, and only prints timestamps when not running inside notebook. \"\"\" if name not in loggers : logger = logging . getLogger ( name ) logger . setLevel ( level ) try : get_ipython () # type: ignore log_format = \" %(message)s \" except NameError : # will fail outside of a notebook https://ipython.org/ log_format = \" %(asctime)s | %(levelname)8s | %(message)s \" stdout_handler = logging . StreamHandler () stdout_handler . setLevel ( level ) if not disable_colors : stdout_handler . setFormatter ( CustomFormatter ( log_format )) logger . addHandler ( stdout_handler ) loggers [ name ] = logger return loggers [ name ]","title":"Utilities"},{"location":"reference/utilities/#utilities","text":"from great_ai.utilities import *","title":"Utilities"},{"location":"reference/utilities/#nlp-tools","text":"Well-tested tools that can be used in production with confidence. The toolbox of feature-extraction functions is expected to grow to cover other domains as well.","title":"NLP tools"},{"location":"reference/utilities/#great_ai.utilities.clean.clean","text":"Clean all XML, LaTeX, PDF-extraction, and Unicode artifacts from the text. The cleaning is quite heavy-weight and can be destructive. However, when working with text, this is usually required to achieve sufficient cleanliness before further processing. Optionally, the text can be turned into ASCII. Carefully consider whether this is absolutely needed for your use-case. Examples: >>> clean ( '
Bj\\\\\\\\\"{o}rn is \\\\t \\\\\\\\textit{happy} \ud83d\ude42 <3
', ... ignore_xml=True ... ) '
Bj\u00f6rn is happy \ud83d\ude42 lt;3
' Args: text: Text to be cleaned. ignore_xml: Do not process/remove XML-tags. ignore_latex: Do not process/remove LaTeX-tags. remove_brackets: Do not remove brackets ([]) convert_to_ascii: Strip (or convert) non-ascii characters. Returns: The cleaned input text with sensibly collapsed whitespace and optionally no markup. \"\"\" if not ignore_xml : text = re . sub ( r \"<[^>]*>\" , \" \" , text ) text = html . unescape ( text ) if not ignore_latex : text = text . replace ( \"%\" , \" \\\\ %\" ) # escape LaTeX comments before parsing as LaTeX try : text = latex . latex_to_text ( text , tolerant_parsing = True , strict_braces = False ) text = text . replace ( \" %s \" , \" \" ) except : logger . exception ( \"Latex parsing error\" ) if convert_to_ascii : text = unicodedata . normalize ( \"NFKD\" , text ) try : text . encode ( \"ASCII\" , errors = \"strict\" ) except UnicodeEncodeError : text = \"\" . join ([ c for c in text if not unicodedata . combining ( c )]) text = unidecode . unidecode ( text ) text = re . sub ( r \"\\b[a-zA-Z](?:[\\t ]+[a-zA-Z]\\b)+\" , lambda m : re . sub ( r \"[\\t ]\" , \"\" , m [ 0 ]), text ) # A R T I C L E => ARTICLE if remove_brackets : text = re . sub ( r \"\\[[^\\]]*\\]\" , \" \" , text ) # fix hypens: break- word => break-word text = re . sub ( r \"(\\S)-\\s+\" , r \"\\1-\" , text ) text = re . sub ( r \"\\s+-(\\S)\" , r \"-\\1\" , text ) # collapse whitespace text = re . sub ( r \"\\s+\" , \" \" , text ) # fix punctuation text = re . sub ( rf \" ([ { joined_left_punctuations } ])\" , r \"\\1\" , text ) text = re . sub ( rf \"([ { joined_right_punctuations } ]) \" , r \"\\1\" , text ) text = text . strip () return text get_sentences ( text , ignore_partial = False , true_case = False , remove_punctuation = False ) # Return the list of sentences found in the input text. Use syntok to segment the sentences. Further processing can be enabled with optional arguments. Examples: >>> get_sentences ( 'This is a sentence. This is a half' ) ['This is a sentence.', 'This is a half'] >>> get_sentences ( 'This is a sentence. This is a half' , ignore_partial = True ) ['This is a sentence.'] >>> get_sentences ( 'I like Apple.' , true_case = True , remove_punctuation = True ) ['i like Apple'] Parameters: Name Type Description Default text str Text to be segmented into sentences. required ignore_partial bool Filter out sentences that are not capitalised/don't end with a punctuation. False true_case bool Crude method: lowercase the first word of each sentence. False remove_punctuation bool Remove all kinds of punctuation. False Returns: Type Description List [ str ] The found sentences (with partial sentences optionally filtered out). Source code in great_ai/utilities/get_sentences.py 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 def get_sentences ( text : str , ignore_partial : bool = False , true_case : bool = False , remove_punctuation : bool = False , ) -> List [ str ]: \"\"\"Return the list of sentences found in the input text. Use [syntok](https://github.com/fnl/syntok) to segment the sentences. Further processing can be enabled with optional arguments. Examples: >>> get_sentences('This is a sentence. This is a half') ['This is a sentence.', 'This is a half'] >>> get_sentences('This is a sentence. This is a half', ignore_partial=True) ['This is a sentence.'] >>> get_sentences('I like Apple.', true_case=True, remove_punctuation=True) ['i like Apple'] Args: text: Text to be segmented into sentences. ignore_partial: Filter out sentences that are not capitalised/don't end with a punctuation. true_case: Crude method: lowercase the first word of each sentence. remove_punctuation: Remove all kinds of punctuation. Returns: The found sentences (with partial sentences optionally filtered out). \"\"\" tokenizer = Tokenizer ( emit_hyphen_or_underscore_sep = True , replace_not_contraction = False ) token_stream = tokenizer . tokenize ( text ) def process ( sentence : str ) -> str : if true_case : sentence = sentence [ 0 ] . lower () + sentence [ 1 :] # very crude method if remove_punctuation : sentence = re . sub ( punctuations_pattern , \" \" , sentence ) return sentence . strip () sentences = [ process ( tokenizer . to_text ( sentence )) for sentence in segment ( token_stream ) ] if ignore_partial : sentences = [ sentence for sentence in sentences if sentence [ 0 ] . isupper () and sentence [ - 1 ] in sentence_ending_punctuations ] return sentences predict_language ( text ) # Predict the language code from text. A thin wrapper over langcodes for convenient language tagging. Examples: >>> predict_language ( 'This is a sentence.' ) 'en' Parameters: Name Type Description Default text Optional [ str ] Text used for prediction. required Returns: Type Description str The predicted language code (en, en-US) or und if a prediction could not be made. Source code in great_ai/utilities/language/predict_language.py 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 def predict_language ( text : Optional [ str ]) -> str : \"\"\"Predict the language code from text. A thin wrapper over [langcodes](https://github.com/rspeer/langcodes) for convenient language tagging. Examples: >>> predict_language('This is a sentence.') 'en' Args: text: Text used for prediction. Returns: The predicted language code (en, en-US) or `und` if a prediction could not be made. \"\"\" if not text : return Language . make () . to_tag () try : language_code = detect ( text ) except LangDetectException : return Language . make () . to_tag () return Language . get ( language_code ) . to_tag () english_name_of_language ( language_code ) # Human-friendly English name of language from its language_code . A thin wrapper over langcodes for convenient language tagging. Examples: >>> english_name_of_language ( 'en-US' ) 'English (United States)' >>> english_name_of_language ( 'und' ) 'Unknown language' Parameters: Name Type Description Default language_code Optional [ str ] Language code, for example, returned by great_ai.utilities.language.predict_language.predict_language . required Returns: Type Description str English name of language. Source code in great_ai/utilities/language/english_name_of_language.py 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 def english_name_of_language ( language_code : Optional [ str ]) -> str : \"\"\"Human-friendly English name of language from its `language_code`. A thin wrapper over [langcodes](https://github.com/rspeer/langcodes) for convenient language tagging. Examples: >>> english_name_of_language('en-US') 'English (United States)' >>> english_name_of_language('und') 'Unknown language' Args: language_code: Language code, for example, returned by [great_ai.utilities.language.predict_language.predict_language][]. Returns: English name of language. \"\"\" if not language_code : language_code = \"und\" return Language . get ( language_code ) . display_name () is_english ( language_code ) # Decide whether the language_code is of an English language. A thin wrapper over langcodes for convenient language tagging. Examples: >>> is_english ( 'en-US' ) True >>> is_english ( None ) False >>> is_english ( 'und' ) False Parameters: Name Type Description Default language_code Optional [ str ] Language code, for example, returned by ` great_ai.utilities.language.predict_language.predict_language . required Returns: Type Description bool Boolean indicating whether it's English. Source code in great_ai/utilities/language/is_english.py 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 def is_english ( language_code : Optional [ str ]) -> bool : \"\"\"Decide whether the `language_code` is of an English language. A thin wrapper over [langcodes](https://github.com/rspeer/langcodes) for convenient language tagging. Examples: >>> is_english('en-US') True >>> is_english(None) False >>> is_english('und') False Args: language_code: Language code, for example, returned by `[great_ai.utilities.language.predict_language.predict_language][]. Returns: Boolean indicating whether it's English. \"\"\" if not language_code : language_code = \"und\" language_code = standardize_tag ( language_code ) return tag_distance ( language_code , \"en\" ) < 15 evaluate_ranking ( expected , actual_scores , target_recall , title = '' , disable_interpolation = False , axes = None , output_svg = None , reverse_order = False , plot = True ) # Render the Precision-Recall curve of a ranking. And improved version of scikit-learn's PR-curve Parameters: Name Type Description Default expected List [ T ] Expected ordering of the elements (rank if it's an integer, alphabetical if a string) required actual_scores List [ float ] Actual ranking scores (need not be on the same scale as expected ) required title Optional [ str ] Title of the plot. '' disable_interpolation bool Do not interpolate. False axes Optional [ plt . Axes ] Matplotlib axes for ploting inside a subplot. None output_svg Optional [ Path ] If specified, save the chart as an svg to the given Path. None reverse_order bool Reverse the ranking specified by expected . False plot bool Display a plot on the screen. True Returns: Type Description Dict [ T , float ] Precision values at given recall. Source code in great_ai/utilities/evaluate_ranking/evaluate_ranking.py 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 def evaluate_ranking ( expected : List [ T ], actual_scores : List [ float ], target_recall : float , title : Optional [ str ] = \"\" , disable_interpolation : bool = False , axes : Optional [ plt . Axes ] = None , output_svg : Optional [ Path ] = None , reverse_order : bool = False , plot : bool = True , ) -> Dict [ T , float ]: \"\"\"Render the Precision-Recall curve of a ranking. And improved version of scikit-learn's [PR-curve](https://scikit-learn.org/stable/auto_examples/model_selection/plot_precision_recall.html#sphx-glr-auto-examples-model-selection-plot-precision-recall-py) Args: expected: Expected ordering of the elements (rank if it's an integer, alphabetical if a string) actual_scores: Actual ranking scores (need not be on the same scale as `expected`) title: Title of the plot. disable_interpolation: Do not interpolate. axes: Matplotlib axes for ploting inside a subplot. output_svg: If specified, save the chart as an svg to the given Path. reverse_order: Reverse the ranking specified by `expected`. plot: Display a plot on the screen. Returns: Precision values at given recall. \"\"\" assert 0 <= target_recall <= 1 if plot and axes is None : fig = plt . figure ( figsize = ( 20 , 10 )) fig . patch . set_facecolor ( \"white\" ) ax = plt . axes () else : ax = axes classes = sorted ( unique ( expected ), reverse = reverse_order ) str_classes = [ str ( c ) for c in classes ] with matplotlib . rc_context ({ \"font.size\" : 20 }): if plot : ax . set_xmargin ( 0 ) draw_f1_iso_lines ( axes = ax ) results : Dict [ T , float ] = {} for i in range ( len ( classes ) - 1 ): binarized_expected = [ ( v < classes [ i ]) if reverse_order else ( v > classes [ i ]) for v in expected ] precision , recall , _ = precision_recall_curve ( binarized_expected , actual_scores ) if not disable_interpolation : for j in range ( 1 , len ( precision )): precision [ j ] = max ( precision [ j - 1 ], precision [ j ]) closest_recall_index = np . argmin ( np . abs ( recall - target_recall )) precision_at_closest_recall = precision [ closest_recall_index ] average_precision = average_precision_score ( binarized_expected , actual_scores ) results [ classes [ i ]] = precision_at_closest_recall if plot : ax . plot ( recall , precision , label = f \" { '|' . join ( str_classes [: i + 1 ]) } \u2194 { '|' . join ( str_classes [ i + 1 :]) } (P@ { target_recall : .2f } = { precision_at_closest_recall : .2f } , AP= { average_precision : .2f } )\" , ) if plot : ax . legend ( loc = \"upper right\" ) ax . axvline ( x = target_recall , linestyle = \"--\" , color = \"#55c6bb\" , linewidth = 2.0 ) if title is None : title = \"Ranking evaluation\" ax . set_title ( f ' { title } ( { \" < \" . join ( str_classes ) } )' , pad = 20 ) ax . set_xlabel ( \"Recall\" ) ax . set_ylabel ( \"Precision\" ) ax . set_xticks ([ target_recall ] + sorted ( ax . get_xticks ())) if plot and output_svg is None : if axes is None : plt . show () elif output_svg : plt . savefig ( output_svg , format = \"svg\" ) return results Parallel processing # Multiprocessing and multithreading-based parallelism with support for async functions. Its main purpose is to implement great_ai.GreatAI.process_batch , however, the parallel processing functions are also convenient for covering other types of mapping needs with a friendlier API than joblib or multiprocess . simple_parallel_map ( func , input_values , * , chunk_size = None , concurrency = None ) # Execute a map operation on an list mimicking the API of the built-in map() . A thin-wrapper over parallel_map . For more options, consult the documentation of parallel_map . Examples: >>> import math >>> list ( simple_parallel_map ( math . sqrt , [ 9 , 4 , 1 ])) [3.0, 2.0, 1.0] Parameters: Name Type Description Default func Callable [[ T ], Union [ V , Awaitable [ V ]]] The function that should be applied to each element of input_values . It can async , in that case, a new event loop is started for each chunk. required input_values Sequence [ T ] An iterable of items that func is applied to. required chunk_size Optional [ int ] Tune the number of items processed in each step. Larger numbers result in smaller communication overhead but less parallelism at the start and end. If chunk_size has a __len__ property, the chunk_size is calculated automatically if not given. None concurrency Optional [ int ] Number of new processes to start. Shouldn't be too much more than the number of physical cores. None Returns: Type Description List [ V ] An iterable of results obtained from applying func to each input value. Raises: Type Description WorkerException If there was an error in the func function in a background process. Source code in great_ai/utilities/parallel_map/simple_parallel_map.py 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 def simple_parallel_map ( func : Callable [[ T ], Union [ V , Awaitable [ V ]]], input_values : Sequence [ T ], * , chunk_size : Optional [ int ] = None , concurrency : Optional [ int ] = None , ) -> List [ V ]: \"\"\"Execute a map operation on an list mimicking the API of the built-in `map()`. A thin-wrapper over [parallel_map][great_ai.utilities.parallel_map.parallel_map.parallel_map]. For more options, consult the documentation of [parallel_map][great_ai.utilities.parallel_map.parallel_map.parallel_map]. Examples: >>> import math >>> list(simple_parallel_map(math.sqrt, [9, 4, 1])) [3.0, 2.0, 1.0] Args: func: The function that should be applied to each element of `input_values`. It can `async`, in that case, a new event loop is started for each chunk. input_values: An iterable of items that `func` is applied to. chunk_size: Tune the number of items processed in each step. Larger numbers result in smaller communication overhead but less parallelism at the start and end. If `chunk_size` has a `__len__` property, the `chunk_size` is calculated automatically if not given. concurrency: Number of new processes to start. Shouldn't be too much more than the number of physical cores. Returns: An iterable of results obtained from applying `func` to each input value. Raises: WorkerException: If there was an error in the `func` function in a background process. \"\"\" input_values = list ( input_values ) # in case the input is mistakenly not a sequence generator = parallel_map ( func = func , input_values = input_values , chunk_size = chunk_size , concurrency = concurrency , ) return list ( tqdm ( generator , total = len ( input_values ), ) ) parallel_map ( func , input_values , * , chunk_size = None , ignore_exceptions = False , concurrency = None , unordered = False ) # Execute a map operation on an iterable stream. A custom parallel map operation supporting both synchronous and async map functions. The func function is serialised with dill . Exceptions encountered in the map function are sent to the host process where they are either raised (default) or ignored. The new processes are forked if the OS allows it, otherwise, new Python processes are bootstrapped which can incur some startup cost. Each process processes a single chunk at once. Examples: >>> import math >>> list ( parallel_map ( math . sqrt , [ 9 , 4 , 1 ], concurrency = 2 )) [3.0, 2.0, 1.0] Parameters: Name Type Description Default func Callable [[ T ], Union [ V , Awaitable [ V ]]] The function that should be applied to each element of input_values . It can async , in that case, a new event loop is started for each chunk. required input_values Union [ Iterable [ T ], Sequence [ T ]] An iterable of items that func is applied to. required chunk_size Optional [ int ] Tune the number of items processed in each step. Larger numbers result in smaller communication overhead but less parallelism at the start and end. If chunk_size has a __len__ property, the chunk_size is calculated automatically if not given. None ignore_exceptions bool Ignore chunks if next() raises an exception on input_values . And return None if func raised an exception in a worker process. False concurrency Optional [ int ] Number of new processes to start. Shouldn't be too much more than the number of physical cores. None unordered bool Do not preserve the order of the elements, yield them as soon as they have been processed. This decreases the latency caused by difficult-to-process items. False Yields: Type Description Iterable [ Optional [ V ]] The next result obtained from applying func to each input value. May contain None -s if ignore_exceptions=True . May have different order than the input if unordered=True . Raises: Type Description WorkerException If there was an error in the func function in a background process and ignore_exceptions=False . Source code in great_ai/utilities/parallel_map/parallel_map.py 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 def parallel_map ( func : Callable [[ T ], Union [ V , Awaitable [ V ]]], input_values : Union [ Iterable [ T ], Sequence [ T ]], * , chunk_size : Optional [ int ] = None , ignore_exceptions : bool = False , concurrency : Optional [ int ] = None , unordered : bool = False , ) -> Iterable [ Optional [ V ]]: \"\"\"Execute a map operation on an iterable stream. A custom parallel map operation supporting both synchronous and `async` map functions. The `func` function is serialised with `dill`. Exceptions encountered in the map function are sent to the host process where they are either raised (default) or ignored. The new processes are forked if the OS allows it, otherwise, new Python processes are bootstrapped which can incur some startup cost. Each process processes a single chunk at once. Examples: >>> import math >>> list(parallel_map(math.sqrt, [9, 4, 1], concurrency=2)) [3.0, 2.0, 1.0] Args: func: The function that should be applied to each element of `input_values`. It can `async`, in that case, a new event loop is started for each chunk. input_values: An iterable of items that `func` is applied to. chunk_size: Tune the number of items processed in each step. Larger numbers result in smaller communication overhead but less parallelism at the start and end. If `chunk_size` has a `__len__` property, the `chunk_size` is calculated automatically if not given. ignore_exceptions: Ignore chunks if `next()` raises an exception on `input_values`. And return `None` if `func` raised an exception in a worker process. concurrency: Number of new processes to start. Shouldn't be too much more than the number of physical cores. unordered: Do not preserve the order of the elements, yield them as soon as they have been processed. This decreases the latency caused by difficult-to-process items. Yields: The next result obtained from applying `func` to each input value. May contain `None`-s if `ignore_exceptions=True`. May have different order than the input if `unordered=True`. Raises: WorkerException: If there was an error in the `func` function in a background process and `ignore_exceptions=False`. \"\"\" config = get_config ( function = func , input_values = input_values , chunk_size = chunk_size , concurrency = concurrency , ) ctx = ( mp . get_context ( \"fork\" ) if \"fork\" in mp . get_all_start_methods () else mp . get_context ( \"spawn\" ) ) ctx . freeze_support () manager = ctx . Manager () input_queue = manager . Queue ( config . concurrency * 2 ) output_queue = manager . Queue ( config . concurrency * 2 ) should_stop = ctx . Event () serialized_map_function = dill . dumps ( func , byref = True , recurse = False ) processes = [ ctx . Process ( name = f \"parallel_map_ { config . function_name } _ { i } \" , target = mapper_function , daemon = True , kwargs = dict ( input_queue = input_queue , output_queue = output_queue , should_stop = should_stop , func = serialized_map_function , ), ) for i in range ( config . concurrency ) ] for p in processes : p . start () try : yield from manage_communication ( input_values = input_values , chunk_size = config . chunk_size , input_queue = input_queue , output_queue = output_queue , unordered = unordered , ignore_exceptions = ignore_exceptions , ) should_stop . set () except WorkerException : should_stop . set () raise except Exception : for p in processes : p . terminate () p . kill () raise finally : for p in processes : p . join () # terminated processes have to be joined else they remain zombies p . close () manager . shutdown () threaded_parallel_map ( func , input_values , * , chunk_size = None , ignore_exceptions = False , concurrency = None , unordered = False ) # Execute a map operation on an iterable stream. Similar to parallel_map but uses threads instead of processes. Hence, it is not helpful in CPU-bound situations. A custom parallel map operation supporting both synchronous and async map functions. Exceptions encountered in the map function are sent to the host thread where they are either raised (default) or ignored. Each process processes a single chunk at once. Examples: >>> list ( threaded_parallel_map ( lambda x : x ** 2 , [ 1 , 2 , 3 ])) [1, 4, 9] Parameters: Name Type Description Default func Callable [[ T ], Union [ V , Awaitable [ V ]]] The function that should be applied to each element of input_values . It can async , in that case, a new event loop is started for each chunk. required input_values Union [ Iterable [ T ], Sequence [ T ]] An iterable of items that func is applied to. required chunk_size Optional [ int ] Tune the number of items processed in each step. Larger numbers result in smaller communication overhead but less parallelism at the start and end. If chunk_size has a __len__ property, the chunk_size is calculated automatically if not given. None ignore_exceptions bool Ignore chunks if next() raises an exception on input_values . And return None if func raised an exception in a worker process. False concurrency Optional [ int ] Number of new threads to start. None unordered bool Do not preserve the order of the elements, yield them as soon as they have been processed. This decreases the latency caused by difficult-to-process items. False Yields: Type Description Iterable [ Optional [ V ]] The next result obtained from applying func to each input value. May contain None -s if ignore_exceptions=True . May have different order than the input if unordered=True . Raises: Type Description WorkerException If there was an error in the func function in a background thread and ignore_exceptions=False . Source code in great_ai/utilities/parallel_map/threaded_parallel_map.py 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 def threaded_parallel_map ( func : Callable [[ T ], Union [ V , Awaitable [ V ]]], input_values : Union [ Iterable [ T ], Sequence [ T ]], * , chunk_size : Optional [ int ] = None , ignore_exceptions : bool = False , concurrency : Optional [ int ] = None , unordered : bool = False , ) -> Iterable [ Optional [ V ]]: \"\"\"Execute a map operation on an iterable stream. Similar to [parallel_map][great_ai.utilities.parallel_map.parallel_map.parallel_map] but uses threads instead of processes. Hence, it is not helpful in CPU-bound situations. A custom parallel map operation supporting both synchronous and `async` map functions. Exceptions encountered in the map function are sent to the host thread where they are either raised (default) or ignored. Each process processes a single chunk at once. Examples: >>> list(threaded_parallel_map(lambda x: x ** 2, [1, 2, 3])) [1, 4, 9] Args: func: The function that should be applied to each element of `input_values`. It can `async`, in that case, a new event loop is started for each chunk. input_values: An iterable of items that `func` is applied to. chunk_size: Tune the number of items processed in each step. Larger numbers result in smaller communication overhead but less parallelism at the start and end. If `chunk_size` has a `__len__` property, the `chunk_size` is calculated automatically if not given. ignore_exceptions: Ignore chunks if `next()` raises an exception on `input_values`. And return `None` if `func` raised an exception in a worker process. concurrency: Number of new threads to start. unordered: Do not preserve the order of the elements, yield them as soon as they have been processed. This decreases the latency caused by difficult-to-process items. Yields: The next result obtained from applying `func` to each input value. May contain `None`-s if `ignore_exceptions=True`. May have different order than the input if `unordered=True`. Raises: WorkerException: If there was an error in the `func` function in a background thread and `ignore_exceptions=False`. \"\"\" config = get_config ( function = func , input_values = input_values , chunk_size = chunk_size , concurrency = concurrency , ) input_queue : queue . Queue = queue . Queue ( config . concurrency * 2 ) output_queue : queue . Queue = queue . Queue ( config . concurrency * 2 ) should_stop = threading . Event () threads = [ threading . Thread ( name = f \"threaded_parallel_map_ { config . function_name } _ { i } \" , target = mapper_function , daemon = True , kwargs = dict ( input_queue = input_queue , output_queue = output_queue , should_stop = should_stop , func = func , ), ) for i in range ( config . concurrency ) ] for t in threads : t . start () yield from manage_communication ( input_values = input_values , chunk_size = config . chunk_size , input_queue = input_queue , output_queue = output_queue , unordered = unordered , ignore_exceptions = ignore_exceptions , ) should_stop . set () for t in threads : t . join ( 1 ) Composable parallel processing # Because both threaded_parallel_map and parallel_map have a streaming interface, it is easy to compose them and end up with, for example, a process for each CPU core with its own thread-pool or event-loop. Longer pipelines are also easy to imagine. The chunking methods help in these compositions. chunk ( values , chunk_size ) # Turn an iterable of items into an iterable of lists (chunks) of items. Each returned chunk is of length chunk_size except the last one the length of which is between 1 and chunk_size . Useful for parallel processing. Examples: >>> list ( chunk ( range ( 10 ), chunk_size = 3 )) [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]] Parameters: Name Type Description Default values Iterable [ T ] The stream of items to pack into chunks. required chunk_size int Desired length of each (but the last) chunk. required Yields: Type Description Iterable [ List [ T ]] The next chunk. Source code in great_ai/utilities/chunk.py 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 def chunk ( values : Iterable [ T ], chunk_size : int ) -> Iterable [ List [ T ]]: \"\"\"Turn an iterable of items into an iterable of lists (chunks) of items. Each returned chunk is of length `chunk_size` except the last one the length of which is between 1 and `chunk_size`. Useful for parallel processing. Examples: >>> list(chunk(range(10), chunk_size=3)) [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]] Args: values: The stream of items to pack into chunks. chunk_size: Desired length of each (but the last) chunk. Yields: The next chunk. \"\"\" assert chunk_size >= 1 result : List [ T ] = [] for v in values : result . append ( v ) if len ( result ) == chunk_size : yield result result = [] if len ( result ) > 0 : yield result unchunk ( chunks ) # Turn a stream of chunks of items into a stream of items (flatten operation). The inverse operation of chunk . Useful for parallel processing. Similar to itertools.chain but ignores None chunks. Examples: >>> list ( unchunk ([[ 0 , 1 , 2 ], [ 3 , 4 , 5 ], [ 6 , 7 , 8 ], [ 9 ]])) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] Parameters: Name Type Description Default chunks Iterable [ Optional [ Iterable [ T ]]] Stream of chunks to unpack. required Yields: Type Description Iterable [ T ] The next item in the flattened iterable. Source code in great_ai/utilities/unchunk.py 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 def unchunk ( chunks : Iterable [ Optional [ Iterable [ T ]]]) -> Iterable [ T ]: \"\"\"Turn a stream of chunks of items into a stream of items (flatten operation). The inverse operation of [chunk][great_ai.utilities.chunk.chunk]. Useful for parallel processing. Similar to itertools.chain but ignores `None` chunks. Examples: >>> list(unchunk([[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]])) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] Args: chunks: Stream of chunks to unpack. Yields: The next item in the flattened iterable. \"\"\" for chunk in chunks : if chunk is not None : yield from chunk Operations # ConfigFile # Bases: Mapping [ str , str ] A small and safe INI -style configuration loader with dict and ENV support. The values can be accessed using both dot- and index-notation. It is compatible with the dict interface. File format example: # comments are allowed everywhere key = value # you can leave or omit whitespace around the equal-sign my_hashtag = \"#great_ai\" # the r-value can be quoted with \" or ' or `. my_var = my_default_value # Default values can be given to env-vars, # see next line. The default value must come first. my_var = ENV : MY_ENV_VAR # If the value starts with the `ENV:` prefix, # it is looked up from the environment variables. Examples: >>> ConfigFile ( 'tests/utilities/data/simple.conf' ) ConfigFile(path=tests/utilities/data/simple.conf) {'zeroth_key': 'test', 'first_key': 'Andr\u00e1s'} >>> ConfigFile ( 'tests/utilities/data/simple.conf' ) . zeroth_key 'test' >>> ConfigFile ( 'tests/utilities/data/simple.conf' ) . second_key Traceback (most recent call last): ... KeyError : 'Key `second_key` is not found in configuration file ... >>> a = ConfigFile ( 'tests/utilities/data/simple.conf' ) >>> { ** a } {'zeroth_key': 'test', 'first_key': 'Andr\u00e1s'} Source code in great_ai/utilities/config_file/config_file.py 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 class ConfigFile ( Mapping [ str , str ]): \"\"\"A small and safe `INI`-style configuration loader with `dict` and `ENV` support. The values can be accessed using both dot- and index-notation. It is compatible with the `dict` interface. File format example: ```toml # comments are allowed everywhere key = value # you can leave or omit whitespace around the equal-sign my_hashtag = \"#great_ai\" # the r-value can be quoted with \" or ' or `. my_var = my_default_value # Default values can be given to env-vars, # see next line. The default value must come first. my_var = ENV:MY_ENV_VAR # If the value starts with the `ENV:` prefix, # it is looked up from the environment variables. ``` Examples: >>> ConfigFile('tests/utilities/data/simple.conf') ConfigFile(path=tests/utilities/data/simple.conf) {'zeroth_key': 'test', 'first_key': 'Andr\u00e1s'} >>> ConfigFile('tests/utilities/data/simple.conf').zeroth_key 'test' >>> ConfigFile('tests/utilities/data/simple.conf').second_key Traceback (most recent call last): ... KeyError: 'Key `second_key` is not found in configuration file ... >>> a = ConfigFile('tests/utilities/data/simple.conf') >>> {**a} {'zeroth_key': 'test', 'first_key': 'Andr\u00e1s'} \"\"\" ENVIRONMENT_VARIABLE_KEY_PREFIX = \"ENV\" def __init__ ( self , path : Union [ Path , str ], * , ignore_missing : bool = False ) -> None : \"\"\"Load and parse a configuration file. Everything is eager-loaded, thus, exceptions may be thrown here. Args: path: Local path of the configuration file. ignore_missing: Don't raise an exception on missing environment variables. Raises: FileNotFoundError: If there is no file at the specified path. ParseError: If the provided file does not conform to the expected format. KeyError: If there is duplication in the keys. ValueError: If an environment variable is referenced but it is not set in the system and `ignore_missing=False`. \"\"\" if not isinstance ( path , Path ): path = Path ( path ) if not path . exists (): raise FileNotFoundError ( path . absolute ()) self . _ignore_missing = ignore_missing self . _path = path self . _key_values : Dict [ str , str ] = {} self . _parse () @property def path ( self ) -> Path : \"\"\"Original path from where the configuration was loaded.\"\"\" return self . _path def _parse ( self ) -> None : with open ( self . _path , encoding = \"utf-8\" ) as f : lines : str = f . read () matches = pattern . findall ( lines ) for key , * values in matches : try : value = next ( v for v in values if v ) except StopIteration : raise ParseError ( f \"\"\"Cannot parse config file ( { self . _path . absolute () } ), error at key ` { key } `\"\"\" ) already_exists = key in self . _key_values if already_exists and not value . startswith ( f \" { self . ENVIRONMENT_VARIABLE_KEY_PREFIX } :\" ): raise KeyError ( f \"Key ` { key } ` has been already defined and its value is ` { self . _key_values [ key ] } `\" ) if value . startswith ( f \" { self . ENVIRONMENT_VARIABLE_KEY_PREFIX } :\" ): _ , value = value . split ( \":\" ) if value not in os . environ : issue = f \"\"\"The value of ` { key } ` contains the \" { self . ENVIRONMENT_VARIABLE_KEY_PREFIX } ` prefix but ` { value } ` is not defined as an environment variable\"\"\" if already_exists : logger . warning ( f \"\"\" { issue } , using the default value defined above (` { self . _key_values [ key ] } `)\"\"\" ) continue elif self . _ignore_missing : logger . warning ( issue ) else : raise ValueError ( f \" { issue } and no default value has been provided\" ) else : value = os . environ [ value ] self . _key_values [ key ] = value def __getattr__ ( self , key : str ) -> str : if key in self . _key_values : return self . _key_values [ key ] raise KeyError ( f \"Key ` { key } ` is not found in configuration file ( { self . _path . absolute () } )\" ) __getitem__ = __getattr__ def __iter__ ( self ) -> Iterator [ str ]: return iter ( self . _key_values ) def __len__ ( self ) -> int : return len ( self . _key_values ) def keys ( self ) -> KeysView [ str ]: return self . _key_values . keys () def values ( self ) -> ValuesView [ str ]: return self . _key_values . values () def items ( self ) -> ItemsView [ str , str ]: return self . _key_values . items () def __repr__ ( self ) -> str : return f \" { type ( self ) . __name__ } (path= { self . _path . as_posix () } ) { self . _key_values } \" __init__ ( path , * , ignore_missing = False ) # Load and parse a configuration file. Everything is eager-loaded, thus, exceptions may be thrown here. Parameters: Name Type Description Default path Union [ Path , str ] Local path of the configuration file. required ignore_missing bool Don't raise an exception on missing environment variables. False Raises: Type Description FileNotFoundError If there is no file at the specified path. ParseError If the provided file does not conform to the expected format. KeyError If there is duplication in the keys. ValueError If an environment variable is referenced but it is not set in the system and ignore_missing=False . Source code in great_ai/utilities/config_file/config_file.py 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 def __init__ ( self , path : Union [ Path , str ], * , ignore_missing : bool = False ) -> None : \"\"\"Load and parse a configuration file. Everything is eager-loaded, thus, exceptions may be thrown here. Args: path: Local path of the configuration file. ignore_missing: Don't raise an exception on missing environment variables. Raises: FileNotFoundError: If there is no file at the specified path. ParseError: If the provided file does not conform to the expected format. KeyError: If there is duplication in the keys. ValueError: If an environment variable is referenced but it is not set in the system and `ignore_missing=False`. \"\"\" if not isinstance ( path , Path ): path = Path ( path ) if not path . exists (): raise FileNotFoundError ( path . absolute ()) self . _ignore_missing = ignore_missing self . _path = path self . _key_values : Dict [ str , str ] = {} self . _parse () path () property # Original path from where the configuration was loaded. Source code in great_ai/utilities/config_file/config_file.py 83 84 85 86 @property def path ( self ) -> Path : \"\"\"Original path from where the configuration was loaded.\"\"\" return self . _path get_logger ( name , level = logging . INFO , disable_colors = False ) # Return a customised logger used throughout the GreatAI codebase. Uses colors, and only prints timestamps when not running inside notebook. Source code in great_ai/utilities/logger/get_logger.py 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 def get_logger ( name : str , level : int = logging . INFO , disable_colors : bool = False ) -> logging . Logger : \"\"\"Return a customised logger used throughout the GreatAI codebase. Uses colors, and only prints timestamps when not running inside notebook. \"\"\" if name not in loggers : logger = logging . getLogger ( name ) logger . setLevel ( level ) try : get_ipython () # type: ignore log_format = \" %(message)s \" except NameError : # will fail outside of a notebook https://ipython.org/ log_format = \" %(asctime)s | %(levelname)8s | %(message)s \" stdout_handler = logging . StreamHandler () stdout_handler . setLevel ( level ) if not disable_colors : stdout_handler . setFormatter ( CustomFormatter ( log_format )) logger . addHandler ( stdout_handler ) loggers [ name ] = logger return loggers [ name ]","title":"Utilities"},{"location":"reference/utilities/#utilities","text":"from great_ai.utilities import *","title":"Utilities"},{"location":"reference/utilities/#nlp-tools","text":"Well-tested tools that can be used in production with confidence. The toolbox of feature-extraction functions is expected to grow to cover other domains as well.","title":"NLP tools"},{"location":"reference/utilities/#great_ai.utilities.clean.clean","text":"Clean all XML, LaTeX, PDF-extraction, and Unicode artifacts from the text. The cleaning is quite heavy-weight and can be destructive. However, when working with text, this is usually required to achieve sufficient cleanliness before further processing. Optionally, the text can be turned into ASCII. Carefully consider whether this is absolutely needed for your use-case. Examples: >>> clean ( '