[Tutor] Setting thresholds in a compact way
David L Neil
PyTutor at DancesWithMice.info
Wed Jan 1 23:45:06 EST 2020
On 2/01/20 5:20 AM, Robert Alexander wrote:
> I am trying to setup a varying time.sleep value (in house) depending on a
> priorly determined threshold.
>
> If threshold <= .9 AND > .8 then sleep should be 6 hours
> if threshold <= 0.8 AND > .4 then sleep should be 4 hours
> if threshold <= .4 AND > .1 then sleep should be 3 hours
> if threshold <= .1 then sleep should be set to 1 hour
>
> Instead of a complex/lengthy series of (nested?) if/ands based on the
> above, I was wondering if I could declare a dictionary such as:
>
> sleep_thresholds_hours = {.9:6, .8:4, .4:3, .1:1}
>
> And then use some sort of single statement to set the sleep_time to the the
> proper value.
>
> Not even quite sure that using floats (albeit single decimal) as dictionary
> keys will be ok.
>
> Getting old and in 2020 even older and my brain fogged by too much food :)
A worked-solution...
Perhaps the seasonal influences explain it, but my (aged and decrepit)
mind prefers to construct the problem in the reverse of how it is stated
here...
Familiar with Cartesian coordinates and similar, to me the
x-axis/time-line starts at zero and goes 'up' to the right - and the
y-axis starts at zero and goes 'up', um, as the y-value goes 'up'. That
said, increasing/rising ranges seem easier (for me) to visualise:
<= .1 1
.4 3
.8 4
.9 6
(per 'excuses', below, am ignoring values of .9 or above, and similarly,
numbers which may/not be 'below' the illustrated range)
Notice that (in mathematical terminology) we now have two (ordered)
vectors (or 1-D matrices - or a single 2-D matrix, if you prefer.
Yeah/nah, leave that until the end...)
Let's call the left column-vector "sleep_threshold", and the right,
"sleep_hours". In Python:
sleep_threshold = [.1, .4, .8, .9]
sleep_hours = [ 1, 3, 4, 6 ]
Proving that we know what we're doing (maybe):-
for ptr in range( 0, len( sleep_threshold ) ):
print( f"{ ptr } { sleep_threshold[ ptr ] } { sleep_hours[ ptr ] }" )
0 0.1 1
1 0.4 3
2 0.8 4
3 0.9 6
(apologies, email word-wrapping may produce a less-than pleasing
graphological presentation. Similarly, when coding tests/"proofs", I've
saved vertical-space by typing the entire for-loop on a single line -
which many of my colleagues will (quite rightly) decry...)
Right, time to get to work! We could (per an earlier suggestion) try
iterating through the "sleep_threshold" list, in order to select the
corresponding "sleep_hours" element:
def sleepy( value ):
for ptr in range( 0, len( sleep_threshold ) ):
if value < sleep_threshold[ ptr ]:
print( f"{ value } results in sleep for {
sleep_hours[ ptr ] }" )
break
Yes, a function *should* be named using a verb which describes what it
does. I don't really know what you want it to do! (what a feeble excuse!)
Also, in this case, I'm only bothering to print the amount of sleep-time
by way of illustration. Presumably, you will want such a function to
return the sleep_hours value, or to execute the sleep right
there-and-then...]
What more criticism? Yes! Why did I put this code inside a function?
1. Presumption on my part (of your needs)
2. Programming style - each separate action is to be carried-out by a
single function/method ("separation of concerns")
3. For ease of testing!
Speaking of testing: how would we know if our magnificent-creation is
working, without testing the code? Normally, I would use PyTest, but as
this is being presented as a running-commentary, let's code some tests,
in-line:-
test_data = [ .05, .1, .2, .6, .85 ]
for test_value in test_data: print( test_value )
0.05
0.1
0.2
0.6
0.85
Please note that I've only used one 'border' value (0.1) and ignored any
concept of "outliers". (I'm going to claim simplicity. You can say
'laziness'...).
Now we're ready to experiment:-
for test_value in test_data: sleepy( test_value )
0.05 results in sleep for 1
0.1 results in sleep for 3
0.2 results in sleep for 3
0.6 results in sleep for 4
0.85 results in sleep for 6
What brilliance! It's almost as good-looking as am I!
Did you notice the 'oops!' in the test-data output? (*and* in the code?)
A very common error when coding such a 'ladder', is the comparison. How
do we regard values 'on the border' - are they 'in' or 'out' (in
mathematics: "inclusive" or "exclusive" of the range)? In this case,
your outline clearly shows them as "inclusive" and therefore the second
test (0.1 to sleep for 3) *failed*.
Let's correct 'my' error (and evaluate the results, again), before we go
any further...
def sleepy( value ):
for ptr in range( 0, len( sleep_threshold ) ):
if value <= sleep_threshold[ ptr ]:
print( f"{ value } results in sleep for {
sleep_hours[ ptr ] }" )
break
for test_value in test_data: sleepy( test_value )
0.05 results in sleep for 1
0.1 results in sleep for 1
0.2 results in sleep for 3
0.6 results in sleep for 4
0.85 results in sleep for 6
That's better! Now are you happy?
Good, but I'm not!
(some say I'm never happy...)
A second, similar, and also startlingly-common, error, in this coding
idiom is the "out by one error". In this case, I've not fallen into such
a 'trap', but essentially the 'danger' is in looping (in this case, over
the test data) one time too few, or one time too many, ie "out by one"!
Is this correct: for ptr in range( 0, len( sleep_threshold ) )?
I describe the above "style" as coding in a Java or C (or ...) idiom. A
'purist' would say that it has *no* place in Python.
Python's language-design gives us a significantly more-powerful for
statement, which can be differentiated from 'the others' by terming it a
"for-each" statement, ie for-each element in a collection, do...
Thus, we don't need to know how large the collection happens to be
(len()), and the 'out by one' error goes away - 'out by one' thrown-out?
- consigned to languish in the history of cyber-space...
OK, so with for-each in mind, we could try:
for threshold in sleep_threshold: etc
but what to do when we want to access the corresponding element in
sleep_hours, ie sleep_hours[ ptr ]???
Back to the drawing-board? Back to using a "pointer"?
No! Python has another trick up its sleeve (another 'battery' - when
they say that Python has "batteries included").
Somehow, we need to link the two lists/column-vectors (sleep_threshold
and sleep_hours). We can do this with zip() - we zip the two lists into
a single list, such that each element of the new list is comprised of
two values.
(remember how other posts talked of using a Python dictionary? A
dictionary's elements have two components, a "key", and a "value".
Similar motivation: finding a method of keeping the two 'together')
NB 'linking' lists using zip() is *not* the same as a ComSc concept or
algorithm called "linked lists"!
Onwards:-
sleep_thresholds_hours = zip( sleep_threshold, sleep_hours )
print( sleep_thresholds_hours )
<zip object at 0x7f747579a640>
Oh dear, it's not going to show us what we want to see. Let's use a bit
more power - more POWER!!!
for row in sleep_thresholds_hours: print( row )
(0.1, 1)
(0.4, 3)
(0.8, 4)
(0.9, 6)
Will you look at that, I'm back to mathematics and raving about
matrices/matrixes! Someone (please) stop him...
Here's revised code, written to be "pythonic", ie using a Python idiom,
and ridding us of any distasteful Java-influence (or whatever) - "we"
are so much better than "they"!
Engaging the powers of zip() and for-each:-
def sleepy_zip( value ):
for sleep_threshold, sleep_hours in sleep_thresholds_hours:
if value <= sleep_threshold:
print( f"{ threshold } results in sleep for {
sleep_hours }" )
break
for test_value in test_data: sleepy_zip( test_value )
Incidentally, if it will help comprehension, the twin-components of each
sleep_thresholds_hours element are a tuple - and therefore you might
prefer the more *explicit* syntax:-
for ( sleep_threshold, sleep_hours ) in sleep_thresholds_hours:
The 'Zen of Python' does recommend preferring the explicit over
implications!
Yahoo! Finally, it's time to go home...
No! Not so fast. Where's the output?
Um, there was none!
Do you 'see' any problem with this?
What happened is that zip() produces an iterator object
(https://docs.python.org/3.5/library/functions.html#zip), and iterators
can only be "consumed" once.
Think about it, if you zip 'up' a piece of clothing (ie use the zip()),
and then "consume" the zip in the for-loop (unzipping it), you can't
thereafter "consume" it again - the zip has been unzipped all the way
'down' and so can't be unzipped any further...
(As 'home-spun wisdom' goes, that's not too bad - or is it "home-zipped
wisdom"?)
So, when I demonstrated the results of the zip() by printing-out the
contents for you, It was "consumed". Thus, when we came to run
sleepy_zip(), there was no 'zip' left to 'unzip', and to borrow from
another piece of 'wisdom': no data in, no data out.
My fault, AGAIN!
Sure enough:-
print( "v"*8 )
for row in sleep_thresholds_hours: print( row )
print( "^"*8 )
vvvvvvvv
^^^^^^^^
(the 'results' should appear between the two lines of 'arrows')
Let's re-start, this time with feeling (yes, I can feel that zip going
'up'!). This time, let's put the zip() where it is going to be consumed,
ie where there's no chance that someone, or something else, will consume
it before we execute the function/do the business:-
def sleepy_zip( value ):
for threshold, hours in zip( sleep_threshold, sleep_hours ):
if value <= threshold:
print( f"{ value } results in sleep for { hours }" )
break
for test_value in test_data: sleepy_zip( test_value )
0.05 results in sleep for 1
0.1 results in sleep for 1
0.2 results in sleep for 3
0.6 results in sleep for 4
0.85 results in sleep for 6
BTW have you spotted the virtue of using "break" - or what might happen
if we didn't? Mind you, I do feel a little short of sleep - so I'm not
complaining!
Hope this helps. If I have misunderstood any of the 'real' criteria,
hopefully you will be able to adapt-to-suit.
As mentioned, normally I use PyTest to help prove my code. Thus the "for
test_value etc" lines would be located elsewhere (rather than in-line).
Also, were I to start writing this all over again, but still using
in-line testing, I'd zip() test_value with the expected results - if we
go to the trouble of assembling a test-data-set, then let's ensure we
also know/remember the (correct) answers! In fact, you might even ask
the computer to check its own results, then:-
assert returned_result == expected_result
Python will come to an abrupt halt if things are not tickety-boo!
Similarly, you could improve the output by formatting the f-strings
within our print()-s. More power to you!
However, in time-honored fashion, I shall leave such 'extras' to you,
"as an exercise" (and/or when you're ready for such more-advanced coding
techniques)...
--
Regards =dn
More information about the Tutor
mailing list