<jupyter_start><jupyter_text>Subsurface Data Analytics  Multidimensional Scaling for Subsurface Data Analytics in Python  <NAME>, Associate Professor, University of Texas at Austin  [Twitter](https://twitter.com/geostatsguy) | [GitHub](https://github.com/GeostatsGuy) | [Website](http://michaelpyrcz.com) | [GoogleScholar](https://scholar.google.com/citations?user=QVZ20eQAAAAJ&hl=en&oi=ao) | [Book](https://www.amazon.com/Geostatistical-Reservoir-Modeling-Michael-Pyrcz/dp/0199731446) | [YouTube](https://www.youtube.com/channel/UCLqEr-xV-ceHdXXXrTId5ig)  | [LinkedIn](https://www.linkedin.com/in/michael-pyrcz-61a648a1)  Subsurface Machine Learning: Multidimensional Scaling for Subsurface Data Analytics Here's a demonstration of mulidimensional scaling for subsurface modeling in Python. This is part of my Subsuface Machine Learning Course at the Cockrell School of Engineering at the University of Texas at Austin.   Multidimensional ScalingA powerful ordination method in inferential statistics / information visualization for exploring / visualizing the similarity (conversely the difference) between individual samples from a high dimensional dataset.  * beyond 2 or 3 features it is difficult to visualize the relationship between samples* for 2 features we can easily visualize the relationships between samples with a scatter plot* for 3 features we can either visualize in 3D or include color or matrix scatter plotsMultidimensional scaling projects the $m$ dimensional data to $p$ dimensions such that $p << m$.* ideally we are able to project to $p=2$ to easily explore the relationships between the samplesWhile principal component analysis (PCA) operates with the covariance matrix, multidimensional scaling operates with the distance / dissimilarity matrix.* you don't need to know the actual feature values, just the distance or dissimilarity between the samples* as with any distance in feature space, we consider feature standardization and weighting* we may also work with a variety of dissimilarity measures Classical Multidimensional ScalingBased on Euclidian distance between samples.The Steps:1. calculate the square distance matrix, $D^{(2)} = \left[ \delta_{i,j}^2 \right]$.2. apply double centering $B = - \frac{1}{2} J D^{(2)} J$3. solve for the Eigen Values, $\lambda_1,\ldots,\lambda_p$4. solve for the projected coordinates, $x^{'}_1, \dots ,x^{'}_p$\begin{equation}X^{'} = E_m \land^{\frac{1}{2}}_m\end{equation}where $E_m$ is the matrix of Eigen vectors and $\land_m$ is the diagonal matrix of Eigen values. General comments about classical multidimensional scaling:* nonlinear dimensionality reduction* no distribution assumption* the transform may not be unique, and may be arbitrarily be translated, rotated and transformed (these do not change the pairwise distances $||x_i - x_j||$. Metric Multidimensional ScalingA generalization of classical multidimensional scaling with a variety of metrics and a loss function optimization.* formulated as an optimization problem to minimize the squate difference between the original and projected pairwise distances\begin{equation}min_{x_1,\ldots,x_m} \sum_{i<j} \left( ||x_i - x_j|| - \delta_{i,j} \right)^2\end{equation}where $||x_i - x_j||$ are the pairwise distances in the projected space ($p$ dimensional) and $\delta_{i,j}$ are the pairwise distances in the original feature space.General comments about metric multidimensional scaling:* dissimilarity measure must be meaningful* dimensionality reduction is performed such that the error in the sample pairwise distance is minimized* there is a variant known as Nonmetric Multidimensional Scaling for ordinal features (categorical with ordering). Checking Multidimensional Scaling ResultsThe multidimensional scaling approach minimizes the square difference of the pairwise distances between all of the data samples and eachother between the projected, lower dimensional, and original feature space.* **stress** is defined as:\begin{equation}Stress_P(x_1,\ldots,x_n) = \left( \sum_{i \ne j = 1,\ldots,n} \left( ||x_i - x_j|| - \delta_{i,j} \right)^2 \right)^{\frac{1}{2}}\end{equation}* it is also useful to visualize the scatterplot of projected vs. original pairwise distances Comparison with Principal Component AnalysisPrincipal component analysis takes the covariance matrix ($m \times m$) between all the features and finds the orthogonal rotation such that the **variance is maximized** over the ordered principle components.  Multidimensional scaling takes the matrix of the pairwise distances ($n \times n$) between all the samples in feature space and finds the nonlinear projects such that the **error in the pairwise distances is minimized**. Random ProjectionAn alternative multidimensional scaling that relies on an random ($p \times n$) projection matrix, with columns with unit length.* first row is a random unit vector * second row is orthogonal to the first row* continue This random approach works practically because of the The Johnson-Lindenstrauss lemma:*In mathematics, the Johnson-Lindenstrauss lemma is a result concerning low-distortion embeddings of points from high-dimensional into low-dimensional Euclidean space. The lemma states that a small set of points in a high-dimensional space can be embedded into a space of much lower dimension in such a way that distances between the points are nearly preserved. The map used for the embedding is at least Lipschitz, and can even be taken to be an orthogonal projection.* - Wikipedia article. Objective In the Stochastic Machine Learning class, I want to provide hands-on experience with solving complicated subsurface modeling problems with data analytics, machine learning. Python provides an excellent vehicle to accomplish this. I have coded a package called GeostatsPy with GSLIB: Geostatistical Library (Deutsch and Journel, 1998) functionality that provides basic building blocks for building subsurface modeling workflows. The objective is to remove the hurdles of subsurface modeling workflow construction by providing building blocks and sufficient examples. This is not a coding class per se, but we need the ability to 'script' workflows working with numerical methods.     Getting StartedHere's the steps to get setup in Python with the GeostatsPy package:1. Install Anaconda 3 on your machine (https://www.anaconda.com/download/). 2. From Anaconda Navigator (within Anaconda3 group), go to the environment tab, click on base (root) green arrow and open a terminal. 3. In the terminal type: pip install geostatspy. 4. Open Jupyter and in the top block get started by copy and pasting the code block below from this Jupyter Notebook to start using the geostatspy functionality. You will need to copy the data file to your working directory.  They are available here:* Tabular data - unconv_MV_v4.csv at https://git.io/fhHLT.There are examples below with GeostatsPy functions. You can go here to see a list of the available functions, https://git.io/fh4eX, and for other example workflows and source code.<jupyter_code>import geostatspy.GSLIB as GSLIB          # GSLIB utilies, visualization and wrapper
import geostatspy.geostats as geostats    # GSLIB methods convert to Python<jupyter_output><empty_output><jupyter_text>We will also need some standard packages. These should have been installed with Anaconda 3.<jupyter_code>import numpy as np                        # ndarrys for gridded data
import pandas as pd                       # DataFrames for tabular data
import os                                 # set working directory, run executables
import matplotlib.pyplot as plt           # for plotting
from scipy import stats                   # summary statistics
import math                               # trigonometry etc.
import scipy.signal as signal             # kernel for moving window calculation
import random                             # for randon numbers
import seaborn as sns                     # for matrix scatter plots
from scipy import linalg                  # for linear regression

from sklearn.manifold import MDS          # multidimensional scaling
from sklearn.random_projection import GaussianRandomProjection # random projection
from sklearn.random_projection import johnson_lindenstrauss_min_dim
from sklearn.random_projection import SparseRandomProjection
from sklearn.metrics.pairwise import euclidean_distances<jupyter_output><empty_output><jupyter_text>Here's a couple of functions that I found online for metrics that we will calculate below.  Set the working directoryI always like to do this so I don't lose files and to simplify subsequent read and writes (avoid including the full address each time).<jupyter_code>#os.chdir("c:/PGE383")                     # set the working directory<jupyter_output><empty_output><jupyter_text>Loading Tabular DataHere's the command to load our comma delimited data file in to a Pandas' DataFrame object.<jupyter_code>#df = pd.read_csv('unconv_MV_v4.csv')      # load our data table 
df = pd.read_csv(r'https://raw.githubusercontent.com/GeostatsGuy/GeoDataSets/master/unconv_MV_v4.csv')
df['TOC'] = np.where(df['TOC']<0.0, 0.0, df['TOC']) # set TOC < 0.0 as 0.0, otherwise leave the same<jupyter_output><empty_output><jupyter_text>Visualizing the DataFrame would be useful and we already learned about these methods in this demo (https://git.io/fNgRW). We can preview the DataFrame by utilizing the 'head' DataFrame member function (with a nice and clean format, see below). With the head command, add parameter 'n=13' to see the first 13 rows of the dataset.<jupyter_code>df.head(n=13)                             # we could also use this command for a table preview<jupyter_output><empty_output><jupyter_text>This dataset has features from 200 unconventional wells including: 0. well index1. well average porosity (%) 2. permeability (mD)3. accoustic impedance (kg/m2s*10^6)4. brittness ratio (%) 5. total organic carbon (%) 6. vitrinite reflectance (%)8. normalized initial production 90 day average (MCFPD). Note, the dataset is synthetic, but has realistic ranges and general multivariate relationships.Ranking features is really an effort to understand the features and their relationships with eachother.  We will start with basic data visualization and move to more complicated methods such are partial correlation and recursive feature elimination. Summary StatisticsLet's check the summary statistics of our data.<jupyter_code>df.describe().transpose()<jupyter_output><empty_output><jupyter_text>Summary statistics are a critical first step in data checking. * this includes the number of valid (non-null) values for each feature (count removes all np.NaN from the totals for each variable).* we can see the general behavoirs such as central tendency, mean, and dispersion, variance. * we can identify issue with negative values, extreme values, and values that are outside the range of plausible values for each property. * We can also establish the feature ranges for plotting.  We could calculate the feature range directly from the data with code like this:```pPormin = np.min(df['Por'].values)           extract ndarray of data table columnPormax = np.max(df['Por'].values)           and calculate min and max```but, this would not result in easy to understand color bars and axis scales, let's pick convenient round numbers. We will also declare feature labels for ease of plotting.<jupyter_code>pormin = 6.0; pormax = 24.0; porname = 'Porosity (%)'; portitle = 'Porosity' # user specified min and max values, and labels for plotting
permmin = 0.0; permmax = 10; permname = 'Permeability (mD)'; permtitle = 'Permeability'                
AImin = 1.0; AImax = 5.0; AIname = 'Acoustic Impedance (kg/m2s*10^6)'; AItitle = 'Acoustic Impedance'
brmin = 10.0; brmax = 85.0; brname = 'Brittleness Ratio (%)'; brtitle = 'Brittleness'
TOCmin = 0.0; TOCmax = 2.2; TOCname = 'Total Organic Carbon (%)'; TOCtitle = 'Total Organic Carbon' 
VRmin = 0.9; VRmax = 2.9; VRname = 'Vitrinite Reflectance (%)'; VRtitle = 'Vitrinite Reflectance'
prodmin = 500.0; prodmax = 9000.0; prodname = 'Normalized Initial Production (MCFPD)'; prodtitle = 'Normalized Initial Production'<jupyter_output><empty_output><jupyter_text>The data looks to be in pretty good shape and for brevity we skip outlier detection. Let's look at the distributions with a martix scatter plot from the Seaborn package.<jupyter_code>sns.pairplot(df,vars=['Por','Perm','AI','Brittle','TOC','VR','Prod'],markers='o',plot_kws={'alpha': 0.2})
plt.subplots_adjust(left=0.0, bottom=0.0, right=0.6, top=0.6, wspace=0.3, hspace=0.2)
plt.show()<jupyter_output><empty_output><jupyter_text>Feature StandardizationSince our model works with dissimilarity (analogous to distance in feature space) measures, we need ot standardize the features.<jupyter_code>from sklearn.preprocessing import StandardScaler

features = ['Por','AI','TOC']
x = df.loc[:,features].values
mu = np.mean(x, axis=0)
sd = np.std(x, axis=0)
xs = StandardScaler().fit_transform(x)

ns_features = []
for i in range(0,len(features)):
    df['NS_'+features[i]] = xs[:,i]
    ns_features.append('NS_'+features[i]) 

df.head(n=13)<jupyter_output><empty_output><jupyter_text>Data Preparation Let's make an ordinal feature from the continuous production:1. low2. medium3. high4. very high production rates.  This will help us visualize the results as we proceed, we can look at wells with different levels of production projected into a variety of lower dimensional spaces with multidimensional scaling.<jupyter_code>bins = [0,2500,5000,7500,10000]                # assign the production bins (these are the fence posts)
labels = ['low', 'med', 'high', 'vhigh']       # assign the labels
category = pd.cut(df['Prod'],bins,labels=labels)     # make the 1D array with the labels for our data
df['tProd'] = category                                # add the new ordinal production feature to our DataFrames    
df.head()
dpalette = sns.color_palette("rocket_r",n_colors = 4)
palette = sns.color_palette("rocket")<jupyter_output><empty_output><jupyter_text>Let's take a look at the matrix scatter plot of our 3 features and the production levels.<jupyter_code>plot = sns.pairplot(df[ns_features + ['tProd']],markers='o',hue = 'tProd', palette = dpalette,diag_kws={'edgecolor':'black'},plot_kws=dict(s=50, edgecolor="black", linewidth=0.5))

plt.subplots_adjust(left=0.0, bottom=0.0, right=1.5, top=1.5, wspace=0.3, hspace=0.2)
plt.show()<jupyter_output><empty_output><jupyter_text>Multidimensional ScalingThe multidimensional scaling method follows the sample pattern as other scikit-learn methods, we instantiate, fit and then apply or transform.Let's run multidimensional scaling on our subset of features ($m = 3$) and project to only 2 features ($p = 2$).* we set the random_state for repeatability, everyone gets the same result from the iterative solution* we use 20 random initializations, the best solution is selected to improve likelihood of selection of (or search resulting in) the global optimum and not a local optimum* we use an increased number of max_iter to improve the convergence<jupyter_code>np.random.seed(1867)                               # set the random number seed, so we all get the same answer
n_components = 2                                   # p, reduced dimensionality space
embedding = MDS(n_components=2,n_init = 20,max_iter = 1000,random_state = 73073) # instantiate and set the hyperparameter
MDS_transformed = embedding.fit_transform(df[ns_features])
MDS_transformed.shape<jupyter_output><empty_output><jupyter_text>The output is 2 multidimensional scaling components.  We have projected our 3 features to 2 features to minimize the error in pairwise distance between the samples.  Let's add the 2 components to our DataFrame.<jupyter_code>df['MDS1'] = MDS_transformed[:,0]
df['MDS2'] = MDS_transformed[:,1]
df.head()<jupyter_output><empty_output><jupyter_text>Let's take a look at the samples projected into the new 2 dimensional feature space.  * note the rotation, translation is arbitrary in this space, only the sample pairwise distances are relevant.<jupyter_code>plt.subplot(121)
pairplot = sns.scatterplot(x = df['MDS1'],y = df['MDS2'],hue = df['tProd'],markers='o',palette = dpalette,edgecolor="black")

plt.subplot(122)
pairplot = sns.scatterplot(x = df['MDS1'],y = df['MDS2'],hue = df['Well'],markers='o',edgecolor="black")

plt.subplots_adjust(left=0.0, bottom=0.0, right=2., top=1.3, wspace=0.3, hspace=0.2,)
plt.show()<jupyter_output><empty_output><jupyter_text>Some comments:* these are the actual samples in this space, we know the values, including the sample number to assist with interpretations* the general relationship between samples is preserved.* the general production transition from low to very high production is preserved.Let's check our model:* we will calculate the original and projected pairwise distances between all the samples* we will cross plot the original vs the projects pairwise distances* we will plot the distribution of the ratio between projects / original pariwise distances<jupyter_code>dists = euclidean_distances(df[ns_features], squared=False).ravel()
nonzero = dists != 0   # select only non-identical samples pairs
dists = dists[nonzero]
projected_dists = euclidean_distances(MDS_transformed, squared=False).ravel()[nonzero]

plt.subplot(221)
plt.scatter(dists,projected_dists,c='red',alpha=0.2,edgecolor = 'black')
plt.arrow(0,0,200,200,width=0.02,color='black',head_length=0.0,head_width=0.0)
plt.xlim(0,15); plt.ylim(0,15)
plt.xlabel("Pairwise Distance: original space")
plt.ylabel("Pairwise Distance: projected space")
plt.title("Pairwise Distance: Projected to %d Components" % n_components)

rates = projected_dists / dists
print("Distance Ratio, mean: %0.2f, standard deviation %0.2f." % (np.mean(rates), np.std(rates)))

plt.subplot(222)
plt.hist(rates, bins=50, range=(0.5, 1.5),color = 'red', alpha = 0.2, edgecolor='k')
plt.xlabel("Distance Ratio: projected / original")
plt.ylabel("Frequency")
plt.title("Pairwise Distance: Projected to %d Components" % n_components)

plt.subplot(223)
plt.hist(dists, bins=50, range=(0., 15.),color = 'red', alpha = 0.2, edgecolor='k')
plt.xlabel("Pairwise Distance")
plt.ylabel("Frequency")
plt.title("Pairwise Distance: Original Data")

plt.subplot(224)
plt.hist(projected_dists, bins=50, range=(0., 15.),color = 'red', alpha = 0.2, edgecolor='k')
plt.xlabel("Pairwise Distance")
plt.ylabel("Frequency")
plt.title("Pairwise Distance: Projected to %d Components" % n_components)

plt.subplots_adjust(left=0.0, bottom=0.0, right=1.7, top=2.3, wspace=0.2, hspace=0.3)
plt.show()<jupyter_output>Distance Ratio, mean: 0.95, standard deviation 0.15.<jupyter_text>We projected to a 2 dimensional feature space and did a pretty good job preserving the pairwise distances between the samples. Observing Specific SamplesLet's reduce the number of wells and actually post the well numbers and observe their spacings in the original and projected features spaces.<jupyter_code>df_subset = df.iloc[:30,:]<jupyter_output><empty_output><jupyter_text>Let's look at out dataset, we will visualize the 3 possible scatter plots between the three features with the sample points labeled by well number (1 through 30).<jupyter_code>plt.subplot(131)
pairplot = sns.scatterplot(x = df_subset[ns_features[0]],y = df_subset[ns_features[1]],hue = df_subset['tProd'],markers='o')
for i, txt in enumerate(df_subset['Well']):
    pairplot.annotate(txt, (df_subset[ns_features[0]][i], df_subset[ns_features[1]][i]))
    
plt.subplot(132)
pairplot = sns.scatterplot(x = df_subset[ns_features[1]],y = df_subset[ns_features[2]],hue = df_subset['tProd'],markers='o')
for i, txt in enumerate(df_subset['Well']):
    pairplot.annotate(txt, (df_subset[ns_features[1]][i], df_subset[ns_features[2]][i]))
    
plt.subplot(133)
pairplot = sns.scatterplot(x = df_subset[ns_features[0]],y = df_subset[ns_features[2]],hue = df_subset['tProd'],markers='o')
for i, txt in enumerate(df_subset['Well']):
    pairplot.annotate(txt, (df_subset[ns_features[0]][i], df_subset[ns_features[2]][i]))
    
plt.subplots_adjust(left=0.0, bottom=0.0, right=2., top=1., wspace=0.3, hspace=0.2)
plt.show()<jupyter_output><empty_output><jupyter_text>We can now see our 30 wells with their indices plots on each of the 3 possible bivariate plots.  Let's perform multidimensional scaling down to 2 components.<jupyter_code>embedding_subset = MDS(n_components=2)
MDS_transformed_subset = embedding_subset.fit_transform(df_subset[ns_features])
MDS_transformed_subset.shape<jupyter_output><empty_output><jupyter_text>Now let's visualize the wells in the projected feature space.<jupyter_code>plt.subplot(221)
pairplot = sns.scatterplot(x = df_subset[ns_features[0]],y = df_subset[ns_features[1]],hue = df_subset['tProd'],markers='o')
for i, txt in enumerate(df_subset['Well']):
    pairplot.annotate(txt, (df_subset[ns_features[0]][i], df_subset[ns_features[1]][i]))
pairplot.legend(loc='upper left')  

plt.subplot(222)
pairplot = sns.scatterplot(x = df_subset[ns_features[1]],y = df_subset[ns_features[2]],hue = df_subset['tProd'],markers='o')
for i, txt in enumerate(df_subset['Well']):
    pairplot.annotate(txt, (df_subset[ns_features[1]][i], df_subset[ns_features[2]][i]))
pairplot.legend(loc='lower right')     
    
plt.subplot(223)
pairplot = sns.scatterplot(x = df_subset[ns_features[0]],y = df_subset[ns_features[2]],hue = df_subset['tProd'],markers='o')
for i, txt in enumerate(df_subset['Well']):
    pairplot.annotate(txt, (df_subset[ns_features[0]][i], df_subset[ns_features[2]][i]))
pairplot.legend(loc='upper left')     
    
plt.subplot(224)
pairplot = sns.scatterplot(x = MDS_transformed_subset[:,0],y = MDS_transformed_subset[:,1],hue = df_subset['tProd'],markers='o')
for i, txt in enumerate(df_subset['Well']):
    pairplot.annotate(txt, (MDS_transformed_subset[i,0], MDS_transformed_subset[i,1]))
pairplot.set_xlabel('MDS1'); pairplot.set_ylabel('MDS2')  
pairplot.legend(loc='lower right') 

plt.subplots_adjust(left=0.0, bottom=0.0, right=1.8, top=1.8, wspace=0.3, hspace=0.2)
plt.show()<jupyter_output><empty_output><jupyter_text>Some observations:* the transformation is not a rotation (like PCA)* the intersample distance is approximately preserved \begin{equation}\delta_{i,j} = \left( (\delta Por_{i,j}^{2}) + (\delta AI_{i,j}^{2}) + (\delta TOC_{i,j}^{2}) \right)^{\frac{1}{2}}\end{equation} ApplicationsThe main benefit with multiple dimensional scaling is the ability to inspect high dimensional feature spaces for relationships between samples.* we may observe that specific wells cluster together* we may observe systematic transitions Random ProjectionLet's demonstrate the use of random projection.* given the low dimensionalilty you will see quite poor performance for some randon seeds<jupyter_code>n_components = 2; seed = 73058
rp = GaussianRandomProjection(n_components=n_components,random_state = seed)

dists = euclidean_distances(df[['Por','AI','TOC']], squared=False).ravel()
nonzero = dists != 0   # select only non-identical samples pairs
dists = dists[nonzero]

projected_data = rp.fit_transform(df[['Por','AI','TOC']])
projected_dists = euclidean_distances(projected_data, squared=False).ravel()[nonzero]

plt.subplot(221)
plt.scatter(dists,projected_dists,c='red',alpha=0.2,edgecolor = 'black')
plt.arrow(0,0,200,200,width=0.02,color='black',head_length=0.0,head_width=0.0)
plt.xlim(0,15); plt.ylim(0,15)
plt.xlabel("Pairwise Distance: original space")
plt.ylabel("Pairwise Distance: projected space")
plt.title("Pairwise Distance: Projected to %d Components" % n_components)

rates = projected_dists / dists
print("Distance Ratio, mean: %0.2f, standard deviation %0.2f." % (np.mean(rates), np.std(rates)))

plt.subplot(222)
plt.hist(rates, bins=50, range=(0., 2.),color = 'red', alpha = 0.2, edgecolor='k')
plt.xlabel("Distance Ratio: projected / original")
plt.ylabel("Frequency")
plt.title("Pairwise Distance: Projected to %d Components" % n_components)

plt.subplot(223)
plt.hist(dists, bins=50, range=(0., 15.),color = 'red', alpha = 0.2, edgecolor='k')
plt.xlabel("Pairwise Distance")
plt.ylabel("Frequency")
plt.title("Pairwise Distance: Original Data")

plt.subplot(224)
plt.hist(projected_dists, bins=50, range=(0., 15.),color = 'red', alpha = 0.2, edgecolor='k')
plt.xlabel("Pairwise Distance")
plt.ylabel("Frequency")
plt.title("Pairwise Distance: Projected to %d Components" % n_components)

plt.subplots_adjust(left=0.0, bottom=0.0, right=1.7, top=2.3, wspace=0.2, hspace=0.3)
plt.show()<jupyter_output>Distance Ratio, mean: 0.86, standard deviation 0.17.
{'fraction_non_alphanumeric': 0.10352673492605233, 'fraction_numerical': 0.020315293352836016, 'mean_word_length': 5.4839304531085356, 'pattern_counts': {'":': 0, '<': 85, '<?xml version=': 0, '>': 80, 'https://': 11, 'lorem ipsum': 0, 'www.': 4, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 8, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '20', 'dir': 'jupyter-structured-clean-dedup', 'id': '139169', 'license': 'MIT', 'n_tokens_mistral': 7775, 'n_tokens_neox': 7146, 'n_words': 2518, 'path': 'SubsurfaceDataAnalytics_Multidimensional_Scaling.ipynb', 'repo_name': 'caf3676/PythonNumericalDemos', 'size': '24619'}