Effective way of finding the median in a list of numbers

Hello Nigel.

My idea was solely, to avoid picking the one that lead to the “wrong” result, over and over, due to some freak pattern in the input, that coincides with the halving. And the code as such, was just meant to illustrate what I meant, as I grasp that sometimes something like that is used, as an “insurance” against the worst case scenario. I really meant to median position, and not the median, as you describe it. The two terms are often used interchangeably though. Maybe I should look over my use of pivot, and pivotvalue, I seem to use pivot for both of those as well. I do reckognize the importance, to communicate such subjects in an unambigous way, and will take greater care for the future.

It’s been a busy day, I’ll post the next version of the median handler: find/quickselect by the afternoon.

By the way, now that a large part of the world that doesn’t write from left to right parttake in programming, wouldn’t it be better to name the the start and end of a range, consistently with “start” and “end” instead of “left” and “right”. This is no critique of you of course, I came to think of it the other day, and felt just that the general convention of using “left” and “right” to denote the start and end of a range, is a bit outdated by now. :slight_smile:

Hah! Having been under the impression that the only dialect of AppleScript since Mac OS 9.0 has been “International English”, I switched my system over to Arabic just now to see what things would look like in Script Editor. I was greeted with the ridiculous spectacle of script texts being in English and written from left to right, but aligned hard against the right margins without indentation. Lists were indeed ordered from right to left instead of vica versa ” but only in the Result pane. Typing and running ‘set l to {1, 2, 3, 4, 5}’ returned ‘{5, 4, 3, 2, 1}’.

But in any case, since I’m unlikely at my age to write about scripts in anything other than a western European language ” specifically British English ” I think I’ll stick with “left” and “right” where convenient to avoid the linguistic acrobatics that would be required otherwise. :slight_smile:

Hello Nigel.

I didn’t mean you, nor just AppleScript, I meant that the whole landscape for using languages has changed, since left or right was introduced, probably sometime in the 1950’s. And I wasn’t after how things are written, but the “mental model” of it. An Arabic, or Chinese person, people from other regions, may start from the right side, and therefore think of the left, as the end of it. Hopefully code is written in English, since that is the Latin of our day. But sticking with a western language, doesn’t mean that we shouldn’t adapt global mental-models we all can share.

Left and right is pretty established by now, and I think it is at least impractical to change it by now. But new algorithms and api’s should probably consider such issues, along with the fact that we now support writing and reading from right to left, in order to provide as much of “wholeness” as possible, making the environment we live and breathe in as friendly as possible on a global scale.

I mentioned it, in general, because it touched the theme of naming things unambiguosly. I actually pondered writing a blog post about it, but dismissed it, as being too “nerdy”. :slight_smile:

Hello.

Here is the last and best version of the “find k’th least element”, that can be used for finding the median of a list, probably in order to pick the root element when building a binary tree in order to keep it balanced.

Hoare’s original version, beat the previous version with almost 50% -again. :slight_smile:


property scriptTitle : "Median Hoare Version"
set maxitems to 1000

script pseudo
	-- pure multiplicative random generator.
	-- Its  faster most of the time than Standard Additions random number  (10000 runs).
	-- it has been  proved that the sequence doesn't repeat itself before 2^31 -2 calls have been made.
	-- yet: No warranties about anything. 
	-- Rosen "Discrete Mathematics" p. 207
	property x0 : 3
	property rmod : 2 ^ 31 - 1
	property multiplier : 7 ^ 5
	
	on rand()
		set x0 to (multiplier * x0) mod rmod
		return (x0 / 1.0E+9)
	end rand
	on init()
		rand()
	end init
end script

pseudo's init()
set l to {}
repeat maxitems times
	set end of l to rand(1, maxitems)
end repeat
set max to 0
repeat with i from 1 to maxitems
	if item i of l > max then set max to item i of l
end repeat
log "max: " & max

set min to (maxitems + 1)
repeat with i from 1 to maxitems
	if item i of l < min then set min to item i of l
end repeat
log "min: " & min

set ll to length of l
set k to ll div 2 + 1 * (((ll mod 2) > 0) as integer)

set t0 to (current date)
if 1 = 1 then
	repeat 1000 times
		set md to find(l, k)
	end repeat
else
	set md to find(l, k)
end if
set t1 to ((current date) - t0) / 1000
log "time T : " & t1
log "median val: " & md
set t0 to (current date)
set m to countingsort(l, max)
set t1 to ((current date) - t0) / 1000
log "time T : " & t1

log "low: " & item (k - 1) of m
log "median: " & item k of m
log "high: " & item (k + 1) of m


(*
	This version uses at most 2n comparisions in the average case ( O(n) )
	and O(n^2) comparisions in worst case. Probability of a worst case is
	greater than ( 1 / n! ) and *much* lesser than 1/n.
*)

on find(|L|, k)
	-- returns the k'th least element of a list. 
	-- By C.A.R Hoare in an article. Implemented in Pascal by N. Wirth
	-- Adapted to AppleScript by McUsr.
	script o
		property A : |L|
	end script
	
	set low to 1
	set high to length of |L|
	
	repeat while high > low
		set x to item k of o's A
		set i to low
		set j to high
		repeat while i ≤ j
			repeat while x > item i of o's A
				set i to i + 1
			end repeat
			repeat while item j of o's A > x
				set j to j - 1
			end repeat
			if i ≤ j then
				set w to item i of o's A
				set item i of o's A to item j of o's A
				set item j of o's A to w
				set i to i + 1
				set j to j - 1
			end if
		end repeat
		if j < k then set low to i
		if k < i then set high to j
	end repeat
	return item k of o's A
end find

on rand(low, high)
	-- returns  a random integer, within low and high inclusive
	-- Made by McUsr
	set k to high - low + 1
	if 0 = 1 then
		return ((random number) * k mod k div 1 + low)
	else
		return ((pseudo's rand()) * k mod k div 1 + low)
	end if
end rand

on countingsort(|L|, k)
	--    countingsort: origin unknown. found it in an algorithms lecture from MIT
	--    Implemented in AppleScript by McUsr 2015/6/24
	script o
		property l : |L|
		property C : missing value
		property Cp : missing value
		property B : missing value
	end script
	set ll to length of o's l
	copy |L| to o's C
	repeat with i from 1 to ll
		set item i of o's C to 0
	end repeat
	
	repeat with i from 1 to ll
		set item (item i of o's l) of o's C to (item (item i of o's l) of o's C) + 1
	end repeat
	copy o's C to o's Cp
	
	repeat with i from 2 to k
		set item i of o's Cp to (item (i - 1) of o's Cp) + (item i of o's C)
	end repeat
	copy o's C to o's B
	
	repeat with j from ll to 1 by -1
		tell item j of o's l
			set item (item it of o's Cp) of o's B to it
			set item it of o's Cp to (item it of o's Cp) - 1
		end tell
	end repeat
	return o's B
end countingsort

Edit
Removed some debugging info.
Incorporated the fast random number generator, just for the hell of it. :wink:

Here’s a different approach by way of experiment. It counts how many values are less than and greater than each value in the range. When neither count is more than the half the length of the range, the current value is either the median value or one of the two which have to be averaged to get it. The huge cost of comparing every value with all the others is mitigated by only doing it with values lying between contracting limits which have proved too high or too low earlier in the process.

The script here actually takes two or three times as long as my other one, but the difference only becomes noticeable with about three thousand or so values. :slight_smile:

Edit: Previous bug fix replaced with a more satisfactory compromise. Also some reworking of comments and a couple of variable name changes.
Further Edit: It turns out that a simpler fix for the bug was simply to leave out the aspect of the “limits” idea which led to it! :rolleyes: The preliminary sample of lowest and highest values is no longer carried out. There’s no discernable effect on performance.

(* Find the median of the values (presumed to be integers) in a range of a list.
By Nigel Garvey 2015.
With odd numbers of items, the median value's the one which would be in the middle if the range were sorted. With even numbers of items, it's the average of the two middle values.

Parameters: (list, range index 1, range index 2)
*)

on medianValue(theList, l, r)
	script o
		property lst : theList
	end script
	
	-- Process the range parameters.
	set listLen to (count theList)
	if (l < 0) then set l to listLen + l + 1
	if (r < 0) then set r to listLen + r + 1
	if (l > r) then set {l, r} to {r, l}
	if ((l < 1) or (r > listLen)) then error "Duff range parameters!" -- Compose as required.
	
	set rangeLen to r - l + 1
	set halfRangeLen to rangeLen div 2
	set oddLen to (rangeLen mod 2 is 1)
	-- Limits to be set and tightened as the search discovers what's too low or too high.
	set tooLow to missing value
	set tooHigh to missing value
	-- Result variables.
	set result1 to missing value
	set result2 to missing value
	
	-- The search takes each value which isn't known to be too low or too high and checks how many of the other values are higher than it and how many are lower. When neither count is more than half the length of the range, the current value is either the median or one of the two values which must be averaged to get it.
	repeat with i from l to r
		set iVal to item i of o's lst
		if (((tooLow is missing value) or (iVal > tooLow)) and ((tooHigh is missing value) or (iVal < tooHigh))) then
			-- This value's within the current limits. Compare it with all the other values in the range.
			set lowerCount to 0
			set higherCount to 0
			repeat with j from l to r
				if (j is i) then
					-- Don't compare it with itself!
				else
					set jVal to item j of o's lst
					-- Increment the lower or higher count (or do nothing) accordingly.
					-- (It's slightly faster to complete the counts and check them afterwards than it is to check after each increment in the hope of finishing early.)
					if (jVal < iVal) then
						set lowerCount to lowerCount + 1
					else if (jVal > iVal) then
						set higherCount to higherCount + 1
					end if
				end if
			end repeat
			
			-- Now act on the count results.
			if (lowerCount > halfRangeLen) then
				-- Over half the other values are lower than this one, so it's too high.
				set tooHigh to iVal
			else if (higherCount > halfRangeLen) then
				-- Over half the other values are higher, so it's too low.
				set tooLow to iVal
			else if (result1 is missing value) then
				-- This value's the first hit. Log it and, if the range is an odd length, end the search.
				set result1 to iVal
				if (oddLen) then exit repeat
			else if (iVal is result1) then
				-- This value's the same as the first hit. Ignore it.
			else
				-- This value's the second hit. Log it and end the search.
				set result2 to iVal
				exit repeat
			end if
		end if
	end repeat
	
	-- Return the first hit if there's only one; otherwise the average of the two, as an integer if possible.
	if (result2 is missing value) then return result1
	set |median| to (result1 + result2) / 2
	tell |median| as integer to if (it is |median|) then set |median| to it
	return |median|
end medianValue

--(* Demo:
set l to {}
repeat with i from 1 to 6
	set end of my l to (random number 1000)
end repeat

log l
-- Find the median of values 1 thru -1 of l.
set m to medianValue(l, 1, -1)

Hello Nigel.

Your last version, looks interesting too. I think that there is something proportional to log n, that makes the difference kick in noticably at 3000 items.

Well, I didn’t come around to it today, but I’m going to present two O(n) versions, at least over some days, so it works properly when I post it. The first is a cheat, and based on CountingSort above, the other is the median of medians algorithm, which is the real deal, that also has a worstcase of O(n). The problem with that solution as I see it, is that you have to calibrate the algorithm with the number of items, as I think you need at least 25 elements for the median of medians to work. Well, N.Wirths says that if you have less than or equal to 10 items then you should sort and pick the median from the sorted elements. I am also a bit eager to use the median for building a complete binary tree, but I haven’t figured yet, if that is possible or a feasible way to do it. (But then I really need the list to be sorted anyway.) :slight_smile:

Hello.

I came by this little random number generator, that promises to generate random numbers, perceived as random by humans, (not totally uniformly distributed). Which may be a technique suitable sometimes, when needing random number s for something. This random number generator, have been suitable for doom, so it should be suitable for small AppleScript games. You can read all about it in the links in the code.

script perlinsRand
	-- https://github.com/id-Software/DOOM/blob/master/linuxdoom-1.10/m_random.c
	-- https://news.ycombinator.com/item?id=9809998
	property randInts : {0, 8, 109, 220, 222, 241, 149, 107, 75, 248, 254, 140, 16, 66, ¬
		74, 21, 211, 47, 80, 242, 154, 27, 205, 128, 161, 89, 77, 36, ¬
		95, 110, 85, 48, 212, 140, 211, 249, 22, 79, 200, 50, 28, 188, ¬
		52, 140, 202, 120, 68, 145, 62, 70, 184, 190, 91, 197, 152, 224, ¬
		149, 104, 25, 178, 252, 182, 202, 182, 141, 197, 4, 81, 181, 242, ¬
		145, 42, 39, 227, 156, 198, 225, 193, 219, 93, 122, 175, 249, 0, ¬
		175, 143, 70, 239, 46, 246, 163, 53, 163, 109, 168, 135, 2, 235, ¬
		25, 92, 20, 145, 138, 77, 69, 166, 78, 176, 173, 212, 166, 113, ¬
		94, 161, 41, 50, 239, 49, 111, 164, 70, 60, 2, 37, 171, 75, ¬
		136, 156, 11, 56, 42, 146, 138, 229, 73, 146, 77, 61, 98, 196, ¬
		135, 106, 63, 197, 195, 86, 96, 203, 113, 101, 170, 247, 181, 113, ¬
		80, 250, 108, 7, 255, 237, 129, 226, 79, 107, 112, 166, 103, 241, ¬
		24, 223, 239, 120, 198, 58, 60, 82, 128, 3, 184, 66, 143, 224, ¬
		145, 224, 81, 206, 163, 45, 63, 90, 168, 114, 59, 33, 159, 95, ¬
		28, 139, 123, 98, 125, 196, 15, 70, 194, 253, 54, 14, 109, 226, ¬
		71, 17, 161, 93, 186, 87, 244, 138, 20, 52, 123, 251, 26, 36, ¬
		17, 46, 52, 231, 232, 76, 31, 221, 84, 37, 216, 165, 212, 106, ¬
		197, 242, 98, 43, 39, 175, 254, 145, 190, 84, 118, 222, 187, 136, ¬
		120, 163, 236, 249}
	property randEntries : length of randInts
	property curRand : 0
	on clearRnd()
		set curRand to 0
	end clearRnd
	on next()
		if curRand < randEntries then
			set curRand to curRand + 1
		else
			set curRand to 1
		end if
		return item curRand of randInts
	end next
end script


perlinsRand's next()

Thanks, McUsrII. I must admit that, despite having implemented many sorts in AppleScript over the years, I’ve never got the hang of expressions like “log n” and “O(n)”. I think it’s because they didn’t convey (to me) why things behaved they way they did ” especially in a high-level language like AppleScript, where as much depends on individual command implementations and data characteristics as on the algorithms themselves. But I think it’s about time I looked into “n” notation more thoroughly! :slight_smile:

True, something that keeps AppleScript reminding me every day. It’s never the obvious solution which in some sort of way AppleScript completely misses its targets.

Hello Nigel.

I don’t really ever saw Big O, or Big Theta or Omega as anything but as a proportion as to how the processing time increases, as the dataset evolves. It is a very coarse measure in itself, just giving the order of magnitude, and it really isn’t anything to worry that much about. It is really just one of the trade-off factors. Readability, maintainability, sheer size of the code and so on, being others. Correctness is of course not a trade-off. I seldom do work on so big datasets in AppleScript, that I have to worry about it, since it doesn’t make sense, unless you vary wildly in the size of the datasets.

In this particular case, it may slow down because the handlers has a complexity of n log n that is, that the processing time increases more than the number of elements (linearly). Then the second handler may be slower in a part of the algorithm, which maybe has a log N complexity by itself, (the inner loop), that makes up for the slow increase in time. I guess this since the time doesn’t increase noticably before 3000 items. Your last algorithm is just a fraction slower, than your previous, and that fraction adds up over time, I actually don’t think it is even a log N increase that is involved, but something that adds up slower, maybe log 1/N, as just a wild guess, not having really calculated anything, not even timed it. Just timing both for say 1000, 2000, and 3000 items, and comparing the differences, should give a good clue as to how they develop in propertions, as the number of items increases.

@DJ Bazzie Wazzie. That is also a good point, But big O notation, and complexity analysis, still gives a little hint about thing may progress, or how they should have progressed, hadn’t the number of items in the list started to degenerate performance by itself for instance, due to number of items in a list larger than some treshold. At least we are more informed when we look it up, than not. -Big Oh notation, provides some guidance, nothing more, but some guidance, is better than none. The more info, the more educated the guess.

Complexity analysis is a huge subject, I personally think it is ok, to be familiar with, but not dwell to deep into, because not before long, you are reading up on finite fields, and the law of total probability, (besides starting to remember arithmetic and geometric progressions, and so on). But well, it has it’s charm as well I guess.

Edit

They say that the longer you write about something, the less you really know about it. At least I know that MIT has an open courseware class on the net in Discrete Mathematics, which is a precursor to their open courseware class “Introduction to Algorithms”. You really need to know the Discrete Mathematics, to get something out of that Algorithms course. Lots of video lectures, and readings online, and even notes at least from the recitations. You can pick and choose from whatever lectures you want to watch. The lectureres, are really aces, but I tend to shrink a little in my chair, when they enthusiastically talks about “very cool mathematics”. :wink:

Hello.

Here is the “cheat” version for finding the median, the reason for this, is that I actually work with a lot of lists those day, where the max element is bounded by the length of the list. That justifies this very special version, -that performs in linear time. O(N). It also sorts the list, which is a necessity, when finding the median with this version.

set thel to {4, 1, 3, 4, 3, 5, 1}
set soughtK to 4 -- median for 7 items is ceil (7 / 2 ) = 4 
countingsortAndMedian(thel, 5, soughtK)
on countingsortAndMedian(|L|, maxval, k)
	--    countingsort: origin unknown. found it in an algorithms lecture from MIT
	--    Implemented in AppleScript by McUsr 2015/6/24
	script o
		property L : |L|
		property C : missing value
		property Cp : missing value
		property B : missing value
	end script
	set ll to length of o's L
	copy |L| to o's C
	repeat with i from 1 to ll
		set item i of o's C to 0
	end repeat
	
	repeat with i from 1 to ll
		set item (item i of o's L) of o's C to (item (item i of o's L) of o's C) + 1
	end repeat
	copy o's C to o's Cp
	
	repeat with i from 2 to maxval
		set item i of o's Cp to (item (i - 1) of o's Cp) + (item i of o's C)
	end repeat
	copy o's C to o's B
	
	repeat with j from ll to 1 by -1
		tell item j of o's L
			set item (item it of o's Cp) of o's B to it
			set item it of o's Cp to (item it of o's Cp) - 1
		end tell
	end repeat
	return {o's B, item k of o's B}
end countingsortAndMedian

I should also be able to build a complete binary tree, while looping over length/2 elements of the sorted array, after having inserted the root element as its root, and populated it with the elements on each side of the median as its children so that I can end up with a complete binary tree, should I wish to. (I see no use for this at the moment, but it may come in handy). The binaray tree, would however be a “read-only” binary tree, in that it would be quite costly to maintain, compared to the cost of re-building it, which should be almost as fast with a small number of items.

is there a way to use this to find Maximum and Minimum values in a list?

thanks

Hi.

Substituting “max:” or “min:” for “median:” seems to work:

use AppleScript version "2.4" -- Mac OS 10.10 (Yosemite) or later.
use framework "Foundation"
use scripting additions

on minAndMaxFromList:theList
	set anNSExpression to current application's NSExpression's expressionForConstantValue:theList
	
	set newNSExpression to current application's NSExpression's expressionForFunction:"min:" arguments:{anNSExpression}
	set minVal to (newNSExpression's expressionValueWithObject:(missing value) context:(missing value))
	
	set newNSExpression to current application's NSExpression's expressionForFunction:"max:" arguments:{anNSExpression}
	set maxVal to (newNSExpression's expressionValueWithObject:(missing value) context:(missing value))
	
	return (current application's NSDictionary's dictionaryWithObjects:{minVal, maxVal} forKeys:{"min", "max"}) as record
end minAndMaxFromList:

set theList to {}
repeat 10 times
	set end of theList to (random number 20)
end repeat

{theList, my minAndMaxFromList:theList}

But this is slightly simpler. It doesn’t work for medians:

use AppleScript version "2.4" -- Mac OS 10.10 (Yosemite) or later.
use framework "Foundation"
use scripting additions

on minAndMaxFromList:theList
	set anNSArray to current application's NSArray's arrayWithArray:theList
	
	set minVal to (anNSArray's valueForKeyPath:"@min.self")
	set maxval to (anNSArray's valueForKeyPath:"@max.self")
	
	return (current application's NSDictionary's dictionaryWithObjects:{minVal, maxval} forKeys:{"min", "max"}) as record
end minAndMaxFromList:

set theList to {}
repeat 10 times
	set end of theList to (random number 20)
end repeat

{theList, my minAndMaxFromList:theList}

I wrote this script in April 2015 to help me with some homework in a statistics class. Perhaps it’s a relevant option.

Hi Marc.

It seems to do the job, despite the implicit coercion of isMode to text at the end being influenced by the TIDs set in the sort handler and the sorted list containing text! :wink: I wasn’t familiar with “mode” in the statistical sense before. From what I’ve been reading about it this morning, it should be either the most frequently occurring value in the data or the co-most frequently occurring values. The interpretation in your script isn’t quite the same.

Once the list is sorted, of course, it’s only necessary to have one repeat, and there are a few other optimisations which can be made.

set valueList to {}
repeat 10 times
	set end of valueList to random number 15
end repeat

if (valueList is {}) then
	set {minValue, maxValue, |mean|, |median|, isMode, midrange, range} to {"None", "None", "None", "None", "None", "None", "None"}
else
	set sortedList to sort(valueList) -- The result's a list of numeric texts, but it doesn't matter here.
	log sortedList
	set listLength to (count sortedList)
	
	-- Initialise various variables to the first item in the sorted list.
	set minValue to sortedList's beginning -- Minimum value.
	set thisValue to minValue -- Preset in case there's only one value and the repeat below isn't executed.
	set sumOfValues to minValue -- Sum for the calculation of the average.
	set valueBeingCounted to minValue -- The value whose occurrences are currently being counted.
	set counter to 1 -- The number of times it's occurred so far.
	set currentMode to {valueBeingCounted} -- The value(s) with the highest occurrence count so far.
	set highestOccurrenceCount to 1 -- The number of times they've occurred.
	-- Work through the rest of the sorted list, updating the sum of the values and the mode data.
	repeat with i from 2 to listLength
		set thisValue to item i of sortedList
		set sumOfValues to sumOfValues + thisValue
		if (thisValue is valueBeingCounted) then
			set counter to counter + 1
		else
			if (counter > highestOccurrenceCount) then
				set currentMode to {valueBeingCounted}
				set highestOccurrenceCount to counter
			else if (counter = highestOccurrenceCount) then
				set end of currentMode to valueBeingCounted
			end if
			set valueBeingCounted to thisValue
			set counter to 1
		end if
	end repeat
	-- If necessary, update the mode from the count in progress at the end of the repeat
	if (counter > highestOccurrenceCount) then
		set currentMode to {valueBeingCounted}
		set highestOccurrenceCount to counter
	else if (counter = highestOccurrenceCount) then
		set end of currentMode to valueBeingCounted
	end if
	
	-- The maximum value is the last that was fetched from the list.
	set maxValue to thisValue
	--Mean (average)
	set |mean| to sumOfValues / listLength
	--Median (middle value or average of two middle values)
	set m to (1 + listLength) div 2
	set |median| to item m of sortedList
	if (listLength - m = m) then set |median| to (|median| + (item (m + 1) of sortedList)) / 2 -- even number of items.
	--Mode (most frequently occurring value(s))
	if (highestOccurrenceCount = 1) then set currentMode to minValue
	set astid to AppleScript's text item delimiters
	set AppleScript's text item delimiters to ", "
	set isMode to "{" & currentMode & "}:" & highestOccurrenceCount & "x"
	set AppleScript's text item delimiters to astid
	--Midrange (average of 1st and last sorted values)
	set midrange to (maxValue + minValue) / 2
	--Range (difference between 1st and last sorted values)
	set range to maxValue - minValue
end if

"Min: " & minValue & " | Max: " & maxValue & return & ¬
	"Mean: " & |mean| & return & ¬
	"Median: " & |median| & return & ¬
	"Mode: " & isMode & return & ¬
	"Midrange: " & midrange & return & ¬
	"RANGE: " & range


on sort(thelist)
	set astid to AppleScript's text item delimiters
	set AppleScript's text item delimiters to linefeed
	set sortedList to (do shell script "echo " & (thelist as text)'s quoted form & " | sort -g")'s paragraphs
	set AppleScript's text item delimiters to astid
	return sortedList -- List of text.
end sort

Shane’s ASObjC code quoted in post #32 can be adapted to return the mode of a list by simply replacing “median:” with “mode:” and changing the coercion at the end to ‘as list’. In this case, the “mode” returned is just a list of the most frequently occurring value(s), with no indication of how often they occur. If there’s only one instance of each value in a list, the result is a list containing just the lowest value. The script above also returns the lowest value instead of “None”, but I’ve no opinion about which is better.

use framework "Foundation"
use scripting additions

on modeOfList:theList
	set anNSExpression to current application's NSExpression's expressionForConstantValue:theList
	set newNSExpression to current application's NSExpression's expressionForFunction:"mode:" arguments:{anNSExpression}
	return (newNSExpression's expressionValueWithObject:(missing value) context:(missing value)) as list
end modeOfList:

set theList to {}
repeat 10 times
	set end of theList to (random number 15)
end repeat

{theList, my modeOfList:theList}