First baseline in Kaggle Inclass competition "How good is your Medium article?"
Import libraries.
import os
import numpy as np
import pandas as pd
from matplotlib import pyplot as plt
%matplotlib inline
import json
from tqdm import tqdm_notebook
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.metrics import mean_absolute_error
The following code will help to throw away all HTML tags from an article content.
from html.parser import HTMLParser
class MLStripper(HTMLParser):
def __init__(self):
self.reset()
self.strict = False
self.convert_charrefs= True
self.fed = []
def handle_data(self, d):
self.fed.append(d)
def get_data(self):
return ''.join(self.fed)
def strip_tags(html):
s = MLStripper()
s.feed(html)
return s.get_data()
Let's have two paths – to raw data (downloaded from competition's page and ungzipped) and to processed data. Change this if you'd like to.
PATH_TO_RAW_DATA = '../../raw_data/'
PATH_TO_PROCESSED_DATA = '../../processed_data/'
Assume you have all data downloaded from competition's page in the PATH_TO_RAW_DATA folder and .gz
files are ungzipped.
!ls -l $PATH_TO_RAW_DATA
Supplementary function to read a JSON line without crashing on escape characters.
def read_json_line(line=None):
result = None
try:
result = json.loads(line)
except Exception as e:
# Find the offending character index:
idx_to_replace = int(str(e).split(' ')[-1].replace(')',''))
# Remove the offending character:
new_line = list(line)
new_line[idx_to_replace] = ' '
new_line = ''.join(new_line)
return read_json_line(line=new_line)
return result
This function takes a JSON and forms a txt file leaving only article content. When you resort to feature engineering and extract various features from articles, a good idea is to modify this function.
def preprocess(path_to_inp_json_file, path_to_out_txt_file):
with open(path_to_inp_json_file, encoding='utf-8') as inp_file, \
open(path_to_out_txt_file, 'w', encoding='utf-8') as out_file:
for line in tqdm_notebook(inp_file):
json_data = read_json_line(line)
content = json_data['content'].replace('\n', ' ').replace('\r', ' ')
content_no_html_tags = strip_tags(content)
out_file.write(content_no_html_tags + '\n')
%%time
preprocess(path_to_inp_json_file=os.path.join(PATH_TO_RAW_DATA, 'train.json'),
path_to_out_txt_file=os.path.join(PATH_TO_PROCESSED_DATA, 'train_raw_content.txt'))
%%time
preprocess(path_to_inp_json_file=os.path.join(PATH_TO_RAW_DATA, 'test.json'),
path_to_out_txt_file=os.path.join(PATH_TO_PROCESSED_DATA, 'test_raw_content.txt'))
!wc -l $PATH_TO_PROCESSED_DATA/*_raw_content.txt
We'll use a linear model (Ridge
) with a very simple feature extractor – CountVectorizer
, meaning that we resort to the Bag-of-Words approach. For now, we are leaving only 50k features.
cv = CountVectorizer(max_features=50000)
%%time
with open(os.path.join(PATH_TO_PROCESSED_DATA, 'train_raw_content.txt'), encoding='utf-8') as input_train_file:
X_train = cv.fit_transform(input_train_file)
%%time
with open(os.path.join(PATH_TO_PROCESSED_DATA, 'test_raw_content.txt'), encoding='utf-8') as input_test_file:
X_test = cv.transform(input_test_file)
X_train.shape, X_test.shape
Read targets from file.
train_target = pd.read_csv(os.path.join(PATH_TO_RAW_DATA, 'train_log1p_recommends.csv'),
index_col='id')
y_train = train_target['log_recommends'].values
Make a 30%-holdout set.
train_part_size = int(0.7 * train_target.shape[0])
X_train_part = X_train[:train_part_size, :]
y_train_part = y_train[:train_part_size]
X_valid = X_train[train_part_size:, :]
y_valid = y_train[train_part_size:]
Now we are ready to fit a linear model.
from sklearn.linear_model import Ridge
ridge = Ridge(random_state=17)
%%time
ridge.fit(X_train_part, y_train_part);
ridge_pred = ridge.predict(X_valid)
Let's plot predictions and targets for the holdout set. Recall that these are #recommendations (= #claps) of Medium articles with the np.log1p
transformation.
plt.hist(y_valid, bins=30, alpha=.5, color='red', label='true', range=(0,10));
plt.hist(ridge_pred, bins=30, alpha=.5, color='green', label='pred', range=(0,10));
plt.legend();
As we can see, the prediction is far from perfect, and we get MAE $\approx$ 1.3 that corresponds to $\approx$ 2.7 error in #recommendations.
valid_mae = mean_absolute_error(y_valid, ridge_pred)
valid_mae, np.expm1(valid_mae)
Finally, train the model on the full accessible training set, make predictions for the test set and form a submission file.
%%time
ridge.fit(X_train, y_train);
%%time
ridge_test_pred = ridge.predict(X_test)
def write_submission_file(prediction, filename,
path_to_sample=os.path.join(PATH_TO_RAW_DATA, 'sample_submission.csv')):
submission = pd.read_csv(path_to_sample, index_col='id')
submission['log_recommends'] = prediction
submission.to_csv(filename)
write_submission_file(prediction=ridge_test_pred,
filename='first_ridge.csv')
With this, you'll get 1.91185 on public leaderboard. This is much higher than our validation MAE. This indicates that the target distribution in test set somewhat differs from that of the training set (recent Medium articles are more popular). This shouldn't confuse us as long as we see a correlation between local improvements and improvements on the leaderboard.
Some ideas for improvement:
C
=1 as a regularization parameter, this can be changed