{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Zipf and Heaps, fitted\n", "\n", "Fitting both laws to real rank counts, the coverage curve, and the length trap that makes type token ratio misleading.\n", "\n", "From chapter 3, [The Empirical Laws of Text](https://nlp.jcrlabz.com/book/empirical-laws/), of the course notes.\n", "\n", "Source: `book/code/worked_examples/empirical_laws.py`" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## What you need\n", "\n", "Nothing. This example uses only the Python standard library." ] }, { "cell_type": "code", "metadata": {}, "execution_count": null, "outputs": [], "source": [ "# Nothing to install: this example uses only the Python standard library.\n", "import sys; print(sys.version.split()[0])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## The script\n", "\n", "Everything the example defines, in one cell. Run it and the definitions\n", "are live in the notebook." ] }, { "cell_type": "code", "metadata": {}, "execution_count": null, "outputs": [], "source": [ "\"\"\"Zipf, Mandelbrot, Heaps and TTR, worked as Chapter 3 works them.\n", "\n", "Four ideas, all of them a straight line hiding inside a curve.\n", "\n", " facts the Brown totals every other section leans on\n", " zipf fit alpha by least squares, and watch it move with the range\n", " mandelbrot add the rank offset beta and watch the error drop\n", " coverage how many tokens the top k words account for\n", " heaps fit the vocabulary growth exponent, then extrapolate and fail\n", " prep which cleaning step moves tokens, and which moves types\n", " ttr why two type-token ratios are not comparable\n", "\n", " python3 empirical_laws.py zipf\n", " python3 empirical_laws.py all\n", "\n", "The data is measured, not invented, and it is measured the way the course\n", "grades it. Every Brown figure below comes from `autograder/nlp_pipeline.py`,\n", "which is the Assignment 1 contract: its TOKEN_RE, lowercased, natural logs,\n", "and fit_heaps sampling the growth curve every 1000 tokens. Run A1 against\n", "your own corpus and the procedure will be the one shown here.\n", "\n", "The budget-speech ranks are the fifteen printed on the Zipf slide.\n", "\n", "Install: nothing, the Python standard library is enough\n", "\"\"\"\n", "\n", "import argparse\n", "import math\n", "\n", "# ------------------------------------------------------------------ data\n", "\n", "# Indian budget speeches. (rank, frequency) as printed on the slide.\n", "BUDGET = [(1, 59042), (2, 46110), (3, 35873), (4, 24661), (5, 22916),\n", " (6, 14426), (7, 14366), (8, 11155), (9, 9760), (10, 9070),\n", " (233, 479), (234, 478), (235, 477), (236, 476), (237, 476)]\n", "BUDGET_V = 92366 # last rank in the frequency table\n", "\n", "# Brown corpus, measured through autograder/nlp_pipeline.py.\n", "BROWN_N = 1_023_734\n", "BROWN_V = 44_324\n", "BROWN_TOP = [(\"the\", 70003), (\"of\", 36473), (\"and\", 28935), (\"to\", 26247),\n", " (\"a\", 23507), (\"in\", 21419), (\"that\", 10596), (\"is\", 10109),\n", " (\"was\", 9815), (\"he\", 9548)]\n", "BROWN_HAPAX_PCT = 39.5 # types occurring exactly once\n", "BROWN_LE2_PCT = 54.3 # types occurring twice or fewer\n", "\n", "# alpha from fit_zipf over a rank range. The range is the point.\n", "BROWN_ALPHA = [(100, 0.9816), (1000, 0.9668), (10000, 1.0726),\n", " (BROWN_V, 1.3492)]\n", "# (rank range, alpha, beta) from fit_mandelbrot\n", "BROWN_MANDELBROT = [(1000, 0.9668, 0.00), (10000, 1.0984, 10.25),\n", " (BROWN_V, 1.5180, 403.25)]\n", "# cumulative share of all tokens held by the top k types, measured\n", "BROWN_COVERAGE = [(10, 24.09), (100, 47.18), (1000, 69.13), (10000, 92.60)]\n", "# vocabulary after N tokens, sampled off the same heaps_curve fit_heaps uses\n", "BROWN_GROWTH = [(1_000, 468), (10_000, 2_584), (100_000, 12_962),\n", " (500_000, 31_854), (1_000_000, 43_648)]\n", "BROWN_HEAPS_K = 16.18\n", "BROWN_HEAPS_B = 0.5764\n", "# (genre, tokens, raw TTR, MSTTR over non-overlapping 8000-token windows)\n", "BROWN_GENRES = [(\"press reportage\", 90_089, 0.1359, 0.3077),\n", " (\"humour\", 18_450, 0.2552, 0.3021),\n", " (\"general fiction\", 58_605, 0.1458, 0.2744),\n", " (\"romance\", 58_966, 0.1307, 0.2520),\n", " (\"learned\", 164_036, 0.0885, 0.2478),\n", " (\"government\", 63_214, 0.1106, 0.2320)]\n", "# effect of each preprocessing knob, as a share of the raw figures\n", "BROWN_PREP = [(\"stop words removed\", 55.5, 99.8),\n", " (\"numbers stripped\", 99.0, 97.5),\n", " (\"Porter stemming\", 100.0, 65.4),\n", " (\"drop count <= 5\", 94.1, 28.0)]\n", "\n", "\n", "# ------------------------------------------------------------------ maths\n", "\n", "def least_squares(xs, ys):\n", " \"\"\"Slope and intercept of the line that minimises squared error.\n", "\n", " Every law in this chapter is a power law, and every power law is a\n", " straight line once you take logarithms of both axes. So the whole of\n", " curve fitting here is this one function applied to logged data.\n", " \"\"\"\n", " n = len(xs)\n", " mx, my = sum(xs) / n, sum(ys) / n\n", " sxy = sum((x - mx) * (y - my) for x, y in zip(xs, ys))\n", " sxx = sum((x - mx) ** 2 for x in xs)\n", " slope = sxy / sxx\n", " return slope, my - slope * mx\n", "\n", "\n", "def sse(xs, ys, slope, intercept):\n", " \"\"\"Sum of squared errors, in log space, of a fitted line.\"\"\"\n", " return sum((y - (slope * x + intercept)) ** 2 for x, y in zip(xs, ys))\n", "\n", "\n", "def fit_zipf(points):\n", " \"\"\"Fit f(r) = C / r^alpha. Returns (alpha, C, error).\"\"\"\n", " xs = [math.log10(r) for r, _ in points]\n", " ys = [math.log10(f) for _, f in points]\n", " slope, intercept = least_squares(xs, ys)\n", " return -slope, 10 ** intercept, sse(xs, ys, slope, intercept)\n", "\n", "\n", "def fit_mandelbrot(points, beta_max=200.0, step=0.1):\n", " \"\"\"Fit f(r) = C / (r + beta)^alpha by scanning beta.\n", "\n", " beta is inside the logarithm, so it cannot be fitted by least squares.\n", " We scan it and least-squares the rest. Coarse, and enough to show that\n", " the valley is shallow.\n", " \"\"\"\n", " ys = [math.log10(f) for _, f in points]\n", " best = None\n", " beta = 0.0\n", " while beta <= beta_max:\n", " xs = [math.log10(r + beta) for r, _ in points]\n", " slope, intercept = least_squares(xs, ys)\n", " err = sse(xs, ys, slope, intercept)\n", " if best is None or err < best[0]:\n", " best = (err, -slope, beta, 10 ** intercept)\n", " beta += step\n", " err, alpha, beta, c = best\n", " return alpha, beta, c, err\n", "\n", "\n", "def harmonic(n, alpha):\n", " \"\"\"H(n, alpha) = sum of r^-alpha for r = 1..n.\n", "\n", " This is the normaliser that turns Zipf's law from a proportionality\n", " into a probability distribution, and it is what makes coverage\n", " computable.\n", " \"\"\"\n", " return sum(r ** (-alpha) for r in range(1, n + 1))\n", "\n", "\n", "# ------------------------------------------------------------------ demos\n", "\n", "def demo_facts():\n", " print(\"\\nBROWN the corpus the rest of this file measures\\n\")\n", " print(f\" tokens N {BROWN_N:>12,}\")\n", " print(f\" types V {BROWN_V:>12,}\")\n", " print(f\" TTR = V/N {BROWN_V/BROWN_N:>12.4f}\")\n", " top10 = sum(c for _, c in BROWN_TOP)\n", " print(f\"\\n {'rank':>5}{'word':>10}{'count':>9}{'% of tokens':>13}\")\n", " print(\" \" + \"-\" * 37)\n", " for i, (w, c) in enumerate(BROWN_TOP, 1):\n", " print(f\" {i:>5}{w:>10}{c:>9,}{100*c/BROWN_N:>12.2f}%\")\n", " print(f\"\\n The top ten types are {100*top10/BROWN_N:.2f}% of all tokens.\")\n", " print(f\" {BROWN_HAPAX_PCT}% of the types occur exactly once, and \"\n", " f\"{BROWN_LE2_PCT}% twice or fewer.\")\n", " print(\"\\n These come from the A1 tokenizer. A stricter rule that keeps\")\n", " print(\" only fully alphabetic tokens gives a different V and a different\")\n", " print(\" hapax share. Neither is wrong. State the tokenizer, always.\")\n", "\n", "\n", "def demo_zipf():\n", " print(\"\\nZIPF f(r) = C / r^alpha, budget speeches\\n\")\n", "\n", " # A student's first instinct: two points and a ratio.\n", " f1 = dict(BUDGET)[1]\n", " f10 = dict(BUDGET)[10]\n", " alpha2 = math.log10(f1 / f10)\n", " print(f\" two-point estimate from r=1 and r=10\")\n", " print(f\" alpha = log10({f1}/{f10}) = {alpha2:.4f}\")\n", " pred = f1 / 235 ** alpha2\n", " print(f\" predict f(235) = {f1} / 235^{alpha2:.4f} = {pred:.0f}\"\n", " f\" actual 477 off by {100*(pred-477)/477:+.0f}%\")\n", "\n", " # The fix: use every point you have, in log space.\n", " alpha, c, err = fit_zipf(BUDGET)\n", " print(f\"\\n least squares over all {len(BUDGET)} points\")\n", " print(f\" alpha = {alpha:.4f} C = {c:,.0f} SSE = {err:.4f}\")\n", " pred = c / 235 ** alpha\n", " print(f\" predict f(235) = {c:,.0f} / 235^{alpha:.4f} = {pred:.0f}\"\n", " f\" actual 477 off by {100*(pred-477)/477:+.0f}%\")\n", "\n", " print(\"\\n Two points threw away thirteen measurements and paid for it.\")\n", " print(\" The head of the distribution is flatter than the body, so any\")\n", " print(\" estimate read off the head alone understates alpha.\")\n", "\n", " # alpha is not a constant of the language. It is a constant of a range.\n", " print(f\"\\n alpha depends on the rank range you fit, Brown corpus:\")\n", " for hi, a in BROWN_ALPHA:\n", " print(f\" ranks 1..{hi:<6} alpha = {a:.4f}\")\n", " print(\" Report the range with the exponent, or the exponent means\")\n", " print(\" nothing. The tail falls faster than the head, so the wider the\")\n", " print(\" window, the steeper the fitted line.\")\n", "\n", "\n", "def demo_mandelbrot():\n", " print(\"\\nMANDELBROT f(r) = C / (r + beta)^alpha, budget speeches\\n\")\n", " a0, c0, e0 = fit_zipf(BUDGET)\n", " alpha, beta, c, err = fit_mandelbrot(BUDGET)\n", " print(f\" {'model':<14}{'alpha':>8}{'beta':>7}{'C':>12}{'SSE':>9}\")\n", " print(\" \" + \"-\" * 50)\n", " print(f\" {'Zipf':<14}{a0:>8.4f}{'0':>7}{c0:>12,.0f}{e0:>9.4f}\")\n", " print(f\" {'Mandelbrot':<14}{alpha:>8.4f}{beta:>7.1f}{c:>12,.0f}{err:>9.4f}\")\n", " pred = c / (235 + beta) ** alpha\n", " print(f\"\\n predict f(235) = {pred:.0f}, actual 477, \"\n", " f\"off by {100*(pred-477)/477:+.0f}%\")\n", " print(f\" The error drops by {100*(e0-err)/e0:.0f}% for one extra\"\n", " f\" parameter.\")\n", "\n", " print(\"\\n beta shifts every rank to the right before the power is\")\n", " print(\" taken, which flattens the curve at small r and leaves the tail\")\n", " print(\" alone. It is a head correction and nothing else.\")\n", "\n", " print(\"\\n On Brown, beta depends entirely on how much of the tail you\")\n", " print(\" fit, because beta is what absorbs the bend:\")\n", " print(f\" {'ranks':<16}{'alpha':>8}{'beta':>10}\")\n", " for hi, a, b in BROWN_MANDELBROT:\n", " print(f\" 1..{hi:<13,}{a:>8.4f}{b:>10.2f}\")\n", " print(\" Fit the head alone and beta is zero, so Mandelbrot buys nothing.\")\n", " print(\" Fit every rank, which is what the A1 grader does, and beta runs\")\n", " print(\" into the hundreds. A large beta is not a red flag. It is what\")\n", " print(\" a single power law needs in order to cover a curve that bends.\")\n", "\n", " print(\"\\n The valley is shallow. Nearby (alpha, beta) pairs fit almost\")\n", " print(\" as well, so the pair is meaningful and neither number is:\")\n", " ys = [math.log10(f) for _, f in BUDGET]\n", " for b in (0.0, 0.5, 1.0, 2.0, 5.0):\n", " xs = [math.log10(r + b) for r, _ in BUDGET]\n", " s, i = least_squares(xs, ys)\n", " print(f\" beta = {b:<5} best alpha = {-s:.4f} \"\n", " f\"SSE = {sse(xs, ys, s, i):.4f}\")\n", "\n", "\n", "def demo_coverage():\n", " print(\"\\nCOVERAGE what fraction of tokens the top k types hold\\n\")\n", " print(\" Zipf as a probability, not a proportionality:\")\n", " print(\" p(r) = r^-alpha / H(V, alpha), H(V,a) = sum r^-a, r=1..V\")\n", " print(\" so the top k types cover H(k, alpha) / H(V, alpha).\\n\")\n", "\n", " alpha = 0.9668 # Brown, fitted on ranks 1..1000\n", " hv = harmonic(BROWN_V, alpha)\n", " print(f\" Brown, V = {BROWN_V:,}, alpha = {alpha} fitted on ranks 1..1000\")\n", " print(f\" {'k':>7}{'model':>10}{'measured':>11}{'% of V':>9}\")\n", " print(\" \" + \"-\" * 37)\n", " for k, measured in BROWN_COVERAGE:\n", " model = 100 * harmonic(k, alpha) / hv\n", " print(f\" {k:>7,}{model:>9.2f}%{measured:>10.2f}%\"\n", " f\"{100*k/BROWN_V:>8.2f}%\")\n", "\n", " print(\"\\n Read the last column against the third. About 2% of the\")\n", " print(\" vocabulary carries about 69% of the running text.\")\n", " print(\" The model tracks the measurement at the head and drifts at the\")\n", " print(\" tail, because a single alpha cannot bend twice.\")\n", "\n", " print(\"\\n This is the number that justifies three later decisions.\")\n", " print(\" a stop list is short because the head is short\")\n", " print(\" a truncated vocabulary loses few tokens, not few types\")\n", " print(\" a subword vocabulary of 32k spends its budget on the head\")\n", "\n", "\n", "def fit_heaps():\n", " \"\"\"The (k, b) that autograder/nlp_pipeline.fit_heaps returns for Brown.\n", "\n", " Not a re-fit of the five rows printed below. fit_heaps samples the\n", " growth curve every 1000 tokens and fits all 1024 points, so quoting a\n", " fit of five hand-picked marks would disagree with the grader.\n", " \"\"\"\n", " return BROWN_HEAPS_B, BROWN_HEAPS_K\n", "\n", "\n", "def demo_heaps():\n", " print(\"\\nHEAPS V(N) = k N^b, Brown, sampled as fit_heaps samples it\\n\")\n", " print(f\" {'N':>11}{'V':>9}{'TTR':>9}\")\n", " print(\" \" + \"-\" * 29)\n", " for n, v in BROWN_GROWTH:\n", " print(f\" {n:>11,}{v:>9,}{v/n:>9.4f}\")\n", "\n", " slope, k_all = fit_heaps()\n", " print(f\"\\n fit_heaps over the whole 1024-point curve:\")\n", " print(f\" b = {slope:.4f} k = {k_all:.2f}\")\n", " print(\" b < 1, so vocabulary growth decelerates. It never stops,\")\n", " print(\" because a power law has no asymptote.\")\n", "\n", " print(\"\\n k and b are COUPLED, and this is the part most readers miss.\")\n", " print(\" A fit that raises b must lower k to pass through the same data,\")\n", " print(\" so a large b always arrives with a small k. Pin b on Brown and\")\n", " print(\" the k that best fits moves with it:\")\n", " print(f\" {'b pinned':>10}{'best k':>12}\")\n", " for pb, pk in [(0.4000, 155.93), (0.5000, 43.17), (0.6000, 11.95)]:\n", " print(f\" {pb:>10.4f}{pk:>12.2f}\")\n", " print(f\" {slope:>10.4f}{k_all:>12.2f} (both fitted freely)\")\n", " print(\" So quote the pair. 'k between 10 and 100 and b between 0.4 and\")\n", " print(\" 0.6' is one statement about a curve, not two independent ones.\")\n", "\n", " # Extrapolation, which is where the law bites back.\n", " early = [p for p in BROWN_GROWTH if p[0] <= 100_000]\n", " xs_e = [math.log(n) for n, _ in early]\n", " ys_e = [math.log(v) for _, v in early]\n", " s_e, i_e = least_squares(xs_e, ys_e)\n", " k_e = math.exp(i_e)\n", " pred = k_e * 1_000_000 ** s_e\n", " actual = dict(BROWN_GROWTH)[1_000_000]\n", " print(f\"\\n Now fit only the first 100,000 tokens and extrapolate.\")\n", " print(f\" On the full curve that prefix gives b = 0.7176, k = 3.50, and\")\n", " print(f\" V(1,000,000) = 70,799 actual {actual:,} off by +62%\")\n", " print(\" One decade of extrapolation, well over half again too many types.\")\n", " print(\" b is not a constant of the language either. It drifts down as N\")\n", " print(\" grows, because the log-log curve is gently concave.\")\n", " print(\" Fit over the range you will actually operate in.\")\n", "\n", " # The link back to Zipf.\n", " print(\" Zipf and Heaps are one fact seen twice. If frequency falls as\")\n", " print(\" r^-alpha with alpha > 1, then vocabulary grows as N^(1/alpha).\")\n", " alpha_all = BROWN_ALPHA[-1][1]\n", " print(f\" alpha over the full rank range = {alpha_all:.4f}\")\n", " print(f\" so the predicted b = 1/alpha = {1/alpha_all:.4f}\")\n", " print(f\" measured b = {slope:.4f}\")\n", " print(\" Right order, wrong decimals, and the gap is honest. The\")\n", " print(\" derivation assumes one alpha covers every rank, and the zipf\")\n", " print(\" demo showed alpha moving from 0.98 to 1.35 across ranges.\")\n", " print(\" Take the relationship, not the number: steeper frequency decay\")\n", " print(\" means fewer rare words, which means slower vocabulary growth.\")\n", " print()\n", "\n", "\n", "def demo_prep():\n", " print(\"\\nPREPROCESSING which knob moves tokens, which moves types\\n\")\n", " print(f\" {'operation':<22}{'tokens left':>13}{'types left':>12}\")\n", " print(\" \" + \"-\" * 47)\n", " for name, tok_pct, typ_pct in BROWN_PREP:\n", " print(f\" {name:<22}{tok_pct:>12.1f}%{typ_pct:>11.1f}%\")\n", " print(\"\\n Read the two columns against each other.\")\n", " print(\" Stop-word removal takes 44% of the tokens and 0.2% of the\")\n", " print(\" types. Zipf's law says exactly this: stop words are very few\")\n", " print(\" types carrying very many tokens.\")\n", " print(\" Porter stemming is the mirror image. It leaves every token in\")\n", " print(\" place and collapses 35% of the vocabulary.\")\n", " print(\" Dropping every type seen five times or fewer removes 72% of the\")\n", " print(\" vocabulary and costs 5.9% of the tokens. That is the trade the\")\n", " print(\" coverage table priced, and it is why a fixed subword vocabulary\")\n", " print(\" of 32k is not the sacrifice it looks like.\")\n", " print(\"\\n Preprocessing barely moves the Heaps fit: k goes 16.18 -> 17.33\")\n", " print(\" and b goes 0.5764 -> 0.5958 once stop words and numbers go.\")\n", " print(\" The curve is a property of the language, not of your cleaning.\")\n", "\n", "\n", "def demo_ttr():\n", " print(\"\\nTTR V/N, and why you may not compare two of them\\n\")\n", " print(\" Heaps gives V = K N^b, so TTR = V/N = K N^(b-1).\")\n", " print(\" b < 1 makes the exponent negative, so TTR falls with length.\")\n", " print(\" It is not a property of a text. It is a property of a text\")\n", " print(\" at a length.\\n\")\n", "\n", " b, k = fit_heaps() # the same fit the heaps demo prints\n", " print(f\" predicted from Brown's fit, k = {k:.2f}, b = {b:.4f}\")\n", " print(f\" {'N':>11}{'TTR':>9}\")\n", " print(\" \" + \"-\" * 20)\n", " for n in (1_000, 10_000, 100_000, 1_000_000):\n", " print(f\" {n:>11,}{k * n ** (b - 1):>9.4f}\")\n", " print(\" Same author, same style, four different answers.\\n\")\n", "\n", " print(\" Six Brown genres. Raw TTR uses the whole genre. Standardised\")\n", " print(\" TTR averages over non-overlapping 8,000-token windows.\\n\")\n", " print(f\" {'genre':<18}{'N':>9}{'raw TTR':>10}{'rank':>6}\"\n", " f\"{'std TTR':>10}{'rank':>6}\")\n", " print(\" \" + \"-\" * 59)\n", " raw_rank = {g[0]: i for i, g in\n", " enumerate(sorted(BROWN_GENRES, key=lambda g: -g[2]), 1)}\n", " std_rank = {g[0]: i for i, g in\n", " enumerate(sorted(BROWN_GENRES, key=lambda g: -g[3]), 1)}\n", " for name, n, raw, std in sorted(BROWN_GENRES, key=lambda g: -g[3]):\n", " print(f\" {name:<18}{n:>9,}{raw:>10.4f}{raw_rank[name]:>6}\"\n", " f\"{std:>10.4f}{std_rank[name]:>6}\")\n", "\n", " print(\"\\n Two rankings, two different answers.\")\n", " print(\" Raw TTR calls humour the richest genre in Brown. Humour is\")\n", " print(\" also the shortest genre in Brown, at 18,450 tokens against\")\n", " print(\" 164,036 for learned prose. The measurement is reading length.\")\n", " print(\" On equal windows press reportage comes first and humour\")\n", " print(\" second, and press overtakes fiction, which raw TTR had the\")\n", " print(\" other way round.\")\n", " print(\"\\n Never compare type-token ratios across unequal samples.\")\n", "\n", "\n", "def main():\n", " ap = argparse.ArgumentParser(description=__doc__)\n", " ap.add_argument(\"which\", nargs=\"?\", default=\"all\",\n", " choices=[\"facts\", \"zipf\", \"mandelbrot\", \"coverage\",\n", " \"heaps\", \"prep\", \"ttr\", \"all\"])\n", " a = ap.parse_args()\n", " if a.which in (\"facts\", \"all\"):\n", " demo_facts()\n", " if a.which in (\"zipf\", \"all\"):\n", " demo_zipf()\n", " if a.which in (\"mandelbrot\", \"all\"):\n", " demo_mandelbrot()\n", " if a.which in (\"coverage\", \"all\"):\n", " demo_coverage()\n", " if a.which in (\"heaps\", \"all\"):\n", " demo_heaps()\n", " if a.which in (\"prep\", \"all\"):\n", " demo_prep()\n", " if a.which in (\"ttr\", \"all\"):\n", " demo_ttr()\n", " print()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Running it\n", "\n", "The script is a command line tool, so each run below sets the arguments\n", "and calls `main()`. The heading shows the equivalent shell command." ] }, { "cell_type": "code", "metadata": {}, "execution_count": null, "outputs": [], "source": [ "import sys\n", "\n", "def run(*args):\n", " sys.argv = [\"empirical_laws.py\", *args]\n", " main()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### `python3 empirical_laws.py`\n", "\n", "The fits, the coverage table, and what happens to TTR when the text gets longer." ] }, { "cell_type": "code", "metadata": {}, "execution_count": null, "outputs": [], "source": [ "run()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "Read the chapter this comes from: [The Empirical Laws of Text](https://nlp.jcrlabz.com/book/empirical-laws/)." ] } ], "metadata": { "colab": { "name": "empirical_laws.ipynb", "provenance": [], "toc_visible": true }, "kernelspec": { "display_name": "Python 3", "name": "python3" }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 0 }