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>
nilstats-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/statsTaking 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"
574And now the cached version should be a lot faster.
xxxxxxxxxx(-> (stats) count time)"Elapsed time: 0.064509 msecs"
574Finding 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
nilEach 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-fightsLet'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}}
nilIt 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}}
nilThe ->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}
nilEverything 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}
nilCharting 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-fighterdouble-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
nilcompare-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-headerxxxxxxxxxx(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-mapx-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-attrxxxxxxxxxx(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 Almeidaxxxxxxxxxx(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]