Apriori algorithm¶
frequent sequences in 'market basket'
https://rasbt.github.io/mlxtend/user_guide/frequent_patterns/association_rules/
Movielense dataset¶
https://github.com/NeoMCHS/AprioriMovieResearch
https://www.kaggle.com/datasets/grouplens/movielens-20m-dataset
Rule generation is a common task in the mining of frequent patterns.¶
-- (Mystery, Drama) → (Crime)
suggesting that people who watch (Mystery, Drama) are also likely watch (Crime). To evaluate the "interest" of such an association rule, different metrics have been developed.
-- The key metric is support.
-- The current implementation `mlxtend.frequent_patterns` make use of the confidence and lift metrics.
from mlxtend.frequent_patterns import apriori, association_rules
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import warnings
warnings.simplefilter("ignore")
Simple data¶
data = {'Milk': [1, 1, 0, 1], 'Bread': [1, 0, 1, 1], 'Butter': [0, 1, 1, 1]}
df = pd.DataFrame(data)
df.head()
| Milk | Bread | Butter | |
|---|---|---|---|
| 0 | 1 | 1 | 0 |
| 1 | 1 | 0 | 1 |
| 2 | 0 | 1 | 1 |
| 3 | 1 | 1 | 1 |
Itemsets (sets of products) with support >= min_support¶
-- support(itemset)=(# rows with itemset)/(# rows)
-- support(A→C)=support(A∪C)
frequent_itemsets = apriori(df, min_support=0.1, use_colnames=True)
print("Frequent Itemsets:\n", frequent_itemsets)
Frequent Itemsets:
support itemsets
0 0.75 (Milk)
1 0.75 (Bread)
2 0.75 (Butter)
3 0.50 (Bread, Milk)
4 0.50 (Milk, Butter)
5 0.50 (Bread, Butter)
6 0.25 (Bread, Milk, Butter)
Rules from itemsets¶
split (partition) the itemset into the antecedent A and the succedent C
-- confidence(A→C)=support(A→C)/support(A) # P(C|A) estimate
--lift(A→C)=confidence(A→C)/support(C) # =1 if A and C are independent
rules = association_rules(frequent_itemsets, metric="confidence", min_threshold=0.6)
rules[['antecedents', 'consequents', 'support', 'confidence', 'lift']]
| antecedents | consequents | support | confidence | lift | |
|---|---|---|---|---|---|
| 0 | (Bread) | (Milk) | 0.5 | 0.666667 | 0.888889 |
| 1 | (Milk) | (Bread) | 0.5 | 0.666667 | 0.888889 |
| 2 | (Milk) | (Butter) | 0.5 | 0.666667 | 0.888889 |
| 3 | (Butter) | (Milk) | 0.5 | 0.666667 | 0.888889 |
| 4 | (Bread) | (Butter) | 0.5 | 0.666667 | 0.888889 |
| 5 | (Butter) | (Bread) | 0.5 | 0.666667 | 0.888889 |
nothing interesting; too few of data
# Step 2: Keep only maximal itemsets
def is_maximal(itemset, all_itemsets):
return not any(
(itemset < other) # strict subset
for other in all_itemsets
)
all_sets = [set(x) for x in frequent_itemsets['itemsets']]
frequent_itemsets['is_maximal'] = [
is_maximal(s, all_sets) for s in all_sets
]
maximal_itemsets = frequent_itemsets[frequent_itemsets['is_maximal']]
# Step 3: Generate rules only from maximal itemsets
#rules = association_rules(maximal_itemsets, metric="confidence", min_threshold=0.6)
print(maximal_itemsets)
#print(rules)
support itemsets is_maximal 6 0.25 (Bread, Milk, Butter) True
import efficient_apriori as effapi
transactions = [('eggs', 'bacon', 'soup'),
('eggs', 'bacon', 'apple'),
('soup', 'bacon', 'banana')]
itemsets, rules = effapi.apriori(transactions, min_support=0.5, min_confidence=1)
print(rules) # [{eggs} -> {bacon}, {soup} -> {bacon}]
# Print out every rule with 2 items on the left hand side,
# 1 item on the right hand side, sorted by lift
rules_rhs = filter(lambda rule: len(rule.lhs) == 2 and len(rule.rhs) == 1, rules)
for rule in sorted(rules_rhs, key=lambda rule: rule.lift):
print(rule) # Prints the rule and its confidence, support, lift, ...
print(itemsets)
# {1: {('bacon',): ItemsetCount(itemset_count=3, members={0, 1, 2}), ...
[{eggs} -> {bacon}, {soup} -> {bacon}]
Movie dataset with PLOT¶
https://www.davidsbatista.net/blog/2017/04/01/document_classification/¶
(Predicts GENRE from the PLOT)
movies = pd.read_csv("data/movies_genres_en.csv", delimiter='\t')#;movies.info()
genre_columns=movies.columns[2:29]
frequent_itemsets = apriori(movies[genre_columns], min_support=0.005, use_colnames=True)
frequent_itemsets.head()
| support | itemsets | |
|---|---|---|
| 0 | 0.105594 | (Action) |
| 1 | 0.087402 | (Adventure) |
| 2 | 0.097010 | (Animation) |
| 3 | 0.011818 | (Biography) |
| 4 | 0.289008 | (Comedy) |
Itemsets sorted by support¶
frequent_itemsets.sort_values(by='support', ascending=False).head(10)
| support | itemsets | |
|---|---|---|
| 7 | 0.391581 | (Drama) |
| 4 | 0.289008 | (Comedy) |
| 18 | 0.163609 | (Romance) |
| 8 | 0.131440 | (Family) |
| 81 | 0.130775 | (Drama, Romance) |
| 5 | 0.129025 | (Crime) |
| 0 | 0.105594 | (Action) |
| 17 | 0.105261 | (Reality-TV) |
| 15 | 0.102616 | (Mystery) |
| 6 | 0.102565 | (Documentary) |
frequent_itemsets[frequent_itemsets['itemsets']==frozenset({'Drama'})]
| support | itemsets | |
|---|---|---|
| 7 | 0.391581 | (Drama) |
Rules from itemsets¶
r_cols=['antecedents', 'consequents', 'support', 'confidence', 'lift']
rules = association_rules(frequent_itemsets, metric="confidence", min_threshold=0.6)
rules["antecedent_len"] = rules["antecedents"].apply(lambda x: len(x))
rules[r_cols].head()
| antecedents | consequents | support | confidence | lift | |
|---|---|---|---|---|---|
| 0 | (Crime) | (Drama) | 0.099101 | 0.768071 | 1.961459 |
| 1 | (Mystery) | (Crime) | 0.069091 | 0.673291 | 5.218285 |
| 2 | (Mystery) | (Drama) | 0.082240 | 0.801430 | 2.046650 |
| 3 | (Romance) | (Drama) | 0.130775 | 0.799312 | 2.041239 |
| 4 | (Thriller) | (Drama) | 0.058629 | 0.776209 | 1.982241 |
Select and order rules according your criteria¶
rules[ (rules['support'] > 0.02) &
(rules['confidence'] > 0.75) &
(rules['lift'] > 1.2) ][r_cols].sort_values(by='support', ascending=False)
| antecedents | consequents | support | confidence | lift | |
|---|---|---|---|---|---|
| 3 | (Romance) | (Drama) | 0.130775 | 0.799312 | 2.041239 |
| 0 | (Crime) | (Drama) | 0.099101 | 0.768071 | 1.961459 |
| 2 | (Mystery) | (Drama) | 0.082240 | 0.801430 | 2.046650 |
| 36 | (Mystery, Drama) | (Crime) | 0.062085 | 0.754928 | 5.851007 |
| 37 | (Mystery, Crime) | (Drama) | 0.062085 | 0.898604 | 2.294808 |
| 4 | (Thriller) | (Drama) | 0.058629 | 0.776209 | 1.982241 |
| 54 | (Mystery, Thriller) | (Drama) | 0.040975 | 0.892731 | 2.279809 |
| 42 | (Thriller, Crime) | (Drama) | 0.038159 | 0.893150 | 2.280878 |
| 45 | (Thriller, Crime) | (Mystery) | 0.032621 | 0.763531 | 7.440650 |
| 118 | (Mystery, Crime, Thriller) | (Drama) | 0.030300 | 0.928852 | 2.372052 |
| 119 | (Thriller, Drama, Crime) | (Mystery) | 0.030300 | 0.794052 | 7.738077 |
| 21 | (Action, Crime) | (Drama) | 0.024011 | 0.761776 | 1.945383 |
Text Preprocessing¶
Preprocessing
tokenizing - split words by spaces (rozdělení na slova mezerami apod.)
counting - count word occurences (počty slov v dokumentu)
stop-word removal - words without meaning, too frequent or too rare (příliš časté, moc málo výskytů)
normalizing - divide by the number of words in the document (vydělení počtem slov v dokumentu)
all in sparce matrices
https://scikit-learn.org/stable/modules/feature_extraction.html#text-feature-extraction
from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer
from sklearn.decomposition import NMF, LatentDirichletAllocation
Download the English stopwords¶
import nltk
from nltk.corpus import stopwords
nltk.download('stopwords')
stop_words = ["qv","de","eric",'luc',"film",'tom','nick','sam','jon','bob','bill','000','10']+stopwords.words('english')
stopwords.words('english')[:10]
[nltk_data] Downloading package stopwords to [nltk_data] C:\Users\Marta\AppData\Roaming\nltk_data... [nltk_data] Package stopwords is already up-to-date!
['a', 'about', 'above', 'after', 'again', 'against', 'ain', 'all', 'am', 'an']
TF vectorization¶
n_features = 1000 #TF vectorization
## remove frequent and rare words, stop_words, select max_features
tf_vectorizer = CountVectorizer(max_df=0.95, min_df=2,
max_features=n_features,
stop_words=stop_words)
## apply the tf_vectorized
tf = tf_vectorizer.fit_transform(movies['plot'])
tf_feature_names = tf_vectorizer.get_feature_names_out()
tf_feature_names[:10]
array(['able', 'accept', 'accepts', 'accident', 'accidentally', 'across',
'act', 'action', 'actor', 'actually'], dtype=object)
## Convert the sparse matrix tf to a list of tuples of feature names for each document
## input for efficient_apriori, not part of the presentation since it does not support lift metric
def get_row(row, tf_feature_names):
return (tuple([tf_feature_names[x] for x in range(len(tf_feature_names)) if row[x]>0 ]))
rows = [get_row(row.toarray().ravel(), tf_feature_names) for row in tf]
itemsets, rules = effapi.apriori(rows, min_support=0.01, min_confidence=0.4)
print(rules)
[{ago} -> {years}, {york} -> {new}, {year} -> {old}]
The word 'love' in plots¶
print('(movies.title, find("love"))')
feature_idx=np.where(tf_feature_names=='love')[0][0]
[(movies['title'].iloc[i],tf[i,feature_idx]) for i in range(10) if tf[i,feature_idx]>0]
(movies.title, find("love"))
[('"#BlackLove" (2015) {Crash the Party (#1.9)}', np.int64(1)),
('"#BlackLove" (2015) {Sealing the Deal (#1.10)}', np.int64(1)),
('"#Hashtag: The Series" (2013)', np.int64(1))]
Latent Dirichlet Allocation (LDA)¶
n_components = 15 #LDA
## apply the LDA with specific n_components
lda = LatentDirichletAllocation(n_components=n_components, max_iter=5,
learning_method='online',
learning_offset=50.,
random_state=0)
topic_frequencies=lda.fit_transform(tf)
lda.components_.shape
(15, 1000)
Ploting the results¶
# Author: Olivier Grisel <olivier.grisel@ensta.org>
# Lars Buitinck
# Chyi-Kwei Yau <chyikwei.yau@gmail.com>
# License: BSD 3 clause
n_top_words = 20 #for plot
n_rows= int(np.ceil(n_components/5))
def plot_top_words(model, feature_names, n_top_words, title):
fig, axes = plt.subplots(n_rows, 5, figsize=(40, 25), sharex=True)
axes = axes.flatten()
for topic_idx, topic in enumerate(model.components_):
top_features_ind = topic.argsort()[:-n_top_words - 1:-1]
top_features = [feature_names[i] for i in top_features_ind]
weights = topic[top_features_ind]
ax = axes[topic_idx]
ax.barh(top_features, weights, height=0.7)
ax.set_title(f'Topic {topic_idx +1}',
fontdict={'fontsize': 30})
ax.invert_yaxis()
ax.tick_params(axis='both', which='major', labelsize=20)
for i in 'top right left'.split():
ax.spines[i].set_visible(False)
fig.suptitle(title, fontsize=40)
plt.subplots_adjust(top=0.90, bottom=0.05, wspace=0.90, hspace=0.3)
# plt.show()
plot_top_words(lda, tf_feature_names, n_top_words, 'Topics in LDA model')
For each component, select the top word¶
component_first_words=[tf_feature_names[lda.components_[i].argmax()] for i in range(n_components)]
print(component_first_words)
['hospital', 'news', 'one', 'mike', 'david', 'night', 'find', 'son', 'life', 'show', 'mr', 'tells', 'money', 'man', 'new']
Preprocessing tf-idf¶
term-frequency times inverse document-frequency¶
$idf(t)=log \frac{n}{1+df(t)}$
($idf(t)=log \frac{1+n}{1+df(t)}+1$) smooth=True
normalizace Euclidean (L2) norm /sqrt(\sum(x_i^2))
https://scikit-learn.org/stable/modules/feature_extraction.html#text-feature-extraction
from time import time
# Use tf-idf features for NMF.
print("Extracting tf-idf features for NMF...")
tfidf_vectorizer = TfidfVectorizer(max_df=0.95, min_df=2,
max_features=n_features,
stop_words='english')
t0 = time()
tfidf = tfidf_vectorizer.fit_transform(movies['plot'])
print("done in %0.3fs." % (time() - t0))
tfidf_feature_names = tfidf_vectorizer.get_feature_names_out()
Extracting tf-idf features for NMF... done in 32.477s.
Non-Negative Matrix Factorization (NMF)¶
ind two non-negative matrices (W, H) whose product approximates the non- negative matrix X. https://scikit-learn.org/stable/modules/generated/sklearn.decomposition.NMF.html?highlight=nmf#sklearn.decomposition.NMF
# Fit the NMF-Frobenius model
print("Fitting the NMF model (Frobenius norm) with tf-idf features, "
"n_samples=%d and n_features=%d..."
% (len(movies['plot']), n_features))
t0 = time()
nmf = NMF(n_components=n_components, random_state=1,init='nndsvda',
#alpha=.1,
l1_ratio=.5).fit(tfidf)
print("done in %0.3fs." % (time() - t0))
Fitting the NMF model (Frobenius norm) with tf-idf features, n_samples=117194 and n_features=1000... done in 63.431s.
plot_top_words(nmf, tfidf_feature_names, n_top_words,
'Topics in NMF model (Frobenius norm)')
# Fit the NMF-KL model
print('\n' * 2, "Fitting the NMF model (generalized Kullback-Leibler "
"divergence) with tf-idf features, n_samples=%d and n_features=%d..."
% (len(movies['plot']), n_features))
t0 = time()
nmf = NMF(n_components=n_components, random_state=1,init='nndsvda',
beta_loss='kullback-leibler', solver='mu', max_iter=1000, #alpha=.1,
l1_ratio=.5).fit(tfidf)
print("done in %0.3fs." % (time() - t0))
tfidf_feature_names = tfidf_vectorizer.get_feature_names_out()
plot_top_words(nmf, tfidf_feature_names, n_top_words,
'Topics in NMF model (generalized Kullback-Leibler divergence)')
Fitting the NMF model (generalized Kullback-Leibler divergence) with tf-idf features, n_samples=117194 and n_features=1000... done in 277.222s.