August 12, 2025·15 min read·technical
Building Systems That Learn From Usage
Building Systems That Learn From Usage
Context: Traditional software assumes it knows how it will be used. We build systems that discover their optimal form through observation.
The Learning Loop
Every user interaction teaches the system something:
- Which features are actually used
- Which paths are most common
- Where users struggle
- What patterns emerge
Implementation: The Event Stream Approach
from collections import defaultdict
import json
from datetime import datetime, timedelta
class UsagePatternLearner:
"""
Learn from usage without explicit training
"""
def __init__(self):
self.event_stream = []
self.patterns = defaultdict(list)
self.adaptations = {}
def observe(self, event):
"""
Every action is a teaching moment
"""
enriched_event = {
**event,
'timestamp': datetime.now(),
'context': self.get_current_context(),
'user_state': self.get_user_state(event['user_id'])
}
self.event_stream.append(enriched_event)
self.extract_patterns(enriched_event)
self.suggest_adaptations()
def extract_patterns(self, event):
"""
Find patterns without being told what to look for
"""
# Sequential patterns
recent_events = self.get_recent_events(event['user_id'], minutes=5)
if len(recent_events) > 1:
sequence = tuple(e['action'] for e in recent_events)
self.patterns['sequences'][sequence] += 1
# Temporal patterns
hour = event['timestamp'].hour
day = event['timestamp'].weekday()
self.patterns['temporal'][(hour, day)].append(event['action'])
# Error patterns
if event.get('error'):
preceding = self.get_preceding_events(event, count=3)
self.patterns['errors'][event['error']].append(preceding)
def suggest_adaptations(self):
"""
Propose system changes based on patterns
"""
adaptations = []
# Frequently repeated sequences could be shortcuts
for sequence, count in self.patterns['sequences'].items():
if count > 10 and len(sequence) > 3:
adaptations.append({
'type': 'create_shortcut',
'sequence': sequence,
'frequency': count
})
# Common error paths need better UX
for error, contexts in self.patterns['errors'].items():
if len(contexts) > 5:
common_path = self.find_common_path(contexts)
adaptations.append({
'type': 'improve_flow',
'error': error,
'common_path': common_path
})
return adaptations
Pattern Recognition Without Labels
The system doesn't need to know what patterns to look for:
def discover_clusters(events, min_similarity=0.7):
"""
Find natural groupings in usage data
"""
from sklearn.cluster import DBSCAN
from sklearn.feature_extraction.text import TfidfVectorizer
# Convert events to feature vectors
event_strings = [json.dumps(e, sort_keys=True) for e in events]
vectorizer = TfidfVectorizer(max_features=100)
features = vectorizer.fit_transform(event_strings)
# Find clusters without knowing how many
clustering = DBSCAN(eps=1-min_similarity, min_samples=3)
clusters = clustering.fit_predict(features)
# Extract meaning from clusters
cluster_patterns = defaultdict(list)
for event, cluster_id in zip(events, clusters):
if cluster_id != -1: # Not noise
cluster_patterns[cluster_id].append(event)
# Name clusters based on common attributes
for cluster_id, cluster_events in cluster_patterns.items():
common_attrs = find_common_attributes(cluster_events)
print(f"Discovered pattern: {common_attrs}")
return cluster_patterns
Adaptive Interfaces
The interface evolves based on usage:
class AdaptiveUI {
private usageHistory: Map<string, number> = new Map()
private userPaths: Map<string, string[]> = new Map()
recordUsage(componentId: string, userId: string) {
// Track component usage
const count = this.usageHistory.get(componentId) || 0
this.usageHistory.set(componentId, count + 1)
// Track user paths
const path = this.userPaths.get(userId) || []
path.push(componentId)
this.userPaths.set(userId, path.slice(-10)) // Keep last 10
}
getOptimalLayout(userId: string): Layout {
const userPath = this.userPaths.get(userId) || []
const globalUsage = Array.from(this.usageHistory.entries())
// Components used frequently should be prominent
const prominentComponents = globalUsage
.sort((a, b) => b[1] - a[1])
.slice(0, 5)
.map(([id]) => id)
// Components used in sequence should be near each other
const sequences = this.findSequences(userPath)
return {
primary: prominentComponents,
grouped: sequences,
hidden: this.getRarelyUsed()
}
}
private findSequences(path: string[]): string[][] {
const sequences: string[][] = []
const seen = new Set<string>()
for (let i = 0; i < path.length - 1; i++) {
const pair = [path[i], path[i + 1]]
const key = pair.join('->')
if (!seen.has(key)) {
sequences.push(pair)
seen.add(key)
}
}
return sequences
}
}
Learning Without Machine Learning
Simple heuristics often outperform complex models:
class HeuristicLearner:
"""
Learn through simple rules, not neural networks
"""
def __init__(self):
self.rules = []
self.performance = defaultdict(float)
def learn_from_outcome(self, context, action, outcome):
"""
If it worked, do more of it. If it didn't, do less.
"""
key = (self.hash_context(context), action)
if outcome > 0:
self.performance[key] += 0.1 # Reinforce
else:
self.performance[key] -= 0.1 # Discourage
# Create new rule if pattern is strong
if abs(self.performance[key]) > 1.0:
self.create_rule(context, action, self.performance[key])
def decide(self, context):
"""
Use learned rules to make decisions
"""
applicable_rules = [
rule for rule in self.rules
if self.matches_context(rule.context, context)
]
if applicable_rules:
# Use highest confidence rule
best_rule = max(applicable_rules, key=lambda r: r.confidence)
return best_rule.action
else:
# Explore new action
return self.explore_action()
The Result
Systems that:
- Get better over time without updates
- Adapt to how they're actually used
- Surface patterns humans missed
- Optimize for real workflows, not imagined ones
The Lesson
The best teacher for a system is its own usage. Build in observation and adaptation from day one, and your system will evolve to fit its users perfectly.
End of current notes. More patterns discovered weekly.
machine-learningpatternsadaptive-systems