Code as a Liberal Art, Spring 2021

Unit 1, Exercise 4 lesson — Wednesday, February 10

Review of last week

Randomness; probability distributions; working with files

Today we're going to build on our reading discussion this week about the aleatory in computation by learning some techniques in computer program code for working with randomness. One of the things that we read and discussed was where the aleatory comes from within the rigid, formal strictures of digital machinery; how can randomness be generated from deterministic processes? We will not be going into the technical details today around how to generate random and pseudo-random numbers. This topic involves complicated mathematics and we could spend an entire semester (or more!) just talking about those mathematic techniques. Instead, we will start with the fact that random numbers are in fact possible to generate, and we will learn the commands that Python provides to generate them.

Building on this, we will look at techniques for working with algorithms that incorporate randomness, or in other words, algorithms that are probabilistic: step-by-step procedures that incorporate and use values that are unpredictable, but within various ranges and likelihoods, which we can shape and control.

This image is an installation shot of a project called "Noplace" by Marek Walczak, Martin Wattenberg, and collaborators. Hundreds of images were gathered and grouped together by topic keywords, then algorithmically selected and collaged together in a way that slowly shifted over time. Pictured here are collages for "utopia", "revolution", and "rapture". You can read more here.

Getting random

In spite of all our conversations about the complexity of generating random numbers, Python implements various random number generating algorithms and provides them to us as a collection of easy-to-use commands. These are provided by a library (in Python also known as a module) aptly called random.

To get started, simply import this module:

import random

So far, I have been sharing with you all the various commands and functions that you have been working with. But I want to point you to the Python documentation itself, so that you may explore by accessing it directly:

Official Python documentation
This is the authoritative standard for all things Python. It is comprehensive and accurate, describing all aspects of the language. There are many other guides out there (w3schools.com is good, as is this guide called Automate the Boring Stuff with Python, by Al Sweigart) and these can be great. But if you want to go straight to the source for definitive information, the official Python docs cannot be beat.

Official programming language documentation like this can be hard to read. I often need to read slowly and carefuly, and think hard about the precise meanings of the terms that are used here. I usually have to read an explanation a few times before I completely wrap my head around all of its details and implications. But since this is the official documentation, that scrutiny is usually worth it for the thorough understanding that it can offer. Unlike much of what one might find when searching for programming help online, official documentation like this a trustworthy and reliable source of information.

In particular, I recommend you look at the tutorial for an instructive introduction, and most powerfully, the "Library Reference", which will tell you everything that you need to know (and more) about Python and its commands.

Scroll about 1-2 screens down, and in the "Numeric and Mathematic Modules" section, find the entry called random, which explains how to "Generate pseudo-random numbers."

To get started, let's look at the function called random(), which is essentially the heart of this module. To get acquainted, let's experiment with this in the Python shell:

>>> import random
>>> random.random()
0.5536582109886334
>>> random.random()
0.519728888240708
>>> random.random()
0.21552893690804353
As you can see, random() returns an unpredictable decimal number between 0 and 1.

There are numerous other shortcuts to make working with randomness easier. But let's start by spending some time with this one.

Using only this function, if you wanted a random integer (a number with no decimal places) between 0 and 9, what might you do? Have a look at this code and think about what it's doing:

>>> int( random.random() * 10 )
First of all, taking a decimal number between 0 and 1 and multiplying it by 10 is going to give us another number that is at minimum 0, and at maximum 10. Think about what you would get if you multiply 10 times .9999 (close to the largest value of random()). You'll get 9.999.

To summarize this: when you are starting with a number in the range of 0 to 1, multiplying scales a range. You are expanding that range of 0 to 1, into the range of 0 and whatever you multiply it by. random.random() * 100 is going to give us a random decimal number somewhere between 0 and 100. This is a useful principle to keep in mind.

But I said I wanted an integer (whole number). That is what the int() command does. It truncates (or cuts off) the decimal part of the number, returning a whole number from a decimal point. If you'd prefer, you could also use round(), which doesn't simply truncate, but actually rounds:

>>> int(.1)
0
>>> int(.6)
0
>>> round(.1)
0
>>> round(.6)
1

OK. Now, what if I wanted a number that was between 50 and 100? What is the range here? 50. So I could use my scaling technique like this: random.random() * 50. But that is going to give a number between 0 and 50, not between 50 and 100. What can I do here? I could simply add 50: 50 + random.random() * 50. Now I'm taking a random number scaled to the range of 0 to 50, and adding 50 to it, shifting that range to 50 to 100. To summarize this principle: when working with numbers like this, addition shifts a range.

Multiplying scales a range, and adding shifts the range. Using these techniques, you can shape the range of values we get from random.random() and manipulate them to span whatever range of randomness we wish. These are very useful principles for creative coding.

Fortunately, Python gives us some commands that we can use directly to achieve the same principles. (I think it is important and valuable later on to understand what is going on here, hence the above explanation before this reveal.)

If you want a randomly generated whole number (integer) from within some range, you can use: random.randrange(start,stop). (Python docs for this.) This takes two arguments and returns a random integer in between those arguments. So for example:

>>> random.randrange(5,10)
6
>>> random.randrange(5,10)
8
>>> random.randrange(5,10)
5
If you only pass in one argument, it returns a number between 0 and that argument:
>>> random.randrange(10)
8
>>> random.randrange(10)
3
>>> random.randrange(10)
1

Reproducible randomness

Another very useful idea in working with randomness is the idea of a random sequence of numbers that are reproducible. This can be useful for testing for example. Let's say you want to draw something that looks random, but you want it to look random the same way each time you run your code. You don't want it to be different each time. There is a command for this: random.seed()

When you call random.seed() with one numerical argument, every time you call random() after that, it will always return the same random-looking sequence. For example, if I run Python twice, I get two different random numbers each time:

$ python 
>>> import random
>>> random.random()
0.9998781067887237
$ python 
>>> import random
>>> random.randrange(1)
0.6638400757429336
But if I call random.seed(), I still get a random number, but it is the same each time:

$ python 
>>> import random
>>> random.seed(42)
>>> random.random()
0.7534071414623441
$ python 
>>> import random
>>> random.seed(42)
>>> random.random()
0.7534071414623441

This is called a pseudo-random number (it was discussed in the 10 PRINT book) because the numbers are drawn from a uniform distribution, so they appear random, but they are in fact deterministic.

This is very very useful in game development. For example, say you want to generate a character or a natural terrain, and you want its appearance to seem random. But you want that apperance to be the same each time the game is run, not changing every time. If that character has a name or ID number, you could use that as the seed. Each time you want to render that character, call seed() with their ID number, and the calls to random() that come after will seem random, but will be random in the same way each time.

As pointed out in class, this is precisely how the seed function works in Minecraft!

Probabilities

What if I want to generate some kind of digital object, say an image with randomly placed colored pixels on a white background, and I want to control how many colored pixels there are? You can ask if statements about random choices to control the probability.

For example, have a look at this code, which creates a new blank image, loops over all the pixels, and sets some of them to a color based on a random value:

Example 1

from PIL import Image
import random

# let's make a 100x100 white image

width = 100
height = 100

img = Image.new("RGB", (width,height), (255,255,255) )

for y in range(height):
    for x in range(width):

        r = random.random()

        if r > .5:
            img.putpixel( (x,y), (0,0,0) )

img.save("rando.png")

The results of running Example 1 three times.

And what if I simply want there to be less black pixels in that image? Have a look at this:

Example 2

from PIL import Image
import random

# let's make a 100x100 white image

width = 100
height = 100

img = Image.new("RGB", (width,height), (255,255,255) )

for y in range(height):
    for x in range(width):

        r = random.random()

        if r > .9:
            img.putpixel( (x,y), (0,0,0) )

img.save("rando.png")

The results of running Example 2 three times.

In Example 1, a pixel is drawn about 50% of the time: assuming a uniform distribution, a random number between 0 and 1 will be greater than .5 about half of the time. In Example 2 however, I am now only drawing the pixel if the random choice is greater than .9, so this will draw a pixel about 10% of the time. Notice the more sparse image.

More nuanced probability shaping

As one last technique, let's start by considering this example:

Example 3

from PIL import Image
import random

# let's make a 100x100 white image

width = 100
height = 100

img = Image.new("RGB", (width,height), (255,255,255) )

# loop 500 times, and each time, pick a random x and a random y
# and draw a pixel there
for n in range(500):

    x = int( random.random() * 100 )
    y = int( random.random() * 100 )

    img.putpixel( (x,y), (0,0,0) )

img.save("rando.png")

But what if we didn't want the pixels evenly spaced out. What if we wanted them randomized, but kind of more clustered at the top?

Example 4

from PIL import Image
import random

# let's make a 100x100 white image

width = 100
height = 100

img = Image.new("RGB", (width,height), (255,255,255) )

# loop 500 times, and each time, pick a random x and a random y
# and draw a pixel there
for n in range(500):

    x = int( random.random() * 100 )
    r = random.random()
    y = int( r * r * 100 )

    img.putpixel( (x,y), (0,0,0) )

img.save("rando.png")
The results of running Example 3 (left) and Example 4 (right). Notice the clustering up around smaller values of y.

I'm sure this is probably a little hard to understand. What I'm doing here is choosing a random number for y, but multiplying it times itself. If you remember back to algebra, and graphing functions, remember that plotting x-squared (x to the second power) looks like a slightly curved shape. (See image.) What I'm doing here is taking the random number between 0 and 1 and squaring it, so it changes the shape of the probability. It means that there is a greater chance the numbers will be smaller. You can play around with a graphing calculator to think about how different functions have different shapes, and how you can use a random variable here as the input to shape the resulting distribution. In addition to x squared, think about x cubed, or x to the fourth power; you can also think about the square root of x, or 1 divided by x.

A graph of x squared.

Random choices

What if I want to do something with randomness that is not just generating a number, but rather randomly selecting something from a list? I could do that like this:

>>> import random
>>> a = ['Birds', 'flying', 'high', 'you', 'know', 'how', 'I', 'feel']
>>> i = random.randrange( len(a) ) 
>>> a[i]
'know'
>>> i = random.randrange( len(a) )
>>> a[i]
'Birds'
What I'm doing here is creating a list called a, then remember that len(a) gives me the length of that list. So I'm passing the length of the list in to the command random.randrange(). That will always give me a value between 0 and the length of the list. I then use that value as the index to my list.

Turns out this is such a common operation that Python makes a shortcut for it: choice(). If you pass a list to this function, it will randomly select one item from the list:

>>> random.choice(a)
'know'
>>> random.choice(a)
'how'

Let's use this principle to randomly select a file. But first, we have to take a detour and think about more ways to access files.

Let's say that I am in a directory (folder) called "Unit 1 exercise 4", and that it contains a subbolder called "images", like this:

From the command line, I can use ls to view the contents:

$ ls
images
$ ls images/
earth.jpg	fire.jpg	newspaper.png	smoke.jpg
      
Now I'm going to run Python and use a function called listdir(), which takes the name of a directory, and returns a list of all the files in that directory. This is in the os library, so I must import it first:
>>> from os import listdir
>>> listdir("images")
['newspaper.png', 'fire.jpg', 'earth.jpg', 'smoke.jpg']
      
Here I have passed in the name images, the name of my subfolder, and listdir() gives me an array of all files within that. Then I could choose a random file in that list like this:
>>> from os import listdir
>>> import random	
>>> files = listdir("images")
>>> file = random.choice(files)
      
Now I could go and use file as I would any filename — for example to open that file with Pillow. This technique will be very useful in getting started with the homework for this week .