├── requirements.txt ├── pyproject.toml ├── README.md ├── poetry.lock ├── .gitignore └── main.py /requirements.txt: -------------------------------------------------------------------------------- 1 | regex~=2020.4.4 2 | pyfiglet~=0.8.post1 -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "poetry.masonry.api" 3 | requires = ["poetry>=0.12"] 4 | 5 | [tool] 6 | [tool.poetry] 7 | authors = ["Cor Pruijs "] 8 | description = "" 9 | name = "coinflipsimulator" 10 | version = "0.1.0" 11 | [tool.poetry.dependencies] 12 | pyfiglet = "^0.8.0" 13 | python = "^3.7" 14 | regex = "^2020.4" 15 | [tool.poetry.dev-dependencies] 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # drudgery 2 | See which implementation is the most performant. 3 | 4 | This project started off as an implementation of the "Coin Flip Streaks" experiment from [automate the boring stuff](https://automatetheboringstuff.com/2e/chapter4/), ended up in an experiment which checks which implementation is the most performant. Thanks to [@bef1re](https://github.com/bef1re) for creating this together with me :-). 5 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | category = "main" 3 | description = "Pure-python FIGlet implementation" 4 | name = "pyfiglet" 5 | optional = false 6 | python-versions = "*" 7 | version = "0.8.post1" 8 | 9 | [[package]] 10 | category = "main" 11 | description = "Alternative regular expression module, to replace re." 12 | name = "regex" 13 | optional = false 14 | python-versions = "*" 15 | version = "2020.4.4" 16 | 17 | [metadata] 18 | content-hash = "5b6ef69c7228ee6f8d33c3af5b627bc9a6a1a7e3d303e84ea7fb883b9b1cfb09" 19 | python-versions = "^3.7" 20 | 21 | [metadata.hashes] 22 | pyfiglet = ["c6c2321755d09267b438ec7b936825a4910fec696292139e664ca8670e103639", "d555bcea17fbeaf70eaefa48bb119352487e629c9b56f30f383e2c62dd67a01c"] 23 | regex = ["08119f707f0ebf2da60d2f24c2f39ca616277bb67ef6c92b72cbf90cbe3a556b", "0ce9537396d8f556bcfc317c65b6a0705320701e5ce511f05fc04421ba05b8a8", "1cbe0fa0b7f673400eb29e9ef41d4f53638f65f9a2143854de6b1ce2899185c3", "2294f8b70e058a2553cd009df003a20802ef75b3c629506be20687df0908177e", "23069d9c07e115537f37270d1d5faea3e0bdded8279081c4d4d607a2ad393683", "24f4f4062eb16c5bbfff6a22312e8eab92c2c99c51a02e39b4eae54ce8255cd1", "295badf61a51add2d428a46b8580309c520d8b26e769868b922750cf3ce67142", "2a3bf8b48f8e37c3a40bb3f854bf0121c194e69a650b209628d951190b862de3", "4385f12aa289d79419fede43f979e372f527892ac44a541b5446617e4406c468", "5635cd1ed0a12b4c42cce18a8d2fb53ff13ff537f09de5fd791e97de27b6400e", "5bfed051dbff32fd8945eccca70f5e22b55e4148d2a8a45141a3b053d6455ae3", "7e1037073b1b7053ee74c3c6c0ada80f3501ec29d5f46e42669378eae6d4405a", "90742c6ff121a9c5b261b9b215cb476eea97df98ea82037ec8ac95d1be7a034f", "a58dd45cb865be0ce1d5ecc4cfc85cd8c6867bea66733623e54bd95131f473b6", "c087bff162158536387c53647411db09b6ee3f9603c334c90943e97b1052a156", "c162a21e0da33eb3d31a3ac17a51db5e634fc347f650d271f0305d96601dc15b", "c9423a150d3a4fc0f3f2aae897a59919acd293f4cb397429b120a5fcd96ea3db", "ccccdd84912875e34c5ad2d06e1989d890d43af6c2242c6fcfa51556997af6cd", "e91ba11da11cf770f389e47c3f5c30473e6d85e06d7fd9dcba0017d2867aab4a", "ea4adf02d23b437684cd388d557bf76e3afa72f7fed5bbc013482cc00c816948", "fb95debbd1a824b2c4376932f2216cc186912e389bdb0e27147778cf6acb3f89"] 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 141 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 142 | 143 | # User-specific stuff 144 | .idea/**/workspace.xml 145 | .idea/**/tasks.xml 146 | .idea/**/usage.statistics.xml 147 | .idea/**/dictionaries 148 | .idea/**/shelf 149 | 150 | # Generated files 151 | .idea/**/contentModel.xml 152 | 153 | # Sensitive or high-churn files 154 | .idea/**/dataSources/ 155 | .idea/**/dataSources.ids 156 | .idea/**/dataSources.local.xml 157 | .idea/**/sqlDataSources.xml 158 | .idea/**/dynamic.xml 159 | .idea/**/uiDesigner.xml 160 | .idea/**/dbnavigator.xml 161 | 162 | # Gradle 163 | .idea/**/gradle.xml 164 | .idea/**/libraries 165 | 166 | # Gradle and Maven with auto-import 167 | # When using Gradle or Maven with auto-import, you should exclude module files, 168 | # since they will be recreated, and may cause churn. Uncomment if using 169 | # auto-import. 170 | # .idea/artifacts 171 | # .idea/compiler.xml 172 | # .idea/jarRepositories.xml 173 | # .idea/modules.xml 174 | # .idea/*.iml 175 | # .idea/modules 176 | # *.iml 177 | # *.ipr 178 | 179 | # CMake 180 | cmake-build-*/ 181 | 182 | # Mongo Explorer plugin 183 | .idea/**/mongoSettings.xml 184 | 185 | # File-based project format 186 | *.iws 187 | 188 | # IntelliJ 189 | out/ 190 | 191 | # mpeltonen/sbt-idea plugin 192 | .idea_modules/ 193 | 194 | # JIRA plugin 195 | atlassian-ide-plugin.xml 196 | 197 | # Cursive Clojure plugin 198 | .idea/replstate.xml 199 | 200 | # Crashlytics plugin (for Android Studio and IntelliJ) 201 | com_crashlytics_export_strings.xml 202 | crashlytics.properties 203 | crashlytics-build.properties 204 | fabric.properties 205 | 206 | # Editor-based Rest Client 207 | .idea/httpRequests 208 | 209 | # Android studio 3.1+ serialized cache file 210 | .idea/caches/build_file_checksums.ser -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import random 2 | import time 3 | 4 | from regex import findall 5 | from statistics import mean 6 | from pyfiglet import figlet_format 7 | 8 | EXPERIMENT_COUNT = 200_000 9 | GRAPH_WIDTH = 50 10 | 11 | 12 | # Generate a sequence of 100 'H' (Heads) and 'T' (Tails) throws 13 | # and return the amount of 6-long chains of the same value 14 | def experiment_a(): 15 | """The original""" 16 | flip_results = random.choices(('H', 'T'), k=100) 17 | streak_count = 0 18 | for i in range(len(flip_results) - 6): 19 | if ('T' not in flip_results[i:i + 6]) or ('H' not in flip_results[i:i + 6]): 20 | streak_count += 1 21 | return streak_count 22 | 23 | 24 | def experiment_b(): 25 | """The authentic original original.""" 26 | flip_results = random.choices(('H', 'T'), k=100) 27 | streak_count = 0 28 | for i in range(len(flip_results) - 6): 29 | if (flip_results[i] == 'H' and 'T' not in flip_results[i:i + 6]) or ( 30 | flip_results[i] == 'T' and 'H' not in flip_results[i:i + 6]): 31 | streak_count += 1 32 | return streak_count 33 | 34 | 35 | def experiment_c(): 36 | """We thought we were being smart here""" 37 | flip_results = "".join(random.choices(('H', 'T'), k=100)) 38 | streak_count = 0 39 | for i in range(len(flip_results) - 6): 40 | if (flip_results[i:i + 6] == "T" * 6) or ( 41 | flip_results[i:i + 6] == "H" * 6): 42 | streak_count += 1 43 | return streak_count 44 | 45 | 46 | def experiment_d(): 47 | """No string replication for extra speed. No strings attached.""" 48 | flip_results = "".join(random.choices(('H', 'T'), k=100)) 49 | streak_count = 0 50 | for i in range(len(flip_results) - 6): 51 | if (flip_results[i:i + 6] == "TTTTTT") or ( 52 | flip_results[i:i + 6] == "HHHHHH"): 53 | streak_count += 1 54 | return streak_count 55 | 56 | 57 | def experiment_e(): 58 | """e.""" 59 | flip_results = "".join(random.choices(('H', 'T'), k=100)) 60 | streak_count = 0 61 | for i in range(len(flip_results) - 6): 62 | if (flip_results[i:i + 6] == flip_results[i] * 6): 63 | streak_count += 1 64 | return streak_count 65 | 66 | 67 | def experiment_f(): 68 | """Our old favourite""" 69 | flip_results = "".join(random.choices(('H', 'T'), k=100)) 70 | streak_count = 0 71 | for i in range(len(flip_results) - 6): 72 | streak_count += (flip_results[i:i + 6] == flip_results[i] * 6) 73 | return streak_count 74 | 75 | 76 | def experiment_g(): 77 | """streak_count += (flip_results[i:i+6] in ('TTTTTT', 'HHHHHH'))""" 78 | flip_results = "".join(random.choices(('H', 'T'), k=100)) 79 | streak_count = 0 80 | for i in range(len(flip_results) - 6): 81 | streak_count += (flip_results[i:i + 6] in ('TTTTTT', 'HHHHHH')) 82 | return streak_count 83 | 84 | def experiment_g2(): 85 | """streak_count += (flip_results[i:i+6] in ('TTTTTT', 'HHHHHH')) choice""" 86 | flip_results = "".join(random.choice(('H', 'T')) for i in range(100)) 87 | streak_count = 0 88 | for i in range(len(flip_results) - 6): 89 | streak_count += (flip_results[i:i + 6] in ('TTTTTT', 'HHHHHH')) 90 | return streak_count 91 | 92 | 93 | def experiment_h(): 94 | """(flip_results[i:i+6] == "HHHHHH" ∨ flip_results[i:i+6] == "TTTTTT")""" 95 | flip_results = "".join(random.choices(('H', 'T'), k=100)) 96 | streak_count = 0 97 | for i in range(len(flip_results) - 6): 98 | streak_count += (flip_results[i:i + 6] == "HHHHHH" 99 | or flip_results[i:i + 6] == "TTTTTT") 100 | return streak_count 101 | 102 | 103 | def experiment_i(): 104 | """Same as h but we cache a var""" 105 | flip_results = "".join(random.choices(('H', 'T'), k=100)) 106 | streak_count = 0 107 | for i in range(len(flip_results) - 6): 108 | temp = flip_results[i:i + 6] 109 | streak_count += (temp == "HHHHHH" or temp == "TTTTTT") 110 | return streak_count 111 | 112 | def experiment_k(): 113 | "Leon's test" 114 | flip_results = "".join(random.choice(('H', 'T')) for i in range(100)) 115 | 116 | streaks_H = len(findall("HHHHHH", flip_results, overlapped=True)) 117 | streaks_T = len(findall("TTTTTT", flip_results, overlapped=True)) 118 | 119 | return streaks_H + streaks_T 120 | 121 | def experiment_k2(): 122 | "Leon's test choices" 123 | flip_results = "".join(random.choices(('H', 'T'), k=100)) 124 | 125 | streaks_H = len(findall("HHHHHH", flip_results, overlapped=True)) 126 | streaks_T = len(findall("TTTTTT", flip_results, overlapped=True)) 127 | 128 | return streaks_H + streaks_T 129 | 130 | 131 | def experiment_l(): 132 | "Leon's test" 133 | flip_results = "".join(random.choice(('H', 'T')) for i in range(100)) 134 | 135 | streaks = len(findall("HHHHHH|TTTTTT", flip_results, overlapped=True)) 136 | 137 | return streaks 138 | 139 | 140 | def experiment_l2(): 141 | "Leon's test choices" 142 | flip_results = "".join(random.choices(('H', 'T'), k=100)) 143 | 144 | streaks = len(findall("HHHHHH|TTTTTT", flip_results, overlapped=True)) 145 | 146 | return streaks 147 | 148 | 149 | def experiment_l3(): 150 | "Art" 151 | flip_results = "".join(random.choices(('H', 'T'), k=100)) 152 | 153 | return len(findall("HHHHHH|TTTTTT", flip_results, overlapped=True)) 154 | 155 | 156 | def experiment_l4(): 157 | "One liner Art" 158 | return len(findall("HHHHHH|TTTTTT", "".join(random.choices(('H', 'T'), k=100)), overlapped=True)) 159 | 160 | 161 | def experiment_r(): 162 | "Roos's algorithm" 163 | randomlist = [] 164 | numberOfSequenceHeads = 0 165 | numberOfSequenceTails = 0 166 | numberOfStreaks = 0 167 | 168 | for experimentNumber in range(100): 169 | number = random.choice(['Heads','Tails']) 170 | randomlist.append(number) 171 | for i in range(len(randomlist)): 172 | if randomlist[i] == 'Heads': 173 | numberOfSequenceTails = 0 174 | numberOfSequenceHeads +=1 175 | if numberOfSequenceHeads ==6: 176 | numberOfStreaks +=1 177 | elif randomlist[i] == 'Tails': 178 | numberOfSequenceHeads = 0 179 | numberOfSequenceTails += 1 180 | if numberOfSequenceTails == 6: 181 | numberOfStreaks +=1 182 | return numberOfStreaks 183 | 184 | 185 | def experiment_u_char(): 186 | """Use bits in number to store coin-flip results (store as char)""" 187 | result = 0 188 | 189 | data = ['T'] * 100 190 | random_number = random.getrandbits(100) 191 | 192 | for current_character in range(0, 100): 193 | if random_number & (1 << current_character): 194 | data[current_character] = 'H' 195 | 196 | random_number = streak_length = 0 197 | last_character = ' ' 198 | for current_character in data: 199 | if current_character == last_character: 200 | streak_length += 1 201 | if streak_length >= 6: 202 | random_number += 1 203 | else: 204 | streak_length = 1 205 | last_character = current_character 206 | 207 | result += random_number 208 | 209 | return result 210 | 211 | 212 | def experiment_u_bits(): 213 | """Use bits in number to store coin-flip results (use bits to process)""" 214 | number_of_samples = 100 215 | streak_length = 6 216 | 217 | data = random.getrandbits(100) 218 | 219 | result = 0 220 | length_of_current_streak = 0 221 | last_sample = None 222 | for sample_index in range(0, number_of_samples): 223 | sample = data & (1 << sample_index) == 0 224 | 225 | if sample != last_sample: 226 | length_of_current_streak = 1 227 | last_sample = sample 228 | else: 229 | length_of_current_streak += 1 230 | if length_of_current_streak >= streak_length: 231 | result += 1 232 | 233 | return result 234 | 235 | 236 | def experiment_u_bits_mask(): 237 | """Use bits in number to store coin-flip results and check with bit-mask (use bits to process)""" 238 | number_of_samples = 100 239 | streak_length = 6 240 | 241 | data = random.getrandbits(100) 242 | 243 | mask = 2 ** streak_length - 1 244 | 245 | result = 0 246 | for sample_index in range(0, number_of_samples - streak_length): 247 | current = data & mask 248 | 249 | if current == 0 or current == mask: 250 | result += 1 251 | 252 | data = data >> 1 253 | 254 | return result 255 | 256 | 257 | # Test runner 258 | def run_test(test_func): 259 | start_time = time.time() 260 | results = [] 261 | for i in range(EXPERIMENT_COUNT): 262 | results.append(test_func()) 263 | 264 | duration = time.time() - start_time 265 | print(f'Result: {mean(results)}') 266 | print(f'Time: \x1b[32m{duration}\x1b[0m') 267 | print() 268 | return duration 269 | 270 | 271 | test_functions = [eval(lcl) for lcl in locals() if lcl.startswith("experiment_")] 272 | 273 | results = {} 274 | for test_func in test_functions: 275 | name = test_func.__name__ 276 | print(f"Running \x1b[31m{name}\x1b[0m") 277 | print(test_func.__doc__) 278 | duration = run_test(test_func) 279 | 280 | results[name] = duration 281 | 282 | 283 | print("\nThe Results:") 284 | 285 | max_name_length = max(map(len, results.keys())) 286 | max_value = max(results.values()) 287 | winner = sorted(results.items(), key=lambda item: item[1])[0][0] 288 | 289 | for res in results: 290 | print(res.rjust(max_name_length, ' ') 291 | + ' | ' 292 | + ('\x1b[32m' if res == winner else '\x1b[31m') 293 | + '═' * int(results[res] / max_value * GRAPH_WIDTH) 294 | + "\x1b[30m" 295 | + '═' * (GRAPH_WIDTH - int(results[res] / max_value * GRAPH_WIDTH)) 296 | + (' \x1b[32m' if res == winner else ' \x1b[30m') 297 | + ("%.4fs" % round(results[res], 4)) 298 | + "\x1b[0m") 299 | 300 | 301 | print("\n\nAnd the absolute winner is...") 302 | print(figlet_format(winner)) 303 | print(f"It seems that {eval(winner).__doc__} was the best approach after all") --------------------------------------------------------------------------------