diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..a78b9c7
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,11 @@
+# To get started with Dependabot version updates, you'll need to specify whic
+# package ecosystems to update and where the package manifests are located.
+# Please see the documentation for all configuration options:
+# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
+
+version: 2
+updates:
+  - package-ecosystem: "" # See documentation for possible values
+    directory: "/" # Location of package manifests
+    schedule:
+      interval: "weekly"
diff --git a/Anamoly_Detection_notes.md b/Anamoly_Detection_notes.md
new file mode 100644
index 0000000..7383458
--- /dev/null
+++ b/Anamoly_Detection_notes.md
@@ -0,0 +1,48 @@
+Inspired from the following [blog post](https://iwringer.wordpress.com/2015/11/17/anomaly-detection-concepts-and-techniques/): 
+Kudos to [Srinath Perera](https://www.linkedin.com/in/srinathperera) for writing this 👍
+
+## Anomaly Detection
+
+![Image](https://iwringer.files.wordpress.com/2015/11/anomelydetectionmethods.jpg?w=656)
+
+Four common classes of machine learning applications:
+
+a. classification <br>
+b. predicting next value [also known as regression] <br>
+c. anamoly detection <br>
+d. discovering data strucuture <br>
+
+### Anamoly Detection
+As the name suggests, the core focus of anamoly detection is to identify data points that deos not align with the rest of the data. In statistics, these data points are also referred as `outliers`
+
+#### Outliers
+Having outliers have **significant effect on the mean and the standard deviation** of your data and hence your results are skewed if they are not dealt properly
+
+#### Applications of Anamoly Detection
+Here are some of the examples where anamoly detection is heavily employed:
+a. fraud detection <br>
+b. surveillance <br>
+c. diagnosis <br>
+d. data cleanup <br>
+e. monitring predicitive maintenance [IoT devices]
+
+##### Since data is categorised as anomalous and non-anomalous, can't we solve it using classification ?
+This assumption is correct as long as the following three conditions hold good:
+
+a. Training data present with us is labelled <br>
+b. Anomalous and non-anomalous classes are balanced (at least 1:5 proportion) <br>
+c. Present data point is not dependent on paast data points [not suitable for time series]
+
+#### Reality
+a. Hard to obtain labelled training data all the time <br>
+b. Real-life scenarios have heavily imbalanced classes, for e.g. fraud detection in credit cards can have the distribution of 1:10^x where x can go from 3 to 6 <br>
+c. One more caveat is that of precision and recall scores for such classifiers ? What is the cost of missing a false positive or a false negative ? <br>
+[**Precision** governs of how many anomalies detected by classifiers are truly anamolies] <br>
+[**Recall** governs of how many anomalies the classifier is able to capture]
+
+### Types of Anomalies
+a. **Point Anomalies**: Individual instance of data is considered as anomalous with respect to rest of data (e.g. purchase with a large transaction value) <br>
+b. **Contexual Anomalies**: The instance of data is considered as anomalous with respect to the context, but not otherwise (e.g. large spike in a trend at middle of night) <br>
+c. **Collective Anomalies**: Unlike the previous two, here we consider a collection of data instances making up for an anomaly with respect to the rest of data <br>
+  i. Events that are actually ordered but showing a degree of disorder (e.g. rhythm in ECG) <br>
+  ii. Unexpected value comnbinations (e.g. buying a large number of expensive items) <br>
diff --git a/README.md b/README.md
index 155c6b2..9e3482e 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,13 @@
-# Machine-Learning-with-Python [![star this repo](http://githubbadges.com/star.svg?user=devAmoghS&repo=Machine-Learning-with-Python)](http://github.com/ddavison/github-badges) [![fork this repo](http://githubbadges.com/fork.svg?user=devAmoghS&repo=Machine-Learning-with-Python)](http://github.com/ddavison/github-badges/fork)
+# Machine-Learning-with-Python ![GitHub stars](https://img.shields.io/github/stars/devAmoghS/Machine-Learning-with-Python?style=for-the-badge)  ![GitHub forks](https://img.shields.io/github/forks/devAmoghS/Machine-Learning-with-Python?label=Forks&style=for-the-badge)
+
+## Star History
+
+[![Star History Chart](https://api.star-history.com/svg?repos=devAmoghS/Machine-Learning-with-Python&type=Date)](https://star-history.com/#devAmoghS/Machine-Learning-with-Python&Date)
+
+
 ![alt text](https://media.istockphoto.com/vectors/machine-learning-3-step-infographic-artificial-intelligence-machine-vector-id962219860?k=6&m=962219860&s=612x612&w=0&h=yricYyUqZbILMHp3IvtenS3xbRDhu1w1u5kk2az5tbo=)
 
-## Small scale machine learning projects to understand the core concepts
+## Small scale machine learning projects to understand the core concepts (order: oldest to newest)
 * Topic Modelling using **Latent Dirichlet Allocation** with newsgroups20 dataset, implemented with Python and Scikit-Learn
 * Implemented a simple **neural network** built with Keras on MNIST dataset
 * Stock Price Forecasting on Google using **Linear Regression**
@@ -21,6 +27,11 @@
 * Sentence generation using n-grams
 * Sentence generation using **Grammars and Automata Theory; Gibbs Sampling** 
 * Topic Modelling using Latent Dirichlet Analysis (LDA)
+* Wrapper for using Scikit-Learn's **GridSearchCV** for a **Keras Neural Network**
+* **Recommender system** using **cosine similarity**, recommending new interests to users as well as matching users as per common interests
+* Implementing different methods for **network analysis** such as **PageRank, Betweeness Centrality, Closeness Centrality, EigenVector Centrality**
+* Implementing methods used for **Hypothesis Inference** such as **P-hacking, A/B Testing, Bayesian Inference**
+* Implemented **K-nearest neigbors** for next presedential election and prediciting voting behavior based on nearest neigbors.
 
 ## Installation notes
 MLwP is built using Python 3.5.  The easiest way to set up a compatible
@@ -29,22 +40,108 @@ environment with the exact version of Python used for development along with all
 dependencies needed to run MLwP.
 
 1.  [Download and install Conda](https://conda.io/docs/download.html).
-2.  Create a Conda environment with Python 3.
+2.  Create a Conda environment with Python 3. 
+
+(**Note**: enter ```cd ~``` to go on **$HOME** , then perform these commands)
 
     ```
-    conda create -n *your env name* python=3.5
+    conda create --name *your env name* python=3.5
     ```
+   
+   You will get the following, mlwp-test is the env name used in this example
+   
+   ```
+   Solving environment: done
+   
+## Package Plan ##
+
+  environment location: /home/user/anaconda3/envs/mlwp-test
+
+  added / updated specs: 
+    - python=3.5
+
+
+The following NEW packages will be INSTALLED:
+
+    ca-certificates: 2018.12.5-0            
+    certifi:         2018.8.24-py35_1       
+    libedit:         3.1.20181209-hc058e9b_0
+    libffi:          3.2.1-hd88cf55_4       
+    libgcc-ng:       8.2.0-hdf63c60_1       
+    libstdcxx-ng:    8.2.0-hdf63c60_1       
+    ncurses:         6.1-he6710b0_1         
+    openssl:         1.0.2p-h14c3975_0      
+    pip:             10.0.1-py35_0          
+    python:          3.5.6-hc3d631a_0       
+    readline:        7.0-h7b6447c_5         
+    setuptools:      40.2.0-py35_0          
+    sqlite:          3.26.0-h7b6447c_0      
+    tk:              8.6.8-hbc83047_0       
+    wheel:           0.31.1-py35_0          
+    xz:              5.2.4-h14c3975_4       
+    zlib:            1.2.11-h7b6447c_3      
+
+Proceed ([y]/n)?  *Press y*
+
+Preparing transaction: done
+Verifying transaction: done
+Executing transaction: done
+#
+# To activate this environment, use:
+# > source activate mlwp-test
+#
+# To deactivate an active environment, use:
+# > source deactivate
+#
+
+   ```
+   The environment is successfully created.
 
 3.  Now activate the Conda environment.
 
     ```
     source activate *your env name*
     ```
+    You will get the following
+    
+    ```
+    (mlwp-test) amogh@hp15X34:~$ 
+    ```
+    Enter `conda list` to get the list of available packages
+    
+    ```
+        (mlwp-test) amogh@hp15X34:~$ conda list
+    # packages in environment at /home/amogh/anaconda3/envs/mlwp-test:
+    #
+    # Name                    Version                   Build  Channel
+    ca-certificates           2018.12.5                     0  
+    certifi                   2018.8.24                py35_1  
+    libedit                   3.1.20181209         hc058e9b_0  
+    libffi                    3.2.1                hd88cf55_4  
+    libgcc-ng                 8.2.0                hdf63c60_1  
+    libstdcxx-ng              8.2.0                hdf63c60_1  
+    ncurses                   6.1                  he6710b0_1  
+    openssl                   1.0.2p               h14c3975_0  
+    pip                       10.0.1                   py35_0  
+    python                    3.5.6                hc3d631a_0  
+    readline                  7.0                  h7b6447c_5  
+    setuptools                40.2.0                   py35_0  
+    sqlite                    3.26.0               h7b6447c_0  
+    tk                        8.6.8                hbc83047_0  
+    wheel                     0.31.1                   py35_0  
+    xz                        5.2.4                h14c3975_4  
+    zlib                      1.2.11               h7b6447c_3 
+    ```
 
 4.  Install the required dependencies.
 
     ```
-    ./scripts/install_requirements.sh
+    (mlwp-test) amogh@hp15X34:~$ conda install --yes --file *path to requirements.txt*
+    ```
+    
+5. In case you are not able to install the packages or getting `PackagesNotFoundError`
+Use the following command ` conda install -c conda-forge *list of packages separated by space*`. For more info, refer issue [#3](https://github.com/devAmoghS/Machine-Learning-with-Python/issues/3) **Unable to install requirements**
+
 
 ## How good is the code ?
 * It is well tested
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..034e848
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,21 @@
+# Security Policy
+
+## Supported Versions
+
+Use this section to tell people about which versions of your project are
+currently being supported with security updates.
+
+| Version | Supported          |
+| ------- | ------------------ |
+| 5.1.x   | :white_check_mark: |
+| 5.0.x   | :x:                |
+| 4.0.x   | :white_check_mark: |
+| < 4.0   | :x:                |
+
+## Reporting a Vulnerability
+
+Use this section to tell people how to report a vulnerability.
+
+Tell them where to go, how often they can expect to get an update on a
+reported vulnerability, what to expect if the vulnerability is accepted or
+declined, etc.
diff --git a/Understanding SQL Queries.md b/Understanding SQL Queries.md
new file mode 100644
index 0000000..5e658d7
--- /dev/null
+++ b/Understanding SQL Queries.md	
@@ -0,0 +1,185 @@
+### Three SQL Concepts you Must Know to Pass the Data Science Interview
+
+#### Credits: Thanks to Jay Feng for writing this [article](https://www.interviewquery.com/blog-three-sql-questions-you-must-know-to-pass/)
+
+#### 1. Getting the first or last value for each user in a `transactions` table.
+
+`transactions`
+
+| column_name       | data_type     |
+--- | --- |
+| user_id       | int     |     
+| created_at    | datetime|     
+| product       | varchar |     
+
+##### Question: Given the user transactions table above, write a query to get the first purchase for each user.
+
+#### Solution:
+
+We want to take a table that looks like this:
+
+ user_id | created_at | product  
+ --- | --- | ---  
+  123    | 2019-01-01 | apple    
+  456    | 2019-01-02 | banana   
+  123    | 2019-01-05 | pear    
+  456    | 2019-01-10 | apple   
+  789    | 2019-01-11 | banana  
+
+and turn it into this
+
+ user_id | created_at | product   
+ --- | --- | ---  
+ 123     | 2019-01-01 | apple      
+ 456     | 2019-01-02 | banana     
+ 789     | 2019-01-11 | banana
+ 
+ The solution can be broken into two parts:
+ - First make a table of `user_id` and the first purchase (i.e. minimum create date). We can get this by the following query
+ 
+```
+SELECT 
+  user_id, MIN(created_at) AS min_created_at
+FROM 
+  transactions
+GROUP BY 1
+```
+
+- Now all we have to do is join this table back to the original on two columns: `user_id` and `created_at`. <br>
+The self join will effectively filter for the first purchase.<br> 
+Then all we have to do is grab all of the columns on the left side table.
+
+```
+SELECT 
+  t.user_id, t.created_at, t.product
+FROM 
+  transactions AS t
+  INNER JOIN (
+    SELECT user_id, MIN(created_at) AS min_created_at
+    FROM transactions
+    GROUP BY 1
+  ) AS t1 ON (t.user_id = t1.user_id AND t.created_at = t1.min_created_at)
+```
+
+#### 2. Knowing the difference between a LEFT JOIN and INNER JOIN in practice.
+
+ `users`
+ 
+ 
+| column_name       | data_type     |
+--- | --- |   
+| id      | int     |     
+| name    | varchar |     
+| city_id | int     |
+
+`city_id` is `id` in the `cities` table
+
+`cities`               
+| column_name       | data_type     |
+--- | --- | 
+| id      | int     |   
+| name    | varchar |       
+
+      
+##### Question: Given the `users` and `cities` tables above, write a query to return the list of cities without any users.
+
+This question aims to test the candidate's understanding of the LEFT JOIN and INNER JOIN
+
+##### What is the actual difference between a LEFT JOIN and INNER JOIN?
+
+**INNER JOIN**: returns rows when there is a match in __both tables__.<br> 
+**LEFT JOIN**: returns all rows from the left table, __even if there are no matches in the right table__.
+
+#### Solution:
+
+We know that each user in the users table must live in a city given the city_id field.<br> 
+However the `cities` table doesn’t have a `user_id` field. <br> 
+In which if we run an INNER JOIN between these two tables joined by the city_id in each table, we’ll get all of the cities that have users and __all of the cities without users will be filtered out.__
+
+But what if we run a LEFT JOIN between cities and users?
+
+cities.name  | users.id
+--- | --- | 
+seattle      | 123
+seattle      | 124
+portland     | null
+san diego    | 534
+san diego    | 564
+
+Here we see that since we are keeping all of the values on the LEFT side of the table, since there’s no match on the city of Portland to any users that exist in the database, the city shows up as NULL. <br>
+Therefore now all we have to do is run a __WHERE filter to where any value in the users table is NULL.__
+
+```
+SELECT 
+  cities.name, users.id
+FROM 
+  cities
+  LEFT JOIN users ON users.city_id = cities.id
+WHERE 
+  users.id IS NULL
+```
+
+#### 3. Aggregations with a conditional statement
+
+`transactions`
+| column_name       | data_type     |
+--- | --- | 
+| user_id       | int     |     
+| created_at    | datetime|     
+| product       | varchar |     
+
+##### Question: Given the same user transactions table as before,write a query to get the total purchases made in the morning versus afternoon/evening (AM vs PM) by day.
+
+We are comparing two groups. Every time we have to compare two groups we must use a GROUP BY
+
+In this case, we need to create a separate column to actually run our GROUP BY on, which in this case, is the difference between AM or PM in the `created_at` field.
+
+```
+CASE 
+ WHEN HOUR(created_at) > 11 THEN 'PM' 
+ ELSE 'AM' 
+END AS time_of_day 
+```
+
+We can cast the created_at column to the hour and set the new column value time_of_day as AM or PM based on this condition. 
+
+Now we just have to run a GROUP BY on the original `created_at` field truncated to the day AND the new column we created that differentiates each row value. <br> 
+The last aggregation will then be the output variable we want which is total purchases by running the COUNT function.
+
+```
+SELECT
+ DATE_TRUNC('day', created_at) AS date
+ ,CASE 
+   WHEN HOUR(created_at) > 11 THEN 'PM' 
+   ELSE 'AM' 
+  END AS time_of_day
+ ,COUNT(*)
+FROM 
+ transactions
+GROUP BY 1,2
+```
+### Bonus Questions
+
+#### 4.Write an SQL query that makes recommendations using the pages that your friends liked. Assume you have two tables: 
+
+`usersAndFriends`
+| column_name       | data_type     |
+--- | --- | 
+| user_id       | int     |     
+| friend    | int| 
+
+`usersLikedPages`
+| column_name       | data_type     |
+--- | --- | 
+| user_id       | int     |     
+| page_id    | int| 
+
+#### It should not recommend pages you already like.
+
+#### 5.Write an SQL query that shows percentage change month over month in daily active users. Assume you have a table: 
+
+`logins`
+| column_name       | data_type     |
+--- | --- | 
+| user_id       | int     |     
+| date    | date| 
diff --git a/Understanding Vanishing Gradient.md b/Understanding Vanishing Gradient.md
new file mode 100644
index 0000000..a0dcaff
--- /dev/null
+++ b/Understanding Vanishing Gradient.md	
@@ -0,0 +1,59 @@
+# Understanding Vanishing Gradients in Neural Networks
+
+#### Credits: Thanks to [Chi-Feng Wang](https://towardsdatascience.com/@reina.wang) for writing this [article](https://towardsdatascience.com/the-vanishing-gradient-problem-69bf08b15484)
+
+![Vanishing Gradient](https://i.stack.imgur.com/YUlyb.jpg)
+
+### TL;DR
+The gradient used in backprop is calculated using the derivative chain rule, meaning it is a product of about as many factors as there are layers (in a vanilla feedforward net). <br> 
+If all those factors are e.g. between 0 and 1 (e.g. due to the choice of 'squishing' activation functions), and some are very small (typical in the earlier layers and when activations are saturated), then the overall product (gradient) will get very small, near zero.<br> 
+The risk of this happening grows with the number of factors (the number of layers). <br>
+The problem is that this may happen for a weight configuration that is nowhere near optimal, yet training will slow down or stop
+
+### Introduction
+
+We all know that neural networks perform learning through the process of forward pass and backward pass.<br> 
+This cycle goes on until we find a optimal value for the cost function that we are trying to minimize. <br>
+The optmization happens with the help of gradient descent.<br>
+
+### What are gradients ?
+Gradients are the derivative of a function. It determines how much change happens when the input to the function is changed by a very big number <br>
+
+Gradients of neural networks are found using backpropagation(backward pass as mentioned above). <br>
+1. Backpropogation finds the derivatives of the network by moving layer by layer from the final layer to the initial one.<br> 
+2. By the chain rule, the derivatives of each layer are multiplied down the network (from the final layer to the initial) to compute the derivatives of the initial layers.
+
+### Why does it happen ?
+
+A very commonly used activation function is the sigmoid function.
+
+The sigmoid function squashes the input value into a range of 0 to 1. <br>
+Hence if there is a large change in the value, there is not much change in the output by the sigmoid. Hence the derivative of this function is very small. <br>
+
+The graph below also shows us the same picture. For very large or small values of x, the derivative of sigmoid is very small (almost closer to zero)
+
+![Sigmoid Function and its Derivative](https://miro.medium.com/max/1000/1*6A3A_rt4YmumHusvTvVTxw.png)
+
+### How does it impact ?
+
+As explained above, we are multiplying gradients with each other in the bacward pass step using chain rule. <br>
+So when we are multiplying a lot of small numbers (almost near zero quantities). The gradient value is descreased very sharply.  <br>
+
+A small gradient means that the weights and biases of the initial layers will not be updated effectively with each training session. 
+
+**Since these initial layers are often crucial to recognizing the core elements of the input data, it can lead to overall inaccuracy of the whole network.**
+
+### Solutions to the vanishing gradients
+
+1. We can use other other activation function like `Relu`
+` Relu(x) = max(x,0)`
+
+2. Using residual networks is also an effective solution where we add the input value X to the next layer before applying the activation.  <br>
+This way the overall derivative is not reduced to a small value. Refer the diagram below.
+
+![A Residual Block](https://miro.medium.com/max/385/1*mxJ5gBvZnYPVo0ISZE5XkA.png)
+
+3. Batch normalization is also an effective solution. We normalize the input value x ==> |x| so that it does not have extremely large or small values and hence the derivative is not very small.  <br>
+We limit the input function to a small range and hence the output from the sigmoid also remains normal. We can see the same behavior that the green region does not have very small derivatives. Refer the diagram below
+
+![Sigmoig function with limited values](https://miro.medium.com/max/700/1*XCtAytGsbhRQnu-x7Ynr0Q.png)
diff --git a/hparams_grid_search_keras_nn.py b/hparams_grid_search_keras_nn.py
new file mode 100644
index 0000000..561a631
--- /dev/null
+++ b/hparams_grid_search_keras_nn.py
@@ -0,0 +1,102 @@
+import numpy as np
+import pandas as pd
+from keras import Sequential
+from keras.layers import Dense
+from keras.wrappers.scikit_learn import KerasClassifier
+from sklearn.model_selection import GridSearchCV
+from sklearn.model_selection import train_test_split
+
+DATA_FILE = ''
+
+feature_cols = ['feat1', 'feat2', 'feat3', 'feat4', 'feat5', 'feat6']
+labels = ['y']
+
+
+def load_data(filepath):
+    data = pd.read_csv(filepath)
+    return data
+
+
+def describe_data(data, name):
+    print('\nGetting the summary for ' + name + '\n')
+    print('Dataset Length:', len(data))
+    print('Dataset Shape:', data.shape)
+    print(data.columns)
+    print(data.dtypes)
+
+
+def create_model():
+    model = Sequential()
+    model.add(Dense(12, input_dim=5, kernel_initializer='uniform', activation='relu'))
+    model.add(Dense(8, kernel_initializer='uniform', activation='relu'))
+    model.add(Dense(1, kernel_initializer='uniform', activation='sigmoid'))
+
+    model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
+
+    return model
+
+
+if __name__ == '__main__':
+
+    data_df = load_data(DATA_FILE)
+
+    data_df = data_df.dropna()
+    print(data_df.isnull().sum(axis=0))
+
+    X_data = data_df[feature_cols]
+    y_data = data_df[['y']]
+
+    # seed for reproducibility
+    seed = 7
+    np.random.seed(seed=seed)
+
+    # train test split
+    X, X_test, y, y_test = train_test_split(X_data, y_data, test_size=.20, random_state=42)
+
+    # train val split
+    X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=.20, random_state=42)
+
+    # summarize the datasets
+    describe_data(X_train, name="X_train")
+    describe_data(X_val, name="X_val")
+    describe_data(X_test, name="X_test")
+    describe_data(y_train, name="y_train")
+    describe_data(y_val, name="y_val")
+    describe_data(y_test, name="y_test")
+
+    # create model
+    model = KerasClassifier(build_fn=create_model)
+
+    # hyperparamater optimization
+    batch_size = [10, 20, 40, 60, 80, 100]
+    epochs = [10, 50, 100]
+    learn_rate = [0.001, 0.01, 0.1, 0.2, 0.3]
+    momentum = [0.0, 0.2, 0.4, 0.6, 0.8, 0.9]
+    weight_constraint = [1, 2, 3, 4, 5]
+    dropout_rate = [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
+    neurons = [1, 5, 10, 15, 20, 25, 30]
+
+    param_grid = dict(batch_size=batch_size, epochs=epochs)
+    grid = GridSearchCV(estimator=model, param_grid=param_grid, n_jobs=-1)
+    grid_result = grid.fit(X=X_train, y=y_train)
+
+    # summarize results
+    print("Best: %f using %s" % (grid_result.best_score_, grid_result.best_params_))
+    means = grid_result.cv_results_['mean_test_score']
+    stds = grid_result.cv_results_['std_test_score']
+    params = grid_result.cv_results_['params']
+    for mean, stdev, param in zip(means, stds, params):
+        print("%f (%f) with: %r" % (mean, stdev, param))
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/hypothesis_inference.py b/hypothesis_inference.py
new file mode 100644
index 0000000..4c50c31
--- /dev/null
+++ b/hypothesis_inference.py
@@ -0,0 +1,205 @@
+from helpers.probability import normal_cdf, inverse_normal_cdf
+import math, random
+
+
+def normal_approximation_to_binomial(n, p):
+    """finds mu and sigma corresponding to a Binomial(n, p)"""
+    mu = p * n
+    sigma = math.sqrt(p * (1 - p) * n)
+    return mu, sigma
+
+
+#####
+#
+# probabilities a normal lies in an interval
+#
+######
+
+# the normal cdf _is_ the probability the variable is below a threshold
+normal_probability_below = normal_cdf
+
+
+# it's above the threshold if it's not below the threshold
+def normal_probability_above(lo, mu=0, sigma=1):
+    return 1 - normal_cdf(lo, mu, sigma)
+
+
+# it's between if it's less than hi, but not less than lo
+def normal_probability_between(lo, hi, mu=0, sigma=1):
+    return normal_cdf(hi, mu, sigma) - normal_cdf(lo, mu, sigma)
+
+
+# it's outside if it's not between
+def normal_probability_outside(lo, hi, mu=0, sigma=1):
+    return 1 - normal_probability_between(lo, hi, mu, sigma)
+
+
+######
+#
+#  normal bounds
+#
+######
+
+
+def normal_upper_bound(probability, mu=0, sigma=1):
+    """returns the z for which P(Z <= z) = probability"""
+    return inverse_normal_cdf(probability, mu, sigma)
+
+
+def normal_lower_bound(probability, mu=0, sigma=1):
+    """returns the z for which P(Z >= z) = probability"""
+    return inverse_normal_cdf(1 - probability, mu, sigma)
+
+
+def normal_two_sided_bounds(probability, mu=0, sigma=1):
+    """returns the symmetric (about the mean) bounds
+    that contain the specified probability"""
+    tail_probability = (1 - probability) / 2
+
+    # upper bound should have tail_probability above it
+    upper_bound = normal_lower_bound(tail_probability, mu, sigma)
+
+    # lower bound should have tail_probability below it
+    lower_bound = normal_upper_bound(tail_probability, mu, sigma)
+
+    return lower_bound, upper_bound
+
+
+def two_sided_p_value(x, mu=0, sigma=1):
+    if x >= mu:
+        # if x is greater than the mean, the tail is above x
+        return 2 * normal_probability_above(x, mu, sigma)
+    else:
+        # if x is less than the mean, the tail is below x
+        return 2 * normal_probability_below(x, mu, sigma)
+
+
+def count_extreme_values():
+    extreme_value_count = 0
+    for _ in range(100000):
+        num_heads = sum(1 if random.random() < 0.5 else 0  # count # of heads
+                        for _ in range(1000))  # in 1000 flips
+        if num_heads >= 530 or num_heads <= 470:  # and count how often
+            extreme_value_count += 1  # the # is 'extreme'
+
+    return extreme_value_count / 100000
+
+
+upper_p_value = normal_probability_above
+lower_p_value = normal_probability_below
+
+
+##
+#
+# P-hacking
+#
+##
+
+def run_experiment():
+    """flip a fair coin 1000 times, True = heads, False = tails"""
+    return [random.random() < 0.5 for _ in range(1000)]
+
+
+def reject_fairness(experiment):
+    """using the 5% significance levels"""
+    num_heads = len([flip for flip in experiment if flip])
+    return num_heads < 469 or num_heads > 531
+
+
+##
+#
+# running an A/B test
+#
+##
+
+def estimated_parameters(N, n):
+    p = n / N
+    sigma = math.sqrt(p * (1 - p) / N)
+    return p, sigma
+
+
+def a_b_test_statistic(N_A, n_A, N_B, n_B):
+    p_A, sigma_A = estimated_parameters(N_A, n_A)
+    p_B, sigma_B = estimated_parameters(N_B, n_B)
+    return (p_B - p_A) / math.sqrt(sigma_A ** 2 + sigma_B ** 2)
+
+
+##
+#
+# Bayesian Inference
+#
+##
+
+def B(alpha, beta):
+    """a normalizing constant so that the total probability is 1"""
+    return math.gamma(alpha) * math.gamma(beta) / math.gamma(alpha + beta)
+
+
+def beta_pdf(x, alpha, beta):
+    if x < 0 or x > 1:  # no weight outside of [0, 1]
+        return 0
+    return x ** (alpha - 1) * (1 - x) ** (beta - 1) / B(alpha, beta)
+
+
+if __name__ == "__main__":
+    mu_0, sigma_0 = normal_approximation_to_binomial(1000, 0.5)
+    print("mu_0", mu_0)
+    print("sigma_0", sigma_0)
+    print("normal_two_sided_bounds(0.95, mu_0, sigma_0)", normal_two_sided_bounds(0.95, mu_0, sigma_0))
+    print()
+    print("power of a test")
+
+    print("95% bounds based on assumption p is 0.5")
+
+    lo, hi = normal_two_sided_bounds(0.95, mu_0, sigma_0)
+    print("lo", lo)
+    print("hi", hi)
+
+    print("actual mu and sigma based on p = 0.55")
+    mu_1, sigma_1 = normal_approximation_to_binomial(1000, 0.55)
+    print("mu_1", mu_1)
+    print("sigma_1", sigma_1)
+
+    # a type 2 error means we fail to reject the null hypothesis
+    # which will happen when X is still in our original interval
+    type_2_probability = normal_probability_between(lo, hi, mu_1, sigma_1)
+    power = 1 - type_2_probability  # 0.887
+
+    print("type 2 probability", type_2_probability)
+    print("power", power)
+    print()
+    print("one-sided test")
+    hi = normal_upper_bound(0.95, mu_0, sigma_0)
+    print("hi", hi)  # is 526 (< 531, since we need more probability in the upper tail)
+    type_2_probability = normal_probability_below(hi, mu_1, sigma_1)
+    power = 1 - type_2_probability  # = 0.936
+    print("type 2 probability", type_2_probability)
+    print("power", power)
+    print()
+
+    print("two_sided_p_value(529.5, mu_0, sigma_0)", two_sided_p_value(529.5, mu_0, sigma_0))
+
+    print("two_sided_p_value(531.5, mu_0, sigma_0)", two_sided_p_value(531.5, mu_0, sigma_0))
+
+    print("upper_p_value(525, mu_0, sigma_0)", upper_p_value(525, mu_0, sigma_0))
+    print("upper_p_value(527, mu_0, sigma_0)", upper_p_value(527, mu_0, sigma_0))
+    print()
+
+    print("P-hacking")
+
+    random.seed(0)
+    experiments = [run_experiment() for _ in range(1000)]
+    num_rejections = len([experiment
+                          for experiment in experiments
+                          if reject_fairness(experiment)])
+
+    print(num_rejections, "rejections out of 1000")
+    print()
+
+    print("A/B testing")
+    z = a_b_test_statistic(1000, 200, 1000, 180)
+    print("a_b_test_statistic(1000, 200, 1000, 180)", z)
+    print("p-value", two_sided_p_value(z))
+    z = a_b_test_statistic(1000, 200, 1000, 150)
+    print("a_b_test_statistic(1000, 200, 1000, 150)", z)
+    print("p-value", two_sided_p_value(z))
diff --git a/interview_prep.md b/interview_prep.md
new file mode 100644
index 0000000..b9f25af
--- /dev/null
+++ b/interview_prep.md
@@ -0,0 +1,228 @@
+### 1. What is multi-collinearity
+
+When two or more predictors are highly correlated to each other such that one predictor 
+can be derived using the linear combinations of other predictors, then the predictors are said to be collinear
+
+### 2. What is the difference between standardisation and normalization ? Why is it useful?
+Standardisation is a scclaing technique in which values are shifted and rescaled so that the mean is 0 and the variance is 1
+
+Normalization is a scaling technique in which values are shifted and rescaled so that they end up ranging between 0 and 1. It is also known as Min-Max scaling
+
+* Algorithms which use gradient descent based optimisation (linear regression, logistic regression, neural networks) will require features to be scaled so that optimization will be faster and the convergence will be more accurate.
+* **Having features on a similar scale can help the gradient descent converge more quickly towards the minima.**
+* Distance algorithms like KNN, K-means, and SVM are most affected by the range of features. This is because behind the scenes they are using distances between data points to determine their similarity.
+* **Therefore, we scale our data before employing a distance based algorithm so that all the features contribute equally to the result.**
+
+![](https://i.pinimg.com/originals/1c/16/04/1c160466f8bfd26ca66a44f79514fb5d.jpg)
+
+### 3. What is the central limit theorem ? Why is it useful ?
+The Central Limit Theorem is about how the sum of many different independent random variables tends towards a normal distribution (bell curve).
+
+For example: suppose you're rolling 2 6-sided dice. The rolls are all independent because one of the rolls doesn't affect any of the other rolls. For a single die, the distribution is the same chance for 1 2 3 4 5 and 6.
+
+But of you add the sum of 2 dice, you will notice that you have a 1/36 chance to get a 2, 2/36 chance to get a 3, 3/36 chance to get a 4, ..., up until 6/36 chance of getting a 7, then the chance decreases again until you're back at 1/36 chance of getting a 12. This is because the values in the middle, like 7, can be reached by getting 1+6, 6+1, 2+5, 5+2, 3+4 and 4+3, whereas the edges like 2 require a single very specific result (1+1) where every single die needs to land on 1.
+
+If you further increase the number of dice you roll, the edge cases become less and less likely, because they keep requiring very specific results, whereas the results in the middle become more likely. The more dice you add, the more it will eventually look like a bell curve.
+
+![](https://prwatech.in/blog/wp-content/uploads/2019/06/CetralLimitThm-1024x512.png)
+
+### 4. What is the inter quartile range ? Why is it useful ?
+
+The interquartile range is a measure of where the “middle fifty” is in a data set. Where a range is a measure of where the beginning and end are in a set, an interquartile range is a measure of where the bulk of the values lie. That’s why it’s preferred over many other measures of spread when reporting things like school performance or SAT scores.
+
+The interquartile range formula is the first quartile subtracted from the third quartile:
+IQR = Q3 – Q1.
+
+![](https://naysan.ca/wp-content/uploads/2020/06/box_plot_ref_needed.png)
+
+#### IQR as a test of normality in a distribution
+
+Use the interquartile range formula with the mean and standard deviation to test whether or not a population has a normal distribution. The formula to determine whether or not a population is normally distributed are:
+Q1 – (σ z1) + X
+Q3 – (σ z3) + X
+Where Q1 is the first quartile, Q3 is the third quartile, σ is the standard deviation, z is the standard score (“z-score“) and X is the mean. In order to tell whether a population is normally distributed, solve both equations and then compare the results. If there is a significant difference between the results and the first or third quartiles, then the population is not normally distributed.
+
+#### IQR as an instrument to detect outliers and to determine the spread of data
+The interquartile range and the quartile deviation refer to the same thing. They both mean the difference between the third quartile (Q3) and the first quartile (Q1). Both are also called midspread or middle fifty.
+
+Some of its applications include determining the spread of data. It is used in the construction of a box plot. It is a good indicator of spread because it is robust with breakpoint of 25%. A breakpoint percentage indicates the number of incorrect observations, before a parameter starts giving a wrong description of the data set. A 25% breakpoint is robust, as it needs a quarter of the data to be incorrect, before it reflects an incorrect spread.
+
+The IQR is also used to determine outliers to the data set. This is in conjuction with the box plot (or the box-and-whisker plot). Outliers are defined as values that are below Q1-1.5*IQR or above Q3+1.5*IQR. There are other methods that could be used to determine whether outliers can be eliminated from the data set.
+
+### 5. What is the difference between t-test and z-test ? Why is it useful ?
+
+![](https://www.wallstreetmojo.com/wp-content/uploads/2019/01/Z-Test-vs-T-Test.png)
+
+
+| Basis                               |                                                                               Z Test                                                                               |                                                                                                   T-Test                                                                                                   |
+|-------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|
+| Basic Definition                    | Z-test is a kind of hypothesis test which ascertains if the averages of the 2 datasets are different from each other when standard deviation or variance is given. | The t-test can be referred to as a kind of parametric test that is applied to an identity, how the averages of 2 sets of data differ from each other when the standard deviation or variance is not given. |
+| Population Variance                 | The Population variance or standard deviation is known here.                                                                                                       | The Population variance or standard deviation is unknown here.                                                                                                                                             |
+| Sample Size                         | The Sample size is large.                                                                                                                                          | Here the Sample Size is small.                                                                                                                                                                             |
+| Key Assumptions                     | All data points are independent. Normal Distribution for Z, with an average zero and variance = 1.                                                                 | All data points are not dependent. Sample values are to be recorded and taken accurately.                                                                                                                  |
+| Based upon (a type of distribution) | Based on Normal distribution.                                                                                                                                      | Based on Student-t distribution.                                                                                                                                                                           |
+
+### 6. Why do we take n-1 when calculating sample variance? Why is it useful ?
+Read about Besel correction for more technical definition
+
+##### Intuitive explaination
+
+If you are giving the standard deviation of an entire population and not a sample you actually do divide by n. However, the denominator is not referencing the number of observations, it's actually referencing degrees of freedom, which is n-1. For you to understand degrees of freedom I would recommend this example using hats.
+
+Basically you divide by the number of things you need to 'know' before you can fill in the blanks yourself. If you are using an entire population, you need every single example as you can't just fill in the blanks. But if you have a sample, you can know all but the last one before you can fill in the blank.
+
+##### Example
+
+![](https://ae01.alicdn.com/kf/HTB1XFW0JXXXXXcKXFXXq6xXFXXX1/225440714/HTB1XFW0JXXXXXcKXFXXq6xXFXXX1.jpg)
+
+Imagine you have a huge bookshelf. You measure the total thickness of the first 6 books and it turns out to be 158mm. This means that the mean thickness of a book based on first 6 samples is 26.3mm.
+Now you take out and measure the first book's thickness (one degree of freedom) and find that it is 22mm. This means that the remaining 5 books must have a total thickness of 136mm
+Now you measure the second book (second degree of freedom) and find it to be 28mm. So you know that the remaining 4 books should have a total thickness of 108mm .
+.
+.
+In this way, by the time you measure the thickness of the 5th book individually (5th degree of freedom) , you automatically know the thickness of the remaining 1 book.
+
+This means that you automatically know the thickness of 6th book even though you have measured only 5. Extrapolating this concept, In a sample of size n, you know the value of the n'th observation even though you have only taken (n-1) measurements. i.e, the opportunity to vary has been taken away for the n'th observation.
+
+This means that if you have measured (n-1) objects then the nth object has no freedom to vary. Therefore, degree of freedom is only (n-1) and not n.
+
+### 7. What are the assumptions of the linear regression model ? Why is it useful ?
+We can divide the basic assumptions of linear regression into two categories based on whether the assumptions are about the explanatory variables (i.e. features) or the residuals.
+
+#### Assumptions about the explanatory variables (features):
+* Linearity
+* No multicollinearity
+
+#### Assumptions about the error terms (residuals):
+* Gaussian distribution
+* Homoskedasticity
+* No autocorrelation
+* Zero conditional mean
+
+### 8. What are the different approches to outlier detection ?  How will you handle the outliers? Why is it useful ?
+### 9. How you assess OLS regression models ?
+Three statistics are used in Ordinary Least Squares (OLS) regression to evaluate model fit: 
+* R-squared, 
+* the overall F-test, and 
+* the Root Mean Square Error (RMSE). 
+
+All three are based on two sums of squares: Sum of Squares Total (SST) and Sum of Squares Error (SSE). SST measures how far the data are from the mean, and SSE measures how far the data are from the model’s predicted values. Different combinations of these two values provide different information about how the regression model compares to the mean model.
+
+##### R-squared and Adjusted R-squared
+
+The difference between SST and SSE is the improvement in prediction from the regression model, compared to the mean model. Dividing that difference by SST gives R-squared. It is the proportional improvement in prediction from the regression model, compared to the mean model. **It indicates the goodness of fit of the model.**
+
+R-squared has the useful property that its scale is intuitive: it ranges from zero to one, with zero indicating that the proposed model does not improve prediction over the mean model, and one indicating perfect prediction. Improvement in the regression model results in proportional increases in R-squared.
+
+One pitfall of R-squared is that it can only increase as predictors are added to the regression model. This increase is artificial when predictors are not actually improving the model’s fit. To remedy this, a related statistic, Adjusted R-squared, incorporates the model’s degrees of freedom. **Adjusted R-squared will decrease as predictors are added if the increase in model fit does not make up for the loss of degrees of freedom. Likewise, it will increase as predictors are added if the increase in model fit is worthwhile.** Adjusted R-squared should always be used with models with more than one predictor variable. It is interpreted as the proportion of total variance that is explained by the model.
+
+There are situations in which a high R-squared is not necessary or relevant. When the interest is in the relationship between variables, not in prediction, the R-square is less important. An example is a study on how religiosity affects health outcomes. A good result is a reliable relationship between religiosity and health. No one would expect that religion explains a high percentage of the variation in health, as health is affected by many other factors. Even if the model accounts for other variables known to affect health, such as income and age, an R-squared in the range of 0.10 to 0.15 is reasonable.
+
+![](https://miro.medium.com/max/1954/1*iFgJVgavYdENdtkssTS6pA.png)
+
+##### The F-test
+
+The F-test evaluates the null hypothesis that all regression coefficients are equal to zero versus the alternative that at least one is not. An equivalent null hypothesis is that R-squared equals zero. A significant F-test indicates that the observed R-squared is reliable and is not a spurious result of oddities in the data set. **Thus the F-test determines whether the proposed relationship between the response variable and the set of predictors is statistically reliable and can be useful when the research objective is either prediction or explanation.**
+
+##### RMSE
+
+The RMSE is the square root of the variance of the residuals. It indicates the absolute fit of the model to the data–how close the observed data points are to the model’s predicted values. **Whereas R-squared is a relative measure of fit, RMSE is an absolute measure of fit.** As the square root of a variance, RMSE can be interpreted as the standard deviation of the unexplained variance, and has the useful property of being in the same units as the response variable. **Lower values of RMSE indicate better fit. RMSE is a good measure of how accurately the model predicts the response, and it is the most important criterion for fit if the main purpose of the model is prediction.**
+
+##### NOTE: The best measure of model fit depends on the researcher’s objectives, and more than one are often useful. The statistics discussed above are applicable to regression models that use OLS estimation. Many types of regression models, however, such as mixed models, generalized linear models, and event history models, use maximum likelihood estimation. 
+
+### 10. What are the loss functions used in logistic regression ?
+
+![](https://miro.medium.com/max/548/1*rdBw0E-My8Gu3f_BOB6GMA.png)
+
+where y is the label (1 for event and 0 for non-event) and p(y) is the predicted probability of the event happening for all N observations.
+Reading this formula, it tells you that, for each time the event occcurs (y=1), it adds log(p(y)) to the loss, that is, the log probability of event happening. Conversely, it adds log(1-p(y)), that is, the log probability of event not happening, for each non-event (y=0)
+
+### 11. Explain random forest in laymen terms ?
+
+Say you have three job offers and you wish to decide which is the best among them, you have the following criterion you use to shortlist a job offer like
+* tools and technology
+* company brand
+* health insurance
+* support for education
+* compensation
+* travel time
+* joining bonus etc.
+
+You reach out to 10 of your connections on LinkedIn and ask them which is the best comapny to join based on 3 random criteria (for eg. c2, c3, c5)
+You make different combinations of criteria while asking to different connections. At the end, you finally select company which is recommended the most from all the responses.
+
+##### This is how a random forest also works
+![](https://cdn.analyticsvidhya.com/wp-content/uploads/2020/02/rfc_vs_dt1.png)
+
+### 12. How does logisitc regression work in laymen terms ?
+### 13. Why is logistic regression bad idea for multiclass classification ?
+### 14. How do you perform the train test split in a timeseries modelling ?
+### 15. What is the impact on timeseries model in case we have latge variation in data ?
+### 16. How do you decide the value of K(value of clusters) in K-means clustering ?
+### 17. What are the advantages and disadvantages of undersampling and oversampling ?
+### 18. Which are some supervised algorithms that are not impacted by imbalanced data ?
+### 19. You are a placement coordinator, you have to design a system for resume recommendation aligning to a company's requirement ?
+a. K means clustering to make clusters
+b. Ranking algorithm to sort for relevance
+
+_Second Strategy_
+
+a. Perform document similarity using Hamming distance (distance based approach)
+b. Compute the JD document distance with the resumes
+c. Shortlist top K resumes 
+
+### 20. How will you encode a feature like PinCode which has very high number of discrete values?
+Target mean encoding
+### 21. How do you design the architecture of a neural network?
+
+## Section II
+
+| Algorithm               | Problem Identification | Evaluation Metric                                                                               | Bias Variance | Impact of outliers | Impact of imbalanced data |   |
+|-------------------------|------------------------|-------------------------------------------------------------------------------------------------|---------------|--------------------|---------------------------|---|
+| Linear Regression       | Regression             | - Coefficient of determination (R2) - Adjusted R2 - Root Mean Square Error (RMSE) - Mean Absolute Error (MAE) - Root Mean Squared Logarithmic Error (RMSLE)| - High Bias Low Variance               | -Impacted by outliers                   |                           |   |
+| Logistic Regression     | Classification         | - Accuracy - Precision - Recall - F-beta score - Area under ROC curve                           | - High Bias Low Variance               | -Impacted by outliers                    |                           |   |
+| Support Vector Machines | Classification         | - Accuracy - Precision - Recall - F-beta score - Area under ROC curve                                                                                                | - Low Bias High Variance               | Sensitive to outliers                    | Sensitive to imbalanced data                          |   |
+| K-nearest neighbors     | Classification         | - Accuracy - Precision - Recall - F-beta score - Area under ROC curve                                                                                                | - Low Bias High Variance                | Sensitive to outliers                   | Sensitive to imbalanced data                           |   |
+| Decision Tree           | Both                   | Both                                                                                                | - Low Bias High Variance                 | - Not impacted by outliers                   | - Not impacted by imbalanced data                           |   |
+| Random Forest           | Both                   | Both                                                                                                | - Low Bias High Variance                 | - Not impacted by outliers                   | - Not impacted by imbalanced data                          |   |
+| K-means clustering      | Clustering             | - Elbow method - Silhoutte Analysis                                                                                                 |               | Senstive to Outliers                   |                           |   |
+|                         |                        |                                                                                                 |               |                    |                           |   |
+
+### 22. Why do CNNs perfom better with images ? (What is it that CNN achieve better than ANN when delaing with image data)
+### 23. Explain K-means clustering in laymen terms ?
+### 24. Does a low coefficient of determination always mean that my model is bad or vice versa ? Explain.
+* R-squared does not measure goodness of fit.
+* R-squared does not measure predictive error.
+* R-squared does not allow you to compare models using transformed responses.
+* R-squared does not measure how one variable explains another.
+
+Ref:- https://data.library.virginia.edu/is-r-squared-useless/#:~:text=Let's%20recap%3A-,R%2Dsquared%20does%20not%20measure%20goodness%20of%20fit.,how%20one%20variable%20explains%20another.
+
+### 24. What is the difference between probability and likelihood ?
+### 25. What is the difference between generative and discriminative models ?
+### 26. How is a decision tree pruned ?
+### 27. What do you understand by the bias variance tradeoff ? 
+
+![](https://djsaunde.files.wordpress.com/2017/07/bias-variance-tradeoff.png)
+
+Bias ia how well the model fits the data. Variance tells us the magnitude of change in the model based on the change in data
+a. Very simple models have high bias and low variance eg. linear models 
+b. Very complex models have low bias and high variance eg. tree based models. Hence they are more prone to overfitting.
+
+How to deal with them ?
+
+| High Bias                                    | High Variance |   |
+|----------------------------------------------|---------------|---|
+| Try getting additional features              | Try getting more training examples              |   |
+| Try adding polynomial features               | Try smaller set of features              |   |
+| Try to decrease the regularization parameter | Try to increase the regularization parameter              |   |
+
+### 28. What is right or left skewness  ? 
+### 29. What is difference between bootstrapping and k-folds cross validation  ?
+overlapping of sample does not happen for k-folds cross validation
+### 30. Which model is better: n_estimators=10 and n_estimators=30 ?
+### 31. Why do we use activation functions in neural networks ?
+### 32. What is the purpose of the optimizers ?
+### 33. How does the stochastic gradient descent optimizer work  ? 
+
+
diff --git a/k_means_clustering/Understanding the algorithm.md b/k_means_clustering/Understanding the algorithm.md
new file mode 100644
index 0000000..9c91e87
--- /dev/null
+++ b/k_means_clustering/Understanding the algorithm.md	
@@ -0,0 +1,46 @@
+### Introduction
+
+* K-means clustering is an unsupervised machine learning algorithm.
+* K-means algorithm is an iterative algorithm that tries to partition the dataset into `K` pre-defined distinct non-overlapping subgroups(clusters) where each data point belongs to only one group. 
+* It tries to make the intra-cluster data points as similar as possible while also keeping the clusters as different (far) as possible. 
+* It assigns data points to a cluster such that the sum of the squared distance between the data points and the cluster’s centroid (arithmetic mean of all the data points that belong to that cluster) is at the minimum. 
+* The less variation we have within clusters, the more homogeneous (similar) the data points are within the same cluster
+
+### Problem Statement
+
+Given some **unlabelled** data points, we have to identify subgroups such that 
+1. Points in the same subgroup are similar to each other.
+2. Points in different subgroup are dissimilar to each other.
+
+**Example used here**
+
+We have the data for a large social networking company which is planning to host meetups for their users. We have the users' location data. Now the VP of Growth want you to `choose the` **meetup locations** `so it becomes convinient for everyone to attend`
+
+### Intuition
+
+* In K-means, `k` is the `no. of subgroups` you want the data to be segregated into ?
+* Optimal value of `k` can be derived by using `elbow method` (discussed below)
+**Centroid Initialization**
+* We begin by initializing `k` random data points as the centroids (first pass)
+* The measure of distance between the data points and centroids can be calculated using either `Euclidean Distance` or `Manhattan Distance`
+
+**Iteration**
+* **Cluster assigment:** We assign a cluster to the data point that is nearest to it.
+* Once all the points are assigned to their nearest centroids, then for each cluster the centroid is calculated again using centroid initialization step.
+* With the new centroids, we repeat the step of cluster assignment.
+* These two steps are iterated as long as `there is no change in cluster assigment of data points` i.e. no data point is moving into a new cluster.
+
+### Choosing K value - Elbow method
+* Elbow method gives us an idea on what a good k number of clusters.
+* This is based on the sum of squared distance (SSE) between data points and their assigned clusters’ centroids. 
+* We pick `k` at the spot where SSE starts to flatten out and forming an elbow. 
+
+Here I am increasing the k value by 1 from `1 to 10` and printing the sum of squared distance with respected `k` value.
+![Code](https://miro.medium.com/max/866/1*9z8erk4kvsnxkfv-QhsHZg.png)
+
+### Note
+
+* K-means gives more weight to the bigger clusters.
+* K-means assumes spherical shapes of clusters (with radius equal to the distance between the centroid and the furthest data point) and doesn’t work well when clusters are in different shapes such as elliptical clusters.
+* If there is overlapping between clusters, K-means doesn’t have an intrinsic measure for uncertainty for the examples belong to the overlapping region in order to determine for which cluster to assign each data point.
+* K-means may still cluster the data even if it can’t be clustered such as data that comes from uniform distributions.
diff --git a/k_means_clustering/data.py b/k_means_clustering/data.py
index 9c9cabc..095405b 100644
--- a/k_means_clustering/data.py
+++ b/k_means_clustering/data.py
@@ -1,2 +1,20 @@
-inputs = [[-14, -5], [13, 13], [20, 23], [-19, -11], [-9, -16], [21, 27], [-49, 15], [26, 13], [-46, 5], [-34, -1],
-          [11, 15], [-49, 0], [-22, -16], [19, 28], [-12, -8], [-13, -19], [-41, 8], [-11, -6], [-25, -9], [-18, -3]]
+inputs = [[-14, -5],
+ [13, 13],
+ [20, 23],
+ [-19, -11],
+ [-9, -16],
+ [21, 27],
+ [-49, 15],
+ [26, 13],
+ [-46, 5],
+ [-34, -1],
+ [11, 15],
+ [-49, 0],
+ [-22, -16],
+ [19, 28],
+ [-12, -8],
+ [-13, -19],
+ [-41, 8],
+ [-11, -6],
+ [-25, -9],
+ [-18, -3]]
diff --git a/k_nearest_neighbors/Understanding the algorithm.md b/k_nearest_neighbors/Understanding the algorithm.md
new file mode 100644
index 0000000..a4689f4
--- /dev/null
+++ b/k_nearest_neighbors/Understanding the algorithm.md	
@@ -0,0 +1,40 @@
+### Introduction
+
+K-nearest nieghbor is a supervised machine learning algorithm.
+
+### Problem Statement
+
+Given some labelled data points, we have to classify a new data point according to its nearest neigbors.
+
+**Example used here**
+
+We have the data for a large social networking company which ran polls for their favroite programming language. The users belong from a group of large cities. Now the VP of Community Engagement want you to `predict the` **favorite programming language** `for the places that were` **not** `part of the survey`
+
+### Intuition
+
+* In kNN, k is the no. of neigbors you will evaluate to decide which group a new data point will belong to ?
+* Value of k is decided by plotting the error rate against the different value of k
+* Once the value of k is initiliazed, we take the nearest the k neigbors from the data point
+* The measure of distance between the data points can be calculated using either `Euclidean Distance` or `Manhattan Distance`
+* Once we calculate the distance of all the k nearest neigbors, we then look for the majority of labels in the neigbots
+* The data point is assigned to the group which has maximum no. of neigbors
+
+### Choosing K value
+* First divide the entire data set into training set and test set. 
+* Apply the KNN algorithm into training set and cross validate it with test set.
+* Lets assume you have a train set `xtrain` and test set `xtest`
+* Now create the model with `k` value `1` and predict with test set data 
+* Check the accuracy and other parameters then repeat the same process after increasing the k value by 1 each time.
+
+
+Here I am increasing the k value by 1 from `1 to 29` and printing the accuracy with respected `k` value.
+![Code](https://qphs.fs.quoracdn.net/main-qimg-9e8fedc07dafba2106eb11f0bfd4ba7d.webp)
+
+### Note
+
+* kNN is impacted by `Imbalanced datasets`. 
+Suppose there are `m` instances of **class 1** and `n` insatnces of **class 2** where `n << m`. 
+In a case where `k > n`, then this may lead to counting of more instances of m and 
+hence it will impact the majority election in k nearest neigbors
+
+* kNN is also very sensitve to `outliers`
diff --git a/k_nearest_neighbors/__pycache__/data.cpython-35.pyc b/k_nearest_neighbors/__pycache__/data.cpython-35.pyc
new file mode 100644
index 0000000..461a7cd
Binary files /dev/null and b/k_nearest_neighbors/__pycache__/data.cpython-35.pyc differ
diff --git a/k_nearest_neighbors/__pycache__/utils.cpython-35.pyc b/k_nearest_neighbors/__pycache__/utils.cpython-35.pyc
new file mode 100644
index 0000000..fe825e7
Binary files /dev/null and b/k_nearest_neighbors/__pycache__/utils.cpython-35.pyc differ
diff --git a/k_nearest_neighbors/data.py b/k_nearest_neighbors/data.py
new file mode 100644
index 0000000..0c5d2f9
--- /dev/null
+++ b/k_nearest_neighbors/data.py
@@ -0,0 +1,77 @@
+cities = [(-86.75, 33.5666666666667, 'Python'),
+ (-88.25, 30.6833333333333, 'Python'),
+ (-112.016666666667, 33.4333333333333, 'Java'),
+ (-110.933333333333, 32.1166666666667, 'Java'),
+ (-92.2333333333333, 34.7333333333333, 'R'),
+ (-121.95, 37.7, 'R'),
+ (-118.15, 33.8166666666667, 'Python'),
+ (-118.233333333333, 34.05, 'Java'),
+ (-122.316666666667, 37.8166666666667, 'R'),
+ (-117.6, 34.05, 'Python'),
+ (-116.533333333333, 33.8166666666667, 'Python'),
+ (-121.5, 38.5166666666667, 'R'),
+ (-117.166666666667, 32.7333333333333, 'R'),
+ (-122.383333333333, 37.6166666666667, 'R'),
+ (-121.933333333333, 37.3666666666667, 'R'),
+ (-122.016666666667, 36.9833333333333, 'Python'),
+ (-104.716666666667, 38.8166666666667, 'Python'),
+ (-104.866666666667, 39.75, 'Python'),
+ (-72.65, 41.7333333333333, 'R'),
+ (-75.6, 39.6666666666667, 'Python'),
+ (-77.0333333333333, 38.85, 'Python'),
+ (-80.2666666666667, 25.8, 'Java'),
+ (-81.3833333333333, 28.55, 'Java'),
+ (-82.5333333333333, 27.9666666666667, 'Java'),
+ (-84.4333333333333, 33.65, 'Python'),
+ (-116.216666666667, 43.5666666666667, 'Python'),
+ (-87.75, 41.7833333333333, 'Java'),
+ (-86.2833333333333, 39.7333333333333, 'Java'),
+ (-93.65, 41.5333333333333, 'Java'),
+ (-97.4166666666667, 37.65, 'Java'),
+ (-85.7333333333333, 38.1833333333333, 'Python'),
+ (-90.25, 29.9833333333333, 'Java'),
+ (-70.3166666666667, 43.65, 'R'),
+ (-76.6666666666667, 39.1833333333333, 'R'),
+ (-71.0333333333333, 42.3666666666667, 'R'),
+ (-72.5333333333333, 42.2, 'R'),
+ (-83.0166666666667, 42.4166666666667, 'Python'),
+ (-84.6, 42.7833333333333, 'Python'),
+ (-93.2166666666667, 44.8833333333333, 'Python'),
+ (-90.0833333333333, 32.3166666666667, 'Java'),
+ (-94.5833333333333, 39.1166666666667, 'Java'),
+ (-90.3833333333333, 38.75, 'Python'),
+ (-108.533333333333, 45.8, 'Python'),
+ (-95.9, 41.3, 'Python'),
+ (-115.166666666667, 36.0833333333333, 'Java'),
+ (-71.4333333333333, 42.9333333333333, 'R'),
+ (-74.1666666666667, 40.7, 'R'),
+ (-106.616666666667, 35.05, 'Python'),
+ (-78.7333333333333, 42.9333333333333, 'R'),
+ (-73.9666666666667, 40.7833333333333, 'R'),
+ (-80.9333333333333, 35.2166666666667, 'Python'),
+ (-78.7833333333333, 35.8666666666667, 'Python'),
+ (-100.75, 46.7666666666667, 'Java'),
+ (-84.5166666666667, 39.15, 'Java'),
+ (-81.85, 41.4, 'Java'),
+ (-82.8833333333333, 40, 'Java'),
+ (-97.6, 35.4, 'Python'),
+ (-122.666666666667, 45.5333333333333, 'Python'),
+ (-75.25, 39.8833333333333, 'Python'),
+ (-80.2166666666667, 40.5, 'Python'),
+ (-71.4333333333333, 41.7333333333333, 'R'),
+ (-81.1166666666667, 33.95, 'R'),
+ (-96.7333333333333, 43.5666666666667, 'Python'),
+ (-90, 35.05, 'R'),
+ (-86.6833333333333, 36.1166666666667, 'R'),
+ (-97.7, 30.3, 'Python'),
+ (-96.85, 32.85, 'Java'),
+ (-95.35, 29.9666666666667, 'Java'),
+ (-98.4666666666667, 29.5333333333333, 'Java'),
+ (-111.966666666667, 40.7666666666667, 'Python'),
+ (-73.15, 44.4666666666667, 'R'),
+ (-77.3333333333333, 37.5, 'Python'),
+ (-122.3, 47.5333333333333, 'Python'),
+ (-89.3333333333333, 43.1333333333333, 'R'),
+ (-104.816666666667, 41.15, 'Java')]
+ 
+cities = [([longitude,  latitude],  language) for longitude,  latitude,  language in cities]
diff --git a/k_nearest_neighbors/model.py b/k_nearest_neighbors/model.py
new file mode 100644
index 0000000..e851395
--- /dev/null
+++ b/k_nearest_neighbors/model.py
@@ -0,0 +1,35 @@
+import random
+from k_nearest_neighbors.data import cities
+from k_nearest_neighbors.utils import knn_classify, random_distances
+from helpers.stats import mean
+
+if __name__ == "__main__":
+
+    # try several different values for k
+    for k in [1, 3, 5, 7]:
+        num_correct = 0
+
+        for location, actual_language in cities:
+
+            other_cities = [other_city
+                            for other_city in cities
+                            if other_city != (location, actual_language)]
+
+            predicted_language = knn_classify(k, other_cities, location)
+
+            if predicted_language == actual_language:
+                num_correct += 1
+
+        print(k, "neighbor[s]:", num_correct, "correct out of", len(cities))
+
+    dimensions = range(1, 101, 5)
+
+    avg_distances = []
+    min_distances = []
+
+    random.seed(0)
+    for dim in dimensions:
+        distances = random_distances(dim, 10000)  # 10,000 random pairs
+        avg_distances.append(mean(distances))     # track the average
+        min_distances.append(min(distances))      # track the minimum
+        print(dim, min(distances), mean(distances), min(distances) / mean(distances))
diff --git a/k_nearest_neighbors/utils.py b/k_nearest_neighbors/utils.py
new file mode 100644
index 0000000..1c5254c
--- /dev/null
+++ b/k_nearest_neighbors/utils.py
@@ -0,0 +1,108 @@
+import random
+from collections import Counter
+
+import matplotlib.pyplot as plt
+
+from helpers.linear_algebra import distance
+from k_nearest_neighbors.data import cities
+
+
+def raw_majority_vote(labels):
+    votes = Counter(labels)
+    winner, _ = votes.most_common(1)[0]
+    return winner
+
+
+def majority_vote(labels):
+    """assumes that labels are ordered from nearest to farthest"""
+    vote_counts = Counter(labels)
+    winner, winner_count = vote_counts.most_common(1)[0]
+    num_winners = len([count
+                       for count in vote_counts.values()
+                       if count == winner_count])
+
+    if num_winners == 1:
+        return winner                     # unique winner, so return it
+    else:
+        return majority_vote(labels[:-1]) # try again without the farthest
+
+
+def knn_classify(k, labeled_points, new_point):
+    """each labeled point should be a pair (point, label)"""
+
+    # order the labeled points from nearest to farthest
+    by_distance = sorted(labeled_points,
+                         key=lambda point_label: distance(point_label[0], new_point))
+
+    # find the labels for the k closest
+    k_nearest_labels = [label for _, label in by_distance[:k]]
+
+    # and let them vote
+    return majority_vote(k_nearest_labels)
+
+
+def plot_state_borders(plt):
+    pass
+
+
+def plot_cities():
+
+    # key is language, value is pair (longitudes, latitudes)
+    plots = { "Java" : ([], []), "Python" : ([], []), "R" : ([], []) }
+
+    # we want each language to have a different marker and color
+    markers = { "Java" : "o", "Python" : "s", "R" : "^" }
+    colors  = { "Java" : "r", "Python" : "b", "R" : "g" }
+
+    for (longitude, latitude), language in cities:
+        plots[language][0].append(longitude)
+        plots[language][1].append(latitude)
+
+    # create a scatter series for each language
+    for language, (x, y) in plots.items():
+        plt.scatter(x, y, color=colors[language], marker=markers[language],
+                          label=language, zorder=10)
+
+    plot_state_borders(plt)    # assume we have a function that does this
+
+    plt.legend(loc=0)          # let matplotlib choose the location
+    plt.axis([-130,-60,20,55]) # set the axes
+    plt.title("Favorite Programming Languages")
+    plt.show()
+
+
+def classify_and_plot_grid(k=1):
+    plots = { "Java" : ([], []), "Python" : ([], []), "R" : ([], []) }
+    markers = { "Java" : "o", "Python" : "s", "R" : "^" }
+    colors  = { "Java" : "r", "Python" : "b", "R" : "g" }
+
+    for longitude in range(-130, -60):
+        for latitude in range(20, 55):
+            predicted_language = knn_classify(k, cities, [longitude, latitude])
+            plots[predicted_language][0].append(longitude)
+            plots[predicted_language][1].append(latitude)
+
+    # create a scatter series for each language
+    for language, (x, y) in plots.items():
+        plt.scatter(x, y, color=colors[language], marker=markers[language],
+                          label=language, zorder=0)
+
+    plot_state_borders(plt)    # assume we have a function that does this
+
+    plt.legend(loc=0)          # let matplotlib choose the location
+    plt.axis([-130,-60,20,55]) # set the axes
+    plt.title(str(k) + "-Nearest Neighbor Programming Languages")
+    plt.show()
+
+#
+# the curse of dimensionality
+#
+
+
+def random_point(dim):
+    return [random.random() for _ in range(dim)]
+
+
+def random_distances(dim, num_pairs):
+    return [distance(random_point(dim), random_point(dim))
+            for _ in range(num_pairs)]
diff --git a/network_analysis/__pycache__/data.cpython-35.pyc b/network_analysis/__pycache__/data.cpython-35.pyc
new file mode 100644
index 0000000..14f6857
Binary files /dev/null and b/network_analysis/__pycache__/data.cpython-35.pyc differ
diff --git a/network_analysis/__pycache__/utils.cpython-35.pyc b/network_analysis/__pycache__/utils.cpython-35.pyc
new file mode 100644
index 0000000..f9f9e46
Binary files /dev/null and b/network_analysis/__pycache__/utils.cpython-35.pyc differ
diff --git a/network_analysis/data.py b/network_analysis/data.py
new file mode 100644
index 0000000..92c068a
--- /dev/null
+++ b/network_analysis/data.py
@@ -0,0 +1,19 @@
+users = [
+    { "id": 0, "name": "Hero" },
+    { "id": 1, "name": "Dunn" },
+    { "id": 2, "name": "Sue" },
+    { "id": 3, "name": "Chi" },
+    { "id": 4, "name": "Thor" },
+    { "id": 5, "name": "Clive" },
+    { "id": 6, "name": "Hicks" },
+    { "id": 7, "name": "Devin" },
+    { "id": 8, "name": "Kate" },
+    { "id": 9, "name": "Klein" }
+]
+
+friendships = [(0, 1), (0, 2), (1, 2), (1, 3), (2, 3), (3, 4),
+               (4, 5), (5, 6), (5, 7), (6, 8), (7, 8), (8, 9)]
+
+endorsements = [(0, 1), (1, 0), (0, 2), (2, 0), (1, 2),
+                (2, 1), (1, 3), (2, 3), (3, 4), (5, 4),
+                (5, 6), (7, 5), (6, 8), (8, 7), (8, 9)]
\ No newline at end of file
diff --git a/network_analysis/model.py b/network_analysis/model.py
new file mode 100644
index 0000000..efba4bb
--- /dev/null
+++ b/network_analysis/model.py
@@ -0,0 +1,23 @@
+from network_analysis.data import users
+from network_analysis.utils import eigenvector_centralities, page_rank
+
+if __name__ == '__main__':
+
+    print("Betweenness Centrality")
+    for user in users:
+        print(user["id"], user["betweenness_centrality"])
+    print()
+
+    print("Closeness Centrality")
+    for user in users:
+        print(user["id"], user["closeness_centrality"])
+    print()
+
+    print("Eigenvector Centrality")
+    for user_id, centrality in enumerate(eigenvector_centralities):
+        print(user_id, centrality)
+    print()
+
+    print("PageRank")
+    for user_id, pr in page_rank(users).items():
+        print(user_id, pr)
\ No newline at end of file
diff --git a/network_analysis/utils.py b/network_analysis/utils.py
new file mode 100644
index 0000000..10c41c9
--- /dev/null
+++ b/network_analysis/utils.py
@@ -0,0 +1,183 @@
+import random
+from collections import deque
+from functools import partial
+
+from helpers.linear_algebra import dot, get_row, get_column, shape, make_matrix, magnitude, scalar_multiply, distance
+from network_analysis.data import users, friendships, endorsements
+
+for user in users:
+    user["friends"] = []
+
+# and populate it
+for i, j in friendships:
+    # this works because users[i] is the user whose id is i
+    users[i]["friends"].append(users[j]) # add i as a friend of j
+    users[j]["friends"].append(users[i]) # add j as a friend of i
+
+
+def shortest_paths_from(from_user):
+
+    # a dictionary from "user_id" to *all* shortest paths to that user
+    shortest_paths_to = {from_user["id"]: [[]]}
+
+    # a queue of (previous_user, next user) that we need to check
+    # starts out with all the pairs (from_user, friend_of_from_user)
+    frontier = deque((from_user, friend)
+                     for friend in from_user["friends"])
+
+    # keep going until we empty the deque
+    while frontier:
+
+        prev_user, user = frontier.popleft()    # remove the user who is first in the queue
+        user_id = user["id"]
+
+        # because of the way we are adding to the queue,
+        # necessarily we already know some shortest paths to prev_user
+        paths_to_prev_user = shortest_paths_to[prev_user["id"]]
+        new_paths_to_user = [path + [user_id] for path in paths_to_prev_user]
+
+        # it is possible we already know a shortest path
+        old_paths_to_user = shortest_paths_to.get(user_id, [])
+
+        # what is the shortest path tot here that we have seen so far ?
+        if old_paths_to_user:
+            min_path_length = len(old_paths_to_user[0])
+        else:
+            min_path_length = float('inf')
+
+        # only keep paths that are not too long and are actually new
+        new_paths_to_user = [path
+                             for path in new_paths_to_user
+                             if len(path) <= min_path_length
+                             and path not in old_paths_to_user]
+
+        shortest_paths_to[user_id] = old_paths_to_user + new_paths_to_user
+
+        # add never-seen neighbors to the frontier
+        frontier.extend((user, friend)
+                        for friend in user["friends"]
+                        if friend["id"] not in shortest_paths_to)
+
+    return shortest_paths_to
+
+
+for user in users:
+    user["shortest_paths"] = shortest_paths_from(user)
+
+for user in users:
+    user["betweenness_centrality"] = 0.0
+
+for source in users:
+    source_id = source["id"]
+    for target_id, paths in source["shortest_paths"].items():
+        if source_id < target_id:       # don't double count
+            num_paths = len(paths)      # how many shortest paths?
+            contrib = 1 / num_paths     # contribution to centrality
+
+            for path in paths:
+                for id in path:
+                    if id not in [source_id, target_id]:
+                        users[id]["betweenness_centrality"] += contrib
+
+
+def farness(user):
+    """the sum of the lengths of the shortest paths to each other user"""
+    return sum(len(paths[0])
+               for paths in user["shortest_paths"].values())
+
+
+for user in users:
+    user["closeness_centrality"] = 1 / farness(user)
+
+"""Eigenvector Centrality"""
+
+
+def matrix_product_entry(A, B, i, j):
+    return dot(get_row(A, i), get_column(B, j))
+
+
+def matrix_multiply(A, B):
+    n1, k1 = shape(A)
+    n2, k2 = shape(B)
+
+    if k1 != n2:
+        raise ArithmeticError("incompatible shapes!")
+
+    return make_matrix(n1, k2, partial(matrix_product_entry, A, B))
+
+
+def vector_as_matrix(v):
+    """returns the vector v (represented as a list) as a n x 1 matrix"""
+    return [[v_i] for v_i in v]
+
+
+def vector_from_matrix(v_as_matrix):
+    """returns the n x 1 matrix as a list of values"""
+    return [row[0] for row in v_as_matrix]
+
+
+def matrix_operation(A, v):
+    v_as_matrix = vector_as_matrix(v)
+    product = matrix_multiply(A, v_as_matrix)
+    return vector_from_matrix(product)
+
+
+def find_eigenvector(A, tolerance=0.00001):
+    guess = [random.random() for _ in A]
+
+    while True:
+        result = matrix_operation(A, guess)
+        length = magnitude(result)
+        next_guess = scalar_multiply(1/length, result)
+
+        if distance(guess, next_guess) < tolerance:
+            return next_guess, length   # eigenvector, eigenvalue
+        guess = next_guess
+
+
+def entry_fn(i, j):
+    return 1 if (i, j) in friendships or (j, i) in friendships else 0
+
+
+n = len(users)
+adjacency_matrix = make_matrix(n, n, entry_fn)
+eigenvector_centralities, _ = find_eigenvector(adjacency_matrix)
+
+"""Directed Graphs and PageRank"""
+for user in users:
+    user["endorses"] = []       # add one list to track outgoing endorsements
+    user["endorsed_by"] = []    # and another to track endorsements
+
+for source_id, target_id in endorsements:
+    users[source_id]["endorses"].append(users[target_id])
+    users[target_id]["endorsed_by"].append(users[source_id])
+
+endorsements_by_id = [(user["id"], len(user["endorsed_by"]))
+                      for user in users]
+
+sorted(endorsements_by_id,
+       key=lambda pair: pair[1],
+       reverse=True)
+
+
+def page_rank(users, damping=0.85, num_iters=100):
+
+    # initially distribute PageRank evenly
+    num_users = len(users)
+    pr = {user["id"]: 1 / num_users for user in users}
+
+    # this is the small fraction of PageRank
+    # that each node gets each iteration
+    base_pr = (1 - damping) / num_users
+
+    for _ in range(num_iters):
+        next_pr = {user["id"]: base_pr for user in users}
+        for user in users:
+            # distribute PageRank to outgoing links
+            links_pr = pr[user["id"]] * damping
+            for endorsee in user["endorses"]:
+                next_pr[endorsee["id"]] += links_pr / len(user["endorses"])
+
+        pr = next_pr
+
+    return pr
\ No newline at end of file
diff --git a/prec_rec_curve.py b/prec_rec_curve.py
new file mode 100644
index 0000000..7bde221
--- /dev/null
+++ b/prec_rec_curve.py
@@ -0,0 +1,143 @@
+import numpy as np
+from sklearn.metrics import confusion_matrix, precision_score, recall_score
+import matplotlib.pyplot as plt
+import matplotlib.patches as ptch 
+
+# Appendix A - working with single threshold
+pred_scores = [0.7, 0.3, 0.5, 0.6, 0.55, 0.9, 0.4, 0.2, 0.4, 0.3]
+y_true = ["positive", "negative", "negative", "positive", "positive", "positive", "negative", "positive", "negative", "positive"]
+
+# To convert the scores into a class label, a threshold is used. 
+# When the score is equal to or above the threshold, the sample is classified as one class. 
+# Otherwise, it is classified as the other class. 
+# Suppose a sample is Positive if its score is above or equal to the threshold. Otherwise, it is Negative. 
+# The next block of code converts the scores into class labels with a threshold of 0.5.
+
+threshold = 0.5
+
+y_pred = ["positive" if score >= threshold else "negative" for score in pred_scores]
+print(y_pred)
+
+r = np.flip(confusion_matrix(y_true, y_pred))
+print("\n# Confusion Matrix (From Left to Right & Top to Bottom: \nTrue Positive, False Negative, \nFalse Positive, True Negative)")
+print(r)
+
+# Remember that the higher the precision, the more confident the model is when it classifies a sample as Positive.
+# Higher the recall, the more positive samples the model correctly classified as Positive.
+
+precision = precision_score(y_true=y_true, y_pred=y_pred, pos_label="positive")
+print("\n# Precision = 4/(4+1)")
+print(precision)
+
+recall = recall_score(y_true=y_true, y_pred=y_pred, pos_label="positive")
+print("\n# Recall = 4/(4+2)")
+print(recall)
+
+# Appendix B - working with multiple thresholds
+y_true = ["positive", "negative", "negative", "positive", "positive", "positive", "negative", "positive", "negative", "positive", "positive", "positive", "positive", "negative", "negative", "negative"]
+
+pred_scores = [0.7, 0.3, 0.5, 0.6, 0.55, 0.9, 0.4, 0.2, 0.4, 0.3, 0.7, 0.5, 0.8, 0.2, 0.3, 0.35]
+
+thresholds = np.arange(start=0.2, stop=0.7, step=0.05)
+
+# Due to the importance of both precision and recall, there is a precision-recall curve that shows 
+# the tradeoff between the precision and recall values for different thresholds. 
+# This curve helps to select the best threshold to maximize both metrics
+
+def precision_recall_curve(y_true, pred_scores, thresholds):
+    precisions = []
+    recalls = []
+    f1_scores = []
+    
+    for threshold in thresholds:
+        y_pred = ["positive" if score >= threshold else "negative" for score in pred_scores]
+
+        precision = precision_score(y_true=y_true, y_pred=y_pred, pos_label="positive")
+        recall = recall_score(y_true=y_true, y_pred=y_pred, pos_label="positive")
+        f1_score = (2 * precision * recall) / (precision + recall)
+        
+        precisions.append(precision)
+        recalls.append(recall)
+        f1_scores.append(f1_score)
+
+    return precisions, recalls, f1_scores
+
+precisions, recalls, f1_scores = precision_recall_curve(y_true=y_true, 
+                                             pred_scores=pred_scores,
+                                             thresholds=thresholds)
+
+print("\nRecall:: 	Precision 	:: F1-Score",)
+for p, r, f in zip(precisions, recalls, f1_scores):
+	print(round(r,4),"\t::\t",round(p,4),"\t::\t",round(f,4))
+
+# np.max() returns the max. value in the array
+# np.argmax() will return the index of the value found by np.max()
+
+print('Best F1-Score: ', np.max(f1_scores))
+idx_best_f1 = np.argmax(f1_scores)
+print('\nBest threshold: ', thresholds[idx_best_f1])
+print('Index of threshold: ', idx_best_f1)
+
+# Can disable comment to display the plot
+
+# plt.plot(recalls, precisions, linewidth=4, color="red")
+# plt.scatter(recalls[idx_best_f1], precisions[idx_best_f1], zorder=1, linewidth=6)
+# plt.xlabel("Recall", fontsize=12, fontweight='bold')
+# plt.ylabel("Precision", fontsize=12, fontweight='bold')
+# plt.title("Precision-Recall Curve", fontsize=15, fontweight="bold")
+# plt.show()
+
+# Appendix C - average precision (AP)
+precisions, recalls, f1_scores = precision_recall_curve(y_true=y_true, 
+                                             pred_scores=pred_scores, 
+                                             thresholds=thresholds)
+
+precisions.append(1)
+recalls.append(0)
+
+precisions = np.array(precisions)
+recalls = np.array(recalls)
+
+print('\nRecall ::',recalls)
+print('Precision ::',precisions)
+
+AP = np.sum((recalls[:-1] - recalls[1:]) * precisions[:-1])
+print("\nAP --", AP)
+
+# Appendix D - Intersection over Union
+
+# gt_box -- 	ground-truth bounding box
+# pred_box --	prediction bounding box 
+def intersection_over_union(gt_box, pred_box):
+
+    inter_box_top_left = [max(gt_box[0], pred_box[0]), max(gt_box[1], pred_box[1])]
+
+    print("\ninter_box_top_left:", inter_box_top_left)
+    print("gt_box:", gt_box)
+    print("pred_box:", pred_box)
+    inter_box_bottom_right = [min(gt_box[0]+gt_box[2], pred_box[0]+pred_box[2]), min(gt_box[1]+gt_box[3], pred_box[1]+pred_box[3])]
+    print("inter_box_bottom_right:", inter_box_bottom_right)
+
+    inter_box_w = inter_box_bottom_right[0] - inter_box_top_left[0]
+    print("inter_box_w:", inter_box_w)
+    inter_box_h = inter_box_bottom_right[1] - inter_box_top_left[1]
+    print("inter_box_h:", inter_box_h)
+
+    intersection = inter_box_w * inter_box_h
+    union = gt_box[2] * gt_box[3] + pred_box[2] * pred_box[3] - intersection
+    
+    iou = intersection / union
+
+    return iou, intersection, union
+
+gt_box1 = [320, 220, 680, 900]
+pred_box1 = [500, 320, 550, 700]
+
+gt_box2 = [645, 130, 310, 320]
+pred_box2 = [500, 60, 310, 320]
+
+iou1 = intersection_over_union(gt_box1, pred_box1)
+print("\nIOU1 ::", iou1)
+
+iou2 = intersection_over_union(gt_box2, pred_box2)
+print("\nIOU2 ::", iou2)
\ No newline at end of file
diff --git a/recommender_systems/__pycache__/data.cpython-35.pyc b/recommender_systems/__pycache__/data.cpython-35.pyc
new file mode 100644
index 0000000..06a51c2
Binary files /dev/null and b/recommender_systems/__pycache__/data.cpython-35.pyc differ
diff --git a/recommender_systems/__pycache__/utils.cpython-35.pyc b/recommender_systems/__pycache__/utils.cpython-35.pyc
new file mode 100644
index 0000000..4e957af
Binary files /dev/null and b/recommender_systems/__pycache__/utils.cpython-35.pyc differ
diff --git a/recommender_systems/data.py b/recommender_systems/data.py
new file mode 100644
index 0000000..aa3b9fb
--- /dev/null
+++ b/recommender_systems/data.py
@@ -0,0 +1,17 @@
+users_interests = [
+    ["Hadoop", "Big Data", "HBase", "Java", "Spark", "Storm", "Cassandra"],
+    ["NoSQL", "MongoDB", "Cassandra", "HBase", "Postgres"],
+    ["Python", "scikit-learn", "scipy", "numpy", "statsmodels", "pandas"],
+    ["R", "Python", "statistics", "regression", "probability"],
+    ["machine learning", "regression", "decision trees", "libsvm"],
+    ["Python", "R", "Java", "C++", "Haskell", "programming languages"],
+    ["statistics", "probability", "mathematics", "theory"],
+    ["machine learning", "scikit-learn", "Mahout", "neural networks"],
+    ["neural networks", "deep learning", "Big Data", "artificial intelligence"],
+    ["Hadoop", "Java", "MapReduce", "Big Data"],
+    ["statistics", "R", "statsmodels"],
+    ["C++", "deep learning", "artificial intelligence", "probability"],
+    ["pandas", "R", "Python"],
+    ["databases", "HBase", "Postgres", "MySQL", "MongoDB"],
+    ["libsvm", "regression", "support vector machines"]
+]
diff --git a/recommender_systems/model.py b/recommender_systems/model.py
new file mode 100644
index 0000000..7eb0028
--- /dev/null
+++ b/recommender_systems/model.py
@@ -0,0 +1,36 @@
+from functools import partial
+
+from recommender_systems.data import users_interests
+from recommender_systems.utils import make_user_interest_vector, cosine_similarity, most_similar_users_to, \
+    user_based_suggestions, most_similar_interests_to, item_based_suggestions
+
+if __name__ == '__main__':
+    unique_interests = sorted(list({interest
+                                    for user_interests in users_interests
+                                    for interest in user_interests}))
+
+    print("unique interests")
+    print(unique_interests)
+
+    user_interest_matrix = map(partial(make_user_interest_vector, unique_interests), users_interests)
+
+    user_similarities = [[cosine_similarity(interest_vector_i, interest_vector_j)
+                          for interest_vector_j in user_interest_matrix]
+                         for interest_vector_i in user_interest_matrix]
+
+    print(most_similar_users_to(user_similarities, 0))
+
+    print(user_based_suggestions(user_similarities, users_interests, 0))
+
+    # item-based
+    interest_user_matrix = [[user_interest_vector[j]
+                             for user_interest_vector in user_interest_matrix]
+                            for j, _ in enumerate(unique_interests)]
+
+    interest_similarities = [[cosine_similarity(user_vector_i, user_vector_j)
+                              for user_vector_j in interest_user_matrix]
+                             for user_vector_i in interest_user_matrix]
+
+    print(most_similar_interests_to(interest_similarities, 0, unique_interests))
+
+    print(item_based_suggestions(interest_similarities, users_interests, user_interest_matrix, unique_interests, 0))
\ No newline at end of file
diff --git a/recommender_systems/utils.py b/recommender_systems/utils.py
new file mode 100644
index 0000000..7157ad9
--- /dev/null
+++ b/recommender_systems/utils.py
@@ -0,0 +1,65 @@
+import math
+from collections import defaultdict
+
+from helpers.linear_algebra import dot
+
+
+def cosine_similarity(v, w):
+    return dot(v, w) / math.sqrt(dot(v, v) * dot(w, w))
+
+
+def make_user_interest_vector(interests, user_interests):
+    return [1 if interest in user_interests else 0
+            for interest in interests]
+
+
+def most_similar_users_to(user_similarities, user_id):
+    pairs = [(other_user_id, similarity)
+             for other_user_id, similarity in
+             enumerate(user_similarities[user_id])
+             if user_id != other_user_id and similarity > 0]
+
+    return sorted(pairs, key=lambda pair: pair[1], reverse=True)
+
+
+def most_similar_interests_to(interest_similarities, interest_id, unique_interests):
+    pairs = [(unique_interests[other_interest_id], similarity)
+             for other_interest_id, similarity in
+             enumerate(interest_similarities[interest_id])
+             if interest_id != other_interest_id and similarity > 0]
+
+    return sorted(pairs, key=lambda pair: pair[1], reverse=True)
+
+
+def user_based_suggestions(user_similarities, users_interests, user_id, include_current_interests=False):
+    suggestions = defaultdict(float)
+    for other_user_id, similarity in most_similar_users_to(user_similarities, user_id):
+        for interest in users_interests[other_user_id]:
+            suggestions[interest] += similarity
+
+    suggestions = sorted(suggestions.items(), key=lambda pair: pair[1], reverse=True)
+
+    if include_current_interests:
+        return suggestions
+    else:
+        return [(suggestion, weight)
+                for suggestion, weight in suggestions
+                if suggestion not in users_interests[user_id]]
+
+
+def item_based_suggestions(interest_similarities, users_interests, user_interest_matrix, unique_interests, user_id, include_current_interests=False):
+    suggestions = defaultdict(float)
+    for interest_id, is_interested in enumerate(user_interest_matrix[user_id]):
+        if is_interested == 1:
+            for interest, similarity in most_similar_interests_to(interest_similarities, interest_id, unique_interests):
+                suggestions[interest] += similarity
+
+    suggestions = sorted(suggestions.items(), key=lambda pair: pair[1], reverse=True)
+
+    if include_current_interests:
+        return suggestions
+    else:
+        return [(suggestion, weight)
+                for suggestion, weight in suggestions
+                if suggestion not in users_interests[user_id]]
+
diff --git a/requirements.txt b/requirements.txt
index d01840b..8d8c8e2 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,53 +1,44 @@
-Keras==2.2.4
-Keras-Application==1.0.6
+Keras==2.13.1
 Keras-Preprocessing==1.0.5
 PySocks==1.6.8
-PyYAML==3.13
-Pygments==2.3.1
+Pygments==2.15.0
 Quandl==3.4.5
 asn1crypto==0.24.0
 backcall==0.1.0
 beautifulsoup4==4.6.3
-certifi==2018.8.24
+certifi==2023.7.22
 cffi==1.11.5
 chardet==3.0.4
-cryptography==2.3.1
+cryptography==44.0.1
 cycler==0.10.0
-decorator==4.3.2
 h5py==2.9.0
-idna==2.7
+idna==3.7
 inflection==0.3.1
-ipython==7.2.0
-ipython-genutils==0.2.0
+ipython==8.10.0
 jedi==0.13.2
 kiwisolver==1.0.1
 matplotlib==3.0.0
-mkl-fft==1.0.6
-mkl-random==1.0.1
 more-itertools==5.0.0
-numpy==1.15.2
+numpy==1.22.0
 pandas==0.23.4
-parso==0.3.2
 patsy==0.5.0
 pexpect==4.6.0
 pickleshare==0.7.5
-pip==10.0.1
-prompt-toolkit==2.0.7	
+pip==23.3
 ptyprocess==0.6.0
 pyOpenSSL==18.0.0
 pycparser==2.19
 pyparsing==2.2.1
 python-dateutil==2.7.3
 pytz==2018.5
-requests==2.19.1
-scikit-learn==0.20.0
-scipy==1.1.0
+requests>=2.20.0
+scikit-learn==1.5.0
+scipy==1.10.0
 seaborn==0.9.0
-setuptools==40.2.0
+setuptools==70.0.0
 six==1.11.0
 statsmodels==0.9.0
-tornado==5.1.1
+tornado==6.4.2
 traitlets==4.3.2
-urllib3==1.23
 wcwidth==0.1.7
-wheel==0.31.1
+wheel==0.38.1
diff --git a/use_cases_insurnace.md b/use_cases_insurnace.md
new file mode 100644
index 0000000..49c3e89
--- /dev/null
+++ b/use_cases_insurnace.md
@@ -0,0 +1,47 @@
+#### Reference:- https://activewizards.com/blog/top-10-data-science-use-cases-in-insurance/
+
+## Other use cases
+
+### Lapse management: 
+##### Identifies policies that are likely to lapse, and how to approach the insured about maintaining the policy. Calculate the probability to lapse
+
+### Recommendation engine: 
+##### Given similar customers, discovers where individual insureds may have too much, or too little, insurance. Then, proactively help them get the right insurance for their current situation.
+
+### Assessor assistant: 
+##### Once a car has been towed to a body shop, use computer vision to help the assessor identify issues which need to be fixed. This helps accuracy, speeds an assessment, and keeps the customer informed with any repairs. Car damage detection
+
+### Property analysis: 
+##### Given images of a property, identifies structures on the property and any condition issues. Insurers can proactively help customers schedule repairs by identifying issues in their roofs, or suggest other coverage when new structures, like a swimming pool, are installed.
+
+### Fraud detection: 
+##### Identifies claims which are potentially fraudulent. Rare events problem. Class imbalance is a huge challenge here
+
+### Personalized offers: 
+##### Improves the customer experience by offering relevant information about the coverage the insured may need based on life events, such as the birth of a child, purchase of a home or car.
+
+### Claims processing
+##### Claims processing includes multiple tasks, including review, investigation, adjustment, remittance, or denial. While performing these tasks, numerous issues might occur:
+
+* Manual/inconsistent processing: Many claims processing tasks require human interaction that is prone to errors.
+* Varying data formats: Customers send data in different formats to make claims.
+* Changing regulation: Businesses need to accord in changing regulations promptly. Thus, constant staff training and process update are required for these companies.
+
+### Claims document processing
+As customers make claims when they are in an uncomfortable position, customer experience and speed are critical in these processes. Thanks to document capture technologies, businesses can rapidly handle large volumes of documents required for claims processing tasks, detect fraudulent claims, and check if claims fit regulations.
+
+### Application processing
+Application processing requires extracting information from a high volume of documents. While performing this task manually can take too long and prone to errors, document capture technologies enable insurance companies to automatically extract relevant data from application documents and accelerate insurance application processes with fewer errors and improved customer satisfaction.
+
+### Insurance pricing
+AI can assess customers’ risk profiles based on lab testing, biometric data, claims data, patient-generated health data, and identify the optimal prices to quote with the right insurance plan. This would decrease the workflow in business operations and reduce costs while improving customer satisfaction.
+
+### Document creation
+Insurance companies need to generate high volumes of documents, including specific information about the insurer. While creating these documents manually consume time and prone to errors, using AI and automation technologies can generate policy statements without mistakes. 
+
+### Responding to customer queries
+Conversational AI technologies can support insurance companies for faster replies to customer queries. For example, a South African insurance company, Hollard, has achieved 98% automation and reduced cost per transaction by 91%, according to its solution providers, LarcAI and UiPath.
+
+
+
+
