<html style="" lang="en" class="js flexbox fontface"><head><meta charset="utf-8"><meta content="width=device-width, initial-scale=1.0" name="viewport"><meta content="OCaml Scientific and Engineering Computing - Tutorial Book" name="description"><meta content="OCaml, Data Science, Data Analytics, Analytics, Functional Programming, Machine Learning, Deep Neural Network, Scientific Computing, Numerical Algorithm, Tutorial, Linear Algebra, Matrix" name="keywords"><meta content="Liang Wang" name="author"><title>Statistical Functions - OCaml Scientific Computing</title><link href="css/app.css" rel="stylesheet"><link href="css/prism.css" rel="stylesheet"><script src="js/min/modernizr-min.js"></script><script src="js/prism.js"></script><script src="https://use.typekit.net/gfj8wez.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML"></script><script>try{Typekit.load();}catch(e){}</script><script data-ad-client="ca-pub-1868946892712371" async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><script async src="https://www.googletagmanager.com/gtag/js?id=UA-123353217-1"></script><script>
      window.dataLayer = window.dataLayer || [];
      function gtag(){dataLayer.push(arguments);}
      gtag('js', new Date());
      gtag('config', 'UA-123353217-1');</script></head><body><div class="title-bar"><div class="title"><h1>OCaml Scientific Computing</h1><h5>1<sup>st</sup> Edition (in progress)</h5><nav><a href="index.html">Home</a><a href="toc.html">Table of Contents</a><a href="faqs.html">FAQs</a><a href="install.html">Install</a><a href="https://ocaml.xyz/package/">API Docs</a></nav></div></div><div class="wrap"><div class="left-column"><a class="to-chapter" href="toc.html"><small>Back</small><h5>Table of Contents</h5></a></div><article class="main-body"><section class="level1" id="statistical-functions">
<h1>Statistical Functions</h1>
<p>Statistics is an indispensable tool for data analysis, it helps us to gain the insights from data. The statistical functions in Owl can be categorised into three groups: descriptive statistics, distributions, and hypothesis tests.</p>
<section class="level2" id="random-variables">
<h2>Random Variables</h2>
<p>We start from assigning probabilities to <em>events</em>. A event may comprise of finite or infinite number of possible outcomes. All possible output make up the <em>sample space</em>. To better capture this assigning processes, we need the idea of <em>Random Variables</em>.</p>
<p>A random variable is a function that associate sample output of events with some numbers of interests. Imagine the classic tossing coin game, we toss the coin four times, and the result is “head”, “head”, “tail”, “head”. We are interested in the number of “head” in this outcome. So we make a Random Variable “X” to denote this number, and <code>X(["head", "head", "tail", "head"]) = 3</code>. You can see that using random variables can greatly reduce the event sample space.</p>
<p>Depending on the number of values it can be, a random variable can be broadly categorised into <em>Discrete</em> Random Variable (with finite number of possible output), and <em>Continuous</em> Random Variable (with infinite number of possible output).</p>
<section class="level3" id="discrete-random-variables">
<h3>Discrete Random Variables</h3>
<p>Back to the coin tossing example. Suppose that the coin is specially minted so that the probability of tossing head is <span class="math inline">\(p\)</span>. In this scenario, we toss for three times. Use the number of heads as a random variable <span class="math inline">\(X\)</span>, and it contains four possible outcomes: 0, 1, 2, or 3.</p>
<p>We can calculate the possibility of each output result. Since each toss is a individual trial, the possibility of three heads <code>P(X=2)</code> is <span class="math inline">\(p^3\)</span>. Two heads includes three cases: HHT, HTH, THH, each has a probability of <span class="math inline">\(p^2(1-p)\)</span>, and together <span class="math inline">\(P(X=2) = 3p^2(1-p)\)</span>. Similarly <span class="math inline">\(P(X=1)=3p(1-p)^2\)</span>, and <span class="math inline">\(P(X=0)=(1-p)^3\)</span>.</p>
<p>Formally, consider a series of <span class="math inline">\(n\)</span> independent trails, each trail containing two possible results, and the result of interest happens at a possibility of <span class="math inline">\(p\)</span>, then the possibility distribution of random variable <span class="math inline">\(X\)</span> is (<span class="math inline">\(X\)</span> being the number of result of interests):</p>
<p><span id="eq:stats:binomial_pdf"><span class="math display">\[P(X=k) = {N\choose k} p^k(1-p)^{n-k}.\qquad(1)\]</span></span></p>
<p>This type of distribution is called the <em>Binomial Probability Distribution</em>. We can stimulate this process of tossing coins with the <code>Stats.binomial_rvs</code> function. Suppose the probability of tossing head is 0.4, and for 10 times.</p>
<div class="highlight">
<pre data-filter-output=">" data-prompt="#" class="command-line"><code class="language-ocaml">let _ =
  let toss = Array.make 10 0 in
  Array.map (fun _ -&gt; Stats.binomial_rvs 0.3 1) toss
;;
&gt;- : int array = [|0; 0; 0; 0; 0; 0; 0; 0; 1; 0|]
</code></pre>
</div>
<p>The equation eq.&nbsp;1 is called the <em>&amp;probability density function</em> (PDF) of this binomial distribution. Formally the PDF of random variable X is denoted with <span class="math inline">\(p_X(k)\)</span> and is defined as:</p>
<p><span class="math display">\[p_X(k)=P({s \in S | X(s) = k}),\]</span></p>
<p>where <span class="math inline">\(S\)</span> is the sample space. This can also be expressed with the code:</p>
<div class="highlight">
<pre data-filter-output=">" data-prompt="#" class="command-line"><code class="language-ocaml">let x = [|0; 1; 2; 3|];;
&gt;val x : int array = [|0; 1; 2; 3|]
let p = Array.map (Stats.binomial_pdf ~p:0.3 ~n:3) x;;
&gt;val p : float array =
&gt;  [|0.342999999999999916; 0.440999999999999837; 0.188999999999999918;
&gt;    0.0269999999999999823|]
Array.fold_left (+.) 0. p;;
&gt;- : float = 0.999999999999999778
</code></pre>
</div>
<p>Aside from the PDF, another related and frequently used idea is to see the probability of random variable <span class="math inline">\(X\)</span> being within a certain range: <span class="math inline">\(P(a \leq X \leq b)\)</span>. It can be rewritten as <span class="math inline">\(P(X \leq b) - P(X \leq a - 1)\)</span>. Here the term <span class="math inline">\(P(X \leq t)\)</span> is called the <em>Cumulative Distribution Function</em> of random variable <span class="math inline">\(X\)</span>. For the binomial distribution, it CDF is:</p>
<p><span class="math display">\[p(X\leq~k)=\sum_{i=0}^k{N\choose i} p^k(1-p)^{n-i}.\]</span></p>
<p>We can calculate the CDF in the 3-tossing problem with code again.</p>
<div class="highlight">
<pre data-filter-output=">" data-prompt="#" class="command-line"><code class="language-ocaml">let x = [|0; 1; 2; 3|];;
&gt;val x : int array = [|0; 1; 2; 3|]
let p = Array.map (Stats.binomial_cdf ~p:0.3 ~n:3) x;;
&gt;val p : float array = [|0.342999999999999972; 0.784; 0.973; 1.|]
</code></pre>
</div>
</section>
<section class="level3" id="continuous-random-variables">
<h3>Continuous Random Variables</h3>
<p>Unlike discrete random variable, a continuous random variable has infinite number of possible outcomes. For example, in uniform distribution, we can pick a random real number between 0 and 1. Apparently there can be infinite number of outputs.</p>
<p>One of the most widely used continuous distribution is no doubt the <em>Gaussian distribution</em>. It’s probability function is a continuous one: <span id="eq:stats:gaussian_pdf"><span class="math display">\[p(x) = \frac{1}{\sqrt{2\pi~\delta}}s\exp{-\frac{1}{2}\left(\frac{t - \mu}{\sigma}\right)^2}\qquad(2)\]</span></span></p>
<p>Here the <span class="math inline">\(\mu\)</span> and <span class="math inline">\(\sigma\)</span> are parameters. Depending on them, the <span class="math inline">\(p(x)\)</span> can take different shapes. Let’s look at an example.</p>
<p>We generate two data sets in this example, and both contain 999 points drawn from different Gaussian distribution <span class="math inline">\(\mathcal{N} (\mu, \sigma^{2})\)</span>. For the first one, the configuration is <span class="math inline">\((\mu = 1, \sigma = 1)\)</span>; whilst for the second one, the configuration is <span class="math inline">\((\mu = 12, \sigma = 3)\)</span>.</p>
<div class="highlight">
<pre><code class="language-ocaml">let noise sigma = Stats.gaussian_rvs ~mu:0. ~sigma;;
let x = Array.init 999 (fun _ -&gt; Stats.gaussian_rvs ~mu:1. ~sigma:1.);;
let y = Array.init 999 (fun _ -&gt; Stats.gaussian_rvs ~mu:12. ~sigma:3.);;</code></pre>
</div>
<p>We can visualise the data sets using histogram plot as below. When calling <code>histogram</code>, we also specify 30 bins explicitly. You can also fine tune the figure using <code>spec</code> named parameter to specify the colour, x range, y range, and etc. We will discuss in details on how to use Owl to plot in a separate chapter.</p>
<div class="highlight">
<pre><code class="language-ocaml">(* convert arrays to matrices *)

let x' = Mat.of_array x 1 999;;
let y' = Mat.of_array y 1 999;;

(* plot the figures *)

let h = Plot.create ~m:1 ~n:2 "plot_02.png" in

Plot.subplot h 0 0;
Plot.set_ylabel h "frequency";
Plot.histogram ~bin:30 ~h x';
Plot.histogram ~bin:30 ~h y';

Plot.subplot h 0 1;
Plot.set_ylabel h "PDF p(x)";
Plot.plot_fun ~h (fun x -&gt; Stats.gaussian_pdf ~mu:1. ~sigma:1. x) (-2.) 6.;
Plot.plot_fun ~h (fun x -&gt; Stats.gaussian_pdf ~mu:12. ~sigma:3. x) 0. 25.;

Plot.output h;;</code></pre>
</div>
<p>In subplot 1, we can see the second data set has much wider spread. In subplot 2, we also plot corresponding the probability density functions of the two data sets.</p>
<figure>
<img alt="" style="width:90.0%" id="fig:stats:plot_02" title="plot 02" src="images/stats/plot_02.png"><figcaption>Figure 1: Probability density functions of two data sets</figcaption>
</figure>
<p>The CDF of Gaussian can be calculated with infinite summation, i.e.&nbsp;integration:</p>
<p><span class="math display">\[p(x\leq~k)=\frac{1}{\sqrt{2\pi}}\int_{-\infty}^k~e^{-t^2/2}dt.\]</span></p>
<p>We can observe this function with <code>gaussian_cdf</code>.</p>
<div class="highlight">
<pre><code class="language-ocaml">let h = Plot.create "plot_gaussian_cdf.png" in
Plot.set_ylabel h "CDF";
Plot.plot_fun ~h ~spec:[ RGB (66,133,244); LineStyle 1; LineWidth 2.; Marker "*" ] (fun x -&gt; Stats.gaussian_cdf ~mu:1. ~sigma:1. x) (-2.) 6.;
Plot.plot_fun ~h ~spec:[ RGB (219,68,55);  LineStyle 2; LineWidth 2.; Marker "+" ] (fun x -&gt; Stats.gaussian_cdf ~mu:12. ~sigma:3. x) 0. 25.;
Plot.(legend_on h ~position:SouthEast [|"mu=1,sigma=1"; "mu=12, sigma=3"|]);
Plot.output h</code></pre>
</div>
<figure>
<img alt="" style="width:70.0%" id="fig:stats:plot_gaussian_cdf" title="plot gaussian cdf" src="images/stats/plot_gaussian_cdf.png"><figcaption>Figure 2: Cumulated density functions of two data sets</figcaption>
</figure>
</section>
<section class="level3" id="descriptive-statistics">
<h3>Descriptive Statistics</h3>
<p>Mean, Variance: Definition, and math derivation of Gaussian as examples.</p>
<p>The definition of moments, and higher moments.</p>
<p>Descriptive statistics are used to summarise the characteristics of data. The commonly used ones are mean, variance, standard deviation, skewness, kurtosis, and etc.</p>
<p>We first draw one hundred random numbers which are uniformly distributed between 0 and 10. Here we use <code>Stats.uniform_rvs</code> function to generate numbers following uniform distribution.</p>
<div class="highlight">
<pre><code class="language-ocaml">let data = Array.init 100 (fun _ -&gt; Stats.uniform_rvs 0. 10.);;</code></pre>
</div>
<p>Then We use <code>mean</code> function calculate sample average. As we can see, it is around 5. We can also calculate higher moments such as variance and skewness easily with corresponding functions.</p>
<div class="highlight">
<pre data-filter-output=">" data-prompt="#" class="command-line"><code class="language-ocaml">Stats.mean data;;
&gt;- : float = 5.18160409659184573
Stats.std data;;
&gt;- : float = 2.92844832850280135
Stats.var data;;
&gt;- : float = 8.57580961271085229
Stats.skew data;;
&gt;- : float = -0.109699186612116223
Stats.kurtosis data;;
&gt;- : float = 1.75165078829330856
</code></pre>
</div>
<p>The following code calculates different central moments of <code>data</code>. A central moment is a moment of a probability distribution of a random variable about the random variable’s mean. The zero-th central moment is always 1, and the first is close to zero, and the second is close to the variance.</p>
<div class="highlight">
<pre data-filter-output=">" data-prompt="#" class="command-line"><code class="language-ocaml">Stats.central_moment 0 data;;
&gt;- : float = 1.
Stats.central_moment 1 data;;
&gt;- : float = -3.13082892944294137e-15
Stats.central_moment 2 data;;
&gt;- : float = 8.49005151658374224
Stats.central_moment 3 data;;
&gt;- : float = -2.75496511397836663
</code></pre>
</div>
</section>
<section class="level3" id="order-statistics">
<h3>Order Statistics</h3>
<p>Order statistics and rank statistics are among the most fundamental tools in non-parametric statistics and inference. The <span class="math inline">\(k^{th}\)</span> order statistic of a statistical sample is equal to its kth-smallest value. The example functions of ordered statistics are as follows.</p>
<div class="highlight">
<pre><code class="language-ocaml">Stats.min;; (* the mininum of the samples *)
Stats.max;; (* the maximum of the samples *)
Stats.median;; (* the median value of the samples *)
Stats.quartile;; (* quartile of the samples *)
Stats.first_quartile;; (* the first quartile of the samples *)
Stats.third_quartile;; (* the third quartile of the samples *)
Stats.interquartile;; (* the interquartile of the samples *)
Stats.percentile;; (* percentile of the samples *)</code></pre>
</div>
<p>In addition to the aforementioned ones, there are many other ordered statistical functions in Owl for you to explore.</p>
<p>IMAGE to show the difference of mean, median, mode, etc.</p>
</section>
</section>
<section class="level2" id="special-distribution">
<h2>Special Distribution</h2>
<p>illustrate the naming convention.</p>
<ul>
<li><code>gaussian_rvs</code> : random number generator.</li>
<li><code>gaussian_pdf</code> : probability density function.</li>
<li><code>gaussian_cdf</code> : cumulative distribution function.</li>
<li><code>gaussian_ppf</code> : percent point function (inverse of CDF).</li>
<li><code>gaussian_sf</code> : survival function (1 - CDF).</li>
<li><code>gaussian_isf</code> : inverse survival function (inverse of SF).</li>
<li><code>gaussian_logpdf</code> : logarithmic probability density function.</li>
<li><code>gaussian_logcdf</code> : logarithmic cumulative distribution function.</li>
<li><code>gaussian_logsf</code> : logarithmic survival function.</li>
</ul>
<p>Stats module supports many distributions. For each distribution, there is a set of related functions using the distribution name as their common prefix.</p>
<p>TODO: Add Poisson Distribution Implementation</p>
<section class="level3" id="gamma-distribution">
<h3>Gamma Distribution</h3>
<p>Definition</p>
<p>PDF, CDF</p>
<p>Application</p>
</section>
<section class="level3" id="beta-distribution">
<h3>Beta Distribution</h3>
</section>
<section class="level3" id="chi-square-distribution">
<h3>Chi-Square Distribution</h3>
</section>
<section class="level3" id="student-t-distribution">
<h3>Student-t Distribution</h3>
</section>
<section class="level3" id="cauchy-distribution">
<h3>Cauchy Distribution</h3>
</section>
</section>
<section class="level2" id="multiple-variables">
<h2>Multiple Variables</h2>
<p>Joint Density</p>
<p>Independence of random variables</p>
<p>Mean and Variance</p>
<p>Multinomial distribution</p>
</section>
<section class="level2" id="hypothesis-tests">
<h2>Hypothesis Tests</h2>
<section class="level3" id="theory">
<h3>Theory</h3>
<p>While descriptive statistics solely concern properties of the observed data, statistical inference focusses on studying whether the data set is sampled from a larger population. In other words, statistical inference make propositions about a population. Hypothesis test is an important method in inferential statistical analysis. There are two hypotheses proposed with regard to the statistical relationship between data sets.</p>
<ul>
<li>Null hypothesis <span class="math inline">\(H_0\)</span>: there is no relationship between two data sets.</li>
<li>Alternative hypothesis <span class="math inline">\(H_1\)</span>: there is statistically significant relationship between two data sets.</li>
</ul>
<p>Type I and Type II errors.</p>
</section>
<section class="level3" id="gaussian-distribution-in-hypothesis-testing">
<h3>Gaussian Distribution in Hypothesis Testing</h3>
<p>The <code>Stats</code> module in Owl supports many different kinds of hypothesis tests.</p>
<ul>
<li>Z-Test</li>
<li>Student’s T-Test</li>
<li>Paired Sample T-Test</li>
<li>Unpaired Sample T-Test</li>
<li>Kolmogorov-Smirnov Test</li>
<li>Chi-Square Variance Test</li>
<li>Jarque-Bera Test</li>
<li>Fisher’s Exact Test</li>
<li>Wald–Wolfowitz Runs Test</li>
<li>Mann-Whitney Rank Test</li>
<li>Wilcoxon Signed-rank Test</li>
</ul>
<p>Now let’s see how to perform a z-test in Owl. We first generate two data sets, both are drawn from Gaussian distribution but with different parameterisation. The first one <code>data_0</code> is drawn from <span class="math inline">\(\mathcal{N}(0, 1)\)</span>, while the second one <code>data_1</code> is drawn from <span class="math inline">\(\mathcal{N}(3, 1)\)</span>.</p>
<div class="highlight">
<pre><code class="language-ocaml">let data_0 = Array.init 10 (fun _ -&gt; Stats.gaussian_rvs ~mu:0. ~sigma:1.);;
let data_1 = Array.init 10 (fun _ -&gt; Stats.gaussian_rvs ~mu:3. ~sigma:1.);;</code></pre>
</div>
<p>Our hypothesis is that the data set is drawn from Gaussian distribution <span class="math inline">\(\mathcal{N}(0, 1)\)</span>. From the way we generated the synthetic data, it is obvious that <code>data_0</code> will pass the test, but let’s see what Owl will test us using its <code>Stats.z_test</code> function.</p>
<div class="highlight">
<pre data-filter-output=">" data-prompt="#" class="command-line"><code class="language-ocaml">Stats.z_test ~mu:0. ~sigma:1. data_0;;
&gt;- : Owl_stats.hypothesis =
&gt;{Owl.Stats.reject = false; p_value = 0.289340080583773251;
&gt; score = -1.05957041132113083}
</code></pre>
</div>
<p>The returned result is a record with the following type definition. The fields are self-explained: <code>reject</code> field tells whether the null hypothesis is rejected, along with the p value and score calculated with the given data set.</p>
<div class="highlight">
<pre><code class="language-ocaml">type hypothesis = {
  reject : bool;
  p_value : float;
  score : float;
}</code></pre>
</div>
<p>From the previous result, we can see <code>reject = false</code>, indicating null hypothesis is rejected, therefore the data set <code>data_0</code> is drawn from <span class="math inline">\(\mathcal{N}(0, 1)\)</span>. How about the second data set then?</p>
<div class="highlight">
<pre data-filter-output=">" data-prompt="#" class="command-line"><code class="language-ocaml">Stats.z_test ~mu:0. ~sigma:1. data_1;;
&gt;- : Owl_stats.hypothesis =
&gt;{Owl.Stats.reject = true; p_value = 5.06534675819424548e-23;
&gt; score = 9.88035435799393547}
</code></pre>
</div>
<p>As we expected, the null hypothesis is accepted with a very small p value. This indicates that <code>data_1</code> is drawn from a different distribution rather than assumed <span class="math inline">\(\mathcal{N}(0, 1)\)</span>.</p>
</section>
<section class="level3" id="two-sample-inferences">
<h3>Two-Sample Inferences</h3>
</section>
<section class="level3" id="goodness-of-fit-tests">
<h3>Goodness-of-fit Tests</h3>
</section>
<section class="level3" id="non-parametric-statistics">
<h3>Non-parametric Statistics</h3>
<p>Wilcoxon Tests</p>
</section>
</section>
<section class="level2" id="covariance-and-correlations">
<h2>Covariance and Correlations</h2>
<p>Correlation studies how strongly two variables are related. There are different ways of calculating correlation. For the first example, let’s look at Pearson correlation.</p>
<p><code>x</code> is our explanatory variable and we draw 50 random values uniformly from an interval between 0 and 10. Both <code>y</code> and <code>z</code> are response variables with a linear relation to <code>x</code>. The only difference is that we add different level of noise to the response variables. The noise values are generated from Gaussian distribution.</p>
<div class="highlight">
<pre><code class="language-ocaml">let noise sigma = Stats.gaussian_rvs ~mu:0. ~sigma;;
let x = Array.init 50 (fun _ -&gt; Stats.uniform_rvs 0. 10.);;
let y = Array.map (fun a -&gt; 2.5 *. a +. noise 1.) x;;
let z = Array.map (fun a -&gt; 2.5 *. a +. noise 8.) x;;</code></pre>
</div>
<p>It is easier to see the relation between two variables from a figure. Herein we use Owl’s Plplot module to make two scatter plots.</p>
<div class="highlight">
<pre><code class="language-ocaml">(* convert arrays to matrices *)

let x' = Mat.of_array x 1 50;;
let y' = Mat.of_array y 1 50;;
let z' = Mat.of_array z 1 50;;

(* plot the figures *)

let h = Plot.create ~m:1 ~n:2 "plot_01.png" in

  Plot.subplot h 0 0;
  Plot.set_xlabel h "x";
  Plot.set_ylabel h "y (sigma = 1)";
  Plot.scatter ~h x' y';

  Plot.subplot h 0 1;
  Plot.set_xlabel h "x";
  Plot.set_ylabel h "z (sigma = 8)";
  Plot.scatter ~h x' z';

  Plot.output h;;</code></pre>
</div>
<p>The subfigure 1 shows the functional relation between <code>x</code> and <code>y</code> whilst the subfiture 2 shows the relation between <code>x</code> and <code>z</code>. Because we have added higher-level noise to <code>z</code>, the points in the second figure are more diffused.</p>
<figure>
<img alt="" style="width:90.0%" id="fig:stats:plot_01" title="plot 01" src="images/stats/plot_01.png"><figcaption>Figure 3: Functional relation between <code>x</code> and the other two variables.</figcaption>
</figure>
<p>Intuitively, we can easily see there is stronger relation between <code>x</code> and <code>y</code> from the figures. But how about numerically? In many cases, numbers are preferred because they are easier to compare with by a computer. The following snippet calculates the Pearson correlation between <code>x</code> and <code>y</code>, as well as the correlation between <code>x</code> and <code>z</code>. As we see, the smaller correlation value indicates weaker linear relation between <code>x</code> and <code>z</code> comparing to that between <code>x</code> and <code>y</code>.</p>
<div class="highlight">
<pre data-filter-output=">" data-prompt="#" class="command-line"><code class="language-ocaml">Stats.corrcoef x y;;
&gt;- : float = 0.991145445979576656
Stats.corrcoef x z;;
&gt;- : float = 0.692163016204755288
</code></pre>
</div>
</section>
<section class="level2" id="analysis-of-variance">
<h2>Analysis of Variance</h2>
</section>
<section class="level2" id="summary">
<h2>Summary</h2>
</section>
</section>
</article></div><a href="ndarray.html" class="next-chapter"><div class="content"><h1><small>Next: Chapter 06</small>N-Dimensional Arrays</h1></div></a><footer><div class="content"><ul><li><a href="http://ocaml.xyz">ocaml.xyz</a></li><li><a href="https://github.com/ryanrhymes">GitHub</a></li></ul><p>Copyright 2017-2020 Liang Wang.</p></div></footer><script src="js/jquery.min.js"></script><script src="js/min/app-min.js"></script></body></html>
{'avg_line_length': 73.89814814814815, 'max_line_length': 1183, 'alphanum_fraction': 0.7182892703504156, 'licenses': ['MIT'], 'repository_name': 'dvincelli/book', 'path': 'docs/stats.html', 'size': 23987, 'lang': 'HTML'}