<jupyter_start><jupyter_text>Descriptive Statistics* <NAME> / @heinrichhartman / <EMAIL>* 2017-09-01* SREConEurope 2017, Dublin, Ireland  (1) Summary StatisticsProblem statment:Given a dataset X:* Characterize the distribution in one or two values* Characterization should be robust, i.e. not severly affected by single outliersEquivalent of an elevator pitch for a data sets.  The Mean ValueThe _mean value_ of $X=[x_1, \dots, x_n]$ is defined as$$ \mu = mean(X) = \frac{1}{n} \sum_{i=1}^n x_i. $$- Represnets center of mass- If the values are close together this is a good representative- Can be easily affected by outliers<jupyter_code># Computation of the mean value

def mean(X):
    return float(sum(X)) / len(X)<jupyter_output><empty_output><jupyter_text>MediansA _median value_ for $x_1, \dots, x_n$ is number $m$ such that   $$ \ \{ i \,|\, x_i \lt m \} = \ \{ i \,|\, x_i \gt m \}. $$So the number of samples smaller than $m$ is equal to the number of samples larger than $m$.(Both should be roughly $n/2$).  Remark:* A Median always exists* Median is not always unique* Very robust to outliers* Less "precise" then the mean for well behaved datasets  The central medianMedians can be computed by sorting the dataset.`S = sorted(X)`Consider the following cases:- if `len(X)` is odd, then `S[(len(X)-1)/2]` is the (unique and) central median.- if `len(X)` is even, then all values between `S[len(X)/2-1]` and `S[len(X)/2]` are median values.    The central median is defined as the mean of those two values.We write $median(X)$ for the central median of $X$.Example:* `median([1,2,3]) = 2`* `median([1,2]) = (1 + 2) / 2`<jupyter_code># Computation of the median
def median_range(X):
    S = sorted(X)
    n = len(X)
    lower_mid = int(math.floor((n-1)/2.))
    upper_mid = int(math.ceil((n-1)/2.))
    return (S[lower_mid], S[upper_mid])

def median(X):
    a,b = median_range(X)
    return (a+b)/2
def plot_median_range(X):
    plt.figure(figsize=(14,1))
    a, b = median_range(X)
    c = (a+b)/2.
    sns.rugplot(X, color="green", height=0.5)
    plt.plot([a,a],[0.5,1],'r-')
    plt.plot([b,b],[0.5,1],'r-')
    plt.plot([c,c],[0.5,1],'r-')
    plt.plot([a,b],[0.5,0.5],'r--')
    plt.title("Median Range: {:.3f} .. {:,.3f}".format(a,b))
    plt.ylim([0,1])
plot_median_range([1,2,3])
plot_median_range([1,2,3,4])<jupyter_output><empty_output><jupyter_text>Truncated Means* Truncated means "interpolate" between means and medianse.* In many cases they provid the increased "precision" of means, while keeping the robustnes against outliers.To arrive at the k-truncated mean of a dataset X, perform the following steps:* Remove a minimum and a maximum value from X, a total of k times.* The remaining samples are called the k-truncation of X and denoted by $T_k X$.* Caculate the mean value of the remaining samples.We deonte the k-truncated mean by `tmean(X,k)`.<jupyter_code>def tmean(X,k):
    X = list(X)
    k = int(k)
    if len(X) <= 2*k:
        return None
    for i in range(k):
        X.remove(max(X))
        X.remove(min(X))
    return mean(X)
# Truncated mean examples
print(tmean([1,2,3,5,100],0))
print(tmean([1,2,3,5,100],1))
print(tmean([1,2,3,5,100],2))
# Plot
def mark(m,height=1,style='r'):
    plt.plot([m,m],[0,height],style)

def plot_mean_median(X):
    plt.figure(figsize=(14,1))
    sns.rugplot(X, color='grey', height=0.5)
    mark(mean(X));                     print("mean:        {:4.2f} (red)".format(mean(X)))
    k = int(math.ceil(10*len(X)/100.))
    mark(tmean(X,k), style='b');       print("tmean(X,{:2}): {:4.2f} (blue)".format(k,tmean(X,k)))
    mark(median(X), style='g');        print("median:      {:4.2f} (green)".format(median(X)))
    plt.show()
# Normally distributed data
Z = [ np.random.normal() for x in range(500) ]
plot_mean_median(Z)

# Mean value of some request rates
X = np.loadtxt("DataSets/RequestRates.csv", delimiter=",")[:,1]
plot_mean_median(X)

# Request Latency Dataset
Y = np.genfromtxt("DataSets/WebLatency.csv", delimiter=",")[:,2][0:500] - 85
plot_mean_median(Y[0:100])
# Mean values can be atypical
plot_mean_median([1,2,3,100,102])<jupyter_output>mean:        41.60 (red)
tmean(X, 1): 35.00 (blue)
median:      3.00 (green)<jupyter_text>(2) Deviation MeasuresGoal measure "typical" deviation form a central value. 1. The _maximal deviation_ is defined as   $$ maxdev(X) = max \{ |X[i] - \mu| \,|\, i=1,\dots,n\}.$$2. The _mean absolute deviation_ is defined as      $$ mad(X) = \frac{1}{n} \sum_{i=1}^n |X[i] - \mu|.$$3. The _standard deviation_ is defined as   $$ \sigma(X) = stddev(X) =  \sqrt{\frac{1}{n} \sum_{i=1}^n (X[i] - \mu)^2}.$$4. The _inter quartile range_ is defined as difference of the 75- and 25-Percentile:   $$ IQR(X) = P(75,X) - P(25,X) $$Properties:* Measure the 'typical' displacement from the mean value.* Standard deviation is popular because it has extremely nice mathematical properties.* The IQR is very robust to outliers.<jupyter_code>def max_dev(X):
    m = mean(X)
    return max(abs(x - m) for x in X)

def mad(X):
    m = mean(X)
    return sum(abs(x - m) for x in X) / float(len(X))

def stddev(X):
    m = mean(X)
    return math.pow(sum((x - m)**2 for x in X) / len(X), 0.5)

def IQR(X): return percentile(75,X) - percentile(0.25,X)

# computation of percentiles is given below
# Plotting helper function
def plot_mean_dev(X, m, s, new_canvas=True):
    print("center    = ", m)
    print("deviation = ", s)
    if new_canvas: plt.figure(figsize=(14,1))
    sns.rugplot(X, color='grey', height=0.5)
    plt.plot([m,m],[0,1],'r-' )
    plt.plot([m-s,m-s],[0,1],'b-')
    plt.plot([m+s,m+s],[0,1],'b-')
    plt.plot([m-s,m+s],[0.5,0.5],'b--')
    if new_canvas: plt.show()
# Standard deviation is a good deviation for normal distributed data

print("Maximal deviation")
plot_mean_dev(Z,mean(Z),max_dev(Z))

print("Standard Deviation")
plot_mean_dev(Z,mean(Z),stddev(Z))

print("Mean Absolute Deviation")
plot_mean_dev(Z,mean(Z),mad(Z))

print("Median IQR")
plot_mean_dev(Z,median(Z),IQR(Z)/2)
X = np.loadtxt("DataSets/RequestRates.csv", delimiter=",")[:,1]
    
print("Maximal deviation")
plot_mean_dev(X,mean(X),max_dev(X))

print("Standard Deviation")
plot_mean_dev(X,mean(X),stddev(X))

print("Mean Absolute Deviation")
plot_mean_dev(X,mean(X),mad(X))

print("Mean Median IQR")
plot_mean_dev(X,median(X),IQR(X)/2)
print("Maximal deviation")
plot_mean_dev(Y,mean(Y),max_dev(Y))

print("Standard Deviation")
plot_mean_dev(Y,mean(Y),stddev(Y))

print("Mean Absolute Deviation")
plot_mean_dev(Y,mean(Y),mad(Y))

print("Mean Median IQR")
plot_mean_dev(Y,median(Y),IQR(Y)/2)<jupyter_output>Maximal deviation
center    =  61.35899999999999
deviation =  1161.641<jupyter_text>Caution with Standard Deviation- Everybody Learns about standard deviation in school- Beautiful mathematical properties!- Everybody knows   - "68% of data falls within 1 std-dev of the mean"  - "95% falls within 2 std-dev of the mean"  - "99.7" falls within 3 std-dev of the mean"* "Problem is: this is utter nonsense". Only true for normally distributed data.* Not good for measuring outliers!_Source:_ Janert - Data Analysis with Open Source Tools  War Story:- Looking at SLA for DB response times- Outlier defined as value larger than $\mu + 3\sigma$- Look at code: Takes '0.3' percentile!- So always have outliers.- And 0.3-percentile was way too large (hours of latency).- Programmer changed code for 1%, 5%, 10% quantiles.- Finally handcoded a threshold- The SLA was never changedSource: Janert - Data Analysis with Open Source Tools  Tukey's outlier DefinitionA k-outlier is a data point X which is either* larger than `P(75) + k * IQR(X)` or* smaller than `P(25) - k * IQR(X)`.An outlier (without k) is an 1.5-outlier.<jupyter_code>def outliers(X, k=1.5):
    iqr = IQR(X)
    lower_bound = percentile(25,X) - k * iqr
    upper_bound = percentile(75,X) + k * iqr
    out = []
    for x in X:
        if x < lower_bound: out.append(x)
        if x > upper_bound: out.append(x)
    return out
def outlier_plot(X):
    plt.figure(figsize=(14,2))
    sns.rugplot(X, color="green", height=0.5)
    a,b = percentile(25,X), percentile(75,X)
    O = outliers(X)
    print("IQR:", b-a)
    print("Outlier count:", len(O))
    mark_range(a,b)
    for o in O:
        plt.plot([o,o],[0.7,0.8],'b--')
    plt.ylim([0,1])
# Normal Data
outlier_plot(Z)
# Web request Rates
outlier_plot(X)
# Request Latencies
outlier_plot(Y)<jupyter_output>IQR: 9.0
Outlier count: 43<jupyter_text>Tukey's BoxplotsShow:* Median* Box around 0.25 and 0.75 Quantiles* "whiskers" from min to max* points for outliersAllows visual clues:* Where is the data concentracted?* How far is it spread?* How skew is the data?<jupyter_code># Normal Data
plt.figure(figsize=(15,1))
sns.boxplot(x=Z, vert=False)
sns.rugplot(Z)
plt.show()
# Tukey Boxplots show outliers as 'points' outside the whiskers

# Web-request dataset
plt.figure(figsize=(15,1))
sns.boxplot(x=X)
sns.rugplot(X)
plt.show()
# Latency Dataset
plt.figure(figsize=(15,1))
sns.boxplot(x=Y, vert=False)
sns.rugplot(Y)
plt.show()<jupyter_output>/opt/conda/lib/python3.6/site-packages/seaborn/categorical.py:2171: UserWarning: The boxplot API has been changed. Attempting to adjust your arguments for the new API (which might not work). Please update your code. See the version 0.6 release notes for more info.
  warnings.warn(msg, UserWarning)<jupyter_text>Demo:- Global duration percentiles with Overlays: [demo](https://demo-hh2.circonus.com/trending/graphs/view/9e85beb8-4658-49f5-95e8-b6cf70ad5ca9set-YVlpVTrN)- Latency percentiles in CAQL [demo](https://demo-hh2.circonus.com/trending/graphs/edit/da4ac22a-55b9-6145-abbc-a0371919e876)  Outlier ScoresThe intuitive meaning of outlier is a value in a dataset, that is "unlikely" to be generated by the same process as the rest of the distribution.A simple outlier score:* Pick a summary statistic $M$: E.g. mean* Pick a deviation measure $D$: E.g. MADThe outlier score of a value $x$ in context $X$ is defined as:$$OutlierScore(X,y) = \frac{M(X) - y}{S(X)}$$It measures how many deviations a sample is away from the mean.* Good for well behaved datasets with a single mode* Problematic for skew distributions* Easy to use in practice Demo:- Outlier Detection in Time Series https://demo-hh2.circonus.com/trending/graphs/view/c7602466-69f4-e6b3-ceae-95049da417c4- Time range: August 30th - September 7th 2015  Takeaway* Don't use the standard-deviation if you can avoid it* If you need a deviation measure use MAD or IQR:  - (+) Simple to calculate  - (+) Simple to interpret  - (-) Not mergable  (3) Measuring Distributions * Classical summary statistics good for describing the body of the distribution* Need information about the tail of the distributions, e.g. for writing good SLAs* Determine outliers in a dataset  Inverse PercentilesThe inverse percentile y of a dataset $X$, is defined as:$$ IP(y, X) = \text{(the percentage of samples in X that are lower than $y$)} = \ \{ i \, | \, x_i \leq y \} / \ X \cdot 100 $$**Properties*** $0 \leq IP(y, X) \leq 100$, dimensionless percentage value* $IP(y,X)$ is monotonically increasing in $y$<jupyter_code>def IP(y,X):
    lower_count = 0
    for x in X:
        if x <= y: lower_count += 1
    return float(lower_count) / len(X)
# Example
import matplotlib as mpl
mpl.rcParams['figure.figsize'] = (15,5)
X = np.loadtxt("DataSets/RequestRates.csv", delimiter=",")[:,1]

grid = np.linspace(0,3000,100)
plt.figure(figsize=(20,5))
plt.plot(grid, [IP(y,X) for y in grid])
plt.title("Inverse Percentiles")
sns.rugplot(X)<jupyter_output><empty_output><jupyter_text>Percentiles* Complement or Inverse to Inverse Percentiles:  - IP: The ratio of samples was below a value $y$ was $?\%$  - Percentile: 90% of all queries where faster than $?$* The definition is a little involved so cover examples firstExamples:* The minimum is a 0%-percentile* A median is a 50%-percentile* The maximum is a 100%-percentile* Quartiles: $0,25,50,75,100$-percentiles* Quantiles: The $r$-quantile is the $r*100$-percentile $0<r<1$.  General Definition of PercentilesPick a number $0\leq p \leq 100$. A $p$%-percentile for $X$ is a value $y$ such that,$$ \\{i \,|\, X[i] \lt y \} \leq \frac{p}{100} \cdot n $$and$$ \\{i \,|\, X[i] \gt y \} \leq \frac{100-p}{100} \cdot n $$Roughly speaking, $y$ divides $X$ in $p\%$ samples that are lower than $y$ and $(100-p)\%$ samples that are larger than $y$. Remarks:* Quantiles always exists* Non unique (like median)* Lot's of ways to choose a quantile function, i.e. interpolate between $s_a$ and $s_b$ cf.    http://en.wikipedia.org/wiki/QuantileEstimating_the_quantiles_of_a_population<jupyter_code># Computation of Quantiles
def percentile_range(p,X):
    q = p/100.0
    S = sorted(X)
    n = len(X)
    upper_d = int(math.ceil(q * (n + 1)))
    lower_d = int(math.floor(q * (n + 1)))
    # edge cases
    if upper_d == 0:   upper_d = 1
    if upper_d == n+1: upper_d = n
    if lower_d == 0:   lower_d = 1
    if lower_d == n+1: lower_d = n
    return (S[lower_d-1], S[upper_d-1])

def percentile(p,X):
    a,b = percentile_range(p,X)
    return (a+b)/2 # mid point percentile. One choice!
def mark_range(a,b):
    plt.plot([a,a],[0.5,1],'r-')
    plt.plot([b,b],[0.5,1],'r-')
    plt.plot([a,b],[0.75,0.75],'r--')

def plot_percentile_range(p,X):
    plt.figure(figsize=(14,2))
    sns.rugplot(X, color="green", height=0.5)
    a,b = percentile_range(p,X)
    mark_range(a,b)
    plt.title("{:n}%-Percentile Range: {:.3f} .. {:,.3f}".format(p, a,b))
    plt.ylim([0,1])
plot_percentile_range(50,[1,2,3,4])
plot_percentile_range(40,[1,2,3,4])
plot_percentile_range(90,X)<jupyter_output><empty_output><jupyter_text>(4) Histograms* Basic Idea: Use area to compare sample ratios, instead of point counts* Easier to answer questios like: "How many voters are older than 30, when presented with a histogram, than a table or a rugplot."* Both a visualization method and a data-storage format  Building your own Histogram1. Define value range of histogram `bin_min`, `bin_max`2. Divide range into bins `bin[k], ..., bin[k]`3. The bins may have variying width 4. Count the number of samples in each bin `sample_count[k]`5. Draw a rectangle for each bin with height:   - `h[k] = sample_count[k]` -- non normalized    - `h[k] = sample_count[k] / bin_width[k]` -- normalized case<jupyter_code>import numpy as np
X = np.loadtxt("DataSets/RequestRates.csv", delimiter=",")[:,1]

# Step 1: Represent bins via list of left-edges
bin_left = [
    500, 700, 800, 900, 1000, 1500, 1800, 2000, 2200
]

bin_min = bin_left[0]
bin_max = bin_left[-1]
bins = [ [bin_left[i-1],bin_left[i]] for i in range(1, len(bin_left)) ]

print("Bins: ", bins)
# Step2: Count number of samples in bins
def count(X, bins):
    def findBin(x, bins):
        for i, bin in enumerate(bins):
            left, right = bin
            if left <= x and x < right:
                return i
        return None
    
    count = [0] * len(bins)
    for x in X:
        i = findBin(x, bins)
        if i != None:
            count[i] += 1

    return count

sample_count = count(X, bins)

print("Counts:", sample_count)
from matplotlib import pyplot as plt
import seaborn as sns

%matplotlib inline

# Step3: Plot bar chart
def my_hist(X, bins, normed=True, **kwargs):
    bin_left = [ l for l, r in bins ]
    bin_width = [ r-l  for l,r in bins ]
    bin_height = [ 
        float(c) / w if normed else c 
        for c,w in zip(count(X, bins), bin_width)
    ]
    plt.bar(bin_left,width=bin_width,height=bin_height, **kwargs)

plt.figure(figsize=(15,5))
my_hist(X,bins)
def plot_hist(X,  bin_min, bin_max, bin_width):
    plt.figure(figsize=(15,5))
    bins =[ [i, i+bin_width] for i in np.arange(bin_min, bin_max, bin_width) ]
    my_hist(X,bins)
    print("Histogram with bin_width ", bin_width)
    plt.show()<jupyter_output><empty_output><jupyter_text>How bin choices affect the apperance<jupyter_code>bin_min = 600
bin_max = 2200
plot_hist(X, bin_min, bin_max, 5)
plot_hist(X, bin_min, bin_max, 50)
plot_hist(X, bin_min, bin_max, 2000)
# -> Only the total count is displayed<jupyter_output>Histogram with bin_width  2000<jupyter_text>Common bin-width choicesRemarks:* Can convert `bin_count` and `bin_width` using relation $$ bin\_max - bin\_min = bin\_count \cdot bin\_width.$$* Choices `bin_min=min(X)` and `bin_max=max(X)` are common. But: dependence on input data.Choices for bin-width:* Excel: Let $n$ be the number of samples, then:  $$ bin\_count = \sqrt{n} $$* Scott's normal reference rule  $$ bin\_width = \frac{3.5 \sigma}{n^{1/3}} $$  where $\sigma$ is the standard deviation (cf. later).  This is optimal for random samples of normally distributed data, in a certain sense.* Freedman–Diaconis rule  $$ bin\_width = \frac{2 IQR}{n^{1/3}} $$    where IQR is the inter-quartile range (cf. later). Like Scott's rule but more robust agains outliers.  A pragmatic choice: HDR HistogramsExample: A decimal HDR Histogram with precision=2 has bin boundaries:      .... 1.0,  1.1,  1.2, ... 1.9,  2.0,  2.1, ...., 9.9; --- bin width = 0.1         10.0, 11.0, 12.0 ... 19.0, 20.0, 21.0 ...., 99 ; --- bin width = 1         ...** Properties: *** Captures large part of float range* Bin boundaries do not depend on data! -> Can aggregate counts!* Bin width increase with growing values* Allows compact memory representation* Implementation available at http://hdrhistogram.org/<jupyter_code>import math
def HDR_bin_width(x):
    if x == 0: return 0
    sign = x / abs(x)
    return sign * 10 ** ( math.floor(math.log10(abs(x)) - 1 ) )

def HDR_bin_left(x):
    w = HDR_bin_width(x)
    if w == 0: return 0
    return math.floor(x / w) * w

def HRD_bin_right(x):
    return bin_left(x) + bin_width(x)
def HDR_hist(X):
    count = {}
    for x in X:
        l = HDR_bin_left(x)
        count[l] = count.get(l,0) + 1
    bin_left = sorted(count.keys())
    bin_width = [ HDR_bin_width(l) for l in bin_left ]
    bin_height = [ abs(count[l] / HDR_bin_width(l))
        for l in bin_left
    ]
    plt.figure(figsize=(15,5))
    plt.bar(bin_left,width=bin_width,height=bin_height)
Y = np.genfromtxt("DataSets/WebLatency.csv", delimiter=",")[:,2][:200] - 85
HDR_hist(Y)
sns.rugplot(Y, color="red")
HDR_hist(X)
sns.rugplot(X, color="red")
# Need re-binning near zero:
HDR_hist(Z)
sns.rugplot(Z, color="red")
plt.ylim(0,200)<jupyter_output><empty_output><jupyter_text>(5) MergabilityA summary statistic $S$ assigns (one or many) values to a dataset $X$. (E.g. mean value.)Let $X$ and $Y$ be two datasets of size $len(X)=n$ and $len(Y)=m$.Denote the merged dataset by $$X .. Y = [X[1],...,X[n], Y[1],...,Y[m]].$$A summary statistic is _mergable_ if there is an algorithm that computes $S(X..Y)$ from $S(X)$ and $S(Y)$.Example:- The sum is mergable: $sum(X..Y) = sum(X)+sum(Y)$.References: [Agrawal, et. al. - Mergeable Summaries (pdf)](https://www.cs.utah.edu/~jeffp/papers/merge-summ.pdf) Exerciese:- Show that the maximum is mergable- Show that the median value is not mergable  Mergability in Monitoring: RollupsIt's a common practice in monitoring to rolled-up data to different time periods.Example:- raw data arrives every second at monitoring agent- the raw data is rolled-up into 1-minute aggregates at the agent and sumbitted downstream - the 1-minute data is then further rolled-up into 5-minute aggregates- the 5-minute aggregates are rolled-up into 1h aggregatesThere are two resons for that practice:- Graphing Performance. Large time range graphs can be constructed from the stored aggregates.- Storage costs. Many tools delete the one-minute data eventually and only the aggregates for historic data.For this proceedure to make sense you need either:- Store all your raw data- Use mergable summaries  Mergability in Monitoring: Service Level MetricsWhen monitoring distributed systems, it's common to have many nodes being dedicated to a single service.Service level metrics, that measure properties of the whole system are of great interested for operations and accounting.Examples of service level metrics include:- Total requests served- Latency Percentiles- Average peer lagsTo collect those service level metrics we need to either:- Stream raw data to a central location and compute the metric there (e.g. statsd, logstash) -> Slow, expensive- Use __mergable summaries__ (e.g. total request counts) that can be computed locally and merged downstream  Mean values are not mergable (on the nose)The summary statistic $S(X)=mean(X)$ is not mergable.Proof: Assume contrary that mean is mergable, then there is a computable function $f$ so that:* `X=[0], Y=[1]; mean(X..Y)=0.5`, hence `f(0,1)=0.5` and* `X=[0,0,0], Y=[1]; mean(X..Y)=0.2`, hence `f(0,1)=0.2`A contradition.  The mean values can be enriched to a mergable summaryWith a little trick, we can pimp up the mean statistic to something mergable.For two datasets $X,Y$ of length $n,m$, we can compute the mean value of the merged dataset as follows:$$mean(X..Y) = \frac{n}{n+m} \cdot mean(X) + \frac{m}{n+m} \cdot mean(Y)$$This shows, that the summary statistic $S(X)=(len(X),mean(X))$ is mergable.Remarks:* Expressions of the form $\sum w_i X[i]$, with $\sum w_i = 1$ are called _weighted means_.* Exercise: How do you aggregate a greater number of sample sets: $X_1, ..., X_k$ ?<jupyter_code># Test
print("RHS", mean([1,2,3,10,11]))
print("LHS", (3*mean([1,2,3]) + 2*mean([10,11]))/5)<jupyter_output>RHS 5.4
LHS 5.4
{'fraction_non_alphanumeric': 0.14676710021521475, 'fraction_numerical': 0.03111256666978572, 'mean_word_length': 4.143166506256016, 'pattern_counts': {'":': 0, '<': 52, '<?xml version=': 0, '>': 49, 'https://': 4, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 1, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '12', 'dir': 'jupyter-structured-clean-dedup', 'id': '604256', 'license': 'MIT', 'n_tokens_mistral': 8172, 'n_tokens_neox': 7543, 'n_words': 2380, 'path': 'work/S4 - Math I - Descriptive Statistics.ipynb', 'repo_name': 'agladkov/DataScience4Ops', 'size': '21408'}