← back to CS 115
Week 12Guttag §Chapter 14238 min full read
7 concepts23 worked examples27 exercises5 exam-level7 figures
What are you here for?

12 Random walks: three small classes, one walk function, and the numbers and pictures that come out of running it many times

Start with this

One question before you read anything. Getting it wrong is the point: it shows you what this section is for.

§12.1 — what seeding a generator does

Three dice rolls with a in the middle. Nothing about walks yet: this is only about what random.seed controls. If you have not met the module before, read the answer, that is what it is here for.

Find(a) Write exactly what this program prints, both lines.
Given
import random

random.seed(4)
a = random.randint(1, 6)
b = random.randint(1, 6)
random.seed(4)
c = random.randint(1, 6)
print(a, b, c)
print(a == c, b == c)
IPython console
Hint 1/4

Decide what the second seed call does to the stream before you think about the numbers themselves.

Hint 2/4

Seeding fixes where the stream starts, so the first number after a seed call is always the same for that seed. Rolls keep coming from the same stream until it is reset.

Hint 3/4

The program seeds with 4, takes two rolls into a and b, seeds with 4 again, and takes one roll into c. So c repeats the first roll of the stream, not the second.

Hint 4/4

One line with the three numbers, then one line with the two comparisons.

Show solution

Track the stream

$$a = \text{first roll of stream } 4$$

the seed call puts the generator at a known place

$$b = \text{second roll of stream } 4$$

no seeding between them, so the stream continues

$$c = \text{first roll of stream } 4$$

the second seed call restarts the same stream

Read the comparisons

$$a == c \Rightarrow \texttt{True}$$

both are the first roll of the same stream

$$b == c \Rightarrow \texttt{False}$$

the second roll of a stream need not equal the first, and here it does not

Answer $$\boxed{\texttt{2 3 2}\;/\;\texttt{True False}}$$
Check

Independent check: the two comparison values must follow from the first line whatever the numbers are, and they do, since the first and third numbers printed are equal while the second differs.

A seed is a position in a stream. Anything that resets the position resets the results.

What this looks like in Spyder
The Variable Explorer tab showing a table with Name, Type, Size and Value columns for the names the program created.

The tab next to Help. It lists every name your program left behind with its type and its size, so it answers the one question a listing cannot: what is in this name right now. The stepper on this page shows the same four columns.

Spyder 5.5.1 on Linux, running the program in its own editor. A different version moves a few toolbar icons; the panes are named the same.

A farmer stands in the middle of a field. Every second she takes one step, north, south, east or west, and the direction is whatever the second happens to bring.

After a hundred thousand seconds, how far from the middle is she? Two answers sound equally good:

  • the steps cancel on average, so she is back near the middle
  • a hundred thousand steps is a long way, so she is far off

Nothing you can do on paper settles which one it is.

By the end of this section you can write the program that answers this, say what its distance does when the number of steps is multiplied by four, and draw the one picture that shows a reader the same thing.

In 60 seconds

A simulation is three cooperating classes plus one function that runs the process once, and the answer is never one run: it is the mean of many runs, reported with its and drawn once.

One step
$$\texttt{stepChoices = [(0,1), (0,-1), (1,0), (-1,0)]}\\ \texttt{random.choice(stepChoices)}$$

Any unbiased walk on a grid. The four pairs are read as (change in x, change in y), each has one chance in four, and a biased walk is the same call on a list that is not balanced.

A place hands back a place
$$\texttt{move(dx, dy)} \Rightarrow \texttt{Location(x+dx, y+dy)}$$

Whenever a step is taken. The call returns a new object and leaves the old one alone, which is what makes it safe for two drunks to share one starting place.

Distance from the start
$$d = \sqrt{(x_1-x_2)^2 + (y_1-y_2)^2}$$

The number a walk reports. It is the straight line back to the start, so a walk of 20 steps can report 2.83, and a walk that comes home reports 0.

Many walks, then one number
$$\text{mean} = \frac{\sum \text{distances}}{\texttt{numTrials}}, \quad \text{steps} \times 4 \Rightarrow \text{mean} \times 2$$

Reporting the result. The doubling rule holds for the fair walk; a walk with a bias in it grows with the number of steps instead, which is how you tell the two apart from the numbers alone.

Three most common mistakes
  1. Reporting one trial as the answer. Ten walks of the same length landed between 2.0 and 15.3 on this page, so a single number from a single walk is not a measurement of anything.

  2. Leaving the drunk where the last trial left him. The reported distances still look right, since each walk measures from where it started, but the walker is never at the again, so every plotted end location and every distance from the origin belongs to a different experiment.

  3. Reading the start location after the loop instead of before it. The distance then comes out 0.0 for every walk, and 0.0 looks like an answer rather than like a bug.

The weights are labs 20, midterm 40 and final 40, so this week is final exam material. The ten labs end with the plotting lab, and the course archive holds no lab, so this material is practised in the exam.

In one archived final paper the two heaviest questions had the two shapes this page drills:

  • write a class with named methods, then a script that uses it
  • trace a short program and write its exact output

That is one paper and not a rule, so read your own paper's point table.

How much time do you have?
10 minutes

The one function every question here is built on, the three lines that make a trial honest, and the four boxed results.

The 60-second card · The walk function, and the checks that make it believable · One walk answers nothing · Formula card
45 minutes

Enough to write the whole simulation from a written specification and to trace someone else's version: the three classes, what each one owns, the walk function and its , and the trial loop that makes the mean mean something.

The 60-second card · One step, picked at random · Location: a place that hands back a place · Field: a dictionary from walker to place · The walk function, and the checks that make it believable · One walk answers nothing · Scaffolding comes off · B · computation
full read

Everything, including the growth rule and how a biased walk breaks it, the choice of picture for each question, and the twenty two ways this material goes wrong.

The 60-second card · Recall first · Conventions · One step, picked at random · Location: a place that hands back a place · Field: a dictionary from walker to place · The walk function, and the checks that make it believable · One walk answers nothing · How the mean grows, and what a bias does to it · Drawing the answer · Method boxes · Look-alike pairs · Scaffolding comes off · Full exam-style question · Practice set · Mistake ledger · Glossary
By the end of this section
  1. Predict what one random step produces: read a choice call over a list of pairs, unpack the pair, and say what seeding does to a run.

  2. Trace a Location through a move call: say which object is new, which one is unchanged, and what the distance between two places comes out as.

  3. Explain what the field's dictionary holds, what a second addDrunk call for the same drunk does, and which key a move rebinds.

  4. Write the walk function so that it reports the distance from the starting place, and test it with walks of zero, one and two steps.

  5. Run many trials of the same walk, report the mean with its largest and smallest, and reset the walker before every trial.

  6. Compare growth rates from a table of means: say what quadrupling the steps does to a fair walk and what it does to a walk with a bias.

  7. Choose the picture that answers the question asked, give each class its own plotting style, and name what a chosen axis limit can hide.

Syllabus coverage

Random Walks — covered

The drunkard's walk as the course builds it

  • one random from a list of pairs
  • the Location
  • Field and Drunk abstractions
  • a walk function that returns the distance from the start
  • trials over that function
  • the mean and its spread
  • sanity checks on tiny walks
  • biased walkers written as subclasses

Data Visualization — covered

Choosing and finishing the picture: the scatter of end locations against the mean distance curve, one style per walker class handed out by a small class, title, axis names and legend, identical axis limits when two panels are compared, and what a frame that hugs the data hides.

Chapter 14 — covered

The chapter this week's line names. Its sections are the walk itself, the sanity checks, the step to several walker classes at once, the plotting of the results, and fields where the places themselves behave oddly, which is an exercise in this page's field block.

fitting a curve to the measured means — deferred

The measured means on this page sit almost exactly on a square root curve, and the natural next question is how to fit that curve and measure how well it fits.

Deferred to the next section, understanding experimental data, where the syllabus puts fitting and where the fit, not the simulation, is the subject. This page measures the growth by quadrupling the steps and comparing means, which needs nothing but division.

the same simulation written with arrays — off syllabus

The array library can hold the trial distances and take their mean in one call, and a lab that already has the data in arrays would do it that way.

Not in this week's line. It appears here in one sentence only, because the mean of a list of distances is the same number either way and this page keeps to lists so that the trial loop stays visible. Not examinable from this page.

Recall first
A class, its constructor and self

class Location(object): with def __init__(self, x, y): inside it, and self.x = x creating a slot on the object being built. A call Location(0, 0) runs that method and hands back the object.

All three classes of the simulation are written this way, and one of them is written twice on this page, once correctly and once with one line changed.

Inheritance and overriding one method

class UsualDrunk(Drunk): inherits everything Drunk has, and a method defined again in the subclass replaces the inherited one for objects of the subclass.

Every kind of walker on this page is a subclass that changes exactly one method, takeStep, and nothing else.

A dictionary key can be any object

d[key] = value stores under the key, key in d asks whether it is there, and for k in d: walks the keys. An object you defined yourself is usable as a key, and it is its own key: two objects built separately are two different keys even when their contents look the same.

The field is one dictionary whose keys are drunk objects, and the two mistakes it invites both come from that sentence.

Two names for one object

After b = a there is one object and two names for it. a is b is then True. A method that changes the object is seen through both names; a method that returns a new object is not.

Two drunks are put on the same starting Location on this page, and whether that is safe depends entirely on this distinction.

Unpacking a tuple into two names

xDist, yDist = (1, 0) binds 1 to the first name and 0 to the second. The right hand side must have exactly as many items as there are names on the left.

A step arrives as one pair and is used as two numbers, in one line, in the field's move method.

Try it yourself first (1 questions)
1§12.3 — two objects that print the same

Two drunk objects are built with the same name and both are used as dictionary keys. This is the trap this section is built on, so getting it wrong here is useful.

Find(a) Write exactly what this program prints, all three lines.
Given
class Drunk(object):

    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return self.name


one = Drunk('Homer')
two = Drunk('Homer')
field = {}
field[one] = 'left'
field[two] = 'right'
print(len(field))
print(field[one], field[two])
print(one == two)
IPython console
Hint 1/4

Ask how many keys the dictionary ends up with before you ask what is stored under them.

Hint 2/4

A dictionary key is the object, not what the object prints as. Two objects built by two separate calls are two different keys unless the class says otherwise.

Hint 3/4

Here one and two are two separate Drunk('Homer') calls, and each is used once as a key, so both entries survive and each keeps its own value.

Hint 4/4

Two entries, then the two values in the order they were asked for, then False.

Show solution

Count the entries

$$\texttt{field[one]}, \texttt{field[two]}$$

two different objects, so two different keys

$$\texttt{len(field)} = 2$$

nothing overwrote anything

Read the lookups

$$\texttt{field[one]} = \texttt{'left'}$$

the value stored under that object

$$\texttt{one == two} \Rightarrow \texttt{False}$$

the default comparison asks whether it is the same object, and it is not

Answer $$\boxed{2\;/\;\texttt{left right}\;/\;\texttt{False}}$$
Check

Independent check: if the two objects had counted as one key, the second assignment would have replaced the first and the second printed line would read right right. It does not.

The name inside an object is for the reader. The identity of the object is what the dictionary uses.

Notation
symbolreads asmeanswatch out
$(dx, dy)$

the pair change in x, change in y

One step, written as a tuple. The first number moves you east when it is positive, the second moves you north.

It is one object, not two arguments. A step is unpacked with xDist, yDist = drunk.takeStep() before it can be used as two numbers.

$random.choice(seq)$

pick one item of seq at random

Returns one of the items, each with the same chance. On a list of four pairs, each pair has one chance in four.

It returns the item, not its index, and it does not remove it. Calling it twice can give the same item twice.

$random.seed(n)$

start the generator at n

Fixes where the stream of random numbers begins, so the whole run repeats exactly on the same interpreter.

Seeding inside the trial loop makes every trial identical, which looks like a working simulation and measures nothing. Seed once, at the top.

$loc.move(dx, dy)$

the place dx, dy away from loc

A new Location, offset from this one. The place it was called on is not changed.

The result has to be kept: loc.move(1, 0) on its own line does nothing at all, the same way sorted(xs) on its own line does nothing.

$a.distFrom(b)$

the distance from a to b

The straight line between two places, from the two coordinate differences.

Symmetric, so the order of a and b does not matter, but the number is a distance and never negative. A negative result means a subtraction is inside the square root by mistake.

$f.drunks[d]$

the place the field has drunk d standing on

One entry of the field's dictionary. The key is the drunk object itself, the value is a Location.

Two drunks with the same name are two different keys, because a drunk object is its own key. The names are for printing, not for looking up.

Conventions used here
Where this section sits, and what it is practised in.

This week comes after the classes, the complexity and the plotting sections, and it uses all three.

The lab sequence has ten labs and the last is the plotting lab, so the archive holds no lab paper for this material. Treat it as exam material: the questions here are shaped like exam questions, not lab steps.

A student who looks for the matching lab and finds none concludes the week is optional. The syllabus line says otherwise.

Every printed block is a real run, and the seeded ones repeat.

Each block of output on this page came out of an interpreter, character for character, and none of it was predicted by eye. A program whose result depends on chance is seeded at the top with random.seed, so it prints the same thing on every run.

Two cautions come with that:

  • a different interpreter version can turn the same seed into a different stream
  • a seeded simulation is still a simulation, and the seed makes the run repeatable rather than the answer exact

In a programming course the printed answer is the whole claim, and a claim about a random program that cannot be reproduced cannot be checked by the reader.

Distance means the straight line back to the start.

Every distance here is the straight line from where a walk started to where it ended, computed from the two coordinate differences. Two numbers it is not:

  • the length of the path walked, which is only the number of steps
  • the count of different places visited

A 20 step walk that ends two east and two north of the start has distance 2.83.

The question the simulation answers is about where the walker ended up, and the two other numbers are easy to compute and easy to report by accident.

How numbers are printed here.

A distance kept for arithmetic is kept in full. A distance shown to a reader goes through format(value, '.2f'), or '.1f' inside a table, and the rounding happens only at the printing. Where a raw float is printed on purpose, the point is to show what Python prints when nobody formats it.

Rounding early and then summing is how a reported mean stops matching the numbers it was computed from.

What this page may use, and what it leaves alone.

Assumed from the earlier sections: numbers, strings, if, while, for, range, len, min, max, sum, sorted, format, def and return, lists, tuples, dictionaries, files, two dimensional lists, classes and inheritance, and the pyplot drawing calls. Added here: the random module, and only three of its functions, choice, seed and randint. Left alone: comprehensions, f-strings, enumerate, zip, and the array library.

The exam is closed book with a printed list of allowed functions, so anything outside that list has to be earned rather than assumed.

Naming, and the two styles the course uses.

The lecture files for this week use the mixed case style, takeStep, moveDrunk, getLoc, numSteps, with capitalised class names. This page keeps it, because the code an exam asks you to read is written in it.

Other weeks of the same course use underscores, get_name, and both are accepted. What is not accepted is switching style inside one file.

Reading a method called takeStep as if it were a new name when you have only seen take_step costs seconds that an exam does not give back.

What the pictures on this page are.

The figures are drawn from the numbers the programs printed, not photographed from a screen, so every tick value can be checked against a number in the text. Colours carry a role and keep it:

  • blue is where a walk starts
  • red is what was measured
  • orange is a helper line or a frame
  • purple is a second series being compared

A picture that disagrees with the numbers printed beside it teaches the disagreement, and a colour that means one thing in one figure and another thing in the next teaches nothing.

12.1One step, picked at random

One step is a pair picked from a list of four, and the picking can be told where to start.

The question in the opening cannot be settled on paper, so we build the walk instead. It begins with a single step.

Solvable with what we have
  • Compute the distance between two places whose coordinates we know.

  • Add a known step to a known position and get the next position.

  • Run a loop a hundred thousand times without complaining.

Not solvable yet
  • Say which way the next step goes, because nothing in the problem says.

  • Answer the farmer's question from one walk, since the next walk differs.

  • Settle it by argument, because the two arguments in the opening both sound right.

The cancelling argument, made precise on the smallest case:

  • the average of the four possible steps is the pair (0, 0)
  • so the average of two steps is (0, 0) as well
  • so the expected distance after two steps looks like zero

Two steps is small enough to check by hand. There are sixteen equally likely walks, and the first worked example counts them.

Why it fails

Averaging the steps is not averaging the distance. Four of the sixteen walks do come home, with distance 0.

The other twelve land 1.41 or 2 away, and a distance is never negative, so nothing cancels them. The mean distance after two steps is 1.21, not 0. The walk has to be measured.

RuleRule 12.1: one random step
Conditions
  • The allowed steps are written as a list of pairs, stepChoices = [(0, 1), (0, -1), (1, 0), (-1, 0)], and each pair is read as (change in x, change in y).

  • random.choice returns one item of the list, each item with the same chance. On four items that is one chance in four.

  • The step arrives as one object and is taken apart with xDist, yDist = drunk.takeStep().

  • random.seed(n) fixes where the stream of choices starts, so the whole run repeats. One call, at the top of the program.

  • The module is random, imported with import random, and this page uses three of its functions: choice, seed and randint.

$$\boxed{\texttt{step = random.choice(stepChoices)}\qquad \texttt{xDist, yDist = step}}$$

Read it as: pick one of the four pairs, with the same chance for each, then read that pair as how far east and how far north this step goes.

Looks like this, but is not

Four things to choose between, so four arguments:

step = random.choice((0, 1), (0, -1), (1, 0), (-1, 0))

choice takes one sequence and picks from inside it, so the four pairs have to be one list. Written as four arguments it raises a TypeError about the number of arguments, which at least fails loudly.

where it endshow many of the 16distance

back at the start

4

0

one east or west and one north or south

8

1.4142

two steps out in one direction

4

2

Read the last two columns together: only a quarter of the walks come home, and the mean of the sixteen distances is 1.2071. The groups are not the same size, which is the part the cancelling argument misses.

counting all sixteen two step walks, to see whether they cancel

The cancelling argument says the mean distance should be zero, and two steps is small enough to settle by counting.

Each step is one of four, so two steps make sixteen walks, all equally likely. Group them by where they end.

The program below builds the same sixteen walks and prints the group sizes and the mean:

stepChoices = [(0, 1), (0, -1), (1, 0), (-1, 0)]
distances = []
for first in stepChoices:
    for second in stepChoices:
        x = first[0] + second[0]
        y = first[1] + second[1]
        distances.append((x**2 + y**2)**0.5)
home = distances.count(0.0)
far = distances.count(2.0)
print('walks:', len(distances))
print('came home:', home)
print('two away:', far)
print('mean:', format(sum(distances) / len(distances), '.4f'))

It prints:

walks: 16
came home: 4
two away: 4
mean: 1.2071
FindThe mean distance after two steps.
Given
  • four possible steps, so sixteen two step walks, all equally likely

  • distance is measured from the start, in a straight line

Solution

Group the sixteen walks by where they end

$$4 \text{ walks} \rightarrow d = 0$$

each first step has exactly one opposite second step, and there are four first steps

$$4 \text{ walks} \rightarrow d = 2$$

the same step twice, once for each of the four directions

$$8 \text{ walks} \rightarrow d = \sqrt{2} \approx 1.4142$$

the remaining ones turn a corner, one unit east or west and one unit north or south

Take the mean of the sixteen distances

$$\text{mean} = \frac{4\cdot 0 + 8\sqrt{2} + 4\cdot 2}{16}$$

a mean over equally likely outcomes is the plain average of the sixteen numbers

$$= \frac{11.3137 + 8}{16} = \boxed{1.2071}$$

so the distance does not cancel: it comes out a little over one step's length

Answer $$\boxed{\text{mean distance after 2 steps} = 1.2071}$$
Check

Independent check against a run of the real simulation: a walk of two steps was repeated a hundred times and the distances that ever appeared were 0.0, 1.4142 and 2.0, which are exactly the three groups counted here, and nothing else appeared.

Sixteen walks is the largest case that can be counted by hand.

The steps cancel and the distances do not. That is why the rest of this section measures distances instead of averaging steps.

five steps out of a seeded stream

The first thing to see is what one call actually hands back. This program seeds the generator, then asks for five steps:

import random

stepChoices = [(0, 1), (0, -1), (1, 0), (-1, 0)]
random.seed(4)
for s in range(5):
    print(random.choice(stepChoices))

It prints:

(0, -1)
(1, 0)
(0, 1)
(-1, 0)
(-1, 0)

Five pairs, and they are pairs rather than numbers: the second one goes east, the third north. The same seed gives the same five on the same interpreter, which is what makes a page like this one checkable.

FindWhat the five calls return, and in what form.
Given
  • the four pairs, in the order written in the list

  • seed 4, then five calls

Solution

Read one call

$$\texttt{random.choice(stepChoices)} \rightarrow \texttt{(0, -1)}$$

the call returns an item of the list, not a position in it

$$\texttt{(0, -1)} \rightarrow \text{south}$$

no change in x, one less in y

Read the five as a walk

$$(0,-1),\ (1,0),\ (0,1),\ (-1,0),\ (-1,0)$$

the printed order is the order they were taken in

$$\text{ends at } (-1, 0)$$

adding the five pairs coordinate by coordinate, south then north cancel and one east against two west leaves one west

Answer $$\boxed{\texttt{(0, -1) (1, 0) (0, 1) (-1, 0) (-1, 0)}}$$
Check

Independent check: the same five steps are the first five of the twenty step walk drawn later in this section, and that picture shows the path leaving the origin southwards and returning to it on the fourth step, which matches these pairs.

A seeded run is a run you can argue about with someone else, because they can reproduce it.

Checkpoint
§12.1 — which call picks one step

Four lines that all look like a way to pick one of four steps. One of them does it.

Find(a) Which line binds step to one of the four pairs, each pair with the same chance?
GivenstepChoices = [(0, 1), (0, -1), (1, 0), (-1, 0)]
Hint 1/4

Ask what each call is being given: one sequence, several arguments, or something that is not a sequence at all.

Hint 2/4

random.choice takes one sequence and returns one of its items. Indexing a list with [0] first hands over one pair, and choosing inside a pair returns a number.

Hint 3/4

The list here is stepChoices, whose items are the four pairs themselves, so the sequence to pass is the list and nothing else.

Hint 4/4

The call whose only argument is the whole list.

Show solution

Test the four arguments

$$\texttt{stepChoices} \rightarrow \text{four pairs}$$

the items of the list are the pairs, so an item is a pair

$$\texttt{stepChoices[0]} \rightarrow (0, 1)$$

this is already one pair, so choosing inside it returns 0 or 1

$$\text{two arguments} \rightarrow \texttt{TypeError}$$

the function takes one sequence

$$4 \rightarrow \texttt{TypeError}$$

an int has no items to choose from

Answer $$\boxed{\texttt{random.choice(stepChoices)}}$$
Check

Independent check on types: the line that works must return something that xDist, yDist = step can unpack, and only a two item pair can be unpacked into two names.

When a call misbehaves, name the argument first. Three of these four lines are argument mistakes.

⚠ Seeding inside the trial loop

The seed call is about reproducibility, and it feels safest next to the code it is making reproducible.

wrong$$\texttt{for t in range(numTrials):}\\\quad \texttt{random.seed(0)}\\\quad \texttt{distances.append(walk(f, d, 100))}$$
right$$\texttt{random.seed(0)}\\\texttt{for t in range(numTrials):}\\\quad \texttt{distances.append(walk(f, d, 100))}$$
⚠ Passing the pairs to choice as separate arguments

In English you choose between four things, so four things go in.

wrong$$\texttt{random.choice((0,1), (0,-1), (1,0), (-1,0))}$$
right$$\texttt{random.choice([(0,1), (0,-1), (1,0), (-1,0)])}$$
⚠ Using the returned pair as a number

A step sounds like one thing, and one thing sounds like one number.

wrong$$\texttt{x = x + random.choice(stepChoices)}$$
right$$\texttt{xDist, yDist = random.choice(stepChoices)}\\\texttt{x = x + xDist}$$

12.2Location: a place that hands back a place

A Location holds two coordinates, and its move method returns a new place instead of changing this one.

A step is a pair of numbers. To use it, there has to be something for it to be applied to.

DefinitionDefinition 12.2: the Location class
Conditions
  • Location(x, y) builds one place. The two coordinates are whole numbers here, because every step is one unit along an axis.

  • move(deltaX, deltaY) returns Location(self.x + deltaX, self.y + deltaY). It does not touch the object it was called on.

  • getX() and getY() hand back the two coordinates, and distFrom(other) hands back the .

  • __repr__ is defined so that printing a place, or a list or dictionary holding places, shows <x, y> rather than an address.

$$\boxed{\texttt{loc.move(dx, dy)} = \texttt{Location(loc.x+dx,\ loc.y+dy)}\qquad d = \sqrt{(x_1-x_2)^2+(y_1-y_2)^2}}$$

Read the first half as: asking a place to move gives you a different place and leaves this one where it was. Read the second as: the distance between two places is the square root of the sum of the two squared coordinate differences.

Looks like this, but is not

A move method could just as well update the place it is called on, and return itself so that calls can be chained:

def move(self, deltaX, deltaY):
    self.x = self.x + deltaX
    self.y = self.y + deltaY
    return self

It works for one walker and breaks silently for two. Anybody else holding that place sees it move as well, including the start location a walk saved so it could measure the distance at the end. The worked example below runs both versions.

one move, and what is left behind

The class as this course writes it, with one move and three questions asked of the result:

class Location(object):

    def __init__(self, x, y):
        """x and y are numbers"""
        self.x = x
        self.y = y

    def move(self, deltaX, deltaY):
        """Hands back a NEW Location deltaX, deltaY away from this one"""
        return Location(self.x + deltaX, self.y + deltaY)

    def getX(self):
        return self.x

    def getY(self):
        return self.y

    def distFrom(self, other):
        """Straight line distance from this place to other"""
        xDist = self.x - other.getX()
        yDist = self.y - other.getY()
        return (xDist**2 + yDist**2)**0.5

    def __repr__(self):
        return '<' + str(self.x) + ', ' + str(self.y) + '>'


start = Location(0, 0)
here = start.move(0, 1)
print(start, here)
print(start.distFrom(here))
print(here is start)

It prints:

<0, 0> <0, 1>
1.0
False

The third line is the one worth pausing on. here and start are different objects, so the distance between them is not zero, and the place saved in start is still usable as a measuring point.

FindThe three printed lines, and which object each name holds.
Given
  • start = Location(0, 0)

  • here = start.move(0, 1)

Solution

Build and move

$$\texttt{start} \rightarrow \texttt{<0, 0>}$$

the constructor stores the two coordinates and nothing else

$$\texttt{here} \rightarrow \texttt{<0, 1>}$$

move returns a second object, one unit north

Measure and compare

$$d = \sqrt{(0-0)^2+(0-1)^2} = 1.0$$

one coordinate difference is zero, so the root is the other difference, and one step north is one unit

$$\texttt{here is start} \rightarrow \texttt{False}$$

the identity test asks whether the two names hold the same object, and move made a new one

Answer $$\boxed{\texttt{<0, 0> <0, 1>}\;/\;\texttt{1.0}\;/\;\texttt{False}}$$
Check

Independent check on the printed form: <0, 0> can only come from the __repr__ of this class, so the first line also proves that printing a Location shows its coordinates rather than its address.

A method that returns a new object is a question. A method that changes the object is an order. Location asks.

the mutating version, and the two names it ruins

The same class with the counterexample's move in it, and two names pointing at one place:

class BadLocation(object):

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def move(self, deltaX, deltaY):
        self.x = self.x + deltaX
        self.y = self.y + deltaY
        return self

    def __repr__(self):
        return '<' + str(self.x) + ', ' + str(self.y) + '>'


origin = BadLocation(0, 0)
homer = origin
marge = origin
homer = homer.move(1, 0)
print(homer, marge, origin)
print(homer is marge)

It prints:

<1, 0> <1, 0> <1, 0>
True

One object was built and three names hold it, so the single move shows up in all three. Nothing raised an error, and this is what makes the mutating version dangerous rather than merely wrong.

FindWhat the three names print after one move, and why.
Given
  • a move method that assigns to self.x and self.y and returns self

  • homer and marge both bound to the same BadLocation

Solution

Count the objects

$$\texttt{origin} = \texttt{homer} = \texttt{marge}$$

two assignments copied a reference, not a place, so there is one object with three names

$$\texttt{homer.move(1, 0)}$$

the method writes into that one object

Read the three names

$$\texttt{<1, 0> <1, 0> <1, 0>}$$

there is nothing else for them to show, since they all name the object that was changed

$$\texttt{homer is marge} \rightarrow \texttt{True}$$

the identity test confirms it: one object, and returning self kept it that way

Answer $$\boxed{\texttt{<1, 0> <1, 0> <1, 0>}\;/\;\texttt{True}}$$
Check

Independent check against the correct class: the same three lines with the returning version of move print <1, 0> <0, 0> <0, 0> and False, so the difference is visible in the printed output and not only in an argument about style.

When two names share an object, the safe method is the one that does not write into it.

Checkpoint
§12.2 — distance both ways round

Two places and three distance questions, one of which is a place measured against itself.

Find(a) Write exactly what this program prints, all three lines.
Given
class Location(object):

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def getX(self):
        return self.x

    def getY(self):
        return self.y

    def distFrom(self, other):
        xDist = self.x - other.getX()
        yDist = self.y - other.getY()
        return (xDist**2 + yDist**2)**0.5


a = Location(1, 2)
b = Location(4, 6)
print(a.distFrom(b))
print(b.distFrom(a))
print(a.distFrom(a))
IPython console
Hint 1/4

Take the two coordinate differences first and only then the square root, and decide before computing whether the order of the two places can matter.

Hint 2/4

The distance is the square root of the sum of the two squared differences. Squaring removes any sign, so swapping the two places cannot change the result.

Hint 3/4

The places here are Location(1, 2) and Location(4, 6), so the differences are 3 and 4, and the third call measures a place against itself.

Hint 4/4

The same number twice, then zero, and all three printed as floats.

Show solution

Measure a to b

$$\Delta x = 1-4 = -3,\quad \Delta y = 2-6 = -4$$

the method subtracts the other place from this one, so both differences are negative here

$$d = \sqrt{9+16} = 5.0$$

the squares turn both signs positive

Measure the other way, and against itself

$$\Delta x = 3,\ \Delta y = 4 \rightarrow 5.0$$

the same squares, so the same root

$$\sqrt{0+0} = 0.0$$

a place is nowhere from itself, and the power of 0.5 returns a float

Answer $$\boxed{5.0\;/\;5.0\;/\;0.0}$$
Check

Independent check by a different route: the two places differ by 3 east and 4 north, and 3, 4, 5 is the standard right triangle, so the hypotenuse is exactly 5 with no rounding involved.

A distance function that can return a negative number, or something other than 0 for a place against itself, is broken and can be caught in two lines.

⚠ Calling move and throwing the result away

Method calls that change things are common, so a call on its own line looks like it did something.

wrong$$\texttt{loc.move(1, 0)}\\\texttt{print(loc)} \rightarrow \texttt{<0, 0>}$$
right$$\texttt{loc = loc.move(1, 0)}\\\texttt{print(loc)} \rightarrow \texttt{<1, 0>}$$
⚠ Writing a move that changes the place it was called on

It is one line shorter and it works, until two names hold the same place.

wrong$$\texttt{self.x = self.x + deltaX}\\\texttt{return self}$$
right$$\texttt{return Location(self.x+deltaX, self.y+deltaY)}$$
⚠ Comparing two places with the equality operator

Two places at the same coordinates are the same place in English.

wrong$$\texttt{Location(0,0) == Location(0,0)} \rightarrow \texttt{False}$$
right$$\texttt{a.getX() == b.getX() and a.getY() == b.getY()}$$

12.3Field: a dictionary from walker to place

The field remembers which place each drunk stands on, and a move rebinds only that drunk's key.

Locations know about places. Something has to remember who is standing where, and to ask the walker which way he is going.

DefinitionDefinition 12.3: the Field class
Conditions
  • A field owns one dictionary, self.drunks = {}, whose keys are drunk objects and whose values are Locations.

  • addDrunk(d, loc) writes self.drunks[d] = loc. For a drunk who is already there this is not an error and not a second entry: it moves him to loc.

  • moveDrunk(drunk) asks the drunk for a step, takes the place he is on, and stores the place that move hands back. The field never decides a direction itself.

  • getLoc(drunk) returns the place, and for a drunk who was never added it prints Drunk not in field and returns None.

  • Two drunks may be added on the same Location object, and that is safe here only because move never writes into a place.

$$\boxed{\texttt{f.drunks[d] = loc}\qquad\texttt{moveDrunk:}\ \texttt{f.drunks[d] = f.drunks[d].move(dx, dy)}}$$

Read it as: the field is a lookup table from walker to place, and moving a walker means replacing his entry with the place his own step leads to. The walker chooses the direction, the field does the bookkeeping.

Looks like this, but is not

The method is called addDrunk, so calling it twice for the same drunk should put him in twice, or at least be refused:

f.addDrunk(homer, Location(0, 0))
f.moveDrunk(homer)
f.addDrunk(homer, Location(0, 0))

The third line is a dictionary assignment to a key that is already there, so it overwrites. The field still has one drunk in it and he has been teleported back to the origin. Used on purpose that is how a trial is reset; used by accident it deletes half a walk.

a field, a scripted walker, and two moves

The field's three methods, driven by a walker whose steps are known in advance so that the output can be predicted. The step list cycles, so this walker goes east, north, east, north:

class Location(object):

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def move(self, deltaX, deltaY):
        return Location(self.x + deltaX, self.y + deltaY)

    def getX(self):
        return self.x

    def getY(self):
        return self.y

    def __repr__(self):
        return '<' + str(self.x) + ', ' + str(self.y) + '>'


class Field(object):
    """A dictionary from a drunk to the place that drunk is standing on."""

    def __init__(self):
        self.drunks = {}

    def addDrunk(self, d, loc):
        self.drunks[d] = loc

    def getDrunks(self):
        return self.drunks

    def moveDrunk(self, drunk):
        if drunk not in self.drunks:
            print('Drunk not in field')
        else:
            xDist, yDist = drunk.takeStep()
            currentLocation = self.drunks[drunk]
            self.drunks[drunk] = currentLocation.move(xDist, yDist)

    def getLoc(self, drunk):
        if drunk not in self.drunks:
            print('Drunk not in field')
        else:
            return self.drunks[drunk]


class ScriptedDrunk(object):

    def __init__(self, name, steps):
        self.name = name
        self.steps = steps
        self.next = 0

    def takeStep(self):
        step = self.steps[self.next % len(self.steps)]
        self.next = self.next + 1
        return step

    def __repr__(self):
        return self.name


f = Field()
tester = ScriptedDrunk('Tester', [(1, 0), (0, 1)])
f.addDrunk(tester, Location(0, 0))
print(f.getLoc(tester))
f.moveDrunk(tester)
f.moveDrunk(tester)
print(f.getLoc(tester))
print(f.getDrunks())

It prints:

<0, 0>
<1, 1>
{Tester: <1, 1>}

The last line is the field's own dictionary, printed as it is. Both the key and the value print readably because both classes define __repr__.

FindThe three printed lines.
Given
  • a whose steps are (1, 0) then (0, 1), repeating

  • added at Location(0, 0), then two moveDrunk calls

Solution

Add, and read the place back

$$\texttt{f.drunks = \{Tester: <0, 0>\}}$$

addDrunk writes one entry, and getLoc reads it back unchanged

Move twice

$$\texttt{(1, 0)} \rightarrow \texttt{<1, 0>}$$

the first step of the script is east, and the field stores the place move returns

$$\texttt{(0, 1)} \rightarrow \texttt{<1, 1>}$$

the second step is north, applied to the place stored by the first move rather than to the origin

Read the dictionary

$$\texttt{\{Tester: <1, 1>\}}$$

one key, one value, and the key prints as its name because the walker class defines a repr

Answer $$\boxed{\texttt{<0, 0>}\;/\;\texttt{<1, 1>}\;/\;\texttt{\{Tester: <1, 1>\}}}$$
Check

Independent check by counting steps: two steps were taken, one east and one north, so the walker must be one east and one north of the origin whatever the code looks like, and <1, 1> is that place.

A walker with a known script is how you test the machinery. Randomness comes back once the machinery is known to work.

two keys sharing one place, and only one of them moving

The sharing question, in a dictionary small enough to print. Two names are stored under two keys and both hold the same Location object:

class Location(object):

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def move(self, deltaX, deltaY):
        return Location(self.x + deltaX, self.y + deltaY)

    def __repr__(self):
        return '<' + str(self.x) + ', ' + str(self.y) + '>'


origin = Location(0, 0)
places = {}
places['Homer'] = origin
places['Marge'] = origin
print(places['Homer'] is places['Marge'])
places['Homer'] = places['Homer'].move(1, 0)
print(places)
print(origin)

It prints:

True
{'Homer': <1, 0>, 'Marge': <0, 0>}
<0, 0>

The first line proves they really do share, and the last proves that the shared place came through the move untouched.

FindThe three printed lines, and whether the sharing causes trouble.
Given
  • one Location object stored under two different keys

  • one of the two entries replaced by the result of a move

Solution

Confirm the sharing

$$\texttt{places['Homer'] is places['Marge']} \rightarrow \texttt{True}$$

both entries were assigned the same object, so the identity test passes

Move one of them

$$\texttt{places['Homer'] = places['Homer'].move(1, 0)}$$

move builds a new place and the assignment rebinds one key to it

$$\texttt{\{'Homer': <1, 0>, 'Marge': <0, 0>\}}$$

the other key was not assigned to, so it still points at the old place

Check the shared object itself

$$\texttt{origin} \rightarrow \texttt{<0, 0>}$$

nothing was written into it, which is the property the whole design leans on

Answer $$\boxed{\texttt{True}\;/\;\texttt{\{'Homer': <1, 0>, 'Marge': <0, 0>\}}\;/\;\texttt{<0, 0>}}$$
Check

Independent check against the mutating version of move: with that version the same three lines print True, then both entries at <1, 0>, then <1, 0> for the origin, so this output is evidence about which move is in use.

Sharing a place is safe. Sharing a place with a method that writes into it is not. The design, not the sharing, is what decides.

Checkpoint
§12.3 — one drunk under two names

A drunk object is bound to two names and used as a key twice. The question is how many entries the dictionary ends up with.

Find(a) Write exactly what this program prints, all three lines.
Given
class Drunk(object):

    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return self.name


homer = Drunk('Homer')
alsoHomer = homer
places = {}
places[homer] = (0, 0)
places[alsoHomer] = (3, 4)
print(len(places))
print(places[homer])
print(homer is alsoHomer)
IPython console
Hint 1/4

Count the objects before counting the keys. The two names are the distraction here.

Hint 2/4

A dictionary key is an object. Assigning through a second name for the same object writes to the same key and replaces the value.

Hint 3/4

Here alsoHomer = homer makes one object with two names, so both assignments use one key, and the second one wins.

Hint 4/4

One entry, the value from the second assignment, and True.

Show solution

Count what exists

$$\texttt{Drunk('Homer')} \text{ called once}$$

one object, and the second name is an assignment rather than a construction

$$\texttt{homer is alsoHomer} \rightarrow \texttt{True}$$

which is exactly why the two assignments collide

Apply the two assignments

$$\texttt{places[homer] = (0, 0)}$$

creates the single entry

$$\texttt{places[alsoHomer] = (3, 4)}$$

same key, so the value is replaced and the length stays 1

Answer $$\boxed{1\;/\;\texttt{(3, 4)}\;/\;\texttt{True}}$$
Check

Independent check: if these were two keys the length would be 2 and the lookup through the first name would still give (0, 0). Both printed values contradict that, and they do it in two different ways.

In this section the same sentence explains a reset and a bug: assigning to an existing key replaces its value.

⚠ Calling addDrunk again in the middle of a walk

The name sounds like adding, and adding somebody who is already there sounds harmless.

wrong$$\texttt{for s in range(numSteps):}\\\quad \texttt{f.addDrunk(d, origin)}\\\quad \texttt{f.moveDrunk(d)}$$
right$$\texttt{f.addDrunk(d, origin)}\\\texttt{for s in range(numSteps):}\\\quad \texttt{f.moveDrunk(d)}$$
⚠ Expecting two walkers with the same name to be one key

The printed form of a drunk is his name, and two identical names look like one walker.

wrong$$\texttt{len(\{Drunk('Homer'): 1, Drunk('Homer'): 2\})} \rightarrow 1$$
right$$\texttt{len(\{Drunk('Homer'): 1, Drunk('Homer'): 2\})} \rightarrow 2$$
⚠ Using the None that getLoc returns for a stranger

The complaint is printed, so it feels as though the program has already dealt with the problem.

wrong$$\texttt{f.getLoc(other).getX()} \rightarrow \texttt{AttributeError}$$
right$$\texttt{if other in f.getDrunks():}\\\quad \texttt{print(f.getLoc(other).getX())}$$

12.4The walk function, and the checks that make it believable

One walk moves the drunk numSteps times and returns how far the end is from the start.

Three classes are in place and nothing has used them together yet. One function does, and it is the function every question in this section is built on.

MethodMethod 12.4: walk(f, d, numSteps)
Conditions
  • The starting place is read BEFORE the loop, start = f.getLoc(d). After the loop it is the ending place, and the distance from a place to itself is zero.

  • The loop is for s in range(numSteps): and its body is one f.moveDrunk(d). The loop variable is not used, which is the signal that the body does not depend on which step this is.

  • The value returned is a number, start.distFrom(f.getLoc(d)), not a place. A caller that wants the place asks the field for it.

  • numSteps of 0 is legal and must return 0.0. This is the first thing to test and the cheapest.

  • The docstring says what is assumed and what comes back, in the shape the course uses: Assumes: f a Field, d a Drunk in f, numSteps an int >= 0.

$$\boxed{\begin{aligned}&\texttt{start = f.getLoc(d)}\\&\texttt{for s in range(numSteps): f.moveDrunk(d)}\\&\texttt{return start.distFrom(f.getLoc(d))}\end{aligned}}$$

Read it as: remember where he is, move him that many times, then measure from the remembered place to where he now is. The order of the three lines is the whole content: remembering after moving measures nothing.

Looks like this, but is not

Every step has length one, so a walk of numSteps steps should have walked numSteps, and that is the distance:

def walk(f, d, numSteps):
    for s in range(numSteps):
        f.moveDrunk(d)
    return numSteps

That is the length of the path, and the path folds back on itself. The walk in the figure above took 20 steps, stood on the origin again on the fourth, and ended 2.83 from where it began.

a scripted walk of four steps, and a walk of none

The function, driven by a walker whose steps repeat east, north, east, north, so that the answer is known before the program runs:

class Location(object):

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def move(self, deltaX, deltaY):
        return Location(self.x + deltaX, self.y + deltaY)

    def getX(self):
        return self.x

    def getY(self):
        return self.y

    def distFrom(self, other):
        xDist = self.x - other.getX()
        yDist = self.y - other.getY()
        return (xDist**2 + yDist**2)**0.5

    def __repr__(self):
        return '<' + str(self.x) + ', ' + str(self.y) + '>'


class Field(object):

    def __init__(self):
        self.drunks = {}

    def addDrunk(self, d, loc):
        self.drunks[d] = loc

    def moveDrunk(self, drunk):
        xDist, yDist = drunk.takeStep()
        self.drunks[drunk] = self.drunks[drunk].move(xDist, yDist)

    def getLoc(self, drunk):
        return self.drunks[drunk]


class ScriptedDrunk(object):

    def __init__(self, name, steps):
        self.name = name
        self.steps = steps
        self.next = 0

    def takeStep(self):
        step = self.steps[self.next % len(self.steps)]
        self.next = self.next + 1
        return step

    def __repr__(self):
        return self.name


def walk(f, d, numSteps):
    """Assumes f a Field, d a drunk in f, numSteps an int >= 0.
       Moves d numSteps times and returns the straight line distance
       between where d ended and where d started."""
    start = f.getLoc(d)
    for s in range(numSteps):
        f.moveDrunk(d)
    return start.distFrom(f.getLoc(d))


f = Field()
tester = ScriptedDrunk('Tester', [(1, 0), (0, 1), (1, 0), (0, 1)])
f.addDrunk(tester, Location(0, 0))
print(walk(f, tester, 4))
print(f.getLoc(tester))
f.addDrunk(tester, Location(0, 0))
print(walk(f, tester, 0))

It prints:

2.8284271247461903
<2, 2>
0.0

The first line is the raw float. Python prints every digit it has when nothing formats the value, and 2.8284271247461903 is what a square root of 8 looks like when it is printed unformatted.

FindThe three printed lines.
Given
  • steps (1, 0), (0, 1) repeating, so four steps go east, north, east, north

  • added at Location(0, 0), then walk(f, tester, 4) and walk(f, tester, 0)

Solution

Walk the four steps

$$\Delta x = 1 + 0 + 1 + 0 = 2,\quad \Delta y = 0 + 1 + 0 + 1 = 2$$

the script alternates, so two steps go east and two go north

$$d = \sqrt{2^2 + 2^2} = \sqrt{8} = 2.8284271247461903$$

the unformatted float, because this print has no format call in it

Walk no steps at all

$$\texttt{f.addDrunk(tester, Location(0, 0))}$$

the walker is put back, otherwise the next walk starts from <2, 2>

$$\texttt{range(0)} \rightarrow \text{no moves}$$

the loop body never runs, so start and end are the same place and the distance is 0.0

Answer $$\boxed{2.8284271247461903\;/\;\texttt{<2, 2>}\;/\;0.0}$$
Check

Independent check on the middle line: the printed place <2, 2> must satisfy the first line, and the square root of 2 squared plus 2 squared is 2.83, which it does. The two lines were produced by different calls, so they agree by construction only if the function is right.

A zero step walk returning 0.0 is the cheapest test this function has. Run it before you trust any number.

the same function with two lines swapped

What happens when the starting place is read after the loop rather than before it. Everything else is identical, and the walker really does move:

class Location(object):

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def move(self, deltaX, deltaY):
        return Location(self.x + deltaX, self.y + deltaY)

    def getX(self):
        return self.x

    def getY(self):
        return self.y

    def distFrom(self, other):
        xDist = self.x - other.getX()
        yDist = self.y - other.getY()
        return (xDist**2 + yDist**2)**0.5


class Field(object):

    def __init__(self):
        self.drunks = {}

    def addDrunk(self, d, loc):
        self.drunks[d] = loc

    def moveDrunk(self, drunk):
        xDist, yDist = drunk.takeStep()
        self.drunks[drunk] = self.drunks[drunk].move(xDist, yDist)

    def getLoc(self, drunk):
        return self.drunks[drunk]


class ScriptedDrunk(object):

    def __init__(self, name, steps):
        self.name = name
        self.steps = steps
        self.next = 0

    def takeStep(self):
        step = self.steps[self.next % len(self.steps)]
        self.next = self.next + 1
        return step


def brokenWalk(f, d, numSteps):
    for s in range(numSteps):
        f.moveDrunk(d)
    start = f.getLoc(d)
    return start.distFrom(f.getLoc(d))


f = Field()
tester = ScriptedDrunk('Tester', [(1, 0), (0, 1)])
f.addDrunk(tester, Location(0, 0))
print(brokenWalk(f, tester, 10))
print(f.getLoc(tester).getX(), f.getLoc(tester).getY())

It prints:

0.0
5 5

Zero, for a walk that took ten steps and ended 7.07 away. The second line is there to prove the walking happened: the walker is standing at 5, 5.

FindWhy the distance is 0.0 while the walker has moved.
Given
  • brokenWalk reads start after the loop

  • a scripted walker taking (1, 0) and (0, 1) alternately, 10 steps

Solution

Follow the two names

$$\texttt{start} = \texttt{f.getLoc(d)} \text{ after the loop}$$

by then the field holds the ending place, so start is bound to it

$$\texttt{f.getLoc(d)} \text{ in the return line}$$

the same place again, since nothing moved in between

Measure a place against itself

$$d = \sqrt{(5-5)^2 + (5-5)^2} = 0.0$$

both coordinate differences are zero

$$\text{walker at } (5, 5)$$

ten alternating steps are five east and five north, so the true distance was 7.07

Answer $$\boxed{\text{reported } 0.0,\ \text{true } 7.07}$$
Check

Independent check that the bug is in the measuring and not in the walking: the printed coordinates are 5 and 5, which is exactly where ten alternating steps must end, so the moving half of the function is correct.

A bug that returns a plausible looking number for every input is worse than one that raises. This one returns 0.0 forever.

the three cheap tests: zero steps, one step, two steps

Before believing a simulation, run it on the cases whose answers are known. This program collects every distance that appears over a hundred walks, for walks of zero, one and two steps:

import random


class Location(object):

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def move(self, deltaX, deltaY):
        return Location(self.x + deltaX, self.y + deltaY)

    def getX(self):
        return self.x

    def getY(self):
        return self.y

    def distFrom(self, other):
        xDist = self.x - other.getX()
        yDist = self.y - other.getY()
        return (xDist**2 + yDist**2)**0.5


class Field(object):

    def __init__(self):
        self.drunks = {}

    def addDrunk(self, d, loc):
        self.drunks[d] = loc

    def moveDrunk(self, drunk):
        xDist, yDist = drunk.takeStep()
        self.drunks[drunk] = self.drunks[drunk].move(xDist, yDist)

    def getLoc(self, drunk):
        return self.drunks[drunk]


class UsualDrunk(object):

    def takeStep(self):
        stepChoices = [(0, 1), (0, -1), (1, 0), (-1, 0)]
        return random.choice(stepChoices)


def walk(f, d, numSteps):
    start = f.getLoc(d)
    for s in range(numSteps):
        f.moveDrunk(d)
    return start.distFrom(f.getLoc(d))


def distancesSeen(numSteps, numTrials):
    """The set of distances a walk of numSteps ever produces in numTrials"""
    seen = []
    d = UsualDrunk()
    for t in range(numTrials):
        f = Field()
        f.addDrunk(d, Location(0, 0))
        dist = walk(f, d, numSteps)
        if dist not in seen:
            seen.append(dist)
    seen.sort()
    return seen


random.seed(7)
print(distancesSeen(0, 100))
print(distancesSeen(1, 100))
print(distancesSeen(2, 100))

It prints:

[0.0]
[1.0]
[0.0, 1.4142135623730951, 2.0]

The third line is the interesting one. Two steps can end 1.41 away, which the naive guess of nothing or two misses, and it is the group of eight corner walks counted at the start of this section.

FindWhat the three lines show, and what each one rules out.
Given
  • walks of 0, 1 and 2 steps, a hundred trials of each, seed 7

  • the set of distances that appeared, sorted

Solution

Read the first two lines

$$\texttt{[0.0]}$$

a walk of no steps can only end where it started, so any other value here would be a bug in the loop bounds

$$\texttt{[1.0]}$$

one step of length one, in any of four directions, is distance one from the start every time

Read the third

$$\texttt{[0.0, 1.4142135623730951, 2.0]}$$

the three groups, and nothing else appeared in a hundred trials

$$\text{no } 1.0 \text{ and no } 3.0$$

two steps cannot leave the walker an odd distance away, and the printed set agrees

Answer $$\boxed{\{0.0\},\ \{1.0\},\ \{0.0,\ 1.41,\ 2.0\}}$$
Check

Independent check against the hand count made earlier: the sixteen two step walks were grouped into distances 0, 1.41 and 2, and this run of the actual simulation produced that set and no other value. The hand count and the program agree, which is what a sanity check is for.

Tiny cases have known answers. That is the only reason to run them, and it is enough of a reason.

Checkpoint
§12.4 — how many times the body runs

Two loops and a list, with no walking in them. The point is the count, because the count is what numSteps controls.

Find(a) Write exactly what this program prints, all three lines.
Given
count = 0
for s in range(1, 10):
    count = count + 1
print(count)
positions = []
x = 0
for s in range(4):
    x = x + 1
    positions.append(x)
print(positions)
print(len(positions), positions[-1])
IPython console
Hint 1/4

Count the values the loop variable takes before you look at what the body does with them.

Hint 2/4

range(a, b) yields the values from a up to b minus 1, so it produces b minus a values. range(n) produces n of them, starting at 0.

Hint 3/4

The first loop is range(1, 10), so it runs 9 times, and the second is range(4), so the list gets 4 items and x ends at 4.

Hint 4/4

Nine, then the four item list, then its length and its last item.

Show solution

Count the two ranges

$$\texttt{range(1, 10)} \rightarrow 9 \text{ values}$$

the stop value is not included, so this is 10 minus 1

$$\texttt{range(4)} \rightarrow 0, 1, 2, 3$$

four values, starting at zero

Read the body's effect

$$\texttt{positions} = [1, 2, 3, 4]$$

x is bumped by one before each append, so the values start at 1 rather than at 0

$$\texttt{len} = 4,\ \texttt{last} = 4$$

one append per pass, and the last bump left x at 4

Answer $$\boxed{9\;/\;\texttt{[1, 2, 3, 4]}\;/\;\texttt{4 4}}$$
Check

Independent check: the length of the list must equal the number of passes through the second loop, and both come out 4, computed two different ways.

In this section the count is the specification: numSteps steps means range(numSteps), and any other spelling is an off by one.

⚠ Reading the starting place after the loop

Both places are needed at the end, so it feels natural to fetch them together.

wrong$$\texttt{for s in range(numSteps): f.moveDrunk(d)}\\\texttt{start = f.getLoc(d)}$$
right$$\texttt{start = f.getLoc(d)}\\\texttt{for s in range(numSteps): f.moveDrunk(d)}$$
⚠ Returning the place instead of the distance

The place is what the field holds, and it is one method call closer.

wrong$$\texttt{return f.getLoc(d)}$$
right$$\texttt{return start.distFrom(f.getLoc(d))}$$
⚠ Writing the loop as range(1, numSteps)

Counting steps from one is how people count, and the loop variable is unused so nothing complains.

wrong$$\texttt{for s in range(1, numSteps)} \rightarrow \texttt{numSteps} - 1 \text{ moves}$$
right$$\texttt{for s in range(numSteps)} \rightarrow \texttt{numSteps} \text{ moves}$$

12.5One walk answers nothing: trials, and what a reset protects

A simulation reports the mean of many trials, and each trial puts the walker back where he began.

The walk function hands back one number. Asked the same question twice it hands back two different numbers, so one of them cannot be the answer.

MethodMethod 12.5: simWalks and the report
Conditions
  • simWalks(numSteps, numTrials) calls walk numTrials times and collects the distances in a list. It returns the list, not the mean, so that the caller can also see the spread.

  • Every trial begins by putting the walker back: f.addDrunk(homer, origin), or a fresh Field for each trial. Without it the walker simply carries on from where the previous trial left him.

  • The report is three numbers, sum(distances)/len(distances), max(distances) and min(distances). A mean on its own hides how wide the answers were.

  • The seed call belongs above the trial loop. Inside it, every trial repeats the same walk and the mean of identical numbers is that number.

  • numTrials is how many walks, numSteps is how long each one is. They are different knobs and the mean depends on both.

$$\boxed{\text{mean} = \frac{\texttt{sum(distances)}}{\texttt{len(distances)}}\qquad\texttt{len(distances)} = \texttt{numTrials}}$$

Read it as: add the distances of all the walks and divide by how many walks there were, which is the number of trials and never the number of steps.

Looks like this, but is not

A long walk is a lot of walking, so one very long walk should be evidence enough:

print(walk(f, homer, 100000))

Length is not repetition. A hundred thousand steps gives one sample of the question, and the next run of the same line gives a different number. Ten walks of a hundred steps on this page landed between 2.0 and 15.3.

how many trialswhat came outwould you report it

1

6.0

no, the next walk gave 11.0

1 again

11.0

no, and the two differ by 5.0

10

8.44

with its range, 2.0 to 15.3

500

8.6

yes, and it barely moves between seeds

Read down the middle column. The first two rows are the same experiment and they disagree by more than half the value, and the last two rows agree with each other to within 0.2 while using different seeds.

ten walks of a hundred steps, reported three ways

The trial loop, with the ten distances printed one per line and then the three report numbers:

import random


class Location(object):

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def move(self, deltaX, deltaY):
        return Location(self.x + deltaX, self.y + deltaY)

    def getX(self):
        return self.x

    def getY(self):
        return self.y

    def distFrom(self, other):
        xDist = self.x - other.getX()
        yDist = self.y - other.getY()
        return (xDist**2 + yDist**2)**0.5


class Field(object):

    def __init__(self):
        self.drunks = {}

    def addDrunk(self, d, loc):
        self.drunks[d] = loc

    def moveDrunk(self, drunk):
        xDist, yDist = drunk.takeStep()
        self.drunks[drunk] = self.drunks[drunk].move(xDist, yDist)

    def getLoc(self, drunk):
        return self.drunks[drunk]


class UsualDrunk(object):

    def takeStep(self):
        stepChoices = [(0, 1), (0, -1), (1, 0), (-1, 0)]
        return random.choice(stepChoices)


def walk(f, d, numSteps):
    start = f.getLoc(d)
    for s in range(numSteps):
        f.moveDrunk(d)
    return start.distFrom(f.getLoc(d))


def simWalks(numSteps, numTrials):
    """Runs numTrials walks of numSteps and returns the list of distances"""
    homer = UsualDrunk()
    origin = Location(0, 0)
    distances = []
    for t in range(numTrials):
        f = Field()
        f.addDrunk(homer, origin)
        distances.append(walk(f, homer, numSteps))
    return distances


random.seed(3)
distances = simWalks(100, 10)
for dist in distances:
    print(format(dist, '.1f'))
mean = sum(distances) / len(distances)
print('Mean:', format(mean, '.2f'))
print('Max:', format(max(distances), '.2f'))
print('Min:', format(min(distances), '.2f'))

It prints:

5.8
15.3
15.3
8.6
5.7
10.2
13.9
4.5
2.0
3.2
Mean: 8.44
Max: 15.30
Min: 2.00

The mean is 8.44 and no single walk reported that. The closest was 8.6, the two extremes were 2.0 and 15.3, and that spread of 13.3 is more than one and a half times the mean.

FindThe ten distances and the three report numbers.
Given
  • numSteps 100, numTrials 10, seed 3

  • a fresh Field and a reset to the origin in every trial

Solution

Read the loop

$$\texttt{for t in range(10)}$$

ten trials, so the list ends with ten numbers in it

$$\texttt{f = Field()}, \texttt{f.addDrunk(homer, origin)}$$

a new field each time is the strongest form of the reset: there is nothing left over to carry on from

Report

$$\text{mean} = \frac{84.4452}{10} = 8.44$$

the sum of the ten distances as the program holds them, not as it printed them: adding the printed one decimal values gives 84.5 and a mean of 8.45

$$\max = 15.30,\ \min = 2.00$$

the two ends, which is what tells the reader how much the mean is hiding

Answer $$\boxed{\text{mean } 8.44,\ \max 15.30,\ \min 2.00}$$
Check

Independent check on the mean by bracketing: the mean of ten numbers must lie between their smallest and largest, so it has to be between 2.00 and 15.30, and a glance at the ten values puts it near the middle of the list rather than at either end.

Ten trials of a hundred steps is a thousand moveDrunk calls.

Report all three numbers. A mean without its range invites the reader to trust it more than it deserves.

one walk against five hundred walks

The same drunk, the same hundred steps, asked three times. The first two are single walks and the third is the mean of five hundred:

import random


class Location(object):

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def move(self, deltaX, deltaY):
        return Location(self.x + deltaX, self.y + deltaY)

    def distFrom(self, other):
        xDist = self.x - other.x
        yDist = self.y - other.y
        return (xDist**2 + yDist**2)**0.5


class UsualDrunk(object):

    def takeStep(self):
        stepChoices = [(0, 1), (0, -1), (1, 0), (-1, 0)]
        return random.choice(stepChoices)


def oneWalk(d, numSteps):
    start = Location(0, 0)
    here = start
    for s in range(numSteps):
        xDist, yDist = d.takeStep()
        here = here.move(xDist, yDist)
    return start.distFrom(here)


random.seed(31)
homer = UsualDrunk()
print('first walk  ', format(oneWalk(homer, 100), '.1f'))
print('second walk ', format(oneWalk(homer, 100), '.1f'))
total = 0
for t in range(500):
    total = total + oneWalk(homer, 100)
print('mean of 500 ', format(total / 500, '.1f'))

It prints:

first walk   6.0
second walk  11.0
mean of 500  8.6

The two single walks differ by almost a factor of two. Either one of them, written down alone, would look like a measurement.

FindHow far apart two single answers can be, and where the mean sits.
Giventwo single walks of 100 steps, then the mean of 500 walks, seed 31
Solution

Compare the two single walks

$$6.0 \text{ against } 11.0$$

the same question, the same length, and a ratio of almost two between the answers

$$\text{neither is wrong}$$

both are real walks; what is wrong is calling either one the answer

Read the mean of five hundred

$$8.6$$

between the two single answers, and this is the number that stays put when the program is run again with another seed

$$\text{500 trials} \rightarrow \text{50,000 moves}$$

the price of a stable answer, and it is paid in a second

Answer $$\boxed{6.0,\ 11.0,\ \text{mean } 8.6}$$
Check

Independent check against a different run: the ten trial run earlier in this section, with another seed, reported a mean of 8.44 for the same question. Two independent estimates within 0.2 of each other is the kind of agreement a single walk never gives.

The number of trials buys stability. The number of steps changes the question being asked.

what the reset actually protects, measured both ways

The reset is easy to drop, and dropping it does not make the distances look wrong, which is why it needs measuring rather than arguing about. This program runs four trials twice, once with the reset and once without, and prints two distances for each trial: from where that trial started, and from the origin of the whole run:

import random


class Location(object):

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def move(self, deltaX, deltaY):
        return Location(self.x + deltaX, self.y + deltaY)

    def getX(self):
        return self.x

    def getY(self):
        return self.y

    def distFrom(self, other):
        xDist = self.x - other.getX()
        yDist = self.y - other.getY()
        return (xDist**2 + yDist**2)**0.5


class Field(object):

    def __init__(self):
        self.drunks = {}

    def addDrunk(self, d, loc):
        self.drunks[d] = loc

    def moveDrunk(self, drunk):
        xDist, yDist = drunk.takeStep()
        self.drunks[drunk] = self.drunks[drunk].move(xDist, yDist)

    def getLoc(self, drunk):
        return self.drunks[drunk]


class UsualDrunk(object):

    def takeStep(self):
        stepChoices = [(0, 1), (0, -1), (1, 0), (-1, 0)]
        return random.choice(stepChoices)


def trial(f, d, numSteps, origin, reset):
    """One trial. Returns the distance walked from where this trial
       started, and the distance from the origin of the whole run."""
    if reset:
        f.addDrunk(d, origin)
    start = f.getLoc(d)
    for s in range(numSteps):
        f.moveDrunk(d)
    end = f.getLoc(d)
    return start.distFrom(end), origin.distFrom(end)


def fourTrials(reset):
    random.seed(3)
    d = UsualDrunk()
    origin = Location(0, 0)
    f = Field()
    f.addDrunk(d, origin)
    for t in range(4):
        fromStart, fromOrigin = trial(f, d, 100, origin, reset)
        print('  trial', t + 1, 'from start', format(fromStart, '.1f'),
              'from origin', format(fromOrigin, '.1f'))


print('with the reset')
fourTrials(True)
print('without the reset')
fourTrials(False)

It prints:

with the reset
  trial 1 from start 5.8 from origin 5.8
  trial 2 from start 15.3 from origin 15.3
  trial 3 from start 15.3 from origin 15.3
  trial 4 from start 8.6 from origin 8.6
without the reset
  trial 1 from start 5.8 from origin 5.8
  trial 2 from start 15.3 from origin 14.4
  trial 3 from start 15.3 from origin 24.7
  trial 4 from start 8.6 from origin 16.5

The first column is identical in both halves. The second column is where the damage is.

FindWhat changes when the reset is dropped, and what does not.
Given
  • four trials of 100 steps, seed 3, run twice

  • reset on in the first half, off in the second

Solution

Read the first column

$$5.8,\ 15.3,\ 15.3,\ 8.6 \text{ in both halves}$$

the walk function measures from where the trial started, and a walk's displacement does not depend on where it starts

$$\text{so the mean looks fine}$$

the bug cannot be caught by looking at the reported mean, which is exactly the danger

Read the second column

$$5.8,\ 15.3,\ 15.3,\ 8.6 \text{ with the reset}$$

the trial start and the origin are the same place, so the two columns must agree, and they do

$$5.8,\ 14.4,\ 24.7,\ 16.5 \text{ without}$$

the walker is wandering further and further from the origin, so every end location plotted after trial one belongs to a different experiment

Answer $$\boxed{\text{distances unchanged},\ \text{end locations wrong}}$$
Check

Independent check that the two halves really ran the same walks: the first column agrees value for value across both halves, which it can only do if the same seeded stream of steps was used in both, so the only difference between the halves is the reset.

Say what a trial measures before you write it. Here it is displacement over numSteps steps, and that sentence is what tells you the reset belongs inside the loop.

Checkpoint
§12.5 — reporting a list of distances

Five distances from an earlier run are already in a list. This is only the reporting half: mean, ends and spread.

Find(a) Write exactly what this program prints, all four lines.
Given
distances = [5.8, 15.3, 15.3, 8.6, 5.7]
mean = sum(distances) / len(distances)
print('trials', len(distances))
print('mean', format(mean, '.2f'))
print('max', max(distances), 'min', min(distances))
print('spread', format(max(distances) - min(distances), '.1f'))
IPython console
Hint 1/4

Decide what each of the four prints is asking for before computing anything, and note which ones are formatted and which are not.

Hint 2/4

The mean is the sum over the length, max and min take the ends, and format(value, '.2f') rounds to two decimals only for printing.

Hint 3/4

The distances again: 5.8, 15.3, 15.3, 8.6, 5.7. Their sum is 50.7 and there are five of them. The third print has no format call in it.

Hint 4/4

The count, the formatted mean, the two ends unformatted, then the formatted spread.

Show solution

Count and average

$$\texttt{len} = 5$$

five numbers went into the list, and the loop that summed them does not change that

$$\text{mean} = \frac{50.7}{5} = 10.14$$

the sum of the five, divided by five, shown with two decimals

Take the ends and the spread

$$\max = 15.3,\ \min = 5.7$$

printed unformatted, so they appear exactly as they were written in the list

$$15.3 - 5.7 = 9.6$$

the spread, formatted to one decimal because the subtraction of two floats can leave a tail of digits

Answer $$\boxed{5\;/\;10.14\;/\;15.3\ 5.7\;/\;9.6}$$
Check

Independent check on the mean: it must sit between 5.7 and 15.3, and 10.14 does. A second check is that five numbers averaging 10.14 must sum to 50.7, which is the sum used.

Format at the printing, never before the arithmetic.

⚠ Dividing by the number of steps instead of the number of trials

Both numbers are in the function's parameter list and both are large.

wrong$$\texttt{mean = sum(distances) / numSteps}$$
right$$\texttt{mean = sum(distances) / len(distances)}$$
⚠ Rounding each distance before summing them

The report shows two decimals, so rounding early looks like tidying up.

wrong$$\texttt{distances.append(round(walk(f, d, n), 1))}$$
right$$\texttt{distances.append(walk(f, d, n))}\\\texttt{print(format(mean, '.2f'))}$$
⚠ Dropping the reset at the top of the trial loop

The reported distances stay believable, so nothing looks broken.

wrong$$\texttt{for t in range(numTrials):}\\\quad \texttt{distances.append(walk(f, d, n))}$$
right$$\texttt{for t in range(numTrials):}\\\quad \texttt{f.addDrunk(d, origin)}\\\quad \texttt{distances.append(walk(f, d, n))}$$
⚠ Seeding inside the trial loop

It makes the run repeatable, which is what was asked for, and it does that.

wrong$$\text{10 identical trials} \rightarrow \text{mean} = \text{that one distance}$$
right$$\text{seed once} \rightarrow \text{10 different trials, mean over all of them}$$

12.6How the mean grows, and what a bias does to it

Quadruple the steps of a fair walk and the mean distance doubles; a biased walk grows with the steps.

The mean is a stable number now, so the question from the opening can finally be asked properly: what does it do when the walk gets longer?

RuleRule 12.6: the growth of the mean distance
Conditions
  • For the fair walk, measured over walks of 25, 100, 400 and 1600 steps: multiplying the steps by four multiplies the mean distance by about two.

  • Equivalently, the mean divided by the square root of the number of steps stays put. In the runs on this page that ratio was between 0.87 and 0.92 over a 64 fold range of walk lengths.

  • Doubling the steps therefore multiplies the mean by about 1.4 and not by 2. Measured: 9.16 at 100 steps and 12.52 at 200.

  • For a walk with a bias, one direction is more likely than its opposite, so there is a of a fixed amount per step and the mean distance grows in proportion to the number of steps. Measured: four times the steps gave about four times the distance.

  • All of this is measured here rather than derived. The course's tool for this question is the simulation, and the numbers above are what it reported.

$$\boxed{\text{fair walk: } \bar d \approx 0.9\sqrt{n}\qquad \text{biased walk: } \bar d \propto n}$$

Read the first as: the mean distance of a fair walk goes like the square root of the number of steps, so four times the walking buys twice the distance. Read the second as: a walk that leans in one direction covers ground in proportion to how long it walks.

Looks like this, but is not

Twice as much walking should get you twice as far, so the mean after 200 steps should be twice the mean after 100:

100 steps -> 9.16, so 200 steps -> 18.3?

Measured, 200 steps gave 12.52, which is 1.37 times the 100 step mean rather than 2. It takes four times the steps to double the mean, and 400 steps did give 18.35. The worked example below is that measurement.

stepsfair meanbiased meanbiased over fair

100

9.0

20.8

2.3

400

17.9

80.9

4.5

1600

36.4

319.1

8.8

Read the last column downwards: the gap between the two walkers is not a fixed factor, it doubles every time the steps are quadrupled. Two different growth rates always separate like that, and a single row would have hidden it.

quadrupling the steps, four times over

Three hundred trials at each of four walk lengths, each length four times the last. The third column divides the mean by the square root of the number of steps:

import random


class Location(object):

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def move(self, deltaX, deltaY):
        return Location(self.x + deltaX, self.y + deltaY)

    def getX(self):
        return self.x

    def getY(self):
        return self.y

    def distFrom(self, other):
        xDist = self.x - other.getX()
        yDist = self.y - other.getY()
        return (xDist**2 + yDist**2)**0.5


class Field(object):

    def __init__(self):
        self.drunks = {}

    def addDrunk(self, d, loc):
        self.drunks[d] = loc

    def moveDrunk(self, drunk):
        xDist, yDist = drunk.takeStep()
        self.drunks[drunk] = self.drunks[drunk].move(xDist, yDist)

    def getLoc(self, drunk):
        return self.drunks[drunk]


class UsualDrunk(object):

    def takeStep(self):
        stepChoices = [(0, 1), (0, -1), (1, 0), (-1, 0)]
        return random.choice(stepChoices)


def walk(f, d, numSteps):
    start = f.getLoc(d)
    for s in range(numSteps):
        f.moveDrunk(d)
    return start.distFrom(f.getLoc(d))


def meanDistance(numSteps, numTrials):
    d = UsualDrunk()
    total = 0
    for t in range(numTrials):
        f = Field()
        f.addDrunk(d, Location(0, 0))
        total = total + walk(f, d, numSteps)
    return total / numTrials


random.seed(11)
print('steps  mean  mean/sqrt(steps)')
for numSteps in [25, 100, 400, 1600]:
    mean = meanDistance(numSteps, 300)
    ratio = mean / numSteps**0.5
    print(numSteps, format(mean, '.2f'), format(ratio, '.2f'))

It prints:

steps  mean  mean/sqrt(steps)
25 4.37 0.87
100 8.85 0.88
400 18.49 0.92
1600 36.91 0.92

The middle column roughly doubles down the table and the third column barely moves. Those are two ways of saying the same thing, and the second one is the one that generalises.

FindWhat the mean does when the number of steps is multiplied by four.
Given
  • walks of 25, 100, 400 and 1600 steps

  • 300 trials at each length, seed 11

Solution

Read the middle column as ratios

$$\frac{8.85}{4.37} = 2.03,\qquad \frac{18.49}{8.85} = 2.09$$

each row has four times the steps of the row above, and the mean comes out about twice as large

$$\frac{36.91}{18.49} = 2.00$$

three quadruplings, three doublings, so this is a pattern and not one lucky pair

Read the third column

$$\frac{\bar d}{\sqrt{n}}: 0.87,\ 0.88,\ 0.92,\ 0.92$$

dividing by the square root of the steps removes the growth and leaves a number that stays put

$$\bar d \approx 0.9\sqrt{n}$$

which is the same statement as the doubling rule, since quadrupling n doubles its square root

Answer $$\boxed{n \times 4 \Rightarrow \bar d \times 2,\qquad \frac{\bar d}{\sqrt n} \approx 0.9}$$
Check

Independent check on the arithmetic of the last row: 0.92 times the square root of 1600, which is 40, gives 36.8, and the measured mean was 36.91. The rule read forwards reproduces the measurement it came from.

The last row alone is 300 trials of 1600 steps, so 480,000 moves, and it ran in about a second.

A growth rule is easiest to see by dividing it out. If the quotient stays flat, the rule is the divisor.

doubling the steps, which does not double the distance

The obvious guess deserves its own measurement. Three hundred trials at 100, 200 and 400 steps:

import random


class Location(object):

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def move(self, deltaX, deltaY):
        return Location(self.x + deltaX, self.y + deltaY)

    def distFrom(self, other):
        xDist = self.x - other.x
        yDist = self.y - other.y
        return (xDist**2 + yDist**2)**0.5


class UsualDrunk(object):

    def takeStep(self):
        stepChoices = [(0, 1), (0, -1), (1, 0), (-1, 0)]
        return random.choice(stepChoices)


def meanDistance(numSteps, numTrials):
    d = UsualDrunk()
    total = 0
    for t in range(numTrials):
        start = Location(0, 0)
        here = start
        for s in range(numSteps):
            xDist, yDist = d.takeStep()
            here = here.move(xDist, yDist)
        total = total + start.distFrom(here)
    return total / numTrials


random.seed(19)
means = []
stepCounts = [100, 200, 400]
for numSteps in stepCounts:
    means.append(meanDistance(numSteps, 300))
for i in range(len(means)):
    print(stepCounts[i], format(means[i], '.2f'))
print('doubling once:', format(means[1] / means[0], '.2f'))
print('doubling twice:', format(means[2] / means[0], '.2f'))

It prints:

100 9.16
200 12.52
400 18.35
doubling once: 1.37
doubling twice: 2.00

One doubling gives 1.37 and two doublings give 2.00. A factor that appears once per doubling and squares to two is the square root of two, 1.414.

FindThe factor one doubling of the steps buys.
Givenwalks of 100, 200 and 400 steps, 300 trials each, seed 19
Solution

Take the two ratios

$$\frac{12.52}{9.16} = 1.37$$

one doubling, and the guess of 2 is out by nearly half

$$\frac{18.35}{9.16} = 2.00$$

two doublings, which is a quadrupling, and there the factor is 2

Check the two against each other

$$1.37^2 = 1.88 \approx 2.00$$

if one doubling multiplies by f, two doublings multiply by f squared, and the measured pair is consistent with that

$$f = \sqrt{2} \approx 1.414$$

the factor the square root rule predicts, and 1.37 is within the wobble of 300 trials

Answer $$\boxed{n \times 2 \Rightarrow \bar d \times \sqrt{2} \approx 1.4}$$
Check

Independent check against the other table: the run at 100 steps here gave 9.16 and the earlier run with a different seed gave 8.85, so the two independent estimates of the same quantity agree to within about three per cent, which is the size of wobble to expect at 300 trials.

When a ratio comes out near 1.4, suspect a square root. When it comes out near the factor itself, suspect a drift.

a walker who leans east, and the rule he breaks

One subclass, one changed line: the step list holds east twice, so east is twice as likely as west. Everything else is the fair walk:

import random


class Location(object):

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def move(self, deltaX, deltaY):
        return Location(self.x + deltaX, self.y + deltaY)

    def getX(self):
        return self.x

    def getY(self):
        return self.y

    def distFrom(self, other):
        xDist = self.x - other.getX()
        yDist = self.y - other.getY()
        return (xDist**2 + yDist**2)**0.5


class Field(object):

    def __init__(self):
        self.drunks = {}

    def addDrunk(self, d, loc):
        self.drunks[d] = loc

    def moveDrunk(self, drunk):
        xDist, yDist = drunk.takeStep()
        self.drunks[drunk] = self.drunks[drunk].move(xDist, yDist)

    def getLoc(self, drunk):
        return self.drunks[drunk]


class UsualDrunk(object):

    def takeStep(self):
        stepChoices = [(0, 1), (0, -1), (1, 0), (-1, 0)]
        return random.choice(stepChoices)


class EastDrunk(object):
    """Same four steps, but east appears twice, so east is twice as likely"""

    def takeStep(self):
        stepChoices = [(0, 1), (0, -1), (1, 0), (-1, 0), (1, 0)]
        return random.choice(stepChoices)


def walk(f, d, numSteps):
    start = f.getLoc(d)
    for s in range(numSteps):
        f.moveDrunk(d)
    return start.distFrom(f.getLoc(d))


def meanDistance(drunkClass, numSteps, numTrials):
    d = drunkClass()
    total = 0
    for t in range(numTrials):
        f = Field()
        f.addDrunk(d, Location(0, 0))
        total = total + walk(f, d, numSteps)
    return total / numTrials


random.seed(13)
drunkClasses = [UsualDrunk, EastDrunk]
for drunkClass in drunkClasses:
    print(drunkClass.__name__)
    for numSteps in [100, 400, 1600]:
        mean = meanDistance(drunkClass, numSteps, 200)
        print(' ', numSteps, format(mean, '.1f'))

It prints:

UsualDrunk
  100 9.0
  400 17.9
  1600 36.4
EastDrunk
  100 20.8
  400 80.9
  1600 319.1

The fair column doubles per quadrupling, as before. The biased column multiplies by about four, which is the signature of a drift rather than of a wander.

FindHow the two growth patterns differ, and where the numbers come from.
Given
  • UsualDrunk against EastDrunk, whose list is [(0,1), (0,-1), (1,0), (-1,0), (1,0)]

  • walks of 100, 400 and 1600 steps, 200 trials each, seed 13

Solution

Read the two columns as ratios

$$\text{fair}: \frac{17.9}{9.0} = 1.99,\ \frac{36.4}{17.9} = 2.03$$

four times the steps, twice the distance, which is the rule from the previous example

$$\text{biased}: \frac{80.9}{20.8} = 3.89,\ \frac{319.1}{80.9} = 3.94$$

four times the steps, four times the distance, so the mean is growing in proportion to n

Check the drift against the step list

$$\text{east } \frac{2}{5},\ \text{west } \frac{1}{5}$$

east appears twice in a list of five, so the average change in x per step is 0.2 to the east

$$0.2 \times 100 = 20 \approx 20.8$$

after 100 steps the drift alone accounts for the measured mean distance, and the wandering adds almost nothing to it

Answer $$\boxed{\text{fair: } \times 2 \text{ per quadrupling},\qquad \text{biased: } \times 4}$$
Check

Independent check at the longest walk: the drift estimate 0.2 times 1600 is 320, and the measured mean was 319.1. The estimate came from counting entries in the step list, not from the simulation, so the two are genuinely separate routes to the same number.

Count the step list before running anything. An unbalanced list means a drift, and a drift beats a wander at every length worth simulating.

Checkpoint
§12.6 — dividing the growth out of a table

Four means are already measured and sit in a list. The program divides each by the square root of its step count, then takes two ratios.

Find(a) Write exactly what this program prints, all six lines.
Given
means = [4.37, 8.85, 18.49, 36.91]
stepCounts = [25, 100, 400, 1600]
for i in range(len(means)):
    ratio = means[i] / stepCounts[i]**0.5
    print(stepCounts[i], format(ratio, '.2f'))
print(format(means[3] / means[2], '.2f'))
print(format(means[1] / means[0], '.2f'))
IPython console
Hint 1/4

Decide what the loop prints per pass and what the two prints after it are comparing, before doing any arithmetic.

Hint 2/4

Dividing a mean by the square root of its step count leaves a number that stays put if the mean grows like that square root. The last two prints are ratios of means, four times apart in steps.

Hint 3/4

The means again, 4.37, 8.85, 18.49 and 36.91, against step counts 25, 100, 400 and 1600. The square roots are 5, 10, 20 and 40.

Hint 4/4

Four lines of step count and ratio, then the 1600 against 400 ratio, then the 100 against 25 ratio.

Show solution

Divide each mean by a square root

$$\frac{4.37}{5} = 0.87,\quad \frac{8.85}{10} = 0.89$$

the roots of 25 and 100 are whole numbers, so these two can be checked without a calculator

$$\frac{18.49}{20} = 0.92,\quad \frac{36.91}{40} = 0.92$$

the quotient is not drifting up or down, which is what flat means here

Take the two quadrupling ratios

$$\frac{36.91}{18.49} = 2.00$$

1600 steps against 400, so four times the walking

$$\frac{8.85}{4.37} = 2.03$$

100 against 25, the other end of the table, and the same factor

Answer $$\boxed{0.87,\ 0.89,\ 0.92,\ 0.92,\ 2.00,\ 2.03}$$
Check

Independent check joining the two halves: if the third column is flat at c then the ratio over a quadrupling must be exactly 2, since the square root of four is two. The printed ratios are 2.00 and 2.03, so the two halves of the table agree.

A flat quotient is a proof of a growth rule that fits on one line of output.

⚠ Reading the square root rule as a doubling rule

Twice the walking sounds like twice the distance, and nothing in the simulation objects.

wrong$$n \times 2 \Rightarrow \bar d \times 2$$
right$$n \times 2 \Rightarrow \bar d \times \sqrt{2} \approx 1.4,\qquad n \times 4 \Rightarrow \bar d \times 2$$
⚠ Claiming a trend from two rows

Two points make a ratio, and a ratio looks like a pattern.

wrong$$\frac{\bar d_2}{\bar d_1} = 2.09 \Rightarrow \text{rule}$$
right$$\text{three or more rows, and the quotient } \frac{\bar d}{\sqrt n} \text{ checked at each}$$
⚠ Comparing means that came from different trial counts

The trial count feels like a detail of how long the program ran, not like part of the measurement.

wrong$$\bar d(100, \texttt{numTrials}{=}10) \text{ vs } \bar d(400, \texttt{numTrials}{=}300)$$
right$$\text{same } \texttt{numTrials} \text{ in every row of one table}$$

12.7Drawing the answer: which picture, and what a frame hides

Pick the picture that answers the question asked, give each walker class its own style, and say what the frame is.

The numbers now say something. A reader takes it in faster from a picture, and can be misled by the wrong picture just as fast.

MethodMethod 12.7: from a simulation to a figure
Conditions
  • Name the question first. Where do the walks end goes to a scatter of end locations; how does the distance grow goes to the mean against the number of steps; what does one walk look like goes to a path, and answers nothing about the mean.

  • Build the lists before drawing anything, and print them. A wrong list draws a wrong picture and no drawing call complains.

  • One style per walker class, handed out in a fixed order so that the same class keeps its style across figures. The lecture files do this with a list of format strings and an index.

  • Finish every panel: title, xlabel, ylabel, legend naming the series in the order they were drawn, and axis([xmin, xmax, ymin, ymax]) when the frame matters.

  • When two panels are compared, give them the same limits. Different limits make different pictures of the same numbers.

$$\boxed{\text{question} \rightarrow \text{lists} \rightarrow \text{one drawing call per series} \rightarrow \text{title, axis names, legend, limits}}$$

Read it as: decide what is being asked, get the numbers into lists and look at them, draw one series per call, then finish the panel so that somebody who did not write it can read it.

Looks like this, but is not

Let each panel choose its own frame, since each should show its own data as clearly as possible:

subplot(1, 2, 1)
plot(x1, y1, 'ro')
axis([-25, 25, -25, 25])
subplot(1, 2, 2)
plot(x2, y2, 'mo')
axis([0, 40, -20, 20])

The two clouds then look alike, both filling their own panel, and the finding disappears into the axis numbers where nobody reads it. The shift is the answer, so the frame has to be shared.

the two lists a scatter needs, built and inspected

A scatter takes a list of x values and a list of y values, so the simulation has to hand back both. This is the data half, with no drawing in it:

import random


class Location(object):

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def move(self, deltaX, deltaY):
        return Location(self.x + deltaX, self.y + deltaY)

    def getX(self):
        return self.x

    def getY(self):
        return self.y


class UsualDrunk(object):

    def takeStep(self):
        stepChoices = [(0, 1), (0, -1), (1, 0), (-1, 0)]
        return random.choice(stepChoices)


def endLocations(numSteps, numTrials):
    """Two lists, the x values and the y values of the final places"""
    d = UsualDrunk()
    xVals = []
    yVals = []
    for t in range(numTrials):
        here = Location(0, 0)
        for s in range(numSteps):
            xDist, yDist = d.takeStep()
            here = here.move(xDist, yDist)
        xVals.append(here.getX())
        yVals.append(here.getY())
    return xVals, yVals


random.seed(0)
xVals, yVals = endLocations(100, 200)
print('walks:', len(xVals))
print('first three x:', xVals[0], xVals[1], xVals[2])
print('first three y:', yVals[0], yVals[1], yVals[2])
print('x from', min(xVals), 'to', max(xVals))
print('y from', min(yVals), 'to', max(yVals))
near = 0
for i in range(len(xVals)):
    if (xVals[i]**2 + yVals[i]**2)**0.5 <= 5:
        near = near + 1
print('ended within 5 of the start:', near)

It prints:

walks: 200
first three x: 6 -4 9
first three y: 2 8 11
x from -21 to 21
y from -19 to 19
ended within 5 of the start: 39

Two hundred walks, ending between 21 west and 21 east, and 39 of them within 5 of the start. Those numbers are what the picture has to agree with.

FindWhat the data looks like before it is drawn.
Given
  • 200 walks of 100 steps, seed 0

  • endLocations returns two lists of equal length

Solution

Check the shape of the data

$$\texttt{len(xVals)} = \texttt{len(yVals)} = 200$$

a drawing call needs two lists of the same length, and the count is printed first for exactly that reason

$$x \in [-21, 21],\quad y \in [-19, 19]$$

so a frame from minus 25 to 25 holds every point with a little room, which is how the limits in the figure were chosen

Sanity check one number against the growth rule

$$39 \text{ of } 200 \text{ within } 5$$

about one walk in five ended close to home, so most walks did not come back

$$\bar d \approx 0.9\sqrt{100} = 9$$

the rule from the previous block says the typical end is around 9 away, and a cloud with a fifth of its points inside 5 is consistent with that

Answer $$\boxed{200 \text{ points},\ x \in [-21, 21],\ y \in [-19, 19]}$$
Check

Independent check on the printed ranges: the largest distance from the origin the cloud can show is bounded by 100, since the walk has 100 unit steps, and the widest coordinate seen is 21. Both are consistent, and a value above 100 would have proved the program wrong.

Print the lists and their lengths before drawing. It is the only check a plotting call will not do for you.

the drawing calls for the scatter, in order

The same simulation with the pyplot calls attached. The drawing is six lines at the end, and five of the six are labelling:

import random
from matplotlib.pyplot import clf, plot, title, xlabel, ylabel, legend, axis


class Location(object):

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def move(self, deltaX, deltaY):
        return Location(self.x + deltaX, self.y + deltaY)

    def getX(self):
        return self.x

    def getY(self):
        return self.y


class Field(object):

    def __init__(self):
        self.drunks = {}

    def addDrunk(self, d, loc):
        self.drunks[d] = loc

    def moveDrunk(self, drunk):
        xDist, yDist = drunk.takeStep()
        self.drunks[drunk] = self.drunks[drunk].move(xDist, yDist)

    def getLoc(self, drunk):
        return self.drunks[drunk]


class UsualDrunk(object):

    def takeStep(self):
        stepChoices = [(0, 1), (0, -1), (1, 0), (-1, 0)]
        return random.choice(stepChoices)


def endLocations(numSteps, numTrials):
    """Two lists: the x values and the y values of the final places"""
    d = UsualDrunk()
    xVals = []
    yVals = []
    for t in range(numTrials):
        f = Field()
        f.addDrunk(d, Location(0, 0))
        for s in range(numSteps):
            f.moveDrunk(d)
        xVals.append(f.getLoc(d).getX())
        yVals.append(f.getLoc(d).getY())
    return xVals, yVals


random.seed(0)
xVals, yVals = endLocations(100, 200)
clf()
plot(xVals, yVals, 'ro', label='end of walk')
plot([0], [0], 'k*', label='start')
title('Where 200 walks of 100 steps ended')
xlabel('steps east')
ylabel('steps north')
legend(loc='upper left')
axis([-40, 40, -40, 40])

There is nothing printed to show here, which is the point: a plotting program's output is the figure. The left panel of the figure above is this program's picture, drawn from the same seeded run.

FindWhat each of the six drawing lines contributes.
Given
  • xVals and yVals from endLocations(100, 200)

  • a red circle per walk, a black star at the start

Solution

Draw the two series

$$\texttt{plot(xVals, yVals, 'ro', label='end of walk')}$$

one call per series, and the label is what the legend will show for it

$$\texttt{plot([0], [0], 'k*', label='start')}$$

the start is one point, so it is passed as two one item lists rather than as two numbers

Finish the panel

$$\texttt{title}, \texttt{xlabel}, \texttt{ylabel}$$

a figure with no axis names cannot be read by anybody who did not write the program

$$\texttt{axis([-40, 40, -40, 40])}$$

a square frame centred on the start, so that a shift in any direction would be visible rather than cropped

Answer $$\boxed{\text{two plot calls, four labelling calls}}$$
Check

Independent check on the chosen limits against the printed data: the widest coordinates in the run were 21 and 19, so a frame of 40 in each direction holds every point, and no point can be lost outside the frame.

Choose the frame from the data you printed, and keep it symmetric when the question is whether the cloud has moved.

one style per class, handed out in a fixed order

With several walker classes in one figure, each needs its own look, and the look has to stay with the class. The lecture files keep a list of format strings and an index; this is the same thing wrapped in a small class:

class StyleIterator(object):
    """Hands out the next plotting style each time it is asked"""

    def __init__(self, styles):
        self.styles = styles
        self.index = 0

    def nextStyle(self):
        style = self.styles[self.index]
        self.index = (self.index + 1) % len(self.styles)
        return style


styles = StyleIterator(['ro-', 'mo--', 'bo:'])
names = ['UsualDrunk', 'EastDrunk', 'ColdDrunk', 'UsualDrunk']
for name in names:
    print(name, styles.nextStyle())

It prints:

UsualDrunk ro-
EastDrunk mo--
ColdDrunk bo:
UsualDrunk ro-

The fourth line is the interesting one: asked a fourth time, the index wraps and the first style comes round again.

FindWhich style each class gets, and what happens on the fourth request.
Given
  • three styles, four requests

  • the index is bumped and wrapped with the remainder operator

Solution

Follow the index

$$0 \rightarrow \texttt{'ro-'},\ 1 \rightarrow \texttt{'mo--'},\ 2 \rightarrow \texttt{'bo:'}$$

the index picks a style and is then bumped, so each call hands out the next one

$$(2 + 1) \bmod 3 = 0$$

the remainder keeps the index inside the list, which is why the fourth request is served rather than raising an IndexError

Read the pairing

$$\text{UsualDrunk} \rightarrow \texttt{'ro-'}$$

red circles with a solid line, and that pairing is what the legend will report

$$\text{fourth request} \rightarrow \texttt{'ro-'}$$

with more classes than styles two of them share a look, which is a reason to have at least as many styles as classes

Answer $$\boxed{\texttt{'ro-'},\ \texttt{'mo--'},\ \texttt{'bo:'},\ \texttt{'ro-'}}$$
Check

Independent check by counting: three styles for four requests must repeat at least one style, and the printed output repeats exactly one, the first. A wrapping bug would have repeated the last style instead, or raised.

A style that travels with the class makes two figures comparable. A style chosen per figure does not.

the frame that hides the finding

Four yearly means that barely move, and the two frames a program could give them:

means = [11.3, 11.9, 12.4, 12.8]
low = min(means)
high = max(means)
print('lowest', low, 'highest', high)
print('span of the data', format(high - low, '.1f'))
print('span of a frame that starts at zero', format(high - 0, '.1f'))
print('rise as a share of the low value',
      format(100 * (high - low) / low, '.1f'), 'percent')

It prints:

lowest 11.3 highest 12.8
span of the data 1.5
span of a frame that starts at zero 12.8
rise as a share of the low value 13.3 percent

The data spans 1.5. A frame that starts at zero is 12.8 tall, so the whole story occupies about a tenth of the panel and looks flat. A frame from 11.3 to 12.8 fills the panel with that same story and looks like a steep climb.

FindHow much of the panel the data occupies under each frame.
Given
  • means 11.3, 11.9, 12.4 and 12.8

  • two candidate frames, one from zero and one hugging the data

Solution

Measure the data against the frame

$$\text{span of the data} = 12.8 - 11.3 = 1.5$$

the actual movement, in the units of the measurement

$$\frac{1.5}{12.8} \approx 0.12$$

under a frame that starts at zero the data uses about an eighth of the height, so the line looks flat

Say the rise in a way that does not depend on the frame

$$\frac{1.5}{11.3} \times 100 = 13.3\%$$

a share of the starting value, which is a number rather than a picture and cannot be restyled

$$\text{report both}$$

give the frame you chose and the rise as a number, and the reader can check one against the other

Answer $$\boxed{\text{span } 1.5,\ \text{rise } 13.3\%,\ \text{frame chosen on purpose}}$$
Check

Independent check that the two frames describe the same data: the printed lowest and highest values are 11.3 and 12.8 in both cases, so nothing about the measurement changed between the two pictures. Only the mapping from numbers to pixels did.

State the frame and give one frame free number beside the picture. A figure that needs its own axis limits to make its point is making the axis limits' point.

Checkpoint
§12.7 — the picture that answers the drift question

A report has to show whether the east biased walker drifts away from the start or merely wanders like the fair one. Four figures are on offer and all four are drawable from the data already collected.

Find(a) Which figure answers the question asked?
Givenend locations of 200 fair walks and 200 biased walks, 100 steps each
Hint 1/4

Say out loud what would count as an answer to the question, then ask which figure shows that and only that.

Hint 2/4

A comparison needs both groups drawn on one scale. Where the clouds sit is the finding, so anything that moves or hides the position of a cloud removes the finding.

Hint 3/4

The data is two sets of end locations from the same walk length, so both sets can be drawn as clouds in the same units, and the question is about where each cloud sits.

Hint 4/4

The two clouds, side by side, with the same limits in both panels.

Show solution

Name what would count as an answer

$$\text{answer} = \text{where each cloud sits}$$

drift is a position, not a rate and not a single trajectory

Test the four candidates

$$\text{shared limits} \rightarrow \text{positions comparable}$$

the same units and the same frame, so a shift is visible as a shift

$$\text{own limits} \rightarrow \text{positions rescaled}$$

each cloud fills its panel and the shift moves into the axis numbers

$$\text{one path} \rightarrow \text{one sample}$$

a single walk cannot separate drift from luck

$$\text{mean curve} \rightarrow \text{growth}$$

it answers a different question, and answers it well

Answer $$\boxed{\text{two scatters, one frame}}$$
Check

Independent check against the figure in this block: drawn with shared limits, the biased cloud is visibly to the right of the fair one and its mean east position is 20.3, so the picture and the number agree.

Write down what would count as an answer before choosing a chart type. The chart type follows from it.

⚠ Comparing two panels that have different axis limits

Each panel looks best when its own data fills it, and the drawing library does that by default.

wrong$$\texttt{axis([-25,25,-25,25])} \text{ and } \texttt{axis([0,40,-20,20])}$$
right$$\texttt{axis([-25,40,-25,25])} \text{ in both panels}$$
⚠ Naming the series in the legend in the wrong order

The legend is written last, by which time the order of the drawing calls has been forgotten.

wrong$$\texttt{plot(fair)}, \texttt{plot(biased)}, \texttt{legend(['biased', 'fair'])}$$
right$$\texttt{plot(..., label='fair')} \text{ per call, then } \texttt{legend()}$$
⚠ Drawing one walk to answer a question about many

A single path is the most interesting looking picture in the whole section.

wrong$$\text{one path} \Rightarrow \text{claim about } \bar d$$
right$$\text{200 end locations, or the mean against } n$$
From a written specification to a working simulation

Any exam part that describes a process and asks for the numbers it produces. It is also the order to write the code in, which is not the order the specification lists things in.

  1. Say what one trial measures, in one sentence

    Here it is the distance from where this walk started to where it ended, over numSteps steps. That sentence decides where the reset goes and what the report means. Write it down before any code.

  2. Write the pieces that do not know about each other

    A place that can offer a new place and measure distances, a walker that can offer a step, and a container that remembers who is where. Each one needs nothing from the other two except the method names.

  3. Write the one function that runs the process once

    Read the starting place, loop numSteps times, measure. Three lines, and their order is the whole content of the function.

  4. Test it on the cases whose answers you know

    Zero steps must give 0.0 and one step must give 1.0. Drive it with a walker whose steps are scripted, so a wrong answer is a bug and not bad luck. This step costs two minutes and catches the swapped lines, the off by one and the returned place.

  5. Wrap it in trials, with the reset inside the loop

    A fresh field, or an addDrunk call, at the top of every trial. Collect the distances in a list rather than summing as you go, so the ends are still available at the end.

  6. Report three numbers, then draw one picture

    Mean, largest, smallest, each through a format call. Then the single figure that answers the question that was asked, labelled, with its frame chosen from the data you printed.

Where it goes wrong
  • Step 1 skipped, so the reset is argued about instead of derived.

  • Step 4 skipped, which is how a function that always returns 0.0 gets reported as a result.

  • Step 5 with the seed call inside the loop, which turns numTrials identical walks into a very stable wrong answer.

  • Step 6 with a mean and no range, which hides that the answers ran from 2.0 to 15.3.

Reading a growth rule off a table of means

A table of means at several walk lengths, and a question asking what happens when the walk gets longer, or whether a walker is biased.

  1. Check that the rows are comparable

    Same walker, same trial count, only the number of steps changing. A row with a different numTrials belongs to a different table.

  2. Make the step counts a constant factor apart

    25, 100, 400, 1600 is four times per row. Two rows cannot tell you a rule, so aim for at least three gaps.

  3. Take the ratio of neighbouring means

    If the ratios sit near 2 while the steps go up by 4, the mean is following the square root. If they sit near 4, the mean is following the steps themselves and the walker has a drift.

  4. Divide the candidate law out and look for flatness

    Compute the mean over the square root of the steps for every row. A column that stays put is the proof; a column that climbs or falls says the law is the wrong one. On this page the column was 0.87 to 0.92 across a 64 fold range.

Where it goes wrong
  • Ratios taken between rows that are not the same factor apart, so the numbers mean nothing.

  • A rule declared from two rows.

  • The flatness column computed with the rounded means rather than the real ones, which moves the last digit and starts an argument about nothing.

move returns a new place: two names stay independent

The class from this section, with three names holding one place, and one move:

class Location(object):

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def move(self, deltaX, deltaY):
        return Location(self.x + deltaX, self.y + deltaY)

    def __repr__(self):
        return '<' + str(self.x) + ', ' + str(self.y) + '>'


origin = Location(0, 0)
homer = origin
marge = origin
homer = homer.move(1, 0)
print(homer, marge, origin)
print(homer is marge)

It prints:

<1, 0> <0, 0> <0, 0>
False
FindWhat the three names show after one move.
Given
  • move returns Location(self.x+dx, self.y+dy)

  • homer, marge and origin all bound to one object

Solution

Apply the move

$$\texttt{homer = homer.move(1, 0)}$$

a new object is built and the name homer is rebound to it

$$\texttt{marge}, \texttt{origin} \text{ unchanged}$$

neither name was assigned to, and nothing was written into the object they hold

Answer $$\boxed{\texttt{<1, 0> <0, 0> <0, 0>}\;/\;\texttt{False}}$$
Check

Independent check: the identity test prints False, which can only happen if a second object exists.

move writes into the place: one change, three names

The same three names, with a move that assigns to self and returns self:

class BadLocation(object):

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def move(self, deltaX, deltaY):
        self.x = self.x + deltaX
        self.y = self.y + deltaY
        return self

    def __repr__(self):
        return '<' + str(self.x) + ', ' + str(self.y) + '>'


origin = BadLocation(0, 0)
homer = origin
marge = origin
homer = homer.move(1, 0)
print(homer, marge, origin)
print(homer is marge)

It prints:

<1, 0> <1, 0> <1, 0>
True
FindWhat the three names show after one move.
Given
  • move assigns to self.x and self.y, then returns self

  • homer, marge and origin all bound to one object

Solution

Apply the move

$$\texttt{self.x = self.x + deltaX}$$

the object itself is changed, so every name that holds it shows the change

$$\texttt{return self}$$

the assignment rebinds homer to the same object it already held, which is why nothing separates

Answer $$\boxed{\texttt{<1, 0> <1, 0> <1, 0>}\;/\;\texttt{True}}$$
Check

Independent check: the identity test prints True, so no second object was ever built.

The two classes differ by two lines and agree on every printed character until a second name holds the same place.

How to tell them apart

Ask what the method returns. A method that returns a new object of its own class leaves its caller's data alone; a method that returns self almost always wrote into it. In this section the walk function saves the starting place and measures from it at the end, so a move that writes into places makes every distance 0.0.

Scaffolding comes off
The common skeleton
  1. Write down what one trial measures, in one sentence.

  2. Put the walker on a known starting place and keep that place.

  3. Apply the steps one at a time, each to the place the last one produced.

  4. Measure from the kept starting place to the place you ended on.

  5. Report the number through a format call, and say how many steps produced it.

1 · fully worked

a scripted walk of six steps, measured

The whole skeleton on one walk. The steps are given, so nothing here depends on chance:

class Location(object):

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def move(self, deltaX, deltaY):
        return Location(self.x + deltaX, self.y + deltaY)

    def distFrom(self, other):
        xDist = self.x - other.x
        yDist = self.y - other.y
        return (xDist**2 + yDist**2)**0.5

    def __repr__(self):
        return '<' + str(self.x) + ', ' + str(self.y) + '>'


steps = [(0, 1), (1, 0), (0, 1), (1, 0), (0, -1), (1, 0)]
start = Location(0, 0)
here = start
for step in steps:
    xDist, yDist = step
    here = here.move(xDist, yDist)
print(here)
print(format(start.distFrom(here), '.2f'))

It prints:

<3, 1>
3.16
FindWhere the walk ends and how far that is from the start.
Given
  • steps [(0,1), (1,0), (0,1), (1,0), (0,-1), (1,0)]

  • start at Location(0, 0)

Solution

Keep the starting place

$$\texttt{start = Location(0, 0)}$$

the name start is never reassigned, which is what makes the measurement at the end possible

$$\texttt{here = start}$$

one object with two names for now, and that is safe because move does not write into places

Apply the six steps

$$\Delta x = 0+1+0+1+0+1 = 3$$

three of the six steps move east and none moves west

$$\Delta y = 1+0+1+0-1+0 = 1$$

two steps north and one south, so one north in total

$$\texttt{here} = \texttt{<3, 1>}$$

each move is applied to the place the previous move returned

Measure and report

$$d = \sqrt{3^2 + 1^2} = \sqrt{10}$$

the two differences are 3 and 1, measured from the kept start

$$\sqrt{10} = 3.1622\ldots \rightarrow 3.16$$

printed through format with two decimals, as every distance shown to a reader here is

Answer $$\boxed{\texttt{<3, 1>},\ d = 3.16}$$
Check

Independent check by parity: six steps, and the walker is 4 grid units from home along the grid, so both are even, as they must be. Also 3.16 lies between 3 and 4, which is where the diagonal of a 3 by 1 rectangle has to lie.

Six steps, distance 3.16. The number of steps is an upper bound on the distance and nothing more.

2 · you write the reasoning

Easier than the one above, and the reasons are gone. Three scripted steps, east, north, east:

class Location(object):

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def move(self, deltaX, deltaY):
        return Location(self.x + deltaX, self.y + deltaY)

    def distFrom(self, other):
        xDist = self.x - other.x
        yDist = self.y - other.y
        return (xDist**2 + yDist**2)**0.5

    def __repr__(self):
        return '<' + str(self.x) + ', ' + str(self.y) + '>'


steps = [(1, 0), (0, 1), (1, 0)]
start = Location(0, 0)
here = start
for step in steps:
    xDist, yDist = step
    here = here.move(xDist, yDist)
print(here)
print(format(start.distFrom(here), '.2f'))

Work out the two printed lines, and write your own reason next to each step before opening the model reasons.

  1. reasoning

    The starting place is kept in its own name, because the measurement at the end needs it.

  2. reasoning

    Two of the three steps have a 1 in the x part, and no step has a negative x part.

  3. reasoning

    One step has a 1 in the y part and nothing cancels it.

  4. reasoning

    Each move is applied to the place the previous move returned, so the pairs add up.

  5. reasoning

    The distance is the root of the sum of the squared differences, printed with two decimals through format.

3 · find the buried error

Harder than the two above, and already written. A student was asked for a function that runs numTrials walks of numSteps steps with a fair walker and prints the mean distance.

It runs, and it prints a plausible number. Two of its four marked steps are wrong:

import random

def simulate(numSteps, numTrials):
    d = UsualDrunk()
    origin = Location(0, 0)
    f = Field()
    f.addDrunk(d, origin)
    total = 0
    for t in range(numTrials):
        random.seed(0)
        total = total + walk(f, d, numSteps)
    print(format(total / numSteps, '.2f'))
the two buried errors (2)
⚠ step 3

The seed call is inside the trial loop, so every trial replays the same walk. The mean of numTrials identical distances is that one distance, and it looks stable because it is the same number every time.

Repeatability was asked for, and the seed call looks safest right next to the code it is making repeatable.

right

Move it above the loop, or out of the function entirely and into the script that calls it.

⚠ step 4

The total is divided by numSteps instead of by numTrials. With 100 trials of 1000 steps the reported mean is a hundred times too small.

Both numbers are parameters of the function and both are large, and the phrase how much walking was done points at the wrong one.

4 · the bare problem
§12.5 — the whole simulation, from a spec

No scaffolding this time. A specification of the kind an exam gives, and the three numbers it wants printed.

Find
  1. (a) Write the whole program: the three classes, the walk function, a simWalks that returns the list of distances, and a report function. Print one line per walk length.

  2. (b) Say which line makes each trial independent of the one before it.

Given
  • a fair walker with the four unit steps

  • walks of 10, 100 and 1000 steps, 100 trials each

  • for each length: mean, largest and smallest distance, two decimals

  • the run must be repeatable, and the seed is 42

Hint 1/4

Write down what one trial measures before writing any code, then list the pieces the specification names: three classes, walk, simWalks, report.

Hint 2/4

One walk is start, loop, measure. One trial is a reset then a walk. The report is mean, max and min over the list of distances, each through a format call.

Hint 3/4

The specification again: 10, 100 and 1000 steps, 100 trials each, seed 42 once at the top, and two decimals on all three reported numbers.

Hint 4/4

The program below prints three lines, one per walk length, and the reset is the addDrunk call at the top of the trial loop.

Show solution

Say what one trial measures

$$\text{distance from the origin after numSteps steps}$$

this sentence is what puts the reset inside the trial loop rather than above it

Write walk, then the trial loop

$$\texttt{start = f.getLoc(d)} \text{ first}$$

the measurement needs the starting place, and after the loop it is gone

$$\texttt{f = Field()}, \texttt{f.addDrunk(d, origin)}$$

a fresh field per trial is the strongest reset: there is nothing left to carry over

Report and check

$$\text{mean } 2.85,\ 8.41,\ 28.78$$

the three printed means, each from 100 trials

$$\frac{8.41}{2.85} = 2.95,\quad \frac{28.78}{8.41} = 3.42$$

ten times the steps should multiply the mean by about 3.16, and both ratios sit near it, which is the check that the program measures what it claims

Answer $$\boxed{\text{three report lines, reset inside the trial loop}}$$
Check

Independent check against the growth rule: 0.9 times the square root of 1000 is 28.5, and the program reported 28.78 for that row without ever being told the rule.

An exam answer that prints its own check is worth more than one that prints only the answer.

Full exam-style question

report a fair walk at three lengths, in exam formatexam format

An exam part in the shape the archived paper uses: a described function and a script that runs it. What is asked for:

  • a report(numSteps, numTrials, drunkClass) that runs the trials
  • it prints the walk length, the mean, the largest and the smallest
  • it returns the mean
  • a script calling it for 10, 100 and 1000 steps, 50 trials, seeded

The three classes are the ones from this section, unchanged:

import random


class Location(object):

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def move(self, deltaX, deltaY):
        return Location(self.x + deltaX, self.y + deltaY)

    def getX(self):
        return self.x

    def getY(self):
        return self.y

    def distFrom(self, other):
        xDist = self.x - other.getX()
        yDist = self.y - other.getY()
        return (xDist**2 + yDist**2)**0.5


class Field(object):

    def __init__(self):
        self.drunks = {}

    def addDrunk(self, d, loc):
        self.drunks[d] = loc

    def moveDrunk(self, drunk):
        xDist, yDist = drunk.takeStep()
        self.drunks[drunk] = self.drunks[drunk].move(xDist, yDist)

    def getLoc(self, drunk):
        return self.drunks[drunk]


class UsualDrunk(object):

    def takeStep(self):
        stepChoices = [(0, 1), (0, -1), (1, 0), (-1, 0)]
        return random.choice(stepChoices)


def walk(f, d, numSteps):
    start = f.getLoc(d)
    for s in range(numSteps):
        f.moveDrunk(d)
    return start.distFrom(f.getLoc(d))


def simWalks(numSteps, numTrials, drunkClass):
    d = drunkClass()
    distances = []
    for t in range(numTrials):
        f = Field()
        f.addDrunk(d, Location(0, 0))
        distances.append(walk(f, d, numSteps))
    return distances


def report(numSteps, numTrials, drunkClass):
    """Prints one line per walk length: mean, max and min distance"""
    distances = simWalks(numSteps, numTrials, drunkClass)
    mean = sum(distances) / len(distances)
    print(drunkClass.__name__, 'walk of', numSteps, 'steps')
    print('  mean', format(mean, '.2f'), 'max', format(max(distances), '.2f'),
          'min', format(min(distances), '.2f'))
    return mean


random.seed(17)
for numSteps in [10, 100, 1000]:
    report(numSteps, 50, UsualDrunk)

Sample Run:

UsualDrunk walk of 10 steps
  mean 3.14 max 7.07 min 0.00
UsualDrunk walk of 100 steps
  mean 9.35 max 19.70 min 1.41
UsualDrunk walk of 1000 steps
  mean 29.89 max 65.60 min 1.41
FindThe program, and the reading of its three lines of output.
Given
  • a fair walker with the four unit steps

  • walks of 10, 100 and 1000 steps, 50 trials each, seed 17

  • print the length, mean, max and min; return the mean

Solution

Read the specification into pieces

$$\texttt{report(numSteps, numTrials, drunkClass)}$$

the walker class is a parameter, so the same report can be asked about a biased walker without being rewritten

$$\text{prints four things, returns one}$$

printing and returning are different jobs, and an exam that asks for both wants both

Check the three reported means against the growth rule

$$3.14,\ 9.35,\ 29.89$$

ten times the steps per row, so the means should be about 3.16 times apart

$$\frac{9.35}{3.14} = 2.98,\quad \frac{29.89}{9.35} = 3.20$$

both near 3.16, so the report agrees with the rule measured earlier in this section

Read the ends, not only the means

$$\min = 0.00 \text{ at 10 steps}$$

some of the fifty short walks came home, which is likely at 10 steps and rare at 1000

$$\max = 65.60 \text{ at 1000 steps}$$

more than twice the mean of that row, which is the kind of number a mean on its own hides

Answer $$\boxed{3.14,\ 9.35,\ 29.89 \text{ with their ends reported}}$$
Check

Independent check on the middle row: 0.9 times the square root of 100 is 9.0, and the reported mean is 9.35, which is inside the wobble of 50 trials. The rule and this run were produced by different programs with different seeds.

A report function with the walker class as a parameter is the answer to two exam questions instead of one.

Practice

A · concept 4 questions
1§12.2 — what a move leaves behind

A claim about the place a move was called on, of the kind a true or false question opens with.

Find(a) True or false: after the second line, start is one step north of where it was.
Given
  • start = Location(0, 0)

  • here = start.move(0, 1)

Hint 1/4

Decide how many Location objects exist after the two lines before deciding what either name shows.

Hint 2/4

move returns a new Location built from the coordinates of this one. It assigns to nothing, so no existing object changes.

Hint 3/4

Here the second line binds the new place to here, and start is not on the left of any assignment after the first line.

Hint 4/4

The claim is false: start is still the origin, and here is the place one north of it.

Show solution

Count and read

$$\text{two objects}$$

one from the constructor, one from the move

$$\texttt{start} \rightarrow \texttt{<0, 0>}$$

it was never reassigned and nothing writes into a Location

Answer $$\boxed{\text{False}}$$
Check

Independent check: here is start prints False, which is only possible if two objects exist, and the first of them is unchanged.

Ask what a method returns before asking what it changed.

2§12.5 — where a seed call belongs

A claim about making a simulation repeatable, of the kind that sounds responsible.

Find(a) True or false: seeding at the top of every trial makes the reported mean more reliable.
Given
  • a trial loop that runs 100 walks of 100 steps

  • random.seed(0) written as the first line inside the loop

Hint 1/4

Ask what the second trial does differently from the first one under this arrangement.

Hint 2/4

A seed call repositions the stream of random numbers. The same seed gives the same numbers, so the same walk.

Hint 3/4

Here every trial reseeds with 0, so all 100 trials walk the same 100 steps and the list of distances holds one value 100 times.

Hint 4/4

The claim is false: the mean becomes a single trial's distance, reported with more confidence than a single trial deserves.

Show solution

Follow the trials

$$\text{trial 1: stream from seed 0}$$

the first walk uses the first 100 numbers of that stream

$$\text{trial 2: stream from seed 0 again}$$

the same 100 numbers, so the same steps and the same distance

Read the report

$$\text{mean of } n \text{ copies of } d = d$$

nothing is averaged over, so the trial count buys nothing

$$\max = \min = d$$

and the range printed beside the mean is zero, which is the visible symptom

Answer $$\boxed{\text{False}}$$
Check

Independent check available on any run: with the seed inside the loop the largest and smallest distances come out equal, and a hundred fair walks of a hundred steps never agree like that.

A range of zero over many trials is a bug report, not a result.

3§12.6 — nine times the steps

A fair walker has already been measured at 100 steps, where the mean distance came out about 9. The question is about a longer walk of the same kind.

Find(a) About what is the mean distance after 900 steps?
Given
  • fair walker, four unit steps

  • mean distance at 100 steps is about 9

Hint 1/4

Ask what the number of steps was multiplied by, and what the growth rule does with that factor.

Hint 2/4

The mean of a fair walk grows like the square root of the number of steps, so the mean is multiplied by the square root of the factor the steps were multiplied by.

Hint 3/4

Here the steps go from 100 to 900, so the factor is 9, and the mean at 100 steps was about 9.

Hint 4/4

The square root of 9 is 3, so the mean is about three times larger.

Show solution

Find the factor

$$\frac{900}{100} = 9$$

the steps were multiplied by nine

$$\sqrt{9} = 3$$

the mean is multiplied by the square root of that factor

Apply it

$$9 \times 3 = 27$$

the mean at 100 steps, multiplied by three

$$0.9\sqrt{900} = 27$$

the same answer from the rule written the other way, which is the check

Answer $$\boxed{\approx 27}$$
Check

Independent check against the measured table: 400 steps gave 18.49 and 1600 gave 36.91, so 900 steps should land between those two, and 27 does.

Multiply the steps by a factor, multiply the mean by its square root. That is the whole rule.

4§12.3 — two walkers on one place

A claim about sharing a starting place between two walkers, which is what the lecture programs actually do.

Find(a) True or false: the two walkers can be added on the same Location object without their walks interfering.
Given
  • origin = Location(0, 0)

  • f.addDrunk(homer, origin) and f.addDrunk(marge, origin)

Hint 1/4

Ask what a move does to the place it was called on, and then ask what the field stores.

Hint 2/4

move returns a new place and writes into nothing, and moveDrunk assigns that new place to one key of the dictionary.

Hint 3/4

Here both keys start out holding the same object, and moving Homer assigns a new place to Homer's key only.

Hint 4/4

The claim is true, and it is true because of how move is written, not because of how the field is written.

Show solution

Follow one move

$$\texttt{move} \rightarrow \text{new object}$$

the shared place is read and never written to

$$\texttt{self.drunks[drunk] = ...}$$

one key is rebound, and the other key is not mentioned

Answer $$\boxed{\text{True}}$$
Check

Independent check: printing the whole dictionary after one move shows one entry changed and one unchanged, which is measured earlier in this section.

Safe sharing is a property of the methods, not of the container.

B · computation 5 questions
1§12.1 — three classes, one seeded step

The , a random walker and a scripted walker in one file. The scripted one is asked for three steps and the random one for one, after a seed call.

Find(a) Write exactly what this program prints, all three lines.
Given
import random


class Location(object):

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def move(self, deltaX, deltaY):
        return Location(self.x + deltaX, self.y + deltaY)

    def getX(self):
        return self.x

    def getY(self):
        return self.y

    def distFrom(self, other):
        xDist = self.x - other.getX()
        yDist = self.y - other.getY()
        return (xDist**2 + yDist**2)**0.5

    def __repr__(self):
        return '<' + str(self.x) + ', ' + str(self.y) + '>'


class Drunk(object):
    """Base class. It knows a name and nothing about walking."""

    def __init__(self, name=None):
        self.name = name

    def __repr__(self):
        if self.name != None:
            return self.name
        return 'Anonymous'


class UsualDrunk(Drunk):
    """Picks one of the four unit steps, each equally likely."""

    def takeStep(self):
        stepChoices = [(0, 1), (0, -1), (1, 0), (-1, 0)]
        return random.choice(stepChoices)


class ScriptedDrunk(Drunk):
    """Takes the steps written on a list, in order. Used for testing."""

    def __init__(self, name, steps):
        Drunk.__init__(self, name)
        self.steps = steps
        self.next = 0

    def takeStep(self):
        step = self.steps[self.next % len(self.steps)]
        self.next = self.next + 1
        return step


homer = UsualDrunk('Homer')
tester = ScriptedDrunk('Tester', [(1, 0), (0, 1)])
print(homer, tester)
print(tester.takeStep(), tester.takeStep(), tester.takeStep())
random.seed(5)
print(homer.takeStep())
IPython console
Hint 1/4

Take the printed lines one at a time, and for the first one ask what each object's printed form comes from.

Hint 2/4

The base class defines how a drunk prints. A scripted walker hands out its steps in order and wraps round with the remainder operator. A seeded choice is fixed once the seed is fixed.

Hint 3/4

The script here is [(1, 0), (0, 1)], asked three times, and the random walker is asked once after random.seed(5).

Hint 4/4

The two names, then the three scripted steps with the first one repeated, then the seeded step.

Show solution

Print the objects

$$\texttt{Homer Tester}$$

the subclasses do not define a repr, so the inherited one runs and it returns the name

Ask the scripted walker three times

$$\texttt{(1, 0) (0, 1)}$$

the first two calls hand out the list in order

$$(2 \bmod 2) = 0 \rightarrow \texttt{(1, 0)}$$

the third call wraps to the front of the list

Ask the random walker once

$$\texttt{(1, 0)}$$

the first item of the stream from seed 5, which happens to be east

Answer $$\boxed{\texttt{Homer Tester}\;/\;\texttt{(1, 0) (0, 1) (1, 0)}\;/\;\texttt{(1, 0)}}$$
Check

Independent check on the middle line: the scripted list has two items and three were requested, so exactly one item must appear twice, and the repeated one must be the first.

A scripted walker is a test instrument. Its wrapping behaviour is part of the instrument.

2§12.2 — a chain of moves and one identity test

Three places built from one, then a distance and two identity tests.

Find(a) Write exactly what this program prints, all three lines.
Given
class Location(object):

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def move(self, deltaX, deltaY):
        return Location(self.x + deltaX, self.y + deltaY)

    def getX(self):
        return self.x

    def getY(self):
        return self.y

    def distFrom(self, other):
        xDist = self.x - other.getX()
        yDist = self.y - other.getY()
        return (xDist**2 + yDist**2)**0.5

    def __repr__(self):
        return '<' + str(self.x) + ', ' + str(self.y) + '>'


a = Location(2, 3)
b = a.move(1, 1)
c = b.move(-1, 0)
print(a, b, c)
print(a.distFrom(c))
print(b is c, a is a.move(0, 0))
IPython console
Hint 1/4

Build the three places on paper first, then answer the distance and the two identity questions from them.

Hint 2/4

Each move returns a new place built from the one it was called on, so a chain of moves leaves every earlier place intact. An identity test asks whether two names hold the same object.

Hint 3/4

Here a is <2, 3>, b is one east and one north of a, and c is one west of b. The last line also moves a by zero.

Hint 4/4

The three places, then the distance from a to c, then two answers that are both False.

Show solution

Build the three places

$$a = \texttt{<2, 3>},\ b = \texttt{<3, 4>}$$

the move adds one to each coordinate of a

$$c = \texttt{<2, 4>}$$

one west of b, which puts it directly north of a

Measure and test identity

$$d = \sqrt{0^2 + 1^2} = 1.0$$

a and c share an x coordinate, so only the y difference is left

$$\texttt{b is c} \rightarrow \texttt{False}$$

different objects and different coordinates

$$\texttt{a is a.move(0, 0)} \rightarrow \texttt{False}$$

same coordinates, different object, because move always builds

Answer $$\boxed{\texttt{<2, 3> <3, 4> <2, 4>}\;/\;1.0\;/\;\texttt{False False}}$$
Check

Independent check on the distance: c is directly north of a by one unit on the grid, so the straight line distance has to be exactly 1.0 with no rounding.

Same coordinates is not the same object. In this design that distinction is what keeps walkers apart.

3§12.5 — trials outside, steps inside

A counter is bumped once per step, with the trial loop outside and the step loop inside. The same function is called with the two numbers swapped.

Find(a) Write exactly what this program prints, both lines.
Given
class Counter(object):

    def __init__(self):
        self.n = 0

    def bump(self):
        self.n = self.n + 1
        return self.n


def trials(numTrials, numSteps):
    c = Counter()
    results = []
    for t in range(numTrials):
        for s in range(numSteps):
            c.bump()
        results.append(c.n)
    return results


print(trials(3, 4))
print(trials(4, 3))
IPython console
Hint 1/4

Work out what the counter holds at the end of each trial, and note that it is never reset between trials.

Hint 2/4

The inner loop runs numSteps times per trial and the counter keeps its value across trials, so the value appended at the end of trial t is t times numSteps.

Hint 3/4

The calls are trials(3, 4) and then trials(4, 3), and each call builds a fresh counter inside the function.

Hint 4/4

Three multiples of four, then four multiples of three.

Show solution

Run the first call

$$\text{trial 1} \rightarrow 4$$

four bumps, and the counter starts at zero

$$\text{trials 2, 3} \rightarrow 8, 12$$

the counter is not reset, so each trial adds four more

Run the second call

$$3, 6, 9, 12$$

four trials of three steps, so the entries step by three and there are four of them

$$3 \times 4 = 4 \times 3$$

the totals match, which is why the last entry is the same in both lists

Answer $$\boxed{\texttt{[4, 8, 12]}\;/\;\texttt{[3, 6, 9, 12]}}$$
Check

Independent check by length: the first list must have as many entries as there are trials, 3 and 4 respectively, and it does. The final entry equals the product in both cases.

numTrials sets how many numbers you get. numSteps sets how large each one is.

4§12.5 — a mean that does not print tidily

Six distances are already measured. The program takes their mean and prints it three ways, then prints the spread.

Find(a) Write exactly what this program prints, all four lines.
Given
distances = [7.2, 13.0, 4.5, 21.1, 9.8, 5.6]
total = 0
for dist in distances:
    total = total + dist
mean = total / len(distances)
print(mean)
print(format(mean, '.2f'))
print(round(mean, 1))
print(max(distances) - min(distances))
IPython console
Hint 1/4

Compute the sum first, then decide what each of the four prints does to it.

Hint 2/4

Dividing two floats can leave a value that has no exact binary form, and an unformatted print shows every digit Python holds. format and round both cut it, in different ways.

Hint 3/4

The distances again: 7.2, 13.0, 4.5, 21.1, 9.8 and 5.6. Their sum is 61.2 and there are six of them, and the last print is a subtraction of two of them.

Hint 4/4

A long float, then two decimals, then one decimal, then the difference of the largest and the smallest.

Show solution

Sum and divide

$$\text{sum} = 61.2,\quad n = 6$$

the loop adds the six values in the order written

$$\frac{61.2}{6} = 10.2$$

on paper, but in binary the result carries a tail and the unformatted print shows it

Print it three ways, then the spread

$$\texttt{format(mean, '.2f')} \rightarrow \texttt{10.20}$$

a string with exactly two decimals, so the zero stays

$$\texttt{round(mean, 1)} \rightarrow 10.2$$

a number, printed as a number, so no trailing zero

$$21.1 - 4.5 = 16.6$$

the spread, and here the subtraction happens to come out exactly

Answer $$\boxed{10.200000000000001\;/\;\texttt{10.20}\;/\;10.2\;/\;16.6}$$
Check

Independent check on the mean: it must lie between 4.5 and 21.1, and 10.2 does. The tail of digits is far below the precision of the measurements, which is why it is a printing question and not a data question.

Report a mean through format. Keep the unformatted value for any arithmetic that follows.

5§12.6 — three runs turned into a growth table

Three sets of trial distances at three walk lengths, and the two columns a growth question needs.

Find(a) Write exactly what this program prints, all three lines.
Given
def meanOf(values):
    total = 0
    for v in values:
        total = total + v
    return total / len(values)


runs = [[3.2, 3.9, 3.7], [6.9, 7.4, 7.3], [14.0, 14.9, 14.0]]
stepCounts = [16, 64, 256]
for i in range(len(runs)):
    mean = meanOf(runs[i])
    print(stepCounts[i], format(mean, '.1f'), format(mean / stepCounts[i]**0.5, '.2f'))
IPython console
Hint 1/4

Work out the three means first, then the divisor each one is compared against.

Hint 2/4

The second column is the mean of that run, and the third is the mean divided by the square root of the step count. A flat third column is the signature of .

Hint 3/4

The runs again: three distances at 16 steps, three at 64 and three at 256, and the square roots of those step counts are 4, 8 and 16.

Hint 4/4

Three lines, each with the step count, the mean to one decimal and the ratio to two.

Show solution

Average the three runs

$$\frac{10.8}{3} = 3.6,\quad \frac{21.6}{3} = 7.2$$

each run holds three distances, so each mean is a sum over three

$$\frac{42.9}{3} = 14.3$$

the third run, and all three are printed with one decimal

Divide by the square roots

$$\frac{3.6}{4} = 0.90,\quad \frac{7.2}{8} = 0.90$$

the roots of 16 and 64 are 4 and 8

$$\frac{14.3}{16} = 0.894 \rightarrow 0.89$$

the root of 256 is 16, and two decimals cut the third digit

Answer $$\boxed{\texttt{16 3.6 0.90}\;/\;\texttt{64 7.2 0.90}\;/\;\texttt{256 14.3 0.89}}$$
Check

Independent check across the rows: the step count is multiplied by four from row to row, so the means should roughly double, and 3.6, 7.2, 14.3 does exactly that. The two columns agree with each other.

Choose step counts whose square roots are whole numbers when you want a table you can check without a calculator.

C · exam level 5 questions
1§12.4 — the walk function and its three tests

An exam part that asks for the function and for the evidence that it works. The evidence is the part most answers leave out.

Find
  1. (a) Write walk(f, d, numSteps) with a docstring in the shape the course uses.

  2. (b) Write a checkWalk(numSteps, steps) that runs one scripted walk and prints the step count, the ending place and the distance to two decimals.

  3. (c) Call it for 0 steps, for 1 step, and for the six step script east, north, east, north, south, east, and say what each line of output rules out.

Given
  • a Location class with move, getX, getY and distFrom

  • a Field class with addDrunk, moveDrunk and getLoc

  • a walker whose steps are given as a list, so that answers are predictable

Hint 1/4

Write down the three lines of the walk function before anything else, and then ask what a test would have to print to catch each of the three ways those lines go wrong.

Hint 2/4

The function reads the starting place, loops numSteps times and measures from the saved place. A scripted walker makes the expected answer computable by hand, so a wrong number is a bug rather than bad luck.

Hint 3/4

The three calls again: 0 steps, 1 step, and the script [(0, 1), (1, 0), (0, 1), (1, 0), (0, -1), (1, 0)]. Their expected distances are 0.00, 1.00 and the root of 10.

Hint 4/4

The program below prints three lines, and the first two of them are the ones that catch the common bugs.

Show solution

Write walk

$$\texttt{start = f.getLoc(d)}$$

first, because after the loop this place is gone

$$\texttt{return start.distFrom(f.getLoc(d))}$$

a number, not a place, and measured from the saved place

Make the answers predictable

$$\text{scripted steps}$$

the expected distance can be computed by hand, which is what makes a failed test mean something

$$0 \rightarrow 0.00,\ 1 \rightarrow 1.00$$

the two cheapest cases, and both are forced

Check the six step case by hand

$$\Delta x = 3,\ \Delta y = 1$$

three easts and one net north out of the six steps

$$\sqrt{10} = 3.16$$

which is what the third line printed

Answer $$\boxed{0.00,\ 1.00,\ 3.16}$$
Check

Independent check on the third line: the six step script ends at <3, 1>, printed by the program itself, and the distance to <3, 1> from the origin is the root of 10, 3.162. The place and the distance were computed by different methods.

A test is only a test if you knew the answer before you ran it.

2§12.3 — a field with holes in it

The chapter ends with fields where the places themselves behave oddly, and this is that exercise in its smallest form. A subclass, one overridden method, and a scripted walker so that the output is predictable.

Find
  1. (a) Write OddField, a subclass of Field whose constructor takes the holes dictionary and whose moveDrunk teleports the walker when he lands on a hole.

  2. (b) Print the walker's place after each of four steps, with the hole at (2, 0) leading back to the origin.

Given
  • a Field class with addDrunk, moveDrunk and getLoc

  • holes given as a dictionary from a coordinate pair to a Location

  • a scripted walker who always steps east, added at the origin

Hint 1/4

Ask what the subclass adds to what the base class already does, and in which order the two things have to happen.

Hint 2/4

A subclass method can call the base class version by name, Field.moveDrunk(self, drunk), and then do its own extra work. The teleport is a dictionary lookup on the coordinates of the place the walker landed on.

Hint 3/4

The holes again, {(2, 0): Location(0, 0)}, and a walker who steps east every time, starting at the origin.

Hint 4/4

The program below prints four lines plus a line each time the walker falls into the hole.

Show solution

Extend the method

$$\texttt{Field.moveDrunk(self, drunk)}$$

the ordinary step happens first, so the walker is on the place that may be a hole

$$\texttt{key = (here.getX(), here.getY())}$$

a pair of numbers can be a dictionary key, while a Location object would be a different key for every visit

Follow the four steps

$$\texttt{<1, 0>}, \text{ then } \texttt{<2, 0>} \rightarrow \texttt{<0, 0>}$$

the second step lands on the hole and the walker is sent back to the origin

$$\text{the cycle repeats}$$

steps three and four do the same thing, so the walker is trapped between the origin and one east

Answer $$\boxed{\texttt{<1, 0>},\ \texttt{<0, 0>},\ \texttt{<1, 0>},\ \texttt{<0, 0>}}$$
Check

Independent check on the key choice: using the Location object itself as the hole key would never match, because each move builds a new object, and the printed output would show the walker sailing past <2, 0> to <3, 0> and <4, 0>.

When a lookup has to match a value rather than an object, key the dictionary on the value.

3§12.6 — deriving a measured mean from a step list

Two walkers were measured at the same walk length and the numbers are far apart. One of them has a step list that is not balanced, and the question is whether its number can be predicted from the list alone.

Find
  1. (a) Compute the average change in x and in y per step for ColdDrunk, straight from its step list.

  2. (b) Use it to predict the mean distance after 400 steps, and compare with the measured 100.5.

  3. (c) Say why the same calculation predicts nothing useful for UsualDrunk.

Given
  • import random
    
    
    class Location(object):
    
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
        def move(self, deltaX, deltaY):
            return Location(self.x + deltaX, self.y + deltaY)
    
        def distFrom(self, other):
            xDist = self.x - other.x
            yDist = self.y - other.y
            return (xDist**2 + yDist**2)**0.5
    
    
    class UsualDrunk(object):
    
        def takeStep(self):
            stepChoices = [(0, 1), (0, -1), (1, 0), (-1, 0)]
            return random.choice(stepChoices)
    
    
    class ColdDrunk(UsualDrunk):
        """Walks like the usual drunk, except that a step south is twice as long"""
    
        def takeStep(self):
            stepChoices = [(0, 1), (0, -2), (1, 0), (-1, 0)]
            return random.choice(stepChoices)
    
    
    def meanDistance(drunkClass, numSteps, numTrials):
        d = drunkClass()
        total = 0
        for t in range(numTrials):
            start = Location(0, 0)
            here = start
            for s in range(numSteps):
                xDist, yDist = d.takeStep()
                here = here.move(xDist, yDist)
            total = total + start.distFrom(here)
        return total / numTrials
    
    
    random.seed(23)
    for drunkClass in [UsualDrunk, ColdDrunk]:
        print(drunkClass.__name__, format(meanDistance(drunkClass, 400, 200), '.1f'))

    It prints:

    UsualDrunk 17.5
    ColdDrunk 100.5
  • ColdDrunk's step list is [(0, 1), (0, -2), (1, 0), (-1, 0)]

Hint 1/4

Average the four pairs of the step list before thinking about the walk at all. The question is whether that average is zero.

Hint 2/4

The average change per step is the sum of the four pairs divided by four, since each is equally likely. Over n steps the walker drifts by n times that average, and a drift that is not zero grows in proportion to n.

Hint 3/4

The list again: [(0, 1), (0, -2), (1, 0), (-1, 0)], and the measured mean at 400 steps is 100.5 for ColdDrunk against 17.5 for UsualDrunk.

Hint 4/4

The average step is a quarter of a unit south, so after 400 steps the drift is about 100, which is the measured number.

Show solution

Average the four pairs

$$\bar{\Delta x} = \frac{0+0+1-1}{4} = 0$$

east and west appear once each, so they cancel

$$\bar{\Delta y} = \frac{1-2+0+0}{4} = -0.25$$

the south step is twice as long as the north one, and nothing balances that

Scale it up to 400 steps

$$400 \times 0.25 = 100$$

a fixed drift per step accumulates in proportion to the number of steps

$$100 \text{ against the measured } 100.5$$

within half a step, from a calculation that never ran the simulation

Try the same thing on the fair walker

$$\bar{\Delta x} = \bar{\Delta y} = 0$$

every step has an opposite of the same length

$$\text{predicted } 0,\ \text{measured } 17.5$$

so for the fair walker the drift calculation is the wrong tool and the square root rule is the right one

Answer $$\boxed{\text{drift } 0.25 \text{ per step} \Rightarrow 100 \text{ at } 400 \text{ steps}}$$
Check

Independent check at another length: the same calculation predicts 25 at 100 steps and 400 at 1600, and both are four times apart exactly as the measured biased column was, which is the growth pattern of a drift rather than of a wander.

Average the step list first. If the average is not zero, the walker has a drift and the square root rule is the wrong rule.

4§12.7 — a figure described in words

A figure specification of the kind a lab paper or an exam gives, with the data already available from the simulation. Write the drawing half only.

Find
  1. (a) Write the drawing part of the program, taking the styles from a list so that each class keeps its own look.

  2. (b) Say why the x axis is drawn on a logarithmic scale here and what would go wrong on a linear one.

Given
  • means for two walker classes at walk lengths 25, 100, 400 and 1600

  • one panel, one line per walker class, markers on the points

  • title, both axis names, a legend naming the classes, and the x axis on a logarithmic scale because the step counts quadruple

Hint 1/4

List the five things the specification names before writing any call, and decide which of them are one call each.

Hint 2/4

One drawing call per series, the style taken from a list indexed by the series number, then the labelling calls. A logarithmic x axis is semilogx in place of plot.

Hint 3/4

The step counts again, 25, 100, 400 and 1600, each four times the last, and there are two walker classes to draw.

Hint 4/4

The program below draws both series in a loop and finishes the panel with four labelling calls.

Show solution

Cut the specification

$$\text{series} \rightarrow \text{one call each}$$

two classes, so the loop runs twice and the style comes from a list indexed by the loop variable

$$\text{labels} \rightarrow \text{four calls}$$

title, two axis names and a legend, and the legend reads the labels passed to the drawing calls

Choose the scale

$$25, 100, 400, 1600$$

the gaps between them are 75, 300 and 1200, so a linear axis crowds three of the four points together

$$\texttt{semilogx} \rightarrow \text{equal multiples, equal space}$$

which is the right frame for a rule stated as a multiplication

Answer $$\boxed{\text{two drawing calls in a loop, four labelling calls}}$$
Check

Independent check on the styles: the list has two entries for two classes, so no class shares a look with another, and the legend has as many entries as there are drawing calls.

A specification that names a scale is naming the question. Quadrupling steps is a multiplicative question.

5§12.5 — one wrong line in a working report

A report function that runs, prints four numbers and is wrong. The walker, the field and the walk function are the ones from this page.

Find(a) Which line makes the reported mean wrong?
Given
def report(numSteps, numTrials):
    d = UsualDrunk()
    origin = Location(0, 0)
    distances = []
    for t in range(numTrials):
        f = Field()
        f.addDrunk(d, origin)
        distances.append(walk(f, d, numSteps))
    print(numTrials, format(sum(distances) / numSteps, '.2f'),
          format(max(distances), '.2f'), format(min(distances), '.2f'))
Hint 1/4

Ask what each printed number is supposed to be, then check the arithmetic of the one that is a mean.

Hint 2/4

A mean is a sum divided by how many values went into the sum. The list holds one distance per trial, so its length is numTrials.

Hint 3/4

Here the list is appended to once per trial, so it holds numTrials distances, and the printed mean divides by numSteps instead.

Hint 4/4

The division in the print call is the wrong one.

Show solution

Count the values

$$\texttt{len(distances)} = \texttt{numTrials}$$

the append runs once per pass of the trial loop

$$\text{divisor should be } \texttt{numTrials}$$

a mean divides by how many values there are

Read the effect

$$\frac{\text{sum}}{\texttt{numSteps}}$$

with 100 trials of 1000 steps this is a thousand times too small, and nothing raises

$$\text{other lines correct}$$

the fresh field and the addDrunk call are the reset, and the append is once per trial

Answer $$\boxed{\texttt{sum(distances) / numSteps}}$$
Check

Independent check with numbers: the largest and smallest distances are printed unmodified, so a mean that comes out far below the printed minimum is impossible for a correct mean and is what this bug produces.

Check a reported mean against the printed minimum. A mean below the minimum is a divisor bug.

D · interleaved 3 questions
1§12.5 — a helper that keeps what it collected

A function collects values into a list and hands the list back. It is called twice, with different data each time.

Find(a) Write exactly what this program prints, all three lines.
Given
def collect(values, into=[]):
    for v in values:
        into.append(v)
    return into


first = collect([1.0, 2.0])
second = collect([3.0])
print(first)
print(second)
print(first is second)
IPython console
Hint 1/4

Ask when the list in the parameter position is created, and how many times that happens.

Hint 2/4

A default value is built once, when the function is defined, and the same object is used on every call that leaves the parameter out. Appending to it changes that one object.

Hint 3/4

Here both calls leave the parameter out, so both append into the same list: first 1.0 and 2.0, then 3.0.

Hint 4/4

Both names show the same three values, and the identity test is True.

Show solution

Locate the construction

$$\texttt{into=[]} \text{ at definition time}$$

the empty list is one object, built once, and stored with the function

$$\text{both calls omit it}$$

so both calls append into that same object

Read the two names

$$\texttt{[1.0, 2.0, 3.0]} \text{ twice}$$

the first call's returned list and the second call's returned list are the same list

$$\texttt{first is second} \rightarrow \texttt{True}$$

which is the proof that one object is being reused

Answer $$\boxed{\texttt{[1.0, 2.0, 3.0]}\;\text{twice},\ \texttt{True}}$$
Check

Independent check: if a fresh list were built per call, the second line would hold one value and the identity test would be False. Both printed values rule that out.

Collect into a list the caller passes in, or build the list inside the function. A default of [] is a shared object.

2§12.1 — a base class and one overridden method

Two classes, one of which replaces a single method of the other, and three questions asked of the pair.

Find(a) Write exactly what this program prints, all three lines.
Given
class Drunk(object):

    def __init__(self, name=None):
        self.name = name

    def takeStep(self):
        return (0, 0)

    def __repr__(self):
        if self.name != None:
            return self.name
        return 'Anonymous'


class ColdDrunk(Drunk):
    """Any step is fine, but south is twice as long as the others"""

    def takeStep(self):
        return (0, -2)


a = Drunk()
b = ColdDrunk('Bart')
print(a, b)
print(a.takeStep(), b.takeStep())
print(isinstance(b, Drunk))
IPython console
Hint 1/4

Decide for each printed value which class's method runs, before working out what that method returns.

Hint 2/4

A method defined in the subclass replaces the inherited one for objects of the subclass. Anything not redefined is inherited as it stands, including the printed form.

Hint 3/4

Here the base object is built with no name, so its repr returns the word for a nameless drunk, and the subclass redefines takeStep only.

Hint 4/4

The two printed forms, then the two steps, then the membership test.

Show solution

Print the two objects

$$\texttt{Anonymous}$$

the base object has no name, and the repr has a branch for exactly that case

$$\texttt{Bart}$$

the subclass inherits the repr unchanged

Take the two steps

$$\texttt{(0, 0)}$$

the base class's takeStep, which stands still

$$\texttt{(0, -2)}$$

the subclass's version, two units south

Test membership

$$\texttt{isinstance(b, Drunk)} \rightarrow \texttt{True}$$

a subclass object is an object of the base class, so a function written for Drunk accepts it

Answer $$\boxed{\texttt{Anonymous Bart}\;/\;\texttt{(0, 0) (0, -2)}\;/\;\texttt{True}}$$
Check

Independent check on the last line: the walk function in this section takes any object with a takeStep method, and it ran on this subclass earlier in the page, which is the same fact the True reports.

Override the one method that differs. Every walker class in this section is four lines long.

3§12.5 — counting the work a simulation does

A walker that counts its own steps instead of taking them, run three times with different trial and step counts.

Find(a) Write exactly what this program prints, all three lines.
Given
class StepCounter(object):

    def __init__(self):
        self.steps = 0

    def takeStep(self):
        self.steps = self.steps + 1
        return (1, 0)


def simWalks(numSteps, numTrials, d):
    for t in range(numTrials):
        for s in range(numSteps):
            d.takeStep()
    return d.steps


d = StepCounter()
print(simWalks(100, 10, d))
d2 = StepCounter()
print(simWalks(1000, 10, d2))
d3 = StepCounter()
print(simWalks(100, 100, d3))
IPython console
Hint 1/4

Work out how many times the inner loop body runs in total for each call, and notice which object is reused between calls.

Hint 2/4

A nested loop runs the outer count times the inner count, so the step total is numTrials times numSteps. Each call here is given its own counter object.

Hint 3/4

The three calls again: 100 steps by 10 trials, then 1000 steps by 10 trials, then 100 steps by 100 trials.

Hint 4/4

A thousand, then ten thousand, then ten thousand again.

Show solution

Multiply

$$100 \times 10 = 1000$$

the inner loop runs numSteps times per trial, and there are numTrials trials

$$1000 \times 10 = 10000,\quad 100 \times 100 = 10000$$

two different shapes of the same total

Keep the counters apart

$$\text{three counter objects}$$

each call is passed its own, so no total carries over

$$\text{cost is linear in both knobs}$$

doubling either the trials or the steps doubles the work, which is the complexity fact behind the trade off

Answer $$\boxed{1000\;/\;10000\;/\;10000}$$
Check

Independent check: the two ten thousands come from multiplying a different pair of numbers, so if either value had been mistyped they would not agree.

Trials buy stability, steps change the question, and both cost the same per unit.

Mistake ledger (25 entries)
⚠ Seeding inside the trial loop

The seed call is about reproducibility, and it feels safest next to the code it is making reproducible.

wrong$$\texttt{for t in range(numTrials):}\\\quad \texttt{random.seed(0)}\\\quad \texttt{distances.append(walk(f, d, 100))}$$
right$$\texttt{random.seed(0)}\\\texttt{for t in range(numTrials):}\\\quad \texttt{distances.append(walk(f, d, 100))}$$
⚠ Passing the pairs to choice as separate arguments

In English you choose between four things, so four things go in.

wrong$$\texttt{random.choice((0,1), (0,-1), (1,0), (-1,0))}$$
right$$\texttt{random.choice([(0,1), (0,-1), (1,0), (-1,0)])}$$
⚠ Using the returned pair as a number

A step sounds like one thing, and one thing sounds like one number.

wrong$$\texttt{x = x + random.choice(stepChoices)}$$
right$$\texttt{xDist, yDist = random.choice(stepChoices)}\\\texttt{x = x + xDist}$$
⚠ Calling move and throwing the result away

Method calls that change things are common, so a call on its own line looks like it did something.

wrong$$\texttt{loc.move(1, 0)}\\\texttt{print(loc)} \rightarrow \texttt{<0, 0>}$$
right$$\texttt{loc = loc.move(1, 0)}\\\texttt{print(loc)} \rightarrow \texttt{<1, 0>}$$
⚠ Writing a move that changes the place it was called on

It is one line shorter and it works, until two names hold the same place.

wrong$$\texttt{self.x = self.x + deltaX}\\\texttt{return self}$$
right$$\texttt{return Location(self.x+deltaX, self.y+deltaY)}$$
⚠ Comparing two places with the equality operator

Two places at the same coordinates are the same place in English.

wrong$$\texttt{Location(0,0) == Location(0,0)} \rightarrow \texttt{False}$$
right$$\texttt{a.getX() == b.getX() and a.getY() == b.getY()}$$
⚠ Calling addDrunk again in the middle of a walk

The name sounds like adding, and adding somebody who is already there sounds harmless.

wrong$$\texttt{for s in range(numSteps):}\\\quad \texttt{f.addDrunk(d, origin)}\\\quad \texttt{f.moveDrunk(d)}$$
right$$\texttt{f.addDrunk(d, origin)}\\\texttt{for s in range(numSteps):}\\\quad \texttt{f.moveDrunk(d)}$$
⚠ Expecting two walkers with the same name to be one key

The printed form of a drunk is his name, and two identical names look like one walker.

wrong$$\texttt{len(\{Drunk('Homer'): 1, Drunk('Homer'): 2\})} \rightarrow 1$$
right$$\texttt{len(\{Drunk('Homer'): 1, Drunk('Homer'): 2\})} \rightarrow 2$$
⚠ Using the None that getLoc returns for a stranger

The complaint is printed, so it feels as though the program has already dealt with the problem.

wrong$$\texttt{f.getLoc(other).getX()} \rightarrow \texttt{AttributeError}$$
right$$\texttt{if other in f.getDrunks():}\\\quad \texttt{print(f.getLoc(other).getX())}$$
⚠ Reading the starting place after the loop

Both places are needed at the end, so it feels natural to fetch them together.

wrong$$\texttt{for s in range(numSteps): f.moveDrunk(d)}\\\texttt{start = f.getLoc(d)}$$
right$$\texttt{start = f.getLoc(d)}\\\texttt{for s in range(numSteps): f.moveDrunk(d)}$$
⚠ Returning the place instead of the distance

The place is what the field holds, and it is one method call closer.

wrong$$\texttt{return f.getLoc(d)}$$
right$$\texttt{return start.distFrom(f.getLoc(d))}$$
⚠ Writing the loop as range(1, numSteps)

Counting steps from one is how people count, and the loop variable is unused so nothing complains.

wrong$$\texttt{for s in range(1, numSteps)} \rightarrow \texttt{numSteps} - 1 \text{ moves}$$
right$$\texttt{for s in range(numSteps)} \rightarrow \texttt{numSteps} \text{ moves}$$
⚠ Dividing by the number of steps instead of the number of trials

Both numbers are in the function's parameter list and both are large.

wrong$$\texttt{mean = sum(distances) / numSteps}$$
right$$\texttt{mean = sum(distances) / len(distances)}$$
⚠ Rounding each distance before summing them

The report shows two decimals, so rounding early looks like tidying up.

wrong$$\texttt{distances.append(round(walk(f, d, n), 1))}$$
right$$\texttt{distances.append(walk(f, d, n))}\\\texttt{print(format(mean, '.2f'))}$$
⚠ Dropping the reset at the top of the trial loop

The reported distances stay believable, so nothing looks broken.

wrong$$\texttt{for t in range(numTrials):}\\\quad \texttt{distances.append(walk(f, d, n))}$$
right$$\texttt{for t in range(numTrials):}\\\quad \texttt{f.addDrunk(d, origin)}\\\quad \texttt{distances.append(walk(f, d, n))}$$
⚠ Seeding inside the trial loop

It makes the run repeatable, which is what was asked for, and it does that.

wrong$$\text{10 identical trials} \rightarrow \text{mean} = \text{that one distance}$$
right$$\text{seed once} \rightarrow \text{10 different trials, mean over all of them}$$
⚠ Reading the square root rule as a doubling rule

Twice the walking sounds like twice the distance, and nothing in the simulation objects.

wrong$$n \times 2 \Rightarrow \bar d \times 2$$
right$$n \times 2 \Rightarrow \bar d \times \sqrt{2} \approx 1.4,\qquad n \times 4 \Rightarrow \bar d \times 2$$
⚠ Claiming a trend from two rows

Two points make a ratio, and a ratio looks like a pattern.

wrong$$\frac{\bar d_2}{\bar d_1} = 2.09 \Rightarrow \text{rule}$$
right$$\text{three or more rows, and the quotient } \frac{\bar d}{\sqrt n} \text{ checked at each}$$
⚠ Comparing means that came from different trial counts

The trial count feels like a detail of how long the program ran, not like part of the measurement.

wrong$$\bar d(100, \texttt{numTrials}{=}10) \text{ vs } \bar d(400, \texttt{numTrials}{=}300)$$
right$$\text{same } \texttt{numTrials} \text{ in every row of one table}$$
⚠ Comparing two panels that have different axis limits

Each panel looks best when its own data fills it, and the drawing library does that by default.

wrong$$\texttt{axis([-25,25,-25,25])} \text{ and } \texttt{axis([0,40,-20,20])}$$
right$$\texttt{axis([-25,40,-25,25])} \text{ in both panels}$$
⚠ Naming the series in the legend in the wrong order

The legend is written last, by which time the order of the drawing calls has been forgotten.

wrong$$\texttt{plot(fair)}, \texttt{plot(biased)}, \texttt{legend(['biased', 'fair'])}$$
right$$\texttt{plot(..., label='fair')} \text{ per call, then } \texttt{legend()}$$
⚠ Drawing one walk to answer a question about many

A single path is the most interesting looking picture in the whole section.

wrong$$\text{one path} \Rightarrow \text{claim about } \bar d$$
right$$\text{200 end locations, or the mean against } n$$
⚠ Answering the opening question by averaging the steps

The steps really do average to nothing, and it takes a count of the sixteen two step walks to see that distances do not.

wrong$$\text{average step} = (0,0) \Rightarrow \bar d = 0$$
right$$\bar d(2\text{ steps}) = 1.2071 \text{, counted over all 16 walks}$$
⚠ Reporting a mean with no range beside it

The mean is what was asked for, and the range looks like extra detail until you see that it was 2.0 to 15.3 around a mean of 8.44.

wrong$$\texttt{print('Mean:', mean)}$$
right$$\texttt{print(mean, max(distances), min(distances))}$$
⚠ Treating a seeded run as an exact answer

Repeatability feels like precision, and the two words are not the same word.

wrong$$\text{seed fixed} \Rightarrow \text{answer exact}$$
right$$\text{seed fixed} \Rightarrow \text{run repeatable, answer still an estimate}$$
Formula card
One random step
$$\texttt{step = random.choice(stepChoices)}$$

stepChoices is one list of pairs; each pair is (change in x, change in y); each item has the same chance.

A place hands back a place
$$\texttt{move(dx, dy)} \Rightarrow \texttt{Location(x+dx, y+dy)}$$

The place the method was called on is not changed, and the returned place has to be kept by an assignment.

Distance between two places
$$d = \sqrt{(x_1-x_2)^2 + (y_1-y_2)^2}$$

Symmetric in the two places, never negative, and zero exactly when the two places have the same coordinates.

The field's two lines
$$\texttt{f.drunks[d] = loc}\qquad \texttt{f.drunks[d] = f.drunks[d].move(dx, dy)}$$

The key is the walker object. Writing to a key that is already there replaces the value, which is how a reset works and how a walk gets deleted by accident.

One walk
$$\texttt{start = f.getLoc(d)};\ \texttt{numSteps} \times \texttt{moveDrunk};\ \texttt{return start.distFrom(f.getLoc(d))}$$

The starting place is read before the loop. numSteps of 0 returns 0.0. The value returned is a number.

Many walks, one number
$$\text{mean} = \frac{\texttt{sum(distances)}}{\texttt{len(distances)}}$$

The denominator is the number of trials. Every trial begins with a reset, and the seed call is above the loop.

How the mean grows
$$\bar d \approx 0.9\sqrt{n}\quad\text{(fair)}\qquad \bar d \propto n\quad\text{(biased)}$$

Measured on this page over walks of 25 to 1600 steps. The fair rule needs a step list whose average step is the pair (0, 0).

Average step, straight from the list
$$\bar{\Delta x} = \frac{\sum \Delta x_i}{k},\qquad \bar{\Delta y} = \frac{\sum \Delta y_i}{k}$$

k is the number of entries in the step list, and entries may repeat, which is how a probability is written into the list.

From a simulation to a figure
$$\text{question} \rightarrow \text{lists} \rightarrow \text{one call per series} \rightarrow \text{labels and limits}$$

Two panels that are being compared take the same axis limits, and each walker class keeps one style across figures.

Check yourself

Close the page and write, from memory:

  • the four step pairs and the call that picks one
  • what move returns and what it leaves alone
  • what the field's dictionary maps to what, and what a second addDrunk call for one walker does
  • the three lines of the walk function, in order
  • the two lines that make a trial honest
  • what four times the steps does to the mean, fair and biased

Then write the simulation from the last rung's specification without looking, and run the zero and one step checks before comparing.

  • Write the step list and the call that picks one step from it, and say what a second seed call with the same number does to the stream?

    c-one-step

  • Say which object is new after a move, which one is unchanged, and compute the distance between two places whose coordinates differ by 3 and 4?

    c-location

  • Say what the field holds, what happens when addDrunk is called twice for one walker, and why two walkers can share one Location object here?

    c-field

  • Write the walk function from memory and say what each of the zero, one and two step checks would catch?

    c-walk

  • Write the trial loop with its reset, report the three numbers, and say where the seed call belongs and why?

    c-trials

  • Predict the mean at 900 steps from the mean at 100, and compute a walker's drift straight from its step list?

    c-growth

  • Choose the figure for a stated question, give each walker class its own style, and say what a frame chosen from the data can hide?

    c-visualize

Glossary (22 terms)
random walkrastgele yürüyüş

A path built one random step at a time, where nothing about the next step depends on the ones already taken.

drunkard's walksarhoş yürüyüşü

The random walk this chapter uses as its example: one unit step per second, north, south, east or west, each equally likely.

simulationsimülasyon

A program that runs a process many times to answer a question about it, when the question cannot be settled by argument on paper.

trialdeneme

One complete run of the process being simulated. Here one trial is one walk of numSteps steps, starting from the origin.

unit stepbirim adım

One move of length one, parallel to an axis, written as a pair of coordinate changes such as (0, 1) for north.

seed

The number that chooses where the stream of random numbers begins. The same seed reproduces the same run on the same interpreter, which is what makes a claim about a random program checkable.

sözde rastgele

Produced by a rule rather than by chance, but spread out like chance. This is why a seed can reproduce a whole run exactly.

deterministik

Producing the same result every time from the same input. A scripted walker is deterministic; a seeded random walker is deterministic in the same sense once the seed is fixed.

scripted walker

A walker whose steps are read from a list in order, used on this page to test the machinery. Its answers can be computed by hand, so a wrong answer is a bug rather than bad luck.

sanity check

A tiny case whose answer is known before the program is run: zero steps must give 0.0 and one step must give 1.0. Its only purpose is to fail when something is broken.

meanortalama

The sum of the measured values divided by how many there are. In a simulation the divisor is the number of trials and never the number of steps.

spreadyayılım

How far apart the answers of separate trials are, reported here as the largest and the smallest distance beside the mean.

resetsıfırlama

Putting the walker back on the starting place before a trial, with an addDrunk call or a fresh field, so that the trial measures a walk from there rather than the next leg of a longer one.

straight line distancedoğrusal uzaklık

The length of the line from where a walk started to where it ended, computed from the two coordinate differences. The number a walk reports.

yol uzunluğu

The total distance walked, which for unit steps is just the number of steps. An upper bound on the straight line distance and usually much larger than it.

originbaşlangıç noktası

The place a walk starts from, Location(0, 0) on this page. Distances are measured from it when every trial resets to it.

driftsürüklenme

A fixed average movement per step, which appears when the step list is not balanced. It accumulates in proportion to the number of steps.

biasyanlılık

A step list in which some direction is more likely, or longer, than its opposite. A biased walker has a drift and breaks the square root rule.

square root growthkarekök büyümesi

The pattern a fair walk's mean distance follows: multiplying the number of steps by four multiplies the mean by about two.

saçılım grafiği

A figure with one mark per observation at its two coordinates, used here to show where many walks ended without saying anything about the order they ran in.

A small object that hands out the next plotting style each time it is asked, so that each walker class keeps one look across figures.

base classtaban sınıf

The class a subclass extends. Here it holds the name and the printed form, and it is not useful on its own: every walker that can actually walk is a subclass.

What comes next
§13 · Understanding Experimental Data (Chapter 18)

The means measured here sit almost exactly on a square root curve, and this page checked that by dividing the curve out by hand. Next comes the machinery for fitting a curve to measured points and for saying how well it fits.

Sources
  • kitapJohn Guttag, Introduction to Computation and Programming Using Python, with Application to Understanding Data, Second Edition, Chapter 14 The chapter this week's syllabus line names. Its sections are the walk itself, the sanity checks, the step to several walker classes, the plotting of the results, and fields whose places behave oddly.
  • ders malzemesiThe week's lecture slide deck on random walks Source of the problem statement, the three abstractions and the walk function's docstring. The deck states that it was adapted from MIT introductory computer science materials under a Creative Commons licence.
  • ders malzemesiThe week's three lecture programs, one walk, ten walks and five walkers, together with the Location, Field and Drunk files Source of the naming style used here, of the plotting of final locations with one colour per walker, and of the mean, largest and smallest report. The programs draw with pyplot and print their numbers unformatted.
  • sabitThe language's own documentation for the random module Used for the three functions this page allows itself, choice, seed and randint, and for the statement that a seeded run repeats on the same interpreter.

Spotted something missing or wrong? tell us · share your own notes or an old exam.

Last updated .