<jupyter_start><jupyter_text>![](http://sigdelta.com/assets/images/sages-sd-logo.png) Analiza danych i uczenie maszynowe w PythonAutor notebooka: <NAME>. Dane tekstoweW tym notebooku prezentujemy podejście do analizy, wektoryzacji danych i uczenia maszynowego z użyciem scikit-learn i innych pakietów w Python. Kroki te są wstępem do uczenia maszynowego.<jupyter_code>%matplotlib inline
import pandas as pd
import numpy as np
import os
import glob
import matplotlib as mpl

# Parametry wykresów
mpl.style.use('ggplot')
mpl.rcParams['figure.figsize'] = (8,6)
mpl.rcParams['font.size'] = 12<jupyter_output><empty_output><jupyter_text>Macierz Term-Document[Macierz Term-Document (TD)](https://en.wikipedia.org/wiki/Document-term_matrix), jest to macierz, w której kolumny stanowią unikalne słowa, a wiersze stanowią wartości ile razy dane słowo wystąpiło w dokumencie. Dla przykładu (za Wikipedią), jeżeli mamy dwa dokumenty:* D1 = "I like databases"* D2 = "I hate databases"to macierz TD możemy przedstawić jako|    | I | like | hate | databases ||----|---|------|------|-----------|| **D1** | 1 | 1    | 0    | 1         || **D2** | 1 | 0    | 1    | 1         |Poniżej ten sam przykład zrealizowany w Pandas:<jupyter_code>sample = pd.DataFrame({
    'docs': ['D1', 'D2'],
    'lines': ['I like databases Databases', 'I hate databases']
})
sample
sample['words'] = sample.lines.str.strip().str.lower().str.split('[\W_]+')
sample
rows = list()
for row in sample[['docs', 'words']].iterrows():
    r = row[1]
    for word in r.words:
        rows.append((r.docs, word))

words = pd.DataFrame(rows, columns=['docs', 'word'])
words
words.pivot_table(index='docs', 
                  columns='word', 
                  aggfunc=lambda v: v['word'].count())\
    .fillna(0)<jupyter_output><empty_output><jupyter_text>Zadanie1. Używając Pandas obliczyć macierz TD dla książek.1. Co może wpłynąć na wynik?<jupyter_code>from sklearn.feature_extraction.text import CountVectorizer

count_vectorizer = CountVectorizer()
count_vectorizer
sample['lines']
X = count_vectorizer.fit_transform(sample['lines'])
X
print(X)
X.toarray()
count_vectorizer.get_feature_names()
pd.DataFrame(X.toarray(), 
             columns=count_vectorizer.get_feature_names(), 
             index=sample['docs'])<jupyter_output><empty_output><jupyter_text>TF-IDFKolejną techniką jest poznane uprzednio [Term Frequency–Inverse Document Frequency (TF-IDF)](https://en.wikipedia.org/wiki/Tf%E2%80%93idf). Jest on popularnym algorytmem do analizy danych tekstowych, używany dość często w pozyskiwaniu danych (data mining).Poniżej przykładowe obliczenie TF-IDF z użyciem wektoryzera scikit-learn.<jupyter_code>from sklearn.feature_extraction.text import TfidfVectorizer

tfidf_vectorizer = TfidfVectorizer()
tfidf_vectorizer
X = tfidf_vectorizer.fit_transform(sample['lines'])
X
X.toarray()
tfidf_vectorizer.get_feature_names()
pd.DataFrame(X.toarray(), 
            columns=tfidf_vectorizer.get_feature_names(), 
            index=sample['docs'])<jupyter_output><empty_output><jupyter_text><NAME>obieństwaJest wiele miar podobieństwa, które liczą jak odległe są pary wartości od siebie. Miary mogą być używane zarówno do wektorów oraz macierzy, jak i słów oraz dokumentów; zobacz [ciekawy opis](http://dataconomy.com/2015/04/implementing-the-five-most-popular-similarity-measures-in-python/) miar wraz z implementacją w Pythonie. Pierwszą miarą, która jest standardową miarą podobieństwa dwóch stringów jest [miara Levenshteina](https://en.wikipedia.org/wiki/Levenshtein_distance). Jest wiele implementacji używania tej miary, niemniej, najłatwiej dostępną implementacją jest zawarta w [difflib](https://docs.python.org/3/library/difflib.html), która jest częścią standardowej biblioteki Pythona.<jupyter_code>import difflib 

def similarity(a, b):
    return difflib.SequenceMatcher(None, a, b).ratio()

similarity('cat', 'cats')
similarity('cat', 'dog')
sample
similarity(sample.at[0, 'lines'], sample.at[1, 'lines'])
similarity('cat', 'catepillar')<jupyter_output><empty_output><jupyter_text>W przypadku dokumentów tekstowych, znane są przynajmniej dwie popularne miary:* cosinusowa* Jaccarda**<NAME>** zdefiniowana jest jako kąt między dwoma wektorami:$$sim(A, B) = \cos(\Theta) = \frac{A \cdot B}{\Vert A \Vert \cdot \Vert B \Vert}$$Zatem miara wymaga formy zwektowyzowanej do obliczenia wartości; musimy się najpierw posłużyć którymś wektoryzatorem aby otrzymać macierz a potem policzyć miarę.<jupyter_code>X = count_vectorizer.fit_transform(sample['lines'])
from sklearn.metrics.pairwise import cosine_similarity

cosine_similarity(X)<jupyter_output><empty_output><jupyter_text>**<NAME>** operuje z kolei na zbiorach i zdefiniowana jest jako:$$sim(A, B) = \frac{|A \cap B|}{| A \cup B |}$$Zatem dla naszego przykładu wygląda to następująco:<jupyter_code>A = sample.words[0]
B = sample.words[1]
A, B
def sim_jaccard(A, B):
    a = set(A)
    b = set(B)
    i = set.intersection(a, b)
    u = set.union(a, b)
    print(i, u)
    return len(i)/len(u)

sim_jaccard(A, B)
from sklearn.metrics import jaccard_similarity_score

list(set(A))
jaccard_similarity_score(list(set(A)), list(set(B)))<jupyter_output><empty_output><jupyter_text>Transformatory i pipelinyScikit-learn wprowadził szereg ułatwień do przetwarzania danych i tworzenia modeli. W ogólności, możemy korzystać z 3 podstawowych elementów:* [transformer](http://scikit-learn.org/stable/modules/generated/sklearn.base.TransformerMixin.html)* [estimator](http://scikit-learn.org/stable/modules/generated/sklearn.base.BaseEstimator.html)* [pipeline](http://scikit-learn.org/stable/modules/generated/sklearn.pipeline.Pipeline.htmlsklearn.pipeline.Pipeline)Transformetry (funkcje zmieniające dane) i estymatory (modele do wyuczenia) łączy się w pipeliny; więcej o tym można przeczytać [w dokumentacji](http://scikit-learn.org/stable/modules/pipeline.html).<jupyter_code>from sklearn.feature_extraction.text import TfidfTransformer

tfidf_transformer = TfidfTransformer()
tfidf_transformer
td = CountVectorizer().fit_transform(sample['lines'])
tfidf_transformer.fit_transform(td)
from sklearn.pipeline import Pipeline, make_pipeline
tfidf_pipeline = make_pipeline(CountVectorizer(), TfidfTransformer())
tfidf_pipeline
X = tfidf_pipeline.fit_transform(sample['lines'])
X.toarray()
pd.DataFrame(X.toarray(), 
             index=sample['docs'], 
             columns=tfidf_pipeline.steps[0][1].get_feature_names())<jupyter_output><empty_output><jupyter_text>Można też samemu nazywać elementy pipelinu:<jupyter_code>list(steps.items())
steps = {
    'count_vect': CountVectorizer(),
    'tfidf_trans': TfidfTransformer()
}

# Pipeline oczekuje kroków jako listy z krotkami (nazwa, obiekt)
tfidf_pipeline = Pipeline(list(steps.items()))
tfidf_pipeline
tfidf_pipeline.steps
tfidf_pipeline.fit_transform(sample['lines'])
# Co tu się dzieje?
steps['count_vect'].get_feature_names()<jupyter_output><empty_output><jupyter_text>Zadanie1. Wykorzystaj narzędzia scikit-learn do stworzenie pełnego pipelinu wykonującego wektoryzację z TF-IDF dla danych z książek.  Funkcje użytkownikaNiekiedy mamy potrzebę korzystania z funkcji wybiegających poza zestaw dostępny w scikit-learn. Są dwie metody:* [FunctionTransformer](http://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.FunctionTransformer.htmlsklearn.preprocessing.FunctionTransformer)* Klasa dziedzicząca po [BaseEstimator i TransformerMixin](http://scikit-learn.org/stable/auto_examples/hetero_feature_union.htmlsphx-glr-auto-examples-hetero-feature-union-py)Obecnie najpierw zalecany jest FunctionTransformer.<jupyter_code>import re
import numpy as np

@np.vectorize
def replace_database(linia):
    return re.sub('database', 'DB', linia, flags=re.IGNORECASE)

replace_database(sample['lines'])
from sklearn.preprocessing import FunctionTransformer

replace_func = FunctionTransformer(replace_database, validate=False)
replace_func.fit_transform(sample['lines'])
new_pipeline = make_pipeline(replace_func, TfidfVectorizer())
new_pipeline
X = new_pipeline.fit_transform(sample['lines'])
X
pd.DataFrame(X.toarray(), 
             index=sample['docs'], 
             columns=new_pipeline.steps[1][1].get_feature_names())<jupyter_output><empty_output><jupyter_text>Drugą metodą tworzenia funkcji użytkownika jest stworzenie klasy, która dziedziczy po klasach `BaseEstimator` i `TransformerMixin`, czyli de facto jak to jest robione dla innych transformerów.<jupyter_code>from sklearn.base import BaseEstimator, TransformerMixin

class ReplaceDatabaseTransformer(BaseEstimator, TransformerMixin):
    def __init__(self, replace_from='database', replace_to='DB'):
        self.replace_from = replace_from
        self.replace_to = replace_to

    def fit(self, x, y=None):
        return self

    def _replace_str(self, line):
        return re.sub(self.replace_from, 
                      self.replace_to, 
                      line, 
                      flags=re.IGNORECASE)
    
    def transform(self, data):
        func = np.vectorize(lambda line: self._replace_str(line))
        return func(data)
new_pipeline2 = make_pipeline(ReplaceDatabaseTransformer(), TfidfVectorizer())
new_pipeline2
X = new_pipeline2.fit_transform(sample['lines'])
X
pd.DataFrame(X.toarray(), 
             index=sample['docs'], 
             columns=new_pipeline2.steps[1][1].get_feature_names())<jupyter_output><empty_output><jupyter_text>Zadanie1. Napisz funkcję zamieniającą wszystkie napotkane [URLe](https://pl.wikipedia.org/wiki/Uniform_Resource_Locator) na stałą wartość, która może być podawana jako argument.  Klasyfikacja danych tekstowychW tej części notebooka przeprowadzimy klasyfikację danych tekstowych. Do analiz będziemy wykorzystywać kolekcję wiadomości SMS, zawierających spam i prawdziwe wiadomości, dostępnym w [repozytorium UCI](https://archive.ics.uci.edu/ml/datasets/SMS+Spam+Collection). Poniższy kod pobiera dane.<jupyter_code>import os
import urllib.request
import zipfile

data_path = 'data'

os.makedirs(data_path, exist_ok=True)

url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/00228/smsspamcollection.zip'
file_name = url.split('/')[-1]
dest_file = os.path.join(data_path, file_name) 

data_file = 'SMSSpamCollection'
data_full = os.path.join(data_path, data_file)

urllib.request.urlretrieve(url, dest_file)

with zipfile.ZipFile(dest_file) as zip_file:
    zip_file.extract(data_file, path=data_path)
import pandas as pd

sms = pd.read_csv(data_full, 
                  sep='\t', 
                  names=['is_spam', 'text'])
sms.head()
from sklearn.model_selection import train_test_split

train_sms, test_sms = train_test_split(sms, test_size=0.2)

print('Train set:')
print(train_sms.describe())
print()
print('Test set:')
print(test_sms.describe())<jupyter_output>Train set:
       is_spam                    text
count     4457                    4457
unique       2                    4189
top        ham  Sorry, I'll call later
freq      3860                      24

Test set:
       is_spam                    text
count     1115                    1115
unique       2                    1092
top        ham  Sorry, I'll call later
freq       965                       6<jupyter_text>Naszym zadaniem jest wytrenowanie klasyfikatora, który klasyfikuje wiadomość SMS jako spam. Podobne zadanie z dość szerokim opisem można znaleźć w [tym wpisie](https://radimrehurek.com/data_science_python/).  Naive BayesZaczniemy od klasyfikatora [Naive Bayes](http://scikit-learn.org/stable/modules/naive_bayes.html). Używa on [twierdzenia Bayesa](https://en.wikipedia.org/wiki/Bayes%27_theorem) do obliczenia prawdopodobieństw poszczególnych klas w zależności od dostępnych opcji, przy założeniu niezależności zmiennych losowych (stąd naiwny). Rozpatrzmy przykład ([źródło](https://www.analyticsvidhya.com/blog/2017/09/naive-bayes-explained/)):![](https://www.analyticsvidhya.com/wp-content/uploads/2015/08/Bayes_41-850x310.png)Poniżej przykład obliczenia prawdopodobieństwa na podstawie metody Bayesa.$$P(Yes | Sunny) = \frac{P( Sunny | Yes)  P(Yes)}{P (Sunny)}$$$$P (Sunny |Yes) = 3/9 = 0.33$$$$P(Sunny) = 5/14 = 0.36$$ $$P( Yes)= 9/14 = 0.64$$$$P (Yes | Sunny) = \frac{0.33 \cdot 0.64}{0.36} = 0.60$$Poniżej, przykład modelu detekcji spamu z użuciem metody Naive Bayes.<jupyter_code>from sklearn.feature_extraction.text import TfidfVectorizer

vectorizer = TfidfVectorizer()
vectorizer
X = vectorizer.fit_transform(train_sms['text'])
X
from sklearn.naive_bayes import MultinomialNB

spam_detector = MultinomialNB().fit(X, train_sms['is_spam'])
spam_detector
i = 17
train_sms.iloc[i, 0], spam_detector.predict(X[i])[0], train_sms.iloc[i, 1]
from sklearn.metrics import classification_report, confusion_matrix

X = vectorizer.transform(test_sms['text'])

y_pred = spam_detector.predict(X)
y_true = test_sms['is_spam']

print(confusion_matrix(y_true, y_pred))
print(classification_report(y_true, y_pred))
from sklearn.metrics import classification_report, confusion_matrix
from sklearn.naive_bayes import MultinomialNB
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.model_selection import train_test_split
import pandas as pd


sms = pd.read_csv(data_full, sep='\t', names=['is_spam', 'text'])
train_sms, test_sms = train_test_split(sms, test_size=0.2)

steps = [('tfidf', TfidfVectorizer()), ('cls', MultinomialNB())]
nb_pipe = Pipeline(steps=steps)
nb_pipe.fit(train_sms['text'], train_sms['is_spam'])

y_pred = nb_pipe.predict(test_sms['text'])
y_true = test_sms['is_spam']

print(confusion_matrix(y_true, y_pred))
print(classification_report(y_true, y_pred))<jupyter_output>[[949   0]
 [ 66 100]]
             precision    recall  f1-score   support

        ham       0.93      1.00      0.97       949
       spam       1.00      0.60      0.75       166

avg / total       0.94      0.94      0.93      1115<jupyter_text>Zadanie1. Zbuduj pipeline wektoryzatora i modelu.1. Użyj `GridSearchCV` do znalezienia najlepszego modelu.1. Popraw tokenizację w celu poprawienia jakości klasyfikacji.1. Użyj innych klasyfikatorów.  Modelowanie tematówModelowanie tematów (topic modelling) jest to zadanie ekstrakcji ważnych elementów z tekstu, które są jego tematami. Wiodącą biblioteką w pythonie do tego celu jest [gensim](https://radimrehurek.com/gensim/).  Latent Dirichlet AllocationJest wiele ciekawych algorytmów wykonujących to zadanie, jednak jednym z najgłośniejszych algorytmów do ekstrakcji tematów jest [Latent Dirichlet Allocation (LDA)](https://en.wikipedia.org/wiki/Latent_Dirichlet_allocation). ![](https://cdn-images-1.medium.com/max/1600/0*II7wZlKViCt4ssBm.png)Jedną z najskuteczniejszych implementaci LDA w Pythonie jest implementacja dostępna w gensim, która posiada też dość rozbudowaną [dokumentację](https://radimrehurek.com/gensim/models/ldamodel.html). Wykorzystajmy zatem LDA do naszego zadania.Do analizy wykorzystamy zbiór [20 newsgroups z scikit-learn](http://scikit-learn.org/stable/datasets/twenty_newsgroups.html).<jupyter_code>from sklearn.datasets import fetch_20newsgroups

newsgroups = fetch_20newsgroups(shuffle=True, random_state=1,
                                remove=('headers', 'footers', 'quotes'))
newsgroups.data[:2]
newsgroups.target
newsgroups.target_names<jupyter_output><empty_output><jupyter_text>Zawęzimy trochę próbkę, żeby uczenie było szybsze.<jupyter_code>n_samples = 2000
newsgroups_samples = newsgroups.data[:n_samples]<jupyter_output><empty_output><jupyter_text>Najpierw tworzymy korpus dla LDA używając narzędzi gensim.<jupyter_code>import nltk
from gensim import corpora, models
from gensim.models.ldamodel import LdaModel

words = [nltk.regexp_tokenize(d.lower(), '\w+') for d in newsgroups_samples]

dictionary = corpora.Dictionary(words)
corpus = [dictionary.doc2bow(text) for text in words]<jupyter_output>C:\ProgramData\Anaconda3\lib\site-packages\gensim\utils.py:862: UserWarning: detected Windows; aliasing chunkize to chunkize_serial
  warnings.warn("detected Windows; aliasing chunkize to chunkize_serial")<jupyter_text>Słownik posiada ID słowa jako klucz i słowo jako wartość.<jupyter_code>from itertools import islice

for k, v in islice(dictionary.items(), 10):
    print('{}: {}'.format(k, v))<jupyter_output>0: well
1: i
2: m
3: not
4: sure
5: about
6: the
7: story
8: nad
9: it<jupyter_text>Korpus posiada listę krotek, lista na dokument; krotki zawierają pary ID słowa i jego liczność w danych dokumencie.<jupyter_code>print(corpus[:2])<jupyter_output>[[(0, 1), (1, 4), (2, 1), (3, 3), (4, 1), (5, 1), (6, 17), (7, 1), (8, 1), (9, 2), (10, 1), (11, 2), (12, 1), (13, 2), (14, 1), (15, 1), (16, 8), (17, 1), (18, 1), (19, 5), (20, 4), (21, 4), (22, 4), (23, 1), (24, 4), (25, 1), (26, 2), (27, 1), (28, 1), (29, 1), (30, 1), (31, 2), (32, 4), (33, 1), (34, 1), (35, 1), (36, 1), (37, 1), (38, 1), (39, 1), (40, 2), (41, 1), (42, 1), (43, 1), (44, 1), (45, 1), (46, 3), (47, 1), (48, 1), (49, 1), (50, 1), (51, 1), (52, 1), (53, 2), (54, 1), (55, 2), (56, 1), (57, 1), (58, 1), (59, 1), (60, 1), (61, 1), (62, 1), (63, 1), (64, 1), (65, 2), (66, 1), (67, 1), (68, 1), (69, 1), (70, 1), (71, 1), (72, 1), (73, 1), (74, 1), (75, 2), (76, 1), (77, 1), (78, 1), (79, 1), (80, 1), (81, 1), (82, 1), (83, 1), (84, 1), (85, 1), (86, 1), (87, 1), (88, 1), (89, 1), (90, 1), (91, 1), (92, 1), (93, 1), (94, 1), (95, 1), (96, 1), (97, 1), (98, 1), (99, 1), (100, 1), (101, 1), (102, 1)], [(0, 1), (1, 2), (2, 1), (5, 1), (6, 2), (9, 1), (17, 2), (19, 2), (21, 1), [...]<jupyter_text>Zadanie1. Dla 10 pierwszych dokumentów wypisz słowa i ich liczność. Teraz wytrenujmy sam model LDA.<jupyter_code>model = LdaModel(corpus=corpus, id2word=dictionary, num_topics=10, alpha="auto")
model
for i in range(10):
    print(model.get_document_topics(corpus[i], minimum_probability=0.1))
k = 0
model.get_topic_terms(topicid=k)
model.print_topics()<jupyter_output><empty_output><jupyter_text>Zadanie1. Zmień ilość tematów; czy coś się zmieniło?1. Zobacz jak tematy mapują się na klasy.1. Popraw tokenizację; możesz np.:    - usunąć nie-słowa    - usunąć wyrazy ze stop listy    - sprowadzić słowa do wspólnej wielkości znaku1. Sprawdź inne metody budowania korpusu, np.:    - macierz TD    - TF-IDF<jupyter_code>from gensim.sklearn_api.ldamodel import LdaTransformer
from gensim.sklearn_api.text2bow import Text2BowTransformer
from sklearn.pipeline import Pipeline

steps = {
    'text2bow': Text2BowTransformer(),
    'lda': LdaTransformer(num_topics=10)
}

p = Pipeline(list(steps.items()))
p
p.fit_transform(newsgroups_samples)
steps['lda'].gensim_model.print_topics()
steps['text2bow'].gensim_model
p.transform(['ala ma kota', 'lala'])<jupyter_output><empty_output><jupyter_text>Możemy też zastosować wbudowaną metodę scikit-learn; zobacz [dokumentację](http://scikit-learn.org/stable/modules/generated/sklearn.decomposition.LatentDirichletAllocation.html).<jupyter_code># Funkcja użytkowa do wyświetlania tematów.
def print_top_words(model, feature_names, n_top_words):
    for topic_idx, topic in enumerate(model.components_):
        message = "Topic #%d: " % topic_idx
        message += " ".join([feature_names[i]
                             for i in topic.argsort()[:-n_top_words - 1:-1]])
        print(message)
    print()
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.decomposition import LatentDirichletAllocation

tf_vectorizer = CountVectorizer(max_df=0.95, min_df=2,
                                max_features=1000,
                                stop_words='english')

tf = tf_vectorizer.fit_transform(newsgroups_samples)

lda = LatentDirichletAllocation(n_components=10, max_iter=5,
                                learning_method='online',
                                learning_offset=50.,
                                random_state=0)

lda.fit(tf)

tf_feature_names = tf_vectorizer.get_feature_names()
print_top_words(lda, tf_feature_names, 10)<jupyter_output><empty_output><jupyter_text>Zadanie1. Zrób pipeline z powyższego przetwarzania.1. Spróbuj użyć TF-IDF.1. Zmień liczbę tematów `n_components` i zobacz co się zmieni.  Non-negative Matrix FactorizationKolejnym algorytmem często używanym do ekstrakcji tematów jest [Non-negative Matrix Factorization](https://en.wikipedia.org/wiki/Non-negative_matrix_factorization). ![](https://upload.wikimedia.org/wikipedia/commons/f/f9/NMF.png)Metoda ta używa faktoryzacji do przybliżenia macierzy V jako iloczynu macierzy H i W. W wyniku działania algorytmu macierze H i W mają właściwości klasteryzacyjne danych w macierzy V. Dokładnie W staje się macierzą centroidów a H indykatorem przyporządkowania do klastrów poszczególnych elementów macierzy V.W praktyce często stosuje się tą metodę jako zamiennik PCA, lecz tylko dla danych pozytywnych, oraz do ekstrakcji tematów.<jupyter_code>from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer
from sklearn.decomposition import NMF, LatentDirichletAllocation

tfidf_vectorizer = TfidfVectorizer(max_df=0.95, min_df=2,
                                   max_features=1000,
                                   stop_words='english')

tfidf = tfidf_vectorizer.fit_transform(newsgroups_samples)

nmf = NMF(n_components=10, random_state=1,
          alpha=.1, l1_ratio=.5).fit(tfidf)

tfidf_feature_names = tfidf_vectorizer.get_feature_names()
print_top_words(nmf, tfidf_feature_names, 10)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.12349030989607715, 'fraction_numerical': 0.029304372249789344, 'mean_word_length': 4.813061224489796, 'pattern_counts': {'":': 0, '<': 88, '<?xml version=': 0, '>': 88, 'https://': 17, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 11, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '22', 'dir': 'jupyter-structured-clean-dedup', 'id': '537071', 'license': 'MIT', 'n_tokens_mistral': 8451, 'n_tokens_neox': 8145, 'n_words': 1734, 'path': 'machine-learning/contrib/ml/5_text.ipynb', 'repo_name': 'giserh/book-python', 'size': '21391'}