Un libraire qui aurait tout lu
52 livres du Project Gutenberg lus par un algorithme, regroupés par thèmes et recommandés par proximité de vocabulaire.
- Livres
- 52
- Tokens bruts
- 7,7 M
- Bruit supprimé
- 60,4 %
- Mots distincts
- 80 741
La question
dataset · Project Gutenberg · 52 livres au format .txt (anglais et français)
« Si l’on ne connaît que le texte d’un livre, peut-on proposer le suivant à lire ? »
Le notebook
Les cellules s’exécutent en défilant. Chaque étape : l’explication, le résultat, et le code si vous voulez creuser. Le code et les sorties viennent directement du dépôt.
Analyse et classement d’une bibliothèque numérique
Pas de notes, pas de genres, pas d’avis : seulement le texte brut de 52 livres. Tout doit émerger du vocabulaire.
1. Nettoyer
Mise en minuscules, suppression des mots vides anglais et français, lemmatisation des verbes.
In [ ]:
1def preprocessing (filename ,book):2 stop_words = set(stopwords.words('english'))3 stop_words.update(stopwords.words('french'))4 lemma = WordNetLemmatizer()5 WORD_TOKEN_OF_BOOK = {filename: word_tokenize(book)}6 CLEAN_TOKENS = {}7 for filename,tokens in WORD_TOKEN_OF_BOOK.items():8 CLEAN_TOKENS[filename] = [9 lemma.lemmatize(token.lower(),pos="v") for token in tokens 10 if token.lower() not in stop_words 11 and token.isalpha()12 ]13 return CLEAN_TOKENS In [ ]:
1# Affichage des métriques du dataset2print("="*60)3print("STATISTIQUES DU DATASET")4print("="*60)5print(f"\n Nombre de livres : {len(BOOKS)}")6print(f" Livres analysés : {', '.join(list(BOOKS.keys())[:3])}... et {len(BOOKS)-3} autres\n")7 8# Statistiques sur les tokens9total_tokens = sum(len(tokens) for tokens in CLEAN_TOKENS_OF_BOOKS.values())10total_raw_tokens = sum(len(word_tokenize(text)) for text in BOOKS.values())11print(f"Tokens bruts (avant nettoyage) : {total_raw_tokens:,}")12print(f"Tokens après nettoyage : {total_tokens:,}")13print(f"Tokens supprimés (stopwords, ponctuation) : {total_raw_tokens - total_tokens:,}")14print(f"Taux de réduction : {((total_raw_tokens - total_tokens) / total_raw_tokens * 100):.1f}%")15 16# Vocabulaire unique17all_tokens = []18for tokens in CLEAN_TOKENS_OF_BOOKS.values():19 all_tokens.extend(tokens)20unique_tokens = len(set(all_tokens))21print(f"\nVocabulaire unique : {unique_tokens:,} mots distincts")22print(f"Densité lexicale : {(unique_tokens / total_tokens * 100):.2f}%")23 24# Statistiques par livre25token_counts = {name: len(tokens) for name, tokens in CLEAN_TOKENS_OF_BOOKS.items()}26avg_tokens = sum(token_counts.values()) / len(token_counts)27print(f"\n✓ Moyenne de tokens par livre : {avg_tokens:,.0f}")28print(f"✓ Livre le plus court : {min(token_counts, key=token_counts.get)} ({min(token_counts.values()):,} tokens)")29print(f"✓ Livre le plus long : {max(token_counts, key=token_counts.get)} ({max(token_counts.values()):,} tokens)")30print("\n" + "="*60)2. Pondérer les mots avec TF-IDF
Un mot fréquent dans un livre mais rare ailleurs est un bon indice de ce qui le rend unique.
In [ ]:
1 2vectorizer = TfidfVectorizer(3 stop_words="english",4 lowercase=True,5 use_idf=True,6 7) 8TF_IDF_MATRIX = vectorizer.fit_transform(corpus)9BOOKS_OF_NAMES = vectorizer.get_feature_names_out()3. Faire émerger des thèmes (LSA)
On compresse la matrice en 3 composantes et on regarde les mots qui pèsent le plus dans chacune.
In [ ]:
1SVD = TruncatedSVD(n_components=3)2LSA_RESULT= SVD.fit_transform(document_term_matrix.T)3 4lsa_topic_matrix = pd.DataFrame(5 LSA_RESULT,6 index=document_term_matrix.columns,7 columns=[f"Topic{i+1}" for i in range(SVD.n_components)]8).reset_index()9 10lsa_topic_matrix.rename(columns={"index":"book"},inplace=True)11#lsa_topic_matrixIn [ ]:
1for i_topic , poids in enumerate(SVD.components_) :2 tries_desc_des_poids = poids.argsort()[::-1]3 mot_du_topic = [BOOKS_OF_NAMES[i] for i in tries_desc_des_poids[:10]]4 print(f"Topic{i_topic+1} : {' '.join(mot_du_topic)}")4. Recommander
Chaque livre devient un vecteur. La distance cosinus donne les plus proches voisins.
In [ ]:
1cosine_distance = 1-cosine_similarity(TF_IDF_MATRIX, TF_IDF_MATRIX)2 3def best_recommended_books(book_name, nb_books =5):4 indices = pd.Series(lsa_topic_matrix["book"])5 if book_name not in list(lsa_topic_matrix["book"]):6 return "Book not found in the database."7 recommended_books = []8 idx = indices[indices == book_name].index[0] 9 score_series = pd.Series(cosine_distance[idx]).sort_values(ascending = True) 10 top_nb_books = dict(score_series.iloc[1:nb_books+1]) 11 12 for indice,score in top_nb_books.items(): 13 recommended_books.append(f"({list(lsa_topic_matrix['book'])[indice]},array([[{score}]])")14 15 return recommended_books16print(best_recommended_books("alice-in-wonderland"))Through the Looking-Glass arrive premier pour Alice au pays des merveilles, avec une distance de 0,16, très loin devant les suivants (≈ 0,82). Le système retrouve la suite sans qu’on la lui indique.
5. Regard critique
Les 3 thèmes n’expliquent que 15,6 % de la variance et un thème regroupe 42 livres sur 52 : la classification est déséquilibrée. La recommandation, elle, fonctionne bien.
In [ ]:
1print("\n" + "="*60)2print("VALIDATION DES RÉSULTATS")3print("="*60)4 5# Variance expliquée par LSA6variance_explained = SVD.explained_variance_ratio_7cumulative_variance = variance_explained.cumsum()8print(f"\n Variance expliquée par composante :")9for i, var in enumerate(variance_explained):10 print(f" - Topic {i+1}: {var*100:.2f}%")11print(f"\nVariance cumulée (3 topics) : {cumulative_variance[-1]*100:.2f}%")12print(" -> Les 3 thèmes capturent la majorité du contenu sémantique")13 14# Distribuzione des livres par thème15distribution = {16 "Topic 1": len(topic1_book),17 "Topic 2": len(topic2_book),18 "Topic 3": len(topic3_book)19}20print(f"\nDistribution des livres par thème :")21for topic, count in distribution.items():22 pct = (count / len(NAMES_BOOKS)) * 10023 print(f" - {topic}: {count} livres ({pct:.1f}%)")24 25print("\n" + "="*60)Un libraire qui aurait tout lu · cellules et sorties issues du dépôt GitHub
Analyse et classement d’une bibliothèque numérique
Pas de notes, pas de genres, pas d’avis : seulement le texte brut de 52 livres. Tout doit émerger du vocabulaire.
Étape 011/5
Nettoyer
Mise en minuscules, suppression des mots vides anglais et français, lemmatisation des verbes.
In [1]def preprocessing (filename ,book):13 l.
1def preprocessing (filename ,book):2 stop_words = set(stopwords.words('english'))3 stop_words.update(stopwords.words('french'))4 lemma = WordNetLemmatizer()5 WORD_TOKEN_OF_BOOK = {filename: word_tokenize(book)}6 CLEAN_TOKENS = {}7 for filename,tokens in WORD_TOKEN_OF_BOOK.items():8 CLEAN_TOKENS[filename] = [9 lemma.lemmatize(token.lower(),pos="v") for token in tokens 10 if token.lower() not in stop_words 11 and token.isalpha()12 ]13 return CLEAN_TOKENSIn [2]Affichage des métriques du dataset30 l.
1# Affichage des métriques du dataset2print("="*60)3print("STATISTIQUES DU DATASET")4print("="*60)5print(f"\n Nombre de livres : {len(BOOKS)}")6print(f" Livres analysés : {', '.join(list(BOOKS.keys())[:3])}... et {len(BOOKS)-3} autres\n")7 8# Statistiques sur les tokens9total_tokens = sum(len(tokens) for tokens in CLEAN_TOKENS_OF_BOOKS.values())10total_raw_tokens = sum(len(word_tokenize(text)) for text in BOOKS.values())11print(f"Tokens bruts (avant nettoyage) : {total_raw_tokens:,}")12print(f"Tokens après nettoyage : {total_tokens:,}")13print(f"Tokens supprimés (stopwords, ponctuation) : {total_raw_tokens - total_tokens:,}")14print(f"Taux de réduction : {((total_raw_tokens - total_tokens) / total_raw_tokens * 100):.1f}%")15 16# Vocabulaire unique17all_tokens = []18for tokens in CLEAN_TOKENS_OF_BOOKS.values():19 all_tokens.extend(tokens)20unique_tokens = len(set(all_tokens))21print(f"\nVocabulaire unique : {unique_tokens:,} mots distincts")22print(f"Densité lexicale : {(unique_tokens / total_tokens * 100):.2f}%")23 24# Statistiques par livre25token_counts = {name: len(tokens) for name, tokens in CLEAN_TOKENS_OF_BOOKS.items()}26avg_tokens = sum(token_counts.values()) / len(token_counts)27print(f"\n✓ Moyenne de tokens par livre : {avg_tokens:,.0f}")28print(f"✓ Livre le plus court : {min(token_counts, key=token_counts.get)} ({min(token_counts.values()):,} tokens)")29print(f"✓ Livre le plus long : {max(token_counts, key=token_counts.get)} ({max(token_counts.values()):,} tokens)")30print("\n" + "="*60)Out[2]
============================================================ STATISTIQUES DU DATASET ============================================================ Nombre de livres : 52 Livres analysés : alice-in-wonderland, Among the Forest People, An Introductory Course of Quantitative Chemical Analysis... et 49 autres Tokens bruts (avant nettoyage) : 7,698,130 Tokens après nettoyage : 3,051,009 Tokens supprimés (stopwords, ponctuation) : 4,647,121 Taux de réduction : 60.4% Vocabulaire unique : 80,741 mots distincts Densité lexicale : 2.65% ✓ Moyenne de tokens par livre : 58,673 ✓ Livre le plus court : The Tale of Timmy Tiptoes (2,290 tokens) ✓ Livre le plus long : The History Of The Decline And Fall Of The Roman Empire (779,357 tokens) ============================================================
Étape 022/5
Pondérer les mots avec TF-IDF
Un mot fréquent dans un livre mais rare ailleurs est un bon indice de ce qui le rend unique.
In [3]vectorizer = TfidfVectorizer(9 l.
1 2vectorizer = TfidfVectorizer(3 stop_words="english",4 lowercase=True,5 use_idf=True,6 7) 8TF_IDF_MATRIX = vectorizer.fit_transform(corpus)9BOOKS_OF_NAMES = vectorizer.get_feature_names_out()Étape 033/5
Faire émerger des thèmes (LSA)
On compresse la matrice en 3 composantes et on regarde les mots qui pèsent le plus dans chacune.
In [4]SVD = TruncatedSVD(n_components=3)11 l.
1SVD = TruncatedSVD(n_components=3)2LSA_RESULT= SVD.fit_transform(document_term_matrix.T)3 4lsa_topic_matrix = pd.DataFrame(5 LSA_RESULT,6 index=document_term_matrix.columns,7 columns=[f"Topic{i+1}" for i in range(SVD.n_components)]8).reset_index()9 10lsa_topic_matrix.rename(columns={"index":"book"},inplace=True)11#lsa_topic_matrixIn [5]for i_topic , poids in enumerate(SVD.components_) :4 l.
1for i_topic , poids in enumerate(SVD.components_) :2 tries_desc_des_poids = poids.argsort()[::-1]3 mot_du_topic = [BOOKS_OF_NAMES[i] for i in tries_desc_des_poids[:10]]4 print(f"Topic{i_topic+1} : {' '.join(mot_du_topic)}")Out[5]
Topic1 : say work make come project time little state know think Topic2 : say alice little look come think know mother tell like Topic3 : acid air water solution oil soap cc temperature work use
Étape 044/5
Recommander
Chaque livre devient un vecteur. La distance cosinus donne les plus proches voisins.
In [6]cosine_distance = 1-cosine_similarity(TF_IDF_MATRIX, TF_IDF_MATRIX)16 l.
1cosine_distance = 1-cosine_similarity(TF_IDF_MATRIX, TF_IDF_MATRIX)2 3def best_recommended_books(book_name, nb_books =5):4 indices = pd.Series(lsa_topic_matrix["book"])5 if book_name not in list(lsa_topic_matrix["book"]):6 return "Book not found in the database."7 recommended_books = []8 idx = indices[indices == book_name].index[0] 9 score_series = pd.Series(cosine_distance[idx]).sort_values(ascending = True) 10 top_nb_books = dict(score_series.iloc[1:nb_books+1]) 11 12 for indice,score in top_nb_books.items(): 13 recommended_books.append(f"({list(lsa_topic_matrix['book'])[indice]},array([[{score}]])")14 15 return recommended_books16print(best_recommended_books("alice-in-wonderland"))Out[6]
['(Through the Looking-Glass,array([[0.15898531450954434]])', '(The Magic Fishbone,array([[0.8198772395624926]])', '(Among the Forest People,array([[0.8276530420034904]])', '(Mother Storie,array([[0.82866930696625]])', '(Sandman_s Goodnight Stories,array([[0.8297552976740471]])']
Through the Looking-Glass arrive premier pour Alice au pays des merveilles, avec une distance de 0,16, très loin devant les suivants (≈ 0,82). Le système retrouve la suite sans qu’on la lui indique.
Étape 055/5
Regard critique
Les 3 thèmes n’expliquent que 15,6 % de la variance et un thème regroupe 42 livres sur 52 : la classification est déséquilibrée. La recommandation, elle, fonctionne bien.
In [7]print("\n" + "="*60)25 l.
1print("\n" + "="*60)2print("VALIDATION DES RÉSULTATS")3print("="*60)4 5# Variance expliquée par LSA6variance_explained = SVD.explained_variance_ratio_7cumulative_variance = variance_explained.cumsum()8print(f"\n Variance expliquée par composante :")9for i, var in enumerate(variance_explained):10 print(f" - Topic {i+1}: {var*100:.2f}%")11print(f"\nVariance cumulée (3 topics) : {cumulative_variance[-1]*100:.2f}%")12print(" -> Les 3 thèmes capturent la majorité du contenu sémantique")13 14# Distribuzione des livres par thème15distribution = {16 "Topic 1": len(topic1_book),17 "Topic 2": len(topic2_book),18 "Topic 3": len(topic3_book)19}20print(f"\nDistribution des livres par thème :")21for topic, count in distribution.items():22 pct = (count / len(NAMES_BOOKS)) * 10023 print(f" - {topic}: {count} livres ({pct:.1f}%)")24 25print("\n" + "="*60)Out[7]
============================================================ VALIDATION DES RÉSULTATS ============================================================ Variance expliquée par composante : - Topic 1: 2.84% - Topic 2: 7.39% - Topic 3: 5.33% Variance cumulée (3 topics) : 15.56% -> Les 3 thèmes capturent la majorité du contenu sémantique Distribution des livres par thème : - Topic 1: 42 livres (80.8%) - Topic 2: 1 livres (1.9%) - Topic 3: 9 livres (17.3%) ============================================================
Ce qu’il faut retenir
- 1Le nettoyage retire 60,4 % des tokens : 7,7 M de mots bruts ramenés à 3 M utiles.
- 2La recommandation retrouve la suite d’Alice (distance 0,16) sans aucune métadonnée.
- 3Les thèmes LSA sont déséquilibrés (42 / 1 / 9 livres) : trois composantes ne suffisent pas.
Et ensuite
- Choisir le nombre de thèmes automatiquement (cohérence, coude).
- Comparer avec des embeddings de phrases (Sentence-BERT).