<jupyter_start><jupyter_text>Sprint 3: Consumer Complaint Database Groep 10: <NAME>, <NAME>, <NAME>  Introduction Tijdens deze laatste sprint beginnen we vanuit een centrale onderzoeksvraag en trachten we deze zo goed mogelijk te beantwoorden door gebruik te maken van verschillende machine learning technieken. De eerste stap is om deze vraag te definiëren en vervolgens de predictors te gaan bepalen waarmee we het antwoord op deze vraag kunnen beantwoorden.  Research Question **"Kunnen we de reactie op een gegeven klacht gaan voorspellen?"**Deze centrale onderzoeksvraag probeert te voorspellen hoe een bedrijf op een ongeziene klacht gaat reageren. Om dit te kunnen beantwoorden moeten we enkele onderliggen vragen stellen die invloed kunnen hebben op het eindresultaat. We gaan dus opzoek naar verschillende parameters die dit resultaat kunnen voorspellen.Onderliggende vragen:- "Heeft de sentiment van een klacht invloed op de reactie van het bedrijf?"- "Heeft de historische ratio waarmee een klacht opgelost geraakt in verhouding met het totaal aantal klachten van het bedrijf invloed op de reactie van het bedrijf?"- "Heeft de geolocatie van waar de klacht afkomstig is invloed op de reactie van het bedrijf?"- "Heeft de productcategorie van de klacht invloed op de reactie van het bedrijf?"De verschillende antwoorden op deze vragen worden vervolgens gebruikt als input om aan de hand van machine learning een classificatie model te ontwikkelen die de centrale onderzoeksvraag tracht te beantwoorden.  Exploratory Data Analysis & Data Preprocessing Hier gaan we gaan onderzoeken welke features in de dataset interessant kunnen zijn om te gebruiken. We bekijken of er een verband is tussen de antwoorden op de gestelde onderliggende vragen van hierboven en het uiteindelijke antwoord op de centrale onderzoeksvraag die we wensen te bekomen.  Dataset We starten zoals gewoonlijk met het inlezen van de volledige dataset.<jupyter_code>import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

complaints_source_df = pd.read_csv('../data/complaints.csv', delimiter=',')
complaints_source_df.head()<jupyter_output><empty_output><jupyter_text>Vervolgens gaan we enkel de features die we wensen te gebruiken gaan kopiëren naar een nieuwe DataFrame en geven deze een eenvoudiger label.<jupyter_code>complaints_df = complaints_source_df[['Complaint ID', 'Product', 'Consumer complaint narrative', 'Company', 'State', 'Company response to consumer']].copy()
complaints_df.columns = ['Complaint ID', 'Product', 'Narrative', 'Company', 'State', 'Response'] 
complaints_df.head()<jupyter_output><empty_output><jupyter_text>Features Voor een aantal features gaan we nu gaan onderzoeken of deze relevant zijn voor het oplossen van de centrale onderzoeksvraag. We gaan ook gaan kijken welke processing nodig is voor elke feature om de onderliggende vragen te kunnen gaan beantwoorden. We trachten dit aan te tonen met meerdere visualisaties.  Response De response stelt de reactie voor van het bedrijf op een gegeven klacht. Dit wordt de target van ons classificatie model die de centrale onderzoeksvraag zal beantwoorden. We stellen echter vast dat er enkele responscategorieën amper klachten bevatten. Om een beter classificatie model te kunnen bekomen gaan we daarom enkele categorieën die amper data bevatten gaan elimineren.<jupyter_code>print('Aantal responsen:', len(complaints_df.Response.unique()))
fig = plt.figure(figsize=(10, 5))
pd.DataFrame({'Count': complaints_df.groupby(['Response']).size()}).plot.barh(title="Aantal klachten per responscategorie", legend=None)
complaints_df.replace({'Response': {'Closed': 'Closed without explanation'}}, inplace= True)
complaints_df = complaints_df[complaints_df['Response'].isin([
    'Closed with non-monetary relief','Closed with monetary relief', 'Closed with explanation',
    'Closed without explanation', 'Untimely response'])]
print('Aantal responsen:', len(complaints_df.Response.unique())) 
fig = plt.figure(figsize=(10, 5))
pd.DataFrame({'Count': complaints_df.groupby(['Response']).size()}).plot.barh(title="Aantal klachten per responscategorie", legend=None)<jupyter_output>Aantal responsen: 5<jupyter_text>Vervolgens valt het ook op dat de data zeer **ongebalanceerd is**. Eventueel kunnen we de data gaan balanceren indien de het classificatie model hier niet goed op zou werken. We zullen het model zowel op de ongebalanceerde als op de gebalanceerde dataset trainen en kijken wat de beste resultaten geeft.<jupyter_code>labels = complaints_df['Response'].value_counts().index
sizes = complaints_df['Response'].value_counts().values
patches, texts = plt.pie(sizes)
plt.legend(patches, labels)
plt.show()<jupyter_output><empty_output><jupyter_text>Product De tweede feature is de productcategorie van de klacht. Zoals in de vorige sprints nemen we opnieuw alle klachten uit sterk gerelateerde product categorieën samen binnen overkoepelende product categorieën.<jupyter_code>complaints_df.replace({'Product': 
             {'Credit reporting, credit repair services, or other personal consumer reports': 
              'Credit reporting, repair, or other', 
              'Credit reporting': 'Credit reporting, repair, or other',
             'Credit card': 'Credit card or prepaid card',
             'Prepaid card': 'Credit card or prepaid card',
             'Payday loan': 'Payday loan, title loan, or personal loan',
             'Money transfer': 'Money transfer, virtual currency, or money service',
             'Virtual currency': 'Money transfer, virtual currency, or money service'}}, 
            inplace= True)
product_count = len(complaints_df.Product.unique())
print('Aantal product categorieën:', product_count)
product_counts = pd.DataFrame({'Count': complaints_df.groupby(['Product']).size()})
fig = plt.figure(figsize=(10, 5))
product_counts.plot.barh(title="Aantal klachten per product categorie", legend=None)<jupyter_output>Aantal product categorieën: 13<jupyter_text>Deze data is ongebalanceerd, maar aangezien we in deze sprint geen classificatie gaan doen tussen de verschillende product categorieën is het niet zinvol om dit te gaan balanceren. Deze feature wordt hier nu louter beschouwd als categorische data. Vervolgens gaan we onderzoeken of het zinvol is om de product categorie in rekening te brengen bij het voorspellen van de responsen van de bedrijven. Dit doen we door per product categorie de verdeling van de verschillende reacties te gaan visualiseren.<jupyter_code>product_response_counts = pd.DataFrame({'Count': complaints_df.groupby(['Product', 'Response']).size()})
product_response_counts.unstack().plot(kind='barh', figsize=(14,6), stacked=True, title="Respons verdeling per product categorie")<jupyter_output><empty_output><jupyter_text>Een aantal zaken vallen ons op bij het bestuderen van deze grafiek:- Er zijn slechts 3 categorieën waarbij het respons "Closed without explanation" voorkomt, namelijk: "Mortgage", "Debt collection" en "Bank account or service"- Bij de categorie "Credit card or prepaid card" is de verhouding waarmee de reactie "Closed with monetary relief" voorkomt veel groter dan bij de overige product categorieën- Elke product categorie heeft over het algemeen een redelijk gelijklopende verdeling over de verschillende responses- De grootste respons categorie "Closed with explanation" is over alle product categorieën heen steeds de grootste Het lijkt ons dus wel interessant om de product categorie op te nemen in het classificatie model aangezien hier toch waarschijnlijk enkele verbanden in de data te vinden zijn.  Consumer complaint narrative De derde feature is de consumer complaint narrative. Voor elke klacht wordt de narrative gebruikt als een document in de vorm van tekstuele data. Deze documenten wensen we te gebruiken om een sentiment analyse uit te voeren en zo de eerste onderliggende vraag te gaan beantwoorden. Om dit mogelijk te maken gaan we net zoals in de vorige sprints heel wat processing gaan uitvoeren op deze documenten.We gaan als volgt te werk:1. alle woorden omvormen naar lowercase2. alle symbolen, cijfers, URL's en whitespace/newlines verwijderen3. alle symbolen ("XXX") die afkomstig zijn van anonimiseren van de data (kredietkaart gegevens) verwijderen4. document opsplitsen in verschillende tokens5. stopwoorden en woorden korter dan 3 karakters verwijderen6. woorden omvormen naar stamvorm en lemmatisering toepassen met behulp van de nltk libraryDit proces is dus volledig analoog als bij onze vorige sprints.<jupyter_code># pip install nltk
import nltk, re
nltk.download('wordnet')

from sklearn.feature_extraction.text import ENGLISH_STOP_WORDS
from nltk.stem import WordNetLemmatizer, SnowballStemmer

def processing(string):
    string = string.lower()
    string = re.sub(r'@\S+|https?:\S+|http?:\S', ' ', string)
    string = re.sub(r'[^A-Za-z0-9]+', ' ', string)
    string = re.sub(r'[^\w\s]', '', string)
    string = re.sub(r'[0-9]|,', '', string)
    string = string.replace(r'\r', ' ')
    string = string.replace(r'\n', ' ')
    string = re.sub(r'[X|x]{2,}', ' ', string)
    tokens = string.split(' ')
    tokens = [w for w in tokens if w not in ENGLISH_STOP_WORDS and len(w) > 2]
    result = [SnowballStemmer('english').stem(WordNetLemmatizer().lemmatize(token, pos='v')) for token in tokens]
    return ' ' . join(result)<jupyter_output>[nltk_data] Downloading package wordnet to
[nltk_data]     C:\Users\woute\AppData\Roaming\nltk_data...
[nltk_data]   Package wordnet is already up-to-date!<jupyter_text>Vervolgens gaan we de processing toepassen met ondersteuning voor multithreading. Hiervoor maken we gebruik van de swifter library. Op die manier wordt de processing in parallele batches uitgevoerd die deze stap wat versnelt. De uitvoer van deze processing wordt opgeslagen in een nieuwe kolom met label 'Narrative_Processed'. **We zullen later bepalen of het gunstig is om de processed narrative te gebruiken in plaats van de oorspronkelijke narrative bij het testen van ons uiteindelijke classificatie model**.<jupyter_code># pip install swifter
%%time

import swifter

complaints_df['Narrative_Processed'] = complaints_df['Narrative'].swifter.apply(processing)
complaints_df[['Narrative', 'Narrative_Processed']].head()<jupyter_output><empty_output><jupyter_text>Company Een andere feature die interessant lijkt is het bedrijf waarnaar de klacht verstuurd is, samen met de response van dat bedrijf op de klacht. Op basis van eerdere observaties kunnen we voor elk bedrijf de verhoudingen zoeken van de soorten reacties ten opzichte van het totaal aantal klachten van dat bedrijf.Deze ratio's kunnen we vervolgens gebruiken als invoer voor een classificatiemodel dat kan voorspellen wat de reactie zal zijn, gegeven het bedrijf.<jupyter_code>company_count = len(complaints_df.Company.unique())
print('Aantal bedrijven:', company_count)<jupyter_output>Aantal bedrijven: 4419<jupyter_text>Om te beginnen willen we voor een aantal bedrijven de verhouding tussen de soorten responses visualiseren. Omdat er zeer veel verschillende bedrijven in de dataset zitten en om een mooi overzicht te bekomen, houden we het hier bij de 10 meest voorkomende.<jupyter_code>counts = complaints_df.groupby('Company').count().reset_index()
biggest_companies = counts.sort_values(by='Response', ascending=False).head(10)['Company'].to_list()
print(biggest_companies)
complaints_filtered = complaints_df[complaints_df['Company'].isin(biggest_companies)]
response_counts = pd.DataFrame({'Count': complaints_filtered.groupby(['Company', 'Response']).size()})
response_counts.head(10)
response_counts.unstack().plot(kind='bar', figsize=(14,6), stacked=True, title="Aantal klachten per bedrijf")<jupyter_output><empty_output><jupyter_text>Zo te zien zijn er drie categorieën van responses die overheersen, maar binnen die drie soorten zijn er wel enkele interessante verdelingen die verschillen van bedrijf tot bedrijf. Nu worden aan de complaints dataset kolommen toegevoegd die voor elke klacht de kans tonen dat een specifieke response gegeven wordt, op basis van het bedrijf waarnaar de klacht gestuurd werd. Voor elk type response is er een kolom toegevoegd. | Kolom  | Response || ------- | -------- || 1 | Closed with explanation || 2 | Closed with non-monetary relief || 3 | Closed with monetary relief || 4 | Closed without explanation || 5 | Untimely response |<jupyter_code># Aantal voorkomens van een bepaalde response tellen per bedrijf
c = pd.DataFrame({'Count': complaints_df.groupby(['Company', 'Response']).size()})

# Dit aantal omzetten in een percentage tegenover het aantal klachten per bedrijf
perc = c.groupby(level=0).apply(lambda x: round(100 * x / float(x.sum()),2)).reset_index()

# De responses pivoteren tot kolommen
gepivoteerd = perc.pivot_table('Count', ['Company'], 'Response')

# De kolommen opnieuw ordenen van meest naar minst voorkomend
gepivoteerd = gepivoteerd.reindex(['Closed with explanation', 'Closed with non-monetary relief', 'Closed with monetary relief',
                                   'Closed without explanation', 'Untimely response'], axis=1).reset_index()

# De complaints mergen met de percentages om een duidelijk overzicht te bekomen
company_df = pd.merge(complaints_df, gepivoteerd, on='Company', how='inner')

#NaN vervangen door 0
company_df = company_df.fillna(0)

company_df
company_df.to_csv('../data/complaints_with_company_ratio.csv')<jupyter_output><empty_output><jupyter_text>State Vervolgens bepalen we of de geografische locatie van een klacht invloed heeft op het uiteindelijke resultaat. Aangezien de state wordt opgeslaan als een afkorting (CA, FL, AZ, ...), hebben we een tabel nodig die voor elke afkorting de corresponderende geografische ligging kan teruggeven. Op die manier willen we de staten die dicht bij elkaar liggen binnen eenzelfde regio gaan onderbrengen (South, West, ...).Deze table is gegenereerd via https://statetable.com/. Het is ook belangrijk om de populatie van elke State in rekening te brengen. Op die manier kunnen we een gewicht toekennen aan elke State binnen elke Region.<jupyter_code>state_table_df = pd.read_csv('../data/regions/state_table.csv', delimiter=',')
state_table_df['census_region_name'].unique()

region_pop_df = pd.read_csv('../data/regions/region_population.csv', delimiter=',')
region_pop_df.head()<jupyter_output><empty_output><jupyter_text>Door middel van de volgende functie kunnen we de state onderverdelen naar de region waarin deze ligt.<jupyter_code>state_table_df = state_table_df.replace({np.nan: None})
state_table_df['census_region_name'].unique()
def give_region(abbr):
    abbr = str(abbr)
    row = state_table_df.loc[state_table_df['abbreviation'] == abbr]
    region = row['census_region_name'].values
    if region.size == 0:
        return None
    else:
        return region[0]
def give_region_population(region):
    region = str(region)
    row = region_pop_df.loc[region_pop_df['Region'] == region]
    pop = row['Population'].values
    if pop.size == 0:
        return None
    else:
        return pop[0]<jupyter_output><empty_output><jupyter_text>Vervolgens voegen we de geografische ligging toe aan de oorspronkelijke dataset.<jupyter_code>import swifter
df_state = complaints_df['State']
complaints_df['Region'] = df_state.swifter.apply(give_region)
complaints_df.head()<jupyter_output><empty_output><jupyter_text>Eenmaal het aantal complaints bekend zijn kunnen we de kans berekenen dat een bepaalde response in een bepaalde regio voorkomt door de invoer van het totale bewonersaantal.<jupyter_code>region_df = pd.DataFrame(complaints_df.groupby(['Region','Response']).size())
region_df.reset_index(inplace=True)
region_df.columns = ['Region','Response','Complaints'] 
region_df['Population'] = region_df['Region'].swifter.apply(give_region_population)
region_df['Chance'] = region_df['Complaints']/region_df['Population']*100<jupyter_output><empty_output><jupyter_text>Per regio de kans op een bepaald antwoord plotten<jupyter_code>def plot(region):   
    onereg_df = region_df.loc[region_df['Region'] == region]
    sizes = onereg_df['Complaints'].tolist()
    labels = onereg_df['Response'].tolist()
    colors = ['green', 'yellow', 'blue', 'orange','red','grey','black']
    fig1, ax1 = plt.subplots(figsize=(12,6))   
    flavor_pie = ax1.pie(sizes, autopct='%1.1f%%', shadow=True, colors=colors, textprops={'fontsize': 18})
    
    centre_circle = plt.Circle((0,0),0.20,fc='white')
    fig = plt.gcf()
    fig.gca().add_artist(centre_circle)

    ax1.axis('equal')  
    plt.tight_layout()
    ax1.set_title("Chance on a certain result in the " + region)
    plt.legend(labels)

    return plt
plot("Midwest").show()   
plot("South").show() 
plot("Northeast").show() 
plot("West").show()<jupyter_output><empty_output><jupyter_text>Hieruit kunnen we besluiten dat de reacties in de verschillende regio's verschillen. Tussen het Noord-Oosten en het Westen merken we bijna een verdubbeling op van het aantal complaints die positief zijn afgesloten door het verkijgen van een geldsom.<jupyter_code>region_df = region_df.replace([np.inf, -np.inf,np.nan], 0.0)
x= region_df['Chance'].unique()
x = x[~numpy.isnan(x)]
ratio = 1/sum(x)

def change_ratio(value):
    return ratio*value

region_df['Chance'] = region_df['Chance'].swifter.apply(change_ratio)
region_df.head()
# #De responses pivoteren tot kolommen
gepivoteerd = region_df.pivot_table('Chance', ['Region'], 'Response')

#De kolommen opnieuw ordenen van meest naar minst voorkomend
gepivoteerd = gepivoteerd.reindex(['Closed with explanation', 'Closed with non-monetary relief', 'Closed with monetary relief',
                                   'Closed without explanation', 'Untimely response'], axis=1).reset_index()

#De complaints mergen met de percentages om een duidelijk overzicht te bekomen
complaints_df = pd.merge(complaints_df, gepivoteerd, on='Region', how='left')
complaints_df.to_csv(r'../data/complaints_with_region_ratio.csv', index = False)
complaints_df<jupyter_output><empty_output><jupyter_text>Extended Feature Extraction Aangezien we een predictor wensen te gebruiken die niet rechtstreeks uit de dataset te bekomen is, moeten we hier dus nog een aanvullende feature extraction gaan uitvoeren. De predictor die we wensen te bekomen uit de eerste onderliggende vraag is de sentiment score van elke klacht. Dit gaan we bekomen door een sentiment analyse over de hele dataset uit te voeren.  Sentiment Analysis Om een sentiment analyse te kunnen doen, hebben we een dataset nodig die tekstuele data bevat en waarvan elk document gelabeld is volgens de polariteit van de sentiment. Hiervoor maken we gebruik van een grote dataset die een verzameling is van 1.6M Twitter berichten. Deze tweets zijn gelabeld volgens sentiment. Vervolgens kunnen we een model trainen die dan gebruikt kan worden om voor elke klacht in de consumer complaint database een sentiment score te gaan bepalen.Deze dataset hebben we verkregen via de website van Sentiment140. Deze dataset mag gebruikt worden op voorwaarde dat de originele bron vermeld wordt.Dataset: http://help.sentiment140.com/for-students/  Data Exploration - Twitter Sentiment Analysis Dataset Om de dataset te gaan gebruiken moeten we deze eerst gaan verkennen. De dataset bevat 6 features:- **target**: de polariteit van de tweet (0 = negatief, 4 = positief)- **ids**: de index van the tweet (0 - 1599999)- **date**: de datum van de tweet (Formaat: Sat May 16 23:58:44 UTC 2009)- **flag**: de query van de tweet, indien er geen query is staat er 'NO_QUERY'- **user**: de gebruiker die de tweet geschreven heeft- **text**: de tekst van de tweetVoor ons zijn enkel de 'target' en 'text' features van belang. We gaan deze dus gaan selecteren om hierop verder te werken.<jupyter_code>filename = "../data/sentiment_analysis/input/twitter_dataset.csv"
twitter_source_df = pd.read_csv(filename, encoding="ISO-8859-1", names=["target", "ids", "date", "flag", "user", "text"])
print("Twitter dataset grootte:", twitter_source_df.shape)
twitter_source_df.head()
twitter_df = twitter_source_df[['target', 'text']].copy()
twitter_df<jupyter_output><empty_output><jupyter_text>Om de betekenis van de target beter te benadrukken vervangen we deze door de bijpassend sentiment:- 0 = NEGATIVE- 4 = POSITIVE<jupyter_code>decode_map = {0: "NEGATIVE", 4: "POSITIVE"}

def decode_sentiment(label):
    return decode_map[int(label)]

twitter_df.target = twitter_df.target.apply(lambda x: decode_sentiment(x))
twitter_df.head()<jupyter_output><empty_output><jupyter_text>Vervolgens gaan we de distributie van de tweets gaan visualiseren. We kunnen vaststellen dat de dataset perfect gebalanceerd is met de helft positieve en de andere helft negatieve tweets.<jupyter_code>from collections import Counter

target_count = Counter(twitter_df.target)

plt.figure(figsize=(3,3))
plt.bar(target_count.keys(), target_count.values())
plt.title("Twitter dataset sentiment distributie")<jupyter_output><empty_output><jupyter_text>Data Preprocessing - Twitter Sentiment Analysis Dataset Om de tekst van de tweets bruikbaar te maken gaan we een analoge processing toepassen als bij de consumer complaint narrative.Hierbij gaan we als volgt te werk:1. Verwijder alle gebruikersnamen (@...), URL's en overbodige whitespace2. Verwijder alle stopwoorden3. Optioneel: converteer alle woorden naar hun stamvorm (we hebben vastgesteld dat dit echter slechtere resultaten geeft voor het model)<jupyter_code>nltk.download('stopwords')
import nltk
from nltk.corpus import stopwords
from nltk.stem import SnowballStemmer

stop_words = stopwords.words("english")
stemmer = SnowballStemmer("english")
TEXT_CLEANING_RE = "@\S+|https?:\S+|http?:\S|[^A-Za-z0-9]+"

def twitter_processing(text, stem=False):
    text = re.sub(TEXT_CLEANING_RE, ' ', str(text).lower()).strip()
    tokens = []
    for token in text.split():
        if token not in stop_words:
            if stem:
                tokens.append(stemmer.stem(token))
            else:
                tokens.append(token)
    return " ".join(tokens)
%%time

import swifter

twitter_df['tokens'] = twitter_df['text'].swifter.apply(twitter_processing)
twitter_df<jupyter_output><empty_output><jupyter_text>Feature Extraction: Word2Vec - Twitter Sentiment Analysis Dataset Om de features uit de tekst te halen gaan we een Word2Vec model ontwikkelen. Om nadien de accuraatheid te kunnen evalueren splitsen we de dataset op in een training (0.8) en test dataset (0.2). Er wordt een random state variabele ingesteld om consistente resultaten te krijgen bij het trainen en testen van het model.<jupyter_code>from sklearn.model_selection import train_test_split

TRAIN_SIZE = 0.8

df_train, df_test = train_test_split(twitter_df, test_size=1-TRAIN_SIZE, random_state=42)
print("Training dataset grootte:", df_train.shape)
print("Test dataset grootte:", df_test.shape)<jupyter_output>Training dataset grootte: (1280000, 3)
Test dataset grootte: (320000, 3)<jupyter_text>Word2Vec gaat de tweets omzetten naar 'word embeddings'. Dit is een vector representatie van de corpus van elke tweet. Deze woord vectoren kunnen in een vector ruimte voorgesteld worden waardoor het algoritme sterk gerelateerde woorden kan gaan detecteren op basis van hoe sterk de context van de woorden in de corpus van de documenten overeenkomt.We beginnen met de training dataset in te lezen naar Word2Vec waardoor deze de documenten kan gebruiken om een vocabularium te genereren. We maken hier opnieuw gebruik van multithreading om het proces te versnellen.<jupyter_code># pip install gensim
import gensim
import multiprocessing
from gensim.models import Word2Vec

W2V_SIZE = 300
W2V_WINDOW = 7
W2V_MIN_COUNT = 10

w2v_model = Word2Vec(size=W2V_SIZE, window=W2V_WINDOW, min_count=W2V_MIN_COUNT, workers=multiprocessing.cpu_count())
documents = [_text.split() for _text in df_train.tokens] 
w2v_model.build_vocab(documents)

vocab = w2v_model.wv.vocab.keys()
print("Aantal woorden in vocabularium:", len(vocab))<jupyter_output>Aantal woorden in vocabularium: 30369<jupyter_text>Vervolgens gaan we het Word2Vec model gaan trainen op deze documenten. We gaan hiervoor 32 keer (epochs) doorheen de training dataset.<jupyter_code>%%time

W2V_EPOCH = 32

w2v_model.train(documents, total_examples=len(documents), epochs=W2V_EPOCH)<jupyter_output>Wall time: 2min 56s<jupyter_text>Om de werking van het Word2Vec model te illustreren, gaan we hier enkele gerelateerde woorden uitschrijven van een gegeven positief en negatief woord.<jupyter_code>w2v_model.wv.most_similar("good")
w2v_model.wv.most_similar("bad")<jupyter_output><empty_output><jupyter_text>De gerelateerde woorden liggen vooral binnen dezelfde sentiment klasse. Toch valt het op dat er ook nog woorden uit de andere klasse als gerelateerd naar boven komen, dit is wel verwacht aangezien Word2Vec betekenis geeft aan de woorden op basis van de context en deze woorden vaak elkaar kunnen vervangen in dezelfde zin. Dit zou echter geen probleem mogen geven bij het trainen van ons neuraal netwerk aangezien dit slechts uitzonderingen zijn. Als tweede illustratie gaan we vragen aan het model om het woord dat niet in het rijtje past eruit te filteren. Dit werkt ook goed.<jupyter_code>w2v_model.wv.doesnt_match("breakfast cereal dinner lunch".split())<jupyter_output><empty_output><jupyter_text>Deep Learning Model - Twitter Sentiment Analysis Dataset<jupyter_code># pip install keras
from keras.preprocessing.text import Tokenizer

tokenizer = Tokenizer()
tokenizer.fit_on_texts(df_train.tokens)

word_count = len(tokenizer.word_index) + 1
print("Totaal aantal woorden:", word_count)<jupyter_output>Using TensorFlow backend.<jupyter_text>Om een neuraal netwerk te bekomen, moeten we nu de tokens van de tekst in sequenties gaan onderverdelen waarvan het totaal aantal karakters gelijk is aan het aantal input neuronen van de eerste laag van ons netwerk. We kiezen voor deze laag een input dimensie van 300 neuronen. Documenten die korter zijn dan 300 karakters worden hierbij aangevuld door padding toe te voegen. Ter info: de lengte van een tweet is maximaal 280 karakters, dus is hier een marge in rekening gebracht.<jupyter_code>from keras.preprocessing.sequence import pad_sequences

SEQUENCE_LENGTH = 300

x_train = pad_sequences(tokenizer.texts_to_sequences(df_train.tokens), maxlen=SEQUENCE_LENGTH)
x_test = pad_sequences(tokenizer.texts_to_sequences(df_test.tokens), maxlen=SEQUENCE_LENGTH)<jupyter_output><empty_output><jupyter_text>De uitvoer van ons neuraal netwerk is de sentiment score en dit is een waarde tussen 0 en 1. Om de target (y) in te stellen moeten we deze gaan transformeren aan de hand van een LabelEncoder. Hiermee worden de waarden 'NEGATIVE' en 'POSITIVE' omgezet naar 0 of 1. Dit doen we zowel bij de targets van de training als bij de test dataset.<jupyter_code>from sklearn.preprocessing import LabelEncoder

encoder = LabelEncoder()
encoder.fit(df_train.target.tolist())

y_train = encoder.transform(df_train.target.tolist())
y_test = encoder.transform(df_test.target.tolist())

y_train = y_train.reshape(-1,1)
y_test = y_test.reshape(-1,1)

print("x_train", x_train.shape)
print("y_train", y_train.shape)
print("x_test", x_test.shape)
print("y_test", y_test.shape)<jupyter_output>x_train (1280000, 300)
y_train (1280000, 1)
x_test (320000, 300)
y_test (320000, 1)<jupyter_text>Als we nu de waarden van de target gaan uitschrijven krijgen we inderdaad enkel nog de waarden 0 of 1.<jupyter_code>print(y_train[:])<jupyter_output>[[1]
 [1]
 [1]
 ...
 [0]
 [0]
 [0]]<jupyter_text>Om het Word2Vec model te gebruiken in ons neuraal netwerk gaan we als eerste laag een Embedding layer gebruiken. Deze laag representeert de bekomen matrix van ons Word2Vec model en heeft dus dezelfde dimensies. Deze laag is dan ook niet trainbaar aangezien deze al opvoorhand aan de hand van Word2Vec getraind is.<jupyter_code>from keras.layers import Embedding

embedding_matrix = np.zeros((word_count, W2V_SIZE))

for word, i in tokenizer.word_index.items():
    if word in w2v_model.wv:
        embedding_matrix[i] = w2v_model.wv[word]
print(embedding_matrix.shape)

embedding_layer = Embedding(word_count, W2V_SIZE, weights=[embedding_matrix], input_length=SEQUENCE_LENGTH, trainable=False)<jupyter_output>(290419, 300)<jupyter_text>Vervolgens gaan we ons neuraal netwerk gaan construeren. Het netwerk is opgebouwd uit 4 lagen:1. **Embedding layer**: input laag, gebruikt voor het mappen van de Word2Vec word embeddings.2. **Dropout layer**: deze laag zou het overfitten van het netwerk moeten reduceren doordat er willekeurig uitvoer van de vorige laag weggelaten wordt (regulariseren), op die manier verhinderen we dat het netwerk ongewenste patronen tussen de neuronen zou gaan leren.3. **LSTM layer**: Long short-term memory laag heeft feedback verbindingen (terugkoppeling) met vorige lagen (wat niet het geval is bij standaard feed-forward netwerken), op die manier kunnen we sequenties van data (tekst) beter gaan verwerken. In deze laag wordt ook dropout toegepast om overfitting tegen te gaan.4. **Dense layer**: output laag, deze laag is een dense laag met slechts 1 uitvoer paramater die de sentiment score gaat voorstellen. Als activatie functie gebruiken we een sigmoid functie waardoor de uitvoer tussen 0 en 1 ligt. Voor negatieve sentiment zal deze waarde dichter bij 0 liggen en voor positieve sentiment dichter bij 1.<jupyter_code>from keras.layers import Dense, Dropout, LSTM
from keras.models import Sequential

model = Sequential()
model.add(embedding_layer)
model.add(Dropout(0.5))
model.add(LSTM(100, dropout=0.2, recurrent_dropout=0.2))
model.add(Dense(1, activation='sigmoid'))

model.summary()<jupyter_output>Model: "sequential_2"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding_2 (Embedding)      (None, 300, 300)          87125700  
_________________________________________________________________
dropout_1 (Dropout)          (None, 300, 300)          0         
_________________________________________________________________
lstm_1 (LSTM)                (None, 100)               160400    
_________________________________________________________________
dense_1 (Dense)              (None, 1)                 101       
=================================================================
Total params: 87,286,201
Trainable params: 160,501
Non-trainable params: 87,125,700
_________________________________________________________________<jupyter_text>Vervolgens gaan we het model gaan compileren. Aangezien onze targets binair zijn (0 of 1) gaan we een binaire loss functie gaan gebruiken. Als optimizer gaan we het "Adam" algoritme gebruiken die een standaard learning rate van 0.001 heeft. Als metrics van het model willen we de accuracy weten.<jupyter_code>model.compile(loss='binary_crossentropy', optimizer="adam", metrics=['accuracy'])<jupyter_output><empty_output><jupyter_text>Om inzicht te krijgen in de inwendige state van het model tijdens het trainen, gaan we enkele callbacks gaan toevoegen. Deze zullen periodiek enkele statistieken uitprinten en  parameters tunen (learning rate). Ook is het mogelijk om het trainen op die manier vroegtijdig te stoppen indien deze geen winst meer boekt.- **ReduceLROnPlateau**: deze callback verlaagt de learning rate zodra de loss niet meer verandert.- **EarlyStopping**: deze callback stopt de training wanneer de accuracy niet meer verbetert.<jupyter_code>from keras.callbacks import ReduceLROnPlateau, EarlyStopping

callbacks = [ReduceLROnPlateau(monitor='val_loss', patience=5, cooldown=0), EarlyStopping(monitor='val_accuracy', min_delta=1e-4, patience=5)]<jupyter_output><empty_output><jupyter_text>Nu gaan we het model effectief gaan trainen. Om dit proces te versnellen gaan we gebruik maken van de CUDA bibliotheek van Nvidia. Hiervoor hebben we de vereiste drivers geinstalleerd. Vervolgens testen we of tensorflow (waarrond Keras gebouwd is) effectief de grafische kaart gaat aanspreken.<jupyter_code>from tensorflow.python.client import device_lib

device_lib.list_local_devices()<jupyter_output><empty_output><jupyter_text>Nu Keras gebruik kan maken van onze GPU gaan we het model gaan trainen op onze train dataset met de fit functie. We gaan 8 keer over de dataset gaan (epochs) en gebruiken een batch grootte van 1024. Dit blijkt een goede afweging te zijn tussen het vereiste geheugen om het model te trainen en de snelheid waarmee het model traint. Om de train dataset te valideren gaan we deze verder opsplitsen in een derde validatie dataset (0.10).<jupyter_code>%%time

EPOCHS = 8
BATCH_SIZE = 1024

history = model.fit(x_train, y_train, batch_size=BATCH_SIZE, epochs=EPOCHS, validation_split=0.1, verbose=1, callbacks=callbacks)<jupyter_output>Train on 1152000 samples, validate on 128000 samples
Epoch 1/8
1152000/1152000 [==============================] - 297s 257us/step - loss: 0.5084 - accuracy: 0.7472 - val_loss: 0.4659 - val_accuracy: 0.7787
Epoch 2/8
1152000/1152000 [==============================] - 296s 257us/step - loss: 0.4832 - accuracy: 0.7641 - val_loss: 0.4568 - val_accuracy: 0.7837
Epoch 3/8
1152000/1152000 [==============================] - 296s 257us/step - loss: 0.4759 - accuracy: 0.7694 - val_loss: 0.4534 - val_accuracy: 0.7862
Epoch 4/8
1152000/1152000 [==============================] - 296s 257us/step - loss: 0.4721 - accuracy: 0.7718 - val_loss: 0.4505 - val_accuracy: 0.7883
Epoch 5/8
1152000/1152000 [==============================] - 295s 256us/step - loss: 0.4694 - accuracy: 0.7737 - val_loss: 0.4482 - val_accuracy: 0.7895
Epoch 6/8
1152000/1152000 [==============================] - 296s 257us/step - loss: 0.4679 - accuracy: 0.7744 - val_loss: 0.4469 - val_accuracy: 0.7893
Epoch 7/8
1152000/1152000 [==[...]<jupyter_text>Model Evaluation - Twitter Sentiment Analysis Dataset Nu we een getraind model hebben gaan we deze gaan evalueren. Dit gaan we doen door een evaluatie score te bepalen van ons model aan de hand van de test dataset.<jupyter_code>score = model.evaluate(x_test, y_test, batch_size=BATCH_SIZE)

print("Model accuracy:", score[1])
print("Model loss:", score[0])<jupyter_output>320000/320000 [==============================] - 31s 96us/step
Model accuracy: 0.7908218502998352
Model loss: 0.4448554313659668<jupyter_text>Vervolgens gaan we onderzoeken of er eventueel overfitting of underfitting gebeurd is. Hiervoor plotten we de accuracy en loss van het trainingsproces doorheen de verschillende epochs.<jupyter_code>acc = history.history['accuracy']
val_acc = history.history['val_accuracy']
loss = history.history['loss']
val_loss = history.history['val_loss']
 
epochs = range(len(acc))
 
plt.plot(epochs, acc, 'b', label='Training acc')
plt.plot(epochs, val_acc, 'r', label='Validation acc')
plt.title('Training en validation accuracy')
plt.legend()
 
plt.figure()
 
plt.plot(epochs, loss, 'b', label='Training loss')
plt.plot(epochs, val_loss, 'r', label='Validation loss')
plt.title('Training en validation loss')
plt.legend()
 
plt.show()<jupyter_output><empty_output><jupyter_text>De accuracy en loss van de validatie dataset volgt een zeer gelijkaardig trainingsverloop als dat van de train dataset. We kunnen besluiten dat er geen overfitting of underfitting is gebeurd. Om het model verder te gaan evalueren gaan we een confusion matrix construeren. Deze visualisatie geeft een goed beeld van hoe goed het model werkt.<jupyter_code>import itertools
from sklearn.metrics import confusion_matrix

def decode_sentiment(score):
    return "NEGATIVE" if score < 0.5 else "POSITIVE"

def plot_confusion_matrix(cm, classes, title='Confusion Matrix', cmap=plt.cm.Blues):
    cm = cm.astype('float') / cm.sum(axis=1)[:, np.newaxis]

    plt.imshow(cm, interpolation='nearest', cmap=cmap)
    plt.title(title, fontsize=30)
    plt.colorbar()
    tick_marks = np.arange(len(classes))
    plt.xticks(tick_marks, classes, rotation=90, fontsize=22)
    plt.yticks(tick_marks, classes, fontsize=22)

    fmt = '.2f'
    thresh = cm.max() / 2.
    for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])):
        plt.text(j, i, format(cm[i, j], fmt), horizontalalignment="center", color="white" if cm[i, j] > thresh else "black")
        
    plt.ylabel('Verwacht label', fontsize=25)
    plt.xlabel('Voorspeld label', fontsize=25)

y_pred_1d = []    
y_test_1d = list(df_test.target)
    
scores = model.predict(x_test, verbose=1, batch_size=1024)
y_pred_1d = [decode_sentiment(score) for score in scores]
    
cnf_matrix = confusion_matrix(y_test_1d, y_pred_1d)

plt.figure(figsize=(5,5))
plot_confusion_matrix(cnf_matrix, classes=df_train.target.unique(), title="Confusion Matrix")
plt.show()
from sklearn.metrics import classification_report

print(classification_report(y_test_1d, y_pred_1d))
from sklearn.metrics import accuracy_score

print(accuracy_score(y_test_1d, y_pred_1d))<jupyter_output>0.790821875<jupyter_text>Nu we het model geëvalueerd hebben en we tevreden zijn met de behaalde accuracy, gaan we het model gaan exporteren zodat deze later opnieuw kan ingeladen worden. Dit is nuttig aangezien het trainen van het model heel wat tijd in beslag neemt en op die manier kan deze stap dus eenvoudig overgeslagen worden in de toekomst bij het verder ontwikkelen.<jupyter_code>KERAS_MODEL = "../data/sentiment_analysis/model/model.h5"
WORD2VEC_MODEL = "../data/sentiment_analysis/model/model.w2v"

model.save(KERAS_MODEL)
w2v_model.save(WORD2VEC_MODEL)<jupyter_output><empty_output><jupyter_text>Predicting Complaints Sentiment Nu kunnen we terug gaan naar onze Customer Complaint Dataset. Hier gaan we nu voor elke klacht een sentiment score gaan berekenen aan de hand van ons deep learning model. Deze score gaan we gaan toevoegen aan onze oorspronkelijke dataset zodat deze verder gebruikt kan worden om een classificatie model te ontwikkelen die antwoord kan geven op onze centrale onderzoeksvraag. We laden eerst de opgeslagen modellen terug in het geheugen.<jupyter_code>from keras.models import load_model 
from gensim.models import Word2Vec 

w2v_model = Word2Vec.load('../data/sentiment_analysis/model/model.w2v')
model = load_model('../data/sentiment_analysis/model/model.h5')<jupyter_output><empty_output><jupyter_text>Nu gaan we voor alle klachten de sentiment score gaan bepalen en deze vervolgens toevoegen aan de oorspronkelijke dataset. We gaan hiervoor de oorspronkelijke narrative gaan gebruiken aangezien dit betere resultaten blijkt te geven dan indien we de processed narrative zouden gebruiken. Dit heeft waarschijnlijk te maken met het feit dat we voor de processing op het Word2Vec model gekozen hebben om geen stemming te gebruiken aangezien dit een hogere accuracy gaf.<jupyter_code>SEQUENCE_LENGTH = 300 

def predict(text):
    x_test = pad_sequences(tokenizer.texts_to_sequences([text]), maxlen=SEQUENCE_LENGTH)
    score = model.predict([x_test])[0]
    return score[0]
import swifter

complaints_df['Sentiment_Score'] = complaints_df['Narrative'].swifter.apply(predict)

complaints_df<jupyter_output><empty_output><jupyter_text>We geven de verdeling van de klachten weer in een histogram.<jupyter_code>sns.distplot(complaints_df['Sentiment_Score'])<jupyter_output><empty_output><jupyter_text>We kunnen hieruit duidelijk afleiden dat de klachten voornamelijk negatief zijn, wat dus ook te verwachten was van klachten. Dit bevestigt dat het model voor onze sentiment analyse naar behoren werkt. We gaan nu deze verdeling eens verder gaan onderzoeken door een negatieve klacht en een "positieve" klacht te gaan uitlezen. We verdelen daarom de scores in meerdere kwantielen en lezen een klacht uit het eerste en laatste kwantiel uit.<jupyter_code>complaints_quantile_df['Sentiment_Quantile'] = pd.qcut(complaints_df['Sentiment_Score'], q=[0, .2, .4, .6, .8, 1], labels=False, precision=0)<jupyter_output><empty_output><jupyter_text>**Negatieve klacht:**<jupyter_code>list(complaints_quantile_df[complaints_quantile_df['Sentiment_Quantile'].isin(['1'])].sample(1).Narrative)<jupyter_output><empty_output><jupyter_text>**Positieve klacht:**<jupyter_code>list(complaints_quantile_df[complaints_quantile_df['Sentiment_Quantile'].isin(['4'])].sample(1).Narrative)<jupyter_output><empty_output><jupyter_text>Nu we de sentiment score voor alle klachten bepaald hebben gaan we de volledige dataset gaan opslaan zodat deze later opnieuw kan ingeladen worden.<jupyter_code>complaints_df.to_csv('../data/complaints_with_sentiment_score.csv')<jupyter_output><empty_output><jupyter_text>Classification Model Nu we alle predictors bepaald hebben kunnen we het classificatie model gaan ontwikkelen. - **Predictors**: Sentiment_Score, Product, Company_Ratio (5x), Region_Ratio (5x)- **Target**: Company response to consumer- **Sampling rate** (training/testing): 80/20 We laden eerste alle hierboven berekende features in het geheugen door de verschillende dataframes samen te mergen.<jupyter_code>complaints_company_df = pd.read_csv('../data/complaints_with_company_ratio.csv', delimiter=',')
complaints_sentiment_df = pd.read_csv('../data/complaints_with_sentiment_score.csv', delimiter=',')
complaints_regio_df = pd.read_csv('../data/complaints_with_region_ratio.csv', delimiter=',')
complaints_sentiment_df.replace({'Company response to consumer': {'Closed': 'Closed without explanation'}}, inplace= True)
complaints_sentiment_df = complaints_sentiment_df[complaints_sentiment_df['Company response to consumer'].isin(
    ['Closed with non-monetary relief',
    'Closed with monetary relief', 'Closed with explanation',
    'Closed without explanation', 'Untimely response'])]<jupyter_output><empty_output><jupyter_text>Om zeker te zijn dat we alle datasets kunnen mergen, bekijken we of deze evenveel rijen bevatten.<jupyter_code>print(complaints_company_df.shape)
print(complaints_sentiment_df.shape)
print(complaints_regio_df.shape)<jupyter_output>(485699, 12)
(485699, 20)
(485699, 12)<jupyter_text>Vervolgens mergen we de verschillende datasets op basis van hun Complaint ID.<jupyter_code># Sentiment scores toevoegen
merged_df = pd.merge(complaints_company_df, complaints_sentiment_df[['Complaint ID', 'Narrative sentiment score']], on='Complaint ID')
merged_df = merged_df.drop('Unnamed: 0', 1)

# Regio historische ratio's toevoegen
merged_df = pd.merge(merged_df, complaints_regio_df[['Complaint ID', 'Region','Closed with explanation', 'Closed with non-monetary relief', 'Closed with monetary relief', 'Closed without explanation', 'Untimely response']],how='left', on='Complaint ID')

# Product ID genereren
merged_df['Product_Id'] = merged_df['Product'].factorize()[0]

merged_df.head()<jupyter_output><empty_output><jupyter_text>1. Imbalanced Dataset Eerst gaan we ons model trainen op de ongebalanceerde dataset. Zoals eerder vastgesteld is de dataset erg ongebalanceerd, daarom zullen we ook het verschil onderzoeken indien we een gebalanceerde dataset gebruiek<jupyter_code>from sklearn.metrics import confusion_matrix
from sklearn.model_selection import train_test_split
from sklearn.svm import LinearSVC

feature_names = ['Product_Id', 'Closed with explanation_x', 'Closed with non-monetary relief_x', 'Closed with monetary relief_x',
                'Closed without explanation_x', 'Untimely response_x','Closed with explanation_y', 'Closed with non-monetary relief_y', 'Closed with monetary relief_y',
                'Closed without explanation_y', 'Untimely response_y', 'Narrative sentiment score']

X = merged_df[feature_names]
y = merged_df['Response']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

print(X_train.shape)
print(y_train.shape)

print(X_test.shape)
print(y_test.shape)
model = LinearSVC()
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
from sklearn import metrics
from sklearn.metrics import accuracy_score

print(metrics.classification_report(y_test, y_pred, target_names= merged_df['Response'].unique()))
print(accuracy_score(y_pred, y_test))
labels = ['Closed with explanation', 'Closed with non-monetary relief', 'Closed with monetary relief',
                'Closed without explanation', 'Untimely response']

conf_mat = confusion_matrix(y_test, y_pred, labels=labels)
fig, ax = plt.subplots(figsize=(8,8))
sns.heatmap(conf_mat, annot=True, cmap="Blues", fmt='d', xticklabels=labels, yticklabels=labels)
plt.ylabel('Correcte response')
plt.xlabel('Voorspelde response')
plt.title("Confusion Matrix", size=16);<jupyter_output><empty_output><jupyter_text>We zien hier duidelijk dat de grootste categorie "Closed with explanation" de beste precisie heeft. Ook een kleinere categorie zoals "Untimely response" wordt hier redelijk goed voorspeld. De meeste fouten zijn te wijten aan het verkeerd voorspellen van de categorie "Closed with explanation" die foutief voorspeld wordt als "Closed with non-monetary relief".  2. Balanced Dataset Hier gaan we analoog tewerk als bij de niet gebalanceerde dataset. We gaan eerst de dataset gaan balanceren waarbij er uit elke categorie maximaal 120000 samples kunnen voorkomen.<jupyter_code>def sampling_k_elements(category, k=120000):
    if len(category) < k:
        return category
    return category.sample(k)

complaints_balanced_df = merged_df.groupby('Response').apply(sampling_k_elements).reset_index(drop=True)
complaints_balanced_df.shape
fig = plt.figure(figsize=(10, 5))
pd.DataFrame({'Count': complaints_balanced_df.groupby(['Response']).size()}).plot.barh(title="Aantal klachten per responscategorie", legend=None)
X_bal = complaints_balanced_df[feature_names]
y_bal = complaints_balanced_df['Response']
X_bal_train, X_bal_test, y_bal_train, y_bal_test = train_test_split(X_bal, y_bal, test_size=0.2)

print(X_bal_train.shape)
print(y_bal_train.shape)

print(X_bal_test.shape)
print(y_bal_test.shape)
model_bal = LinearSVC()
model_bal.fit(X_bal_train, y_bal_train)
y_bal_pred = model.predict(X_bal_test)
print(metrics.classification_report(y_bal_test, y_bal_pred, target_names= complaints_balanced_df['Response'].unique()))
print(accuracy_score(y_bal_pred, y_bal_test))
conf_mat_bal = confusion_matrix(y_bal_test, y_bal_pred, labels=labels)
fig, ax = plt.subplots(figsize=(8,8))
sns.heatmap(conf_mat_bal, annot=True, cmap="Blues", fmt='d', xticklabels=labels, yticklabels=labels)
plt.ylabel('Correcte response')
plt.xlabel('Voorspelde response')
plt.title("Confusion Matrix", size=16);<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.09412676541508784, 'fraction_numerical': 0.0183861522562866, 'mean_word_length': 5.913082303914273, 'pattern_counts': {'":': 0, '<': 198, '<?xml version=': 0, '>': 198, 'https://': 1, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '52', 'dir': 'jupyter-structured-clean-dedup', 'id': '388303', 'license': 'MIT', 'n_tokens_mistral': 15831, 'n_tokens_neox': 14998, 'n_words': 4683, 'path': 'notebooks/sprint3_sentiment_analysis/sprint3.ipynb', 'repo_name': 'WouterStemgee/ugent_applied-machine-learning-consumer-complaints-database', 'size': '46477'}