<jupyter_start><jupyter_text>Problem Introduction Nudging customers to payed products by utilizing data produced by appsCompanies often provide free premium product/services in an attempt to transition their customers to premium membership. In this case study, the services offered by a mobile app are examined. Customers have a 24 hour frame of free premium membership. Our goal is to determine which users are less likely to subscribe to the paid membership so that greater marketing offers can be directed at them in a bid to make them premium customers. The company could then offer a free one-month trial or even better a 50% off yearly subscription so that they win them over as premium users. After all, if you are running marketing offers you don't want to offer them to cutomers who will anyway subsribe to the premium membership eh?  Bird's eye viewAs with many machine learning pipelines the roadmap has its similarities. Here is a bird's eye view of what steps we will go throw in this mini project.1. Question and required data2. Acquire the data3. Data preprocessing4. Prepare the data for the Machine Learning model5. Train the model on the training data6. Make predictions on the test data7. Evaluate the model8. If the performance is not satisfactory adjust the model.     8.1 Acquire more data    8.2 Play with hyperparameters     8.3 Choose a different model altogether.9. Interpret the model and report results visually and numerically<jupyter_code>import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from dateutil import parser
import seaborn as sn
sn.set()

from sklearn.model_selection import train_test_split, cross_val_score, GridSearchCV
from sklearn.preprocessing import StandardScaler, MinMaxScaler
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import confusion_matrix, accuracy_score, f1_score, precision_score, recall_score
from sklearn.pipeline import Pipeline
from sklearn.decomposition import PCA<jupyter_output><empty_output><jupyter_text>2. The DataThe fields in the data describe what companies usually track from their users with real-world distribution and patterns. This data makes it possible to see the date and time of app installation as well as **most importantly**, the features with which the users engaged within the app. Thus we have a great insight of what is called *app behavior*: the list of app screens the user looked at. Explanations of the columns1. user: an id identifying the user2. first_open: the day and time that the user first oppened the app3. dayofweek: day of the week in numerical form. 0-Sunday, 6-Saturday4. hour: the hour of the day when the app was first oppened5. age: the age of the customer6. screen_list: every screen name visited by the user in the first 24 hours of premium membership 7. numscreens: the number of screens visited by the user8. minigame: whether the players played a game offered by the app9. used_premium_feature: whether the user has used the premium features even though he had them for free the first 24h hours10. enrolled: whether the user enrolled for premium. **The classes that will be predicted.**11. enrolled_date: the date they enrolled. This is the only feature which is not restricted to the first 24h12. liked: whether the user hit the like button (for at least one feature)<jupyter_code>features = pd.read_csv("data/appdata10.csv")
features.sample(6)<jupyter_output><empty_output><jupyter_text>3. Data Preprocessing1. Tidy Data2. Missing Data3. Outliers4. Class-Imbalance Problem5. Feature correlation  Tidy DataThe data seems to already be [tidy](https://cran.r-project.org/web/packages/tidyr/vignettes/tidy-data.html).  Missing Data & OutliersA great way to identify both missing data and outliers is computing descriptive statistics on our dataframe.This is trivial with pandas. *describe()* also automatically filters out attributes which are not nummerical<jupyter_code>print("The dataset has {} samples and {} features(including the class)".format(*features.shape))
features.describe()<jupyter_output>The dataset has 50000 samples and 12 features(including the class)<jupyter_text>It does not look like we have outliers.Additionally, visualizing the features always helps. Visualizing features also helps in **getting comfortable with the data** which is a crucial step for any data scientist in his quest to get insight from data.<jupyter_code># Convert the hour feature to int
# astype casts a pandas type to the specified datatype
features['hour'] = features['hour'].str.slice(1,3).astype(int)
# Pick a bunch of features to visualize
features_temp = features.drop(columns=['user', 'screen_list', 'enrolled_date', 'hour', 'first_open', 'enrolled'], 
                                                                                                      axis=1)
features_temp.head()
plt.figure(figsize=(15,8))

# Set the title for all plots
plt.suptitle("Parts of the feature vector", fontsize=20, c='gray')

# The loop starts from 1 because the subplot function requires it so
for i in range(1, features_temp.shape[1]+1):
    # Add a subplot
    plt.subplot(2,3,i)
    
    # Get the current plot
    figure = plt.gca()
    
    # Set the title of the current plot
    figure.set_title(features_temp.columns[i-1])
    
    # Specify bins to be the number of unique values/features to make the plot better looking
    f_bins = np.size(features_temp.iloc[:, i-1].unique())
    
    plt.hist(features_temp.iloc[:,i-1], bins=f_bins)
    plt.xlabel("Meaure")
    plt.ylabel("Count")
    
# Give some padding to avoid overlap in graphs
plt.tight_layout(pad=2.5)<jupyter_output><empty_output><jupyter_text>Here for the binary attributes *minigame*, *used_premium_feature* and *liked* we can basically see that most of the users did not play the minigame use the premium features or liked any features. Then we can see that regarding days of the week, things stand pretty much even. Most of our users seem to be late 20s, early 30s (as can be also seen in the descriptive statistics). Finally, the average *num_screens* seems to lie around 20.**To sum up there seem to be no outliers**<jupyter_code>print(features.isnull().sum())<jupyter_output>user                        0
first_open                  0
dayofweek                   0
hour                        0
age                         0
screen_list                 0
numscreens                  0
minigame                    0
used_premium_feature        0
enrolled                    0
enrolled_date           18926
liked                       0
dtype: int64<jupyter_text>**There also seem not to be any missing data**  Class Imbalance Problem<jupyter_code>print("There seems to be no class imbalance problem. \n {}% of the data belongs to enrolled and {}% to not enrolled."
                 .format(*(100*features['enrolled'].value_counts()/features.shape[0])))<jupyter_output>There seems to be no class imbalance problem. 
 62.148% of the data belongs to enrolled and 37.852% to not enrolled.<jupyter_text>Feature CorrelationA high correlation between two features indicates that there is redundancy and one of them can be removed. Moreover such features would most likely ruin our model.  Correlation with the class variableLets check out a kind of variable importance analysis where we see the correlation between the features and the class variable (enrolled).<jupyter_code>features_temp.corrwith(features['enrolled']).plot.bar(figsize=(10,6), title='Correlation with class',
                                                         fontsize=15, rot=45)
plt.show()<jupyter_output><empty_output><jupyter_text>The most important variable (in terms of correlation) seems to be *numscreens*. The more features the users have tried out, the more likely they seem to be to enroll for premium. Playing the minigame also helps. Both of these make a lot of sense.Interestingly, however, using more premium features seems to be negatively correlated. This is strange.  Correlation Matrix: Correlation of features with each-other.As already mentioned, we do not want our features to be correlated with each-other. Not only are those features redundant but moreover they might ruin our model. The assumption when building machine learning models is that features are independent from each-other.<jupyter_code>sn.set(style='white', font_scale=1.5)

# Compute the correlation matrix
# The correlation matrix is an array where the main diagonal separetes two identical triangles
corrMatrix = features_temp.corr()

# Create a mask for the upper triangle so that we can ignore it later when building the heatmap
# When we pass this mask to the heatmap function it will generate no data for the upper triangle
mask = np.zeros_like(corrMatrix, dtype=np.bool)
# Get the indices of the upper-triangle of arr: triu_indices_from
mask[np.triu_indices_from(mask)] = True

# Generate a custom diverging colormap
# Colormap for the different values of the correlation matrix
cmap = sn.diverging_palette(220, 10, as_cmap=True)

plt.figure(figsize=(9, 7))
plt.title("Correlation Matrix of Features")

# Draw the heatmap
sn.heatmap(corrMatrix, square=True, mask=mask, cmap=cmap, vmax=.3, center=0, linewidths=1.0,
          cbar_kws={"shrink": 0.8})

plt.show()<jupyter_output><empty_output><jupyter_text>We can see that *numscreens* seems to be slightly correlated with *minigame* and *used_premium_features* which makes sense since the more screens you go through the more likely you should be to hit those types of screens. It also makes sense that *minigame* is correlated to the premium features.All in all we can say that our variables do not appear to be linearly dependent and we can continue our analysis.  4. Prepare the data for the Machine Learning Model1. Feature Engineering2. One-Hot Encoding3. Class and Features4. Train/Test5. Feature Scaling  Feature EngineeringWhat we want to do is fine tune the *enrolled* variable. We need to set a threshold on when we expect a user to convert to a paid member. If we do not set a threshold we cannot validate our model for future datasets and on production we want to predict continually and not wait weeks or months before making a prediction. For e.g., if we have a time limit of 3 days for enrollments to be considered then we only need to wait 3 days after the users have installed the app to make a prediction (whether the user is unlikely to subscribe) and thus the marketing people can target them with offers.<jupyter_code>features.dtypes<jupyter_output><empty_output><jupyter_text>First, convert the dates to date type.<jupyter_code>features['first_open'] = [parser.parse(date) for date in features['first_open']]

features['enrolled_date'] = [parser.parse(date) if isinstance(date, str) else date for date in features['enrolled_date']]

features.dtypes<jupyter_output><empty_output><jupyter_text>Now that we converted the dates into their proper type we can plot a distribution of the time since enrolled in a bid to find a threshold.<jupyter_code># Calculate the absolute difference and convert it to hours
features['difference'] = abs((features['first_open'] - features['enrolled_date']).astype('timedelta64[h]'))

# Plot
sn.set()
plt.figure(figsize=(12,10))
plt.hist(features['difference'].dropna(), range=[0,100])
plt.title("Difference between first open and subscription")
plt.show()<jupyter_output><empty_output><jupyter_text>Pretty much everything happens within the first 10 hours but lets set a threshold of 48 hours for training our model. In other words we will train our model based on data gathered from users which decided if they wanted to subscribe to premium within the first 48 hours. This means that when our model reaches production, every time a user installs the app and is granted the premium features for free our model will yield an answer as to how likely this user is to continue using these premium features (therefore subscribing) after two days.<jupyter_code># Remove the records which have a greater difference
features.drop(index=features[features['difference']>48].index, axis=0, inplace=True)

# Drop columns that are no longer needed
features.drop(columns=['difference', 'enrolled_date', 'first_open'], axis=1, inplace=True)

print("Shape of our dataframe",features.shape)<jupyter_output>Shape of our dataframe (43713, 10)<jupyter_text>So, to conclude our feature engineering we added a new column *difference* and removed records with a greater difference than 48 hours. Finally, we dropped 3 columns: *difference*, *enrolled_date* and *first_open*.  One-Hot EncodingEssentially [one-hot encoding](https://machinelearningmastery.com/why-one-hot-encode-data-in-machine-learning/) means converting categorical data into numerical data. In this case our most promising feature, *screen_list* is a categorial attribute.<jupyter_code>features[['screen_list', 'numscreens']]
print("Max number of screens in all samples:", max(features['numscreens']))<jupyter_output>Max number of screens in all samples: 325<jupyter_text>Feature Engineering If we simply convert the *screen_list* categorical data into numerical features we will blow up the number of features. A better strategy is to find the most common screens and only portray them. All the other, less common screens we will put into one single column.<jupyter_code>counter={}
for index, row in features.iterrows():
    screens = row['screen_list'].split(",")
    
    for sc in screens:
        if sc not in counter.keys():
            counter[sc]=1
        else:
            counter[sc]+=1

# Sort
counter = {k: v for k, v in sorted(counter.items(), key=lambda item: item[1], reverse=True)}

# Pick the top 80 screens
top_screens = np.array([key for key, val in counter.items()])[:80]
top_screens<jupyter_output><empty_output><jupyter_text>The strategy here is basically to create features for the popular screens and for the rest to just create one single feature<jupyter_code># This will be needed later, just add one comma
features['screen_list'] = features['screen_list'].astype(str)+','

# Add new columns for the top screens
for sc in top_screens:
    # Add a 1 or 0 if the screen is found in the screen list
    features[sc] = features['screen_list'].str.contains(sc).astype(int)
    # Remove the screen from the screen list
    features['screen_list'] = features['screen_list'].replace(sc+',', '')<jupyter_output><empty_output><jupyter_text>We are now essentially left with the the other screen_lists. We will just create one sum up column for them<jupyter_code># We added a comma earlier because we essentially are counting commas and the last term doesn't have one
features['other_screens'] = features['screen_list'].str.count(",")

# Finally drop the screen_list column
features.drop('screen_list', axis=1, inplace=True)

print("Shape of dataframe:", features.shape)<jupyter_output>Shape of dataframe: (43713, 90)<jupyter_text>However, we are not quite done yet because it looks like many screens might be correlated with each-other. When we printed them out earlier we can notice that we have *Saving9, Saving8* or *Credit2, Credit1*. Therefore we recompute the correlation matrix. This is called funneling. If they belong to the same funnel then they should be combined in one single feature.<jupyter_code>corrMatrix2 = np.array(features.corr())
correlatedFeat=[]

for i in range(9, len(corrMatrix2)):
    for j in range(9, i):
        if abs(corrMatrix2[j][i]) > 0.6:
            correlatedFeat.append((features.columns[i], features.columns[j]))

correlatedFeat
savings_screens = [
    "Saving1",
    "Saving4",
    "Saving5",
    "Saving6",
    "Saving7",
    "Saving8",
    "Saving9",
    "Saving10",
]
# Sum all the fields in savings_count by column
features['savings_count'] = features[savings_screens].sum(axis=1)
# Drop the other savings fields
features.drop(columns=savings_screens, axis=1, inplace=True)<jupyter_output><empty_output><jupyter_text>We removed all the savings because they are highligh correlated and we aggregated them all into a big saving's column.Lets funnel other groups.<jupyter_code>cm_screens = [
    "Credit3",
    "Credit3Container",
    "Credit3Dashboard",
    "Credit2",
    "Credit1"
]

# sum by column
features['cm_screens'] = features[cm_screens].sum(axis=1)
features.drop(columns=cm_screens, axis=1, inplace=True)
cc_screens = [
    "CC1",
    "CC1Category",
    "CC3"
]
features["cc_screens"] = features[cc_screens].sum(axis=1)
features.drop(columns=cc_screens, axis=1, inplace=True)
loan_screens=[
    "Loan",
    "Loan2",
    "Loan3",
    "Loan4",
]
features['loan_screens'] = features[loan_screens].sum(axis=1)
features.drop(columns=loan_screens, axis=1, inplace=True)
features.columns
print("Final shape of dataset", features.shape)<jupyter_output>Final shape of dataset (43713, 74)<jupyter_text>Separate class from featuresOur predicted class and the independent variables should be distinct<jupyter_code>labels = features['enrolled']
features.drop('enrolled', axis=1, inplace=True)<jupyter_output><empty_output><jupyter_text>Split into Train-Test<jupyter_code>train_features, test_features, train_labels, test_labels = train_test_split(features, labels, test_size=0.2,
                                                                           random_state=0)
print(train_features.shape)
print(test_features.shape)
print(train_labels.shape)
print(test_labels.shape)<jupyter_output>(34970, 73)
(8743, 73)
(34970,)
(8743,)<jupyter_text>Lets remove the user id for now and store it somewhere for later use<jupyter_code>train_uid=train_features['user']
train_features.drop('user', axis=1, inplace=True)

test_uid=test_features['user']
test_features.drop('user', axis=1, inplace=True)<jupyter_output>C:\Users\rejna\Anaconda3\lib\site-packages\pandas\core\frame.py:3997: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  errors=errors,<jupyter_text>Feature scalingFeature scaling is a very important component of machine learning algorithms that are dependent on distance measures (euclidean, [chebyshev](https://en.wikipedia.org/wiki/Chebyshev_distance), Manhattan distance etc). Such algorithms include Linear and Logistic Regression, SVN and Neural Networks. Other algorithms like Naive Bayes or Decision Trees can work without previously scaling the features.The reason is that unless normalization occurs, then the whole distance measure will be dominated by the greatest feature. Suppose for instance that you have a feature vector of two features, namely age and salary. Because age will be in the values raging from 15-80 and salary from 5k-10M the whole distance will be totally dominated by the salary. To avoid this we perform feature scaling.<jupyter_code># The standard scaler will return numpy arrays but this will not have the columns or any index
# which we need for our model! We will therefore use some intermediatte variables.
scaler = StandardScaler()

train_features2 = pd.DataFrame(scaler.fit_transform(train_features))
# Set column names
train_features2.columns = train_features.columns
# Set indices
train_features2.index = train_features.index.values

test_features2 = pd.DataFrame(scaler.transform(test_features))
test_features2.columns = test_features.columns
test_features2.index = test_features.index.values
train_features = train_features2
test_features = test_features2<jupyter_output><empty_output><jupyter_text>5. Training The ModelGenerally speaking, regularization is a method used to damping the overfitting of a model: We introduce a small amount of bias into the training set so that we might perform better on unseen data. We will set the model to be a lasso regularization. We have too many features that came up as a result of the one-hot encoding of the screen's feature. **Lasso regularization will ensure that any features that are redundant are discarded.**<jupyter_code># A random state ensures that our results are replicable
classifier = LogisticRegression(random_state=3, penalty='l1', solver='liblinear')

# classifier = LogisticRegression(random_state=3) Achieves same results. In the docs of sklearn it is mentioned
# that regularization happens by default
classifier.fit(train_features, train_labels)<jupyter_output><empty_output><jupyter_text>6. Make predictions on the test data<jupyter_code>predictions = classifier.predict(test_features)
predictions<jupyter_output><empty_output><jupyter_text>7. Evaluating the Model<jupyter_code>print("The accuracy of our model is {}% ".format(100* round(accuracy_score(test_labels, predictions), 2)))
print("Precision {}%".format(100*round(precision_score(test_labels, predictions), 2)))
print("Recall {}%".format(100* round(recall_score(test_labels, predictions), 2)))
print("F1 score {}%".format(100*round(f1_score(test_labels, predictions), 2)))<jupyter_output>The accuracy of our model is 89.0% 
Precision 97.0%
Recall 83.0%
F1 score 89.0%<jupyter_text>A precision and accuracy of 89% is pretty good. The other measures also support this.  Confusion MatrixThe confusion matrix is a great tool to evaluate our model<jupyter_code>confusionMat = confusion_matrix(test_labels, predictions)
plt.figure(figsize=(9,7))
sn.heatmap(confusionMat, square=True, annot=True, fmt='d', cbar=True)
plt.xlabel("Predicted"); plt.ylabel("Ground Truth"); plt.show()<jupyter_output><empty_output><jupyter_text>We can indeed tell that our model is doing a good job!The biggest problem is with those *134* users which were predicted as having enrolled but in reality they didn't (False Positives).   8. Adjusting the ModelEssentially there are three things that we can do1. Acquiring more data: As things stand we cannot acquire more2. Playing with Hyperparameters: 3. Choosing a different model altogether: here we only use Logistic Regression  K-Fold cross validation[K-fold](https://www.youtube.com/watch?v=fSytzGwwBVw) cross validation partitions our data in k-folds (for instance 10) and it iterates in such a way that each fold is used for testing once with the rest of the folds being testing data.<jupyter_code>accuracies = cross_val_score(estimator=classifier, X=train_features, y=train_labels, cv=10)
print("Accuracy with a 10-fold cross validation:", round(100*np.mean(accuracies), 2))<jupyter_output>Accuracy with a 10-fold cross validation: 88.82<jupyter_text>It looks like the performance is the same!  Hyperparameter tunningAlso using **Pipeline**. The benefits of [pipeline](https://scikit-learn.org/stable/modules/generated/sklearn.pipeline.Pipeline.htmlsklearn.pipeline.Pipeline) are that it enforces a desired order of application steps and we do not have to perform data scaling on train and test sets seperately as it automatically makes sure of that during cross validation.We will tune two hyperparameters ([here](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html?highlight=logisticregressionsklearn.linear_model.LogisticRegression) the docs link):1. **C**: The strength of regularization. A smaller C means as stronger regularization.2. **penalty**: norm used in penalization. Lasso, regression etc.We will use [GridSearch](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.GridSearchCV.html?highlight=gridsearchcvsklearn.model_selection.GridSearchCV) to try out a bunch of values for the two above-mentioned hyperparameters.<jupyter_code>features.drop(inplace=True, axis=1, columns='user')
train_features2, test_features2, train_labels2, test_labels2 = train_test_split(features, labels, test_size=0.2,
                                                                           random_state=0)
steps = [('scaler', StandardScaler()), ('LR', LogisticRegression())]
pipeline = Pipeline(steps)

# Try 12 different combos
grid_search = {
    'LR__C':[0.001, 0.01, 0.1, 1, 10, 100],
    'LR__solver':['liblinear'],
    'LR__penalty':['l1', 'l2']
}

lr_best = GridSearchCV(estimator=pipeline, param_grid=grid_search, scoring='neg_mean_absolute_error', cv=5, 
                                                                                   verbose=5, n_jobs=-1)
lr_best.fit(train_features2, train_labels2);<jupyter_output>Fitting 5 folds for each of 12 candidates, totalling 60 fits<jupyter_text>Take a look at the best hyperparameters:So essentially we have no difference as the default parameters that we were using remain the same. We originally used *l1* as regularization and the default value of *C* is 1.Hyperparameter tuning did not improve the model's performance. The results are the same with both *StandardScaler* as well as *MinMaxScaler*<jupyter_code>lr_best.best_params_<jupyter_output><empty_output><jupyter_text>Principal Component Analysis[PCA](https://scikit-learn.org/stable/modules/generated/sklearn.decomposition.PCA.html) is a popular way to reduce the dimensionability of our data. With a feature vector of 72 features there might be redundant features. Let's use PCA to find out more.**When to use PCA?**1. You want to reduce the number of features in the feature vector but are not sure which ones to take out2. You want to ensure that features are independent from each-other3. You do not mind making the new features less interpretableHowever, if the answer to the 3rd question is no, do not use it.<jupyter_code>print("Our feature vector consists of {} features".format(features.shape[1]))
scale = StandardScaler()
scaled_features = scale.fit_transform(features)

pca = PCA(n_components=10)
pca.fit(scaled_features)

new_features = pca.transform(scaled_features)
print("Our new feature vector consists of {} orthogonal hyperplanes".format(new_features.shape[1]))

print("Total variance explained by the new features {}%".format(round(100*sum(pca.explained_variance_ratio_), 2)))<jupyter_output>Our new feature vector consists of 10 orthogonal hyperplanes
Total variance explained by the new features 47.47%<jupyter_text>Hyperparameter Tuning with PCAWe now repeat hyperparameter tuning using PCA<jupyter_code>pipeline = [('scaler', StandardScaler()), ('PCA', PCA()), ('LR', LogisticRegression())]
pipeline = Pipeline(pipeline)

grid_search = {
    'LR__C':[0.01, 0.1, 1, 10],
    'LR__solver':['liblinear'],
    'LR__penalty':['l1'],
    'PCA__n_components': [i for i in range(30, 70)]
}

lr_best = GridSearchCV(estimator=pipeline, param_grid=grid_search, scoring='neg_mean_absolute_error', cv=3, 
                                                                                   verbose=5, n_jobs=-1)
lr_best.fit(train_features2, train_labels2);
lr_best.best_params_
predictions3 = lr_best.best_estimator_.predict(test_features2)
print(round(100*accuracy_score(test_labels, predictions3), 2))<jupyter_output>88.68<jupyter_text>Although PCA can grant us with a lower number of independent features (orthogonal to each-other), in this case it does not seem to improve performance.  9. Interpret the model and report results visually and numerically<jupyter_code># Combine the results into a final data frame
final_results = pd.concat([test_uid, test_labels], axis=1).dropna()
final_results['predicted'] = predictions
# Reset the indices
final_results = final_results.reset_index(drop=True)
final_results<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.08028715844993041, 'fraction_numerical': 0.01637242692843015, 'mean_word_length': 4.871612903225807, 'pattern_counts': {'":': 1, '<': 115, '<?xml version=': 0, '>': 117, 'https://': 9, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 18, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '32', 'dir': 'jupyter-structured-clean-dedup', 'id': '621873', 'license': 'MIT', 'n_tokens_mistral': 7947, 'n_tokens_neox': 7469, 'n_words': 3007, 'path': 'Subscribing Users/Subscribing Users.ipynb', 'repo_name': 'rlleshi/MachineLearning', 'size': '27302'}