Probability and Statistics – Random Walk

(** 인터넷 익스플로러에서 보면 파이썬 코드 줄이 제대로 적용되어 보이지 않을 수 있습니다.**)

이번에는 파이썬을 활용하여 랜덤워크(Random Walk)를 시뮬레이션해보자. 랜덤워크는 술 주정뱅이가 어느 시작 지점에 서 있고 주기적으로 무작위 방향을 선택해서 한 걸음씩 이동하는 궤적이다. 일정 시간이 지났을 때 시작 지점에서부터 얼마만큼 거리가 떨어져 있을지 시뮬레이션으로 살펴볼 예정이다.

지금까지 파이썬 예제와 다르게 다소 많은 코드가 필요하다. 특히 클래스를 통해 코드를 구조화하려고 한다. 술 주정뱅이를 표현하는 클래스 Drunk를 다음과 같이 작성한다.

import pylab
import random

class Drunk(object):
    def __init__(self, name = None):
        self.name = name

    def __str__(self):
        if self != None:
            return self.name
        return 'Anonymous'

파이썬 클래스는 class 키워드로 선언하고, 부모 클래스를 object로 하는 클래스 Drunk를 만든다. init 함수는 이 클래스의 생성자이고, str는 이 클래스의 객체를 문자열로 바꾸어 표현하는 멤버 함수이다. 두 함수의 첫 번째 인자로 키워드 self를 받는데 이는 멤버 함수를 호출할 때 사용한 호출 객체다. 즉 두 함수는 객체를 통해 호출하는 동적 멤버 함수이다.

init 생성자는 인자로 이름 name을 받는다. 이름을 지정하지 않으면 기본으로 None을 설정한다.

str 함수는 Drunk 클래스 객체의 이름이 지정되어 있으면 그것으로 이 객체를 표현하고 지정되어 있지 않으면 기본 값 'Anonymous'로 표현하도록 구성되어 있다.

클래스 Drunk의 객체를 만드는 방법은 Drunk(...)와 같다. 다음 파이썬 코드는 Drunk 클래스 객체를 만들어 변수 drunk에 대입한다.

>>> drunk = Drunk('Drunkard')
>>> drunk.name
'Drunkard'

Drunk 클래스를 상속받아 세가지 술 주정뱅이를 표현하는 클래스 UsualDrunk, ColdDrunk, EWDrunk를 정의한다. 각 클래스는 모두 takeStep 메소드로 이동 방향을 결정하는 규칙을 정의한다.

class UsualDrunk(Drunk):
    def takeStep(self):
        stepChoices = [(0.0,1.0), (0.0,-1.0), (1.0,0.0), (-1.0,0.0)]
        return random.choice(stepChoices)

class ColdDrunk(Drunk):
    def takeStep(self):
        stepChoices = [(0.0,1.0), (0.0,-2.0), (1.0,0.0), (-1.0,0.0)]
        return random.choice(stepChoices)

class EWDrunk(Drunk):
    def takeStep(self):
        stepChoices = [(1.0,0.0), (-1.0,0.0)]
        return random.choice(stepChoices)

UsualDrunk 클래스는 동서남북을 x,y좌표 리스트 [(0.0,1.0), (0.0,-1.0), (1.0,0.0), (-1.0,0.0)]로 표현하여 이 중 한 방향을 무작위로 선택하여 리턴하도록 작성되었다.

ColdDrunk 클래스는 남쪽으로 더 빠르게 이동하도록 작성되었고, EWDrunk는 동서 방향으로만 이동하도록 작성되었다.

>>> choi = UsualDrunk('Choi')
>>> kim = ColdDrunk('Kim')
>>> park = EWDrunk('Park')
>>> choi.takeStep()
(0.0, -1.0)
>>> kim.takeStep()
(1.0, 0.0)
>>> park.takeStep()
(1.0, 0.0)

이제 3가지 이동 가능한 캐릭터가 준비되었고, 이 캐릭터들의 위치와 들판을 표현하는 클래스 Location과 Field를 작성한다.

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.x
        yDist = self.y - other.y
        return (xDist ** 2 + yDist ** 2) ** 0.5

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

class Field(object):
    def __init__(self):
        self.drunks = {}

    def addDrunk(self, drunk, loc):
        if drunk in self.drunks:
            raise ValueError('Duplicate drunk')
        else:
            self.drunks[drunk] = loc

    def moveDrunk(self, drunk):
        if drunk not in self.drunks:
            raise ValueError('Drunk not in field')
        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:
            raise ValueError('Drunk not in field')
        return self.drunks[drunk]

Location 클래스의 멤버변수는 x,y로 x축과 y축 좌표값을 저장한다. Field 클래스의 멤버변수는 drunks는 키와 값의 쌍을 저장하는 딕셔너리를 저장한다.

>>> l1 = Location(0,0)
>>> l2 = Location(0,0)
>>> l3 = Location(0,0)
>>> print(l1)
<0, 0>
>>> l1.distFrom(Location(10,10))
14.142135623730951
>>> print(l1.move(5,5))
<5, 5>
>>> f = Field()
>>> f.addDrunk(choi, l1)
>>> f.addDrunk(park, l2)
>>> f.addDrunk(kim, l3)
>>> f.moveDrunk(choi)
>>> print(f.getLoc(choi))
<0.0, -1.0>
>>> f.moveDrunk(kim)
>>> print(f.getLoc(kim))
<-1.0, 0.0>

앞에서 만든 3개의 캐릭터 초기 좌표 (0,0)으로 l1, l2, l3를 만든다. l1을 print문으로 출력하여 이 초기 좌표를 확인할 수 있다. l1.distFrom(Location(10,10))와 같이 두 좌표 l1과 (10,10) 사이의 거리를 계산할 수 있다. l1.move(deltaX, deltaY)를 통해 l1의 현재 위치에서 deltaX와 deltaY 떨어진 위치로 이동한 위치를 계산하여 리턴한다.

그 다음으로 들판에 대한 클래스 Field의 쓰임새를 살펴보자. 우선 addDrunk 메소드를 통해 3개의 캐릭터 choi, park, kim을 3개의 위치 l1, l2, l3로 지정하여 들판에 추가한다. moveDrunk 메소드를 통해 choi 캐릭터의 위치를 이동시키고 getLoc 메소드로 이 새로운 위치 좌표를 얻어 print 함수로 출력해본다. kim 캐릭터에 대해서도 동일한 형태로 실행해본다.

이제가지 만든 클래스들을 조합하여 랜덤워크 시뮬레이션을 구성한다. walk 함수는 들판 f에서 술 주정뱅이 d를 numSteps 번 만큼 무작위로 이동하게 한 다음 처음 시작 좌표에서 마지막에 도달한 좌표 사이의 거리를 구한다.

simWalks 함수는 술 주정뱅이 클래스 dClass (UsualDrunk, ColdDrunk, EWDrunk 중 하나)를 받아 numTrials 시도를 하되 각 시도마다 numSteps 걸음을 랜덤워크하는 시뮬레이션을 진행한다. 각 시도마다 도달한 위치가 처음 시작 위치와 얼마만큼 떨어져 있는지 리스트로 모아 함수 리턴값으로 반환한다.

drunkTest 함수는 술 주정뱅이 클래스 dClass를 numTrials 시도를 하되 walkLenghths 튜플 (예: (10,100,1000, 10000))에 지정한 걸음 수만큼 무작위로 걷도록 시뮬레이션한다.

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, dClass):
    Homer = dClass()
    origin = Location(0.0, 0.0)
    distances = []
    for t in range(numTrials):
        f = Field()
        f.addDrunk(Homer, origin)
        distances.append(walk(f, Homer, numSteps))
    return distances

def drunkTest(walkLengths, numTrials, dClass):
    for numSteps in walkLengths:
        distances = simWalks(numSteps, numTrials, dClass)
        print(dClass.__name__, 'random walk of', numSteps, 'steps')
        print(' Mean =', sum(distances)/len(distances))
        print(' Max =', max(distances), 'Min =', min(distances))

drunkTest 함수를 사용하여 랜덤워크 시뮬레이션을 진행해보자. 각 시도마다 10걸음 랜덤워크를 진행한 경우 시작 좌표로부터 평균 2.8, 최대 7.07 최소 0.0만큼 떨어져 있고, 100걸음 랜덤워크를 진행한 경우 평균 9.0, 최대 22.847, 최소 0.0만큼 떨어져 있고 등등 결과를 확인할 수 있다. 100걸음 이상, 10000걸음 랜덤워크를 진행한 경우도 시작 좌표로부터 떨어져 있는 평균 거리가 아주 크지 않다는 것을 확인할 수 있다.

>>> drunkTest((10,100,1000, 10000), 100, UsualDrunk)
UsualDrunk random walk of 10 steps
 Mean = 2.8051352402584557
 Max = 7.0710678118654755 Min = 0.0
UsualDrunk random walk of 100 steps
 Mean = 9.0013316663762
 Max = 22.847319317591726 Min = 0.0
UsualDrunk random walk of 1000 steps
 Mean = 27.426085567471535
 Max = 69.87131027825369 Min = 2.8284271247461903
UsualDrunk random walk of 10000 steps
 Mean = 86.83702965390277
 Max = 210.304541082688 Min = 3.1622776601683795

마지막으로 세 가지 랜덤워크를 시뮬레이션하면 어떻게 이동하는지 경로에 점을 찍어 그래프로 보여주는 파이썬 프로그램을 작성한다. 이 그래프로 세 가지 랜덤워크의 특성을 분명하게 비교할 수 있을 것이다.

xAxisKim = []
yAxisKim = []

xAxisPark = []
yAxisPark = []

xAxisLee = []
yAxisLee = []

f = Field()
kim = UsualDrunk('Kim')
park = ColdDrunk('Park')
lee = EWDrunk('Lee')

f.addDrunk(kim, Location(0,0))
f.addDrunk(park, Location(0,0))
f.addDrunk(lee, Location(0,0))

for e in range(100):
    xAxisKim.append(f.getLoc(kim).getX())
    yAxisKim.append(f.getLoc(kim).getY())
    f.moveDrunk(kim)

    xAxisPark.append(f.getLoc(park).getX())
    yAxisPark.append(f.getLoc(park).getY())
    f.moveDrunk(park)

    xAxisLee.append(f.getLoc(lee).getX())
    yAxisLee.append(f.getLoc(lee).getY())
    f.moveDrunk(lee)

# plot styles: 'bo', 'b+', 'r^', 'mo'

pylab.plot(xAxisKim,yAxisKim,'bo', label='UsualDrunk')
pylab.plot(xAxisPark,yAxisPark,'b+', label='ColdDrunk')
pylab.plot(xAxisLee,yAxisLee,'r^', label='EWDrunk')

pylab.legend()

pylab.show()

이 파이썬 프로그램에서 술 취한 사람 Kim, Park, Lee를 고려한다. Kim은 UsualDrunk, Park은 ColdDrunk, Lee는 EWDrunk로 지정하여 들판을 만들어 추가하자. 반복문 for에서 세 사람의 이동 경로를 moveDrunk 메소드를 통해 만들고 각각 xAxisKim yAxisKim, xAxisPark, yAxisPark, xAxisLee, yAxisLee에 리스트로 추가한다. 예제는 각각 100걸음씩 시뮬레이션하도록 단순하게 작성하였다.

 

이 시뮬레이션 결과를 그래프로 보여준다. 세 사람의 이동 좌표를 표시할 때 스타일 'bo', 'b+', 'r^'을 지정하여 o, +, ^으로 구분하여 보여준다. 스타일과 함께 label도 지정하여 나중에 pylab.legend() 함수로 범례를 그래프에 추가할 때 어떤 모양이 어느 클래스의 술 취한 사람인지 구분해서 보여준다.