<jupyter_start><jupyter_text>MultiLabel Classification- Toy Apples and Basketball dataset (1 Text Feature column and 1 mulitlabel target column) ---  Import modules<jupyter_code># Standard
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# TFIDF
from sklearn.feature_extraction.text import TfidfVectorizer

# Preprocessing
from sklearn.preprocessing import LabelEncoder
from sklearn.preprocessing import MultiLabelBinarizer

# ML
from sklearn.linear_model import LogisticRegression
from sklearn.multioutput import MultiOutputClassifier

# Train-Test split
from sklearn.model_selection import train_test_split

# AST
import ast

# Spacy
import spacy

# Make column transformer
from sklearn.compose import make_column_transformer

# Pipeline
from sklearn.pipeline import make_pipeline

# Metrics
from sklearn.metrics import jaccard_score<jupyter_output><empty_output><jupyter_text>ConfigurationRequired to run the rest of the notebook for any dataset that has a Text column which serves as the features for a NLP multilable classification task<jupyter_code># Excel Datafile (if csv then change to pd.read_csv)
datafile = 'apples_and_basketball.xlsx'

# Features column
TEXT_CONTENT_COLUMN = "Text"

# Target
TARGET = "Label"<jupyter_output><empty_output><jupyter_text>Import Data<jupyter_code>df = pd.read_excel(datafile)
df.shape
df<jupyter_output><empty_output><jupyter_text>Data Preparation  1. Convert label list imported as strings to lists<jupyter_code>type(df[TARGET][0])
df['label_ast'] = df[TARGET].apply(lambda x: ast.literal_eval(x))
type(df['label_ast'][0])
df.head(2)<jupyter_output><empty_output><jupyter_text>2. Label Counts<jupyter_code>df['label_count'] = df['label_ast'].apply(len)
df.head(3)<jupyter_output><empty_output><jupyter_text>3. Label Distribution<jupyter_code>plt.hist(df['label_count'])
plt.title('Label Distribution across samples')
plt.xlabel('# of labels per sample')
plt.ylabel('# of samples having label(s)')
plt.show()<jupyter_output><empty_output><jupyter_text>4. Spacy Stopwords<jupyter_code>from spacy.cli.download import download
download(model="en_core_web_lg")
nlp_lg = spacy.load('en_core_web_lg')
nlp_lg
# Spacy stopwords

sp_stopwords = spacy.lang.en.stop_words.STOP_WORDS
print('Type   : ', type(sp_stopwords))
print('Length : ', len(sp_stopwords))<jupyter_output>Type   :  <class 'set'>
Length :  326<jupyter_text>5. Vectorize Features using TFIDF<jupyter_code># Will use the spacy stopwords later
tfidf = TfidfVectorizer(stop_words='english')
tfidf.get_params();
# Vectorize the feature column using make_column_transformer 

preprocessor = make_column_transformer((tfidf, TEXT_CONTENT_COLUMN),  # "Test" 
                                       remainder='passthrough'
                                      )<jupyter_output><empty_output><jupyter_text>6. Convert Target via Label Encoder<jupyter_code># Get all unique lables from the target

all_labels_flattened_list = [inner_value for inner_list in list(df['label_ast']) for inner_value in inner_list]

all_unique_labels = list(set(all_labels_flattened_list))
all_unique_labels<jupyter_output><empty_output><jupyter_text>Pipeline - 1- 1 step pipeline of vectorizing feature column using TFIDF and re-producing the original df and the preprocessed df along with the other non-preprocessed columns- Just for viewing and analysis<jupyter_code>pipe1 = make_pipeline(preprocessor)
pipe1.fit(df)
# TFIDF Feature names

tfidf_feature_names = preprocessor.named_transformers_.tfidfvectorizer.get_feature_names()
tfidf_feature_names
# TDIDF processed array of df 
tfidf_processed_csr = pipe1.transform(df)

# Creating a df of the csr (just for analysis)
# Since we used "passthrough" from the other columns we
# need to provide their names to the processed df along
# with the tfidf feature names
df_processed = pd.DataFrame(tfidf_processed_csr, 
                            columns=tfidf_feature_names + list(df.columns[-3:]))
df_processed.shape
df_processed.head(3)<jupyter_output><empty_output><jupyter_text>Train-Test split<jupyter_code># Form X and y required for tts

X_processed = df_processed.loc[:, df_processed.columns.isin(tfidf_feature_names)]
y_processed = df_processed['label_ast']

X_processed.shape, y_processed.shape
X_train, X_test, y_train_ast, y_test_ast = train_test_split(X_processed, y_processed, test_size=0.3)

print(X_train.shape)
print(y_train_ast.shape)
print(X_test.shape)
print(y_test_ast.shape)
# Examining the split of data

y_train_ast
y_test_ast
# Instantiate multilabel binarizer

mlb = MultiLabelBinarizer(classes=all_unique_labels)
y_train = mlb.fit_transform(y_train_ast)
y_train
y_test = mlb.transform(y_test_ast)
y_test<jupyter_output><empty_output><jupyter_text>---  TODO: Stratify multilabels - Figure out how ---  Convert Target- `Possibly help for stratification ... but not sure ...`-  **Does not look like it helps :-(**<jupyter_code>le = LabelEncoder()

le.fit_transform(all_unique_labels)

df_processed['label_ast_lblencoded'] = df_processed['label_ast'].apply(lambda x: le.transform(x))
df_processed.sample(4)<jupyter_output><empty_output><jupyter_text>ML Models (Supervised)<jupyter_code>lr = LogisticRegression()
lr.get_params();
mo = MultiOutputClassifier(estimator=lr)
mo.get_params();<jupyter_output><empty_output><jupyter_text>Fit and Predict**`TO DO:`** Put the below in an overall pipeline of 2 steps:1. Preprocessing 2. ML algorithms for classification<jupyter_code># Starting off with just passing what we 
# obtained so far into a 1 step pipeline

mo.fit(X_train, y_train);<jupyter_output><empty_output><jupyter_text>If you do not convert y_train to the required format (binary array or sparse matrix) the following error occurs:    - ERROR message:    - `ValueError: You appear to be using a legacy multi-label data representation. Sequence of sequences are no longer supported; use a binary array or sparse matrix instead - the MultiLabelBinarizer transformer can convert to this format.`- Multioutput classifier can take different text labels as input (does not NEED to be binarized). However, the target input to fitting the model requires an array. Since its an array, the rows and columns require values in it. So, I cannot just pass ['apple'] for one sample and ['apple', 'basketball'] for another sample since that has different shapes. And therefore, in the code above I used multilabel binarizer.     - Note, I could have passed non-binary values (since multioutput classifier can handle that), but that too needs to be in a array with fixed number of rows and columns. So i can pass np.array([['apple', 'nothing'], ['basketball','nothing']]) but that has the same overall effect same as using the multilabel binarizer.<jupyter_code>y_pred = mo.predict(X_test)
y_pred
# Predicted label descriptions

mlb.inverse_transform(y_pred)

# Printing per line
for x in mlb.inverse_transform(y_pred):
    print(x)
# True Label descriptions

mlb.inverse_transform(y_test)

# Printing per line
for x in mlb.inverse_transform(y_test):
    print(x)
# Seeing the corresponding X_test

X_test
# Seeing the corresponding full sentences

df.loc[X_test.index, :]<jupyter_output><empty_output><jupyter_text>---  Model Evaluation  1. Jaccard Score<jupyter_code># From sklearn library

jaccard_score(y_test, y_pred, average='samples')
# Manual

def jaccard_score_manual(y_true, y_pred):
    # Compute jaccard score
    # np.min element wise will pick up 0 if 0 and 1 are the 2 elements
    # in the 2 arrays being compared, which means no element was intersecting
    # np.min will pick 1 if 1 and 1 are the 2 elements in the 2 arrays
    # which means both elements were 1 and were intersecting
    # HOWEVER, if both elements are 0 and 0, np.min will pick 0,
    # which is correct, but, if the arrays are made of only 1 element
    # each then the jaccard score is 0. But then again, we use jaccard
    # score with multilabel classification and it is fair to assume that
    # the 2 arrays being compared will not have only 1 element each.
    
    jaccard = np.minimum(y_true, y_pred).sum(axis=1) / np.maximum(y_true, y_pred).sum(axis=1)
    print(jaccard)
    return jaccard.mean()
# Call
jaccard_score_manual(y_test, y_pred)
# Checking manual result with sklearn's jaccard score

assert jaccard_score(y_test, y_pred, average='samples') == jaccard_score_manual(y_test, y_pred), "Jaccard manual and sklearn mismatch"
y_test
y_pred
np.minimum(y_test, y_pred).sum(axis=1)
np.minimum(y_test, y_pred)
np.maximum(y_test, y_pred).sum(axis=1)
jaccard_score(np.array([[1,1]]), np.array([[1,1]]), average='samples')
# Weighted Jaccard score
# From sklearn library

jaccard_score(y_test, y_pred, average='weighted')
def jaccard_score_manual_weighted(y_true, y_pred):
    
    jaccard = np.minimum(y_true, y_pred).sum(axis=1) / np.maximum(y_true, y_pred).sum(axis=1)
    print(jaccard)
    return jaccard.mean()
# Call: Weighted
jaccard_score_manual(y_test, y_pred)<jupyter_output>[1.  0.5 0.5 1. ]<jupyter_text>2. Average Precision ---  Model Coefficients<jupyter_code>idx = 0
mo.estimators_[idx].coef_
df_coeffs = pd.DataFrame({'Features':tfidf_feature_names, 
                          'Coefficients':mo.estimators_[idx].coef_[0]})<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.09047515730093296, 'fraction_numerical': 0.007268387936645694, 'mean_word_length': 5.613342898134864, 'pattern_counts': {'":': 0, '<': 68, '<?xml version=': 0, '>': 68, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 5, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '17', 'dir': 'jupyter-structured-clean-dedup', 'id': '508595', 'license': 'MIT', 'n_tokens_mistral': 3136, 'n_tokens_neox': 3011, 'n_words': 856, 'path': 'multilabel_classification.ipynb', 'repo_name': 'ashish-sharma-as/multioutput_classifier', 'size': '9218'}