<jupyter_start><jupyter_text>Modeling with Scikit-Learn In this notebook, we're going to tie everything we've learned so far to do some modeling with scikit-learn. As we've seen, we have in the violation descriptions a large number of, relatively, free text fields.Working with text data is a particularly attractive use case for machine learning. It's also often a messy one that can involve working with a lot of boilerplate code. The library [scikit-learn](http://scikit-learn.org/stable/) provides many features for working with text data.First, let's take a closer look at scikit-learn.  Preliminaries: Scikit-LearnThe `scikit-learn` package provides a robust set of machine learning algorithms for Python. Like all of the packages, we have seen so far, scikit-learn is built upon the core Python scientific stack (i.e. NumPy, SciPy, Cython). One of the biggest reasons of why scikit-learn is so popular is that it has a simple, consistent API, making it useful for a wide range of statistical learning applications. The different components of scikit-learn can be combined to make powerful and expressive pipelines for analyzing data.Scikit-learn provides facilities for* **supervised learning** algorithms that learn from a training set with **labels**, or targets, to generalize to other inputs like **regression** and **classification**.* **unsupervised learning** algorithms that learn structure in the data from a training set of unlabeled examples like **clustering** or **density estimators*** **dimensionality reduction** algorithms which reduce the number of **features**, or columns, while preserving information about the data* **model selection** for choosing the best parameters and models* **preprocessing** for getting data ready to apply machine learning algorithms  Representing Data in Scikit-LearnMost machine learning algorithms implemented in scikit-learn expect data to be stored in a two-dimensional array or matrix. The arrays can be either **numpy** arrays, or in some cases **scipy.sparse** matrices. The size of the array is expected to be [n_samples, n_features]* **n_samples**: The number of samples: each sample is an item to process (e.g. classify). A sample can be a document, a picture, a sound, a video, a row in database or CSV file, or whatever you can describe with a fixed set of quantitative traits.* **n_features**: The number of features or distinct traits that can be used to describe each item in a quantitative manner. Features are generally real-valued, but may be boolean or discrete-valued in some cases.The number of features (almost always) must be fixed in advance. However it can be very high dimensional (e.g. millions of features) with most of them being zeros for a given sample. This is a case where `scipy.sparse` matrices and other techniques can be useful, in that they are much more memory-efficient than numpy arrays.  Aside: NumPy ArraysWe haven't talked much about NumPy arrays. NumPy arrays, however, are the fundamental data structure in Python data stack. A NumPy array is an object that represents a homogeneously typed, multidimensional array. The array provides an efficient (close to the hardware) data structure for scientific, or array-oriented, computing. First, let's look at the NumPy import convention.<jupyter_code>import numpy as np<jupyter_output><empty_output><jupyter_text>And create an array from a Python list. This is an array of all integers.<jupyter_code>x = np.array([1, 2, 3, 4, 5])<jupyter_output><empty_output><jupyter_text>We can't assign something that's not an integer.<jupyter_code>x[0] = 'a'<jupyter_output><empty_output><jupyter_text>We can perform indexing operations much like we saw with pandas earlier, but without the convenience of labels. You can use regular Python slicing syntax.<jupyter_code>x[:3]
x[::2]<jupyter_output><empty_output><jupyter_text>Or what's called **fancy indexing** by using Boolean or integer indexes.<jupyter_code>x[[True, False, True, False, True]]<jupyter_output><empty_output><jupyter_text>We can perform operations on NumPy arrays like `sum`.<jupyter_code>np.array([1, 2, 3, 4, 5]).sum()<jupyter_output><empty_output><jupyter_text>And we can perform linear algebra operations, like taking the dot product.<jupyter_code>x = np.array([[1, 2, 3], 
              [4, 5, 6],
              [7, 8, 9]])

x
y = np.array([[4], [5], [6]])

y
x.dot(y)<jupyter_output><empty_output><jupyter_text>Array Creation Both the `zeros` and `ones` functions can be useful for creating data.<jupyter_code>np.zeros(5, dtype=float)
np.zeros(5, dtype=int)
np.zeros(5, dtype=complex)
np.ones(5, dtype=float)<jupyter_output><empty_output><jupyter_text>Two other handy functions to know about are `arange`, `linspace`, and `logspace`.`np.arange` creates an array of a range of integers.<jupyter_code>np.arange(10)<jupyter_output><empty_output><jupyter_text>`np.linspace` and `np.logspace` create linearly and logarithmically-spaced grids, respectively, with a fixed number of points.<jupyter_code>np.linspace(0, 1, num=4)
np.logspace(-1, 3, num=5)<jupyter_output><empty_output><jupyter_text>ExerciseCreate an array with 1000 numbers, 0 to 999, using `arange`. Have a look at the `reshape` method on arrays to turn this into an array with 10 columns.<jupyter_code># Type your solution here
a1 = np.arange(1000)
a2 = a1.reshape(100,10)
a2
# %load solutions/numpy_reshape.py
import numpy as np


np.arange(1000).reshape(-1, 10)<jupyter_output><empty_output><jupyter_text>Finally, it is often useful to create arrays with random numbers that follow a specific distribution. The np.random module contains a number of functions that can be used to this effect, for example this will produce an array of 5 random samples taken from a standard normal distribution (0 mean and variance 1) $ X \sim N(0, 1) $:$$f(x \mid \mu = 0, \sigma=1) = \sqrt{\frac{1}{2\pi\sigma^2}}\exp {-\frac{x^2}{2\sigma^2} }$$<jupyter_code>np.random.randn(5)<jupyter_output><empty_output><jupyter_text>$X \sim N(9, 3)$<jupyter_code>norm10 = np.random.normal(loc=9, scale=3, size=10)
norm10
np.random.normal(loc = 0, scale = 1, size = 5)<jupyter_output><empty_output><jupyter_text>Exercise Generate a NumPy array of 1000 random numbers sampled from a Poisson distribution, with parameter `lam=5`. What is the modal value in the sample? You maybe interested in using `np.bincounts`.<jupyter_code># Type your solution here
p1 = np.random.poisson(lam = 5, size = 10)
print(p1)
np.bincount(p1).argmax()
# %load solutions/random_poisson.py
import numpy as np

y = np.random.poisson(lam=5, size=1000)
bins = np.bincount(y)

print(bins.argmax())<jupyter_output>5<jupyter_text>NumPy and the SciPy libraries also provide much more than data structures like more facilities for linear algebra, matrix decompositions, optimization, clustering, polynomials, unit testing, etc.  Scikit-Learn Quickstart Let's take a quick look at scikit-learn to fix ideas before going much further. We'll have a look at the canonical iris dataset, which consists of a set of measurements for flowers, each being a member of one of three species: Iris Setosa, Iris Versicolor or Iris Virginica.<jupyter_code>from sklearn.datasets import load_iris
dta = load_iris()<jupyter_output><empty_output><jupyter_text>The features of the data consists of<jupyter_code>dta.feature_names<jupyter_output><empty_output><jupyter_text>The labels consist of<jupyter_code>dta.target_names
dta.data[:10]
dta.target[::10]<jupyter_output><empty_output><jupyter_text>Let's fit a logistic regression model on all of the iris data.<jupyter_code>from sklearn.linear_model import LogisticRegression
model = LogisticRegression()
model.fit(dta.data, dta.target)
model.predict(dta.data)<jupyter_output><empty_output><jupyter_text>`scikit-learn` interfaceThe power of scikit-learn comes from the fact that they share a common, unified API, consisting of three complementary interfaces:* **estimator** interface for building and ﬁtting models* **predictor** interface for making predictions* **transformer** interface for converting data.The estimator interface is at the core of the library. It deﬁnes instantiation mechanisms of objects and exposes a fit method for learning a model from training data. All supervised and unsupervised learning algorithms (*e.g.*, for classiﬁcation, regression or clustering) are oﬀered as objects implementing this interface. Machine learning tasks like feature extraction, feature selection or dimensionality reduction are also provided as estimators.Scikit-learn strives to have a uniform interface across all methods. For example, a typical **estimator** follows this template: ```pythonclass Estimator:      def fit(self, X, y=None):        """Fit model to data X (and y)"""        self.some_attribute_ = self.some_fitting_method(X, y)        return self                def predict(self, X_test):        """Make prediction based on passed features"""        pred = self.make_prediction(X_test)        return pred``` For a given scikit-learn estimator object named model, several methods are available. Irrespective of the type of estimator, there will be a fit method:* model.fit : fit training data. For supervised learning applications, this accepts two arguments: the data X and the labels y (e.g. `model.fit(X, y)`). For unsupervised learning applications, this accepts only a single argument, the data X (e.g. `model.fit(X)`).> During the fitting process, the **state** of the **estimator** is stored in attributes of the estimator instance named with a trailing underscore character (`_`). The **predictor** interface extends the notion of an estimator by adding a predict method that takes an array `X_test` and produces predictions based on the learned parameters of the estimator. In the case of supervised learning estimators, this method typically returns the predicted labels or values computed by the model. Some unsupervised learning estimators may also implement the predict interface, such as k-means, where the predicted values are the cluster labels. all **supervised estimators** are expected to have the following methods:* `model.predict` : given a trained model, predict the label of a new set of data. This method accepts one argument, the new data X_new (e.g. `model.predict(X_new)`), and returns the learned label for each object in the array.* `model.predict_proba` : For classification problems, some estimators also provide this method, which returns the probability that a new observation has each categorical label. In this case, the label with the highest probability is returned by `model.predict()`.* `model.score` : for classification or regression problems, most (all?) estimators implement a score method. Scores are between 0 and 1, with a larger score indicating a better fit. Since it is common to modify or ﬁlter data before feeding it to a learning algorithm, some estimators in the library implement a **transformer** interface which deﬁnes a `transform` method. It takes as input some new data `X_test` and yields as output a transformed version. Preprocessing, feature selection, feature extraction and dimensionality reduction algorithms are all provided as transformers within the library. **unsupervised estimators** will always have these methods:* `model.transform` : given an unsupervised model, transform new data into the new basis. This also accepts one argument  `X_new`, and returns the new representation of the data based on the unsupervised model.* `model.fit_transform` : some estimators implement this method, which more efficiently performs a fit and a transform on the same input data. Let's take a look at some examples of each of these using the Chicago Health Inspection data.<jupyter_code>from load_data import dta
dta = dta.drop(["violations"], axis='columns').join(
    dta.violations.str.split("|", expand=True)
    .unstack()
    .dropna()
    .str.strip()
    .reset_index(level=0, drop=True)
    .to_frame()
    .rename(columns={0: 'violations'}),
    how='right'
)
dta.info()<jupyter_output><class 'pandas.core.frame.DataFrame'>
Int64Index: 117723 entries, 44249 to 2102275
Data columns (total 15 columns):
address            117723 non-null object
aka_name           116415 non-null object
city               117670 non-null object
dba_name           117723 non-null object
facility_type      117638 non-null category
inspection_date    117723 non-null datetime64[ns]
inspection_type    117723 non-null category
latitude           117356 non-null float64
license_           117714 non-null float64
longitude          117356 non-null float64
results            117723 non-null category
risk               117721 non-null category
state              117703 non-null object
zip                117694 non-null object
violations         117723 non-null object
dtypes: category(4), datetime64[ns](1), float64(3), object(7)
memory usage: 11.4+ MB<jupyter_text>Bag-of-WordsFirst, we need to take our text and turn it in to numerical features. A common assumption for doing machine learning on text is what's known as the bag of words assumption. This means that we assume that the order of the words as they occur in a document doesn't matter to discern the general meaning of the document. This is commonly done in the following steps1. Build what's called a **vocabulary**, which is a mapping from integers to possible words, $w$, in your corpus, or collection of documents.2. Using this vocabulary, assign a number to the count of each word occuring in any document.What you're left with is a matrix $X$, where each value $X[i,j]$ is the count of word $j$ in document $i$.$X$ is a matrix of dimension `n_documents` by `n_vocabulary`. This is large. Luckily, most words don't occur in every document. If they did, we would not be able to separate the documents according to topics.For this reason, bag of words documents are often high-dimensional, sparse datasets. We don't need to keep the zeros in memory.  Tokenize Ok, so how do we do this? Text is often really messy, has punctuation, and has a bunch of words that every text has to have but don't necessarily connote topical meaning. These words are called stop words such as "the," "a," or "an."We turn human writing into a set of feature vectors by taking care of these issues. This process is called tokenization.scikit-learn provides some nice facilities for building a dictionary of features and transforming documents to feature vectors. The first of these that we will look at is the **CountVectorizer** transformer.Recall from above that a transformer is an estimator that provides a transform method.<jupyter_code>from sklearn.feature_extraction.text import CountVectorizer<jupyter_output><empty_output><jupyter_text>In scikit-learn, all of the estimators take their options when you instantiate the estimator. Here, we say that we want to remove stop-words using a list of common english language words that we won't need.<jupyter_code>count_vectorizer = CountVectorizer(stop_words='english')<jupyter_output><empty_output><jupyter_text>Then we need to *fit* the transformer on the data. Calling fit always returns the object itself. We'll see why later.Calling a `fit` method on an estimator actually does the learning. Any learned parameters are now attached to the estimator with an underscore (`_`) appended.<jupyter_code>count_vectorizer.fit(dta.violations)<jupyter_output><empty_output><jupyter_text>In the case of `CountVectorizer` there is a dictionary called `vocabulary_` which stores a mapping from the known vocabulary to the column in the sparse matrix which contains the counts for that word.<jupyter_code>type(count_vectorizer.vocabulary_)
len(count_vectorizer.vocabulary_)<jupyter_output><empty_output><jupyter_text>Finally, we need to transform our original data using transform.<jupyter_code>count_matrix = count_vectorizer.transform(dta.violations)<jupyter_output><empty_output><jupyter_text>Count matrix is a **sparse matrix**, provided by the SciPy library. The number of samples is equal to the number of violations that we have in the data. The number of columns is the cardinality of our vocabulary. The entries are the counts of each word in the document.<jupyter_code>count_matrix<jupyter_output><empty_output><jupyter_text>Sparse matrices behave a lot like plain numpy arrays. For example, we can ask for the sum of each word over all the documents.<jupyter_code>count_matrix.sum(0)<jupyter_output><empty_output><jupyter_text>ExerciseWhat is the most frequent word in this vocabulary? Explore the `count_vectorizer` method to see if it offers anything helpful in uncovering this.<jupyter_code># Type your solution here
%load solutions/frequent_word.py<jupyter_output><empty_output><jupyter_text>Tf-Idf Looking at the most common word we already see one issue with using raw counts. Another issue is that longer documents will have higher counts of words. Commonly, we use a technique known as **term frequency - inverse document frequency**, or **tf-idf**, instead of counts to do analysis on text data, which mitigates these issues.The *term frequency* is a measure of the frequency of a word in a document. Term frequency in document $i$ for word $j$ is$$tf_{ij}=\frac{w_{ij}}{\sum_jw_{ij}}$$You might go about computing this.<jupyter_code>from sklearn.preprocessing import normalize

tf = normalize(count_matrix, norm='l1', axis=1)<jupyter_output><empty_output><jupyter_text>Using the `l1`-norm across axis 1 (the column) index, we now have frequencies within the document. Each document now sums to 1.One thing to point out here is that summations over a `scipy.sparse` matrix returns a numpy `matrix`. This is mostly for historical reasons, and I *don't* recommend working with the `matrix` data structure if you can avoid it. You can turn this into an array by accessing the matrix's `A` attribute.<jupyter_code>tf.sum(1)
tf.sum(1).A<jupyter_output><empty_output><jupyter_text>Another important concept is that of inverse document frequency. This is a measure of how important a word is. Words like stop words or words that are otherwise popular in a corpus will still have a high term frequency. Inverse document frequency is a way to downweight the frequent terms but upweight the rare ones. The inverse document frequency is$$idf = \log\left(\frac{N_{\text{documents}}}{N_{\text{documents with term}}}\right)$$or$$idf = \log\left(\frac{N_{\text{documents}}}{1 + N_{\text{documents with term}}}\right)$$in case your vocabulary is a superset of the words in your documents.So tf-idf is$$\text{tf-idf} = tf \times idf$$Scikit-learn actually uses a slightly different definition.Of course, scikit-learn provides a transformer for tf-idf<jupyter_code>from sklearn.feature_extraction.text import TfidfVectorizer<jupyter_output><empty_output><jupyter_text>Let's prepare our TfidfVectorizer. We'll remove stop-words, remove any words that don't occur in at least 50 documents and remove words that occur in 85% or more documents.Finally, we'll use a **regular expression** pattern to determine what exactly a token (or word) is. In this case, we deviate from the scikit-learn default by not allowing numbers to be words.<jupyter_code>tfidf_vect = TfidfVectorizer(
    stop_words='english', 
    min_df=50,
    max_df=.85, 
    token_pattern=r"(?u)\b[A-Za-z_][A-Za-z_]+\b"
)<jupyter_output><empty_output><jupyter_text>Notice here that we can combine the fitting and the transformation by taking advantage of the `fit_transform` method.<jupyter_code>X = tfidf_vect.fit_transform(dta.violations)
X<jupyter_output><empty_output><jupyter_text>Using these more restrictive criteria above, we've greatly reduced the dimensionality of the feature space, while ideally preserving the most useful information on the contents of the documents.  Dimensionality Reduction Let's take a look at another kind of transformer in scikit-learn, one that provides dimensionality reduction. Here we'll use Truncated SVD on the tf-idf matrix. Formally, this is known as Latent Semantic Analysis (LSA), because it transforms the documents to a low-dimensional "semantic" space. Truncated SVD is a lot like Principle Components Analysis (PCA), except that the decomposition is on the documents rather than the covariance matrix. Mathematically, truncated SVD applied to training samples X produces a low-rank approximation $X_k$:$$X \approx X = U_k \Sigma_k V_k^\top$$After this operation, $U_k \Sigma_k^\top$ is the transformed training set with k features (called `n_components` in the API).To also transform a test set $X$, we multiply it with $V_k$:$$X' = X V_k$$If we were to center the matrix $X$ then TruncatedSVD would be equivalent to PCA. Not doing so allows us to continue to work with sparse matrices as documents almost always produce. `TruncatedSVD` is available under the `decomposition` namespace.<jupyter_code>from sklearn.decomposition import TruncatedSVD<jupyter_output><empty_output><jupyter_text>Here we'll fit the `TruncatedSVD` transformer using 10 components. This is an arbitrary choice. In practice, you may want to tune the number of components using whatever metric is appropriate for your task. Note that we use `random_state` here to make sure that our results are repeatable. Any of the algorithms in scikit-learn that are non-deterministic will provide a `random_state` keyword. It is really important that you use it to ensure **repeatable** results.<jupyter_code>n_components = 10

svd = TruncatedSVD(
    n_components=n_components, 
    random_state=0
)<jupyter_output><empty_output><jupyter_text>We'll use `fit_transform` to perform the singular value decomposition (up to the first $k$ components) and to project the original matrix into the reduced space.<jupyter_code>X_reduced = svd.fit_transform(X)
X_reduced
X_reduced.shape<jupyter_output><empty_output><jupyter_text>ExerciseWrite a loop that prints the top ~6 words for each component according to the the magnitude of their loadings (i.e., the absolute value of the `components_` attribute). Note, you can extract the words from the vocabulary dictionary like this.<jupyter_code>words = np.array(sorted(tfidf_vect.vocabulary_.keys()))
words[:15]
# Type your solution here
%load solutions/top_words_loadings.py<jupyter_output><empty_output><jupyter_text>Clustering Now that we've done some dimensionality reduction, we may be interested in clustering the documents in this reduced space. Scikit-Learn has a number of clustering algorithms. Here we'll use K-means.The K-means algorithm clusters data by separating it into groups of equal variance, choosing them in order to minimize the within-cluster sum of squares. Formally, we divide $n$ samples into $k$ clusters $C$. Each cluster is defined by its mean, or centroid, $u_i$.$$\sum_{i=0}^n\underset{u_j\in C}\min (\|x_j - u_i\|^2)$$K-means proceeds as follows:1. We pick $k$ random points from the dataset and call them the cluster centroids2. We assign each data point to its closest centroid.3. We recompute the centroids.4. The distance between the old and new centroids are computed until they stop moving.Eventually k-means will converge. However, there is no guarantee that it will converge to a global optimum. One thing we can do to mitigate this is to pick better starting points than $k$ random points in the data. Scikit-learn uses a better choice by default through the `init='k-means++'` argument, which attempts to pick starting centroids that are generally 'far' from each other.First, let's normalize the data row-wise. If we do this, for documents the euclidean distance above becomes cosine similarity. Now we're performing spherical k-means so that all of our comparisons between documents are equal and indpendent of the size of the document.<jupyter_code>from sklearn.preprocessing import Normalizer

normalizer = Normalizer(copy=True)
X_norm = normalizer.fit_transform(X_reduced)
np.linalg.norm(X_norm, axis=1)<jupyter_output><empty_output><jupyter_text>Here, we have some guidance on the number of clusters, we might expect, so let's use it.Recall that we found 45 distinct violation numbers in the data.<jupyter_code>n_clusters = (dta.violations.str.extract("(\d+)(?=\.)", expand=False)
              .astype(int).unique().shape[0])
n_clusters
from sklearn.cluster import KMeans

kmeans = KMeans(n_clusters=n_clusters, random_state=0)

kmeans.fit(X_norm)<jupyter_output><empty_output><jupyter_text>ExercisePlot the histogram of the number of found clusters vs. our known violations. They won't line up exactly. I.e., the bins found for clustering will be different than the extracted violation numbers.What are the first five violations in, say, the first three clusters?<jupyter_code># Type your solution here
# %load solutions/plot_clusters.py
import matplotlib.pyplot as plt

fig, axes = plt.subplots(nrows=2, figsize=(12, 10))

axes[0].hist(kmeans.labels_, bins=n_clusters)
axes[1].hist((dta.violations.str.extract("(\d+)(?=\.)", expand=False)
              .astype(int)
              .rank(method='dense')), bins=n_clusters);

# dta.violations[kmeans.labels_ == 0].iloc[:5].values
# dta.violations[kmeans.labels_ == 1].iloc[:5].values
# dta.violations[kmeans.labels_ == 2].iloc[:5].values<jupyter_output><empty_output><jupyter_text>ExerciseSee if you can strip out the comments and still find (semi-)meaningful clusters. Here's a hint, you'll want to again use regular expressions and the `str` accessor in pandas. Here, we can use a *lookbehind* (`(?<=)`) to capture via `()` one or more (`+`) of any character (`.`) that follows the word "Comments."<jupyter_code>import re

result = re.search("(?<=Comments:)(.+)", 
                   "1. This is a violation. Comments: This was a really egregious violation.")

result
result.group()
# Type your solution here
%load solutions/clustering_comments.py<jupyter_output><empty_output><jupyter_text>Modeling Can we predict a pass/fail rating from features in the data? First, let's start to build our modeling set by turning the data back into inspection level data.<jupyter_code>dta.columns<jupyter_output><empty_output><jupyter_text>For the sake of clarity, let's drop all inspection results that weren't Pass or Fail. We can use another DataFrame method `isin` to do this.<jupyter_code>dta.results.unique()
dta = dta.loc[dta.results.isin(['Pass', 'Fail'])]<jupyter_output><empty_output><jupyter_text>Now let's pull out the columns that we think may be helpful in predicting.<jupyter_code>columns = [
    "inspection_id",
    "inspection_date", 
    "inspection_type", 
    "facility_type", 
    "results", 
    "risk",
    "zip"
]

modeling_dta = (
    dta.reset_index()
    .drop_duplicates(["inspection_id"])
    .loc[:, columns]
)
modeling_dta.head()<jupyter_output><empty_output><jupyter_text>Let's do some **feature engineering** to turn our data into some more features we think may be predictive of passing or failing scores. We might use the number of violations per establishment during an inspection.<jupyter_code>number_of_violations = dta.groupby(dta.index).size()<jupyter_output><empty_output><jupyter_text>Maybe some violation severity is weather dependent (on average).<jupyter_code>modeling_dta["month"] = modeling_dta.inspection_date.dt.month.astype(str)
import pandas as pd<jupyter_output><empty_output><jupyter_text>Finally, let's add in the number of previously failed inspections. We'll ignore the fact that we may care more about the rate of failed inspections or the full history of failures.<jupyter_code>key = (
    "address", 
    "dba_name", 
)


def failed_last(df):
    # return a Series to name the result and preserve the index
    # it's important to use `values` here or pandas will try to
    # align the indices you provide vs. what's in the series
    return pd.Series((df.shift(1).results == 'Fail').cumsum().values, 
                     name='num_fails',
                     index=df.inspection_id)


fail_num = (dta
            .reset_index()
            .drop_duplicates(['inspection_id'])
            .sort_values(['address', 'dba_name', 'inspection_date'])
            .groupby(key)
            .apply(failed_last)
            .reset_index(level=[0, 1], drop=True)
            .reset_index())

fail_num.head()
modeling_dta = modeling_dta.merge(fail_num)<jupyter_output><empty_output><jupyter_text>I always like to check shapes after merging to make sure I got the results I expected. By the way, there is a fantastic library [engarde](https://github.com/TomAugspurger/engarde) for doing direct defensive/assertive programming. It allows you to write functions like```python@is_shape((1290, 10))@unique_indexdef make_design_matrix('data.csv'):    out = ...    return out```<jupyter_code>modeling_dta.shape
modeling_dta.head()<jupyter_output><empty_output><jupyter_text>Let's join the violations data back together and run truncated SVD on the tf-idf matrix to reduce the dimensionality.<jupyter_code>violations = dta.violations.groupby(dta.index).apply(lambda df: " ".join(df))<jupyter_output><empty_output><jupyter_text>Again, let's be defensive here and make sure our violations data is sorted the same as our modeling data.<jupyter_code>violations = violations.loc[modeling_dta.set_index('inspection_id').index]
X = tfidf_vect.fit_transform(violations)

svd = TruncatedSVD(n_components=10, random_state=0)

X_reduced = svd.fit_transform(X)<jupyter_output><empty_output><jupyter_text>The last thing we'll need to do is to turn all of these non-numeric features into numeric features.<jupyter_code>modeling_dta.head()
dummied = pd.get_dummies(
    modeling_dta[['inspection_type', 'facility_type', 'risk', 'zip', 'month', 'num_fails']]
)
dummied.head()<jupyter_output><empty_output><jupyter_text>*Technically* you probably don't want to do it this way. We'll discuss why below. We can now join these values with the LSA results above.<jupyter_code>X = np.column_stack((dummied, X_reduced))<jupyter_output><empty_output><jupyter_text>Let's pull out the field that we want to try to predict, `results`. We'll use another scikit-learn transformer, `LabelEncoder` to do prepare the Pass/Fail column for modeling.The `LabelEncoder` learns and transforms any labels to values from `0` to `n_classes - 1`<jupyter_code>from sklearn.preprocessing import LabelEncoder
encoder = LabelEncoder()

y = encoder.fit_transform(modeling_dta.results)
y<jupyter_output><empty_output><jupyter_text>Since the `LabelEncoder` preserves lexicographical ordering, Pass is now 1. When we're doing predictive modeling, the main thing we're concerned with is 'does my model generalize.' Will it accurately predict on data that the model hasn't seen during training.To answer this question, we might split our dataset into a sample to train on and a separate sample to test on. Let's again use scikit-learn to prepare our train and test sets.<jupyter_code>from sklearn.model_selection import train_test_split

(X_train, X_test, 
 y_train, y_test) = train_test_split(X, y, test_size=.25, random_state=1)<jupyter_output><empty_output><jupyter_text>Let's check that our classes were relatively evenly distributed across our train and test splits. There are better ways to ensure this with scikit-learn, but we will just do a quick spot check.<jupyter_code>y_train.mean()
y_test.mean()<jupyter_output><empty_output><jupyter_text>Aside: Decision TreesA decision tree is a model that recursively partitions a dataset in such a way as to lead to the largest information gain. For each partition, a simple model is fit, say, a constant. Decision trees are good at capturing non-linearities and feature interactions by design.Here we'll look at the `DecisionTreeClassifier` model estimator from scikit-learn.<jupyter_code>from sklearn.tree import DecisionTreeClassifier, export_graphviz

dtree = DecisionTreeClassifier(random_state=0, max_depth=4)

dtree.fit(X_train, y_train)
columns = dummied.columns.tolist()
n_components = X_reduced.shape[1]

columns += ['x{}'.format(i) for i in range(n_components)]
columns<jupyter_output><empty_output><jupyter_text>**Note**: If you can't run this part, don't worry. You'll need to have graphviz installed. (`conda install graphviz` *should* do the trick.)<jupyter_code>export_graphviz(dtree, out_file='tree.dot', feature_names=columns)
!dot -Tpng tree.dot -o tree.png
from IPython.display import Image
Image('tree.png')<jupyter_output><empty_output><jupyter_text>How well does our decision tree do out of sample? We can use `predict_proba` method available on most classifiers to get our predicted probabilities for each sample for each class.<jupyter_code>proba = dtree.predict_proba(X_test)
proba<jupyter_output><empty_output><jupyter_text>We might check the area under the receiver operating characteristic curve to see how well our shallow tree does. Intuitively, this measure tells you if you were to grab a sample at random, how likely is your classifier to predict the correct label. Scikit-learn provides a number of useful metrics under the `sklearn.metrics` namespace.<jupyter_code>from sklearn.metrics import roc_auc_score

roc_auc_score(y_test, proba[:, 1])<jupyter_output><empty_output><jupyter_text>Now we'll see if we can do slightly better using a RandomForest a model. The RandomForest model is, roughly, an ensemble of decision trees. We will fit 500 full-depth trees, selecting features and rows at random, and then average the votes from each tree to get our classification results.<jupyter_code>from sklearn.ensemble import RandomForestClassifier
clf = RandomForestClassifier(n_estimators=500, n_jobs=4)

clf
clf.fit(X_train, y_train)<jupyter_output><empty_output><jupyter_text>All of the model estimators have a `score` method, which is by default the average accuracy.<jupyter_code>clf.score(X_test, y_test)<jupyter_output><empty_output><jupyter_text>Of course, we have a roughly 1:3 class imbalance here, so we may also want to check the AUC score and have a look at the confusion matrix.In the confusion matrix the true labels are the rows, and the predicted labels are the columns. Here we see that we're slightly high on our false positive rate. Inspections that were a Fail are being predicted as Pass.<jupyter_code>from sklearn.metrics import confusion_matrix
confusion_matrix(y_test, clf.predict(X_test))
roc_auc_score(y_test, clf.predict_proba(X_test)[:, 1])<jupyter_output><empty_output><jupyter_text>Let's make `columns` into a numpy array, so we can use fancy-indexing.<jupyter_code>columns = np.array(columns)<jupyter_output><empty_output><jupyter_text>Using the `feature_importances_` attribute of the trained Random Forest, we can get a sense of what features are important. Roughly speaking this is a measure across the decision trees of how influential a feature was in improving predictions.<jupyter_code>clf.feature_importances_<jupyter_output><empty_output><jupyter_text>Let's rank the features in descending order and see which ones are the most important.<jupyter_code>idx = clf.feature_importances_.argsort()[::-1]

list(zip(clf.feature_importances_[idx][:35], columns[idx][:35]))<jupyter_output><empty_output><jupyter_text>Putting It All Together Consider all of the steps that we've been through so far. We've* loaded our data* cleaned our data* extracted and engineered features* fit a model* evaluated this model, and* iterated on these stepsScikit-learn is designed from the ground up to make these steps easy for users with minimal boilerplate. Recall the main scikit-learn objects -- the `Transformer`, `Predictor`, and `Estimator`.The scikit-learn `Pipeline` abstraction builds on these interfaces to allow us to put together a chain of transformers and estimators and use the pipeline, as if it were an estimator itself.  Transformer InterfaceRecall the transformer interface. A transformer is intended to filter or modify the data in a supervised or unsupervised way.```pythonnew_data = obj.transform(data)```The interface is```pythonclass Transformer:    def fit(self, X, y=None):        """"""        return self    def transform(self, X, y=None):        return X    def fit_transform(self, X, y=None):        self.fit(X, y)        return self.transform(X, y)```  Estimator InterfaceRecall the estimator and predictor interfaces.```pythonclass Estimator:    def fit(self, X, y=None):        """Fit model to data X (and y)"""        self.some_attribute = self.some_fitting_method(X, y)        return self    def predict(self, X_test):        """Make prediction based on passed features"""        pred = self.make_prediction(X_test)        return pred```  PipelinesPutting it together with a Pipeline```pythonfrom sklearn.pipeline import Pipelineestimator = Pipeline(steps=[    ('transformer1', Transformer(*args1)),    ('transformer2', Transformer(*args2)),    ('estimator', Estimator(*args))])estimator.fit(X_train, y_train)y_fitted = estimator.predict(X_test)```By chaining together transformer estimators, our code is much easier to deal with than it would have been otherwise.Under the hood, this calls fit on the first transformer, then transform on X and passes the transformed X to the next transformer until the final estimator. At the final estimator, the pipeline simply calls fit on the previously transformed X and y.Each of these estimators become a `named_step` on the Pipeline object. Available to be inspected```pythonestimator.named_steps['transformer2']```  ExerciseTake a bit and look back at what we've done in this notebook. Make a scikit-learn `Pipeline` object that uses only the comments data. Transform the comment data to tf-idf, perform TruncatedSVD, and fit a RandomForest model. What happens to the ROC-AUC when you change the number of components in the TruncatedSVD to 2? To 20? Your final classifier step should look like```pythonestimator.fit(violations_train, y_train)```We start here, because you're often going to need to either write your own Transformers to deal with pandas DataFrames, or you might be interested in exploring the [sklearn-pandas](https://github.com/scikit-learn-contrib/sklearn-pandas) library.You may also be interested in exploring the [FeatureUnion](http://scikit-learn.org/stable/modules/generated/sklearn.pipeline.FeatureUnion.html) transformer to write your own transformer for both the text and non-text data in the same pipeline.<jupyter_code># Your solution here
%load solutions/sklearn_pipeline.py<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.08155593264586869, 'fraction_numerical': 0.009058869599269026, 'mean_word_length': 5.35257048092869, 'pattern_counts': {'":': 0, '<': 262, '<?xml version=': 0, '>': 261, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '65', 'dir': 'jupyter-structured-clean-dedup', 'id': '193264', 'license': 'MIT', 'n_tokens_mistral': 11338, 'n_tokens_neox': 10672, 'n_words': 4499, 'path': '5 - Modeling with scikit-learn.ipynb', 'repo_name': 'nguyenthlam/pycon-ds', 'size': '38305'}