<jupyter_start><jupyter_text>Kaggle's Predicting Red Hat Business ValueThis is a first quick & dirty attempt at Kaggle's [Predicting Red Hat Business Value](https://www.kaggle.com/c/predicting-red-hat-business-value) competition. Loading in the data<jupyter_code>import pandas as pd

people = pd.read_csv('people.csv.zip')
people.head(3)
actions = pd.read_csv('act_train.csv.zip')
actions.head(3)<jupyter_output><empty_output><jupyter_text>Joining together to get dataset<jupyter_code>training_data_full = pd.merge(actions, people, how='inner', on='people_id', suffixes=['_action', '_person'], sort=False)
training_data_full.head(5)
(actions.shape, people.shape, training_data_full.shape)<jupyter_output><empty_output><jupyter_text>Building a preprocessing pipeline<jupyter_code># %load "preprocessing_transforms.py"
from sklearn.base import TransformerMixin, BaseEstimator
import pandas as pd


class BaseTransformer(BaseEstimator, TransformerMixin):
    def fit(self, X, y=None, **fit_params):
        return self

    def transform(self, X, **transform_params):
        return self


class ColumnSelector(BaseTransformer):
    """Selects columns from Pandas Dataframe"""

    def __init__(self, columns, c_type=None):
        self.columns = columns
        self.c_type = c_type

    def transform(self, X, **transform_params):
        cs = X[self.columns]
        if self.c_type is None:
            return cs
        else:
            return cs.astype(self.c_type)


class SpreadBinary(BaseTransformer):

    def transform(self, X, **transform_params):
        return X.applymap(lambda x: 1 if x == 1 else -1)


class DfTransformerAdapter(BaseTransformer):
    """Adapts a scikit-learn Transformer to return a pandas DataFrame"""

    def __init__(self, transformer):
        self.transformer = transformer

    def fit(self, X, y=None, **fit_params):
        self.transformer.fit(X, y=y, **fit_params)
        return self

    def transform(self, X, **transform_params):
        raw_result = self.transformer.transform(X, **transform_params)
        return pd.DataFrame(raw_result, columns=X.columns, index=X.index)


class DfOneHot(BaseTransformer):
    """
    Wraps helper method `get_dummies` making sure all columns get one-hot encoded.
    """
    def __init__(self):
        self.dummy_columns = []

    def fit(self, X, y=None, **fit_params):
        self.dummy_columns = pd.get_dummies(
            X,
            prefix=[c for c in X.columns],
            columns=X.columns).columns
        return self

    def transform(self, X, **transform_params):
        return pd.get_dummies(
            X,
            prefix=[c for c in X.columns],
            columns=X.columns).reindex(columns=self.dummy_columns, fill_value=0)


class DfFeatureUnion(BaseTransformer):
    """A dataframe friendly implementation of `FeatureUnion`"""

    def __init__(self, transformers):
        self.transformers = transformers

    def fit(self, X, y=None, **fit_params):
        for l, t in self.transformers:
            t.fit(X, y=y, **fit_params)
        return self

    def transform(self, X, **transform_params):
        transform_results = [t.transform(X, **transform_params) for l, t in self.transformers]
        return pd.concat(transform_results, axis=1)

training_data_full.columns
for col in training_data_full.columns:
    print("in {} there are {} unique values".format(col, len(training_data_full[col].unique())))
None<jupyter_output>in people_id there are 151295 unique values
in activity_id there are 2197291 unique values
in date_action there are 411 unique values
in activity_category there are 7 unique values
in char_1_action there are 52 unique values
in char_2_action there are 33 unique values
in char_3_action there are 12 unique values
in char_4_action there are 8 unique values
in char_5_action there are 8 unique values
in char_6_action there are 6 unique values
in char_7_action there are 9 unique values
in char_8_action there are 19 unique values
in char_9_action there are 20 unique values
in char_10_action there are 6516 unique values
in outcome there are 2 unique values
in char_1_person there are 2 unique values
in group_1 there are 29899 unique values
in char_2_person there are 3 unique values
in date_person there are 1196 unique values
in char_3_person there are 43 unique values
in char_4_person there are 25 unique values
in char_5_person there are 9 unique values
in char_6_person there are 7 unique value[...]<jupyter_text>Potential trouble with high dimensionalityNotice that char_10_action, group_1 and others have a ton of unique values; one-hot encoding will result in a dataframe with thousands of columns. Being lazy and getting as fast as possible to a first attempt, let's skip those and only consider categorical variable with ~20 or less unique values. We'll get smarter about dealing with these variables to reinclude them in our model on a subsequent attempt<jupyter_code>from sklearn.pipeline import Pipeline

from sklearn.preprocessing import Imputer, StandardScaler

cat_columns = ['activity_category',
       'char_1_action', 'char_2_action', 'char_3_action', 'char_4_action',
       'char_5_action', 'char_6_action', 'char_7_action', 'char_8_action',
       'char_9_action', 'char_1_person',
       'char_2_person', 'char_3_person',
       'char_4_person', 'char_5_person', 'char_6_person', 'char_7_person',
       'char_8_person', 'char_9_person', 'char_10_person', 'char_11',
       'char_12', 'char_13', 'char_14', 'char_15', 'char_16', 'char_17',
       'char_18', 'char_19', 'char_20', 'char_21', 'char_22', 'char_23',
       'char_24', 'char_25', 'char_26', 'char_27', 'char_28', 'char_29',
       'char_30', 'char_31', 'char_32', 'char_33', 'char_34', 'char_35',
       'char_36', 'char_37']

q_columns = ['char_38']

preprocessor = Pipeline([
    ('features', DfFeatureUnion([
        ('quantitative', Pipeline([
            ('select-quantitative', ColumnSelector(q_columns, c_type='float')),
            ('impute-missing', DfTransformerAdapter(Imputer(strategy='median'))),
            ('scale', DfTransformerAdapter(StandardScaler()))
        ])),
        ('categorical', Pipeline([
            ('select-categorical', ColumnSelector(cat_columns)),
            ('apply-onehot', DfOneHot()),
            ('spread-binary', SpreadBinary())
        ])),
    ]))
])<jupyter_output><empty_output><jupyter_text>Sampling to reduce runtime in training large datasetIf we train models based on the entire test dataset provided it exhausts the memory on my laptop. Again, in the spirit of getting something quick and dirty working, we'll sample the dataset and train on that. We'll then evaluate our model by testing the accuracy on a larger sample.<jupyter_code>from sklearn.cross_validation import train_test_split

training_frac = 0.05
test_frac = 0.8

training_data, the_rest = train_test_split(training_data_full, train_size=training_frac, random_state=0)
test_data = the_rest.sample(frac=test_frac)
training_data.shape
test_data.shape
wrangled = preprocessor.fit_transform(training_data)
wrangled.head()<jupyter_output><empty_output><jupyter_text>Putting together classifiers<jupyter_code>from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression

pipe_lr = Pipeline([
        ('wrangle', preprocessor),
        ('lr', LogisticRegression(C=100.0, random_state=0))
    ])

pipe_rf = Pipeline([
        ('wrangle', preprocessor),
        ('rf', RandomForestClassifier(criterion='entropy', n_estimators=10, random_state=0))
    ])
feature_columns = cat_columns + q_columns 
def extract_X_y(df):
    return df[feature_columns], df['outcome']

X_train, y_train = extract_X_y(training_data)
X_test, y_test = extract_X_y(test_data)<jupyter_output><empty_output><jupyter_text>Reporting utilitiesSome utilities to make reporting progress easier<jupyter_code>import time
import subprocess

class time_and_log():
    
    def __init__(self, label, *, prefix='', say=False):
        self.label = label
        self.prefix = prefix
        self.say = say
    
    def __enter__(self):
        msg = 'Starting {}'.format(self.label)
        print('{}{}'.format(self.prefix, msg))
        if self.say:
            cmd_say(msg)
        self.start = time.process_time()
        return self

    def __exit__(self, *exc):
        self.interval = time.process_time() - self.start
        msg = 'Finished {} in {:.2f} seconds'.format(self.label, self.interval)
        print('{}{}'.format(self.prefix, msg))
        if self.say:
            cmd_say(msg)
        return False
    
def cmd_say(msg):
    subprocess.call("say '{}'".format(msg), shell=True)<jupyter_output><empty_output><jupyter_text>Cross validation and full test set accuracyWe'll cross validate within the training set, and then train on the full training set and see how well it performs on the full test set.<jupyter_code>from sklearn.metrics import accuracy_score
from sklearn.cross_validation import cross_val_score
import numpy as np

models = [
    ('logistic regression', pipe_lr), 
    ('random forest', pipe_rf), 
]

for label, model in models:
    print('Evaluating {}'.format(label))
    say('Evaluating {}'.format(label))
#     with time_and_log('cross validating', say=True, prefix=" _"):
#         scores = cross_val_score(estimator=model,
#                              X=X_train,
#                              y=y_train,
#                              cv=5,
#                              n_jobs=1)
#         print('  CV accuracy: {:.3f} +/- {:.3f}'.format(np.mean(scores), np.std(scores)))
    with time_and_log('fitting full training set', say=True, prefix=" _"):
        model.fit(X_train, y_train)  
    with time_and_log('evaluating on full test set', say=True, prefix=" _"):
        print("  Full test accuracy ({:.2f} of dataset): {:.3f}".format(
                test_frac, 
                accuracy_score(y_test, model.predict(X_test))))<jupyter_output>Evaluating logistic regression
 _Starting fitting full training set
 _Finished fitting full training set in 121.04 seconds
 _Starting evaluating on full test set
  Full test accuracy (0.80 of dataset): 0.861
 _Finished evaluating on full test set in 288.49 seconds
Evaluating random forest
 _Starting fitting full training set
 _Finished fitting full training set in 21.32 seconds
 _Starting evaluating on full test set
  Full test accuracy (0.80 of dataset): 0.923
 _Finished evaluating on full test set in 292.85 seconds<jupyter_text>Preparing the submissionRandom forest beat logistic regression, let's start with a submission using that.But first, let's see what the submission is supposed to look like:<jupyter_code>pd.read_csv('sample_submission.csv.zip').head(5)<jupyter_output><empty_output><jupyter_text>And now let's prepare the submission by fitting on the full provided training set and using it to predict on the provided test set.<jupyter_code>kaggle_test_df = pd.merge(
    pd.read_csv('act_test.csv.zip'), 
    people, 
    how='inner', on='people_id', suffixes=['_action', '_person'], sort=False)
kaggle_test_df.head(2)
kaggle_test_df.shape
X_kaggle_train, y_kaggle_train = extract_X_y(training_data_full)
with time_and_log('fitting rf on full kaggle training set', say=True): 
    pipe_rf.fit(X_kaggle_train, y_kaggle_train)
with time_and_log('preparing kaggle submission', say=True):
    submission_df = kaggle_test_df[['activity_id']].copy()
    submission_df['outcome'] = pipe_rf.predict(kaggle_test_df)
    submission_df.to_csv("predicting-red-hat-business-value_1_rf.csv", index=False)<jupyter_output>Starting preparing kaggle submission
Finished preparing kaggle submission in 84.99 seconds
{'fraction_non_alphanumeric': 0.10191465613462694, 'fraction_numerical': 0.01897484330728943, 'mean_word_length': 4.057750759878419, 'pattern_counts': {'":': 0, '<': 38, '<?xml version=': 0, '>': 38, 'https://': 1, 'lorem ipsum': 0, 'www.': 1, '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': '10', 'dir': 'jupyter-structured-clean-dedup', 'id': '228141', 'license': 'MIT', 'n_tokens_mistral': 3754, 'n_tokens_neox': 3549, 'n_words': 1015, 'path': 'kaggle/predicting-red-hat-business-value/predicting-red-hat-business-value.ipynb', 'repo_name': 'krosaen/ml-study', 'size': '11647'}