Class: KeystrokeDynamics::Analysis
- Inherits:
-
Object
- Object
- KeystrokeDynamics::Analysis
- Defined in:
- lib/keystroke_dynamics/analysis.rb
Overview
Summary
The Analysis class is used to:
-
gather keystroke data from attached GTK widgets
-
calculate character-specific statistics from the gathered keystroke data to base a metric on
-
calculate an average metric, consisting of min/max/mean seek/hold times per character, over several instances of keystroke data using these statistics
-
calculate the deviiation between these average metrics
-
calculate general statistics from the gathered keystroke data, to provide real time feedback to users
Data structures
First, a signal handler is attached to log key data. These are keystrokes with metadata such as time_pressed, time_released. These are elaborated on by calculating the hold time and the seek time since the last character interactively. These keystrokes are stored in an array which ensures they stay in an ordered position. The statistics and metric functions operate on an array of these keystroke arrays, which allows for test data from several widgets to be analyzed simultaneously. This metric is a hash of keystrokes with more explicit biometric information, namely min/max/mean seek/hold times. This metric can be tested for simmilarity to a refrence metric by counting the number of deviations in ms from the reference metric. The reference metric is actually calculated using the same metric function, because it is able to condense many instances of widgets (arrays of arrays) to a metric. Averaging over many widgets ensures any deviation gets more flattened out, and thus makes the metric more accurate.
Notes
This class is instantiable as an Analysis, to capture input and do simple statistics on it. However, it also houses all the logic to calculate and compare the metric as class methods, which can be called from everywhere. (So it is partly like a static class would be in other languages.)
Instance Attribute Summary collapse
-
#keystrokes ⇒ Object
This array houses the analyzed keystrokes, with hashes like
{:time_pressed, :time_released, :character, :seek_time, :hold_time}
.
Class Method Summary collapse
-
.compare_metrics(metric_test, metric_ref) ⇒ Object
Returns deviation as an int betwoon 0 and 1.
-
.metric(keystroke_array_array) ⇒ Object
Calculates mean, min and max seek/hold times per character from an array of keystroke arrays.
-
.statistics(keystroke_array_array) ⇒ Object
Calculates total seeks/holds, total seeks/holds time and min/max hold/seek values for each character analyzed.
Instance Method Summary collapse
-
#analyze_keys(widget_array) ⇒ Object
Attaches signal handlers to (an array of) widgets so that keystroke dynamics data can be collected from them.
-
#initialize ⇒ Analysis
constructor
A new instance of Analysis.
-
#last_keystroke ⇒ Object
Returns last recorded keystroke hash.
-
#mean_hold ⇒ Object
Returns mean hold time for all analyzed keystrokes.
-
#mean_kps ⇒ Object
Returns mean number of keystrokes per second.
-
#mean_seek ⇒ Object
Returns mean seek time for all analyzed keystrokes.
Constructor Details
#initialize ⇒ Analysis
Returns a new instance of Analysis.
39 40 41 42 43 |
# File 'lib/keystroke_dynamics/analysis.rb', line 39 def initialize # Using an array ensures the keystrokes are stored in the order they were inserted. # This is important because the release event handler needs to look through the list to find the last key which was pressed, but possibly not yet released. @keystrokes = [] end |
Instance Attribute Details
#keystrokes ⇒ Object
This array houses the analyzed keystrokes, with hashes like {:time_pressed, :time_released, :character, :seek_time, :hold_time}
. If the concept of hashes is unfamilliar to you, it might help to think of them like simple structs.
37 38 39 |
# File 'lib/keystroke_dynamics/analysis.rb', line 37 def keystrokes @keystrokes end |
Class Method Details
.compare_metrics(metric_test, metric_ref) ⇒ Object
Returns deviation as an int betwoon 0 and 1. Returns 0 if the max deviation is reached or exceeded. Returns 0.5 if half of the max deviation is reached. Returns 1 if the deviation is 0, ie the hashes of keystrokes match perfectly.
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 |
# File 'lib/keystroke_dynamics/analysis.rb', line 141 def self.compare_metrics(metric_test, metric_ref) deviation = 0 metric_test.each_pair do |idx,keystroke| if metric_test[idx.to_sym].is_a?(Hash) && metric_ref[idx.to_sym].is_a?(Hash) # Deviation will increase by the amount of ms seeks and holds differ from mean. mean_seek_diff = (metric_test[idx.to_sym][:mean_seek].to_i - metric_ref[idx.to_sym][:mean_seek].to_i) mean_hold_diff = (metric_test[idx.to_sym][:mean_hold].to_i - metric_ref[idx.to_sym][:mean_hold].to_i) if mean_seek_diff > 0 deviation += mean_seek_diff else deviation -= mean_seek_diff end if mean_hold_diff > 0 deviation += mean_hold_diff else deviation -= mean_hold_diff end # Deviation will increase by the amount of ms seeks and holds exceed min/max holds and seeks. if metric_test[idx.to_sym][:mean_hold] < metric_ref[idx.to_sym][:min_hold] deviation += (metric_ref[idx.to_sym][:min_hold] - metric_test[idx.to_sym][:mean_hold]).to_i end if metric_test[idx.to_sym][:mean_hold] > metric_ref[idx.to_sym][:max_hold] deviation += (metric_test[idx.to_sym][:mean_hold] - metric_ref[idx.to_sym][:max_hold]).to_i end if metric_test[idx.to_sym][:mean_seek] < metric_ref[idx.to_sym][:min_seek] deviation += (metric_ref[idx.to_sym][:min_seek] - metric_test[idx.to_sym][:mean_seek]).to_i end if metric_test[idx.to_sym][:mean_seek] > metric_ref[idx.to_sym][:max_seek] deviation += (metric_test[idx.to_sym][:mean_seek] - metric_ref[idx.to_sym][:max_seek]).to_i end end end if(deviation > MAX_ALLOWED_DEVIATION) return 0 elsif((0 < deviation) && (deviation < MAX_ALLOWED_DEVIATION)) return 1-(deviation.to_f/MAX_ALLOWED_DEVIATION) else return 1 end end |
.metric(keystroke_array_array) ⇒ Object
Calculates mean, min and max seek/hold times per character from an array of keystroke arrays. Returns a hash with {character.to_sym => {:mean_seek, :mean_hold, :min_seek, :max_seek, :min_hold, :max_hold}}
.
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 |
# File 'lib/keystroke_dynamics/analysis.rb', line 186 def self.metric(keystroke_array_array) stats = self.statistics(keystroke_array_array) metric = {} stats.each_pair do |idx, key| metric[idx] = { :mean_hold => (key[:hold_total] / key[:holds]), :mean_seek => (key[:seek_total] / key[:seeks]), :min_seek => key[:min_seek].to_i, :max_seek => key[:max_seek].to_i, :min_hold => key[:min_hold].to_i, :max_hold => key[:max_hold].to_i } end metric end |
.statistics(keystroke_array_array) ⇒ Object
Calculates total seeks/holds, total seeks/holds time and min/max hold/seek values for each character analyzed. Returns a hash with {character.to_sym => {:seek_total, :hold_total, :seeks, :holds, :character, :min_seek, :max_seek, :min_hold, :max_hold}}
.
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 |
# File 'lib/keystroke_dynamics/analysis.rb', line 204 def self.statistics(keystroke_array_array) stats = {} keystroke_array_array.each do |keystroke_array| keystroke_array.each do |keystroke| key = keystroke[:character].to_sym stats[key] = {:seek_total => 0, :hold_total => 0, :seeks => 0, :holds => 0, :character => keystroke[:character]} unless stats[key].is_a?(Hash) stats[key][:seek_total] += keystroke[:seek_time].to_i stats[key][:hold_total] += keystroke[:hold_time].to_i stats[key][:seeks] += 1 stats[key][:holds] += 1 stats[key][:min_seek] = keystroke[:seek_time].to_i if stats[key][:min_seek].to_i > keystroke[:seek_time].to_i stats[key][:max_seek] = keystroke[:seek_time].to_i if stats[key][:max_seek].to_i < keystroke[:seek_time].to_i stats[key][:min_hold] = keystroke[:hold_time].to_i if stats[key][:min_hold].to_i > keystroke[:hold_time].to_i stats[key][:max_hold] = keystroke[:hold_time].to_i if stats[key][:max_hold].to_i < keystroke[:hold_time].to_i end end stats end |
Instance Method Details
#analyze_keys(widget_array) ⇒ Object
Attaches signal handlers to (an array of) widgets so that keystroke dynamics data can be collected from them.
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
# File 'lib/keystroke_dynamics/analysis.rb', line 46 def analyze_keys() = [] if !.is_a?(Array) .each do || .add_events(Gdk::Event::KEY_PRESS) .add_events(Gdk::Event::KEY_RELEASE) .signal_connect("key-press-event") do |w, e| if(e.keyval) keystroke = {:time_pressed => e.time, :character => Gdk::Keyval.to_name(e.keyval).to_s} last = last_keystroke || {} # Calculates seek time. if(last[:time_pressed] != nil) keystroke[:seek_time] = (keystroke[:time_pressed] - last[:time_pressed]) if last[:time_pressed] != nil end @keystrokes << keystroke end # Lets the event propagate up to the original signal handler. false end .signal_connect("key-release-event") do |w, e| if(e.keyval) # Calculates hold time. # Iterates through the array in reverse to find the last pressed, but not yet released key. @keystrokes.reverse_each do |keystroke| if(keystroke[:time_released] == nil) keystroke[:time_released] = e.time keystroke[:hold_time] = (e.time - keystroke[:time_pressed]) if keystroke[:time_pressed] != nil end end end # Lets the event propagate up to the original signal handler. false end end end |
#last_keystroke ⇒ Object
Returns last recorded keystroke hash.
83 84 85 86 87 88 89 90 91 92 |
# File 'lib/keystroke_dynamics/analysis.rb', line 83 def last_keystroke # Looks if we are still in the middle of our last keystroke, and returns it if necessary. @keystrokes.reverse_each do |keystroke| if(keystroke[:time_released] == nil) return keystroke end end # If all the logged keystrokes were released, return the last one in the array. @keystrokes.last end |
#mean_hold ⇒ Object
Returns mean hold time for all analyzed keystrokes. Used to display realtime statistics in the application.
111 112 113 114 115 116 117 118 119 120 121 122 |
# File 'lib/keystroke_dynamics/analysis.rb', line 111 def mean_hold mean = Analysis.metric([@keystrokes]) mean_hold = 0 mean.each_pair do |idx, keystroke| mean_hold += keystroke[:mean_hold].to_i end if mean.size.to_i != 0 return (mean_hold.to_i / mean.size.to_i).to_i else return 0 end end |
#mean_kps ⇒ Object
Returns mean number of keystrokes per second. Used to display realtime statistics in the application.
126 127 128 129 130 131 132 133 134 135 |
# File 'lib/keystroke_dynamics/analysis.rb', line 126 def mean_kps last = last_keystroke || {} first = @keystrokes.first || {} time_in_ms = (last[:time_pressed].to_i - first[:time_pressed].to_i).to_f if time_in_ms != 0 return (@keystrokes.size.to_f / ((time_in_ms / 1000).to_f)).to_i else return 0 end end |
#mean_seek ⇒ Object
Returns mean seek time for all analyzed keystrokes. Used to display realtime statistics in the application.
96 97 98 99 100 101 102 103 104 105 106 107 |
# File 'lib/keystroke_dynamics/analysis.rb', line 96 def mean_seek mean = Analysis.metric([@keystrokes]) mean_seek = 0 mean.each_pair do |idx, keystroke| mean_seek += keystroke[:mean_seek].to_i end if mean.size.to_i != 0 return (mean_seek.to_i / mean.size.to_i).to_i else return 0 end end |