# Install latest from the GitHub repository:
# $ pip install git+https://github.com/risheekkumarb/fastpragma.git
# or from conda:
# $ conda install -c risheekkumarb fastpragma
# or from pypi:
# $ pip install fastpragmafastpragma
Installation
Install latest from the GitHub repository:
Install from conda:
Install from pypi:
Documentation
Documentation can be found hosted on this GitHub repository’s pages. Additionally you can find package manager specific guidelines on conda and pypi respectively.
Usage
fastpragma now covers four working layers:
- Data — declare profile and event sources with
DataSource, fit aTokenizerthroughPRAGMADataset, and write entity-sharded Parquet withwrite_kv - Dataloading — load shards with
pragma_dl,pragma_dls, orPRAGMADataLoader.from_path, with entity grouping, token budgets, packed events, and optional MLM masking - Model — build the encoder-only PRAGMA architecture with
PRAGMAModelor thepragma_modelpresetsS,M, andL - Training and tasks — pretrain with
pragma_learner, resume and validate runs, extract entity embeddings, or build task dataloaders and classification models for fine-tuning
The end-to-end workflow has been run on MovieLens 100K and UCI Online Retail.
import polars as pl, torch, numpy as np
from fastai.data.external import untar_data, URLs
from fastcore.all import *
from fastai.torch_core import to_device, default_device
from fastpragma.data import *
from fastpragma.dataloader import *
from fastpragma.model import *
from fastpragma.pretrain import *
from fastpragma.finetune import *Data format
fastpragma accepts profile data with one row per entity and event data with many timestamped rows per entity. Both are declared with DataSource.
Each source can declare:
cats: categorical fieldsconts: continuous numerical fieldssigned_conts: continuous fields where sign is represented separatelytexts: free-text fields, handled as low-cardinality tokens or BPElifelong: timestamp or milestone fields in profile datatime_col: the timestamp column for event dataentity_col: the entity identifier shared across sources
DataSource validates the declaration and supports construction from DataFrames or files.
Example: MovieLens 100K
This example loads the classic MovieLens 100K dataset into polars DataFrames.
Creating data sources
Use DataSource to declare how each DataFrame should be interpreted.
Profile sources use is_profile=True and normally do not need a time_col. Event sources provide a time_col.
path = untar_data(URLs.ML_100k)
events_df = pl.scan_csv(path/'u.data', separator='\t', has_header=False, new_columns=['user_id','movie_id','rating','timestamp'])
events_df = events_df.with_columns(pl.from_epoch('timestamp', time_unit='s').alias('timestamp'))
ratings = DataSource(events_df, entity_col='user_id', cats=['movie_id','rating'], time_col='timestamp', name='events_df')
ratingsDataSource(columns=['user_id', 'movie_id', 'rating', 'timestamp'], name=events_df cats=['movie_id', 'rating'], conts=[], texts=[], time_col='timestamp')
shape: (5, 4)
┌─────────┬──────────┬────────┬─────────────────────┐
│ user_id ┆ movie_id ┆ rating ┆ timestamp │
│ --- ┆ --- ┆ --- ┆ --- │
│ i64 ┆ i64 ┆ i64 ┆ datetime[μs] │
╞═════════╪══════════╪════════╪═════════════════════╡
│ 196 ┆ 242 ┆ 3 ┆ 1997-12-04 15:55:49 │
│ 186 ┆ 302 ┆ 3 ┆ 1998-04-04 19:22:22 │
│ 22 ┆ 377 ┆ 1 ┆ 1997-11-07 07:18:36 │
│ 244 ┆ 51 ┆ 2 ┆ 1997-11-27 05:02:03 │
│ 166 ┆ 346 ┆ 1 ┆ 1998-02-02 05:33:16 │
└─────────┴──────────┴────────┴─────────────────────┘
profile_df = pl.scan_csv(path/'u.user', separator='|', has_header=False, new_columns=['user_id','age','gender','occupation','zip_code'])
profile = DataSource(profile_df, entity_col='user_id', cats=['gender','zip_code'], conts=['age'], texts=['occupation'], name='users', is_profile=True)
profileDataSource(columns=['user_id', 'age', 'gender', 'occupation', 'zip_code'], name=users cats=['gender', 'zip_code'], conts=['age'], texts=['occupation'], time_col=None)
shape: (5, 5)
┌─────────┬─────┬────────┬────────────┬──────────┐
│ user_id ┆ age ┆ gender ┆ occupation ┆ zip_code │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ i64 ┆ i64 ┆ str ┆ str ┆ str │
╞═════════╪═════╪════════╪════════════╪══════════╡
│ 1 ┆ 24 ┆ M ┆ technician ┆ 85711 │
│ 2 ┆ 53 ┆ F ┆ other ┆ 94043 │
│ 3 ┆ 23 ┆ M ┆ writer ┆ 32067 │
│ 4 ┆ 24 ┆ M ┆ technician ┆ 43537 │
│ 5 ┆ 33 ┆ F ┆ other ┆ 15213 │
└─────────┴─────┴────────┴────────────┴──────────┘
Building a PRAGMADataset
PRAGMADataset combines one optional profile source and one or more event sources.
It fits a tokenizer, converts sources into key-value-time tokens, and writes entity-sharded parquet files. show_summary() gives an overview of sources and vocabularies.
dataset = PRAGMADataset(profile=profile, events=[ratings], entity_col="user_id", out_path="data")
tok = dataset.fit_tokenizer(num_buckets=10, cardinality_threshold=100)
shard_dir, n_keys, n_vals= dataset.write_kv(eval_time="1998-04-01T00:00:00", n_shards=4)
n_keys, n_valsKeys: 11, Vals: 2514, BPE: none
tokenizing profile
tokenizing event source 0: events_df
combining sources
(11, 2514)
Dataloaders
pragma_dl creates a PyTorch DataLoader from tokenized Parquet shards. It groups records by entity, packs variable-length events under a token budget, and can apply MLM masking.
pragma_dls creates fastai train and validation DataLoaders. PRAGMADataLoader.from_path reloads a saved tokenizer and shard directory. preflight, validate_shard, validate_shards, and validate_split check shard structure, dtypes, and vocabulary sizes before loading.
shards = sorted(Path(shard_dir).glob("shard_*.parquet"))
dl = pragma_dl(shards, entity_col="user_id", max_tokens=30000, shuffle=True, tok=tok, mask=True)For training with fastai, split the shard list into training and validation shards, then use preflight to validate shard structure, dtypes, and tokenizer vocabulary sizes before constructing pragma_dls.
valid_shards,train_shards = shards[-1:],shards[:-1]
preflight(train_shards, valid_shards, tok)
dls = pragma_dls(train_shards, valid_shards, tok, max_tokens=30000, valid_batches=2)Model
PRAGMAModel is an encoder-only PRAGMA-style model with profile, event, and history encoders. It supports static and lifelong profile fields, packed variable-length events, shared key/value embeddings, RoPE, calendar features, an [USR] profile token, and an [EVT] event token.
The model includes an MLM head tied to the value vocabulary. pragma_model constructs preset sizes S, M, and L. Task-specific classification models are available through get_classification_model for fine-tuning.
model = pragma_model("S", n_keys=tok.n_keys, n_vals=tok.n_vals)Pretraining
pragma_learner builds a PRAGMAModel and wraps it in a fastai Learner for masked-token pretraining. The workflow has been executed end to end on MovieLens 100K and UCI Online Retail, including categorical, continuous, and BPE text fields.
Implemented training support includes:
preflight,validate_shard,validate_shards, andvalidate_splitsave_state,load_state,resume_state,resumed_pragma_dls, andresumed_pragma_learnerPeriodicSaveCB,ResumeCB,GradAccumCB, andThroughputCBentity_embsfor extracting per-entity representations from a batchpragma_task_dls,get_classification_model, andpragma_task_splitterfor classification fine-tuning
learn = pragma_learner(dls, tok.n_keys, tok.n_vals, sz='S')
if torch.cuda.is_available(): learn = learn.to_fp16()
learn.fit(1, lr=1e-3, cbs=[ThroughputCB(20)])| epoch | train_loss | valid_loss | time |
|---|---|---|---|
| 0 | 6.593944 | 6.100394 | 00:03 |
b,_ = first(dls.valid)
b = to_device(b, default_device())
embs = entity_embs(learn.model.model, b)
len(embs), first(embs.items())[0], first(embs.items())[1].shape(115, 4, torch.Size([192]))
Example 2: UCI Online Retail
This example uses real transaction data rather than a small teaching dataset. Each row is a purchase event, and CustomerID identifies the entity whose history will be modeled.
The preparation step removes rows without a customer, cancelled invoices, and non-positive quantities or prices. The remaining columns map directly onto the DataSource API: StockCode and Country are categorical fields, Quantity and UnitPrice are continuous fields, Description is text for BPE tokenization, and InvoiceDate supplies event time.
url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/00352/Online%20Retail.xlsx'
retail = pl.read_excel(url).drop_nulls('CustomerID').with_columns(
pl.col('CustomerID').cast(pl.Int64).alias('customer_id'),
pl.col('InvoiceNo').cast(pl.Utf8),
pl.col('StockCode').cast(pl.Utf8),
pl.col('Description').fill_null('').cast(pl.Utf8)
).filter(
~pl.col('InvoiceNo').str.starts_with('C'),
pl.col('Quantity') > 0,
pl.col('UnitPrice') > 0
)
retail.head()| InvoiceNo | StockCode | Description | Quantity | InvoiceDate | UnitPrice | CustomerID | Country | customer_id |
|---|---|---|---|---|---|---|---|---|
| str | str | str | i64 | datetime[ms] | f64 | i64 | str | i64 |
| "536365" | "85123A" | "WHITE HANGING HEART T-LIGHT HO… | 6 | 2010-12-01 08:26:00 | 2.55 | 17850 | "United Kingdom" | 17850 |
| "536365" | "71053" | "WHITE METAL LANTERN" | 6 | 2010-12-01 08:26:00 | 3.39 | 17850 | "United Kingdom" | 17850 |
| "536365" | "84406B" | "CREAM CUPID HEARTS COAT HANGER" | 8 | 2010-12-01 08:26:00 | 2.75 | 17850 | "United Kingdom" | 17850 |
| "536365" | "84029G" | "KNITTED UNION FLAG HOT WATER B… | 6 | 2010-12-01 08:26:00 | 3.39 | 17850 | "United Kingdom" | 17850 |
| "536365" | "84029E" | "RED WOOLLY HOTTIE WHITE HEART." | 6 | 2010-12-01 08:26:00 | 3.39 | 17850 | "United Kingdom" | 17850 |
purchases = DataSource(
retail.lazy(), entity_col='customer_id',
cats=['StockCode','Country'], conts=['Quantity','UnitPrice'],
texts=['Description'], time_col='InvoiceDate', name='purchases'
)
purchasesDataSource(columns=['InvoiceNo', 'StockCode', 'Description', 'Quantity', 'InvoiceDate', 'UnitPrice', 'CustomerID', 'Country', 'customer_id'], name=purchases cats=['StockCode', 'Country'], conts=['Quantity', 'UnitPrice'], texts=['Description'], time_col='InvoiceDate')
shape: (5, 9)
┌───────────┬───────────┬───────────┬──────────┬───┬───────────┬───────────┬───────────┬───────────┐
│ InvoiceNo ┆ StockCode ┆ Descripti ┆ Quantity ┆ … ┆ UnitPrice ┆ CustomerI ┆ Country ┆ customer_ │
│ --- ┆ --- ┆ on ┆ --- ┆ ┆ --- ┆ D ┆ --- ┆ id │
│ str ┆ str ┆ --- ┆ i64 ┆ ┆ f64 ┆ --- ┆ str ┆ --- │
│ ┆ ┆ str ┆ ┆ ┆ ┆ i64 ┆ ┆ i64 │
╞═══════════╪═══════════╪═══════════╪══════════╪═══╪═══════════╪═══════════╪═══════════╪═══════════╡
│ 536365 ┆ 85123A ┆ WHITE ┆ 6 ┆ … ┆ 2.55 ┆ 17850 ┆ United ┆ 17850 │
│ ┆ ┆ HANGING ┆ ┆ ┆ ┆ ┆ Kingdom ┆ │
│ ┆ ┆ HEART ┆ ┆ ┆ ┆ ┆ ┆ │
│ ┆ ┆ T-LIGHT ┆ ┆ ┆ ┆ ┆ ┆ │
│ ┆ ┆ HO… ┆ ┆ ┆ ┆ ┆ ┆ │
│ 536365 ┆ 71053 ┆ WHITE ┆ 6 ┆ … ┆ 3.39 ┆ 17850 ┆ United ┆ 17850 │
│ ┆ ┆ METAL ┆ ┆ ┆ ┆ ┆ Kingdom ┆ │
│ ┆ ┆ LANTERN ┆ ┆ ┆ ┆ ┆ ┆ │
│ 536365 ┆ 84406B ┆ CREAM ┆ 8 ┆ … ┆ 2.75 ┆ 17850 ┆ United ┆ 17850 │
│ ┆ ┆ CUPID ┆ ┆ ┆ ┆ ┆ Kingdom ┆ │
│ ┆ ┆ HEARTS ┆ ┆ ┆ ┆ ┆ ┆ │
│ ┆ ┆ COAT ┆ ┆ ┆ ┆ ┆ ┆ │
│ ┆ ┆ HANGER ┆ ┆ ┆ ┆ ┆ ┆ │
│ 536365 ┆ 84029G ┆ KNITTED ┆ 6 ┆ … ┆ 3.39 ┆ 17850 ┆ United ┆ 17850 │
│ ┆ ┆ UNION ┆ ┆ ┆ ┆ ┆ Kingdom ┆ │
│ ┆ ┆ FLAG HOT ┆ ┆ ┆ ┆ ┆ ┆ │
│ ┆ ┆ WATER B… ┆ ┆ ┆ ┆ ┆ ┆ │
│ 536365 ┆ 84029E ┆ RED ┆ 6 ┆ … ┆ 3.39 ┆ 17850 ┆ United ┆ 17850 │
│ ┆ ┆ WOOLLY ┆ ┆ ┆ ┆ ┆ Kingdom ┆ │
│ ┆ ┆ HOTTIE ┆ ┆ ┆ ┆ ┆ ┆ │
│ ┆ ┆ WHITE ┆ ┆ ┆ ┆ ┆ ┆ │
│ ┆ ┆ HEART. ┆ ┆ ┆ ┆ ┆ ┆ │
└───────────┴───────────┴───────────┴──────────┴───┴───────────┴───────────┴───────────┴───────────┘
customers_df = retail.group_by('customer_id').agg(pl.col('Country').last()).lazy()
customers = DataSource(customers_df, entity_col='customer_id', cats=['Country'], name='customers', is_profile=True)
dataset = PRAGMADataset(profile=customers, events=[purchases], entity_col='customer_id', out_path='retail_data')The profile source contains one row per customer and carries the customer’s country as static context. The purchase source remains an event stream with many timestamped rows per customer.
Passing both sources to PRAGMADataset lets fastpragma combine static customer information with the chronological purchase history before tokenization and sharding.
# Fit vocabularies and numerical buckets.
tok = dataset.fit_tokenizer( num_buckets=10, cardinality_threshold=100, bpe_vocab_size=100)
# Write tokenized entity shards.
shard_dir,n_keys,n_vals = dataset.write_kv(eval_time="2011-12-01T00:00:00", n_shards=4)
Keys: 10, Vals: 3716, BPE: 259
tokenizing profile
tokenizing event source 0: purchases
combining sources
Here bpe_vocab_size=100 enables subword tokenization for the free-text descriptions. The resulting tokenizer reports BPE: 259, confirming that the text column produced a BPE vocabulary rather than being treated only as a categorical field.
write_kv then writes the tokenized records to four entity shards. The later dataloader, model, and learner cells use those same shards, so this is a complete data-to-pretraining test on mixed categorical, continuous, and BPE inputs.
shards = sorted(Path(shard_dir).glob('shard_*.parquet'))
valid_shards,train_shards = shards[-1:],shards[:-1]
preflight(train_shards, valid_shards, tok)
max_profile_toks,max_lifelong_toks,max_events,max_event_toks = 100,100,520,24 ## additional protection for less memory systems
dls = pragma_dls(train_shards, valid_shards, tok, max_tokens=3000, valid_batches=1, prefetch=0)
len(shards),tok.n_keys,tok.n_vals(4, 10, 3975)
learn = pragma_learner(dls, tok.n_keys, tok.n_vals, sz='S').to_fp16()
learn.fit_one_cycle(1, lr_max=1e-3)/usr/local/lib/python3.12/dist-packages/fastai/callback/fp16.py:47: FutureWarning: `torch.cuda.amp.autocast(args...)` is deprecated. Please use `torch.amp.autocast('cuda', args...)` instead.
self.autocast,self.learn.scaler,self.scales = autocast(dtype=dtype),GradScaler(**self.kwargs),L()
/usr/local/lib/python3.12/dist-packages/fastai/callback/fp16.py:47: FutureWarning: `torch.cuda.amp.GradScaler(args...)` is deprecated. Please use `torch.amp.GradScaler('cuda', args...)` instead.
self.autocast,self.learn.scaler,self.scales = autocast(dtype=dtype),GradScaler(**self.kwargs),L()
| epoch | train_loss | valid_loss | time |
|---|---|---|---|
| 0 | 3.150082 | 3.353010 | 02:03 |
learn.save('pretrain_model.pth')Path('models/pretrain_model.pth.pth')
finetuning example
labels = retail.group_by('customer_id').agg((pl.col('Quantity') * pl.col('UnitPrice')).sum().alias('target'))
threshold = labels.select(pl.col('target').median()).item()
labels = labels.with_columns((pl.col('target') >= threshold).cast(pl.Int64).alias('label'))
rng = np.random.default_rng(42)
ids = rng.permutation(labels['customer_id'].to_numpy())
n_train = int(len(ids) * .8)
train_ids,valid_ids = ids[:n_train],ids[n_train:]def split_shards(shards, labels, entity_col, out):
out = Path(out)
out.mkdir(parents=True, exist_ok=True)
ids = labels[entity_col].to_list()
for i,p in enumerate(shards):
df = pl.read_parquet(p).filter(pl.col(entity_col).is_in(ids))
if len(df): df.write_parquet(out/f'shard_{i}.parquet')
return sorted(out.glob('shard_*.parquet'))train_shards = split_shards(shards, train_labels, 'customer_id', 'retail_train')
valid_shards = split_shards(shards, valid_labels, 'customer_id', 'retail_valid')retail_dls = pragma_task_dls(
train_shards, valid_shards, train_labels, valid_labels,
'customer_id', target_col='label', dtype=torch.long,
max_tokens=12000, tok=tok)from fastai.learner import Learner
from fastai.losses import CrossEntropyLossFlat
from fastai.metrics import accuracy
from fastai.optimizer import Adamcls_model = get_classification_model(
tok.n_keys, tok.n_vals, sz='S', n_classes=2,
pretrain=True, pretrain_path='models/pretrain_model.pth.pth'
)
retail_learn = Learner(
retail_dls, cls_model,
loss_func=CrossEntropyLossFlat(),
opt_func=Adam,
metrics=accuracy,
splitter=pragma_task_splitter
)retail_learn.fit_one_cycle(1, lr_max=1e-3)| epoch | train_loss | valid_loss | accuracy | time |
|---|---|---|---|---|
| 0 | 0.399607 | 0.398961 | 0.829493 | 01:32 |
Current implemented API
Data
DataSourcewraps a polarsLazyFrame, validates declared columns, and supportsfrom_dfandfrom_file- Source fields support categorical, continuous, signed continuous, textual, event-time, and lifelong/profile data
Tokenizerbuilds shared key/value vocabularies, numerical buckets, low-cardinality text tokens, and BPE vocabularies; it supportssaveandloadPRAGMADatasetcombines one profile source with event sources, fits or loads a tokenizer, writes entity-sharded Parquet withwrite_kv, and reports summaries withshow_summary
Dataloading and validation
PRAGMADataLoaderstreams shards, groups records by entity, packs events under token budgets, and optionally applies MLM maskingpragma_dl,pragma_dls, andPRAGMADataLoader.from_pathprovide PyTorch and fastai loading pathspreflight,validate_shard,validate_shards, andvalidate_splitvalidate shard structure and tokenizer compatibility
Model and pretraining
PRAGMAModelprovides profile, event, and history encoders with shared embeddings, RoPE, calendar features, packed events, and an MLM headpragma_modelprovides model presetsS,M, andLpragma_learnercreates a fastai learner for masked-token pretraining- Checkpoint, resume, gradient-accumulation, periodic-save, and throughput callbacks are implemented
entity_embsextracts entity representations
Fine-tuning
pragma_task_dlsbuilds task dataloaders from labelled entity dataget_classification_modelbuilds a classification model with optional pretrained weightspragma_task_splittersupplies the fastai parameter split for task training
Verified workflows
MovieLens 100K and UCI Online Retail both run through data preparation, tokenization, sharding, dataloading, model construction, and pretraining. Online Retail also runs through customer-level label creation, shard splitting, classification fine-tuning, and pretrained-weight loading.
Remaining work
The core data-to-pretraining pipeline and a classification fine-tuning path are implemented and tested on MovieLens 100K and UCI Online Retail.
The main remaining additions are:
- A friendlier high-level API, including possible
SourceSchemaandPRAGMADataset.dataloaders()conveniences - A top-level model-loading API and broader save/load helpers for learners, heads, tokenizers, and weights
- Minimal examples that run without downloading a dataset
- Richer batch inspection and
show_batch()displays - Higher-level embedding extraction methods
- Additional task heads for regression, recommendation, and retrieval
- LoRA fine-tuning and linear probing for frozen embeddings
- Optional richer text-encoder integrations
- More complete temporal-feature documentation
- Additional tests and smoke-test notebooks