import pandas as pd
import numpy as np
from mlxtend.frequent_patterns import apriori, association_rules
from mlxtend.preprocessing import TransactionEncoder
import matplotlib.pyplot as plt
import seaborn as snsModule 2: Association Rule Mining
Case Study
This paper examines which metrics best predict human expert judgments of “interestingness” in association rule mining within educational data. Association rule mining is a technique that finds patterns where a set of variables (the “if-clause”) predict another variable’s value (the “then-clause”).

Download the paper here
The authors used educational data from the ASSISTments system involving 724 students, focusing on affect (boredom, concentration, frustration) and disengagement (off-task behavior, gaming the system). They generated 120 representative association rules from this data and had four domain experts rate each rule’s interestingness on a scale of 1-5.
The researchers then analyzed which standard metrics best correlated with expert judgments. They found that:
Jaccard, Cosine, and Support metrics had the highest correlation with expert ratings
A combined model using Lift, Phi Coefficient, Cosine, and Conviction best predicted expert judgments
The findings partially supported Merceron & Yacef’s (2008) recommendation to use Cosine and Lift as interestingness measures
Interestingly, Cosine correlated negatively with interestingness in their study, contrary to previous recommendations, suggesting that interestingness may be related to rarity once support and confidence are accounted for.
Python Activity
STEP 1 Import the Packages
STEP 2 Load and Prepare Data
def prepare_data_for_rules(df):
"""
Prepare data for association rule mining by creating transaction data
representing state transitions (from problem t to problem t+1)
"""
# Group by student
transactions = []
for student_id, student_data in df.groupby('student_id'):
# Sort by problem ID
student_data = student_data.sort_values('problem_id')
# Create transitions
for i in range(len(student_data) - 1):
current_problem = student_data.iloc[i]
next_problem = student_data.iloc[i+1]
# Get states for current problem
current_states = []
for state in df.columns[2:]: # Skip student_id and problem_id
if current_problem[state] == 1:
current_states.append(f"{state}_t1")
# Get states for next problem
next_states = []
for state in df.columns[2:]: # Skip student_id and problem_id
if next_problem[state] == 1:
next_states.append(f"{state}_t2")
# Add the transaction as the combined current and next states
transactions.append(current_states + next_states)
print(f"Created {len(transactions)} transactions for association rule mining")
# Print sample transactions
print("\nSample transactions (first 3):")
for i, transaction in enumerate(transactions[:3]):
print(f" Transaction {i+1}: {transaction}")
# Convert to one-hot encoded format for apriori
te = TransactionEncoder()
te_data = te.fit_transform(transactions)
return pd.DataFrame(te_data, columns=te.columns_), transactions
df = pd.read_csv("arm-data.csv")
print("\nSample of generated data:")
print(df.head())
# Prepare data for association rule mining
print("\nPreparing data for association rule mining...")
transaction_data, raw_transactions = prepare_data_for_rules(df)
Sample of generated data:
Unnamed: 0 student_id problem_id bored engaged_concentration \
0 0 0 0 0 0
1 1 0 1 1 0
2 2 0 2 1 0
3 3 0 3 0 1
4 4 0 4 0 1
frustrated confused off_task gaming_the_system hint_requested \
0 0 0 0 0 1
1 0 0 1 0 0
2 0 1 1 1 1
3 0 1 0 1 0
4 0 1 0 0 1
answer_correct answer_incorrect
0 0 1
1 1 0
2 1 0
3 0 1
4 0 1
Preparing data for association rule mining...
Created 7000 transactions for association rule mining
Sample transactions (first 3):
Transaction 1: ['hint_requested_t1', 'answer_incorrect_t1', 'problem_id_t2', 'bored_t2', 'off_task_t2', 'answer_correct_t2']
Transaction 2: ['problem_id_t1', 'bored_t1', 'off_task_t1', 'answer_correct_t1', 'bored_t2', 'confused_t2', 'off_task_t2', 'gaming_the_system_t2', 'hint_requested_t2', 'answer_correct_t2']
Transaction 3: ['bored_t1', 'confused_t1', 'off_task_t1', 'gaming_the_system_t1', 'hint_requested_t1', 'answer_correct_t1', 'engaged_concentration_t2', 'confused_t2', 'gaming_the_system_t2', 'answer_incorrect_t2']
STEP 3 Find Rules
def find_association_rules(df, min_support=0.03, min_confidence=0.1):
"""
Find association rules and calculate interestingness metrics
"""
print(f"Finding frequent itemsets with min_support={min_support}...")
# Find frequent itemsets
frequent_itemsets = apriori(df, min_support=min_support, use_colnames=True)
print(f"Found {len(frequent_itemsets)} frequent itemsets")
if len(frequent_itemsets) == 0:
print("No frequent itemsets found! Try lowering the min_support threshold.")
return pd.DataFrame()
print(f"Generating association rules with min_confidence={min_confidence}...")
# Generate association rules
rules = association_rules(frequent_itemsets, metric="confidence", min_threshold=min_confidence)
print(f"Found {len(rules)} association rules")
if len(rules) == 0:
print("No association rules found! Try lowering the min_confidence threshold.")
return pd.DataFrame()
print("Calculating additional interestingness metrics...")
# Calculate additional interestingness metrics from the paper
# Calculate Phi Coefficient (similar to correlation coefficient)
rules['phi'] = rules.apply(lambda row:
(row['support'] - row['antecedent support'] * row['consequent support']) /
np.sqrt(row['antecedent support'] * row['consequent support'] *
(1 - row['antecedent support']) * (1 - row['consequent support'])),
axis=1)
# Calculate Cosine similarity
rules['cosine'] = rules.apply(lambda row:
row['support'] / np.sqrt(row['antecedent support'] * row['consequent support']),
axis=1)
# Jaccard metric
rules['jaccard'] = rules['support'] / (rules['antecedent support'] + rules['consequent support'] - rules['support'])
# Conviction metric
rules['conviction'] = (1 - rules['consequent support']) / (1 - rules['confidence'])
# Replace infinite values with a large number
rules.replace([np.inf, -np.inf], 999, inplace=True)
return rules
# Find association rules
print("\nFinding association rules...")
# Try different support thresholds if needed
for support in [0.03, 0.02, 0.01]:
rules = find_association_rules(transaction_data, min_support=support)
if len(rules) > 0:
print(f"Successfully found rules with support threshold {support}")
break
print(f"No rules found with support threshold {support}, trying lower threshold...")
Finding association rules...
Finding frequent itemsets with min_support=0.03...
Found 459 frequent itemsets
Generating association rules with min_confidence=0.1...
Found 3191 association rules
Calculating additional interestingness metrics...
Successfully found rules with support threshold 0.03
STEP 4 Calculating Interestingness Metrics
def identify_interesting_rules(rules, top_n=10):
"""
Identify interesting rules using the metrics identified in the paper
"""
if len(rules) == 0:
return pd.DataFrame()
# Create a combined interestingness score based on the paper's findings
# Using Lift, Phi, Cosine, and Conviction
rules['combined_score'] = (
rules['lift'] * 0.4 +
abs(rules['phi']) * 0.3 +
rules['cosine'] * 0.2 +
np.log1p(rules['conviction']) * 0.1
)
# Get rules about transitions from one state to another
# Find rules where antecedents are from time t1 and consequents from time t2
transition_rules = rules[
rules['antecedents'].apply(lambda x: any('_t1' in item for item in x)) &
rules['consequents'].apply(lambda x: any('_t2' in item for item in x))
]
print(f"Found {len(transition_rules)} rules representing transitions between problems")
if len(transition_rules) == 0:
# If no transition rules found, return regular rules
print("No transition rules found. Returning top rules by combined score.")
interesting_rules = rules.sort_values('combined_score', ascending=False).head(top_n)
else:
# Sort by combined score
interesting_rules = transition_rules.sort_values('combined_score', ascending=False).head(top_n)
return interesting_rules
# Identify interesting rules
print("\nIdentifying interesting rules using metrics from the paper...")
interesting_rules = identify_interesting_rules(rules)
if len(interesting_rules) == 0:
print("No interesting rules identified.")
Identifying interesting rules using metrics from the paper...
Found 1624 rules representing transitions between problems
STEP 5 Visualization
def analyze_metrics_correlation(rules):
"""
Analyze the correlation between different interestingness metrics
similar to the paper's approach
"""
# Select metrics to analyze
metrics = ['support', 'confidence', 'lift', 'conviction', 'cosine', 'jaccard', 'phi', 'combined_score']
correlation = rules[metrics].corr()
# Visualize correlation
plt.figure(figsize=(10, 8))
sns.heatmap(correlation, annot=True, cmap='coolwarm', fmt='.2f')
plt.title('Correlation Between Interestingness Metrics')
plt.tight_layout()
return correlation
def format_rule(row):
"""Format a rule for better readability"""
antecedents = ', '.join([item.replace('_t1', ' (t1)') for item in list(row['antecedents'])])
consequents = ', '.join([item.replace('_t2', ' (t2)') for item in list(row['consequents'])])
return f"IF {antecedents} THEN {consequents}"
# Display top interesting rules
print("\nTop 5 most interesting rules according to combined metric:")
print("-"*70)
for i, (_, rule) in enumerate(interesting_rules.head(5).iterrows(), 1):
print(f"{i}. {format_rule(rule)}")
print(f" Support: {rule['support']:.3f}, Confidence: {rule['confidence']:.3f}")
print(f" Lift: {rule['lift']:.3f}, Phi: {rule['phi']:.3f}, Cosine: {rule['cosine']:.3f}")
print(f" Conviction: {rule['conviction']:.3f}, Jaccard: {rule['jaccard']:.3f}")
print(f" Combined Score: {rule['combined_score']:.3f}")
print()
# Analyze correlation between metrics
print("\nAnalyzing correlation between interestingness metrics...")
correlation = analyze_metrics_correlation(rules)
Top 5 most interesting rules according to combined metric:
----------------------------------------------------------------------
1. IF off_task_t2, off_task (t1), gaming_the_system (t1) THEN bored (t2), bored_t1
Support: 0.031, Confidence: 0.901
Lift: 5.441, Phi: 0.374, Cosine: 0.412
Conviction: 8.414, Jaccard: 0.184
Combined Score: 2.595
2. IF off_task_t2, off_task (t1), gaming_the_system_t2 THEN bored (t2), bored_t1
Support: 0.033, Confidence: 0.876
Lift: 5.293, Phi: 0.381, Cosine: 0.421
Conviction: 6.751, Jaccard: 0.196
Combined Score: 2.520
3. IF off_task_t2, bored (t1), gaming_the_system_t2 THEN off_task_t1, bored (t2)
Support: 0.033, Confidence: 0.597
Lift: 5.441, Phi: 0.380, Cosine: 0.426
Conviction: 2.209, Jaccard: 0.253
Combined Score: 2.492
4. IF off_task (t1), bored_t2 THEN off_task (t2), bored_t1, gaming_the_system (t2)
Support: 0.033, Confidence: 0.305
Lift: 5.441, Phi: 0.380, Cosine: 0.426
Conviction: 1.358, Jaccard: 0.253
Combined Score: 2.461
5. IF bored_t2, bored (t1) THEN off_task (t2), off_task_t1, gaming_the_system_t1
Support: 0.031, Confidence: 0.188
Lift: 5.441, Phi: 0.374, Cosine: 0.412
Conviction: 1.189, Jaccard: 0.184
Combined Score: 2.449
Analyzing correlation between interestingness metrics...
