Typing Analytics in Emacs
I read an article on the Wolfram Blog earlier this year about analyzing typing patterns. Mr. Letourneau looked at this from the point of view of security – could personal typing style be used in lieu of password mechanisms? But I was inspired to simply find out something about something about my typing speed.
Tldr
I wrote a short Emacs script to track my typing speed. The rest is interesting to you if you care about Emacs scripting, or if you're interested algorithmically. The code can be obtained from my git server. Go look at some sample output and marvel at the bounty of data.
Almost all of my typing takes place in Emacs. Basically the only other typing I do is emails (where I still use the GMail web interface). Since Emacs captures such a wide swath of my typing, I figured that an Emacs hook would be the perfect place to do some basic typing analytics.
Gathering the Data
Luckily, Emacs has a hook called post-self-insert-hook
, which it runs
every time a character is inserted into the buffer1 [1 Why the name? Emacs considers every key press to be a command shortcut; when a key is pressed just to insert the associated character into the test, the command it runs is called self-insert
.], 2 [2 Note that the self-insert
command is special: it is hard-coded into the core Emacs input loop, so you can't defadvice
it or otherwise override it. Or, you can, but Emacs will run the original version. Thus you must use the hook.], 3 [3 Why not a minor mode instead? I'd rather not have to worry about turning it on and off when major modes change. As it is, I turn it on globally during Emacs startup, and after that don't bother with it.].
Since Emacs is single-threaded, I needed to ensure that my hook was
fast enough that it doesn't slow down my typing. I decided that (like
the original article), I'd track the time gap between typing any
particular pair of characters; from now on, I'll call a pair of
characters a "digraph"4 [4 I know that this isn't quite the right term, but it was what I sprang for first.].
The speed constraint tells me that I need to keep the hook fast. That means keeping little state and doing simple things with it, because Emacs-Lisp is not particularly fast. What state do I need to keep? Well, for every digraph, I'll need to track at least the total time spent in that transition, and the number of times I've typed that transition (this lets me later compute the average). Also, to know what digraph was typed, I need the last character typed and the time it was typed.
(defvar *key-press-last* 0) (defvar *key-press-last-time* 0) (defvar *key-press-table* (make-vector (* 96 96) 0.0)) (defvar *key-press-counts* (make-vector (* 96 96) 0))
You'll note that my table of times and counts is 96 by 96. Why 96? Because I'll be tracking only the printable ASCII characters. Tracking the unprintable characters wouldn't add much (Tab, which is rarely used in Emacs, and Newline) and tracking non-ASCII characters would be too space-consuming.
(defun every-key-press-to-idx (last next) (+ (* (- last 32) 96) (- next 32)))
Next, on every character insert, I'd like to update these tables.
(defun record-every-key-press () (let ((char last-command-event) (time (current-time)) (last-char *key-press-last*) (last-time *key-press-last-time*)) (setq *key-press-last* char) (setq *key-press-last-time* time)
First, I grab the just-pressed key and the current time to the microsecond. I store the previously-pressed key and time, and update those bindings.
In Emacs-Lisp, the time is returned as three integers: the most-significant 16 bits of the seconds since the Epoch; the least-significant 16 bits; and the microsecond count. This means that comparing times is somewhat nontrivial. I'm going to simplify things by firstly ignoring any transitions that take longer than a second or so; transitions that long are likely breaks to read code or something, so would just skew the statistics. And, to simplify further, I'll ignore transitions that happen across a second transition that changes the most-significant 16 bits of the time. This drops 1.4 transitions per day at the worst, so is a very minor loss of possible data. Since the transition speed should be independent of these most-significant 16 bits, this shouldn't even lead to skew.
(when (and (listp last-time) (= (car last-time) (car time)) (>= last-char 32) (>= char 32) (< last-char 256) (< char 256)) (let* ((last-time (cdr last-time)) (time (cdr time)) (sd (- (car time) (car last-time)))) (when (< sd 2)
Finally, we calculate the time delta td
and add it to the correct
entries in our key press tables.
(let* ((md (- (cadr time) (cadr last-time)))
(td (+ sd (/ (float md) 1000000.0)))
(entry (every-key-press-to-idx last-char char))
(entry-sum (aref *key-press-table* entry))
(entry-count (aref *key-press-counts* entry)))
(aset *key-press-table* entry (+ entry-sum td))
(aset *key-press-counts* entry (+ entry-count 1))))))))
This function is defined and bound to post-self-insert-hook
, like so:
(add-hook 'post-self-insert-hook #'record-every-key-press)
This accumulates sweet, sweet data about our typing patterns. All that's left is be able to load, save, and auto-save this file:
(defun save-every-key-press () (interactive) (let ((file every-key-press-filename)) (with-temp-buffer (print *key-press-table* (current-buffer)) (print *key-press-counts* (current-buffer)) (when (file-writable-p file) (write-region (point-min) (point-max) file))))) (defun load-every-key-press () (interactive) (let ((file every-key-press-filename)) (with-temp-buffer (insert-file-contents file) (setq *key-press-table* (read (current-buffer))) (setq *key-press-counts* (read (current-buffer)))))) (defun autosave-every-key-press () (interactive) (setq every-key-press-timer (run-with-idle-timer every-key-press-autosave-frequency t 'save-every-key-press)))
I have all of this set up in my .emacs.d/init.el
to run on startup, so
literally all of my Emacs use has this hook enabled. Even on my
ridiculously-slow laptop, this works well and I don't notice any lag.
Displaying the Data
True Emacs mastery would involve presenting the data within Emacs as well, but I don't yet have that skill. Instead, I wrote an HTML application to visualize the data, with JavaScript computations and
canvas
pictures.
The first step is getting the data format into something JavaScript can use. In theory, I could load the Emacs save format with AJAX, then parse it in JavaScript. But that seemed downright silly. Instead, I'll use a language actually good at munging plain text; in my case, this language is Python.
INFILE = sys.argv[1] if len(sys.argv) > 1 else os.path.expanduser("~/.emacs.d/keys") OUTFILE = sys.argv[2] if len(sys.argv) > 2 else "keys.jsonp" with open(INFILE) as f: # First line blank assert not f.readline().strip() # Second line has an array v = f.readline().strip() assert v and v[0] == "[" and v[-1] == "]" data = [[float(s)] for s in v[1:-1].split()] # Third line blank assert not f.readline().strip() # Fourth line has another array v = f.readline().strip() assert v and v[0] == "[" and v[-1] == "]" for i, s in enumerate(v[1:-1].split()): data[i].append(int(s)) assert len(data) == 96 * 96 with open(OUTFILE, "w") as f: f.write("load_data(") f.write(str(data)) f.write(")")
I'm a big fan of using the right tool for the right job, and I think
this is a good example. I run this script every once in a while to
update the display, though in theory I might kick it off from Emacs
or a cron
job.
Note that the Python writes out a file in the format
load_data([[time, cnt], ...])
This is because I'm going to usually be loading this HTML report
generator from the local disk, and AJAX requests don't necessarily
work in all browsers when browsing on file://
; so instead, I'm simply
going to run this file after loading all of my other JavaScript:
window.data = void(0); function load_data(data) { for (var i in data) { if (data[i][1] == 0) delete data[i]; } window.data = data }
With the data loaded, I can plot it in canvas and run some basic analytics. I'm not going to run through the implementation there; feel free to peek in if you want. Let's instead talk about some the data I capture.
The Data
I've been running the setup above for a few months now, on and off. Since I use both my desktop and my laptop, I have two separate sets of data (I ought to work on merging them). I'll use my desktop set, which is a little less populated. I've put up this output as an example; you might want to look at it to follow along.
This set of key transitions spans 76883 key strokes, for a total typing time of 4 hours and 6 minutes. This might seem odd, but keep in mind this is four hours of continuous typing. I'm rarely typing continuously in anything, since even when coding or writing, I need to stop and think about what to code or write. There are 2838 different digraphs among these transitions, which is approximately 31% of the space of all digraphs that I record.
If you look at the graph, you see that the most frequent transitions are kind of obvious: "e ", "in", "th", " t", "s ", " ", and "er". There's a vague peak, wherein almost all key strokes take somewhere between 60 milliseconds (that's "io") and 224 milliseconds (" a"). In the future, I'll track standard deviations for each digraph. That'll actually let us use this as an authentication mechanism (as Mr. Letourneau suggested).
But there's more data to dig up. For example, we can calculate your real-world typing speed. Typing tests often give higher-than-average readings because you're typing relatively simple texts; because you have the text in front of you, instead of composing on the fly; and because in the real world you use far stranger combinations of characters.
To calculate the real-world typing speed, I compute the frequency of space characters (as the first character of a digraph, for simplicity), divide the total number of transitions by this (to compute the average word length), and then use the average transition time to compute the average typing speed. This isn't quite perfect for various reasons, but I don't think it is too far off an estimate. My typing test typing speed clocks in at 78 words per minute (I just tried it); my real-world speed is a paltry 47. Not surprising: much of my typing is programming, which involves odd punctuation and long variable names, compared to the simple English text of typing tests. I think the vast speed difference is pretty interesting though. I conjecture that typing tests are a good relative measure, but a terrible absolute measure, of typing speed.
My favorite analysis, though, is more in-depth. I'd like to measure the difference in typing speed between using different hands or the same hand for a key transition; for using the same or different fingers; and so on. We're told, for example, that alternating which hand is used is one of the reasons Dvorak and Colemak layouts are superior to Qwerty. Let's quantify that.
The first step is to figure out which fingers type which characters. I experimented a bit with my own typing and got the following mapping:
// l and r stand for left and right; L and S for Lowercase and Shifted var hands = { lL: ["az", "123wsx", "4ed", "56rtfgcv", " "], lS: ["AZ", "!@#WSX", "$ED", "%^RTFGCV", ""], rL: [";'/", "pol0.", "9ik", "78yuhjnmb", ""], rS: [":\"", "POL)>", "(IK", "&*YUHJNMB", ""] }
The member names, like lL
, describe the hand (left or right) and case
(whether shift is pressed); I always shift a character with the
opposite hand's pinky finger. The keys are presented by finger, from
pinky to thumb. For example, you can see that I type spaces with my
left thumb. You might type keys differently, but let's allow this to
be demonstrative.
I can then go through every digraph and classify it as same or different hands and same or different fingers. I collapse the different-hand variants (same or different fingers), because they gave the same results – there's no "priming" I could see of the same finger across different hands. This leads to the following table:
Transition Type | Time |
---|---|
Different Hands | 170 ms |
Same Hands, Different Fingers | 182 ms |
Same Hands, Same Fingers | 243 ms |
Double-tapping a Key | 263 ms |
Overall Average | 183 ms |
Nothing about this table is too unexpected – of course using the same finger twice in a row is slow, since it needs to be moved. But the fact that double-tapping a key is slower yet is interested – it suggests that the bottleneck is actually tapping the key, not moving it. Also, the jump from different fingers to same fingers is so much larger than from different to same hands that it seems like a better advertisement for alternate keyboard layouts would be that they minimize same-finger digraphs.
Links
If you'd like to run this same typing analysis for yourself, go to
my git repository and download keylogger.el
.
Footnotes:
Why the name? Emacs considers every key press to be a command shortcut; when a key is pressed just to insert the associated character into the test, the command it runs is called self-insert
.
Note that the self-insert
command is special: it is hard-coded into the core Emacs input loop, so you can't defadvice
it or otherwise override it. Or, you can, but Emacs will run the original version. Thus you must use the hook.
Why not a minor mode instead? I'd rather not have to worry about turning it on and off when major modes change. As it is, I turn it on globally during Emacs startup, and after that don't bother with it.
I know that this isn't quite the right term, but it was what I sprang for first.