Professional fighting and the world of Clojure // Exploring fighters and their fights with the Clojure programming language.
NOTE: This was written in a Gorilla REPL and is known to not format/indent Clojure code correctly
A namespace allows other namespaces to use it's components and to avoid symbol collision. Using the :as
keyword means that the required namespace will be given an alias within
the current namespace. Aliases are usally a shorter, clearer,
representation of an original namespace name.
Using :refer
imports foreign symbols into the current namespace so that the symbol can be used without the original namespace prefix.
(ns ufc
(:require
[gorilla-plot.core :as plot]
[gorilla-renderable.core :as r]
[gorilla-repl.html :as h]
[clojure.pprint :refer [pprint
print-table]]
[clojure.data.json :as json]
[hiccup.core :refer [html]]))
(println "My NS => " *ns*)
My NS => #<Namespace ufc>
nil
stats-json-fp
represents the default file path relative to the project directory and is used as a fallback file path.
entry-point
is the default key which holds an array value.
stats*
loads the data file and returns the value of the entry point.
stats
calls stats*
but memoizes the results to improve the performance of future calls.
xxxxxxxxxx
;; Define the default dataset file
(def stats-json-fp "complete-fighter-stats.json")
;; Define the default JSON key
(def entry-point
"Default JSON key for main data"
"fighters")
;; Function gets the JSON value of entry-point
(defn stats*
"Loads JSON file and returns entry-point value."
([fp] (stats* fp entry-point))
([fp entry-point]
(-> fp
slurp
json/read-str
(get entry-point {}))))
;; Create a version of stats* that caches the results
(let [f (memoize stats*)]
(defn stats
"Loads JSON file and returns cached entry-point value."
([]
(f stats-json-fp))
([^:String fp]
(f fp entry-point))
([^:String fp
^:String entry-point]
(f fp entry-point))))
#'ufc/stats
Taking a peek at the dataset, the first read is going to be much slower than future reads.
(-> (stats) count time)
"Elapsed time: 1617.038439 msecs"
574
And now the cached version should be a lot faster.
xxxxxxxxxx
(-> (stats) count time)
"Elapsed time: 0.064509 msecs"
574
Finding possible attributes of a fighter is as simple as pulling the keys off of a fighter hash-map.
(let [fighter (first (stats))
fighter-attributes (keys fighter)
attr-sz (count fighter-attributes)]
(println
(format "A single fighter has %d attributes => \n"
attr-sz))
(doseq [a fighter-attributes]
(println a)))
A single fighter has 18 attributes =>
Age
CareerStats
UFCWeightClass
Fights
UFCRecord
FightingOutOf
Weight
MMAID
Record
DOB
Reach
Height
FighterID
Born
UFCRanking
Name
Stance
Accolade
nil
Each fighter has an array of objects under the Fights
key. fighter-without-fights
is created so that fighters can be presented without every single fight associated with them.
xxxxxxxxxx
(defn fighter-without-fights
"Returns a fighter without fights collection"
[fighter-map]
(dissoc fighter-map "Fights"))
#'ufc/fighter-without-fights
Let's take a look.
(def example-fighter
(rand-nth (stats)))
(-> example-fighter
fighter-without-fights
pprint)
{"Age" "37",
"CareerStats"
{"StrikingDefense" "61.30",
"TakedownDefense" "31.25",
"KDAverage" "0.0000",
"AverageFightTime" "11:25",
"TakedownAverage" "3.7412",
"TakedownAccuracy" "38.14",
"SLpM" "2.0155",
"SubmissionsAverage" "1.2133",
"AverageFightTime_Seconds" "685",
"StrikingAccuracy" "49.18",
"SApM" "1.1999"},
"UFCWeightClass"
{"WeightClassID" nil, "Description" nil, "Abbreviation" nil},
"UFCRecord"
{"Wins" "13", "Losses" "5", "Draws" "0", "NoContests" "0"},
"FightingOutOf" {"City" nil, "State" nil, "Country" nil},
"Weight" "170",
"MMAID" nil,
"Record" {"Wins" "13", "Losses" "5", "Draws" "0", "NoContests" "0"},
"DOB" "1976-11-29",
"Reach" "74.0",
"Height" "72",
"FighterID" "287",
"Born" {"City" "Weehawken", "State" "New Jersey", "Country" "USA"},
"UFCRanking" {"Current" nil, "Previous" nil},
"Name"
{"FirstName" "Ricardo", "LastName" "Almeida", "NickName" "Big Dog"},
"Stance" "Orthodox",
"Accolade" {"Type" nil, "Name" nil}}
nil
It looks okay, but some of the numeric values are represented as Strings.
enforce-type
is used to bruteforce an acceptable representation value. Original object when all possibilities are exhausted.recursive-enforce-type
is used to apply enforce-type to nested values. Eagerly evaluated, so becareful.fighter-without-fights
calls recursive-enforce-type after removing a fighters fight array collection.;; Bruteforce numeric type enforcement
(defn enforce-type
"Integer first, Float second, original object on fail"
[x]
(try
(Integer/parseInt x)
(catch Exception ex
(try
(Float/parseFloat x)
(catch Exception ex x)))))
;; The function used to modify fighter-without-fights fn
(defn recursive-enforce-type
"recursively calls enforce-type on a fighter map"
[x]
(reduce merge
(for [[k v] x
:let [f (if-not (map? v)
enforce-type
recursive-enforce-type)]]
(hash-map k (f v)))))
;; Add recursive-enforce-type to this fn
(defn fighter-without-fights
"Returns a fighter without fights collection"
[fighter-map]
(recursive-enforce-type
(dissoc fighter-map "Fights")))
;; Let's see the results
(-> example-fighter
fighter-without-fights
pprint)
{"Age" 37,
"CareerStats"
{"StrikingDefense" 61.3,
"TakedownDefense" 31.25,
"KDAverage" 0.0,
"AverageFightTime" "11:25",
"TakedownAverage" 3.7412,
"TakedownAccuracy" 38.14,
"SLpM" 2.0155,
"SubmissionsAverage" 1.2133,
"AverageFightTime_Seconds" 685,
"StrikingAccuracy" 49.18,
"SApM" 1.1999},
"UFCWeightClass"
{"Description" nil, "Abbreviation" nil, "WeightClassID" nil},
"UFCRecord" {"NoContests" 0, "Wins" 13, "Draws" 0, "Losses" 5},
"FightingOutOf" {"Country" nil, "City" nil, "State" nil},
"Weight" 170,
"MMAID" nil,
"Record" {"NoContests" 0, "Wins" 13, "Draws" 0, "Losses" 5},
"DOB" "1976-11-29",
"Reach" 74.0,
"Height" 72,
"FighterID" 287,
"Born" {"Country" "USA", "City" "Weehawken", "State" "New Jersey"},
"UFCRanking" {"Current" nil, "Previous" nil},
"Name"
{"NickName" "Big Dog", "LastName" "Almeida", "FirstName" "Ricardo"},
"Stance" "Orthodox",
"Accolade" {"Type" nil, "Name" nil}}
nil
The ->numerics function is needed for finding numeric values in a fighter map. Eagerly evaluated, so becareful.
xxxxxxxxxx
(defn ->numerics
"Recursively filters for numeric values from a fighter map"
[x]
(let [data (recursive-enforce-type x)]
(reduce merge
(for [[k v] data]
(if-not (map? v)
(if-not (number? v)
{}
(hash-map k v))
(->numerics v))))))
(-> example-fighter
fighter-without-fights
->numerics
pprint)
{"Age" 37,
"StrikingDefense" 61.3,
"NoContests" 0,
"TakedownDefense" 31.25,
"KDAverage" 0.0,
"TakedownAverage" 3.7412,
"Weight" 170,
"Wins" 13,
"Draws" 0,
"Reach" 74.0,
"Height" 72,
"FighterID" 287,
"TakedownAccuracy" 38.14,
"SLpM" 2.0155,
"SubmissionsAverage" 1.2133,
"Losses" 5,
"AverageFightTime_Seconds" 685,
"StrikingAccuracy" 49.18,
"SApM" 1.1999}
nil
Everything should be good to go after removing some ID numbers.
without
removes any kv pair from any map object->numeric-map
creates a hash-map based on ->numerics
. All versions of "*ID" are removed.xxxxxxxxxx
(defn without
"Removes values from a fighter map"
[x & ks]
(apply dissoc x ks))
(defn ->numeric-map
"Returns numeric map without IDs"
[x]
(-> x
fighter-without-fights
->numerics
(without "MMAID"
"WeightClassID"
"FighterID")))
(-> example-fighter
->numeric-map
pprint)
{"Age" 37,
"StrikingDefense" 61.3,
"NoContests" 0,
"TakedownDefense" 31.25,
"KDAverage" 0.0,
"TakedownAverage" 3.7412,
"Weight" 170,
"Wins" 13,
"Draws" 0,
"Reach" 74.0,
"Height" 72,
"TakedownAccuracy" 38.14,
"SLpM" 2.0155,
"SubmissionsAverage" 1.2133,
"Losses" 5,
"AverageFightTime_Seconds" 685,
"StrikingAccuracy" 49.18,
"SApM" 1.1999}
nil
Charting the values only requires two attribute charting functions and an attribute loop.
attr-chart
creates a raw Gorilla REPL plotting object and alters internal attributes to create a horizontal value display.single-fighter-attr-chart
calls attr-chart and places the result into a display body with the attribute key as the display header title.xxxxxxxxxx
(defn attr-chart
[fighter-numerical-map k color]
(r/render
(let [v (get fighter-numerical-map k)
v (if (nil? v) 0 v)
chart (plot/list-plot [[v 0]]
:opacity 0.80
:aspect-ratio 0.40
:plot-size 540
:symbol-size 200
:color color
:plot-range [[0 (+ v 10)] :all])]
(assoc-in chart [:content :height] 10))))
(defn single-fighter-attr-chart
[fighter-numerical-map k color]
(reify
r/Renderable
(render [_]
{:type :list-like
:open ""
:close ""
:separator ""
:items
[{:type :html
:content (html [:h3 {:style {:float :left}} k])
:value (pr-str k)}
(attr-chart fighter-numerical-map
k
color)]})))
;; Charting Loop
(let [fighter (->numeric-map example-fighter)
attributes (keys fighter)]
(for [a attributes]
(single-fighter-attr-chart fighter a "green")))
(Age
StrikingDefense
NoContests
TakedownDefense
KDAverage
TakedownAverage
Weight
Wins
Draws
Reach
Height
TakedownAccuracy
SLpM
SubmissionsAverage
Losses
AverageFightTime_Seconds
StrikingAccuracy
SApM
)
A single function can merge the original charting sequence of two seperate fighters to produce a single chart.
example-fighter2
is the comparison object for example-fighter
double-fighter-attr-chart
calls attr-chart
for each fighter passed with a fighter's value display color. xxxxxxxxxx
;; Define a second fighter for comparison charts
(def example-fighter2 (rand-nth (stats)))
(defn double-fighter-attr-chart
[fighter-numerical-map
color
fighter2-numerical-map
color2
k]
(reify
r/Renderable
(render [_]
{:type :list-like
:open ""
:close ""
:separator ""
:items
[{:type :html
:content (html [:h3 {:style {:float :left}} k])
:value (pr-str k)}
(r/render
(plot/compose
(attr-chart fighter-numerical-map
k
color)
(attr-chart fighter2-numerical-map
k
color2)))]})))
;; Charting Loop
(let [fighter (->numeric-map example-fighter)
fighter2 (->numeric-map example-fighter2)
attributes (keys fighter)]
(for [a attributes]
(double-fighter-attr-chart fighter
"orange"
fighter2
"red"
a)))
(Age
StrikingDefense
NoContests
TakedownDefense
KDAverage
TakedownAverage
Weight
Wins
Draws
Reach
Height
TakedownAccuracy
SLpM
SubmissionsAverage
Losses
AverageFightTime_Seconds
StrikingAccuracy
SApM
)
Each fighting comparison chart will need a competitor header, so there will be two functions to make this happen.
fighter-header
builds a HTML display of a single fighter for Gorilla REPL Renderable.fighter-vs-header
takes two fighters and their assigned value colors. Calls fighter-header
for each fighter and merges the display with a H3 HTML element with innerHTML value of "VS".xxxxxxxxxx
(defn fighter-header
[fighter color]
(let [names ["FirstName"
"NickName"
"LastName"]
fname (interpose " "
(map #(get-in fighter
["Name" %])
names))
fname (reduce str fname)]
{:type :html
:content (html
[:h2 {:style (str "color:" color)}
fname])
:value fname}))
(defn fighter-vs-header
[fighter
color
fighter2
color2]
(reify
r/Renderable
(render [_]
{:type :list-like
:open ""
:close ""
:separator (html [:h3 "VS"])
:items
[(fighter-header fighter color)
(fighter-header fighter2 color2)]})))
(fighter-vs-header example-fighter
"purple"
example-fighter2
"blue")
Ricardo Big Dog Almeida
VS
Antoni Hardonk
compare-fighters
calls fighter-vs-header
with the supplied fighters and their assigned colors. Merges the result of fighter-vs-header
with every attribute found in a valid numeric-map of the first supplied fighter.xxxxxxxxxx
(defn compare-fighters
[fighter
color
fighter2
color2]
(let [fnm (->numeric-map fighter)
fnm2 (->numeric-map fighter2)
attributes (keys fnm)]
(into
[(fighter-vs-header fighter
color
fighter2
color2)]
(for [a attributes]
(double-fighter-attr-chart fnm
color
fnm2
color2
a)))))
;; Generate the chart
(compare-fighters example-fighter
"purple"
example-fighter2
"blue")
[Ricardo Big Dog Almeida
VS
Antoni Hardonk
Age
StrikingDefense
NoContests
TakedownDefense
KDAverage
TakedownAverage
Weight
Wins
Draws
Reach
Height
TakedownAccuracy
SLpM
SubmissionsAverage
Losses
AverageFightTime_Seconds
StrikingAccuracy
SApM
]
A visual comparison is nice but sometimes it's better to have a clear winner.
comparatives
is a hash-map of attributes and how they should be perceived.comparative-outcome
will return a collection with the
first value being a string stating how the attribute should be
perceived. The second value is the winner if not a value of :tied
(def comparatives
{"Age" :lower
;; Average Fight Time
"AverageFightTime_Seconds" :higher
;; Knockdowns Landed
"KDAverage" :lower
;; Strikes Absorbed Per Minute
"SApM" :lower
;; Strikes Landed Per Minute
"SLpM" :higher
"StrikingAccuracy" :higher
"StrikingDefense" :higher
"SubmissionsAverage" :higher
"TakedownAccuracy" :higher
"TakedownAverage" :higher
"TakedownDefense" :higher
"Height" :higher
"Reach" :higher
"Draws" :lower
"Losses" :lower
"NoContests" :lower
"Wins" :higher
"Weight" :higher})
(defn comparative-outcome
"Returns coll [^:String better-measure k].
k is :fighter, :fighter2, or :tied"
[fighter
fighter2
k]
(let [win-when (get comparatives k)
win-fn (case win-when
:lower min
:higher max)
fnm (->numeric-map fighter)
f2nm (->numeric-map fighter2)
fv (get fnm k)
f2v (get f2nm k)]
[(condp = win-when
:lower "Lower is better"
:higher "Higher is better")
(if (= fv f2v)
:tied
(condp = (win-fn fv f2v)
fv :fighter
f2v :fighter2))]))
;; Comparing the Age of two example fighters
(comparative-outcome example-fighter
example-fighter2
"Age")
["Lower is better" :fighter]
Knowing who's best isn't the same as stating who's best. This is where comparative-outcome-named
comes in.
comparative-outcome-named
calls comparative-outcome
and assigns the results to the supplied fighters.(defn comparative-outcome-named
[fighter
fighter2
k]
(let [full (fn [fighter]
(reduce str
(interpose " "
(map #(get-in fighter
["Name" %])
["FirstName"
"NickName"
"LastName"]))))
possible {:fighter (full fighter)
:fighter2 (full fighter2)
:tied "Tied"}
[measure-string winner] (comparative-outcome fighter
fighter2
k)]
(format "%s - %s"
measure-string
(winner possible))))
;; Comparing the Age of two example fighters
(print "Age: "
(comparative-outcome-named example-fighter
example-fighter2
"Age"))
Age: Lower is better - Ricardo Big Dog Almeida
nil
compare-fighters-with-attr-wins
calls double-fighter-attr-chart
for every possible attribute between two fighters and then merges the results with a call to fighter-vs-header
xxxxxxxxxx
(defn compare-fighters-with-attr-wins
[fighter
color
fighter2
color2]
(let [fnm (->numeric-map fighter)
f2nm (->numeric-map fighter2)
attributes (keys fnm)]
(reduce into
[(fighter-vs-header fighter
color
fighter2
color2)]
(for [a attributes]
[(double-fighter-attr-chart fnm
color
f2nm
color2
a)
(comparative-outcome-named fighter
fighter2
a)]))))
;; Compare all attributes
(compare-fighters-with-attr-wins example-fighter
"blue"
example-fighter2
"green")
[Ricardo Big Dog Almeida
VS
Antoni Hardonk
Age
"Lower is better - Ricardo Big Dog Almeida" StrikingDefense
"Higher is better - Ricardo Big Dog Almeida" NoContests
"Lower is better - Tied" TakedownDefense
"Higher is better - Antoni Hardonk" KDAverage
"Lower is better - Ricardo Big Dog Almeida" TakedownAverage
"Higher is better - Ricardo Big Dog Almeida" Weight
"Higher is better - Antoni Hardonk" Wins
"Higher is better - Ricardo Big Dog Almeida" Draws
"Lower is better - Tied" Reach
"Higher is better - Antoni Hardonk" Height
"Higher is better - Antoni Hardonk" TakedownAccuracy
"Higher is better - Ricardo Big Dog Almeida" SLpM
"Higher is better - Antoni Hardonk" SubmissionsAverage
"Higher is better - Ricardo Big Dog Almeida" Losses
"Lower is better - Ricardo Big Dog Almeida" AverageFightTime_Seconds
"Higher is better - Ricardo Big Dog Almeida" StrikingAccuracy
"Higher is better - Antoni Hardonk" SApM
"Lower is better - Ricardo Big Dog Almeida"]
Comparing a single fighter with all fighters in the UFC requires seveal new functions.
x-numeric-map
creates a map call fn to ->numeric-map
x-gen-fighters-attr
creates a map call fn to get a value for the supplied attr keyx-gen-numeric-attr
creates a fn composed of x-numeric-map
and a result of a x-gen-fighters-attr
xxxxxxxxxx
(def x-numeric-map
(map ->numeric-map))
(defn x-gen-fighters-attr
[k]
(map #(get % k)))
(defn x-gen-numeric-attr
[attr]
(let [x-map (x-gen-fighters-attr attr)]
(comp
x-numeric-map
x-map)))
(defn list-plot
[attr color]
(plot/list-plot
(sequence (x-gen-numeric-attr attr)
(stats))
:color color))
(defn fighter-compare-all-attr
[fighter color attr]
(plot/compose
(list-plot attr "black")
(plot/list-plot
(sequence (x-gen-numeric-attr attr)
[fighter])
:color color
:symbol-size 240)))
;; Compare
(fighter-compare-all-attr example-fighter
"red"
"Age")
fighter-vs-fighter-compare-all-attr
calls fighter-compare-all-attr
for every supplied fighter and their assigned color. The results are merged to create a single attribute chart.fighters-attr-vs-all
compares a single attribute of two fighters against every other fighter in the UFC to create a single image.xxxxxxxxxx
(defn fighter-vs-fighter-compare-all-attr
[fighter
color
fighter2
color2
attr]
(plot/compose
(fighter-compare-all-attr fighter
color
attr)
(fighter-compare-all-attr fighter2
color2
attr)))
(defn fighters-attr-vs-all
[fighter
color
fighter2
color2
attr]
(let [f fighter
fcolor color
f2 fighter2
f2color color2]
(reify
r/Renderable
(render [_]
{:type :list-like
:open ""
:close ""
:separator ""
:items
[{:type :html
:content (html
[:p
[:h3 attr]
(comparative-outcome-named f f2 attr)])
:value attr}
(r/render
(fighter-vs-fighter-compare-all-attr f
fcolor
f2
f2color
attr))]}))))
;; Compare
(fighters-attr-vs-all example-fighter
"purple"
example-fighter2
"orange"
"Age")
Age
Lower is better - Ricardo Big Dog Almeida
xxxxxxxxxx
(defn fighters-vs-all
[fighter
color
fighter2
color2]
(let [attrs (-> fighter
->numeric-map
keys)]
(into [(fighter-vs-header fighter
color
fighter2
color2)]
(for [a attrs]
(fighters-attr-vs-all fighter
color
fighter2
color2
a)))))
;; Compare
(fighters-vs-all example-fighter
"purple"
example-fighter2
"orange")
[Ricardo Big Dog Almeida
VS
Antoni Hardonk
Age
Lower is better - Ricardo Big Dog Almeida StrikingDefense
Higher is better - Ricardo Big Dog Almeida NoContests
Lower is better - Tied TakedownDefense
Higher is better - Antoni Hardonk KDAverage
Lower is better - Ricardo Big Dog Almeida TakedownAverage
Higher is better - Ricardo Big Dog Almeida Weight
Higher is better - Antoni Hardonk Wins
Higher is better - Ricardo Big Dog Almeida Draws
Lower is better - Tied Reach
Higher is better - Antoni Hardonk Height
Higher is better - Antoni Hardonk TakedownAccuracy
Higher is better - Ricardo Big Dog Almeida SLpM
Higher is better - Antoni Hardonk SubmissionsAverage
Higher is better - Ricardo Big Dog Almeida Losses
Lower is better - Ricardo Big Dog Almeida AverageFightTime_Seconds
Higher is better - Ricardo Big Dog Almeida StrikingAccuracy
Higher is better - Antoni Hardonk SApM
Lower is better - Ricardo Big Dog Almeida]