lyst / lyst/lightfm

Temporal splitting

Open
#663 1 comment 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
5.1k
Forks
724
PR merge metrics
No merged PRs in 30d

Description

For those asking questions about splitting data. Here is what I believe is a suitable method for splitting time-sorted interactions held in ```df``` as user-item interactions with implicit strength and item and user metadata. This represents the most rigorous method for testing the algorithm and tuning hyper parameters as it reflects the challenge of using data of the past to predict the future.

```
# Split by time
num_splits = 4

def tail_split(df, size):
test_start = int(len(df) * size)
train_end = int(len(df) - test_start)
train = df.iloc[:-test_start].copy()
test = df.iloc[train_end:].copy()
return train, test

train_all, test = tail_split(df, 0.1)
from sklearn.model_selection import TimeSeriesSplit
cv_splits = [(train, val) for train, val in TimeSeriesSplit(n_splits = num_splits).split(train_all)]
cv_data = [(train_all.iloc[i[0]], train_all.iloc[i[1]]) for i in cv_splits]

# Build knowledge of all user and item possibilities
# Item features
item_features = df[['item', 'Category', 'Division']].drop_duplicates().astype(str)
item_features = item_features.loc[item_features['item'].isin(df['item'])]
item_features.set_index('item', inplace = True)
item_feature_names = set()
for i in item_features.columns:
item_feature_names.update(item_features[i])
item_features = list(zip(item_features.index, item_features.values.tolist()))
# User features
user_features = df[['user', 'Sector', 'SubSector']].drop_duplicates().astype(str)
user_features = user_features.loc[user_features['user'].isin(df['user'])]
user_features.set_index('user', inplace = True)
user_feature_names = set()
for i in user_features.columns:
user_feature_names.update(user_features[i])
user_features = list(zip(user_features.index, user_features.values.tolist()))

# Create numeric labels for users, items and features
from lightfm.data import Dataset
dataset = Dataset()
dataset.fit(users = df['user'],
items = df['item'],
user_features = list(user_feature_names),
item_features = list(item_feature_names))
user_features_matrix = dataset.build_user_features(user_features)
item_features_matrix = dataset.build_item_features(item_features)
# Get mappings
user_id_map, user_feature_map, item_id_map, item_feature_map = dataset.mapping()

# Build datasets for each split
final_cv_data = []
for train, val in cv_data:
(train, weights) = dataset.build_interactions(list(zip(train['user'], train['item'], train['strength'])))
(val, weights) = dataset.build_interactions(list(zip(val['user'], val['item'], val['strength'])))
final_cv_data.append((train, val))

(train_all, weights) = dataset.build_interactions(list(zip(train_all['user'], train_all['item'], train_all['strength'])))
(test, weights) = dataset.build_interactions(list(zip(test['user'], test['item'], test['strength'])))
(all_data, weights) = dataset.build_interactions(list(zip(df['user'], df['item'], df['strength'])))

k = 5 # Num recommendations

# Fit the training set and tune on the validation set
from lightfm import LightFM
from lightfm.evaluation import auc_score
from lightfm.evaluation import precision_at_k
from lightfm.evaluation import recall_at_k
def sample_hyperparameters():
while True:
yield {"no_components": np.random.randint(10, 100),
"learning_schedule": np.random.choice(["adagrad", "adadelta"]),
"loss": np.random.choice(["warp", "bpr"]),
"learning_rate": np.random.exponential(0.05),
"item_alpha": np.random.exponential(1e-8),
"user_alpha": np.random.exponential(1e-8),
"max_sampled": np.random.randint(5, 15),
"num_epochs": np.random.randint(5, 150),}
def random_search(cv_data, user_features_matrix, item_features_matrix, k, num_samples, num_threads = 1):
for hyperparams in itertools.islice(sample_hyperparameters(), num_samples):
num_epochs = hyperparams.pop("num_epochs")
model = LightFM(**hyperparams)
scores = []
for train, val in cv_data:
model.fit(train,
user_features = user_features_matrix,
item_features = item_features_matrix,
epochs = num_epochs,
num_threads = num_threads,
verbose = True)
recall = recall_at_k(model, val,
train_interactions = train,
user_features = user_features_matrix,
item_features = item_features_matrix,
k = k,
num_threads = num_threads).mean()
scores.append(recall)
hyperparams["num_epochs"] = num_epochs
yield (sum(scores) / len(scores), hyperparams)
(score, hyperparams) = max(random_search(final_cv_data, user_features_matrix, item_features_matrix, k, num_samples = 20), key = lambda x: x[0])

# Fit entire training set
num_epochs = hyperparams.pop("num_epochs")
model = LightFM(**hyperparams)
model.fit(train_all,
user_features = user_features_matrix,
item_features = item_features_matrix,
epochs = num_epochs,
verbose = True)
```

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reviewing the temporal-splitting example in the issue, including scikit-learn's TimeSeriesSplit and the LightFM evaluation flow. The scope and destination are not specified; done would require a maintainer-approved, documented approach for time-based train, validation, and test splitting.

Written by the indexing model from the issue text.

Assessment

Tech stack
numpy, python, scikit-learn
Domain
data, machine-learning, testing-qa
Issue type
Documentation
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.